游戏客户端开发基础-移动篇(视频习题)

GitHub - noxke/TencentGameClientOpenCourse: 腾讯游戏客户端公开课2023 腾讯菁英班

1.静态分析

Java层

使用jadx打开题目gslab.demo.apk文件,查看AndroidManifest.xml文件获取软件入口activity为com.example.x86demo.MainActivity

查看该入口activity类的onCreate方法

TextView对象tv设置的字符串来自stringFromJNI方法

查看其余部分,stringFromJNInative-lib中的native函数

Native层

该软件提供了armabi-v7ax86两种处理器架构的libnative-lib.so文件,此处对arm架构so文件进行分析

使用readelf命令查看libnative-lib.so的导出函数

stringFromJNI导出函数的偏移地址为0x08bdd,完整导出名称为Java_com_example_x86demo_MainActivity _stringFromJNI

arm指令集每条指令长度为32位,存在指令对其,指令地址低两位均为0,而此处偏移地址最低位为1,表示处理器执行到该处时进入Thumb模式,指令长度变为16位或32位,实际指令偏移地址为0x8bdc

使用ida32打开libnative-lib.so文件,定位到导出函数,指令长度大多为16位,表示处理器执行该函数时处于Thumb模式

反编译查看该函数

程序使用C++的string类构造string对象并返回,注意到除了sub_E36E2CA6函数外其它函数均为库函数,因此选择该函数进行hook

2.动态调试

使用真机(android 13)进行调试,调试工具为android studio和ida

Java层

stringFromJNI调用处的smali代码下断点,连接设备进行调试

程序在断点处断下,单步跳过查看stringFromJNI函数的返回值(v1寄存器)

函数返回结果为字符串对象Hello from C++

继续运行程序,屏幕显示Hello from C++字符串

Native层

开启adb端口转发,将ida的dbgsrv上传到手机,进入shell切换到root权限启动dbgsrv

android studio上开启调试,程序在smali代码断下

ida对stringFromJNI函数下断点,连接调试器并附加到com.example.x86demo进程,按F9继续运行

android studio单步执行,ida上程序在stringFromJNI函数断下

此处处理器处于Thumb模式

断点到sub_E3722CA6函数内部

函数只做了参数传递并调用sub_E3723258函数,sub_E3722CA6的参数为R0寄存器,值为0xFF98C0E8,显然为一个地址,跳转到该地址处

猜测该位置为一个结构体,结构体第一个整数为0x11,第二个整数为0x0E,第三个整数为0x77EB6850,第三个数明显为地址,跳转到该地址处

该地址处为字符串Hello from C++,对该字符串进行修改测试

ida使用F9运行,android studio单步断下,查看stringFromJNI返回值

返回值变为了上面修改的字符串,继续运行程序,屏幕上显示修改后的字符串

3.使用ptrace注入并hook程序

上述分析中选择sub_E36E2CA6函数进行hook,该函数偏移为0x8ca6,后续称为proc

该函数的调用关系为onCreate->stringFromJNI->proc,由于该函数在程序启动时执行,几乎没有等待时间,所以在正常执行的情况下不能够保证100%hook成功

本题使用traphook的方式修改proc函数的参数,思路如下:

  1. ptrace在proc函数开始位置设置软件断点

  2. waitpid捕获进程停止信息

  3. ptrace获取proc函数参数R0寄存器,得到字符串地址

  4. ptrace修改字符串地址处的字符串

  5. ptrace禁用断点,恢复程序执行

代码实现

ptrace函数需要待附加进程的pid,使用pidof com.example.x86demo命令可以获得软件的pid

想要hook proc函数,需要知道其内存中的加载地址,在静态分析中得到了其偏移地址proc_offset = 0x8ca6,还需要libnative-lib.so文件加载的基地址才能计算出proc函数的地址,安卓进程加载的库文件信息可以在/proc/pid/maps文件中找到

如图,本次程序运行libnative-lib.so的加载地址为lib_base = e381d000proc函数的地址为proc_addr = lib_base + proc_offset

c语言中使用popen函数执行命令行目录并获取结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 获取进程pid和libnative-lib.so加载的基地址
while (1)
{
fp = popen("pidof com.example.x86demo && cat /proc/`pidof com.example.x86demo`/maps | grep libnative-lib.so", "r");
fread(cli_output, 1, 1024, fp);
pclose(fp);
if (strlen(cli_output) != 0)
{
sscanf(cli_output, "%d%lx", &demo_pid, &lib_base);
if (lib_base == 0)
{
continue;
}
printf("demo_pid: %d\n", demo_pid);
printf("libnative-lib.so image base: 0x%lx\n", lib_base);
break;
}
}

使用ptrace附加到进程并将proc函数第一条指令修改为软件断点,设置断点前需要保存原始指令,确保后续能够使程序正确运行,设置断点后恢复进程运行

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned int bp_code = 0x0000BEFF;  // 设置FF号断点
// 需要hook的函数偏移
unsigned long proc_offset = 0x8ca6;
// 需要hook的函数地址
unsigned long proc_addr = lib_base + proc_offset;
// 附加到目标进程
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
waitpid(pid, &status, 0);
// 先保存断点前的指令,再将其设置为断点代码
ori_code = ptrace(PTRACE_PEEKDATA, pid, (void *)proc_addr, NULL);
ptrace(PTRACE_POKEDATA, pid, (void *)proc_addr, (void *)bp_code);
printf("set breakpoint at 0x%lx\n", proc_addr);
ptrace(PTRACE_CONT, pid, NULL, NULL);

当处理器执行到软件断点时会发出SIGTRAP信号,使用waitpid捕获断点信号,并获取寄存器信息,判断PC寄存器地址是否为断点地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 捕获断点
while (1)
{
waitpid(pid, &status, 0);
if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP)
{
// 获取寄存器信息
ptrace(PTRACE_GETREGS, pid, NULL, &regs);
unsigned long pc = regs.uregs[15];
printf("break at 0x%lx\n", pc);
if (pc == proc_addr)
{
// hook操作
}
}
ptrace(PTRACE_CONT, pid, NULL, NULL);
}

当程序在设置的断点处断下后,寄存器R0的值为proc函数的参数,其值为地址,R0+8为字符串地址,使用ptrace修改R0+8处的字符串,实现对stringFromJNI返回值的修改

完成修改后需要将断点恢复为原始指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 获取R0寄存器
unsigned long r0 = regs.uregs[0];
unsigned long str_addr = ptrace(PTRACE_PEEKDATA, pid, (void *)(r0 + 8), NULL);
char ori_str[0x20];
char new_str[0x20];
strcpy(new_str, hack_str);
for (int i = 0; i < 0x20; i += 4)
{
*(unsigned long *)(ori_str + i) = ptrace(PTRACE_PEEKDATA, pid, (void *)(str_addr + i), NULL);
ptrace(PTRACE_POKEDATA, pid, (void *)(str_addr + i), (void *)(*(unsigned long *)(new_str + i)));
}
printf("str at 0x%lx\n", str_addr);
printf("ori str: %s\n", ori_str);
printf("new str: %s\n", new_str);
// 恢复原来的指令
ptrace(PTRACE_POKEDATA, pid, (void *)proc_addr, (void *)ori_code);

编译程序并上传到手机测试

x86的hook实现与arm的实现基本相同,除了proc函数的偏移地址不同,以及断点指令不同以外,还需要注意x86处理器在执行int3断点后会将EIP+1,捕获断点后除了将断点指令恢复,还需要将EIP寄存器恢复


游戏客户端开发基础-移动篇(视频习题)
https://blog.noxke.icu/2023/10/02/TencentGameClient/客户端安全开发基础-移动篇(视频习题)/
作者
noxke
发布于
2023年10月2日
许可协议