1. 程式人生 > >在格式化字串的邊緣試探

在格式化字串的邊緣試探

格式化字串

format string基本介紹

此漏洞由printf類函式在使用時直接使用使用者可控字串作為格式化字串使用所導致。如printf(s),在執行時,使用者可以通過給予s特殊的值造成程式崩潰或洩露記憶體地址,乃至任意地址寫控制程式流程。

利用原理

獲取引數


在使用printf函式時,第一個引數為格式化字串指標,之後根據格式符向下尋找需要的引數並根據格式符進行解釋。但是在printf(s)過程中,若字串s中存在格式符,仍會按照次序將相應位置的變數進行解析並輸出。

特殊用法

在利用格式化字串漏洞的過程中有幾個至關重要的特殊用法

%5$d -> 5$表示指定使用第五個引數
%240c -> 表示輸出240個字元
%n -> 不產生輸出,將再次之前已經輸出的字元個數寫入對應引數所指向的記憶體


%7$hhn -> 將輸出字元個數寫入第七個引數所指向的區域,寫入一個位元組(x86)(half half)

利用%n向指定記憶體中寫是改變程式流程的關鍵,而其他格式符則用於洩露記憶體資訊或為配合%n寫資料。

例題  

 echo  


一個可以迴圈利用的格式化字串,可以通過%n將printf的地址改寫成為system函式的地址,下一次輸入”/bin/sh”後執行printf(“/bin/sh”)而實際上呼叫了system函式,獲得shell。

根據除錯中的棧結構,0xffdd69b0位置儲存格式化字串指標,其下有指向_IO_2_1_stdin_和_GLOBAL_OFFSET_TABLE_的指標。(如果有需要可以通過洩露這兩個變數查詢所使用的libc庫,進而獲得其他變數或函式的偏移。)
其下0xffdd69cc為輸入的格式化串的起始地址,可以在格式化字串中佈局需要改寫的記憶體地址,按照順序目標地址所在位置分別為printf函式

格式化字串的第7, 8, 9 ,10個引數,即分別使用%7$hhn-%10$hhn逐位元組覆寫(一次輸出太多位元組容易導致連結斷開,故使用hhn),除了起始16個位元組為地址,其餘使用%240c等進行佔位.
使用pwntools的fmtstr_payload()函式自動生成利用字串。

pwnlib.fmtstr.fmtstr_payload(offset, writes, numbwritten=0, write_size=’byte’) → str
Makes payload with given parameter. It can generate payload for 32 or 64 bits architectures. The size of the addr is taken from context.bits


Parameters:

  • offset (int) – the first formatter’s offset you control
  • writes (dict) – dict with addr, value {addr: value, addr2: value2}
  • numbwritten (int) – number of byte already written by the printf function
  • write_size (str) – must be byte, short or int. Tells if you want to write byte by byte, short by short or int by int (hhn, hn or n)

函式中offset即為棧中指向被改寫區域指標相對格式化字串指標的偏移(作為第幾個引數),在上圖中指向printf.got的指標位於格式化字串指標下第七個的位置,offset即為7.
writes是一個*字典 *,為要改寫的值和目標值,即用value的值替換掉記憶體中key指向的區域。
numbwritten即為在之前已經輸出的字元數,write_size為mei每次改寫的size,一般使用byte(hhn),以避免程式崩潰或連線斷開。
所以本題payload可以直接使用函式生成
payload = fmtstr_payload(7, {printf_got: system_plt})
//好像是因為system函式未呼叫過,got中沒有裝載地址,同時printf非首次呼叫不再經過plt查詢?我將下圖中printf和system的got和plt任意調換均不能成功。

echo2

變成了64bit且開啟了PIE保護。
stack
可以看到在呼叫printf函式時,棧中儲存著很多與全域性變數和函式相關的地址,可以利用格式化字串洩露兩個函式地址,從而查詢使用的libc版本(inndy網站中提供了題目使用的連結庫,故不需要此步驟),繼而確定elf基址和libc的偏移量。
在嘗試中發現stdout函式地址輸出為nil,setbuf和init無法從libc中獲取地址,所以採用c08處的main和be8處的__libc_start_main.
base
//這裡有一點是本地的時候be8處地址為__libc_start_main+241,但是遠端時,需要令接收到的值-240,才能滿足基址後三位為000.
0x7fffffffcbe8 —▸ 0x7ffff7a5a2b1 (__libc_start_main+241) ◂— mov edi, eax
排除掉隨機地址的障礙之後就可以像echo一樣覆蓋函式地址,但是由於64位程式中地址為0x7fffffffxxxx的形式,前兩個位元組為\x00,不能向printf函式中寫入,可以覆寫exit函式,但是傳遞’/bin/sh’有障礙。
這裡可以利用libc庫中固有的一個執行execv(‘/bin/sh’)的gadget。可以在libc中通過查詢字串/bin/sh找到。
gadget將此gadget的地址逐位寫到exit_got,即可獲得shell。有一點是fmtstr_payload生成的利用字串會把地址放在前面,而地址中存在\x00會導致printf中斷,所以不能正常使用,還是手動構造。

echo3

這道題的主要問題在於格式化字串不在stack中,也就無法直接利用payload構造改寫的地址。這類題目的策略在於利用棧中存在的指向特定區域的變數,不斷構造跳板,最終實現對指定地址的改寫。

一般改寫時選用此類棧變數的值為棧中的地址,且該值指向另一個棧地址的位置構造跳板,進行改寫。
一般步驟為:

1.leak棧地址和libc基址
2.將指向與目標地址接近的地址的棧變數連綴到跳板上
3.改寫上一步相近的地址為目標地址
4.對目標地址內容進行改寫

選擇上圖中0x1e和0x1f形式的位置作為跳板,因為要改寫棧變數為got地址(0x0804****), 所以將0x13,0x15處等內容和目標相近的地址連綴到跳板處,隨後改寫0x13,0x15的內容,最終實現

1e:78│ 0x0458 —▸ 053c —▸ 0x042c(0x13) -> 0x804a020(exit_got)

1f :7c│ 0x045c —▸ 0534 —▸ 0x0434(0x15) -> 0x804a022(exit_got+2)

使用printf修改記憶體時,每次最多對一個地址修改兩個位元組(hn),因而構造兩個跳板,分別指向目標地址的高低兩個位置(x64大概要構造4個跳板),多重跳板進行改寫時,每次都要注意指標的層次關係

echo3這道題,有幸學長提前進行了點播,指出了最大的一個坑:使用不同的libc執行時,棧結構有差異。所以在本地除錯的時候就通過指定libc,產生與遠端相同的棧結構.

io = process(‘./echo3’, env = {‘LD_PRELOAD’:’../libc-2.23.so.i386’})

另一個比較麻煩的點在於棧的位置由隨機數決定,所以每次執行的棧結構不盡相同,但只有三十種情況。解決辦法是挑選一個比較合適的棧結構(有豐富棧變數,指向libc全域性變數等),對照固定的棧結構進行設計,執行時多次執行,進行棧結構碰撞。
在這個程式中要求輸入五次然後exit,所以先改寫了exit_got,使呼叫exit的時候再跳轉到迴圈部分,從而實現無限輸入(利用exit跳轉回之後棧結構發生一定變化,需要適當調整),再覆寫printf_got的內容為system_addr即可。
弄完之後發現,其實沒必要改寫exit,根據上面的分析,完全可以利用改寫exit_got的機會直接改寫printf_got,並在4次輸入內完成,第五次直接輸入’/bin/sh’,獲得shell.

Ref: