Writeup: The Flag Is A Lie - CyberEdu CTF (Reverse)
Overview
A Unity IL2CPP Linux game (built with Unity 6000.3.8f1) presents a Portal-inspired testing facility where the player solves puzzles involving crate pushing. The readme warns: "You wake up in a suspicious testing facility. In exchange for solving a few puzzles, you'll be rewarded with a nice flag... but, can you really trust it?" — hinting that the in-game flag is fake and the real flag is hidden elsewhere.
Analysis
1. Identifying the Game Structure
The game ships as a standard Unity IL2CPP build:
TheFlagIsALie.x86_64 # Linux executable
GameAssembly.so # Compiled IL2CPP native code
TheFlagIsALie_Data/
global-metadata.dat # IL2CPP metadata (v39)
level0 # Main game scene
level1 # Secondary scene
Logs/session-20260225-111621.unrl # Encrypted recording file (~22 MB)
Using Cpp2IL, the game's C# classes were recovered. Key scripts found:
PlayerScript— Player movement/interactionExitGameOnTrigger— CallsApplication.Quiton trigger_o9b4256010e— Obfuscated name for FlagObfuscationController (42 methods)_oc1e208cb7a— AES key holder class
2. Understanding the FlagObfuscationController
The FlagObfuscationController tracks every game entity's position and records it to a .unrl binary file. The recording format is:
Header: "UNRL" + uint32 version (2) + byte encrypted_flag (1)
Records: [4-byte IV_len][16-byte IV][4-byte CT_len][48-byte ciphertext] ...
Each record is AES-CBC encrypted. Decrypted records contain:
type(1 byte): 1=Create, 2=Delete, 3=UpdateentityId(4 bytes): Entity identifiertimestamp(8 bytes): Double precision timeposition(12 bytes): Vector3 (x, y, z) — for Create/Updaterotation(16 bytes): Quaternion — for Create/Update
Total: 312,098 records tracking 9,568 entities over the play session.
3. Extracting the AES Key
The AES key is stored as 16 int32 fields in the _oc1e208cb7a MonoBehaviour, serialized in level1. By reading the raw serialized data at the known offset:
Offset 0x78: 26 00 00 00 c1 00 00 00 c1 00 00 00 56 00 00 00
Offset 0x88: a2 00 00 00 2d 00 00 00 31 00 00 00 74 00 00 00
Offset 0x98: e5 00 00 00 eb 00 00 00 f7 00 00 00 c1 00 00 00
Offset 0xa8: c8 00 00 00 b9 00 00 00 4b 00 00 00 6e 00 00 00
Each int32 is truncated to a byte (values < 256), yielding the 16-byte AES key:
26 c1 c1 56 a2 2d 31 74 e5 eb f7 c1 c8 b9 4b 6e
4. Decrypting Entity Positions
Using the extracted key to decrypt all records:
from Crypto.Cipher import AES
import struct
key = bytes([0x26, 0xc1, 0xc1, 0x56, 0xa2, 0x2d, 0x31, 0x74,
0xe5, 0xeb, 0xf7, 0xc1, 0xc8, 0xb9, 0x4b, 0x6e])
with open("session-20260225-111621.unrl", "rb") as f:
header = f.read(9) # UNRL header
while True:
iv_len_data = f.read(4)
if len(iv_len_data) < 4:
break
iv_len = struct.unpack('<I', iv_len_data)[0]
iv = f.read(iv_len)
ct_len = struct.unpack('<I', f.read(4))[0]
ct = f.read(ct_len)
cipher = AES.new(key, AES.MODE_CBC, iv)
pt = cipher.decrypt(ct)
pad_len = pt[-1]
pt = pt[:-pad_len] # Strip PKCS7 padding
rec_type = pt[0]
entity_id = struct.unpack('<i', pt[1:5])[0]
if rec_type in (1, 3) and len(pt) >= 25:
x, y, z = struct.unpack('<fff', pt[13:25])
# Track final position per entity
5. Discovering the Hidden Wall
Among 9,568 entities, entities 1-180 are special "wall" entities. All 180 are created at random positions initially but are updated throughout the session to their final positions on a wall at X ≈ 610.78 - 611.12.
Key observations:
- All 180 wall entities survive until the end (never deleted)
- Final positions: X ≈ 610-611, Y ≈ -1.7 to 6.1, Z ≈ 186 to 401
- All rotations are identity quaternions (0, 0, 0, 1)
6. Rendering the Flag
Plotting the 180 wall entity positions on the Y-Z plane (side view of the wall) initially produced unreadable characters. The key insight was that the text is mirrored — it's meant to be read from the other side of the wall (flipping the Z axis):
from PIL import Image, ImageDraw
# Z-flipped rendering (view from behind the wall)
for x, y, z in positions:
px = int((z_max - z) * scale) # Flip Z axis
py = int((y_max - y) * scale)
draw.rectangle(...)
This reveals the flag in pixel-font block letters:
UNR{CERTIFIED_CRATE_PUSHER}
The wall blocks form 3-pixel-wide, 5-pixel-tall characters arranged on a ~215-unit wide wall, readable only when viewed from behind (Z-axis mirrored).
Summary
| Step | Technique |
|---|---|
| 1 | Cpp2IL to recover C# class/method names from IL2CPP binary |
| 2 | Parse Unity serialized data in level1 to extract AES key |
| 3 | Decrypt 312K AES-CBC encrypted entity position records from .unrl |
| 4 | Track final positions of 180 wall entities through Create/Update records |
| 5 | Render wall entity positions as 2D image, Z-axis mirrored |
The challenge title "The Flag Is A Lie" (a Portal reference) hints that the flag shown in-game is fake. The real flag is hidden in the encrypted entity tracking data — 180 wall blocks that spell out the answer when viewed from the correct direction.
Flag
UNR{CERTIFIED_CRATE_PUSHER}