Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
LdrShuffle — 利用 DLL PEB 模块结构操控实现的代码执行/注入技术 | Kitploit
工具/GitHubGitHub/rwxstoned/ldrshuffle
代码分析漏洞利用横向移动Shellcode后渗透利用渗透测试红队Payload 开发二进制利用
GitHubrwxstoned/ldrshuffle

LdrShuffle

利用 DLL PEB 模块结构操控实现的代码执行/注入技术

查看仓库
289441年前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

LdrShuffle

通过修改运行时已加载模块的 EntryPoint 实现隐蔽代码执行。

摘要

Windows 进程在运行时加载了各种模块。其中每个模块都定义了一个 DllMain() 函数,该函数会在进程或线程创建/销毁时被调用(共有四种可能的情形)。

为了在进程生命周期内正确调用这些函数,Windows 加载器函数(ntdll!Ldrp*)会引用一个条目列表,其中包含每个模块的关键参数(包括 EntryPoint 字段)。

通过覆写某个 DLL 的 EntryPoint,我们可以确保代码执行被重定向到我们选择的位置。

使用场景

这既可以用作代码执行原语,也可以用于 API 代理,即:以不引起怀疑的调用栈来运行某些 API,因为它们将由合法的 Windows 函数调用。

这也可以用于在远程进程中触发执行,前提是攻击者能够在目标进程上读写内存。与 Threadless Injection 类似,它提供了在不调用与执行相关的经典 API(CreateRemoteThread、QueueUserAPC)的情况下在进程中执行代码的能力。

挑战

Windows 进程中的模块加载/卸载是一个复杂的主题,带来许多挑战,可能存在不稳定、竞态条件和崩溃。例如,一个与作为 DllMain() 函数的一部分运行代码相关的众所周知的障碍在于:存在加载器锁(Loader Lock),并且我们运行在一个尚未完全设置或正在终止过程中的线程中。

因此,我尝试恰当地记录哪些操作可行,哪些不可行。例如,虽然大多数常规 API 调用可以执行,但运行完整功能的 beacon 需要满足某些要求(例如在单独的进程中),以避免 wininet.dll 或 winhttp.dll 中使用的函数导致的死锁。

实现

Windows 中 DLL 加载回顾

每个进程在运行时维护一个 _LDR_DATA_TABLE_ENTRY 结构列表。这些结构包含与 DLL 相关的许多细节,例如其 EntryPoint(我们将覆写它)、名称、某些哈希、时间戳、各种标志等。其中一些结构有文档记录,而另一些则没有。

可以通过以下 WinDbg 命令查看:

dt nt!_LDR_DATA_TABLE_ENTRY 0xdeadbeef

root@kitploit:~
0:006> dt nt!_LDR_DATA_TABLE_ENTRY 0x18d1c4838c0
ntdll!_LDR_DATA_TABLE_ENTRY
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x0000018d`1c485de0 - 0x0000018d`1c4832b0 ]
   +0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x0000018d`1c485df0 - 0x0000018d`1c4832c0 ]
   +0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x0000018d`1c4832d0 - 0x0000018d`1c482c80 ]
   +0x030 DllBase          : 0x00007ffe`e87e0000 Void
   +0x038 EntryPoint       : 0x00007ffe`e8838d00 Void
   +0x040 SizeOfImage      : 0x2fe000
   +0x048 FullDllName      : _UNICODE_STRING "C:\WINDOWS\System32\KERNELBASE.dll"
   +0x058 BaseDllName      : _UNICODE_STRING "KERNELBASE.dll"
   +0x068 FlagGroup        : [4]  "???"
   +0x068 Flags            : 0x8a2cc
   +0x068 PackagedBinary   : 0y0
   +0x068 MarkedForRemoval : 0y0
   +0x068 ImageDll         : 0y1
(...)
   +0x0e0 MappingInfoIndexNode : _RTL_BALANCED_NODE
   +0x0f8 OriginalBase     : 0x00007ffe`e87e0000
   +0x100 LoadTime         : _LARGE_INTEGER 0x01db5f86`2fa735fc
   +0x108 BaseNameHashValue : 0x235bec4
   +0x10c LoadReason       : 0 ( LoadReasonStaticDependency )
   +0x110 ImplicitPathOptions : 0x4000
   +0x114 ReferenceCount   : 1
   +0x118 DependentLoadFlags : 0x800
   +0x11c SigningLevel     : 0 ''

该结构的地址可以通过遍历进程 PEB 中的 PEB_LDR_DATA 结构所引用的双向链表来获得。

dt nt!_PEB_LDR_DATA 0xb4b4c3c3

注意 DontCallForThreads 标志。顾名思义,如果设置了该标志,操作系统将不会针对线程事件(即 DLL_THREAD_ATTACH 或 DLL_THREAD_DETACH)调用该模块的 DllMain()。

在创建 DLL 时,必须遵循以下模板才能与操作系统加载器函数协同工作:

root@kitploit:~
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.
}

实现的技术细节

设置 API 调用

如上所述,该技术临时覆写 DLL 的 EntryPoint 以重定向执行。由于我们只控制执行重定向,因此需要预先做一些安排来处理我们要运行什么、使用什么参数,以及如何取回返回值。

为此,在堆上定义一个 DATA_T 结构,使其在整个过程中保持可访问。

该结构定义如下:

root@kitploit:~
typedef struct _DATA_T {
    // LDR structures manipulation
    ULONG_PTR   runner;             // malicious entry point to execute
    ULONG_PTR   bakOriginalBase;    // backup of overwritten OriginalBase
    ULONG_PTR   bakEntryPoint;      // backup of overwritten EntryPoint
    HANDLE      event;              // event signalling that the Runner has executed
    // function call
    ULONG_PTR   ret;                // return value
    DWORD       createThread;       // run this API call in a new thread (required for wininet/winhttp)
    ULONG_PTR   function;           // Windows API to call
    DWORD       dwArgs;             // number of args
    ULONG_PTR   args[MAX_ARGS];     // array of args
} DATA_T, * PDATA_T;

要设置 API 执行,需要准备好这些字段。ret 值用于在执行后收集返回值。event 用于同步,表示执行已完成。所有其他字段都是输入,定义要调用哪个 API(function)、使用哪些参数(dwArgs 和 args[])、执行重定向到的 Runner() 函数地址,以及被覆写的原始 DLL 条目的备份(bakOriginalBase 和 bakEntryPoint)。

对于这些在 DllMain() 设置中无法良好运行的复杂 API 函数,需要将 createThread 字段设置为 1(这包括许多 wininet 和 winhttp 库)。

以下是在 PoC 中可见的设置调用 MessageBoxA() 的示例:

root@kitploit:~
 pDataT->dwArgs = 4;
 pDataT->runner = (ULONG_PTR)Runner;
 pDataT->function = (ULONG_PTR)MessageBoxA;
 pDataT->args[0] = (ULONG_PTR)0;
 pDataT->args[1] = (ULONG_PTR)"Hello";
 pDataT->args[2] = (ULONG_PTR)"LDRSHUFFLE";
 pDataT->args[3] = (ULONG_PTR)MB_OKCANCEL;
 pDataT->event = CreateEventA(NULL, FALSE, FALSE, "ExecEvt");

修改 _LDR_DATA_TABLE_ENTRY

UpdateLdr() 函数负责在目标模块的 _LDR_DATA_TABLE_ENTRY 中执行正确的修改。

RestoreLdr() 将在稍后阶段恢复这些更改(由 Runner() 调用)。

这些函数本质上定位 PEB 并遍历模块结构以识别正确的 DLL 及其字段。在头文件中,我复用了 Batsec 在其 DarkLoadLibrary 中使用的定义,并且我鼓励读者查看这个项目和 相关的 MDSec 博客文章,以受益于他在 Windows 模块加载内部机制方面所做的出色工作。

注意:此 PoC 加载一个牺牲 DLL(SACRIFICIAL_DLL_NAME)并对此 DLL 执行这些更改。但是,修改已加载的 DLL 也完全可行。事实上,这正是跨进程注入所采用的方法。出于稳定性考虑,我建议避免修改重要的 DLL,例如 ntdll 或 kernel32,它们也更容易受到安全解决方案的审查。

执行

在线程创建或销毁时,执行被重定向到 Runner(),它充当该模块的伪 DllMain()。该函数随后将:

  • 在堆上定位用于运行调用/获取返回值的数据结构(PDATA_T 结构)
  • 将 PEB RestoreLdr() 恢复到其原始状态
  • 执行正常的 DllMain() 调用(本质上是代理正常的 DLL 调用)。

此时,“正常”的操作系统执行已经完成。然后继续执行我们的载荷:

  • 根据存储在 DATA_T 结构中的值和参数执行我们的恶意 API 调用。如果该 API 被标记为在新线程中运行(createThread = 1),则此调用将在新线程中执行。
  • 最后,发出一个事件信号(pDataT->event),以便我们的主代码知道调用已执行。

当 Windows 最终调用我们的伪 EntryPoint(即 Runner() 函数地址)时,调用栈如下所示:

Callstack on MessageBoxA()

API 代理示例

提供的 PoC 包含一个调用 MessageBoxA() 的示例。

它还包含一个使用 wininet 进行 HTTP 下载的演示。定义 HTTP 变量以启用该代码。

Callstack on MessageBoxA()

跨进程注入示例

上述原则归结为在进程内存空间中进行读写,以便在未来的任意时间点触发代码执行。

稍作调整,这些读写操作就可以应用于远程进程,以覆写其某个 DLL 的 EntryPoint。

一个先决条件是能够在进程内存空间中读写,即:

OpenProcess(PROCESS_VM_READ, FALSE, dwPid) 和 OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, PID)

PoC 中还有一个额外项目,名为 LdrInject,演示如何执行这些步骤。简而言之,它执行以下操作:

  • 在 ReadPEB() 中,它遍历目标进程中的 _LDR_DATA_TABLE_ENTRY 列表,以确定适合覆写的 DLL。请注意,该 DLL 必须具有 DontCallForThreads == 0,因为我们希望 Windows 在线程创建时调用该 DLL 的 EntryPoint。我们也不会选择列表中最先出现的 DLL,因为它们更容易受到安全产品的审查(ntdll.dll、kernel32.dll...)。

  • 该 DLL 的详细信息存储在 PEBINJ_DATA 数据结构中。

  • shellcode(此处为 beacon)通过 InjectShellcodeToRemoteProcess() 写入远程进程空间。

  • 两次 WriteProcessMemory() 调用覆写 DLL 的 EntryPoint,并将其备份到 OriginalBase 中,以便稍后恢复。

此时,下一个 DLL_THREAD_ATTACH 或 DLL_THREAD_DETACH 事件将导致 shellcode 被调用。在运行 beacon 的背景下,这会带来一定的限制和注意事项,下一节将详细介绍。

Cobalt Strike Beacon

该技术导致在非常特定情况下执行 shellcode。加载器锁(Loader Lock)处于活动状态(因为操作系统认为它正在加载/卸载 DLL);线程要么正在创建,要么正在销毁;一般来说,可能存在线程同步问题、死锁等。

在测试过程中,观察到了两个挑战:

  • 运行典型的 Cobalt Strike beacon 在使用 wininet.dll 或 winhttp.dll 中的 API 时会导致死锁。

  • 在线程销毁时运行会导致稳定性问题,因为我们运行在一个正在被销毁的线程中。

为了提高稳定性,我们必须:

  • 确保 beacon 将在新线程中运行。因此,UDRL 将在调用通常的 Cobalt Strike 反射 DLL 入口点之前执行 CreateThread。

  • 只在线程创建时运行,而不是在线程消亡时运行。为此,我们确保当操作系统调用 EntryPoint 时,调用原因是 fdwReason == DLL_THREAD_ATTACH:

winApi.CreateThread(NULL, 4096, (LPTHREAD_START_ROUTINE)&runner, (LPVOID)&ct_data, 0, &dwThId);

而不是通常的

((DLLMAIN)entryPoint)((HINSTANCE)loaderStart, 4, NULL);

root@kitploit:~
    ULONG_PTR __cdecl ReflectiveLoader(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
        // only run for a Thread creation event
        
        if (fdwReason != DLL_THREAD_ATTACH) {
            return TRUE;
        }

        ...
    }

这两个额外步骤已嵌入到 UDRL 的演示中。

测试

已针对 LdrShuffle 测试的 API 列表

VirtualAlloc

VirtualProtect

CreateThread

Sleep

MessageBoxA

InternetOpenW(需要使用 createThread = 1 运行)

InternetOpenUrlA(需要使用 createThread = 1 运行)

TODO

  • 继续为 LdrShuffle 测试更多 API
下载工具