使用lief工具写一个类似于腾讯so保护这样一个东西。本篇记录lief工具的使用和TX加固的原理,不会对ELF文件格式进行记录。
一、工具介绍
首先,lief是一个文件解析工具,可以解析ELF、PE、DEX等,用途比较广泛。
github 地址:https://github.com/lief-project/LIEF
我使用的是python版,在使用过程中经常出现毛病,所以并不建议使用python版。最好是不怕麻烦自己写一个,就可以避免很多不稳定导致的fix代码。
二、TX SO加密介绍
首先,王者荣耀采用的是il2cpp,比较关键的so有3个,libil2cpp.so、libGameCore.so、libtprt.so。其中libtprt.so为保护so。
王者荣耀加载被保护so(il2cpp或GameCore)时,会优先加载链接库。所以,整个流程unity启动后,会优先加载libtprt.so。
libtprt.so对自身的tptext节进行了加密,在加载过程中执行init_array进行解密,跟踪mprotect,可以看到解密代码:
使用lief快速解密:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
tptext节里的函数主要用于初始化各类保护,会被jni_onload调用。现在tprt已经被成功加载,现在就需要解密libil2cpp.so
由于libGameCore.so小一点,分析起来方便,所以后面就不分析il2cpp了。先观察ELF结构,可以发现它的LOAD段格外多。
个人分析他比原生多了3个LOAD段,新增的第一个LOAD段(04),里面是.dynamic 和 .init_array。很好理解,因为强行添加了一个依赖库和init_array,所以需要生成新的.dynamic,至于init_array个人猜测是添加字符串混淆之类的东西。
新增的第二个LOAD段(05),是新的.rel.plt,用ida分析,可以明显看出有增加项,且增加项在新增的第三个LOAD段(06):
跳转到0x228cfcc,可以看到这是一个got表,并且它的plt表在新增的第二个LOAD段(05)。
对添加的LOAD段有一定了解了,再看一下修改后的.dynamic有什么变化:
需要关注的,我已经添加了红色方框。大部分只是修改偏移,指向新的节(JMPREL、INIT_ARRAY),值得注意的是DT_INIT,这个是init段,比init_array更早执行。更适合用来解密。
通过取off_1cc的地址 - off_1cc的值(地址为base+0x1cc,值是自己设置的,为1cc)所以算出来是当前so的基址,然后调用sub_2289b58并且传入基址。
sub_2289b58就是解密函数,网上对它的分析很多了,总的就是调用g_tprt_pfn_array(“.text”,base,3)对当前的text段进行解密。(“.text”这个字符串,在新增的第三个LOAD(06)中,意味着是写死的)
所以总结一下:
MTP对SO新增了3个LOAD段,第一个LOAD新增.dynamic、.init_array,第二个LOAD段是映射新的.rel.plt、.plt、.text节,第三个LOAD段用来映射.got、.data节。
MTP在program header 之后添加了一段汇编,在init时执行,获取当前SO的基址,传入解密函数,并调用libtprt.so的导出函数g_tprt_pfn_array进行text节解密。
三、代码实现
由于一次写完大概率会很乱思路不够清晰,所以分几步写,方便理解,也好记录。
步骤一:添加INIT段,并调用任意函数
首先,生成一个SO,很简单,有一个PrintLog函数没有调用,所以LOG只有一条:
执行以下代码:
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 |
|
可以看到函数PrintLog优先于JNI函数执行,并且传入的参数为基址。
步骤二:向so中添加可执行代码
需要记住的几个总结:
rel.plt ->got -> plt -> extern表(导入函数)
rel.plt ->got -> text(导出)
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 |
|
编译一个so,里面的示范代码:
十分简单,就是一个打印log,将编译好的so拖入ida,可以看到,虽然我只写了一个函数,实际上so运行时还需要许多其他的函数。
执行以下代码:
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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
|
将intermediate.so改名为libnative-lib.so,在apk里能成功执行。不报错就是胜利!
ida打开intermediate.so可以看到相关的函数,虽然把plt解析成了函数,不过不影响。
再对比libadd.so,可以看到,函数内容是一致的:
成功运行不报错~
结合步骤一和步骤二,让添加的decrypt函数执行起来
有两种方式,TX是让plt-》got表之间的偏移不变,就不需要修改plt表。
如果修改了plt、got之间的偏移,就需要通过修改字节码来实现正常运行。
首先可以看到plt表有三条指令,获取当前地址,添加偏移,跳转。
第一个红框,由于是0不好理解,我换成0x01. 那么就是取当前地址+0x0100000
第二个红框,0x62,向r12添加 0x62000
第三个红框,0xf00c,通过a8028 - a801c = c 。所以低12位为添加的偏移-》0x00c、0x1fc
由于其他代码与之前的相同,就没必要重复粘贴了。将新增的plt表修复相关代码粘贴出来。
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 |
|
修复plt表后,执行so,就可以看到我们注入的decrypt函数优先于JNI函数执行了。为什么需要修复plt表呢?是因为decrypt中使用了android_log这个系统函数(在正常解密函数中无法避免使用系统函数)
到此,只需要将libadd.so中decrypt函数替换成真正的解密代码就行了。甚至是简单异或都可以实现so加密。