リモートデバッグのアタッチプロトコル¶
このプロトコルを使うと、外部ツールから実行中の CPython プロセスにアタッチし、Python コードをリモートで実行できます。
別の Python プロセスへのアタッチには、プラットフォームによって追加の権限や設定が必要な場合があります。
リモートデバッグの無効化¶
リモートデバッグのサポートを無効にするには、次のいずれかを使います。
インタープリタを起動する前に、環境変数
PYTHON_DISABLE_REMOTE_DEBUGを1に設定します。コマンドラインオプション
-X disable-remote-debugを使います。ビルドフラグ
--without-remote-debugを指定して Python をコンパイルします。
必要な権限¶
リモートデバッグのために実行中の Python プロセスにアタッチするには、ほとんどのプラットフォームで特別な設定が必要です。 具体的な要件と問題の解決手順は、オペレーティングシステムによって異なります。
Linux
通常は自分のプロセスをデバッグできますが、よく使われる設定の中にはそれを無効にするものがあります。
一部の Linux ディストリビューションでは、システムのセキュリティを強化するために ptrace の制限 (別名「Yama」) を有効にしています。
最近の setpriv コマンド (2025 年 6 月リリースの util-linux 2.41) を使うと、プロセスごとに ptrace の制限を緩められます。
setpriv --ptracer any python3
(この設定は デバッグ対象の プロセスに対して行います。) 次のコマンドで、再起動するまで全プロセスの ptrace の制限を無効にすることもできます。
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
通常は /etc/sysctl.d で、この設定を永続化することもできます。
注釈
ptrace_scope を無効にするとシステムのセキュリティが弱まるため、安全性の要求が低い環境でのみ行ってください。
セキュリティフィルターによって ptrace システムコールが無効になっている場合もあります。
特に、一部のコンテナーソフトウェアの古いバージョンではよくあることでした。
Docker 19.03 以降 (2019 年リリース) と containerd 1.6.7 以降 (2022 年リリース) は、Linux カーネル 4.8 以降で実行すると、コンテナー内での ptrace システムコールの使用を自動的に許可します。
これらのバージョンに更新できない場合は、--security-opt seccomp=unconfined のようなオプションを指定してコンテナーを作成し、そのコンテナーのシステムコール用セキュリティフィルターを無効にできます。
ただし、コンテナーの隔離が弱まるため、安全性の要求が低い環境でのみ行ってください。
自分が所有して いない プロセスをトレースするには、スーパーユーザーまたは同等の権限が必要です。
これは、set-user-ID や set-group-ID のプロセスなど、セキュリティ資格情報を変更したプロセスにも当てはまります (Python ではまれです)。
sudo -E を付けてデバッグコマンドを実行してみてください。
注釈
CAP_SYS_PTRACE ケーパビリティは、自分のプロセスだけでなく あらゆる プロセスをデバッグできる点で、スーパーユーザー権限と同等です。
インターネット上では、ptrace の制限やシステムコールフィルターを回避するためにこのケーパビリティを使うよう勧める情報を見かけるかもしれません。
sudo と同様に実際に機能する場合もありますが、デバッグする側のプロセスに必要以上のアクセス権を与えるため、安全性の要求が低い環境でのみ行ってください。
最後に、一つのプロセスを同時にトレースできるのは一つのトレーサーだけです。
strace や gdb などですでに Python プロセスにアタッチしている場合、同時にリモートデバッグは使えません。
(スーパーユーザー権限でもこの制限は回避できません。)
macOS
デフォルトでは、macOS は他のプロセスをデバッグする機能を無効にしています。
Python のバイナリに、デバッグを許可する エンタイトルメント を含む アドホックコード署名 を付けることで、そのバイナリをデバッグ可能にできます。 (アドホック「署名」は単なる設定です。 実際の暗号署名は含まず、証明書や Apple Developer Program への加入なども必要ありません。)
次のコマンドは、必要なエンタイトルメントを含む get-task-allow.plist ファイルを作成し、Python のバイナリに追加します。
echo '{"com.apple.security.get-task-allow": true}' | plutil -convert xml1 -o get-task-allow.plist -
codesign --sign - --entitlements get-task-allow.plist path/to/bin/python3
where path/to/bin/python3 is the path to your Python binary, which you can
find by e.g. running which python3 or evaluating sys.base_executable at
the Python REPL. (These instructions are for a non-framework build of Python.
Framework builds may need to be configured differently.)
その後は、そのバイナリで起動した自分の Python プロセスをデバッグできるはずです。
また、Linux と同様に、sudo などのスーパーユーザー権限を持つプロセスはこの検査の対象にならず、システム上のどのユーザーのプロセスもデバッグできます。
ただし、OS が提供するコマンドなどの特定のバイナリには、システム整合性保護による追加の検査があります。
Windows
別のプロセスにアタッチするには、通常、管理者権限でデバッグツールを実行する必要があります。 コマンドプロンプトまたはターミナルを管理者として起動してください。
管理者権限があっても、SeDebugPrivilege 特権を有効にしないとアクセスできないプロセスもあります。
ファイルやフォルダーへのアクセスの問題を解決するには、セキュリティのアクセス許可を調整します。
ファイルまたはフォルダーを右クリックし、プロパティ を選択します。
セキュリティ タブで、アクセスできるユーザーとグループを確認します。
編集 をクリックしてアクセス許可を変更します。
自分のユーザーアカウントを選択します。
アクセス許可 で、必要に応じて 読み取り または フル コントロール にチェックを入れます。
適用、OK の順にクリックして確定します。
注釈
先に進む前に、必要な権限 をすべて満たしていることを確認してください。
この節では、外部ツールから実行中の CPython プロセスに Python スクリプトを注入し、実行するための低水準のプロトコルを説明します。
この仕組みは、リモートの Python プロセスに .py ファイルの実行を指示する sys.remote_exec() 関数の基盤です。
ただし、この節はその関数の使い方を説明するものではありません。
対象の Python プロセスの pid と実行する Python ソースファイルのパスを入力として受け取る、基盤となるプロトコルを詳しく説明します。
ここで説明する情報を使えば、プログラミング言語にかかわらず、このプロトコルを独自に実装できます。
警告
注入したスクリプトは、インタープリタが安全に評価できる地点に到達してから実行されます。 そのため、対象プロセスの実行状態によっては、実行が遅れることがあります。
スクリプトを注入すると、対象プロセスのインタープリタが次に安全に評価できる地点に到達したときに、そのスクリプトを実行します。 これにより、実行中の Python アプリケーションの動作や構造を変更せずに、リモート実行機能を提供できます。
以降の節では、メモリ内でインタープリタの構造体を見つける方法、内部フィールドに安全にアクセスする方法、コードの実行を開始させる方法など、プロトコルの手順を説明します。 プラットフォームによる違いは該当箇所に記し、各操作を明確にするための実装例も示します。
PyRuntime 構造体の位置の特定¶
外部ツールが実行時に見つけやすいように、CPython は PyRuntime 構造体をバイナリ内の専用セクションに配置します。
このセクションの名前と形式はプラットフォームによって異なります。
例えば、ELF システムでは .PyRuntime、macOS では __DATA,__PyRuntime を使います。
ツールはディスク上のバイナリを調べることで、この構造体のオフセットを見つけられます。
PyRuntime 構造体には CPython のグローバルなインタープリタの状態が格納されており、インタープリタのリスト、スレッド状態、デバッガーを支援するフィールドなどの内部データにアクセスできます。
To work with a remote Python process, a debugger must first find the memory
address of the PyRuntime structure in the target process. This address
can’t be hardcoded or calculated from a symbol name, because it depends on
where the operating system loaded the binary.
PyRuntime の位置を特定する方法はプラットフォームによって異なりますが、大まかな手順は共通です。
対象プロセスで Python のバイナリまたは共有ライブラリがロードされたベースアドレスを調べます。
ディスク上のバイナリを使って
.PyRuntimeセクションのオフセットを調べます。ベースアドレスにセクションのオフセットを加え、メモリ内のアドレスを求めます。
以下の節では、サポートされる各プラットフォームでの方法とコード例を示します。
Linux (ELF)
Linux で PyRuntime 構造体の位置を特定するには、次の手順を実行します。
プロセスのメモリマップ (例えば
/proc/<pid>/maps) を読み、Python の実行ファイルまたはlibpythonがロードされたアドレスを調べます。バイナリの ELF セクションヘッダーを解析し、
.PyRuntimeセクションのオフセットを取得します。手順 1 のベースアドレスにそのオフセットを加え、
PyRuntimeのメモリアドレスを求めます。
実装例を示します。
def find_py_runtime_linux(pid: int) -> int:
# Step 1: Try to find the Python executable in memory
binary_path, base_address = find_mapped_binary(
pid, name_contains="python"
)
# Step 2: Fallback to shared library if executable is not found
if binary_path is None:
binary_path, base_address = find_mapped_binary(
pid, name_contains="libpython"
)
# Step 3: Parse ELF headers to get .PyRuntime section offset
section_offset = parse_elf_section_offset(
binary_path, ".PyRuntime"
)
# Step 4: Compute PyRuntime address in memory
return base_address + section_offset
Linux では、別のプロセスのメモリを読む主な方法が二つあります。
一つは /proc ファイルシステムを使う方法で、具体的にはプロセスのメモリに直接アクセスできる /proc/[pid]/mem を読みます。
これには、対象プロセスと同じユーザーであるか root 権限を持つなど、適切な権限が必要です。
もう一つは、プロセス間でより効率的にメモリをコピーできる process_vm_readv() システムコールを使う方法です。
ptrace の PTRACE_PEEKTEXT 操作でもメモリを読めますが、一度に 1 ワードしか読めず、トレースする側とされる側のプロセス間で複数回のコンテキスト切り替えが必要なため、はるかに低速です。
For parsing ELF sections, the process involves reading and interpreting the ELF file format structures from the binary file on disk. The ELF header contains a pointer to the section header table. Each section header contains metadata about a section including its name (stored in a separate string table), offset, and size. To find a specific section like .PyRuntime, you need to walk through these headers and match the section name. The section header then provides the offset where that section exists in the file, which can be used to calculate its runtime address when the binary is loaded into memory.
ELF ファイル形式の詳細は ELF の仕様 を参照してください。
macOS (Mach-O)
macOS で PyRuntime 構造体の位置を特定するには、次の手順を実行します。
task_for_pid()を呼び出し、対象プロセスのmach_port_tタスクポートを取得します。 このハンドルは、mach_vm_read_overwriteやmach_vm_regionなどの API でメモリを読むために必要です。メモリ領域を調べ、Python の実行ファイルまたは
libpythonを含む領域を探します。Load the binary file from disk and parse the Mach-O headers to find the section named
PyRuntimein the__DATAsegment. On macOS, symbol names are automatically prefixed with an underscore, so thePyRuntimesymbol appears as_PyRuntimein the symbol table, but the section name is not affected.
実装例を示します。
def find_py_runtime_macos(pid: int) -> int:
# Step 1: Get access to the process's memory
handle = get_memory_access_handle(pid)
# Step 2: Try to find the Python executable in memory
binary_path, base_address = find_mapped_binary(
handle, name_contains="python"
)
# Step 3: Fallback to libpython if the executable is not found
if binary_path is None:
binary_path, base_address = find_mapped_binary(
handle, name_contains="libpython"
)
# Step 4: Parse Mach-O headers to get __DATA,__PyRuntime section offset
section_offset = parse_macho_section_offset(
binary_path, "__DATA", "__PyRuntime"
)
# Step 5: Compute the PyRuntime address in memory
return base_address + section_offset
macOS で別のプロセスのメモリにアクセスするには、Mach-O 固有の API とファイル形式を使います。
まず、task_for_pid() で task_port ハンドルを取得し、対象プロセスのメモリ空間にアクセスできるようにします。
このハンドルを使うと、mach_vm_read_overwrite() などの API でメモリを操作できます。
mach_vm_region() で仮想メモリ空間を走査すると、プロセスのメモリを調べられます。
proc_regionfilename() は、各メモリ領域にロードされたバイナリファイルの特定に役立ちます。
Python のバイナリまたはライブラリを見つけたら、その Mach-O ヘッダーを解析して PyRuntime 構造体の位置を特定します。
Mach-O 形式では、コードとデータをセグメントとセクションに分けて格納します。
PyRuntime 構造体は、__DATA セグメント内の __PyRuntime というセクションにあります。
実行時のアドレスを求めるには、バイナリのベースアドレスとなる __TEXT セグメントを見つけ、続いて目的のセクションを含む __DATA セグメントを探します。
ベースアドレスと Mach-O ヘッダーの適切なセクションオフセットを組み合わせて、最終的なアドレスを求めます。
macOS で別のプロセスのメモリにアクセスするには、通常、root 権限やデバッグする側のプロセスに付与された特別なセキュリティエンタイトルメントなど、昇格された権限が必要です。
Windows (PE)
Windows で PyRuntime 構造体の位置を特定するには、次の手順を実行します。
ToolHelp API を使い、対象プロセスにロードされたすべてのモジュールを列挙します。 これには CreateToolhelp32Snapshot、Module32First、Module32Next などの関数を使います。
python.exeまたはpythonXY.dllに対応するモジュールを特定し、ベースアドレスを記録します。XとYは Python のメジャーバージョン番号とマイナーバージョン番号です。PyRuntimセクションを探します。 PE 形式のセクション名には 8 文字の制限 (IMAGE_SIZEOF_SHORT_NAMEとして定義) があるため、元の名前PyRuntimeは切り詰められます。 このセクションにPyRuntime構造体があります。セクションの相対仮想アドレス (RVA) を取得し、モジュールのベースアドレスに加えます。
実装例を示します。
def find_py_runtime_windows(pid: int) -> int:
# Step 1: Try to find the Python executable in memory
binary_path, base_address = find_loaded_module(
pid, name_contains="python"
)
# Step 2: Fallback to shared pythonXY.dll if the executable is not
# found
if binary_path is None:
binary_path, base_address = find_loaded_module(
pid, name_contains="python3"
)
# Step 3: Parse PE section headers to get the RVA of the PyRuntime
# section. The section name appears as "PyRuntim" due to the
# 8-character limit defined by the PE format (IMAGE_SIZEOF_SHORT_NAME).
section_rva = parse_pe_section_offset(binary_path, "PyRuntim")
# Step 4: Compute PyRuntime address in memory
return base_address + section_rva
Windows で別のプロセスのメモリにアクセスするには、CreateToolhelp32Snapshot() や Module32First()/Module32Next() などの Windows API 関数で、ロードされたモジュールを列挙します。
OpenProcess() 関数は対象プロセスのメモリ空間にアクセスするためのハンドルを返し、ReadProcessMemory() でメモリを操作できるようにします。
ロードされたモジュールを列挙して Python のバイナリまたは DLL を探すことで、プロセスのメモリを調べられます。
見つかったら、その PE ヘッダーを解析して PyRuntime 構造体の位置を特定します。
PE 形式では、コードとデータをセクションに分けて格納します。
PyRuntime 構造体は「PyRuntim」というセクションにあります (PE の名前の長さ制限が 8 文字なので、「PyRuntime」から切り詰められています)。
実行時のアドレスを求めるには、モジュールエントリーからモジュールのベースアドレスを調べ、PE ヘッダーで目的のセクションを探します。
ベースアドレスと PE セクションヘッダーのセクション仮想アドレスを組み合わせて、最終的なアドレスを求めます。
Windows で別のプロセスのメモリにアクセスするには、通常、管理者権限やデバッグする側のプロセスに付与された SeDebugPrivilege 特権など、適切な権限が必要です。
_Py_DebugOffsets の読み取り¶
PyRuntime 構造体のアドレスを特定したら、次は PyRuntime ブロックの先頭にある _Py_DebugOffsets 構造体を読み取ります。
この構造体は、インタープリタやスレッド状態のメモリを安全に読み取るために必要な、バージョンごとのフィールドオフセットを提供します。 オフセットは CPython のバージョンによって異なるため、使用前に互換性を確認する必要があります。
デバッグ用オフセットを読み取って検査するには、次の手順を実行します。
対象プロセスの
PyRuntimeアドレスから、_Py_DebugOffsets構造体のサイズと同じバイト数のメモリを読み取ります。 この構造体はPyRuntimeメモリブロックの先頭にあります。 そのレイアウトは CPython の内部ヘッダーで定義され、同じマイナーバージョン内では変わりませんが、メジャーバージョンが変わると変更される可能性があります。構造体に有効なデータが含まれているか確認します。
cookieフィールドは、想定するデバッグ用マーカーと一致しなければなりません。versionフィールドは、デバッガーが使う Python インタープリタのバージョンと一致しなければなりません。デバッガーか対象プロセスのいずれかがプレリリース版 (アルファ版、ベータ版、リリース候補版など) を使っている場合は、バージョンが完全に一致しなければなりません。
free_threadedフィールドは、デバッガーと対象プロセスの両方で同じ値でなければなりません。
構造体が有効なら、格納されたオフセットを使ってメモリ内のフィールドの位置を特定できます。 いずれかの検査に失敗した場合は、誤った形式でメモリを読まないように、デバッガーは操作を中止するべきです。
_Py_DebugOffsets を読み取って検査する実装例を示します。
def read_debug_offsets(pid: int, py_runtime_addr: int) -> DebugOffsets:
# 手順 1: 対象プロセスの PyRuntime アドレスからメモリを読む
data = read_process_memory(
pid, address=py_runtime_addr, size=DEBUG_OFFSETS_SIZE
)
# 手順 2: バイト列を _Py_DebugOffsets 構造体に復元する
debug_offsets = parse_debug_offsets(data)
# 手順 3: 構造体の内容を検証する
if debug_offsets.cookie != EXPECTED_COOKIE:
raise RuntimeError("Invalid or missing debug cookie")
if debug_offsets.version != LOCAL_PYTHON_VERSION:
raise RuntimeError(
"Mismatch between caller and target Python versions"
)
if debug_offsets.free_threaded != LOCAL_FREE_THREADED:
raise RuntimeError("Mismatch in free-threaded configuration")
return debug_offsets
警告
プロセスの一時停止を推奨
競合状態を避けてメモリの一貫性を確保するため、インタープリタの内部状態を読み書きする前に、対象プロセスを一時停止することを強く推奨します。 Python ランタイムは通常の実行中に、スレッドの作成や破棄など、インタープリタのデータ構造の変更を並行して行うことがあります。 その結果、無効なメモリの読み書きが発生する可能性があります。
デバッガーは、ptrace でプロセスにアタッチするか、SIGSTOP シグナルを送ることで実行を一時停止できます。
デバッガー側のメモリ操作が完了してから実行を再開するべきです。
注釈
プロファイラーやサンプリング方式のデバッガーなど、一部のツールはプロセスを停止せずに動作する場合があります。 その場合、更新途中のメモリや一貫性のないメモリを扱えるように、明示的に設計しなければなりません。 ほとんどのデバッガー実装では、プロセスを一時停止するのが最も安全で堅牢な方法です。
インタープリタとスレッド状態の位置の特定¶
リモートの Python プロセスにコードを注入して実行する前に、デバッガーは実行を予約するスレッドを選ぶ必要があります。
リモートコードの注入を制御するフィールドが、PyThreadState オブジェクトに埋め込まれた _PyRemoteDebuggerSupport 構造体にあるためです。
デバッガーはこれらのフィールドを変更して、注入したスクリプトの実行を要求します。
PyThreadState 構造体は、Python インタープリタ内で実行するスレッドを表します。
スレッドの評価コンテキストを保持し、デバッガーと協調するためのフィールドを含みます。
そのため、有効な PyThreadState の位置を特定することが、リモート実行を開始させるための重要な前提条件です。
通常は役割や ID に基づいてスレッドを選びます。 ほとんどの場合はメインスレッドを使いますが、ネイティブスレッド ID で特定のスレッドを指定するツールもあります。 対象のスレッドを選んだら、デバッガーはインタープリタと対応するスレッド状態の構造体をメモリ内で探さなければなりません。
関連する内部構造体の定義は次のとおりです。
PyInterpreterStateは、隔離された Python インタープリタのインスタンスを表します。 各インタープリタは、インポート済みモジュール、組み込みの状態、スレッド状態のリストをそれぞれ保持します。 ほとんどの Python アプリケーションは一つのインタープリタを使いますが、CPython は同じプロセス内での複数のインタープリタをサポートします。PyThreadStateは、インタープリタ内で実行するスレッドを表します。 実行状態と、デバッガーが使う制御用フィールドを含みます。
スレッドの位置を特定するには、次の手順を実行します。
オフセット
runtime_state.interpreters_headを使い、PyRuntime構造体から最初のインタープリタのアドレスを取得します。 ここが、動作中のインタープリタの連結リストの先頭です。オフセット
interpreter_state.threads_mainを使い、選んだインタープリタに対応するメインスレッドの状態にアクセスします。 通常、最も確実に対象にできるのはこのスレッドです。必要に応じて、オフセット
interpreter_state.threads_headを使って全スレッド状態の連結リストをたどります。 各PyThreadState構造体にはnative_thread_idフィールドがあり、目的のスレッド ID と比較して特定のスレッドを探せます。有効な
PyThreadStateが見つかったら、そのアドレスを、デバッガーの制御用フィールドへの書き込みや実行の予約など、プロトコルの後続の手順で使えます。
メインスレッドの状態の位置を特定する実装例を示します。
def find_main_thread_state(
pid: int, py_runtime_addr: int, debug_offsets: DebugOffsets,
) -> int:
# 手順 1: PyRuntime から interpreters_head を読む
interp_head_ptr = (
py_runtime_addr + debug_offsets.runtime_state.interpreters_head
)
interp_addr = read_pointer(pid, interp_head_ptr)
if interp_addr == 0:
raise RuntimeError("No interpreter found in the target process")
# 手順 2: インタープリタから threads_main ポインターを読む
threads_main_ptr = (
interp_addr + debug_offsets.interpreter_state.threads_main
)
thread_state_addr = read_pointer(pid, threads_main_ptr)
if thread_state_addr == 0:
raise RuntimeError("Main thread state is not available")
return thread_state_addr
次の例は、ネイティブスレッド ID でスレッドを探す方法を示します。
def find_thread_by_id(
pid: int,
interp_addr: int,
debug_offsets: DebugOffsets,
target_tid: int,
) -> int:
# threads_head から連結リストをたどる
thread_ptr = read_pointer(
pid,
interp_addr + debug_offsets.interpreter_state.threads_head
)
while thread_ptr:
native_tid_ptr = (
thread_ptr + debug_offsets.thread_state.native_thread_id
)
native_tid = read_int(pid, native_tid_ptr)
if native_tid == target_tid:
return thread_ptr
thread_ptr = read_pointer(
pid,
thread_ptr + debug_offsets.thread_state.next
)
raise RuntimeError("Thread with the given ID was not found")
有効なスレッド状態の位置を特定したら、デバッガーは次の節で説明するように、制御用フィールドを変更して実行を予約できます。
制御情報の書き込み¶
有効な PyThreadState 構造体を特定したら、デバッガーは内部の制御用フィールドを変更して、指定した Python スクリプトの実行を予約できます。
インタープリタはこれらのフィールドを定期的に検査し、正しく設定されていれば、評価ループの安全な地点でリモートコードの実行を開始します。
各 PyThreadState には、デバッガーとインタープリタの通信に使う _PyRemoteDebuggerSupport 構造体があります。
フィールドの位置は _Py_DebugOffsets 構造体で定義されており、次のものを含みます。
debugger_script_path: Python ソースファイル (.py) の完全なパスを保持する固定長バッファー。 実行を開始するとき、対象プロセスがこのファイルにアクセスして読み取れる必要があります。debugger_pending_call: 整数のフラグ。1に設定すると、スクリプトを実行する準備ができたことをインタープリタに伝えます。eval_breaker: 実行中にインタープリタが検査するフィールド。 このフィールドのビット 5 (_PY_EVAL_PLEASE_STOP_BIT、値は1U << 5) を立てると、インタープリタは処理を中断してデバッガーからの操作を確認します。
注入を完了するには、デバッガーは次の手順を実行しなければなりません。
スクリプトの完全なパスを
debugger_script_pathバッファーに書き込みます。debugger_pending_callを1に設定します。eval_breakerの現在の値を読み取り、ビット 5 (_PY_EVAL_PLEASE_STOP_BIT) を立て、更新した値を書き戻します。 これにより、デバッガーからの操作を確認するようインタープリタに通知します。
実装例を示します。
def inject_script(
pid: int,
thread_state_addr: int,
debug_offsets: DebugOffsets,
script_path: str
) -> None:
# _PyRemoteDebuggerSupport の先頭のオフセットを求める
support_base = (
thread_state_addr +
debug_offsets.debugger_support.remote_debugger_support
)
# 手順 1: debugger_script_path にスクリプトのパスを書く
script_path_ptr = (
support_base +
debug_offsets.debugger_support.debugger_script_path
)
write_string(pid, script_path_ptr, script_path)
# 手順 2: debugger_pending_call を 1 に設定する
pending_ptr = (
support_base +
debug_offsets.debugger_support.debugger_pending_call
)
write_int(pid, pending_ptr, 1)
# 手順 3: eval_breaker の _PY_EVAL_PLEASE_STOP_BIT
# (ビット 5、値は 1 << 5) を立てる
eval_breaker_ptr = (
thread_state_addr +
debug_offsets.debugger_support.eval_breaker
)
breaker = read_int(pid, eval_breaker_ptr)
breaker |= (1 << 5)
write_int(pid, eval_breaker_ptr, breaker)
これらのフィールドを設定したら、デバッガーは (プロセスを一時停止していた場合は) 実行を再開できます。 インタープリタは次に安全に評価できる地点で要求を処理し、ディスクからスクリプトを読み込んで実行します。
実行中、スクリプトファイルが存在し、対象プロセスからアクセスできる状態を保つ責任はデバッガーにあります。
注釈
スクリプトは非同期に実行されます。 注入の直後にスクリプトファイルを削除してはいけません。 デバッガーは、注入したスクリプトによる作用を観測してからファイルを削除するべきです。 何を観測するかは、スクリプトの目的によります。 例えば、リモートプロセスからソケットへの接続があるまで待ってから、スクリプトを削除する方法があります。 そのような作用を観測できたら、ファイルは不要になったと判断できます。
まとめ¶
リモートプロセスに Python スクリプトを注入して実行する手順は次のとおりです。
対象プロセスのメモリ内で
PyRuntime構造体の位置を特定します。PyRuntimeの先頭にある_Py_DebugOffsets構造体を読み取り、検証します。オフセットを使って、有効な
PyThreadStateの位置を特定します。Python スクリプトのパスを
debugger_script_pathに書き込みます。debugger_pending_callフラグを1に設定します。eval_breakerフィールドの_PY_EVAL_PLEASE_STOP_BITを立てます。一時停止していた場合はプロセスの実行を再開します。 スクリプトは、次に安全に評価できる地点で実行されます。
セキュリティと脅威モデル¶
リモートデバッグのプロトコルは、GDB や LLDB などのネイティブデバッガーと同じ OS の基本機能を使います。
プロセスへのアタッチには、Linux の ptrace / Yama LSM、macOS の task_for_pid、Windows の SeDebugPrivilege など、それらのデバッガーと 同じ権限 が必要です。
Python が新たな権限昇格の経路を導入するわけではありません。
攻撃者がプロセスへのアタッチに必要な権限をすでに持つ場合、GDB を使ってメモリを読んだりコードを注入したりすることもできます。
この機能で何をセキュリティ上の脆弱性とみなすか、また、みなさないかは、次の原則で定義されます。
- アタッチには OS レベルの権限が必要
On every supported platform the operating system gates cross-process memory access behind privilege checks (
CAP_SYS_PTRACE, root, or administrator rights). A report that demonstrates an issue only after these privileges have already been obtained is not a vulnerability in CPython, since the OS security boundary was already crossed.- 侵害されたプロセスを読み取る際のクラッシュやメモリエラーは脆弱性ではない
対象プロセスからインタープリタの内部状態を読むツールは、そのメモリの形式が正しいと信頼する必要があります。 対象プロセスが破損しているか、攻撃者に制御されている場合、デバッガーやプロファイラーがクラッシュしたり、無意味な出力を生成したり、予測できない動作をしたりする可能性があります。 これは、
ptraceに基づくすべてのデバッガーが受け入れているのと同じリスクです。 この種類のバグ (破損した状態の読み取りによるバッファーオーバーフロー、セグメンテーション違反、未定義動作) は、セキュリティの問題とは みなしません が、堅牢性を高める修正は歓迎します。- 対象プロセスの脆弱性は対象外
デバッグ対象の Python プロセスがすでに侵害されている場合、攻撃者はそのプロセス内の実行をすでに制御しています。 その状態からさらに影響を与えられることを示しても、リモートデバッグのプロトコルの脆弱性にはなりません。
PYTHON_DISABLE_REMOTE_DEBUG を使う場面¶
環境変数 PYTHON_DISABLE_REMOTE_DEBUG (および同等のフラグ -X disable-remote-debug) を使うと、運用者は 多層防御 の対策として、プロトコルのプロセス内で動く部分を無効にできます。
OS レベルの権限検査ですでに権限のないアクセスを防いでいても、プロセスのデバッグやプロファイリングを想定せず、攻撃対象となる機能を減らすことを優先する環境では役立つことがあります。
例えば、セキュリティを強化した環境やサンドボックス内への配置が該当します。
この変数を設定しても、OS レベルの他のデバッグインターフェース (ptrace、/proc、task_for_pid など) には影響 しません。
それぞれの権限モデルに従って引き続き利用できます。