[LitCTF 2026] lit_elgamal_handshake

LitCTF 2026 lit_elgamal_handshake

  • [LitCTF 2026 lit_elgamal_handshake --- ElGamal 私钥泄露题解](#[LitCTF 2026] lit_elgamal_handshake — ElGamal 私钥泄露题解)
    • 前置知识
      • [1. 模运算](#1. 模运算)
      • [2. 模逆](#2. 模逆)
      • [3. 字节串与整数的转换](#3. 字节串与整数的转换)
      • [4. 离散对数问题](#4. 离散对数问题)
    • 一、题目描述
    • [二、ElGamal 加密流程](#二、ElGamal 加密流程)
    • [三、漏洞分析:长期私钥 x 被日志泄露](#三、漏洞分析:长期私钥 x 被日志泄露)
    • [四、为什么不需要知道 k](#四、为什么不需要知道 k)
    • 五、解题脚本
    • 六、脚本解释
      • [1. 头部依赖导入](#1. 头部依赖导入)
      • [2. `read_instance()`:读取并清洗题目参数](#2. read_instance():读取并清洗题目参数)
      • [3. 命令行入口配置](#3. 命令行入口配置)
      • [4. 私钥一致性校验(安全校验)](#4. 私钥一致性校验(安全校验))
      • [5. 核心解密逻辑](#5. 核心解密逻辑)
      • [6. 执行入口](#6. 执行入口)
    • 七、实际计算结果
    • Flag
    • [Key Takeaway](#Key Takeaway)

LitCTF 2026 lit_elgamal_handshake --- ElGamal 私钥泄露题解

现代密码学 专栏:

https://blog.csdn.net/r_feynman_/category_13190241.html
Crypto 密码解析实战靶场:https://blog.csdn.net/r_feynman_/category_13194584.html


前置知识

1. 模运算

密码学中经常在模 p p p 的意义下进行计算。表达式:

a ≡ b ( m o d p ) a \equiv b \pmod p a≡b(modp)

表示 a a a 和 b b b 除以 p p p 后的余数相同。Python 中的 % p 就可以完成取模操作:

python 复制代码
value = (a * b) % p

模运算具有乘法结合律,因此可以使用快速模幂计算大指数:

python 复制代码
pow(a, e, p)  # 计算 a^e mod p

相比先计算完整的 a e a^e ae 再取模,pow(a, e, p) 更快,也不会产生不必要的超大整数。


2. 模逆

如果存在整数 u u u,使得:

a u ≡ 1 ( m o d p ) au \equiv 1 \pmod p au≡1(modp)

那么 u u u 就称为 a a a 在模 p p p 下的乘法逆元,记作:

u ≡ a − 1 ( m o d p ) u \equiv a^{-1} \pmod p u≡a−1(modp)

当 p p p 为素数且 a ≢ 0 ( m o d p ) a \not\equiv 0 \pmod p a≡0(modp) 时,逆元一定存在。Python 中可以使用:

python 复制代码
inverse = pow(a, -1, p)

这相当于求 a − 1   m o d   p a^{-1} \bmod p a−1modp。在本题中,需要使用共享密钥的模逆来消除 c2 中的乘法掩码。


3. 字节串与整数的转换

题目中的 FLAG 是字节串,但 ElGamal 在计算时使用整数明文,因此加密前执行:

python 复制代码
m = bytes_to_long(FLAG)

解密得到整数 m m m 后,需要使用逆操作恢复 FLAG:

python 复制代码
flag = long_to_bytes(m)

例如:

text 复制代码
b"litctf{...}"  <->  整数 m

4. 离散对数问题

ElGamal 的安全性通常依赖离散对数问题:已知:

y = g x ( m o d p ) y = g^x \pmod p y=gx(modp)

在 g g g、 y y y 和 p p p 都公开的情况下,求出 x x x 通常很困难。类似地,已知 c 1 = g k c_1=g^k c1=gk 也不能轻易得到随机数 k k k。

本题并没有要求我们解决离散对数,因为题目直接通过调试日志泄露了私钥指数 x。因此可以绕过离散对数,直接计算解密所需的共享密钥。


一、题目描述

题目给出了一组 ElGamal 公钥、一个密文,以及一条本不应该出现在日志中的调试信息:长期私钥指数 x

python 复制代码
p, g, y, x = generate_elgamal_keypair(bits=512)
k = randrange(1, p - 2)
m = bytes_to_long(FLAG)

c1 = pow(g, k, p)
c2 = (m * pow(y, k, p)) % p

输出中额外打印了:

python 复制代码
# [DEBUG] prod accidentally logged the long-term secret:
x = 633366293219022684108628483753423657477324253833657141033762971761747669344649667887002347907882241246119223126492863291886751205505360049793728851371884

目标是利用泄露的 x 解密 (c1, c2),恢复 FLAG。


二、ElGamal 加密流程

ElGamal 在乘法群 Z p ∗ \mathbb{Z}_p^* Zp∗ 中工作。密钥生成过程为:

  1. 随机选择大素数 p p p;
  2. 选择生成元(或至少选择满足题目条件的) g g g;
  3. 随机选择私钥指数 x x x;
  4. 计算公钥:

y = g x ( m o d p ) y = g^x \pmod p y=gx(modp)

加密时,发送方随机选择一次性随机数 k k k,并计算:

c 1 = g k ( m o d p ) c_1 = g^k \pmod p c1=gk(modp)

c 2 = m ⋅ y k ( m o d p ) c_2 = m \cdot y^k \pmod p c2=m⋅yk(modp)

因此密文为:

( c 1 , c 2 ) = ( g k , m ⋅ y k ) ( m o d p ) (c_1,c_2)=(g^k,\ m\cdot y^k)\pmod p (c1,c2)=(gk, m⋅yk)(modp)

正常情况下,攻击者不知道 x x x,从 c 1 = g k c_1=g^k c1=gk 或 y = g x y=g^x y=gx 中求出指数属于离散对数问题,不能直接计算共享密钥 y k y^k yk。


三、漏洞分析:长期私钥 x 被日志泄露

题目的关键错误在于:

python 复制代码
# [DEBUG] prod accidentally logged the long-term secret:
x = 633366293219022684108628483753423657477324253833657141033762971761747669344649667887002347907882241246119223126492863291886751205505360049793728851371884

这里的 x 是长期私钥,而不是加密时使用的临时随机数 k。一旦 x 泄露,就不需要求解离散对数,也不需要分解 p p p,只需要直接计算:

c 1 x = ( g k ) x = g k x = ( g x ) k = y k ( m o d p ) c_1^x = (g^k)^x = g^{kx} = (g^x)^k = y^k \pmod p c1x=(gk)x=gkx=(gx)k=yk(modp)

令:

s = c 1 x ( m o d p ) s=c_1^x\pmod p s=c1x(modp)

那么 s s s 就是 ElGamal 加密时使用的共享密钥(掩码):

s = y k ( m o d p ) s=y^k\pmod p s=yk(modp)

而密文第二部分满足:

c 2 = m ⋅ s ( m o d p ) c_2=m\cdot s\pmod p c2=m⋅s(modp)

两边乘以 s − 1 s^{-1} s−1,即可恢复明文:

m = c 2 ⋅ s − 1 ( m o d p ) m=c_2\cdot s^{-1}\pmod p m=c2⋅s−1(modp)

其中 s − 1 s^{-1} s−1 是 s s s 在模 p p p 下的乘法逆元。

Python 中可以直接使用:

python 复制代码
shared = pow(c1, x, p)
m = c2 * pow(shared, -1, p) % p

pow(shared, -1, p) 就是在计算:

s h a r e d − 1 ( m o d p ) shared^{-1}\pmod p shared−1(modp)


四、为什么不需要知道 k

题目中 k 是随机生成的:

python 复制代码
k = randrange(1, p - 2)

乍看之下,似乎还需要先求出 k。实际上不需要。

因为:

c 1 x = ( g k ) x = g k x c_1^x=(g^k)^x=g^{kx} c1x=(gk)x=gkx

另一方面:

y k = ( g x ) k = g x k y^k=(g^x)^k=g^{xk} yk=(gx)k=gxk

指数乘法满足交换律,因此:

c 1 x = y k c_1^x=y^k c1x=yk

这一步直接从泄露的 x 得到了加密所需的共享密钥,完全绕过了离散对数。


五、解题脚本

python 复制代码
#!/usr/bin/env python3
from Crypto.Util.number import long_to_bytes

p = 9000784855376359808051354825193962042770028561343848432778443672755982397391267124312572697249531643069409873722736348916207732622884411596948807031140651
g = 3
y = 269130883529708333054320571854006406481346665463416017026083074488011546059928157925990665431751017523964760326934454181952822744463714981243407307134357
c1 = 5245857426274383693193378669425243235151460522527004924092730024427525619244222247576829782077334810173274945751493387545849499010408499951268967774043627
c2 = 6059939492718262451327758167005534191200936922719178843825888167191062504030471358635203794720371216217447404436172970111033824674731063386612549785069654
x = 633366293219022684108628483753423657477324253833657141033762971761747669344649667887002347907882241246119223126492863291886751205505360049793728851371884

assert pow(g, x, p) == y
shared = pow(c1, x, p)
m = c2 * pow(shared, -1, p) % p
print(long_to_bytes(m).decode())

运行:

bash 复制代码
from __future__ import annotations

import argparse
import re
from pathlib import Path

from Crypto.Util.number import long_to_bytes


def read_instance(path: Path) -> tuple[int, int, int, int, int, int]:
    """Parse p, g, y, c1, c2 and the accidentally logged x from output.txt."""
    values: dict[str, int] = {}
    pattern = re.compile(r"^\s*(p|g|y|c1|c2|x)\s*=\s*(\d+)\s*$")

    for line in path.read_text(encoding="utf-8").splitlines():
        # The official output may contain Markdown comment prefixes such as '# '.
        line = re.sub(r"^\s*#+\s?", "", line)
        match = pattern.fullmatch(line)
        if match:
            values[match.group(1)] = int(match.group(2))

    names = ("p", "g", "y", "c1", "c2", "x")
    missing = [name for name in names if name not in values]
    if missing:
        raise ValueError(f"文件缺失参数: {', '.join(missing)}")

    return tuple(values[name] for name in names)  # type: ignore[return-value]


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "-i",
        "--input",
        type=Path,
        default=Path(__file__).with_name("output.txt"),
        help="题目输出文件 (默认: output.txt)",
    )
    args = parser.parse_args()

    p, g, y, c1, c2, x = read_instance(args.input)

    # Check that the leaked exponent is consistent with the public key.
    if pow(g, x, p) != y:
        raise ValueError("泄露的 x 与公钥 y 不匹配")

    # c1^x = (g^k)^x = (g^x)^k = y^k, the ElGamal shared secret.
    shared = pow(c1, x, p)

    # c2 = m * y^k (mod p), so m = c2 / shared (mod p).
    m = c2 * pow(shared, -1, p) % p
    flag = long_to_bytes(m)

    print(flag.decode())


if __name__ == "__main__":
    main()

输出:

text 复制代码
litctf{elgamal_leak_makes_happy_decrypt}

六、脚本解释

本脚本针对ElGamal长期私钥x泄露漏洞 实现解密,整体分为文件解析、合法性校验、共享密钥还原、明文解密、输出Flag五个阶段。

1. 头部依赖导入

python 复制代码
from __future__ import annotations
import argparse
import re
from pathlib import Path
from Crypto.Util.number import long_to_bytes
  • argparse:提供命令行参数,自定义密文文件路径;
  • re:正则表达式,清洗注释、匹配并提取数值;
  • Path:跨平台安全处理文件路径;
  • long_to_bytes:将解密后的大整数明文转换为字节字符串,打印可读Flag。

2. read_instance():读取并清洗题目参数

python 复制代码
def read_instance(path: Path) -> tuple[int, int, int, int, int, int]:
    values: dict[str, int] = {}
    pattern = re.compile(r"^\s*(p|g|y|c1|c2|x)\s*=\s*(\d+)\s*$")
    for line in path.read_text(encoding="utf-8").splitlines():
        line = re.sub(r"^\s*#+\s?", "", line)
        match = pattern.fullmatch(line)
        if match:
            values[match.group(1)] = int(match.group(2))
    names = ("p", "g", "y", "c1", "c2", "x")
    missing = [name for name in names if name not in values]
    if missing:
        raise ValueError(f"文件缺失参数: {', '.join(missing)}")
    return tuple(values[name] for name in names)
  1. 逐行读取文件,先使用正则清除行首#注释标记,兼容Markdown格式的输出文本;
  2. 正则匹配 p / g / y / c1 / c2 / x 6个关键数值;
  3. 校验全部参数是否齐全,缺失则抛出异常;
  4. 返回6个大整数:素数模数 p p p、生成元 g g g、公钥 y y y、密文对 ( c 1 , c 2 ) (c_1,c_2) (c1,c2)、泄露的长期私钥 x x x。

3. 命令行入口配置

python 复制代码
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
    "-i",
    "--input",
    type=Path,
    default=Path(__file__).with_name("output.txt"),
    help="题目输出文件 (默认: output.txt)",
)
args = parser.parse_args()
p, g, y, c1, c2, x = read_instance(args.input)

通过 -i 指定输入文件;不传参数时默认读取同目录下 output.txt,调用解析函数拿到全部密码学参数。

4. 私钥一致性校验(安全校验)

python 复制代码
if pow(g, x, p) != y:
    raise ValueError("泄露的 x 与公钥 y 不匹配")

ElGamal公钥定义: y = g x ( m o d p ) y = g^x \pmod p y=gx(modp)。

这一步用来验证日志泄露的私钥x是合法可信的值,防止参数篡改或错误。校验失败直接抛出异常终止程序。

5. 核心解密逻辑

python 复制代码
shared = pow(c1, x, p)
m = c2 * pow(shared, -1, p) % p
flag = long_to_bytes(m)
print(flag.decode())
  1. 计算共享掩码密钥

    s h a r e d = c 1 x ( m o d p ) shared = c_1^x \pmod p shared=c1x(modp)

    利用指数交换性质: c 1 = g k ⇒ c 1 x = g x k = y k c_1=g^k \Rightarrow c_1^x = g^{xk}=y^k c1=gk⇒c1x=gxk=yk,直接得到加密时的会话密钥,无需破解离散对数求随机数 k k k

  2. 求模逆并还原明文

    密文满足 c 2 = m ⋅ s h a r e d ( m o d p ) c_2 = m \cdot shared \pmod p c2=m⋅shared(modp),变形得到:

    m = c 2 ⋅ s h a r e d − 1 ( m o d p ) m = c_2 \cdot shared^{-1} \pmod p m=c2⋅shared−1(modp)

    pow(shared, -1, p) 用于计算模 p p p 乘法逆元,完成除法运算。

  3. 格式转换输出

    long_to_bytes(m) 将数值明文还原为原始字节流,decode() 转为字符串输出最终Flag。

6. 执行入口

python 复制代码
if __name__ == "__main__":
    main()

保证脚本可以直接独立运行,作为完整的离线解密利用程序。

小结攻击思路

漏洞根源:服务端意外打印了长期私钥 x x x

攻击捷径:不用求解离散对数、不用爆破随机数 k k k;直接通过泄露私钥还原会话共享密钥,一步完成ElGamal解密。

代码 作用
pow(g, x, p) 验证泄露的私钥 x 确实对应公钥 y
pow(c1, x, p) 计算共享密钥 c 1 x = y k c_1^x=y^k c1x=yk
pow(shared, -1, p) 计算共享密钥的模逆
c2 * ... % p 从 c 2 = m ⋅ y k c_2=m\cdot y^k c2=m⋅yk 中消去共享密钥,恢复 m m m
long_to_bytes(m) 将整数明文还原成字节串
.decode() 将字节串转换为可读字符串

七、实际计算结果

本题计算得到的共享密钥为:

text 复制代码
s = 4062928099126093781468426744109214385048532272466318905272555255697923612733981316319590946428151660170631877259053272768441667830635194496511945007603360

解出的明文整数为:

text 复制代码
m = 904556569191331988668603914708495020853184821941780054054172829706415872888837467726082984670333

将其转换为字节串:

text 复制代码
b'litctf{elgamal_leak_makes_happy_decrypt}'

Flag

text 复制代码
litctf{elgamal_leak_makes_happy_decrypt}

Key Takeaway

  • ElGamal 的安全性依赖于私钥指数 x 不被泄露。
  • 这里不需要求解离散对数,也不需要恢复临时随机数 k
  • 私钥泄露后可直接计算 c 1 x = y k c_1^x=y^k c1x=yk,再用模逆还原明文。
  • 调试日志、异常日志和生产日志中都不应输出长期私钥、会话密钥或随机数等敏感材料。
相关推荐
黄金龙PLUS4 小时前
SPARKLE置换算法的优缺点
算法·网络安全·密码学·哈希算法·同态加密
却道天凉_好个秋2 天前
密码学(二):base64原理
密码学·base64
0xBADCODE3 天前
CTF Writeup 合集
安全·web安全·网络安全·系统安全·密码学·php·ctf
Sagittarius_A*3 天前
协议与应用基础(三):HTTPS、证书链与中间人攻击
网络协议·http·信息安全·https·密码学·中间人攻击·证书链
玫幽倩4 天前
2025MoeCTF(Pwn全)
网络·安全·pwn·ctf·新生赛·二进程·moectf
岁岁的O泡奶4 天前
[NSSCTF 2022 Spring Recruit]babyphp
经验分享·web·ctf
0xBADCODE4 天前
动态DEX加载+反射+DES硬编码密钥:安卓三层逆向实战
android·java·python·安全·网络安全·逆向·ctf
黄金龙PLUS4 天前
ASCON置换算法的优缺点
算法·网络安全·密码学·哈希算法·同态加密
纯.Pure_Jin(g)6 天前
靶场PHP函数整理+防御方式
开发语言·php·ctf