分析
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
signed int i; // [rsp+4h] [rbp-Ch]
void *buf; // [rsp+8h] [rbp-8h]
sleep(0);
printf("here is a gift %p, good luck ;)\n", &sleep);
fflush(_bss_start);
close(1);
close(2);
for ( i = 0; i <= 4; ++i )
{
read(0, &buf, 8uLL);
read(0, buf, 1uLL);
}
exit(1337);
}
分析题目,利用点在 main 函数中,且:
- 除了 canary 保护全开
- libc 基地址和 libc 版本
- 能够任意位置写 5 字节
此题劫持函数流有两种思路,一种是利用stdout
的函数表,一种是_dl_fini
函数中的函数指针,下面对于这两种解法进行描述。
思路分析1
利用exit
函数
利用的是在程序调用exit
后,会遍历_IO_list_all
,调用_IO_2_1_stdout_
下的vatable
中_setbuf
函数.
可以先修改两个字节在当前vtable附近伪造一个fake_vtable
,然后使用 3 个字节修改fake_vtable中
_setbuf的内容为
one_gadget`.
思路分析2
修改stdout函数表
因为glibc是2.23的,没有vtable的检查,因此修改函数表不会引起程序的错误。
查看exit函数的源码,exit中存在一条函数调用链,exit->__run_exit_handlers->_IO_cleanup->_IO_flush_all_lockp
。看到最后这个_IO_flush_all_lockp
就感觉应该可以利用这一点拿shell。这个函数里关键的源码是:
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
从源码中可以看到,如果可以控制stdin、stdout或者stderr中实现fp->_mode <= 0以及fp->_IO_write_ptr > fp->_IO_write_base同时修改vtable里面的_IO_OVERFLOW为one gadget,那么就可以顺利的劫持控制流。
经过测试,五字节的修改思路为:
- 修改stdout中_IO_write_ptr最后一字节,实现fp->_IO_write_ptr > fp->_IO_write_base
- 修改stdout中vtable的倒数第二字节,实现该伪造的_IO_OVERFLOW存在libc相关地址
- 最后修改伪造的_IO_OVERFLOW的后三字节为one gadget。
- 经过这五字节的修改,执行exit函数时会最终执行one gadget,获得shell。
思路分析3
修改_dl_fini
函数指针
还是查看exit函数的源码,一条调用链是exit->_dl_fini,查看_dl_fini源码:
_dl_fini (void)
{
...
#ifdef SHARED
int do_audit = 0;
again:
#endif
for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
{
/* Protect against concurrent loads and unloads. */
__rtld_lock_lock_recursive (GL(dl_load_lock));
unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
/* No need to do anything for empty namespaces or those used for
auditing DSOs. */
if (nloaded == 0
...
可以看到该函数调用了__rtld_lock_lock_recursive
函数,再看这个函数的定义:
# define __rtld_lock_lock_recursive(NAME) \
GL(dl_rtld_lock_recursive) (&(NAME).mutex)
查看宏GL的定义:
# if IS_IN (rtld)
# define GL(name) _rtld_local._##name
# else
# define GL(name) _rtld_global._##name
# endif
_rtld_global
是一个结构体,所以__rtld_lock_lock_recursive函数实际上是结构体中的一个函数指针,在gdb实际调试出现的指令为:
0x7f7420f80b27 <_dl_fini+119>:
lea rdi,[rip+0x215e1a] # 0x7f7421196948 <_rtld_global+2312>
=> 0x7f7420f80b2e <_dl_fini+126>:
call QWORD PTR [rip+0x216414] # 0x7f7421196f48 <_rtld_global+3848>
所以可以修改_rtld_global结构体的__rtld_lock_lock_recursive指针,将其修改为one gadget即可。
事实上,好像只要修改三个字节就可以实现了。
查找vtables偏移
ida中,先定位到.data
段,然后alt+T
搜索_IO_file_jumps
,在stderr附近即可找到。
.data:00000000003C56F8 dq offset _IO_file_jumps // vtables
.data:00000000003C5700 public stderr
.data:00000000003C5700 stderr dq offset _IO_2_1_stderr_
.data:00000000003C5700 ; DATA XREF: LOAD:000000000000BAF0↑o
.data:00000000003C5700 ; fclose+F2↑r ...
.data:00000000003C5708 public stdout
.data:00000000003C5708 stdout dq offset _IO_2_1_stdout_
.data:00000000003C5708 ; DATA XREF: LOAD:0000000000009F48↑o
.data:00000000003C5708 ; fclose+E9↑r ...
.data:00000000003C5710 public stdin
.data:00000000003C5710 stdin dq offset _IO_2_1_stdin_
.data:00000000003C5710 ; DATA XREF: LOAD:0000000000006DF8↑o
.data:00000000003C5710 ; fclose:loc_6D340↑r ...
.data:00000000003C5718 dq offset sub_20B70
.data:00000000003C5718 _data ends
.data:00000000003C5718
.bss:00000000003C5720 ; ===========================================================================
one_gadget的搜索技巧
首先在IDA中搜索libc的字符串/bin/sh
然后找到交叉引用的地方
先看第一个交叉引用
我们发现在
0x45294
的地方调用了execve("/bin/sh", &v20, environ);
,但经过调试,这个没有效果。最后我们在
0xF02B0
找到了execve("/bin/sh", &v38, environ);
这个有效的one_gadget调试
用pwndbg对开了pie的程序下一个基地址的断点:
b *$rebase(偏移)
PS
- 在cat flag的时候要进行一个
cat flag > &0
重定向文件流的操作才能看见flag,应该是把stdout关掉了的原因
或者 - 由于程序关闭了stdout,拿到shell后,使用
exec /bin/sh 1>&0
执行sh并重定向标准输出流到标准输入流,即可与shell正常交互。
网友评论