一、前言
在本篇文章中,将描述动态链接库(DLL)搜索顺序劫持的改建,以及攻击者可能如何将其用于Windows系统上的用户态持久性。这种技术可以对应到MITRE ATT&CK框架中的T1038:DLL搜索顺序劫持。
由于种种原因,DLL劫持对于攻击者来说很有帮助,但本文将重点介绍与自动启动应用程序结合使用的持久化用法。例如,在默认情况下,Slack和Microsoft Teams会在系统启动时运行,因此如果能将其中一个应用程序的DLL进行劫持,每当用户登录系统时,攻击者就可以持久访问其目标。
在介绍DLL、DLL搜索顺序和DLL劫持的概念之后,我们将探讨自动化DLL劫持侦查的方法。在这篇文章中,我们将介绍在Slack、Microsoft Teams和Visual Studio中如何侦查DLL劫持。
最后,我注意到,有许多DLL劫持是可以在不同应用程序之间共享的。我调查了其根本原因,发现如果应用程序没有位于C:WindowsSystem32路径下,使用特定Windows API调用的应用程序可能会存在DLL劫持的风险。
在这里,我要感谢我的同事Josiah Massari(@Airzero24),他在最初发现了一些DLL劫持事件,并在处理过程中阐述了他的方法,从而激发我研究自动化侦查的方法。
二、关于DLL
DLL是一个包含代码和数据的库,可以同时由多个程序使用。
Windows应用程序可以使用某一个LoadLibrary*
函数来利用DLL中的功能。应用程序可以引用为该应用程序自定义创建的DLL,也可以引用位于System32路径下的已有DLL。开发人员可以定义应用程序从System32加载DLL,以使用Windows中已实现的功能,这样就不必再自行编写特定功能。
例如,如果应用程序需要发出HTTP请求,那么开发人员就可以利用WinHTTP库(winhttp.dll
),而不需要再使用原始套接字实现HTTP请求。
三、DLL搜索顺序劫持
由于DLL在磁盘上以文件的形式存在,因此我们可能会产生疑问,应用程序怎么知道要从哪里加载DLL?Microsoft在这里完整记录了DLL的搜索顺序。
从Windows XP SP2开始,操作系统在默认情况下就已经启用了安全DLL搜索模式(HKEY_LOCAL_MACHINESystemCurrentControlSetControlSession ManagerSafeDllSearchMode
)。在启用安全的DLL搜索模式后,搜索顺序如下:
1、加载应用程序的目录;
2、系统目录,使用GetSystemDirectory
函数获取该目录的路径;
3、16位系统目录,没有获取该目录路径的函数,但会对该目录进行搜索;
4、Windows目录,使用GetWindowsDirectory
函数获取该目录的路径;
5、当前目录;
6、PATH环境变量中列出的目录,这里不包括App Paths
注册表项指定的每个应用程序路径,在计算DLL搜索路径是不会使用到App Paths
键。
在系统中可以包含同一个动态链接库(DLL)的多个版本。应用程序可以通过指定完整路径或使用其他机制(例如清单)来控制DLL的加载位置。
如果应用程序未指定从哪里加载DLL,那么Windows将会默认使用上述DLL搜索顺序。因此,作为攻击者来说,往往就会对DLL搜索的第一个位置(加载应用程序的目录)感兴趣。
如果应用程序开发人员希望从C:WindowsSystem32加载DLL,但在应用程序中没有明确写入,那么就会在搜索System32中的合法DLL之前,先加载应用程序目录中被植入的恶意DLL。这种恶意DLL加载方式被称为DLL劫持,攻击者以这种方式将恶意代码加载到受信任或已签名的应用程序中。
四、使用DLL劫持技术实现持久化
在攻击者将恶意DLL植入到易受攻击的位置之后,就可以利用DLL劫持技术实现持久化,在存在漏洞的应用程序或服务运行时执行恶意代码。我的同事@Airzero24发现有攻击者在Microsoft OneDrive、Microsoft Teams和Slack中,使用了userenv.dll
来实现DLL劫持。
攻击者针对了特定的应用程序进行攻击,因为在默认情况下,这些程序会被配置为在Windows启动时自动启动。我们可以在任务管理器中找到它们。
配置为开机时自动启动的Windows应用程序:
为了验证DLL劫持,我创建了一个DLL Shellcode加载程序,它将会启动一个Cobalt Strike Beacon。我将恶意DLL重命名为userenv.dll
,并将其复制到易受攻击的应用程序的目录中。在启动该应用程序之后,我就看到了新的Beacon回调。
通过DLL劫持实现Cobalt Strike Beacon:
在进程管理器中,我们验证了易受攻击的应用程序确实已经加载了我的恶意DLL。
进程管理器显示恶意DLL已加载。
五、自动化DLL劫持侦查
在我们研究已知的DLL劫持之后,我想看看是否可以找到其他的DLL劫持。
在测试过程中,我所使用的代码可以在这里找到:https://github.com/slyd0g/DLLHijackTest
5.1 案例分析:Slack
在一开始,我在进程管理器(ProcMon)中使用了以下过滤器:
1、进程名称为slack.exe
;
2、结果包含NOT FOUND
;
3、路径以.dll
结尾。
使用ProcMon过滤丢失的DLL:
接下来,我启动了Slack,并观察ProcMon中是否存在Slack正在搜索但无法找到的DLL。
通过ProcMon发现潜在的DLL劫持攻击点:
我将这些数据从ProcMon导出为CSV文件,以便在PowerShell中轻松进行解析。
使用当前的Shellcode加载程序DLL,我们不能轻松确定Slack成功加载的DLL名称。为此,我创建了一个新的DLL,该DLL使用GetModuleHandleEx
和GetModuleFileName
来确定加载的DLL的名称,并将其写入文本文件。
我们的下一个目标是解析CSV文件,以获得DLL路径列表,遍历这个路径列表,将测试DLL复制到指定的路径,启动目标进程,停止目标进程,并删除测试DLL。如果这个测试DLL能成功加载,就会将这个文件名写入到结果文件中。
在这一过程完成时,我们就有了一个文本文件,其中包含可以用于DLL劫持的列表。
在我的DLLHijackTest项目中,主要使用PowerShell脚本来完成所有工作。它负责接受ProcMon生成的CSV文件中包含的路径、恶意DLL路径、要启动的进程路径以及要传递给该进程的所有参数。
Get-PotentialDLLHijack
参数:Get-PotentialDLLHijack.ps1
:
在几分钟后,我检查了在“恶意”DLL中指定的文本文件中,是否包含可用的DLL劫持,并发现了针对Slack的以下劫持:
PS C:UsersJohnDesktop> Get-PotentialDLLHijack -CSVPath .Logfile.CSV -MaliciousDLLPath .DLLHijackTest.dll -ProcessPath "C:UsersJohnAppDataLocalslackslack.exe"C:UsersJohnAppDataLocalslackapp-4.6.0WINSTA.dll
C:UsersJohnAppDataLocalslackapp-4.6.0LINKINFO.dll
C:UsersJohnAppDataLocalslackapp-4.6.0ntshrui.dll
C:UsersJohnAppDataLocalslackapp-4.6.0srvcli.dll
C:UsersJohnAppDataLocalslackapp-4.6.0cscapi.dll
C:UsersJohnAppDataLocalslackapp-4.6.0KBDUS.DLL
5.2 案例分析:Microsoft Teams
再次进行以上的步骤:
1、使用ProcMon识别出潜在的DLL劫持,并将数据导出为CSV文件;
2、确定启动进程的路径;
3、确定要传递给进程的所有参数;
4、使用适当的参数运行Get-PotentialDLLHijack.ps1
。
最后发现,Microsoft Teams存在以下劫持:
PS C:UsersJohnDesktop> Get-PotentialDLLHijack -CSVPath .Logfile.CSV -MaliciousDLLPath .DLLHijackTest.dll -ProcessPath "C:UsersJohnAppDataLocalMicrosoftTeamsUpdate.exe" -ProcessArguments '--processStart "Teams.exe"'C:UsersJohnAppDataLocalMicrosoftTeamscurrentWINSTA.dll
C:UsersJohnAppDataLocalMicrosoftTeamscurrentLINKINFO.dll
C:UsersJohnAppDataLocalMicrosoftTeamscurrentntshrui.dll
C:UsersJohnAppDataLocalMicrosoftTeamscurrentsrvcli.dll
C:UsersJohnAppDataLocalMicrosoftTeamscurrentcscapi.dll
C:UsersJohnAppDataLocalMicrosoftTeamscurrentWindowsCodecs.dll
C:UsersJohnAppDataLocalMicrosoftTeamscurrentTextInputFramework.dll
需要特别说明的是,在这里需要对PowerShell脚本进行少量修改,才能终止Teams.exe
,因为我的脚本试图终止它尝试启动的进程,即Update.exe
。
5.3 案例分析:Visual Studio Code
重复上述过程,我发现了Visual Studio Code存在以下劫持:
PS C:UsersJohnDesktop> Get-PotentialDLLHijack -CSVPath .Logfile.CSV -MaliciousDLLPath .DLLHijackTest.dll -ProcessPath "C:UsersJohnAppDataLocalProgramsMicrosoft VS CodeCode.exe"C:UsersJohnAppDataLocalProgramsMicrosoft VS CodeWINSTA.dll
C:UsersJohnAppDataLocalProgramsMicrosoft VS CodeLINKINFO.dll
C:UsersJohnAppDataLocalProgramsMicrosoft VS Codentshrui.dll
C:UsersJohnAppDataLocalProgramsMicrosoft VS Codesrvcli.dll
C:UsersJohnAppDataLocalProgramsMicrosoft VS Codecscapi.dll
5.4 共同的DLL劫持
在进行上述分析之后,我们注意到,Slack、Microsoft Teams和Visual Studio Code共享了以下的DLL劫持:
WINSTA.dll
LINKINFO.dll
ntshrui.dll
srvcli.dll
cscapi.dll
这非常值得关注,我们想要分析是什么导致了这样的情况。
六、理解共享的DLL劫持
当Slack尝试加载WINSTA.dll
、LINKINFO.dll
、ntshrui.dll
、srvcli.dll
和cscapi.dll
时,我观察了堆栈跟踪。
6.1 延迟加载的DLL
在加载WINSTA.dll
、LINKINFO.dll
、ntshrui.dll
和srvcli.dll
时,我注意到堆栈跟踪中存在相似之处。
Code.exe尝试加载WINSTA.dll
时的堆栈跟踪:
Teams.exe尝试加载LINKINFO.dll
时的堆栈跟踪:
Stack尝试加载ntshrui.dll
时的堆栈跟踪:
堆栈跟踪始终包含对_tailMerge_<dllname>_dll
、delayLoadHelper2
的调用,然后是LdrResolveDelayLoadedAPI
。这三个应用程序之间的行为是一致的。
我可以确定,这样的行为与延迟加载的DLL有关。在加载WINSTA.dll
时的堆栈跟踪中,我们发现负责延迟加载的模块是wtsapi32.dll
。
于是,在Ghidra中打开wtsapi32.dll
,并选择“Search” -> “For Strings” -> “Filter: WINSTA.dll”,双击找到的字符串,就可以查看其在内存中的位置。
wtsapi32.dll
中的“WINSTA.dll”字符串:
右键单击内存中的位置,我们就可以找到对该地址的所有引用。
寻找对WINSTA.dll
的引用:
在引用之后,我们看到WINSTA.dll
字符串会传递给名为ImgDelayDescr
的结构。我们查看有关此结构的文档,可以确认它与延迟加载的DLL相关。
typedef struct ImgDelayDescr {
DWORD grAttrs; // attributes
RVA rvaDLLName; // RVA to dll name
RVA rvaHmod; // RVA of module handle
RVA rvaIAT; // RVA of the IAT
RVA rvaINT; // RVA of the INT
RVA rvaBoundIAT; // RVA of the optional bound IAT
RVA rvaUnloadIAT; // RVA of optional copy of original IAT
DWORD dwTimeStamp; // 0 if not bound,
// O.W. date/time stamp of DLL bound to (Old BIND)
} ImgDelayDescr, * PImgDelayDescr;
可以将这个结构传递给__delayLoadHelper2
,它将使用LoadLibrary/GetProcAddress
加载指定的DLL,并在延迟加载导入地址表(IAT)中修补导入函数的地址。
FARPROC WINAPI __delayLoadHelper2(
PCImgDelayDescr pidd, //Const pointer to a ImgDelayDescr struct
FARPROC * ppfnIATEntry //A pointer to the slot in delay load IAT
);
查找对ImgDelayDescr
结构的其他引用,我们可以找到对__delayLoadHelper2
的调用,该调用随后会调用ResolveDelayLoadedAPI
。在这里,我已经将函数名称、类型和变量进行重命名,以使其更加易于理解。
在Ghidra中查看的__delayLoadHelper2
和ResolveDelayLoadedAPI
:
非常好!这就与我们在Slack尝试加载WINSTA.dll
时在ProcMon堆栈跟踪中看到的结果相匹配了。
ProcMon中的__delayLoadHelper2
和ResolveDelayLoadedAPI
:
在WINSTA.dll
、LINKINFO.dll
、ntshrui.dll
和srvcli.dll
之中,行为是一致的。每个延迟加载的DLL之间,主要区别在于它们的父DLL。在这三个应用程序中:
wtsapi32.dll
延迟加载WINSTA.dll
;
shell32.dll
延迟加载LINKINFO.dll
;
LINKINFO.dll
延迟加载ntshrui.dll
;
ntshrui.dll
延迟加载srvcli.dll
。
观察到了什么有趣的地方吗?似乎shell32.dll
加载了LINKINFO.dll
,而LINKINFO.dll
又加载了ntshrui.dll
,最后由ntshrui.dll
加载了srvcli.dll
。
6.2 NetShareGetInfo和NetShareEnum中的DLL劫持
当Slack尝试加载cscapi.dll
时,我观察了堆栈跟踪,看到其中有一个LoadLibraryExW
调用,该调用似乎源自srvcli.dll
。
加载cscapi.dll
时的堆栈跟踪:
我们再次使用Ghidra打开srvcli.dll
,并选择“Search” -> “For Strings” -> “Filter: cscapi.dll”,双击找到的字符串,并在跟踪引用,看能否找到预期的LoadLibrary
调用。
srvcli.dll
在cscapi.dll
上调用LoadLibrary
:
我们对包含LoadLibrary
调用的函数进行重命名,并跟踪引用,最后找到了两个函数的位置:
NetShareEnum
NetShareGetInfo
NetShareEnum
加载cscapi.dll
:NetShareGetInfo
加载cscapi.dll
:
通过调用NetShareEnum
和NetShareGetInfo
,可以对PoC程序进行验证。
NetShareEnum
加载cscapi.dll
:NetShareGetInfo
加载cscapi.dll
。
七、结果
以下DLL劫持存在于Slack中:
C:UsersJohnAppDataLocalslackapp-4.6.0WINSTA.dll
C:UsersJohnAppDataLocalslackapp-4.6.0LINKINFO.dll
C:UsersJohnAppDataLocalslackapp-4.6.0ntshrui.dll
C:UsersJohnAppDataLocalslackapp-4.6.0srvcli.dll
C:UsersJohnAppDataLocalslackapp-4.6.0cscapi.dll
C:UsersJohnAppDataLocalslackapp-4.6.0KBDUS.DLL
以下DLL劫持存在于Microsoft Teams中:
C:UsersJohnAppDataLocalMicrosoftTeamscurrentWINSTA.dll
C:UsersJohnAppDataLocalMicrosoftTeamscurrentLINKINFO.dll
C:UsersJohnAppDataLocalMicrosoftTeamscurrentntshrui.dll
C:UsersJohnAppDataLocalMicrosoftTeamscurrentsrvcli.dll
C:UsersJohnAppDataLocalMicrosoftTeamscurrentcscapi.dll
C:UsersJohnAppDataLocalMicrosoftTeamscurrentWindowsCodecs.dll
C:UsersJohnAppDataLocalMicrosoftTeamscurrentTextInputFramework.dll
以下DLL劫持存在于Visual Studio Code中:
C:UsersJohnAppDataLocalProgramsMicrosoft VS CodeWINSTA.dll
C:UsersJohnAppDataLocalProgramsMicrosoft VS CodeLINKINFO.dll
C:UsersJohnAppDataLocalProgramsMicrosoft VS Codentshrui.dll
C:UsersJohnAppDataLocalProgramsMicrosoft VS Codesrvcli.dll
C:UsersJohnAppDataLocalProgramsMicrosoft VS Codecscapi.dll
此外,我发现使用NetShareEnum
和NetShareGetInfo
的程序,由于其中包含硬编码的LoadLibrary
调用,因此以cscapi.dll
的形式引入了DLL劫持。通过Ghidra和PoC,我们最终得以确认了这种行为。
八、总结
概括来说,DLL劫持是攻击者在已签名、受信任的应用程序中实现代码执行的一种方式。为防范此风险,我编写了一个工具来帮助自动化发现DLL劫持。使用该工具,我们成功发现了Slack、Microsoft Teams、Visual Studio Code中存在的DLL劫持风险。
我注意到这三个应用程序与其DLL劫持存在重叠的地方,并调查了根本原因。在研究过程中,我重点介绍了我的研究方法。最终,我理解了延迟加载DLL的原理,并从中确定了两个API调用,正是这两个API调用(NetShareEnum
加载cscapi.dll
和NetShareGetInfo
加载cscapi.dll
)将DLL劫持引入了所有调用它们的程序之中。
感谢大家抽出宝贵的时间来阅读这篇文章,希望通过本文,能让大家对于Windows API、Ghidra、ProcMon、DLL和DLL劫持都能有所了解。
九、致谢
非常感谢我的同事Daniel Heinsen(@hotnops)、Lee Christensen(@tifkin_)和Matt Hand(@matterpreter),他们帮助我解决了Ghidra和ProcMon中遇到的一些问题。