反调试在代码保护中扮演着很重要的角色,虽然不能完全阻止攻击者,但是还是能加大攻击者的时间成本,一般与加壳结合使用。反调试可以分为两类:一类是检测,另一类是攻击,前者是去想各种办法去检测程序是否在被调试,如果正在被调试的话做出一些“反”的举措,比如退出等等,后者是采用攻击的方法,就是想办法让调试器不能正常工作或者是让调试器崩溃,从而阻止它,本章主要讲解检测方面的知识。
前言
反调试在代码保护中扮演着很重要的角色,虽然不能完全阻止攻击者,但是还是能加大攻击者的时间成本,一般与加壳结合使用。
反调试可以分为两类:一类是检测,另一类是攻击,前者是去想各种办法去检测程序是否在被调试,如果正在被调试的话做出一些“反”的举措,比如退出等等,后者是采用攻击的方法,就是想办法让调试器不能正常工作或者是让调试器崩溃,从而阻止它,本章主要讲解检测方面的知识。
一、实战详解分析-关键文件检测
1、IDA案例思路分析
反调试通俗意思就是,程序挂起后突然出现八个F:FFFFFFFF或者在java层运行程序一直运行不起来等情况!
第一个反调试会检测android_server,文件名检测!来分析下android_server源文件
接下来分析下filecheck,用IDA打开
拖入后,IDA反编译
这是编译可执行文件,那么和so文件有什么区别呢?
SO文件是可以找到JNI_onload,那么编译可执行文件在IDA如何找逻辑所在处呢?
可看到main函数的入口函数,在Exports搜索start:
进来后看到main函数,还有了BL libc_init指令,这时候看看有几个参数?
如果这个函数的参数超过了四个以上(>4),跳转的地址就得用其他的寄存器来替代,libc_init当它这里没有被代替的时候,往下找最大的寄存器只出现了R3为止,没有出现R4以上的寄存器,那么就可以猜测传入的个数的参数为:0-3就是四个参数!TAB查看伪C代码:
双击main方法
这样就是编译可执行程序所要找的main函数
就算在重新打开IDA反编译,还是直接进入main函数处,那么得知道如何进入main函数的地方
开始分析,TAB键打开伪c代码
隐藏类型后:
check()检查,执行下面的循环if,判断if需要执行检查check,双击check进入
v0 = opendir("/data/local/tmp"); opendir打开/data/local/tmp目录给V0,这是文件指针
result = getpid(); getpid给result。getpid是当前进程的ID。
v2 = result; result给V2
就是说当V0不为空的时候,要执行while里面的循环逻辑,
V3=readdir(v0); readdir()返回参数dir 目录流的下个目录进入点。返回值:成功则返回下个目录进入点. 有错误发生或读取到目录文件尾则返回NULL.
v4 = v3 == 0; 拿V3和0对比
v5 = (v3 + 19); 计算v3+19
if ( v4 ): 判断V3=readdir(v0);是否为null,为空结束循环
如果/data/local/tmp目录下有android_server就会直接kill结束进程,这就是一个文件反调试逻辑思路。
2、案例思路源码分析
过掉文件检查不难,直接改名字即可!开始分析下面的案例
C语言从这个main函数开始执行,首先对数组的开始一个声明,主要关心check函数,
check就是检测android_server文件的!和之前理解的是一样的,定义了一个字符串指针指向目录/data/local/tmp,然后定义一个文件dir操作,用opendir打开tmp目录,打开后获取pid,接下来判断有没有打开,如果打开不为空(!=)就打开成功下面的while条件代码,currentDir有定义了一个指针,当读取的文件指针不为空(null),就继续往下读取。
遍历一个文件时,如果你的指针指向的下一个位不为空,意味着你的指针的后面还有文件。
3、Android底层详解
上传文件名为android_server和filecheck可执行文件**(android_server文件在IDA的dbgsrv目录下)**
adb push C:\test\filecheck data/local/tmp
adb push C:\test\android_server data/local/tmp
成功上传到底层,可执行文件
为filecheck赋最高权限
chmod 777 filecheck
执行filecheck文件
我们看到执行失败,报处kliied
为android_server改名测试
可以看到改名后,filecheck正常执行
二、实战详解分析-调试端口检测
1、源码分析调试端口检测
打开checkTCP.c文件开始分析
直接分析check方法
首先C文件中执行定义字符数组char buf[0x1000]={0};,然后执行命令cat /proc/net/tcp |grep :5D8A",这里grep :5D8A就是只查找存在5D8A字符的那一行,在/proc/net/tcp文件中5D8A代表端口的意思,5D8A是十六进制,转为十进制就是23946
如果存在23946这个端口就进入while循环,执行kill
2、IDA分析调试端口检测
找到可执行文件
使用IDA打开,找到main函数,开始分析
如何找到main函数
1、左边列表中
2、在Exports搜索start,双击进入找到main双击进入
找到main函数后,查看伪C代码
双击sub_728
这里的伪C代码和前面分析的c语言源码是一样的
3、Android底层详解
上传可执行文件,上传android_server文件
adb push C:\test\checkTCP data/local/tmp
adb push C:\test\android_server data/local/tmp
为两个文件赋执行权限
测试没有启动23946端口状态
执行checkTCP文件
没有输出,完成程序执行
测试启动23946端口状态
执行android_server,启动23946端口
打开一个新窗口,执行checkTCP文件
我们看到输入了cat /proc/net/tcp |grep :5D8A结果,并killed了checkTCP程序
三、实战详解分析-进程名称检测
1、源码分析进程名称检测
找到c语言源码
开始分析源码
直接从main函数开始分析,看到coursecheck方法
sprintf(filename, "/proc/%d/status", pid); 意思就是获取(getpid)pid值放到%d里面,然后可读的形式filename打开(r)操作,就是获取此时进程程序的一个状态。
FILE *fd=fopen(filename,"r");
if(fd!=NULL)
开打一个文件fd,如果打开成功就进行while循环
fgets是大小,(line,bufsize,fd)指向指针的地方,
if(strstr(line,"TracerPid")!=NULL); strstr会判断有没有TracerPid字符串
int statue =atoi(&line[10]); atoi会截取字符串前十个字符,意思就是检测TracerPid:,这个冒号后一位置不等于0的话,就表示TracerPid发生改变,就意味着在调试。
下面还对android_server进行检测,如果有着杀死进程。
2、IDA分析进程名称检测
在实战中进程遇到进程名称检测,开始分析关于进程检测的可执行文件
在main函数中找到coursecheck方法
双击sub_794进入
getpid获取程序的pid,然后fopen指向命令,如果指向成功就进行检测TracerPid值,这里只是v6,这里v6在IDA识别为未定义的值。
3、Android底层详解
上传进程检测可执行文件及android_server文件
adb push C:\test\BubbleSort data/local/tmp
adb push C:\test\android_server data/local/tmp
赋权BubbleSort文件
chmod 777 BubbleSort
执行程序
打开一个新窗口,查看BubbleSort文件运行状态
ps | grep BubbleSort
11032是进程名称
查看该进程下的status文件
cat /proc/11032/status
执行命令后就会遍历程序里面的内容,如果有TracerPid则不等于null,进入if,atoi检测前十位就是TracerPid:,判断TracerPid:后面的内容,如果为0就不进入下一个if,如果不为0就进入if继续执行。
那么现在让他发生变化,需要使用IDA!
先运行android_server启动,先给android_server,并不用默认端口启动
mv android_server test
./test -p12345
在电脑端进行端口转发
adb forward tcp:12345 tcp:12345
完成后,打开IDA,连接本地的12345端口
配置IDA连接
配置IDA选项
继续ps查询BubbleSort程序
ps | grep BubbleSort
根据进程名称11536查看进程文件夹中的status内容
可以看到这时候TracerPid内容不为0了,为11389,发生了变化
源码提示如果检测到TracerPid的值不等于0对程序进程打印然后杀死进程。
如何过滤掉?首先他要获取TracerPid,返回值为R0,那么修改R0或者注释即可,或者刷机修改系统内容,或者把TracerPid修改回去都可以
进程名称检测:
当动态调试的时候找到TracerPid赋值的地方,手动把它赋值修改为0即可。
四、实战详解分析-轮循检查技术
简述:轮循检查主要通过safe_attach函数和handle_events函数来实现的,轮询检测反调试技术基于循环检测进程的状态,判断当前进程是否在被调试。
**优点:**实现比较简单
**缺点:**系统资源消耗大
**原理:**读取进程的/proc/[pid]/status文件,通过该文件得到调试,当前进程的调试器(检测调试器的[pid])
**实现:**通过status文件内的TracerPid字段的值判断当前进程或线程是否正在被调试。
status文件中各字段简述:
Name:进程名称
State:进程状态
Tgid:一般进程的名称
Pid:一般进程的ID,它的值和getpid函数的返回值相等
PPid:父进程的ID
TracerPid:实现调试功能的进程ID,值为0表示当前进程未被调试。
绕过反调试检测的方案:
1、动态调试时修改TracerPid字段值为0,
2、修改内核,让TracerPid字段值为负值
1、源码分析-轮循检测技术
打开main.cpp
从main函数入口分析,main函数中调用了anti_debugger()方法,anti_debugger有调用了anti_debugger_thread方法,anti_debugger_thread中有调用了check_debugger方法
下面详细分析check_debugger中的内容
fopen中f是file,就是打开文件操作的意思,fopen打开path,path是上面传入path路径的256并以rt模式打开,如果打开不为空(!=null),就执行while循环,fgets就是打开指向的fp文件,if通过strncmp函数进行判断,使用line参数和TRACERPID参数进行对比,最多比较前 TRACERPID_LEN个字节,
如line参数和TRACERPID参数内容一致,进入if判断,if中tracerPid为0直接打印,还有if (!tracerPid)不为0就返回flase!!
这就是简单的一个检查遍历然后根据不同的结果返回不同的值。
2、IDA分析轮循检测技术
上传轮循检测的可执行文件poll_anti_debug,并赋执行权限
adb push C:\test\poll_anti_debug data/local/tmp
chmod 777 poll_anti_debug
目前上传了检测调试程序,还需要上传调试程序
上传debugger调试程序,并赋权
adb push C:\test\debugger data/local/tmp
chmod 777 debugger
运行检测调试程序poll_anti_debug
一直在循环打印TracerPid: 0
我们开启debugger程序,来模拟我们现在正在调试,将pid值设置为poll_anti_debug程序的pid
我们看到poll_anti_debug检测到TracerPid的值发生了变化。
这就是轮循检测。
五、实战详解分析-self-debugging反调试
1、原理简述
原理:
Self的英文意思是自己,顾名思义,self-debugging就是通过调试自身检测出是否被调试。
父进程创建一个子进程,通过子进程调试父进程。
特点:
非常实用、高效率的实时反调试技术
优点(可以作为受保护进程的主流反调试方案):
消耗的系统资源较少
几乎不影响受保护的进程性能
可以轻易地阻止其他进程调试受保护的进程
缺点:
实现比较复杂
实现:
核心ptrace函数
进程的信号机制
注意:
进程暂停状态比较多
暂停状态
**signal-delivery-stop状态:**调试器和被调试进程之间的关系
**group-stop状态(难):**sigcont信号
同时满足两个条件:进程/线程处于被调试状态;被调试进程/线程收到了暂停信号-->重置为0
sigstop
sigtstp
sigttin
sigttou
syscall-stop状态
ptrace-event-stop状态
反-反调试方法:
让父进程不fork
把while函数循环去掉
不能调试父进程,但是可以调试子进程,配合双IDA调试,挂起子进程
下图可以明确表示出self-debugging反调试特点:
fork是一个函数,fork函数fork出一个子进程来调试自己,那么别的函数就无法进程调试了,那么通过调试fork出来的子进程从而调试父进程。
同一时刻,一个进程只能被一个进程附加(调试)
用的比较少,还没进程名称检查用的多
2、self-debugging反调试案例详解
将测试文件debugger和self-debugging上次到测试机
adb push C:\test\debugger data/local/tmp
adb push C:\test\self-debugging data/local/tmp
上传成功后进行赋权
chmod 777 debugger self-debugging
找个进程调试,我们选择nfc
ps | grep com.
nfc进程的PID是3251,我们通过debugger程序开始调试
./debugger 3251
前面见过如果一个进程被调试,他的TracerPid会发生什么变化?查看下:
cat /proc/3251/status
此时TracerPid变化为5574,5574是什么呢
ps | grep 5574
可以看到5574是debugger程序的PID,那么结论就是,TracerPid会变成调试器的PID
运行self-debugging:
可以看到main pid为5636是主进程的PID
child pid为5637是子进程的PID
这是我在用debugger程序去调试self-debugging测试一下
./debugger 5636
PTRACE_ATTACH: Operation not permitted
不允许操作,可以看到这就是验证了之前讲的,一个进程只能被一个进程附加(调试)。那么怎么办呢
如果一个主进程不能被附加,就附加他的子进程即可。
./debugger 5637
这时候就可以了,这就是self-debugging反调试的流程和原理,绕过self-debugging直接附加在他的子进程即可。
六、实战详解分析-Java层反调试
JDWP协议
JDWP 是 Java Debug Wire Protocol 的缩写,它定义了调试器(debugger)和被调试的 Java 虚拟机(target vm)之间的通信协议
安卓程序动态调试条件(两个满足之一)
1、在AndroidMainfest.xml中,application标签下,Android:debuggable=true
2、系统默认调试,在bulid.prop(boot.img),ro.debugable=1
Android SDK中有android.so.debug类提供了一个isDebuggerConnected方法,用于判断JDWP调试器是否正在工作。
java层反调试基于以上两个原理执行
1、静态分析-实战案例
jadx开打测试apk
找到Oncreate
isDebuggerConnercted方法,用于判断JDWP调试器是否正在工作。
Debug.isDebuggerConnected()获取一个值进行比较,如果为真,就进行加载 loadLibrary();库。
所以会根据isDebuggerConnected进行一个判断,只要符合条件成立就执行if里面的逻辑,不成立不加载加载 loadLibrary库。这就是在java层进行反调试。也能用来保护代码。
那如何绕过判断JDWP调试器呢?
使用AndroidKiller反编制apk安装包:
遇到加壳先不管,加壳后期会教,主要用该APK熟悉java层反调试的逻辑,以及如何修改的思路。
AndroidKiller反编译后,直接工程搜索:
isDebuggerConnected
通过工程搜索找到smile代码处,那么可看到有判断条件,绕过的方法很多,将if-nez修改为if-eqz,或者删除,注释或者该判断条件等等。
七、实战案例分析
案例一:静态分析-AntiDebug
拿到一个apk第一步先去查壳,第二步adroidkiller查看没有签名,然后就可以逆向破解逻辑了
jadx反编译antiDebug.apk,并查询onCreate方法
System.loadLibrary("antidebug");直接加载antidebug,那么说明逻辑在so库里面,ida分析
找到so文件,使用IDA打开
滴入IDA后,第一步搜索静态注册找到jni_OnLoad函数
查看伪C代码
两个赋值之后进行if判断,if判断用了或运算符,就是括号里面四个参数有一个成立if条件成立
那么只要找到四个参数的返回值进行修改,让他们返回值整理翻一个一个假即可
anti_time()方法分析
1)先查anti_time(),双击函数名称进入
首先定义结构体类型,时间类型timeval,
定义v0=getpid,然后调用了同一个函数gettimefday传入两个不同的值。
v1=tv.tv_sec - v3.tv_sec;:通过传入不同的参数调用tv_sec做差值获取到v1,如果v1小于等于1返回0,否则就kill进程。这就是一个简单的时间测试,脱壳也会在之后教学。
anti_breakpoint()方法分析
目的是想要返回值不触发,只需要里面函数的返回值都为0 即可,来分析一下逻辑,这里很多if嵌套,if在嵌套while等等,那么最终不执行return 1,可以在很多if中进行修改判断条件即可,方法很多。
anti_pthread()方法分析
pthread_self创建子线程
pipe(&pipefd); pipe是管道的意思,意思是是实现进程通信。
pthread_create创建线程,传入四个参数,查看第三个参数:anti_thread
最后都是return 0这里就是获取一个线程。
案例二:动态分析
上面通过静态的方式简单熟悉了代码,现在通过动态分析反调试如何绕过该方法。
1、环境调试
1、安装apk
adb install C:\test\AntiDebug.apk
2、上传android_server
利用adb将android_server文件传送到真机下指定的文件目录内(android_server文件在IDA\dbgsrv目录下)
adb push C:\test\android_server data/local/tmp
可以去android目录下面去看看是否成功
adb devices
adb shell
su
cd /data/local/tmp/
ls -al
将android_server改名为test,并赋予777权限给test
mv android_server test
chmod 777 test
3、启动环境
然后直接运行test,设置端口为22222
./test -p22222
进行将22222端口转发到电脑端
adb forward tcp:22222 tcp:22222
4、挂起程序
在jadx中打开apk,MainActivity.xml中有标出
com.qianyu.antidebug.MainActivity
挂起程序
adb shell am start -D -n com.qianyu.antidebug/.MainActivity
安卓启动的端口是23946
开启后再次打开另外一个CMD窗口,将安卓上面的23946端口转发到win电脑上
adb forward tcp:23946 tcp:23946
接下接下来就是要调试APK安装到真机上
adb install E:\IDA7.0\test\javandk1.apk
5、配置DDMS
6、配置IDA
然后勾选三项配置
然后F9进入run状态
加载so文件
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8600
再次run
libantidebug.so就加载进来了,这样就可以开始IDA动态分析了
2、动态调试分析
在Modules找到libantidebug.so的JNI-onload:
在JNI_onload下断点
F9执行
可以看到红色变为蓝色后,程序就运行到了下断点出。
此时进来后看到状态寄存器T=1:
说明他这里面都是thumb模式指令。
0 2 4 6 8 A,都是每次增加两位
接下来进行动静结合深入理解
通过解压apk,在libs下找到so库文件,丢入IDA打开JNI_onload,并按F5查看伪c代码
这四个函数和反调试有关,来找一个函数位置,复制到汇编代码
接下来和动态一起从上往下依次分析,R3是什么
直接F8过来,R3是GetEnv,继续步入
可看到BLX R3就是源码中的GetEnv,if条件如果Env获取成功的话就会执行后面的三个函数
通过前面就知道,这里三个函数会在这里进行反调试,如何跳过不执行呢?
同步PC寄存器
接下来吧三个函数NOP掉即可!F2修改为00后,F2保存
操作完成后可以看到三个函数就没有了。
查看下registerNative,双击进入
此时BL R5里面有四个参数,遇到registerNative共四个参数,只需要看第三个参数即可,这里有R5寄存器有四个参数,可以跳转到R5查看:F4
这时候F4跳转过来后,查看第三个参数R2点击箭头进入
可以看到识别地址后什么也没有,前面是没有分析该函数源码,查看源码
在c文件中jni_onload充当什么作用呢?
main入口函数:
在java层要使用loadLibrary加载so库,loadLibrary会便利so库,就是以main做为入口函数。
所以分析的C文件是要从JNI_Onload这里开始分析:
可以看到JNINativeMethod nativeMethod[]={};,这里为空,什么都没做!返回为空。那么这时候就绕过了反调试保护。
八、总结
本章节从静态调试和动态调试出发,详解解读了文件检测、端口检测、进程名称检测、定时轮询检测、self-debugging反调试,JDWP协议反调试。
关键文件检测:对特定目录下的特定文件名称进行检测,如:android_server等,如果存在,将结束程序。
调试端口检测:对正在运行的特定端口进行检测,如:23946等,如果存在,将结束程序。
进程名称检测:对正在运行的特定进程名称进行检测,如:android_server等,如果存在,将结束程序。
轮循检测:启动一个循环,定时对TracerPid的值等其他特点进行检测,如果发生被调试的特征的变化,将结束进程。
self-debugging反调试:利用一个进程只能被调试一次的特点,创建一个子进程用来对自身进行调试,让其他调试程序无法调试。
JDWP协议反调试:通过一个isDebuggerConnected方法,用于判断JDWP调试器是否正在工作,如果存在,将结束程序。
- 本文作者: 嗯嗯呐
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/776
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!