sat1sf1 [Unbreakable 2024]

rev crypto
writeup by: zenbassi

Challenge Description

Made my own hashing function sah652.

Here the super-secure hash of my secret: 2033251f4b3161e4455a4c261e3f631e18653c3a6c136e30304037373e6e1f6c6f6448673e686b1e18603d10306d323f3a4b626eee636c3c3c62483592123e6d6c6c3a49ca

Feeling generous to share some hints about my secret that you definitely will not able to recover:

Text length: 69 characters

Flag regex: CTF{[a-f0-9]{64}}

Flag contains somewhere the text: beebeef


Analysing the implementation we deduce that each byte of the hash is obtained by xoring together some of the bytes which make up the original flag, and potentially some other known values. Moreover, we know part of the original flag and a crib. This is enough information to logically deduce some unknown bytes. This creates a cascading effect, which enables us to recover the full flag, if we know the position of the crib, which is brute-forcible.


Our original solution involved lazily building the equations which form each byte of the hash. We used a few tricks to reduce these equations. Firstly, we noticed a number of equation with only one unknown variable. The unknown in these instanced can be obtained by xor-ing the corresponding byte from the hash with the other known values. There second trick was that we could xor together two different equations which had overlapping terms, such that the result would be a different equation with fewer unknown terms. These tricks, paired with brute-forcing the crib’s position enables us to recover the flag.

On a closer inspection, we notice that we’re dealing with a logical formula where the unknown terms are the characters of the flag. If it’s satisfiable, the model for this formula should be unique, and should correspond to our flag. After an upsolving session and some discord hints, we came up with a simpler solution based on the Z3 SMT solver 1. We build the formula in the z3 format and then find a model for it. A partial solution can be checked out below.

variables = {i: BitVec(f'v_{i}', 7) for i in range(200)}
state0 = [variables[i] for i in range(200)]
out = SAH3_652(state0)

hashul = bytes.fromhex("2033251f4b3161e4455a4c261e3f631e18653c3a6c136e3" \
                        "0304037373e6e1f6c6f6448673e686b1e18603d10306d32" \
s = Solver()
for i in range(69):
    # add constraints based on hash
    s.add(simplify(out[i] == int(hashul[i])))

# add constraints based on known values
s.add(variables[0]  == ord('C'))
s.add(variables[1]  == ord('T'))
s.add(variables[2]  == ord('F'))
s.add(variables[3]  == ord('{'))
s.add(variables[68] == ord('}'))

for i in range(69, 200):
    s.add(variables[i] == 0)

# add constraints based on crib
and_conds = []
ss = 'beebeef'
for off in range(8, 58):
    # for each offset, we have a set of possible constraints
    terms = [variables[off + i] == ord(c) for i, c in enumerate(ss)]

# check for satisfiability and get model
print(f'our theorem is:', s.check())
m = s.model()

# print solution
print("".join([chr(m[variables[i]].as_long()) for i in range(69)]))