1 背景
在Android开发过程中,防作弊一直是老生常谈的问题,而模拟器和反调试的检测往往是防作弊中的重要一环。
2 模拟器检测
模拟器检测的本质就是要利用模拟器和真机之间的微小差异,从而判断当前设备是否为模拟器,具体检测技术框架整理如图1所示。
通常是在入口点、入口页面分析有没有模拟器检测,一种检测代码的Java实现如下所示
1 | package com.qianyu.antiemulator; |
3 self_debugging反调试检测
反调试在代码保护中扮演着很重要的角色,虽然不能完全阻止攻击者,但是还是能加大攻击者的时间成本,一般与加壳结合使用,核心还是加壳部分。
反调试可以分为两类:一类是检测,另一类是攻击,前者是去想各种办法去检测程序是否在被调试,如果正在被调试的话做出一些“反”的举措,比如退出等等,(当然这里退出不是一个万全之策,因为你暴露了反调试的位置点,这样攻击者就比较容易过反调试,更好的是想办法不让攻击者发现,并且跳到另一个位置,让攻击者懵逼)。
在这里看到的反调试都是以检测手段为主,也是比较常用的思路,当然也是一种低级反调试,因为容易爆露反调试点,只有在做反调试系统的时候这些手段交杂多用,才能给逆向者增加一定的成本开销。
3.1 关键文件检测
通过so文件是可以找到JNl_onload,它是编译可执行文件,进在Exports模块下搜索start,找main函数的入口函数。
3.1.1 基本思路
我们知道在调试进程的时候,进程会被IDA中的android_server ptrace,并且这个进程名字存在于/proc/pid/cmdline中,当然这里的pid指的是android_server的进程号,这个可以通过TracePid来获得。
因此说在这里可以检测这个名字,如果有这个名字说明正在被调试,那我们就可以kill掉这个程序。
一般情况下,android_server都会放在/data/local/tmp/文件夹下,因此我们可以检测这个文件夹是不是有android_server文件,如果有,程序就退出。
在这里我们可以发现这个思路好像很白痴,如果我们把这个名字改掉,然后放到别的目录下,就可以对付这个反调试策略了。
3.1.2 具体检测操作
有了基本思路,接下来我们就开始实现,实现的示例代码如下:
1 | //filecheck.cpp |
通过上述实现,关键文件检测达到的效果如图2所示。
3.2 端口检测
3.2.1 基本思路
我们知道android_server的默认监听的端口号是23946,所以可以通过检测这个端口号来起到一定的反调试作用,在Linux系统中在/proc/net/tcp会记录这些连接信息。
在这里我们可以换个端口就可以对付这个反调试策略了。
3.2.2 具体检测操作
有了基本思路,接下来我们就开始实现,实现的示例代码如下:
1 | //checkTCP.cpp |
通过上述实现,调试的时候检测到23946有tcp连接会退出,达到的效果如图3所示。
3.3 进程名称检测
3.3.1 基本思路
在调试状态下,Linux会向/proc/pid/status写入一些进程状态信息,比如最大的变化是TracerPid字段会写入调试进程的pid,图4是在调试前后/proc/pid/status的文件的变化。
解决方案:
一是以debug模式启动,在JNI_Onload处下断点,找到那个调用方法NOP掉;
二是直接静态分析JNI_Onload,直接去掉方法的调用。
3.3.2 具体检测操作
有了基本思路,接下来我们就开始实现,实现的示例代码如下:
1 | //filecheck.cpp |
【注意】调试可执行程序是Debugger —> run;调试so是Debugger —> attach
3.4 轮循检测
轮询检测反调试技术基于循环检测进程的状态,目的是判断当前进程是否正在被调试,优点是实现比较简单,缺点是系统资源消耗大。
3.4.1 基本思路
读取进程的/proc/pid/status文件,通过该文件得到调试当前进程的调试器(检测调试器的pid)
3.4.2 具体检测操作
通过status文件内的TracerPid字段的值判断当前进程或线程是否正在被调试
【status文件信息】
Name:进程名称
State:进程的状态
Tgid:一般指进程的名称
Pid:一般指进程Id,他的值与getting函数的返回值相等
PPid:父进程的Id
TraceerPid:实现调试功能的进程Id,值为0表示当前进程未被调试
1 | // loop.cpp |
通过上述实现,调试的时候检测到有程序调试时会打印出TracerPid的值,达到的效果如图5所示。
反-反调试方案:
- 静态修改检测函数或者Hook
- 编译安卓源码使让TracerPid永久为0
3.5 self-debugging反调试检测
原理:父进程创建一个子进程,通过子进程调试父进程,非常实用、高效的实时反调式技术。
- 优点:可以作为受保护进程的主流反调试方案;消耗的系统资源比较少;几乎不影响受保护进程性能;可以轻易地阻止其他进程调式受保护的进程
- 缺点:实现比较复杂
3.5.1 基本思路
实现:核心ptrace函数和进程的信号机制
【注意】进程暂停状态比较多
1 | // main.cpp |
3.5.2 具体检测操作
对self_debugging来说,pid为3263,子进程pid为3264,debugger通过调试self_debugging的子进程,进而调试self_bugging,一个进程只能被一个进程附加。
反-反调试手段:
- 让父进程不fork
- 把while函数循环去掉
- 不能调试父进程,但可以调试子进程,配合双IDA调试,挂起子进程
通过status文件内的TracerPid字段的值判断当前进程或线程是否正在被调试
3.6 Java层反调试检测
此示例用来演示Java层手动绕过百度加固Debug.isDebuggerConnected反调试的方法
JDWP协议动态调试,安卓程序动态调试条件(两个满足之一即可):
1.在AndroidMainfest.xml中,application标签下Android:debuggable=true
2.系统默认调试,在build.prop(boot.img),ro.debugable=1Android SDK中有android.os.debug类提供了一个isDebuggerConnected方法,用于判断JDWP调试器是否正在工作
3.6.1 基本思路
使用AndroidKiller反编译APK,找到isDebuggerConnected方法调用,修改程序运行流程即可绕过反调试检测。
3.6.2 具体检测操作
首先使用Jadx反编译小灰机APK,然后查看AndroidMainfest.xml文件,找到application标签下的android:name=”com.baidu.protect.StubApplication”即入口界面,如图8所示。
然后去看StubApplication下的onCreate()方法,我们关注isDebuggerConnected()方法,因为它是用于判断JDWP调式器是否正在工作的标志。
Debug.isDebuggerConnected获取到一个值进行比较,如果为真就进行加载so库,如图9所示。
所以只有符合条件成立才会执行if里面的逻辑,进行加载so库,这就是在Java层进行反调试,也能用来保护代码。
因此,我们使用AndroidKiller反编译APK,在进而在StubApplication中找到isDebuggerConnected的方法调用,并将if-nez v0, :cond_0修改为if-eqz v0, :cond_0,如图10所示。
3.7 so层反调试检测
此示例用来演示so层手动绕过反调试检测的方法
3.7.1 基本思路
使用IDA进行动静结合分析so文件,然后在动态调试中注释掉so层的检测代码,即可绕过反调试检测。
3.7.2 具体检测操作
首先使用Jadx反编译AntiDebug.apk,然后查看AndroidMainfest.xml文件,发现只有一个MainActivity,进行找到MainActivity只有加载so库antidebug的代码比较可疑,如图11所示。
静态分析
使用IDA Pro打开APK解压出来的so库libantidebug.so,然后在Exports中发现没有静态注册而只有动态注册的方法,进入JNI_OnLoad后按Tab查看C伪代码如图12所示。
然后进行代码分析,赋值后的if判断用了或运算符,后面四个参数有一个满足条件即满足if判断,需要使这四个函数都为假才能绕过。
动态分析
依次输入如下命令进行分析:
./android_server -p 11111
adb forward tcp:11111 tcp:11111
adb shell am start -D -n com.qianyu.antidebug/.MainActivity
打开monitor.bat窗口
打开IDA,填写主机号,端口号(与转发端口一致),勾选三项,选择进程com.qianyu.antidebug,双击进来,F9运行
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8600
因为此APK加载了多个so库,所以需要多点击几次F9才能加载到指定的so文件libantidebug.so,如图13所示。
接着在Modules中双击进入libantidebug.so后,再双击进入JNI_OnLoad,按Tab转为伪C代码,对应关系如图14所示。
因此,我这里需要修改图14中关于反调试的黄色框中的代码,按F2输入00 00 00 00(MOVS R0, R0)后再按F2提交,即可将Z9anti_timev、Z15anti_breakpointv和Z12anti_pthreadv分别转化为MOVS R0, R0,如图15所示。
最终,我们可以在JNI_OnLoad的汇编代码处下断点,按F8后能绕过反调试检测的代码,如图16所示。
实现so层反调试的示例代码如下:
1 | // antidebug.cpp |
4 参考文献
[1]丰生强. Android软件安全权威指南[M]. 电子工业出版社, 2019.
[2]https://www.youtube.com/watch?v=zW74yNjpGZg
[3]https://blog.csdn.net/feibabeibei_beibei/article/details/60956307
[4]https://www.cnblogs.com/momin/p/11422567.html
[5]https://blog.csdn.net/freeking101/article/details/106755116
[6]https://www.freebuf.com/articles/mobile/291894.html
[7]https://blog.csdn.net/YJJYXM/article/details/108516203
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,可以邮件至 xingshuaikun@163.com。