> ## Content Index
> Fetch the complete content index at: https://taiwanding.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Buffer Overflow 沒你想的可怕：從「杯子裝太滿」講到拿 Shell
- URL: https://taiwanding.com/buffer-overflow-mei-ni-xiang-de-ke-pa-cong-bei-zi-zhuang-tai-man-jiang-dao-na-shell/
- Published: 2026-07-16T10:44:30.000Z
- Updated: 2026-07-20T07:18:40.000Z
- Author: Kevin Chen
- Tags: #buffer overflow, #MBPTL

*這篇是上一篇 MBPTL 通關紀錄的延伸番外。上一篇打到最後一關（Phase 7，那個 31337 的 binary），我只放了「送一串 A 加三個地址就拿到 shell」的結果，沒有解釋為什麼。這篇就專門把那串看起來像黑魔法的東西，用最白話的方式拆給你看——不需要任何 pwn 基礎，看完你會發現它其實沒那麼可怕。*

---

## 先講一句話：這個漏洞的本質是「寫太多，滿出來」

你一定遇過網頁表單「最多輸入 20 字」的限制。那個限制不是為了刁難你——是因為程式在背後準備了一塊「剛好 20 格」的空間放你的字。如果程式忘了檢查長度，而你硬塞了 200 個字進去，會發生什麼事？

答案是：多出來的字不會憑空消失，它們會**往旁邊的記憶體溢出去**，覆蓋掉本來放在那裡的東西。這就是 buffer overflow（緩衝區溢位）——名字聽起來很硬，但概念就是「杯子裝太滿，水漫到桌上」。

問題在於：漫出去的水，剛好淹到了一個很關鍵的東西。

## 圖解一：正常輸入 vs 塞爆

先看程式在記憶體裡怎麼擺放。它準備了一排格子（叫 buffer）放你的輸入，而在這排格子的「正上方」，藏著一個叫「回程地址」的東西——記著程式這段做完之後，下一步該回去哪裡繼續。

Buffer overflow 記憶體示意圖正常輸入只填滿 buffer；超長輸入淹過 buffer、蓋掉程式的返回位址。 正常輸入 超長輸入（塞爆） 回程地址（return） saved RBP buffer（放你的輸入）128 格空位 輸入「Kevin」→ 只填幾格，安全 回程地址 ← 被蓋掉！ AAAA…（淹過來） AAAA…AAAA128 格全塞滿 輸入 140 個 A → 滿出來、蓋到上面 往上淹 白話比喻 buffer 就像一排 128 格的信箱，最上面那格寫著「事情辦完後回哪裡」。 你只該寫進信箱格子裡，但沒人擋你——你狂寫，一路滿出來， 把「回哪裡」那格也覆蓋成你要的地址。程式辦完事，就照你寫的地址走。 

左邊是正常情況：你輸入「Kevin」，只填了幾格，上面的「回程地址」安然無恙。右邊是攻擊：你輸入 140 個 A，把 128 格填滿之後還有剩，多出來的 A 一路往上淹，**把「回程地址」那格也蓋掉了**。

（技術細節一句話：這個 lab 的程式用了一個叫 `gets()` 的老函式讀取輸入——它是出了名的危險，因為它*完全不檢查長度*，你要塞多長它就收多長。現代程式早就不該用它，但拿來當教學範例剛剛好。）

## 圖解二：蓋掉「回程地址」，等於接管方向盤

光是蓋掉一格資料，聽起來好像也還好？關鍵在於「回程地址」這格**不是普通資料，是程式的下一步指令**。

程式每做完一段工作，都會回頭看這一格，照上面寫的位址跳過去繼續執行。既然我們剛剛把它蓋成了自己想要的地址——程式就會乖乖跳去我們指定的地方。

劫持回程地址程式執行完後會照回程地址那格跳過去；蓋掉它就等於指定程式下一步去哪。 程式辦完事，看這一格 回程地址被我們蓋成一個地址 AAAA…（墊料） buffer 塞滿的 A 跳去 我們指定的地方程式乖乖跳過來執行 關鍵轉折 程式每做完一段，都會回頭看「回程地址」那格，照上面寫的位址跳過去繼續。 既然我們剛剛把那格蓋成自己想要的地址——程式就會跳去我們指定的地方。 從「蓋掉一格」變成「控制程式下一步」，這就是拿到主控權的瞬間。 

這一步是整個攻擊的靈魂：**從「我能覆蓋一格記憶體」升級成「我能決定程式下一步跳去哪」**。方向盤到手了。

## 圖解三：跳去哪才有用？用現成零件拼出一個 shell

方向盤到手了，但新問題來了——**跳去哪**才能拿到主控權？我們想要的是一個 shell（能下指令的命令列），但程式裡又沒有一段寫好的「請給我一個 shell」。

好消息是：程式裡有**零件**。這個 lab 的 binary 裡剛好躺著兩樣東西——一個能「執行指令」的函式（叫 `system`），跟一段寫著 `/bin/sh`（就是 shell 的名字）的文字。我們要做的，是把這兩個零件**組起來**，變成「執行 `/bin/sh`」。這個手法叫 **ret2libc**。

組裝需要三步。這裡有個規則要先知道：在這種電腦上，要叫一個函式「處理某樣東西」，得先把那樣東西的地址放進一個叫 `rdi` 的暫存器（可以想成「函式的收件匣」）。所以順序是：先把 `/bin/sh` 放進收件匣，再叫 `system` 動工。

ret2libc 三步驟用程式現有的零件，分三步組出 system 開啟 /bin/sh。 用現成零件拼出「執行 /bin/sh」 ① 跳 gadget借一小段現成碼 ② 放進收件匣收件匣 = "/bin/sh" ③ 叫 system 動工開啟 shell 這三步，其實就是疊在堆疊上的三個地址 ③ system 的地址 ② "/bin/sh" 的地址 ① gadget 的地址 136 個 A（墊料） 墊料把洞填到剛好，接著三個地址被「回程 → gadget → system」依序串起來， 程式像被牽著走一樣，一步步自己走進「執行 /bin/sh」。 

那個「① gadget」是什麼？它是我們從程式**現有的機器碼裡挖出來的一小段碎片**，剛好能做「把一個值放進收件匣，然後繼續」這件事。我們不能無中生有寫程式碼進去（後面會講為什麼），但我們可以**借程式自己身上的零件**來用——這就是這門技術聰明的地方，全程沒有夾帶任何一行自己的程式碼，純粹是把別人的積木重新排列。

而下半張圖揭曉了那串神秘 payload 的真面目：它就是**「一堆墊料 + 三個地址」**依序疊在記憶體上。墊料負責把洞填到剛好頂到「回程地址」，然後三個地址一個接一個，把程式像牽線木偶一樣，一步步牽進 `system("/bin/sh")`。

回到上一篇那串「看不懂的 magic」，現在對照起來就清楚了：

```
payload = b'A' * 136        # 墊料：把洞填滿，剛好頂到回程地址
        + p64(gadget)       # ① 蓋掉回程地址 → 跳去 gadget
        + p64(binsh)        # ② 被 gadget 放進收件匣的值 = "/bin/sh"
        + p64(system)       # ③ 最後跳去 system，動工
```

那個「136」不是亂數，是精算出來的——buffer 有 128 格，再加上中間 8 格，剛好 136 格之後就是「回程地址」。多一格少一格都會失敗。

## 那為什麼現代程式大多不會這樣被打？三道鎖

看到這裡你可能會想：這麼簡單，那全世界的程式不是都完蛋了？其實沒有——現代編譯器早就裝了三道防禦，讓上面每一步都變得極難。這個 lab 之所以能一路暢通，是因為它**刻意把三道鎖全關了**當教材。

三道防禦機制現代程式有三道鎖防 buffer overflow：金絲雀、位址隨機化、堆疊不可執行。 現代程式的三道鎖（這關全沒上） 金絲雀Stack Canary 回程地址前放一個隨機暗號，被改到就當場中止 位址隨機化PIE / ASLR 每次執行位址都變，你寫不死零件的地址 堆疊不可執行NX 不能把惡意碼塞進去直接跑（所以我們用「借」的） 三道鎖任何一道上了，難度就跳級——這關剛好三道全關，才會是入門教材。 

- **金絲雀（Stack Canary）**：在「回程地址」前面偷偷放一個隨機暗號。程式跳回去之前會先檢查暗號還在不在——你溢位時一定會蓋到它，暗號一對不上，程式當場自爆，你根本來不及劫持。名字來自礦工帶金絲雀下礦坑：鳥先死，人就知道有毒氣。
- **位址隨機化（PIE / ASLR）**：程式每次執行，零件擺放的位置都*隨機洗牌*。上面 payload 裡那三個「地址」你根本寫不死——這次是這個位置，下次就變了。（如果你有玩過 ARM 漏洞，這個你應該不陌生，繞過 ASLR 是進階題的家常便飯。）
- **堆疊不可執行（NX）**：早年的攻擊是直接把惡意程式碼塞進 buffer 裡跑。NX 這道鎖規定「這塊記憶體只能放資料、不准當程式執行」。*這也正是為什麼我們不自己塞程式碼、而是去「借」程式現有的零件*——ret2libc 這套手法就是為了繞過 NX 而生的。

這三道鎖只要上了任何一道，難度都會跳一整級。真實世界的程式通常三道全開，所以現代的 pwn 高手，大半功夫都花在「怎麼一道一道繞過這些鎖」——那是另一個深得多的坑了。

## 收尾

把整條線串起來，一句話：

> 程式忘了檢查長度 → 我塞爆 buffer、淹過去蓋掉「回程地址」 → 於是我能決定程式下一步跳去哪 → 我用它自己身上的零件，一步步把它牽進一個 shell。

回頭看上一篇那串 `b'A'*136 + 三個地址`，現在應該不再是黑魔法了——它就是「墊料填到剛好，再用三個地址把程式牽著走」。

如果你跟我一樣，以前看到 pwn 就直接跳過，希望這篇讓你覺得：它硬歸硬，但骨架其實很好懂。真正難的不是這個入門版，而是三道鎖全開之後怎麼繞——但那個，就留到你真的想跳進這個坑的那天再說吧。