ptrace とデバッグインターフェース

このページの目次

ptrace は、トレーサーが権限ルールに従ってトレシー(追跡対象)スレッドを観察・制御することを可能にします。例えば、停止ポイントでレジスタの読み取り、メモリ検査、実行の再開、またはステップ実行などが行えます。Linux のネイティブデバッカーやシステムコールトレーサーはこれらを頻繁に使用しますが、プロセス全体、スレッドグループ、および単一の追跡対象スレッドは同一のオブジェクトではありません。

/proc/PID/mem、process_vm_readv、および seccomp user notification はそれぞれ部分的な問題を解決しますが、それらを互いに置き換え可能な「ptrace の次世代」として直接見なすことはできません。

追跡関係の確立後、操作可能な状態になるまで待機する必要がある

PTRACE_TRACEME はトレシーによって呼び出され、親プロセスによる追跡を要求しますが、これにより他の任意のプロセスがその後に権限を持つことを意味するものではありません。PTRACE_ATTACH はアタッチを行い対応する停止フローをトリガーしますが、トレーサーは wait によって確認する必要があります。PTRACE_SEIZE は追跡関係のみを確立し、即時停止は行いません。その後、PTRACE_INTERRUPT を使用して停止を要求できます。

ptrace リクエストは通常、TID(スレッドID)でスレッドを指定します。1つのスレッドには1つのトレーサーしか存在できず、あるTIDにアタッチしても、既存のすべてのスレッドや将来作成されるスレッドが自動的に制御下に収まるわけではありません。既存のスレッドを列挙し、必要に応じて clone、fork、exec、exit などのイベントオプションを設定する必要があります。

一部の読み書きリクエストでは、トレシーが適切な ptrace 停止状態にあることが必要です。プロセスが「実行していないように見える」ことや、直前に ATTACH が呼び出されただけでは、レジスタの読み取りがすぐに成功することを保証するものではありません。ptrace(2)

停止状態はデバッカーの中核である

デバッグループ:停止理由を特定してから再開方法を決める waitpid がトレシーのステータスを返す 終了、終了シグナル、それとも停止? システムコール停止 エントリ/exit情報の読み取り シグナル配信停止 元のシグナルを転送するかどうかを決定 ptraceイベントまたはその他の停止 イベントに応じてスレッドと実行状態を処理 SIGTRAPは唯一の停止理由ではありません。waitが2回行われるたびに、ちょうど1つのシステムコールのエントリとexitがあるとは限りません。

システムコールのエントリとexitの間には、シグナル、exec、終了、またはその他のイベントが挟まることがあります。実際のループでは、WIFEXITED、WIFSIGNALED、WIFSTOPPED を区別してから、具体的な停止タイプを解釈する必要があります。無条件に2回の wait を1ペアとして扱うべきではありません。

PTRACE_O_TRACESYSGOOD を使用すると、システムコール停止のシグナル識別子に 0x80 が付与され、通常の SIGTRAP と区別しやすくなります。サポートされている場合、PTRACE_GET_SYSCALL_INFO を使用して、より明確なエントリやexitなどの情報を取得でき、アーキテクチャ固有のレジスタレイアウトへの直接依存を減らすことができます。ただし、システムコールの再起動やイベント変換の処理は依然として必要です。

シグナル配信停止後、0 で再開するとそのシグナルが抑制される可能性があります。元のシグナルを渡すと、シグナルの配信が継続されます。すべての停止に対して一律に 0 を渡して、追跡がプログラムのセマンティクスに対して完全に透過的であると仮定してはいけません。グループ停止、ptrace イベント停止、および通常のシグナル停止の処理は異なります。

システムコールのペアリングはスレッドに属し、他の停止ではエントリ/exit状態を反転できない TID 11:entry read 現在のスレッドの呼び出しを記録 TID 12:entry write 別のステートを作成 TID 11:signal stop シグナルを識別し、exitとして扱わない TID 12:exit write スレッド12のペアリングのみを終了 TID 11:exit read スレッド11のペアリングのみを終了 図はオフラインのイベントシーケンスです。実際の収集では、syscallの再起動、exec、スレッドの終了、および不一致を処理する必要があります。

分類とペアリングは別々に行う

まず、wait のステータスに基づいて終了と停止を区別し、次に停止情報からイベントを識別します。「エントリ/exit」のブール値を先に反転してから、シグナルを無視しようとしないでください。PTRACE_GET_SYSCALL_INFO を使用する際も、返された op を確認し、明確な ENTRY/EXIT のみが対応する処理に入るようにします。SECCOMP や NONE を通常のペアリングに無理やり当てはめることはできません。ptrace 停止と syscall info

以下は、ptrace を呼び出さないオフラインのイベントモデルです。これは、2つのスレッドが交差する際に、グローバルな in_syscall ブール値がなぜ不十分であるかを示しています。スレッドが終了したが、通常の syscall exit がない場合、未完成の観察記録を別途記録します。

pending = {}
completed = []
incomplete = []
signal_stops = []
events = [
    (11, "entry", "read"),
    (12, "entry", "write"),
    (11, "signal", "SIGUSR1"),
    (12, "exit", 3),
    (11, "exit", 8),
    (12, "entry", "exit_group"),
    (12, "terminated", 0),
]
for tid, kind, value in events:
    if kind == "entry":
        assert tid not in pending
        pending[tid] = value
    elif kind == "exit":
        assert tid in pending
        completed.append((tid, pending.pop(tid), value))
    elif kind == "signal":
        signal_stops.append((tid, value))
    elif kind == "terminated":
        if tid in pending:
            incomplete.append((tid, pending.pop(tid)))
assert completed == [(12, "write", 3), (11, "read", 8)]
assert signal_stops == [(11, "SIGUSR1")]
assert incomplete == [(12, "exit_group")]
assert not pending

モデル内の assert は、入力の中断によってペアリングが壊れることを露呈するためのものでです。実際のトレーサーは、アタッチ時にすでにシステムコール内にある場合、exec や再起動パスにある場合、プログラムエラーとして直ちに終了することはできません。観察の起点とイベントのセマンティクスに基づいて状態を再構築し、確実に関連付けられない記録にはマーキングを行う必要があります。所要時間の統計も、実際にペアリングされた区間のみをカバーできず、exit がない呼び出しを所要時間ゼロとして記録することはできません。

停止分類適用可能なアクション流用すべきでないアクション
明確な syscall entry/exit対応する呼び出し情報を読み取り、TID で関連付ける毎回の SIGTRAP でペアリングを反転させる
signal-delivery-stopシグナル情報を読み取り、転送するかどうかを決定すべてのシグナルを一律に破棄する
ptrace eventスレッド、exec、または終了状態を更新イベントコードを通常のシステムコール結果として扱う
group-stop使用されたアタッチ方式に応じてジョブ制御を処理無条件に CONT を行い、既存の停止セマンティクスを変更する
終了済み/終了シグナルスレッドのステータスと未完成の記録を回収存在しないレジスタを読み続ける

1回のシグナル停止は、syscall ペアリングを乱すか?

A を syscall に入れ、次に B を入れ、A にシグナル停止を追加し、最後に B の呼び出しを先に終了させます。2つの呼び出しは正しくペアリングされます。グローバルなエントリ/exit スイッチを使用すると、2番目のエントリが誤って exit として認識されます。スレッド終了時にペアリングされていない記録は、未完成としてマークする必要があり、所要時間ゼロとして記録してはいけません。

図を準備しています
1回のシグナル停止は、syscall ペアリングを乱すか?

syscall entry/exit は、まず分類してから TID でペアリングする必要があります。シグナル停止は呼び出しを完了せず、スレッドの終了は未完成の観察を残します。

システムコールの観察と擾乱

PTRACE_SYSCALL リクエストは、関連するシステムコールの境界で停止します。エントリでは呼び出し番号と引数を読み取り、exit では戻り値を読み取ることができます。raw で返される負の errno は、libc 包装層の -1 + errno とは異なります。x86-64 ではシステムコールの第4引数は r10 にあり、通常の C 呼び出しの rcx ではありません。arm64 にも独自の ABI があります。

各停止には、カーネル処理、トレーサーのスケジューリング、および情報の読み取りが含まれるため、頻繁なシステムコールは明らかな擾乱を受けます。オーバーヘッドを正確な4回のコンテキストスイッチに固定することはできず、追跡された実行時間を通常の性能の代替として直接使用することもできません。

strace は表示内容をフィルタリングできますが、表示フィルタリングと、ptrace 停止が発生するかどうかは異なるレベルの問題です。一部の最適化モードでは、seccomp を使用して無関係な停止を減らすことがあります。パフォーマンスのホットスポットは、まずサンプリングや BPF による集約で特定し、その後、細粒度の追跡で具体的な動作を検証します。

レジスタとメモリの読み取り

PTRACE_GETREGS は、すべてのアーキテクチャで同じインターフェースを提供するわけではありません。PTRACE_GETREGSET を対応する note 型と iovec と組み合わせて使用し、レジスタセットごとに状態を取得するのがより適しています。ただし、ターゲットアーキテクチャに基づいてバイトレイアウトを解釈する必要があります。

PEEK 系操作で成功して読み取られたデータ自体が -1 である可能性があるため、errno と組み合わせて確認する必要があります。

#include <errno.h>
#include <stddef.h>
#include <sys/ptrace.h>
#include <sys/types.h>

int peek_word(pid_t tid, const void *address, long *out) {
    errno = 0;
    long value = ptrace(PTRACE_PEEKDATA, tid, address, NULL);
    if (value == -1 && errno != 0) return -1;
    *out = value;
    return 0;
}

呼び出し元は、まず権限を取得し、ターゲットを適切な状態に停止させる必要があります。マシンワード単位でメモリを読み取ることは、長さ、アラインメント、ページ境界、およびターゲットのワード長に関係します。1つの word の読み取りに成功しても、隣接するページも読み取れるとは限りません。

ソフトウェアブレークポイントとハードウェアブレークポイント

x86 では、ソフトウェアブレークポイントは、ターゲット命令の先頭を単一バイトの INT3 で置き換えることが一般的です。ヒットすると、デバッカーはブレークポイントを識別し、元の命令を復元し、アーキテクチャのルールに従って PC を調整します(INT3 の停止位置は通常、そのバイトを既に超えています)。その後、元の命令を実行し、再度ブレークポイントを挿入します。

「復元後に再開する」だけでは、再挿入やマルチスレッドの競合を見逃すことになります。別のスレッドが、ブレークポイントが一時的に削除されている間にその位置を通り過ぎる可能性があります。デバッカーは、協調のための一時停止、ステップ実行、またはその他の戦略を必要とします。arm64 の BRK コーディングおよび PC の処理は異なり、0xCC をすべてのターゲットに汎用化することはできません。

ハードウェアの実行ブレークポイントやウォッチポイントは、アーキテクチャのデバッグリソースによって実装され、数、長さ、およびアラインメントの制限があります。最適化により、1つのソースコード行が複数のアドレスに対応したり、ある変数に読み取り可能なメモリ位置が存在しなかったりすることがあります。これは単なるレジスタの読み取りミスではありません。

/proc/PID/mem と process_vm_readv

/proc/PID/mem は、ターゲットの仮想アドレスを使用してファイルオフセット経由でメモリにアクセスすることを許可しますが、開く際には ptrace アクセスモードやその他のセキュリティルールに従う必要があり、権限を回避するものではありません。ターゲットを事前に停止する必要があるかどうか、およびどのアドレスにアクセスできるかは、一貫性の要件と具体的なインターフェースの動作に依存します。proc_pid_mem(5)

process_vm_readv/writev は、プロセス間でメモリをバッチ転送できます。Linux 3.2 で提供されており、5.x 特有のものではありません。部分的な転送が行われる可能性があり、ベクター操作全体がアトミックであることは保証されず、ターゲットのフリーズも提供しません。変更中のリストを読み取ると、かつて同時に存在しなかったフィールドの組み合わせが組み立てられる可能性があります。process_vm_readv(2)

バッチ読み取りはアクセスオーバーヘッドを削減するのに適していますが、ステップ実行、ブレークポイント、またはレジスタ停止のセマンティクスは提供しません。アクセス権限とページ保護の詳細を、「新しいカーネルではすべて許可される」と一般化してはいけません。

部分的に成功した読み取り結果は、コピーされた接頭辞が1つ存在することを証明するのみ

ローカルのターゲットバッファ容量が 8192 で、process_vm_readv が 4096 を返すと仮定します。最初の 4096 バイトのみが今回の成功したコピーデータであり、後半部分は新しいスナップショットとして解析することはできません。「戻り値が負でない」だけでは、構造全体が完全であることを証明するものではありません。解析結果を提出する前に、実際の長さが必要なフィールドをカバーしているかどうかを確認する必要があります。

読み取り戦略改善できる点依然として解決できない点
リモート iovec をページ単位で分割未知アドレスでの障害範囲を小さな領域に制限オブジェクト全体がページを跨ぐ場合の一貫性
実際の転送長を確認読み込まれていない尾部の解析を回避2回の読み取りの間にターゲットが内容を変更する
ヘッダーを先に読み取り、長さに基づいて本文を読み取る割り当て規模と読み取り範囲を制限ヘッダーと本文が異なるバージョンに属している
オブジェクトを変更するすべてのスレッドを停止カバーされた範囲内で並行変更を減少他のプロセスによる共有マップなどの外部変更者

process_vm_readv の部分的な転送は iovec 要素の境界で処理されます。長さが不明な文字列を読み取る際、1つのリモート iovec が未知のページを境界なく跨がないようにしてください。より重要なのは、コピープロセスがアプリケーションオブジェクトのアトミックスナップショットセマンティクスを提供しないことです。process_vm_readv 転送規約

例えば、古い length=100 を読み取り、すでに置き換えられた新しいポインターを読み取ったとします。個別のフィールドはどちらも読み取れますが、組み合わせるとかつて存在したとは限りません。ターゲットプログラムが協力できる場合、ターゲット自身が生成して公開した不変のスナップショットを使用するのが、一貫性を定義しやすくなります。協力できない場合、結果に収集範囲と一貫性の制限を保持する必要があります。

seccomp user notification:システムコールの一部の処理を委任

seccomp フィルターは、一致する呼び出しをリスナーに通知し、監視プロセスが結果、エラー、またはサポートされている場合は元の呼び出しの継続、fd の注入を返すことを可能にします。これは特定システムコールのプロキシや仮想化に適していますが、一般的なユーザーコマンドデバッカーではありません。

通知に含まれるユーザーポインターが指すメモリは、他のスレッドによって変更される可能性があります。パスを読んでチェックを行い、元のシステムコールが同じポインターを引き続き使用することを許可すると、TOCTOU(Time-of-Check to Time-of-Use)が存在します。呼び出しが実行される際には、内容が既に異なる可能性があります。通知IDの有効性チェックは、ライフサイクルの問題の一部しか解決できず、ユーザーメモリの内容が凍結されていることを証明するものではありません。

したがって、CONTINUE 方式をポインターコンテンツに対するセキュリティ承認の一般的なソリューションとして扱うことはできず、監視側の「チェック/変更/承認」をアトミックトランザクションとして記述することもできません。必要に応じてコピー、操作のプロキシ、および制御されたリソースの返却を行い、インターフェースの具体的な競合処理ルールに従ってください。seccomp_unotify(2)

Yama はアタッチの制限であり、基本権限を代替するものではない

ptrace_scope核心的な意味
0古典的な権限チェック;どのユーザーでもどのプロセスを追跡できるわけではない
1制限付きアタッチ;通常、適切な祖先関係または明示的な許可関係が必要;管理者権限の例外も条件付き
2対応する CAP_SYS_PTRACE 権限を持つ管理者のみが、制限付き追跡操作を実行可能
3対応するアタッチと TRACEME 追跡を禁止;設定後、この値を低下させることはできない

具体的には、資格情報、dumpable、ユーザー名前空間、その他の LSM、および capability と組み合わせて考慮する必要があります。「2」を「子孫のみが追跡可能」と記述してはいけません。現代の Linux にも、すべての権限環境で PID 1 に対して一律に禁止する単純なルールは存在しません。Yama ドキュメント

権限失敗を診断する際は、グローバル制限を直接変更するのではなく、まず親子関係、TID、資格情報、およびシステムポリシーを確認してください。特権実行を持つプログラムを追跡すると、exec 時の資格情報昇挙動が変更される可能性があるため、デバッグ状態と通常の実行状態が異なる場合があります。

観察インターフェースの選択

ブレークポイント、ステップ実行、および制御されたレジスタアクセスには、デバッカー/ptrace を選択します。バッチメモリチェックには process_vm_readv を組み合わせ、高頻度動作の統計には perf/BPF を検討します。システムコールのプロキシには、seccomp 通知プロトコルに基づいて設計します。すべてのソリューションは、観察範囲と一貫性を明確にする必要があり、ツールが速いからといって、証拠が自動的に完全になるわけではありません。

キーワード:ptrace, tracee, TID, syscall stop, TRACESYSGOOD, GETREGSET, breakpoint, process_vm_readv, seccomp notification, Yama, TOCTOU