← Back to Writeups
CUSTOMEasyReversing

Почувствуй радугу (Feel the Rainbow)

XESXOR9/2/20264 min read
#reversing#custom#easy#disassembly_hash_order_recover#per_character_md5_reversal#rainbow_table_attack

Почувствуй радугу (Feel the Rainbow)

Platform: bug-makers | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-05-27 | Status: Solved Techniques: disassembly_hash_order_recovery, per_character_md5_reversal, rainbow_table_attack

Summary

Task: C++ ELF binary that checks each input character by computing its MD5 hash and comparing against 24 hardcoded hashes (17 unique). Solution: build rainbow table for single-byte MD5, recover full 24-hash sequence from main() disassembly to handle repeated characters.

Почувствуй радугу (Feel the Rainbow) — Bug Makers CTF

Description

Больше хешей – надежнее. Или нет? (More hashes – more secure. Or not?)

English summary: given a C++ ELF binary md5_flag_checker that reads user input and validates it character-by-character using MD5 hashes. The goal is to reverse the hashes and recover the flag.

Analysis

Binary reconnaissance

$ file md5_flag_checker
ELF 64-bit LSB pie executable, x86-64, dynamically linked, not stripped

Key symbols from the symbol table:

  • check_char_md5 — takes a single character and an expected hash, computes MD5 of the character and compares
  • calculate_md5 — standard MD5 implementation
  • MD5_Transform — MD5 block transform function ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The MD5 implementation uses standard initialization constants (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476), confirming it is unmodified MD5.

Algorithm

  1. Binary reads user input and expects exactly 24 characters
  2. For each character at position i, it calls check_char_md5(input[i], expected_hash[i])
  3. The function computes MD5(single_byte) and compares the hex digest against the hardcoded expected hash
  4. If all 24 characters match, the input is accepted as the correct flag

The trap: unique vs. total hashes

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

Running strings on the binary reveals only 17 unique 32-character hex strings in the .rodata section. However, the flag is 24 characters long, meaning some hashes are reused for repeated characters.

Extracting only unique hashes and building a set loses the ordering and duplicates, producing a wrong result like BugCTF{r3v_h4sck} (17 chars). The correct approach requires disassembling main() to recover the exact sequence of 24 hash pointer references.

Hash address references from main() disassembly

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

The 24 lea instructions in main() reference hash strings at these addresses:

0x6168, 0x6190, 0x61b8, 0x61e0, 0x6208, 0x6230, 0x6258, 0x6280,
0x62a8, 0x62d0, 0x62f8, 0x6320, 0x6348, 0x6370,
0x6320, 0x62f8,  ← repeats: h, _
0x6398, 0x6280, 0x6348, 0x6398, 0x63c0, 0x62a8, 0x6280, 0x63e8

Note addresses 0x6320 (h), 0x62f8 (_), 0x6280 (r), 0x6348 (4), 0x6398 (c), 0x62a8 (3) each appear multiple times.

Solution

Step 1: Extract all 24 hashes in order

From the disassembly, the ordered hash sequence is: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

#HashChar
19d5ed678fe57bcca610140957afab571B
27b774effe4a349c6dd82ad4f4f21d34cu
3b2f5ff47436671b6e533d8dc3614845dg
40d61f8370cad1d412f80b84d143e1257C
5b9ece18c950afbfa6b0fdbfa4ff731d3T
6800618943025315f869e4e1f09471012F
7f95b70fdc3088560732a5ac135644506{
84b43b0aee35624cd95b910189b3dc231r
9eccbc87e4b5ce2fe28308fd9f2a7baf33
109e3669d19b675bd57058fd4664205d2av
11b14a7b8059d9c055954c92674ce60032_
122510c39011c5be704182423e3a695e91h
13a87ff679a2f3e71d9181a67b7542122c4
1403c7c0ace395d80182db07ae2c30f034s
152510c39011c5be704182423e3a695e91h
16b14a7b8059d9c055954c92674ce60032_
174a8a08f09d37b73795649038408b5f33c
184b43b0aee35624cd95b910189b3dc231r
19a87ff679a2f3e71d9181a67b7542122c4
204a8a08f09d37b73795649038408b5f33c
218ce4b16b22b58894aa86c421e8759df3k
22eccbc87e4b5ce2fe28308fd9f2a7baf33
234b43b0aee35624cd95b910189b3dc231r
24cbb184dd8e05c9709e5dcaedaa0495cf}
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 2: Build rainbow table and reverse

python
#!/usr/bin/env python3
import hashlib

# All 24 hashes in order from main() disassembly
hashes = [
    '9d5ed678fe57bcca610140957afab571',  # B
    '7b774effe4a349c6dd82ad4f4f21d34c',  # u
    'b2f5ff47436671b6e533d8dc3614845d',  # g
    '0d61f8370cad1d412f80b84d143e1257',  # C
    'b9ece18c950afbfa6b0fdbfa4ff731d3',  # T
    '800618943025315f869e4e1f09471012',  # F
    'f95b70fdc3088560732a5ac135644506',  # {
    '4b43b0aee35624cd95b910189b3dc231',  # r
    'eccbc87e4b5ce2fe28308fd9f2a7baf3',  # 3
    '9e3669d19b675bd57058fd4664205d2a',  # v
    'b14a7b8059d9c055954c92674ce60032',  # _
    '2510c39011c5be704182423e3a695e91',  # h
    'a87ff679a2f3e71d9181a67b7542122c',  # 4
    '03c7c0ace395d80182db07ae2c30f034',  # s
    '2510c39011c5be704182423e3a695e91',  # h (repeat)
    'b14a7b8059d9c055954c92674ce60032',  # _ (repeat)
    '4a8a08f09d37b73795649038408b5f33',  # c
    '4b43b0aee35624cd95b910189b3dc231',  # r (repeat)
    'a87ff679a2f3e71d9181a67b7542122c',  # 4 (repeat)
    '4a8a08f09d37b73795649038408b5f33',  # c (repeat)
    '8ce4b16b22b58894aa86c421e8759df3',  # k
    'eccbc87e4b5ce2fe28308fd9f2a7baf3',  # 3 (repeat)
    '4b43b0aee35624cd95b910189b3dc231',  # r (repeat)
    'cbb184dd8e05c9709e5dcaedaa0495cf',  # }
]
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Build rainbow table: MD5(single_byte) -> character
lookup = {}
for i in range(32, 127):
    c = chr(i)
    h = hashlib.md5(c.encode()).hexdigest()
    lookup[h] = c

# Reverse all 24 hashes in order
flag = ''.join(lookup.get(h, '?') for h in hashes)
print(flag)  # BugCTF{REDACTED}

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