Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
CryptoMedium

Polite Email

CTF writeup for Polite Email from openECSC

>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, writes 002_mail.bin and 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)

  1. Create a Python venv and install fastcrc inside it:

    python3 -m venv venv

    venv/bin/pip install --upgrade pip

    venv/bin/pip install fastcrc

  1. Run the solver from the 001_polite-email folder:

    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.

  1. 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.bin so 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 fastcrc system-wide if your environment is managed.

  • If you prefer ncat --ssl instead of openssl s_client, the same payloads work; in my environment ncat was not installed so I used openssl.

>Solver.py

python
#!/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()