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路径搜索的变化

早期情况
  1. 程序所在的目录

  2. 加载DLL时所在的当前目录

  3. 系统目录(SYSTEM32)

  4. 16位系统目录(SYSTEM)

  5. Windows目录

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

XP SP2之后

微软为了防止DLL劫持的发生,在XP SP2之后,添加了一个SafeDllSearchModel的注册表。该注册表的路径如下:HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\SafeDllSearchModel

当SafeDllSearchModel的值设置为1,即安全DLL搜索模式开启时,查找DLL目录的顺序如下

  1. 程序所在目录

  2. 系统目录(SYSTEM32)

  3. 16位系统目录(SYSTEM)

  4. WIndows目录

  5. 加载DLL时所在的当前目录

  6. 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搜索顺序如下

  1. 应用程序所在目录

  2. 系统目录(SYSTEM32)

  3. 16位系统目录。没有获取该目录路径的函数,但会对其进行搜索

  4. Windows目录。使用GetWindowsDirectory函数获取此目录的路径

  5. 当前目录

  6. 环境变量中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...


本站由 慕乐 使用 Stellar 创建。