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

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

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

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

工具目录

分类

查看所有分类
Loading categories
工具/GitHubGitHub/mgeeky/threadstackspoofer
渗透测试框架漏洞利用框架内存取证Shellcode恶意软件分析红队Payload 开发
GitHubmgeeky/threadstackspoofer

ThreadStackSpoofer

Thread Stack Spoofing - 一种高级内存中规避技术的概念验证,能够更好地隐藏注入的shellcode的内存分配,以躲避扫描器和分析师。

查看仓库
1.2k19244年前Kitploit 审核通过

最受欢迎

查看全部 →

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

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

线程栈欺骗 / 调用栈欺骗概念验证

一种高级内存驻留规避技术的概念验证实现,用于欺骗线程调用栈。该技术可绕过基于线程的内存检查规则,在进程内存中更好地隐藏 shellcode。

简介

本示例实现了线程栈欺骗技术,旨在规避恶意软件分析师、AV 和 EDR 通过检查线程调用栈中 shellcode 帧的引用。其核心思路是隐藏线程调用栈中指向 shellcode 的引用,从而伪装包含恶意代码的内存分配。

本实现与我的 ShellcodeFluctuation 一起,为红队工具提供了与商业 C2 产品相媲美的示例,让我们在红队工具中不落下风。💪

实现已变更

当前实现与最初发布的版本有较大差异。原因是我发现有一种更简单的方法可以终止线程调用栈的处理过程,并隐藏 shellcode 相关的帧——只需将我们控制的第一个帧的返回地址写入 0 即可:

root@kitploit:~
void WINAPI MySleep(DWORD _dwMilliseconds)
{
    [...]
    auto overwrite = (PULONG_PTR)_AddressOfReturnAddress();
    const auto origReturnAddress = *overwrite;
    *overwrite = 0;

    [...]
    *overwrite = origReturnAddress;
}

之前使用 StackWalk64 的实现可在此 commit c250724 中查看。

当前实现更加稳定,在 Debug 和 Release 模式下,以及 和 两种架构上均能正常工作。

x64
x86

演示

这是未欺骗时的调用栈示例:

未欺骗

这是启用线程栈欺骗后的调用栈:

已欺骗

如上所示,调用栈中的最后一帧是我们的 MySleep 回调函数。人们可能会想,这是否立即引入了新的危害指标(IOC)?检测规则可能会寻找线程调用栈没有展开到系统库中预期的线程入口点的情况:

root@kitploit:~
kernel32!BaseThreadInitThunk+0x14
ntdll!RtlUserThreadStart+0x21

然而,被欺骗线程的调用栈乍看之下可能有些奇怪,但我在系统上简要检查后发现,其他线程的调用栈也未展开到上述入口点:

合法调用栈

上图为未修改的 Total Commander x64 的一个线程。如图所示,其初始调用栈帧与我们的非常相似。

当存在可以简单模仿的行为进程时,我们何必还要费心伪造调用栈呢?

工作原理

大致算法如下:

  1. 从文件读取 shellcode 内容。
  2. 从 dbghelp.dll 获取所有必要的函数指针,调用 SymInitialize。
  3. 钩住 kernel32!Sleep,指向我们自己的回调函数。
  4. 通过 VirtualAlloc + memcpy + CreateThread 注入并启动 shellcode。线程应从 runShellcode 函数开始,以避免线程的 StartAddress 指向异常位置(如 ntdll!RtlUserThreadStart+0x21)。
  5. 一旦 Beacon 尝试休眠,我们的 MySleep 回调函数被调用。
  6. 我们将栈上最后一个返回地址覆盖为 0,从而终止调用栈。
  7. 最后调用 ::SleepEx,让 Beacon 在等待后续通信时休眠。
  8. 休眠结束后,恢复之前保存的原始函数返回地址,继续执行。

函数返回地址分布在线程栈内存区域中,由 RBP/EBP 寄存器指向。要在栈上找到它们,首先需要收集帧指针,然后对其解引用以进行覆盖:

栈帧

(上图摘自 Eli Bendersky 的文章 x86-64 上的栈帧布局)

root@kitploit:~
	*(PULONG_PTR)(frameAddr + sizeof(void*)) = Fake_Return_Address;

ThreadStackSpoofer 的初始实现在 walkCallStack 和 spoofCallStack 函数中完成了这些操作,但当前实现表明,这些努力并非维护隐匿调用栈所必需。

示例运行

用法:

root@kitploit:~
C:\> ThreadStackSpoofer.exe <shellcode> <spoof>

其中:

  • <shellcode> 是 shellcode 文件的路径
  • <spoof> 当为 1 或 true 时启用线程栈欺骗,其他值禁用。

启用 Beacon 线程调用栈欺骗的示例运行:

root@kitploit:~
PS D:\dev2\ThreadStackSpoofer> .\x64\Release\ThreadStackSpoofer.exe .\tests\beacon64.bin 1
[.] 读取 shellcode 字节...
[.] 钩住 kernel32!Sleep...
[.] 注入 shellcode...
[+] Shellcode 正在运行。
[>] 原始返回地址: 0x1926747bd51。终止调用栈...

===> MySleep(5000)

[<] 恢复原始返回地址...
[>] 原始返回地址: 0x1926747bd51。终止调用栈...

===> MySleep(5000)

[<] 恢复原始返回地址...
[>] 原始返回地址: 0x1926747bd51。终止调用栈...

如何使用?

查看代码及其实现,理解概念后,将这个概念重新实现到你用于红队任务的 Shellcode 加载器中。这是高级内存驻留规避的又一种技术,可提高团队不被反病毒软件、EDR 和恶意软件分析师发现的机会。

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

  • 进程堆加密 - 参考这篇博文:Hook Heaps and Live Free —— 它可以让你规避 Beacon 配置提取器,如 BeaconEye
  • 将 Beacon 的内存页保护更改为 RW(从 RX/RWX)并加密其内容 - 使用 Shellcode Fluctuation 技术 —— 在休眠前完成(可规避诸如 Moneta 或 pe-sieve 等扫描器)
  • 清除反射加载器残留,避免基于内存签名的检测
  • 休眠前取消所有已钩住的函数(如 AMSI、ETW、WLDP),之后再重新钩住。

实际上这还不是真正的栈欺骗

正如有人指出的,这里的技术并未真正名副其实地成为栈欺骗器。因为我们仅仅覆盖了线程栈上的返回地址,并没有欺骗栈本身的其他区域。此外,我们使调用栈变得不可展开,这看起来很奇怪,因为系统无法正确遍历整个调用栈帧链。

不过,我意识到这些缺点,目前保持现状,因为我主要关心的是规避自动扫描器——它们可能遍历进程、枚举其线程、遍历线程栈并捕捉任何指向非镜像内存(如 SEC_PRIVATE——由 VirtualAlloc 等动态分配的内存)的返回地址。专注的恶意软件分析师会立即发现异常并认为该线程可疑,进而追踪我们的植入物。我对此非常确定。然而,我不认为目前诸如 AV/EDR 等自动扫描器会实现能够实际遍历每个线程栈以验证其是否可展开的启发式逻辑 ¯\_(ツ)_/¯ 。

当然,这个项目(以及 C2 框架中的商业实现)给了 AV 和 EDR 供应商考虑实现覆盖这种新颖规避技术的适当启发式逻辑的理由。

要改进此技术,可以尝试实现真正的线程栈欺骗器,通过插入精心伪造的栈帧(通过逆向展开过程构建)来达成。阅读以下更多想法。

实现真正的线程栈欺骗器

与 namazso 数小时的讨论让我明白,要实现一个合适的线程栈欺骗器,我们需要逆向 x64 调用栈展开过程。 首先,需要仔细理解下方链接(a)中解释的栈展开过程。系统在遍历 x64 架构上的线程调用栈时,不会仅仅依赖线程栈上散布的返回地址,而是:

  1. 取返回地址
  2. 尝试使用 RtlLookupFunctionEntry 识别包含该地址的函数
  3. 该函数返回 RUNTIME_FUNCTION、UNWIND_INFO 和 UNWIND_CODE 结构。这些结构描述了函数的起始地址、结束地址,以及所有修改 RBP 或 RSP 的代码序列。
  4. 系统需要了解调用栈中每个函数发生的所有栈和帧指针修改,以便虚拟地回滚这些修改,并在调用被处理的调用栈帧时虚拟恢复栈指针(这在 RtlVirtualUnwind 中实现)。
  5. 系统处理所检查函数的所有 UNWIND_CODE,以精确计算该帧的返回地址和栈指针值的位置。
  6. 通过这种模拟,系统能够向下遍历调用栈链并有效地“展开”调用栈。

为了干扰这个过程,我们需要逆转它——即拥有我们自己的 RtlVirtualUnwind 逆向形式。我们需要遍历模块(比如 kernel32)中定义的函数,扫描每个函数的 UNWIND_CODE 代码,并仔细地逆向模拟它们(与 RtlVirtualUnwind 和具体来说 RtlpUnwindPrologue 相反),以找到栈上应该放置我们虚假返回地址的位置。

namazso 提到,需要引入 3 个虚假栈帧来很好地缝合调用栈:

  1. 一个“去同步”帧(可视为一个小工具帧),其展开方式与我们的 MySleep 调用者不同(具有不同的 UWOP - 展开操作代码)。我们通过查看模块中的所有函数,检查它们的 UWOP,计算虚假帧应该有多大。该帧的 UWOP 必须不同于我们的 MySleep 调用者。
  2. 接下来我们要找的帧是一个通过从栈弹出到 RBP 来展开的函数——基本通过 UWOP_PUSH_NONVOL 代码实现。
  3. 第三个帧我们需要一个通过代码 UWOP_SET_FPREG 从 RBP 恢复 RSP 的函数。

恢复后的 RSP 必须设置为从控制流进入 MySleep 时的 RSP 值,这样所有我们的帧都会被隐藏,作为第三个小工具展开的结果。

要开始这个过程,可以通过解引用 IMAGE_DIRECTORY_ENTRY_EXCEPTION 数据目录项来遍历可执行文件的 .pdata。考虑下面的示例:

root@kitploit:~
    ULONG_PTR imageBase = (ULONG_PTR)GetModuleHandleA("kernel32");
    PIMAGE_NT_HEADERS64 pNthdrs = PIMAGE_NT_HEADERS64(imageBase + PIMAGE_DOS_HEADER(imageBase)->e_lfanew);

    auto excdir = pNthdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];
    if (excdir.Size == 0 || excdir.VirtualAddress == 0)
        return;

    auto begin = PRUNTIME_FUNCTION(excdir.VirtualAddress + imageBase);
    auto end = PRUNTIME_FUNCTION(excdir.VirtualAddress + imageBase + excdir.Size);

    UNWIND_HISTORY_TABLE mshist = { 0 };
    DWORD64 imageBase2 = 0;

    PRUNTIME_FUNCTION currFrame = RtlLookupFunctionEntry(
        (DWORD64)caller,
        &imageBase2,
        &mshist
    );

    UNWIND_INFO *mySleep = (UNWIND_INFO*)(currFrame->UnwindData + imageBase);
    UNWIND_CODE myFrameUwop = (UNWIND_CODE)(mySleep->UnwindCodes[0]);

    log("1. MySleep RIP UWOP: ", myFrameUwop.UnwindOpcode);

    for (PRUNTIME_FUNCTION it = begin; it < end; ++it)
    {
        UNWIND_INFO* unwindData = (UNWIND_INFO*)(it->UnwindData + imageBase);
        UNWIND_CODE frameUwop = (UNWIND_CODE)(unwindData->UnwindCodes[0]);

        if (frameUwop.UnwindOpcode != myFrameUwop.UnwindOpcode)
        {
            // 找到用于去同步小工具帧的候选函数

        }
    }

这个过程有点复杂,但归根结底是逆向线程调用栈展开过程,用精心选择的其它帧替换任意栈帧,类似于 ROP 的方法。

本 PoC 未遵循此算法进行复制,因为根据我目前的理解,我接受调用栈终止于基于 EXE 的栈帧,并且我不想使我的 shellcode 加载器或本 PoC 过于复杂。把这个实现并公开分享的练习留给有兴趣的读者。或者,如果我有更多空闲时间,也许会亲自尝试实现。

更多信息:

  • a) x64 异常处理 - 栈展开过程详解
  • b) RtlpUnwindPrologue 和 RtlVirtualUnwind 的示例实现
  • c) .pdata 段
  • d) RtlpUnwindPrologue 的另一个示例实现

注意事项

如果你计划将这一功能添加到自己的 shellcode 加载器/工具中,务必避免取消挂钩 kernel32.dll。尝试解除 kernel32 的钩子会恢复原始的 Sleep 功能,从而阻止我们的回调被调用。如果回调不被调用,线程将无法自行欺骗自己的调用栈。

如果你需要那样做,则可能需要运行另一个监控线程,确保 Beacon 线程在休眠时被欺骗。

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

这样,你可以在 kernel32 中保留钩子:

root@kitploit:~
beacon> unhook kernel32
[*] 运行 unhook。
    将跳过以下模块: wmp.dll, kernel32.dll
[+] 主机回连,发送 9475 字节
[+] 收到输出:
ntdll.dll            <.text>
Unhook 完成。

修改后的 unhook-bof,支持忽略指定模块


最后说明

此 PoC 旨在与 Cobalt Strike 的 Beacon shellcode 配合使用。Beacon 会调用 kernel32!Sleep 以等待来自 C2 的进一步指令。本加载器利用此事实,钩住 Sleep 来执行其内部维护。

如果其他 shellcode(如 Meterpreter)不使用 Sleep 来冷却,则此实现可能不适用。由于这仅是展示技术的概念验证,我不打算添加对其他 C2 框架的支持。

当你理解了概念后,肯定能够将其转化为你的 shellcode 需求,并为你所用。

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


☕ 支持 ☕

这个项目和其他项目都是无数个不眠之夜和大量辛勤工作的成果。如果你喜欢我所做的事情,并欣赏我始终回馈社区的做法, 请考虑请我喝杯咖啡(或者更好,请我喝杯啤酒)来表达感谢!💪


作者

root@kitploit:~
   Mariusz Banach / mgeeky, 21
   <mb [at] binary-offensive.com>
   (https://github.com/mgeeky)
下载工具