行业新闻

dll 劫持和应用研究

dll 劫持和应用研究

前言

2020年12月,SolarWinds 攻击事件引发全球的关注(https://us-cert.cisa.gov/ncas/alerts/aa20-352a),攻击团队在 2020年上旬通过对 SolarWinds Orion 产品实现供应链攻击,导致诸多厂商被攻击,造成了不可估量的损失。

这种国家间的 APT 攻击包含了大量的技术细节,其中供应链攻击的实现,也就是 SUNBURST 后门植入这一块引起了我极大的兴趣。

2021年1月,网上公开了 SUNBURST 后门植入的分析,后续又有安全研究者对植入细节进一步的优化,根据这些内容我展开了对 dll 劫持的学习和研究。本文对 dll 劫持进行了详细的介绍,并模仿 SUNBURST 后门植入的方法,尝试对 C 编译器实现"供应链攻击"。

本文测试环境为Windows7 x64 sp1,开发环境为:MinGW-x64(gcc-8.1.0) + Python3.6

后门植入和优化

我们可以简单看看 SUNBURST 后门植入的过程(https://www.crowdstrike.com/blog/sunspot-malware-technical-analysis/):由一个名为taskhostsvc.exe的程序进行完成,该程序通过计划任务设置随主机启动运行。

taskhostsvc.exe启动后通过创建互斥体保证只有一个实例在运行,然后每秒从进程中搜索MsBuild.exe进程(Microsoft Build Engine),找到后通过读取MsBuild.exe的内存,从命令行参数中获取构建项目的目录路径;

随后在项目目录下寻找 Orion 产品的InventoryManager.cs源码文件,并使用包含有恶意代码的源码文件进行替换,等待MsBuild.exe编译完成,最后再还原该文件,完成后门的植入;如下图image

SUNBURST后门植入的流程

当然这个过程中还需要很多技术细节来保证后门与原始项目代码之间的兼容性,以及植入过程的隐蔽性等等;后续就有安全研究者说到在 APT 攻击中上文中的植入过程不够完美,比如计划任务和周期性的进程扫描很容易暴露攻击行为,其次监控MsBuild.exe运行到最终替换源码文件,这中间的执行时间可能影响后门植入的成功率。

安全研究者提出了使用 dll 劫持来优化后门植入的过程(https://www.a12d404.net/ranting/2021/01/17/msbuild-backdoor.html),其研究过程发现MSBuild.exe启动过程中会去优先加载指定目录的 dll,如下:image

MSBuild优先加载的部分dll文件(ref:https://www.a12d404.net/ranting/2021/01/17/msbuild-backdoor.html)

如果我们将恶意 dll 重命名并放置在这些"load-not-found-dll"的路径下,就可以实现 dll 劫持,执行我们的恶意代码,原文作者根据这种方式编写代码进行了演示。相比于taskhostsvc.exe,使用这种方式就不需要额外的进程来进行监控了,并且 dll 在程序执行前加载、在程序执行后释放,这个时间点也很适合用于对程序进行控制和清理。

dll劫持概要

dll(动态链接库)作为 windows 的函数库,有助于促进代码的模块化、代码重用、有效的内存使用并减少磁盘空间;一个应用程序运行时可能需要依赖于多个 dll 的函数才能完成功能,如果控制其中任一 dll,那么便可以控制该应用程序的执行流程。

要学习 dll 劫持,那必须先了解 dll 的搜索顺序,这也是攻防的兵家必争之地,微软近年来也不断的在这一块进行加固,对于桌面程序(非UWP应用)目前默认 dll 的搜索顺序为(https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order):

1.应用程序加载的目录

2.系统目录,使用 GetSystemDirectory 获取该路径

3.16 位系统目录

4.Windows 目录,使用 GetWindowsDirectory 获取该路径

5.当前目录

6.PATH 环境变量中列出的目录

默认情况下HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode处于开启状态;如果手动设置为 0,关闭该安全选项,搜索顺序为:在以上顺序基础上,将5.当前目录修改至2.系统目录的位置,其他顺移。

应用程序加载 dll 时如果仅指定 dll 名称时,那么将按照以上顺序搜索 dll 文件;不过在加载之前还需要满足以下两条规范:

1.当内存中已加载相同模块名称的 dll 时,系统将直接加载该 dll,不会进行搜索;除非设置了 dll 重定向选项

2.如果要加载的 dll 模块属于 Known DLLs,系统直接加载系统目录下的该 dll,不会进行搜索;Known DLLs 列表:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs

由于 dll 迭代更新可能出现不兼容的问题,微软提出 dll 重定向解决方案,以便应用程序可以自定义选择加载 dll

在了解以上基础内容后,在 dll 搜索路径上对文件进行替换,那么便可以实现 dll 劫持。

微软在 dll 这一块所做的安全加固详情可以参考:https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-security

dll函数转发

使用恶意 dll 替换原文件,应用程序便可以加载我们的 dll 并执行恶意代码,但是应用程序运行依赖于 dll 提供的函数,恶意 dll 必须提供相同的功能才能保证应用程序的正常运行;所以我们先来了解下 dll 函数转发。

1.手动转发
当 dll 的导出函数比较少时,我们可以按照正常的 dll 开发流程,逐个定义函数名称,然后在函数内部使用LoadLibrary()函数调用原 dll 的对应函数完成功能,如下:image

LoadLibrary转发函数

2.def文件
当 dll 的导出函数太多时,我们就不能手动转发了,使用模块定义(.def)文件编写导出函数的信息,由链接器自动实现函数转发,细节可以参考 https://docs.microsoft.com/en-us/cpp/build/reference/module-definition-dot-def-files?view=msvc-160

我们尝试生成version.dll的恶意 dll:在test.c文件中编写恶意代码插入到DllMain的执行流程中,并在test.def中编写函数转发规则:image

使用dll模块定义进行函数转发

随后进行编译生成 dll 文件,gcc 编译如下:

gcc -Wall -shared test.c test.def -o version.dll

.def文件中我们将原始的文件命名为version_origin.dll,当应用程序运行时将加载我们恶意的version.dll,当调用函数时,将由恶意的version.dll进行函数转发:

something.exe => version.dll (malware) => version_origin.dll

这里我编写了个 Python 脚本可以根据 dll 自动生成模块定义文件dllproxy_def_generate.py,使用如下:image

自动生成模块定义文件

部分 dll 导出函数没有导出名称,只有导出序号,Gcc 和 Tcc 不支持按序号导出的函数转发,读者遇到的话可以使用 VisualStdio

3.pragma预处理
除了上文使用模块定义文件来实现函数转发,还可以使用pragma来实现,这一块细节可以参考 https://docs.microsoft.com/en-us/cpp/preprocessor/comment-c-cpp?view=msvc-160,同样以version.dll为例,函数转发的源码如下:image

pragma实现dll函数转发

不过pragma关键词只有 Microsoft 编译器提供。

路径劫持

根据以上知识,我们可以自由的生成恶意 dll 文件,并且通过函数转发使其调用原始的 dll 函数,完全不会影响应用程序的正常运行。除此之外,恶意代码我们一般可以添加到DllMain中,这样在加载 dll 时便可以触发代码,或者添加到指定函数中,精确劫持程序流程,这取决于实际的场景。到这里我们的恶意 dll 就已经准备就绪了。

根据 dll 的类型,我们可以将劫持大致可以分为两种方式:

1.自定义dll
有些应用程序使用了自定义 dll,这个 dll 是该应用程序特有的,只会被该程序加载和使用。该 dll 可能放置在应用程序同目录下,或者 PATH 环境变量中,或者特定目录通过LoadLibrary([路径])来加载。

这种我们使用恶意 dll 替换目标文件,然后再将原始 dll 重命名并放置在应用程序同目录下(以便函数转发可以顺利进行),当应用程序启动时就可以加载我们的恶意 dll。

2.公共dll
当然我们还可以对公共 dll 进行劫持(比如系统 dll ),但是公共 dll 一般都会提前被其他进程加载,当新的应用程序需要加载时,将直接从内存进行加载和调用,如果我们使用恶意 dll 替换了公共 dll,需要通过重启才能生效。

比如下面是我们使用恶意 dll 替换msvcrt.dll,恶意 dll 在加载时输出应用程序路径,重启后可以看到:image

劫持系统dll示例

由于是公共 dll,那么所有的程序都会加载恶意 dll,这种方法可以用于监控、蜜罐等场景。

1.替换系统dll,可以用普通用户修改文件为拥有者,然后再设置读写权限,就可以修改和替换文件了
2.不能劫持ntdll.dll / kernel32.dll等非常底层的 dll,因为这些 dll 实现了程序装载、函数转发等功能

dll重定向劫持

在我们的学习过程中发现,有些应用程序只依赖了系统 dll,并且这些 dll 已经被其他程序加载了,比如 MinGW(gcc) 只依赖了kernel32.dllmsvcrt.dll,除了上文公共 dll 劫持,还有其他更好的办法吗?

在「0x02 dll劫持概要」中我们还提到一种特例:dll 重定向(https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-redirection),当模块名称相同的 dll 已经被其他应用程序加载到内存中时,可以使用该方法强制加载指定的 dll 文件。通过这种方式,也可以实现 dll 劫持。

dll重定向默认为关闭状态,我们在注册表中HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options添加DevOverrideEnable (DWORD)字段并设置为 1,来开启该功能,重启后生效。image

注册表开启dll重定向

我们有两种方式来使用 dll 重定向:

1. .local
在应用程序同目录下,创建AppName.exe.local的目录,应用程序启动时将优先从该目录下加载 dll 文件。

我们编写了个HelloWorld的 C 程序,生成恶意的msvcrt.dll进行演示,目录结构为:

.
├── test.exe.local
│?? ├── msvcrt.dll
│?? └── msvcrt_origin.dll
├── helloworld.c
└── test.exe

运行演示如下:image

local重定向示例

2.manifest
还可以使用 manifest 配置文件(xml文件),优先级高于.local,详细可以参考 https://docs.microsoft.com/en-us/windows/win32/sbscs/application-manifests,构建目录结构如下:

.
├── helloworld.c
├── msvcrt_origin.dll
├── msvcrt.dll
├── msvcrt.dll.manifest
├── test.exe
└── test.exe.manifest

其中 manifest 文件内容为:image

manifest内容示例

同样可以加载恶意 dll 文件。

Tcc劫持实现

Tcc(Tiny C Compiler) 是一个相当小的 C 编译器,我们从简单的开始对 C 编译器进行"供应链攻击"。

我这里使用了Tcc 0.9.27版本,结合逆向分析可以确定tcc.exe依赖了同目录下的libtcc.dll文件,直接替换该 dll 文件即可。

我们模仿 SUNBURST 后门植入的方法,编写恶意代码 demo 如下(libtcc.c+ libtcc.def):

image

Tcc劫持代码demo

编译后使用恶意 dll 替换libtcc.dll,并将原始文件重命名为libtcc_origin.dll,运行演示如下:

image

Tcc劫持演示

可以看到通过 Tcc 编译的程序,执行时触发了恶意代码backdoor

Gcc劫持实现

我们再来尝试下 Gcc(MinGW),通过分析发现他只依赖了kernel32.dllmsvcrt.dll,那么这里我们劫持msvcrt.dll文件,使用 dll 重定向的方法让 Gcc 加载恶意 dll 文件。

使用 Tcc 中的测试代码进行编译,然后在 Gcc 目录下添加gcc.exe.local文件夹,并将msvcrt.dll / msvcrt_origin.dll放在文件夹下,如下:image

Gcc目录下dll重定向文件夹

运行演示如下:image

Gcc劫持演示

总结

在这里感谢 DAWU@知道创宇404实验室 小伙伴在我学习研究过程中提供的帮助。本文从 dll 劫持的基础出发,逐步讲解和演示 dll 劫持,对 dll 劫持的部分场景和利用进行说明,最后模仿了 SUNBURST 后门植入的方法实现了对 C 编译器的"供应链攻击"。

实际上文中提到的对编译器进行"供应链攻击"的方法还可以进一步优化,因为我们的方法会重新写入文件,从而会修改文件的写入时间,可能会暴露攻击行为;我们还可以通过逆向分析编译器的执行流程,更加精确的劫持读文件的函数,在内存中植入恶意代码,读者可以自行尝试下。

不过 dll 劫持的攻防对抗已经发展很长时间了,微软在保证功能的前提下,已经提供了较为完善的防御措施;对于上文介绍的劫持方法和场景,防御时可以按照文中的技术细节如:路径、注册表、文件进行排查。

References:

https://us-cert.cisa.gov/ncas/alerts/aa20-352a
https://www.crowdstrike.com/blog/sunspot-malware-technical-analysis/
https://www.a12d404.net/ranting/2021/01/17/msbuild-backdoor.html
https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order
https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-redirection
https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-security
https://docs.microsoft.com/en-us/cpp/build/reference/module-definition-dot-def-files?view=msvc-160
https://docs.microsoft.com/en-us/cpp/preprocessor/comment-c-cpp?view=msvc-160
https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-redirection
https://docs.microsoft.com/en-us/windows/win32/sbscs/application-manifests
https://www.exploit-db.com/docs/english/13140-api-interception-via-dll-redirection.pdf
https://github.com/erocarrera/pefile
https://github.com/tothi/dll-hijack-by-proxying
https://stackoverflow.com/questions/37252457/is-there-a-way-to-make-windows-7-x64-load-ntdll-dll-from-local-directory-not-sy

作者:0x7F@知道创宇404实验室

关闭