APP测试系列-绕过TracerPID检测 Hook加密数据

说来惭愧,站内文章多月未更,客官抱歉,后续会逐步恢复更新。此文章首发 Seebug Paper https://paper.seebug.org/3215/ 转载请注明来源。

一、起因

在对某一 APP 进行测试时,发现该 APP 对传输的数据包进行了加密,近几年 APP 方面的防护也逐渐提高,大型企业的 APP 传输数据包加密也是常规操作了,在尝试进行 Hook 时出现了报错,其中的报错信息引出了该文。本文主要内容为通过修改 Android 内核文件从而实现绕过 TracerPID 检测进而 Hook 加密数据。

1

二、分析

对该 APP 进行抓包时,发现 APP 请求数据包以及返回包均作了加密处理,看到数据加密,本能反应反编译查看源码,定位加密算法。

2

将 APK 拖进 GDA 反编译,下载地址:http://www.gda.wiki:9090/,APP 已被加固。

3

脱壳后获取到多个 dex,将 dex 打包在一个文件夹后拉入GDA 进行分析,根据上面加密的特征搜索 xxxKey 关键字定位算法位置。

4

代码如下,根据定位可知 str = xxxDES3Util.encode(str); 数据传输使用了 xxxDES3Util.encode() 方法,可以先对该方法进行 Hook 查看其函数输出的数据是什么。

5

由于个人使用习惯,转而利用 Jadx 打开 dex 然后利用 Jadx 自带的插件对该方法生成 Frida Hook 脚本。对比两图可知,不同的软件进行反编译代码会存在一定的差异,所以在进行反编译时不妨多尝试几个软件,对比一下。

6

Jadx 中鼠标放置到需要进行 Hook 的方法处,然后右键复制为 frida 代码即可得到一个 js 脚本,稍作修饰即可直接利用进行 hook。

7

得到的脚本如下:

1
2
3
4
5
6
7
let xxxDES3Util = Java.use("cn.xxxx.encryption.xxxDES3Util");
xxxDES3Util["encode"].implementation = function (str) {
console.log(`xxxDES3Util.encode is called: str=${str}`);
let result = this["encode"](str);
console.log(`xxxDES3Util.encode result=${result}`);
return result;
};

完善后的完整 Hook 脚本如下:

1
2
3
4
5
6
7
8
9
Java.perform(function () {
let xxxDES3Util = Java.use("cn.xxxx.encryption.xxxDES3Util");
xxxDES3Util["encode"].implementation = function (str) {
console.log(`xxxDES3Util.encode is called: str=${str}`);
let result = this["encode"](str);
console.log(`xxxxDES3Util.encode result=${result}`);
return result;
};
});

电脑 ADB 连接手机,手机上启动 frida server,尝试进行 Hook 查看返回结果,出现报错如下,程序自动退出,由报错信息可知,引出问题,关键在于:

1
FATAL EXCEPTION:SafeGuardThread

1

直接在反编译中搜索关键字 SafeGuardThread,粗略查看代码,大概猜到是一个检测手段, 关键信息在 /proc/%d/statusTracerPid 搜索引擎搜索关键字得知该代码为一个反调试的手段,网上存在相同代码的解决方案。

9

整合上面信息,由于请求数据加密且存在 TracerPid 反调试机制,想要进一步分析需要进行反反调试,想要绕过机制,首先得了解它的具体反调试手段是什么。通过一番搜索,得知 TracerPid 反调试机制为:

  • 当进程正常运行时,它的 TracerPID 值为 0,表示没有被调试。

  • 当调试器附加到进程上时,操作系统会为该进程设置一个非 0 的 TracerPID 值,用来标识当前正在调试该进程。

  • TracerPID 反调试机制会定期检查进程的 TracerPID 值,如果发现它不为 0,就判断该进程正在被调试,然后采取相应的反调试措施,比如退出程序或抛出异常等。

没有被调试的正常进程 TracerPid 值。

10

当调试器附加到进程上时的 TracerPid 值。

11

三、绕过 TracerPid 检测

绕过 TracerPid 检测机制常见的有两种手段:1、Hook 关键代码,修改 TracerPid 值为 0,绕过检测2、修改 Android 系统内核,使 TracerPid 值默认为 0 。这边直接选用第二种的方法,直接修改 Android 设备中的 TracerPid 值,一劳永逸,当下次遇到同样的反调试方案时直接利用该设备测试即可。该绕过方法的主要操作步骤为:

  • 提取手机设备的 boot.img(系统镜像文件)。
  • 修改 boot.img 中的 kerner(内核文件)中的 TracerPid 值。
  • 刷入修改后的 boot.img 文件,使 TracerPid 值默认为 0。

1、提取 boot.img

boot.img 为系统镜像文件,包含了 Android 设备在启动过程中所需的关键系统组件,是确保设备正确启动和运行的基础,它包含了以下几个主要部分:( 本文设备为 小米 Mix 2s 已 root )

  • 内核(Kernel): Android操作系统的核心部分,负责管理硬件资源、驱动设备、内存管理等基础功能。

  • 初始化系统(Init): 负责在系统启动时初始化各种服务和进程,是引导系统启动的关键组件。

  • Recovery: 一个备用的操作系统环境,用于在设备出现问题时进行系统修复和恢复。

  • Ramdisk: 一个临时的内存文件系统,在系统启动时用于加载必要的驱动和配置文件。

1、首先需要利用具有传输数据的数据线连接手机设备,然后电脑利用 adb 连接设备,通常 boot.img 位于手机设备的 /dev/block/platform/soc/xxx/by-name ,其中 xxxsoc 目录下的一个文件夹,每台设备的文件夹名称不一致,具体名称自行查看即可,操作命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
# adb 连接设备
D:\app\platform-tools>adb.exe shell

# root 权限进行 boot.img 目录
polaris:/ $ su
polaris:/ # cd /dev/block/platform/soc/
polaris:/dev/block/platform/soc # ls
1d84000.ufshc
polaris:/dev/block/platform/soc # cd 1d84000.ufshc/
polaris:/dev/block/platform/soc/1d84000.ufshc # ls
polaris:/dev/block/platform/soc/1d84000.ufshc # cd by-name/
polaris:/dev/block/platform/soc/1d84000.ufshc/by-name # ls

根据上面命令可知 boot.img 位于 by-name 目录下。

12

通过 ls -al 命令可以查看到 boot.img 文件的存储分区位置为 /dev/block/sde45

1
polaris:/dev/block/platform/soc/1d84000.ufshc/by-name # ls -al

13

2、利用 dd (Data Duplicator 数据复制器)命令,将 /dev/block/sde45 中的 boot.img 复制到 /data/local/tmp 目录处,原因是避免设备权限问题无法直接利用 adb pull 该文件至电脑处,复制至 /data/local/tmp 处普通权限亦能 pull。

1
dd if=/dev/block/sde45 of=/data/local/tmp/boot.img

14

利用 adb pull boot.img至电脑。

1
adb pull /data/local/tmp/boot.img D:\app\platform-tools

15

16

2、修改内核文件

需要用到 Android_boot_image_editor 进行修改,下载地址:https://github.com/cfig/Android_boot_image_editor,笔者利用的是 Ubuntu。

1)下载工具

1
git clone https://github.com/cfig/Android_boot_image_editor.git

17

2)将刚才从 Android 设备中 pull 出来的 boot.img 上传至 ubuntu 的 Android_boot_image_editor 工具处

18

19

3)利用 Android_boot_image_editor 解压 boot.img,执行命令后会下载一下依赖,等待一会即可

1
./gradlew unpack

20

出现 BUILD SUCCESSFUL 解包完成

21

解包完的文件在 build/unzip_boot 目录下

22

4)修改 build/unzip_boot 文件夹中的 kernel 文件,我们需要的操作就是解开 kernel 文件,然后修改部分参数,实现 TracerPid 值默认为0,从而实现绕过检测。需要注意的是,先利用 binwalk 分析一下 kernel 文件,存在一个 gzip 文件,gzip 下面是设备文件,其中 0x0 下面第一个 HEXDECIMAL 等一下需要用到

1
binwalk kernel

23

5)现在我们需要提取出头部的 gzip,解压修改后拼接回去,这边利用网上现有的 python 脚本进行提取,其中 0xD3FD68 根据自己的 kernel 文件利用 binwalk 查询后自行进行替换

1
2
3
4
with open("./kernel","rb") as f:
content=f.read(0xD3FD68)
with open("./kernel_core.gz","wb") as f:
f.write(content)

24

执行完 py 脚本后得到 kernel_core.gz 文件

25

将 kernel_core.gz 文件复制出来 windows 上然后进行解压,利用 010 Editor 进行修改,下载地址:https://www.sweetscape.com/010editor/,直接 Ctrl + F 搜索 Text 类型:TracerPid

26

鼠标放置 00 00 即定位到 TracerPid 中的第二个 . 处,修改为 30 09,修改后,点击保存ctrl+s,若不放心可保存退出后重新利用 010 editor 打开查看是否已经修改

27

28

6)将 kernel_core 重命名为 kernel,然后拷贝回 Android_boot_image_editor 目录下进行压缩

1
gzip -n -f -9 kernel

29

30

7)移动到 Android_boot_image_editor-master/build/unzip_boot 目录下

31

然后利用脚本对原 kernel 进行拼接(注意脚本中的 f.seek()参数值为此前利用 binwalk 查询到的值)

1
2
3
4
5
6
7
8
9
10
with open("./kernel.gz","rb") as f:
content=f.read()

with open("./kernel","rb") as f:
f.seek(0xD3FD68)
content += f.read()

with open("./kernel_new","wb") as f:
f.write(content)

32

执行完脚本后,该目录会出现一个名为 kernel_new 的新文件

33

8)删除原 kernel 文件,并将 kernel_new 修改为 kernel,并删除多余文件,可对比之前解压的

原始解压文件

34

将原 kernel 删除后,将 kernel_new重命名为 kernel 并删除多余文件后

35

9)最后回到 Android_boot_image_editor 根目录下,执行命令重新打包

1
./gradlew pack

36

boot.img.signed 为重打包后的文件,即我们修改 TracerPid 值后需要重新刷回手机的 boot.img 文件

37

3、刷入新的 boot.img

刷入 boot.img,注意需要先将旧的 boot.img 以及修改后的 boot.img 拷贝进手机上,这一点非常重要,因为如果你没有把系统原先的 boot.img 文件保存下来,当新的 boot.img 文件存在问题可能导致无法进入系统,那手机设备就处于一个危险的处境,这时若保存了旧的 boot.img 文件时,直接刷回去就可以恢复原先的系统。这边利用第三方的 recovery 重新输入 boot.img(刷入recovery请看文末)。

1)将新旧的 boot.img 放置在手机设备上的同一目录下,此处 boot_new.img 即上文中 boot.img.signed 文件,修改名字即可。

38

2)手机进行 recovery 模式,选择 boot_new.img 刷入新的系统镜像文件,刷入后重启手机即可。

39

四、Hook 分析

通过上面的修改系统内核实现了绕过 TracerPid 检测,现在重新利用 frdia Hook 加密函数。

1
2
3
4
5
6
7
8
9
Java.perform(function () {
let xxxDES3Util = Java.use("cn.xxxx.encryption.xxxDES3Util");
xxxDES3Util["encode"].implementation = function (str) {
console.log(`xxxDES3Util.encode is called: str=${str}`);
let result = this["encode"](str);
console.log(`xxxDES3Util.encode result=${result}`);
return result;
};
});

成功进行 hook 程序无闪退,hook 结果如下,加密前和加密后的结果均已显示出来。

1
frida -UF -l test.js

40

通过对比抓包软件数据包,可知输出中的加密结果与抓包的加密数据结果一致。

41

根据上图 hook 结果可知,未加密的数据为。

42

直接将未加密的包放到抓包软件重放,可绕过加密机制,输出结果亦为未加密的结果,后续的功能测试也能利用该方法进行测试。

43

根据上面的代码,跟进加密算法,查看具体的加密算法如何。

44

加密算法使用 3DES desede/CBC/OKCS5Padding 模式。

45

继续利用 Jadx hook encode() 和 decode() 查看具体输出情况。

1
frida -UF -l test.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Java.perform(function(){
let xxxDES3Util = Java.use("cn.xxxx.encryption.xxxDES3Util");
xxxDES3Util["encode"].implementation = function (str) {
console.log(`xxxDES3Util.encode is called: str=${str}`);
let result = this["encode"](str);
console.log(`xxxDES3Util.encode result=${result}`);
return result;
};

xxxDES3Util["decode"].implementation = function (str) {
console.log(`xxxDES3Util.decode is called: str=${str}`);
let result = this["decode"](str);
console.log(`xxxDES3Util.decode result=${result}`);
return result;
};

});

部分如下,数据包的请求及响应的加解密均可查看。

46

根据已知的信息,编写一个frida rcp 实现同步输出加解密数据包。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import frida
import sys
import time

# 处理RPC调用的函数
def on_message(message, data):
# 如果message的类型是'send'
if message['type'] == 'send':
# 打印message的payload内容
print(message['payload'])
else:
# 打印整个message
print(message)

# 用于 hook 编码和解码方法的 JavaScript 代码
hook_script = """
Java.perform(function () {
// 获取目标类'cn.xxxx.encryption.xxxDES3Util'
var xxxDES3Util = Java.use("cn.xxxx.encryption.xxxDES3Util");

// 重写'encode'方法的实现
xxxDES3Util.encode.overload("java.lang.String").implementation = function (input) {
// 打印未加密的请求数据包
console.log("**************************************************** 请求数据包 ****************************************************\n");
console.log("未加密请求数据包:" + input);
// 调用原始的'encode'方法并获取加密后的输出
var output = this.encode(input);
// 打印加密后的请求数据包
console.log("加密后请求数据包: " + output);
// 返回加密后的数据包
return output;
};

// 重写'decode'方法的实现
xxxDES3Util.decode.overload("java.lang.String").implementation = function (input) {
// 打印加密后的响应数据包
console.log("**************************************************** 响应数据包 ****************************************************\n");
console.log("加密后响应数据包: " + input);
// 调用原始的'decode'方法并获取解密后的输出
var output = this.decode(input);
// 打印解密后的响应数据包
console.log("未加密响应数据包: " + output);
// 返回解密后的数据包
return output;
};
});
"""

# 获取USB设备
device = frida.get_usb_device()

# 注入目标进程,替换为目标进程的名称或包名
pid = device.spawn(["cn.xxx.xxxx"])
device.resume(pid)
# 等待进程启动
time.sleep(1)
# 连接到指定进程
session = device.attach(pid)
# 使用 hook_script 字符串作为脚本内容
script = session.create_script(hook_script)
# 当脚本产生消息时会调用这个函数进行处理
script.on("message", on_message)
# 这将开始执行 hook 操作
script.load()

print("Frida hook RPC is running. Press Ctrl+C to exit.")
sys.stdin.read()

47

五、扩展

1、刷入 Recovery

这边利用利用搞机助手刷入,具体输入方式各自可根据自身设备选择。

48

在助手引导模式中选择刷入REC,若刷入成功,点击是后手机会自动进入 recovery 模式。

49

输入一个 password ,输入开机密码即可。

50

根据自己手机选择正确的 twrp ,笔者使用的是小米的 mix2s 所以对应支持的 twrp 下载地址为https://twrp.me/xiaomi/xiaomimimix2s.html,其它机器自行查询支持的 twrp。

51

六、总结

最初的问题源自一个报错信息。通过分析这个报错信息,我们定位到了APP中的反调试机制。进一步深入了解了这种反调试手段的原理后找寻合适的方法来反反调试,最终初步解决了APP加密数据的问题。

七、参考链接

1
2
https://zone.huoxian.cn/d/325-tracerpid
https://www.sec4.fun/2021/04/16/bypassptrace/