脱壳技术

脱壳技术

壳是最早出现的一种专用加密软件技术,分为压缩壳和加密壳

压缩壳

对文件进行压缩,减小软件的体积。如UPX、ASPack和PECompact

加密壳

比如Armadillo、EXECryptor、Themida、WinLicense

VMProtect

将指定的代码进行变形和虚拟化处理后,很好的隐藏代码算法,防止算法被逆向。

壳的加载过程

  1. 加壳程序在初始化时保存个寄存器值,待执行完毕后恢复寄存器的内容
  2. 获取壳本身需要使用的API地址
  3. 解密原程序各个区块的数据
  4. IAT的初始化
  5. 重定位项的处理
  6. Hook API
  7. 跳转到原始入口点OEP

PE脱壳

这里对手动脱壳技术进行简单的总结

寻找OEP

寻找程序真正的程序入口点“OEP”

根据跨段指令寻找OEP

绝大多数PE加壳程序在被加密的程序中加上一个或多个区块,当外壳代码处理完毕后就会跳到程序本身的代码处。所以根据跨段的转移指令就可以找到真正的程序入口点。

首先使用LordPE打开加壳前的实例,获取其入口点RVA 1130h,查看区块如图

加壳后,程序的入口点为0x13000h,查看区块如下

可以对比两个区块,发现加壳后多了一个**.pediy**区块,这个区块就是外壳,此时的入口地址就是外壳的地址。当外壳开始加载后,会通过各种方式加载所需的API地址,解密程序各区块中的数据,填充IAT,随后跳到OEP(0x401130h)上执行。

使用OD打开加壳后的程序,加壳程序入口点代码如下

进入第二部分,主要工作是还原各区块数据

继续跟踪,到程序准备返回OEP,如下

1
2
3
4
5
6
00020270        | 8B85 89020000    | mov eax,dword ptr ss:[ebp+2 | ;取出原来的入口RVA 1130H
00020276 | 0385 51030000 | add eax,dword ptr ss:[ebp+3 | ;加上映像基地址,此处为401130h
0002027C | 0185 84020000 | add dword ptr ss:[ebp+284],eax | ;将401130放到
00020282 | 61 | popad |
00020283 | 68 00000000 | push 0 | ;0为OEP
00020288 | C3 | ret |

至此外壳代码处理完成,用跨段的转移指令跳转到真正的入口点执行解压后的程序,转移指令如下

1
2
00020283        | 68 00000000      | push 0                      |  ;0为OEP
00020288 | C3 | ret |

下面为运行时的指令,跳转到401130h

进入0x401130h,可以看到如下代码

此时位于.text

上述过程中外壳使用了两次跨段转移指令,第一次从.pediy去跳转到外壳的第2部分,第二次是从第2部分跳转到程序本身代码所处的区块,在本例中是.text区块。这就是判断该处为OEP的关键。就是一步一步进行跟踪,直到到达代码段本身。

用内存访问断点寻找OEP

由于前面通过跨段指令寻找EOP,需要手动跟踪到达代码段。OD中可以直接对代码段设置断点,当对代码段设置内存访问断点时,一定会中断在外壳对代码进行读取的那句指令上。

在内存布局中,对代码段设置内存访问断点

执行程序,使之中断在如下代码处,则OEP为0x401130

这个方法的关键是待代码段解压完毕后对代码段设置内存访问断点。

根据栈平衡原理寻找OEP

由于加壳程序在初始化时要先保存各寄存器的值,带外壳执行完毕后恢复现场。所以必须遵循栈平衡原理,如下

1
2
3
4
5
PUSHAD   ;PUSHAD相当于push eax/ebx/ecx/edx/esp/ebp/esi/edi
...... ;外壳代码
POPAD ;POPAD相当于pop edi/esi/ebp/esp/edx/ecx/ebx/eax
JMP OEP :准备跳转到OEP
...... :解压后程序的源代码

根据栈平衡对esp设段,很快找到OEP。首先打开该程序,如下进入外壳时使用pushad指令,再进入外壳代码

程序运行之前,寄存器和堆栈数据如下

执行PUSHAD指令后,各寄存器的值将被压入0019FF54-0019FFCC的栈中

此时esp指向0x0019ff54,对这个地址设置硬件访问断点,如下图

上述指令调用POPAD来恢复现场,在push <rebpe.sub_401130>这段指令时停下来,发现此时的寄存器值都恢复如上图的值,如下

当外壳程序执行完成后,遵守栈平衡原理,当其跳转到OEP时,esp的值不会改变,代码如下

1
2
3
00401130 <rebpe | push ebp                          |
00401131 | mov ebp,esp |
00401133 | push FFFFFFFF |

入口点第一句指令是”push ebp”,可以对其进行设置硬件写入断点,可方便到达OEP附近。

根据编译语言特点寻找OEP

各类语言编译的文件入口点都有自己的特点。例如Visual c++6.0程序来说,默认入口代码有4个版本:WinMainCRTStartup、mainCRTStartup、启动部分有GetCommandLineA(W)、GetVersion、GetStartupInfoA(W)等函数,可以通过这些函数设置断点,来定位程序的OEP。

下面对GetVersion函数设置断点,在命令行窗口使用BP GetVersion设置函数断点

逐步跟踪到GetVersion()下面位置

继续跟踪就来到了OEP附近,如下图所示

如果对常见语言的入口代码比较熟悉,就很容易地完成脱壳修复或定位OEP等工作。

抓取内存映像

把内存指定地址的映像文件读出,用文件等形式将其保存下来的过程。一般情况下,当外壳来到OEP时进行Dump是正确的。还有在外壳将压缩的全部代码释放到内存中并初始化一些项目时也可以选择合适的位置进行Dump。

常见的Dump工具有LordPE、PETools等。这类工具一般利用Moudule32Next来获取欲Dump进行的基本信息。这里使用LordPE进行Dump。

首先勾选完全Dump

然后运行程序选中,抓取文件并保存

这里修改modBaseSize的值,对SizeOfimage进行纠正实现Dump

Dump成功

重建输入表

破坏源程序的输入表是加密外壳必备的功能。所以脱壳时需要重建IAT,获取未加密的IAT,跟踪加壳程序对IAT的处理过程,修改相关指令,不让外壳指令加密IAT。

确定IAT地址和大小

输入表重建的关键就是在于IAT的获取。一般程序的IAT是连续排列的,以一个DWORD字的0作为结束。只要确定IAT的一个点就能获取整个IAT的地址和大小。

寻找GetVersion函数

可以在数据窗口中看到很多函数地址

这里的GetVersionExA在0x77431A00

GetVersion在下面的地址中

IAT是一个连续排列的数据,向上翻屏直到出现”00”找到IAT的起始地址。如上图4050B8H就是IAT的结尾,也就是说起始位置在405000h,大小为B8h

切换为窗口显示模式,可以清晰看到函数地址

重建输入表

根据IAT重建输入表

可以使用ImportREC等专业的输入表重建工具完成这项工作。这里使用实例Reb_IT.exe进行分析,可以看到如下代码

1
2
004052BE                                   | popad                              |
004052BF | jmp reb_it.401000 |

这里使用jmp指令后开始进入OEP,OEP地址为401000。随后在004052BF设置断点,中断后运行LordPE将内存数据Dump出来保存。接下来就是重建IAT的过程,我们先来找IAT起始位置和大小,如下

可以看到上面有两个IAT,因为有两个DLL(kernel32.dll和user32.dll)。这两个IAT以DWORD类型的0隔开。接下来我们在Dump下来的程序中找一块空间用来重建输入表,将DLL名和函数名写入

由于是内存映像文件,文件偏移和相对虚拟地址是相等的。然后构造指向函数名地址的IMAGE_THUNK_DATA数组,如下图,每个数组间隔2字节,用0填充

然后根据上面的数组来构建IID数组,用来指向上面的数组地址,从0x2010h开始,大小是28h,如下

输入表的相对虚拟地址存储在PE文件头的目录表中,为130h。向130h中写入输入表地址为”2010 0000”和”2800 0000”,这里使用LordPE写入

用ImportREC重建输入表

需满足以下几个条件:

  • 目标文件已经被Dump
  • 目标文件必须正在运行
  • 已知目标程序的真正的OPE或IAT的偏移量和大小

首先运行工具,选择rebpe.exe进程

指定正确的OEP,填写”1130”。此时工具在重建输入表时会用此值修正入口点。随后让其自动检测IAT的偏移量和大小,如下图出现了提示信息,表明OEP发挥了作用。否则就手动填写IAT的RVA和大小。

然后点击获取输入表,分析IAT的结构,得到基本信息。如下得到找到的两个输入表函数

接着修复已脱壳的程序Dump.exe。点击修复转存文件,选择之前的Dump.exe文件,设置新的RVA为545ch,然后进行修复。我们可以来运行下脱壳后并修复完成的程序

DLL脱壳

DLL动态链接库脱壳和EXE文件脱壳差不多,只是多了个基址重定位表。因为外壳很有可能破坏了原始的重定位表,重定位数据对于DLL的动态链接库是必须的,所以需要将原始的重定位表换个形式存储。

附加数据

在一些特殊的PE文件中,各个区块的正式数据之后还有一些数据,这些数据不属于任何区块,不能被映射到内存中,这些数据被称为附加数据。

附加数据的起点是一个区块的末尾,终点是文件的末尾,查看实例overlay.exe,如下图所示最后一个区块的文件偏移值为3800h。

使用工具查看目标文件3800h的数据为附加数据

使用PEID查看该程序,显示overlay就表明存在附加数据

该程序使用UPX压缩壳

带有附加数据的文件在脱壳时必须将附加数据粘贴回去,如果文件中有访问附加数据的指针,也要进行修正。

将程序使用OD打开,定位到OEP处,如下所示

原文件如下

dump下来的文件如下

两者区别不能是Dump下来的文件不能将原来的文字显示出来,由于附加数据没有映射到内存里,抓取的映像文件里也没有附加数据。使用CreateFileA设置断点,程序在如下代码处中断

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
00401040                | push ecx                                   |
00401041 | push 0 |
00401043 | call dword ptr ds:[<&GetModuleFileNameA>] |;取自身文件名
00401049 | push 0 |
0040104B | push 80 |
00401050 | push 3 |
00401052 | push 0 |
00401054 | push 1 |
00401056 | push 80000000 |
0040105B | lea edx,dword ptr ss:[ebp-104] |
00401061 | push edx | edx:"D:\\Pen test\\Reverse\\PEDIY_BOOK4_v2\\PEDIY_BOOK4_v2\\chap16\\16.6 附加数据\\原文件\\overlay.exe"
00401062 | call dword ptr ds:[<&CreateFileA>] |;打开自身
00401068 | mov dword ptr ss:[ebp-11C],eax | [ebp-11C]:"Reverse\\PEDIY_BOOK4_v2\\PEDIY_BOOK4_v2\\chap16\\16.6 附加数据\\原文件\\overlay.exe"
0040106E | cmp dword ptr ss:[ebp-11C],FFFFFFFF | [ebp-11C]:"Reverse\\PEDIY_BOOK4_v2\\PEDIY_BOOK4_v2\\chap16\\16.6 附加数据\\原文件\\overlay.exe"
00401075 | jne overlay.40107E |
00401077 | xor eax,eax |
00401079 | jmp overlay.401158 |
0040107E | push 0 |
00401080 | mov eax,dword ptr ss:[ebp-11C] | [ebp-11C]:"Reverse\\PEDIY_BOOK4_v2\\PEDIY_BOOK4_v2\\chap16\\16.6 附加数据\\原文件\\overlay.exe"
00401086 | push eax |
00401087 | call dword ptr ds:[<&GetFileSize>] |
0040108D | mov dword ptr ss:[ebp-118],eax |
00401093 | cmp dword ptr ss:[ebp-118],FFFFFFFF |
0040109A | jne overlay.4010A3 |
0040109C | xor eax,eax |
0040109E | jmp overlay.401158 |
004010A3 | mov ecx,dword ptr ss:[ebp-118] |
004010A9 | push ecx |
004010AA | call overlay.401428 |
004010AF | add esp,4 |
004010B2 | mov dword ptr ss:[ebp-120],eax |
004010B8 | mov edx,dword ptr ss:[ebp-120] |
004010BE | mov dword ptr ss:[ebp-108],edx |
004010C4 | mov eax,dword ptr ss:[ebp-118] |
004010CA | push eax |
004010CB | push 0 |
004010CD | mov ecx,dword ptr ss:[ebp-108] |
004010D3 | push ecx |
004010D4 | call overlay.4013D0 |
004010D9 | add esp,C |
004010DC | push 0 |
004010DE | push 0 |
004010E0 | push 3800 |;注意这个值
004010E5 | mov edx,dword ptr ss:[ebp-11C] | [ebp-11C]:"Reverse\\PEDIY_BOOK4_v2\\PEDIY_BOOK4_v2\\chap16\\16.6 附加数据\\原文件\\overlay.exe"
004010EB | push edx | edx:"D:\\Pen test\\Reverse\\PEDIY_BOOK4_v2\\PEDIY_BOOK4_v2\\chap16\\16.6 附加数据\\原文件\\overlay.exe"
004010EC | call dword ptr ds:[<&SetFilePointer>] |;移动读写指针
004010F2 | push 0 |
004010F4 | lea eax,dword ptr ss:[ebp-114] |
004010FA | push eax |
004010FB | push 100 |
00401100 | mov ecx,dword ptr ss:[ebp-108] |
00401106 | push ecx |
00401107 | mov edx,dword ptr ss:[ebp-11C] | [ebp-11C]:"Reverse\\PEDIY_BOOK4_v2\\PEDIY_BOOK4_v2\\chap16\\16.6 附加数据\\原文件\\overlay.exe"
0040110D | push edx | edx:"D:\\Pen test\\Reverse\\PEDIY_BOOK4_v2\\PEDIY_BOOK4_v2\\chap16\\16.6 附加数据\\原文件\\overlay.exe"
0040110E | call dword ptr ds:[<&ReadFile>] |
00401114 | mov dword ptr ss:[ebp-110],eax |
0040111A | mov eax,dword ptr ss:[ebp-11C] | [ebp-11C]:"Reverse\\PEDIY_BOOK4_v2\\PEDIY_BOOK4_v2\\chap16\\16.6 附加数据\\原文件\\overlay.exe"
00401120 | push eax |
00401121 | call dword ptr ds:[<&CloseHandle>] |
00401127 | mov ecx,dword ptr ss:[ebp-108] |
0040112D | push ecx |
0040112E | mov edx,dword ptr ss:[ebp-10C] |
00401134 | push edx | edx:"D:\\Pen test\\Reverse\\PEDIY_BOOK4_v2\\PEDIY_BOOK4_v2\\chap16\\16.6 附加数据\\原文件\\overlay.exe"
00401135 | call dword ptr ds:[<&SetWindowTextA>] |;将附加数据显示到文本框中

当CreateFileA打开一个文件后,文件指针默认指向文件的第一个字节,使用SetFilePointer函数设置指针,指向附加数据,然后用ReadFile函数将附加数据读出。由于脱壳后文件大小发生变化,追加后的附加数据地址已经改变,需要修正SetFilePointer的参数,使其指向附加数据,代码如下

1
2
3
004010DC                | push 0                                     |
004010DE | push 0 |
004010E0 | push E000 |

对于带有附加数据的程序,在抓取内存映像后,必须将附加数据追加到脱壳文件的最后,同时修正读取附加数据的相应指针。

压缩壳

压缩壳以减小文件体积为目标,因此生成的IAT都是未加密的,使用Import REC可以轻易重建其输入表,例如ASPack、UPX等。

UPX外壳

UPX外壳可以使用UPX自身去除,脱壳命令为

1
UPX -d 文件名

但为了阻止使用UPX自身来脱壳,一些工具例如UPXPR和UPX-Scrambler会对加壳文件进行处理,使得脱壳失败。

UPXPR保护

通过修改一些UPX加壳的标志来实现加壳保护机制。只要修复了这些标志,就能使用UPX自身脱壳。

如下图,直接使用UPX进行脱壳显示脱壳失败,提示文件被保护。

查看其区块信息,第一、二字符不是UPX0和UPX1,而是其他字符。所以第一步是去恢复这两个块名。

直接在LordPE上面修改块名,修改后块表如下

查看UPX外壳字节信息,如下字符串信息中会有一个”UPX!”标志信息。UPXPR会将此标志删除,导致不能解压。

我们可以看到在”UPX!”后面的4字节均是**”0c 09 ? ?”或者”0d 09 ? ?”。找到这些关键字节,就可以定位并且恢复UPX标志。”UPX!”字节表示为“55 50 58 21”**。

在恢复块表和UPX标志后,在”0c 09 ? ?”之后还有24个字节,任何字节被修改都无法正常解压。使用UPXFIX_by_DiKeN工具进行修复。

手动脱壳

以EdrLib.dll加UPX壳,对其进行手动UPX脱壳,首先查看区块信息,UPX0的RAWSIZE为0,应该是解压后的数据映射到此区块中。

程序入口点在0000E640,基址为00400000

定位到程序入口点,找到OEP为0040E640

ASPack外壳

ASPack外壳运行时,有一段时间将程序完全解密,此时内存映像是加壳前的状态,输入表、重定位表都是完整的。所以ASPack脱壳只要适时抓取内存映像,修正PE头的输入表、重定位表的地址即可。

寻找OEP

该DLL的区块表如下,ASPack加壳时没有合并区块,.aspack和.adata是外壳的指向程序和数据,脱壳后可以去除。

在外壳代码的第一行中断,代码如下

1
2
001FD001 >  60              pushad   ;在此下断点
001FD002 E8 03000000 call 001FD00A

在EdrLib.dll运行装载成功后,关闭loaddll.exe界面,程序会再次中断在入口点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
001FD001 >  60              pushad
001FD002 E8 03000000 call 001FD00A ;
001FD007 - E9 EB045D45 jmp 457CD4F7
001FD00C 55 push ebp
001FD00D C3 retn
001FD00E E8 01000000 call 001FD014
001FD013 EB 5D jmp short 001FD072
001FD015 BB EDFFFFFF mov ebx, -13
001FD01A 03DD add ebx, ebp
001FD01C 81EB 00D00000 sub ebx, 0D000
001FD022 83BD 22040000 0>cmp dword ptr [ebp+422], 0
001FD029 899D 22040000 mov dword ptr [ebp+422], ebx ;保存当前基址
001FD02F 0F85 65030000 jnz 001FD39A ;第二次进入入口处就会跳转

由于DLL已被解压,外壳不会再次对DLL文件进行解压缩,所以001FD02F一行将跳过外壳解压代码直接来到OEP处,示例如下

1
2
3
4
5
6
7
8
9
10
11
12
001FD39A    B8 40120000     mov     eax, 1240       ;此处为OEP的RVA
001FD39F 50 push eax
001FD3A0 0385 22040000 add eax, dword ptr [ebp+422]
001FD3A6 59 pop ecx
001FD3A7 0BC9 or ecx, ecx
001FD3A9 8985 A8030000 mov dword ptr [ebp+3A8], eax ;将计算出来的OEP放到001FD3BAh处
001FD3AF 61 popad
001FD3B0 75 08 jnz short 001FD3BA
001FD3B2 B8 01000000 mov eax, 1
001FD3B7 C2 0C00 retn 0C
001FD3BA 68 40121F00 push 001F1240
001FD3BF C3 retn ;跳到OEP处

将EdrLib.dll装载后。基地址不是默认的4000000h,新的基址地址为001FD000H,此时OEP的RVA为1240h。

解压分析

由于外壳会向区块中写入数据,可以对区块地址设置内存断点,.text的区块的RVA为1000h,所以真实地址为001f1000h,对此地址设置内存写断点,监控001f1000h处内存数据的变换,解压代码如下

1
2
3
4
5
6
7
8
9
10
001FD16F    8B3E            mov     edi, dword ptr [esi]
001FD171 03BD 22040000 add edi, dword ptr [ebp+422] ;edi指向区块地址
001FD177 8BB5 52010000 mov esi, dword ptr [ebp+152] ;esi指向已还原的数据
001FD17D C1F9 02 sar ecx, 2 ;ecx是区块数据的大小
001FD180 F3:A5 rep movs dword ptr es:[edi], dword p>
001FD182 8BC8 mov ecx, eax
001FD184 83E1 03 and ecx, 3
001FD187 F3:A4 rep movs byte ptr es:[edi], byte ptr>
001FD189 5E pop esi
001FD18A 68 00800000 push 8000 ;可在此设断点,抓取映像文件

内存中就是完整的源程序,可以使用工具抓取内存中映像保存为dumped.dll。

输入表

只需找到输入表的地址即可。根据API函数调用,确定IAT的RVA是7000h-70E4h。因为在dumped.dll文件里输入表是完整存在的,以KERNEL32.dll为突破口,反推IID结构的地址。

KERNEL32.dll地址为000077D0h,该地址是IID结构中的Name的值,而name值的形式为去查看在内存中的地址为000076A0h

由于每个IID数组之间有DWORD隔开,所以第一个IID数组的地址为00007694h,存放的IID结构中的name值地址为000076E4,如下

所以第一个IID数组地址就是输入表的地址,为7694h。

基址重定位

ASPack没有破坏重定位表,只需确定重定位表的大小和地址即可。使用十六进制工具查看映像文件,确定重定位表的地址和起始RVA,重定位表一般以”00100000”开始,显示的字符栏中是可见的ASCLL字符。可以判断出首地址为0000c000,大小为000005c0。

PE文件修正

  1. LordPE修正OEP
  2. 修正输入表,填写RVA的地址和大小
  3. 修正基质重定位,填写重定位表的地址和大小
  4. 删除无用区块

加密壳

以加密保护为主要目的,使用各种反跟踪技术,保护OEP隐藏和IAT加密。

ASProtect是一款经典的加密壳,下面介绍下其保护技术的一些要点。

Emulate standard system functions

将API入口处的一段代码抽出来并放到外壳里执行,从中调用系统API。这样,对API入口地址设置断点的方法将会失效。如下加壳前的API调用语句如下

加壳后的API调用语句如下

在调试采用这类方法保护的程序时,将断点设到API函数结尾返回处。

stolen bytes

外壳将程序的部分代码变形并搬到外壳段中。

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2024 John Doe
  • 访问人数: | 浏览次数:

让我给大家分享喜悦吧!

微信