恶意软件重获自由通行证——StackMoonwalk++调用栈欺骗技术深度解析

admin 2025-12-23 01:37:15 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 这篇文章深入介绍了StackMoonwalk++技术,一种高级调用栈欺骗方法,用于绕过现代EDR系统的检测。作者展示了如何伪造调用栈以隐藏恶意软件的真实来源,同时通过ROP链技术实现对恶意载荷的加密和解密,有效规避了包括ElasticSecurityLabs在内的多种安全检测机制。该技术不仅能够创建完全可展开且看起来合法的调用栈,还能在执行期间加密shellcode,显著提高了恶意软件的隐蔽性。 综合评分: 94 文章分类: 恶意软件,漏洞分析,红队,二进制安全,安全开发


cover_image

恶意软件重获自由通行证——StackMoonwalk++ 调用栈欺骗技术深度解析

klezVirus

securitainment

2025年12月22日 15:02 中国香港

TL;DR

随着检测策略越来越重视调用栈遥测和验证,攻击者也在采用更复杂的规避技术来应对。基于我们之前关于 Stack Moonwalk 和 Eclipse 检测算法的研究,本研究引入了一种利用 moonwalking 技术的新方式,其能力远超基础的栈去同步化。

在本文中,我们将展示一个 PoC 来扩展 FullMoon,它不仅允许我们在程序执行期间伪造调用栈以隐藏调用的真实来源,还能避免通常采用 Moonwalking 时留下的若干指标和痕迹。最后,我们将展示如何改进该技术,不仅能在每次调用时伪造栈,还能在执行几乎任何目标 Windows API 时加密我们的恶意载荷。

Overview

本文所涵盖的研究是 Arash Parsa(又名 waldo-irc)、Athanasios Tserpelis(又名 trickster0) 和我 (Alessandro Magnosi,又名 klezVirus) 在 DEFCON 31 上发表的题为 “StackMoonwalk” 的联合研究的后续工作。

关于该技术的博客文章可以在这个博客中找到。我建议先阅读一下,这有助于更好地理解本文的内容。

2023 年,我分享了一个简短预告,展示了我即将在此探讨的内容。它可能引发了一些好奇心,但提供的细节很少,让许多人想知道真正的目标是什么以及它实际上是如何工作的。

Introduction

如前所述,Stack Moonwalk技术需要一组非常特定的栈帧来伪造调用栈,同时保持其完全可展开性。

尽管 Windows 提供了许多这样的帧 (例如,仅在 KernelBase中就超过 100 个),但找到一个既可展开又对操作系统逻辑有效的序列并非易事。

Frame Sequence Definition

设:

  • Q

    为一个函数

  • Q(s) = F

    表示函数 Q在栈上分配的帧

  • 一个帧序列表示为:

    F₁, F₂, ..., Fₙ对于某个整数 n

定义:

  • C[i]

    为属于函数 Fᵢ的指令集

  • 一个特定指令为 Cᵢⱼ(函数 Fᵢ的第 j条指令)

Valid Stack Frame Sequence Requirements

对于一个有效且逻辑合理的帧序列 F[1-4],必须满足以下条件:

  1. F₁

    执行 UWOP_SET_FPREG操作。

  2. F₂

    对 RBP执行 UWOP_PUSH_NONVOL操作。

  3. F₃

    包含一个 ROP gadget:

  • C₃g = JMP [NON_VOL_REG]
  1. F₄

    仅执行 UWOP_ALLOC_SMALL,并以栈轴心 gadget 结束:

  • C₄g = ADD RSP, X; RET
  • 其中 X足够大以存储被伪造函数的所有参数。
  1. 对于 Fᵢ其中 i ∈ [1, 2],存在一个调用指令 Cᵢⱼ使得:
  • Cᵢ = CALL Fᵢ₊₁
  1. 对于 Fᵢ其中 i ∈ [3, 4],存在:
  • Cᵢ = CALL Fᵢ₊₁

    ,并且

  • Cᵢ₊₁ == Cᵢg

    (gadget 指令紧跟在调用之后)

Detection

为了检测 Stack Moonwalk技术的滥用,我们提出了 Eclipse算法,该算法通过对返回地址之前的指令进行严格检查来扩展 RtlVirtualUnwind

对于每个以逆序排列的帧,应用以下检查:

  1. 返回地址处的指令

    是否为 JOP / COP / ROP gadget?

  2. 返回地址之前的指令

    是否为 CALL?

  3. CALL的地址是否与当前帧的 Begin-Address 匹配

    ?

How Eclipse was adapted by major EDRs

基于与 Eclipse 类似的思路,Elastic Security Labs 在真实世界的检测管道中实施调用栈检查方面采取了重要步骤。他们的方法通过将运行时栈跟踪与丰富的符号和模块元数据相关联来增强传统的栈展开,旨在检测异常或间接执行路径,特别关注那些用于代理敏感 API 调用的路径。

Elastic 的检测逻辑针对 Eclipse 强调的相同规避表面,但具有更强的环境上下文和进程级遥测能力,能够识别诸如 trampoline 链、基于回调的执行或栈去同步化等模式。

在评估规避方法之前,有必要明确调用栈分析中检测的构成要素。我们将研究基于 Elastic 的操作模型进行基准测试。检测事件不是单一异常,而是通过语义标签增强的栈检查结果。该检测逻辑区分调用栈中的若干可疑条件类别,总结如下:

| 标签 | 描述 | | — | — | | native_api | 直接调用 Native API,绕过 Win32 API 层 | | direct_syscall | 来自标准 API 层之外的系统调用 | | proxy_call | 栈表明这是一个旨在掩盖真实来源的代理调用 | | shellcode | 镜像映射之外的可执行内存调用敏感 API | | image_indirect_call | 由动态函数解析前置的调用 | | image_rop | 在没有适当 CALL 指令的情况下到达的栈条目 (ROP 行为) | | image_rwx | 栈中的返回地址指向 RWX 镜像 | | unbacked_rwx | 栈引用非镜像的可写内存。在 JIT 或 shellcode 中常见 | | truncated_stack | 栈过早结束,表明篡改或损坏 |

Detecting Moonwalking

在文章 Call Stacks: No More Free Passes For Malware 中,John Uhlmann 详细介绍了 Elastic 最近对调用栈增强能力的改进,旨在检测常见和规避性的调用栈异常。该内容为寻求增强调用栈遥测及其在现代检测策略中作用理解的防御者提供了有价值的见解。

向 Elastic 致敬,感谢他们发布详细的博客和检测规则。这种透明度不仅加强了防御能力,还激发了有意义的社区贡献,推动攻防研究的共同进步。

这些标签说明检测依赖于两阶段模型:

  1. 调用者识别

    :系统必须可靠地展开调用栈并定位负责调用敏感 API(例如 VirtualAllocExNtCreateThreadEx等) 的帧。

  2. 调用者资格审查

    :一旦识别,系统评估调用者帧是否表现出已知的危险信号——例如不可写的代码内存、不当的调用指令来源或无支持的可执行内存区域。

在实践中,这意味着基于栈的检测逻辑严重依赖于栈解析的准确性和元数据 (例如展开信息、PE 映射和内存权限) 的可信度。这造成了一个固有的限制:检测的真相来源最终是调用栈本身,而调用栈可以被攻击者操纵以隐藏执行来源并逃避上述任何标签的分类。

一个明显的例子立即在 Elastic 提供的特定检测逻辑中观察到,该逻辑实际上源自我们 Eclipse 算法的初始实现:通过检查返回之前的指令 (即调用点) 是否确实是调用指令来检测被伪造的帧。这意味着,在栈中向后追溯,我们确保该指令在下面列出的指令之中。

预期的 CALL 指令

另一个例子是依赖检测调用栈中特定的隐藏或去同步化 (desync) gadget。检测隐藏帧的问题在于 kernel32和 kernelbase中大量合法的帧函数表现出类似的结构模式,使可靠的区分变得困难。类似地,依赖识别特定的去同步化 gadget 是有缺陷的:存在许多可行的替代方案来调用恢复函数,这些方案不依赖于固定的寄存器或指令序列。

Elastic 的检测逻辑还假设通过隐藏内存中的原始调用者模块,结果用户模块将是 “未确定的”。虽然这在该技术的某些实现中成立,但我们将演示为什么这一假设在更广泛的条件下会失败。

在可以识别调用者的情况下,可以进一步分析相关的内存区域以查找异常,这可能导致更精确的检测。在 stack moonwalking 的情况下,通常假设模块由于其固有的动态行为 (准确地说,基于 ROP) 而不会在内存中保持加密状态。换句话说,假设调用者在执行时无法加密自身。然而,正如我们将展示的,即使这一假设也是根本错误的。

Let the fun begin! Getting back our free passes

Bypassing Call Instruction Checks

这种检测逻辑的主要问题是,不幸的是,Windows 中存在若干 gadget 可以用作合适的去同步 gadget,并且也在前面附加了一个虚假的调用指令。

对于隐藏 gadget,许多函数确实是 “包装器”,仅负责某些参数的初始化,然后直接返回被包装的函数。一个例子是 VirtualAllocEx,它转而调用 VirtualAllocExNuma:

在 WinDbg 中反汇编的 VirtualAllocExNuma

如观察所示,该函数符合有效的隐藏 gadget 的条件,并在预期的调用点包含合法的调用指令。

去同步 gadget 的情况更为微妙。在 Windows 上,有多个机会可以找到看似包含有效调用指令的去同步 gadget。虽然指定的目标地址有时可能指向未分配的内存页,但它指向内存中的有效区域并非不现实。因此,不仅要验证该指令确实是调用,还要确认目标地址既可解析又映射到可行的内存位置,这一点至关重要。

在内存中找到的伪 CALL 返回

我们更进一步,识别能够绕过 Eclipse 的 JMP/ROP/COP 检测逻辑的 gadget。虽然更复杂,但核心绕过策略保持不变:通过精心选择的合规 gadget 定位执行路径。

有趣的是,我们还可以滥用鲜为人知的 gadget 作为调用和去同步 gadget 之间的 “填充”,前提是它们在栈或返回值修改方面是无害的。例如,像 fcomp这样的指令不引入任何有意义的副作用,可以用来混淆控制流而不会破坏执行。

在 wininet 中找到的可能 gadget

Bypassing “Undetermined” Final User Module and/or Private RW/X Memory Regions

另一个容易绕过的检测技术是假设最终调用者模块必须保持不可解析。在我们之前的研究中,我们暗示了利用额外 DLL(超出 kernelbase) 来定位 gadget 或构造合适帧的可行性。我们最初依赖 kernelbase的唯一原因是由于它方便且一致地提供所需的原语。然而,这一观察可能无意中导致了一个有缺陷的概括:必须使用 DLL 来构造调用栈。但为什么止步于此?为什么不利用被注入或侧载的目标进程的主镜像基址?

为了测试这一假设,我们修改了欺骗例程以允许其作为 shellcode 被注入并远程调用。为了进行比较,我们制作了一个标准的 Metasploit 消息框 shellcode,以对比典型内存中 shellcode 所显示的指标与使用栈欺骗增强的指标。这种设置使我们能够在受控执行环境中观察我们方法的实际影响。

作为注入的目标,我们选择了 OneDrive.exe。选择这一目标的理由将在后续章节中详细讨论。

对于加载和执行方法,由于我们的重点不在于基于注入模式的检测,因此我们选择了使用 VirtualAllocExWriteProcessMemory和 CreateRemoteThread的标准原始注入方法。目前,我们仅专注于分析在内存中执行典型 MessageBox shellcode 时的调用栈。

检测到调用栈异常

如观察所示,调用栈中途中断,表明内存中执行或从缺少运行时异常 (UNWIND) 元数据的代码执行。栈上的返回地址指向目标进程内堆位置的中间,特别是 RWX (读 – 写 – 执行) 私有内存区域。线程启动地址也发生相同的情况,它指向相同内存区域的开始,这是我们用 CreateRemoteThread启动线程的地方。

内存中可见的 Shellcode

要全面绕过此检测逻辑,必须满足以下条件:

  1. 消除调用栈中对欺骗调用者的任何直接引用。
  2. 确保线程的启动地址解析为合法帧 (理想情况下是 “最终用户模块” 的开始)
  3. 隐藏执行期间使用的 RWX 或 RX 内存区域的存在

如前所述,虽然实现点 (2) 相对简单,但使用 stack moonwalk 技术满足条件 (1) 和 (3) 通常被认为是不可行的。然而,我们的实现通过对原始方法的以下修改证明了相反的情况:

  1. 代码在运行时动态解析线程启动地址,消除对编译器内联函数的依赖
  2. 第一个栈帧重新定位在注入进程的镜像基址内,确保与合法执行上下文对齐
  3. 建立自定义 ROP 链以加密并更改 shellcode 区域的内存保护,有效地隐藏部署后的 RWX 或 RX 属性

在此阶段,出现了一个非平凡的挑战:如果我们将必要的函数指针推入栈以解密和保护包含 shellcode 的内存区域,我们如何保持干净且合理的调用栈?ROP 链的执行固有地破坏了预期的调用 – 返回结构,破坏了栈展开机制,或者至少会揭示异常或可疑调用序列的存在。

这正是基于 ROP 链的技术更容易被检测的原因。正如 theFlink (Code White Security) 在识别睡眠信标的研究 DeepSleep 中所指出的,非典型或碎片化调用栈的存在 (通常是基于 ROP 的执行的副产品) 作为识别分阶段或混淆有效载荷的强启发式。

ROP 睡眠加密

此外,由于 shellcode 现在在内存中完全加密,在相应的内存区域被解密并恢复其执行权限之前,无法访问栈恢复功能。

解决方案是 stack moonwalk 技术本身提供的功能所固有的。如前所述,stack moonwalking 导致位于 BaseThreadInitThunk帧和该技术选择的第一个被欺骗帧之间的每个帧在重构的调用栈中变得有效不可见。这意味着在该区域内插入的任何额外帧,包括用于促进解密或内存保护更改的帧,也将对基于栈的检测逻辑保持隐藏。

但这如何帮助我们呢?设 F₁、F₂ 为 stack moonwalk 技术选择的前两个帧。设 G_D 为去同步帧 Gadget,G_C 为隐藏 gadget。设 R_E 表示负责将内存保护修改为 RW (读 – 写) 并加密 shellcode 区域的 ROP 链。相反,设 R_D 表示执行逆操作的相应 (镜像)ROP 链:解密内存并将其保护恢复为 RX (读 – 执行)。

新的欺骗工作流程

主要思想如下:我们将在栈中同时放置 R_E 和 R_D 链。我们将解密链 R_D 放置在隐藏区域中,而加密链 R_E 将在要欺骗的目标函数之后推送。

返回时,首先执行 R_E,紧接着执行被欺骗的函数目标。此时,栈布局精确地符合 stack moonwalk 技术所需的结构,导致完全可展开且干净的调用栈,模仿合法的执行流程。

由于我们在此阶段不能依赖恢复函数——因为 shellcode 仍然是加密的——并且我们不能简单地继续通过栈返回 (因为帧 F₁ 和 F₂ 从未打算在返回时执行),我们需要使用一个巧妙但简单的技巧。我们将把去同步 gadget 指向一个大的栈轴转 gadget,它将跳过 F 帧并小心地落在隐藏区域,正好在我们之前放置 R_D 链的地方。这将允许适当地解密内存中的 shellcode。此时,我们可以安全地将执行重定向到恢复函数,该函数将重构原始栈状态,就像在欺骗序列之前一样。

我们新创建的栈将如下所示:

栈欺骗成功

内存加密和保护成功

以下视频显示了该技术的实际应用:

Evasion Evaluation

公共检测工具完全无法识别 StackMoonwalk++ 引入的调用栈篡改。值得注意的是,Hunt-Sleeping-Beacons 和 Get-InjectedThreadEx 等工具无法通过调用栈分析识别 SM++ Shellcode 或其相关的睡眠行为。

为了确保公平评估,我们明确配置线程进入 DelayExecution状态,这是这些工具旨在标记的已知行为特征。尽管如此,如所展示的,这两个工具都未能检测到我们技术的存在。

目标进程扫描异常

hasherazade 的优秀项目 hollows_hunter 在依赖调用栈分析时同样无法检测到 StackMoonwalk++ 引入的恶意行为——如下面的示例所示。

Hollows Hunter (基于栈的扫描)

然而,由于我们使用 SystemFunction032作为加密和解密例程,当使用 /obfus 3参数运行时,hollows_hunter 可以检测到加密的 shellcode。在此配置下,该工具识别混淆内存区域的特征模式,即使调用栈仍然无法检测。

作为最后一步,我们确认 Moonwalk 技术,通过本博客文章中引入的改进增强,成功绕过了我们公开可用的 Eclipse 检测算法实现。

Eclipse 扫描

Code

此代码可在 Moonwalk++ 找到。由于 GitHub 不允许在名称中使用 +,因此称为 Moonwalk–。

Disclaimer

代码丑陋至极,与原始 PoC 一样。它应该让你看到正在发生什么而不会有太多复杂性,但……好吧,我想你需要自己弄清楚一些东西。

最重要的文件是 ASM 文件。mooninject.asm代码一旦汇编,就是你将在 PoC 中找到的 shellcode。

Closing Remarks

检测通常依赖于假设。当然,这些假设变得越脆弱,在实践中就越容易绕过。考虑到这一点,我想结束 Moonwalk 系列。在我看来,这项技术一直被严重低估或误解。

就我而言,在过去的几年里,我很享受玩这个技术的乐趣,而这个最终作品是我展示如果稍微推动一下它可以走多远的方式。

Thanks

和我之前关于 StackMoonwalk 的博客文章一样,我想再次感谢 namazso 他之前参与了导致 StackMoonwalk 创建的研究,以及他之前的支持。

当然,非常感谢我的好友 Arash Parsa(又名 waldo-irc) 和 Athanasios Tserpelis(又名 trickster0),他们帮助我完成了最初的 Stack Moonwalk 研究。

References

  • StackMoonwalking
  • John Uhlmann, Elastic Security Labs. 2023. Call Stacks: No More Free Passes for Malware
  • thefLink/Hunt-Sleeping-Beacons

Malware Just Got Its Free Passes Back!

免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。


免责声明:

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

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

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

本文转载自:securitainment klezVirus《恶意软件重获自由通行证——StackMoonwalk++ 调用栈欺骗技术深度解析》

评论:0   参与:  0