3 minutes
🇬🇧 CyberApocalypse 2025 - rev/endless_cycle
Table of contents
Description
The elves have whispered to Elowen that the key to the Dragon's Heart is never directly visible;
instead, there are clues in all the sounds and movements hidden in the world.
File Information
File: challenge
Type: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 4.4.0, BuildID[sha1]=155
a486c0ae2b9b06e0678261204f2a81b9c7947, stripped
In this challenge, we’ll be reversing a stripped 64-bit ELF executable.
Initial Code Analysis
Since the binary is stripped, there are no symbols available. We first need to identify the main
function by examining the __libc_start_main
call.
After renaming variables for clarity, we can see that the function operates in two distinct phases:
Phase 1: Dynamically Loading Code into Memory
The program uses two nested loops to generate code by seemingly randomly selecting characters to build a program in the memory_chunk
. This approach prevents effective static analysis since the actual executable code is generated at runtime.
Phase 2: Executing the Generated Code and Checking the Result
In the second phase, the program executes the code loaded into memory_chunk
and displays a validation message if the return value equals 1.
Runtime Analysis
When executing the program, we’re prompted with “What is the flag?” (to which I responded with “conflict”).
This prompt string doesn’t appear in our initial decompilation, indicating it’s part of the dynamically generated code in memory_chunk
.
Based on this observation, we can infer that the generated code:
- Prompts for user input
- Performs some manipulation on the input
- Compares it against the actual flag
- Returns 1 if the entered flag is correct, 0 otherwise
Dynamic Analysis through Debugging
Since static analysis is limited by the runtime code generation, we need to debug the program to examine the actual code in memory.
Locating the Code in Memory
To view the dynamically generated code, we run the program and interrupt it with CTRL+C
when it’s waiting for input. This reveals the current execution address in memory:
Examining the Disassembled Code
In the disassembly, we observe:
- Two values being loaded:
0x67616c6620656874
and0x2073692074616857
, which correspond to the ASCII string “What is the flag?” - Code that reads user input and XORs it with the constant
0xbeefcafe
- A comparison between the XORed input and a value stored at memory address
0x7ffff7ffa084
Extracting the Flag Bytes
The disassembly indicates the flag is 26 characters long (0x1a
in hexadecimal). We extract 26 bytes from the address where the expected flag value is stored:
Solution
Understanding that the flag has been XORed with the key 0xbeefcafe
, we can reverse this operation by applying the same XOR again to recover the original flag. When implementing the solution, we must be careful about endianness.
Solution Script
#!/usr/bin/python3
# Memory bytes containing the expected flag value (XORed)
memory_bytes = [
0xb6, 0x9e, 0xad, 0xc5, 0x92, 0xfa, 0xdf, 0xd5,
0xa1, 0xa8, 0xdc, 0xc7, 0xce, 0xa4, 0x8b, 0xe1,
0x8a, 0xa2, 0xdc, 0xe1, 0x89, 0xfa, 0x9d, 0xd2,
0x9a, 0xb7
]
# XOR key (in little-endian byte order)
key = [0xFE, 0xCA, 0xEF, 0xBE]
flag = ""
for i in range(len(memory_bytes)):
decoded_byte = memory_bytes[i] ^ key[i % 4]
flag += chr(decoded_byte)
print("Flag:", flag)
Conclusion
As a beginner in reverse engineering, I found this challenge very fun. It provided an opportunity to practice debugging techniques and learn about runtime code generation evasion techniques.
If you’ve identified any errors in my approach or know of a more efficient solution, please feel free to contact me on Discord!