note20200903

专用加密软件

压缩壳

  1. UPX
  2. ASPack

加密壳

  1. ASProtect
  2. Armadillo
  3. EXECryptor
  4. Themida

虚拟机保护软件

书中介绍的虚拟机与P-CODE类似,将一系列指令解释成字节码后放在一个解释引擎中执行,从而实现对软件的保护。

概念

一个虚拟机引擎主要由编译器、解释器、虚拟CPU环境组成,并且搭配一个或多个指令系统。虚拟机在运行时,先根据自定义的指令把已知的x86指令解释成字节码并放在PE文件中,然后将原始代码删除,改成类似这样的代码:

1
2
push bytecode
jmp VstartVM

然后进入虚拟机执行循环。

比较典型的有较好保护强度的是VMProtect,使用起来也极为方便,针对要保护的函数,设置保护,然后点击编译即可:

脱壳技术

基础知识

壳的加载过程

  1. 保存入口参数

加壳程序初始化会保存各寄存器的值,待外壳执行完毕后回复各寄存器的内容,最后跳到原程序执行。

通常会使用如下一些指令进行保存和恢复寄存器内容:

1
2
pushad/popad
pushfd/popfd
  1. 获取壳本身需要使用的API地址

一般情况下,外壳的输入表只有GetProcAddressGetModuleHandleLoadLibrary三个api函数,从而调用其他api函数。

  1. 解密原程序各个区块的数据

加壳时加密各个区块,所以脱壳时就会解密各个区块数据。

  1. IAT初始化

加壳时构造了一个自建输入表,并让PE文件头数据目录表中的输入表指针指向自建的输入表,PE装载器会对自建的输入表进行填写。程序原有的输入表被外壳变形后存储,IAT的填写会由外壳程序实现。

  1. 处理重定位项

EXE文件在x86 32位系统中一般使用的基址位0x400000,此时就不需要进行重定位,因为系统给的基址也是0x400000,加壳软件此时就会删除PE文件中保持重定位信息的区块(.reloc)。

但是对于DLL动态链接库文件,加壳的DLL就会比加壳的EXE在修正时多一个重定位表格。

  1. Hook API

外壳为了能够填充输入表,可以填充Hook API代码的地址,从而间接得到程序的控制权。

  1. 跳转到OEP

此时壳会将控制权还给原程序。一般的加壳工具会使用”Stolen Bytes”技术,即是将OEP代码段搬到外壳的空间再将代码删除,这样就会提高脱壳难度。

手动脱壳

手动脱壳过程一般分为三步:寻找OEP;抓取内存映像文件;重建PE文件。

寻找OEP

一般的非加密壳在执行外壳程序以后,会将原程序解压,还原,并把控制权还给解压后的真正程序,再跳转到原来的程序入口,此时就会有一条明显的”分界线”,解压后的真正的程序入口点称为”OEP”。

根据跨段指令寻找OEP

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

例子加壳前后的入口点RVA对比:

加壳前的入口点RVA=0x1130:

加壳后的入口点RVA=0x13000:

加壳后程序还多了一个”.pediy”区段,这个就是外壳程序:

使用OD单步步入到分配外壳地址的地方:

1
2
3
4
5
00413108    6A 04           push 0x4
0041310A 68 00100000 push 0x1000
0041310F FFB5 8F000000 push dword ptr ss:[ebp+0x8F]
00413115 6A 00 push 0x0
00413117 FF95 B8000000 call dword ptr ss:[ebp+0xB8] ; kernel32.VirtualAlloc

此时分配的即是外壳第二部分需要的内存空间。

调用Aplib函数:

1
2
3
0041312C    50              push eax
0041312D 53 push ebx ; RebPE_-_.0041944E
0041312E E8 04000000 call RebPE_-_.00413137

跳转到外壳第二部分:

1
2
3
00413133    5A              pop edx                                                        ; RebPE_-_.<ModuleEntryPoint>
00413134 55 push ebp ; RebPE_-_.<ModuleEntryPoint>
00413135 - FFE2 jmp edx

外壳的第二部分用于还原各区块数据,初始化原程序,如填充IAT等。

进一步走可以走到解压代码的部分。

申请内存:

1
2
3
4
5
6
7
8
9
001C00A0    BB A1020000     mov ebx,0x2A1
001C00A5 833C2B 00 cmp dword ptr ds:[ebx+ebp],0x0
001C00A9 74 47 je short 001C00F2
001C00AB 53 push ebx
001C00AC 6A 04 push 0x4
001C00AE 68 00100000 push 0x1000
001C00B3 FF342B push dword ptr ds:[ebx+ebp]
001C00B6 6A 00 push 0x0
001C00B8 FF95 4D030000 call dword ptr ss:[ebp+0x34D] ; kernel32.VirtualAlloc

取得需要解压区块的RVA:

1
2
3
001C00C1    8BC3            mov eax,ebx
001C00C3 03C5 add eax,ebp
001C00C5 8B78 04 mov edi,dword ptr ds:[eax+0x4]

解压数据并将解压的数据写回:

1
2
3
4
5
6
001C00CE    56              push esi
001C00CF 57 push edi ; RebPE_-_.00401000
001C00D0 FF95 55030000 call dword ptr ss:[ebp+0x355] ; RebPE_-_.00413137
001C00D6 8B0C2B mov ecx,dword ptr ds:[ebx+ebp]
001C00D9 56 push esi
001C00DA F3:A4 rep movs byte ptr es:[edi],byte ptr ds:[esi]

离开解压数据的循环就是填充IAT的部分。

对转存后的输入表进行初始化:

1
2
3
4
001C0181    8B95 91020000   mov edx,dword ptr ss:[ebp+0x291]
001C0187 03D5 add edx,ebp
001C0189 8B3A mov edi,dword ptr ds:[edx]
001C018B 0BFF or edi,edi ; RebPE_-_.00412A70

修正重定位数据:

1
2
3
001C01F6    8BB5 99020000   mov esi,dword ptr ss:[ebp+0x299]
001C01FC 0BF6 or esi,esi ; kernel32.76820000
001C01FE 74 33 je short 001C0233

走到popad的地方,此时就将保存的寄存器状态恢复 ,进一步走就是OEP:

1
2
3
001C0282    61              popad
001C0283 68 30114000 push 0x401130
001C0288 C3 retn

OEP:

1
2
3
00401130  /.  55            push ebp                                                       ;  SFX 代码真正入口点
00401131 |. 8BEC mov ebp,esp
00401133 |. 6A FF push -0x1

使用内存访问断点寻找OEP

在主进程模块的.text段设置内存访问断点:

1
2
3
4
5
6
7
8
9
Memory map, 条目 12
地址=00401000
大小=00004000 (16384.)
属主=RebPE_-_ 00400000
区段=.text
包含=代码
类型=Imag 01001002
访问=R
初始访问=RWE

f9后程序中断处:

1
00413145    A4              movs byte ptr es:[edi],byte ptr ds:[esi]

这里是程序使用的aPLib解压函数aP_depack_asm。将内存断点删除,走出该函数,就达到了外壳代码处:

1
2
3
4
001C00D6    8B0C2B          mov ecx,dword ptr ds:[ebx+ebp]
001C00D9 56 push esi
001C00DA F3:A4 rep movs byte ptr es:[edi],byte ptr ds:[esi]
001C00DC 5E pop esi

这里是不断地将各个区段进行解压。

解压完成在对.text段设置内存访问断点,f9走就可以到达OEP:

1
2
3
4
5
00401130  /.  55            push ebp                                                       ;  SFX 代码真正入口点
00401131 |. 8BEC mov ebp,esp
00401133 |. 6A FF push -0x1
00401135 |. 68 B8504000 push RebPE_-_.004050B8
0040113A |. 68 FC1D4000 push RebPE_-_.00401DFC ; SE 处理程序安装

根据栈平衡寻找OEP(ESP定律)

原理就是pushad/popadpushfd/popfd一定会成对出现。这样对push时的某个寄存器的值下硬件访问断点,当恢复寄存器时一定会访问这个地址,然后就会触断。

pushad后,对0x0019FF58设置硬件访问断点,该地址保存的是esi寄存器的值,那么popad时一定会访问这个地址。

触断后即到popad

进一步就是OEP:

根据编译语言特点寻找OEP

不同的编译器编译出的特点都不相同,书中样例在初始化时会调用GetVersion函数,对其下断点寻找OEP即可。

Dump映像文件

一般使用LordPE或者OD自带的ODDump即可。

重建输入表

手动重构IAT(书中样例)

  1. 使用esp定律定位到OEP:

  1. 确定IAT表格:

  1. 填充IMAGE_IMPORT_BY_NAME结构:

  1. 填充IMAGE_THUNK_DATA数组:

  1. 构建IID结构:

  1. 修改输入表:

  1. 结果(导入表成功):

Ollydump直接导出

达到OEP后直接使用Ollydbg的插件OllyDump导出:

即可。

使用ImportRCE重建输入表(使用书中的RebPE为例)

  1. 选中调试中的rebpe程序:

  1. 修正OEP并且寻找IAT的偏移:

  1. get import获取IAT基本信息:

  1. Fix Dump选取PE Lord dump出的文件,导出的正常文件为源文件名称后加”_”:

输入表此时会位于”.mackt”段中:

也可以将输入表放在程序中的冗余段中,更改New Import Infos中的RVA即可。

DLL文件脱壳

正常脱壳(DLL载入时寻找OEP)

  1. pushad后下硬件断点:

  1. f9后走一段达到OEP:

DLL退出时寻找OEP

  1. popad的地方下断点,DLL装载成功后将loaddll.exe关闭,cpu会再次断在popad处:

  1. 跑到判断DLL状态(装载/退出)的代码:

  1. 走一段到外壳的第二段:

  1. 再步过一段到OEP:

Dump映像文件

由于在载入DLL文件的过程中使用了重定位,所以直接使用LordPE dump的文件的基地址是重定位之后的地址,所以需要在dump时将系统对DLL的重定位操作跳过。

这里的寻找重定位思路如下:

  1. 定位到OEP后在OEP以后的代码中寻找使用了重定位之后的代码,这里使用的是位于0x003c1253的代码:

  1. f2重新载入DLL文件,下内存访问断点在.rdata段,这样中断以后.text段时解压完毕的:

  1. 在数据区对0x003c1253后面的40字节设置内存写入断点,这样当该基地址要被修正为3c时就会中断:

  1. 这时cpu中断的地方就是重定位的代码:

  1. 越过该代码(NOP掉),再到达OEP就可以看到下面原先重定位了的代码已经恢复了:

  1. 然后使用LordPE进行dump即可。

修正IAT

使用Import REC进行修正,这里需要手动填写IAT的RVA以及大小:

Fix Dump使用刚才LordPE dump的文件即可。

构造重定位表

这里可以直接使用书中给的ReloREC.exe程序添加重定位表,这里实际测试以后会出现错误: