← Back to Writeups
CUSTOMMediumReversing

redcolor

XESXOR9/2/20265 min read
#reversing#custom#medium#elf_header_corruption_fix#entry_point_recovery#pie_start_recovery#vm_bytecode_disassembly#jump_table_dispatch#vaddr_to_file_offset_mapping#xor_decryption#static_emulation

redcolor

Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: elf_header_corruption_fix, entry_point_recovery, pie_start_recovery, vm_bytecode_disassembly, jump_table_dispatch, vaddr_to_file_offset_mapping, xor_decryption, static_emulation

Summary

Walkthrough of the CUSTOM Reversing redcolor. Task: stripped x86-64 PIE with a corrupted ELF entry point (e_entry=0xFFFFFFFFFFFFFFFE, the RED field in the header hex-dump image) that crashes at startup; main() is a custom stack-based bytecode VM.

Description

"OK, this binary doesn't work... I think it's because of this red color:" (with two links: an image = a hex dump of the ELF header with one field highlighted in RED, and the file redcolor.elf)

English summary: We are given a stripped x86-64 PIE binary that "doesn't work," and an image that is a hex dump of the ELF header with a single field highlighted in red. The red field is the corrupted ELF entry point. The goal is to fix the header so the binary runs, then reverse the embedded logic to recover the flag. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Analysis

Recon

$ file redcolor.elf
redcolor.elf: ELF 64-bit LSB pie executable, x86-64, dynamically linked,
              stripped, for GNU/Linux, BuildID..., GCC (Debian 9.3.0-10)

16032-byte stripped GCC-compiled PIE.

The provided PNG is a hex dump of the ELF header. The one field highlighted in red is e_entry (the entry point), shown as 0xFFFFFFFFFFFFFFFE. In the raw file this is confirmed at file offset 0x18:

$ xxd -s 0x18 -l 8 redcolor.elf
00000018: ffff ffff ffff ffff

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

That corrupted e_entry is exactly why the binary "doesn't work": the loader transfers control to an invalid address at startup, causing an immediate crash.

Semantic clue: "red color" == the red-highlighted ELF header field == the corrupted entry point. The flag's text (REDACTED) jokingly confirms this is really an ELF-forensics puzzle.

Fixing the entry point

This is a standard GCC PIE, so _start sits at the start of .text (vaddr 0x1060; it wires up __libc_start_main with main at 0x1145). The fix is to restore e_entry to 0x1060 by writing 60 10 00 00 00 00 00 00 at file offset 24 (0x18): ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

python
data = bytearray(open("redcolor.elf","rb").read())
data[0x18:0x20] = (0x1060).to_bytes(8,"little")   # e_entry -> _start
open("redcolor_fixed.elf","wb").write(data)

Running the patched binary on the macOS host was unreliable (Docker/Rosetta and faithful qemu-user both segfaulted inside glibc's first printf — an emulation/frame quirk), so the actual logic was recovered statically by emulating the binary's embedded VM.

The embedded VM

main() at 0x1145 is a custom stack-based bytecode VM interpreter. It executes a bytecode program stored in .data at vaddr 0x4060..0x4688. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

  • VM registers are frame-relative: pc = [rbp-0x170] (points into the bytecode), sp = [rbp-0x168] (VM stack pointer, init to rbp-0x160, 8-byte elements). A data/local base is at rbp-0x60.
  • An init routine at 0x1a2f does malloc(0x22) (34 bytes), stores the pointer at 0x46a8, and fills it with a hardcoded encrypted buffer.
  • Dispatch loop at 0x11b4 reads a 1-byte opcode from *pc and uses three int32-offset jump tables in .rodata for opcode ranges: 0xf7..0xfe (@0x200c), 0x5a..0xc5 (@0x202c), 1..0x3a (@0x21dc). Unmapped opcodes just advance pc. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Critical tooling detail — vaddr→file-offset mapping: in this file .data vaddr 0x4040 maps to file offset 0x3040 (delta 0x1000), while .rodata has vaddr == file offset. Getting this right was required to read the correct bytecode bytes.

Decoded opcode set

OpcodeMeaning
0x13 PUSHBpush u8 immediate as int32
0x18 PUSHQpush qword immediate
0x5b LEA_STKpush address rbp-0x60 + int32
0x84 LEA_Gpush address G + int32, G=[0x4680]=0x2004
0x07 DUP / 0xfe DUPQduplicate top
0x23 IMULmultiply
0x88/0xfd/0xbe/0xbfADD variants
0xb6 XOR32xor
0xf7 SETLsigned <
0x64/0x9a/0xa3stores via pointer (32/8/qword)
0x3a/0xfc/0xc5loads via pointer (qword/32/8)
0xfa TRUNC8 / 0x12 SEXT8 / 0xa8 MOVSLQwidth conversions
0x67 STC32store constant 32
0x7b IF0_BUFif operand==0 push buffer ptr 0x46a8
0x01 JMP (relative) / 0x2a JNZ (cond. relative)
0x5a HALT / 0x03 PRINTprintf("%s\n", buf)
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The bytecode algorithm

Recovered by disassembling the VM program:

  1. Store a 33-byte KEY into the frame at rbp-0x60+0x18+i.
  2. Loop i = 0..32 (while i < 0x21): buf[i] = (sext8(buf[i]) ^ sext8(key[i])) & 0xff. Sign-extending both operands then truncating the low byte is equivalent to a plain byte XOR.
  3. printf("%s\n", buf).

Solution

Spot the red e_entry field → patch file offset 0x18 to 0x1060 → emulate/reverse the tiny XOR VM. The whole encrypted buffer and key are hardcoded, so the flag is a one-line XOR: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

python
#!/usr/bin/env python3
# 1. (optional) fix the ELF entry point so the binary would actually run
data = bytearray(open("redcolor.elf", "rb").read())
data[0x18:0x20] = (0x1060).to_bytes(8, "little")  # e_entry: 0xFFFFFFFFFFFFFFFE -> _start
open("redcolor_fixed.elf", "wb").write(data)

# 2. hardcoded data recovered from the VM bytecode / init routine
KEY = [0x41,0x0a,0x10,0x61,0x94,0xca,0x76,0xb5,0xcb,0xcf,0xe5,0x3a,0x7c,
       0x39,0x4b,0x4a,0xcb,0x99,0x31,0x9d,0x22,0xf6,0xe5,0xc6,0xb5,0xb9,
       0x09,0x54,0xce,0x19,0xc2,0x2b,0xa4]
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

BUF = [0x12,0x5a,0x72,0x22,0xc0,0x8c,0x0d,0xd4,0x94,0xa3,0xd4,0x4e,0x08,
       0x55,0x78,0x15,0xa9,0xa8,0x45,0xc2,0x12,0x90,0xba,0xa0,0x85,0xcb,
       0x3a,0x3a,0xbd,0x28,0xa1,0x58,0xd9]  # 34th byte is a 0 terminator

# 3. the VM's XOR-decrypt loop
flag = bytes(b ^ k for b, k in zip(BUF, KEY))
print(flag.decode())   # SPbCTF{REDACTED}

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

Key Takeaways / Lessons

  • Mastering elf_header_corruption_fix, entry_point_recovery, pie_start_recovery, vm_bytecode_disassembly, jump_table_dispatch, vaddr_to_file_offset_mapping, xor_decryption, static_emulation was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-07-02 • CUSTOM • redcolor • Difficulty: Medium