diff --git a/doc/1.5.1_c_basic.md b/doc/1.5.1_c_basic.md index 9bcdd27..44741b8 100644 --- a/doc/1.5.1_c_basic.md +++ b/doc/1.5.1_c_basic.md @@ -174,7 +174,7 @@ $ cat /usr/include/limits.h ## 格式化输出函数 #### 格式化输出函数 C 标准中定义了下面的格式化输出函数(参考 `man 3 printf`): -```text +```c #include int printf(const char *format, ...); @@ -264,7 +264,7 @@ int vsnprintf(char *str, size_t size, const char *format, va_list ap); #### 例子 ```c printf("Hello %%"); // "Hello %" -printf("Hello world!"); // "Hello world!" +printf("Hello World!"); // "Hello World!" printf("Number: %d", 123); // "Number: 123" printf("%s %s", "Format", "Strings"); // "Format Strings" diff --git a/doc/3.3.1_format_string.md b/doc/3.3.1_format_string.md index b69d0d9..469b3b1 100644 --- a/doc/3.3.1_format_string.md +++ b/doc/3.3.1_format_string.md @@ -7,10 +7,261 @@ ## 格式化输出函数和格式字符串 -在 C 语言基础章节中,我们详细介绍了格式化输出函数和格式化字符串的内容。在开始探索格式化字符串漏洞之前,强烈建议回顾该章节。 +在 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$ 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 (: 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()` 之后,函数首先获取第一个参数,一次读取一个字符。如果字符不是 `%`,字符直接复制到输出中。否则,读取下一个字符,获取相应的参数并解析输出。 + +接下来我们修改一下上面的程序,给格式字符串加上 `%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$ +[----------------------------------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$ +[----------------------------------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()` 是不会知道它取出的参数比提供给它的参数多了。然而并没有这样的标记。 ## 格式化字符串漏洞示例