[IO_FILE]伪造vtable 2018 HCTF the_end

分析

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正常交互。