格式化字符串,也是一種比較常見的漏洞類型。會觸發該漏洞的函數很有限。主要就是printf
還有sprintf
,fprintf
等等c庫中print家族的函數。 我們先來看看printf
的函數聲明
>int printf(const char* format,...)
這個是每個學過c語言的人一定會知道、會使用的函數。先是一個字符串指針,它指向的一個format字符串。后面是個數可變的參數。
一般人可能會這么用它
char str[100];
scanf("%s",str);
printf("%s",str);
這個程序沒有問題。然后會有一些人為了偷懶會寫成這種樣子
char str[100];
scanf("%s",str);
printf(str)
這個程序在printf
處用了一種偷懶的寫法。這看起來是沒有什么問題。但是卻產生了一個非常嚴重的漏洞。
千萬不要將printf
中的format字符串的操縱權交給用戶。保證printf
函數的第一個參數是不可變的,在程序員的掌握中的。
這里我們就要詳細的講述一下printf
的運行原理了。因為64位上printf
函數的行為發生了許多變化。這里暫時不進行說明。不過如果清楚了漏洞的產生原因,依然可以使用此漏洞。
先看看正常的情況
#include <stdio.h>
int main(void)
{
printf("%d%d%d%d%s",5,6,8,0x21,"test");
return 0;
}
首先,看看匯編的源碼,額暫時搞不到,還是手寫吧
.data
str db "test",0
format db "%d%d%d%d%s",0
.code
push str
push 21h
push 8
push 6
push 5
push format
call printf
差不多就這樣。這個時候的棧就會是這個樣子的。
-00000003 db ? ;
-00000002 db ? ;
-00000001 db ? ;
+00000000 s db 4 dup(?)
+00000004 r db 4 dup(?)
+00000008 format db 4 ;"%d%d%d%c"
+0000000c %d db 4 ; 4
+00000010 %d db 4 ; 6
+00000014 %d db 4 ; 8
+00000018 %x db 4 ; 0x21
+0000001c %s db 4 ; "test"
+0000001c ; end of stack variables
(~~額,不要吐槽的那原始的棧結構表示方式,用過IDA的應該知道~~)。
根據cdecl的函數調用規定,函數的從最右邊的參數開始,逐個壓棧。如果要傳入的是一個字符串,那么就將字符串的指針壓棧。這一切都井井有條的進行著。如果是一般的函數,函數的調用者和被調用者都應該知道函數的參數個數以及每個參數的類型。對于不相同的類型,編譯器還會自動的進行類型的轉換,或者是發生編譯錯誤,提醒程序的編寫者。
但是,到了printf
函數,一切就不一樣了。因為printf
是c語言中少有的支持可變參數的庫函數。對于可變參數的函數,一切就變得模糊了起來。函數的調用者可以自由的指定函數參數的數量和類型,被調用者無法知道在函數調用之前到底有多少參數被壓入棧幀當中。所以printf
函數要求傳入一個format參數用以指定到底有多少,怎么樣的參數被傳入其中。然后它就會忠實的按照函數的調用者傳入的格式一個一個的打印出數據。
當然這會產生一個嚴重的問題。如果我們無意或者有意,在format中,或者說我們要求printf
打印的數據數量大于我們所給的數量會怎樣?printf
函數不可能知道棧幀中哪一些數據是傳入它參數,哪些是屬于函數調用者的數據。看下面段代碼
#include <stdio.h>
int main(void)
{ printf("%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x");
return 0; }
這里我們只給了printf
一個參數,卻讓其打印出12個int類型的數據,我們編譯運行看看會有什么結果。
這里可以看到,printf
忠實的按照我們意愿打印出了12個數值。這些數值不是我們輸入的參數,而是保存在棧中的其他的數值。通過這個特性,黑客們就創造出了格式化字符串的漏洞。
可能有人(~~像我一樣的弱渣~~)會對這個漏洞的危害感到疑惑,因為它似乎只是打印一些沒有用的垃圾數據而已。其實,它的危害一點不比棧溢出漏洞的危害小,如果使用得當,甚至比棧溢出效果更好。如果棧溢出是粗暴的地毯式轟炸的話,格式化字符串漏洞就是一位可怕的狙擊手。一擊便可致命。
至于此漏洞的利用方式,主要有2種
剛才也看到了printf
可以打印出調用者棧幀中的信息。在0day攻擊當中,如何獲得對方內存中的數據是非常重要的一個技巧,而格式化字符串漏洞的其中一個利用方法便是能夠獲得內存中那些本不應該被我們知道的數據。這個過程我們稱之為leak內存。0day攻擊中一種重要的方法ret to libc就是以leak基地址為前提的。
只要我們在format中填入足夠的參數,那么printf
就可以打出儲存在棧中的,那些本不能被知道的信息。只要計算好format在棧中的地址與需要leak的信息地址之差。就可以得到想要的數據
比如format在0x20
處而dest數據在0x00
處。他們一共相差32個字節,那么我們就可以構造"%f%f%f%d,%x"
這樣的字符串。逗號前面會的"%f%f%f%d"
可以打印出比foramt更高位的28個字節的數據,當然這不是我們想要的。然后最后的一個%x
便可以以16進制的形式打印出我們想要的數據了。
然后,更進一步,我們知道格式化字符串還有%s
參數。那么,如果在棧中保存有指向我們感興趣數據的指針,我們就可以在打印指針的時候使用一個%s
來打印別的地方的內容。而且一般的程序都會將用戶輸入的數據儲存在棧上。這就給了我們一個構造指針的機會,再結合格式化字符串漏洞,幾乎可以得到所有內存數據。
也許格式化字符串漏洞可以打印內存信息這一點不讓人奇怪。但是格式化字符串其實也可以修改內存中的數據。我們來看看下面這一段代碼。
#include <stdio.h>
int main(void)
{ int a; printf("aaaaaaa%n\n",&a);
printf("%d\n",a);
return 0; }
這是一段有點神奇的代碼,讓我們看看它的運行結果。
可以發現a的值被printf
函數修改為了7。這就是%n
的功效了。這是一個不常用到的參數。它的功能是將%n
之前printf
已經打印的字符個數賦值給傳入的指針。通過%n
我們就可以修改內存中的值了。和%s
leak內存一樣,只要棧中有我們需要修改的內存的地址就可以使用格式化字符串的漏洞修改它。
當然,如果需要修改的數據是相當大的數值時,我們可以使用%02333d這種形式。在打印數值右側用0補齊不足位數的方式來補齊足。
可以看出,格式化字符串可以修改的內存范圍更加廣。只要構造出指針,就可以改寫內存中的任何數值。和棧溢出的地毯轟炸不同。這種一次只能改寫一個dword大小的內存的攻擊方式更加精而致命
最好的學習方法就是實踐,現在我們就來實驗一下格式字符串漏洞的功效。 首先,代碼
#include <stdio.h>
int main(void)
{ int flag = 0;
int *p = &flag; char a[100];
scanf("%s",a);
printf(a);
if(flag == 2000)
{
printf("good!!\n");
}
return 0;
}
使用gcc編譯。這里<-- 是我編譯成的可執行文件。可以拿去試試。
然后拖進IDA中分析一下棧結構,調用printf
函數時候的棧結構是這樣的
-00000010 r dd ? <-- 這里是printf的返回地址,向上就是printf的棧幀
-00000010 format dd ?
-00000010 dd ?
-00000010 dd ?
-00000010 dd ?
-0000000C flag dd ?
-00000008 p dd ?
-00000004 a db ?
-00000000 db ? ; <-- 再向下就都是a數組的空間
我們可以需要修改的變量是flag,而指針p便是指向flag的指針。所以可以通過p來修改flag的值為2000,從而達到我們打印出good!!的目標
>%010x%010x%010x%01970x%n
這個便是我構造出的poc,很短,但是很強悍(→_→)。 那么我們來看看效果吧
oh yeah!!!!