這期內(nèi)容當(dāng)中小編將會(huì)給大家?guī)碛嘘P(guān)Linux pwn中如何格式化字符串漏洞,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
專注于為中小企業(yè)提供成都網(wǎng)站設(shè)計(jì)、網(wǎng)站制作服務(wù),電腦端+手機(jī)端+微信端的三站合一,更高效的管理,為中小企業(yè)孝昌免費(fèi)做網(wǎng)站提供優(yōu)質(zhì)的服務(wù)。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動(dòng)了上1000+企業(yè)的穩(wěn)健成長,幫助中小企業(yè)通過網(wǎng)站建設(shè)實(shí)現(xiàn)規(guī)模擴(kuò)充和轉(zhuǎn)變。
printf函數(shù)族是一個(gè)在C編程中比較常用的函數(shù)族。通常來說,我們會(huì)使用printf([格式化字符串],參數(shù))的形式來進(jìn)行調(diào)用,例如
char s[20] = “Hello world!\n”;printf(“%s”, s);
然而,有時(shí)候?yàn)榱耸∈乱矔?huì)寫成
char s[20] = “Hello world!\n”;printf(s);
事實(shí)上,這是一種非常危險(xiǎn)的寫法。由于printf函數(shù)族的設(shè)計(jì)缺陷,當(dāng)其第一個(gè)參數(shù)可被控制時(shí),攻擊者將有機(jī)會(huì)對任意內(nèi)存地址進(jìn)行讀寫操作。
首先我們來看一個(gè)自己寫的簡單例子~/format_x86/format_x86
這是一個(gè)代碼很簡單的程序,為了留后門,我調(diào)用system函數(shù)寫了一個(gè)showVersion()
.剩下的就是一個(gè)無線循環(huán)的讀寫,并使用有問題的方式調(diào)用了printf().正常來說,我們輸入什么都會(huì)被原樣輸出但是當(dāng)我們輸入一些特定的字符時(shí)輸出出現(xiàn)了變化。
可以看到,當(dāng)我們輸入printf可識(shí)別的格式化字符串時(shí),printf會(huì)將其作為格式化字符串進(jìn)行解析并輸出。原理很簡單,形如printf(“%s”, “Hello world”)
的使用形式會(huì)把第一個(gè)參數(shù)%s作為格式化字符串參數(shù)進(jìn)行解析,在這里由于我們直接用printf輸出一個(gè)變量,當(dāng)變量也正好是格式化字符串時(shí),自然就會(huì)被printf解析。那么后面輸出的內(nèi)容又是什么呢?我們繼續(xù)做實(shí)驗(yàn)。
我們直接在call _printf
一行下斷點(diǎn)然后以調(diào)試方式啟動(dòng)程序,然后輸入一大串%x.,輸出結(jié)果如圖此時(shí)的棧情況如圖我們很容易發(fā)現(xiàn)輸出的內(nèi)容正好是esp-4開始往下的一連串?dāng)?shù)據(jù)。所以理論上我們可以通過疊加%x來獲取有限范圍內(nèi)的棧數(shù)據(jù)。那么我們有可能泄露其他數(shù)據(jù)嗎?我們知道格式化字符串里有%s,用于輸出字符。其本質(zhì)上是讀取對應(yīng)的參數(shù),并作為指針解析,獲取到對應(yīng)地址的字符串輸出。我們先輸入一個(gè)%s觀察結(jié)果。我們看到輸出了%s后還接了一個(gè)換行,對應(yīng)的棧和數(shù)據(jù)如下:
棧頂是第一個(gè)參數(shù),也就是我們輸入的%s, 第二個(gè)參數(shù)的地址和第一個(gè)參數(shù)一樣,作為地址解析指向的還是%s和回車0x0A。由于此時(shí)我們可以通過輸入來操控棧,我們可以輸入一個(gè)地址,再讓%s正好對應(yīng)到這個(gè)地址,從而輸出地址指向的字符串,實(shí)現(xiàn)任意地址讀。
通過剛剛的調(diào)試我們可以發(fā)現(xiàn),我們的輸入從第六個(gè)參數(shù)開始(上圖從棧頂往下數(shù)第六個(gè)‘000A7325’ = %s\n\x00)
。所以我們可以構(gòu)造字符串”\x01\x80\x04\x08%x.%x.%x.%x.%s“
。這里前面的地址是ELF文件加載的地址08048000+1,為什么不是08048000后面再說,有興趣的可以自己試驗(yàn)一下。
由于字符串里包括了不可寫字符,我們沒辦法直接輸入,這回我們用pwntools+IDA附加的方式進(jìn)行調(diào)試。
我們成功地泄露出了地址0x08048001
內(nèi)的內(nèi)容。
經(jīng)過剛剛的試驗(yàn),我們用來泄露指定地址的payload對讀者來說應(yīng)該還是能夠理解的。由于我們的輸入本體恰好在printf讀取參數(shù)的第六個(gè)參數(shù)的位置,所以我們把地址布置在開頭,使其被printf當(dāng)做第六個(gè)參數(shù)。接下來是格式化字符串,使用%x處理掉第二到第五個(gè)參數(shù)(我們的輸入所在地址是第一個(gè)參數(shù)),使用%s將第六個(gè)參數(shù)作為地址解析。但是如果輸入長度有限制,而且我們的輸入位于printf的第幾十個(gè)參數(shù)之外要怎么辦呢?疊加%x顯然不現(xiàn)實(shí)。因此我們需要用到格式化字符串的另一個(gè)特性。
格式化字符串可以使用一種特殊的表示形式來指定處理第n個(gè)參數(shù),如輸出第五個(gè)參數(shù)可以寫為%4$s,第六個(gè)為%5$s,需要輸出第n個(gè)參數(shù)就是%(n-1)$[格式化控制符]。因此我們的payload可以簡化為”\x01\x80\x04\x08%5$s”
雖然我們可以利用格式化字符串漏洞達(dá)到任意地址讀,但是我們并不能直接通過讀取來利用漏洞getshell,我們需要任意地址寫。因此我們在本節(jié)要介紹格式化字符串的另一個(gè)特性——使用printf進(jìn)行寫入。
printf有一個(gè)特殊的格式化控制符%n,和其他控制輸出格式和內(nèi)容的格式化字符不同的是,這個(gè)格式化字符會(huì)將已輸出的字符數(shù)寫入到對應(yīng)參數(shù)的內(nèi)存中。我們將payload改成“\x8c\x97\x04\x08%5$n”
,其中0804978c是.bss段的首地址,一個(gè)可寫地址。執(zhí)行前該地址中的內(nèi)容是0printf執(zhí)行完之后該地址中的內(nèi)容變成了4,查看輸出發(fā)現(xiàn)輸出了四個(gè)字符“\x8c\x97\x04\x08”,回車沒有被計(jì)算在內(nèi)。我們再次修改payload為“\x8c\x97\x04\x08%2048c%5$n”
,成功把0804978c
里的內(nèi)容修改成0x804
現(xiàn)在我們已經(jīng)驗(yàn)證了任意地址讀寫,接下來可以構(gòu)造exp拿shell了。
由于我們可以任意地址寫,且程序里有system函數(shù),因此我們在這里可以直接選擇劫持一個(gè)函數(shù)的got表項(xiàng)為system的plt表項(xiàng),從而執(zhí)行system(“/bin/sh”)
。劫持哪一項(xiàng)呢?我們發(fā)現(xiàn)在got表中只有四個(gè)函數(shù),且printf函數(shù)可以單參數(shù)調(diào)用,參數(shù)又正好是我們輸入的。因此我們可以劫持printf為system,然后再次通過read讀取”/bin/sh”,此時(shí)printf(“/bin/sh”)
將會(huì)變成system(“/bin/sh”)
。根據(jù)之前的任意地址寫實(shí)驗(yàn),我們很容易構(gòu)造payload如下:
printf_got = 0x08049778system_plt = 0x08048320payload = p32(printf_got)+”%”+str(system_plt-4)+”c%5$n”
將payload發(fā)送過去,可以發(fā)現(xiàn)此時(shí)got表中的printf項(xiàng)已經(jīng)被劫持
此時(shí)再次發(fā)送”/bin/sh”就可以拿shell了。
但是這里還有一個(gè)問題,如果讀者真的自己調(diào)試了一遍就會(huì)發(fā)現(xiàn)單步執(zhí)行時(shí)call _printf
一行執(zhí)行時(shí)間額外的久,且最后io.interactive()
時(shí)屏幕上的光標(biāo)會(huì)不停閃爍很長一段時(shí)間,輸出大量的空字符。使用io.recvall()
讀取這些字符發(fā)現(xiàn)數(shù)據(jù)量高達(dá)128.28MB。這是因?yàn)槲覀兊膒ayload中會(huì)輸出多達(dá)134513436個(gè)字符
由于我們所有的試驗(yàn)都是在本機(jī)/虛擬機(jī)和docker之間進(jìn)行,所以不會(huì)受到網(wǎng)絡(luò)環(huán)境的影響。而在實(shí)際的比賽和漏洞利用環(huán)境中,一次性傳輸如此大量的數(shù)據(jù)可能會(huì)導(dǎo)致網(wǎng)絡(luò)卡頓甚至中斷連接。因此,我們必須換一種寫exp的方法。
我們知道,在64位下有%lld, %llx等方式來表示四字(qword)長度的數(shù)據(jù),而對稱地,我們也可以使用%hd, %hhx這樣的方式來表示字(word)和字節(jié)(byte)長度的數(shù)據(jù),對應(yīng)到%n上就是%hn, %hhn。為了防止修改的地址有誤導(dǎo)致程序崩潰,我們?nèi)匀恍枰淮涡园裧ot表中的printf項(xiàng)改掉,因此使用%hhn時(shí)我們就必須一次修改四個(gè)字節(jié)。那么我們就得重新構(gòu)造一下payload
首先我們給payload加上四個(gè)要修改的字節(jié)
printf_got = 0x08049778 system_plt = 0x08048320 payload = p32(printf_got)payload += p32(printf_got+1)payload += p32(printf_got+2)payload += p32(printf_got+3)
然后我們來修改第一位。由于x86和x86-64都是大端序,printf_got對應(yīng)的應(yīng)該是地址后兩位0x20
payload += “%”payload += str(0x20-16)payload += “c%5$hhn”
這時(shí)候我們已經(jīng)修改了0x08049778處的數(shù)據(jù)為0x20,接下來我們需要修改0x08049778
+2處的數(shù)據(jù)為0x83。由于我們已經(jīng)輸出了0x20個(gè)字節(jié)(16個(gè)字節(jié)的地址+0x20-16個(gè)%c),因此我們還需要輸出0x83-0x20個(gè)字節(jié)
payload += “%”payload += str(0x83-0x20)payload += “c%6$hhn”
繼續(xù)修改0x08049778+4,需要修改為0x04,然而我們前面已經(jīng)輸出了0x83個(gè)字節(jié),因此我們需要輸出到0x04+0x100=0x104字節(jié),截?cái)嗪笞兂?x04
payload += “%”payload += str(0x104-0x83)payload += “c%7$hhn”
修改0x08049778+6
payload += “%”payload += str(0x08-0x04)payload += “c%8$hhn”
最后的payload為'\x78\x97\x04\x08\x79\x97\x04\x08\x7a\x97\x04\x08\x7b\x97\x04\x08%16c%5$hhn%99c%6$hhn%129c%7$hhn%4c%8$hhn'
當(dāng)然,對于格式化字符串payload,pwntools也提供了一個(gè)可以直接使用的類Fmtstr,具體文檔見http://docs.pwntools.com/en/stable/fmtstr.html ,我們較常使用的功能是fmtstr_payload(offset, {address:data}, numbwritten=0, write_size=’byte’)
。第一個(gè)參數(shù)offset是第一個(gè)可控的棧偏移(不包含格式化字符串參數(shù)),代入我們的例子就是第六個(gè)參數(shù),所以是5。第二個(gè)字典看名字就可以理解,numbwritten
是指printf在格式化字符串之前輸出的數(shù)據(jù),比如printf(“Hello [var]”)
,此時(shí)在可控變量之前已經(jīng)輸出了“Hello ”共計(jì)六個(gè)字符,應(yīng)該設(shè)置參數(shù)值為6。第四個(gè)選擇用 %hhn(byte), %hn(word)還是%n(dword).在我們的例子里就可以寫成fmtstr_payload(5, {printf_got:system_plt})
獲取本例子shell的腳本見于附件,此處不再贅述。
學(xué)習(xí)完32位下的格式化字符串漏洞利用,我們繼續(xù)來看現(xiàn)在已經(jīng)變成主流的64位程序。我們打開例子~/format_x86-64/format_x86-64
。事實(shí)上,這個(gè)程序和上一節(jié)中使用的例子是同一個(gè)代碼文件,只不過編譯成了64位的形式。和上一個(gè)例子一樣,我們首先看一下可控制的棧地址偏移。根據(jù)上個(gè)例子,我們的輸入位于棧頂,所以是第一個(gè)參數(shù),偏移應(yīng)該是0.但是問題來了,棧頂不應(yīng)該是字符串地址嗎?別忘了64位的傳參順序是rdi, rsi, rdx, rcx, r8, r9,接下來才是棧,所以這里的偏移應(yīng)該是6.我們可以用一串%llx.來證明這一點(diǎn)。有了偏移,got表中的printf和plt表中的system也可以直接從程序中獲取,我們就可以使用fmtstr_payload來生成payload了。然而我們會(huì)發(fā)現(xiàn)這個(gè)payload無法修改got表中的printf項(xiàng)為plt的system然而查看內(nèi)存,發(fā)現(xiàn)payload并沒有問題那么問題出在哪呢?我們看一下printf的輸出
可以看到我們第一次輸入的payload只剩下空格(\x20),\x10和`(\x60)三個(gè)字符。這是為什么呢?
我們回頭看看payload,很容易發(fā)現(xiàn)緊接在\x20\x10\x60三個(gè)字符后面的是\x00,而\x00正是字符串結(jié)束符號(hào),這就是為什么我們在上一節(jié)中選擇0x08048001而不是0x08048000測試讀取。由于64位下用戶可見的內(nèi)存地址高位都帶有\(zhòng)x00(64位地址共16個(gè)16進(jìn)制數(shù)),所以使用之前構(gòu)造payload的方法顯然不可行,因此我們需要調(diào)整一下payload,把地址放到payload的最后。
由于地址中帶有\(zhòng)x00,所以這回就不能用%hhn分段寫了,因此我們的payload構(gòu)造如下
offset = 6printf_got = 0x00601020system_plt = 0x00400460payload = “%” + str(system_plt) + “c%6$lln” + p64(printf_got)
這個(gè)payload看起來好像沒什么問題,不過如果拿去測試,你就會(huì)發(fā)現(xiàn)用io.recvall()讀完輸出后程序馬上就會(huì)崩潰。這是為什么呢?如果你仔細(xì)看右下角的棧,你就會(huì)發(fā)現(xiàn)構(gòu)造好的地址錯(cuò)位了。
因此我們還需要調(diào)整一下payload,使地址前面的數(shù)據(jù)恰好為地址長度的倍數(shù)。當(dāng)然,地址所在offset也得調(diào)整。調(diào)整后的結(jié)果如下:
offset = 8printf_got = 0x00601020system_plt = 0x00400460payload = “a%” + str(system_plt-1) + “c%6$lln” + p64(printf_got)
這回就可以了。
從上面的兩個(gè)例子我們可以發(fā)現(xiàn),之所以能成功利用格式化字符串漏洞getshell,很多時(shí)候都是因?yàn)槌绦蛑写嬖谘h(huán)。如果程序中不存在循環(huán)呢?之前我們試過使用ROP技術(shù)劫持函數(shù)返回地址到start,這回我們將使用格式化字符串漏洞做到這一點(diǎn)。
我們打開例子~/MMA CTF 2nd 2016-greeting/greeting
同樣的,這個(gè)32位程序的got表中有system(看左邊),而且存在一個(gè)格式化字符串漏洞。計(jì)算偏移值和詳細(xì)構(gòu)造payload的步驟此處不再贅述。這個(gè)程序主要的問題在于我們需要用printf來觸發(fā)漏洞,然而我們從代碼中可以看到printf執(zhí)行完之后就不會(huì)再調(diào)用其他got表中的函數(shù),這就意味著即使成功觸發(fā)漏洞劫持got表也無法執(zhí)行system。這時(shí)候就需要我們想辦法讓程序可以再次循環(huán)。
之前的文章中我們就提到過,雖然寫代碼的時(shí)候我們以main函數(shù)作為程序入口,但是編譯成程序的時(shí)候入口并不是main函數(shù),而是start代碼段。事實(shí)上,start代碼段還會(huì)調(diào)用__libc_start_main
來做一些初始化工作,最后調(diào)用main函數(shù)并在main函數(shù)結(jié)束后做一些處理。其流程見于鏈接http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html
大致如下圖簡單地說,在main函數(shù)前會(huì)調(diào)用.init段代碼和.init_array
段的函數(shù)數(shù)組中每一個(gè)函數(shù)指針。同樣的,main函數(shù)結(jié)束后也會(huì)調(diào)用.fini段代碼和.fini._arrary
段的函數(shù)數(shù)組中的每一個(gè)函數(shù)指針。
而我們的目標(biāo)就是修改.fini_array數(shù)組的第一個(gè)元素為start。需要注意的是,這個(gè)數(shù)組的內(nèi)容在再次從start開始執(zhí)行后又會(huì)被修改,且程序可讀取的字節(jié)數(shù)有限,因此需要一次性修改兩個(gè)地址并且合理調(diào)整payload??捎玫哪_本同樣見于附件。
在checksec腳本的檢查項(xiàng)中,我們之前提到過了NX的作用,本節(jié)我們介紹一下另外兩個(gè)和Linux pwn中格式化字符串漏洞常用的利用手段相關(guān)的緩解機(jī)制RELRO和FORTIFY首先我們介紹一下RELRO,RELRO是重定位表只讀(Relocation Read Only)的縮寫。重定位表即我們經(jīng)常提到的ELF文件中的got表和plt表。關(guān)于這兩個(gè)表的來源和作用,我們會(huì)在介紹ret2dl-resolve的文章中詳細(xì)介紹?,F(xiàn)在我們首先需要知道的是這兩個(gè)表,正如其名,是為程序外部的函數(shù)和變量(不在程序里定義和實(shí)現(xiàn)的函數(shù)和變量,比如read。顯然你在自己的代碼里調(diào)用read函數(shù)的時(shí)候不用自己寫一個(gè)read函數(shù)的實(shí)現(xiàn))的重定位做準(zhǔn)備的。由于重定位需要額外的性能開銷,出于優(yōu)化考慮,一般來說程序會(huì)使用延遲加載,即外部函數(shù)的內(nèi)存地址是在第一次被調(diào)用時(shí)(例如read函數(shù),第一次調(diào)用即為程序第一次執(zhí)行call read)被找到并且填進(jìn)got表里面的。因此,got表必須是可寫的。但是got表可寫也給格式化字符串漏洞帶來了一個(gè)非常方便的利用方式,即修改got表。正如前面的文章所述,我們可以通過漏洞修改某個(gè)函數(shù)的got表項(xiàng)(比如puts)為system函數(shù)的地址,這樣一來,我們執(zhí)行call puts實(shí)際上調(diào)用的卻是system,相應(yīng)的,傳入的參數(shù)也給了system,從而可以執(zhí)行system(“/bin/sh”)??梢赃@么操作的程序使用checksec檢查的結(jié)果如下圖
其RELRO項(xiàng)為Partial RELRO.
而開頭的圖中顯示的RELRO: Full RELRO意即該程序的重定位表項(xiàng)全部只讀,無論是.got還是.got.plt都無法修改。我們找到這個(gè)程序(在《stack canary與繞過的思路》的練習(xí)題中),在call read上下斷點(diǎn),修改第一個(gè)參數(shù)buf為got表的地址以嘗試修改got表,程序不會(huì)報(bào)錯(cuò),但是數(shù)據(jù)未被修改,read函數(shù)返回了一個(gè)-1
顯然,當(dāng)程序開啟了Full RELRO保護(hù)之后,包括格式化字符串漏洞在內(nèi),試圖通過漏洞劫持got表的行為都將會(huì)被阻止。
接下來我們介紹另一個(gè)比較少見的保護(hù)措施FORTIFY,這是一個(gè)由GCC實(shí)現(xiàn)的源碼級別的保護(hù)機(jī)制,其功能是在編譯的時(shí)候檢查源碼以避免潛在的緩沖區(qū)溢出等錯(cuò)誤。簡單地說,加了這個(gè)保護(hù)之后(編譯時(shí)加上參數(shù)-D_FORTIFY_SOURCE=2)一些敏感函數(shù)如read, fgets, memcpy, printf等等可能導(dǎo)致漏洞出現(xiàn)的函數(shù)都會(huì)被替換成__read_chk, __fgets_chk, __memcpy_chk, __printf_chk
等。這些帶了chk的函數(shù)會(huì)檢查讀取/復(fù)制的字節(jié)長度是否超過緩沖區(qū)長度,通過檢查·諸如%n之類的字符串位置是否位于可能被用戶修改的可寫地址,避免了格式化字符串跳過某些參數(shù)(如直接%7$x)等方式來避免漏洞出現(xiàn)。開啟了FORTIFY保護(hù)的程序會(huì)被checksec檢出,此外,在反匯編時(shí)直接查看got表也會(huì)發(fā)現(xiàn)chk函數(shù)的存在
上述就是小編為大家分享的Linux pwn中如何格式化字符串漏洞了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識(shí),歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。