BLOWFISH
Challenge
The service ("blow fish" / "unblow fish") is an encrypt/decrypt oracle over 8-byte blocks in a CBC-like chain: each plaintext block is combined with the previous ciphertext block before whatever core transform runs. That per-block combination is a plain XOR, which makes the whole chain malleable — you can steer what any given block decrypts to as long as you control which ciphertext block feeds into it.
The idea
Feeding the oracle a chosen ciphertext block c_i right after a chosen "previous block" X makes the transform operate on X XOR c_i. From an initial known plaintext/ciphertext/signature transcript you get one linear equation (over GF(2)) per block pair, K_i = p_i XOR c_{i-1}. The target you actually want — a plaintext block equal to a chosen JSON fragment like b': true} ' — is just another vector in that same GF(2) space, so linear algebra (Gaussian elimination / XOR basis) over the known K_i vectors tells you exactly which subset of blocks to XOR together to reach it.
def solve_target(K_list, target):
basis, history = [], []
for i, val in enumerate(K_list):
curr_val, curr_hist = val, {i}
for b_val, b_hist in zip(basis, history):
hb = b_val.bit_length() - 1
if (curr_val >> hb) & 1:
curr_val ^= b_val
curr_hist = curr_hist.symmetric_difference(b_hist)
if curr_val != 0:
… insert curr_val/curr_hist into basis, keeping it sorted by leading bit …
res_hist = set()
for b_val, b_hist in zip(basis, history):
hb = b_val.bit_length() - 1
if (target >> hb) & 1:
target ^= b_val
res_hist = res_hist.symmetric_difference(b_hist)
return list(res_hist) if target == 0 else None
Solution
Build the XOR basis from the leaked transcript, solve for the combination of block indices that reaches the target JSON fragment, then walk that combination one query at a time — each "unblow fish" call folds one more chosen ciphertext block into the running state via the oracle’s own chaining, carrying the running signature forward — until the running plaintext block equals the target exactly. Splice that forged block into the original message and send it back through the "blow fish" oracle to get the flag.
for idx in indices:
query_hex = f"{curr_X:016x}" + c_blocks[idx + 1]
query_sig = curr_sig + sig_blocks[idx + 1]
fish, new_sig = unblow_fish(query_hex, query_sig)
curr_X = int(fish[16:32], 16)
curr_sig = get_sig_blocks(new_sig)[1]
final_hex = p_blocks[0] + p_blocks[1] + f"{curr_X:016x}"
final_sig = p_sigs[0] + p_sigs[1] + curr_sig
flag, _ = blow_fish(final_hex, final_sig)