反调试实战案例分析

1 背景

反调试,在我们脱壳的第一步。反调试虽然不能完全阻止攻击者,但是还是能加大攻击者的时间成本,一般与加壳结合使用,核心还是加壳部分。
反调试可以分为两类:一类是检测,另一类是攻击。
本文主要是对Android逆向中的反调试进行案例分析。

2 so逆向之IDA绕过AliCrackme反调试

2.1 Jdax反编译APK文件

来自阿里聚安全的AliCrackme.apk,如图1所示,随机输入密码ihui后报错验证码校验失败

图1 程序基本运行逻辑

通过Jadx反编译APK并分析后,发现调用了校验逻辑Java层的securityCheck()对应的SO库crackme,如图2所示。

图2 找到校验逻辑的so层

2.2 IDA静态分析

使用IDA打开APK解压出来的libcrackme.so文件,在模块列表中查找JNI_OnLoad或者JAVA_函数,发现二者皆有。
因此,我们首先分析动态注册函数JNI_OnLoad()函数的伪C代码。
根据伪代码分析逻辑,我们想找到动态注册函数RegisterNative,但是这里没有,最后一个函数调用了GetEnv,也就是说,Java的调用方法并不是通过动态注册的而是通过静态注册的,如图3所示。

图3 Java调用的方法是通过静态注册的

接下来,我们分析静态注册函数securityCheck
先根据头文件jni.h和基本经验修改函数的形参,分析代码运行逻辑知将input_str作为函数GetStringUTFChars的参数传进去并返回值给v5,接着在while循环中对比v6和v7即注册码和输入字符串,如果不相等直接break,然后返回0,如图4所示。

图4 静态注册方法分析

因此,我们就可以进行动态调试程序,在while循环中的适当位置加断点即可获得注册码。

2.3 IDA动态调试

通过反复调试发现在图5中断点出BLX R7的位置跳出报错FFFFFFFF,此处应该就是就是反调试检测位置了,如图5所示。

图5 发现反调试检测位置

F7进来该函数,出现pthread_create新建一个线程不停的检测TracerPid这个字段是否不为0,不为0,就立即退出程序,如图6所示。

图6 发现反调试代码

我们可以通过F2修改BLX R700 00 00 00直接替换掉该指令,因为so文件有固定格式,删除指令后多段内容的偏移值容易发生错乱,如图7所示。

图7 F2替换BLX R7指令

在静态注册方法securityCheck()的while循环的开头处下断点,然后取消勾选三项,F9到断点后在手机上输入ihui并点击按钮,在IDA的[R0]处会出现输入的字符串ihui,如图8所示。

图8 发现输入的字符串ihui

然后双击[R2]并按A键,将数据转化为字符串,查看v6的值即为注册码aiyou,bucuoo,如图9所示。

图9 发现注册码

最后,重新打开APP输入注册码即可破解成功,其实这里就是将正确的字符和我们自己输入的字符进行比较。

2.4 另解:frida hook native层静态注册方法

之前的解法就是先nop掉JNI_OnLoad中的反调试,然后再动态调试到securitycheck方法中获取相关寄存器的值。图4中将输入的字符串与v7进行比较,v7又来自于v6,v6的值等于off_628c。

当然,我们已经知道这是错误的值,在动态运行的过程中会对其进行处理,但是我们可以hook这个偏移值。
首先通过导出表函数获取securitycheck函数的值,然后根据函数值算出so基址0x11A8,加上0x628c偏移接着使用readPointer读取地址,readUtf8String读取地址中的值,具体的代码如下:

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
import frida, sys

jscode = """
setImmediate(function () {
send("Hook Start...");
var exports = Module.enumerateExportsSync("libcrackme.so");
var securityCheck = undefined;
for(var i=0;i<exports.length;i++){
if(exports[i].name == "Java_com_yaotong_crackme_MainActivity_securityCheck") {
securityCheck = exports[i].address;
break;
}
}
var flag = Memory.readUtf8String(Memory.readPointer(securityCheck.sub(0x000011A8).add(0x0000628C)));
send(flag);
send("Hook End...");
});
"""

def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)

process = frida.get_usb_device().attach('自毁程序密码')
script = process.create_script(jscode)
script.on('message', on_message)
script.load()
sys.stdin.read()

这种解法也是基于之前IDA动静分析so文件后得出的,如图10所示,可以说是殊途同归。

图10 获得flag

3 so逆向之IDA绕过FindTracer反调试

3.1 Jdax反编译APK文件

打开APP后,发现有一个信封的图标,点击后出现Everything fine.的提示信息,如图11所示。

图11 FindTracer程序运行效果

使用Jdax打开FindTracer.apk后,通过分析AndroidManifest.xml后进而到MainActivity中,发现了程序的运行逻辑以及FindTracer类下面在so层实现的findTracer()函数,如图12所示。

图12 发现so层实现的findTracer函数

3.2 IDA静态分析

使用IDA打开APK解压出来的libFindTracer.so文件,在模块列表中查找JNI_OnLoad或者JAVA_函数,发现只有JNI_OnLoad
因此,我们分析动态注册函数JNI_OnLoad()函数的伪C代码。
根据伪代码分析逻辑,我们想找到动态注册函数RegisterNative,但是这里没有。在So load success处的else处的sub_147c()函数内部有FindClass和RegisterNatives的调用,如图13所示。

图13 在so加载成功处找到RegisterNatives函数

接下来,我们分析findTracer()函数,该函数是对反调试进行检测,如果出现反调试迹象即返回1,进而导致so加载失败,点击信封后提示信息会由Everything fine.变成Fuck, a tracer has been found!!!

1
2
3
4
5
6
7
8
9
10
11
12
bool findTracer() {
char v2; // [sp+Ch] [bp+Ch]
char v3; // [sp+Dh] [bp+Dh]
char v4; // [sp+Eh] [bp+Eh]
char v5; // [sp+Fh] [bp+Fh]

v2 = isDebuggerRunning();
v3 = isAttachThread();
v4 = isAttachThreadTid();
v5 = isProcessTid();
return v2 || v3 || v4 || v5;
}

因此,我们就可以进行动态调试程序,修改findTrace()的4个反调试检测函数的返回值为0即可绕过反调试

3.3 IDA动态调试

通过3.2节的静态分析后,我们先进行动态调试前的准备工作,然后依次修改findTrace()的4个反调试检测函数的返回值为0,修改方法是在汇编代码处右键->Keypatch->Patcher,将MOVS R2, #1修改为MOVS R2, #0,依次类推,如图14所示。

图14 修改反调试检测函数的返回值

最后,取消勾选三项,F9运行程序,点击信封处会出现Everything fine.的提示信息,成功绕过反调试。

4 参考文献

[1]https://mp.weixin.qq.com/s/Mz-zumsSICFuGoNj6C0cFg
[2]https://blog.csdn.net/YJJYXM/article/details/110857185
[3]https://blog.csdn.net/weixin_42011443/article/details/105897429


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,可以邮件至 xingshuaikun@163.com。

×

喜欢就点赞,疼爱就打赏