Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Out of Bytecode - Python スクリプトからメモリ上の関数オブジェクトを呼ぶまで

FFRI Security, Inc.
December 25, 2023
460

Out of Bytecode - Python スクリプトからメモリ上の関数オブジェクトを呼ぶまで

新ローレイヤー勉強会#1 "Out of Bytecode - Python スクリプトからメモリ上の関数オブジェクトを呼ぶまで"
(https://ffri-security.connpass.com/event/304019/)

FFRI Security, Inc.

December 25, 2023
Tweet

Transcript

  1. FFRI Security, Inc. FFRI Security, Inc. https://www.ffri.jp/ 第1回 新 ローレイヤー勉強会

    Out of Bytecode Python スクリプトからメモリ上の関数オブジェクトを呼ぶまで
  2. FFRI Security, Inc. 目次 • Python Internal – Python のバイトコード

    – LOAD_CONST の (奇妙な) 仕様 – Python のオブジェクト • LOAD_CONST の OOB で偽装オブジェクトを呼び出す – プリミティブと手法の流れ – 細かい点の調整 – 別解 – まとめ • 参考文献 2
  3. FFRI Security, Inc. Python VM • CPython では、Python のコードがスタックベースの VM

    で動くバイトコードにコンパイルされて実行される • VM にはスタックの他に、定数の取得や変数の参照のための領域が存在 (↓は関連命令の例) – LOAD_CONST: 定数の格納場所から添字を指定して push – LOAD_FAST: 変数の格納場所から添字を指定して push – STORE_FAST: スタックから pop して添字で指定した変数へ代入 • バイトコードは dis モジュールの dis 関数で逆アセンブルが可能 – 与える形式は次のように複数ある (dis 関数の説明より引用) • Disassemble the x object. x can denote either a module, a class, a method, a function, a generator, an asynchronous generator, a coroutine, a code object, a string of source code or a byte sequence of raw bytecode. 3
  4. FFRI Security, Inc. コードオブジェクト • バイトコードや定数、変数の情報が入っているオブジェクト (↓は主なメンバ) – co_code: バイトコード

    (bytes 型) – co_consts: 定数のタプル – co_varnames: 変数名のタプル • 組み込みの compile 関数で生成されるオブジェクトがこれ – exec や eval といった関数で実行出来る 6
  5. FFRI Security, Inc. コードオブジェクトの作り方 • types モジュールの CodeType クラスをコンストラクタとしてコードオブジェクトを作れる –

    バイトコードや co_consts のような低級な引数の指定が可能 – 黒魔術的だが、関数のコードオブジェクトを差し替えれば、名前はそのままに別の動作をすることも可能 7
  6. FFRI Security, Inc. LOAD_CONST の境界外参照 (CPython) • LOAD_CONST は co_consts

    (タプル) に対して GETITEM で要素を取得している – GETITEM マクロはデバッグビルドで「なければ」 PyTuple_GET_ITEM が使われる • “PyTuple_GetItem() に似ていますが、引数に対するエラーチェックを行いません。” • PyTuple_GET_ITEM は ob_item というメンバ (PyObject * の配列) に対して添字参照をする – 前述したように添字のチェックは無く、負の数でもタプルのサイズを超える値でも指定が可能 → 配列の外側を参照しても「PyObjectを指すポインタ」があった場合、それが push される 8 境界チェックが存在しない
  7. FFRI Security, Inc. PyObject • CPython において、全てのオブジェクトのベースとなるのが PyObject (C 言語の構造体)

    – Tips: id 関数でオブジェクトのアドレスがわかる – 赤: PyObject – 青: PyVarObject – 緑: PyTupleObject 9 タプルの例 ※ (おそらく) デバッグビルドのみ有効なマクロ
  8. FFRI Security, Inc. 例: タプルオブジェクトの構造 • PyVarObject を内包しているため、ob_size に要素数が入る •

    続けて、含まれている要素の PyObject を指すポインタの配列が ob_item に入る – id 関数が返す値からの相対アドレスは + 0x18 10
  9. FFRI Security, Inc. 例: バイトオブジェクトの構造 • PyVarObject を内包しているため、ob_size にバイト長が入る •

    バイト列の実体は ob_sval に char の配列として入る → メモリ上で偽装したオブジェクトを得たい場合は id(fake_obj) + 0x20 を参照すれば良い 11
  10. FFRI Security, Inc. 関数はどのようにして呼ばれるか • “CALL_FUNCTION n” というバイトコードでス タックの関数を n

    引数で呼ぶ – n 個の引数を pop した時にスタックトップに ある関数が呼ばれる • 最終的に _PyObject_MakeTpCall という関 数に辿り着く – callable 変数に呼んだオブジェクトが入る • call 変数に代入された関数ポインタが呼ばれる • Py_TYPE マクロで ob_type を参照し、更にそ の tp_call が call 変数に代入される 12 中略 (call や引数に対するチェックが続いている) _PyObject_MakeTpCall より
  11. FFRI Security, Inc. 前半のまとめ • Python の VM と PyObject

    について軽く説明 – 定数値は LOAD_CONSTS のようなバイトコードで co_consts から取得している – CALL_FUNCTION バイトコードではオブジェクトの ob_type->tp_call が呼ばれる • LOAD_CONST バイトコードでは co_consts の長さを超えて PyObject を取得出来る Q. この OOB を利用して tp_call に関数ポインタを仕込んだメモリ上の偽装オブジェクトを呼び出せないか? 13
  12. FFRI Security, Inc. 問題設定 • 任意の Python コードが実行出来る状態で任意のコード/コマンドを実行せよ (特にシェルを起動せよ) –

    これだけでは自明 (制約を課した上で CTF で見る程度) • 本勉強会の趣旨に合うように「メモリに関連する仕様を利用して」という (曖昧な) 制約を設定 • 便利な組み込み関数や import もできる限り使用禁止 (手法の都合上 id 関数は許可) • CTF の Pwn と同様に、対象の環境は既知 – 特に、libc が手に入っているという状況 → libc の配置アドレスが判明すれば、関数のアドレスも判明 – 今回の環境 • Ubuntu 22.04 (WSL) • Python 3.10.12 14
  13. FFRI Security, Inc. 方針 • OOB を使ってメモリ上の Python オブジェクトを呼び出すバイトコードを実行する –

    自分でメモリ上にオブジェクトを配置し、それを LOAD_CONST で push して呼び出す – 呼び出すオブジェクトの ob_type->tp_call に system 関数のアドレスを仕込んでおく • これらを実現するために – Q1. LOAD_CONST で参照するための添字をどうやって得るか ? – Q2. system 関数のアドレスをどうやって得るか? • どちらを解決するにしても、何らかのアドレスを知るための手段が必要 → 前述のデバッグ時に使っていた関数を思い出すと……? 15
  14. FFRI Security, Inc. プリミティブ • オブジェクトが位置しているアドレスを取得するためのプリミティブ (addrof プリミティブ) が必要 –

    Python では id 関数が PyObject のアドレスを返すのでこれがそのまま使える • co_consts の ob_item に 相当する場所は id(co_consts) + 0x18 – ob_item は PyObject のポインタの配列なので、id(fake_obj) を指すポインタをメモリ上に用意 – このポインタのアドレスを id 関数で求めて id(co_consts) + 0x18 との差を計算すると co_consts に対する添字が判明する (ポインタの配列なので 8 で割る) • 偽装オブジェクトやポインタは、bytes 型の変数を定義した時、対応する PyBytesObject から +0x20の ところに実体を持つことから作成可能 (アドレスが指定出来ないが fakeobj プリミティブに近い) – 対応するアドレスは id(fake_obj) + 0x20で判明する – ポインタは指しているアドレスをリトルエンディアンでバイト列に変換して作成出来る 16
  15. FFRI Security, Inc. 相対アドレスが予測しやすいオブジェクト • 空文字や空タプルといったオブジェクトは libc から見て固定アドレスに配置される – 厳密には実行ごとに

    ±0x1000 程度の誤差があるが、低くない確率で当てることが出来る • addrof (id 関数) でこれらのアドレスを特定してから固定のオフセットを引けば libc のアドレスが判明する → libc 中の system 関数のアドレスが判明する 17 この2つのメモリマップ間の距離が (ほぼ) 一定
  16. FFRI Security, Inc. CALL_FUNCTION 時のメモリの状況 18 ob_refcnt 偽装オブジェクトを指すポインタ (ob_item[n] に相当、OOBで参照)

    ob_refcnt ob_type ob_size ob_item[0] • プリミティブを利用して右図のようなメモリレイアウト を構成する – 各ポインタの値は id 関数で取得出来る – 偽装オブジェクトやポインタは bytes 型変数 から作成出来る ob_type ob_refcnt ob_type … (中略) tp_call (system) fake_data: systemを含む PyTypeObject fake_object: 偽装した PyObject co_consts co_consts->ob_item ptr_fake_object tp_call までのパディング
  17. FFRI Security, Inc. EXTENDED_ARG バイトコード • Python のバイトコードは 2 バイトなので

    “LOAD_CONST n” の n は 1 バイトの値しか指定できない • 指定するオフセットは負の値 (符号ビットが立つので 4 バイト確定) になるため、このままでは指定できない • EXTENDED_ARG バイトコードを使うと引数をビットシフトする形で2バイト以上の値を指定出来る 19
  18. FFRI Security, Inc. 細かい調整 • 関数ポインタを設定することは出来たが、system 関数の引数が制御可能かという問題が残っている – One Gadget

    は出来れば使いたくない (ある程度試したが使えなかった) • 実際に libc の system 関数を呼び出した時のレジスタを確認してみる 20
  19. FFRI Security, Inc. 細かい調整 • 関数ポインタを設定することは出来たが、system 関数の引数が制御可能かという問題が残っている – One Gadget

    は出来れば使いたくない (ある程度試したが使えなかった) • 実際に libc の system 関数を呼び出した時のレジスタを確認してみる 21 rdi, r13 が指す先が制御可能 特に rdi は第1引数
  20. FFRI Security, Inc. 細かい調整: rdi が指す先 • rdi レジスタが指す先に現れた “gakeobj”

    は偽装したオブジェクトの ob_refcnt に入れたバイト列に対応 – 右下図のように “fakeobj” というマーカーを入れて呼び出した際の状況 – 参照カウンタが増減して変化していることに注意 – “callable->ob_type->tp_call(callable, ...)” という呼び出しをしている事に由来 • system の引数として使うのに多少の問題がある – ob_refcnt の増減を考慮する必要がある – ob_refcnt は unsigned long long 型なので8バイトまでしか使えない • “/bin/sh” なら7文字なので (上の問題が解決するなら) 問題無し 22
  21. FFRI Security, Inc. 細かい調整: ob_refcnt を調整する • ob_refcnt 部分を色々と変えて試してみると、単純に 1

    増えるだけ – “fakeobj” が “gakeobj” になったこととも矛盾しない – 例: “AAAAAAAA” -> “BAAAAAAA”, “11111111” -> “21111111” • 先頭の文字が影響を受けることから、ASCIIコードで 1 小さい ‘.’ を入れれば ‘/’ になる → “.bin/sh” を ob_refcnt に入れておけば、system が呼ばれる時に “/bin/sh” になる 23 ob_refcnt に “AAAAAAAA” を指定した場合 ob_refcnt に “11111111” を指定した場合
  22. FFRI Security, Inc. ソースコード (オブジェクトとポインタの作成、添字の計算) 24 CALL_FUNCTION で呼ばれるオブジェクトを作成 - fake_data:

    tp_call に system のポインタを配置 - fake_object: ob_type が fake_data を指す “LOAD_CONST n” の n を計算 (co_consts->ob_item[n] に相当) fake_object を指すポインタを作成 ※ ソースコード全体は後に共有する予定です
  23. FFRI Security, Inc. ソースコード (バイトコードの記述) 25 EXTENDED_ARG で LOAD_CONST の引数を構築

    コードオブジェクトを作成して execで実行 ※ ソースコード全体は後に共有する予定です
  24. FFRI Security, Inc. 手法のまとめ • “LOAD_CONST n” はオブジェクトを指すポインタの配列の参照なので、co_consts からの相対位置が わかるポインタが指すオブジェクトは

    LOAD_CONST でスタックに乗せられる – 通常、n は1バイト (8 bit) の数しか指定出来ないが、EXTENDED_ARG で拡張出来る • 各プリミティブは次のようにして実現出来る – addrof: id 関数 – 偽装オブジェクト作成: PyBytesObject の先頭から 0x20 バイト目以降 • よくある fakeobj と違ってアドレスは指定出来ないが、作成後に addrof でアドレスは取得出来る • “LOAD_CONST n” の OOB を利用して、作っておいたオブジェクトをスタックに乗せて、 CALL_FUNCTION で呼び出すと ob_type->tp_call に仕込んでおいた関数ポインタが呼び出される 26
  25. FFRI Security, Inc. 別解 • ところで Python にも system という関数が存在する

    (os モジュール内) – 機能は libc の system 関数と同じ • この関数に対応するオブジェクトは明示的に os モジュールをインポートせずとも、最初からメモリ上に存在 – 更に空のタプル等から見た相対アドレスも変わらないため、オブジェクトのアドレスは id を使わずとも既知 → “import os” が禁じられている場合でも有効 • 正規のオブジェクトを用いて呼び出すので、引数として文字列の「オブジェクト」が使える – ob_refcnt に強引に文字列を入れなくて良い – co_consts に入れても良いが、せっかくなので LOAD_CONST の OOB でスタックに積んでみる 27
  26. FFRI Security, Inc. 別解 • os.system のアドレスは co_consts (空タプル) から固定オフセットを足すことで取得

    – ※ id(os.system) でも可能 • “/bin/sh” のアドレスは単純に id 関数で取得 • いずれもアドレスが取得出来たので、co_consts からの相対アドレスから添字を計算出来る 28
  27. FFRI Security, Inc. これが出来ると何が嬉しいのか? • 「任意の Python コードが書ける状況で任意コード実行」をしているため、実用上の嬉しさは無い • 一方、CTF

    では様々な制約の元でシェル起動を求める問題が出題される (下記は例) – co_consts が空 – import 不可 – ビルトインの関数が削除される – などなど • こういった問題を解く楽しさに加えて、対象の内部構造や原理に詳しくなれるというメリットがある 29
  28. FFRI Security, Inc. 参考文献 • pythonの関数オブジェクトをいじる話 – HackMD • Deep

    dive into Python's VM: Story of LOAD_CONST bug • インターナルPython ー PythonのオブジェクトとPyObject構造体の関係 - NO!と言えるようになりたい • Python 3.10 の Docs とソースコード – GitHub - python/cpython at 3.10 – 共通のオブジェクト構造体 (common object structure) — Python 3.10.13 ドキュメント – タプルオブジェクト (tuple object) — Python 3.10.13 ドキュメント • 似たようなコンセプトの CTF の問題 – 0CTF/TCTF 2020 Quals – PyAuCalc – HITCON CTF 2022 – V O I D – TSG CTF 2023 - bypy 30