iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0

前面幾天我們花了不少時間在 Reverse Engineering。

從:

Assembly
Static Analysis
Dynamic Analysis
GDB
Control Flow
Xref
Crackme

一路到昨天實際分析一個稍微完整一點的 Reverse Challenge。

而從今天開始,我們要正式進入這個系列的另外一半:

Pwn

如果說 Reverse Engineering 的目標是:

理解 Binary 到底在做什麼。

那 Pwn 更像是:

找到 Binary 裡面的漏洞,並利用它改變原本的程式行為。

而 Binary Exploitation 中最經典的起點,就是:

Buffer Overflow

0x00 Pwn 是什麼?

在 CTF 裡面,Pwn 通常指的是:

Binary Exploitation

也就是針對 Binary 中的漏洞進行利用。

例如一個正常的程式原本只能:

Input
  ↓
Program Logic
  ↓
Output

但如果程式存在漏洞,我們可能可以讓流程變成:

Input
  ↓
Memory Corruption
  ↓
改變 Program Control Flow
  ↓
執行原本不應該執行的程式碼

例如:

取得 Flag
呼叫某個 Function
控制 RIP
執行 Shellcode
ROP
取得 Shell

這些都是之後會慢慢碰到的東西。


0x01 Reverse 跟 Pwn 有什麼差別?

其實 Reverse 和 Pwn 並不是完全分開的兩個東西。

例如我們前面分析 Crackme 時會問:

Password 在哪裡檢查?

哪個 Function 負責驗證?

Success Path 在哪裡?

程式什麼時候會 print_flag()?

這些都是 Reverse。

而 Pwn 則會進一步問:

有沒有辦法不照原本的邏輯走?

有沒有 Memory Corruption?

能不能控制 Return Address?

能不能直接跳到 print_flag()?

所以很多 Pwn 題目的流程其實是:

Reverse
   ↓
理解程式
   ↓
找到 Vulnerability
   ↓
Exploit

也就是說,前面學的 Reverse 並沒有白學。

反而是接下來做 Pwn 很重要的基礎。


0x02 第一個 Vulnerable Program

先來看一個很簡單的 C Program:

#include <stdio.h>
#include <unistd.h>

void vuln() {
    char buffer[32];

    puts("Input:");

    read(0, buffer, 128);
}

int main() {
    vuln();

    return 0;
}

乍看之下程式非常簡單:

建立一個 buffer
↓
讀取使用者輸入
↓
程式結束

但仔細看:

char buffer[32];

代表:

buffer 只有 32 bytes

可是下面卻:

read(0, buffer, 128);

允許使用者最多輸入:

128 bytes

所以就出現一個問題:

如果我輸入超過 32 bytes,剩下的資料會去哪裡?

這就是今天的重點。


0x03 Buffer Overflow

Buffer Overflow 中文通常翻成:

緩衝區溢位

假設我們有:

char buffer[8];

Memory 原本只準備:

buffer

+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+

              8 bytes

如果我們輸入:

AAAAAAAA

剛好:

8 bytes

沒有問題。

但如果輸入:

AAAAAAAAAAAAAAAA

也就是:

16 bytes

那前面 8 bytes 會進入 buffer。

剩下的資料就可能繼續往旁邊的 Memory 寫。

概念上就會變成:

Buffer          Other Data
────────        ──────────

AAAAAAAA        AAAAAAAA
^^^^^^^^        ^^^^^^^^
合法範圍         Overflow

這就是:

Buffer Overflow

0x04 問題是:旁邊到底有什麼?

如果只是多寫幾個 A,好像也沒什麼。

真正的問題在於:

Stack 上除了 Local Variable,還有其他很重要的資料。

前面介紹 Stack 的時候有提到 Function Call。

簡化來看的話,一個 Stack Frame 可能類似:

High Address

+------------------+
|  Return Address  |
+------------------+
|    Saved RBP     |
+------------------+
|                  |
|     buffer       |
|                  |
+------------------+

Low Address

其中非常重要的一個東西就是:

Return Address

它會告訴 CPU:

這個 Function 執行結束之後,要回去哪裡繼續執行?

假設:

main() {
    vuln();
    puts("Done");
}

流程可能是:

main()
  ↓
vuln()
  ↓
ret
  ↓
回到 main()
  ↓
puts("Done")

CPU 必須知道:

vuln() 結束後要回 main() 的哪個位置

這個位置就是 Return Address。


0x05 如果 Return Address 被蓋掉呢?

回到剛才的程式:

char buffer[32];

read(0, buffer, 128);

如果我們一直輸入:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...

資料可能一路超過:

buffer

繼續覆蓋:

Saved RBP

甚至最後碰到:

Return Address

概念上變成:

+------------------+
| AAAAAAAAAAAAAAAA | ← Return Address
+------------------+
| AAAAAAAAAAAAAAAA | ← Saved RBP
+------------------+
| AAAAAAAAAAAAAAAA |
| AAAAAAAAAAAAAAAA | ← buffer
+------------------+

這時候 Function 執行:

ret

CPU 原本應該拿到:

正常的 Return Address

結果現在拿到的卻可能是:

0x4141414141414141

為什麼是 41?

因為 ASCII:

'A' = 0x41

所以:

AAAAAAAA

在 Memory 裡面就是:

41 41 41 41 41 41 41 41

0x06 來讓程式 Crash

先編譯剛才的程式。

這邊為了學習 Buffer Overflow,先把部分保護關掉:

gcc chall.c -o chall \
    -fno-stack-protector \
    -no-pie \
    -g

接著正常執行:

./chall

輸入:

AAAA

程式正常結束。

但如果一次塞很多資料:

python3 -c 'print("A"*100)' | ./chall

就可能看到:

Segmentation fault

也就是:

程式 Crash 了

這時候我們已經成功做到第一件事:

使用我們可以控制的 Input,破壞程式的 Memory。


0x07 用 GDB 看看發生什麼事

前面學過的 GDB 現在就派上用場了。

先產生一個 Input:

python3 -c 'print("A"*100)' > input

接著:

gdb ./chall

執行:

run < input

程式可能會在 Function Return 附近 Crash。

這時可以看看 Stack:

x/10gx $rsp

可能會看到類似:

0x7fffffffe000: 0x4141414141414141
0x7fffffffe008: 0x4141414141414141
0x7fffffffe010: 0x4141414141414141

也就是 Stack 上出現大量:

0x4141414141414141

這代表:

我們輸入的 A

已經不只存在 buffer 裡面了。

而是一路 Overflow 到其他 Stack Data。


0x08 Crash 不等於 Exploit

這邊有一個很重要的觀念。

我們現在做到的是:

Buffer Overflow
↓
Memory Corruption
↓
Crash

但:

讓程式 Crash 跟真正完成 Exploit 是兩回事。

如果今天只能:

AAAAAAAAAAAA
↓
Segmentation Fault

那頂多只能算找到一個 Crash。

我們真正想做到的是:

Input
  ↓
Buffer Overflow
  ↓
精確覆蓋 Return Address
  ↓
控制 RIP
  ↓
讓程式跳到我們指定的位置

也就是從:

Crash

進一步變成:

Control

這才是 Binary Exploitation 真正有趣的地方。


0x09 為什麼要控制 RIP?

在 x86-64 中:

RIP

代表目前正在執行的 Instruction Address。

例如:

RIP = 0x401156

代表 CPU 現在正在執行:

0x401156

附近的 Instruction。

如果我們有辦法控制:

RIP

那就代表:

我們可能可以決定 CPU 下一步去哪裡執行。

例如 Binary 裡面剛好存在:

void win() {
    system("/bin/sh");
}

正常情況下程式可能永遠不會呼叫:

win()

但如果 win() 位址是:

0x401176

那如果我們可以把 Return Address 改成:

0x401176

Function ret 時:

ret
 ↓
0x401176
 ↓
win()

程式就可能直接跑進 win()。

這也是之後會介紹的經典:

ret2win

0x0A 從 Reverse 到 Pwn

到這裡應該可以看到 Reverse 和 Pwn 是怎麼接起來的。

Reverse 時,我們會找:

win() 在哪裡?

Function Address 是多少?

程式有哪些 Branch?

Return Address 怎麼運作?

Pwn 則會問:

我能不能利用漏洞:

Return Address
      ↓
改成
      ↓
win()

所以:

Reverse
   ↓
知道程式可以去哪裡

Pwn
   ↓
想辦法讓程式真的去那裡

這也是為什麼 Binary Exploitation 通常離不開 Reverse Engineering。


0x0B 現代程式當然沒有這麼簡單

如果 Buffer Overflow 只需要:

塞爆 Buffer
↓
改 Return Address
↓
跳到 Shellcode

那事情就太簡單了。

現代 Binary 通常還會有各種 Exploit Mitigation:

Stack Canary
NX
PIE
ASLR
RELRO

可以先使用:

checksec --file=chall

查看 Binary Protection。

不同 Protection 會限制不同的 Exploit 手法。

例如:

Canary
→ 防止簡單 Stack Overflow

NX
→ Stack 通常不能直接執行程式碼

PIE + ASLR
→ Address 不再固定

RELRO
→ 保護部分 GOT 結構

這些東西之後都會慢慢介紹。

現在第一階段先專注在:

Stack
Buffer Overflow
Return Address
RIP

就好。


0x0C Pwn 的基本思考流程

之後拿到一題簡單的 Pwn Challenge,可以先按照這個方向:

Binary
  ↓
file / checksec
  ↓
執行程式
  ↓
Reverse
  ↓
找到 Input
  ↓
找到 Vulnerability
  ↓
嘗試 Crash
  ↓
GDB
  ↓
觀察 Stack
  ↓
找到 Offset
  ↓
控制 RIP
  ↓
Exploit

今天我們目前做到:

找到 Vulnerability
↓
成功 Crash

下一個問題就是:

到底要輸入幾個 Byte,才會剛好碰到 Return Address?

總不能每次都:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...

然後慢慢猜。

所以接下來就要開始找:

Offset

0x0D 小結

今天正式從 Reverse Engineering 進入 Pwn。

首先認識了:

Pwn
Binary Exploitation
Buffer Overflow
Stack Overflow
Return Address
RIP
Memory Corruption

最重要的概念可以整理成:

固定大小的 Buffer
        ↓
程式卻允許輸入過多資料
        ↓
Buffer Overflow
        ↓
覆蓋 Stack Data
        ↓
Return Address
        ↓
Control Flow

但今天我們只是成功讓:

Program Crash

真正的 Exploit 還需要做到:

Crash
 ↓
找到 Offset
 ↓
控制 Return Address
 ↓
控制 RIP

所以下一篇就來實際處理:

Stack Buffer Overflow
+
Cyclic Pattern
+
Offset
+
控制 RIP

也就是第一次真正開始把:

Buffer Overflow

變成:

Binary Exploitation

上一篇
Day15|Reverse 實戰 & 結論
下一篇
Day17|Stack Buffer Overflow
系列文
從零開始の Binary CTF:30 天系統化學習 Rev & Pwn 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言