>polite-email
Short, easy-to-follow write-up describing how to find the flag for the polite-email Cryptography challenge.
Summary
The challenge provides a small Python service polite-email/email.py which returns the flag only when two conditions are met:
-
the submitted data contains a polite email message addressed to the author, and
-
five CRC checks (three CRC32 variants and two CRC64 variants) of the submitted data match the same MAC value.
Because CRC is linear over GF(2), we can append a carefully crafted suffix to the polite email so that all five CRCs evaluate to the same value (we choose 0). Using linear algebra (Gaussian elimination over GF(2)) we solve for the suffix bits. With that suffix and MAC = 0 the service accepts the mail and prints the flag.
Files I added
001_solve.py— solver that builds the GF(2) matrix, solves for a 32-byte suffix, writes002_mail.binand prints the mail hex and MAC to use.
When you run the solver it writes 002_mail.bin and prints a mail hex string that you can submit to the service with MAC 0.
Why this works (short)
CRC functions (standard CRC32/CRC64 variants included in the fastcrc package) are linear maps on the message bits when you treat message extension consistently. If we fix a prefix (the polite email body) and append an L-byte suffix, each bit of that suffix contributes linearly to every bit of every CRC output. The combined outputs (three 32-bit CRCs and two 64-bit CRCs) form a 224-bit linear function of the 8*L suffix bits. We want to choose suffix bits x such that CRC(prefix || suffix) = target (we used target = 0 for all algorithms). This is M * x = base, where base is the vector of current CRC bits if the suffix were zero. Solve over GF(2) using Gaussian elimination and build bytes from the bit solution.
I used L = 32 bytes (256 bits) which gives a tall/wide matrix with a solution in this instance.
How to reproduce locally (recommended)
- Create a Python venv and install
fastcrcinside it:
python3 -m venv venv
venv/bin/pip install --upgrade pip
venv/bin/pip install fastcrc
- Run the solver from the
001_polite-emailfolder:
venv/bin/python3 001_solve.py
The script prints Mail hex: (the full message as hex) and MAC (use decimal): 0 and writes 002_mail.bin.
- Test locally (optional) by running the provided program and feeding the printed inputs. Example interactive test that I used:
# from /path/to/001_polite-email
FLAG=openECSC{example_flag} venv/bin/python3 polite-email/email.py
# then paste inputs when prompted:
# Enter name: attacker
# Enter mail: <paste mail hex printed by 001_solve.py>
# Enter MAC: 0
You should see the program print a response containing the flag from the environment variable.
Exploiting the remote instance
I used the first remote instance you provided. ncat --ssl is normally fine, but the environment here had openssl available so I used openssl s_client to connect and provide the inputs non-interactively.
Example (replace the mail hex with the one produced by your 001_solve.py run):
/usr/bin/openssl s_client -connect 042f49b0-9d51-43d6-b69e-62e628492dce.openec.sc:31337 -quiet <<'IN'
attacker
<mail-hex-from-001_solve>
0
IN
The remote service responds with a polite message containing the flag in the format openECSC{...}.
Example flag (what I got)
When I ran the exploit against your first remote instance I got the following flag in the service output:
openECSC{when_politeness_fails_chinese_remainder_theorem_usually_works_23456789054}
Notes & improvements
-
I used a 32-byte suffix; it is likely possible to find smaller suffixes with a more careful basis selection or by searching for sparse solutions.
-
The solver currently fixes the target CRC to 0 for all algorithms. It would be trivial to adapt the solver to target any common MAC value.
-
The script currently prints the mail hex and writes
002_mail.binso you can re-use the bytes directly if you want to avoid hex conversion issues.
Safety / reproducibility
-
Run the solver in a virtualenv and avoid installing
fastcrcsystem-wide if your environment is managed. -
If you prefer
ncat --sslinstead ofopenssl s_client, the same payloads work; in my environmentncatwas not installed so I usedopenssl.
>Solver.py
#!/usr/bin/env python3
"""
Solver for http101 (polite-email) challenge.
This script finds a suffix to append to the polite email such that all
CRC implementations used in the challenge produce the same MAC (we target 0).
It works by treating CRC as a linear function over GF(2) and solving a
system of linear equations for a chosen suffix length.
Usage: run from the challenge folder. Requires fastcrc installed in the venv.
"""
from fastcrc import crc32, crc64
from itertools import product
import os
def make_polite(sender: str):
return f"Dear Challenge Author.\n\nPretty please give me the flag.\n\nBest regards,\n{sender}".encode()
def compute_crcs(data: bytes):
vals = []
for a in ['autosar', 'iscsi', 'iso_hdlc']:
vals.append(getattr(crc32, a)(data))
for a in ['go_iso', 'xz']:
vals.append(getattr(crc64, a)(data))
return vals
def int_to_bits(x: int, width: int):
return [(x >> i) & 1 for i in range(width)]
def bits_to_int(bits):
x = 0
for i, b in enumerate(bits):
if b:
x |= (1 << i)
return x
def build_matrix(prefix: bytes, L_bytes=32):
"""Build matrix M over GF(2) with 224 rows (3*32 + 2*64) and 8*L columns.
Column j is the delta in CRC-vector when flipping bit j in the suffix
(compared to a zero suffix of length L).
"""
ncols = 8 * L_bytes
# base crc with zero suffix
zero_suffix = bytes([0]) * L_bytes
base = compute_crcs(prefix + zero_suffix)
base_bits = []
for i, val in enumerate(base):
width = 32 if i < 3 else 64
base_bits.extend(int_to_bits(val, width))
# prepare matrix as list of columns (each column is list of bits)
cols = [None] * ncols
for j in range(ncols):
# build suffix with single bit j set
bidx = j // 8
bit_in_byte = j % 8
s = bytearray(L_bytes)
s[bidx] = 1 << bit_in_byte
vals = compute_crcs(prefix + bytes(s))
col_bits = []
for i, val in enumerate(vals):
width = 32 if i < 3 else 64
col_bits.extend(int_to_bits(val, width))
# column is delta = col_bits XOR base_bits
delta = [ (cb ^ bb) for cb, bb in zip(col_bits, base_bits) ]
cols[j] = delta
if j % 32 == 0:
print(f'Built column {j}/{ncols}')
return cols, base_bits
def solve_gf2(cols, target):
"""Solve M x = target over GF(2). cols is list of column vectors (each list of bits).
Returns solution bitlist x of length ncols or None.
Uses Gaussian elimination on augmented matrix built from columns.
"""
ncols = len(cols)
nrows = len(cols[0])
# Build matrix as list of rows for elimination
# We'll construct row-major matrix A where A[r][c] = cols[c][r]
A = [ [cols[c][r] for c in range(ncols)] + [target[r]] for r in range(nrows) ]
row = 0
pivots = [-1] * nrows
for col in range(ncols):
# find pivot
sel = None
for r in range(row, nrows):
if A[r][col]:
sel = r
break
if sel is None:
continue
# swap
A[row], A[sel] = A[sel], A[row]
pivots[row] = col
# eliminate other rows
for r in range(nrows):
if r != row and A[r][col]:
# XOR row
for c in range(col, ncols+1):
A[r][c] ^= A[row][c]
row += 1
if row == nrows:
break
# Check consistency
for r in range(row, nrows):
if A[r][ncols]:
return None
# back substitution, set free vars to 0
x = [0] * ncols
for r in range(row-1, -1, -1):
c = pivots[r]
if c == -1:
continue
s = A[r][ncols]
for cc in range(c+1, ncols):
if A[r][cc] and x[cc]:
s ^= 1
x[c] = s
return x
def main():
sender = 'attacker'
prefix = make_polite(sender)
print('Prefix len', len(prefix))
L = 32
print('Building matrix with suffix length', L)
cols, base = build_matrix(prefix, L_bytes=L)
# target mac: choose 0 for all algorithms -> target bits = base_bits XOR desired (desired is 0)
# We want final CRCs equal to 0, so target = base_bits (since base is CRC(prefix+zeros))
target = base[:] # we need to flip bits to reach zero
print('Solving linear system...')
sol = solve_gf2(cols, target)
if sol is None:
print('No solution found')
return
# build suffix bytes
Lbits = 8 * L
sbytes = bytearray(L)
for j in range(Lbits):
if sol[j]:
sbytes[j//8] |= (1 << (j%8))
final = prefix + bytes(sbytes)
cr = compute_crcs(final)
print('Final CRCs:', cr)
# For local test, write final mail bytes to a file and print mail hex + mac
outpath = '002_mail.bin'
with open(outpath, 'wb') as f:
f.write(final)
print(f'Wrote final mail to {outpath} (len={len(final)})')
print('Mail hex:')
print(final.hex())
print('MAC (use decimal): 0')
if __name__ == '__main__':
main()