概述
注入方法
CreateRemoteThread + LoadLibraryA/W(最经典)SetWindowsHookEx(钩子注入)QueueUserAPC + LoadLibraryA(APC注入)- 注册表/配置文件 +
AppInit_DLLs(系统级) - 内存补丁 + 改执行流 (
Inline Hook/代码补丁,高级)
CreateRemoteThread + LoadLibraryA/W(线程)
原理
- 在目标进程里创建一个新线程,让这个线程的入口点指向
LoadLibraryA(dllpath),DLL一加载就自动执行DllMain
理解
- 为什么可以让新创建的线程的入口点指向
LoadLibraryA(dllpath)?- 因为
LoadLibraryA的函数签名恰好符合线程入口函数的约定
- 因为
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
HANDLE CreateRemoteThread( HANDLE hProcess, LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, // ← 重点:线程入口地址 LPVOID lpParameter, // ← 线程参数 DWORD dwCreationFlags, LPDWORD lpThreadId ); // CreateRemoteThread 不关心 lpStartAddress 指向的是什么函数 typedef DWORD (WINAPI *LPTHREAD_START_ROUTINE)(LPVOID lpParam); HMODULE WINAPI LoadLibraryA(LPCSTR lpLibFileName); |
- 新线程什么时机执行?
CreateRemoteThread返回时,线程已创建但不一定已运行(取决于内核调度)- 用
WaitForSingleObject主动等待线程结束,期间当前线程会释放CPU
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
当前线程 目标进程 | | CreateRemoteThread() | ↓ (系统调用) | 内核: 创建新线程结构 | 新线程.RIP = LoadLibraryA 地址 | 新线程.RCX = lpDllPath | 新线程.状态 = Ready (准备执行,但还没被调度) | ← CreateRemoteThread 返回(hThread 和 dwThreadId) | | WaitForSingleObject(hThread, INFINITE) ← 阻塞等待 | (当前线程进入等待,释放 CPU) | | 目标进程的另一个线程: | 内核调度器选择新线程运行 | 新线程开始执行 LoadLibraryA | ... 加载 DLL ... | LoadLibraryA 返回,线程结束 | | ← WaitForSingleObject 返回(新线程已结束) | (当前线程被唤醒) | | GetExitCodeThread(hThread, &dwExitCode) | dwExitCode = DLL 基址 | |
实现
OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid)拿目标进程句柄VirtualAllocEx(hProcess, nullptr, dllpath_len, MEM_COMMIT, PAGE_READWRITE)在目标进程分配内存放DLL路径WriteProcessMemory(hProcess, lpRemote, dllpath, dllpath_len, nullptr)把DLL路径字符串写进去CreateRemoteThread(hProcess, nullptr, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpRemote, 0, nullptr)创建线程,入口是LoadLibraryA,参数是DLL路径地址WaitForSingleObject(hThread, INFINITE)等线程完成
特点
- 优点
- 最直白、最兼容,
Win2000+都能用 - 不需要知道目标进程内存布局
- 代码短,容易理解
- 最直白、最兼容,
- 缺点
- 创建一个新的可见线程,有
PID、有栈,痕迹明显(调试器一眼能看到) - 如果目标进程做了线程白名单限制(某些反
cheat),这条线程可能被识别为异常
- 创建一个新的可见线程,有
示例代码
app,目标进程- 先启动目标进程,然后启动注入器,传入
pid和dll全路径
- 先启动目标进程,然后启动注入器,传入
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#include <windows.h> #include <cstdio> #include <thread> #include <chrono> int main() { printf("========================================\n"); printf("目标应用程序 (TargetApp.exe)\n"); printf("PID: %lu\n", GetCurrentProcessId()); printf("========================================\n"); printf("\n"); printf("这个程序会持续运行 2 分钟\n"); printf("在这段时间内,你可以用 Injector.exe 向它注入 DLL\n"); printf("\n"); printf("例如,打开另一个命令行窗口,运行:\n"); printf(" Injector.exe %lu C:\\path\\to\\TargetDll.dll\n", GetCurrentProcessId()); printf("\n"); printf("DLL 加载日志会写到: C:\\temp\\dll_log.txt\n"); printf("\n"); // 主循环 - 每秒打印一次进程信息 for (int i = 0; i < 120; ++i) { printf("[%3d秒] 进程运行中... (PID=%lu)\n", i, GetCurrentProcessId()); // 模拟一些工作(这样使用 CPU) for (int j = 0; j < 100; ++j) { volatile int dummy = i * j; // 防优化 } Sleep(1000); // 等待 1 秒 } printf("\n程序即将退出\n"); return 0; } |
- 要注入的
dll
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
// dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "pch.h" #include <windows.h> #include <cstdio> void LogToFile(const char* message) { FILE* f = fopen("d:\\download_repo\\fadfss\\test\\log\\dll_log.txt", "a"); if (f) { fprintf(f, "[TargetDll] %s\n", message); fclose(f); } } BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) { switch (dwReason) { case DLL_PROCESS_ATTACH: { // DLL 首次被加载到进程 LogToFile("DLL_PROCESS_ATTACH: DLL 被加载!"); // 这里可以做任何初始化工作 // 例如:hook 某些 API、启动监控线程、修改进程行为等 // MessageBox 演示(如果目标进程有 GUI) // MessageBoxA(nullptr, "DLL Injected!", "Success", MB_OK); // 演示:输出一些进程信息到日志 char buf[256]; sprintf_s(buf, sizeof(buf), "进程ID=%lu, 线程ID=%lu", GetCurrentProcessId(), GetCurrentThreadId()); LogToFile(buf); break; } case DLL_PROCESS_DETACH: { // DLL 即将被卸载(进程退出或显式 FreeLibrary) LogToFile("DLL_PROCESS_DETACH: DLL 即将卸载"); // 清理资源 break; } case DLL_THREAD_ATTACH: { // 新线程被创建 // LogToFile("DLL_THREAD_ATTACH"); break; } case DLL_THREAD_DETACH: { // 线程即将终止 // LogToFile("DLL_THREAD_DETACH"); break; } } return TRUE; } extern "C" __declspec(dllexport) void HelloFromDll() { LogToFile("HelloFromDll() 被调用!"); MessageBoxA(nullptr, "Hello from injected DLL!", "DLL Export", MB_OK | MB_ICONINFORMATION); } |
- 注入器
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
#include <windows.h> #include <cstdio> #include <cstdlib> #include <cstring> void WaitForProcessReady(DWORD pid) { Sleep(500); // 简单粗暴,实战中可以用更复杂的同步机制 } bool InjectDLL(DWORD targetPID, const char* dllPath) { printf("[*] 注入器启动\n"); printf("[*] 目标PID: %lu\n", targetPID); printf("[*] DLL路径: %s\n", dllPath); // 1. 打开目标进程 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPID); if (!hProcess) { printf("[!] OpenProcess 失败, GetLastError=%lu\n", GetLastError()); return false; } printf("[+] OpenProcess 成功, 句柄=%p\n", hProcess); // 2. 在目标进程分配内存,放 DLL 路径 size_t dllPathLen = strlen(dllPath) + 1; // +1 for null terminator LPVOID lpRemoteDllPath = VirtualAllocEx( hProcess, nullptr, dllPathLen, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (!lpRemoteDllPath) { printf("[!] VirtualAllocEx 失败, GetLastError=%lu\n", GetLastError()); CloseHandle(hProcess); return false; } printf("[+] VirtualAllocEx 成功, 远程地址=%p, 大小=%zu\n", lpRemoteDllPath, dllPathLen); // 3. 把 DLL 路径写进目标进程内存 if (!WriteProcessMemory(hProcess, lpRemoteDllPath, (LPVOID)dllPath, dllPathLen, nullptr)) { printf("[!] WriteProcessMemory 失败, GetLastError=%lu\n", GetLastError()); VirtualFreeEx(hProcess, lpRemoteDllPath, 0, MEM_RELEASE); CloseHandle(hProcess); return false; } printf("[+] WriteProcessMemory 成功, 已将 DLL 路径写入目标进程\n"); // 4. 在目标进程创建远程线程 // 线程入口 = LoadLibraryA (kernel32.dll 导出的函数) // 线程参数 = 刚才分配的远程内存地址(DLL 路径) HANDLE hThread = CreateRemoteThread( hProcess, nullptr, 0, (LPTHREAD_START_ROUTINE)LoadLibraryA, // 线程入口 lpRemoteDllPath, // 线程参数(LoadLibraryA 的参数,即 DLL 路径) 0, nullptr); if (!hThread) { printf("[!] CreateRemoteThread 失败, GetLastError=%lu\n", GetLastError()); VirtualFreeEx(hProcess, lpRemoteDllPath, 0, MEM_RELEASE); CloseHandle(hProcess); return false; } printf("[+] CreateRemoteThread 成功, 线程句柄=%p\n", hThread); // 5. 等待远程线程执行完成 // LoadLibraryA 会返回 DLL 的基址(>0 表示成功) printf("[*] 等待远程线程执行...\n"); DWORD waitResult = WaitForSingleObject(hThread, INFINITE); if (waitResult != WAIT_OBJECT_0) { printf("[!] WaitForSingleObject 失败或超时\n"); } else { printf("[+] 远程线程执行完成\n"); } // 6. 获取线程退出码(LoadLibraryA 的返回值 = DLL 基址) DWORD threadExitCode = 0; if (GetExitCodeThread(hThread, &threadExitCode)) { if (threadExitCode != 0) { printf("[+] DLL 加载成功! 基址=%p\n", (LPVOID)threadExitCode); } else { printf("[!] DLL 加载失败! LoadLibraryA 返回 NULL\n"); } } // 7. 清理 // 注意:不释放目标进程的内存,因为 DLL 需要保持加载状态 // 如果要"注入后立即卸载",可以这样做: // VirtualFreeEx(hProcess, lpRemoteDllPath, 0, MEM_RELEASE); CloseHandle(hThread); CloseHandle(hProcess); printf("[+] 注入完成\n"); return true; } int main(int argc, char** argv) { if (argc < 3) { printf("用法: %s <目标进程PID> <DLL完整路径>\n", argv[0]); printf("例: %s 1234 C:\\path\\to\\TargetDll.dll\n", argv[0]); return 1; } DWORD targetPID = (DWORD)atoi(argv[1]); const char* dllPath = argv[2]; // 检查 DLL 文件是否存在(可选,但推荐) HANDLE hDllCheck = CreateFileA(dllPath, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr); if (hDllCheck == INVALID_HANDLE_VALUE) { printf("[!] DLL 文件不存在或无法打开: %s\n", dllPath); return 1; } CloseHandle(hDllCheck); printf("[+] DLL 文件存在\n"); // 执行注入 if (InjectDLL(targetPID, dllPath)) { printf("\n[+] 注入成功!\n"); return 0; } else { printf("\n[!] 注入失败\n"); return 1; } } |
SetWindowsHookEx (钩子)
原理
- 给目标进程的线程挂一个系统钩子(比如消息钩子),钩子回调函数在
DLL里,DLL一被加载就执行DllMain,回调函数可以自己卸钩
理解
- 比如注册了消息钩子,什么时机会触发这个消息?
- 不同的钩子类型,触发时机完全不同
- 比如
WH_GETMESSAGE钩子在目标线程调用GetMessage()或PeekMessage()API时触发
|
1 2 3 4 5 6 |
HHOOK SetWindowsHookEx( int idHook, // 钩子类型,比如 WH_GETMESSAGE HOOKPROC lpfn, // 钩子回调函数(在你的 DLL 里) HINSTANCE hmod, // DLL 的模块句柄 DWORD nThreadId // 目标线程 ID(0 = 全局钩子) ); |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
GetMessage() 被调用 ↓ GetMessage 内部执行(在目标线程里) ↓ 内核检查:"这个线程有没有装 WH_GETMESSAGE 钩子?" ↓ 有钩子 → 系统加载钩子 DLL(如果还没加载) → 调用钩子回调函数 → 回调函数执行(在目标线程的上下文里) → 回调函数返回 ↓ GetMessage 继续执行原逻辑(获取消息) ↓ GetMessage 返回 |
|
1 2 3 4 5 6 7 8 9 |
// 例子 1:注册到特定线程(线程 ID = 5678) SetWindowsHookEx(WH_GETMESSAGE, MyHookProc, hDll, 5678); // 只有线程 5678 调用 GetMessage 时,钩子才会触发 // 线程 1234、9999 的 GetMessage 不会触发这个钩子 // 例子 2:注册全局钩子(nThreadId = 0) SetWindowsHookEx(WH_GETMESSAGE, MyHookProc, hDll, 0); // 系统中所有线程的 GetMessage 都会触发这个钩子 // (但需要管理员权限,且有额外的系统性能影响) |
- 触发消息钩子时,由于回调函数在
DLL里面,如果此时DLL还没加载,会自动加载这个DLL吗?- 是的,系统会自动加载
|
1 2 3 4 5 6 7 8 9 10 11 |
// 步骤 1:你调用 SetWindowsHookEx 注册钩子 HHOOK hHook = SetWindowsHookEx(WH_GETMESSAGE, MyHookProc, hDll, 5678); // 注意:SetWindowsHookEx 返回时,DLL 还**不一定**被加载 // 步骤 2:目标线程(线程 5678)调用 GetMessage while (GetMessage(&msg, nullptr, 0, 0)) { // ← GetMessage 被调用 // 内核检查到有钩子 // 如果 DLL 还没加载 → 自动加载 DLL // ↓ 调用钩子回调 ... } |
实现
- 把要注入的
DLL编译成包含钩子回调函数 SetWindowsHookEx(WH_GETMESSAGE, DllCallbackFunc, hDllModule, targetThreadId)给目标线程挂钩子- 目标线程一旦处理消息,系统自动加载那个
DLL、调钩子 - 钩子函数里自己
UnhookWindowsHookEx卸钩,函数返回 DLL留在目标进程地址空间(或自己FreeLibrary)
特点
- 优点
- 不显式创建线程,更隐蔽
- 利用系统消息循环,不用
WaitForSingleObject
- 缺点
- 只能钩线程级钩子,需要知道目标线程
ID - 依赖消息循环,如果目标线程没有消息循环就无效
Win10+加强了钩子的安全检查,某些钩子类型被限制
- 只能钩线程级钩子,需要知道目标线程
QueueUserAPC + LoadLibraryA (APC )
原理
- 用
QueueUserAPC往目标线程的异步过程调用(APC)队列里塞一个调用,指向LoadLibraryA - 线程进入可警告状态(
alertable wait)时这个APC会被执行
理解
- 什么是线程的异步过程调用(
APC)队列?- 数据结构层面:每个线程都有一个私有的
APC队列,存放待执行的异步过程调用
- 数据结构层面:每个线程都有一个私有的
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 线程对象(内核维护,应用看不到,但这是真实存在的) struct THREAD { DWORD thread_id; // 线程 ID HANDLE process_handle; // 所属进程 CONTEXT registers; // CPU 寄存器(RIP, RSP, 等) // ↓ APC 队列(关键) LIST_ENTRY apc_queue; // 一个链表,存放 APC 请求 // LIST_ENTRY 指向一个链: // [APC#1] → [APC#2] → [APC#3] → NULL ULONG apc_pending_count; // 队列里有多少个待处理的 APC // ... 其他字段 ... }; // 每个 APC 的结构(内核维护) struct APC { LIST_ENTRY list_entry; // 链表节点,链到 apc_queue PAPCFUNC kernel_routine; // APC 的内核回调(可选) PAPCFUNC apc_routine; // ← 这就是你传给 QueueUserAPC 的函数 PVOID apc_context; // ← 这就是你传给 QueueUserAPC 的参数 // ... 其他字段 ... }; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 往某个线程的 APC 队列里加一个请求 QueueUserAPC( (PAPCFUNC)LoadLibraryA, // 要执行的函数 hThread, // 目标线程 (ULONG_PTR)lpDllPath // 函数的参数 ); // 内核会创建一个 APC 结构,加到该线程的 apc_queue 链表里 // 目前没有应用层 API 可以: // - 查看某个线程的 APC 队列内容 // - 删除队列里的某个 APC // - 优先级调整 APC // (这些都是内核级操作,应用看不到) |
- 什么是线程的可警告状态?
- 线程的可警告状态是指线程处于等待状态,且设置了允许
APC中断这个等待 - 关键的四个字:中断等待
- 线程的可警告状态是指线程处于等待状态,且设置了允许
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
┌─ 线程的等待状态 ─────────────────┐ │ │ │ ┌─ 可警告(alertable) wait ─┐ │ │ │ │ │ │ │ 等待中,但允许 APC 中断 │ │ │ │ → 如果有 APC,立刻中断 │ │ │ │ 等待,去执行 APC │ │ │ │ │ │ │ └──────────────────────────┘ │ │ │ │ ┌─ 不可警告 wait ──────────┐ │ │ │ │ │ │ │ 等待中,不允许 APC 中断 │ │ │ │ → 即使有 APC,也继续等待 │ │ │ │ (APC 堆积在队列里) │ │ │ │ │ │ │ └──────────────────────────┘ │ │ │ └──────────────────────────────────┘ |
|
1 2 3 4 5 6 7 8 9 10 |
// 不可警告的等待 WaitForSingleObject(hEvent, INFINITE); // 即使线程的 APC 队列里堆了 100 个 APC, // 这个等待也会一直卡住,APC 不会被执行 // 可警告的等待 WaitForSingleObjectEx(hEvent, INFINITE, TRUE); // ↑ // 第三个参数 = TRUE = 可警告 // 如果有 APC 在队列里,这个等待会被中断,APC 被执行 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 可警告状态的本质是允许 APC 中断当前的等待: 线程在等待 Event A: ① WaitForSingleObjectEx(hEvent, INFINITE, TRUE); // 可警告 其他线程往这个线程的 APC 队列里加了一个请求: ② QueueUserAPC(MyFunc, hThread, param); 内核马上就: ③ 中断 WaitForSingleObjectEx 的等待 ④ 返回 WAIT_IO_COMPLETION(表示被 APC 中断) ⑤ 在线程上下文里执行 MyFunc(param) ⑥ MyFunc 执行完后,控制权回到应用 ⑦ 应用可以选择:继续等待 Event A,或者做其他事 // 对比不可警告: WaitForSingleObject(hEvent, INFINITE); // 不可警告 // ① 线程开始等待 // ② 即使有 APC,内核也不会中断这个等待 // ③ 等待会一直卡住,直到 Event A 被 Signal // ④ APC 堆积在队列里,等到线程进入下一个可警告等待时才被执行 |
- 什么时机线程会进入可警告状态?
- 取决于应用代码调用的
API - 线程不是自动进入可警告状态的。只有当线程调用某些特定的
API,并且传入"允许警告"的参数,线程才会进入可警告状态
- 取决于应用代码调用的
|
1 |
// 常见的会进入可警告状态的 API |
API |
如何使用 | 说明 |
SleepEx |
SleepEx(1000, TRUE) |
Sleep 的可警告版本 |
WaitForSingleObjectEx |
WaitForSingleObjectEx(h, INFINITE, TRUE) |
第三个参数 TRUE |
WaitForMultipleObjectsEx |
WaitForMultipleObjectsEx(n, h, FALSE, INFINITE, TRUE) |
第五个参数 TRUE |
MsgWaitForMultipleObjectsEx |
MsgWaitForMultipleObjectsEx(...) |
消息队列的可警告等待 |
GetQueuedCompletionStatus |
默认可警告 | 异步 I/O 完成端口 |
ReadFileEx |
配合回调 | 异步读文件 |
WriteFileEx |
配合回调 | 异步写文件 |
SignalObjectAndWait |
SignalObjectAndWait(h1, h2, INFINITE, TRUE) |
第四个参数 TRUE |
I/O 完成端口相关 API |
多个 | 通常都可警告 |
|
1 |
// 对应的不可警告版本(没有参数让你设置) |
| 不可警告 | 可警告版本 |
Sleep |
SleepEx(..., TRUE) |
WaitForSingleObject |
WaitForSingleObjectEx(..., TRUE) |
WaitForMultipleObjects |
WaitForMultipleObjectsEx(..., TRUE) |
- 为什么线程进入可警告状态时这个
APC会被执行?Windows内核的设计里,APC机制是用来实现异步操作和系统级回调的- 当线程显式进入可警告状态时,内核默认:"这个线程现在愿意处理异步请求"。所以内核会:
|
1 2 3 4 5 6 |
线程进入可警告等待 ↓ 内核检查:"这个线程的 APC 队列里有待处理的请求吗?" ↓ 有 → 立刻中断等待,执行 APC 无 → 继续等待(之后来的 APC 会中断等待) |
实现
OpenProcess/OpenThread拿目标线程- 在目标进程分配内存放
DLL路径(VirtualAllocEx+WriteProcessMemory) QueueUserAPC((PAPCFUNC)LoadLibraryA, hThread, (ULONG_PTR)lpDllPath)队APC- 触发目标线程进入可警告状态(比如它在
WaitForSingleObject/Sleep会自动进入) APC被执行,DLL加载
特点
- 优点
- 没有创建新线程的痕迹(没有新
TID) - 相对隐蔽
- 没有创建新线程的痕迹(没有新
- 缺点
- 依赖目标线程进入可警告状态,无法强制
- 如果线程在执行用户代码、没有系统调用,
APC队列里的请求会堆积,什么时候执行不确定 - 某些反
cheat专门检测APC队列异常
注册表/配置文件 + AppInit_DLLs (系统级)
原理
- 把
DLL路径写进HKLM\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs,操作系统在加载任何用户模式进程时会自动加载这些DLL(通过user32.dll的初始化)
理解
- 是注入到了
user32.dll里面了吗- 不是注入到
user32.dll里面
- 不是注入到
|
1 2 3 4 5 6 7 8 9 10 11 12 |
注册表: AppInit_DLLs = "MyDll.dll" ✓ 正确理解: 任何应用程序加载 user32.dll 时 → user32.dll 的初始化代码读注册表 → 看到 AppInit_DLLs → 把这里列出的 DLL 加载到"这个应用程序进程" → MyDll.dll 被加载到"应用程序的地址空间" 所以 MyDll.dll 被加载到的地方是: 浏览器进程、记事本进程、任何调用应用... 不是 user32.dll 本身(user32.dll 没有自己的进程) |
实现
- 以管理员权限修改注册表:
AppInit_DLLs = "your.dll"或多个用分号分隔 - 重启系统或新启动的进程会自动加载
- 注意:
LoadAppInit_DLLs值要设为1
特点
- 优点
- 一次配置、全局生效(之后启动的所有用户进程都加载)
- 无需逐个注入,省事
- 缺点
- 需要管理员权限
- 很明显的注册表痕迹,任何清理工具都能找到
- 现代
Windows(Win10 1607+)默认禁用,需要特定条件才work
从Windows 10 version 1607开始,AppInit_DLLs的功能被大幅限制 - 本质是持久化手段,不是动态注入
示例代码
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
#include <windows.h> int main() { HKEY hKey; // 打开注册表项 if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows", 0, KEY_WRITE, &hKey) != ERROR_SUCCESS) { printf("打开注册表失败\n"); return 1; } // 1. 写 AppInit_DLLs const char* dllPath = "C:\\MyDll.dll"; if (RegSetValueExA(hKey, "AppInit_DLLs", 0, REG_SZ, (const BYTE*)dllPath, strlen(dllPath) + 1) != ERROR_SUCCESS) { printf("写 AppInit_DLLs 失败\n"); RegCloseKey(hKey); return 1; } // 2. 写 LoadAppInit_DLLs = 1 DWORD dwEnabled = 1; if (RegSetValueExA(hKey, "LoadAppInit_DLLs", 0, REG_DWORD, (const BYTE*)&dwEnabled, sizeof(dwEnabled)) != ERROR_SUCCESS) { printf("写 LoadAppInit_DLLs 失败\n"); RegCloseKey(hKey); return 1; } RegCloseKey(hKey); printf("注册表已设置,重启系统后生效\n"); // 或者新启动的应用程序会被感染 return 0; } |
内存补丁 + 改执行流 (Inline Hook/代码补丁,高级)
原理
- 扫描目标进程内存,定位某个已加载模块,解析其
PE头,对其代码段做inline patch(改几个字节跳转到你的代码),后续执行流就被劫持了
理解
pe相关
实现
VirtualQueryEx扫目标进程内存,找MZ头- 解析
PE头,找到想要hook的函数地址 VirtualProtectEx改页保护为可写WriteProcessMemory改那个函数前几字节为jmp your_code或call your_codeVirtualProtectEx改回原保护- 你的代码执行后再跳回,原函数继续
特点
- 优点
- 非常隐蔽,没有新线程、没有钩子、没有
APC - 对已有线程立刻生效,不需要等可警告状态
- 非常隐蔽,没有新线程、没有钩子、没有
- 缺点
- 非常脆弱,依赖目标模块的内存布局和版本,版本一变、函数地址变、
patch的位置就错了 - 如果
patch的位置碰巧在某个指令中间,会破坏指令,目标崩溃 - 需要知道目标进程的内存映像(
PE结构、导入表、IAT),工作量大 - 改完代码后要保证原指令流完整性(
call了要push return address,改了栈要恢复),极容易出bug
- 非常脆弱,依赖目标模块的内存布局和版本,版本一变、函数地址变、
注入原理
概述
- 前面五种方法,底层原理其实分两大类
- 第一类:加载
DLL让系统自动执行DllMainCreateRemoteThread + LoadLibraryA/W(最经典)SetWindowsHookEx(钩子注入)QueueUserAPC + LoadLibraryA(APC注入)
- 第二类:绕过系统,自己接管
- 注册表/配置文件 +
AppInit_DLLs(系统级) - 内存补丁 + 改执行流 (
Inline Hook/代码补丁,高级)
- 注册表/配置文件 +
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ 关于异常的捕获和dump文件的生成07/05
- ♥ Windows进程通信相关03/10
- ♥ Windows 核心编程 _ 进程二06/19
- ♥ Windbg:命令总览学习一04/06
- ♥ Windows 核心编程 _ 作业07/01
- ♥ 51CTO:Linux C++网络编程五08/20