Bytecode Manipulation
Overview
Pythonのコードはバイトコードにコンパイルされて実行される。code objectを直接操作することで、通常では不可能な操作を実現できる。
Code Object の基礎
def f(): return 1
code = f.__code__print(code.co_code) # バイトコードprint(code.co_consts) # 定数print(code.co_names) # 名前print(code.co_varnames) # ローカル変数名types.CodeType によるコード生成
import types
# Python 3.8+code = types.CodeType( 0, # argcount 0, # posonlyargcount (3.8+) 0, # kwonlyargcount 0, # nlocals 1, # stacksize 0, # flags bytes([100, 0, 83]), # co_code: LOAD_CONST 0, RETURN_VALUE (None,), # consts (), # names (), # varnames '', # filename '', # name 0, # firstlineno b'', # lnotab)IMPORT_FROM = LOAD_ATTR トリック
Python 3.9 以降、IMPORT_FROM オペコードは内部的に LOAD_ATTR と同様に動作する。
# LACTF 2023 Pycjail で使用されたテクニック# from os import system は内部的に:# 1. IMPORT_NAME os# 2. IMPORT_FROM system (= LOAD_ATTR system)# 3. STORE_NAME systemこのトリックにより、from X import Y 構文で属性アクセスが可能。
co_consts の書き換え
# Python 3.11 以前では __code__.replace() で操作可能def f(): return 'safe'
# 定数を書き換えf.__code__ = f.__code__.replace(co_consts=(None, 'malicious'))print(f()) # 'malicious'marshal によるコード操作
import marshal
def f(): return 1
# シリアライズserialized = marshal.dumps(f.__code__)
# デシリアライズして実行code = marshal.loads(serialized)exec(code)バイトコードの直接生成
# Python 3.6+ のワードコード形式# 各命令は2バイト (opcode, arg)
import dis
# LOAD_GLOBAL 0 (print)# LOAD_CONST 1 ('hello')# CALL_FUNCTION 1# RETURN_VALUE
bytecode = bytes([ 116, 0, # LOAD_GLOBAL 0 100, 1, # LOAD_CONST 1 131, 1, # CALL_FUNCTION 1 83, 0, # RETURN_VALUE])LOAD_CONST 範囲外アクセス
古いPythonバージョンでは、co_consts の範囲外インデックスを指定することで、メモリ上の他のオブジェクトにアクセスできた。
# 歴史的な脆弱性 (現在は修正済み)# TSJ CTF 2022 "Just a pyjail" で使用Python 3.11+ の変更点
Python 3.11 では、バイトコード形式が大幅に変更された:
co_codeは不変 (immutable)- 新しい
co_exceptiontable - 可変長命令 (specialized instructions)
CACHE擬似命令
# 3.11+ では CodeType.replace() を使用code = code.replace(co_consts=new_consts)printable ASCII バイトコード
jailCTF 2024 parseltongue では、printable ASCII文字のみでバイトコードを構成するテクニックが使用された。
# printable ASCII のオペコードのみ使用# 例: LOAD_FAST (124 = '|')Frame オブジェクト経由のアクセス
import sys
def f(): frame = sys._getframe() return frame.f_code
# frame.f_code からコードオブジェクトにアクセスTips
dis.dis()でバイトコードを人間が読める形式で表示- Python バージョンによりバイトコード形式が異なる
- 3.6 以降はワードコード (2バイト/命令)
- 3.11 以降は可変長命令
sys.version_infoでバージョンを確認して対応を変える