Buffer Overflow 沒你想的可怕:從「杯子裝太滿」講到拿 Shell
這篇是上一篇 MBPTL 通關紀錄的延伸番外。上一篇打到最後一關(Phase 7,那個 31337 的 binary),我只放了「送一串 A 加三個地址就拿到 shell」的結果,沒有解釋為什麼。這篇就專門把那串看起來像黑魔法的東西,用最白話的方式拆給你看——不需要任何 pwn 基礎,看完你會發現它其實沒那麼可怕。
先講一句話:這個漏洞的本質是「寫太多,滿出來」
你一定遇過網頁表單「最多輸入 20 字」的限制。那個限制不是為了刁難你——是因為程式在背後準備了一塊「剛好 20 格」的空間放你的字。如果程式忘了檢查長度,而你硬塞了 200 個字進去,會發生什麼事?
答案是:多出來的字不會憑空消失,它們會往旁邊的記憶體溢出去,覆蓋掉本來放在那裡的東西。這就是 buffer overflow(緩衝區溢位)——名字聽起來很硬,但概念就是「杯子裝太滿,水漫到桌上」。
問題在於:漫出去的水,剛好淹到了一個很關鍵的東西。
圖解一:正常輸入 vs 塞爆
先看程式在記憶體裡怎麼擺放。它準備了一排格子(叫 buffer)放你的輸入,而在這排格子的「正上方」,藏著一個叫「回程地址」的東西——記著程式這段做完之後,下一步該回去哪裡繼續。
左邊是正常情況:你輸入「Kevin」,只填了幾格,上面的「回程地址」安然無恙。右邊是攻擊:你輸入 140 個 A,把 128 格填滿之後還有剩,多出來的 A 一路往上淹,把「回程地址」那格也蓋掉了。
(技術細節一句話:這個 lab 的程式用了一個叫 gets() 的老函式讀取輸入——它是出了名的危險,因為它完全不檢查長度,你要塞多長它就收多長。現代程式早就不該用它,但拿來當教學範例剛剛好。)
圖解二:蓋掉「回程地址」,等於接管方向盤
光是蓋掉一格資料,聽起來好像也還好?關鍵在於「回程地址」這格不是普通資料,是程式的下一步指令。
程式每做完一段工作,都會回頭看這一格,照上面寫的位址跳過去繼續執行。既然我們剛剛把它蓋成了自己想要的地址——程式就會乖乖跳去我們指定的地方。
這一步是整個攻擊的靈魂:從「我能覆蓋一格記憶體」升級成「我能決定程式下一步跳去哪」。方向盤到手了。
圖解三:跳去哪才有用?用現成零件拼出一個 shell
方向盤到手了,但新問題來了——跳去哪才能拿到主控權?我們想要的是一個 shell(能下指令的命令列),但程式裡又沒有一段寫好的「請給我一個 shell」。
好消息是:程式裡有零件。這個 lab 的 binary 裡剛好躺著兩樣東西——一個能「執行指令」的函式(叫 system),跟一段寫著 /bin/sh(就是 shell 的名字)的文字。我們要做的,是把這兩個零件組起來,變成「執行 /bin/sh」。這個手法叫 ret2libc。
組裝需要三步。這裡有個規則要先知道:在這種電腦上,要叫一個函式「處理某樣東西」,得先把那樣東西的地址放進一個叫 rdi 的暫存器(可以想成「函式的收件匣」)。所以順序是:先把 /bin/sh 放進收件匣,再叫 system 動工。
那個「① 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 之所以能一路暢通,是因為它刻意把三道鎖全關了當教材。
- 金絲雀(Stack Canary):在「回程地址」前面偷偷放一個隨機暗號。程式跳回去之前會先檢查暗號還在不在——你溢位時一定會蓋到它,暗號一對不上,程式當場自爆,你根本來不及劫持。名字來自礦工帶金絲雀下礦坑:鳥先死,人就知道有毒氣。
- 位址隨機化(PIE / ASLR):程式每次執行,零件擺放的位置都隨機洗牌。上面 payload 裡那三個「地址」你根本寫不死——這次是這個位置,下次就變了。(如果你有玩過 ARM 漏洞,這個你應該不陌生,繞過 ASLR 是進階題的家常便飯。)
- 堆疊不可執行(NX):早年的攻擊是直接把惡意程式碼塞進 buffer 裡跑。NX 這道鎖規定「這塊記憶體只能放資料、不准當程式執行」。這也正是為什麼我們不自己塞程式碼、而是去「借」程式現有的零件——ret2libc 這套手法就是為了繞過 NX 而生的。
這三道鎖只要上了任何一道,難度都會跳一整級。真實世界的程式通常三道全開,所以現代的 pwn 高手,大半功夫都花在「怎麼一道一道繞過這些鎖」——那是另一個深得多的坑了。
收尾
把整條線串起來,一句話:
程式忘了檢查長度 → 我塞爆 buffer、淹過去蓋掉「回程地址」 → 於是我能決定程式下一步跳去哪 → 我用它自己身上的零件,一步步把它牽進一個 shell。
回頭看上一篇那串 b'A'*136 + 三個地址,現在應該不再是黑魔法了——它就是「墊料填到剛好,再用三個地址把程式牽著走」。
如果你跟我一樣,以前看到 pwn 就直接跳過,希望這篇讓你覺得:它硬歸硬,但骨架其實很好懂。真正難的不是這個入門版,而是三道鎖全開之後怎麼繞——但那個,就留到你真的想跳進這個坑的那天再說吧。
Member discussion