详细请见 wiki ;
壳
之前已经讲过其概念;这里更多的是脱壳的一些技巧,记录练手;
更多术语名词在之前提及过;
单步跟踪法
单步跟踪法的原理就是通过步过 (F8), 步入(F7) 和运行到 (F4) 功能, 完整走过程序的自脱壳过程, 跳过一些循环恢复代码的片段, 并用单步进入确保程序不会略过 OEP. 这样可以在软件自动脱壳模块运行完毕后, 到达 OEP, 并 dump 程序.
要点:
- 打开程序按 F8 单步向下, 尽量实现向下的 jmp 跳转;
- 会经常遇到大的循环, 这时要多用 F4 来跳过循环;
- 如果函数载入时不远处就是一个 call(近 call), 那么我们尽量不要直接跳过, 而是进入这个 call;
- 一般跳转幅度大的 jmp 指令, 都极有可能是跳转到了原程序入口点 (OEP);
用题举例:
打开后即是一个加壳文件,并有着 pusha 指令;
直接挂在开始处启动调试;
像图一的这种call就叫近call(基本上这个函数里只有几句话加1个call);
中间图的内容是跟进到找不到近call后可以看到这一系列的call在调用windows api,什么Module,ProcAddress一类的;
再往下走就能进入一个解码循环中,最后的通路在经过一番绕之后发现在 40D15F 这个地址;
继续往下走.. 之后还会有些循环,在这些循环中,向下跳的指令如果没有判断执行,很可能就是这条路,如左图所示;
跳过之后能发现 popa 指令,这与 pusha 相对应,回复其寄存器状态;
跳转到对应函数后,有push指令和retn,意思是将该十六进制内容压入栈中,并利用这个数据,返回到此十六进制地址;这便是此程序的OEP(从D000变到1000,跳转很明显);
ESP定律法
ESP 定律的原理在于利用程序中堆栈平衡来快速找到 OEP.
由于在程序自解密或者自解压过程中, 不少壳会先将当前寄存器状态压栈, 如使用
pushad
, 在解压结束后, 会将之前的寄存器值出栈, 如使用popad
. 因此在寄存器出栈时, 往往程序代码被恢复, 此时硬件断点触发. 然后在程序当前位置, 只需要少许单步操作, 就很容易到达正确的 OEP 位置.
简单来讲,在执行 pushad 之后, esp会确定下来,在 popad 执行后,也是此时的esp值,在此时esp栈上打个内存断点,则可检测两次esp同值时的时刻,第二次便是 popad 执行时;
要点:
- 程序刚载入开始 pushad/pushfd;
- 将全部寄存器压栈后就设对 ESP 寄存器设硬件断点;
- 运行程序, 触发断点;
- 删除硬件断点开始分析;
用题举例:
还是之前的那个程序:
执行后打开此时esp的栈中位置,打上断点,F4执行,会弹出一个硬件断点被捕获的窗口,点击后可以看到来到了上次 popa 执行之后的地方;
这个方法非常好用;
一步到达OEP法
说白了就是搜索 text 为 popad 之类的东西,然后查看其结构是不是壳的转到OEP位置的地方,然后直接在这个地方断点,直接过去;
只能说,能用的壳比较有限;一般 转到OEP处的 jmp 指令 跳转会比较大;
内存dump
找到OEP后,即可dump出脱壳后的程序:
点击 IDA 的 file > script command > 写入脚本并用 IDC 运行;
1 | static main(void) |
反调试
wiki上基本上都是说明,实际操作会来的更少,不过能了解一下,也能为后期搞反调试带来些许帮助;
NtGlobalFlag
原理:
在 32 位机器上, NtGlobalFlag
字段位于PEB
(进程环境块)0x68
的偏移处, 64 位机器则是在偏移0xBC
位置. 该字段的默认值为 0. 当调试器正在运行时, 该字段会被设置为一个特定的值,一般是0x70;
该字段包含有一系列的标志位. 由调试器创建的进程会设置以下标志位:
1 | FLG_HEAP_ENABLE_TAIL_CHECK (0x10) |
检测其值就能判断是否处于调试中;
PEB结构在汇编中加入的形式是经典的 fs:30h 段寄存器偏移;
这时候在PEB结构上往下偏移并找到 NtGlobalFlag;
之后检测;
如下为32位系统的 debug 检测:
1 | mov eax, fs:[30h] ;Process Environment Block |
64位中, PEB结构加入形式是 gs:lodsq,也是加到eax寄存器中;
绕过的核心思想:
在eip指向 mov al, [eax+68h]
找到其内存位置并重新修改其值为 0.
对于修改 NtGlobalFlag 初值可以用注册表,这里不详细说明;
Heap Flags
Heap flags
包含有两个与NtGlobalFlag
一起初始化的标志: Flags
和ForceFlags
. 这两个字段的值不仅会受调试器的影响, 还会由 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 | HEAP_GROWABLE (2) |
ForgeFlags:
1 | HEAP_TAIL_CHECKING_ENABLED (0x20) |
获取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 | BOOL WINAPI CheckRemoteDebuggerPresent( |
在被调试的时候,会将第二个参数指向的值变为0xffffffff;
简单的绕过只是将第二个参数的值在执行该函数后改变为0;
而这个函数本质是在对NtQueryInformationProcess的使用;
NtQueryInformationProcess
1 | NTSTATUS WINAPI NtQueryInformationProcess( |
在第二个参数中,有一个信息类型叫做: 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;
简单的对策便是 patch 右上图 1 为 0,右下图 70h 为 2 * 70h;
接下来遇到的是查看进程调试以及时间差;
首先改写 jz 为 jnz;
之后GetTickCount返回一个距离程序开始的时间,中间的图是一个sleep循环;最后比较1000;
这里只需简单粗暴改jbe为jmp;
接下来就是判断 process monitor 以及进程名,和是否虚拟机;
具体思想也是改值;
真实题里,这些反调试函数大大小小也是会比较的,但会有混淆,或者藏于线程,TLS等中去,更难发现;
而一般的题,确实大可不必去hook API,除非是线程里循环检测的反调试,一开就G的那种,但其实也可以静态patch;
还有那种判断过后卡几个call再G的反调试,真真的恶心人;
总结
熟悉了下手脱壳以及ida的 dump内存;熟悉硬件断点的使用;知晓一些简单的反调试原理;
感觉反调试原理大多与 PEB 结构有关系;所以接下来会考虑 开坑 PEB;