redcolor
redcolor
Platform: spbctf | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-07-02 | Status: Solved 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
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. Solution: restore e_entry to _start (0x1060) at file offset 0x18, then statically disassemble/emulate the VM to recover a 33-byte XOR-decrypt loop yielding the flag.
redcolor — spbctf
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 read-only • copy alloweddata = 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 torbp-0x160, 8-byte elements). A data/local base is atrbp-0x60. - An init routine at
0x1a2fdoesmalloc(0x22)(34 bytes), stores the pointer at0x46a8, and fills it with a hardcoded encrypted buffer. - Dispatch loop at
0x11b4reads a 1-byte opcode from*pcand uses three int32-offset jump tables in.rodatafor opcode ranges:0xf7..0xfe(@0x200c),0x5a..0xc5(@0x202c),1..0x3a(@0x21dc). Unmapped opcodes just advancepc.
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
| Opcode | Meaning |
|---|---|
0x13 PUSHB | push u8 immediate as int32 |
0x18 PUSHQ | push qword immediate |
0x5b LEA_STK | push address rbp-0x60 + int32 |
0x84 LEA_G | push address G + int32, G=[0x4680]=0x2004 |
0x07 DUP / 0xfe DUPQ | duplicate top |
0x23 IMUL | multiply |
0x88/0xfd/0xbe/0xbf | ADD variants |
0xb6 XOR32 | xor |
0xf7 SETL | signed < |
0x64/0x9a/0xa3 | stores via pointer (32/8/qword) |
0x3a/0xfc/0xc5 | loads via pointer (qword/32/8) |
0xfa TRUNC8 / 0x12 SEXT8 / 0xa8 MOVSLQ | width conversions |
0x67 STC32 | store constant 32 |
0x7b IF0_BUF | if operand==0 push buffer ptr 0x46a8 |
0x01 JMP (relative) / 0x2a JNZ (cond. relative) | |
0x5a HALT / 0x03 PRINT | printf("%s\n", buf) |
| |
The bytecode algorithm
Recovered by disassembling the VM program:
- Store a 33-byte KEY into the frame at
rbp-0x60+0x18+i. - Loop
i = 0..32(whilei < 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. 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 read-only • copy allowed#!/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}