anti-debugging-techniques について
Linux と Windows のバイナリにおけるアンチデバッグ機構を分析・回避するセキュリティスキルです。検出手法、回避策、分析ツールをまとめています。
主な機能
- 01Linux の ptrace とプロセス検査
- 02Windows API と PEB の検査
- 03タイミングとシグナルの検出
- 04TLS コールバックと VEH の手法
- 05回避策とツールの参照情報
活用シーン
- 01デバッガー起動時に終了するバイナリの分析
- 02マルウェアのアンチデバッグ検査の調査
- 03CTF のアンチデバッグ課題への対応
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ yaklang/ hack-skills --skill anti-debugging-techniquesインストール後、Claude Code で/anti-debugging-techniquesを入力して読み込まれたか確認します。
同梱ファイルANTI_DEBUG_MATRIX.md
詳細
スキル:アンチデバッグ技術 — 検出と回避のプレイブック
AI LOAD INSTRUCTION:Linux と Windows における高度なアンチデバッグ技術を扱います。ptrace、PEB フラグ、NtQueryInformationProcess、タイミング攻撃、シグナルベースの検出、TLS コールバック、VEH のトリック、およびそれぞれに対応する回避方法を網羅しています。基本モデルは、ユーザーモードとカーネルモードの検出の違いや、それぞれに適したパッチ方法を見落としがちです。
0. 関連するルーティング
- バイナリが制御フローのフラット化、VM 保護、または文字列暗号化も使用している場合は、code-obfuscation-deobfuscation
- アンチデバッグ処理がカスタム VM ディスパッチャー内にある場合は、vm-and-bytecode-reverse
- アンチデバッグチェックをシンボリックに完全にスキップしたい場合は、symbolic-execution-tools
高度なリファレンス
以下が必要な場合は、ANTI_DEBUG_MATRIX.md も読み込んでください:
- 技法 × OS × 検出方法 × 回避方法の完全な相互参照マトリックス
- 技法ごとの信頼性評価と誤検知に関する注意事項
- ツールの互換性一覧(GDB、x64dbg、WinDbg、Frida、ScyllaHide)
すぐに使える回避策
| 検出クラス | 最初の回避策 | 代替策 |
|---|---|---|
| ptrace ベース(Linux) | LD_PRELOAD で ptrace() をフック → 0 を返す | トレーサーを隠すカーネルモジュール |
| PEB.BeingDebugged(Windows) | fs:[0x30]+0x2 の PEB バイトにパッチを適用 | ScyllaHide の自動パッチ |
| タイミングチェック(rdtsc) | rdtsc の後に条件付き BP を設定し、レジスターを修正 | Frida で rdtsc の戻り値をフック |
| IsDebuggerPresent | 呼び出しを NOP にする/戻り値をフック | x64dbg の組み込み隠蔽機能 |
| INT 2D / UD2 例外 | VEH を設定して適切に処理 | TitanHide ドライバー |
1. Linux のアンチデバッグ技術
1.1 ptrace(PTRACE_TRACEME)
古典的な自己アタッチ方法です。プロセスは ptrace(PTRACE_TRACEME, 0, 0, 0) を呼び出します。デバッガーがすでにアタッチされている場合、呼び出しは失敗します(-1 を返します)。
if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1) {
exit(1); // debugger detected
}
回避方法:
| 方法 | 手順 |
|---|---|
LD_PRELOAD シム | 共有ライブラリーをコンパイルします:long ptrace(int r, ...) { return 0; } を定義し、LD_PRELOAD を設定します |
| バイナリパッチ | ptrace の呼び出しを NOP にするか、戻り値のチェックにパッチを適用します |
| GDB catch | catch syscall ptrace → 戻り時に $rax を 0 に変更します |
| カーネルモジュール | sys_ptrace をフックして複数のトレーサーを許可します |
1.2 /proc/self/status — TracerPid
FILE *f = fopen("/proc/self/status", "r");
// parse TracerPid: if non-zero → debugger attached
回避策:/proc/self に FUSE ファイルシステムをマウントするか、fopen/fread を LD_PRELOAD でフックして、TracerPid を 0 にフィルタリングします。
1.3 タイミングチェック(rdtsc / clock_gettime)
2 点間の経過時間を測定します。デバッガーでのシングルステップ実行は、目立った遅延を引き起こします。
rdtsc
mov ebx, eax ; save low 32 bits
; ... protected code ...
rdtsc
sub eax, ebx
cmp eax, 0x1000 ; threshold
ja debugger_detected
回避策:2 回目の rdtsc の後にハードウェアブレークポイントを設定し、比較を通過するように eax を変更します。または、Frida を使ってタイミング関数を置き換えます。
1.4 シグナルベースの検出(SIGTRAP)
volatile int caught = 0;
void handler(int sig) { caught = 1; }
signal(SIGTRAP, handler);
raise(SIGTRAP);
if (!caught) exit(1); // debugger swallowed the signal
デバッガーがアタッチされている場合、SIGTRAP はハンドラーに配信されず、デバッガーによって消費されます。回避策:GDB で handle SIGTRAP nostop pass を使い、シグナルを転送します。
1.5 /proc/self/maps と LD_PRELOAD の検出
デバッガーや計装に特徴的な、挿入されたライブラリーやメモリー領域をチェックします。
FILE *f = fopen("/proc/self/maps", "r");
while (fgets(buf, sizeof(buf), f)) {
if (strstr(buf, "frida") || strstr(buf, "LD_PRELOAD"))
exit(1);
}
回避策:fopen("/proc/self/maps") をフックしてフィルタリング済みの内容を返すか、Frida のエージェントライブラリーの名前を変更します。
1.6 環境変数のチェック
一部の保護機能は、LD_PRELOAD、LINES、COLUMNS(GDB のターミナルが設定)、またはデバッガー固有の環境変数をチェックします。
回避策:起動前に疑わしい環境変数の設定を解除するか、getenv() をフックします。
2. Windows のアンチデバッグ技術
2.1 IsDebuggerPresent / CheckRemoteDebuggerPresent
if (IsDebuggerPresent()) ExitProcess(1);
BOOL debugged = FALSE;
CheckRemoteDebuggerPresent(GetCurrentProcess(), &debugged);
if (debugged) ExitProcess(1);
回避策:kernel32!IsDebuggerPresent をフックして 0 を返すか、PEB に直接パッチを適用します。
2.2 PEB フラグ
| フィールド | オフセット(x64) | デバッグ時の値 | 通常の値 |
|---|---|---|---|
BeingDebugged | PEB+0x02 | 1 | 0 |
NtGlobalFlag | PEB+0xBC | 0x70 (FLG_HEAP_*) | 0 |
ProcessHeap.Flags | Heap+0x40 | 0x40000062 | 0x00000002 |
ProcessHeap.ForceFlags | Heap+0x44 | 0x40000060 | 0 |
mov rax, gs:[0x60] ; PEB
movzx eax, byte [rax+0x02] ; BeingDebugged
test eax, eax
jnz debugger_detected
回避策:4つすべてのフィールドをゼロにします。ScyllaHide が自動的にこれを行います。
2.3 NtQueryInformationProcess
| 情報クラス | 値 | デバッグ時の戻り値 |
|---|---|---|
ProcessDebugPort | 0x07 | 0 以外のポート |
ProcessDebugObjectHandle | 0x1E | 有効なハンドル |
ProcessDebugFlags | 0x1F | 0(反転されています!) |
回避策:ntdll!NtQueryInformationProcess をフックし、情報クラスごとにクリーンな値を返します。
2.4 ハードウェアブレークポイントの検出
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(GetCurrentThread(), &ctx);
if (ctx.Dr0 || ctx.Dr1 || ctx.Dr2 || ctx.Dr3)
ExitProcess(1);
回避策:GetThreadContext をフックして DR0~DR3 をゼロにするか、事前に NtSetInformationThread(ThreadHideFromDebugger) を使用します(皮肉なことに、これはアンチデバッグ手法そのものです)。
2.5 INT 2D / INT 3 / UD2 の例外トリック
INT 2D はカーネルのデバッグサービス割り込みです。デバッガーがない場合は STATUS_BREAKPOINT が発生します。デバッガーがある場合は動作が異なり(バイトがスキップされます)。
xor eax, eax
int 2dh
nop ; debugger may skip this byte
; ... divergent execution path ...
回避策:VEH で処理するか、割り込み命令にパッチを適用します。
2.6 TLS コールバック
TLS コールバックは main() / WinMain() より前に実行されます。ここに配置されたアンチデバッグチェックは、デバッガーの初期ブレークより前に実行されます。
回避策:x64dbg では、「Break on TLS Callbacks」オプションを設定します。WinDbg では、sxe ld を使ってモジュールの読み込み時にブレークします。
2.7 NtSetInformationThread(ThreadHideFromDebugger)
NtSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);
この呼び出しの後、スレッドはデバッガーから見えなくなり、ブレークポイントとステップ実行が何も表示されずに機能しなくなります。
回避策:ThreadInfoClass == 0x11 の場合に NOP を実行するよう、NtSetInformationThread をフックします。
2.8 VEH ベースの検出
デバッガー固有の動作(シングルステップフラグ、デバッガーのセマンティクスを伴うガードページ違反)について EXCEPTION_RECORD をチェックする Vectored Exception Handler を登録します。
回避策:VEH のロジックを理解し、例外チェーンが非デバッグ実行時と同じように動作することを確認します。
3. 高度な多層テクニック
3.1 自己デバッグ(fork + ptrace)
プロセスは子プロセスを fork し、その子プロセスが ptrace を介して親プロセスにアタッチします。外部デバッガーがすでにアタッチされている場合、子プロセスの ptrace は失敗します。
pid_t child = fork();
if (child == 0) {
if (ptrace(PTRACE_ATTACH, getppid(), 0, 0) == -1)
kill(getppid(), SIGKILL);
else
ptrace(PTRACE_DETACH, getppid(), 0, 0);
_exit(0);
}
wait(NULL);
回避策:fork() の戻り値にパッチを適用するか、監視用の子プロセスを終了またはデタッチします。
3.2 マルチプロセスのデバッグ検出
親プロセスと子プロセスが互いのデバッグ状態を協調して確認し、相互監視パターンを形成します。
回避策:両方のプロセスにアタッチします(GDB の follow-fork-mode を使うか、2つのデバッガーインスタンスを使用します)。
3.3 複数のチェックポイントを用いたタイミングベースの検出
複数の関数にタイミングチェックを分散させ、累積したずれを比較します。合計値がしきい値を超えたままになるため、単一のパッチでは失敗します。
回避策:Frida の Interceptor.replace を使い、すべてのタイミングソース(rdtsc、clock_gettime、QueryPerformanceCounter)を、制御された値を返すように置き換えます。
3.4 ナノマイト / INT3 パッチ
元の条件付きジャンプが INT3(0xCC)に置き換えられます。親デバッガープロセスが各 INT3 を処理し、条件を評価して、子プロセスの EIP を適切に設定します。
回避策:すべての INT3 ハンドラーをトレースして元のジャンプテーブルを再構築し、バイナリにパッチを適用します。
4. 対抗ツール
| ツール | プラットフォーム | 機能 |
|---|---|---|
| ScyllaHide | Windows(x64dbg/IDA/OllyDbg) | PEB への自動パッチ、NtQuery* のフック、スレッドの隠蔽、タイミングの修正 |
| TitanHide | Windows(カーネルドライバー) | すべてのユーザーモードチェックをカーネルレベルで隠蔽 |
| Frida | クロスプラットフォーム | スクリプトベースで任意の関数をフックし、タイミングを偽装 |
| LD_PRELOAD shims | Linux | 読み込み時に ptrace、getenv、fopen を置き換え |
| GDB scripts | Linux | catch syscall、条件付き BP、レジスターの修正 |
| Qiling | クロスプラットフォーム | フルシステムエミュレーション、すべてのハードウェアチェックを回避 |
5. 体系的な回避方法
Step 1: Static analysis — identify anti-debug calls
└─ Search for: ptrace, IsDebuggerPresent, NtQuery, rdtsc,
GetTickCount, SIGTRAP, INT 2D, TLS directory entries
Step 2: Classify each check
├─ API-based → hook or patch the call
├─ Flag-based → patch PEB/proc fields
├─ Timing-based → spoof time source
├─ Exception-based → forward/handle exception correctly
└─ Multi-process → handle both processes
Step 3: Apply bypass (order matters)
1. Load ScyllaHide / set LD_PRELOAD (covers 80% of checks)
2. Handle TLS callbacks (break before main)
3. Patch remaining custom checks (Frida or binary patch)
4. Verify: run with breakpoints, confirm no premature exit
Step 4: Validate bypass completeness
└─ Set BP on ExitProcess/exit/_exit — if hit unexpectedly,
a check was missed → trace back from exit call
6. 判断フローチャート
Binary exits/crashes under debugger?
│
├─ Crashes immediately before main?
│ └─ TLS callback anti-debug
│ └─ Enable TLS callback breaking in debugger
│
├─ Crashes at startup?
│ ├─ Linux: check for ptrace(TRACEME)
│ │ └─ LD_PRELOAD hook or NOP patch
│ └─ Windows: check IsDebuggerPresent / PEB
│ └─ ScyllaHide or manual PEB patch
│
├─ Crashes after some execution?
│ ├─ Consistent crash point → API-based check
│ │ ├─ NtQueryInformationProcess → hook return values
│ │ ├─ /proc/self/status → filter TracerPid
│ │ └─ Hardware BP detection → hook GetThreadContext
│ │
│ ├─ Variable crash point → timing-based check
│ │ └─ Hook rdtsc / QueryPerformanceCounter
│ │
│ └─ Crash on breakpoint hit → exception-based check
│ ├─ INT 2D / INT 3 trick → handle in VEH
│ └─ SIGTRAP handler → GDB: handle SIGTRAP pass
│
├─ Debugger loses control silently?
│ └─ ThreadHideFromDebugger
│ └─ Hook NtSetInformationThread
│
├─ Child process detects and kills parent?
│ └─ Self-debugging (fork+ptrace)
│ └─ Patch fork() or handle both processes
│
└─ All basic bypasses applied but still detected?
└─ Multi-layer / custom checks
├─ Use Frida for comprehensive API hooking
├─ Full emulation with Qiling
└─ Trace all calls to exit/abort to find remaining checks
7. CTF と実環境でのパターン
一般的なCTFのアンチデバッグパターン
| パターン | 頻度 | 簡単な回避方法 |
|---|---|---|
単独の ptrace(TRACEME) | 非常によくある | LD_PRELOAD のワンライナー |
IsDebuggerPresent + NtGlobalFlag | よくある | ScyllaHide |
| ループ内でのrdtscによるタイミング計測 | そこそこある | 比較しきい値をパッチ |
| signal(SIGTRAP) + raise | そこそこある | GDBのシグナル転送 |
| fork + ptraceウォッチドッグ | まれだが厄介 | 子プロセスを終了するか、forkをパッチ |
| NanomiteによるINT3置換 | まれ(高度) | ジャンプテーブルを再構築 |
実環境での保護
| プロテクター | 主なアンチデバッグ機能 | 推奨ツール |
|---|---|---|
| VMProtect | PEB + タイミング計測 + ドライバーレベル | TitanHide + ScyllaHide |
| Themida | 多層PEB + SEH + タイミング計測 | ScyllaHide + 手動パッチ |
| Enigma Protector | IsDebuggerPresent + CRCチェック | x64dbg + ScyllaHide |
| UPX(カスタム) | 通常なし(パッキングのみ) | 標準的なアンパック |
| カスタム(マルウェア) | 多種多様 | 解析にはFrida + Qiling |
8. クイックリファレンス — 回避チートシート
Linuxワンライナー
# LD_PRELOAD anti-ptrace
echo 'long ptrace(int r, ...) { return 0; }' > /tmp/ap.c
gcc -shared -o /tmp/ap.so /tmp/ap.c
LD_PRELOAD=/tmp/ap.so ./target
# GDB: catch and bypass ptrace
(gdb) catch syscall ptrace
(gdb) commands
> set $rax = 0
> continue
> end
Fridaによるアンチデバッグ回避(クロスプラットフォーム)
// Hook IsDebuggerPresent (Windows)
Interceptor.replace(
Module.getExportByName('kernel32.dll', 'IsDebuggerPresent'),
new NativeCallback(() => 0, 'int', [])
);
// Hook ptrace (Linux)
Interceptor.replace(
Module.getExportByName(null, 'ptrace'),
new NativeCallback(() => 0, 'long', ['int', 'int', 'pointer', 'pointer'])
);
// Timing spoof
Interceptor.attach(Module.getExportByName(null, 'clock_gettime'), {
onLeave(retval) {
// manipulate timespec to hide debugger delay
}
});
x64dbg ScyllaHideのクイックセットアップ
- Plugins → ScyllaHide → Options
- チェック:PEB BeingDebugged、NtGlobalFlag、HeapFlags
- チェック:NtQueryInformationProcess(すべてのクラス)
- チェック:NtSetInformationThread(HideFromDebugger)
- チェック:GetTickCount、QueryPerformanceCounter
- 適用 → デバッグセッションを再起動