格式化字符串漏洞原理
定義
維基百科:
格式化字符串(英語:format string),是一些程序設計語言在格式化輸出API函數中用于指定輸出參數的格式與相對位置的字符串參數,例如C、C++等程序設計語言的printf類函數,其中的轉換說明(conversion specification)用于把隨后對應的0個或多個函數參數轉換為相應的格式輸出;格式化字符串中轉換說明以外的其它字符原樣輸出。
格式化字符串我們在利用的時候主要分為三部分
- 格式化字符串函數
- 格式化字符串
- 后續參數,可選

常見的格式化字符串漏洞函數有
這些格式化函數利用format string格式化字符串來指定串的格式,在格式串內部使用一些如%開頭的格式說明符來占據一個位置,在后面的變參列表中提供相應的變量,最終函數就會使用相應位置的變量來代替那個說明符,產生一個調用者想要的字符串。
下面給出幾個比較關鍵的參數格式
- %x(%lx): 十六進制
- %p: 指針形式
- %s: 參數所指向的內存字符串
- %n: 將格式化串中該特殊字符之前的字符數量寫入參數中,獲取參數的地址
每一種具體的參數定義可以參考維基百科
接著我們簡單了解下參數定位
在正常的情況下格式化字符串所需的參數是依次往后索引的,如%p, %x其對應于第1、2個參數。
也有一些特殊情況,如%d$m形式:其中d代表數字(1, 2, ···),用來定位參數中的第d個參數(從1開始算);m為前面所述的關鍵參數格式之一(x, p, s, n, ···)。
例如
#include <stdio.h>
int main(int argc, char const *argv[])
{
int output = 0xcafebabe;
printf("%p, %p\n", 1, 2, &output);
printf("%1$p\n", 1, 2, &output);
printf("%2$p\n", 1, 2, &output);
printf("%3$p\n", 1, 2, &output);
return 0;
}
輸出結果如下
格式化字符串漏洞原理
之間介紹棧溢出時我們知道,在x86下,函數的參數是通過棧來傳遞的
我們先來看一個例子
#include <stdio.h>
int main()
{
printf("%s %x %s", "hello fmt\n", 0xcafebabe, "\n");
return 0;
}
我們在printf處斷下,查看當前棧信息

函數參數是從右向左入棧的,可以看到printf()函數的參數從高地址到低地址依次為"\n"指針--->0xcafebabe變量--->"hello fmt"字符串指針--->格式化字符串指針
跑完之后就是這樣的結果
下面我們觸發格式化字符串漏洞
根據 cdecl 的調用約定,在進入 printf() 函數之前,將參數從右到左依次壓棧。進入 printf() 之后,函數首先獲取第一個參數,一次讀取一個字符。如果字符不是 %,字符直接復制到輸出中。否則,讀取下一個非空字符,獲取相應的參數并解析輸出。(注意:% d 和 %d 是一樣的)
#include <stdio.h>
int main()
{
printf("%s %x %s %x %x %x", "hello fmt\n", 0xcafebabe, "\n");
return 0;
}
同樣在這里斷下

查看當前棧空間

運行查看結果

可以看到我們直接將棧上高地址的數據打印出來
如果大家都是好孩子,輸入正常的字符,程序就不會有問題。
我們可以總結出,其實格式字符串漏洞發生的條件就是格式字符串要求的參數和實際提供的參數不匹配。下面我們討論兩個問題:
- 為什么可以通過編譯?
- 因為
printf()函數的參數被定義為可變的。 - 為了發現不匹配的情況,編譯器需要理解
printf()是怎么工作的和格式字符串是什么。然而,編譯器并不知道這些。 - 有時格式字符串并不是固定的,它可能在程序執行中動態生成。
- 因為
printf()函數自己可以發現不匹配嗎?printf()函數從棧中取出參數,如果它需要 3 個,那它就取出 3 個。除非棧的邊界被標記了,否則printf()是不會知道它取出的參數比提供給它的參數多了。然而并沒有這樣的標記。
參考