格式化字串漏洞
格式化字串漏洞
常用的可利用函式
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int dprintf(int fd, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
轉換指示符
字元 | 型別 | 使用 |
---|---|---|
d | 4-byte | Integer |
u | 4-byte | Unsigneg Integer |
x | 4-byte | Hex |
s | 4-byte prt | String |
c | 1-byte | Character |
長度
字元 | 型別 | 使用 |
---|---|---|
hh | 1-byte | Char |
h | 2-byte | Short int |
l | 4-byte | long int |
ll | 8-byte prt | long long int |
format str 漏洞 例項
#include<stdio.h> #include<string.h> #include<stdlib.h> #include<math.h> void main(void) { char *format="%s"; char *arg1="hello world !\n"; printf(format,arg1); }
原理
在x86結構下,格式化字串的引數是通過棧傳遞的。
c程式碼
#include<stdio.h>
void main() {:
printf("%s %d %s", "Hello World!", 233, "\n");
getchar();
}
轉換成彙編程式碼
Dump of assembler code for function main: 0x0804843b <+0>: lea ecx,[esp+0x4] 0x0804843f <+4>: and esp,0xfffffff0 0x08048442 <+7>: push DWORD PTR [ecx-0x4] 0x08048445 <+10>: push ebp 0x08048446 <+11>: mov ebp,esp 0x08048448 <+13>: push ecx 0x08048449 <+14>: sub esp,0x4 0x0804844c <+17>: push 0x8048500 0x08048451 <+22>: push 0xe9 0x08048456 <+27>: push 0x8048502 0x0804845b <+32>: push 0x804850f 0x08048460 <+37>: call 0x8048300 <printf@plt> 0x08048465 <+42>: add esp,0x10 0x08048468 <+45>: call 0x8048310 <getchar@plt> 0x0804846d <+50>: nop 0x0804846e <+51>: mov ecx,DWORD PTR [ebp-0x4] 0x08048471 <+54>: leave 0x08048472 <+55>: lea esp,[ecx-0x4] 0x08048475 <+58>: ret End of assembler dump.
棧內的情況如圖:
指令儲存在0xffffcc54附近
printf格式化後的字串 “hello world! 233 \n ” 字串儲存在0x804b008位置
而事先儲存的字串 hello world則儲存在0x804b008位置 \n與233 也在其附近
main函式在呼叫call printf@plt前通過push將printf的四個引數統一入棧
0x0804844c <+17>: push 0x8048500 // "\n"
0x08048451 <+22>: push 0xe9 // 233
0x08048456 <+27>: push 0x8048502 // "hello world"
0x0804845b <+32>: push 0x804850f // "%s %d %s"
Call printf 圖
%x %x %x %3$s
#include<stdio.h>
void main() {
printf("%s %d %s %x %x %x %2$d", "Hello World!", 233, "\n");
}
out:"Hello World! 233 f7f763dc ,ffa328a0 ,0,233"
其中%s 是字串轉換符 %d是數字轉換符 %x是16進位制轉換符 而%2$d則是引數複用符號(棧中格式字串後12個位置的字串所在地址的內容以數字的格式輸出)
1、如果引數複用符號所複用的變數為數字型別的就轉換為16進位制輸出
2、如果引數複用符號所複用的變數為字串型別的話 就輸出其記憶體地址
3、設定輸出變數的位元組數例如%016x,就是以 16位元組長度輸出記憶體中第一個引數
即可通過%12%s的格式輸出棧中格式字串後12個位置的字串所在地址的內容 達到獲取指定記憶體地址內容的效果
格式化字串漏洞利用
通過提供格式字串,我們就能夠控制格式化函式的行為。
使程式崩潰
格式化字串漏洞通常要在程式崩潰時才會被發現,所有利用格式化字串漏洞最簡單的方式就是使程序崩潰。在linux中,儲存無效的指標會引起程序收到SIGSEGV訊號,從而使程式非正常終止併產生核心儲存。我們找到核心儲存中儲存了程式崩潰時的許多重要資訊,這些資訊或是後續攻擊利用的關鍵。
利用類似下面的格式化字串即可觸發漏洞:
printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s");
1、對於每一個%s,printf()都要從棧中獲取一個數字,把該數字視為一個地址,然後列印地址指向的記憶體內容,直到出現一個null字元。
2、因為不可能獲取的每一個數字都是地址,數字所對應的記憶體可能並不存在。
3、還有可能獲得的數字確實是一個地址,但是該地址是被保護的。
檢視棧內容
使程式崩潰只是進行驗證了漏洞,我們還能利用格式化漏洞來獲得記憶體的內容。
格式化字串函式會根據格式字串從棧上取值。由於32位系統上棧是由高地址向低地址增長,而printf()函式的引數是以逆序被壓入棧的,所以引數在記憶體中出現的順序在printf()呼叫時的順序是一樣的。
覆寫棧內容
1、%n 轉換指示符將 %n 當前已經成功寫入流或緩衝區中的字元個數儲存到地址由引數指定的整數中。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
void main(void)
{
int i;
char str[]="hello";
printf("%s %n\n",str,&i);
printf("%d\n",i);
}
out: hello\n 6
通常情況下,我們要需要覆寫的值是一個 shellcode 的地址,而這個地址往往是一個很大的數字。這時我們就需要通過使用具體的寬度或精度的轉換規範來控制寫入的字元個數,即在格式字串中加上一個十進位制整數來表示輸出的最小位數,如果實際位數大於定義的寬度,則按實際位數輸出,反之則以空格或 0 補齊(0 補齊時在寬度前加點. 或 0)。如:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<math.h>
void main(void)
{
int i;
printf("%10u%n\n",i,&i);
printf("%d\n",i);
printf("%.50u%n\n",i,&i);
printf("%d\n",i);
printf("%0100u%n\n",i,&i);
printf("%d\n",i);
}