0x00 DLL是什么
动态链接库(英语:Dynamic-link library,缩写为DLL)是微软公司在 Windows系统中实现共享函数库概念的一种实现方式。这些库函数的扩展名是.DLL
、.OCX
(包含ActiveX控制的库)或者.DRV
(旧式的系统驱动程序)。总而言之,它包含了可由多个程序同时使用的代码和数据。
每个DLL文件都会有一个默认的DLLMain(入口函数),根据MSDN的描述以下四种情况会调用此函数:
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // handle to DLL module
DWORD fdwReason, // reason for calling function
LPVOID lpvReserved ) // reserved
{
// Perform actions based on the reason for calling.
switch( fdwReason )
{
case DLL_PROCESS_ATTACH:
// Initialize once for each new process.
// Return FALSE to fail DLL load.
break;
case DLL_THREAD_ATTACH:
// Do thread-specific initialization.
break;
case DLL_THREAD_DETACH:
// Do thread-specific cleanup.
break;
case DLL_PROCESS_DETACH:
if (lpvReserved != nullptr)
{
break; // do not do cleanup if process termination scenario
}
// Perform any necessary cleanup.
break;
}
return TRUE; // Successful DLL_PROCESS_ATTACH.
}
进程载入DLL
进程卸载DLL
当前进程正在创建新线程
线程正在完全退出
另外值得注意的一点是,在线程退出时,如果使用的API不是ExitThread
而是TerminateThread
,那么此函数不会被调用。
除了入口函数,我们还应该关注的一个地方就是该文件的导出函数表,这里也会提供被需要使用的函数逻辑块。
0x01 DLL路径搜索的变化
早期情况
程序所在的目录
加载DLL时所在的当前目录
系统目录(SYSTEM32)
16位系统目录(SYSTEM)
Windows目录
PATH环境变量中列出的目录
XP SP2之后
微软为了防止DLL劫持的发生,在XP SP2之后,添加了一个SafeDllSearchModel
的注册表。该注册表的路径如下:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\SafeDllSearchModel
当SafeDllSearchModel的值设置为1,即安全DLL搜索模式开启时,查找DLL目录的顺序如下
程序所在目录
系统目录(SYSTEM32)
16位系统目录(SYSTEM)
WIndows目录
加载DLL时所在的当前目录
PATH环境变量中列出的目录
Win7以上版本
微软为了更进一步的防御系统的DLL被劫持,将一些容易被劫持的系统DLL写进了一个注册表项中,那么凡是此项下的DLL文件就会被禁止从EXE自身所在的目录下调用,而只能从系统目录即SYSTEM32目录下调用。注册表路径如下:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
以前经常使用的一些劫持DLL已经被加入了KnownDLLs注册表项,这就意味着使用诸如usp10.dll,lpk.dll,ws2_32.dll去进行DLL劫持已经失效了。
所以在win7及以上当启用了SafeDllSearchMode搜索顺序如下
应用程序所在目录
系统目录(SYSTEM32)
16位系统目录。没有获取该目录路径的函数,但会对其进行搜索
Windows目录。使用
GetWindowsDirectory
函数获取此目录的路径当前目录
环境变量中PATH的所有目录(不包括App Paths注册表所指定的目录)
微软允许用户在上述注册表路径中添加ExcludeFromKnownDlls
注册表项,排除一些被KnownDLLs
保护的DLL。也就是说,只要在ExcludeFromKnownDlls
注册表项中添加你想劫持的DLL名称就可以对该DLL进行劫持,不过修改之后需要重新启动电脑才能生效。
根据以上DLL搜索顺序我们可以知道不管是什么情况下,应用程序首先都会从应用程序所在的目录加载DLL文件,如果没有才会依次搜索。那么,利用这个特性,攻击者就可以伪造一个相同名称的dll,只要这个dll不在KnownDLLs注册表项中,我们就可以对该dll进行劫持测试。
Win10的受保护的dll如下(看起来还是蛮多的)
0x02 寻找可劫持的DLL
接下来我们以系统自带的记事本(Notepad.exe)为例,查看一下exe加载了哪些dll吧
大名鼎鼎的火绒剑🗡
ProcessHacker🔍
微软家的进程监视器 Process Monitor
除此之外还有很多别的优秀工具,这里我就不一一展示了
0x03 劫持测试
首先我们得确认一下想要劫持的目标程序需要载入的所有dll文件,并且还不能被KnownDLLs
所保护,这里我用python写了个小工具来实现这个功能(由衷感叹一句python是真的方便,遍历模块直接一个函数就搞定了,C还得手动创建快照循环遍历/(ㄒoㄒ)/~~)
# -*- coding: utf-8 -*-
# @Time : 2024/5/25 下午6:54
# @Author : JokerDev
# @File : DllHijectDemo.py
# @Software: PyCharm
import winreg
import os
import psutil
# 查询进程内的模块信息
def list_dll_modules(pid):
modulelist = []
# 获取进程句柄
try:
process = psutil.Process(pid)
except psutil.NoSuchProcess:
print(f"No process with PID {pid} found.")
return
# 打印进程信息
print(f"Process ID: {process.pid}")
print(f"Process Name: {process.name()}")
# 读取进程内的模块信息
try:
modules = process.memory_maps()
for module in modules:
module_path = module.path
if module_path.lower().endswith('.dll'):
modulelist.append(os.path.basename(module_path))
# print(f"Module Name: {os.path.basename(module_path)}, Path: {module_path}")
return modulelist
except Exception as e:
print(f"Could not retrieve modules for PID {pid}: {e}")
# 查询注册表中的 KnownDLLs
def query_known_dlls():
DllKnownlist = []
try:
# 打开注册表路径
registry_path = r"SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs"
reg_key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, registry_path)
# 获取注册表键的值
index = 0
while True:
try:
value_name, value_data, value_type = winreg.EnumValue(reg_key, index)
DllKnownlist.append(value_name)
# print(f"Name: {value_name}, Data: {value_data}")
index += 1
except OSError:
# 枚举结束
break
# 关闭注册表键
winreg.CloseKey(reg_key)
return DllKnownlist
except Exception as e:
print(f"Error accessing registry: {e}")
def main():
DllKnownlist = query_known_dlls()
target_pid = input("Please input the target PID: ")
Modulelist = list_dll_modules(int(target_pid))
module_ok = [module for module in Modulelist if module not in DllKnownlist]
for m in module_ok:
print(f"Module Name: {m}")
if __name__ == '__main__':
main()
首先我们可以自己实现一个简单的小程序和dll文件来验证这个过程,我使用VS 2022 创建了一个项目,项目结构如下,代码我已经上传Github,有兴趣的小伙伴可以点个star⭐~
MainApp 主启动程序,它载入自编写的dll文件执行功能
MainDll 自编写的dll文件,它被主启动程序载入
HijackDll 用来劫持的dll
首先我们先启动我们的小程序,使用火绒剑查看进程信息
得到PID为7456,并且也是成功的载入了我们自己的MainDll,然后就能使用python脚本去验证可以用来劫持的dll名称
然后将我们准备好的劫持dll文件名改成MainDll.dll并替换目录下的原文件,可以看到内容已经被替换了,说明我们自己的dll劫持验证成功 0v0
0x04 经典白加黑
todo...