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

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

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

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

工具目录

分类

查看所有分类
Loading categories
ShellcodeFluctuation — 一种高级内存规避技术,在 RW/NoAccess 与 RX 之间波动切换 shellcode 的内存保护,并对其内容进行加密/解密。 | Kitploit
工具/GitHubGitHub/mgeeky/shellcodefluctuation
Shellcode后渗透利用红队Payload 开发对抗性攻击
GitHubmgeeky/shellcodefluctuation

ShellcodeFluctuation

一种高级内存规避技术,在 RW/NoAccess 与 RX 之间波动切换 shellcode 的内存保护,并对其内容进行加密/解密。

查看仓库
1.1k1634年前Kitploit 审核通过

最受欢迎

查看全部 →

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

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

Shellcode Fluctuation PoC

A PoC implementation for an another in-memory evasion technique that cyclically encrypts and decrypts shellcode's contents to then make it fluctuate between RW (or NoAccess) and RX memory protection. When our shellcode resides in RW or NoAccess memory pages, scanners such as Moneta or pe-sieve will be unable to track it down and dump it for further analysis.

简介

在发布 ThreadStackSpoofer 之后,我收到了一些关于以下 README 要点的提问:

在 Beacon 睡眠前,将其内存页保护从 RX/RWX 改为 RW,并加密其内容(这样可以规避诸如 Moneta 或 pe-sieve 之类的扫描器)

在此之前,我曾相当确信社区已经知道如何加密/解密其载荷并翻转内存保护,以简单规避那些寻找异常可执行区域的内存扫描器。 事实证明并非如此,因此我决定发布这个未武器化的 PoC,以记录又一种规避策略,并为社区提供可用的示例实现。

这个 PoC 演示了一种相当简单的技术,红队社区早已熟知(所以我其实并没有带来什么新东西),希望借此揭开某些商业框架在演示针对上述两款内存扫描器的规避能力时所展现的“魔法”背后的秘密。

以下是对波动到 RW 时的对比(另一种选择是波动到 PAGE_NOACCESS——见下文描述):

  1. Beacon 未加密
  2. Beacon 已加密(波动中)

对比

该实现连同我的 ThreadStackSpoofer,为攻防安全社区带来了可与商业 C2 产品功能比肩的示例实现,使我们在红队工具上也能不落下风。💪


工作原理?

该程序执行自注入 shellcode(大致通过经典的 VirtualAlloc + memcpy + CreateThread)。 当 shellcode 运行(此实现专门针对 Cobalt Strike Beacon 植入体)时,将会挂钩一个 Windows 函数,以拦截 Beacon 进入睡眠的时机 kernel32!Sleep。 每当被挂钩的 MySleep 函数被调用时,它会定位其内存分配边界,将保护属性翻转为 RW,并对其中存储的所有字节进行 xor32 加密。 在等待了预期的时间后,当 shellcode 返回到我们的 MySleep 处理程序时,我们会解密 shellcode 的数据,并将保护属性翻转回 RX。

波动到 PAGE_READWRITE 的工作方式如下

  1. 从文件中读取 shellcode 的内容。
  2. 挂钩 kernel32!Sleep,使其指向我们的回调函数。
  3. 通过 VirtualAlloc + memcpy + CreateThread 注入并启动 shellcode。与我们在 ThreadStackSpoofer 中所做的相反,这里我们不在 ntdll 中挂钩任何东西来启动 shellcode,而是从我们自己的函数跳转到它。这样做的目的是避免在内存中留下指向被修改的 ntdll 内存的简单 IOC。
  4. 一旦 Beacon 尝试睡眠,我们的 MySleep 回调就会被调用。
  5. Beacon 的内存分配被加密,保护属性翻转为 RW
  6. 然后我们取消挂钩原始 kernel32!Sleep,以避免在内存中留下表明 Sleep 已被 trampoline(内联挂钩)的简单 IOC。
  7. 调用原始 ::Sleep,让 Beacon 在等待后续通信时保持睡眠。
  8. 睡眠结束后,我们解密 shellcode 的数据,将内存保护翻转回 RX,然后重新挂钩 kernel32!Sleep,以确保拦截后续的睡眠。

波动到 PAGE_NOACCESS 的工作方式如下

  1. 从文件中读取 shellcode 的内容。
  2. 挂钩 kernel32!Sleep,使其指向我们的回调函数。
  3. 通过 VirtualAlloc + memcpy + CreateThread 注入并启动 shellcode ...
  4. 初始化向量化异常处理程序(VEH),以设置我们自己的处理程序来捕获 访问冲突 异常。
  5. 一旦 Beacon 尝试睡眠,我们的 MySleep 回调就会被调用。
  6. Beacon 的内存分配被加密,保护属性翻转为 PAGE_NOACCESS
  7. 然后我们取消挂钩原始 kernel32!Sleep,以避免在内存中留下表明 Sleep 已被 trampoline(内联挂钩)的简单 IOC。
  8. 调用原始 ::Sleep,让 Beacon 在等待后续通信时保持睡眠。
  9. 睡眠结束后,我们重新挂钩 kernel32!Sleep,以确保拦截后续的睡眠。
  10. 随后 shellcode 尝试恢复执行,这会因它的页面被标记为 NoAccess 而引发访问冲突。
  11. 我们的 VEH 处理程序捕获该异常,解密并将内存保护翻转回 RX,shellcode 随即恢复执行。

这并非一项新技术

这项技术并不是全新的,也不是我自己发明的。它仅仅是一个展示概念及其实际应用的实现,旨在让我们攻防安全社区赶上商业 C2 框架所提供的功能。

实际上,几年前我通过 Josh Lospinoso 在他出色的 Gargoyle 中的工作,第一次接触到了翻转 shellcode 内存保护这一想法。

更多背景资料:

  • gargoyle——一种内存扫描规避技术
  • 使用 Cobalt Strike 和 Gargoyle 绕过内存扫描器

Gargoyle 通过利用调用 VirtualProtect 的 ROP 序列,将自感知、自波动 shellcode 的概念又向前推进了一步。 不过,尽管这项技术令人印象深刻,但要将其与 Cobalt Strike 的 Beacon 结合使用同样困难,因为你必须杀死其线程,并在内存中不断重新初始化 Beacon。

这远非完美,但既然我们已经立足于自己的自注入加载器进程,就可以对 shellcode 运行的环境为所欲为,想怎么隐藏它就怎么隐藏它。这项技术(以及之前的 ThreadStackSpoofer)展示了以这种方式运行 shellcode 的优势。

波动到 PAGE_NOACCESS 的实现灵感来自于 ORCA666 在他的 https://github.com/ORCA666/0x41 注入器中展示的工作。 他证明了:

  1. 我们可以初始化一个向量化异常处理程序(VEH),
  2. 将 shellcode 的页面翻转为无访问权限(no-access)
  3. 然后捕获 Access Violation 异常——这些异常会在 shellcode 想要恢复执行时立即发生,此时再解密并将其内存页面翻转回读取+执行(Read+Execute)。

该实现包含了这一想法,可通过 <fluctuate> 中的选项 2 启用。 也请务必查看他的其他项目。


演示

工具 ShellcodeFluctuation 接受三个参数:第一个是 shellcode 的路径,第二个是我们功能的修饰符。``` Usage: ShellcodeFluctuation.exe : -1 - Read shellcode but dont inject it. Run in an infinite loop. 0 - Inject the shellcode but don't hook kernel32!Sleep and don't encrypt anything 1 - Inject shellcode and start fluctuating its memory with standard PAGE_READWRITE. 2 - Inject shellcode and start fluctuating its memory with ORCA666's PAGE_NOACCESS.

root@kitploit:~
### Moneta(看似)误报```
C:\> ShellcodeFluctuation.exe beacon64.bin -1

So firstly we'll see what Moneta64 scanner thinks about process that does nothing dodgy and simply resorts to run an infinite loop:

moneta 误报

As we can see there's some 误报 (at least how I consider it) allegedly detecting Mismatching PEB module / Phantom image. The memory boundaries point at the ShellcodeFluctuate.exe module itself and could indicate that this module however being of MEM_IMAGE type, is not linked in process' PEB - which is unusual and sounds rather odd. The reason for this IOC is not known to me and I didn't attempt to understand it better, yet it isn't something we should be concerned about really.

If anyone knows what's the reason for this detection, I'd be very curious to hear! Please do reach out.

未加密的 Beacon```

C:> ShellcodeFluctuation.exe beacon64.bin 0

root@kitploit:~
第二个用例展示了在我们进程中运行的 Beacon 的内存 IOC,该 Beacon 不使用任何定制的 `Artifact Kits`、`User-Defined Reflective Loaders`(例如我的 [`ElusiveMice`](https://github.com/mgeeky/ElusiveMice)),也不执行任何会破坏我们结果的初始操作。

![moneta not encrypted](https://assets.kitploit.com/production/public/readmes/50718/6a4e3a230a362c25da6f4aa6955b822d2a83215848c1bc09566a27ea9db653c9/dd2e0560a688886c0fe06debe92f153e129daa6654cb17c2976edee8661e3bd3-display-v1.webp)

我们可以看到 `Moneta64` 正确识别出指向我们 shellcode 所在位置的 `Abnormal private executable memory`。
这是一个非常强的内存 IOC,会暴露我们的 shellcode,使其可能被自动化扫描器转储和分析。这可不太好。

### 使用 RW 保护的加密 Beacon```
C:\> ShellcodeFluctuation.exe beacon64.bin 1

现在来看第三个用例,从本实现的角度来看最有趣,即 波动 Beacon。

moneta encrypted

除了第一个 IOC(在某种程度上被认为是 误报)之外,我们还看到了一个新的 IOC,表明 kernel32.dll 内存被修改了。 然而,这次没有 Abnormal private executable memory IOC。我们的波动(反复加密/解密以及内存保护切换是激活的)。

另外需要说明的是,pe-sieve 在使用 /data 3 选项时也会检测到植入的 PE(除非指定该选项,否则不会进行检测):

pe-sieve

我目前的假设是,PE-Sieve 检测到的特征与 Moneta 相同(在下面的 kernel32.dll 中的修改代码 中描述)——即 PE 映射的模块具有非空的工作集,这显然是某种代码注入的明显事实。 这被标记为 Implanted PE / Implanted。如果是这样,结论与 Moneta 的观察类似。我认为从检测的角度来看,我们不必太在意这个 IOC。

目前,除了挂钩 kernel32!Sleep 之外,我想不到更好的办法来中途拦截 shellcode 的执行(这里说的是 Cobalt Strike)。因此,我们必然会留下这类 IOC。

但是嘿,与文件系统上(C:\Windows\System32\kernel32.dll)的文件相比,仍然没有一个字节不同,也没有任何函数被挂钩,这是怎么回事?😉

具有 PAGE_NOACCESS 保护的加密 Beacon```

C:> ShellcodeFluctuation.exe beacon64.bin 2

root@kitploit:~
![no-access](https://assets.kitploit.com/production/public/readmes/50718/03f80df765f37efb3242dce12c613091ab79896e584fbfc2a309a1295c4544c6/327fe5cc3e3ebe6db8277bd091361f7ae42ee4925e21aa87cfa9287b404a1ab5-display-v1.webp)

这实际上会导致 shellcode 在 `RX` 和 `NA` 页面之间波动。

目前我不确定切换到 `PAGE_NOACCESS` 而不是 `PAGE_READWRITE` 有什么好处。


### kernel32.dll 中被修改的代码

那么,那个被修改的 `kernel32` IOC 又是怎么回事呢?

现在,让我们试着追查这个 IOC 的根源,看看这里到底是怎么回事。

首先,我们会转储所提到的内存区域——即 `kernel32.dll` 的 `.text`(代码)节。为此,我们使用 `ProcessHacker`,以利用公开已知且稳定的工具:

![dump-kernel](https://assets.kitploit.com/production/public/readmes/50718/0b1a4700514b15ec5199ead8f09ccd7c4e5fefc2302a9a9eee0d4e6d77ea48b4/bae9be0ce4bdcede99f235f42431b159bbbf35539f36fe58906fe991e4aacdb4-display-v1.webp)

我们转储据称被修改的 kernel32 的代码节,然后对运行在未修改该区域的进程中的 kernel32 执行相同操作。

获得两个转储后,我们可以逐字节比较它们(使用我的 [expdevBadChars](https://github.com/mgeeky/expdevBadChars))以查找任何不一致之处:

![bindiff](https://assets.kitploit.com/production/public/readmes/50718/4df33e426c7d23554953e2117e3706ee9af1b53b3dd4a60f7b05bcabb988bc85/3db05a8375ded834d576d29083dab9b646cbd83b791a591717fbe0e56ca9a64e-display-v1.webp)

结果发现它们彼此匹配。显然,`kernel32.dll` 中没有任何一个字节被修改,原因在于我们在调用 `kernel32!Sleep` 之前先对其进行了取消挂钩:

`main.cpp:31:````
    HookTrampolineBuffers buffers = { 0 };
    buffers.originalBytes = g_hookedSleep.sleepStub;
    buffers.originalBytesSize = sizeof(g_hookedSleep.sleepStub);

    //
    // Unhook kernel32!Sleep to evade hooked Sleep IOC. 
    // We leverage the fact that the return address left on the stack will make the thread
    // get back to our handler anyway.
    //
    fastTrampoline(false, (BYTE*)::Sleep, &MySleep, &buffers);

    // Perform sleep emulating originally hooked functionality.
    ::Sleep(dwMilliseconds);

So what's causing the IOC being triggered? 让我们更仔细地检查一下 Moneta:

moneta

在 Moneta 的 Ioc.cpp 中,就在报告 MODIFIED_CODE IOC 的第 104 行附近,我们可以稍微修改一下代码,以更好地展示它分析 kernel32 池的确切时刻。 现在:

  1. 进行检查以确保 kernel32 的区域是可执行的。我们看到实际上该区域是可执行的 a = true
  2. 获取该模块的私有内存量。这里我们看到 kernel32 有 b = 0x1000 个私有字节。怎么会这样?应该是 0 个才对。
  3. 如果可执行分配具有超过 0 字节的私有内存(a && b),则报告 IOC
  4. 而这正是我们当时正在检查 kernel32 的证据。

当 Windows 映像加载器将 DLL 模块映射到进程的内存空间时,底层内存页面将根据场景被标记为 MEM_MAPPED 或 MEM_IMAGE。 每当我们修改 MEM_MAPPED/MEM_IMAGE 分配中的哪怕一个字节,系统就会分离出单个内存页(假设我们修改的字节少于 PAGE_SIZE 且未跨越页边界),以指示无法映射回原始映像的片段。

然后这一观察结果被用作 IOC - 映像在其内存区域内不应有 MEM_PRIVATE 分配,因为那将表明该区域内某些字节曾被修改。即使这些字节在比较时与原始模块的字节匹配,Moneta 也会正确地检测到代码修改。

要全面了解 Moneta、进程注入实现及相关 IOC 的内部工作原理,请阅读 Forrest Orr 撰写的以下顶级文章:

  1. 掩盖恶意内存痕迹 – 第一部分:幻影 DLL Hollowing
  2. 掩盖恶意内存痕迹 – 第二部分:混入误报之中
  3. 掩盖恶意内存痕迹 – 第三部分:绕过防御性扫描器

这是 Forrest 完成的真正杰出的研究和文档,干得漂亮,伙计!

尤其是第二篇文章概述了这种检测的理由,正如我们读到 Forrest 教给我们的:

如果模块已被合法加载并添加到 PEB,shellcode 植入物仍然会被检测到,因为 0x1000 字节(1 页)的内存被私有映射到地址空间中,并由 Moneta 通过查询其工作集获取 - 从而导致如上所示的修改代码 IOC。

总而言之,我们留下了一个 IOC,但我们是否应该为此担心? 即使存在 IOC,也没有可见的被窃取字节,因此没有直接的引用指向我们的 shellcode,也无法将我们的 shellcode 技术与其它技术区分开来。

长话短说 - 我们真的不必担心这个 IOC。:-)

但商业框架不会留下 IOC

有人可能会说,这个实现远非完美,因为它留下了一些东西,仍然存在 IOC,而商业产品表明它们没有类似的痕迹。

当这个论点摆上台面时,我需要提醒一下,商业框架完全控制其植入物和 shellcode 加载器的源代码,因此可以将它们很好地相互集成,从而避免自行挂钩和修改自己 shellcode 的必要。在这里,我们需要挂钩 kernel32!Sleep,以便在 Cobalt Strike 的 Beacon 即将进入睡眠之前拦截其执行,从而继续我们的清理工作。如果有一种更好的机制让我们无需挂钩 sleep 就能介入,那就完美了。

然而,Cobalt Strike 引入了 Sleep Mask 的概念,但其大小限制在数百字节,使我们完全无法将这种逻辑引入 mask 本身(否则我们也无需挂钩 Sleep,从而像商业产品一样不留下任何 IOC)。

另一个论点可能是,商业框架将这类逻辑集成到它们的 Reflective Loaders 中,而我们这里却将其留在 EXE 承载程序中。 确实如此,但做出这一决定的原因有两点:

  1. 我需要非常谨慎地发布这类技术,以避免帮助现实世界中的犯罪分子将其武器化,从而带来另一个 Petya 反过来困扰我们的风险。因此,我决定省略一些我在专业工具中使用的血腥细节,这些工具用于交付商业、合同制的对抗模拟演练。希望发布这颗种子能遇到社区中的专业人士,他们能够在自己的工具中发展这一概念,前提是他们具备适当的技能。

  2. 我更愿意将整个逻辑转移到 Cobalt Strike 的 User-Defined Reflective Loader 中,以提升红队团队在投递阶段的成功率。但首先,见第 (1) 点;其次,该技术目前对其 RDLL 的大小限制为 5KB,因此我也完全无法在其中实现。对于我们这些为内部对抗模拟演练构建自定义 C2 和植入物的人来说,现在收到了一份示例实现,它必将帮助我们相应地改进自己的工具。


如何使用?

查看代码及其实现,理解这一概念,并在你自己的、用于投递红队任务的 Shellcode 加载器中重新实现它。 这是又一种高级内存规避技术,可提高你的团队不被杀毒软件、EDR 和检查你植入物的恶意软件分析师发现的机会。

在开发你的高级 shellcode 加载器时,你可能还想实现:

  • 进程堆加密 - 从这篇博客文章中获得灵感:Hook Heaps and Live Free - 它可以让你规避诸如 BeaconEye 之类的 Beacon 配置提取器
  • 在睡眠之前 伪造你的线程调用栈(这可以规避试图检查进程线程及其调用栈以寻找这些线程引用的 MEM_PRIVATE 内存分配的扫描器)
  • 清除 Reflective Loader 留下的任何残留物,以避免内存中基于签名的检测
  • 在睡眠前取消挂钩你可能已挂钩的所有内容(如 AMSI、ETW、WLDP),然后再重新挂钩。

示例运行

使用场景:``` Usage: ShellcodeFluctuation.exe : -1 - Read shellcode but dont inject it. Run in an infinite loop. 0 - Inject the shellcode but don't hook kernel32!Sleep and don't encrypt anything 1 - Inject shellcode and start fluctuating its memory with standard PAGE_READWRITE. 2 - Inject shellcode and start fluctuating its memory with ORCA666's PAGE_NOACCESS.

root@kitploit:~
其中:
- `<shellcode>` 是 shellcode 文件的路径
- `<fluctuate>` 如上所述,取 `-1`、`0` 或 `1`


欺骗 beacon 线程调用栈的示例运行:```
C:\> ShellcodeFluctuation.exe ..\..\tests\beacon64.bin 1

[.] Reading shellcode bytes...
[.] Hooking kernel32!Sleep...
[.] Injecting shellcode...
[+] Shellcode is now running. PID = 9456
[+] Fluctuation initialized.
    Shellcode resides at 0x000002210C091000 and occupies 176128 bytes. XOR32 key: 0x1e602f0d
[>] Flipped to RW. Encoding...

===> MySleep(5000)

[.] Decoding...
[>] Flipped to RX.
[>] Flipped to RW. Encoding...

===> MySleep(5000)

注意事项

如果你打算将此功能添加到自己的 shellcode 加载器 / 工具中,请务必 避免 取消挂钩 kernel32.dll。 尝试取消挂钩 kernel32 将恢复原始 Sleep 功能,从而阻止我们的回调被调用。 如果回调没有被调用,线程将无法自行伪造其调用栈。

如果你确实需要这样做,那么你可能需要运行另一个看门狗线程,确保 Beacon 线程在休眠时能够被伪造调用栈。

如果你使用的是 Cobalt Strike 和 Raphael Mudge 编写的 BOF unhook-bof,请务必查看我的 Pull Request,它为 BOF 添加了一个可选参数,用于指定不应取消挂钩的库。

这样你就可以在 kernel32 中维护自己的挂钩:``` beacon> unhook kernel32 [*] Running unhook. Will skip these modules: wmp.dll, kernel32.dll [+] host called home, sent: 9475 bytes [+] received output: ntdll.dll <.text> Unhook is done.

root@kitploit:~
[已修改的 `unhook-bof`,增加忽略指定模块的选项](https://github.com/mgeeky/unhook-bof)

---

## 最后说明

此 PoC 旨在与 Cobalt Strike 的 Beacon shellcode 配合使用。众所周知,Beacon 会调用 `kernel32!Sleep` 以等待其 C2 的进一步指令。该加载器正是利用这一点,通过挂钩 `Sleep` 来执行其自身的内务处理。

这种实现可能不适用于市面上其他不使用 `Sleep` 进行冷却等待的 shellcode(例如 _Meterpreter_)。由于这仅仅是一个展示该技术的 _概念验证_,我不打算增加对其他任何 C2 框架的支持。

当你理解了这一概念,想必你就能将其转化为你自己的 shellcode 需求,并针对你的优势调整该方案。

请不要打开与“此代码不适用于 XYZ shellcode”相关的 Github issues,这类问题将被立即关闭。

---

### ☕ 表示支持 ☕

这个项目以及其他项目都是无数个不眠之夜和**大量辛勤工作**的成果。如果你喜欢我所做的事情,并感激我始终回馈社区,[请考虑请我喝杯咖啡](https://github.com/sponsors/mgeeky) _(或者更好,来杯啤酒)_ 以表达感谢!💪

---

## 作者```   
   Mariusz Banach / mgeeky, 21
   <mb [at] binary-offensive.com>
   (https://github.com/mgeeky)
下载工具