前置知识

壳的类型

1
2
压缩壳:压缩的目的是减少程序体积,如 ASPack、UPX、PECompact 等;
加密壳:加密是为了防止程序被反编译(反汇编)、跟踪和调试,如 ASProtect、Armadillo、 EXECryptor、Themida、VMProtect。

加壳代码执行

1
2
3
4
5
1、保存入口参数
加壳程序在初始化时会保存个寄存器的值,待外壳执行完毕后,再恢复各寄存器的内容,最后跳到原程序执行。通常用 pushad/popad、pushfd/popfd 指令对来保存与恢复现场环境。(pushad 用来保存所有通用寄存器的状态,而 pushfd 仅用来保存标志寄存器的状态,一般在 32 位中使用)

2、获取壳本身需要使用的API地址
在一般情况下,外壳的输入表中只有 GetProAddress、GetModuleHandle和 LoadLibrary 这3个API函数。甚至只有 Kernel32.dll 及 GetProcAddress。如果需要使用其他 API 函数,可以通过函数LoadLibraryA(W) 或 LoadLibraryExA(W) 将 DLL 文件映射调用进程的地址空间,函数返回的HINSTANCE 值用于表示文件映像所映射的虚拟内存地址。

壳代码的基本流程

1
2
3
4
5
6
7
(1)保存寄存器环境 pushad pushfd
(2)获取 API 函数的地址,加载一些必要的 API
(3)解密代码和数据
(4)填充 IAT
(5)修复重定位
(6)恢复寄存器环境 popad popfd
(7)可能会进行大跳转到 OEP

断点类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1、INT3断点(软件断点):
利用 F2 设置的断点就是 INT3 断点,打INT3断点的时候,断点处就会被CCh(INT3指令的机器码)替换。以OD为例当我们在某一汇编指令处设下INT3断点后,调试器会把所设断点地址处的第一个字节改为0xCC(即INT3指令),并把原字节保存。之所以我们看起来OD的此地址处字节没有发生任何变化是因为OD为了维持汇编代码的可读性并没有将改变后的指令进行重新反汇编。

- 优点:数量没有限制,操作简单。
- 缺点:因为改变机器码所以易被检测,只能在代码段中使用,而且因为其是基于中断的所以当中断描述符表被破坏时其将无效

2、硬件断点
使用4个调试寄存器 DR0,DR1,DR2,DR3,设置地址,使用一个寄存器DR7设定状态,最多设置4个硬件断点,通过寄存器来记录需要中断的位置,可以分为:访问中断、写入中断、执行中断。

- 优点:速度比较快,在INT3断点容易被发现的地方使用硬件断点
— 缺点:数量有限

3、内存断点
内存断点实际上是改变了一个内存区域或一个内存页的权限。内存断点的实现方式是将你欲下断地址所在的内存页增加一个名为PAGE_NOACCESS的属性,这个属性会把当前内存页设为禁止任何形式的访问,如果进行访问会触发一个内存访问异常。

补充:
1.内存断点很消耗资源,因为PAGE_NOACCESS属性一设置就是一整个内存页无法访问,那么当程序访问该内存页中非断点地址的内容同样会触发异常。
2.虽然内存断点的效率经常很不理想,但是因为仅仅是修改了一个内存属性,所以内存断点可以下数量非常多、单断点范围非常大。这是它的优势。
3.只在写入时断下的内存断点通常是将内存属性设为PAGE_EXECUTE_READ,也就是不可写来实现的。对这种属性的内存进行写操作将会触发异常。

正片开始

  • 单步跟踪法
  • ESP 定律
  • 内存访问断点
1
2
3
4
5
6
7
8
9
1、单步跟踪法
主要用到 F8 与 F4

2、ESP定律
ESP定律简单来说就是堆栈平衡,有进栈就要有出栈


3、内存访问断点
壳做的事就是将加密或压缩的代码复原也就是对 .text 段的写入,如果我们对.text 段下了内存断点,那么中断的时候 .text 代码可能还没有完全写入,就需要不停地按 F9,另一种方法是再 .rsrc 段下断点,当发生中断时说明 .text 代码已经恢复完成了,这时候再在 .text 上下断点,当可执行完成跳转到OEP时就会停下来。