安卓逆向入门-安全合规

Android基础

Android要求所有APK使用证书进行数字签名,否则无法安装或更新。签名利用摘要和非对称加密技术确保APK 由开发者发布且未被修改,摘要是用哈希算法计算出来的APK内部文件唯一映射值,相当于APK指纹。签名用开发者私钥进行加密。用户端安装APK时系统重新计算APK文件摘要,用开发者公钥解密签名中摘要,两者对比一致则说明APK来源可信且未被修改。

Android 11及之前支持4中应用签名方案。v1签名是最基本的基于JAR的签名方案。v2签名在Android 7引入,提高验证速度并增强完整性保证。v3签名在Android 9引入,支持密钥轮替。v4签名在Android 11引入,根据APK所有字节计算得出Merkle哈希树,需要通过v2或v3签名进行补充,签名信息单独存储在.apk.idsig文件中。应用验证时,系统优先寻找并校验最高版本的签名,无法找到则逐级向下寻找,直到找到兼容的签名方案。

签名后应用中新增META-INF文件夹,文件夹中包含3个文件。MANIFEST.MF记录应用中每个文件的Hash摘要,除了META-INF文件夹。*.SF文件记录MANIFEST.MF文件的摘要和MANIFEST.MF中每个数据块的Hash摘要。*.RSA文件记录*.SF文件的签名和包含公钥的开发者证书。

第一步先遍历应用中文件并计算文件对应SHA-1摘要,对文件摘要进行BASE64编码,然后将其写入MANIFEST.MF签名文件。第二步计算MANIFEST.MF文件的SHA-1摘要,进行BASE64编码后写入*.SF文件,再次计算MANIFEST.MF文件中每一条摘要内容的SHA-1摘要,将摘要内容进行BASE64编码后写入*.SF签名文件。第三步计算整个*.SF文件的数字签名,将数字签名和开发者的X.509数字证书写入*.RSA文件。

Android应用安装时设计的系统目录有:

1
2
3
4
5
6
7
/system/app 存放系统预装应用
/vendor/app 存放设备厂商预装额应用
/data/app-private 存放受DRM数字版权管理保护的应用
/data/app 存放用户安装的应用
/data/data 存放已安装应用程序的数据
/data/system 该目录中packages.xml记录每个已安装应用的name、codePath、flags、versions、userid等信息
/data/app/包名/oat 安装应用时将DEX文件复制到该目录并转为OAT文件

应用安装时系统默认将其安装到/data/data目录,想将应用安装到SD卡中,则在AndroidManifest中增加android:installLocation属性,值设为preferExternal或auto。

应用安装时,第一步通过DefaultContainerService将自身复制到/data/app/package目录中,动态库复制到/data/app/package/lib目录中,将DEX文件复制到/data/app/包名/oat目录中,并进行优化处理。第二步解析安装包中资源文件并进行签名校验,将从AndroidManifest.xml中读取的应用信息写入/data/system/packages.xml文件。第三步在/data/data/package目录中创建所需数据目录,将应用数据复制到相关目录中。第四步将AndroidManifest.xml中声明的组件信息注册到PackageManagerService,再通过Launcher应用将应用图标添加至桌面,完成安装,且系统发送一条应用完成安装的广播。

Android系统中每个应用程序安装时都被分配唯一用户编号UID,只要应用程序不被卸载,UID不变动。Android根据应用UID为每个应用设置一个独立内核级应用沙盒,所有应用再沙盒中独立运行,默认情况下应用之间不能交互。Android系统沙盒机制的安全性在不断加强:

1
2
3
4
5
Android 5:利用SELinux的强制访问控制MAC机制,实现系统与应用间的隔离,采用基于UID的自主访问DAC策略,确保第三方应用之间的隔离。
Android 6:扩展SELinux沙盒功能,实现跨越物理用户边界的应用隔离。更改应用主目录的默认访问权限,确保除了目录属主拥有读写和可执行权限外,所属组内用户和所属组外其他用户均无访问权限。
Android 8:应用通过基于Linux提供的Seccomp BPF模式过滤运行,Android通过该模式限制应用对系统API的调用。
Android 9:非特权应用运行在不同SELinux沙盒中,针对各个应用采用MAC策略,防止替换安全默认设置,防止应用数据让所有人可访问。
Android 10:应用默认被授予外部存储空间的分区访问权限,可直接访问外部存储空间上应用专属目录以及本应用创建的特定类型媒体文件,不需要申请与存储空间相关的权限。

拥有系统权限的应用在访问系统资源时有很多特权,而非系统应用访问系统资源时,需要通过DAC、MAC安全策略等。

Binder实现基于客户端/服务端(C/S)架构的进程间通信IPC,通过Android接口定义语言AIDL来定义通信接口及交换数据的格式,本质时通过共享内存方式实现进程通信。Binder在内核空间创建两个内存缓冲区,两个内存缓冲区之间建立mmap内存映射关系,同时在内核共享数据缓存区和接收进程的用户空间地址之间建立映射关系。发送方进程用copy_from_user函数将共享数据复制到内核中内存缓冲区。由于内存缓冲区和接收进程用户空间存在内存映射,接收进程便可直接读取共享数据。

安全防护

通信防抓包

检测系统是否开启代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
package com.test.preventmitm;
import android.content.Context;
import android.os.Build;
import android.text.TextUtils;
import java.io.InputStream;
import java.net.URL;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import okhttp3.CertificatePinner;
import okhttp3.OkHttpClient;
public class PreventMitm {
/**
* 检测手机是否开启代理
* @param context
* @return
*/
public static boolean isWifiProxy(Context context) { //就这个函数
final boolean IS_ICS_OR_LATER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
String proxyAddress;
int proxyPort;
if (IS_ICS_OR_LATER) {
proxyAddress = System.getProperty("http.proxyHost");
String portStr = System.getProperty("http.proxyPort");
proxyPort = Integer.parseInt((portStr != null ? portStr : "-1"));
} else {
proxyAddress = android.net.Proxy.getHost(context);
proxyPort = android.net.Proxy.getPort(context);
}
return (!TextUtils.isEmpty(proxyAddress)) && (proxyPort != -1);
}
public OkHttpClient getOkhttpClient(Context mContext){
return new OkHttpClient.Builder().certificatePinner(getCertPinner(mContext)).build();
}
// 获取证书
public static CertificatePinner getCertPinner(Context mContext) {
Certificate cert = null;
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream input = mContext.getAssets().open("test.cer");
cert = cf.generateCertificate(input);
input.close();
} catch (Exception e) {
e.printStackTrace();
}
String pinning = "";
if (cert != null)
pinning = CertificatePinner.pin(cert);
return new CertificatePinner.Builder().add("www.test.com", pinning).build();
}
public HttpsURLConnection getTrustConnection(String url){
HttpsURLConnection conn = null;
try{
SSLContext mSSLContext = SSLContext.getInstance("TLS");
mSSLContext.init(null, new TrustManager[]{new ClientTrustManager()}, new SecureRandom());
conn = (HttpsURLConnection) new URL(url).openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
}catch (Exception e){
e.printStackTrace();
}
return conn;
}
class ClientTrustManager implements X509TrustManager{
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
// 校验服务端证书是否被替换
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
}

也可以设置网络框架使用无代理模式,不走系统代理:

1
2
3
4
5
6
//HttpsURLConnection
URL url=new URL(urlAddress);
HttpsURLConnection httpon=(HttpsURLConnection)url.openConnection(Proxy.NO_PROXY);

//OkHttp
OkHttpClient.Builder().retryOnConnectionFailure(true).proxy(Proxy.NO_PROXY).build();

证书校验

SSL Pinning,校验本地预埋的服务端证书与服务端返回的证书是否一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//HttpsURLConnection
TrustManager[] trustAllCerts=new TrustManager[]{
new X509TrustManager(){
public X509Certificate[] getAcceptedIssuers(){
return null;
}
public void checkClientTrusted(X509Certificate[] certs,String authType){}
public void checkServerTrusted(X509Certificate[] certs,String authType){
String expectedPublicKey="xxx"; //RSA公钥的BASE64编码
if(!certs[0].getPublicKey().equals(expectedPublicKey)){
throw new RuntimeException("证书校验失败");
}
}
}
};
SSLContext sslContext=SSLContext.getInstance("TLS");
sslContext.init(null,trustAllCerts,null); //初始化SSL上下文
URL url=new URL("https://test.com"); //打开链接
HttpsURLConnection connection=(HttpsURLConnection)url.openConnection();
connection.setSSLSocketFactory(sslContext.getSocketFactory());

//OkHttp
Certificate cert=null;
try{
CertificateFactory cf=CertificateFactory.getInstance("X.509");
InputStream input.mContext.getAssets().open("test.cer");
cert=cf.generateCertificate(input);
input.close();
}catch(Exception e){
e.printStackTrace();
}
String pinning="";
if(cert!=null)
pinning=CertificatePinner.pin(cert);
CertificatePinner certificatePinner=new CertificatePinner.Builder().add("www.test.com",pinning).build();
OkHttpClient client=new OkHttpClient.Builder().certificatePinner(certificatePinner).build();
Request request=new Request.Builder().url("https://www.test.com").build();
Response response=client.newCall(request).execute();

通信加密

一般用ECDHE密钥协商算法生成密钥,它使用非对称ECDSA签名算法对服务端公钥进行签名,客户端进行验签,HTTPS TLS/1.3的握手过程就使用ECDHE算法。第一步客户端和服务端分别进行密钥初始化,分别生成公私钥,之后客户端向服务端发送客户端公钥。第二步服务端对服务端公钥进行签名,并将服务端公钥和签名发送给客户端,客户端对签名进行验签。第三步客户端和服务端使用对方公钥和自己私钥生成最终加密密钥。

设备指纹

Android中比较稳定的设备参数有:

字段名 字段含义 特性
imei/meid 国际移动设备识别码 相同厂商同型号设备小概率号码碰撞
model 设备型号 同厂商同型号设备相同
screen 屏幕真实分辨率 同厂商同型号设备相同
memory 手机内存大小 同厂商同型号设备相同
serialno 设备序列号 同型号设备小概率号码碰撞,Android 11开始需要申请权限
drmuid 数字版权相关串号 高版本系统同一设备每个应用获取的值可能不同
oaid 移动联盟设备唯一标识 仅能获取加入移动联盟厂商的设oaid
cid SD卡序列号 可能获取失败
android_id 设备唯一标识码 同型号设备小概率号码碰撞
mac 设备网卡MAC地址 高版本系统可能随机返回虚假值,需要申请权限

获取设备序列号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//法一
public static String getSerialno(){
StringBuilder serial=new StringBuilder();
try{
if(Build.VERSION.SDK_INT>Build.VERSION_CODES.N)
serial.append(Build.SERIAL);
else{
Class<?> c=Class.forName("android.os.SystemProperties");
Method get=c.getMethod("get",String.class);
serial.append((String)get.invoke(c,"ro.serialno"));
}
}catch(Exception e){
serail.append(padding);
}
return serial.toString();
}

另一个方法:

1
2
3
4
5
6
7
8
9
//法二
void getSerialno(char** serialno){
FILE* fp;
*serialno(char*)calloc(128,sizeof(char));
fp=popen("cat /sys/devices/soc0/serial_number","r");
fgets(*serialno,128,fp);
pclose(fp);
return;
}

获取数字版权相关串号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static String getDrmUid(){
StringBuilder deviceUniqueId=new StringBuilder();
try{
if(Build.VERSION.SDK_INT>=18){
MediaDrm mediaDrm=new MediaDrm(new UUID(-1301668207276963122L,-6645017420763422227L));
if(mediaDrm!=null){
@SuppressLint("WrongConstant");
byte[] ret=mediaDrm.getPropertyByteArray("deviceUniqueId");
if(ret!=null)
deviceUniqueId.append(MBase64.encode(ret));
mediaDrm.release();
}
}
}catch(Exception e){
deviceUniqueId.append(exceptError);
}
return deviceUniqueId.toString();
}

获取SD卡序列号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static String getSDcardCid(Context context){
StringBuilder cid=new StringBuilder();
String path1="/sys/block/mmcblk0/source.device/type";
try{
String path_cid=null;
File mFile=new File(path1);
FileReader mFileReader=new FileReader(path1);
BufferedReader mBufferedReader=new BufferedReader(mFileReader);
if(mBufferedReader.readLine().toLowerCase().contentEquals("mmc")){
path_cid="/sys/block/mmcblk0/source.device/";
FileReader mFileReader2=new FileReader(path_cid+"cid");
BufferedReader mBufferedReader2=new BufferedReader(mFileReader2);
cid.append(mBufferedReader2.readLine());
mFileReader2.close();
mBufferedReader2.close();
return cid.toString();
}
mFileReader.close();
mBufferedReader.close();
}catch(Exception e){
e.printStackTrace();
}
return cid.toString();
}

获取网卡MAC地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//法一
String getMACAddress(Context context){
String macaddr;
Enumeration<NetworkInterface> interfaces=NetworkInterface.getNetworkInterfaces();
while(interfaces.hasMoreElements()){
NetworkInterface net=interfaces.nextElement();
byte[] addr=net.getHardwareAddress();
if(addr==null||addr.length==0)
continue;
StirngBuilder buf=new StringBuilder();
for(byte b:addr)
buf.append(String.format("%02x:",b));
if(buf.length()>0)
buf.deleteCharAt(buf.length()-1);
if(net.getName().equals("wlan0")){
macaddr=buf.toString();
break;
}
}
return macaddr;
}
//法二
String getMacAddress(){
String macAddr;
InetAddress ip=getLocalInetAddress();
byte[] b=NetworkInterface.getByInetAddress(ip).getHardwareAddress();
StringBuilder buffer=new StringBuffer();
int count=0;
for(int i=0;i<b.length;i++){
if(i!=0){
buffer.append(':');
count++;
}
String str=Integer.toHexString(b[i]&0xFF);
buffer.append(str.length()==1?0+str:str);
}
if(count!=0)
macAddr=buffer.toString().toUpperCase();
return macAddr;
}

首先用AES将采集的设备数据进行整体加密,然后结合当前用户SESSIONID和salt计算SHA1签名,最后将加密后的设备数据和签名用“.”拼接,整体用BASE64编码上传服务器。服务器端同理,首先服务端对数据解析拆分,然后利用当前用户ID查询服务端存储的SESSIONID,计算出签名,最有对比服务器计算的签名和客户端上报的签名是否一致。一致则正常对上报的数据解密,否则直接丢弃。

Android指纹生成方案可以为:

1
SUID=SHA1(imei+model+screen+mac+android_id+serialno+cid+oaid)

一般客户端每次使用设备指纹时先读取本地存储的设备指纹,本地读取失败再重新请求服务端获取,存储再设备上涉及安全存储问题。客户端接收设备指纹后对其加密,并获取当前时间戳结合加密后设备指纹和salt生成防篡改签名,然后将加密后设备指纹、防篡改签名和时间戳进行BASE6编码,最后将处理后的设备指纹隐藏到当前设备。

路径名 路径含义
/sdcard/Android/.backup/.config 沙箱根目录
/sdcard/DCIM/.config 相册目录
/sdcard/.tconfig sdcard根目录
/sdcard/.SystemConfig/.backup/.config sdcard根目录
/sdcard/Android/data/包名/.backup/.config 应用沙箱目录

服务端数据库查询结果数量为6,即6个Android核心特征,有imei、serialno、drmuid、cid、android_id、mac,其中权重android_id>serialno>drmuid>mac>sdcard。若所有查询结果中匹配的核心特征数不同,则以匹配数量最多的查询结果为准,取对应的设备指纹。若匹配的核心特征数量相同,则按核心特征值权重筛选,未匹配则认为是个新设备,用上报的设备特征生成设备指纹。

模拟器检测

可以检测模拟器属性特征:

1
2
3
4
5
6
7
8
9
10
char* gplist[12]={"virtualbox","vbox86","nox","droid4x","windroy","andy_vm_custom","qemu.sf.fake_camera","ttVM","microcvirt","ro.simulated.phone","simulated","nemud","microvirtd"};
char* buf=(char*)malloc(LENGTH*sizeof(char));
FILE* fp=popen("getprop","r");
while(fgets(buf,LENGTH*sizeof(char),fp)!=NULL)
for(num=0;num<12;num++)
if(NULL!=strcasestr(buf,gplist[num])){
buffer[count]=(char*)malloc(LENGTH*sizeof(char));
strcpy(buffer[count],gplist[num]);
break;
}

也可检测模拟器文件特征:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int check_file_exist(const char* filePath){
//法一
if(access(filePath,R_OK)==0)
return 1;
int sys_access_ret=syscall(SYS_access,filePath,F_OK); //法二
if(sys_access_ret!=-1)
return 1;
struct stat statBuf; //法三
int statRet=stat(filePath,&statBuf);
if(statRet==0)
return 1;
char* dirPath=dirname(filePath); //法四
char* fileName=basename(filePath);
int cd=chdir(dirPath);
if(cd!=0)
return -1;
if(access(fileName,R_OK)==0)
return 1;
chdir("/");
return -1;
};
//...
char* flist[2]={"/system/lib/libnoxd.so","/system/lib/vboxsf.ko"};
for(num=0;num<2;num++)
if(check_file_exist(flist[num])!=0){
buffer[count]=(char*)malloc(LENGTH*sizeof(char));
strcpy(buffer[count],flist[num]);
break;
};

还可以模拟器特征应用检测,有些设备上存在只属于模拟器厂商预装的应用:

1
2
3
4
5
6
7
8
char* pkg="com.xxx.xxx";
FILE* fp=popen("pm list package -3","r");
char buffer[128];
while((fgets(buffer,sizeof(buffer),fp))!=NULL)
if(strcasestr(buffer,pkg)!=NULL){
pclose(fp);
return 1;
};

Root检测

对Android设备进行Root时需要在系统中植入特定可执行文件,可检测是否存在这类文件判断设备是否被执行Root操作:

1
char* feature[]={"/su","/su/bin/su","/sbin/su","/data/local/xbin/su","/data/local/bin/su","/data/local/su","/system/xbin/su","/system/bin/su","/system/sd/xbin/su","/system/bin/failsafe/su","/system/bin/cufsdosck","/system/xbin/cufsdosck","/system/bin/cufsmgr","/system/xbin/cufsmgr","/system/bin/cufaevdd","/system/xbin/cufaevdd","/system/bin/conbb","/system/xbin/conbb"};

Magisk工具提供了自身隐藏功能,但没隐藏它的命令行工具,检测Shell命令判断是否安装Magisk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int checkMagisk(void){
char* buffer=(char*)calloc(26,sizeof(char));
FILE* fp=popen("magisk --list","r");
char* flag="not found";
while(fgets(pread_info,25,fp)!=NULL){
if(strstr(pread_info,flag)!=NULL){
free(buffer);
pclose(fp);
return -1;
};
memset(buffer,0,26);
};
free(buffer);
pclose(fp);
return 1;
};

函数Hook检测

Android应用中Java方法或实例被Hook后属性变更为Native属性,这里检查目标方法的属性是否变更为Native属性判断Java函数是否遭到Hook攻击:

1
2
3
4
5
6
7
8
9
10
11
public boolean checkHook(){
try{
Method field=Demo.class.getDeclareMethod("test");
if(Modifier.isNative(field.getModifiers()))
return true;
else
return false;
}catch(Exception e){
e.printStackTrace();
}
}

攻击者可能对isNative函数进行Hook攻击并篡改。每个Java函数都有一个accessFlags属性,属性值为ACC_NATIVE(0x0100)时表示该函数为Native函数,即该函数遭到Hook攻击。accessFlags为隐藏API,需要JNI层通过反射方式获取属性值:

1
2
3
4
5
6
7
8
9
10
11
jstring checkJavaHook(JNIEnv* env,jobject obj){
char* clazz="android/content/Contex";
char* method="getPackageName";
char* sign="()Ljava/lang/String;";
jclass targetClazz=enc->FindClass(clazzname);
jmethodID methodId=env->GetMethodID(clazz,method,sign);
art::mirror_9_0::ArtMethod* artmeth=reinterpret_cast<art::mirror_9_0::ArtMethod*>(methodId);
uint32_t flag=artmeth->access_flags_;
snprintf(result,sizeof(result),"0x%08x",flag);
return charToJstring(env,result);
};

Java层也可实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public String checkJavaHook(Class cls,String methodName){
try{
Method method=cls.getDeclaredMethod(methodName);
if(Build.VERSION.SDK_INI<Build.VERSION_CODES.M){
int flag=(int)artMethod.invoke(method);
return "0x"+String.format("%08x",flag);
}
else{
int flag=(int)accessFlags.get(method);
return "0x"+String.format("%08x",flag);
}
}catch(Exception e){
e.printStackTrace();
}
return "nosupported version";
}

GOT Hook检测

当动态库调用外部目标函数时,先经过过程链接表PLT,跳转至全局偏移表GOT获取目标全局偏移,再通过基地址加上目标函数偏移计算出目标函数真实地址。目前ARM架构CPU不支持延迟绑定机制,所有外部函数引用再动态库链接时已将偏移地址填充到GOT中,不需要函数地址次触发调用时用_dl_runtime_resolve获取函数偏移地址并填充至GOT中。GOT Hook就是将目标函数在GOT中函数偏移地址替换成攻击者自己的函数地址。

GOT Hook涉及到的ELF节表有:

节表名 含义
.dynsym 动态符号表,包含动态库中所有符号信息,如函数名、变量名等
.dynstr 字符串表,包含用于动态链接的字符串,是与符号表相关的名字
.hash 哈希表,包含符号表中所有Hash值,通过此表可快速查找目标函数名
.rela.plt 重定位表,包含PLT中RELA类型动态库重定向信息
.rela.dyn 重定位表,包含PLT以外RELA类型动态库重定向信息

解析目标SO获取.hash内容,遍历符号表中索引,结合.dynsym和.dynstr判断目标函数是否存在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
uintptr_t GotHook::getFuncAddrInMem(const char* target_func) {
//查找符号 .hash -> .dynsym -> .dynstr
Elf_Sym *target = nullptr;
uintptr_t target_addr = 0;
uint32_t hash = elf_hash((const uint8_t *) target_func);
auto *dynsym = reinterpret_cast<Elf_Sym *>(lib_base + p_Elf_Rel_Dyn.sym_tab_offset);
char *dynstr = reinterpret_cast<char *>(lib_base + p_Elf_Rel_Dyn.str_tab_offset);
for (uint32_t i = p_Elf_Hash.buckets[hash % p_Elf_Hash.buckets_cnt]; 0 != i; i = p_Elf_Hash.chains[i]) {
Elf_Sym *tmp_sym = dynsym + i;
unsigned char type = ELF_ST_TYPE(tmp_sym->st_info);
if (STT_FUNC != type && STT_GNU_IFUNC != type && STT_NOTYPE != type)
continue;
LOGE("find target function ====> %s", dynstr + tmp_sym->st_name);
if (0 == strcmp(reinterpret_cast<const char *>(dynstr + tmp_sym->st_name), target_func)) {
target = tmp_sym;
break;
}
}
//遍历 .rel.plt / .rela.plt 和 .rel.dyn / .rela.dyn,获取偏移,计算内存地址
auto * rela_plt = reinterpret_cast<Elf_Rela *>(lib_base + p_Elf_Rel_Dyn.rela_plt_offset);
for (int i = 0; i < p_Elf_Rel_Dyn.rela_plt_size; i++)
if (&(dynsym[ELF_R_SYM(rela_plt[i].r_info)]) == target && ELF_R_TYPE(rela_plt[i].r_info) == ELF_R_JUMP_SLOT)
target_addr = *(uintptr_t *) (lib_base+rela_plt[i].r_offset);
auto * rela_dyn = reinterpret_cast<Elf_Rela *>(lib_base + p_Elf_Rel_Dyn.rela_dyn_offset);
for (int i = 0; i < p_Elf_Rel_Dyn.rela_dyn_size; i++)
if (&(dynsym[ELF_R_SYM(rela_dyn[i].r_info)]) == target && (ELF_R_TYPE(rela_dyn[i].r_info) == ELF_R_ABS || ELF_R_TYPE(rela_dyn[i].r_info) == ELF_R_GLOB_DAT))
target_addr = *(uintptr_t *) (lib_base+rela_dyn[i].r_offset);
return target_addr;
}

然后将目标函数原始地址和从内存中获取的目标函数地址进行对比,两者不一致则目标函数受到了GOT Hook攻击。

Inline Hook检测

将目标函数头部指令替换为跳转指令,改变原函数执行流程。原函数执行到被替换指令位置时会强制跳转至伪造的函数中执行,伪造的函数通常保留原函数调用地址,以便伪造函数执行完毕后跳转回原函数继续执行。检测Inline Hook思路为:获取待检测目标文件,分析目标文件中函数符号表;枚举函数中每个函数,获取目标函数地址;获取文件中目标函数头部机器指令序列;获取目标函数在内存中机器指令序列;对比两次获取的机器指令序列,得出检测结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/**
* 初始化各种常用的指针,方便其它功能使用
* @param p_base elf文件映射到内存的起始地址
*/
void InlineHook::initSectionPtrs(void * p_base) {
p_shstrtab = ((char *)p_base + p_Elf_Shdr[p_Elf_Ehdr->e_shstrndx].sh_offset);;
int sh_num = p_Elf_Ehdr->e_shnum;
for(int indx = 0; indx < sh_num; ++indx)
switch (p_Elf_Shdr[indx].sh_type) {
case SHT_DYNSYM: {//.dynsym (导入导出的符号(函数,全局变量))
p_Elf_Sym_Dynsym = reinterpret_cast<Elf_Sym *>((char *) p_base + p_Elf_Shdr[indx].sh_offset);
dynsym_item_count = p_Elf_Shdr[indx].sh_size / p_Elf_Shdr[indx].sh_entsize;
break;
};
case SHT_STRTAB: {//.dynstr .shstrtab .strtab
char *name = p_shstrtab + p_Elf_Shdr[indx].sh_name;
char *table = ((char *) p_base + p_Elf_Shdr[indx].sh_offset);
if (strcmp(name, ".dynstr") == 0)
p_dynstr = table;
break;
};
};
return;
};

/**
* 从dynsym对应的符号表中查找: 函数或者变量 对应的Elf_Sym结构体
* @param target_func 要查找的符号名
* @param sym 传出参数,如果查询成功,sym保存了to_find符号对应的符号信息
* @return 成功返回true, 识别返回false
*/
bool InlineHook::findTargetFuncSym(const char* target_func, Elf_Sym & sym) {
bool result = false;
if(target_func == nullptr || p_Elf_Sym_Dynsym == nullptr || dynsym_item_count == 0)
return result;
for(int index = 0; index < dynsym_item_count; ++index){
//st_name == 0, 表示符号没有名字,不是我们想要的
if(p_Elf_Sym_Dynsym[index].st_name == 0)
continue;
const char * sys_name = p_dynstr + p_Elf_Sym_Dynsym[index].st_name;
int type = ELF_ST_TYPE(p_Elf_Sym_Dynsym[index].st_info);
if(strcmp(target_func, sys_name) == 0 && (type == STT_FUNC || type == STT_OBJECT)){//查找成功
sym = p_Elf_Sym_Dynsym[index];
result = true;
break;
};
};
return result;
};

bool InlineHook::getMachineCode(const char * target_func, char * buffer, int buffer_len) {
Elf_Sym sym;
if(findTargetFuncSym(target_func, sym)) {
char * p_machine_code = nullptr;
char * p_file = static_cast<char *>(base_addr);
if(sym.st_value & 0x00000001) //for thumb
p_machine_code = p_file + (sym.st_value - 1);
else
p_machine_code = p_file + sym.st_value;
if(buffer_len > sym.st_size)
memcpy(buffer, p_machine_code, sym.st_size);
else
memcpy(buffer, p_machine_code, buffer_len);
return true;
};
return false;
};

bool InlineHook::checkInlineHook(const char *target_func, int system_version) {
if (target_func == nullptr)
return false;
if (!initELF(LIB_PATH)) {
LOGE("initELF libc.so failed");
return false;
};
void * p_lib_handle = dlopen(LIB_PATH, RTLD_NOW);
if(!p_lib_handle) {
LOGE("load libc.so failed");
return false;
};
char file_code_buffer[8] = {0}; // 用于存储文件中的机器码
char mem_code_buffer[8] = {0}; // 用于存储内存中的机器码
// 获取文件中函数的机器码
if(!getMachineCode(target_func, file_code_buffer, sizeof(file_code_buffer))) {
dlclose(p_lib_handle);
LOGE("parse libc.so failed");
return false;
};
// 获取函数地址
intptr_t p_func_address = reinterpret_cast<intptr_t>(dlsym(p_lib_handle, target_func));
// android10部分机器代码段不可读的,强制修改为可读
if(system_version >= 1) {
intptr_t start = PAGE_START(p_func_address);
int ret = mprotect(reinterpret_cast<void *>(start), static_cast<size_t>(getpagesize()), PROT_READ | PROT_EXEC);
if(ret != 0) {
dlclose(p_lib_handle);
LOGE("mprotect failed");
return false;
};
};
//根据指令类型不同,从不同的起始地址拷贝机器码
if(p_func_address & 0x00000001)
memcpy(mem_code_buffer, (char *)(p_func_address - 1), sizeof(mem_code_buffer));
else
memcpy(mem_code_buffer, (char *)p_func_address, sizeof(mem_code_buffer));
string hex_code_of_file = changeMachineCodeToHex(file_code_buffer, sizeof(file_code_buffer));
string hex_code_of_mem = changeMachineCodeToHex(mem_code_buffer, sizeof(file_code_buffer));
LOGE("file code:%s",hex_code_of_file.c_str());
LOGE("meme code:%s",hex_code_of_mem.c_str());
dlclose(p_lib_handle);
if (hex_code_of_file == hex_code_of_mem)
return false;
return true;
};

GOT Hook与Inline Hook的完整代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
//check_got_hook.cpp
#include "check_got_hook.h"
uintptr_t getFuncOrginalAddress(const char* lib_path, const char* func_name){
void *handle = dlopen(lib_path, RTLD_LAZY);
return (uintptr_t) dlsym(handle, func_name);
};
void getLibPath(const char *src, char *dest){
while (*src != '/')
*src++;
strncpy(dest, src, strlen(src) - 1);
};
uintptr_t getLibModuleAddr(const char *module_name, char* lib_path){
char buff[256] = "\n";
char cmd[256] = "\n";
uintptr_t addr = 0;
pid_t pid = getpid();
sprintf(cmd, "/proc/%d/maps",pid);
FILE *fp = fopen(cmd, "r");
while (fgets(buff, sizeof(buff), fp))
if (strstr(buff, module_name) && sscanf(buff, "%" SCNxPTR, &addr) == 1){
getLibPath(buff, lib_path);
return addr;
};
fclose(fp);
return 0;
};
bool GotHook::initELF(const char * lib_path){
FILE *fp = fopen(lib_path, "rb");
if (fp == nullptr)
return false;
int fd = fileno(fp);
struct stat stat;
if (fstat(fd, &stat) != 0)
return false;
int len = static_cast<size_t>(stat.st_size);
base_addr = mmap(nullptr, len, PROT_READ, MAP_PRIVATE, fd, 0);
if (base_addr == MAP_FAILED)
return false;
//初始化Elf header
p_Elf_Ehdr = static_cast<Elf_Ehdr *>(base_addr);
p_Elf_Phdr = reinterpret_cast<Elf_Phdr *>((char *)base_addr + p_Elf_Ehdr->e_phoff);
initDynamic();
return true;
};
bool GotHook::initDynamic(){
uintptr_t dynamic_size = 0;
uintptr_t dyn_offset = 0;
int ph_num = p_Elf_Ehdr->e_phnum;
for (int index = 0; index < ph_num; index++)
if (PT_DYNAMIC == p_Elf_Phdr[index].p_type){
dynamic_size = p_Elf_Phdr[index].p_memsz;
dyn_offset = p_Elf_Phdr[index].p_vaddr;
}
p_Elf_Dyn = reinterpret_cast<Elf_Dyn *>((char *) lib_base + dyn_offset);
for (int i = 0; i < dynamic_size/sizeof(Elf_Dyn); ++i){
switch (p_Elf_Dyn[i].d_tag){
case DT_SYMTAB: // .dynsym
p_Elf_Rel_Dyn.sym_tab_offset = p_Elf_Dyn[i].d_un.d_ptr;
break;
case DT_STRTAB: // .dynstr
p_Elf_Rel_Dyn.str_tab_offset = p_Elf_Dyn[i].d_un.d_ptr;
break;
case DT_JMPREL:// .rel.plt / .rela.plt
p_Elf_Rel_Dyn.rela_plt_offset = p_Elf_Dyn[i].d_un.d_ptr;
break;
case DT_PLTRELSZ:
p_Elf_Rel_Dyn.rela_plt_size = p_Elf_Dyn[i].d_un.d_val/sizeof(Elf_Rela);
break;
case DT_RELA: // .rel.dyn / .rela.dyn
p_Elf_Rel_Dyn.rela_dyn_offset = p_Elf_Dyn[i].d_un.d_ptr;
break;
case DT_RELASZ:
p_Elf_Rel_Dyn.rela_dyn_size = p_Elf_Dyn[i].d_un.d_val / sizeof(Elf_Rel);
break;
case DT_HASH: // .hash
auto rawdata = (const uint32_t *) (lib_base + p_Elf_Dyn[i].d_un.d_ptr);
p_Elf_Hash.buckets_cnt = rawdata[0];
p_Elf_Hash.chains_cnt = rawdata[1];
p_Elf_Hash.buckets = &(rawdata[2]);
p_Elf_Hash.chains = &(p_Elf_Hash.buckets[p_Elf_Hash.buckets_cnt]);
break;
};
};
return true;
};
uint32_t elf_hash(const uint8_t *name){
uint32_t h = 0, g;
while (*name){
h = (h << 4) + *name++;
g = h & 0xf0000000;
h ^= g;
h ^= g >> 24;
};
return h;
};
uintptr_t GotHook::getFuncAddrInMem(const char* target_func) {
//查找符号 .hash -> .dynsym -> .dynstr
Elf_Sym *target = nullptr;
uintptr_t target_addr = 0;
uint32_t hash = elf_hash((const uint8_t *) target_func);
auto *dynsym = reinterpret_cast<Elf_Sym *>(lib_base + p_Elf_Rel_Dyn.sym_tab_offset);
char *dynstr = reinterpret_cast<char *>(lib_base + p_Elf_Rel_Dyn.str_tab_offset);
for (uint32_t i = p_Elf_Hash.buckets[hash % p_Elf_Hash.buckets_cnt]; 0 != i; i = p_Elf_Hash.chains[i]) {
Elf_Sym *tmp_sym = dynsym + i;
unsigned char type = ELF_ST_TYPE(tmp_sym->st_info);
if (STT_FUNC != type && STT_GNU_IFUNC != type && STT_NOTYPE != type)
continue;
LOGE("find target function ====> %s", dynstr + tmp_sym->st_name);
if (0 == strcmp(reinterpret_cast<const char *>(dynstr + tmp_sym->st_name), target_func)) {
target = tmp_sym;
break;
};
};
//遍历 .rel.plt / .rela.plt 和 .rel.dyn / .rela.dyn,获取偏移,计算内存地址
auto * rela_plt = reinterpret_cast<Elf_Rela *>(lib_base + p_Elf_Rel_Dyn.rela_plt_offset);
for (int i = 0; i < p_Elf_Rel_Dyn.rela_plt_size; i++)
if (&(dynsym[ELF_R_SYM(rela_plt[i].r_info)]) == target && ELF_R_TYPE(rela_plt[i].r_info) == ELF_R_JUMP_SLOT)
target_addr = *(uintptr_t *) (lib_base+rela_plt[i].r_offset);
auto * rela_dyn = reinterpret_cast<Elf_Rela *>(lib_base + p_Elf_Rel_Dyn.rela_dyn_offset);
for (int i = 0; i < p_Elf_Rel_Dyn.rela_dyn_size; i++)
if (&(dynsym[ELF_R_SYM(rela_dyn[i].r_info)]) == target && (ELF_R_TYPE(rela_dyn[i].r_info) == ELF_R_ABS || ELF_R_TYPE(rela_dyn[i].r_info) == ELF_R_GLOB_DAT))
target_addr = *(uintptr_t *) (lib_base+rela_dyn[i].r_offset);
return target_addr;
};
bool GotHook::checkGotHook(const char* lib_name, const char* func_name){
char lib_path[256] = {0};
lib_base = getLibModuleAddr(lib_name, lib_path);
if (lib_base == 0)
return false;
LOGI("Check CheckJavaHook lib:[%s]\n", lib_path);
// 获取文件中目标函数的原始地址
uintptr_t orgin_func_addr = getFuncOrginalAddress(lib_path, func_name);
LOGE("%s origin address == %lu\n", func_name, orgin_func_addr);
initELF(lib_path);
// 获取内存中目标函数的地址 (解析Segment)
uintptr_t target_addr = getFuncAddrInMem(func_name);
LOGE("target function addr ====> %lu", target_addr);
if (orgin_func_addr != target_addr)
return true;
return false;
};

//check_got_hook.h
#ifndef CHECK_GOT_HOOK_H
#define CHECK_GOT_HOOK_H
#include <jni.h>
#include <string>
#include <cstddef>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <dlfcn.h>
#include <sys/types.h>
#include <unistd.h>
#include <inttypes.h>
#include <elf.h>
#include "comm_elf.h"
#include "common.h"
#if defined(__aarch64__)
#define LIB_PATH "/system/lib64/libc.so"
#else
#define LIB_PATH "/system/lib/libc.so"
#endif
#if defined(__arm__)
#define ELF_R_JUMP_SLOT R_ARM_JUMP_SLOT //.rela.plt
#define ELF_R_GLOB_DAT R_ARM_GLOB_DAT //.rela.dyn
#define ELF_R_ABS R_ARM_ABS32 //.rela.dyn
#elif defined(__aarch64__)
#define ELF_R_JUMP_SLOT R_AARCH64_JUMP_SLOT
#define ELF_R_GLOB_DAT R_AARCH64_GLOB_DAT
#define ELF_R_ABS R_AARCH64_ABS64
#endif
class GotHook{
public:
GotHook(){
base_addr = nullptr;
lib_base = 0;
p_Elf_Ehdr = nullptr;
p_Elf_Shdr = nullptr;
p_Elf_Phdr = nullptr;
p_Elf_Dyn = nullptr;
p_Elf_Rel_Table = nullptr;
}
bool checkGotHook(const char* lib_name, const char* func_name);
private:
bool initELF(const char * lib_path);
bool initDynamic();
uintptr_t getFuncAddrInMem(const char* target_func);
private:
typedef struct{
uint64_t str_tab_offset;
uint64_t sym_tab_offset;
uint64_t rela_plt_offset;
uint32_t rela_plt_size;
uint64_t rela_dyn_offset;
uint32_t rela_dyn_size;
}Elf_Rel_Dyn;
typedef struct{ //.hash
const uint32_t *buckets;
uint32_t buckets_cnt;
const uint32_t *chains;
uint32_t chains_cnt;
}Elf_Hash;
void * base_addr;
Elf_Ehdr * p_Elf_Ehdr; //ELF文件头
Elf_Shdr * p_Elf_Shdr; //ELF节头
Elf_Phdr * p_Elf_Phdr; //ELF程序头
Elf_Dyn * p_Elf_Dyn; //指向.dynamic段
Elf_Rel * p_Elf_Rel_Table; //指向重定位表
Elf_Sym * p_Elf_Sym_Got_Table; //指向Got表数组
uintptr_t lib_base;
Elf_Rel_Dyn p_Elf_Rel_Dyn; //指向重定向信息
Elf_Hash p_Elf_Hash;
};
#endif //CHECK_GOT_HOOK_H

//check_inline_hook.cpp
#include "check_inline_hook.h"
using namespace std;
string changeMachineCodeToHex(const char * input, int len){
string hex_code;
for(int i = 0; i < len; ++i){
char buffer[10] = {0};
sprintf(buffer,"%02X ", input[i]);
hex_code.append(buffer);
};
return hex_code;
};
InlineHook:: ~InlineHook() {//释放内存
if (lib_fp)
fclose(lib_fp);
if (base_addr != nullptr && base_addr != MAP_FAILED)
munmap(base_addr, lib_file_size);
};
bool InlineHook::initELF(const char * lib_path){ //加载elf到内存, 初始化:节头表指针 程序头表指针
lib_fp = fopen(lib_path, "rb");
if (lib_fp == nullptr)
return false;
int fd = fileno(lib_fp);
struct stat stat;
if (fstat(fd, &stat) != 0)
return false;
lib_file_size = static_cast<size_t>(stat.st_size);
base_addr = mmap(nullptr, lib_file_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (base_addr == MAP_FAILED)
return false;
//初始化Elf header
p_Elf_Ehdr = static_cast<Elf_Ehdr *>(base_addr);
p_Elf_Shdr = reinterpret_cast<Elf_Shdr *>((char *)base_addr + p_Elf_Ehdr->e_shoff);
initSectionPtrs(base_addr);
return true;
};
/**
* 初始化各种常用的指针,方便其它功能使用
* @param p_base elf文件映射到内存的起始地址
*/
void InlineHook::initSectionPtrs(void * p_base) {
p_shstrtab = ((char *)p_base + p_Elf_Shdr[p_Elf_Ehdr->e_shstrndx].sh_offset);;
int sh_num = p_Elf_Ehdr->e_shnum;
for(int indx = 0; indx < sh_num; ++indx)
switch (p_Elf_Shdr[indx].sh_type) {
case SHT_DYNSYM: {//.dynsym (导入导出的符号(函数,全局变量))
p_Elf_Sym_Dynsym = reinterpret_cast<Elf_Sym *>((char *) p_base + p_Elf_Shdr[indx].sh_offset);
dynsym_item_count = p_Elf_Shdr[indx].sh_size / p_Elf_Shdr[indx].sh_entsize;
break;
};
case SHT_STRTAB: {//.dynstr .shstrtab .strtab
char *name = p_shstrtab + p_Elf_Shdr[indx].sh_name;
char *table = ((char *) p_base + p_Elf_Shdr[indx].sh_offset);
if (strcmp(name, ".dynstr") == 0)
p_dynstr = table;
break;
};
};
return;
};
/**
* 从dynsym对应的符号表中查找: 函数或者变量 对应的Elf_Sym结构体
* @param target_func 要查找的符号名
* @param sym 传出参数,如果查询成功,sym保存了to_find符号对应的符号信息
* @return 成功返回true, 识别返回false
*/
bool InlineHook::findTargetFuncSym(const char* target_func, Elf_Sym & sym) {
bool result = false;
if(target_func == nullptr || p_Elf_Sym_Dynsym == nullptr || dynsym_item_count == 0)
return result;
for(int index = 0; index < dynsym_item_count; ++index){
//st_name == 0, 表示符号没有名字,不是我们想要的
if(p_Elf_Sym_Dynsym[index].st_name == 0)
continue;
const char * sys_name = p_dynstr + p_Elf_Sym_Dynsym[index].st_name;
int type = ELF_ST_TYPE(p_Elf_Sym_Dynsym[index].st_info);
if(strcmp(target_func, sys_name) == 0 && (type == STT_FUNC || type == STT_OBJECT)){//查找成功
sym = p_Elf_Sym_Dynsym[index];
result = true;
break;
};
};
return result;
};
bool InlineHook::getMachineCode(const char * target_func, char * buffer, int buffer_len) {
Elf_Sym sym;
if(findTargetFuncSym(target_func, sym)) {
char * p_machine_code = nullptr;
char * p_file = static_cast<char *>(base_addr);
if(sym.st_value & 0x00000001) //for thumb
p_machine_code = p_file + (sym.st_value - 1);
else
p_machine_code = p_file + sym.st_value;
if(buffer_len > sym.st_size)
memcpy(buffer, p_machine_code, sym.st_size);
else
memcpy(buffer, p_machine_code, buffer_len);
return true;
};
return false;
};
bool InlineHook::checkInlineHook(const char *target_func, int system_version) {
if (target_func == nullptr)
return false;
if (!initELF(LIB_PATH)) {
LOGE("initELF libc.so failed");
return false;
};
void * p_lib_handle = dlopen(LIB_PATH, RTLD_NOW);
if(!p_lib_handle) {
LOGE("load libc.so failed");
return false;
};
char file_code_buffer[8] = {0}; // 用于存储文件中的机器码
char mem_code_buffer[8] = {0}; // 用于存储内存中的机器码
// 获取文件中函数的机器码
if(!getMachineCode(target_func, file_code_buffer, sizeof(file_code_buffer))) {
dlclose(p_lib_handle);
LOGE("parse libc.so failed");
return false;
};
// 获取函数地址
intptr_t p_func_address = reinterpret_cast<intptr_t>(dlsym(p_lib_handle, target_func));
// android10部分机器代码段不可读的,强制修改为可读
if(system_version >= 1) {
intptr_t start = PAGE_START(p_func_address);
int ret = mprotect(reinterpret_cast<void *>(start), static_cast<size_t>(getpagesize()), PROT_READ | PROT_EXEC);
if(ret != 0) {
dlclose(p_lib_handle);
LOGE("mprotect failed");
return false;
};
};
//根据指令类型不同,从不同的起始地址拷贝机器码
if(p_func_address & 0x00000001)
memcpy(mem_code_buffer, (char *)(p_func_address - 1), sizeof(mem_code_buffer));
else
memcpy(mem_code_buffer, (char *)p_func_address, sizeof(mem_code_buffer));
string hex_code_of_file = changeMachineCodeToHex(file_code_buffer, sizeof(file_code_buffer));
string hex_code_of_mem = changeMachineCodeToHex(mem_code_buffer, sizeof(file_code_buffer));
LOGE("file code:%s",hex_code_of_file.c_str());
LOGE("meme code:%s",hex_code_of_mem.c_str());
dlclose(p_lib_handle);
if (hex_code_of_file == hex_code_of_mem)
return false;
return true;
};

//check_inline_hook.h
#ifndef CHECK_INLINE_HOOK_H
#define CHECK_INLINE_HOOK_H
#include <jni.h>
#include <fcntl.h>
#include <cstring>
#include <sys/system_properties.h>
#include <sys/mman.h>
#include <string>
#include <dlfcn.h>
#include <sys/ptrace.h>
#include "common.h"
#include "comm_elf.h"
#if defined(__aarch64__)
#define LIB_PATH "/system/lib64/libc.so"
#else
#define LIB_PATH "/system/lib/libc.so"
#endif
// Returns the address of the page containing address 'x'.
#define PAGE_START(x) ((x) & PAGE_MASK) // 把后12位置0
class InlineHook{
public:
InlineHook(){
base_addr = nullptr;
lib_fp = nullptr;
p_Elf_Ehdr = nullptr;
lib_file_size = 0;
p_Elf_Sym_Dynsym = nullptr;
dynsym_item_count = 0;
p_shstrtab = nullptr;
p_dynstr = nullptr;
}
virtual ~InlineHook();
bool checkInlineHook(const char* symbol, int version);
private:
void initSectionPtrs(void * p_base);
bool initELF(const char * lib_path);
//获取函数的机器码
bool getMachineCode(const char * target_func, char * buffer, int buffer_len);
//给一个符号,查找它在dynsym表中对应的信息, 查找效率低
bool findTargetFuncSym(const char* target_func, Elf_Sym & sym);
private:
void * base_addr;
size_t lib_file_size;
FILE* lib_fp;
Elf_Ehdr * p_Elf_Ehdr; //ELF文件头
Elf_Shdr * p_Elf_Shdr; //ELF节头
Elf_Sym * p_Elf_Sym_Dynsym; //指向动态符号表数组
size_t dynsym_item_count; //数组元素个数
char * p_shstrtab; // 指向.shstrtab,字符串表, 这个表只存储节的名字
char * p_dynstr; // 指向.dynstr,字符串表,这个表里面存储了所有用于动态链接的符号(函数名,变量名)的名字
};
#endif //CHECK_INLINE_HOOK_H

//comm_elf.h
#ifndef COMM_ELF_H
#define COMM_ELF_H
#include <elf.h>
#if defined(__aarch64__)
#define Elf_Ehdr Elf64_Ehdr
#define Elf_Shdr Elf64_Shdr
#define Elf_Phdr Elf64_Phdr
#define Elf_Sym Elf64_Sym
#define Elf_Dyn Elf64_Dyn
#define Elf_Rel Elf64_Rel
#define Elf_Rela Elf64_Rela
#define ELF_R_SYM ELF64_R_SYM
#define ELF_R_TYPE ELF64_R_TYPE
#define ELF_Word Elf64_Word
#define Elf_Addr Elf64_Addr
#define ElfW(what) Elf64_ ## what
#else
#define Elf_Ehdr Elf32_Ehdr
#define Elf_Shdr Elf32_Shdr
#define Elf_Phdr Elf32_Phdr
#define Elf_Sym Elf32_Sym
#define Elf_Dyn Elf32_Dyn
#define Elf_Rel Elf32_Rel
#define Elf_Rela Elf32_Rela
#define ELF_R_SYM ELF32_R_SYM
#define ELF_R_TYPE ELF32_R_TYPE
#define ELF_Word Elf32_Word
#define Elf_Addr Elf32_Addr
#define ElfW(what) Elf32_ ## what
#endif //__aarch64__
#endif //COMM_ELF_H

//common.h
#ifndef _COMMON_H_
#define _COMMON_H_
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <libgen.h>
#include <unistd.h>
#include <sys/stat.h>
#include <time.h>
#include <sys/file.h>
#include <android/log.h>
#define TAG "CHECK-HOOK"
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
#define CALLOC(num,type) (type*)calloc(num, sizeof(type))
#define LOGINFO 1 //日志开关,1为开,其它为关
#define LOGIDEBUG 1 //日志开关,1为开,其它为关
#define LOGIERROR 1 //日志开关,1为开,其它为关
/**
* release版本 需要关闭开关
* 必须注释掉日志输出代码
*/
#if(LOGINFO==1)
// 定义info信息
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#else
#define LOGI(...) NULL
#endif
#if(LOGIDEBUG==1)
// 定义debug信息
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#else
#define LOGD(...) NULL
#endif
#if(LOGIERROR==1)
// 定义error信息
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
#else
#define LOGE(...) NULL
#endif
#define safe_free(p) mm_safe_free((void **)&(p))
#ifdef __cplusplus
extern "C" {
#endif
void mSafeFree(void **pp);
#ifdef __cplusplus
}
#endif
#endif

//native-lib.cpp
#include <jni.h>
#include <string>
#include <cstddef>
#include <sys/mman.h>
#include <dlfcn.h>
#include <sys/types.h>
#include <unistd.h>
#include <elf.h>
#include "comm_elf.h"
#include "common.h"
#include "xhook.h"
#include "test.h"
#include "check_got_hook.h"
#include "check_inline_hook.h"
void* (*oldmalloc)(size_t __byte_count);
extern "C" JNIEXPORT jstring JNICALL Java_com_check_hook_MainActivity_checkGotHook(JNIEnv* env,jobject /* this */) {
// 开启GOT CheckJavaHook
xhook_register(".*/libnative-lib\\.so$", "malloc", (void *)(my_malloc), (void**)(&oldmalloc));
xhook_refresh(1);
say_hello();
const char* target_func = "malloc";
char hook[56] = "\n";
char safe[56] = "\n";
sprintf(hook, "%s was be got hooked!",target_func);
sprintf(safe, "%s is safe!",target_func);
//checkGotHook("libc.so", "malloc");
GotHook mGotHook;
bool result = mGotHook.checkGotHook("libnative-lib.so", target_func);
if (result){
LOGE("%s was be got hooked!\n", target_func);
return env->NewStringUTF(hook);
};
return env->NewStringUTF(safe);
};
extern "C" JNIEXPORT jstring JNICALL Java_com_check_hook_MainActivity_checkInlineHook(JNIEnv* env,jobject /* this */) {
const char* target_func = "malloc";
char hook[56] = "\n";
char safe[56] = "\n";
sprintf(hook, "%s was be inline hooked!",target_func);
sprintf(safe, "%s is safe!",target_func);
InlineHook mInlineHook;
bool result = mInlineHook.checkInlineHook(target_func, 1);
if (result){
LOGE("%s was be inline hooked!\n", target_func);
return env->NewStringUTF(hook);
};
LOGE("%s is safe!\n", target_func);
return env->NewStringUTF(safe);
};

//test.c
#include "test.h"
#include "common.h"
void* (*oldmalloc)(size_t __byte_count);
void *my_malloc(size_t size){
LOGE("%zu bytes memory are allocated by libnative-lib.so\n", size);
return oldmalloc(size);
};
void say_hello(){
char *buf = malloc(512);
//LOGE("Malloc address ===> %lu\n", &malloc);
if(NULL != buf)
snprintf(buf, 1024, "%s", "hello\n");
};

//test.h
#pragma once
#ifndef CHECKNATIVEHOOK_TEST_H
#define CHECKNATIVEHOOK_TEST_H
#include <stdlib.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include "common.h"
#include <elf.h>
#ifdef __cplusplus
extern "C" {
#endif
void *my_malloc(size_t size);
void say_hello();
#ifdef __cplusplus
}
#endif
#endif //CHECKNATIVEHOOK_TEST_H

//xhook.h
#ifndef XHOOK_H
#define XHOOK_H 1
#ifdef __cplusplus
extern "C" {
#endif
#define XHOOK_EXPORT __attribute__((visibility("default")))
int xhook_register(const char *pathname_regex_str, const char *symbol,void *new_func, void **old_func) XHOOK_EXPORT;
int xhook_ignore(const char *pathname_regex_str, const char *symbol) XHOOK_EXPORT;
int xhook_refresh(int async) XHOOK_EXPORT;
void xhook_clear() XHOOK_EXPORT;
void xhook_enable_debug(int flag) XHOOK_EXPORT;
void xhook_enable_sigsegv_protection(int flag) XHOOK_EXPORT;
#ifdef __cplusplus
}
#endif
#endif

调试状态检测

当Android系统的ro.debuggable属性值为1,即使应用调试属性android:debuggable为false,也能被调试器挂起调试。该属性值默认0,为1则认为有安全风险,普通用户权限无法修改。检测代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int checkPhoneDebugstate(){ //法一
FILE* fp=NULL;
char* buf=(char*)malloc(MAX*sizeof(char)+1);
int flag=0;
memset(buf,0,MAX*sizeof(char));
fp=fopen("/default.prop","r");
while(fgets(buf,MAX*sizeof(char),fp)!=NULL)
if(strstr(buf,"ro.debuggable=1")!=NULL)
flag=1;
free(buf);
fclose(fp);
return flag;
};

int checkPhoneDebugstate(){
int debug=0;
char* result=(char*)calloc(12,sizeof(char));
int ret=__system_property_get("ro.debuggable",result);
if((ret>0)&&(strcasestr(result,"1")!=NULL))
debug=1;
free(result);
return debug;
};

VPN状态检测

VPN服务启动则处于风险环境中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static int isVpnUsed() { //是否正在使用VPN
int result = 0;
try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface mNetworkInterface = (NetworkInterface) interfaces.nextElement();
if (!mNetworkInterface.isUp() || mNetworkInterface.getInterfaceAddresses().size() == 0)
continue;
// 如果满足条件则是开启了vpn
if ("tun0".equals(mNetworkInterface.getName()) || "ppp0".equals(mNetworkInterface.getName())) {
result = 1;
return result;
};
};
} catch (Throwable e) {
//e.printStackTrace();
};
return result;
};

代理状态检测

代理开启则处于风险环境中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 检测手机是否开启代理
* @param context
* @return
*/
public static boolean isWifiProxy(Context context) {
final boolean IS_ICS_OR_LATER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
String proxyAddress;
int proxyPort;
if (IS_ICS_OR_LATER) {
proxyAddress = System.getProperty("http.proxyHost");
String portStr = System.getProperty("http.proxyPort");
proxyPort = Integer.parseInt((portStr != null ? portStr : "-1"));
} else {
proxyAddress = android.net.Proxy.getHost(context);
proxyPort = android.net.Proxy.getPort(context);
};
return (!TextUtils.isEmpty(proxyAddress)) && (proxyPort != -1);
};

USB调试状态检测

如下:

1
2
3
4
5
6
7
boolean isUsbDebug(){
try{
return (Settings.Secure.getInt(Context.getContentResolver(),Settings.Secure.ADB_ENABLED,0)>0);
}catch(Exception e){
e.printStackTrace();
};
};

充电状态检测

例如同一个IP地址下多个设备同时处于充电状态时,以为黑灰产人员批量控制设备:

1
2
3
4
5
6
7
8
9
int isCharging(){
int charging=0;
char* result=(char*)calloc(12,sizeof(char));
int ret=__system_property_get("sys.usb.state",result);
if((ret>0)&&(strcasestr(result,"midi")!=NULL))
charging=1;
free(result);
return charging;
};

隐私合规

应用上架合规

软件著作权申请:国内应用上架应用商城时应用商城要求开发者提供当前应用的“计算机软件著作权登记证书”,开发者可通过中国版权保护中心官网进行账号注册,并申请软件著作权登记。申请周期为30~60天,版权保护中心办法的证书为纸质版,部分应用商店为方便审核会要求提供电子版,可通过易版权网站将纸质版证书上传核验,申请响应电子版。电子版证书包含App电子版权认证证书和计算机软件著作权登记证书。App上架到应用商城时,其名称需要和计算机软件著作权证书中名称一致,若发生变更则需要在版权保护中心官网申请软件登记事项变更或补充申请。

ICP备案/ICP许可证:从事非经营性互联网信息服务者需要进行ICP备案,从事经营性互联网信息服务者需取得互联网信息服务增值电信业务ICP经营许可证。应用上架前需要对使用的域名进行ICP备案,开发者可通过自己的网络服务提供商进行ICP备案。开发者可通过电信业务市场综合管理信息系统进行申请ICP许可证。

App备案:境内从事互联网信息服务的App主办者须完成App备案,复用的ICP备案系统。不同平台App名称一致则仅需要备案一次,包名不一致无所谓。App内部使用的域名需要写到二级以上,最多四级。App从事某些服务时,须经有关主管部门审核同意,并在填写备案信息时提供对应前置审批文件。颁发备案号后,该备案信息需要在App显著为止进行公示,要求用户3步以内可查看,点击相关链接后能跳转至备案网站。

1
2
3
4
5
6
7
8
9
出版:游戏需要相关材料,网络出版需要网络出版服务许可证
广播电影电视节目:信息网络传播视听节目许可证
药品和医疗器械:互联网药品信息服务资格证书
文化:网络文化经营许可证
新闻:互联网信息服务许可证
网络预约车:网络预约出租汽车经营许可证
校外培训:教育部门允许并开展校外培训的审批文件
宗教:人民政府宗教事务部门审核同意文件
互联网金融:金融监管部门审批许可文件

安全评估:具有舆论属性或社会动员能力的应用应在上线前开展安全评估,在全国互联网安全管理服务平台进行申请。评估方法推荐自评估,回答问题后,公安机关对提交的安全评估报告进行审核,若发现存在不合规项可能进入现场深恶黑缓解,发现问题后整改后可申请复测。

CCRC认证:教育行政部门和学校开发和使用的存储100万以上个人信息的教育App应通过CCRC认证,该认证过程复杂,周期为半年到一年时间。

算法备案:使用算法推荐服务、提供公众舆论表达渠道或社会动员能力、AIGC类算法。

合规实践

隐私政策:App首次启动应用程序时,应通过弹窗等明显方式提示用户阅读隐私政策,App内账号登陆和注册相关页面提供访问隐私政策的链接,App功能介面也要提供隐私政策访问入口。从App主页面到隐私政策访问入口,用户点击次数不超过4次。每次进入登陆、注册页面,都需要用户主动勾选同意隐私政策,禁止默认勾选或用户无勾选情况下登录注册。隐私政策应与儿童隐私政策、用户协议并列展示。隐私政策浏览方式有TextView、WebView和跳转至系统默认浏览器。

权限申请:最小必要、动态申请、用户可知、不强制不捆绑,索取某些权限被拒后App不应因此退出关闭。

个人信息收集:略。

双清单:个人信息收集清单、第三方信息共享清单、隐私政策摘要、系统权限调用清单。

个性化推荐与定向推送:应用中存在个性化推荐/定向推送的功能时,需要在应用内增加相关功能开关,用户能根据自己喜好和需求选择是否开启个性化推荐。未开启个性化推荐时不应含有“推荐”之类字样标题,可改用“热门”或“精选”等。

自启动与关联启动:除非是提供服务所必须或存在合理场景,应用程序在未经用户同意或缺乏合理使用场景情况下,不得自启动或与第三方应用程序关联启动。可通过删除Manifest.xml中广播监听服务来关闭自启动服务。

1
2
3
4
5
6
7
8
9
10
11
<receiver
android:name="com.demo.EventReceiver"
tools:node="remove"/>
<receiver
android:name="com.demo.receivers.NetworkStatusReceiver"
android:exported="true"
tools:node="remove">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
</intent-filter>
</receiver>

广告展示:开屏广告需要跳转到第三方的,必须有跳过/关闭按钮,与跳转/下载按钮保持距离,并设在项四周延伸50dp或至屏幕边缘。点击页面中跳转/下载按钮以外部分不得执行操作,不得静默下载。广告中必须标识广告字样,时间不超过5喵,禁止误导。弹窗广告大致同上。摇一摇广告触发加速度不小于$15\mathrm{m/s^2}$,转动角度大于$35^\circ$。

违规整改

工业和信息化部及其下属各地通信管理局、中央网络安全和信息化委员会办公室会定期对辖区内企业发布的App进行专项合规治理行动。教育部及其下属电化教育管也定期对应用市场中发布的教育类应用进行合规检查。