详细请见 wiki

之前已经讲过其概念;这里更多的是脱壳的一些技巧,记录练手;

更多术语名词在之前提及过;

单步跟踪法

单步跟踪法的原理就是通过步过 (F8), 步入(F7) 和运行到 (F4) 功能, 完整走过程序的自脱壳过程, 跳过一些循环恢复代码的片段, 并用单步进入确保程序不会略过 OEP. 这样可以在软件自动脱壳模块运行完毕后, 到达 OEP, 并 dump 程序.

要点:

  1. 打开程序按 F8 单步向下, 尽量实现向下的 jmp 跳转;
  2. 会经常遇到大的循环, 这时要多用 F4 来跳过循环;
  3. 如果函数载入时不远处就是一个 call(近 call), 那么我们尽量不要直接跳过, 而是进入这个 call;
  4. 一般跳转幅度大的 jmp 指令, 都极有可能是跳转到了原程序入口点 (OEP);

用题举例:

打开后即是一个加壳文件,并有着 pusha 指令;

直接挂在开始处启动调试;

像图一的这种call就叫近call(基本上这个函数里只有几句话加1个call);

中间图的内容是跟进到找不到近call后可以看到这一系列的call在调用windows api,什么Module,ProcAddress一类的;

再往下走就能进入一个解码循环中,最后的通路在经过一番绕之后发现在 40D15F 这个地址;

debug

继续往下走.. 之后还会有些循环,在这些循环中,向下跳的指令如果没有判断执行,很可能就是这条路,如左图所示;

跳过之后能发现 popa 指令,这与 pusha 相对应,回复其寄存器状态;

跳转到对应函数后,有push指令和retn,意思是将该十六进制内容压入栈中,并利用这个数据,返回到此十六进制地址;这便是此程序的OEP(从D000变到1000,跳转很明显);

find

ESP定律法

ESP 定律的原理在于利用程序中堆栈平衡来快速找到 OEP.

由于在程序自解密或者自解压过程中, 不少壳会先将当前寄存器状态压栈, 如使用pushad, 在解压结束后, 会将之前的寄存器值出栈, 如使用popad. 因此在寄存器出栈时, 往往程序代码被恢复, 此时硬件断点触发. 然后在程序当前位置, 只需要少许单步操作, 就很容易到达正确的 OEP 位置.

简单来讲,在执行 pushad 之后, esp会确定下来,在 popad 执行后,也是此时的esp值,在此时esp栈上打个内存断点,则可检测两次esp同值时的时刻,第二次便是 popad 执行时;

要点:

  1. 程序刚载入开始 pushad/pushfd;
  2. 将全部寄存器压栈后就设对 ESP 寄存器设硬件断点
  3. 运行程序, 触发断点;
  4. 删除硬件断点开始分析;

用题举例:

还是之前的那个程序:

breakpoint

执行后打开此时esp的栈中位置,打上断点,F4执行,会弹出一个硬件断点被捕获的窗口,点击后可以看到来到了上次 popa 执行之后的地方;

这个方法非常好用;

一步到达OEP法

说白了就是搜索 text 为 popad 之类的东西,然后查看其结构是不是壳的转到OEP位置的地方,然后直接在这个地方断点,直接过去;

只能说,能用的壳比较有限;一般 转到OEP处的 jmp 指令 跳转会比较大;

内存dump

找到OEP后,即可dump出脱壳后的程序:

点击 IDA 的 file > script command > 写入脚本并用 IDC 运行;

1
2
3
4
5
6
7
8
9
static main(void)
{
auto fp, begin, end, dexbyte;
fp = fopen("your\\dumped\\file\\path", "wb");
begin = r0; //OEP位置
end = r0 + r1; //r1为大小,一般填90000
for ( dexbyte = begin; dexbyte < end; dexbyte ++ )
fputc(Byte(dexbyte), fp);
}

反调试

wiki上基本上都是说明,实际操作会来的更少,不过能了解一下,也能为后期搞反调试带来些许帮助;

NtGlobalFlag

原理:

在 32 位机器上, NtGlobalFlag字段位于PEB(进程环境块)0x68的偏移处, 64 位机器则是在偏移0xBC位置. 该字段的默认值为 0. 当调试器正在运行时, 该字段会被设置为一个特定的值,一般是0x70;

该字段包含有一系列的标志位. 由调试器创建的进程会设置以下标志位:

1
2
3
FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
FLG_HEAP_ENABLE_FREE_CHECK (0x20)
FLG_HEAP_VALIDATE_PARAMETERS (0x40)

检测其值就能判断是否处于调试中;

PEB结构在汇编中加入的形式是经典的 fs:30h 段寄存器偏移;

这时候在PEB结构上往下偏移并找到 NtGlobalFlag;

之后检测;

如下为32位系统的 debug 检测:

1
2
3
4
5
mov eax, fs:[30h] ;Process Environment Block
mov al, [eax+68h] ;NtGlobalFlag
and al, 70h
cmp al, 70h
je being_debugged

64位中, PEB结构加入形式是 gs:lodsq,也是加到eax寄存器中;

绕过的核心思想:

在eip指向 mov al, [eax+68h] 找到其内存位置并重新修改其值为 0.

对于修改 NtGlobalFlag 初值可以用注册表,这里不详细说明;

Heap Flags

Heap flags包含有两个与NtGlobalFlag一起初始化的标志: FlagsForceFlags. 这两个字段的值不仅会受调试器的影响, 还会由 windows 版本而不同, 字段的位置也取决于 windows 的版本.

  • Flags 字段:
    • 在 32 位 Windows NT, Windows 2000 和 Windows XP 中, Flags位于堆的0x0C偏移处. 在 32 位 Windows Vista 及更新的系统中, 它位于0x40偏移处.
    • 在 64 位 Windows XP 中, Flags字段位于堆的0x14偏移处, 而在 64 位 Windows Vista 及更新的系统中, 它则是位于0x70偏移处.
  • ForceFlags 字段:
    • 在 32 位 Windows NT, Windows 2000 和 Windows XP 中, ForceFlags位于堆的0x10偏移处. 在 32 位 Windows Vista 及更新的系统中, 它位于0x44偏移处.
    • 在 64 位 Windows XP 中, ForceFlags字段位于堆的0x18偏移处, 而在 64 位 Windows Vista 及更新的系统中, 它则是位于0x74偏移处.

一般而言,NtGlobalFlag 设置后,Heap Flags 也会设置;

调试器存在则:

Flags字段:

1
2
3
4
5
HEAP_GROWABLE (2)
HEAP_TAIL_CHECKING_ENABLED (0x20)
HEAP_FREE_CHECKING_ENABLED (0x40)
HEAP_SKIP_VALIDATION_CHECKS (0x10000000)
HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)

ForgeFlags:

1
2
3
HEAP_TAIL_CHECKING_ENABLED (0x20)
HEAP_FREE_CHECKING_ENABLED (0x40)
HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)

获取heap位置:

kernel32中的 GetProcessHeap();

PEB结构中查找,同样,一般动用fs,gs段寄存器就很容易是搞PEB的;

The Heap

堆在初始化时,会检查 heap flags;

设置 tail checking enable (尾部检测),那么会分配 0xABABABAB 给堆块尾;

设置 free checking enbale ,那么当需要额外字节填充堆块,会用 0xFEEEFEEE;

那么检测这些字节,可以得知是否在被调试,避免了动PEB的经典形象;

首先要先知道堆指针,且现代程序堆都会加密;

Int 3

无论何时触发了一个软件中断异常, 异常地址以及 EIP 寄存器的值都会同时指向产生异常的下一句指令. 但断点异常是其中的一个特例.

EXCEPTION_BREAKPOINT(0x80000003)异常触发时, Windows 会认定这是由单字节的 “CC“ 操作码 (也即Int 3指令) 造成的. Windows 递减异常地址以指向所认定的 “CC“ 操作码, 随后传递该异常给异常处理句柄. 但是 EIP 寄存器的值并不会发生变化.

因此, 如果使用了 CD 03(这是 Int 03 的机器码表示),那么当异常处理句柄接受控制时, 异常地址是指向 03 的位置.

IsDebuggerPresent

这个是典中典;

没调试的时候,返回的就是0;

实际上这个函数只是返回了 BeingDebugged 标志的值,也是PEB结构中的内容;

绕过:hook函数,或者改PEB表;

CheckRemoteDebuggerPresent

存在于kernel32中,检测指定的进程的调试状态;

1
2
3
4
BOOL WINAPI CheckRemoteDebuggerPresent(
_In_ HANDLE hProcess,
_Inout_ PBOOL pbDebuggerPresent
);

在被调试的时候,会将第二个参数指向的值变为0xffffffff;

简单的绕过只是将第二个参数的值在执行该函数后改变为0;

而这个函数本质是在对NtQueryInformationProcess的使用;

NtQueryInformationProcess

1
2
3
4
5
6
7
NTSTATUS WINAPI NtQueryInformationProcess(
_In_ HANDLE ProcessHandle, //进程句柄
_In_ PROCESSINFOCLASS ProcessInformationClass, //信息类型
_Out_ PVOID ProcessInformation, //写入信息缓冲区
_In_ ULONG ProcessInformationLength, //缓冲区大小
_Out_opt_ PULONG ReturnLength
);

在第二个参数中,有一个信息类型叫做: ProcessDebugPort;宏为7;

此时该函数通过查询EPROCESS结构体的DebugPort字段, 当进程正在被调试时, 返回值为0xffffffff

ZwSetInformationThread

这个函数给线程设置信息,可以设置:ThreadHideFromDebugger,禁止线程调试;

处于调试状态,执行完:ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, 0, 0) ,程序就会退出;

绕过:

ThreadHideFromDebugger,宏为0x11,如果看见这个函数,且有参数0x11,改之即可;

练习题:

一开始让输入password,进IDA之后就发现,密码就是很简单的 “I have a pen.”,在原程序里输入确实输出了 “correct”;但后面为何有这么多调试检测?这说明这道题的flag是需要通过调试查找的;

首先的关卡是 IsDebuggerPresent() ,以及NtGlobalFlag;

first

简单的对策便是 patch 右上图 1 为 0,右下图 70h 为 2 * 70h;

接下来遇到的是查看进程调试以及时间差;

second

首先改写 jz 为 jnz;

之后GetTickCount返回一个距离程序开始的时间,中间的图是一个sleep循环;最后比较1000;

这里只需简单粗暴改jbe为jmp;

接下来就是判断 process monitor 以及进程名,和是否虚拟机;

具体思想也是改值;

真实题里,这些反调试函数大大小小也是会比较的,但会有混淆,或者藏于线程,TLS等中去,更难发现;

而一般的题,确实大可不必去hook API,除非是线程里循环检测的反调试,一开就G的那种,但其实也可以静态patch;

还有那种判断过后卡几个call再G的反调试,真真的恶心人;

总结

熟悉了下手脱壳以及ida的 dump内存;熟悉硬件断点的使用;知晓一些简单的反调试原理;

感觉反调试原理大多与 PEB 结构有关系;所以接下来会考虑 开坑 PEB;