七、格式化字元串漏洞
原文: Format String Vulnerability 譯者: 飛龍
printf ( user_input );
上面的代碼在 C 程式中十分常見。這一章中,我們會發現如果程式使用權限運作(例如 Set-UID 程式),可能造成什麼問題。
1 格式化字元串
- 什麼是格式化字元串?
被列印的文本是printf ("The magic number is: %d\n", 1911);
,後面是格式化參數The magic number is:
。它在輸出中由參數 1911 替換。是以輸出是這樣:%d
。除了The magic number is: 1911
,還有幾種其它的格式化參數,每種都有不同的含義。下面的表格總結了這些格式化參數:%d
參數 含義 傳遞方式 ------------------------------------------ %d 十進制 (int) 傳值 %u 無符号十進制 (unsigned int) 傳值 %x 十六集進制 (unsigned int) 傳值 %s 字元串 ((const) (unsigned) char *) 傳址 %n 目前為止寫入的字元數 (* int) 傳址
-
棧和它在格式化字元串中的作用
格式化函數的行為格式化字元串控制。函數從棧上擷取由格式化字元串請求的參數。
printf ("a has value %d, b has value %d, c is at address: %08x\n", a, b, &c);
- 如果格式化字元串和實際參數之間不比對,會如何?
printf ("a has value %d, b has value %d, c is at address: %08x\n", a, b);
- 在上面的例子中,格式化字元串請求三個參數,但是程式實際上提供了兩個(也就是
和a
)。b
- 這個可以通過編譯器嘛?
- 函數
定義為參數長度可變的函數。是以,通過檢視參數數量,一切都正常。printf
- 為了尋找不比對,編譯器需要了解
如何工作,以及格式化字元串是什麼意思。但是,編譯器不會做這種分析。printf
- 有時,格式化字元串不是個字元串常量。它在程式執行期間生成。是以,這裡編譯器沒有辦法發現不比對。
- 函數
-
可能檢測不比對嗎?printf
-
從棧上擷取參數。如果格式化字元串需要三個參數,它會從棧上擷取三個參數。除非棧上存在标記,printf
不知道它超出了提供給它的參數範圍。printf
- 由于不存在标記,
會繼續從棧上抓取資料。在不比對的情況下,它會抓取一些不屬于這個函數調用的資料。printf
-
- 在上面的例子中,格式化字元串請求三個參數,但是程式實際上提供了兩個(也就是
2 格式化字元串漏洞攻擊
- 使程式崩潰
printf ("%s%s%s%s%s%s%s%s%s%s%s%s");
- 對于每一個
,%s
會從棧上抓取一個數值,将其看做位址,并将由該位址指向的記憶體内容列印為字元串,直到遇到了空字元(數值 0 而不是字元 0)。printf
- 由于
抓取的數值可能不是有效位址,由該數值指向的記憶體可能不存在(也就是沒有實體記憶體賦給這個位址),程式就會崩潰。printf
- 也可能數值碰巧是有效位址,但是位址空間被保護了(也就是為核心空間預留)。這樣的話,程式也會崩潰。
- 對于每一個
- 檢視棧
printf ("%08x %08x %08x %08x %08x\n");
- 這讓
函數從棧上擷取五個參數,并将其展示為填充長度為 8 的十六進制數值。是以輸出可能為:printf
40012980 080628c4 bffff7a4 00000005 08059c04
- 這讓
- 檢視任何位址的記憶體
- 我們需要提供記憶體位址。但是我們不能修改代碼,我們隻能提供格式化字元串。
- 如果我們使用
,而不指定記憶體位址,printf(%s)
就會從棧上擷取目标位址。函數維護了初始的棧指針,是以它知道棧上參數的位置。printf
- 觀察:格式化字元串通常位于棧上。如果我們可以将目标位址編碼在格式化字元串中,目标位址就能在棧上。下面的示例中,格式化字元串儲存在緩沖區中,它位于棧上。
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");
-
是目标位址的四個位元組。在 C 語言中,\x10\x01\x48\x08
讓編譯器将十六進制值 0x10 放入目前位置。這個值隻占一個位元組。如果我們不使用\x10
,直接将 10 放入字元串,就會儲存 ASCII 值 1 和 0。它們的 ASCII 值是 49 和 48。\x
-
讓棧指針沿着格式化字元串移動。%x
- 這裡是攻擊方式,如果
包含下面的格式化字元串:user_input
"\x10\x01\x48\x08 %x %x %x %x %s"
- 本質上,我們使用四個
來使%x
的指針,向我們儲存在格式化字元串中的位址移動。一旦到達了目标,我們就會像printf
提供printf
,使其列印出位址%s
的内容。函數0x10014808
會将記憶體看做字元串,并列印出來,知道到達了字元串尾部(空字元)。printf
-
和傳給user_input
函數的位址之間的棧空間并不是printf
的。但是,由于程式中的格式化字元串漏洞。printf
将它們看做比對格式化字元串中printf
的參數。%x
- 這個攻擊的關鍵就是弄清楚
user_input
的位址的距離。這個距離決定了在提供printf
之前,你需要向格式化字元串插入多少個%s
。%x
- 在程序的記憶體中向任何位址寫入整數
-
:目前為止寫入的字元數量,儲存在一個整數中,它由相應參數表示。%n
int i; printf ("12345%n", &i);
- 它使
将 5 寫入變量printf
i
- 使用檢視任意位址記憶體的相同方式,我們可以使
将整數寫入任意位址。隻需要将上面例子中的printf
替換為%s
,就會覆寫%n
位址處的内容。0x10014808
- 使用這個攻擊,攻擊者可以做這些事情:
- 覆寫控制通路權限的重要程式标志位
- 覆寫棧上的傳回位址,函數指針,以及其他
- 但是,寫入的值由
之前已列印的字元數量決定。是否真的可以寫入任意整數呢?%n
- 使用僞造的輸出字元。為了寫入值 1000,應該事先列印 1000 個僞造字元的間隔。
- 為了避免過長的格式化字元串,我們可以使用格式化标志的寬度限定。
-
- 預防措施
- 位址空間随機化:就像用于保護緩沖區溢出攻擊的預防措施那樣,位址空間随機化攻擊者難以找到他們想要讀取或寫入什麼位址。(譯者注:但是仍然有一些區域無法随機化,比如 PLT)。