安卓逆向-反调试与绕过反调试的几种姿势

反调试在代码保护中扮演着很重要的角色,虽然不能完全阻止攻击者,但是还是能加大攻击者的时间成本,一般与加壳结合使用。 反调试可以分为两类:一类是检测,另一类是攻击,前者是去想各种办法去检测程序是否在被调试,如果正在被调试的话做出一些“反”的举措,比如退出等等,后者是采用攻击的方法,就是想办法让调试器不能正常工作或者是让调试器崩溃,从而阻止它,本章主要讲解检测方面的知识。

前言

  1. 反调试在代码保护中扮演着很重要的角色,虽然不能完全阻止攻击者,但是还是能加大攻击者的时间成本,一般与加壳结合使用。
  2. 反调试可以分为两类:一类是检测,另一类是攻击,前者是去想各种办法去检测程序是否在被调试,如果正在被调试的话做出一些“反”的举措,比如退出等等,后者是采用攻击的方法,就是想办法让调试器不能正常工作或者是让调试器崩溃,从而阻止它,本章主要讲解检测方面的知识。

一、实战详解分析-关键文件检测

1、IDA案例思路分析

反调试通俗意思就是,程序挂起后突然出现八个F:FFFFFFFF或者在java层运行程序一直运行不起来等情况!

第一个反调试会检测android_server,文件名检测!来分析下android_server源文件

image-20210923220233320

接下来分析下filecheck,用IDA打开

image-20210923220426176

拖入后,IDA反编译

image-20210923220525204

这是编译可执行文件,那么和so文件有什么区别呢?

SO文件是可以找到JNI_onload,那么编译可执行文件在IDA如何找逻辑所在处呢?

可看到main函数的入口函数,在Exports搜索start:

image-20210926144617107

image-20210928171115139

进来后看到main函数,还有了BL libc_init指令,这时候看看有几个参数?

image-20210928171249061

如果这个函数的参数超过了四个以上(>4),跳转的地址就得用其他的寄存器来替代,libc_init当它这里没有被代替的时候,往下找最大的寄存器只出现了R3为止,没有出现R4以上的寄存器,那么就可以猜测传入的个数的参数为:0-3就是四个参数!TAB查看伪C代码:

image-20210928172353677

双击main方法

image-20210928173258956

这样就是编译可执行程序所要找的main函数

就算在重新打开IDA反编译,还是直接进入main函数处,那么得知道如何进入main函数的地方

开始分析,TAB键打开伪c代码

image-20210928173729206

隐藏类型后:

image-20210928173908137

check()检查,执行下面的循环if,判断if需要执行检查check,双击check进入

image-20210928180011857

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、案例思路源码分析

过掉文件检查不难,直接改名字即可!开始分析下面的案例

image-20210928211601485

C语言从这个main函数开始执行,首先对数组的开始一个声明,主要关心check函数,

image-20210928211806646

check就是检测android_server文件的!和之前理解的是一样的,定义了一个字符串指针指向目录/data/local/tmp,然后定义一个文件dir操作,用opendir打开tmp目录,打开后获取pid,接下来判断有没有打开,如果打开不为空(!=)就打开成功下面的while条件代码,currentDir有定义了一个指针,当读取的文件指针不为空(null),就继续往下读取。

遍历一个文件时,如果你的指针指向的下一个位不为空,意味着你的指针的后面还有文件。

3、Android底层详解

上传文件名为android_server和filecheck可执行文件(android_server文件在IDA的dbgsrv目录下)

  1. adb push C:\test\filecheck data/local/tmp
  2. adb push C:\test\android_server data/local/tmp

image-20210928235820590

成功上传到底层,可执行文件

为filecheck赋最高权限

  1. chmod 777 filecheck

image-20210929000057209

执行filecheck文件

image-20210929000308206

我们看到执行失败,报处kliied

为android_server改名测试

image-20210929000506490

可以看到改名后,filecheck正常执行

二、实战详解分析-调试端口检测

1、源码分析调试端口检测

image-20211014205714171

打开checkTCP.c文件开始分析

直接分析check方法

image-20210929000940227

首先C文件中执行定义字符数组char buf[0x1000]={0};,然后执行命令cat /proc/net/tcp |grep :5D8A”,这里grep :5D8A就是只查找存在5D8A字符的那一行,在/proc/net/tcp文件中5D8A代表端口的意思,5D8A是十六进制,转为十进制就是23946

image-20210929000900782

image-20210929103201116

如果存在23946这个端口就进入while循环,执行kill

image-20210929103318832

2、IDA分析调试端口检测

找到可执行文件

image-20210929103449507

使用IDA打开,找到main函数,开始分析

image-20210929103647692

如何找到main函数

  1. 1、左边列表中
  2. 2、在Exports搜索start,双击进入找到main双击进入

找到main函数后,查看伪C代码

image-20210929104234815

双击sub_728

image-20210929104256542

这里的伪C代码和前面分析的c语言源码是一样的

3、Android底层详解

上传可执行文件,上传android_server文件

  1. adb push C:\test\checkTCP data/local/tmp
  2. adb push C:\test\android_server data/local/tmp

image-20210929105228340

为两个文件赋执行权限

image-20210929105427798

测试没有启动23946端口状态

执行checkTCP文件

image-20210929105844964

没有输出,完成程序执行

测试启动23946端口状态

执行android_server,启动23946端口

image-20210929105526149

打开一个新窗口,执行checkTCP文件

image-20210929105620546

我们看到输入了cat /proc/net/tcp |grep :5D8A结果,并killed了checkTCP程序

三、实战详解分析-进程名称检测

1、源码分析进程名称检测

找到c语言源码

image-20211014205735864

开始分析源码

image-20211014205753157

直接从main函数开始分析,看到coursecheck方法

image-20210929112248303

image-20210929112312427

sprintf(filename, “/proc/%d/status”, pid); 意思就是获取(getpid)pid值放到%d里面,然后可读的形式filename打开(r)操作,就是获取此时进程程序的一个状态。

FILE *fd=fopen(filename,”r”);

if(fd!=NULL)

开打一个文件fd,如果打开成功就进行while循环

image-20210929165405874

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方法

image-20210929203558069

双击sub_794进入

image-20210929203658298

getpid获取程序的pid,然后fopen指向命令,如果指向成功就进行检测TracerPid值,这里只是v6,这里v6在IDA识别为未定义的值。

3、Android底层详解

上传进程检测可执行文件及android_server文件

  1. adb push C:\test\BubbleSort data/local/tmp
  2. adb push C:\test\android_server data/local/tmp

image-20210929204254368

赋权BubbleSort文件

  1. chmod 777 BubbleSort

image-20210929204410339

执行程序

image-20210929204546605

打开一个新窗口,查看BubbleSort文件运行状态

  1. ps | grep BubbleSort

image-20210929205251240

11032是进程名称

查看该进程下的status文件

  1. cat /proc/11032/status

image-20210929205647476

执行命令后就会遍历程序里面的内容,如果有TracerPid则不等于null,进入if,atoi检测前十位就是TracerPid:,判断TracerPid:后面的内容,如果为0就不进入下一个if,如果不为0就进入if继续执行。

那么现在让他发生变化,需要使用IDA!

先运行android_server启动,先给android_server,并不用默认端口启动

  1. mv android_server test
  2. ./test -p12345

image-20210929211145238

在电脑端进行端口转发

  1. adb forward tcp:12345 tcp:12345

image-20210929211233196

完成后,打开IDA,连接本地的12345端口

image-20210929211305930

配置IDA连接

image-20210929212208781

配置IDA选项

image-20210929211606263

继续ps查询BubbleSort程序

  1. ps | grep BubbleSort

image-20210929212233379

根据进程名称11536查看进程文件夹中的status内容

image-20210929212411910

可以看到这时候TracerPid内容不为0了,为11389,发生了变化

image-20210929212648205

源码提示如果检测到TracerPid的值不等于0对程序进程打印然后杀死进程。

如何过滤掉?首先他要获取TracerPid,返回值为R0,那么修改R0或者注释即可,或者刷机修改系统内容,或者把TracerPid修改回去都可以

进程名称检测:

  1. 当动态调试的时候找到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、源码分析-轮循检测技术

image-20210930152700791

打开main.cpp

image-20210930153102297

从main函数入口分析,main函数中调用了anti_debugger()方法,anti_debugger有调用了anti_debugger_thread方法,anti_debugger_thread中有调用了check_debugger方法

下面详细分析check_debugger中的内容

image-20210930153538397

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,并赋执行权限

  1. adb push C:\test\poll_anti_debug data/local/tmp
  2. chmod 777 poll_anti_debug

image-20210930161630958

目前上传了检测调试程序,还需要上传调试程序

image-20210930162250120

上传debugger调试程序,并赋权

  1. adb push C:\test\debugger data/local/tmp
  2. chmod 777 debugger

image-20210930162356198

运行检测调试程序poll_anti_debug

image-20210930162502139

一直在循环打印TracerPid: 0

我们开启debugger程序,来模拟我们现在正在调试,将pid值设置为poll_anti_debug程序的pid

image-20210930164223315

我们看到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反调试特点:

image-20211009150633702

fork是一个函数,fork函数fork出一个子进程来调试自己,那么别的函数就无法进程调试了,那么通过调试fork出来的子进程从而调试父进程。

同一时刻,一个进程只能被一个进程附加(调试)

用的比较少,还没进程名称检查用的多

2、self-debugging反调试案例详解

将测试文件debugger和self-debugging上次到测试机

  1. adb push C:\test\debugger data/local/tmp
  2. adb push C:\test\self-debugging data/local/tmp

image-20211009151616260

上传成功后进行赋权

  1. chmod 777 debugger self-debugging

image-20211009151725191

找个进程调试,我们选择nfc

  1. ps | grep com.

image-20211009151823524

nfc进程的PID是3251,我们通过debugger程序开始调试

  1. ./debugger 3251

image-20211009152328706

前面见过如果一个进程被调试,他的TracerPid会发生什么变化?查看下:

  1. cat /proc/3251/status

image-20211009152656089

此时TracerPid变化为5574,5574是什么呢

  1. ps | grep 5574

image-20211009152756381

可以看到5574是debugger程序的PID,那么结论就是,TracerPid会变成调试器的PID

运行self-debugging:

image-20211009153004145

可以看到main pid为5636是主进程的PID

child pid为5637是子进程的PID

这是我在用debugger程序去调试self-debugging测试一下

  1. ./debugger 5636

image-20211009154000703

  1. PTRACE_ATTACH: Operation not permitted

不允许操作,可以看到这就是验证了之前讲的,一个进程只能被一个进程附加(调试)。那么怎么办呢

如果一个主进程不能被附加,就附加他的子进程即可。

  1. ./debugger 5637

image-20211009154354730

这时候就可以了,这就是self-debugging反调试的流程和原理,绕过self-debugging直接附加在他的子进程即可。

六、实战详解分析-Java层反调试

  1. JDWP协议
  2. JDWP Java Debug Wire Protocol 的缩写,它定义了调试器(debugger)和被调试的 Java 虚拟机(target vm)之间的通信协议
  3. 安卓程序动态调试条件(两个满足之一)
  4. 1、在AndroidMainfest.xml中,application标签下,Android:debuggable=true
  5. 2、系统默认调试,在bulid.prop(boot.img),ro.debugable=1
  6. Android SDK中有android.so.debug类提供了一个isDebuggerConnected方法,用于判断JDWP调试器是否正在工作。

java层反调试基于以上两个原理执行

1、静态分析-实战案例

jadx开打测试apk

image-20211009163103887

找到Oncreate

image-20211009171612858

isDebuggerConnercted方法,用于判断JDWP调试器是否正在工作。

image-20211009171722685

Debug.isDebuggerConnected()获取一个值进行比较,如果为真,就进行加载 loadLibrary();库。

image-20211009171909279

所以会根据isDebuggerConnected进行一个判断,只要符合条件成立就执行if里面的逻辑,不成立不加载加载 loadLibrary库。这就是在java层进行反调试。也能用来保护代码。

那如何绕过判断JDWP调试器呢?

使用AndroidKiller反编制apk安装包:

image-20211013115155513

遇到加壳先不管,加壳后期会教,主要用该APK熟悉java层反调试的逻辑,以及如何修改的思路。

image-20211013115414711

AndroidKiller反编译后,直接工程搜索:

isDebuggerConnected

image-20211013115600685

通过工程搜索找到smile代码处,那么可看到有判断条件,绕过的方法很多,将if-nez修改为if-eqz,或者删除,注释或者该判断条件等等。

七、实战案例分析

案例一:静态分析-AntiDebug

image-20211013215458553

拿到一个apk第一步先去查壳,第二步adroidkiller查看没有签名,然后就可以逆向破解逻辑了

jadx反编译antiDebug.apk,并查询onCreate方法

image-20211014004728215

image-20211014004759140

System.loadLibrary(“antidebug”);直接加载antidebug,那么说明逻辑在so库里面,ida分析

找到so文件,使用IDA打开

image-20211014005026207

image-20211014005111204

滴入IDA后,第一步搜索静态注册找到jni_OnLoad函数

image-20211014005416765

查看伪C代码

image-20211014005653133

两个赋值之后进行if判断,if判断用了或运算符,就是括号里面四个参数有一个成立if条件成立

image-20211014005829110

那么只要找到四个参数的返回值进行修改,让他们返回值整理翻一个一个假即可

anti_time()方法分析

1)先查anti_time(),双击函数名称进入

image-20211014010036758

首先定义结构体类型,时间类型timeval,

定义v0=getpid,然后调用了同一个函数gettimefday传入两个不同的值。

v1=tv.tv_sec - v3.tv_sec;:通过传入不同的参数调用tv_sec做差值获取到v1,如果v1小于等于1返回0,否则就kill进程。这就是一个简单的时间测试,脱壳也会在之后教学。

anti_breakpoint()方法分析

image-20211014153229272

目的是想要返回值不触发,只需要里面函数的返回值都为0 即可,来分析一下逻辑,这里很多if嵌套,if在嵌套while等等,那么最终不执行return 1,可以在很多if中进行修改判断条件即可,方法很多。

anti_pthread()方法分析

image-20211014153948338

pthread_self创建子线程

pipe(&pipefd); pipe是管道的意思,意思是是实现进程通信。

pthread_create创建线程,传入四个参数,查看第三个参数:anti_thread

image-20211014154316612

最后都是return 0这里就是获取一个线程。

案例二:动态分析

上面通过静态的方式简单熟悉了代码,现在通过动态分析反调试如何绕过该方法。

1、环境调试

1、安装apk

  1. adb install C:\test\AntiDebug.apk

image-20211014155013486

2、上传android_server

利用adb将android_server文件传送到真机下指定的文件目录内(android_server文件在IDA\dbgsrv目录下)

  1. adb push C:\test\android_server data/local/tmp

image-20210814155335583

可以去android目录下面去看看是否成功

  1. adb devices
  2. adb shell
  3. su
  4. cd /data/local/tmp/
  5. ls -al

image-20210814155601486

将android_server改名为test,并赋予777权限给test

  1. mv android_server test
  2. chmod 777 test

image-20211014155516994

3、启动环境

然后直接运行test,设置端口为22222

  1. ./test -p22222

image-20211014155606681

进行将22222端口转发到电脑端

  1. adb forward tcp:22222 tcp:22222

image-20211014160109128

4、挂起程序

在jadx中打开apk,MainActivity.xml中有标出

image-20211014155857589

  1. com.qianyu.antidebug.MainActivity

挂起程序

  1. adb shell am start -D -n com.qianyu.antidebug/.MainActivity

image-20211014160248189

安卓启动的端口是23946

开启后再次打开另外一个CMD窗口,将安卓上面的23946端口转发到win电脑上

  1. adb forward tcp:23946 tcp:23946

image-20210814160750956

接下接下来就是要调试APK安装到真机上

  1. adb install E:\IDA7.0\test\javandk1.apk

image-20210814161526810

5、配置DDMS

image-20211014160411454

6、配置IDA

image-20211014161414385

然后勾选三项配置

image-20211014161519397

image-20211014161501910

然后F9进入run状态

image-20211014161601770

加载so文件

  1. jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8600

image-20211014161709972

再次run

image-20211014161732179

libantidebug.so就加载进来了,这样就可以开始IDA动态分析了

2、动态调试分析

在Modules找到libantidebug.so的JNI-onload:

image-20211014161958408

在JNI_onload下断点

image-20211014162205639

F9执行

image-20211014162255608

可以看到红色变为蓝色后,程序就运行到了下断点出。

此时进来后看到状态寄存器T=1:

image-20211014162408419

说明他这里面都是thumb模式指令。

image-20211014162715013

0 2 4 6 8 A,都是每次增加两位

接下来进行动静结合深入理解

通过解压apk,在libs下找到so库文件,丢入IDA打开JNI_onload,并按F5查看伪c代码

image-20211014163136817

image-20211014163154513

这四个函数和反调试有关,来找一个函数位置,复制到汇编代码

image-20211014165459018

接下来和动态一起从上往下依次分析,R3是什么

image-20211014165741162

直接F8过来,R3是GetEnv,继续步入

image-20211014165930366

可看到BLX R3就是源码中的GetEnv,if条件如果Env获取成功的话就会执行后面的三个函数

image-20211014170047426

通过前面就知道,这里三个函数会在这里进行反调试,如何跳过不执行呢?

同步PC寄存器

image-20211014170706665

接下来吧三个函数NOP掉即可!F2修改为00后,F2保存

image-20211014170900638

操作完成后可以看到三个函数就没有了。

查看下registerNative,双击进入

image-20211014171019584

image-20211014171027828

image-20211014171104090

此时BL R5里面有四个参数,遇到registerNative共四个参数,只需要看第三个参数即可,这里有R5寄存器有四个参数,可以跳转到R5查看:F4

image-20211014172613510

这时候F4跳转过来后,查看第三个参数R2点击箭头进入

image-20211014172654237

可以看到识别地址后什么也没有,前面是没有分析该函数源码,查看源码

image-20211014172820224

在c文件中jni_onload充当什么作用呢?

main入口函数:

在java层要使用loadLibrary加载so库,loadLibrary会便利so库,就是以main做为入口函数。

所以分析的C文件是要从JNI_Onload这里开始分析:

image-20211014173153591

可以看到JNINativeMethod nativeMethod[]={};,这里为空,什么都没做!返回为空。那么这时候就绕过了反调试保护。

八、总结

  1. 本章节从静态调试和动态调试出发,详解解读了文件检测、端口检测、进程名称检测、定时轮询检测、self-debugging反调试,JDWP协议反调试。
  2. 关键文件检测:对特定目录下的特定文件名称进行检测,如:android_server等,如果存在,将结束程序。
  3. 调试端口检测:对正在运行的特定端口进行检测,如:23946等,如果存在,将结束程序。
  4. 进程名称检测:对正在运行的特定进程名称进行检测,如:android_server等,如果存在,将结束程序。
  5. 轮循检测:启动一个循环,定时对TracerPid的值等其他特点进行检测,如果发生被调试的特征的变化,将结束进程。
  6. self-debugging反调试:利用一个进程只能被调试一次的特点,创建一个子进程用来对自身进行调试,让其他调试程序无法调试。
  7. JDWP协议反调试:通过一个isDebuggerConnected方法,用于判断JDWP调试器是否正在工作,如果存在,将结束程序。
  • 发表于 2021-10-19 18:04:09
  • 阅读 ( 10318 )
  • 分类:漏洞分析

1 条评论

嗯嗯呐
嗯嗯呐

4 篇文章

站长统计