如何创建无操作(no-op)版本DLL以规避危险依赖?
这问题我之前处理过不少,核心思路很明确:因为你的app.exe和d.dll都是加载时隐式链接,系统启动时会强制寻找dangerous.dll并加载它。我们只需要做一个“占位”的DLL,导出所有d.dll需要从dangerous.dll获取的符号,但每个导出的函数/变量都做无操作实现,这样既满足链接要求,又完全避开原危险DLL的漏洞。
下面是具体步骤:
1. 确定需要导出的符号列表
首先得搞清楚d.dll到底需要从dangerous.dll导入哪些东西。你可以用Windows SDK里的dumpbin工具来查:
- 直接查看原
dangerous.dll的导出表:dumpbin /exports "path/to/original/dangerous.dll" - 或者查看
d.dll的导入表(更精准,因为只需要d.dll用到的符号):dumpbin /imports "path/to/d.dll" | findstr "dangerous.dll"
把输出里的所有函数名、序号记下来,还要注意函数的调用约定(比如__stdcall还是__cdecl)和参数/返回值类型——这些必须和原DLL完全一致,否则可能导致栈溢出或者崩溃。
2. 创建无操作DLL的代码和导出定义
你有两种方式定义导出符号:
方式一:使用.def文件(推荐,更直观)
先写一个dangerous.def文件,格式如下:
LIBRARY dangerous EXPORTS ; 把第一步查到的所有导出函数按序号和名称列出来 DangerousFunc1 @1 DangerousFunc2 @2 DangerousGlobalVar @3 DATA ; 如果有导出变量,加DATA标记 ; ... 其他符号
然后写一个简单的C文件dangerous.c,实现空函数和DLL入口:
#include <windows.h> // DLL入口函数,什么都不用做,直接返回TRUE 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; } return TRUE; } // 每个导出函数的无操作实现,参数和返回值必须和原DLL匹配 void __stdcall DangerousFunc1(int param1) { // 空实现,啥都不做 } int __cdecl DangerousFunc2() { // 如果原函数有返回值,返回一个合理的默认值(比如0) return 0; } // 导出变量的定义 int DangerousGlobalVar = 0;
方式二:使用__declspec(dllexport)
如果不想写.def文件,可以在代码里直接标记导出:
#include <windows.h> BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { return TRUE; } __declspec(dllexport) void __stdcall DangerousFunc1(int param1) {} __declspec(dllexport) int __cdecl DangerousFunc2() { return 0; } __declspec(dllexport) int DangerousGlobalVar = 0;
这种方式要注意,编译出来的DLL导出序号可能和原DLL不一致——如果d.dll是按序号导入的,就必须用.def文件来保证序号匹配,否则会加载失败。
3. 编译生成无操作DLL
用Visual Studio的编译器(cl.exe)编译:
- 如果用.def文件:
cl /LD dangerous.c dangerous.def - 如果不用.def文件:
cl /LD dangerous.c
编译后会生成dangerous.dll和对应的.lib文件(其实lib文件你用不上,因为d.dll已经是编译好的)。
4. 测试验证
把生成的dangerous.dll放到app.exe的同一目录下(或者系统的DLL搜索路径里),然后启动app.exe。如果一切正常,app会顺利加载,完全不会用到原危险DLL的任何功能——因为你的app根本不会调用那些依赖危险DLL的接口,而假DLL只是满足了加载要求而已。
关键注意事项
- 调用约定和类型匹配:一定要保证假函数的参数、返回值、调用约定和原DLL完全一致,否则
d.dll调用这些函数时可能出现栈错误或者崩溃。 - 导出序号:如果
d.dll是按序号导入符号的,必须用.def文件指定和原DLL相同的序号,否则系统会找不到对应的符号。 - 导出变量:如果原DLL有导出全局变量,假DLL里必须定义相同名称和类型的变量,不能只写函数。
内容的提问来源于stack exchange,提问作者M Katz




