0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AlpacaHack B-SIDE: HellCouple Writeup

0
Last updated at Posted at 2026-02-10

HellCouple

脆弱なカップル!

import secrets
import hashlib
from Crypto.Cipher import AES
import os

FLAG = os.getenv("FLAG", "Alpaca{dummy}").encode()

# https://datatracker.ietf.org/doc/html/rfc3526#section-2
p = 0xffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca237327ffffffffffffffff
g = 2

alice_private = secrets.randbelow(p)
bob_private = secrets.randbelow(p)

alice_public = pow(g, alice_private, p)
bob_public = pow(g, bob_private, p)

print("alice_public:", alice_public)
print("bob_public:", bob_public)
print("leak:", alice_private & (2**1500 - 1))

shared_key = pow(alice_public, bob_private, p)
assert shared_key == pow(bob_public, alice_private, p)
session_key = hashlib.sha256(shared_key.to_bytes(p.bit_length() // 8, "big")).digest()

cipher = AES.new(session_key, AES.MODE_CTR)
encrypted_flag = cipher.nonce + cipher.encrypt(FLAG)
print("encrypted_flag:", encrypted_flag.hex())

解法

alice_private(以下$a$)とalice_public(以下$A$)は、それぞれ以下で表されます。

\begin{align}
a &= leak + k \cdot 2^{1500} \\
A &\equiv g^a \pmod p
\end{align}

ただし、$k$は、上位36bit($p$のbit長は1536bit)です。
ここで、$A$$a$を代入すると、

A = g^{leak + k \cdot 2^{1500}} \equiv g^{leak} \cdot (g^{2^{1500}})^k \pmod p

と式変形ができ、結果的に未知変数は$k$となります。
このままだと求めづらいので、未知変数$k$だけに変形していきましょう。
まず、$g^{leak}$を左辺に移し、それを$T$と定義します。

T = A \cdot (g^{leak})^{-1}

ここで、$A\equiv g^{leak + k \cdot 2^{1500}} \pmod p$より、

T \equiv  g^{leak + k \cdot 2^{1500}}\cdot (g^{leak})^{-1}

となります。
指数法則より、$ g^{leak} \cdot (g^{2^{1500}})^k$となので、

T \equiv  g^{leak} \cdot (g^{2^{1500}})^k \cdot (g^{leak})^{-1}

$g^{leak}$とその逆元が打ち消しあうため、最終的に

T \equiv (g^{2^{1500}})^k \pmod p

となります。$g^{2^{1500}}$$H$と定義すると、

T \equiv H^k \pmod p

ときれいな式かつ$k$を求めるという離散対数問題に帰着することができます。

最後にこの離散対数問題を解く方法を考えてみましょう。
今回、未知変数のbit長は36bitであることから、$2^{36}$回程度探索をすればよさそうです。
しかし、$2^{36} = 68719476736$回であることからそのまま総当たりするのは厳しいでしょう。

ここで、離散対数問題を解くためのアルゴリズムBaby-step Giant-step(BSGS)を用います。
BSGSは、

g^x \equiv h \pmod p

$x$の探索範囲$0 \le x < n$を$\sqrt{n}$にできます。
ステップ数$m \approx \sqrt{n}$として、未知$x$

x = im + j\space (0 \le i, j < m)

と分解します。
すると、

g^x = g^{im+j} = (g^m)^i \cdot g^j

より、

(g^m)^i \cdot g^j  \equiv h \pmod p

と表せます。両辺$(g^m)^{-i}$をかけると

g^j\equiv h \cdot (g^m)^{-i} \pmod p

となり、これらが成立するような$i, j$を探索する問題と考えることができます。

まず、$g^j \pmod p \space(j = 0, 1, \dotsm, m-1)$を全部計算してハッシュ表に入れます。
この操作はBaby-stepと呼ばれます。
そして$h \cdot (g^m)^{-i} \space (i_0,1,\dots, m)$で計算し、Baby-stepで作成したハッシュ表に存在するか確認します。また、この操作はGiant-stepと呼ばれます。
もし、みつかったら上記式が成立するため、

h \equiv (g^m)^i \cdot g^j \equiv g^{im+j} \equiv g^x

より、解は$im+j$であることがわかります。$N = 2^{36}$とすると、$sqrt{2^{36}} = 262144$より明らかに間に合うでしょう。

このようにして秘密鍵$a$を復元し、共有鍵 -> SHA256 -> AES-CTRの順で復号すれば解けるでしょう。

コードは以下のようになります。

import math
import hashlib
from Crypto.Cipher import AES

p = 0xffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca237327ffffffffffffffff
g = 2
LOW = 1500
MASK = (1 << LOW) - 1

d = {}
with open("../distfiles/output.txt", "r") as f:
    for line in f:
        line = line.strip()
        if not line or ":" not in line:
            continue
        k, v = line.split(":", 1)
        d[k.strip()] = v.strip()

A = int(d["alice_public"])
B = int(d["bob_public"])
leak = int(d["leak"]) & MASK
enc = bytes.fromhex(d["encrypted_flag"])

H = pow(g, 1 << LOW, p)
T = (A * pow(pow(g, leak, p), p - 2, p)) % p

bound = 1 << (p.bit_length() - LOW)
m = math.isqrt(bound - 1) + 1

tbl = {}
e = 1
for j in range(m):
    if e not in tbl:
        tbl[e] = j
    e = (e * H) % p

factor = pow(pow(H, p - 2, p), m, p)
gamma = T
k = None
for i in range(m + 1):
    j = tbl.get(gamma)
    if j is not None:
        x = i * m + j
        if x < bound:
            k = x
            break
    gamma = (gamma * factor) % p

if k is None:
    raise SystemExit("k not found")

a = leak + k * (1 << LOW)
if pow(g, a, p) != A:
    raise SystemExit("Not valid a")

s = pow(B, a, p)
key = hashlib.sha256(s.to_bytes(p.bit_length() // 8, "big")).digest()
nonce, ct = enc[:8], enc[8:]
flag = AES.new(key, AES.MODE_CTR, nonce=nonce).decrypt(ct)
print(flag.decode())

Flag: Alpaca{1_hat3_c0u913s:fire:}

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?