pycを読む(アセンブリ味)
pycファイルだけが手元にあり,なにが書いてあるか読みたいときがある.
pycのバイトコードは逆アセンブルして読める.
やり方と例文をまとめる.
ただし,これは低レベルな命令になるから可読性は良くない.
直接pythonコードにまで戻す方法があるから,そっちを使ったほうが良い.
厳密な仕様までは調べてないから,間違いはあるかも.指摘歓迎.
環境
各種バージョン
以下のとおり.
$ uname -a Linux poppycompass 4.18.6-arch1-1-ARCH #1 SMP PREEMPT Wed Sep 5 11:54:09 UTC 2018 x86_64 GNU/Linux $ python2 Python 2.7.15 (default, Jun 27 2018, 13:05:28) [GCC 8.1.1 20180531] on linux2
Arch Linuxの64ビット版.
つまりただのLinux,pythonは2系.
環境作成
環境はローカルに作成した.
$ virtualenv2 ENV $ . ENV/bin/activate $ pip install dis
pycを逆アセンブルするスクリプト
以下のスクリプトで変換できる.
# pyc_disasm.py import dis, marshal code = open("sample.pyc", "rb").read()[8:] code = marshal.loads(code) dis.dis(code)
ここでは,sample.pyc
というファイルを逆アセンブルする.
sample.py
に変換したいコードを書けばよい.
使い方は簡単.
$ python2 -m compileall sample.py # pyc生成 $ python2 ./pyc_disasm.py
変換一覧
逆アセンブルしたものはアセンブリ味になっている.
pythonによる記述と逆アセンブルしたものの比較をいくつか挙げておく.
各命令の左にある数値はアドレスのようだ.
ライブラリのインポート
普通にインポート
before
import random
after
0 LOAD_CONST 0 (-1) 3 LOAD_CONST 1 (None) 6 IMPORT_NAME 0 (random) 9 STORE_NAME 0 (random)
ちょっと書き方を変えたインポート
before
from random import *
after
0 LOAD_CONST 0 (-1) 3 LOAD_CONST 1 (('*',)) 6 IMPORT_NAME 0 (random) 9 IMPORT_STAR
変数定義
before
a = ''
after
0 LOAD_CONST 0 ('') 3 STORE_NAME 0 (a)
関数呼び出し
before
def hoge(a,b): return a+b hoge(1,2)
after
最初の部分が関数定義,後半が呼び出したときの部分.
関数呼び出しは,関数名・引数・CALL・戻り値格納先の順になる
0 LOAD_CONST 0 (<code object hoge at 0x7fc3dec66b30, file "./sample.py", line 1>) 3 MAKE_FUNCTION 0 6 STORE_NAME 0 (hoge) 9 LOAD_NAME 0 (hoge) 12 LOAD_CONST 1 (1) 15 LOAD_CONST 2 (2) 18 CALL_FUNCTION 2 21 POP_TOP 22 LOAD_CONST 3 (None) 25 RETURN_VALUE
演算
before
2以上の変数による演算.
d = a + b + c
after
a
, b
, c
には,文字列と数値のどちらをいれてもコード自体は変わらない.
直前のLOAD_NAME
で入れる値が変わるだけ.
BINARY_ADD
は相手が文字列だと連結,数値だと数値演算を行う.
3つの変数が相手だと,2回BINARY_ADD
する.
以下のコードは数値版.
18 LOAD_NAME 0 (a) 21 LOAD_NAME 1 (b) 24 BINARY_ADD 25 LOAD_NAME 2 (c) 28 BINARY_ADD 29 STORE_NAME 3 (d) 32 LOAD_CONST 1 (None) 35 RETURN_VALUE
文字列連結だと以下の通り
before
allchar = string.ascii_letters + string.punctuation + string.digits # import string後
after
80 LOAD_NAME 0 (string) 83 LOAD_ATTR 9 (ascii_letters) 86 LOAD_NAME 0 (string) 89 LOAD_ATTR 10 (punctuation) 92 BINARY_ADD 93 LOAD_NAME 0 (string) 96 LOAD_ATTR 11 (digits) 99 BINARY_ADD 100 STORE_NAME 12 (allchar)
ループ
before
sum = 0 for i in range(100): sum += i
after
FOR_ITER
にジャンプしてループを回しているようだ.
0 LOAD_CONST 0 (0) 3 STORE_NAME 0 (sum) 6 SETUP_LOOP 30 (to 39) 9 LOAD_NAME 1 (range) 12 LOAD_CONST 1 (100) 15 CALL_FUNCTION 1 18 GET_ITER >> 19 FOR_ITER 16 (to 38) 22 STORE_NAME 2 (i) 25 LOAD_NAME 0 (sum) 28 LOAD_NAME 2 (i) 31 INPLACE_ADD 32 STORE_NAME 0 (sum) 35 JUMP_ABSOLUTE 19 >> 38 POP_BLOCK >> 39 LOAD_CONST 2 (None) 42 RETURN_VALUE
ありがちなコード
before
とあるCTF問題にあった一文.
password = ('').join((choice(allchar) for a in range(randint(60, 60))))
after
繰り返し部はGET_ITER
が入るようだ.
103 LOAD_CONST 6 ('') 106 LOAD_ATTR 13 (join) 109 LOAD_CONST 7 (<code object <genexpr> at 0x7f2fb42d9330, file "ransomware.py", line 11>) 112 MAKE_FUNCTION 0 115 LOAD_NAME 14 (range) 118 LOAD_NAME 15 (randint) 121 LOAD_CONST 8 (60) 124 LOAD_CONST 8 (60) 127 CALL_FUNCTION 2 130 CALL_FUNCTION 1 133 GET_ITER 134 CALL_FUNCTION 1 137 CALL_FUNCTION 1 140 STORE_NAME 16 (password)
おわりに
正直,いざpyc読むときにはこんな低レベルなやつを読むのは労力が要る.正にドM.
pythonコードに直に変換する方法がある.
そっちを使ったほうが余程楽.