← Back to Writeups
CUSTOMMediumReversing

Cucumbers

XESXOR9/2/20264 min read
#reversing#custom#medium#byte_transform_reimplementatio#check_oracle_emulation#docker_amd64_emulation#function_pointer_dispatch_reco#per_bit_transform_application#static_analysis_objdump

Cucumbers

Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: byte_transform_reimplementation, check_oracle_emulation, docker_amd64_emulation, function_pointer_dispatch_recovery, per_bit_transform_application, static_analysis_objdump

Summary

Walkthrough of the CUSTOM Reversing Cucumbers. 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 const

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:

indexsymboladdress
0t00x400646
1t10x400658
2t20x400674
3t30x4006a5
4t40x4006c4

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-bit add eax,eax), al = a >> 2 (unsigned shr 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
acc = 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:

  1. Reimplement t0–t4 and the .data function-pointer ordering in Python.
  2. Rebuild databuf from the four movabs qwords (little-endian) plus the trailing 0.
  3. Reimplement check(i) exactly (two bit-driven loops + parity fold).
  4. Emit check(0) .. check(len-1).
python
def 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
echo "REDACTED" | \
  docker run --rm -i --platform linux/amd64 -v "$PWD":/w -w /w ubuntu:16.04 ./cucumbers.elf
# -> Correct!

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering byte_transform_reimplementation, check_oracle_emulation, docker_amd64_emulation, function_pointer_dispatch_recovery, per_bit_transform_application, static_analysis_objdump was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-06-24 • CUSTOM • Cucumbers • Difficulty: Medium