Ringtrue
Ringtrue
Platform: CACTF2026 | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-07-27 | Status: Solved Techniques: neural_network_inversion, smt_solving, integer_linear_model, sha256_keystream, xor_decryption, elf_data_extraction, objdump_disassembly
Summary
Task: not-stripped x86-64 ELF implements a quantized int8 MLP (8-8-8-8) whose output is checked for exact integer equality against an embedded reference signature; all weights/biases live readably in .data. Solution: extract parameters, model the piecewise-linear network in z3 to invert the input ('SaltCrwn'), then reproduce the SHA-256 keystream XOR to decrypt the sealed flag.
Ringtrue — CACTF2026
Description
House Eastreach found one of the broken Signet's shards. Instead of melting it down, they put it to work. The shard still holds a bit of the dragon Astrael's power, so Eastreach built a small device around it and taught it to listen for one voice. Play the right eight tones and the shard rings true, the vow sealed inside opens, and it can carry a king's command again. Play anything else and nothing happens. Sera can't guess the tones. There are eight of them, each can be any value, and far too many to try one by one. But Eastreach was careless and left the device open to read, with everything it was taught still written plainly inside. That carelessness is the whole opening she needs.
English summary: A single ELF binary (ringtrue) reads eight space-separated integer "tones". It runs them through a small quantized neural network ("one voice" detector). Only the exactly correct 8-integer input makes the device "ring true" and decrypt a sealed vow (the flag). All learned parameters are stored in the binary in cleartext — the narrative hint ("everything it was taught still written plainly inside") points at inverting the network rather than brute-forcing.
Analysis
Recon
ringtrue: ELF 64-bit LSB PIE, x86-64, dynamically linked, not stripped, GCC (Ubuntu 13.3.0), BuildID present.- Strings show a themed "CinderOS / House Eastreach - Relic Division / Brine Signet Resonance Console" boot sequence. Key lines:
npu: resonance_core.tflm - MLP 8-8-8-8, int8, leaky ... oknpu: weights symmetric int8 (zp=0), per-tensor scalecrypto: vault-seal = xor-stream, key = attunement-derived
- Prompt reads "eight tone samples, space-separated".
- Named data symbols (
nm/objdump -t):W_SCALE(12 B = 3 floats),L0_W/L1_W/L2_W(64 B each = 8×8 int8 weight matrices),L0_B/L1_B/L2_B(32 B each = 8 int32 biases),ECHO_S(64 B = 8 int64 reference signature),VOW_LEN(4 B = 30),VOW_CIPHER(30 B). Functions:main,dense,draw_ring,banner,boot_line,clear_screen,nap.
Understanding the binary (objdump -d -M intel)
The core is a quantized MLP 8-8-8-8 acting as a "one voice" detector.
- Input:
__isoc99_sscanfwith format%d %d %d %d %d %d %d %dreads exactly 8 signed ints; the return value must equal 8 (else "needs exactly eight samples"). dense(W, B, X, out)(at0x1309):rdi=W(int8),rsi=B(int32 viamovsxd),rdx=X(int64 elements),rcx=out(int64). For each outputjin 0..7:out[j] = B[j] + sum_{i=0..7} W_int8[j*8 + i] * X_int64[i].Wis indexed as bytes (movsx byte),Xas qwords (8*rax).- Activation after L0 and L1 (loops at
0x1acband0x1b12):a = out[j]; rcx = 2*a; if a < 0: a = 2*aviatest rax,rax; cmovs rax, rcx. Sof(a) = a if a>=0 else 2*a— the "leaky" from the boot string, with slope 2 on the negative side. No activation after L2. - Final check (
0x1b54–0x1b7e): compares all 8 int64 outputs of L2 againstECHO_S;ebxstays 1 only if every qword matches (cmovneclears it on any mismatch). Ifebx != 0→ device "RINGS TRUE", decrypt+print the sealed vow; else "nothing happens". - Red herring: the
W_SCALEfloats (0.015625,0.0078119…,0.011719…) drive only a cosmetic on-screen resonance/echo visualization, not the correctness check. The verification is pure exact integer equality againstECHO_S, so the whole problem reduces to integer linear algebra with a piecewise-linear activation.
Solution
1. Extract constants from .data
.data vaddr 0x5000 = file offset 0x5000. Parsed directly in Python (no execution needed):
W_SCALE:(0.015625, 0.0078119998797774315, 0.011718999594449997)— cosmetic only.VOW_LEN:30VOW_CIPHER(hex):1c9d59313d0da8fbca056a2ac0b6a51172dfd75c2b216c9dc67296b48d6fECHO_S(8× int64):(1542223, 574187, -2694563, -3518303, 383776, 576877, 2637871, -2518822)L0_B:(541, -548, 1968, 1610, 68, -1078, -42, -2020)L1_B:(-1709, 704, 1565, 304, -209, 1690, -1151, 245)L2_B:(506, -234, 1551, 1479, 1058, 1130, 1320, 330)L0_W,L1_W,L2_W: 8×8 int8 matrices (values in the solve script below).
2. Invert the network with z3
Brute-forcing 8 arbitrary integers is infeasible ("far too many to try one by one"), but every learned parameter is readable, so the network can be inverted directly with an SMT solver. Model each layer with Int variables and encode the activation as If(v >= 0, v, 2*v); bound inputs to [-1000, 1000] and assert L2 output == ECHO_S.
Solver returns sat with X = [83, 97, 108, 116, 67, 114, 119, 110] = ASCII "SaltCrwn" (Salt Crown) — thematically the correct "voice" for House Eastreach's Brine Signet.
python read-only • copy allowed#!/usr/bin/env python3 # solve.py — invert the int8 MLP and decrypt the sealed vow from z3 import Int, Solver, If, sat import hashlib, struct L0_W = [5,13,10,14,1,0,-15,12, 4,4,3,1,11,-3,-10,-1, 10,3,1,-11,-12,5,12,14, 10,2,3,7,7,-6,-1,-4, 16,10,-10,12,5,6,2,-5, 15,-1,-7,-3,9,7,1,-5, 6,-12,-8,-10,0,7,-15,15, 4,-9,-4,12,-4,16,15,5] L1_W = [14,-1,13,-3,-15,10,1,-14, 16,16,12,-7,14,5,-8,-7, -2,3,7,15,-12,11,1,12, -9,-8,-14,-2,-3,3,-1,0, -11,-13,6,7,-3,-4,0,16, -4,2,-4,11,16,-10,5,-5, -8,-4,1,8,3,-14,-12,6, -13,-12,5,13,14,-15,15,5] L2_W = [9,-15,0,-16,2,-15,6,-6, 8,0,-5,-4,-1,10,-7,3, -9,-15,-14,-7,-5,-4,-11,9, 9,-9,15,13,1,-8,1,-3, -13,9,-8,4,-16,11,11,12, -10,-12,-1,-10,-1,11,6,-4, 6,-4,-5,-13,-3,8,6,-4, -2,-12,-15,4,16,-9,13,-13] L0_B = [541,-548,1968,1610,68,-1078,-42,-2020] L1_B = [-1709,704,1565,304,-209,1690,-1151,245] L2_B = [506,-234,1551,1479,1058,1130,1320,330] ECHO_S = [1542223,574187,-2694563,-3518303,383776,576877,2637871,-2518822] def leaky(v): return If(v >= 0, v, 2*v) def dense(W, B, X): return [B[j] + sum(W[j*8+i]*X[i] for i in range(8)) for j in range(8)] s = Solver() X = [Int(f"x{i}") for i in range(8)] for x in X: s.add(x >= -1000, x <= 1000) h0 = [leaky(v) for v in dense(L0_W, L0_B, X)] h1 = [leaky(v) for v in dense(L1_W, L1_B, h0)] out = dense(L2_W, L2_B, h1) # no activation on L2 for j in range(8): s.add(out[j] == ECHO_S[j]) assert s.check() == sat m = s.model() tones = [m[X[i]].as_long() for i in range(8)] print("tones:", tones, "->", bytes(tones)) # "SaltCrwn" # --- decrypt the sealed vow (flag) --- seed = bytes(t & 0xff for t in tones) # low byte of each input cipher = bytes.fromhex("1c9d59313d0da8fbca056a2ac0b6a51172dfd75c2b216c9dc67296b48d6f") ks = hashlib.sha256(seed + struct.pack("<I", 0)).digest() # 30 bytes -> block 0 only flag = bytes(c ^ k for c, k in zip(cipher, ks)) print(flag.decode())
3. Decrypt the sealed vow (flag)
On a correct match the binary derives an SHA-256 keystream and XORs VOW_CIPHER (SHA-256 round constants + message-schedule code visible in main around 0x1ce5–0x1e64; final XOR loop at 0x1ee0):
seed= the low byte of each of the 8 input integers ="SaltCrwn"(8 bytes).- For each 32-byte output block: message =
seed(8 B) || block_index(4 B)= a 12-byte SHA-256 message, padded (0x80at offset 12, length0x60= 96 bits), i.e.keystream_block = SHA256(seed || block_index). - Keystream XORed with
VOW_CIPHER.VOW_LEN = 30, so onlyblock_index = 0is needed (byte order irrelevant since it is 0).
The reproduced Python decryption (last lines of solve.py) yields the flag: "hey signet, I am your king" — matching the "carry a king's command again" narrative.
Environment note: the solving host was macOS/darwin (no qemu-user, no readelf), so the ELF was not executed. The entire solution is static reverse engineering + z3 inversion + a faithful Python re-implementation of the flag-decryption routine.