Cucumbers
Cucumbers
Platform: spbctf | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-24 | Status: Solved Techniques: byte_transform_reimplementation, check_oracle_emulation, docker_amd64_emulation, function_pointer_dispatch_recovery, per_bit_transform_application, static_analysis_objdump
Summary
Task: not-stripped x86-64 C crackme that reads a string and prints Correct! when 31 characters match a per-index check(i). Solution: recover a 5-entry function-pointer table and four movabs data constants from the disassembly, reimplement the byte transforms and the bit-driven check() in Python, and emit check(0..30) as the flag.
Cucumbers — spbctf (rev-kids20)
Description
cucumbers.elf — find the input that makes the program print
Correct!.
A dynamically linked, not stripped ELF 64-bit LSB x86-64 binary (GCC Ubuntu 5.4.0, Ubuntu 16.04) from the forkbomb.ru rev-kids20 set (re5). The program reads one string and validates it character by character; we must recover the accepted string.
Binary: https://rev-kids20.forkbomb.ru/files/rev/re5/cucumbers.elf
Analysis
main reads input with scanf("%32s", buf). For each index i from 0 to strlen(buf)-1 it computes an expected byte with check(i) and compares it against buf[i], incrementing a match counter on equality. It prints Correct! only when the counter equals 0x1f (31). So the flag is simply the concatenation check(0) .. check(30) — 31 bytes, no brute force needed.
The binary is not stripped, so nm exposes the structure directly: main, check, transform, and the transform helpers t0–t4.
Function-pointer dispatch table
A 5-entry function-pointer table lives in .data at 0x601060:
| index | symbol | address |
|---|---|---|
| 0 | t0 | 0x400646 |
| 1 | t1 | 0x400658 |
| 2 | t2 | 0x400674 |
| 3 | t3 | 0x4006a5 |
| 4 | t4 | 0x4006c4 |
The byte transforms (recovered from objdump -d -M intel)
All operations are byte-width (& 0xFF) unless noted:
- t0(a) =
a ^ 0x61 - t1(a):
edx = a + a(32-bitadd eax,eax),al = a >> 2(unsignedshr al,2), return(al ^ edx) & 0xFF - t2(a):
v = a; v += 0x25; v -= 0x28; v += 0x73; v ^= 0xAA; v <<= 7; v >>= 1(unsigned byte); v |= 0x10 - t3(a):
t0(~a & 0xFF) - t4(a):
v = ~a; v ^= 0xBC; v -= 0x0C; v = v >> 7; v |= 0x10; v += 0x05; v >>= 1(byte ops)
check(i) logic
c read-only • copy allowedacc = 0; // Loop 1: apply the transform selected by each set bit of i for (j = 0; j <= 5; j++) if ((i >> j) & 1) acc = funcs[j](acc); // i < 32 -> only bits 0..4, indices valid // Loop 2: count set bits and fold the data byte in for each count = 0; for (j = 0; j <= 5; j++) if ((i >> j) & 1) { count++; acc ^= databuf[i]; } // if an even number of bits were set, fold the data byte once more if ((count & 1) == 0) acc ^= databuf[i]; return acc & 0xFF;
The data buffer
databuf is built from four movabs constants written onto the stack at [rbp-0x30], each stored little-endian, followed by a 0 byte (33 bytes total):
0x4f746374ee74144d
0xfcbdace331fd8add ; movabs of -0x342531cce027523
0x5f5566663a615579
0x0b3e657964683a65
Solution
There is no input search to perform — every expected byte is deterministically produced by check(i). The plan:
- Reimplement
t0–t4and the.datafunction-pointer ordering in Python. - Rebuild
databuffrom the fourmovabsqwords (little-endian) plus the trailing0. - Reimplement
check(i)exactly (two bit-driven loops + parity fold). - Emit
check(0) .. check(len-1).
python read-only • copy alloweddef b(x): return x & 0xFF def t0(a): return b(b(a) ^ 0x61) def t1(a): a = b(a) edx = a + a # 32-bit add eax,eax al = a >> 2 # shr al,2 (unsigned byte) return b(al ^ edx) def t2(a): v = b(a) v = b(v + 0x25); v = b(v - 0x28); v = b(v + 0x73) v = b(v ^ 0xAA); v = b(v << 7); v = v >> 1 return b(v | 0x10) def t3(a): return t0(b(~b(a))) def t4(a): v = b(~b(a)); v = b(v ^ 0xBC); v = b(v - 0x0C) v = (v >> 7) | 0x10 v = b(v + 0x05) return b(v >> 1) funcs = [t0, t1, t2, t3, t4] import struct qwords = [0x4f746374ee74144d, 0xfcbdace331fd8add, 0x5f5566663a615579, 0x0b3e657964683a65] databuf = b"".join(struct.pack("<Q", q) for q in qwords) + b"\x00" databuf = list(databuf) def check(i): acc = 0 for j in range(6): if (i >> j) & 1 and j < len(funcs): acc = funcs[j](acc) cnt = 0 for j in range(6): if (i >> j) & 1: cnt += 1 acc = b(acc ^ databuf[i]) if (cnt & 1) == 0: acc = b(acc ^ databuf[i]) return b(acc) s = bytes(check(i) for i in range(len(databuf))) print(s.decode('latin1'))
Output:
b'REDACTED\x00\x00'
i.e. the 31-character flag REDACTED.
Verification
Native execution is impossible on macOS (exec format error, no qemu). Verified with Docker amd64 emulation:
bash read-only • copy allowedecho "REDACTED" | \ docker run --rm -i --platform linux/amd64 -v "$PWD":/w -w /w ubuntu:16.04 ./cucumbers.elf # -> Correct!