iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0
Security

CTF 菜鳥修練日誌系列 第 24 篇

Day 24:【Reverse】Python 編譯檔逆向:.pyc 反編譯成原始碼

  • 分享至 

  • xImage
  •  

1. 前言與核心概念

Python 雖然是直譯式語言(Interpreted Language),但在執行 .py 檔案時,Python 解譯器(CPython)會先將原始碼編譯成位元組碼(Bytecode),並快取為 .pyc 檔案(通常放在 __pycache__ 資料夾中)以加速後續載入。

在 CTF 競賽的 Reverse 題目中,經常會拿到一個 .pyc 檔案,或是被打包成單一執行檔的 Python 程式(如 PyInstaller)。

雖然 .pyc 檔是二進位格式,但它保留了豐富的抽象語法樹(AST)與指令結構。透過反編譯器 (Decompiler),我們能輕鬆將位元組碼還原回人類可讀的高階 Python 原始碼!


2. Python 位元組碼與反編譯工具

不同的 Python 版本(如 3.8, 3.10, 3.11+)其位元組碼結構會有些許差異,常用解題工具如下:

  1. uncompyle6 / decompyle++ (pycdc):
    • uncompyle6:經典的 Python 反編譯器,對 Python 2.7 至 3.8 的支援度極佳。
    • pycdc (decompyle++):由 C++ 撰寫的強大反編譯工具,支援較新版本的 Python(如 3.10+)。
  2. pyinstxtractor:
    • 若題目提供的是用 PyInstaller 打包好的 .exe 或二進位檔,需先用此腳本解包出內含的 .pyc 檔案。

3. Python 實戰演練:從原始碼到反編譯挖寶

我們在 WSL 中親自建立一個含 Flag 的 Python 腳本,將其編譯為 .pyc,並使用 Python 內建的 dis 模組與反編譯邏輯來進行逆向還原!

步驟一:建立原始碼並編譯為 .pyc

在 WSL 終端機執行以下命令,生成 .pyc 編譯檔:

# 1. 建立包含 Flag 的範例 Python 腳本
cat << 'EOF' > target_script.py
def check_flag():
    secret_flag = "picoCTF{pyc_d3c0mp1l3_1s_s0_34sy_99f2a}"
    user_input = input("Enter password: ")
    if user_input == "python_master_2026":
        print(f"Correct! Flag: {secret_flag}")
    else:
        print("Wrong password!")

if __name__ == "__main__":
    check_flag()
EOF

# 2. 將 .py 檔編譯為 .pyc 位元組碼檔
python3 -m py_compile target_script.py

# 3. 複製編譯好的 .pyc 檔至當前目錄
cp __pycache__/target_script.cpython-*.pyc challenge.pyc

步驟二:使用 Python 反組譯模組 dis 檢視 Bytecode

若不使用第三方工具,Python 內建的 dis 模組可以將 .pyc 檔案反組譯為位元組碼指令(Opcode):

python3 -m dis challenge.pyc

在輸出結果中,你可以看到 LOAD_CONST 指令後直接帶出了字串常數 "picoCTF{pyc_d3c0mp1l3_1s_s0_34sy_99f2a}"!

步驟三:撰寫 Python 自動化 .pyc 常數提取腳本

在 CTF 中,即便不進行完整的程式碼結構還原,我們也能寫腳本直接剖析 .pyc 的 Code Object(co_consts),把裡面所有的字串常數硬挖出來:

# 創建 day24_pyc_reverse.py
cat << 'EOF' > day24_pyc_reverse.py
import marshal
import dis

def extract_consts_from_pyc(pyc_path):
    print(f"[*] 正在分析 .pyc 檔案: '{pyc_path}'")
    
    with open(pyc_path, "rb") as f:
        # 跳過 .pyc 標頭 (Header) 讀取 Code Object
        # Python 3.7+ 標頭長度通常為 16 bytes
        f.seek(16)
        code_obj = marshal.load(f)
        
    print("[*] 成功讀取 Code Object,開始提取字串常數 (co_consts)...")
    
    # 遞迴搜尋 Code Object 中的常數
    def search_consts(code):
        for const in code.co_consts:
            if isinstance(const, str) and "picoCTF" in const:
                print(f"[🎯] 成功挖到硬編碼 Flag: {const}")
            elif hasattr(const, 'co_consts'):
                search_consts(const)

    search_consts(code_obj)

if __name__ == "__main__":
    extract_consts_from_pyc("challenge.pyc")
EOF

python3 day24_pyc_reverse.py

4. 執行結果預期

在 WSL 終端機執行腳本後,即可直接解析 .pyc 檔案的內部架構,精準印出隱藏的 Flag:

[*] 正在分析 .pyc 檔案: 'challenge.pyc'
[*] 成功讀取 Code Object,開始提取字串常數 (co_consts)...
[🎯] 成功挖到硬編碼 Flag: picoCTF{pyc_d3c0mp1l3_1s_s0_34sy_99f2a}

https://ithelp.ithome.com.tw/upload/images/20261008/20184211RIEzCalCTj.png

5. 防禦與逆向對抗 (Anti-Decompile)

為了防止 .pyc 檔案被輕易反編譯還原,常見的保護手段包括:

  1. Bytecode 混淆 (Bytecode Obfuscation):使用 PyArmor 等工具對位元組碼進行加密與動態解密包裝。
  2. 修改 Header / Opcode:故意篡改 .pyc 檔案頭部的 Magic Number 或重構 Python 解譯器的 Opcode 對照表,使標準反編譯工具報錯崩潰。
  3. 編譯為 Native Code:使用 Cython 將 Python 程式碼先轉譯為 C 語言,再編譯成 .so 或 .pyd 二進位動態庫,大幅拉高逆向門檻。

上一篇
Day 23:【Reverse】逆向工程第一步:靜態分析與 strings 挖寶
下一篇
Day 25:【Reverse】神器 Ghidra 初體驗:C/C++ 執行檔的反組譯與 Decompile
系列文
CTF 菜鳥修練日誌 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言