探索DLL搜索顺序劫持的原理和自动化侦查方法

 

一、前言

在本篇文章中,将描述动态链接库(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使用GetModuleHandleExGetModuleFileName来确定加载的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.dllLINKINFO.dllntshrui.dllsrvcli.dllcscapi.dll时,我观察了堆栈跟踪。

6.1 延迟加载的DLL

在加载WINSTA.dllLINKINFO.dllntshrui.dllsrvcli.dll时,我注意到堆栈跟踪中存在相似之处。

Code.exe尝试加载WINSTA.dll时的堆栈跟踪:

Teams.exe尝试加载LINKINFO.dll时的堆栈跟踪:

Stack尝试加载ntshrui.dll时的堆栈跟踪:

堆栈跟踪始终包含对_tailMerge_<dllname>_dlldelayLoadHelper2的调用,然后是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中查看的__delayLoadHelper2ResolveDelayLoadedAPI

非常好!这就与我们在Slack尝试加载WINSTA.dll时在ProcMon堆栈跟踪中看到的结果相匹配了。

ProcMon中的__delayLoadHelper2ResolveDelayLoadedAPI

WINSTA.dllLINKINFO.dllntshrui.dllsrvcli.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.dllcscapi.dll上调用LoadLibrary

我们对包含LoadLibrary调用的函数进行重命名,并跟踪引用,最后找到了两个函数的位置:

NetShareEnum
NetShareGetInfo

NetShareEnum加载cscapi.dll

NetShareGetInfo加载cscapi.dll

通过调用NetShareEnumNetShareGetInfo,可以对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

此外,我发现使用NetShareEnumNetShareGetInfo的程序,由于其中包含硬编码的LoadLibrary调用,因此以cscapi.dll的形式引入了DLL劫持。通过Ghidra和PoC,我们最终得以确认了这种行为。

 

八、总结

概括来说,DLL劫持是攻击者在已签名、受信任的应用程序中实现代码执行的一种方式。为防范此风险,我编写了一个工具来帮助自动化发现DLL劫持。使用该工具,我们成功发现了Slack、Microsoft Teams、Visual Studio Code中存在的DLL劫持风险。

我注意到这三个应用程序与其DLL劫持存在重叠的地方,并调查了根本原因。在研究过程中,我重点介绍了我的研究方法。最终,我理解了延迟加载DLL的原理,并从中确定了两个API调用,正是这两个API调用(NetShareEnum加载cscapi.dllNetShareGetInfo加载cscapi.dll)将DLL劫持引入了所有调用它们的程序之中。

感谢大家抽出宝贵的时间来阅读这篇文章,希望通过本文,能让大家对于Windows API、Ghidra、ProcMon、DLL和DLL劫持都能有所了解。

 

九、致谢

非常感谢我的同事Daniel Heinsen(@hotnops)、Lee Christensen(@tifkin_)和Matt Hand(@matterpreter),他们帮助我解决了Ghidra和ProcMon中遇到的一些问题。

(完)