拾い物のコンパス

まともに書いたメモ

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ビット版.
つまりただのLinuxpythonは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コードに直に変換する方法がある.
そっちを使ったほうが余程楽.

参考

pycファイルの逆アセンブル - one day in summer

.pycファイルを作成する方法メモ - かせきのうさぎさん