我分析的这款应用是韩国某大型娱乐公司旗下的爱豆聊天App。先选择想聊天的爱豆,然后付费就能和TA聊天了,可以和TA主动发消息。也能看到TA的日常动态。
这个应用的包名是:Y29tLmV2ZXJ5c2luZy5seXNu,版本:MS42LjEw。调试时为最新版。
本文有的地方参考了GPT,有错误的还请大家指正。
有了这个软件,你可以和下图的或其他爱豆聊天了。

使用的是Root过的Google Pixel 3 手机,手机存在的风险有以下几点:
可以看到这个应用至少检测到了我的两个风险,frida和root。
frida脚本如下:
输出结果如下:
这说明检测逻辑位于libdxxcuxd.so 。接下来就应该对这个so进行分析了。
先看看它有哪些初始化函数,现在的壳或者风险检测组件都喜欢把检测的逻辑放在这里,如下图所示。

只有DT_INIT_ARRAY 类型的函数,没有DT_INIT 类型的。如果有DT_INIT 类型的,那么我们优先看DT_INIT 类型的,因为它的执行时间早于DT_INIT_ARRAY 类型的函数。且两者都优先于JNI_OnLoad执行。如下图所示。

有两个初始化函数,为了方便阅读,我已经重命名了。
先说说调试的结论,要想用frida绕过这个风险检测,直接nop一个函数是不行的,因为会各种各样奇怪的崩溃问题。所以个人觉得更好的做法是修改跳转,让程序检测不出来即可。
这个函数如下图所示。

GPT回答是:
Android App 进程里:
/app_process64 或者:com.xxx.xxx 取决于:
是否被 zygote 派生
有没有改 argv[0]
有没有调用 setproctitle
是否做了反调试/伪装
很多壳、加固、反作弊都会检查它。
我的手机的值如下:
看到字符串.sandbox 和com.lbe.parallel ,说明它在检测沙箱、双开环境。
很多虚拟环境,如VirtualApp,双开助手,平行空间,VirtualXposed。会注入:SOMETHING=.sandbox 。或者包含路径/data/user/0/com.xxx.sandbox/ 。
com.lbe.parallel 是平行空间(Parallel Space),著名的双开框架。
检测environ 是因为沙箱特别喜欢修改环境变量,例如:
或者添加:
因此environ 是反虚拟化的重要检测点。
sub_294E0中调用了my_crc32 来检查内存,偏移是sub_10B44 。


下图是sub_294E0 的伪代码,含注释。

可以看到从A4078处开始,检查10CA8h个字节的内存crc32值。sub_294E0 是被init_arr1 调用的,首次调用时记下crc32到全局变量dword_A4088 ,之后被调用时对比这段内存是否和最初的保持一致,如果不一致,就说明内存被修改了并调用Kill_15DF0 以创建线程的形式退出程序。
LOG_7E2A8 是日志函数,它会打印出风险字符串,也是弹窗显示的文字。
本小节的 sub_294E0 在别处被引用多次,说明有多个地方做了内存完整性检查,因此我们要注意。
sub_164A8 检查了_progname 是否含blackdex 字符串,blackdex是一款脱壳的工具,自然也被组件视为风险因素了。

这个函数位于sub_3DDB4,且我使用的frida版本是16.5.9的。
有计算_progname的crc32,和安装异常处理的sigHandler。

然后就是经典的检测frida环节。调用了scandir 搜索/proc/%d/task ,%d是进程PID。看看它有哪些线程?
再读取/proc/%s/comm ,看看有没有和frida相关的字符串,有就说明App被frida“入侵”了。

这段代码里有好几个flag,可以说是用于做风险分数评估的。根据这些分数风控组件来决定要不要继续执行。
为什么要看/proc/%d/task 和/proc/%s/comm 呢?
先用frida注入一个自己写的App试试看。列举出有哪些线程。

然后逐个线程的看。

所以说这是libdxxcuxd.so 在这里检查frida的原因。
不要忘了,本节的sub_3DDB4 还检查了其它的风险,调用了函数RiskDetect_1D25C ,如下图所示。

检测的字符串有以下:(/proc/%d/maps中)
返回0表示没有风险,-1表示有风险。
这个函数代码少,暂时不是绕过反调试的关键。

sub_1017C 如下图所示。

dlopen_46914是自定义的dlopen,v19是输出变量,存放so文件各区段的关键信息。
dlsym_10138 和dlsym_4B3E8 都是寻找符号的函数,就是找到对应函数名的地址。其中dlsym_10138 内部调用了dlsym_4B3E8 。



寻找/linker64里的关键函数,然后放到全局变量里。
先寻找切入点。因为风控组件的代码启动的比较早,所以我习惯在linker即将.init_proc 时注入frida的脚本。把linker64 拷贝到电脑上。
用IDA打开google_pixel3_linker64 ,即拷贝出来的linker64 。
搜索关键字call_constructors ,得到这个函数和偏移是2C274 。

frida脚本如下:
然后就能打印出以下内容:
这说明我们的代码是注入的比较早的。我们得先绕过frida检测,才能绕过其它的风险检测。
该函数被sub_3DDB4 调用。这个函数只是检测了许许多多的风险字符串,内部调用的只是字符串拷贝或者寻找子串的函数,没有额外的影响线程运行的函数。因此把它的返回值改为0即可。
这个函数头部的指令可以改为如下:
对应的frida脚本如下:
调用:

我们不能粗暴地把scandir 改成非法的,比如-1 ,这样按照执行流程的话会让App退出的。

此外,还有一个全局变量flag2_A5D9C 的值要是0x20 。即绿色部分的指令应该被执行。再来看看init_arr1 处的代码,如下图所示。

如果flag2_A5D9C & 0x20 为0,则App会退出。所以此时的想法是让程序在执行scandir 时直接跳转至loc_3E928 处,即不让风控组件扫描目录。


这样就能让flag2_A5D9C & 0x20 不为0了,也就不会调用kill_15FD0 函数了。
经过调试flag2_A5D9C 可能会被其它的线程或函数修改成flag2_A5D9C & 0x20 为0的情况,为了保险起见,我们还要看哪里调用了sub_3DDB4 。
有两处调用了sub_3DDB4 ,如下图所示。

把条件跳转TBNZ W1, #5, loc_100E8 ,改成B loc_100E8 。

把TBNZ W1, #5, loc_8419C 改成b loc_8419C 。
frida实现强制跳转的代码如下:
调用:
写好的frida脚本此时如下:
但是有报错:
重点是:signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x7a4299fcbf 和pc 0000007a0c45a918 这有助于我们锁定报错的位置。
7a0c45a918 - 7a0c44a000 = 10918h,说明报错的位置在so的10918h处。SEGV_ACCERR 的意思是访问的内存地址是存在的,但是因为权限问题报错。例如某段内存是只读,但这条指令尝试写入,就会出现这种错误。
10918h如下图所示。

这是一个字符串拷贝的函数,STRB在存储字符到内存时有权限问题。被调用的地方看lr 0000007a0c4ceba4 ,偏移值是84BA4

个人不理解为啥这里会报错?v66,也就是x24的值,如果是风险分数,分数越高,是不是就访问到了权限为只读的内存了呢?这里有点奇怪……加上这个函数代码量太大了,实在分析不下去了哈哈。有懂的朋友可以帮忙看下,谢谢啦。

无论如何,先试试再说吧。把出错的内存权限改成rw-。
调用:
部分输出如下:
如此看来确实是因为权限不够导致的错误,至少现在没有再报访问错误了。
root检测一般是检查以下路径的文件是否存在:
幸运的是,这个App的风控组件没有用到什么SVC #0 去打开文件,不然真不好用frida去hook了。
写出的hookFopen脚本如下:
调用:
这里不建议用console.log('Call stack:\n' + Thread.backtrace(null, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n')); 去打印日志,因为打印出来的调用堆栈是错的,在这里准确的方法是打印出x30 即lr 寄存器的值。并过滤出参数含/su 的。
对了,这里再提一下,组件提前把函数地址写到了全局变量,直接hook 11F2C 效果会好些,能少跟踪一个层级。

函数Fill_Global_FuncTable_18FA4 获取libc.so 的很多函数地址写入全局变量里。

输出如下:
还有一段错误,提示找不到so文件了,暂且不管它,我们绕过root检测再说。
因为我们的错误是/sbin/su 存在,所以看到这里。
0x78167a8138 - libdxxcuxd.so! 0x7816795000 = 0x13138

经过多次调试,得到以下调用的逻辑链条。

下两张图分别是sub_1C058 和sub_3FEA0 的截图。


那么保险的做法是:
此时完整的代码如下:
App没有提示frida或root。但是它重启了,并再次提示root。这属于正常现象,因为重启之后我们注入的frida代码是不存在的。
也许程序调用了kill 相关的函数促使进程退出的。
[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。