0ctf2017 | EasiestPrintf

Author Avatar
Aryb1n 7月 06, 2018

这题看起来不很复杂…

有个函数没见过,alloca

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  void *v3; // esp@3
  char buf; // [sp+3h] [bp-15h]@1
  int fd; // [sp+4h] [bp-14h]@1
  int v6; // [sp+8h] [bp-10h]@3
  int v7; // [sp+Ch] [bp-Ch]@1

  v7 = *MK_FP(__GS__, 20);
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stderr, 0, 2, 0);
  alarm(0x3Cu);
  sleep(3u);
  fd = open("/dev/urandom", 0);
  if ( read(fd, &buf, 1u) != 1 )
    exit(-1);
  close(fd);
  v6 = buf;
=>  v3 = alloca(16 * ((buf + 30) / 0x10u));
=>  do_read();
=>  leave();
}

据说这里的alloca能够手动随机化栈地址…没太懂…而且这…分配的v3好像没用上啊…

在之后的do_read里, 可以泄露一次地址的内容

leave里有一个格式化字符串的洞, 之后就直接exit了

而且这题除了PIE, 其他的保护都开了, 所以不能改got表…

按照之前学过的套路…那应该是改malloc_hook之类的东西…leak出来libc_base, 算出来malloc_hook的位置, 然后写one_gadget…可…该去写啥东西呢

不过, 只能leak一次, 根据一个地址, 然后在libcdatabase里找吗…

这题没有看到malloc或者free…改了malloc_hook or free_hook好像也没用…我们的printf或者exit会用这个吗…

woc, 好像…有可能, 诶?
但我们开始把stdin, stdout, stderr的缓冲区都设置为空了….printf还会和堆地址有关系吗

查了查…好像是说在输出宽度比0x1000 - 0x20大的时候…在调用printf的时候会会调用malloc, 在printf结束之前会调用free来释放, 就是会在printf调用结束之后发现一个堆(因为已经free掉了…所以只能看到一个topchunk了)

做了一下实验…还真的是…下面的例子确实会使用堆…

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main() {
    setbuf(stdin, 0);
    setbuf(stdout, 0);
    setbuf(stderr, 0);
    printf("%65505c", 50); // 这里如果是65504就不会产生堆..
    getchar();
    return 0;
}

所以, 这样就OK了

雾…这里注意一下, 如果不关掉stdout的缓冲区的话…那么只要调用printf, 就会留下一个堆上的缓冲区, 而且printf调用后, 不会立即被释放…之后还能用的, 如下图所示

db-peda$ heap all
Top Chunk:  0x804b408
Last Remainder:  0x0
0x804b000 SIZE=0x408 DATA[0x804b008] |ssss............................| INUSED PREV_INUSE
0x804b408 SIZE=0x20bf8 TOP_CHUNK

这个和上面说的, 即使取消stdout缓冲区, 在输出一定宽度内容的时候printf也会申请堆块是不一样的

之后找了一下wp…因为最开始选做这题是因为记得某次有人说这题可以通过改虚表来实现控制流劫持…这里学习一下

要跟着看一下printf源码, 打开gdb先打一下这个…之前好像记得哪里看到说这个不支持递归操作…所以指定到printf.c在的目录

gdb-peda$ directory /home/haibin/glibc/glibc-2.23/stdio-common/

emmmmm, 可以看到printf实际上是调用了vprintf, 而且第一个参数是stdout

26    int
27    __printf (const char *format, ...)
28    {
29      va_list arg;
30      int done;
31    
32      va_start (arg, format);
33      done = vfprintf (stdout, format, arg);
34      va_end (arg);
35    
36      return done;
37    }

在关于_IO_FILE的资料里说到…
printf, puts会调用到vtable里的_IO_sputn, 在gdb里观察一下

gdb-peda$ p *_IO_list_all
$3 = {
  file = {
    _flags = 0xfbad2087, 
    _IO_read_ptr = 0xf7f97d07 <_IO_2_1_stderr_+71> "", 
    _IO_read_end = 0xf7f97d07 <_IO_2_1_stderr_+71> "", 
    _IO_read_base = 0xf7f97d07 <_IO_2_1_stderr_+71> "", 
    _IO_write_base = 0xf7f97d07 <_IO_2_1_stderr_+71> "", 
    _IO_write_ptr = 0xf7f97d07 <_IO_2_1_stderr_+71> "", 
    _IO_write_end = 0xf7f97d07 <_IO_2_1_stderr_+71> "", 
    _IO_buf_base = 0xf7f97d07 <_IO_2_1_stderr_+71> "", 
    _IO_buf_end = 0xf7f97d08 <_IO_2_1_stderr_+72> "",
    _IO_save_base = 0x0, 
    _IO_backup_base = 0x0, 
    _IO_save_end = 0x0, 
    _markers = 0x0, 
    _chain = 0xf7f97d60 <_IO_2_1_stdout_>, 
    _fileno = 0x2, 
    _flags2 = 0x0, 
    _old_offset = 0xffffffff, 
    _cur_column = 0x0, 
    _vtable_offset = 0x0, 
    _shortbuf = "", 
    _lock = 0xf7f98864 <_IO_stdfile_2_lock>, 
    _offset = 0xffffffffffffffff, 
    _codecvt = 0x0, 
    _wide_data = 0xf7f97420 <_IO_wide_data_2>, 
    _freeres_list = 0x0, 
    _freeres_buf = 0x0, 
    __pad5 = 0x0, 
    _mode = 0x0, 
    _unused2 = '\000' <repeats 39 times>
  }, 
  vtable = 0xf7f96ac0 <_IO_file_jumps>
}

系统中打开的文件都是一个叫做_IO_FILE_plus的结构体

struct _IO_FILE_plus{
    struct _IO_FILE *file;
    const struct _IO_jump_t *vtable;
}

_IO_FILE结构体中的_chain, 把系统中的文件给链接起来

而这个链表头在_IO_list_all. 比如系统中打开一个文件file1, 那就是

_IO_list_all -> file1 -> stderr -> stdout -> stdin

我们看一下, 我这里vatble有啥

gdb-peda$ p *((struct _IO_FILE_plus*)(_IO_list_all.file._chain)).vtable 
$11 = {
  __dummy = 0x0, 
  __dummy2 = 0x0, 
  __finish = 0xf7e4e980 <_IO_new_file_finish>, 
  __overflow = 0xf7e4f3a0 <_IO_new_file_overflow>, 
  __underflow = 0xf7e4f140 <_IO_new_file_underflow>, 
  __uflow = 0xf7e50220 <__GI__IO_default_uflow>, 
  __pbackfail = 0xf7e510b0 <__GI__IO_default_pbackfail>, 
=>  __xsputn = 0xf7e4e5f0 <_IO_new_file_xsputn>,  
  __xsgetn = 0xf7e4e200 <__GI__IO_file_xsgetn>, 
  __seekoff = 0xf7e4d4a0 <_IO_new_file_seekoff>, 
  __seekpos = 0xf7e504c0 <_IO_default_seekpos>, 
  __setbuf = 0xf7e4d2e0 <_IO_new_file_setbuf>, 
  __sync = 0xf7e4d1d0 <_IO_new_file_sync>, 
  __doallocate = 0xf7e428c0 <__GI__IO_file_doallocate>, 
  __read = 0xf7e4e5a0 <__GI__IO_file_read>, 
  __write = 0xf7e4e050 <_IO_new_file_write>, 
  __seek = 0xf7e4dd90 <__GI__IO_file_seek>, 
  __close = 0xf7e4d2b0 <__GI__IO_file_close>, 
  __stat = 0xf7e4e030 <__GI__IO_file_stat>, 
  __showmanyc = 0xf7e51240 <_IO_default_showmanyc>, 
  __imbue = 0xf7e51250 <_IO_default_imbue>
}

第八个的这个__xsputn就是printf, puts, fwrite之类写函数会用到的

如果能改写这个指针…

等一下…我们在_IO_FILE的定义中…有一个const…那vatble的内容真的可以改吗…? 不能吧…但应该能伪造一个vtable.然后改变stdoutvatble指向我们的fake_vtable

这题难道还能构造一个vatble不成…?

查了一下…这个const好像是glibc-2.23加入的…而我刚好就是2.23, 好叭

可能比赛时候环境里libc版本比较低, 可以直接更改stdoutvtable里的__xsputnone_gadget…?

算了, 先睡觉…明天再找找wp