pyjail wiki

Bytecode Manipulation

Jan 15, 2024

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 でバージョンを確認して対応を変える