一 前言
加固厂商SO的加壳,一般都是采用UPX壳实现的。如果用原生的进行加壳,可以使用原生UPX进行脱壳。但是对于定制的UPX壳使用原生UPX是无法脱壳的。定制版的UPX一般有以下2种形式:
1 将里面的魔术字符串改掉。下图是UPX源码中定义的魔术字以及修改后的例子:
源码中的魔术字 修改后的例子
2 加密算法改掉或数据结构进行更改。
这里讨论是如何手脱一个定制的UPX壳,尤其是更改加密算法或者数据结构的定制的UPX壳(以AJM为例)。先看一下脱壳后的样子:
脱壳前 脱壳后
二 UPX壳loader
加壳一般需要加密和解密2个过程,UPX壳也不例外。UPX壳的解密是由 upx loader实现的。在UPX加壳过程中,将loader放在init段,确保loader先获得程序执行的控制权。
Loader程序是由汇编代码实现的,下面是upx源码中针对arm平台的汇编代码:
在 dynamic段中,init标志值为:0x0C,见下图:
下面是loader在dynamic中的位置:
当loader开始运行时的步骤如下:
从loader执行流程可以看到如下:
1、loader是由汇编代码内嵌实现的,因此要解决重定位的问题,所以其中的函数调用都是使用系统调用实现的,比如mmap 以及mprotect等函数。
2、loader 程序要将加密数据解密,解密后的数据要覆盖加密数据。
3、由于upx使用的是压缩算法,因此解密后的数据会比加密的数据大,此时loader程序也会被解密后的数据覆盖,所以loader在解密前要拷贝到内存中执行。
三 linker加载so
Android系统的linker加载是通过dlopen函数实现的。以4.4源码为例如下:
四 手脱UPX壳的原理
前面说加壳一般需要加密和解密2个过程,UPX壳也一样。我们手脱指的是不用研究加密算法,动态脱壳。当upx的loader执行完后,代码也解密完成了,并且加密的数据被解密后的数据覆盖,是否可以直接在这个时间点,将SO从内存中dump出来就可以了呢?
确实可以在这个时间点将so从内存中dump出来,但是只有这样是不行的,需要解决以下4个问题:
● 由于此时代码已经重定位完成,dump后plt段等信息已经包含了重定位后的信息,也就是绝对地址,此时静态用IDA打开,很多函数都无法识别。
● Segment段中的Load段需要恢复,UPX加密后将原始的load段的大小由原来的加密前的大小改成了加密后的大小,因此需要修复。
● Segment段中的 dynamic段在文件中的偏移需要修复。UPX将dynamic在文件中的偏移改变了。
● section 段修复。对于section段的修复论坛里面有很多,这里就不讲了。
因此解决了前面三个问题就可以实现手脱UPX壳了。
1 解决重定位的问题
我们的目标是手脱UPX壳,因此我们可以将linker中执行重定位代码nop掉,就是不让linker执行重定位的操作,自然PLT等段中就不会带有重定位的信息。
通过前面分析linker加载so的过程,soinfo_link_image调用soinfo_relocate实现重定位,下图是源码中相关调用:
下面是在linker中的对应的汇编代码( 对于如何找到对应汇编代码,只要在IDA中搜索字符串“[ relocating %s plt ]”就可以找到):
直接将上面的对sub_1464函数改成对应的”c0 46 c0 46”(thumb nop指令) 就可以。我们知道如果SO本身又调用了其他的SO,此时会优先加载其他的SO,因此注意nop的时机,避免将不是UPX壳的其它SO重定位也执行不了。
2 如何恢复segment段的load的大小
我们知道upx壳的loader会对加密的数据进行解密,解密后的数据大小,就是对应load段的大小。
下面是UPX壳部分数据排列顺序:
下面是对应SO文件init入口数据:
l_info p_info b_info结构大小都为12个字节,如下:
根据上面的loader地址我们知道第一个l_info结构的地址为0x6508。见下图:
我们先不关心l_info和p_info结构,我们只关心b_info结构。对于b_info结构,第一个成员是压缩前的大小,第二个成员是压缩后的大小。因此我们可以通过b_info获取到被压缩数据的压缩前的大小:
从上图可知,第一个b_info结构对应的压缩块:
● 压缩的前大小为:0x154
● 压缩后大小为0xA2
这个块是对应ehdr和phdr的,我们跳过这个块,到达第一个load段对应的b_info结构。其地址为 :0x652C+0xA2 = 0x65CE,见下图:
从上图中对应的b_info结构,我们知道第一个load段压缩前大小为:0XDDC58。
3 内存dump的时机
我们必须在UPX解密后,并执行原始SO的init前进行dump。因此我们就在linker执行完第一个init函数,也就是upx的loader函数后开始dump。因此断点的位置下图中的0x274A位置:
4 修复load段
将上图中第一个load段的0x8B290改为我们之前获得0Xddc58。同时将第二个段的文件偏移和虚拟地址偏移改成一样,都为E5C60。
5 修复dynamic段
对于UPX壳,原始的dymic段的文件位置被修改了,但是对应加载后的内存的位置并没有修改,因此我们直接将其在文件中的偏移改成与内存偏移一致就OK。如下图:
将0x8E888改成0xE6888。
四 总结
1 将linker中的 soinfo_link_image函数中对soinfo_relocate调用 NOP(避免重定位造成内存dump后IDA分析问题)
2 在upx init(loader)入口处找到第一个l_info地址,从而获取到第一个b_info结构地址;
3 跳过第一个b_info对应的压缩数据块,到下一个压缩段的b_info;
4 从b_info结构中获得压缩前的大小;
5 执行完upx的解压后,从内存中dump UPX壳;
6 将第一个load段的文件大小 虚拟地址大小,物理地址大小都改成压缩前的大小;
7 将第二个段的文件大小,虚拟地址大小,物理地址大小都改成相同;
8 将dysmic修正(将文件偏移改成与虚拟地址偏移一致);
9 将sht清0(后者在文件结尾找个位置,同时将sht_size清0)。(也可以根据dynamic修复)。
转自:https://bbs.pediy.com/thread-221997.htm
来源:oschina
链接:https://my.oschina.net/u/4294583/blog/4405018