格式化字符串漏洞實驗
一宴猾、 實驗描述
格式化字符串漏洞是由像 printf(user_input) 這樣的代碼引起的,其中 user_input 是用戶輸入的數據逗抑,具有 Set-UID root 權限的這類程序在運行的時候吹艇,printf 語句將會變得非常危險稀并,因為它可能會導致下面的結果:
使得程序崩潰
任意一塊內存讀取數據
修改任意一塊內存里的數據
最后一種結果是非常危險的,因為它允許用戶修改 set-UID root 程序內部變量的值碟绑,從而改變這些程序的行為俺猿。
本實驗將會提供一個具有格式化漏洞的程序茎匠,我們將制定一個計劃來探索這些漏洞。
二押袍、實驗預備知識講解
2.1 什么是格式化字符串诵冒?
printf ("The magic number is: %d", 1911);
試觀察運行以上語句,會發(fā)現(xiàn)字符串"The magic number is: %d"中的格式符%d 被參數(1911)替換谊惭,因此輸出變成了“The magic number is: 1911”汽馋。
格式化字符串大致就是這么一回事啦。
除了表示十進制數的%d圈盔,還有不少其他形式的格式符豹芯,一起來認識一下吧~
格式符
含義
含義(英)
傳
%d
十進制數(int)
decimal
值
%u
無符號十進制數 (unsigned int)
unsigned decimal
值
%x
十六進制數 (unsigned int)
hexadecimal
值
%s
字符串 ((const) (unsigned) char *)
string
引用(指針)
%n
%n 符號以前輸入的字符數量 (* int)
number of bytes written so far
引用(指針)
( *%n的使用將在 2.5 節(jié)中做出說明)
2.2 棧與格式化字符串
格式化函數的行為由格式化字符串控制,printf 函數從棧上取得參數驱敲。
printf ("a has value %d, b has value %d, c is at address: %08x\n",a, b, &c);
2.3 如果參數數量不匹配會發(fā)生什么告组?
如果只有一個不匹配會發(fā)生什么?
printf ("a has value %d, b has value %d, c is at address: %08x\n",a, b);
在上面的例子中格式字符串需要 3 個參數癌佩,但程序只提供了 2 個木缝。
該程序能夠通過編譯么?printf()是一個參數長度可變函數围辙。因此我碟,僅僅看參數數量是看不出問題的。
為了查出不匹配姚建,編譯器需要了解 printf()的運行機制矫俺,然而編譯器通常不做這類分析。
有些時候掸冤,格式字符串并不是一個常量字符串厘托,它在程序運行期間生成(比如用戶輸入),因此稿湿,編譯器無法發(fā)現(xiàn)不匹配铅匹。
那么 printf()函數自身能檢測到不匹配么?printf()從棧上取得參數饺藤,如果格式字符串需要 3 個參數包斑,它會從棧上取 3 個,除非棧被標記了邊界涕俗,printf()并不知道自己是否會用完提供的所有參數罗丰。
既然沒有那樣的邊界標記。printf()會持續(xù)從棧上抓取數據再姑,在一個參數數量不匹配的例子中萌抵,它會抓取到一些不屬于該函數調用到的數據。
如果有人特意準備數據讓 printf 抓取會發(fā)生什么呢?
2.4 訪問任意位置內存
我們需要得到一段數據的內存地址绍填,但我們無法修改代碼萎坷,供我們使用的只有格式字符串。
如果我們調用 printf(%s) 時沒有指明內存地址, 那么目標地址就可以通過 printf 函數沐兰,在棧上的任意位置獲取哆档。printf 函數維護一個初始棧指針,所以能夠得到所有參數在棧中的位置
觀察: 格式字符串位于棧上. 如果我們可以把目標地址編碼進格式字符串,那樣目標地址也會存在于棧上住闯,在接下來的例子里瓜浸,格式字符串將保存在棧上的緩沖區(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 函數得到格式字符串中的目標內存地址 (該地址也存在于棧上), 我們就可以訪問該地址比原。(注:代碼中引號內容為 user_input 數組內容的展開)
printf ("\x10\x01\x48\x08 %x %x %x %x %s");
\x10\x01\x48\x08 是目標地址的四個字節(jié)插佛, 在 C 語言中, \x10 告訴編譯器將一個 16 進制數 0x10 放于當前位置(占 1 字節(jié))。如果去掉前綴\x10 就相當于兩個 ascii 字符 1 和 0 了量窘,這就不是我們所期望的結果了雇寇。
%x 導致棧指針向格式字符串的方向移動(參考 1.2 節(jié))
如圖所示蚌铜,我們使用四個%x 來移動 printf 函數的棧指針到我們存儲格式字符串的位置锨侯,一旦到了目標位置,我們使用%s 來打印冬殃,它會打印位于地址 0x10014808 的內容囚痴,因為是將其作為字符串來處理,所以會一直打印到結束符為止审葬。
user_input 數組到傳給 printf 函數參數的地址之間的椛罟觯空間不是為了 printf 函數準備的。但是涣觉,因為程序本身存在格式字符串漏洞痴荐,所以 printf 會把這段內存當作傳入的參數來匹配%x。
最大的挑戰(zhàn)就是想方設法找出 printf 函數棧指針(函數取參地址)到 user_input 數組的這一段距離是多少官册,這段距離決定了你需要在%s 之前輸入多少個%x生兆。
2.5 在內存中寫一個數字
%n: 該符號前輸入的字符數量會被存儲到對應的參數中去
int i;printf ("12345%n", &i);
數字 5(%n 前的字符數量)將會被寫入 i 中
運用同樣的方法在訪問任意地址內存的時候,我們可以將一個數字寫入指定的內存中攀隔。只要將上一小節(jié)(1.4)的%s 替換成%n 就能夠覆蓋 0x10014808 的內容皂贩。
利用這個方法栖榨,攻擊者可以做以下事情:重寫程序標識控制訪問權限
重寫椑バ冢或者函數等等的返回地址
然而,寫入的值是由%n 之前的字符數量決定的婴栽。真的有辦法能夠寫入任意數值么满粗?用最古老的計數方式, 為了寫 1000愚争,就填充 1000 個字符吧映皆。
為了防止過長的格式字符串挤聘,我們可以使用一個寬度指定的格式指示器。(比如(%0 數字 x)就會左填充預期數量的 0 符號)
三捅彻、 實驗內容
實驗 1
用戶需要輸入一段數據组去,數據保存在 user_input 數組中,程序會使用 printf 函數打印數據內容步淹,并且該程序以 root 權限運行从隆。更加可喜的是,這個程序存在一個格式化漏洞缭裆。讓我們來看看利用這些漏洞可以搞些什么破壞键闺。
程序說明:
程序內存中存在兩個秘密值,我們想要知道這兩個值澈驼,但發(fā)現(xiàn)無法通過讀二進制代碼的方式來獲取它們(實驗中為了簡單起見辛燥,硬編碼這些秘密值為 0x44 和 0x55)。盡管我們不知道它們的值缝其,但要得到它們的內存地址倒不是特別困難挎塌,因為對大多數系統(tǒng)而言,每次運行程序内边,這些內存地址基本上是不變的勃蜘。實驗假設我們已經知道了這些內存地址,為了達到這個目的假残,程序特意為我們打出了這些地址缭贡。
有了這些前提以后我們需要達到以下目標:
找出 secret[1]的值
修改 secret[1]的值
修改 secret[1]為期望值
注意:因為實驗環(huán)境是 64 位系統(tǒng),所以需要使用%016llx 才能讀取整個字辉懒。但為了簡便起見阳惹,對程序進行了修改了,使用%08x 也能完成實驗眶俩。
有了之前預備知識的鋪墊莹汤,先自己嘗試一下,祝玩的愉快:)
程序如下:
/* 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: 編譯時可以添加以下參數關掉棧保護颠印。)
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 的旁邊,你還是沒辦法從棧上獲得它的地址钞楼,這對你來說構成了一個挑戰(zhàn)喇闸,因為沒有它的地址你怎么利用格式字符串讀寫呢。但是真的就沒招了么?
3.1.1 找出 secret[1]的值
1.首先定位 int_input 的位置燃乍,這樣就確認了%s 在格式字符串中的位置唆樊。
2.輸入 secret[1]的地址,記得做進制轉換刻蟹,同時在格式字符串中加入%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 變量相關的所有語句澳厢。同時設置關閉地址隨機化選項环础。
sysctl -w kernel.randomize_va_space=0
關閉地址隨機化后,這樣每次運行程序得到的 secret 地址就都一樣了剩拢,讓我們再來一次實驗 1 中的攻擊吧线得。不過在此之前你需要知道這些:
如何讓 scanf()接受任意數字?
通常徐伐,scanf 將會為你停頓贯钩,打印輸入。有時办素,你想要編程得 到一個數 0x05 (不是字符“5”)角雷,不幸的是,當你將“5"作為輸入,scanf 實際得到的是 5 的 ASCII 值 0x35,而不是 0x05性穿。
這個問題的一個解決辦法是使用文件勺三。我們可以很容易地寫一 C 程 序將 0x05 (不是“5")存入一個文件(我們叫它 my string),然后運行輸入被重定向到 mystring 的漏洞程序。這樣季二,scanf 將從文件 mystring 中獲得輸入而不是鍵盤檩咱。 你需要注意一些特殊數字揭措,如 0x0A (新行)胯舷,0x0C (換頁)刻蚯,0x0D (返回),0x20 (空 格)桑嘶,scanf 將它們視為分隔符炊汹,如果在 scanf 里我們僅有一個"%s"的話,它將停止讀取這些 特殊符號之后的任何內容逃顶。如果這些數字出現(xiàn)在地址屮讨便,你必須想辦法避開。為簡化任務以政, 如果你不走運地在 secret 的地址中碰到這些特殊數字霸褒,我們允許你在為 secret[2]分配內存地址 之前加上一個 malloc 語句。這個額外的 malloc 語句可以改變 secret 地址的值盈蛮。
以下程序將一個格式化字符串寫入了一個叫 mystring 的文件废菱,前 4 個字節(jié)由任意你想放 入格式化字符串的數字構成,接下來的字節(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]的值
讓我們先以上面提供的寫程序為基礎殊轴,熟悉一下基礎流程。
修改 vul_prog.c 后編譯 vul_prog.c 與 write_string.c
然后通過 write_string 程序將內容輸入進 mystring 文件中袒炉,文件內容包括代碼中加入的頭四個字節(jié)和你之后輸入的內容旁理。
寫入文件后,輸入以下命令:
./vul_prog < mystring
大功告成我磁!
0x4c = 76 = 88+8 個逗號+開頭 4 個字節(jié)孽文。
四、 練習
在實驗樓環(huán)境安步驟進行實驗夺艰,并截圖
您已經完成本課程的所有實驗叛溢,干的漂亮!*
版權聲明
本課程所涉及的實驗來自Syracuse SEED labs劲适,并在此基礎上為適配實驗樓網站環(huán)境進行修改楷掉,修改后的實驗文檔仍然遵循 GNU Free Documentation License。
本課程文檔 github 鏈接:https://github.com/shiyanlou/seedlab
附Syracuse SEED labs版權聲明:
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.