导语:DLL的意思就是动态链接库(Dynamic-Link Libraries),它是包含很多函数和数据的一种模块,可以被其他模块调用(应用或DLL)。
目的
我最近看到一个帖子,大家也可以看看,说的是最新发布的win10 Build1709有个新版的Windows Defender,标记了Invoke-shellcode模块(这不仅是Empire才有的问题,Metasploit和其他流行框架也都被标记了,它们开始变得越来越过时了)。这意味着从红队攻击的角度来看,我们需要新的shellcode注入技术,需要在绕过AV方面更具创造性。所以我想开始搞这个系列文章,关于不同的注入技巧,希望最终能形成一个完整的操作指南。
我想从DLL注入和在内存中映射和执行DLL的不同技术开始,因为我经常听到这个技术,而且非常有兴趣,不过一直没机会深入探索。
我们首先从定义DLL并理解它们的组件开始。
概览
DLL的意思就是动态链接库(Dynamic-Link Libraries),它是包含很多函数和数据的一种模块,可以被其他模块调用(应用或DLL)。完整的DLL文档,可以在微软的网站上找到,链接在这里。
DLL可以定义两类函数,导出函数和内部函数。导出函数可以被其他模块调用,也可以在定义它们的DLL中调用,而内部函数只能在定义它们的DLL中调用。
DLL和内存管理
加载DLL的每个进程都将其映射到其虚拟地址空间。在进程将DLL加载到其虚拟地址后,它可以调用导出的DLL函数。
系统维护着每个DLL的进程引用计数。当线程加载DLL时,引用计数加1。当进程终止时,或者引用计数变为零时(仅运行时动态链接),DLL将从进程的虚拟地址空间中卸载。
调用入口函数
只要出现以下任何一个事件,系统就会调用入口函数:
1.进程加载DLL。对于使用加载时动态链接的进程,DLL在进程初始化期间加载。对于使用运行时链接的进程,DLL在LoadLibrary或LoadLibraryEx返回之前加载。
2.进程卸载DLL。当进程终止或调用FreeLibrary函数并且引用计数变为零时,将卸载DLL。
3.在已加载DLL的进程中创建新线程。
4.已加载DLL的进程的线程正常终止,而不是使用TerminateThread或TerminateProcess来终止。
进程或线程引起函数被调用时,系统便会调用入口函数。这使得DLL可以使用入口函数在调用进程的虚拟地址空间中分配内存或打开进程可访问的句柄。
根据Microsoft的文档,有两种方法可以调用DLL中的函数:
· 加载时动态链接:对导出的DLL函数进行显式调用,就好像它们是本地函数一样。这要求您将模块与包含这些函数的DLL的导入库链接。导入库为系统提供加载DLL所需的信息,并在加载应用程序时找到导出的DLL函数。
· 运行时动态链接:使用LoadLibrary或LoadLibraryEx函数在运行时加载DLL。加载DLL后,模块调用GetProcAddress函数来获取导出的DLL函数的地址。该模块使用GetProcAddress返回的函数指针调用导出的DLL函数。这样就不需要导入库了。
运行时动态链接
当应用程序调用LoadLibrary或LoadLibraryEx函数时,系统会尝试查找DLL。如果搜索成功,系统将DLL模块映射到进程的虚拟地址空间并增加引用计数。如果对LoadLibrary或LoadLibraryEx的调用指定了一个DLL,其代码已经映射到调用进程的虚拟地址空间,则该函数只返回DLL的句柄并递增DLL引用计数。
当线程调用LoadLibrary或LoadLibraryEx时,系统就会调用入口函数。如果系统找不到DLL或者入口函数返回FALSE,那么LoadLibrary或LoadLibraryEx返回NULL。如果LoadLibrary或LoadLibraryEx成功,它将返回DLL模块的句柄。该进程可以使用此句柄来识别调用GetProcAddress,FreeLibrary或FreeLibraryAndExitThread的函数中的DLL。
GetModuleHandle函数返回GetProcAddress,FreeLibrary或FreeLibraryAndExitThread中使用的句柄。该进程可以使用GetProcAddress来获取DLL中导出函数的地址,这个DLL使用LoadLibrary,LoadLibraryEx或GetModuleHandle返回的DLL模块句柄。
DLLMain入口函数
动态链接库的可选入口,由BOOL WINAPI DllMain()函数定义。当系统启动或终止进程或线程时,它使用进程的第一个线程为每个加载的DLL调用入口函数。当使用LoadLibrary和FreeLibrary函数加载或卸载DLL时,系统也会调用DLL的入口函数。
DllMain是库定义的函数名的占位符。您必须指定生成DLL时使用的实际名称。在初始进程启动期间或调用LoadLibrary之后,系统会扫描加载的DLL列表以查找进程。对于尚未使用DLL_PROCESS_ATTACH值调用的每个DLL,系统调用DLL的入口函数。此调用是在线程引起进程地址空间发生改变时进行的,例如进程的主线程或调用LoadLibrary的线程。
因为在调用入口函数时Kernel32.dll肯定会被加载到进程地址空间中,所以在Kernel32.dll中调用函数不会导致在执行初始化代码之前使用DLL。因此,入口函数可以调用Kernel32.dll中不加载其他DLL的函数。
现在我们已经定义了一些我研究时发现有用的结构,让我们深入研究。
发现目标进程
对于本系列中的大多数技术,我们需要做的第一件事就是找到我们想要注入的目标进程。遍历和列举进程的方法有几种,但我发现最有用的是Process Walking。它使用CreateToolhelp32Snapshot函数拍摄快照,快照中包含进程列表,并包含每个当前正在执行的进程的信息。您可以使用Process32First函数获取列表中第一个进程的信息。获取列表中的第一个进程后,可以使用Process32Next函数遍历进程列表来查找后面的进程,直到找到你的目标进程(例如putty.exe)或者只是将该进程添加到向量中并打印出所有的进程。找到目标进程后,我们使用OpenProcess函数来获取进程的句柄(示例如下)。
您可以在此处找到完整源代码(作为独立工具)。我们还将它作为辅助函数合并到我们的DLL注入工具中。
DLL注入
DLL注入简单地定义为将DLL插入另一个进程的空间然后执行其代码的过程。以下是OpenSecurityResearch博客文章中对此过程进行简化的可视化视图:
可以在以下代码片段中看到传统注入技术的示例:
简要分析一下在这个LoadLibrary示例中我们所做的事情(稍后我们将深入探讨并对其中的某些部分进行扩展):
我们的第一个参数是HANDLE hProcess,它可以由OpenProcess函数定义(见下文),第二个参数是LPCSTR dllpath,它定义了完整的dll路径,例子如下:
HANDLE hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION, FALSE, pe32.th32ProcessID);
一旦我们有了进程句柄,我们就可以使用VirtualAllocEx在目标进程的虚拟内存空间中分配一个内存区域。
LPVOID lpBaseAddress = VirtualAllocEx(hProcess, NULL, strlen(dllpath), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
然后,我们使用WriteProcessMemory函数通过DLL payload的完整路径将数据写入进程。
WriteProcessMemory(hProcess, lpBaseAddress, dllpath, strlen(dllpath), NULL);
然后,我们找到LoadLibrary的地址,并使用GetProcAddress和GetModuleHandle函数存储该内存地址。
LPVOID lpLibraryAddress = (LPVOID)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");
最后,我们在目标进程中执行LoadLibrary函数,使用CreateRemoteThread函数将DLL payload作为库进行强制加载。我们也可以使用NtCreateThreadEx或RtlCreateUserThread,这些都是未记录的函数,但思想是一样的。由于CreateRemoteThread执行LoadLibrary函数,目标进程将立即使用DLL_PROCESS_ATTACH函数执行DLL的DllMain入口函数。
CreateRemoteThread(hProcess, NULL, (LPTHREAD_START_ROUTINE)lpLibraryAddress, lpBaseAddress, NULL, NULL);
使用这种方法,我们必须定义DLLMain入口函数,也很简单,示例如下:
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { ::MessageBox(NULL, L"Hello World!", L"Test DLL", MB_OK); return TRUE; }
注意:这只是基本的POC,不过如果你看一下动态链接库最佳实践,你会看到DllMain函数就是用来在编译时初始化静态数据结构和成员的,创建和初始化同步对象,分配内存并初始化动态数据结构。不过,有些函数应该避免使用,例如User32.dll和Gdi32.dll。因为,在User32.dll中实现的MessageBox函数可能会导致问题(崩溃或死锁)。
第一个例子比较基础,而且动静很大,因为CreateRemoteThread(大多数安全产品都会定位)和LoadLibrary函数调用更新了该进程中的数据结构,所以,任何监视进程的人(使用ProcMon)都可以看到每个进程中加载的每个DLL的名称。
替代方案就是在远程进程中为DLL的实际内容分配足够的空间,但这很棘手,因为我们必须手动解析并将二进制映射到内存中。我们将在下一节探讨这种替代方案 – 反射DLL注入(敬请期待)。