格式化字符串漏洞實驗
一、 實驗描述
格式化字符串漏洞是由像 printf(user_input) 這樣的代碼引起的,其中 user_input 是用戶輸入的數(shù)據(jù),具有 Set-UID root 權(quán)限的這類程序在運行的時候,printf 語句將會變得非常危險,因為它可能會導(dǎo)致下面的結(jié)果:
使得程序崩潰
任意一塊內(nèi)存讀取數(shù)據(jù)
修改任意一塊內(nèi)存里的數(shù)據(jù)
最后一種結(jié)果是非常危險的,因為它允許用戶修改 set-UID root 程序內(nèi)部變量的值,從而改變這些程序的行為。
本實驗將會提供一個具有格式化漏洞的程序,我們將制定一個計劃來探索這些漏洞。
二、實驗預(yù)備知識講解
2.1 什么是格式化字符串?
printf ("The magic number is: %d", 1911);
試觀察運行以上語句,會發(fā)現(xiàn)字符串"The magic number is: %d"中的格式符%d 被參數(shù)(1911)替換,因此輸出變成了“The magic number is: 1911”。
格式化字符串大致就是這么一回事啦。
除了表示十進制數(shù)的%d,還有不少其他形式的格式符,一起來認識一下吧~
格式符
含義
含義(英)
傳
%d
十進制數(shù)(int)
decimal
值
%u
無符號十進制數(shù) (unsigned int)
unsigned decimal
值
%x
十六進制數(shù) (unsigned int)
hexadecimal
值
%s
字符串 ((const) (unsigned) char *)
string
引用(指針)
%n
%n 符號以前輸入的字符數(shù)量 (* int)
number of bytes written so far
引用(指針)
( *%n的使用將在 2.5 節(jié)中做出說明)
2.2 棧與格式化字符串
格式化函數(shù)的行為由格式化字符串控制,printf 函數(shù)從棧上取得參數(shù)。
printf ("a has value %d, b has value %d, c is at address: %08x\n",a, b, &c);

2.3 如果參數(shù)數(shù)量不匹配會發(fā)生什么?
如果只有一個不匹配會發(fā)生什么?
printf ("a has value %d, b has value %d, c is at address: %08x\n",a, b);
在上面的例子中格式字符串需要 3 個參數(shù),但程序只提供了 2 個。
該程序能夠通過編譯么?printf()是一個參數(shù)長度可變函數(shù)。因此,僅僅看參數(shù)數(shù)量是看不出問題的。
為了查出不匹配,編譯器需要了解 printf()的運行機制,然而編譯器通常不做這類分析。
有些時候,格式字符串并不是一個常量字符串,它在程序運行期間生成(比如用戶輸入),因此,編譯器無法發(fā)現(xiàn)不匹配。
那么 printf()函數(shù)自身能檢測到不匹配么?printf()從棧上取得參數(shù),如果格式字符串需要 3 個參數(shù),它會從棧上取 3 個,除非棧被標(biāo)記了邊界,printf()并不知道自己是否會用完提供的所有參數(shù)。
既然沒有那樣的邊界標(biāo)記。printf()會持續(xù)從棧上抓取數(shù)據(jù),在一個參數(shù)數(shù)量不匹配的例子中,它會抓取到一些不屬于該函數(shù)調(diào)用到的數(shù)據(jù)。
如果有人特意準(zhǔn)備數(shù)據(jù)讓 printf 抓取會發(fā)生什么呢?
2.4 訪問任意位置內(nèi)存
我們需要得到一段數(shù)據(jù)的內(nèi)存地址,但我們無法修改代碼,供我們使用的只有格式字符串。
如果我們調(diào)用 printf(%s) 時沒有指明內(nèi)存地址, 那么目標(biāo)地址就可以通過 printf 函數(shù),在棧上的任意位置獲取。printf 函數(shù)維護一個初始棧指針,所以能夠得到所有參數(shù)在棧中的位置
觀察: 格式字符串位于棧上. 如果我們可以把目標(biāo)地址編碼進格式字符串,那樣目標(biāo)地址也會存在于棧上,在接下來的例子里,格式字符串將保存在棧上的緩沖區(qū)中。
int main(int argc, char argv[]){ char user_input[100]; ... ... / other variable definitions and statements / scanf("%s", user_input); / getting a string from user / printf(user_input); / Vulnerable place */ return 0;}
如果我們讓 printf 函數(shù)得到格式字符串中的目標(biāo)內(nèi)存地址 (該地址也存在于棧上), 我們就可以訪問該地址。(注:代碼中引號內(nèi)容為 user_input 數(shù)組內(nèi)容的展開)
printf ("\x10\x01\x48\x08 %x %x %x %x %s");
\x10\x01\x48\x08 是目標(biāo)地址的四個字節(jié), 在 C 語言中, \x10 告訴編譯器將一個 16 進制數(shù) 0x10 放于當(dāng)前位置(占 1 字節(jié))。如果去掉前綴\x10 就相當(dāng)于兩個 ascii 字符 1 和 0 了,這就不是我們所期望的結(jié)果了。
%x 導(dǎo)致棧指針向格式字符串的方向移動(參考 1.2 節(jié))

如圖所示,我們使用四個%x 來移動 printf 函數(shù)的棧指針到我們存儲格式字符串的位置,一旦到了目標(biāo)位置,我們使用%s 來打印,它會打印位于地址 0x10014808 的內(nèi)容,因為是將其作為字符串來處理,所以會一直打印到結(jié)束符為止。
user_input 數(shù)組到傳給 printf 函數(shù)參數(shù)的地址之間的棧空間不是為了 printf 函數(shù)準(zhǔn)備的。但是,因為程序本身存在格式字符串漏洞,所以 printf 會把這段內(nèi)存當(dāng)作傳入的參數(shù)來匹配%x。
最大的挑戰(zhàn)就是想方設(shè)法找出 printf 函數(shù)棧指針(函數(shù)取參地址)到 user_input 數(shù)組的這一段距離是多少,這段距離決定了你需要在%s 之前輸入多少個%x。
2.5 在內(nèi)存中寫一個數(shù)字
%n: 該符號前輸入的字符數(shù)量會被存儲到對應(yīng)的參數(shù)中去
int i;printf ("12345%n", &i);
數(shù)字 5(%n 前的字符數(shù)量)將會被寫入 i 中
運用同樣的方法在訪問任意地址內(nèi)存的時候,我們可以將一個數(shù)字寫入指定的內(nèi)存中。只要將上一小節(jié)(1.4)的%s 替換成%n 就能夠覆蓋 0x10014808 的內(nèi)容。
利用這個方法,攻擊者可以做以下事情:重寫程序標(biāo)識控制訪問權(quán)限
重寫棧或者函數(shù)等等的返回地址
然而,寫入的值是由%n 之前的字符數(shù)量決定的。真的有辦法能夠?qū)懭肴我鈹?shù)值么?用最古老的計數(shù)方式, 為了寫 1000,就填充 1000 個字符吧。
為了防止過長的格式字符串,我們可以使用一個寬度指定的格式指示器。(比如(%0 數(shù)字 x)就會左填充預(yù)期數(shù)量的 0 符號)
三、 實驗內(nèi)容
實驗 1
用戶需要輸入一段數(shù)據(jù),數(shù)據(jù)保存在 user_input 數(shù)組中,程序會使用 printf 函數(shù)打印數(shù)據(jù)內(nèi)容,并且該程序以 root 權(quán)限運行。更加可喜的是,這個程序存在一個格式化漏洞。讓我們來看看利用這些漏洞可以搞些什么破壞。
程序說明:
程序內(nèi)存中存在兩個秘密值,我們想要知道這兩個值,但發(fā)現(xiàn)無法通過讀二進制代碼的方式來獲取它們(實驗中為了簡單起見,硬編碼這些秘密值為 0x44 和 0x55)。盡管我們不知道它們的值,但要得到它們的內(nèi)存地址倒不是特別困難,因為對大多數(shù)系統(tǒng)而言,每次運行程序,這些內(nèi)存地址基本上是不變的。實驗假設(shè)我們已經(jīng)知道了這些內(nèi)存地址,為了達到這個目的,程序特意為我們打出了這些地址。
有了這些前提以后我們需要達到以下目標(biāo):
找出 secret[1]的值
修改 secret[1]的值
修改 secret[1]為期望值
注意:因為實驗環(huán)境是 64 位系統(tǒng),所以需要使用%016llx 才能讀取整個字。但為了簡便起見,對程序進行了修改了,使用%08x 也能完成實驗。
有了之前預(yù)備知識的鋪墊,先自己嘗試一下,祝玩的愉快:)
程序如下:
/* vul_prog.c / include <stdlib.h>include <stdio.h>define SECRET1 0x44define SECRET2 0x55int main(int argc, char argv[]){ char user_input[100]; int secret; long int_input; int a, b, c, d; / other variables, not used here./ / The secret value is stored on the heap / secret = (int ) malloc(2sizeof(int)); / getting the secret / secret[0] = SECRET1; secret[1] = SECRET2; printf("The variable secret's address is 0x%8x (on stack)\n", &secret); printf("The variable secret's value is 0x%8x (on heap)\n", secret); printf("secret[0]'s address is 0x%8x (on heap)\n", &secret[0]); printf("secret[1]'s address is 0x%8x (on heap)\n", &secret[1]); printf("Please enter a decimal integer\n"); scanf("%d", &int_input); / getting an input from user / printf("Please enter a string\n"); scanf("%s", user_input); / getting a string from user / / Vulnerable place / printf(user_input); printf("\n"); / Verify whether your attack is successful */ printf("The original secrets: 0x%x -- 0x%x\n", SECRET1, SECRET2); printf("The new secrets: 0x%x -- 0x%x\n", secret[0], secret[1]); return 0;}
(ps: 編譯時可以添加以下參數(shù)關(guān)掉棧保護。)
gcc -z execstack -fno-stack-protector -o vul_prog vul_prog.c
一點小提示:你會發(fā)現(xiàn) secret[0]和 secret[1]存在于 malloc 出的堆上,我們也知道 secret 的值存在于棧上,如果你想覆蓋 secret[0]的值,ok,它的地址就在棧上,你完全可以利用格式化字符串的漏洞來達到目的。然而盡管 secret[1]就在它的兄弟 0 的旁邊,你還是沒辦法從棧上獲得它的地址,這對你來說構(gòu)成了一個挑戰(zhàn),因為沒有它的地址你怎么利用格式字符串讀寫呢。但是真的就沒招了么?
3.1.1 找出 secret[1]的值
1.首先定位 int_input 的位置,這樣就確認了%s 在格式字符串中的位置。

2.輸入 secret[1]的地址,記得做進制轉(zhuǎn)換,同時在格式字符串中加入%s。

大功告成!U 的 ascii 碼就是 55。
3.1.2 修改 secret[1]的值
1.只要求修改,不要求改什么?簡單!不明白%n 用法的可以往前回顧一下。

大功告成 x2!
3.1.3 修改 secret[1]為期望值
1.要改成自己期望的值,咋辦?填 1000 豈不累死?!可以用填充嘛!

哦對了,0x3e8 = 1000。
大功告成 x3!
實驗 2
現(xiàn)在讓我們把第一個 scanf 語句去掉,并去掉與 int_input 變量相關(guān)的所有語句。同時設(shè)置關(guān)閉地址隨機化選項。
sysctl -w kernel.randomize_va_space=0
關(guān)閉地址隨機化后,這樣每次運行程序得到的 secret 地址就都一樣了,讓我們再來一次實驗 1 中的攻擊吧。不過在此之前你需要知道這些:
如何讓 scanf()接受任意數(shù)字?
通常,scanf 將會為你停頓,打印輸入。有時,你想要編程得 到一個數(shù) 0x05 (不是字符“5”),不幸的是,當(dāng)你將“5"作為輸入,scanf 實際得到的是 5 的 ASCII 值 0x35,而不是 0x05。
這個問題的一個解決辦法是使用文件。我們可以很容易地寫一 C 程 序?qū)?0x05 (不是“5")存入一個文件(我們叫它 my string),然后運行輸入被重定向到 mystring 的漏洞程序。這樣,scanf 將從文件 mystring 中獲得輸入而不是鍵盤。 你需要注意一些特殊數(shù)字,如 0x0A (新行),0x0C (換頁),0x0D (返回),0x20 (空 格),scanf 將它們視為分隔符,如果在 scanf 里我們僅有一個"%s"的話,它將停止讀取這些 特殊符號之后的任何內(nèi)容。如果這些數(shù)字出現(xiàn)在地址屮,你必須想辦法避開。為簡化任務(wù), 如果你不走運地在 secret 的地址中碰到這些特殊數(shù)字,我們允許你在為 secret[2]分配內(nèi)存地址 之前加上一個 malloc 語句。這個額外的 malloc 語句可以改變 secret 地址的值。
以下程序?qū)⒁粋€格式化字符串寫入了一個叫 mystring 的文件,前 4 個字節(jié)由任意你想放 入格式化字符串的數(shù)字構(gòu)成,接下來的字節(jié)由鍵盤輸入。
include <sys/types.h>include <sys/stat.h>include <fcntl.h>int main(){ char buf[1000]; int fp, size; unsigned int address; / Putting any number you like at the beginning of the format string */ address = (unsigned int *) buf; address = 0x113222580; / Getting the rest of the format string / scanf("%s", buf+4); size = strlen(buf+4) + 4; printf("The string length is %d\n", size); / Writing buf to "mystring" */ fp = open("mystring", O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); if (fp != -1) { write(fp, buf, size); close(fp); } else { printf("Open failed!\n"); }}
3.2.1 修改 secret[0]的值
讓我們先以上面提供的寫程序為基礎(chǔ),熟悉一下基礎(chǔ)流程。
修改 vul_prog.c 后編譯 vul_prog.c 與 write_string.c
然后通過 write_string 程序?qū)?nèi)容輸入進 mystring 文件中,文件內(nèi)容包括代碼中加入的頭四個字節(jié)和你之后輸入的內(nèi)容。
寫入文件后,輸入以下命令:
./vul_prog < mystring
大功告成!
0x4c = 76 = 88+8 個逗號+開頭 4 個字節(jié)。
四、 練習(xí)
在實驗樓環(huán)境安步驟進行實驗,并截圖
您已經(jīng)完成本課程的所有實驗,干的漂亮!*
版權(quán)聲明
本課程所涉及的實驗來自Syracuse SEED labs,并在此基礎(chǔ)上為適配實驗樓網(wǎng)站環(huán)境進行修改,修改后的實驗文檔仍然遵循 GNU Free Documentation License。
本課程文檔 github 鏈接:https://github.com/shiyanlou/seedlab
附Syracuse SEED labs版權(quán)聲明:
Copyright Statement Copyright 2006 – 2009 Wenliang Du, Syracuse University. The development of this document is funded by the National Science Foundation’s Course, Curriculum, and Laboratory Improvement (CCLI) program under Award No. 0618680 and 0231122. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation. A copy of the license can befound at http://www.gnu.org/licenses/fdl.html.