直接系统调用 VS 间接系统调用
字数 3650 2025-10-02 20:35:35

直接系统调用与间接系统调用技术详解

1. 核心概念与背景

在Windows系统中,用户模式(User Mode)的应用程序通过系统调用(System Call)接口来请求内核模式(Kernel Mode)的服务,例如分配内存、创建线程等。这些调用通常通过ntdll.dll中的函数(如NtAllocateVirtualMemory)实现,这些函数内部包含特定的syscall指令用于切换到内核。

为了监控和防御恶意行为,终端检测与响应(EDR)等安全产品广泛采用了用户模式API Hook技术,特别是在ntdll.dll中。其常见实现方式是内联Hook(Inline Hook),即在API函数的起始处插入一条jmp指令,将执行流重定向到EDR的监控模块(如hooking.dll)。EDR在此处对参数等进行审查:若判断为合法,则跳回ntdll原函数继续执行syscall进入内核;若判断为恶意,则终止进程。

为了绕过这种Hook,攻击者发展出了直接系统调用(Direct Syscall)间接系统调用(Indirect Syscall) 技术。

2. 直接系统调用 (Direct Syscall)

2.1 基本原理

直接系统调用的核心思想是:不调用ntdll.dll中被Hook的函数,而是由攻击者在自己的程序中重新实现系统调用存根(Syscall Stub),即自己编写包含syscall指令的汇编代码。这样,执行syscall指令的代码位于程序自身的内存空间,而非ntdll.dll的空间,从而避开了EDR的Hook。

2.2 关键技术细节

  1. 获取系统调用号(SSN, System Call Number)

    • 每个Nt*函数在ntdll.dll中都有一个对应的系统调用号,存储在EAX寄存器中用于执行syscall
    • 虽然函数代码可能被Hook,但其系统调用号通常仍存储在函数体内的特定偏移处(例如,在函数开始后的第4或第5个字节)。
    • 通过GetProcAddress获取ntdll.dll中目标函数的地址,然后从该地址的固定偏移处读取SSN。
    • 示例代码:
      HANDLE hNtdll = GetModuleHandleA("ntdll.dll");
      UINT_PTR pNtAllocateVirtualMemory = (UINT_PTR)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
      DWORD wNtAllocateVirtualMemory = ((unsigned char*)(pNtAllocateVirtualMemory + 4))[0]; // 从特定偏移读取SSN
      
  2. 实现汇编存根(Assembly Stub)

    • 为每个需要使用的Nt*函数编写一个汇编过程。
    • 存根代码主要完成两件事:
      • 将参数从RCX寄存器移动到R10寄存器(因为syscall指令使用R10而非RCX)。
      • 将之前获取的SSN移动到EAX寄存器。
      • 执行syscall指令。
      • 执行ret返回。
    • 示例汇编代码(MASM语法):
      .CODE
      EXTERN wNtAllocateVirtualMemory:DWORD ; 声明外部变量,存储SSN
      
      NtAllocateVirtualMemory PROC
          mov r10, rcx    ; 参数从rcx移至r10
          mov eax, wNtAllocateVirtualMemory ; SSN移至eax
          syscall         ; 执行系统调用
          ret             ; 返回
      NtAllocateVirtualMemory ENDP
      END
      
  3. 动态获取SSN的进阶方法

    • 上述简单读取SSN的方式可能不稳定或易被检测。
    • 更高级的技术包括:
      • Hell's Gate: 解析ntdll.dll的PE结构,从导出表中查找函数并提取SSN,避免使用可能被Hook的GetProcAddress
      • Halo's Gate: Hell's Gate的进化版,解决了系统调用号排序可能随机化的问题。
      • FreshyCalls / Syswhispers2/3: 类似原理的自动化工具,用于生成直接系统调用的头文件和汇编代码。

2.3 直接系统调用的局限性(IoC)

虽然绕过了基于Hook的检测,但直接系统调用引入了新的可疑指标(IoC),EDR可以据此进行检测:

  • syscall指令的来源syscall指令在程序自身的内存空间中执行,而不是在ntdll.dll的预期内存区域中。
  • 调用栈异常:执行流从程序空间跳转到内核,返回地址也在程序空间,这与正常模式(ntdll.dll -> 内核 -> ntdll.dll)不同。

3. 间接系统调用 (Indirect Syscall)

3.1 基本原理

间接系统调用是直接系统调用的进化,旨在消除上述IoC。其核心思想是:只在自己程序中实现部分存根,而关键的syscall指令和ret返回指令仍然在ntdll.dll的原始内存地址中执行

3.2 关键技术细节

  1. 获取SSN和syscall指令地址

    • 与直接系统调用相同,需要先从ntdll.dll中读取目标函数的SSN。
    • 关键新增步骤:定位ntdll.dll中目标函数内部的syscall指令的确切地址。
      • 通常,syscall指令在函数体内有一个相对固定的偏移(例如,示例中为+0x12)。
      • 示例代码:
        UINT_PTR pNtAllocateVirtualMemory = (UINT_PTR)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
        DWORD wNtAllocateVirtualMemory = ((unsigned char*)(pNtAllocateVirtualMemory + 4))[0]; // 获取SSN
        UINT_PTR syscallAddrInNtdll = pNtAllocateVirtualMemory + 0x12; // 计算ntdll中syscall指令的地址
        
  2. 修改汇编存根

    • 在自己的汇编存根中,不再使用syscall指令,而是使用无条件跳转指令(jmp 跳转到ntdll.dll中刚才找到的syscall指令地址。
    • 这样,syscall指令本身是在ntdll.dll的合法内存空间中执行的。
    • 示例汇编代码修改:
      .CODE
      EXTERN wNtAllocateVirtualMemory:DWORD
      EXTERN syscallAddrNtAllocateVirtualMemory:QWORD ; 声明外部变量,存储ntdll中的syscall地址
      
      NtAllocateVirtualMemory PROC
          mov r10, rcx
          mov eax, wNtAllocateVirtualMemory
          jmp syscallAddrNtAllocateVirtualMemory ; 跳转到ntdll中的syscall指令,而非自己调用
      NtAllocateVirtualMemory ENDP
      END
      
    • 注意:此存根中没有ret指令。执行流程是:jmpntdll -> 执行syscall进入内核 -> 内核返回至ntdll -> ntdll中的ret指令将返回地址弹出,返回到最初调用我们自定义NtAllocateVirtualMemory函数的程序地址。这使得返回地址也在程序空间,但仍是一个潜在IoC。

3.3 优势与剩余的IoC

  • 优势
    • 消除了直接系统调用最明显的IoCsyscall指令不在ntdll.dll中。现在syscall指令在ntdll.dll的合法内存区域执行,对EDR来说看起来更“正常”。
    • ret指令也在ntdll.dll中执行。
  • 剩余的/新的IoC
    • 调用栈/返回地址异常:虽然syscallretntdll中,但ntdll中的ret指令返回到的地址是程序自身的内存空间,而不是类似kernel32.dlluser32.dll等更高级API的地址。高级的EDR通过分析调用栈(Call Stack)可以发现这种不寻常的返回路径(从ntdll直接返回到一个未知的程序内存区域)。
    • 跳转行为:从程序空间jmpntdll.dll中间某条指令的行为也可能被视为异常。

3.4 间接系统调用的局限性与应对

  • ETW(Event Tracing for Windows):如果EDR启用了ETW等深度监控技术,可以捕获整个调用链的详细信息,包括返回地址。单纯的间接系统调用可能不足以绕过这类检测。
  • 调用栈欺骗(Call Stack Spoofing):为了应对调用栈分析,需要配合调用栈欺骗技术。即在发起系统调用前,手动伪造堆栈上的返回地址,使其指向一个合法的系统模块(如kernelbase.dll),让调用栈看起来更像是一次合法的API调用链。
  • SSN随机化:系统调用号可能随Windows版本更新而变化,因此需要 robust 的方法(如Halo's Gate)来动态可靠地获取正确的SSN。

4. 总结对比

特性 标准API调用 (被Hook) 直接系统调用 (Direct Syscall) 间接系统调用 (Indirect Syscall)
syscall指令位置 ntdll.dll 程序自身内存空间 ntdll.dll
ret指令位置 ntdll.dll 程序自身内存空间 ntdll.dll
返回地址路径 ntdll -> 合法调用者 (e.g., kernel32) 程序 -> 程序 ntdll -> 程序
主要绕过目标 - 绕过ntdll的Hook 绕过Hook并隐藏syscall来源
主要IoC - syscall来源、返回路径 返回路径(需配合调用栈欺骗)
技术复杂度 - 中-高

5. 实践建议与思考

  1. 动态获取是关键:静态硬编码SSN和syscall地址会导致兼容性极差,必须使用动态解析ntdll.dllPE结构的方法(Hell's/Halo's Gate)。
  2. 组合技术:没有银弹。间接系统调用常需与调用栈欺骗、直接内存操作(避免使用可疑API)、ETW绕过等技术结合使用,才能形成有效的规避链。
  3. 检测视角:从防御者角度看,监控syscall指令的执行上下文(是否在ntdll镜像内)和分析调用栈的合理性(从内核返回后是否跳转到了非预期的模块地址)是检测此类规避手法的有效手段。
  4. 持续演变:这是一个猫鼠游戏。微软和EDR厂商也在不断引入新的缓解机制(如内核模式调用保护),而攻击技术也随之持续进化。

直接系统调用与间接系统调用技术详解 1. 核心概念与背景 在Windows系统中,用户模式(User Mode)的应用程序通过系统调用(System Call)接口来请求内核模式(Kernel Mode)的服务,例如分配内存、创建线程等。这些调用通常通过 ntdll.dll 中的函数(如 NtAllocateVirtualMemory )实现,这些函数内部包含特定的 syscall 指令用于切换到内核。 为了监控和防御恶意行为,终端检测与响应(EDR)等安全产品广泛采用了 用户模式API Hook 技术,特别是在 ntdll.dll 中。其常见实现方式是 内联Hook(Inline Hook) ,即在API函数的起始处插入一条 jmp 指令,将执行流重定向到EDR的监控模块(如 hooking.dll )。EDR在此处对参数等进行审查:若判断为合法,则跳回 ntdll 原函数继续执行 syscall 进入内核;若判断为恶意,则终止进程。 为了绕过这种Hook,攻击者发展出了 直接系统调用(Direct Syscall) 和 间接系统调用(Indirect Syscall) 技术。 2. 直接系统调用 (Direct Syscall) 2.1 基本原理 直接系统调用的核心思想是: 不调用 ntdll.dll 中被Hook的函数 ,而是由攻击者在自己的程序中重新实现系统调用存根(Syscall Stub),即自己编写包含 syscall 指令的汇编代码。这样,执行 syscall 指令的代码位于程序自身的内存空间,而非 ntdll.dll 的空间,从而避开了EDR的Hook。 2.2 关键技术细节 获取系统调用号(SSN, System Call Number) : 每个 Nt* 函数在 ntdll.dll 中都有一个对应的系统调用号,存储在EAX寄存器中用于执行 syscall 。 虽然函数代码可能被Hook,但其系统调用号通常仍存储在函数体内的特定偏移处(例如,在函数开始后的第4或第5个字节)。 通过 GetProcAddress 获取 ntdll.dll 中目标函数的地址,然后从该地址的固定偏移处读取SSN。 示例代码: 实现汇编存根(Assembly Stub) : 为每个需要使用的 Nt* 函数编写一个汇编过程。 存根代码主要完成两件事: 将参数从RCX寄存器移动到R10寄存器(因为 syscall 指令使用R10而非RCX)。 将之前获取的SSN移动到EAX寄存器。 执行 syscall 指令。 执行 ret 返回。 示例汇编代码(MASM语法): 动态获取SSN的进阶方法 : 上述简单读取SSN的方式可能不稳定或易被检测。 更高级的技术包括: Hell's Gate : 解析 ntdll.dll 的PE结构,从导出表中查找函数并提取SSN,避免使用可能被Hook的 GetProcAddress 。 Halo's Gate : Hell's Gate的进化版,解决了系统调用号排序可能随机化的问题。 FreshyCalls / Syswhispers2/3 : 类似原理的自动化工具,用于生成直接系统调用的头文件和汇编代码。 2.3 直接系统调用的局限性(IoC) 虽然绕过了基于Hook的检测,但直接系统调用引入了新的可疑指标(IoC),EDR可以据此进行检测: syscall 指令的来源 : syscall 指令在程序自身的内存空间中执行,而不是在 ntdll.dll 的预期内存区域中。 调用栈异常 :执行流从程序空间跳转到内核,返回地址也在程序空间,这与正常模式( ntdll.dll -> 内核 -> ntdll.dll )不同。 3. 间接系统调用 (Indirect Syscall) 3.1 基本原理 间接系统调用是直接系统调用的进化,旨在消除上述IoC。其核心思想是: 只在自己程序中实现部分存根,而关键的 syscall 指令和 ret 返回指令仍然在 ntdll.dll 的原始内存地址中执行 。 3.2 关键技术细节 获取SSN和 syscall 指令地址 : 与直接系统调用相同,需要先从 ntdll.dll 中读取目标函数的SSN。 关键新增步骤 :定位 ntdll.dll 中目标函数内部的 syscall 指令的确切地址。 通常, syscall 指令在函数体内有一个相对固定的偏移(例如,示例中为 +0x12 )。 示例代码: 修改汇编存根 : 在自己的汇编存根中, 不再使用 syscall 指令 ,而是使用 无条件跳转指令( jmp ) 跳转到 ntdll.dll 中刚才找到的 syscall 指令地址。 这样, syscall 指令本身是在 ntdll.dll 的合法内存空间中执行的。 示例汇编代码修改: 注意:此存根中没有 ret 指令。执行流程是: jmp 到 ntdll -> 执行 syscall 进入内核 -> 内核返回至 ntdll -> ntdll 中的 ret 指令将返回地址弹出, 返回到最初调用我们自定义 NtAllocateVirtualMemory 函数的程序地址 。这使得返回地址也在程序空间,但仍是一个潜在IoC。 3.3 优势与剩余的IoC 优势 : 消除了 直接系统调用最明显的IoC : syscall 指令不在 ntdll.dll 中。现在 syscall 指令在 ntdll.dll 的合法内存区域执行,对EDR来说看起来更“正常”。 ret 指令也在 ntdll.dll 中执行。 剩余的/新的IoC : 调用栈/返回地址异常 :虽然 syscall 和 ret 在 ntdll 中,但 ntdll 中的 ret 指令返回到的地址是程序自身的内存空间,而不是类似 kernel32.dll 或 user32.dll 等更高级API的地址。高级的EDR通过分析调用栈(Call Stack)可以发现这种不寻常的返回路径(从 ntdll 直接返回到一个未知的程序内存区域)。 跳转行为 :从程序空间 jmp 到 ntdll.dll 中间某条指令的行为也可能被视为异常。 3.4 间接系统调用的局限性与应对 ETW(Event Tracing for Windows) :如果EDR启用了ETW等深度监控技术,可以捕获整个调用链的详细信息,包括返回地址。单纯的间接系统调用可能不足以绕过这类检测。 调用栈欺骗(Call Stack Spoofing) :为了应对调用栈分析,需要配合 调用栈欺骗 技术。即在发起系统调用前,手动伪造堆栈上的返回地址,使其指向一个合法的系统模块(如 kernelbase.dll ),让调用栈看起来更像是一次合法的API调用链。 SSN随机化 :系统调用号可能随Windows版本更新而变化,因此需要 robust 的方法(如Halo's Gate)来动态可靠地获取正确的SSN。 4. 总结对比 | 特性 | 标准API调用 (被Hook) | 直接系统调用 (Direct Syscall) | 间接系统调用 (Indirect Syscall) | | :--- | :--- | :--- | :--- | | syscall 指令位置 | ntdll.dll | 程序自身内存空间 | ntdll.dll | | ret 指令位置 | ntdll.dll | 程序自身内存空间 | ntdll.dll | | 返回地址路径 | ntdll -> 合法调用者 (e.g., kernel32 ) | 程序 -> 程序 | ntdll -> 程序 | | 主要绕过目标 | - | 绕过 ntdll 的Hook | 绕过Hook并隐藏 syscall 来源 | | 主要IoC | - | syscall 来源、返回路径 | 返回路径(需配合调用栈欺骗) | | 技术复杂度 | - | 中 | 中-高 | 5. 实践建议与思考 动态获取是关键 :静态硬编码SSN和 syscall 地址会导致兼容性极差,必须使用动态解析 ntdll.dll PE结构的方法(Hell's/Halo's Gate)。 组合技术 :没有银弹。间接系统调用常需与调用栈欺骗、直接内存操作(避免使用可疑API)、ETW绕过等技术结合使用,才能形成有效的规避链。 检测视角 :从防御者角度看,监控 syscall 指令的执行上下文(是否在 ntdll 镜像内)和分析调用栈的合理性(从内核返回后是否跳转到了非预期的模块地址)是检测此类规避手法的有效手段。 持续演变 :这是一个猫鼠游戏。微软和EDR厂商也在不断引入新的缓解机制(如内核模式调用保护),而攻击技术也随之持续进化。