安卓逆向入门-安全合规
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 {
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
| URL url=new URL(urlAddress); HttpsURLConnection httpon=(HttpsURLConnection)url.openConnection(Proxy.NO_PROXY);
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
| 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"; if(!certs[0].getPublicKey().equals(expectedPublicKey)){ throw new RuntimeException("证书校验失败"); } } } }; SSLContext sslContext=SSLContext.getInstance("TLS"); sslContext.init(null,trustAllCerts,null); URL url=new URL("https://test.com"); HttpsURLConnection connection=(HttpsURLConnection)url.openConnection(); connection.setSSLSocketFactory(sslContext.getSocketFactory());
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) { 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; } } 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
|
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: { 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: { 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; };
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){ 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) 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)); 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
| #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; 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: p_Elf_Rel_Dyn.sym_tab_offset = p_Elf_Dyn[i].d_un.d_ptr; break; case DT_STRTAB: p_Elf_Rel_Dyn.str_tab_offset = p_Elf_Dyn[i].d_un.d_ptr; break; case DT_JMPREL: 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: 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: 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) { 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; }; }; 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); uintptr_t target_addr = getFuncAddrInMem(func_name); LOGE("target function addr ====> %lu", target_addr); if (orgin_func_addr != target_addr) return true; return false; };
#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 #define ELF_R_GLOB_DAT R_ARM_GLOB_DAT #define ELF_R_ABS R_ARM_ABS32 #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{ 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_Shdr * p_Elf_Shdr; Elf_Phdr * p_Elf_Phdr; Elf_Dyn * p_Elf_Dyn; Elf_Rel * p_Elf_Rel_Table; Elf_Sym * p_Elf_Sym_Got_Table; uintptr_t lib_base; Elf_Rel_Dyn p_Elf_Rel_Dyn; Elf_Hash p_Elf_Hash; }; #endif
#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){ 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; 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; };
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: { 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: { 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; };
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){ 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) 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)); 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; };
#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
#define PAGE_START(x) ((x) & PAGE_MASK) 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); 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_Shdr * p_Elf_Shdr; Elf_Sym * p_Elf_Sym_Dynsym; size_t dynsym_item_count; char * p_shstrtab; char * p_dynstr; }; #endif
#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 #endif
#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 #define LOGIDEBUG 1 #define LOGIERROR 1
#if(LOGINFO==1) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) #else #define LOGI(...) NULL #endif #if(LOGIDEBUG==1) #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__) #else #define LOGD(...) NULL #endif #if(LOGIERROR==1) #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
#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 ) { 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); 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 ) { 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); };
#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); if(NULL != buf) snprintf(buf, 1024, "%s", "hello\n"); };
#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
#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() { 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; if ("tun0".equals(mNetworkInterface.getName()) || "ppp0".equals(mNetworkInterface.getName())) { result = 1; return result; }; }; } catch (Throwable e) { }; return result; };
|
代理状态检测
代理开启则处于风险环境中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
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进行专项合规治理行动。教育部及其下属电化教育管也定期对应用市场中发布的教育类应用进行合规检查。