App分析
引擎
lib
文件夹下有libil2cpp.so
,assets\bin\Data\Managed\Metadata
下有global-metadata.dat
,说明是unity3D引擎的il2cpp编译方式。global-metadata.dat
和libil2cpp.so
均被加密。
ida 打开 libsec2021.so 不识别,用readelf查看发现e_phentsize
字段有问题,大小为23,将23改为 32 后可以正常识别。
检测绕过
libsec2021.so
还是壳的so,同样字符串被整体加密处理了,但是解密算法直接inline,而且看起来密文并不完全在一个数组中。
同样frida spawn模式启动app然后ida attach上去调试,发现调试退出的地方都长这样
使用ida脚本对这些地方批量下断点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import idc
base = 0xD1E05000 # base of libsec2021
ea = base + 0x58A0
end = base + 0x416d4
idc.create_insn(ea)
while ea < end:
ins = idc.generate_disasm_line(ea, 0)
if ins == "EOR R1, R1, R2":
ea_1 = idc.next_head(ea)
ins = idc.generate_disasm_line(ea_1, 0)
if ins == "BLX R1":
ida_dbg.add_bpt(ea_1)
ea = ea_1
ea = ea + 4
idc.create_insn(ea)
print("Finished")
|
经过调试发现 check 的位 置应该在 sub_2E3C8 中,这个函数经过了混淆,直接调用改间接调用,尝试用 keystone 去一下混淆: (这里利用了 ida 会解释这个间接调用的位置,写在调用指令后的注释 中,所以可以直接从注释中取调用的函数地址,不用自己计算):
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
| import keystone as ks
import idautils
import ida_bytes
md32 = ks.Ks(ks.KS_ARCH_ARM, ks.KS_MODE_ARM)
sub_str = "; sub_"
def getASM(str, ea):
try:
ans = md32.asm(str, ea)[0]
except Exception:
ans = []
return ans
def main():
cur_addr = 0x58A0
idc.create_insn(cur_addr)
while cur_addr < 0x416D4:
idc.create_insn(cur_addr)
ins = idc.generate_disasm_line(cur_addr, 0)
if ins == "":
print(hex(cur_addr), "null")
if ins[0] == 'B' and ins.find(" R") > 0:
if ins.find(sub_str) >= 0:
idx = ins.find(sub_str) + len(sub_str)
addr = ins[idx:]
b_ins = ins[:ins.find(' ')]
print(hex(cur_addr), ins)
new_ins = b_ins + ' ' + "0x" + addr
new_asmins = getASM(new_ins, cur_addr)
print(addr, new_ins, new_asmins)
for i in range(len(new_asmins)):
ida_bytes.patch_byte(cur_addr + i, new_asmins[i])
cur_addr = cur_addr + 4
idc.create_insn(cur_addr)
main()
|
Patch 前后:(有一些 keystone 会解析失败,然后一些系统调用函数没有加进来,比 如说 new [])
手动f9将断下来的地方patch。这时程序会断在libunity
中,应该是利用程序校验和解密unity,patch导致密钥计算不正确解密失败所以出错退出了。
从libunity中的initproc函数调试,发现libunity使用了libsec2021导出表g_sec2021_p_array
中的第一个函数sub_39140
去解密。
调试这个函数,sub_33B08
返回一个bss段的地址,这个地址处存储的是一个指向堆的指针。
sub_340D8
将sem_wait
接收到的值写入上述地址中,结合初赛的题目可以猜测这个地方就是key存储的位置。因为程序被patch导致此处key值发生了改变所以libunity
解密不正确。直接用GG挂上去看内存,得到此处存储的值为de 42 78 27 03 20 00 00
,将程序patch修改此处的值为固定值即可。
使用findcrypt
可以检测出AES加密常数,解密算法为AES CBC算法,经过调试和hook发现key是classes.dex,AndroidManifest.xml,代码段,以及 com/tencent/games/sec2021/Sec2021Application这个字符串的crc32值,即为277842de277842de
,iv为算法中写死的值[ 0, 4, 8, 0xC, 0x10, 0x14, 0x18, 0x1C, 0x20, 0x24, 0x28, 0x2C, 0x30, 0x34, 0x38, 0x3C]
。
patch完成之后已经可以调试了。
程序逻辑修改
准备解密 metadata,直接用GG从内存dump出来 il2cpp.so,替换原始so的.text和.rodata,搜字符串找到 initialize函数。
动态调试截获sub_5B9238的返回值就是解密后的metadata文件。但是直接扔到il2cppdumper中会报错。
参考FlappyBirdStyleGame代码,直接编译后发现libil2cpp.so是一样的,这样就能用il2cppdumper恢复符号。找到了OnTriggerEnter2D的位置,修改触碰Obstacle
后的行为,具体只要把00540E60
的BNE语句改成B语句。
关键就是过掉这个检测,patch本身很容易,但是由于程序有解密操作,所以直接在解密函数中判断并 patch。 用 bss 段后一小段未使用空间用来计数,当前是第几次解密。如果是第三次解密就进行 patch,把 0x1A 替换为 0xEA。
函数放在解密过后的校验过程里。重打包以后即可达到撞杆不死的效果。
(同样,因为是AES CBC模式的加密,可以将需要修改代码段所在的整个block进行加密。)
其他解法
hot patch
来自shyoshyo师傅的题解。
在过反调试的时候可以从崩溃日志中查看得到出错退出的位置
因为直接patch会导致后续解密失败,可以选择hot patch的方式,修改/proc/pid/mem文件动态修改App内存。
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
| void ipatch(int mem_fd, unsigned long long addr, unsigned char old, unsigned char new, int dir)
{
unsigned char buf1[] = {0, 0};
unsigned char buf2[] = {0, 0};
unsigned char buf3[] = {dir ? old : new, 0};
lseek64(mem_fd, addr, SEEK_SET);
read(mem_fd, buf1, 1);
lseek64(mem_fd, addr, SEEK_SET);
write(mem_fd, buf3, 1);
lseek64(mem_fd, addr, SEEK_SET);
read(mem_fd, buf2, 1);
printf("%s %08llx: %02x -> %02x\n", dir ? "old" : "new", addr, buf1[0], buf2[0]);
}
int main(int argc, char *argv[])
{
//…
sprintf(mem_file_name, "/proc/%s/mem", argv[1]);
mem_fd = open(mem_file_name, O_RDWR);
long long addr = findaddr(argv[1], "sec2021", "00000000");
if (addr != -1)
{
printf("start patch ...\n");
ipatch(mem_fd, addr + 0x01B541 - 1, 0x2A, 0x00, 0);
ipatch(mem_fd, addr + 0x01B543 - 1, 0x00, 0xA0, 0);
// 省略若干,都可以根据崩溃日志分析得到
ipatch(mem_fd, addr + 0x00024602, 0x2F, 0xA0, 0);
ipatch(mem_fd, addr + 0x00024606, 0xE0, 0x00, 0);
ipatch(mem_fd, addr + 0x0004B230, 0x9f, 0x12, 0);
ipatch(mem_fd, addr + 0x0004B231, 0x13, 0x34, 0);
ipatch(mem_fd, addr + 0x0004B232, 0x79, 0x56, 0);
ipatch(mem_fd, addr + 0x0004B233, 0x66, 0x78, 0);
printf("patch done %d ...\n", 0);
}
return 0;
}
|
然后通过静态注入的方式注入一个共享库,然后在共享库里放 hot-patch 的代码(以及绕过包重签名的)。注入利用 libmain,方式如下:将原来的 libmian.so 重命名为 libmain2.so,注入用的新代码写在 libmain.so 里,并 libmian.so 拉 libmain2.so,而且libmain.so 要将 libmain2.so 导出的 JNI_OnLoad 向调用者传过去。libmian.so 的入口是 my_init()。
对照 il2cpp 和 il2cppdumper 源码分析,发现偏移值对不上,把开头三个 string 相关区块挪到中间去 了,恢复脚本关键代码如下
分析过程中发现对字符串区段有加密,异或加密
恢复之后丢到 il2cppdumper,还是有问题。 把版本号改成 0x18,成功恢复出 dll。 运行 ida 脚本后找到关键函数PlayerController$$OnTriggerEnter2D
参考链接
https://blog.xhyeax.com/2021/04/10/gslab2021-final-android/
https://www.52pojie.cn/thread-1420796-1-1.html