bike
bike
Platform: B01Lersc | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-19 | Status: Solved Techniques: reverse_engineering, shannon_fano_tree_reconstruction, prefix_code_encoding, float32_precision_matching, bit_stream_packing
Summary
Task: 64-bit ELF builds a Shannon-Fano-like prefix-code tree from a global frequency table, walks 32 bytes of user input bit-by-bit, and compares the decoded leaf sequence to a fixed 35-byte buffer. Solution: faithfully reconstruct the tree using float32 arithmetic (critical), encode the expected bytes via the resulting codebook, and pack the 246-bit stream MSB-first into 31 input bytes.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
b01lersc| ID:20260419_b01lersc_bike - Tags: elf, x86_64, bit_packing, shannon_fano, prefix_code, float32, custom_tree, full_mitigations
- Indicators: 64-bit ELF with Full RELRO/Canary/NX/PIE/SHSTK/IBT, recursive build_tree(arr, lo, hi) that splits by cumulative frequency <= total/2.0, global freqs table of 128 entries at fixed VA with {byte, pad, float32} layout, fixed expected buffer compared against bit-walked leaf output, input walked bit-by-bit MSB-first, left=bit 0, right=bit 1, reset to root on leaf hit
- Source:
20260419_b01lersc_bike.md
Foothold
Vulnerability / Misconfiguration
- Reverse_engineering
- Shannon_fano_tree_reconstruction
- Prefix_code_encoding
- Float32_precision_matching
- Bit_stream_packing
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- reverse_engineering
- shannon_fano_tree_reconstruction
- prefix_code_encoding
- float32_precision_matching
- bit_stream_packing
- Tags: elf, x86_64, bit_packing, shannon_fano, prefix_code, float32, custom_tree, full_mitigations
Original Writeup
<details><summary>Click to expand original content</summary>Description
Get your bike down from the tree!!
A 64-bit ELF binary with all modern mitigations enabled (Full RELRO, Stack canary, NX, PIE, SHSTK, IBT). The program asks for input, decodes it bit-by-bit through a tree built from a global frequency table, and compares the decoded output against a fixed 35-byte buffer. The goal is to find the input that produces the expected decoded sequence — which spells out the flag.
Analysis
Program structure
The binary contains three functions:
setup— disables stdout buffering.build_tree(arr, lo, hi)— recursive constructor that returns a pointer to a node (leaf or internal).main— reads up to 31 bytes viafgets, walks them through the tree, and compares against the globalexpectedbuffer.
The global data
At VA 0x4020 there is a freqs array of 128 entries, each laid out as:
struct entry {
uint8_t byte; // +0x00
uint8_t pad[3]; // +0x01..+0x03
float freq; // +0x04 (float32)
}; // sizeof = 8
The table covers all 128 ASCII values in a specific (non-monotonic within tied freqs) order. Frequencies are in descending order and take one of the following values:
0.18, 0.16, 0.13, 0.12, 0.11, 0.10, 0.09, 0.08, 0.07, 0.06, 0.05, 0.04, 0.03, 0.02
At VA 0x4420 there is a 35-byte expected buffer:
53 03 64 52 55 3e 4a 6b 04 66 0f 26 4c 42 3c 18 58 4a 43 53 16 63 7f 0d 0a 36 0b 16 4c 44 05 2c 74 45 43
How build_tree works
This is a classic top-down Shannon–Fano-style split on an already sorted-by-frequency array:
Node *build_tree(entry *arr, int lo, int hi) {
if (lo == hi)
return leaf(arr[lo].byte, arr[lo].freq);
float total = 0.0f;
for (int i = lo; i < hi; i++) // NOTE: hi is EXCLUSIVE here
total += arr[i].freq;
float running = 0.0f;
int split = lo;
for (int i = lo; i <= hi; i++) { // NOTE: hi is INCLUSIVE here
if (arr[i].freq + running > total / 2.0f)
break;
running += arr[i].freq;
split = i;
}
Node *L = build_tree(arr, lo, split);
Node *R = build_tree(arr, split + 1, hi);
return internal(L, R);
}
Two subtleties are worth highlighting — both of which become gotchas during reimplementation:
- The total is summed over
[lo, hi)(exclusive upper bound) while the split-finding loop iterates over[lo, hi](inclusive upper bound). This asymmetry is intentional and must be preserved exactly. - All arithmetic is IEEE-754 single precision (
float32). Using Python's defaultfloat(which isfloat64) yields a slightly different tree at several boundaries whererunning + arr[i].freqsits neartotal/2.0— the rounding differs between the two widths.
How main decodes the input
Pseudocode:
current = root
count = 0
for i in range(strlen(input)):
for j in range(8):
if current.is_leaf():
output[count] = current.byte
count += 1
current = root
bit = (input[i] >> (7 - j)) & 1 # MSB first
current = current.left if bit == 0 else current.right
Then the binary compares output[0..count-1] against expected[0..count-1]. Printing Congrats on getting your bike down! on match.
Note that the leaf-emit happens at the start of each inner iteration, so the walker can pass through a leaf, emit it, and immediately continue into the next bit — this is the standard prefix-code decoding loop. Inputs that leave the walker mid-path at EOF are simply silently ignored by the final comparison.
Solution
The task is an inverse problem: given the fixed codebook (defined by the tree) and the target byte sequence expected, produce the MSB-first bit stream that encodes it, and pack those bits into bytes.
Steps:
- Parse the
.datasection of the ELF at runtime and extract bothfreqs(128 entries) andexpected(35 bytes). - Rebuild the tree with exactly the same control flow and
float32arithmetic as the C code. - Walk the tree to produce a codebook
byte -> "01..."(the left edge is0, the right edge is1). - Concatenate
codes[b]for each bytebinexpected— this yields 246 bits. - Pad to
31 * 8 = 248bits with0bits and pack MSB-first into 31 bytes.
With proper float32 arithmetic the result decodes to:
bctf{REDACTED|
The last character is | because the final two padding zero-bits landed inside its encoding. Since the final character of any CTF flag is almost always }, and } differs from | only in the low two bits, replacing the last byte gives the intended flag:
bctf{REDACTED}
The binary accepts this input because the trailing bits of }, after the 246 bits that completely consume expected, land partway through the tree and therefore never produce another leaf — so they are effectively don't-cares, exactly as the problem design allows.
Solver script
#!/usr/bin/env python3
"""Solve bike — reverse the Shannon-Fano-like decoder using float32 precision."""
import numpy as np
from elftools.elf.elffile import ELFFile
with open("bike", "rb") as f:
data = f.read()
f.seek(0)
elf = ELFFile(f)
for sec in elf.iter_sections():
if sec.name == ".data":
data_vma = sec["sh_addr"]
data_off = sec["sh_offset"]
break
freqs_off = data_off + (0x4020 - data_vma)
expected_off = data_off + (0x4420 - data_vma)
freqs = []
for i in range(128):
entry = data[freqs_off + i * 8 : freqs_off + (i + 1) * 8]
b = entry[0]
f_val = np.frombuffer(entry[4:8], dtype=np.float32)[0]
freqs.append((b, f_val))
expected = data[expected_off : expected_off + 35]
class Node:
__slots__ = ("byte", "left", "right")
def __init__(self, byte=None, left=None, right=None):
self.byte, self.left, self.right = byte, left, right
def is_leaf(self):
return self.left is None and self.right is None
f32 = np.float32
def build_tree(arr, lo, hi):
if lo == hi:
return Node(byte=arr[lo][0])
total = f32(0.0)
for i in range(lo, hi): # hi EXCLUSIVE
total = f32(total + arr[i][1])
running = f32(0.0)
split = lo
i = lo
while i <= hi: # hi INCLUSIVE
candidate = f32(arr[i][1] + running)
half = f32(total / f32(2.0))
if candidate > half:
break
running = f32(running + arr[i][1])
split = i
i += 1
return Node(left=build_tree(arr, lo, split),
right=build_tree(arr, split + 1, hi))
root = build_tree(freqs, 0, 0x7f)
codes = {}
def walk(node, path):
if node.is_leaf():
codes[node.byte] = path
return
walk(node.left, path + "0")
walk(node.right, path + "1")
walk(root, "")
bits = "".join(codes[b] for b in expected) # 246 bits
bits_padded = bits + "0" * (31 * 8 - len(bits))
out = bytearray(int(bits_padded[i:i+8], 2) for i in range(0, 31 * 8, 8))
# out == b'bctf{REDACTED|'
# Replace trailing '|' with '}' — the same encoded prefix matches,
# trailing bits fall off the end of the tree walk and are ignored.
out[-1] = ord('}')
print(bytes(out).decode())
Running it produces:
bctf{REDACTED}
And feeding the same bytes to the binary:
</details>$ ./bike < input.bin flag? Congrats on getting your bike down!
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR