通用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

一种通用DLL劫持技术研究相关推荐

  1. Windows 下的 7 种 DLL 劫持技术

    本文讲的是Windows 下的 7 种 DLL 劫持技术,在本文中,我将列出半打可以在Windows运行用户模式的进程中使用DLL注入技术.也许可能会有更多类似的技术,但我正在和你分享的是我所拥有的第 ...

  2. 转帖:DLL劫持技术详解(lpk.dll)

    说起DLL劫持技术,相信大家都不会陌生,因为这种技术的应用比较广泛,比如木马后门的启动.破解程序的内存补丁.外挂插件的注入以及加密狗的模拟等.之所以DLL劫持技术深受黑客们的喜爱,主要是因为该技术可以 ...

  3. DLL劫持技术详解(lpk.dll)

    说起DLL劫持技术,相信大家都不会陌生,因为这种技术的应用比较广泛,比如木马后门的 启动.破解程序的内存补丁.外挂插件的注入以及加密狗的模拟等.之所以DLL劫持技术深受黑客们的喜爱,主要是因为该技术可 ...

  4. 微信PC端技术研究-消息防撤销

    微信PC端技术研究-消息防撤销 by anhkgg 2018年11月30日 0x1. 写在前面 不知道大家有没有遇到过这种情况,微信收到消息,但是没有及时查看,然后闲暇时去看的时候,消息被撤销了,撤销 ...

  5. 后渗透篇:劫持技术(lpk.dll劫持游戏注入【Win7 实例】)

    当你的才华 还撑不起你的野心时 那你就应该静下心来学习 lpk.dll劫持游戏注入 由于 输入表中只包含DLL名而没有它的路径名,因此加载程序必须在磁盘上搜索 DLL文件.首先会尝试从当前程序所在的目 ...

  6. 近300个 Windows 10 可执行文件易受 DLL 劫持攻击

     聚焦源代码安全,网罗国内外最新资讯! 编译:奇安信代码卫士团队 一个简单的 VBScript就能使用户获得管理员权限并完全绕过Windows 10 的 UAC. 英国普华永道公司的安全研究员 Wie ...

  7. NTA告警引发的dll劫持思考(溯源)

    目录 溯源 DLL DLL Hijacking Vulnerability(DLL劫持漏洞) DLL劫持攻击 转发式劫持 DLL劫持防御 某天在客户局点巡检时,NTA设备发现了CS的告警:定位了被入侵 ...

  8. 老树开新花:DLL劫持漏洞新玩法

      DLL劫持漏洞已经是一个老生常谈,毫无新鲜感的话题了.DLL劫持技术也已经是黑客们杀人越货,打家劫舍必备的武器.那么,随着Win10的诞生,微软是否已经修复了此漏洞?同时在当前的安全环境下,DLL ...

  9. Win7下实现 lpk.dll劫持游戏注入

    由于输入表中只包含DLL名而没有它的路径名,因此加载程序必须在磁盘上搜索DLL文件.首先会尝试从当前程序所在的目录加载DLL,如果没找到,则在Windows系统目录中查找,最后是在环境变量中列出的各个 ...

最新文章

  1. 自动驾驶系统关系与自动泊车原理
  2. 脑机接口成唯一沟通方式,渐冻症晚期父亲终向4岁儿子表达爱意
  3. vf6.0 如何把命令窗口字体变大些_终端命令行工具iTerm2 for Mac免费版
  4. 频繁项集-------产生强关联规则的过程
  5. Navigator 对象,能够清楚地知道浏览器的相关信息
  6. Mercurial hg web server的配置
  7. element引入的组件大小高度不对_Angular 2:尝试使用ElementRef访问组件高度时的奇怪行为...
  8. js splice方法_我用JS刷LeetCode | Day 8
  9. html模板文件打开空白,Webpack打包index打开空白
  10. 【数据结构】栈的编写以及栈的简单应用
  11. BeanUtils.copyProperties使用
  12. 使用Python解压,对比文件
  13. Qt:#pragma comment(lib,“ws2_32.lib“) 报错
  14. Python爬虫入门之初遇lxml库
  15. 【微信测试号实战——02】编写你独有的微信消息模板
  16. 上海疫情中的云婚礼:千人“吃席” 这场婚礼太温暖
  17. 系统传输过程中 中文点 · 对方无法解析的问题查找
  18. Spring父子类同属性父类属性隐藏
  19. 计蒜客 最后一个单词的长度
  20. 数字图像直方图处理涉及的数学知识介绍

热门文章

  1. 够快联想坚果云这三个产品哪个更好
  2. MATLAB批量处理生成profili生成的翼型数据
  3. Android简单美观计算器(界面部分)
  4. 【渝粤题库】陕西师范大学202901小学生心理辅导作业(高起专 、专升本)
  5. python入门笔记(1)
  6. 学完C语言,学什么, 怎么学,之后学什么
  7. 快速傅里叶变换(研二的我终于弄懂了)
  8. 关于Google智能眼镜的设计
  9. 编译原理——语法制导翻译并产生中间代码(while,复合语句和过程调用语句的翻译)
  10. 常用校验算法(累加和、异或和)