C語言格式化字符串漏洞實驗
格式化字符串漏洞實驗
在線實驗環境: 格式化字符串漏洞實驗
一、 實驗描述
格式化字符串漏洞是由像printf(user_input)這樣的代碼引起的,其中user_input是用戶輸入的數據,具有Set-UID root權限的這類程序在運行的時候,printf語句將會變得非常危險,因為它可能會導致下面的結果:
- 使得程序崩潰
- 任意一塊內存讀取數據
- 修改任意一塊內存里的數據
最后一種結果是非常危險的,因為它允許用戶修改set-UID root程序內部變量的值,從而改變這些程序的行為。
本實驗將會提供一個具有格式化漏洞的程序,我們將制定一個計劃來探索這些漏洞。
二、實驗預備知識講解
2.1 什么是格式化字符串?
printf ("The magic number is: %d", 1911);
試觀察運行以上語句,會發現字符串"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 的使用將在1.5節中做出說明)
2.2 棧與格式化字符串
格式化函數的行為由格式化字符串控制,printf函數從棧上取得參數。
printf ("a has value %d, b has value %d, c is at address: %08x\n",a, b, &c);
2.3 如果參數數量不匹配會發生什么?
如果只有一個不匹配會發生什么?
printf ("a has value %d, b has value %d, c is at address: %08x\n",a, b);
- 在上面的例子中格式字符串需要3個參數,但程序只提供了2個。
- 該程序能夠通過編譯么?
- printf()是一個參數長度可變函數。因此,僅僅看參數數量是看不出問題的。
- 為了查出不匹配,編譯器需要了解printf()的運行機制,然而編譯器通常不做這類分析。
- 有些時候,格式字符串并不是一個常量字符串,它在程序運行期間生成(比如用戶輸入),因此,編譯器無法發現不匹配。
- 那么printf()函數自身能檢測到不匹配么?
- printf()從棧上取得參數,如果格式字符串需要3個參數,它會從棧上取3個,除非棧被標記了邊界,printf()并不知道自己是否會用完提供的所有參數。
- 既然沒有那樣的邊界標記。printf()會持續從棧上抓取數據,在一個參數數量不匹配的例子中,它會抓取到一些不屬于該函數調用到的數據。 </ul> </li>
- 如果有人特意準備數據讓printf抓取會發生什么呢? </ul>
- 我們需要得到一段數據的內存地址,但我們無法修改代碼,供我們使用的只有格式字符串。
- 如果我們調用 printf(%s) 時沒有指明內存地址, 那么目標地址就可以通過printf函數,在棧上的任意位置獲取。printf函數維護一個初始棧指針,所以能夠得到所有參數在棧中的位置
- 觀察: 格式字符串位于棧上. 如果我們可以把目標地址編碼進格式字符串,那樣目標地址也會存在于棧上,在接下來的例子里,格式字符串將保存在棧上的緩沖區中。
2.4 訪問任意位置內存
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函數得到格式字符串中的目標內存地址 (該地址也存在于棧上), 我們就可以訪問該地址.
printf ("\x10\x01\x48\x08 %x %x %x %x %s");
-
\x10\x01\x48\x08 是目標地址的四個字節, 在C語言中, \x10 告訴編譯器將一個16進制數0x10放于當前位置(占1字節)。如果去掉前綴\x10就相當于兩個ascii字符1和0了,這就不是我們所期望的結果了。
- %x 導致棧指針向格式字符串的方向移動(參考1.2節)
- 下圖解釋了攻擊方式,如果用戶輸入中包含了以下格式字符串
- 如圖所示,我們使用四個%x來移動printf函數的棧指針到我們存儲格式字符串的位置,一旦到了目標位置,我們使用%s來打印,它會打印位于地址0x10014808的內容,因為是將其作為字符串來處理,所以會一直打印到結束符為止。
- user_input數組到傳給printf函數參數的地址之間的棧空間不是為了printf函數準備的。但是,因為程序本身存在格式字符串漏洞,所以printf會把這段內存當作傳入的參數來匹配%x。
- 最大的挑戰就是想方設法找出printf函數棧指針(函數取參地址)到user_input數組的這一段距離是多少,這段距離決定了你需要在%s之前輸入多少個%x。
2.5 在內存中寫一個數字
%n: 該符號前輸入的字符數量會被存儲到對應的參數中去
int i; printf ("12345%n", &i);
- 數字5(%n前的字符數量)將會被寫入i 中
- 運用同樣的方法在訪問任意地址內存的時候,我們可以將一個數字寫入指定的內存中。只要將上一小節(1.4)的%s替換成%n就能夠覆蓋0x10014808的內容。
- 利用這個方法,攻擊者可以做以下事情:
- 重寫程序標識控制訪問權限
- 重寫棧或者函數等等的返回地址
- 然而,寫入的值是由%n之前的字符數量決定的。真的有辦法能夠寫入任意數值么?
- 用最古老的計數方式, 為了寫1000,就填充1000個字符吧。
- 為了防止過長的格式字符串,我們可以使用一個寬度指定的格式指示器。(比如(%0數字x)就會左填充預期數量的0符號) </ul> </li> </ul>
- 找出secret[1]的值
- 修改secret[1]的值
- 修改secret[1]為期望值
三、 實驗內容
用戶需要輸入一段數據,數據保存在user_input數組中,程序會使用printf函數打印數據內容,并且該程序以root權限運行。更加可喜的是,這個程序存在一個格式化漏洞。讓我們來看看利用這些漏洞可以搞些什么破壞。
程序說明:
程序內存中存在兩個秘密值,我們想要知道這兩個值,但發現無法通過讀二進制代碼的方式來獲取它們(實驗中為了簡單起見,硬編碼這些秘密值為0x44和0x55)。盡管我們不知道它們的值,但要得到它們的內存地址倒不是特別困難,因為對大多數系統而言,每次運行程序,這些內存地址基本上是不變的。實驗假設我們已經知道了這些內存地址,為了達到這個目的,程序特意為我們打出了這些地址。
有了這些前提以后我們需要達到以下目標:
注意:因為實驗環境是64位系統,所以需要使用%016llx才能讀取整個字。但為了簡便起見,對程序進行了修改了,使用%08x也能完成實驗。
有了之前預備知識的鋪墊,先自己嘗試一下,祝玩的愉快:)
程序如下:
/ vul_prog.c /
include <stdlib.h>
include <stdio.h>
define SECRET1 0x44
define SECRET2 0x55
int 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", ∫_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; }</code></pre>
(ps: 編譯時可以添加以下參數關掉棧保護。)
gcc -z execstack -fno-stack-protector -o vul_prog vul_prog.c
一點小提示:你會發現secret[0]和secret[1]存在于malloc出的堆上,我們也知道secret的值存在于棧上,如果你想覆蓋secret[0]的值,ok,它的地址就在棧上,你完全可以利用格式化字符串的漏洞來達到目的。然而盡管secret[1]就在它的兄弟0的旁邊,你還是沒辦法從棧上獲得它的地址,這對你來說構成了一個挑戰,因為沒有它的地址你怎么利用格式字符串讀寫呢。但是真的就沒招了么?
3.1 找出secret[1]的值
1.首先定位int_input的位置,這樣就確認了%s在格式字符串中的位置。
2.輸入secret[1]的地址,記得做進制轉換,同時在格式字符串中加入%s。
大功告成!U的ascii碼就是55。
3.2 修改secret[1]的值
1.只要求修改,不要求改什么?簡單!不明白%n用法的可以往前回顧一下。
大功告成x2!
3.3 修改secret[1]為期望值
1.要改成自己期望的值,咋辦?填1000豈不累死?!可以用填充嘛!
哦對了,0x3e8 = 1000。 大功告成x3!
來自:https://github.com/shiyanlou/seedlab/blob/master/formatstring.md