工具|NocturneLdr—CET兼容的调用栈伪装Loader

admin 2026-08-04 05:47:47 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: NocturneLdr是一个研究导向的Windowsx64shellcodeloader,核心目标是产生对WinDbg、ProcessHacker与IntelCETshadowstack都不可区分的合法调用栈。其技术内核是BYOUD的工程化实现,通过将代码注入合法模块.text段并注册真实unwind元数据来绕过CET校验。项目设计亮点包括高质量注释、可逆性变更跟踪、CRT-free与编译期哈希。但存在严重代码质量问题,如FindShieldGadget可能越界读未映射内存、硬编码偏移导致跨版本脆弱、大量重复代码与死代码。建议仅用于研究,生产使用前需修复稳定性问题。 综合评分: 84 文章分类: 红队,渗透测试


cover_image

工具 | NocturneLdr — CET 兼容的调用栈伪装 Loader

原创

GLM-5.2 GLM-5.2

赛博生存指南

2026年7月29日 08:28 日本

在小说阅读器读本章

去阅读

仓库地址:https://github.com/xec412/NocturneLdr 本文是对开源 Windows x64 shellcode loader NocturneLdr 的源码级质量评估。阅读对象为红队研究者、蓝队/EDR 工程师,以及关注 Windows 底层对抗的开发者。NocturneLdr 围绕一个明确目标设计:产生对 WinDbg、Process Hacker 与 Intel CET shadow stack 都不可区分的合法调用栈。技术内核是 klezVirus BYOUD(Bring Your Own Unwind Data)的工程化实现,叠加 Zilean 风格的睡眠混淆、堆加密、EAF bypass 与线程池代理调用。本文侧重代码工程质量与实现正确性评估,不构成使用指引。


目录

  1. 项目定位与技术栈
  2. 架构与模块划分
  3. 设计亮点
  4. 代码质量问题
  5. 安全与健壮性
  6. 量化评分
  7. 改进建议与结论

1. 项目定位与技术栈

NocturneLdr 是一个研究导向的 Windows x64 shellcode loader。它不追求 payload 投递的通用性,而是把全部工程精力压在一个对抗点上:调用栈完整性(call stack integrity)

现代 EDR 与人工分析师把调用栈作为首要检测信号。未 backed 的返回地址、错位的栈帧、动态函数表痕迹,都是强恶意指标。传统栈伪装能处理一部分,但在 Intel CET(Control-flow Enforcement Technology)的 shadow stack 校验下会失效——硬件维护了一份只读的返回地址副本,必须与软件栈一致。

NocturneLdr 的解法是换思路:不伪造假栈帧,而是把代码注入到合法模块的 .text 段,并用该模块真实的 donor unwind 元数据注册一条 RUNTIME_FUNCTION。Windows 展开器(unwinder)随后用真实 unwind 信息走栈,产生的帧指向一个签名、backed 的 DLL(windows.storage.dll)。因为代码物理上位于模块地址范围内,unwind 链结构合法,软件栈遍历与 CET 硬件校验都看到一条合法调用链。

构建特性(vcxproj 确认):

  • /GS- 关闭栈 cookie,/EHA- 关闭 SEH 表生成,/Gs999999 关闭栈探测
  • 自定义入口、自实现 memset/memcpyoperator new/delete 重载走 HeapAlloc
  • 编译期 DJB2 哈希(constexpr),二进制中零 API 名字符串
  • IAT 只保留良性 USER32 导入作为伪装

2. 架构与模块划分

源码规模适中:6 个头文件、9 个 C++ 实现、5 段 MASM 汇编。按 namespace 干净拆分,职责单一。

include/
├── Common.h          全局 g_Win32 API 结构与跨模块函数声明
├── Structs.h         NT 结构与自定义类型(约 1500 行,含大量未用模板)
├── Primitives.h      编译期哈希、CRT 替代、new/delete 重载
├── InitializeAPI.h   运行时 API 解析初始化
├── IatCamouflage.h   IAT 伪装(良性 USER32 导入)
└── Debug.h           调试控制台与 DBGPRINT 宏
src/
├── Main.cpp          入口与编排
├── Resolver.cpp      PEB 遍历与哈希解析(含 EAF bypass)
├── StackSpoofing.cpp 栈欺骗编排
├── Unwind.cpp        unwind 解析与动态/反向函数表操作
├── Context.cpp       变更跟踪与回滚
├── StackUtils.cpp    栈原点与 cookie 检测
├── Proxy.cpp         线程池代理调用
├── Zilean.cpp        睡眠混淆、堆加密、ROP 链
├── Intrinsic.cpp     memset
├── Debug.cpp         调试控制台分配
└── *.asm             ShadowGate / Unguard / ApiStub / StackSearch / SetRegister

Common.h 用单一全局结构 g_Win32 集中管理所有动态解析的 API 指针,按 Nt/K32/AdvApi32/CryptBase 分组,跨模块查找方便。这是一个值得肯定的组织方式。


3. 设计亮点

3.1 注释质量很高,这是最大的优点

几乎每个函数都有块注释说明”做什么 + 为什么”。例如 Unwind.cpp 解释 SRW lock 定位逻辑,Resolver.cpp:260 解释三种 CALL 指令编码差异,Context.cpp:164 注释正确指出回滚必须逆序、PAGE_NOACCESS 必须先撤销。对研究型项目而言,这种”why 注释”远比”what 注释”有价值。

3.2 BYOUD 变更跟踪与回滚

Context.cpp 的 BYOUD_CONTEXT 配合 BYOUD_CHANGE 联合体,记录每一处篡改:缓存清零、.pdata 保护、反向表收缩、动态表插入、类型篡改、shellcode 注入。RevertAllSpoofChanges 逆序回滚。这是工程化的可逆性设计,比一次性破坏强得多,也使整体操作可以在 payload 返回后被干净撤销。

3.3 CRT-free 与编译期哈希

Primitives.h:161 重载全局 operator new/delete 走 HeapAllocIntrinsic.cpp 自实现 memsetPrimitives.h:56 自实现 memcpy/memcmp/strlenPrimitives.h:22 用 constexpr DJB2,哈希在编译期求值为立即数,运行时只比对常量,二进制中不残留 API 名字符串。

3.4 EAF bypass:gadget 路由读

Unguard.asm 的 ShieldedRead 与 Resolver.cpp:83 配合:所有导出表读取经一个 ntdll 内的 MOV RAX,[RAX];RET gadget 完成,让 EAF 的硬件断点看到的是签名模块内的读,而不是 loader 自身的解引用。设计思路干净,ShieldedRead 还保留了 gadget 为空时退回普通解引用的兜底分支。

3.5 线程池代理调用

Proxy.cpp 把 LoadLibraryWNtProtectVirtualMemory 经 RtlQueueWorkItem 转移到 ntdll 线程池 worker 上执行,调用源伪装成合法的 ntdll 线程池回调。WorkItemNtProtectVirtualMemory 用一段汇编 stub(ApiStub.asm)把参数重新摆进寄存器再 jmp 到真实 API,避免了直接 call

3.6 多层栈欺骗编排

StackSpoofing.cpp:259 的 RegisterSpoofedRuntimeFunction 把整条链按正确顺序串起:donor unwind 借用 → 反向函数表收缩 → ntdll 缓存失效 → RtlAddFunctionTable 加 Type=0 篡改 → .pdata 置 PAGE_NOACCESS。每一步都登记进 BYOUD_CONTEXT。层次清晰,编排正确。


4. 代码质量问题

4.1 严重:潜在崩溃与正确性

FindShieldGadget 可能越界读未映射内存Resolver.cpp:46-63)。

Memory = (ULONG_PTR)...ntdll.dll... + 0x1000;
for&nbsp;(ULONG_PTR Len =&nbsp;0; Len <&nbsp;0x1000&nbsp;*&nbsp;0x1000; Len++) { &nbsp;// 扫描 16 MB
if&nbsp;(Primitive::MemoryCompare((PVOID)(Memory + Len), Pattern,&nbsp;4) ==&nbsp;0)

循环上限 0x1000 * 0x1000 是 16 MB,但 ntdll 的 SizeOfImage 通常远小于此。一旦扫描越过 ntdll 末尾进入未映射页,立即触发 access violation。应当用 SizeOfImage 收敛上界,并预留 sizeof(Pattern) 的读边界。

FindShieldGadget 每次解析都全量扫描Resolver.cpp:83)。LdrShieldedSymbolResolveByHash 内部每次都调 FindShieldGadget(),而 InitializeAPI.h 会调用它几十次。结果是初始化阶段对 ntdll 做了几十次 16 MB 线性扫描。gadget 应解析一次后缓存。

RevertAllSpoofChanges 永远返回 TRUE,错误被吞Context.cpp:161Context.cpp:224)。

BOOL B0K = TRUE; &nbsp;&nbsp;// 注意是字母 O,命名也怪
...
return&nbsp;B0K; &nbsp; &nbsp; &nbsp; &nbsp;// 从不置 FALSE

回滚中任何一步失败都不会上报,调用方无法感知清理不完整。B0K 这个命名(字母 O 而非数字 0)也增加了阅读负担。

Random32 注释与实现不符Zilean.cpp:33-45)。注释说”derived from KUSER_SHARED_DATA interrupt time fields”,实现却是 _rdrand32_step(硬件随机指令)。误导性注释。

堆加密:注释与 README 说 RC4,实际是自定义 XORZilean.cpp:369)。MaskHeap 调用的是 XorCipherZilean.cpp:15,带位置相关 ^ j 的 XOR),不是 SystemFunction032(RC4)。而 SystemFunction032 在 InitializeAPI.h:115 被解析却从未使用——既是死代码,也是文档与实现不符。

跨版本脆弱的硬编码偏移与魔数

  • Context.cpp:203*(PDWORD)((ULONG_PTR)pEntry + 0x50) 硬编码 DYNAMIC_FUNCTION_TABLE.Type 字段偏移 0x50。
  • StackSpoofing.cpp:35Unwind.cpp:653+ 0x300000/0x200000 范围检查,1004/1019/1017 循环边界。
  • Unwind.cpp:642:依赖 RtlAddFunctionTable 内 33 C9 48 89 43 28 E8 字节序列。

这些 pattern scan 与具体 Windows 构建强耦合,版本升级极易失效,且没有任何版本检测或兜底路径。这是该类技术的通病,但本项目没有做任何缓解。

4.2 中等:可维护性与重复

三段几乎逐行重复的代码

  • StackSpoofing.cpp:15-105FlushFunctionTableCache 与 FlushFunctionEntryCache 约 40 行,只有目标函数名字符串(RtlLookupFunctionTable 与 RtlLookupFunctionEntry)不同,应抽公共函数。
  • Zilean.cpp:53-357SuspendThreadsResumeThreadsGetRandomThreadContext 各约 90 行,NtQuerySystemInformation 双调用加线程遍历框架完全相同,应抽取 EnumerateProcessThreads(callback)。这是全仓库最严重的重复。

Structs.h 大量未使用的结构(约 1500 行)。PS_CREATE_INFOPS_ATTRIBUTE_*FILE_INFORMATION_CLASSRTL_USER_PROCESS_PARAMETERSPROCESS_BASIC_INFORMATION 等 maldev 模板结构定义了却全项目无引用。可删减约三到四成行数,降低维护负担与编译产物体积。

GetRandomThreadContext 名不副实Zilean.cpp:258)。名为 Random,实现是”取第一个非当前线程”,且不排除 worker 线程,与 SuspendThreads 的排除逻辑不一致。

SharedSleep 的沙箱漂移检测是空操作Proxy.cpp:31-41)。

for&nbsp;(...; SharedTimeStamp() < Start64; Index++); &nbsp; &nbsp; &nbsp;// 已经 sleep 完
if&nbsp;((SharedTimeStamp() - Start64) >&nbsp;2000)&nbsp;return; &nbsp; &nbsp;&nbsp;// 检测在循环之后,return 也无意义

注释说检测时间加速,但检测点在 sleep 结束之后,且之后没有代码,return 没有实际效果。

4.3 轻微:风格与一致性

死代码ConstructShadowGateParamsVaStackSpoofing.cpp:223)全项目未调用;SystemFunction032 解析未用(见上)。

错误处理不一致Main.cpp:105 等处对 NtCreateEventRtlQueueWorkItem 的返回值未检查就直接使用句柄,而 Zilean.cpp 内部却检查得很细。

README 与编译配置的表述张力。README 称”custom entry point”,vcxproj 实为标准 mainNocturne.vcxproj:132);README 同时声称 /NODEFAULTLIB(CRT-free)与”static CRT /MT“,两者互斥,表述需澄清。

命名小瑕疵B0K(字母 O)、STATUS_UNSUCCESSFULL(多一个 L)、头文件名 Primitives.h 在 guard 宏里拼成 PRIMIVITES_H


5. 安全与健壮性

| 项 | 评价 | | — | — | | 内存安全 | LocateTextAppendCursorUnwind.cpp:724)用 0xCC/0x00 启发式判断 .text 空闲区。若合法代码末尾恰有 0x00 立即数,会误判边界,可能覆盖合法指令。启发式不可靠。 | | 可重入性 | AppendCodeToText 内 static DWORD s_dwBytesWrittenUnwind.cpp:777)是隐式累积状态,函数不可重入,并发或重入会错位。 | | 栈使用 | MaskImage 在 worker 栈上放 CONTEXT Ctx[10](约 12 KB),偏大但可接受。 | | 递归 | 转发导出 LdrShieldedSymbolResolveByHash 递归无深度上限(Resolver.cpp:143),理论上可被恶意转发链耗尽栈。 | | EAF 一致性 | 主解析路径走 ShieldedRead,但转发名读取 MemoryCopy(Forwarder, (PVOID)FunctionAddress, ...)Resolver.cpp:127)直接解引用,绕过了 gadget,削弱了 EAF bypass 的覆盖完整性。 |


6. 量化评分

| 维度 | 评分 | 说明 | | — | — | — | | 架构与模块化 | 8 | namespace 隔离清晰,职责单一 | | 注释与文档 | 8.5 | 几乎全函数”why 注释”,研究价值高 | | 技术深度 | 9 | BYOUD 加反向表/缓存/WinDbg bypass 加 Zilean ROP,少见的高完成度实现 | | 可维护性 | 5 | 大量重复、死代码、魔数、跨版本脆弱 | | 健壮性与正确性 | 5.5 | 潜在越界崩溃、错误吞没、注释与实现不符 | | 一致性 | 6 | 错误处理风格不统一,命名小瑕疵 |

综合来看,作为研究型与学习型项目,工程质量在同类 maldev 项目里属于上乘;作为可长期维护的工程代码,则存在明显的重复与技术债。


7. 改进建议与结论

按优先级给出改进方向:

  1. 修 FindShieldGadget 边界(用 SizeOfImage)并缓存 gadget——一次性消除崩溃风险,同时大幅提速初始化。
  2. 抽取线程枚举公共函数,消除 Zilean.cpp 三段重复,收益最大的可维护性提升。
  3. 合并两个 Flush*Cache 为参数化单函数。
  4. 修正注释与 README 与实现的不符(Random32MaskHeap 的 RC4 与 XOR、SystemFunction032 死代码、entry point 表述)。
  5. RevertAllSpoofChanges 真正传播失败,并修正 B0K 命名。
  6. 清理 Structs.h 未用结构与未调用函数,降低噪声。

结论:NocturneLdr 是一份技术含量很高的调用栈对抗研究实现,亮点在于编排完整的 BYOUD 管线、可逆的变更跟踪与高质量的”why 注释”。短板集中在工程化层面——重复代码、死代码、与 Windows 版本强耦合的 pattern scan、以及若干注释与实现脱节之处。它适合作为学习 Windows unwind 机制与高级栈欺骗的参考样本,若要投入实际长期使用,则需要先补齐边界检查、缓存与重复消除这些基础工程项。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:赛博生存指南 GLM-5.2 GLM-5.2《工具 | NocturneLdr — CET 兼容的调用栈伪装 Loader》

自动化渗透测试与AI红队 网络安全文章

自动化渗透测试与AI红队

文章总结: 本文档系统梳理了授权渗透测试方法论、AI辅助安全评估框架、红蓝对抗体系建设与蓝队自动化响应技术。核心结论是渗透测试必须基于明确书面授权,AI可提升测
评论:0   参与:  0