← Back to Writeups
CUSTOMMediumReversing

refunc

XESXOR9/10/20264 min read
#reversing#custom#medium#computed_goto_vm_recognition#dead_code_discovery#docker_cross_arch_run#rel32_call_patching#string_xref_analysis#linux

refunc

Platform: spbctf | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-24 | Status: Solved Techniques: computed_goto_vm_recognition, dead_code_discovery, docker_cross_arch_run, rel32_call_patching, string_xref_analysis

Summary

Task: stripped x86-64 PIE ELF (kids/junior rev track) whose normal control flow never reaches the flag-printing code. Solution: locate the dead 'secret' function via string cross-references, then patch main's rel32 call to reroute control flow into it (avoiding a full reverse of the embedded bytecode VM) and run the patched binary under Docker linux/amd64.

refunc — spbctf

Description

File: https://rev-kids20.forkbomb.ru/files/rev/new7/refunc.elf Junior / kids rev track ("rev-kids20").

A stripped x86-64 PIE ELF is provided. Running it normally prompts for a name and only ever prints Hello, world. The flag-printing logic exists in the binary but is never reached. The task name refunc ("re-func" / reconstruct the function call path) is the hint: we must reroute execution into a dead "secret" function.

Analysis

Recon

file reports a stripped x86-64 LSB PIE executable, dynamically linked for GNU/Linux 3.2.0, BuildID sha1 bb42a3a68414d1e912b58880f9bdb18b695acd1a, ~102 KB. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

strings reveals the interesting rodata:

  • Enter your name:
  • Hello, world
  • Hello, admin! Do you remember secret function?
  • World, hello, its secret!

Notable imports: strncmp, sscanf, getchar, fopen/fclose, malloc, fprintf, syscall.

main() — 0x1286

main calls a name-reading helper fcn.0x120b (prints Enter your name:, mallocs 0x64 bytes, reads chars with getchar until 0/newline/0xff). It then checks cmp al, 0x61 ('a') on the first character. Both branches eventually fall into fcn.0x11e5, which only prints Hello, world. So the normal execution path can never print the flag — no input bypasses it. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Finding the secret function

Cross-referencing the rodata strings (0x9018 = "Hello, admin!..." and 0x9058 = "World, hello, its secret!") shows they are referenced only by two functions that nothing in the program ever calls:

  • fcn.0x11f8 — prints Hello, admin! Do you remember secret function?
  • fcn.0x12c4 — the secret function: prints World, hello, its secret! then call 0x5b2d.

These are pure dead code; no edge in the normal control-flow graph reaches them. The challenge name refunc points directly at reconstructing the missing call. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The VM at fcn.0x5b2d

fcn.0x5b2d (~3951 bytes) is a stack-based bytecode interpreter:

  • Initializes via fcn.0x509d, setting the instruction pointer (var_640h) to the bytecode program at 0xe4c0 plus a constant-table pointer.
  • Main dispatch: reads a byte at IP, sub eax, 5, cmp eax, 0xf9 (bounds check), then indexes a 250-entry jump table at 0x9cb8 (jmp rax) — a classic computed-goto dispatch compiled from a large switch.
  • Opcode handlers implement stack ops: push immediate, add, xor, pop (e.g. xor edx, ecx; mov [rax], edx then sub var_638h, 8).
  • A nested sub-dispatch (switch table at 0xa0a0, 10 cases) implements I/O opcodes: printf, puts, and raw write via syscall wrappers (fcn.0x1872 = write wrapper, fcn.0x6d2e = output helper). This is what emits the computed flag.
  • The bytecode at 0xe4c0 shows a clear pattern: a descending data byte (0x68='h', 0x67='g', 0x66='f', 0x65='e', ...) interleaved with opcodes 0x31, 0x1d, 0x05, 0x95, 0x7b, 0x56, 0x2c, 0xad. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Rather than fully emulate the VM, we just need to reach fcn.0x12c4, which runs the VM that prints the flag.

Solution

Patch main so that, instead of calling the name-reading helper, it calls the secret function fcn.0x12c4.

At file offset 0x1293, main contains call fcn.0x120b, encoded as the rel32 near-call e8 73 ff ff ff. We rewrite the target to 0x12c4:

rel32 = 0x12c4 - (0x1293 + 5) = 0x2c
new bytes: e8 2c 00 00 00

Patch script:

python
#!/usr/bin/env python3
data = bytearray(open('refunc.elf', 'rb').read())
off = 0x1293
assert data[off:off+5] == bytes.fromhex('e873ffffff')  # call fcn.0x120b
data[off:off+5] = bytes.fromhex('e82c000000')           # call 0x12c4
open('refunc_patched.elf', 'wb').write(data)

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

The host is macOS, which can't exec a Linux ELF, so run the patched binary under Docker (--platform linux/amd64):

bash
docker run --rm --platform linux/amd64 -v "$PWD":/w -w /w debian:bullseye-slim \
  bash -c "echo test | ./refunc_patched.elf"

Output:

World, hello, its secret!
start...
spbctf{REDACTED}
Hello, world

The patched call routes execution into fcn.0x12c4, which prints the banner and invokes the VM at 0x5b2d; the VM's bytecode program at 0xe4c0 computes and writes the flag — no full VM reversing required. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍