從一道CTF題學習python位元組碼到原始碼逆向

2022-12-25 21:00:45

概述:

該題來源為2022愛春秋冬季賽ezpython,難度不是很大剛好適合我這樣的萌新入門

題目:

 3           0 LOAD_CONST               1 (204)
              3 LOAD_CONST               2 (141)
              6 LOAD_CONST               3 (44)
              9 LOAD_CONST               4 (236)
             12 LOAD_CONST               5 (111)
             15 LOAD_CONST               6 (140)
             18 LOAD_CONST               6 (140)
             21 LOAD_CONST               7 (76)
             24 LOAD_CONST               3 (44)
             27 LOAD_CONST               8 (172)
             30 LOAD_CONST               9 (7)
             33 LOAD_CONST               9 (7)
             36 LOAD_CONST              10 (39)
             39 LOAD_CONST              11 (165)
             42 LOAD_CONST              12 (70)
             45 LOAD_CONST               9 (7)
             48 LOAD_CONST              10 (39)
             51 LOAD_CONST              13 (166)
             54 LOAD_CONST              11 (165)
             57 LOAD_CONST              14 (134)
             60 LOAD_CONST              14 (134)
             63 LOAD_CONST               6 (140)
             66 LOAD_CONST               1 (204)
             69 LOAD_CONST              11 (165)
             72 LOAD_CONST               9 (7)
             75 LOAD_CONST              10 (39)
             78 LOAD_CONST              15 (230)
             81 LOAD_CONST               6 (140)
             84 LOAD_CONST              11 (165)
             87 LOAD_CONST              12 (70)
             90 LOAD_CONST               3 (44)
             93 LOAD_CONST               8 (172)
             96 LOAD_CONST              16 (102)
             99 LOAD_CONST              17 (6)
            102 LOAD_CONST               6 (140)
            105 LOAD_CONST               1 (204)
            108 LOAD_CONST              15 (230)
            111 LOAD_CONST              15 (230)
            114 LOAD_CONST               7 (76)
            117 LOAD_CONST              18 (198)
            120 LOAD_CONST              19 (38)
            123 LOAD_CONST              20 (175)
            126 BUILD_LIST              42
            129 STORE_FAST               0 (flag)

  4         132 SETUP_LOOP              54 (to 189)
            135 LOAD_GLOBAL              0 (range)
            138 LOAD_CONST              21 (42)
            141 CALL_FUNCTION            1
            144 GET_ITER            
        >>  145 FOR_ITER                40 (to 188)
            148 STORE_FAST               1 (i)

  5         151 LOAD_FAST                0 (flag)
            154 LOAD_FAST                1 (i)
            157 BINARY_SUBSCR                           //讀取列表中的值
            158 LOAD_CONST              22 (5)
            161 BINARY_RSHIFT       
            162 LOAD_FAST                0 (flag)
            165 LOAD_FAST                1 (i)
            168 BINARY_SUBSCR       
            169 LOAD_CONST              23 (3)
            172 BINARY_LSHIFT       
            173 BINARY_OR           
            174 LOAD_CONST              24 (255)
            177 BINARY_AND          
            178 LOAD_FAST                0 (flag)
            181 LOAD_FAST                1 (i)
            184 STORE_SUBSCR        
            185 JUMP_ABSOLUTE          145
        >>  188 POP_BLOCK           
        >>  189 LOAD_CONST               0 (None)
            192 RETURN_VALUE        

分析:

先了解python的位元組碼結構,如下:

原始碼行號 | 指令在函數中的偏移 | 指令符號 | 指令引數 | 實際引數值

第一個為原始碼行號,從上述題目中我們可以瞭解到,本題只有5行原始碼,但是隻給了從第3行開始的位元組碼

第二個指令在函數中的偏移和第四個指令引數對於做題來說不是太過重要,我們在做題的時候主要還是看指令符號和實際引數值

在第3行的位元組碼指令符號中,有LOAD_CONST ,BUILD_LIST ,STORE_FAST 三種型別

LOAD_CONST 載入常數,通常為整數值

BUILD_LIST 建立一個列表

STORE_FAST 一般用於儲存值到區域性變數

理解了上述指令符號含義後,我們來看題目中第3行的位元組碼,先看第一個

 

 

 分析後知道載入了一個常數,數值為204,以此向後分析發現,大部分一樣,至此我們知道載入瞭如下常數

204, 141, 44, 236, 111, 140, 140, 76, 44, 172, 7, 7, 39, 165, 70, 7, 39, 166, 165, 134, 134, 140, 204, 165, 7, 39, 230, 140, 165, 70, 44, 172, 102, 6, 140, 204, 230, 230, 76, 198, 38, 175

然後我們來這裡

 

 

 這裡建立一個列表,長度為42,你也可以通過數前面載入的常數發現,剛好是42個常數,然後儲存值到變數flag中,那這裡儲存的值是什麼呢?其實就是位於該指令符號前面的所有的值。

綜上我們就得到python原始碼的第3行原始碼

flag = [204, 141, 44, 236, 111, 140, 140, 76, 44, 172, 7, 7, 39, 165, 70, 7, 39, 166, 165, 134, 134, 140, 204, 165, 7, 39, 230, 140, 165, 70, 44, 172, 102, 6, 140, 204, 230, 230, 76, 198, 38, 175]

接著分析第4行位元組碼:

 

 

 這裡的指令符號有:

SETUP_LOOP,用於開始迴圈,括號裡的189表示迴圈退出點(位元組碼結構中的第二個指令在函數中偏移)

LOAD_GLOBAL,用來載入全域性變數,包括指定函數名,類名,模組名等全域性符號

LOAD_CONST ,(請看上文)

CALL_FUNCTION,用來表示前面載入全域性變數的引數個數

GET_ITER,FOR_ITER ,獲取引數,開始迭代。這兩個不需要過多理解,屬於for-in結構特有的,它們通常同時出現。

STORE_FAST,(請看上文)

綜上分析:開始迴圈,載入了一個全域性函數range,載入了一個常數42,range函數的引數為1,執行迴圈的變數i,得到原始碼如下:

for i in range(42):

繼續分析第5行位元組碼:

 

 

 這裡出現了前面沒出現過的位元組碼有:

BINARY_SUBSCR ,讀取迭代器中某個下標的值

BINARY_RSHIFT,進行右移運算

BINARY_LSHIFT ,左移運算

BINARY_OR,或運算

BINARY_AND ,與運算

STORE_SUBSCR ,修改迭代器中某個下標的值

JUMP_ABSOLUTE ,回到迴圈起點

RETURN_VALUE  ,函數結束標誌

POP_BLOCK,特有的,不用特別理解,對轉換回原始碼不重要,通常和LOAD_CONST               0 (None)一起出現。

整體理解:先讀取了flag[i]的值,然後載入一個常數5進行右移,得到如下:

flag[i] >> 5

然後繼續讀取flag[i]的值,載入一個常數3進行左移,得到如下:

flag[i] << 3

然後進行或運算,得到如下:

(flag[i] >> 5) | (flag[i] << 3)

載入一個常數255,接著進行與運算,得到如下:

(flag[i] >> 5) | (flag[i] << 3) & 255

最後存入flag[i],得到如下:

flag[i] = (flag[i] >> 5) | (flag[i] << 3) & 255

注意位元組碼從上到下的順序和原始碼運算的優先順序順序。

最終從全部的位元組碼翻譯過來的原始碼如下:

flag = [204, 141, 44, 236, 111, 140, 140, 76, 44, 172, 7, 7, 39, 165, 70, 7, 39, 166, 165, 134, 134, 140, 204, 165, 7, 39, 230, 140, 165, 70, 44, 172, 102, 6, 140, 204, 230, 230, 76, 198, 38, 175]
for i in range(42):
    flag[i] = (flag[i] >> 5) | (flag[i] << 3) & 255

最終解題指令碼如下:

flag = [204, 141, 44, 236, 111, 140, 140, 76, 44, 172, 7, 7, 39, 165, 70, 7, 39, 166, 165, 134, 134, 140, 204, 165, 7, 39, 230, 140, 165, 70, 44, 172, 102, 6, 140, 204, 230, 230, 76, 198, 38, 175]

for i in range(42):
    flag[i] = (flag[i] >> 5) | (flag[i] << 3) & 255

str_flag = ''
for i in flag:
    str_flag += chr(i)
print(str_flag)

得到flag:flag{ddbae889-2895-44df-897d-2ae30df77b61}

參考連結:

死磕python位元組碼-手工還原python原始碼 - 知乎 (zhihu.com)

python 位元組碼死磕 - 大步向前blue - 部落格園 (cnblogs.com)