深入内存的暗流:用 OllyDbg 实时追踪一次经典的进程注入攻击
你有没有想过,一个看似无害的记事本程序(notepad.exe),是如何在毫无征兆的情况下突然弹出一个“Hello World”对话框的?这不是魔法,而是典型的内存注入攻击。而我们要做的,不是去发动攻击,而是像一名数字侦探一样,用OllyDbg把整个过程从头到尾看得清清楚楚。
这不仅是一次技术演示,更是一场对 Windows 用户态执行机制的深度解剖。我们将亲手布下监控、触发注入、实时观察内存变化和执行跳转——所有这一切,都发生在你眼前的一块汇编代码区域里。
为什么是 OllyDbg?它凭什么能看见“看不见”的行为?
尽管现在很多人转向 x64dbg 或者 IDA Pro + 调试器组合,但如果你刚入门逆向分析,想快速理解“程序运行时到底发生了什么”,OllyDbg 依然是最好的起点。
它不像 WinDbg 那样深陷内核泥潭,也不像 IDA 需要先静态反编译再动调。OllyDbg 是那种你双击就开、附加进程就能看寄存器和堆栈的工具,轻量、直观、响应快。
更重要的是,它的核心能力正好匹配我们今天的任务:
- 动态拦截 API 调用;
- 实时查看内存内容与属性;
- 跟踪 EIP(指令指针)是否跳去了不该去的地方;
- 单步执行远程线程创建后的第一行代码。
这些正是识别内存注入的关键线索。
⚠️ 注意:OllyDbg 只支持 32 位程序。如果你想跟踪 64 位目标,请使用 x64dbg。但我们今天选择 notepad.exe 这类传统 32 位宿主,正是为了最大化兼容性和可观察性。
内存注入的本质:把“毒药”喂进别人的嘴里
所谓内存注入,说白了就是这么几步:
- 找个“健康”的进程当替身(比如
notepad.exe); - 拿到它的操作权限(
OpenProcess); - 在它体内申请一块“藏身之地”(
VirtualAllocEx分配 RWX 内存); - 把恶意代码(Shellcode)偷偷塞进去(
WriteProcessMemory); - 让它自己主动执行那段代码(
CreateRemoteThread启动新线程);
整个过程就像给一个人注射了一段外来 DNA,让他开始做原本不会做的事。
而我们的任务,就是在受害者还没发病之前,抓住每一个可疑动作。
攻击代码长什么样?一段会说话的 Shellcode
下面这段 C 程序,就是一个最简单的内存注入器:
#include <windows.h> #include <tlhelp32.h> // 调用 MessageBoxA("Hello World") 的 Shellcode unsigned char shellcode[] = "\x6A\x00\x68\x6C\x6C\x20\x20\x68\x6F\x72\x6C\x64\x68\x65\x20\x57" "\x68\x48\x65\x6C\x6C\x8B\xCC\x53\x51\x52\x50\xB8\xE8\xAF\x00\x00" "\xFF\xD0\xC3"; DWORD GetTargetProcessId(const char* processName) { HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); PROCESSENTRY32 pe32 = {0}; pe32.dwSize = sizeof(pe32); if (Process32First(hSnapshot, &pe32)) { do { if (strcmp(pe32.szExeFile, processName) == 0) { CloseHandle(hSnapshot); return pe32.th32ProcessID; } } while (Process32Next(hSnapshot, &pe32)); } CloseHandle(hSnapshot); return 0; } int main() { DWORD pid = GetTargetProcessId("notepad.exe"); if (!pid) { printf("[-] Notepad not found.\n"); return -1; } HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); if (!hProc) { printf("[-] Failed to open process.\n"); return -1; } // 在目标进程中分配可执行内存 LPVOID pRemoteMem = VirtualAllocEx(hProc, NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (!pRemoteMem) { printf("[-] Memory allocation failed.\n"); CloseHandle(hProc); return -1; } // 写入 Shellcode WriteProcessMemory(hProc, pRemoteMem, shellcode, sizeof(shellcode), NULL); // 创建远程线程执行 Shellcode HANDLE hThread = CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteMem, NULL, 0, NULL); if (hThread) { printf("[+] Injected successfully! Thread ID: %lu\n", GetThreadId(hThread)); WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); } else { printf("[-] Remote thread creation failed.\n"); } CloseHandle(hProc); return 0; }别被那一串\x开头的数据吓到,那只是机器码的十六进制表示。这段 Shellcode 的作用非常明确:调用MessageBoxA显示一个消息框。
真正关键的是后面的四个 API 调用:OpenProcess→VirtualAllocEx→WriteProcessMemory→CreateRemoteThread
它们构成了现代内存注入的标准模板。
开始跟踪!七步还原攻击全过程
我们现在切换角色:不再是攻击者,而是防御方。我们要用 OllyDbg 去监控notepad.exe,看看当外部程序试图注入时,系统究竟暴露出了哪些痕迹。
第一步:准备干净的实验环境
建议使用一台Windows 7 32位虚拟机(XP 也可),关闭杀软、UAC 和 DEP(便于观察,实战中需注意差异)。打开记事本,并记住它的 PID(可以用任务管理器或命令行tasklist | findstr notepad查看)。
第二步:用 OllyDbg 附加上去
启动 OllyDbg,按Alt+P,找到notepad.exe并点击 Attach。
你会看到 CPU 窗口暂停在某个地址上,EIP 指向当前正在执行的指令。此时程序已被“冻结”,任何后续行为都会被我们捕获。
第三步:设下天罗地网——API 断点
在 OllyDbg 的命令栏输入以下三条断点命令:
bp kernel32.VirtualAllocEx bp kernel32.WriteProcessMemory bp kernel32.CreateRemoteThread这三个 API 就是我们要盯死的目标。一旦有人调用它们,调试器就会立即中断,让我们有机会检查上下文。
💡 提示:如果发现断点不生效,可能是 API 被重定向到了
API-MS-WIN-*DLL 上。可以尝试用模块名加函数名的方式设置:
bp kernel32.VirtualAllocEx
或右键 → “Set breakpoint” → 输入完整路径。
第四步:发动攻击!
回到主机,编译并运行上面的注入器程序。
不出意外的话,OllyDbg 会在几秒内中断——第一个被捕获的通常是VirtualAllocEx。
第五步:揪出“非法占地”行为
当命中VirtualAllocEx时,查看堆栈参数:
| 参数 | 含义 |
|---|---|
hProcess | 目标进程句柄(应为 notepad 的有效句柄) |
lpAddress | 请求分配的地址(通常为 NULL,由系统决定) |
dwSize | 分配大小(对照 shellcode 长度判断是否合理) |
flAllocationType | 应为MEM_COMMIT \| MEM_RESERVE |
flProtect | 关键!必须是PAGE_EXECUTE_READWRITE(RWX) |
RWX 内存本身就是高危信号。正常程序极少需要同时可写又可执行的页面。DEP 存在就是为了阻止这种行为,但通过VirtualAllocEx可以绕过。
记录返回值(即分配的地址),比如0x003A0000,这是 Shellcode 即将落脚的位置。
第六步:锁定“投毒”现场 ——WriteProcessMemory
接下来,程序会调用WriteProcessMemory。
查看堆栈中的lpBuffer参数——这是攻击机本地存放 Shellcode 的地址。你可以跳转过去(Ctrl+G),在“Dump”窗口中看到原始字节:
6A 00 68 6C 6C 20 20 68 ...选中这些数据,右键 → “Follow in Disassembler”,OllyDbg 会尝试反汇编这段数据。你会发现它解析成了一系列合法的 x86 指令:
push 0 push 'll ' push 'orld' push 'e W' push 'Hell' mov ecx, esp ...这就是典型的字符串压栈 + 调用 API 的模式。虽然没有符号信息,但经验丰富的分析员一眼就能看出这是在准备调用MessageBoxA。
第七步:见证“夺舍”时刻 ——CreateRemoteThread
最后一个断点,也是最关键的一步:CreateRemoteThread。
关注lpStartAddress参数——它应该等于前面VirtualAllocEx返回的那个地址(如0x003A0000)。
这意味着:一个新的线程即将从我们刚刚写入的 Shellcode 处开始执行!
此时你可以:
- 跳转到该地址(
Ctrl+G); - 在反汇编窗口中确认代码内容;
- 按
F7单步进入,亲眼看着第一条push 0被执行; - 观察堆栈如何逐步构建参数;
- 最终看到
call eax触发MessageBoxA,屏幕上弹出“Hello World”。
这一刻,你不仅看到了攻击成功,还完整复现了控制流劫持的全过程。
如何区分“合法”与“恶意”?检测的艺术
当然,不是所有调用WriteProcessMemory的都是攻击。调试器、游戏修改器、性能监控工具也会用这些 API。
所以真正的检测,靠的是上下文关联分析:
| 行为特征 | 恶意可能性 |
|---|---|
VirtualAllocEx+ RWX +WriteProcessMemory+ 非模块地址执行 | ⚠️ 极高 |
| 写入内存的数据熵值高(加密/编码) | 🔍 高 |
lpStartAddress指向堆或未映射区域 | ⚠️ 高 |
| 来源进程为未知可执行文件或临时目录 | ⚠️ 高 |
| 线程创建后迅速退出注入器 | ⚠️ 中高 |
结合多个指标,才能做出准确判断。
实战技巧:让 OllyDbg 更好用
在真实分析中,还有一些实用技巧可以提升效率:
✅ 开启 API 日志
进入Options → Debugging options → Events,勾选Log API calls。这样每次调用都会记录到日志窗口,方便回溯。
✅ 使用快照恢复环境
配合 VMware/VirtualBox 快照功能,每次测试前一键还原干净状态,避免残留影响。
✅ 绕过反调试
有些高级 Shellcode 会检测是否处于调试环境,例如调用IsDebuggerPresent()。
你可以:
- 在该函数入口打补丁:mov eax, 0; ret
- 或使用插件(如 HideDebugger)隐藏调试器痕迹
✅ 结合 Scylla 恢复 IAT
如果 Shellcode 调用了非导入表中的 API(如动态加载user32.MessageBoxA),可用 Scylla 插件帮助识别 API 地址来源。
总结:看懂底层,才能防住高层
通过这次完整的跟踪实验,你应该已经建立起一种直觉:
内存注入不是神秘的技术,而是一系列可观察、可拦截、可分析的行为序列。
而 OllyDbg 正是帮你把这些抽象概念具象化的最佳工具。它让你看到:
- 内存是如何被悄悄分配的;
- 数据是如何变成代码的;
- 控制流是如何被悄然转移的。
这些技能不仅适用于分析 PoC 级别的注入器,更是你在面对 APT 攻击、无文件恶意软件、反射式 DLL 注入等复杂威胁时,进行深度狩猎的基础。
下次当你听说某款 EDR 检测到了“异常线程创建”,不妨想想:它是怎么知道那个地址不该被执行的?答案,往往就藏在CreateRemoteThread的参数里,在PAGE_EXECUTE_READWRITE的标记里,在那一片不该存在的 RWX 内存里。
而你要做的,就是学会去看。
如果你也曾在调试器中见过 EIP 跳进一片灰色内存区而心头一紧,欢迎在评论区分享你的“破案”经历。