In this write-up

These notes document the October 2026 CTF challenge. Commands, addresses and outputs refer to the supplied challenge environment.

1. Overview#

We are given:

  • check.py
  • dials.json
  • bundle.zip (packaging; the solution uses the supplied checker and JSON)

The checker reads dials.json, asks for a combination, and verifies it against a dense system of linear equations modulo 257.

The goal is to recover the input bytes that satisfy the system.


2. Inspecting the files#

First, look at the checker:

cat -n check.py

Output:

import json
from pathlib import Path
lock = json.loads(Path(__file__).with_name("dials.json").read_text())
candidate = input("Combination: ").encode()
ok = len(candidate) == len(lock["target"])
if ok:
    ok = all(sum(a*b for a,b in zip(row,candidate)) % 257 == expected
             for row,expected in zip(lock["matrix"],lock["target"]))
print("Unlocked" if ok else "Locked")

So the checker:

  1. Reads dials.json.
  2. Takes the user input as bytes.
  3. Requires the input length to equal len(lock["target"]).
  4. For each row in lock["matrix"], computes:
    sum_j matrix[i][j] · candidate[j] (mod 257)
    
    and compares it to lock["target"][i].

This is exactly a system of linear equations over the finite field GF(257).


3. Structure of dials.json#

A quick Python inspection shows:

{
  "modulus": 257,
  "matrix": [ ... 37 rows ... ],
  "target": [ ... 37 values ... ]
}

The matrix has 37 rows, and target has 37 entries.

Therefore, the candidate must be 37 bytes long.

So we have:

  • 37 unknowns x_0, x_1, ..., x_{36}
  • 37 equations
  • All arithmetic modulo 257

The modulus 257 is prime, so every non-zero element has a modular inverse. This means we can solve the system with Gaussian elimination.


4. Mathematical formulation#

Let:

  • A be the 37 × 37 matrix from dials.json["matrix"]
  • b be the target vector from dials.json["target"]
  • x be the unknown candidate bytes

We need:

A x ≡ b (mod 257)

Since 257 is prime, we can perform standard Gaussian elimination over GF(257).


5. Solver script#

Create a file called solve.py:

import json

with open('dials.json') as f:
    data = json.load(f)

mod = data['modulus']
A = data['matrix']
b = data['target']
n = len(A)  # 37

# Build augmented matrix [A | b]
M = [row[:] + [b[i]] for i, row in enumerate(A)]

# Gaussian elimination over GF(mod)
for i in range(n):
    # Find pivot
    pivot = None
    for r in range(i, n):
        if M[r][i] % mod != 0:
            pivot = r
            break

    if pivot is None:
        print("No unique solution")
        exit(1)

    # Swap rows
    M[i], M[pivot] = M[pivot], M[i]

    # Normalize pivot row
    inv = pow(M[i][i], -1, mod)
    for c in range(i, n + 1):
        M[i][c] = (M[i][c] * inv) % mod

    # Eliminate other rows
    for r in range(n):
        if r != i:
            factor = M[r][i]
            if factor != 0:
                for c in range(i, n + 1):
                    M[r][c] = (M[r][c] - factor * M[i][c]) % mod

# Extract solution
x = [M[i][n] for i in range(n)]

print("Solution ints:", x)
print("Solution bytes:", bytes(x))

try:
    print("String:", bytes(x).decode('ascii'))
except Exception as e:
    print("Decode error:", e)

6. Running the solver#

python3 solve.py

Output:

Solution ints: [116, 105, 98, 101, 98, 123, 101, 113, 117, 97, 116, 105, 111, 110, 115, 95, 98, 100, 53, 101, 53, 99, 56, 100, 50, 102, 52, 101, 48, 48, 97, 97, 101, 48, 52, 99, 125]
Solution bytes: b'tibeb{equations_bd5e5c8d2f4e00aae04c}'
String: tibeb{equations_bd5e5c8d2f4e00aae04c}

7. Verification#

Check the candidate with the original checker:

echo -n 'tibeb{equations_bd5e5c8d2f4e00aae04c}' | python3 check.py

It should print:

Unlocked

8. Flag#

tibeb{equations_bd5e5c8d2f4e00aae04c}

Interesting / Tricky Points#

  1. Modulus 257 is prime
    This is important because Gaussian elimination requires division. Since 257 is prime, every non-zero pivot has an inverse modulo 257.

  2. 257 vs 256
    The modulus is one more than 256. This is common in CTFs because bytes range from 0 to 255, but modular arithmetic mod 257 allows values up to 256. In this challenge, the recovered bytes are all valid ASCII, so no issue arises.

  3. Dense system
    The matrix is fully dense, so simple substitution or guessing would not work. Gaussian elimination is the intended solution.

  4. Length check
    The checker enforces len(candidate) == len(lock["target"]), which is 37. If your solver returns a different length, something is wrong.

  5. bundle.zip
    The shown solution uses check.py and dials.json directly. No additional archive layer is required by the recorded method.


Final Answer#

tibeb{equations_bd5e5c8d2f4e00aae04c}
Back to all write-upsBack to top