一种通用 DLL 劫持技术研究 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
anhkgg
V2EX    Windows

一种通用 DLL 劫持技术研究

  •  
  •   anhkgg 2018-12-06 14:52:49 +08:00 2655 次点击
    这是一个创建于 2506 天前的主题,其中的信息可能已经所发展或是发生改变。

    通用 DLL 劫持技术研究
    by anhkgg
    2018 年 11 月 29 日

    写在前面

    Dll 劫持相信大家都不陌生,理论就不多说了。Dll 劫持的目的一般都是为了自己的 dll 模块能够在别人进程中运行,然后做些不可描述的事情。

    为了让别人的程序能够正常运行,通常都需要在自己的 dll 中导出和劫持的目标 dll 相同的函数接口,然后在自己的接口函数中调用原始 dll 的函数,如此使得原始 dll 的功能能够正常被使用。导出接口可以自己手工写,也可以通过工具自动生成,比如著名的Aheadlib。这种方法的缺点就是针对不同的 dll 需要导出不同的接口,虽然有工具帮助,但也有限制,比如不支持 x64。

    除此之外,很早之前就知道一种通用 dll 劫持的方法,原理大致是在自己的 dll 的 dllmian 中加载被劫持 dll,然后修改 loadlibrary 的返回值为被劫持 dll 加载后的模块句柄。这种方式就是自己的 dll 不用导出和被劫持 dll 相同的函数接口,使用更加方便,也更加通用。

    下面就尝试分析一下如何实现这种通用的 dll 劫持方法。

    原理分析

    随便写一个测试代码:

    //mydll.dll 伪造的用于劫持 mydll.dll 的 dll 代码 //mydll.dll.1 是把 test.exe 加载的原始 dll 修改为这个名字 BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: __debugbreak(); HMODULE hmod = LoadLibraryW("mydll.dll.1"); case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } //test.exe void main() { LoadLibraryW(L"mydll.dll"); } 

    用 windbg 加载看看堆栈,如下所示。在 test 中通过 LoadLibraryW 加载 mydll.dll ,最后进入 mydll!DllMain。现在需要分析系统映射 dll 之后是如何把基地址返回给 LoadLibraryW,然后才能想办法把这个值给修改成加载 mydll.dll.1 的值。

    0:000> kvn # ChildEBP RetAddr Args to Child WARNING: Stack unwind information not available. Following frames may be wrong. 00 0025eaf8 6e4112ec 6e410000 00000000 00000000 mydll+0x101d 01 0025eb38 6e4113c9 6e410000 00000001 00000000 mydll+0x12ec 02 0025eb4c 77d889d8 6e410000 00000001 00000000 mydll!DllMain+0x13 03 0025eb6c 77d95c41 6e4113ad 6e410000 00000001 ntdll!LdrpCallInitRoutine+0x14 04 0025ec60 77d9052e 00000000 74e92d11 77d77c9a ntdll!LdrpRunInitializeRoutines+0x26f (FPO: [Non-Fpo]) 05 0025edcc 77d9232c 0025ee2c 0025edf8 00000000 ntdll!LdrpLoadDll+0x4d1 (FPO: [Non-Fpo]) 06 0025ee00 75ee88ee 0037429c 0025ee40 0025ee2c ntdll!LdrLoadDll+0x92 (FPO: [Non-Fpo]) 07 0025ee38 761b3c12 00000000 00000000 00000001 KERNELBASE!LoadLibraryExW+0x15a (FPO: [Non-Fpo]) 08 0025ee4c 6848e3f5 0025ee58 003a0043 0055005c kernel32!LoadLibraryW+0x11 (FPO: [Non-Fpo]) 09 0025f068 6848d1de d9131536 00000000 00000000 test!start+0x2b5 0a 0025f09c 6848e245 013a0000 761b3c26 76b3ea5f test!start+0x21e86e 0b 0025f328 013a1918 013a0000 0037187a 00000000 test!start+0x105 0c 0025fb44 013a30b9 013a0000 00000000 0037187a test+0x1918 0d 0025fb90 761b3c45 7ffd9000 0025fbdc 77d937f5 test+0x30b9 0e 0025fb9c 77d937f5 7ffd9000 74e93b01 00000000 kernel32!BaseThreadInitThunk+0xe (FPO: [Non-Fpo]) 0f 0025fbdc 77d937c8 013a312b 7ffd9000 00000000 ntdll!__RtlUserThreadStart+0x70 (FPO: [Non-Fpo]) 10 0025fbf4 00000000 013a312b 7ffd9000 00000000 ntdll!_RtlUserThreadStart+0x1b (FPO: [Non-Fpo]) 

    先去 reactos 翻看一下,找到如下的函数调用结构。在 LdrLoadDll 参数中 BaseAddress 就是最后返回给 LoadLibraryW 的值,所以继续看 BaseAddress 是如何赋值的。BaseAddress 继续传给 LdrpLoadDll,在 LdrpLoadDll 中,首先通过 LdrpMapDll 映射 dll 模块,返回一个 LdrEntry 的 LDR_DATA_TABLE_ENTRY 结构,保存了 dll 加载的基址、大小、名字等信息。接着 LdrEntry 会插入到 peb->ldr 链表结构中,然后调用 LdrpRunInitializeRoutines,在 LdrpRunInitializeRoutines 中最终会调用 DllMain,此处不继续深入分析。最后 LdrEntry->DllBase 赋值给 BaseAddress。到此流程分析清楚,下面考虑如何修改这个值。

    NTSTATUS NTAPI LdrLoadDll(IN PWSTR SearchPath OPTIONAL, IN PULONG DllCharacteristics OPTIONAL, IN PUNICODE_STRING DllName, OUT PVOID *BaseAddress) { Status = LdrpLoadDll(RedirectedDll, SearchPath, DllCharacteristics, DllName, BaseAddress, TRUE); } NTSTATUS NTAPI LdrpLoadDll(IN BOOLEAN Redirected, IN PWSTR DllPath OPTIONAL, IN PULONG DllCharacteristics OPTIONAL, IN PUNICODE_STRING DllName, OUT PVOID *BaseAddress, IN BOOLEAN CallInit) { Status = LdrpMapDll(DllPath, DllPath, NameBuffer, DllCharacteristics, FALSE, Redirected, &LdrEntry); //插入 peb->ldr 链表 Status = LdrpRunInitializeRoutines(NULL); if (NT_SUCCESS(Status)) { /* Return the base address */ *BaseAddress = LdrEntry->DllBase; } } LdrpRunInitializeRoutines-> LdrpCallInitRoutine -> DllMain 

    记得映像中的那种方法,是通过堆栈回溯到 LdrpLoadDll 中,找到 LdrEntry 进行修改(不确实是否准备,时间久远了),但因为 LdrEntry 是局部变量,不同系统可以不一样,兼容性差一些。但看到这个调用流程之后,其实还有另一种方式。LdrEntry->DllBase 赋值给 BaseAddress,那么在赋值之前把这个 LdrEntry->DllBase 修改了即可,在 DllMain 正好是修改的时机,但是不需要使用堆栈回溯的方式。因为 LdrEntry 已经插入到 peb->ldr 中,那么在 DllMain 中可以直接获取 peb->ldr 遍历链表找到目标 dll 堆栈的 LdrEntry 就是需要修改的 LdrEntry,然后修改即可。

    不过这个分析都是基于 reactos 来的,还是需要确认一下真是 windows 系统的 ntdll 是如何首先的。

    在 win7 x64 系统中,ntdll 的关键代码如下所示。差别是 LdrpLoadDll 直接返回的 ldrentry,而不是 BaseAddress,在 LdrpLoadDll 内部流程基本和 reactos 一致。所以方案应该可行,后续验证确实证明可行。

    int __fastcall LdrLoadDll() { v11 = LdrpLoadDll(v5, v9, v10, 1, 0i64, &dataentry); v12 = v11; if ( v11 >= 0 ) *dllbase = dataentry->DllBase; } 

    尝试实现

    实现其实非常简单,关键代码如下所示。两部分代码,一个是加载原始 dll 模块( mydll.dll.1 )拿到真是的模块句柄 hMod (基地址),第二个就是遍历 peb->ldr 找到 mydll.dll 的 ldrentry,然后修改 dllbase 为 hMod。

    void* NtCurrentPeb() { __asm { mov eax, fs:[0x30]; } } PEB_LDR_DATA* NtGetPebLdr(void* peb) { __asm { mov eax, peb; mov eax, [eax + 0xc]; } } VOID SuperDllHijack(LPCWSTR dllname, HMODULE hMod) { WCHAR wszDllName[100] = { 0 }; void* peb = NtCurrentPeb(); PEB_LDR_DATA* ldr = NtGetPebLdr(peb); for (LIST_ENTRY* entry = ldr->InLoadOrderModuleList.Blink; entry != (LIST_ENTRY*)(&ldr->InLoadOrderModuleList); entry = entry->Blink) { PLDR_DATA_TABLE_ENTRY data = (PLDR_DATA_TABLE_ENTRY)entry; memset(wszDllName, 0, 100 * 2); memcpy(wszDllName, data->BaseDllName.Buffer, data->BaseDllName.Length); if (!_wcsicmp(wszDllName, dllname)) { data->DllBase = hMod; break; } } } VOID DllHijack(HMODULE hMod) { TCHAR tszDllPath[MAX_PATH] = { 0 }; GetModuleFileName(hMod, tszDllPath, MAX_PATH); PathRemoveFileSpec(tszDllPath); PathAppend(tszDllPath, TEXT("mydll.dll.1")); HMODULE hMod1 = LoadLibrary(tszDllPath); SuperDllHijack(L"mydll.dll", hMod1); } BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: DllHijack(hModule); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } 

    总结

    经测试在 win7 x84 和 win10 x64 中即是有效的,其他系统未测试,如果有问题,请留言或自行解决。

    害怕这种方案不行,还想了另一种思路,在 dllmain 中 hook LdrpLoadDll 的返回调用地址处,修改 dataentry 的值,因为 LdrLoadDll 函数接口固定,所以这种方式也应该是通用的,不过实现起来其实还比现在的麻烦些,所以只是保留了这种思路,并未去实现验证,留给爱折腾的朋友吧。

    最后,代码上传了 github,https://github.com/anhkgg/SuperDllHijack

    广告:欢迎关注公众号汉客儿和 QQ 群(753894145),一起交流学习

    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     6259 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 27ms UTC 02:35 PVG 10:35 LAX 19:35 JFK 22:35
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86