间接系统调用与C2定制

admin 2026-09-02 06:04:44 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文深入解析间接系统调用技术,用于绕过EDR对ntdll的inlinehook,通过定位ntdll内syscallgadget执行系统调用,使返回地址合法化,规避调用栈检测。同时探讨C2定制、SleepMask及完整用户态免杀链,提供实战级规避方案,对红队与安全研究有较高参考价值。 综合评分: 88 文章分类: 免杀,红队,安全开发,安全工具


间接系统调用与C2定制

原创

pandazhengzheng pandazhengzheng

安全分析与研究

2026年8月30日 22:00 广东

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

目录

  1. 问题背景:ntdll 的 inline hook
  2. 系统调用的底层机制
  3. Direct Syscalls(直接系统调用)
  4. Indirect Syscalls(间接系统调用)
  5. SSN 解析技术族
  6. Indirect Syscalls 的实现详解
  7. Indirect Syscalls 的检测与规避
  8. C2 通信的检测面
  9. Havoc 框架架构
  10. C2 定制关键技术
  11. Sleep Mask 与内存规避
  12. 完整用户态免杀链
  13. 小结

1. 问题背景:ntdll 的 inline hook

1.1 hook 的位置

EDR 在 ntdll.dll 的每个关键 Nt*/Zw* 函数序言安装 inline hook,将控制权转到 EDR 的处理例程。典型 hook 形式(x64):

原始 NtXxx 序言:
    mov r10, rcx
&nbsp; &nbsp; mov eax, <SSN>
&nbsp; &nbsp; syscall
&nbsp; &nbsp; ret

被 hook 后:
&nbsp; &nbsp; jmp <edr_handler> &nbsp; &nbsp; &nbsp; &nbsp;; 或 mov rax, addr; jmp rax
&nbsp; &nbsp; <原始字节备份在 trampoline>

1.2 为什么前几节未解决

  • 第二节(APC/Fiber)解决”执行载体”,但 shellcode 内部仍调用 ntdll
  • 第三节(Module Stomping/Call Stack Spoofing)解决”内存与栈特征”,但 shellcode 调用 ntdll 时控制权仍经过被 hook 的序言。

只要 shellcode 调用 ntdll!NtXxx,inline hook 就触发。绕过 hook 的唯一方式是不经过 ntdll 的 stub 发起系统调用——这就是 syscall 类技术的核心。

1.3 系统调用的本质

ntdll!NtXxx 只是系统调用的用户态包装:设置 SSN 到 EAX、设置 r10、执行 syscall 指令进入内核。**syscall 指令本身才是系统调用的入口**,ntdll 的 stub 只是方便调用。若攻击者自己 emit mov r10, rcx; mov eax, SSN; syscall; ret,即可不经过 ntdll 发起系统调用。


2. 系统调用的底层机制

2.1 syscall 指令的语义

x64 syscall 指令完成:

  1. 保存用户态 RIP 到 RCX(RCX = next instruction after syscall);
  2. 保存用户态 RFLAGS 到 R11;
  3. 清除 RFLAGS 中的 IF(关中断);
  4. 加载内核态 CS/SS(MSR.LSTAR → RIP,进入内核 syscall 入口);
  5. 内核根据 EAX(SSN)查 SSDT/SSDTShadow 派发到对应内核例程。

2.2 SSN(System Service Number)

每个系统服务在 SSDT 中有一个序号(SSN)。ntdll!NtXxx 的 stub 中 mov eax, <SSN> 即设置该序号。SSN 因 Windows 版本与补丁而异,不能硬编码。

2.3 ntdll stub 的标准形式

x64 下,几乎所有 NtXxx stub 形式统一:

mov r10, rcx &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; Windows 内核 syscall 约定:参数1通过 r10 传递(因为 syscall 会破坏 rcx)
mov eax, <SSN> &nbsp; &nbsp; &nbsp; &nbsp;; 系统服务号
syscall &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 进入内核
ret &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; 返回(部分函数后有 ret 0/ret 4,但 Nt* 通常是 ret)

少数函数(如 NtCreate* 等需保存更多寄存器)可能有额外 prologue,但 syscall 前必有 mov r10, rcx; mov eax, SSN


3. Direct Syscalls(直接系统调用)

3.1 原理

Direct Syscalls 在 shellcode 内部直接 emit mov r10, rcx; mov eax, SSN; syscall; ret 序列,不经过 ntdll 的 stub,从而不触发其上的 inline hook。

3.2 实现

shellcode 内嵌汇编(或 C 内联汇编):

NTSTATUS&nbsp;MyNtAllocateVirtualMemory(HANDLE ProcessHandle, PVOID *BaseAddress, ...)&nbsp;{
&nbsp; &nbsp;&nbsp;// 设置参数(x64 调用约定:rcx=ProcessHandle, rdx=BaseAddress, ...)
&nbsp; &nbsp;&nbsp;// 然后执行 syscall
&nbsp; &nbsp;&nbsp;__asm__&nbsp;volatile&nbsp;(
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"mov r10, rcx\n"
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"mov eax, %0\n"&nbsp; &nbsp; &nbsp;&nbsp;// SSN
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"syscall\n"
&nbsp; &nbsp; &nbsp; &nbsp; :
&nbsp; &nbsp; &nbsp; &nbsp; :&nbsp;"i"&nbsp;(SSN_NtAllocateVirtualMemory)
&nbsp; &nbsp; &nbsp; &nbsp; :&nbsp;"rax",&nbsp;"r10",&nbsp;"rcx",&nbsp;"r11",&nbsp;"memory"
&nbsp; &nbsp; );
}

或用动态构造的 shellcode stub:

direct_syscall:
&nbsp; &nbsp; mov r10, rcx
&nbsp; &nbsp; mov eax, [ssn] &nbsp; &nbsp; &nbsp;; 动态填入 SSN
&nbsp; &nbsp; syscall
&nbsp; &nbsp; ret

3.3 致命缺陷:调用栈暴露

当 syscall 指令执行进入内核后,**syscall 的返回地址(即 syscall 指令的下一条指令地址)落在 shellcode 所在的私有内存**。EDR 在内核态(ETW-TI、回调)或 hook 处理时回溯调用栈,会发现:

  • syscall 返回地址在 MEM_PRIVATE(shellcode 内部);
  • 合法系统调用的返回地址应在 ntdll 的 stub 内(syscall 指令的下一条是 ret,地址在 ntdll)。

这是极强的异常信号:合法进程的系统调用返回地址几乎必然在 ntdll,而 Direct Syscalls 的返回地址在私有内存,一目了然。

3.4 Direct Syscalls 的适用与局限

  • 绕过用户态 hook:✓ 完全绕过(不经过 stub);
  • 调用栈暴露:✗ 返回地址在私有内存;
  • ETW-TI 仍感知:✗ 内核埋点仍记录系统调用(但 ETW-TI 不直接检查返回地址,主要靠后续栈回溯);
  • 适用场景:仅当 EDR 不做调用栈检查时有效;现代 EDR 普遍检查,故 Direct Syscalls 已不够。

4. Indirect Syscalls(间接系统调用)

4.1 核心思路

Indirect Syscalls 在 Direct Syscalls 基础上修复”返回地址暴露”问题:让 syscall 指令本身从一个合法的、未被 hook 的 ntdll 区域执行,使得 syscall 的返回地址落在 ntdll 内。

4.2 关键观察

ntdll 中每个 NtXxx stub 末尾都是 syscall; ret。即便其序言被 hook 覆写,stub 末尾的 syscall; ret 通常未被覆写(hook 一般只覆写序言前几字节,跳转到 handler,handler 内部调用 trampoline 执行原始序言 + 原始 syscall)。

因此,ntdll 中存在大量 syscall; ret gadget——可以是任意 NtXxx 的末尾,不限于目标函数。

4.3 实现思路

  1. 解析 ntdll:找到目标 NtXxx 的 SSN;
  2. 寻找一个 syscall; ret gadget:在 ntdll 中扫描任意一个未被 hook 覆盖的 syscall 指令;
  3. 设置寄存器r10 = rcxeax = 目标 SSN
  4. 跳转到该 gadgetjmp <ntdll 中的 syscall; ret>

此时:

  • syscall 指令在 ntdll 内执行,返回地址落在 ntdll 内(gadget 的 ret 后一条指令地址);
  • 未经过被 hook 的 stub 入口,inline hook 不触发
  • 系统调用号通过 eax 传入,内核正确派发到目标服务。

4.4 与 Direct Syscalls 的对比

| 维度 | Direct Syscalls | Indirect Syscalls | | — | — | — | | 绕过用户态 hook | ✓ | ✓ | | syscall 返回地址 | 私有内存(暴露) | ntdll 内(合法) | | 调用栈最近一层 | 异常 | 合法 | | 实现复杂度 | 低 | 中 | | 依赖 ntdll 完整性 | 否(自己 emit) | 是(需读 ntdll 找 gadget) |



免责声明:

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

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

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

本文转载自:安全分析与研究 pandazhengzheng pandazhengzheng《间接系统调用与C2定制》

评论:0   参与:  0