Flag: hiCTF{ru5t_vm_g4te_and_h4ndler_ch41n_152bad75bf}
Recovered from the live service (the server itself printed it):
$ python3 exploit.py --remote --host titan-relay-2409d376ae08.chall.hiddeninvestigations.net
[*] trying path b'Z:/flag.txt.' capsule 392 bytes
=== TITAN RELAY :: route consensus vm ===
[+] capsule accepted
hiCTF{ru5t_vm_g4te_and_h4ndler_ch41n_152bad75bf}
Artifact
titan_relay.exe - PE32+ x86-64 Rust console app, 286208 B. Served under Wine inside
ubuntu:24.04 via socat -T 60 TCP-LISTEN:7260,reuseaddr,fork EXEC:"./run_titan.sh work",su=ctf
(the platform proxies it to :1337 over TLS). The real flag lives at /flag.txt (mode 444);
the flag.txt shipped in the zip is a placeholder.
Console / surface
=== TITAN RELAY :: route consensus vm ===
1) load capsule
2) quit
Option > Capsule length > Capsule hex >
You upload a hex byte-string ("capsule") which the app treats as bytecode for a small VM.
Why a raw capsule never runs (the first trap)
The loader decrypts the uploaded capsule before executing it:
plain[i] = cipher[i] ^ ((0x71 + 0x3d * i) & 0xff)
(SIMD at 0x1400031bc–0x140003301; scalar tail at 0x1400032ec computes key = 0x71 + 0x3d*i.)
So a "valid" opcode like 0x51 sends 0x51 ^ 0x71 and the VM runs off the end →
silent [!] route fault contained. Capsules must be encrypted with that keystream.
VM layout
- Dispatch loop inlined in
mainat0x1400035a7–0x140003c6c; two decode tables:T1 @0x14003c164(0x71 - 0xB1),T2 @0x14003c0a0(0x21 - 0x51). - 8 registers at
state+0x18; memorymem[0..15]atstate+0x58; arelayarray atstate+0xd8whose first 3 entries leaklo32(canary),hi32(canary),hi32^valid- so the canary is obtainable from inside the VM.
The primitive: SUBMIT → host CreateFileW (arbitrary file read)
SUBMIT = opcode 0x51, handler 0x140003b26–0x140003b4e, which requires:
state+0x08 == state+0x00(canary echoed)state+0x10 == 0x1400018c0(the validator's PIE-relative address)reg6 != 0
then call 0x1400018c0 at 0x140003c62. The host validator expects the 0x20-byte struct at reg6:
| off | value |
|---|---|
| +0x00 | canary |
| +0x08 | pointer to the filename buffer |
| +0x10 | lo32(can) ^ hi32(can) |
| +0x14 | 0x0b (11 - the filename length) |
| +0x18 | +lo32(can) |
The buffer is de-obfuscated as buf[i] ^ canary_byte(map(i)) with map = 0..7,0..2 and the
constraint buf[11] == canary_byte(3); the resulting 11-byte NUL-free UTF-8 string is passed to
File::open and the contents are printed. Failure prints [!] order rejected.
Exploit
exploit.py builds a capsule that (1) reads the canary via RELAY, (2) derives the validator
address and all gate values, (3) sets up the 0x20-byte validator struct with the target filename,
and (4) issues SUBMIT; the whole capsule is XOR-encrypted with the loader keystream.
The target path used is the 11-byte Z:/flag.txt. (Wine maps Z: → /, so this opens
/flag.txt; the trailing byte satisfies the 11-byte length/NUL-free constraint).
1python3 exploit.py --local # proves the primitive against the shipped binary
2python3 exploit.py --remote --host <instance> # pulls the real flag
Verification
The flag came out of the live server process - the reliable for a pwn service
(the local run can only prove the file-read primitive, since the shipped flag.txt is
hiCTF{fake_flag_for_local_testing}).