逆向-反调试
反调试技术(部分)
动态调试是一种观察程序运行状态的一种手段。
逆向工程中的动态调试的目的主要有:验证静态分析结果和观察程序运行时的数据。
而为了防止我们对程序进行调试,程序开发者通常会设置反调试来检测自己开发的程序是否正在被调试。
花指令通常干扰静态分析;反调试与之相反,主要为了干扰动态调试
API反调试
Windows内部提供了一些用于检测调试器的API
基于PEB(Process Environment Block,进程环境块)的静态反调试,存放进程信息的一个结构体
TEB (Thread Environment Block,线程环境块)结构体,进程中的每一个线程都对应着一个TEB结构体
1 | kd>dt_TEB |
PEB结构体中中,BeingDebugged
、ProcessHeap
、NtGlobalFlag
是与调试信息相关
BeingDebugged
:当进程处于被调试状态时,值为1,否则为0。1
2
3
4mov eax,dword ptr fs:[18]; //获取TEB地址,AddressOfTEB
mov eax,dword ptr ds:[eax+30]; //通过TEB获取PEB的地址,AddressOfPEB
mov eax byte ptr da:[eax+2]; //获取PEB偏移为2的结构体元素BeginDebugged,并返回这个值
retnProcessHeap
:指向Heap结构体,偏移0xC处为Flags
成员,偏移0x10处为ForceFlags
成员。通常情况下,
Flags
的值为2,ForceFlags
的值为0,当进程被调试时会发生改变1
2
3mov eax,dword ptr fs:[0x18]; //TEB的起始地址
mov eax,dword ptr ds:[eax+30]; //PEB的地址
mov eax,dword ptr ds:[eax+18]; //PEB.Processheap的地址NGlobalFlag
:占四个字节,默认值为0。当进程处于被调试状态时,第一个字节会被置为0x70。
IsDebuggerPresent()
1 | BOOL IsDebuggerPresent(); |
返回值为1表示当前进程被调试的状态,反之为0.
1 | call IsDebuggerPresent |
当程序处于3环(低权限)时, FS:[0]
寄存器指向TEB,TEB向后偏移0x30字节FS[0x30h]
的位置保存的是PEB结构体的地址。
在 PEB 0x2 偏移处存储的是一字节长度的 BeingDebugged
标志位,
IsDebuggerPresent
函数本质是读取该进程对应 PEB 的 BeingDebugged
标志位并返回。
1 | mov eax,dword ptr fs:[18]; //获取TEB地址,AddressOfTEB |
- 绕过方法
- 只需要将PEB 0x2偏移处的结构体元素
BeingDebugged
标志设为0
即可 (或改变一下返回值) - 把判断
je
改为jne
- 只需要将PEB 0x2偏移处的结构体元素
CheckRemoteDebuggerPresent()
另一个常用的API是CheckRemoteDebuggerPresent
,返回值为1表示当前进程被调试的状态,反之为0
1 | BOOL WINAPI CheckRemoteDebuggerPresent( |
如果调试器存在 (通常是检测自己是否正在被调试), 该函数会将pbDebuggerPresent
指向的值设为0xffffffff
函数在判断 hProcess
不是 0 以及检查输出参数指针是否为NULL
后就调用了 NtQueryInformationProcess
函数
NtQueryInformationProcess()
检索有关指定进程的信息。
1 | __kernel_entry NTSTATUS NtQueryInformationProcess( |
参数解释:
ProcessHandle:要为其检索信息的进程句柄。
ProcessInformationClass:要检索的进程信息的类型。
ProcessInformation:指向调用应用程序提供的缓冲区的指针,函数将请求的信息写入其中。 写入的信息的大小因 ProcessInformationClass 参数的数据类型而异。
ProcessInformationLength:指向的缓冲区的大小(以字节为单位)。
ReturnLength:指向变量的指针,其中函数返回所请求信息的大小。 如果函数成功,则这是 由 ProcessInformation 参数指向的缓冲区中写入的信息的大小 (如果缓冲区太小,则为成功接收信息) 所需的最小缓冲区大小。
进程名反调试
CreateToolhelp32Snapshot()
遍历当前系统中的进程列表,检测是否存在与调试器相关的进程名
1 |
|
- 调试器进程名列表:在
debuggerNames
数组中列出了常见的调试器进程名(如ollydbg.exe
,x64dbg.exe
等)。可以根据需要添加更多的调试器进程名。 - CreateToolhelp32Snapshot:这是一个Windows API,用于创建系统中所有进程的快照,以便遍历这些进程。
- Process32First 和 Process32Next:这些函数用于遍历进程快照中的每一个进程。
- tolower:为了匹配时忽略大小写,将进程名全部转换为小写进行比较。
- ExitProcess:如果发现调试器进程,程序直接退出。
- 这种方法可以用来检测外部调试器是否正在运行,但它不是百分之百可靠,因为高级调试器可能会通过修改进程名或隐藏自己来规避检测。
实例
Fuko’s starfish
一个很典的 windows exe 逆向,考察基本的 dll 程序调试,windows 常见反调试和常见花指令。
程序开始时,在挂载 dll 的同时启动一个线程函数(dll中)。
在 exe 初始化和加载 dll 的过程中都会加载一个全局对象,在全局对象的构造函数中设置了一个反调试。
这里只需要 patch
掉 push rax
到pop rax
的代码就能看到真实逻辑。
反调试IsDebuggerpersent()
用keypatch
把jz
改为jez
,或直接改成jmp
对密钥进行了初始化。这里开始的初始化是个障眼法,在花指令后继续更新了密钥,可以看到在后面还有一段更新密钥的代码。
然后玩过游戏后,进入加密逻辑。这里其实就是一个标准 aes-ecb
加密算法。唯一需要注意的是,在密钥扩展前,会对key
进行最后的操作。如果在调试状态,则会输出hmm...
,如果不在调试状态,则会将密钥异或 0x17
。
- CreateToolhelp32Snapshot()
- CheckRemoteDebuggerPresent()
首先获取正确的 key:
1 |
|
然后拿到密文,进行 aes-ecb
解密
1 | from Crypto.Cipher import AES |