Windows逆向工程是安全研究、恶意代码分析、漏洞挖掘与兼容性修复的核心技能。专业视角下,它不是"用IDA看汇编",而是静态结构解析 + 动态行为观测 + 对抗绕过三位一体的工程体系。本文从PE格式入手,逐步深入到反调试对抗与代码补丁实战。
Windows可执行文件遵循PE(Portable Executable)格式。理解其结构是逆向的第一步。
关键结构:DOS头 → NT头 → 节表 → 节数据。
#include <windows.h>
#include <stdio.h>
void ParsePE(const char* path) {
HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
LPVOID base = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
if (dos->e_magic != IMAGE_DOS_SIGNATURE) { puts("Not PE"); return; }
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((BYTE*)base + dos->e_lfanew);
printf("Machine: 0x%04X\n", nt->FileHeader.Machine);
printf("Sections: %d\n", nt->FileHeader.NumberOfSections);
printf("EntryPoint RVA: 0x%08X\n", nt->OptionalHeader.AddressOfEntryPoint);
printf("ImageBase: 0x%p\n", (void*)nt->OptionalHeader.ImageBase);
// 遍历节表
PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
for (int i = 0; i < nt->FileHeader.NumberOfSections; i++) {
printf(" [%s] VA=0x%08X VSize=0x%X RawSize=0x%X\n",
sec[i].Name, sec[i].VirtualAddress,
sec[i].Misc.VirtualSize, sec[i].SizeOfRawData);
}
UnmapViewOfFile(base); CloseHandle(hMap); CloseHandle(hFile);
}RVA转FOA是定位数据的关键:需要遍历节表找到包含该RVA的节,再计算偏移。IAT、导出表、资源都依赖此转换。
导入表揭示程序调用了哪些API,是判断行为
void DumpImports(LPVOID base) {
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((BYTE*)base + dos->e_lfanew);
DWORD impRva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
if (!impRva) return;
PIMAGE_IMPORT_DESCRIPTOR imp = (PIMAGE_IMPORT_DESCRIPTOR)((BYTE*)base + impRva);
for (; imp->Name; imp++) {
printf("DLL: %s\n", (char*)((BYTE*)base + imp->Name));
PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)((BYTE*)base + imp->FirstThunk);
PIMAGE_THUNK_DATA orig = (PIMAGE_THUNK_DATA)((BYTE*)base + imp->OriginalFirstThunk);
for (; orig->u1.AddressOfData; orig++, thunk++) {
if (IMAGE_SNAP_BY_ORDINAL(orig->u1.Ordinal)) continue;
PIMAGE_IMPORT_BY_NAME n = (PIMAGE_IMPORT_BY_NAME)((BYTE*)base + orig->u1.AddressOfData);
printf(" %s\n", n->Name);
}
}
}实战中优先关注 VirtualAlloc、CreateRemoteThread、WriteProcessMemory、LoadLibrary 等高危API组合。
x64dbg 与 WinDbg 是主力工具。但目标常带反调试,需识别并绕过。
常见反调试与绕
// 1) IsDebuggerPresent —— 最简单的检测
BOOL isDebug = IsDebuggerPresent();
// 绕过:在调试器中修改PEB.BeingDebugged为0
// PEB地址:x64下 gs:[0x60],x86下 fs:[0x30]
#ifdef _WIN64
PPEB peb = (PPEB)__readgsqword(0x60);
#else
PPEB peb = (PPEB)__readfsdword(0x30);
#endif
peb->BeingDebugged = 0;
// 2) NtQueryInformationProcess 检测调试端口
typedef NTSTATUS(NTAPI* pNtQIP)(HANDLE, ULONG, PVOID, ULONG, PULONG);
pNtQIP NtQIP = (pNtQIP)GetProcAddress(GetModuleHandleA("ntdll.dll"),
"NtQueryInformationProcess");
ULONG debugPort = 0;
NtQIP(GetCurrentProcess(), 7 /*ProcessDebugPort*/, &debugPort, sizeof(debugPort), NULL);
// 3) 时间差检测
DWORD t1 = GetTickCount();
// ... 被检测代码 ...
DWORD t2 = GetTickCount();
if (t2 - t1 > 100) { /* 疑似被单步调试 */ }对抗策略:硬件断点替代软件断点(不修改代码字节)、隐藏调试器(ScyllaHide插件)、对时间检测做补丁或Hook。
逆向的最终目标常是修改行为。补丁分文件补丁与内存补丁。
内存补丁:
// 将目标函数开头改为 ret,实现函数禁用
void PatchRet(LPVOID target) {
DWORD old;
VirtualProtect(target, 1, PAGE_EXECUTE_READWRITE, &old);
*(BYTE*)target = 0xC3; // x86/x64 的 ret
VirtualProtect(target, 1, old, &old);
FlushInstructionCache(GetCurrentProcess(), target, 1);
}
// 将条件跳转改为无条件跳转(绕过校验)
void PatchJmp(LPVOID addr, BOOL alwaysJump) {
DWORD old;
VirtualProtect(addr, 6, PAGE_EXECUTE_READWRITE, &old);
if (alwaysJump) {
// 0x74/0x75 (JZ/JNZ) → 0xEB (JMP short)
if (*(BYTE*)addr == 0x74 || *(BYTE*)addr == 0x75)
*(BYTE*)addr = 0xEB;
else if (*(BYTE*)addr == 0x0F) // 近跳转 JZ/JNZ
*(BYTE*)((BYTE*)addr + 1) = (*(BYTE*)((BYTE*)addr + 1) == 0x84) ? 0x85 : 0x84;
}
VirtualProtect(addr, 6, old, &old);
}文件补丁则需修改PE后重算校验和,或直接修改节数据。专业工具链用 LIEF 或 pefile 做程序化补丁。
Windows逆向的专业性,体现在结构解析的准确性、动态观测的全面性、对抗绕过的灵活性。PE解析是地基,反调试是门槛,补丁是手段,而合规是底线。掌握这套体系,才能在恶意分析、漏洞挖掘与安全对抗中游刃有余。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。