← Back to Writeups
HTBMediumCryptography

Multiplication as a Service

D3v0o0Nu119/2/20267 min read
#cryptography#htb#medium#brute_force_dlp#chinese_remainder_theorem#invalid_curve_attack#pohlig_hellman_algorithm#small_subgroup_confinement

Multiplication as a Service

Platform: Tjctf 2026 | Category: Cryptography | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-05-15 | Status: Solved Techniques: brute_force_dlp, chinese_remainder_theorem, invalid_curve_attack, pohlig_hellman_algorithm, small_subgroup_confinement

Summary

Task: ECC scalar multiplication oracle on y²=x³+2x+3 over F_10007 that does not validate input points lie on the curve. Solution: Invalid curve attack — send points from curves with smooth orders, solve small DLPs via Pohlig-Hellman, reconstruct secret via CRT.

Recon

Port scan

bash
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: tjctf-2026 | ID: 20260515_tjctf_multiplication_as_a_service
  • Tags: crt, ecc, pohlig_hellman, discrete_logarithm, scalar_multiplication, invalid_curve_attack, point_validation
  • Indicators: ECC scalar multiplication oracle, server does not validate point is on curve, point_add uses only coefficient a, never checks b, small prime field (p=10007)
  • Source: 20260515_tjctf_multiplication_as_a_service.md

Foothold

Vulnerability / Misconfiguration

  1. brute_force_dlp
  2. chinese_remainder_theorem
  3. invalid_curve_attack
  4. pohlig_hellman_algorithm
  5. small_subgroup_confinement
bash
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

bash
sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
bash
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • brute_force_dlp
  • chinese_remainder_theorem
  • invalid_curve_attack
  • pohlig_hellman_algorithm
  • small_subgroup_confinement
  • Tags: crt, ecc, pohlig_hellman, discrete_logarithm, scalar_multiplication, invalid_curve_attack, point_validation

Original Writeup

<details><summary>Click to expand original content</summary>

Multiplication as a Service — TJCTF 2026

Description

"Mendacem oportet esse memorem." — A liar must have a good memory.

The server implements an ECC scalar multiplication oracle. It reads a flag, converts it to an integer secret_d = int.from_bytes(flag, "big"), and for any user-supplied point P = (x, y) computes and returns Q = secret_d * P on the curve y² = x³ + 2x + 3 over F_10007. The server runs in a loop, accepting unlimited queries.

The goal is to recover secret_d (and thus the flag) using only the oracle responses.

Analysis

Server Code

python
P = 10007
A = 2
B = 3

def point_add(P1, P2):
    # ...
    if x1 == x2 and y1 == y2:
        s = (3 * x1 * x1 + A) * mod_inv(2 * y1, P)  # Only uses A, never B!
    else:
        s = (y2 - y1) * mod_inv(x2 - x1, P)
    s %= P
    x3 = (s * s - x1 - x2) % P
    y3 = (s * (x1 - x3) - y1) % P
    return (x3, y3)

Critical vulnerability: The point_add function only uses the curve coefficient a=2 (in the point doubling formula s = (3x² + a) / 2y). It never checks or uses b=3. The server also never validates that the input point (x, y) satisfies y² ≡ x³ + 2x + 3 (mod 10007).

This means the server will compute scalar multiplication on any curve of the form y² = x³ + 2x + b' for arbitrary b', as long as we supply a point from that curve. The arithmetic is identical — only a matters for the addition/doubling formulas.

Attack Strategy: Invalid Curve Attack

The invalid curve attack exploits missing point validation to confine the secret scalar into small subgroups on attacker-chosen curves:

  1. Find invalid curves with smooth orders: For each b' ∈ [0, 10006] where b' ≠ 3, compute the order of E': y² = x³ + 2x + b' over F_10007. Factor each order and collect small prime factors.

  2. Find points of specific prime order: For each small prime q dividing some invalid curve's order, find a point of exact order q on that curve by computing (order/q) * G for a random point G on E'.

  3. Query the oracle: Send each point of order q to the server. Since the server only uses a=2, it computes Q = secret_d * P using the invalid curve's group structure. The result Q lies in the subgroup of order q.

  4. Solve small DLP: Since q is small (≤1000), brute-force the discrete logarithm: find k such that k * P = Q, giving secret_d ≡ k (mod q).

  5. CRT reconstruction: Combine all residues via the Chinese Remainder Theorem. If the product of all primes exceeds secret_d, the flag is uniquely determined.

Key Numbers

  • 131 distinct small primes collected from ~10,000 invalid curves over F_10007
  • Combined CRT modulus: 1026 bits — more than sufficient for the ~250-bit flag
  • Flag recovered at k=0 (direct CRT result, no offset needed)

Solution

Step 1: Precompute Invalid Curve Data

For each b' ∈ [0, 10006], compute the curve order using the formula #E = p + 1 - t where t is the trace of Frobenius (computable by counting points or using Schoof's algorithm — for p=10007 brute force is fast). Factor each order and for each small prime factor q, find a point of exact order q.

This produces prime_data.json mapping each small prime to (b_value, point_coordinates, curve_order).

Step 2: Query Oracle and Solve DLPs

python
#!/usr/bin/env python3
"""
Invalid Curve Attack on ECC scalar multiplication oracle.
Loads precomputed prime data, queries remote server, recovers flag via CRT.
"""

from pwn import *
import json

P = 10007
A = 2

def mod_inv(x, p):
    return pow(x % p, -1, p)

def point_add(P1, P2, a=A, p=P):
    if P1 is None:
        return P2
    if P2 is None:
        return P1
    x1, y1 = P1
    x2, y2 = P2
    if x1 == x2 and (y1 + y2) % p == 0:
        return None
    if x1 == x2 and y1 == y2:
        s = (3 * x1 * x1 + a) * mod_inv(2 * y1, p)
    else:
        s = (y2 - y1) * mod_inv(x2 - x1, p)
    s %= p
    x3 = (s * s - x1 - x2) % p
    y3 = (s * (x1 - x3) - y1) % p
    return (x3, y3)

def extended_gcd(a, b):
    if a == 0:
        return b, 0, 1
    g, x1, y1 = extended_gcd(b % a, a)
    return g, y1 - (b // a) * x1, x1

def crt(residues, moduli):
    result, mod = residues[0], moduli[0]
    for i in range(1, len(residues)):
        r2, m2 = residues[i], moduli[i]
        g, x, _ = extended_gcd(mod, m2)
        if (r2 - result) % g != 0:
            continue
        lcm = mod * m2 // g
        result = (result + mod * ((r2 - result) // g) * x) % lcm
        mod = lcm
    return result, mod

def query_server(io, x, y):
    io.recvuntil(b"x = ")
    io.sendline(str(x).encode())
    io.recvuntil(b"y = ")
    io.sendline(str(y).encode())
    response = io.recvline().decode().strip()
    if "inf" in response:
        return None
    parts = response.replace("Q = ", "").split()
    return (int(parts[0]), int(parts[1]))

def solve_dlp(Q, point, prime):
    """Brute-force DLP in subgroup of small prime order."""
    if Q is None:
        return 0
    cur = None
    for i in range(prime):
        if cur == Q:
            return i
        cur = point_add(cur, point, A, P)
    return None

def main():
    # Load precomputed invalid curve data (131 primes)
    with open("prime_data.json") as f:
        raw = json.load(f)
    prime_data = {}
    for prime_str, (b, pt, order) in raw.items():
        prime_data[int(prime_str)] = (b, tuple(pt), order)
    primes = sorted(prime_data.keys())
    print(f"[*] Loaded {len(primes)} primes from cache")

    # Connect to remote
    context.log_level = 'error'
    io = remote("tjc.tf", 31313, timeout=15)
    io.recvuntil(b"Curve:")
    io.recvline()

    residues, moduli = [], []
    for i, prime in enumerate(primes):
        b, point, order = prime_data[prime]
        Q = query_server(io, point[0], point[1])
        k = solve_dlp(Q, point, prime)
        if k is not None:
            residues.append(k)
            moduli.append(prime)
        if (i + 1) % 20 == 0:
            print(f"[*] Progress: {i+1}/{len(primes)} primes queried")
    io.close()

    # CRT reconstruction
    secret_d, mod = crt(residues, moduli)
    print(f"[*] secret_d ≡ {secret_d} (mod M)")
    print(f"[*] Modulus has {mod.bit_length()} bits")

    # Recover flag
    for k in range(100000):
        candidate = secret_d + k * mod
        if candidate <= 0:
            continue
        try:
            flag_bytes = candidate.to_bytes(
                (candidate.bit_length() + 7) // 8, "big"
            )
            if b"tjctf{" in flag_bytes:
                print(f"[+] FLAG: {flag_bytes.decode()}")
                return
        except:
            pass

if __name__ == "__main__":
    main()

Step 3: Run and Recover Flag

[*] Loaded 131 primes from cache
[*] Progress: 20/131 primes queried
[*] Progress: 40/131 primes queried
...
[*] Progress: 120/131 primes queried
[*] secret_d ≡ 13480003234703408403126709436951282838902635060439314674537647253922534796914045 (mod 590882...011790)
[*] Modulus has 1026 bits
[+] FLAG: tjctf{REDACTED}
</details>

signed by XESXOR