关于网站建设的ppt,网站 做实名认证吗,朝阳网站制作设计,常州市工程建设交易网聊聊Android签名检测总结与反思
背景#xff1a;
这篇文章只讲Android端签名检测#xff0c;安卓发展到现在#xff0c;因为国内环境没有谷歌市场#xff0c;所以很多官方推荐的Api没法使用 #xff0c;所以国内的签名检测方式也是“千奇百怪” 。发展至今每种方法都有一…聊聊Android签名检测总结与反思
背景
这篇文章只讲Android端签名检测安卓发展到现在因为国内环境没有谷歌市场所以很多官方推荐的Api没法使用 所以国内的签名检测方式也是“千奇百怪” 。发展至今每种方法都有一些绕过或者对抗手段这些方法很难说就一定准 但是我们能做的就是取尽可能的提高攻击者的成本提升Apk的签名检测能力防止灰黑产进行攻击 。基础的什么Java获取签名信息这种基础方案这里暂时跳过 不在过多叙述 。这篇文章主要分为Java和Native两部分 分别从不同的视角取检测签名包括如何对抗等 , 其中包含一些大厂和企业壳的核心检测签名思路 。
Java层检测签名
1、IPC协议获取签名信息
首先讲这块之前需要先简单讲一下IPC协议的实现 。
安卓基础架构就是CS架构。每个App都是客户端服务端只有一个 客户端和服务端是不同的进程 。
这样做的客户端一旦发生崩溃不影响服务端 服务端也可以根据不同的uid实现不同的鉴权操作 。当我们获取一些apk信息的时候都是客户端发送IPC协议 。
服务端接收以后进行处理利用binder进行通讯然后把数据写到客户端内部客户端拿到 。这块设计很多技术点 比如动态代理IPC协议 Binder通讯等
上面说起来可能很难懂 首先我们去Hook PackageInfo的构造方法 根据栈回溯看看如果想要生产一个PackageInfo到底需要哪些流程 。
然后讲一下为什么推荐使用IPC协议去获取签名。由点到面中间可能会存在哪些问题 。大家在测试的时候最好在谷歌原生的系统上测试如果是国产厂商可能会添加自己的一些拦截器 。测试版本Android 11 。从上往下看 一步一步讲一下 每一步到底做什么的 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 //触发hook回调,命中PackageInfo构造方法 19、at RuntimeClazz.constructor(Unknown Source:11) //PackageInfo真正的构造方法 18、at android.content.pm.PackageInfo.init(PackageInfo.java:29) 17、at java.lang.reflect.Method.invoke(Native Method) 16、at com.runtime.api.rposed.RposedBridge.invokeMethod(RposedBridge.java:314) 15、at com.runtime.api.rposed.RposedBridge.-$$Nest$sminvokeMethod(Unknown Source:0) 14、at com.runtime.api.rposed.RposedBridge$HookInfo.callback(RposedBridge.java:378) //hook拦截回调 13、at RuntimeClazz.constructor(Unknown Source:14) 12、at android.content.pm.PackageInfo$1.createFromParcel(PackageInfo.java:500) //这块相当于是对服务端的数据去解析把Parcel装载的数据转换成PackageInfo,再则里面进行PackageInfo的构造。 11、at android.content.pm.PackageInfo$1.createFromParcel(PackageInfo.java:497) //读取数据类型开始去解析服务端发送过来的数据 。 //android.os.Parcel 这个对象大家可以理解成“小盒子”里面存放了数据包不管是请求服务端发送协议的数据包 //还是服务端返回结果的数据包都是通过Parcel进行装载 。 10、android.os.Parcel.readTypedObject(Parcel.java:3982) //android.content.pm.IPackageManager$Stub$Proxy 是PackageManager内部的一个代理人也就是服务端的代理人 //代理设计模式的好处就是,代理人只有其本地的部分功能不同的功能需要的权限也不一样 方便处理。 //安卓端每个服务内部都存在一个服务端的代理人所谓的动态代理就是通过反射的方式强制把代理人给替换成自己的实现对象 。 //伪造服务端代理人实现非注入式hook ,比如一般的沙箱为了稳定性不会采用类似lsplant的方式去修改函数地址的hook 。 //所以检测服务端代理人的class名称是否包含proxy或者检测服务端代理人的classloader是否是系统的classloader。 //都是很常见的检测沙箱的方式这个方法是PackageManager本地逻辑处理完把请求交给服务端代理人IPackageManager //这个方法就是核心逻辑所在IPC协议的拼接底层调用的binder去通讯通讯完毕去解析 。都是在这个方法里面处理的 。 9、at android.content.pm.IPackageManager$Stub$Proxy.getPackageInfo(IPackageManager.java:4764) //未找到cache 8、at android.content.pm.PackageManager.getPackageInfoAsUserUncached(PackageManager.java:8224) 7、at android.content.pm.PackageManager.access$100(PackageManager.java:96) 6、at android.content.pm.PackageManager$2.recompute(PackageManager.java:8236) 5、at android.content.pm.PackageManager$2.recompute(PackageManager.java:8233) 4、at android.app.PropertyInvalidatedCache.query(PropertyInvalidatedCache.java:374) //根据uid去查询cache 3、at android.content.pm.PackageManager.getPackageInfoAsUserCached(PackageManager.java:8251) //getPackageInfo的一个包装根据uid再次查询 2、at android.app.ApplicationPackageManager.getPackageInfoAsUser(ApplicationPackageManager.java:202) //当调用获取getPackageInfo时候的调用栈 1、at android.app.ApplicationPackageManager.getPackageInfo(ApplicationPackageManager.java:179)
总结 客户端IPC协议流程图简述 发起人App调用获取PackageInfo- PackageManager收到请求判断是否存在cache如果不存在 -服务端代理人 - 发起Binder协议-服务端接收 处理完毕写入-客户端接收。 这块我们发现很多逻辑都是没用的都是客户端去为了各种方便封装了很多方法但是最终目的都是为了发送IPC协议和服务端通讯
但是其中中间每一层级都可能导致我们的程序被Hook导致我们拿到的数据是不安全的 比如攻击者是直接hook PackageManager 或者直接hook PackageManager代理人然后修改对应的数据内容。导致我们的数据不安全 被替换 。
所以得出结论 我们可以直接模拟IPC协议直接和服务端通讯因为客户端这些都是不安全的 直接通讯绕过这些复杂的处理逻辑但是弊端就是需要兼容不同的android版本 , 否则可能会导致解析失败。 实现代码如下 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 try { PackageManager packageManager getBaseContext().getPackageManager(); Object IPC_PM_Obj RposedHelpers.getObjectField(packageManager, mPM); //取binder IBinder mRemote (IBinder) RposedHelpers.getObjectField(IPC_PM_Obj, mRemote); Parcel _data Parcel.obtain(); Parcel _reply Parcel.obtain(); _data.writeInterfaceToken(android.content.pm.IPackageManager); _data.writeString(getPackageName()); _data.writeLong(PackageManager.GET_SIGNATURES); _data.writeInt(android.os.Process.myUid()); //自定义flag boolean _status mRemote.transact(TransactCase.TRANSACTION_getPackageInfo(), _data, _reply, 0); _reply.readException(); PackageInfo packageInfo _reply.readTypedObject(PackageInfo.CREATOR); _data.recycle(); _reply.recycle(); CLog.e(ipc sign info - : packageInfo.signatures[0].toCharsString()); } catch (Throwable e) { CLog.i(IPC_TEST_getPackageInfo error e); } public static int TRANSACTION_getPackageInfo() { if(TRANSACTION_getPackageInfo -1) { try { Field field null; try { Class? pkmIPCClazz Class.forName(android.content.pm.IPackageManager$Stub); field pkmIPCClazz.getDeclaredField(TRANSACTION_getPackageInfo); } catch (Throwable e) { CLog.e( getTranscationId forName error e.getMessage()); } assert field ! null; field.setAccessible(true); TRANSACTION_getPackageInfo field.getInt(null); } catch (Throwable e) { e.printStackTrace(); CLog.e( getTranscationId error e.getMessage()); } } return TRANSACTION_getPackageInfo; }
反思
这种方式一定是安全的么
不是的如果这块有个细节就是 我们本质上还是需要通过binder驱动进行通讯 和PackageInfo.CREATOR解析 。
如果攻击者 直接hook了binder驱动 或者hook解析方法还是可以绕过 。比如binder也是会有一些入口还是可能被Hook劫持 。
binder的入口android.os.BinderProxy-transact方法 。
或者直接hook签名解析的方法因为在getPackageInfo也是需要去解析签名信息的可以直接hook签名信息解析这块逻辑 。
android.content.pm.PackageParser-generatePackageInfo 系统解析签名方法PackageInfo.CREATOR PackageInfo Parcel 解析签名方法都是可以的 。
还可以继续增强么
可以根据安卓源码去自实现binder驱动和解析过程 然后连本地的binder也不信任直接和服务端通讯也是可以的 。
Java层还有比较好的签名检测手段么
目前推荐的只有这么一种大部分都是SO层做的检测Java层还可以做一些代理人的动态代理包括一些代理人的classloader或者
PackageInfo.CREATOR的classloader之类的去做对抗 。还有一个tee检测也很不错 用于检测各种证书也是容易被hook这块不多说了 重点放在So层 。
So层的基础svc拦截就可以bypass掉很多小白 。
Native层检测签名
1、基础SVC读取签名
这块什么是svc就不多说了说了很多遍了 。感兴趣可以看 [原创]聊聊大厂设备指纹其二Hunter环境检测思路详解!-Android安全-看雪-安全社区|安全招聘|kanxue.com
我之前再看雪发的文章 。
这种方式主要是直接在方法里面内敛svc的方式去openat打开已安装的apk 然后去解析证书文件 。
如果svc层不做处理 很难绕过检测 。解析apk签名过程如下 代码来自Magisk ,调用 checkSign 对返回的字符串进行判断即可 。 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 #define APK_SIGNING_BLOCK_MAGIC APK Sig Block 42 #define SIGNATURE_SCHEME_V2_MAGIC 0x7109871a #define EOCD_MAGIC 0x6054b50 // Top-level block container struct signing_block { uint64_t block_sz; struct id_value_pair { uint64_t len; struct /* v2_signature */ { uint32_t id; uint8_t value[0]; // size (len - 4) }; } id_value_pair_sequence[0]; uint64_t block_sz_; // *MUST* be same as block_sz char magic[16]; // APK Sig Block 42 }; struct len_prefixed { uint32_t len; }; // Generic length prefixed raw data struct len_prefixed_value : public len_prefixed { uint8_t value[0]; }; // V2 Signature Block struct v2_signature { uint32_t id; // 0x7109871a uint32_t signer_sequence_len; struct signer : public len_prefixed { struct signed_data : public len_prefixed { uint32_t digest_sequence_len; struct : public len_prefixed { uint32_t algorithm; len_prefixed_value digest; } digest_sequence[0]; uint32_t certificate_sequence_len; len_prefixed_value certificate_sequence[0]; uint32_t attribute_sequence_len; struct attribute : public len_prefixed { uint32_t id; uint8_t value[0]; // size (len - 4) } attribute_sequence[0]; } signed_data; uint32_t signature_sequence_len; struct : public len_prefixed { uint32_t id; len_prefixed_value signature; } signature_sequence[0]; len_prefixed_value public_key; } signer_sequence[0]; }; // End of central directory record struct EOCD { uint32_t magic; // 0x6054b50 uint8_t pad[8]; // 8 bytes of irrelevant data uint32_t central_dir_sz; // size of central directory uint32_t central_dir_off; // offset of central directory uint16_t comment_sz; // size of comment char comment[0]; } __attribute__((packed)); /* * A v2/v3 signed APK has the format as following * * --------------- * | zip content | * --------------- * | signing block | * --------------- * | central dir | * --------------- * | EOCD | * --------------- * * Scan from end of file to find EOCD, and figure our way back to the * offset of the signing block. Next, directly extract the certificate * from the v2 signature block. * * All structures above are mostly just for documentation purpose. * * This method extracts the first certificate of the first signer * within the APK v2 signature block. */ static string read_certificate(int fd) { uint32_t size4; uint64_t size8; // Find EOCD for (int i 0;; i) { // i is the absolute offset to end of file uint16_t comment_sz 0; lseek(fd, -((off_t) sizeof(comment_sz)) - i, SEEK_END); read(fd, comment_sz, sizeof(comment_sz)); if (comment_sz i) { // Double check if we actually found the structure lseek(fd, -((off_t) sizeof(EOCD)), SEEK_CUR); uint32_t magic 0; read(fd, magic, sizeof(magic)); if (magic EOCD_MAGIC) { break; } } if (i 0xffff) { // Comments cannot be longer than 0xffff (overflow), abort return {}; } } // We are now at EOCD sizeof(magic) // Seek and read central_dir_off to find start of central directory uint32_t central_dir_off 0; { constexpr off_t off offsetof(EOCD, central_dir_off) - sizeof(EOCD::magic); lseek(fd, off, SEEK_CUR); } read(fd, central_dir_off, sizeof(central_dir_off)); // Next, find the start of the APK signing block { constexpr int off sizeof(signing_block::block_sz_) sizeof(signing_block::magic); lseek(fd, (off_t) (central_dir_off - off), SEEK_SET); } read(fd, size8, sizeof(size8)); // size8 block_sz_ char magic[sizeof(signing_block::magic)] {0}; read(fd, magic, sizeof(magic)); if (memcmp(magic, APK_SIGNING_BLOCK_MAGIC, sizeof(magic)) ! 0) { // Invalid signing block magic, abort return {}; } uint64_t signing_blk_sz 0; lseek(fd, (off_t) (central_dir_off - size8 - sizeof(signing_blk_sz)), SEEK_SET); read(fd, signing_blk_sz, sizeof(signing_blk_sz)); if (signing_blk_sz ! size8) { // block_sz ! block_sz_, invalid signing block format, abort return {}; } // Finally, we are now at the beginning of the id-value pair sequence for (;;) { read(fd, size8, sizeof(size8)); // id-value pair length if (size8 signing_blk_sz) { // Outside of the id-value pair sequence; actually reading block_sz_ break; } uint32_t id; read(fd, id, sizeof(id)); if (id SIGNATURE_SCHEME_V2_MAGIC) { read(fd, size4, sizeof(size4)); // signer sequence length read(fd, size4, sizeof(size4)); // signer length read(fd, size4, sizeof(size4)); // signed data length read(fd, size4, sizeof(size4)); // digest sequence length lseek(fd, (off_t) (size4), SEEK_CUR); // skip all digests read(fd, size4, sizeof(size4)); // cert sequence length read(fd, size4, sizeof(size4)); // cert length string cert; cert.resize(size4); read(fd, (void *) cert.data(), size4); return cert; } else { // Skip this id-value pair lseek(fd, (off_t) (size8 - sizeof(id)), SEEK_CUR); } } return {}; } string checkSign(JNIEnv * env,const char* apkPath){ int fd1 static_castint (raw_syscall(__NR_openat, AT_FDCWD, reinterpret_castconst char *(apkPath), O_RDONLY | O_CLOEXEC, 0640 )); if(fd1-1){ return {}; } std::string md5Result Base64Utils::VTEncode(read_certificate(fd1)); close(fd1); return md5Result; } string checkSign(JNIEnv * env,int fd1){ if(fd1 -1){ return {}; } std::string md5Result Base64Utils::VTEncode(read_certificate(fd1)); return md5Result; }
2、文件权限
当我们打开fd的时候文件很有可能会被IO重定向重定向到一个新的文件但是这个文件不可能是系统的因为没权限。
所以当我们得到这个fd的时候可以再次通过fstat去获取已经打开的这个fd 是不是一个系统文件系统文件的gid和uid都是1000 。
确认我们打开的fd是系统文件 。因为/data/app/包名/base.apk是一个系统文件 。
对抗的话可以直接svc fstat 退出阶段对返回结果进行bypass即可 。
3、Readlinkat反查路径
当我们打开fd的时候文件很有可能会被IO重定向重定向到一个新的文件我们根据fd通过readlinkat 反查fd路径判断和传入的路径是否相等
如果相等则认为正确 。也是在SVC Readlinkat推出阶段对查询的buff进行二次覆盖 。
4、Readlinkat返回值截断
这个思路主要是根据 Readlinkat的返回返回值进行检测在创建数组的时候 数组大小采用Readlinkat返回值的大小 。
Readlinkat返回值 返回值返回的是一个查询路径结果的长度如果攻击者只改了Readlinkat的参数改了路径但是返回值忘记修改这样他的返回值就会被阶段也就是大小不匹配也可以检测出来 路径和传入的原始路径是否相等 。
5、Inode:
inode是linux的文件或者目录的唯一标识符每个文件都是独一无二的 如果文件被关闭再次打开一个新的文件可能会被占用不过一些系统apk 不会被删除和关闭 。
在maps里面 每个item类似如下 7041e02000-7041e4e000 r--p 00000000 fc:01 25131 /apex/com.android.conscrypt/lib64/libc.so 这里面的倒数25131 第二项就是inode我们当得到一个fd的时候可以获取文件的fd 信息 然后再去maps里面获取已经打开的文件fd信息 。
判断打开文件的inode和内存里面的inode是否相等 。
对抗的话可以直接svc fstat 退出阶段对返回结果进行bypass即可 。
6、对已经打开的fd进行签名获取:
这个方法过于激进很少部分人知道 很多手机在刚启动的时候会自动打开 /data/app/包名/base.apk
比如 我们可以直接遍历已经打开的fd 然后对这个readlinkat反查路径 , 如果是base.apk的fd直接对fd进行签名解析 。相当于对已经打开的文件进行签名解析 。
因为这个apk是系统启动阶段就被打开了 所以不会走IO重定向的逻辑 相对安全 。
经过测试有的手机App在开机启动阶段不会打开 只适合Apk再启动阶段打开了base.apk 直接对已经打开的fd解析即可 。可以逃逸IO重定向 。
对抗的话 也很简单3种方案 如果有人通过已经打开的文件去遍历fd , 在readlinkat 结果处理 如果是base.apk直接隐藏掉 。 直接处理getdents64和getdents 在遍历阶段直接找不到对应的fd即可 直接close但是有的版本在close会报 fdsan异常需要通过 android_fdsan_set_error_level 设置禁用
总结
上述这些方案基本覆盖大多数手段 Hunter的核心检测思路也是上面这几种 。签名Native部分代码如下 。 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 jobject getApkSign(JNIEnv *env, [[maybe_unused]] jclass engine, jobject context) { const char *path getApkPath(env, context); if(path nullptr){ return getItemData(env, APK签名验证失败, getApkPath null, true, RISK_LEAVE_DEADLY, TAG_REPACKAGE); } int fd my_openat(AT_FDCWD, reinterpret_castconst char *(path), O_RDONLY | O_CLOEXEC, 0640); if(fd 0){ return getItemData(env, APK签名验证失败, string (getApkSign open error \n)path, true, RISK_LEAVE_DEADLY, TAG_REPACKAGE); } #define TEMP_APK_SIGN_STR xxxxxx //GET svc apk sign auto svc_apk_sign Base64Utils::VTEncode(read_certificate(fd)).substr(0, 10); LOG(INFO) getApkSign apk sign svc_apk_sign; //1、check svc get sign match if (svc_apk_sign Base64Utils::VTDecode(TEMP_APK_SIGN_STR)) { //2、check apk sign for open fd auto base_fd_list getBaseApkFd(path); if(!base_fd_list.empty()){ //disabled fdsan change_fdsan_error_level(); for(int temp_fd:base_fd_list){ auto apk_sign_fd checkSign(env, temp_fd).substr(0, 10); LOG(INFO) get open fd apk sign apk_sign_fd; if(svc_apk_sign ! Base64Utils::VTDecode(TEMP_APK_SIGN_STR)){ if(fd0){ close(fd); } return getItemData(env, APK签名验证失败, open fd cache error, false, RISK_LEAVE_DEADLY, TAG_REPACKAGE); } } } //3、check apk path char buff[PATH_MAX] {0}; std::string fdPath(/proc/); fdPath.append(to_string(getpid())).append(/fd/).append(to_string(fd)); long len raw_syscall(__NR_readlinkat, AT_FDCWD, fdPath.c_str(), buff, PATH_MAX); if (len 0) { if(fd0){ close(fd); } return getItemData(env, APK签名验证失败, readlinkat error, true, RISK_LEAVE_DEADLY, TAG_REPACKAGE); } //4、截断,如果攻击者hook了readlinkat,只修改了参数,没修改返回值也可以检测出来。 buff[len] \0; LOG(INFO) check apk sign path buff; if (my_strcmp(path, buff) 0) { LOG(INFO) check apk sign path success ; //start check memorylocation inode struct stat statBuff {0}; long stat raw_syscall(__NR_fstat, fd, statBuff); if (stat 0) { if(fd0){ close(fd); } LOG(ERROR) check apk sign path fail __NR_fstat0; return getItemData(env, APK签名验证失败, fstat error, true, RISK_LEAVE_DEADLY, TAG_REPACKAGE); } //5、check uidgid (1000 system group) if (statBuff.st_uid ! 1000 statBuff.st_gid ! 1000) { if(fd0){ close(fd); } LOG(ERROR) check apk sign giduid fail ; return getItemData(env, APK签名验证失败, nullptr, true, RISK_LEAVE_DEADLY, TAG_REPACKAGE); } //6、check Inode size_t inode getFileInMapsInode(path); if (statBuff.st_ino ! inode) { if(fd0){ close(fd); } LOG(ERROR) check apk sign inode fail statBuff.st_ino maps - inode; return getItemData(env, APK签名验证失败, nullptr, true, RISK_LEAVE_DEADLY, TAG_REPACKAGE); } LOG(ERROR) check apk sign success! uid- statBuff.st_uid gid- statBuff.st_gid; } else { if(fd0){ close(fd); } LOG(ERROR) check apk sign path fail ; return getItemData(env, APK签名验证失败, nullptr, true, RISK_LEAVE_DEADLY, TAG_REPACKAGE); } LOG(INFO) check apk sign success; if(fd0){ close(fd); } return nullptr; } else { if(fd0){ close(fd); } LOG(ERROR) check apk sign fail svc_apk_sign; //check sign fail return getItemData(env, APK签名验证失败, nullptr, true, RISK_LEAVE_DEADLY, TAG_REPACKAGE); } }
有没有一种方式可以轻轻松松绕过上面全部的检测呢
经常有人问我签名检测太难不知道应该怎么绕过 有没有一种可以快速绕过全部检测的方案呢并且我可以随便修改apk信息不被发现 。
其实很简单在root手机上直接使用核心破解或者小米的三方lsp插件 Cemiuiler , 页面如下 。
直接禁用系统的签名验证直接安装也就是在破坏了原始签名的情况下 未签名直接安装 。 **因为你没有修改apk原始的签名信息他读取到的还是原始的签名信息签名文件不会有任何变化 。**你二次签名本质的目的都是为了通过系统的签名检测 。
相关文章: