作者: Hcamael@知道創宇404實驗室
發布時間:2017-03-20

上周末的0CTF出現了一個pyc的題目,但是Pyopcode損壞,于是手擼了一波

題目: https://github.com/Hcamael/CTF_repo/tree/master/0CTF%202017/Re3%28py%29

通過pyc還原出py網上的資料挺多了,py也有專門的庫可以還原,但是0CTF這題卻無法還原,目測是opcode損壞,同時根據題目描述,也知道是要修復pyc文件。

這里用到兩個庫,一個dis,可以把二進制反編譯CPython bytecode。一個是marshal,可以把字符串轉換成pyopcode對象

>>> import dis, marshal
>>> f = open("crypt.pyc")
>>> f.read(4)
'\x03\xf3\r\n'               # magic number
>>> f.read(4)                # time
'f4oX'
>>> code = marshal.load(f)
# 對我們有用的屬性有:
>>> code.co_argcount          # 參數的個數
0
>>> code.co_varnames          # 局部變量
()
>>> code.co_consts            # 常量 
(-1, None, <code object encrypt at 0x7f1987df65b0, file "/Users/hen/Lab/0CTF/py/crypt.py", line 2>, <code object decrypt at 0x7f1987e10430, file "/Users/hen/Lab/0CTF/py/crypt.py", line 10>)
# 從這個常量中我們可以看出,該py文件中定義了兩個函數,encrypt和decrypt
>>> code.co_code                  
'\x99\x00\x00\x99\x01\x00\x86\x00\x00\x91\x00\x00\x99\x02\x00\x88\x00\x00\x91\x01\x00\x99\x03\x00\x88\x00\x00\x91\x02\x00\x99\x01\x00S'
# CPython bytecode的二進制, 可以通過dis反編譯
>>> dis.disassemble_string(code.co_code)
          0 <153>               0
          3 <153>               1
          6 MAKE_CLOSURE        0
          9 EXTENDED_ARG        0
         12 <153>               2
         15 LOAD_DEREF          0
         18 EXTENDED_ARG        1
         21 <153>               3
         24 LOAD_DEREF          0
         27 EXTENDED_ARG        2
         30 <153>               1
         33 RETURN_VALUE   
# 發現bytecode損壞,根本無法閱讀

二進制對應的bytecode可以參考: https://github.com/Python/cpython/blob/2.7/Include/opcode.h

從上面的參考連接可以得知153沒有對應的bytecode,所以猜測bytecode損壞

每個bytecode所代表的意義: https://docs.Python.org/2/library/dis.html

>>> code.co_name              # 當前對象名 
'<module>'
>>> code.co_names             # 當前對象中使用的對象名
('rotor', 'encrypt', 'decrypt')          
# 從上可以看出,encrypt和decrypt是我們定義的兩個函數,那么rotor我們可以猜測是通過import rotor得來的

rotor的使用可以參考: https://docs.Python.org/2.0/lib/module-rotor.html

# 我們可以通過以下方式查看兩個函數中的信息
>>> enc = code.co_consts[2]
>>> dec = code.co_consts[3]
>>> enc.co_argcount
1
>>> dec.co_argcount
1
# 兩個函數中都有一個傳入的參數
>>> enc.co_varnames
('data', 'key_a', 'key_b', 'key_c', 'secret', 'rot')
>>> dec.co_varnames
('data', 'key_a', 'key_b', 'key_c', 'secret', 'rot')
# 兩個函數中的局部變量, 我們可以猜測,data是傳入的參數,需要加解密的數據
>>> enc.co_consts
(None, '!@#$%^&*', 'abcdefgh', '<>{}:"', 4, '|', 2, 'EOF')
>>> dec.co_consts
(None, '!@#$%^&*', 'abcdefgh', '<>{}:"', 4, '|', 2, 'EOF')
# 兩個函數中的常量,我們可以猜測key_a, key_b, key_c三個變量對應的值
>>> enc.co_code
"\x99\x01\x00h\x01\x00\x99\x02\x00h\x02\x00\x99\x03\x00h\x03\x00a\x01\x00\x99\x04\x00F\x99\x05\x00'a\x02\x00a\x01\x00'a\x03\x00'\x99\x06\x00F'\x99\x05\x00'a\x02\x00\x99\x06\x00F'\x99\x07\x00'h\x04\x00\x9b\x00\x00`\x01\x00a\x04\x00\x83\x01\x00h\x05\x00a\x05\x00`\x02\x00a\x00\x00\x83\x01\x00S"
>>> dec.co_code
"\x99\x01\x00h\x01\x00\x99\x02\x00h\x02\x00\x99\x03\x00h\x03\x00a\x01\x00\x99\x04\x00F\x99\x05\x00'a\x02\x00a\x01\x00'a\x03\x00'\x99\x06\x00F'\x99\x05\x00'a\x02\x00\x99\x06\x00F'\x99\x07\x00'h\x04\x00\x9b\x00\x00`\x01\x00a\x04\x00\x83\x01\x00h\x05\x00a\x05\x00`\x02\x00a\x00\x00\x83\x01\x00S"
# 發現兩個函數bytecode的二進制是一樣的,操作是一樣的?
>>> enc.co_name
'encrypt'
>>> enc.co_names
('rotor', 'newrotor', 'encrypt')
>>> dec.co_name
'decrypt'
>>> dec.co_names
('rotor', 'newrotor', 'decrypt')
# 通過研究rotor的用法,猜測兩個函數的區別可能是在于rotor.newrotor(key).encrypt(data)和rotor.newrotor(key).decrypt(data)

所以現在的問題就在于,key是怎么來的,然后就開始了手擼CPython bytecode

>>> dis.disassemble_string(dec.co_code)
          0 <153>               1
          3 BUILD_SET           1
          6 <153>               2
          9 BUILD_SET           2
         12 <153>               3
         15 BUILD_SET           3
         18 STORE_GLOBAL        1 (1)
         21 <153>               4
         24 PRINT_EXPR     
         25 <153>               5
         28 <39>           
         29 STORE_GLOBAL        2 (2)
         32 STORE_GLOBAL        1 (1)
         35 <39>           
         36 STORE_GLOBAL        3 (3)
         39 <39>           
         40 <153>               6
         43 PRINT_EXPR     
         44 <39>           
         45 <153>               5
         48 <39>           
         49 STORE_GLOBAL        2 (2)
         52 <153>               6
         55 PRINT_EXPR     
         56 <39>           
         57 <153>               7
         60 <39>           
         61 BUILD_SET           4
         64 <155>               0
         67 DELETE_ATTR         1 (1)
         70 STORE_GLOBAL        4 (4)
         73 CALL_FUNCTION       1
         76 BUILD_SET           5
         79 STORE_GLOBAL        5 (5)
         82 DELETE_ATTR         2 (2)
         85 STORE_GLOBAL        0 (0)
         88 CALL_FUNCTION       1
         91 RETURN_VALUE   

右邊的數字為操作數,括號里的是注釋

因為題目啥信息也沒給我們。。。所以修bytecode只能靠猜

我們先假設這里所有的153為同一個操作符,同理所有的39也為同一個

先看第一部分

          0 <153>               1
          3 BUILD_SET           1
          6 <153>               2
          9 BUILD_SET           2
         12 <153>               3
         15 BUILD_SET           3

這是最容易猜的地方,右邊的操作數為123,在看常量和局部變量的tuple,可以猜測是:

key_a = '!@#$%^&*'
key_b = 'abcdefgh'
key_c = '<>{}:"'

然后去上面給的參考文檔里,查出對應的bytecode

          0 LOAD_CONST           1
          3 STORE_FAST           1
          6 LOAD_CONST           2
          9 STORE_FAST           2
         12 LOAD_CONST           3
         15 STORE_FAST           3

再去opcode.h中查其對應的值進行替換

>>> dis.disassemble_string(dec.co_code.replace("\x99","\x64").replace("\x68","\x7d"))
          0 LOAD_CONST          1 (1)
          3 STORE_FAST          1 (1)
          6 LOAD_CONST          2 (2)
          9 STORE_FAST          2 (2)
         12 LOAD_CONST          3 (3)
         15 STORE_FAST          3 (3)
         18 STORE_GLOBAL        1 (1)
         21 LOAD_CONST          4 (4)
         24 PRINT_EXPR     
         25 LOAD_CONST          5 (5)
         28 <39>           
         29 STORE_GLOBAL        2 (2)
         32 STORE_GLOBAL        1 (1)
         35 <39>           
         36 STORE_GLOBAL        3 (3)
         39 <39>           
         40 LOAD_CONST          6 (6)
         43 PRINT_EXPR     
         44 <39>           
         45 LOAD_CONST          5 (5)
         48 <39>           
         49 STORE_GLOBAL        2 (2)
         52 LOAD_CONST          6 (6)
         55 PRINT_EXPR     
         56 <39>           
         57 LOAD_CONST          7 (7)
         60 <39>           
         61 STORE_FAST          4 (4)
         64 <155>               0
         67 DELETE_ATTR         1 (1)
         70 STORE_GLOBAL        4 (4)
         73 CALL_FUNCTION       1
         76 STORE_FAST          5 (5)
         79 STORE_GLOBAL        5 (5)
         82 DELETE_ATTR         2 (2)
         85 STORE_GLOBAL        0 (0)
         88 CALL_FUNCTION       1
         91 RETURN_VALUE 

繼續,發現肛不動了。。。。然后從底下開始肛

         64 <155>               0
         67 DELETE_ATTR         1 (1)
         70 STORE_GLOBAL        4 (4)
         73 CALL_FUNCTION       1
         76 STORE_FAST          5 (5)
         79 STORE_GLOBAL        5 (5)
         82 DELETE_ATTR         2 (2)
         85 STORE_GLOBAL        0 (0)
         88 CALL_FUNCTION       1
         91 RETURN_VALUE

根據之前得到的結論,可以猜測這里的代碼是:

    xxx = rotor.newrotor(secret)
    return xxx.decrypt(data)

所以猜測上面的操作數,0指的是局部變量data,1指的是全局變量newrotor, 2猜測可能是全局變量decrypt, 4指的是局部變量secret, 5指的是局部變量rot,<155>的0可能指的是全局變量rotor

然后查找bytecode,改成:

         64 LOAD_GLOBAL         0
         67 LOAD_ATTR           1 (1)
         70 LOAD_FAST           4 (4)
         73 CALL_FUNCTION       1
         76 STORE_FAST          5 (5)
         79 LOAD_FAST           5 (5)
         82 LOAD_ATTR           2 (2)
         85 LOAD_FAST           0 (0)
         88 CALL_FUNCTION       1
         91 RETURN_VALUE

發現,合情合理,使人姓胡.....

然后和上面一樣,整體替換bytecode:

>>> dis.disassemble_string(dec.co_code.replace("\x99","\x64").replace("\x68","\x7d").replace("\x61","\x7c").replace("\x60","\x6a").replace("\x9b","\x74"))
          0 LOAD_CONST          1 (1)
          3 STORE_FAST          1 (1)
          6 LOAD_CONST          2 (2)
          9 STORE_FAST          2 (2)
         12 LOAD_CONST          3 (3)
         15 STORE_FAST          3 (3)
         18 LOAD_FAST           1 (1)
         21 LOAD_CONST          4 (4)
         24 PRINT_EXPR     
         25 LOAD_CONST          5 (5)
         28 <39>           
         29 LOAD_FAST           2 (2)
         32 LOAD_FAST           1 (1)
         35 <39>           
         36 LOAD_FAST           3 (3)
         39 <39>           
         40 LOAD_CONST          6 (6)
         43 PRINT_EXPR     
         44 <39>           
         45 LOAD_CONST          5 (5)
         48 <39>           
         49 LOAD_FAST           2 (2)
         52 LOAD_CONST          6 (6)
         55 PRINT_EXPR     
         56 <39>           
         57 LOAD_CONST          7 (7)
         60 <39>           
         61 STORE_FAST          4 (4)
         64 LOAD_GLOBAL         0 (0)
         67 LOAD_ATTR           1 (1)
         70 LOAD_FAST           4 (4)
         73 CALL_FUNCTION       1
         76 STORE_FAST          5 (5)
         79 LOAD_FAST           5 (5)
         82 LOAD_ATTR           2 (2)
         85 LOAD_FAST           0 (0)
         88 CALL_FUNCTION       1
         91 RETURN_VALUE
         18 LOAD_FAST           1 (1)
         21 LOAD_CONST          4 (4)
         24 PRINT_EXPR     
         25 LOAD_CONST          5 (5)
         28 <39>           
         29 LOAD_FAST           2 (2)
         32 LOAD_FAST           1 (1)
         35 <39>           
         36 LOAD_FAST           3 (3)
         39 <39>           
         40 LOAD_CONST          6 (6)
         43 PRINT_EXPR     
         44 <39>           
         45 LOAD_CONST          5 (5)
         48 <39>           
         49 LOAD_FAST           2 (2)
         52 LOAD_CONST          6 (6)
         55 PRINT_EXPR     
         56 <39>           
         57 LOAD_CONST          7 (7)
         60 <39>           
         61 STORE_FAST          4 (4)

猜測這部分就是局部變量secret計算的方法,最后一句STORE_FAST 4,猜測就是把上面計算后的值儲存到secret

整體看看,發現主要就剩<39>PRINT_EXPR不通順了。。。然后他們都是沒操作數的,所以排除了調用函數,調用屬性之類的

之后聯想到最開頭對key_a, key_b, key_c的賦值,然后目前的bytecode中沒有任何運算操作

再來看這部分

         18 LOAD_FAST           1 ("!@#$%^&*")
         21 LOAD_CONST          4 (4)
         24 PRINT_EXPR

所以猜測PRINT_EXPR, 是字符串和整型之間的操作運算

         29 LOAD_FAST           2 ("abcdefgh")
         32 LOAD_FAST           1 ("!@#$%^&*")
         35 <39>

這里猜測<39>是字符串和字符串之間的操作運算

到這里,我們來想想,整型和字符串之間的操作有啥?

>>> "a"*3
>>> "aaa"[1]
>>> "aaa"[:1]
>>> "aaa"[1:]

字符串和字符串之間呢?

>>> "aaa" + "bbb"                 # 我只想到了這一個

從上面可以猜測出,<39>可能是字符串拼接的操作,然后PRINT_EXPR需要一個一個去試,現在我們可以還原出decrypt函數了:

# import rotor
def decrypt(data):
    key_a = "!@#$%^&*"
    key_b = "abcdefgh"
    key_c = '<>{}:"'
    secret=key_a*4 + "|" + (key_b+key_a+key_c)*2 + "|" + key_b*2 + "EOF"
    # secret=key_a[4] + "|" + (key_b+key_a+key_c)[2] + "|" + key_b[2] + "EOF"
    # secret=key_a[4:] + "|" + (key_b+key_a+key_c)[2:] + "|" + key_b[2:] + "EOF"
    # secret=key_a[:4] + "|" + (key_b+key_a+key_c)[:2] + "|" + key_b[:2] + "EOF"
    rot = rotor.newrotor(secret)
    return rot.decrypt(data)

簡直難受.......


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/443/