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コードに直に変換する方法がある.
そっちを使ったほうが余程楽.