18、注入技术(上):DLL注入原理与实战
大家好,欢迎来到逆向工程的第十八讲。今天咱们聊点硬核的——DLL注入。说实话,这玩意儿在安全圈里名声不太好,但作为安全研究员,你必须要懂。不懂注入,你就没法理解恶意软件是怎么潜伏的,也没法做深入的内存分析。
我最早接触DLL注入,是在分析一个勒索软件样本的时候。那个样本把自己伪装成系统更新程序,通过注入把核心逻辑塞进了explorer.exe。当时我花了整整两天才把它的注入链理清楚。从那以后,我就下定决心,必须把注入技术吃透。
核心概念:DLL注入的本质,就是让一个目标进程主动加载我们指定的DLL。一旦DLL被加载,它就在目标进程的地址空间内执行,拥有和该进程相同的权限和资源访问能力。
18.1 为什么需要DLL注入?
你可能会问:我直接写个exe跑起来不就行了?为什么要费劲去注入?
原因其实很简单。很多安全软件、游戏反作弊系统、甚至操作系统本身,都会限制外部进程的访问权限。但如果你能成为目标进程的一部分——比如成为notepad.exe的一个线程——那你就能绕过很多限制。
我举个例子。你在分析一个恶意软件时,想hook它的某个API调用。如果你从外部做,很容易被检测到。但如果你注入一个DLL进去,在内部做hook,那就隐蔽得多。
常见的应用场景包括:
- 安全软件:监控进程行为、拦截恶意API调用
- 游戏外挂:读取内存数据、修改游戏逻辑(嗯,这个不提倡)
- 调试工具:注入hook DLL进行函数追踪
- 恶意软件:窃取信息、持久化驻留
18.2 两种经典注入方式
注入方式有很多种,但最经典、最基础的,就是下面这两种。我个人建议初学者先把这两个吃透,其他的都是变种。
18.2.1 CreateRemoteThread 注入
这是最直接的方式。说白了,就是在目标进程里创建一个远程线程,让这个线程去调用LoadLibrary加载你的DLL。
具体步骤是这样的:
- 用
OpenProcess打开目标进程,获取句柄 - 用
VirtualAllocEx在目标进程里分配一块内存 - 用
WriteProcessMemory把DLL路径写进去 - 用
CreateRemoteThread在目标进程里启动一个线程 - 线程入口点设为
LoadLibrary,参数就是刚才写的DLL路径
代码实现大概长这样:
// 获取目标进程句柄
HANDLE hProcess = OpenProcess(
PROCESS_ALL_ACCESS, FALSE, dwProcessId);
// 在目标进程分配内存
LPVOID pRemoteMemory = VirtualAllocEx(
hProcess, NULL, 256,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// 写入DLL路径
WriteProcessMemory(hProcess, pRemoteMemory,
szDllPath, strlen(szDllPath) + 1, NULL);
// 获取LoadLibrary地址
LPTHREAD_START_ROUTINE pLoadLibrary =
(LPTHREAD_START_ROUTINE)GetProcAddress(
GetModuleHandle("kernel32.dll"),
"LoadLibraryA");
// 创建远程线程
HANDLE hThread = CreateRemoteThread(
hProcess, NULL, 0,
pLoadLibrary, pRemoteMemory, 0, NULL);
// 等待线程结束
WaitForSingleObject(hThread, INFINITE);
// 清理
CloseHandle(hThread);
CloseHandle(hProcess);
注意:这种方法在x64系统上注入32位进程到64位进程时会失败。因为LoadLibrary的地址在不同位数进程间不通用。我曾经在这个坑里爬了整整一个下午。
18.2.2 SetWindowsHookEx 注入
这种方法更隐蔽一些。它利用Windows的消息钩子机制,当目标进程处理某个消息时,系统会自动把钩子DLL加载进去。
我个人的经验是,这种方法适合做键盘记录或者消息监控。但缺点也很明显——它依赖消息队列,如果目标进程没有消息循环,那就白搭。
实现步骤:
- 编写一个DLL,里面实现钩子过程(Hook Procedure)
- 在DLL中,
DllMain里做你想做的事 - 用
SetWindowsHookEx安装钩子 - 指定钩子类型(比如
WH_KEYBOARD)和DLL句柄
// 安装全局钩子
HHOOK hHook = SetWindowsHookEx(
WH_KEYBOARD_LL, // 低级键盘钩子
(HOOKPROC)HookProcedure, // 钩子函数
hInstance, // DLL模块句柄
0); // 0表示全局钩子
// 钩子过程
LRESULT CALLBACK HookProcedure(
int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode >= 0)
{
// 在这里处理按键事件
// 或者执行你的注入逻辑
}
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
小技巧:SetWindowsHookEx注入的DLL会被映射到所有拥有消息队列的进程中。如果你只想注入特定进程,可以在钩子过程里检查当前进程ID,不是目标就跳过。
18.3 进程间通信(IPC)
DLL注入进去了,然后呢?你得和它通信吧?不然你都不知道它有没有成功加载,也不知道它干了什么。
这里我介绍两种最常用的IPC方式。
18.3.1 命名管道
管道就像一根水管,一头连着你(注入器),一头连着目标进程里的DLL。数据从一头流到另一头。
我比较喜欢用命名管道,因为它简单可靠。服务端(通常是注入器)创建管道,客户端(DLL)连接上来,然后就可以双向通信了。
// 服务端(注入器)
HANDLE hPipe = CreateNamedPipe(
"\\\\.\\pipe\\MyInjectPipe",
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE,
PIPE_UNLIMITED_INSTANCES,
1024, 1024, 0, NULL);
// 等待客户端连接
ConnectNamedPipe(hPipe, NULL);
// 发送命令
char cmd[] = "DO_SOMETHING";
WriteFile(hPipe, cmd, strlen(cmd), &written, NULL);
// 客户端(DLL)
HANDLE hPipe = CreateFile(
"\\\\.\\pipe\\MyInjectPipe",
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
// 读取命令
char buffer[1024];
ReadFile(hPipe, buffer, 1024, &read, NULL);
18.3.2 共享内存
共享内存更快,因为它不需要数据拷贝。说白了,就是两个进程共同映射同一块物理内存。
在Windows上,我们用CreateFileMapping和MapViewOfFile来实现。
// 创建共享内存
HANDLE hMapFile = CreateFileMapping(
INVALID_HANDLE_VALUE,
NULL,
PAGE_READWRITE,
0,
4096, // 4KB共享内存
"Global\\MySharedMemory");
// 映射到进程地址空间
LPVOID pSharedMem = MapViewOfFile(
hMapFile,
FILE_MAP_ALL_ACCESS,
0, 0, 4096);
// 写入数据
strcpy((char*)pSharedMem, "Hello from injector!");
// 在DLL中读取
HANDLE hMapFile = OpenFileMapping(
FILE_MAP_ALL_ACCESS,
FALSE,
"Global\\MySharedMemory");
LPVOID pSharedMem = MapViewOfFile(
hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 4096);
printf("%s", (char*)pSharedMem);
对比总结:管道适合小数据量、需要同步的场景;共享内存适合大数据量、追求性能的场景。我一般混合使用——用共享内存传数据,用管道传控制信号。
18.4 实战:编写一个简单的DLL注入器
好了,理论说完了,咱们动手写一个。这个注入器支持两种注入方式,并且通过命名管道和注入的DLL通信。
先看整体架构:
注入器的核心代码,我把它封装成了一个类:
class Injector
{
public:
bool Inject(DWORD pid, const char* dllPath)
{
// 1. 打开目标进程
HANDLE hProcess = OpenProcess(
PROCESS_ALL_ACCESS, FALSE, pid);
if (!hProcess) return false;
// 2. 在目标进程分配内存
size_t pathLen = strlen(dllPath) + 1;
LPVOID pRemoteMem = VirtualAllocEx(
hProcess, NULL, pathLen,
MEM_COMMIT, PAGE_READWRITE);
if (!pRemoteMem) {
CloseHandle(hProcess);
return false;
}
// 3. 写入DLL路径
WriteProcessMemory(hProcess, pRemoteMem,
dllPath, pathLen, NULL);
// 4. 获取LoadLibrary地址
HMODULE hKernel32 = GetModuleHandle("kernel32.dll");
LPTHREAD_START_ROUTINE pLoadLib =
(LPTHREAD_START_ROUTINE)GetProcAddress(
hKernel32, "LoadLibraryA");
// 5. 创建远程线程
HANDLE hThread = CreateRemoteThread(
hProcess, NULL, 0,
pLoadLib, pRemoteMem, 0, NULL);
if (!hThread) {
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return false;
}
// 6. 等待加载完成
WaitForSingleObject(hThread, 5000);
// 7. 清理
CloseHandle(hThread);
CloseHandle(hProcess);
return true;
}
};
对应的DLL代码也很简单:
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD reason, LPVOID lpReserved)
{
if (reason == DLL_PROCESS_ATTACH)
{
// 创建通信线程
CreateThread(NULL, 0,
CommunicationThread, NULL, 0, NULL);
}
return TRUE;
}
DWORD WINAPI CommunicationThread(LPVOID lpParam)
{
// 连接命名管道
HANDLE hPipe = CreateFile(
"\\\\.\\pipe\\InjectPipe",
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL);
if (hPipe == INVALID_HANDLE_VALUE)
return 1;
// 循环处理命令
char buffer[256];
DWORD read;
while (ReadFile(hPipe, buffer, 256, &read, NULL))
{
// 解析命令并执行
if (strcmp(buffer, "GET_PROCESS_INFO") == 0)
{
// 返回进程信息
char info[256];
sprintf(info, "PID: %d, Threads: %d",
GetCurrentProcessId(),
GetCurrentThreadId());
WriteFile(hPipe, info, strlen(info), &written, NULL);
}
}
CloseHandle(hPipe);
return 0;
}
避坑指南:我曾经在DLL里直接调用MessageBox做测试,结果目标进程是服务程序,没有桌面,消息框弹不出来,整个注入就卡死了。记住,在DLL里不要做任何UI操作,除非你确定目标进程有窗口。
18.5 检测与防御
作为安全研究员,光会注入还不够,你还得知道怎么防。这里我简单提几个检测点:
| 检测方法 | 原理 | 绕过思路 |
|---|---|---|
| 枚举进程模块 | 用CreateToolhelp32Snapshot枚举所有加载的DLL |
使用反射式注入,不经过LoadLibrary |
| 检测远程线程 | 用Thread32First检查线程入口点 |
使用APC注入或线程劫持 |
| Hook关键API | Hook CreateRemoteThread等函数 |
使用内核态注入或硬件断点 |
| 内存扫描 | 扫描可疑的内存区域 | 加密DLL内容,运行时解密 |
嗯,这里要注意,检测和绕过是一个猫鼠游戏。没有绝对安全的系统,也没有绝对隐蔽的注入。作为安全研究员,我们的目标是理解这些技术,而不是滥用它们。
好了,这一讲的内容就到这里。DLL注入是逆向工程中非常基础也非常重要的技术。我建议你亲手写一个注入器,注入到notepad.exe里试试。只有亲手踩过坑,才能真正理解这些API的细节。
课后练习:写一个DLL注入器,支持CreateRemoteThread和SetWindowsHookEx两种方式。注入的DLL通过共享内存向注入器报告目标进程的当前工作目录。试试看,能不能在10分钟内搞定?
公众号:蓝海资料掘金营,微信deep3321