tiles + ai
tiles + ai
Platform: B01Lersc | Category: Reversing | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-18 | Status: Solved Techniques: amx_b_layout_unpack, amx_emulation, column_permutation_tracking, sparse_matrix_extraction, state_space_bfs
Summary
Task: static x86_64 ELF that uses Intel AMX (tmm tile registers, tdpbssd) to implement a puzzle โ each hex-pair input multiplies one column of a 3x(16x16) byte state by a precomputed W matrix; after 3 rounds state[1][1,0] must equal 1. Solution: extract B/C/W matrices and initial states from .rodata, unpack the AMX-B interleaved layout to logical form, build a Python emulator of tdpbssd, and BFS over the reduced state space (each row holds at most one '1', so state = tuple of column indices).
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
b01lersc| ID:20260418_b01lersc_tiles_ai - Tags: state_machine, x86_64, matrix_multiplication, amx, intel_amx, tile_registers, tdpbssd, ldtilecfg, static_elf
- Indicators: unknown opcodes tdpbssd / tileloadd / tilestored / ldtilecfg in disasm, tmm0..tmm7 tile registers, challenge description mentions 'matrix multiplication, tile config struct at .rodata with rows/bytes_per_row fields (AMX TILECFG layout), requires Intel SDE with Sapphire Rapids preset to run
- Source:
20260418_b01lersc_tiles_ai.md
Foothold
Vulnerability / Misconfiguration
- Amx_b_layout_unpack
- Amx_emulation
- Column_permutation_tracking
- Sparse_matrix_extraction
- State_space_bfs
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- amx_b_layout_unpack
- amx_emulation
- column_permutation_tracking
- sparse_matrix_extraction
- state_space_bfs
- Tags: state_machine, x86_64, matrix_multiplication, amx, intel_amx, tile_registers, tdpbssd, ldtilecfg, static_elf
Original Writeup
<details><summary>Click to expand original content</summary>Description
I love matrix multiplication ๐
ncat --ssl tiles--ai.opus4-7.b01le.rs 8443
Files: a single static x86_64 ELF chall. The binary must be run under Intel SDE 10.8.0 with the Sapphire Rapids preset because it uses Intel AMX (Advanced Matrix Extensions), an ISA extension only present on very recent server CPUs.
The server runs three rounds. For each round it reads a line of hex digits from the user, runs a state transition, and only proceeds to the next round if the state reaches a specific value. If all three rounds succeed the server prints the flag.
Analysis
AMX crash course
AMX adds eight 2D "tile" registers tmm0..tmm7, each up to 16 rows ร 64 bytes, configured via a TILECFG structure loaded by ldtilecfg. The key instruction is:
tdpbssd tmm_dst, tmm_a, tmm_b
It computes dst[m,n] += sum_k a[m,k] * b[k/4, n*4 + k%4] with 8-bit signed operands and 32-bit accumulators. The B operand uses a special AMX-B interleaved layout: a logical K ร N matrix is laid out so that four consecutive rows of the logical matrix are packed into one row of the tile register, with each group of 4 bytes representing one column-block. Unpacking this correctly is the first non-trivial step.
Binary structure
Disassembly shows the tile config at 0x410100:
| Tile | Rows | Bytes/row |
|---|---|---|
| tmm0, tmm1, tmm2 | 16 | 16 (A operands, regular 16ร16 byte) |
| tmm3, tmm4, tmm5 | 4 | 64 (B operands, AMX-B layout of 16ร16) |
| tmm6, tmm7 | 16 | 64 (C accumulators, 16ร16 int32) |
.rodata contains four important tables:
| Address | Content |
|---|---|
0x409000 + h*0x100 | B(h) โ 16ร16 byte matrix = e_h * e_h^T (column-h extractor) |
0x40a000 + h*0x100 | C(h) โ 16ร16 byte matrix = I - e_h * e_h^T (column-h eraser) |
0x40b000 + h_high*0x2400 + l*0x900 | W(h_high, l) โ a 3ร3 grid of 16ร16 matrices |
0x40f800 + i*0x300 | initial state for round i (3 ร 16ร16 = 768 bytes) |
where h is 0..15, h_high = h >> 3, and l is 0..3.
Main loop semantics
The state is three 16ร16 byte matrices S[0], S[1], S[2] (a "block" ร "row" ร "column" cube). Input is consumed as hex pairs (h, l) where h is a full hex digit (0โ15) and l is a hex digit but the binary rejects everything except l โ {0,1,2,3}.
For each pair, only column h of every block is touched. Let v_k = column h of block k (a 16-vector). The operation is:
new_v_m[r] = ( ฮฃ_k W(h_high, l)[m,k] ยท v_k )[r] mod 256
and then each block's column h is replaced by new_v_m. All other columns are preserved (implemented via the C(h) zero-out + B(h) re-inject trick: first multiply by I - e_h e_h^T, then add back the new column).
After every pair, a validity predicate is evaluated on rows 0โ35 (block 0 + block 1 + first 4 rows of block 2, which gives 36 constrained rows). Each of those rows must:
- Contain only bytes in
[0, 127], and - Sum to less than 2.
So every constrained row has at most one 1 and the rest zeros โ i.e. the row is either empty or points at a single "column label" 0โ15. This collapses the state space enormously.
After all pairs of a round, the check is S[1][1][0] == 1 (byte at offset 0x110 from the state base).
The W matrices
Decoding each W(h_high, l) produces only four non-pathological maps:
(h_high, l) | Meaning |
|---|---|
(0, 0) | "shift up" within blocks, with transfer block 1 โ block 0 |
(0, 1) | "shift down" within blocks, with transfer block 0 โ block 1 |
(1, 2) | "fold / shift up by 6" |
(1, 3) | "shift down by 6" |
The other four combinations (0,2), (0,3), (1,0), (1,1) are filled with the constant 7, so applying any of them to a non-empty column produces row sums โฅ 7 and instantly fails the validity predicate. This is how the binary enforces the rule "first hex char in 0..7 โ second must be 0 or 1; first hex char in 8..15 โ second must be 2 or 3".
Semantically each input pair picks one of 8 legal transformations and applies it to the column-h "track" across the three blocks. The track has 36 positions (rows) with merge/destruction at specific boundaries and cross-block transfers โ a small state machine per column.
Solution
Step 1 โ extract matrices from .rodata
All four tables are extracted directly from the ELF. The only subtlety is unpacking AMX-B interleaved layout for B(h) and C(h):
def amx_b_to_logical(raw):
# 4 K-blocks ร 16 N-dwords ร 4 bytes-per-dword
M = [[0]*16 for _ in range(16)]
for k_block in range(4):
for n_dword in range(16):
for j in range(4):
M[k_block*4 + j][n_dword] = raw[k_block*64 + n_dword*4 + j]
return M
W is stored in regular row-major 16ร16 layout (it's used as the A-operand in tdpbssd, not the B-operand).
Step 2 โ Python emulator
The emulator operates on the reduced state โ for each constrained row we store only the column index of the 1 (or -1 for "empty"):
def normalize(S):
result = []
for m in range(3):
rows = 16 if m < 2 else 4
block = []
for r in range(rows):
ones = [c for c, v in enumerate(S[m][r]) if v == 1]
block.append(ones[0] if len(ones) == 1 else (-1 if not ones else -2))
result.append(tuple(block))
return tuple(result)
Applying an op means: reconstruct column h as vectors, run the tdpbssd sum with 8-bit truncation, replace column h, re-validate.
Step 3 โ BFS per round
Because the state is a tuple of 36 small integers, the reachable space is tiny (~4400 states for the hardest round). Plain BFS with only the 8 legal moves per step finds optimal solutions quickly:
def bfs(initial, max_depth=60):
init = normalize(initial)
q = deque([(init, "")])
seen = {init}
moves = [(h, l) for h in range(16) for l in ((0,1) if h<8 else (2,3))]
while q:
norm, path = q.popleft()
S = state_from_normalized(norm)
for h, l in moves:
ns = apply_op(S, h, l)
if ns is None: continue
nn = normalize(ns)
if nn in seen: continue
seen.add(nn)
np = path + f"{h:x}{l}"
if ns[1][1][0] == 1:
return np
q.append((nn, np))
Solutions found:
| Round | Length | Input |
|---|---|---|
| 0 | 10 pairs | 01e2e210f3f3f3010101 |
| 1 | 5 pairs | 01f320e201 |
| 2 | 52 pairs | 0120a2a2c231f2f2f2109393019311e320b211e3e300e31010923092921111c311d230e23030d310f3209201e2e210b3b3b30101 |
Step 4 โ submit to remote
import socket, ssl, time
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
solutions = [
"01e2e210f3f3f3010101",
"01f320e201",
"0120a2a2c231f2f2f2109393019311e320b211e3e300e31010923092921111c311d230e23030d310f3209201e2e210b3b3b30101",
]
s = socket.create_connection(("tiles--ai.opus4-7.b01le.rs", 8443))
s = ctx.wrap_socket(s, server_hostname="tiles--ai.opus4-7.b01le.rs")
s.settimeout(10)
time.sleep(1)
out = b""
for inp in solutions:
while True:
try:
c = s.recv(4096)
if not c: break
out += c
except socket.timeout:
break
s.send(inp.encode() + b"\n")
time.sleep(2)
time.sleep(3)
try:
out += s.recv(8192)
except socket.timeout:
pass
print(out.decode("latin-1"))
The server prints the flag after the third round is accepted.
</details>Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR