x64代码注入实战:从x86迁移到Windows 11的完整复现指南
手头这本《逆向工程核心原理》我翻了不止一遍。书里“代码注入”那一章当年在 Win7 的 32 位虚拟机上跑通时MessageBox 从目标进程弹出来的瞬间确实有“看懂黑魔法”的错觉。最近想把这套实验原封不动搬到 64 位 Windows 11 上才发现问题比想象中多书上的 x86 代码要么编不过要么编译通过后远程线程一执行就崩要么 MessageBox 根本弹不出来。这篇文章就是这次复现过程的完整记录包含两份可运行的注入器代码、一整套 x64 注入桩的机器码偏移分析、以及我在 Windows 11 上踩过的所有坑。适合正在读这本书、想把手动代码注入从 x86 迁移到 x64 的读者也适合刚接触逆向工程和进程注入、想搞清楚 VirtualAllocEx / WriteProcessMemory / CreateRemoteThread 三者关系的朋友。1. 书中实验的原始面貌与64位改造思路1.1 书上那套代码注入到底在做什么“代码注入”在书里的位置很靠前它其实是在解释一个非常朴素的问题Windows 的进程虚拟地址空间是互相隔离的我凭什么能在别人的进程里执行我的代码答案就三条 APIVirtualAllocEx 在目标进程的地址空间里申请一块内存WriteProcessMemory 往这块内存里写东西CreateRemoteThread 在目标进程里开一条线程让这块内存里的代码跑起来。书里的实验本质就是用这三板斧在目标进程里弹一个 MessageBox或者让目标进程加载一个我写的 DLL。用生活化的比喻目标进程是一间上锁的房间OpenProcess 是敲门拿到授权VirtualAllocEx 是让房东在墙上开一个洞WriteProcessMemory 是把家具搬进去CreateRemoteThread 是按下开关让家具通电运转。整个流程的精华不在于那几个 API 名字而在于“两个进程的地址空间是隔离的所有跨进程操作都必须由操作系统代为完成”这个底层认知。书上的原始版本还带着那个时代的痕迹目标进程喜欢用 notepad.exe注入器用 x86 编译DLL 路径用 char 数组配合 LoadLibraryA代码里到处都是 printf 甚至 fopen。这些细节放在今天的 64 位 Windows 11 上每一项都可能变成一个小坑。1.2 x86能跑x64为什么翻车把书里的实验从 x86 迁移到 x64核心矛盾不是语法而是三件事调用约定变了、地址宽度变了、系统防御机制变了。x86 时代函数调用参数全部压栈调用约定还分 cdecl、stdcall、fastcall一个 CS 课程就能讲清楚。x64 统一成一套规则前四个整数或指针参数依次进 RCX、RDX、R8、R9多余的参数才压栈调用前栈还要按 16 字节对齐被调函数还默认可以占用 32 字节的“影子区”。这意味着你从书里抄来的“push 参数、call MessageBoxA”在 x64 上根本不是那么回事。地址宽度带来的影响更直接。x86 的 4 字节指针写死函数地址在当年的实验环境里没问题x64 早在 Vista 时代就普及了 ASLRkernel32、user32 这些系统 DLL 的基址每次开机随机化你没法把 MessageBoxW 的地址当作常量写进注入代码里。再从防御层面说现在的系统默认开 DEP数据执行保护你在目标进程里申请的内存页如果不带 EXECUTE 标志远程线程一启动就触发 0xC0000005。下面这张表基本概括了 x86 到 x64 的主要变化也是本次复现所有改造工作的依据对比项x86书里的时代x64当前环境指针 / 地址宽度4 字节8 字节函数参数传递全部压栈前四个参数走 RCX / RDX / R8 / R9调用前栈对齐无强制要求调用点栈需按 16 字节对齐Shadow Space无被调函数可占用 32 字节影子区函数地址可写死ASLR 导致 DLL 基址每次启动随机字符串类型常见 char / LoadLibraryA更规范用 wchar_t / LoadLibraryW进程位数不匹配不存在出现 WOW64 层跨位直接失败这本书的价值恰恰在这里它教你的是“注入”这个思维模型而 x64 迁移逼着你把每个环节重新理解一遍。下面进入实操。2. 实验环境与工具链准备2.1 Windows 11 VS2022 的实测环境我的实验机是 Windows 11 专业版24H2 内部版本开发环境是 Visual Studio 2022工具集用的 v143平台选择 x64。需要说明的是我做实验之前专门在“配置管理器”里给解决方案添加了 x64 平台。很多从书里抄代码的人第一步就栽在这里默认只编译 x86注入器本身是 32 位目标进程是 64 位CreateRemoteThread 直接不起作用。辅助工具我准备了三个。Process Explorer 用来确认目标进程的模块列表注入后直接看 myinject.dll 有没有出现在 target.exe 下面这是最直观的验证手段。x64dbg 用来调试那种“黑盒看起来正常但实际没生效”的情况比如远程线程明明创建成功但桩代码在目标进程里跑飞了附加进去单步就能看到。Process Monitor 可以观察注入 DLL 是否触发了文件、注册表操作适合验证 DllMain 里的日志代码有没有执行。实验目录我统一放在 C:\lab 下里面放了三个工程target目标进程、injector_dll实验一的注入器 被注入的 DLL、injector_stub实验二的纯代码注入器。强烈建议把整个目录加入 Windows 安全中心的排除项否则 VS 刚编译出来的注入器第一次运行很容易被实时防护拦掉或者被注入的 DLL 直接被隔离删除。这不是什么见不得人的事而是这类改动进程行为的程序在杀软眼里天然可疑。2.2 编译老代码时绕不开的三个报错从书里抄代码时第一关是编译。我实测下来有三个报错出现频率最高都在 VS2022 的 x64 配置下触发。第一个是 C4996。书里的老代码普遍用 fopen、strcpy 这类 CRT 函数x64 配置下 VS 直接提示“fopen: This function or variable may be unsafe”并建议改用 fopen_s 等带 _s 后缀的安全版本。这不是 x64 特有的问题但很多人的 x86 工程时代旧、警告被忽略惯了切到 x64 重新编译时突然被当成错误弹出来就懵了。两个办法要么在文件头部加#define _CRT_SECURE_NO_WARNINGS要么老老实实改 fopen_s。我建议改 fopen_s因为后面被注入的 DLL 里写日志文件正好用得上一次到位。第二个是链接错误 LNK1123 或 LNK1112。这类报错通常是“模块计算机类型 x86 与目标计算机类型 x64 冲突”的变体多半是工程里混入了 32 位的 .lib 或 .obj或者某个依赖库只提供了 x86 版本。处理思路就一条确认整个依赖链条全部是 x64 版本。第三个是“error LNK2019 无法解析的外部符号”常见于你自己写的函数声明和实现不一致或者调用了某个需要额外链接库的 API。比如后面实验二准备用 EnumProcessModulesEx 做严谨版地址解析就需要在代码里加#pragma comment(lib, psapi.lib)否则链接器找不到入口。这三个报错看着简单但在复现老书实验时特别容易让人泄气。我的习惯是先把“能编译出 x64 的 Hello World”跑通再往里加注入代码不要一上来就死磕整份书源码。3. 复现实验一CreateRemoteThread LoadLibrary 的DLL注入3.1 注入器核心实现DLL 注入是书上“代码注入”章节最经典的实验思路至今仍是很多挂钩框架和调试工具的底子把目标 DLL 的完整路径写进目标进程再让目标进程自己调 LoadLibraryW 把 DLL 加载进来。因为 DLL 里可以写任意逻辑所以这种注入的“攻击面”最大验证也最方便。我实现的 x64 注入器核心代码如下。注意几个关键点不要直接 OpenProcess 传 PROCESS_ALL_ACCESS按最小权限原则声明就够路径字符串用 wchar_tLoadLibraryW 的地址从本进程的 kernel32.dll 里取。#include windows.h #include tlhelp32.h #include iostream static DWORD FindProcessIdByName(const wchar_t* name) { HANDLE snap CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snap INVALID_HANDLE_VALUE) return 0; PROCESSENTRY32W pe { sizeof(PROCESSENTRY32W) }; BOOL ok Process32FirstW(snap, pe); while (ok) { if (_wcsicmp(pe.szExeFile, name) 0) { CloseHandle(snap); return pe.th32ProcessID; } ok Process32NextW(snap, pe); } CloseHandle(snap); return 0; } int main() { DWORD pid FindProcessIdByName(Ltarget.exe); if (pid 0) { std::wcout L[-] 找不到目标进程 std::endl; return 1; } HANDLE hProcess OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, pid); if (!hProcess) { std::wcout L[-] OpenProcess 失败, 错误码: GetLastError() std::endl; return 1; } wchar_t dllPath[] LC:\\lab\\myinject.dll; SIZE_T pathSize (wcslen(dllPath) 1) * sizeof(wchar_t); LPVOID remotePath VirtualAllocEx(hProcess, NULL, pathSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (!remotePath) { std::wcout L[-] VirtualAllocEx 失败, 错误码: GetLastError() std::endl; CloseHandle(hProcess); return 1; } SIZE_T written 0; if (!WriteProcessMemory(hProcess, remotePath, dllPath, pathSize, written)) { std::wcout L[-] WriteProcessMemory 失败, 错误码: GetLastError() std::endl; VirtualFreeEx(hProcess, remotePath, 0, MEM_RELEASE); CloseHandle(hProcess); return 1; } FARPROC pLoadLibraryW GetProcAddress(GetModuleHandleW(Lkernel32.dll), LoadLibraryW); HANDLE hThread CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pLoadLibraryW, remotePath, 0, NULL); if (!hThread) { std::wcout L[-] CreateRemoteThread 失败, 错误码: GetLastError() std::endl; VirtualFreeEx(hProcess, remotePath, 0, MEM_RELEASE); CloseHandle(hProcess); return 1; } WaitForSingleObject(hThread, INFINITE); DWORD exitCode 0; GetExitCodeThread(hThread, exitCode); std::wcout L[] 远程线程退出码: 0x std::hex exitCode std::endl; CloseHandle(hThread); VirtualFreeEx(hProcess, remotePath, 0, MEM_RELEASE); CloseHandle(hProcess); return 0; }这里有个细节值得展开为什么 LoadLibraryW 的地址可以在本进程里取然后直接拿到目标进程用因为 kernel32.dll 这类系统映像的 ASLR 重定位是开机时统一做的所有进程在同一引导会话里加载的 kernel32 基址一致。这个假设在 Windows 11 上实测是成立的也是无数常规注入器和加载器敢这么写的原因。真要较真可以用 EnumProcessModulesEx 去目标进程里拿模块基址再做换算实验二会演示这个更严谨的做法。3.2 被注入的DLL与被忽略的Loader Lock被注入的 DLL 本身也要用 x64 编译。我的 DLL 里放了一个工作线程弹出 MessageBox同时用 fopen_s 写一个日志文件验证代码确实在目标进程内执行#include windows.h #include stdio.h DWORD WINAPI Worker(LPVOID) { MessageBoxW(NULL, LInjected via CreateRemoteThread!, LLab Demo, MB_OK); FILE* fp nullptr; fopen_s(fp, C:\\lab\\injected_log.txt, w); if (fp) { fprintf_s(fp, PID%lu TID%lu\n, GetCurrentProcessId(), GetCurrentThreadId()); fclose(fp); } return 0; } BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID) { if (reason DLL_PROCESS_ATTACH) { DisableThreadLibraryCalls(hModule); CreateThread(NULL, 0, Worker, NULL, 0, NULL); } return TRUE; }一个容易踩的坑不要直接在 DllMain 的 DLL_PROCESS_ATTACH 分支里调 MessageBox 或 LoadLibrary。DllMain 执行时持有 loader lock一旦在这个锁里等待用户输入或加载其他模块很容易和系统加载器形成死锁。很多老的示例代码不讲究这个在 Win7 上碰巧能跑Win11 上进程可能直接卡死或崩溃。稳健的做法就是上面这样DllMain 里只做最小初始化把重活交给 CreateThread 出来的工作线程。我一开始偷懒把 MessageBox 直接写在 DllMain 里结果注入成功后 target.exe 界面假死x64dbg 附加进去看到主线程卡在 ntdll 的加载锁等待上才反应过来是 loader lock 的问题。这个教训值一次完整的崩溃分析。3.3 完整操作步骤与验证实验一的完整操作流程如下编译 target.exe。它只是一个打印 PID 后卡住的控制台程序用来当靶子。运行 target.exe记下输出的进程 ID或者直接用注入器按进程名查找。编译 injector_dll.exe 和 myinject.dll确认两者都是 x64。运行注入器看到控制台输出远程线程退出码 0x0000ABCD 或类似非零值。在 target.exe 的屏幕上弹出 MessageBox点掉之后 C:\lab\injected_log.txt 出现内容包含目标进程的 PID 和线程 ID。打开 Process Explorer找到 target.exe确认其模块列表里出现 myinject.dll 以及它的加载基址。target.exe 的代码很简单#include windows.h #include stdio.h int main() { printf(target.exe 进程ID: %lu\n, GetCurrentProcessId()); printf(按回车键退出...\n); getchar(); return 0; }这里我特意没有用 notepad.exe 当靶子。Windows 11 的记事本早就变成了商店版 App进程模型和消息机制都跟经典桌面应用不一样注入进去经常遇到权限或加载位置的问题干扰因素太多。自己写一个 target.exe 只留一个 getchar注入成功与否一目了然。提示注入器和目标进程的完整性级别必须匹配。如果 target.exe 以管理员身份运行而注入器是普通权限OpenProcess 会返回错误码 5拒绝访问。反过来也一样。测试时最简单的方式是两者都用普通权限启动。4. 复现实验二64位纯代码注入无DLL手写Shellcode桩4.1 为什么x86桩直接照搬会崩溃实验一的 DLL 注入解决了“能不能加载我的代码”的问题但书里还有另一类更硬核的玩法不引入任何 DLL直接把一段机器码写进目标进程开个远程线程执行。这段机器码通常被称为“桩”stub或 shellcode。书里的 x86 实验会写类似这样的桩把 MessageBoxA 的参数一个个 push 进栈再把 MessageBoxA 的入口地址塞进 EAXcall 过去。这个思路在 x86 上成立拿到 x64 上直接崩原因就在 1.2 节的表里x64 函数参数不进栈进寄存器栈还要 16 字节对齐。我一开始天真地把 x86 桩的机器码逐字节“翻译”成 64 位版本比如把 push 改成 mov rcx 之类结果远程线程一启动目标进程就崩x64dbg 一看是访问违例。后来才意识到 x64 下参数传递和数据寻址都需要重新设计尤其是字符串指针和函数地址带不进机器码指令里的那个瞬间——你需要一套运行时才填进去的“占位地址”机制。4.2 64位注入桩设计含机器码偏移表我设计的 x64 桩采用“运行前打补丁”的思路桩本体里预留三处 8 字节占位注入器在 WriteProcessMemory 之前把文本指针、标题指针、MessageBoxW 地址这三个真实值填进去。这样桩本身完全位置无关不需要在运行期做 RIP 相对寻址逻辑最直白也最容易验证。桩的机器码布局如下我手工核对了每条指令的长度和偏移这部分建议自己也跟着算一遍正好练习读机器码偏移机器码汇编含义备注0x0048 83 EC 40sub rsp, 0x40预留影子区并凑 16 字节对齐0x0433 C9xor ecx, ecx第1个参数 hWnd NULL0x0648 BA [8字节]mov rdx, imm64第2个参数文本指针运行时填充0x1049 B8 [8字节]mov r8, imm64第3个参数标题指针运行时填充0x1A41 B9 00 00 00 00mov r9d, 0第4个参数 uType MB_OK0x2048 B8 [8字节]mov rax, imm64MessageBoxW 地址运行时填充0x2AFF D0call rax调用 MessageBoxW0x2C48 83 C4 40add rsp, 0x40恢复栈0x3233 C0xor eax, eax清零返回值0x34C3ret线程入口返回字符串从 0x35 开始连续排放先放文本再放标题。三处占位地址的写入位置分别是桩 0x08rdx 的立即数、桩 0x12r8 的立即数、桩 0x22rax 的立即数。这个偏移表是整个实验二最花时间的部分建议准备一张草稿纸逐条指令算长度算完再对照反汇编验证。提示分配远程内存时代码页必须用 PAGE_EXECUTE_READWRITE。我用 PAGE_READWRITE 分配时远程线程一启动目标进程就触发 DEP 保护直接 0xC0000005。这是 x64 时代和书当年环境差异最大的地方之一。4.3 注入器组装与地址填充注入器的核心工作是把上面的字节数组组装好再把真实地址填进去。代码如下#include windows.h #include tlhelp32.h #include psapi.h #include iostream #include vector #include cstring static DWORD FindProcessIdByName(const wchar_t* name) { /* 同实验一 */ } int main() { DWORD pid FindProcessIdByName(Ltarget.exe); if (pid 0) return 1; HANDLE hProcess OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, pid); if (!hProcess) return 1; const BYTE rawStub[] { 0x48, 0x83, 0xEC, 0x40, 0x33, 0xC9, 0x48, 0xBA, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x49, 0xB8, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x41, 0xB9, 0x00, 0x00, 0x00, 0x00, 0x48, 0xB8, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0xFF, 0xD0, 0x48, 0x83, 0xC4, 0x40, 0x33, 0xC0, 0xC3 }; const char* text Stub injection on x64; const char* title Lab; const DWORD kTextOff 0x35; const DWORD kTitleOff kTextOff (DWORD)strlen(text) 1; std::vectorBYTE payload(rawStub, rawStub sizeof(rawStub)); payload.resize(kTitleOff strlen(title) 1); memcpy(payload.data() kTextOff, text, strlen(text) 1); memcpy(payload.data() kTitleOff, title, strlen(title) 1); LPVOID remoteBuf VirtualAllocEx(hProcess, NULL, payload.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (!remoteBuf) { /* 错误处理 */ } // ---- 严谨版在目标进程中定位 user32.dll 和 MessageBoxW ---- DWORD needed 0; EnumProcessModulesEx(hProcess, NULL, 0, needed, LIST_MODULES_ALL); std::vectorHMODULE mods(needed / sizeof(HMODULE)); EnumProcessModulesEx(hProcess, mods.data(), needed, needed, LIST_MODULES_ALL); ULONGLONG targetUser32 0; for (auto m : mods) { wchar_t name[MAX_PATH] {0}; if (GetModuleBaseNameW(hProcess, m, name, MAX_PATH) _wcsicmp(name, Luser32.dll) 0) { targetUser32 (ULONGLONG)m; break; } } HMODULE localUser32 GetModuleHandleW(Luser32.dll); FARPROC localMsg GetProcAddress(localUser32, MessageBoxW); ULONGLONG rva (ULONGLONG)localMsg - (ULONGLONG)localUser32; ULONGLONG targetMsg targetUser32 rva; // ---- 填充桩内占位 ---- *(ULONGLONG*)(payload.data() 0x08) (ULONGLONG)((BYTE*)remoteBuf kTextOff); *(ULONGLONG*)(payload.data() 0x12) (ULONGLONG)((BYTE*)remoteBuf kTitleOff); *(ULONGLONG*)(payload.data() 0x22) (ULONGLONG)targetMsg; SIZE_T written 0; WriteProcessMemory(hProcess, remoteBuf, payload.data(), payload.size(), written); HANDLE hThread CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuf, NULL, 0, NULL); if (hThread) { WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); } VirtualFreeEx(hProcess, remoteBuf, 0, MEM_RELEASE); CloseHandle(hProcess); return 0; }这里我用了“严谨版”的地址解析通过 EnumProcessModulesEx 在目标进程模块列表里找 user32.dll 的真实基址再用本进程里算出的 MessageBoxW RVA 相加。相比直接拿本进程的 GetProcAddress 结果去用这个方法不依赖“系统 DLL 在所有进程基址一致”的假设逻辑上更干净也顺带演示了跨进程模块查询的常用姿势。实验一之所以没用这套是因为 kernel32.dll 在所有标准进程里必然加载且基址一致性在实测中足够可靠少一层代码就少一个出错点。如果你的目标是快速验证可以直接把第四步换成FARPROC pMsg GetProcAddress(GetModuleHandleW(Luser32.dll), MessageBoxW);然后填*(ULONGLONG*)(payload.data() 0x22) (ULONGLONG)pMsg;。我在实验机上两种写法都跑通了。5. 真实踩坑记录与问题排查5.1 六个高频问题的现场实录复现过程中我先后踩了六个坑每个都花了不少时间定位。第一个OpenProcess 返回错误码 5。当时 target.exe 是以管理员身份开的注入器是普通权限权限不对称直接拒绝。把两边都调到同一完整性级别后解决。第二个远程线程创建成功但 MessageBox 不弹Process Explorer 里也看不到任何模块加载。最后用 x64dbg 附加目标进程发现线程入口执行到了非法指令——因为我错误地把 x86 桩编译成 64 位进程的目标机器码在实际执行时根本不是预期指令。这是位数不匹配的典型症状。第三个VirtualAllocEx 用 PAGE_READWRITE 分配代码内存CreateRemoteThread 一启动目标进程直接崩溃。这是 DEP 拦截改成 PAGE_EXECUTE_READWRITE 解决。真实的注入工具通常会把“写”和“执行”分开先 PAGE_READWRITE 写完再改 PAGE_EXECUTE_READ减少暴露面实验里用读写执行一体页最省事。第四个远程线程已结束但 MessageBox 还没弹出来就消失了。原因是 WaitForSingleObject 返回后立刻 VirtualFreeEx 释放了远程内存此时 MessageBox 的代码已经执行完、正在显示模态窗口属于正常现象等用户点掉才会继续。如果不想让线程提前退出反而说要等窗口关闭就别在注入器里傻等线程结束只管关闭句柄即可。第五个32 位 DLL 硬塞给 64 位进程。远程线程确实创建了LoadLibraryW 也确实被调用了但返回值为 NULL错误码 193不是有效的 Win32 应用程序。这个没法“调通”只能重新编译 DLL 为 64 位。第六个首次运行注入器被 Windows Defender 拦截。这不是逻辑问题是行为特征问题。解决方式是把实验目录加入排除项同时在真实项目里要意识到任何 CreateRemoteThread PAGE_EXECUTE_READWRITE 的组合都极其显眼EDR 类产品几乎一抓一个准。5.2 一个容易被忽略的小技巧读取远程线程退出码很多人写完注入器就收工了从不看远程线程的退出码。这个习惯在实验一二里会漏掉关键信息。因为 CreateRemoteThread 的线程函数是 LoadLibraryW 本身所以线程退出码其实就是 LoadLibraryW 的返回值也就是目标进程里 DLL 实例的 HMODULE。如果退出码是 0说明 LoadLibrary 失败了后面所有验证都白搭。具体姿势WaitForSingleObject 等线程结束然后 GetExitCodeThread 取退出码非零即成功。我在实验一的注入器里已经把这个值打印出来了实验二也可以照做。这个小技巧在野外的排查价值极高凡是“远程线程创建成功但目标进程毫无反应”的场景先看退出码基本能区分是加载失败还是业务代码没执行。5.3 常见问题速查表为了方便以后回看我把常见问题整理成一张表症状可能原因处理方式OpenProcess 返回 5完整性级别不匹配 / 目标受保护统一权限级别PPL 进程无解远程内存写不进返回 299部分 WriteProcessMemory 成功检查写入长度和指针计算线程创建成功但 MessageBox 不弹位数不匹配 / x86 桩给 x64确认两边同为 x64目标进程崩溃异常码 0xC0000005代码页无 EXECUTE / 地址修正错误改 PAGE_EXECUTE_READWRITE核对偏移LoadLibrary 返回值 0 / 错误码 193注入的 DLL 位数不对重新编译 DLL 为 x64注入后目标进程无响应DllMain 里做了重活 / loader lock移到工作线程C4996 fopen 报错VS 安全函数检查用 fopen_s 或宏关闭警告链接 LNK1112 / LNK2019依赖库位数不匹配 / 缺 lib全套 x64补 psapi.lib 等这张表基本覆盖了这次实验的绝大多数出问题场景。其中“加载不出来”这一类我的经验是先从位数查起再从权限查起最后才怀疑代码逻辑本身。6. 实验之外的一些思考6.1 这套老技术在今天的位置有人会觉得都 2025 年了VirtualAllocEx WriteProcessMemory CreateRemoteThread 这套组合已经烂大街不值得学。我的看法相反。这套组合是理解现代注入类工具的基础也是很多安全软件、调试器、挂钩框架的底层原语。你看 MinHook、Detours 这类库的底层实现照样离不开“改进程内存、改指令开头”这些动作只是在“怎么改得更隐蔽、更稳定”上做了大量工程化。在游戏逆向和反外挂研究领域这套原语更是绕不开的地基。你只有理解了远程线程是怎么创建的、内存页的 EXECUTE 标志意味着什么、EDR 为什么盯着 CreateRemoteThread才能真正看懂反作弊系统在检测什么以及为什么某些注入手法在实战里活不过三秒。学习层面我的建议是把它当作一个“进程内代码执行的最小实现”来研究而不是当攻击工具来背流程。6.2 防御视角为什么实验变难了Windows 11 上复现这个实验最大的感受不是“做不到”而是“每一步都在被系统盯着”。DEP 先拦一道ASLR 让硬编码地址失效受保护进程机制让 OpenProcess 直接吃闭门羹EDR 则盯着 VirtualAllocEx 的页权限和 CreateRemoteThread 的调用链。CFG、ACG 这些更后面的防御机制一旦开启单纯靠这几条 API 的玩法基本归零。这是好事。它逼着研究者往更底层走NtCreateThreadEx、APC 注入、硬件断点、回调机制甚至直接手写 PEB 遍历去解析函数地址。书里的 x86 时代是“学一个 API 就能跑通实验”现在的 x64 时代是“理解机制才能真正跑通实验”。从这个角度说这次复现对我最大的收获不是成功弹出了 MessageBox而是把当年囫囵吞枣的“原理”真正落到了寄存器、机器码和系统行为层面。最后再分享一个个人习惯以后凡是遇到这种“老书实验迁移到新系统”的题目我都会先列一张 x86 vs x64 的差异表再动手改代码。机器码偏移逐字节核对、远程线程退出码必查、内存页权限必看这三件事做好剩下的只是时间问题。这本书的代码注入章节在 64 位 Windows 11 上跑通的那一瞬间比当年在虚拟机上跑通更有成就感因为你知道自己是真正理解了它。

相关新闻

LeetCode 92 反转链表II全解:头插法、递归与边界避坑指南

LeetCode 92 反转链表II全解:头插法、递归与边界避坑指南

刚刷题群里有朋友问我:LeetCode 103 反转链表 II 这题怎么解?其实光这个说法本身,就藏着新手最常见的一个坑——LeetCode 上编号 103 的题目是二叉树的锯齿形层序遍历,而真正叫“反转链表 II”的是第 92 题,标题里的 1…

2026/10/7 10:30:08 阅读更多 →
论坛社区源码部署与APP封装实战:从解压到上线的完整指南

论坛社区源码部署与APP封装实战:从解压到上线的完整指南

简介:这是一套完整的APP论坛社区源码包,涵盖网站端PHP后台、HTML前端、JavaScript交互逻辑以及APP封装安装包,面向需要快速搭建社区论坛、学习PHP开发或进行二次开发的站长与开发者。压缩包共含1095个文件,以PHP、HTML、JS、CSS为…

2026/10/7 10:30:08 阅读更多 →
EDGE浏览器打印方向错乱?从原理到修复彻底解决纵横旋转问题

EDGE浏览器打印方向错乱?从原理到修复彻底解决纵横旋转问题

很多人第一次遇到EDGE浏览器打印预览界面纵横旋转错误,都会觉得是浏览器坏了。比如你打开一个文档页,想用A4纸纵向打印,点“打印”后预览却变成了横向排版,文字从中间被裁开,右侧空出一大片;又比如你在预览…

2026/10/7 10:30:08 阅读更多 →

最新新闻

基于Qwen3-VL-Embedding-8B的语义文搜图系统实践

基于Qwen3-VL-Embedding-8B的语义文搜图系统实践

1. 为什么选 Qwen3-VL-Embedding-8B 做语义级文搜图1.1 从"标签检索"到"语义检索"先说个很常见的场景。你手头有一批商品图、素材图或者本地相册,想找到"一只橘猫趴在窗台上晒太阳"的图片。如果按老办法,你得先给每张图打…

2026/10/7 11:07:55 阅读更多 →
Agent-Reach 实战:用 CLI 为 AI Agent 构建稳定触达层

Agent-Reach 实战:用 CLI 为 AI Agent 构建稳定触达层

1. 从"Agent-Reach"这个名字说起:它到底想解决什么问题第一次看到 Agent-Reach 这个项目名,我的直觉是它跟"让 AI Agent 够得着东西"有关。Reach 这个词在工程语境里通常有两层意思:一是"触达",二是…

2026/10/7 11:07:55 阅读更多 →
Windows远程文件共享安全加固:从共享权限到SMB协议防护

Windows远程文件共享安全加固:从共享权限到SMB协议防护

做Windows远程文件共享这件事,每年都能碰到一堆翻车现场。最常见的姿势是这样的:右键文件夹 → 属性 → 共享 → 下拉列表选Everyone → 权限改成完全控制 → 确定。然后呢,内网里任何一台机器都能往里写东西,运气差点&#xff0c…

2026/10/7 11:07:55 阅读更多 →
Ubuntu新手生存指南:Shell、文件系统与命令管道底层逻辑

Ubuntu新手生存指南:Shell、文件系统与命令管道底层逻辑

1. 项目概述:这不是一份“教程”,而是一张Ubuntu新手的生存地图你刚装好Ubuntu,桌面看着清爽,图标点得挺顺,但一打开终端,光标在那儿闪——像在等你下命令,又像在嘲笑你手足无措。你搜“ubuntu怎…

2026/10/7 11:07:55 阅读更多 →
物联网断路器设计全解析:硬件架构、通信协议与云端接入实践

物联网断路器设计全解析:硬件架构、通信协议与云端接入实践

简介:针对传统断路器缺乏智能监控与远程控制的问题,这份PDF完整介绍了物联网断路器的设计过程,适合电气自动化、嵌入式开发和智能电网方向的学习者参考。文档从系统总体方案入手,依次讲解传感器模块电路、主控电路、软件程序设计、…

2026/10/7 11:07:55 阅读更多 →
鸡蛋掉落:从朴素DP到最优解的动态规划进阶指南

鸡蛋掉落:从朴素DP到最优解的动态规划进阶指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 11:06:55 阅读更多 →

日新闻

ROS2机械臂仿真与运动控制:从URDF建模到Gazebo实战全解析

ROS2机械臂仿真与运动控制:从URDF建模到Gazebo实战全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:01:58 阅读更多 →
用浏览器直接改ESP32的WiFi密码:NVS键值配置工具设计与实现

用浏览器直接改ESP32的WiFi密码:NVS键值配置工具设计与实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:02:00 阅读更多 →
芯片封装缺陷检测:扫描声学显微镜(SAT)原理与实操指南

芯片封装缺陷检测:扫描声学显微镜(SAT)原理与实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:02:00 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 7:15:40 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 5:29:09 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 9:29:10 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 8:21:32 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 4:21:51 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/6 1:18:13 阅读更多 →