什么是加壳?#
本章演示了对 upx 加壳程序进行脱壳。
加壳是指通过一种压缩或者加密的手段将程序的可执行代码隐藏起来,避免被轻易的逆向。加壳会在程序中加入额外的区段(STUB,存根),在程序开始运行后,将加密的文件进行解密并保存到内存中其他区段,或者创建原程序中的区段,然后跳转到解密后的代码执行。
大部分通过破坏 IAT(import table)也就是导入函数表,以及文件头(HEADER)来保护文件。它们会加入反调试代码来避免被脱壳出原始文件。
通过 die 查看是否加壳。
上图可以知道是 upx 3.91 版本加壳,程序是 32 位,i386 架构。
加载加壳文件#
加载加壳文件时,取消勾选创建输入段
,勾选手动加载
。
点击 ok 后,弹出如下窗口,点击确定。
加壳后程序的入口
原始程序入口
加壳后程序的入口,地址是 0x409BE0,而原始文件的地址是 0x401000。
文件和内存占用#
对比这 2 个文件的区段,在加壳文件的文件头下面有个 upx0 的区段,占用的内存比原始文件中的其他区段要大。
原始文件
加壳文件
加壳文件的 upx0 区块结束于0x409000
,而在原始文件 header 以下区段从0x401000
到0x408200
,当一个程序执行时,它在硬盘上可能只占用 1k,但在内存中可能占用 20k 或者更大。
如上图,在原始文件 CODE 区段的起始地址是0x401000
,区段文件的大小 (Section size in file) 为 0x600 字节,而内存大小 (Virtual size) 占用 0x1000 字节。
转到加壳文件中,如上图,upx0 区段的起点是0x401000
,upx0 区段再硬盘的大小为 0,而内存占用却是 0x8000 字节,程序在这里占用了足够大的空间来保存原始的程序代码,然后跳转过来执行。
加壳文件 0x401000 处的跳转
0x401000 前置的 dword_表示数据类型为 DWORD,"?" 号表示只占用了内存位置而未保存任何内容,dup 表示 0xc00 个 dword,也就是 0x3000 字节。0x404000 同样也占用了 0x1400 字节。
那么总共就是 0x8000 个字节用于存储原始代码的内容。
如下图,在 0x401000 处,按 x 键,可以看到此处有两个引用(稍后回来看这部分内容)。
可执行代码的引用
upx1 区段文件占用是 0xe00,内存占用是 0x1000。
upx1 区段的文件及内存占用
可能程序使用了一些简单的加密隐藏了原始代码,对于这个区段的起点 0x409000 有几处引用。
0x409000 的引用
有一处引用来自(下方,down)可执行部分,点击跳转到该处。
程序入口
stub 和 oep#
上图程序入口之后的 stub 中,ESI 寄存器传入 0x409000 这个地址,如下图所示,可执行代码在原始文件已加壳代码的下方,同属于 upx1 区块,所以在 upx1 区块中,存在原始文件加密后保存的内容以及 0x409be0 之后的 stub 代码。
跟踪到的可执行代码
下图中,可以发现,程序从 0x409000 处开始读取内容,通过某种运算之后再保存到0x401000
(EDI=ESI-0x8000
)。程序读取 ESI 指向的内容作为来源,然后执行操作后,再存储到 EDI 指向的内容,恢复成原始代码。
回到 upx0 区段,在 upx0 区段处,有一处引用
0x401000 处的引用
下图中有一处无条件跳转到 0x401000 处,也就是上图中的 0x401000 处的引用。
jmp near
是直接向之后的地址数跳转的指令,那么这里执行完 stub 并且生成原始代码后,程序会跳转到 0x401000 处 (OEP,original entry point),也就是原始程序的入口(程序最开始执行的地方),相应的存根入口 (stub entry point) 是 0x409be0。
之后原始程序入口直接叫做ORIGINAL ENTRY POINT
或OEP
。如果是一个加壳之后的程序,无法知道它的具体位置,而在此程序中,程序实有 OEP 的,OEP 就是 0x401000。
寻找 OEP#
大部分情况下,是无法获取原程序的,所以无法直接获得加壳程序的 OEP 地址。那么,接下来介绍如何寻找 OEP。
当 STUB 完成解密操作并且生成原始代码之后,会跳转执行程序。一般来说,在写入原始代码的区块执行的第一条指令就是 OEP。
在进入 OEP 之前设置断点,查看执行到这里之前是否已经生成了原始程序。
在跳转处设置断点
选择 local windows debugger 调试,然后开始调试,运行到断点处。
运行到断点处
程序运行到断点处跳转,按 F8 单步运行。
弹出警告信息 upx0 区段原先被解析为数据,点击是,将它解析为代码。
运行到 OEP
现在程序已经解密出原始代码并跳转执行,这里的代码和原始程序中 0x401000 处的代码非常类似。但是由于没有定义为函数 (loc_401000),所以无法切换到图形视图。不过这也可以自动实现。
在 IDA 界面左下角有一个隐藏菜单,右键选择 “重新分析程序”,现在回到 0x401000 处,显示 sub_401000 表示这是一个函数,按空格则可以切换回图形视图。
重新分析后的代码
sub_401000
通过执行断点寻找 OEP#
另一种寻找 OEP 的方法,找到第一个区段第一条执行的指令,这种方法有时候会成功。
在调试器中启动加壳后的程序并且在入口处暂停。
Stub entry point
转到一个区段的起点,也就是 0x401000
按 F2 设置断点,并且设置执行时中断,而不是读取或写入的时候,否则会在解密时读取密码或者写入原始代码时中断,由于不知道具体的位置,作者设置了覆盖整个区段的断点(0x8000 字节)。
设置完成后所有的指令都变红了。
对整个区段设置断点
然后打开菜单栏-调试器-断点-断点列表
,禁用其他断点。
然后运行程序。
触发断点
当这种方法奏效时,发现 OEP 就是 0x401000,禁用这个断点。
OEP
重新分析之后,0x401000 识别为函数。
目前为止,介绍了 2 种寻找 OEP 的方法,创建解密代码的内存快照。接下来要做的就是 DUMP(转存)以及重建 IAT 来获得没有加壳并且能够运行的程序。