moectf-2026密码学方向writeup-牢大学密码真的有出路吗
密码学入门指北
Crypto 入门指北
作者:youzi、hata
Note:以下介绍内容若有不能理解的,可以多问问 AI 大模型
一、什么是密码学 ?
引言
与大家熟知的”账号密码”不同的是,密码学研究的是如何利用数学规律和算法对信息进行加密/解密。通过数学上不可解决的困难问题或算法设计,来确保信息安全。而大家一般设置的”账号密码”,学名为”口令(password)“,是密码学的一个研究范畴。
密码学的基本目的是使得两个在不安全信道中通信的人,通常称为 Alice 和 Bob,以一种使他们的敌手 Eve 不能明白和理解通信内容的方式进行通信。而这样的不安全信道在实际中是普遍存在的,例如电话线或计算机网络。一段明文经过密钥加密后,就会变为一段”让人看不懂”的密文,而经过密钥解密后,就会变回有意义的明文。
一个非常反初学者直觉的是,正确实现并被证明安全的加密方案不可能被任何人攻破(除非在理论研究上产生突破)。在 CTF 比赛中,出题人一般会公开加密算法,但会暴露出一些漏洞或者提供额外信息,使得存在攻击方法。
研究方面
密码学大体可分为古典密码学和现代密码学。而现代密码学则是 CTF 比赛中的重点,也是我们主要学习的。

上面这张图很好地展示了基本上所有常见加密体系都在做的事情:首先,通过一个生成算法来随机生成一对用于加密和解密的密钥;加密方通过加密密钥和加密算法来加密原文,得到密文;解密方通过解密密钥和解密算法来解密密文,最后还原出原文。
根据加密/解密密钥是否相同,可分为对称密码体系和非对称密码体系:
- 对称:加密/解密密钥相同,以 DES、AES 等为代表。
- 非对称:加密/解密密钥不同,以 RSA、ElGamal 等为代表。
当然,密码学不只研究加密/解密,还有例如哈希函数、数字签名等内容,这些在本次 MoeCTF 中均有可能涉及到!
二、什么是 CTF 比赛中的 Crypto 方向 ?
CTF 比赛中的 Crypto 方向,一般来说注重于现代密码学中的 Cryptoanalysis(密码分析)。在比赛中,出题人往往会将加密算法和密文等信息公开给参赛选手。参赛选手需要分析加密算法中的缺点、漏洞,或是利用题目提供的额外信息,寻找相应的求解方法。
大体上题目可分为两类:
- 静态题目:题目会提供若干附件,选手只需要分析附件,编写脚本,即可得到 flag。
- 动态题目:题目除了提供附件外,还会提供一个在线环境,在平台上体现为”题目可开启在线环境”,需要按照要求与远程环境进行交互,满足一定条件后就可以得到 flag 或者拿到求解 flag 所需数据。(远程题目一般需要使用
nc指令连接远程环境,具体操作见下面内容)
密码人需要掌握常见密码算法原理,在题目练习中学习攻击套路,以及熟练编写解题脚本。这样就能顺利地读懂并解决一些简单的 CTF 题目。未来,你一定会遇到更加困难的 CTF 题目,这时则需要多思考,多阅读大佬/出题人的 wp(writeup,指解题报告),学习解题思想,不断积累,进步。
三、基础知识与工具介绍
Note:笔者的建议是先做题,遇到不会的再去学习。不必学完知识再做题。
打 CTF 和做学术是不一样的,前者更需要的是你在面对具体题目的时候,即时学习并且运用的能力。
基础知识
这里列举一些可能会用到的知识:
- 初等数论(数竞生狂喜):整除、gcd、同余、剩余系等
- 现代密码学:RSA、DES 等等
- 线性代数、近世代数:向量、矩阵、群、环、域等
- 算法基础:复杂度计算、二分、DP、快速幂等
关于数学知识和算法知识,大家可以从数学部分简介 - OI Wiki以及基础数学知识 - CTF Wiki上学习。但笔者建议还是多看看书,这里给大家推荐一些我入门的书籍:
- 密码学数学知识一本通:《公钥密码学的数学基础》(第二版)王小云
- 进阶学习:《Mathematics of Public Key Cryptography》(Steven D. Galbraith)
- 数论深入学习:《数论讲义 上册》(第二版)柯召、孙琦
- 密码原语学习:《现代密码学》(第5版)杨波
但是但是但是,千万不要被数学给劝退!
眼下你只需了解个大概即可(甚至不怎么了解也无所谓),正如一开始所说的 CTF 更需要的是在面对具体题目的时候,即时学习并且运用的能力,结合题目慢慢来或许更轻松一些。
相关工具
Python
就像中学时代的习题,需要用数学符号和语言来描述,CTF 题目也需要一个载体,而大多数题目基于 Python 这一编程语言。建议先完成赛事中”从此开始”部分的 Python 相关内容。
下面仅介绍几个建议安装的 Python 包:
1. pycryptodome
提供了各种密码学算法的实现,最重要,基本上所有题目都会用到:
pip install pycryptodome2. pwntools
交互题好帮手,但是只能在 Linux 环境下使用。推荐阅读题目 [wsl_Sign1n] 提供的附件后安装使用:
pip install pwntoolsMarkdown
Markdown 是一种轻量级标记语言,排版语法简洁。平常我们见到的公式、图片、代码块等富文本,就可以通过 Markdown 来编写。
有些时候,题目或 writeup 中会给出 .md 文件,这就是用 Markdown 编写的文档。当然,你可以用记事本打开或编辑 .md 文件,但它不能渲染。你可以使用在线 Markdown 编辑器进行预览,也可以使用 VS Code 的插件——Markdown All in One,也可以使用轻量的编辑器 Typora。在这里,笔者推荐 Obsidian,有着更强大的知识库管理和插件支持:Obsidian - Sharpen your thinking
SageMath
SageMath 是一个基于 Python 语言的强大数学工具,在 Crypto in CTF 中有着广泛的应用。
你可以将其简单地理解为集成了许多第三方库的 Python,因此其基础语法和 Python 是一样的,很好上手!
有关它的安装与使用,我在 Moe 2026 出了一道题 [wsl_Sign1n],其中给出了详细的安装指南,只要你在 WSL 里成功安装就能获得 flag。
题目连接
比赛中有不少题目都需要连接远程环境,因此同样建议先完成”从此开始”中的相关内容。
总之,CTF 无论什么方向,第一步肯定都绕不过工具安装和环境配置,希望你能克服困难,迈出这一步!
四、如何学习
在这里笔者会提供一些有用的学习渠道,有效弥补信息差。
1. LLM & Agent
AI 可以说是这个时代最重要的学习工具。首先,在大模型的选择上,你要做的第一件事就是扔掉你的豆包(它的幻觉会坑死你),可以选择 DeepSeek/GLM/MiniMax 等国产模型,感兴趣的话也可以试试 GPT、Claude Opus 等国外模型。当然,如果你从未尝试过,那么我们强烈推荐 DeepSeek | 深度求索,登录即用,便捷且强大!
**Agent(智能体)**的本质是自动执行任务的程序,核心在于让大模型不只回答问题,而是按步骤完成电脑操作。通过接入大模型,Agent 可以在你的电脑上完成它能做到的任意操作,比如读取本地的资料并教你学习知识,帮你下载东西/配置环境,在本地编写代码并测试。以上任务只要和 Agent 说”一些”话就能完成。
比较实用的 Agent 有 Claude Code、Codex 等,你可以在网上搜索这些强大工具的使用方法!
2. CTF 练习
在实践中学习,切忌纸上谈兵!在学习知识的基础之上,多去训练场练习,多参加比赛。
- 训练 - 西电 CTF 终端 xdu练习场,推荐在这里练习往年 MoeCTF 的题目,在互联网上查找题解,加速入门!
- CTF²——一个练习场
- CryptoHack——题目种类多,难度平缓,非常不错的练习网站
3. Blogs
多读大佬的博客和 WP 进行学习:
- Van1sh:https://jayxv.github.io/
- 糖醋小鸡块:https://tangcuxiaojikuai.xyz/
- 传奇密码手 d33b4t0:https://d33b4t0.com/(适合进阶观看)
4. 其它有用的
- Z-Library——电子书下载
- 维基百科——自由的百科全书,关于密码学的内容收录很详细
- Cryptology ePrint Archive——密码学论文收录
五、如何应对困难
- 遇到不会做的题目时,一种行之有效的方法是去互联网上搜索类似的题目,比如搜索关键词
ctf crypto rsa,结合题目考察知识点,加上ctf crypto这些关键词,可以搜到很多有用的东西! - 为什么不问问万能的 AI 呢?遇到所有的困难都可以求助 AI,比如说”我该如何安装 Python?“,只需根据指引一步一步来,遇到问题再次询问即可。另外注意问法会有非常大的影响(不要说:“直接给我 flag”,这样你什么都学不到)。事实上,AI 100% 可以解决此次 MoeCTF 的所有密码题目,但是,我们仍希望你在最初的阶段能够善用 AI,希望你能从中学到知识,收获乐趣!
- 阅读《提问的智慧》,领会其主要精神。这对你提问 AI 也很有帮助,当然你也可以去求助群里的 Crypto 管理员,我们会尽可能地提供帮助!
- 尝试改变你的求解策略,切忌钻牛角尖。“山重水复疑无路,柳暗花明又一村。“
这是密码学的签到题,请阅读密码学入门指北,开启密码学的旅途吧。
pdf里面有一道基于 Diffie-Hellman 密钥交换的加密题目,
DH 密钥交换简介
Diffie-Hellman(DH)密钥交换是一种经典的非对称密码协议,由 Whitfield Diffie 和 Martin Hellman 于 1976 年提出。它允许两个通信方(Alice 和 Bob)通过不安全的公开信道协商出一个共享密钥,而窃听者(Eve)即便截获了所有通信内容,也无法计算出这个共享密钥。
核心数学原理:基于离散对数问题(Discrete Logarithm Problem, DLP)的困难性——即给定 ,在已知 和 的情况下,计算 是极其困难的(当 很大时)。
DH 密钥交换的工作流程:
公开参数:大质数 p 和生成元 g
Step 1: Alice 选择私钥 a(保密) Alice 计算公钥 A = g^a mod p,将 A 发送给 Bob
Step 2: Bob 选择私钥 b(保密) Bob 计算公钥 B = g^b mod p,将 B 发送给 Alice
Step 3: Alice 计算共享密钥: shared_secret = B^a mod p = (g^b)^a = g^(ab) mod p Bob 计算共享密钥: shared_secret = A^b mod p = (g^a)^b = g^(ab) mod p
结果: Alice 和 Bob 得到了相同的共享密钥 g^(ab) mod p安全性分析:
- 攻击者 Eve 可以截获 , , , ,但无法在合理时间内从 计算出私钥
- 攻击者无法从 计算出私钥
- 因此,攻击者无法计算出共享密钥
DH 的经典攻击场景:
| 攻击方式 | 描述 |
|---|---|
| 离散对数攻击 | 当 太小时,可直接求解 |
| 中间人攻击(MITM) | 攻击者在 Alice 和 Bob 之间截获并替换公钥 |
| 私钥泄露 | 若任何一方的私钥泄露,共享密钥即可被公开计算(本题的攻击点) |
| 小子群攻击 | 当 有小的素因子时,可降维恢复私钥 |
alice_public = pow(g, alice_private, p) #生成Alice的公钥bob_public = pow(g, bob_private, p) #生成Bob的公钥shared_secret = pow(bob_public, alice_private, p) #共享密钥key = shared_secret % 256flag = b"moectf{...}"ciphertext = [x ^ key for x in flag] #逐字节异或加密print(f"p = {p}")print(f"g = {g}")print(f"alice_public = {alice_public}")print(f"bob_public = {bob_public}")print(f"alice_private = {alice_private}") #泄露了私钥!print(f"ciphertext = {ciphertext}")"""6 / 7p = 170141183460469231731687303715884105727g = 3alice_public = 42348823944539164637318232035973172471bob_public = 101533001028006636636416596392258549313alice_private = 20260704ciphertext = [45, 47, 37, 35, 52, 38, 59, 36, 40, 31, 48, 50, 41, 54, 33, 52,37, 31, 43, 37, 57, 31, 51, 40, 47, 53, 44, 36, 31, 51, 52, 33, 57, 31, 51,37, 35, 50, 37, 52, 61]""从输出结果来看,题目泄露了alice_private,即alice的私钥,那么可以使用
shared_secret = pow(bob_public, alice_private, p)解出共享密钥,再由题目中对key的提示key = shared_secret % 256,取低八位,得到key。
从ciphertext = [x ^ key for x in flag]可以看出是对flag的每个字节使用key进行按位异或,那么进行逆运算得到脚本
p = 170141183460469231731687303715884105727bob_public = 101533001028006636636416596392258549313alice_private = 20260704ciphertext = [45, 47, 37, 35, 52, 38, 59, 36, 40, 31, 48, 50, 41, 54, 33, 52, 37, 31, 43, 37, 57, 31, 51, 40, 47, 53, 44, 36, 31, 51, 52, 33, 57, 31, 51, 37, 35, 50, 37, 52, 61]
shared_secret = pow(bob_public, alice_private, p)key = shared_secret % 256flag = bytes([x ^ key for x in ciphertext])print(flag.decode())输出结果
moectf{dh_private_key_should_stay_secret}moeSign1n
只需要输入比赛的名称,就能得到 flag …吗?
源码:
from Crypto.Util.number import *from random import randint
flag = b'moectf{???}'p = getPrime(512)q = getPrime(512)n = p * qphi = (p - 1) * (q - 1)while 1: e = randint(2, n) if GCD(e, phi) == 1: d = pow(e, -1, phi) break
menu = '''1. get ciphertext.2. submit.3. quit.'''
while 1: print(menu) choice = int(input()) if choice == 1: m = bytes.fromhex(input("what message(hex form) do u want to encrtpt?\n")) if m == b'MoeCTF 2026': print('no cheat!') continue print(pow(bytes_to_long(m), e, n)) elif choice == 2: enc = int(input("plz sumbit the ciphertext.\n")) dec = long_to_bytes(pow(enc, d, n)) if dec == b'MoeCTF 2026': print("Welcome to MoeCTF 2026!") print(flag) exit() else: print("incorrect!") elif choice == 3: print("bye!") exit() else: print("invalid input!") continuejustXOR
真正的大高衔接 belike…
源码:
from Crypto.Util.number import long_to_bytes
flag = b'moectf{???}'# just a normal big primeM = 2039129633208009090414901212304234091626233923923301042398416489123719065081065776033127561876033127924471
def gen_key(base, n): a = [base] for _ in range(n - 1): new_val = (3 * a[-1] + 2) % M a.append(new_val) return a[-1]
n = 10**25key = gen_key(1, n)enc = bytes([x ^ y for x, y in zip(flag, long_to_bytes(key))])# b'it\x0bM\xfa-\xe3T{\xaa\x0f@\xa2\xf1@\xbd\x86e\x85\x9e\xfcp_o\x8f\xccd\x13\xceW\x13\x14\x11\x055b\xcbk\xa0'可以看出本题的核心是将key解出,将key与enc按位异或可得出flag。
观察key的生成函数可以发现,有
def gen_key(base, n): a = [base] for _ in range(n - 1): new_val = (3 * a[-1] + 2) % M a.append(new_val) return a[-1]发现
迭代次数 ,不可能直接循环计算。
因此一定能算出一个递推公式
首先发现
根据加密先用待定系数法可以设出
首先
可以先推出指数,这里的c = 3。
接着设定特殊常数解:
再代入已知的值有 :
那么
得到脚本:
from Crypto.Util.number import long_to_bytesn = 10**25M = 2039129633208009090414901212304234091626233923923301042398416489123719065081065776033127561876033127924471enc = b'it\x0bM\xfa-\xe3T{\xaa\x0f@\xa2\xf1@\xbd\x86e\x85\x9e\xfcp_o\x8f\xccd\x13\xceW\x13\x14\x11\x055b\xcbk\xa0'key = (2 * pow(3, n - 1, M) - 1) % Mflag = bytes([x ^ y for x, y in zip(enc, long_to_bytes(key))])print(flag.decode())得到输出结果:
moectf{a_simple_sequence_problem_for_u}ez_f3mr4t
Thi5 rs4 s3ems e2.
源码:
from Cryptodome.Util.number import *from gmpy2 import next_primefrom secret import flag
p = getStrongPrime(512)q = next_prime(p ^ ((1<<512)-1))n = p * qe = 65537
m = long_to_bytes(flag)assert m < nc = pow(m, e, n)
#n = 0x308244e7a7de386723c92ba62e35bc22c3ec1b93023e1551408344a4ba31c6203da849aedee0cadf26a3442f9fd652f7d97c053a3f2c298eab6c0f0c2d0b4642a81765ddb00b690425eb212d5520327ac2d53a22922448399fecb54fbc04dbb68fa33fee7666cb9e05278b5f5f1330b3918d3a7def580fcc00f6f596f16eba3b#c = 0x13a77e34f09b8a2291cdb397ea0e5cb9e86f691c6565b35c76e6bb6248b67db24315e22fe1dc5321a8820320cdd9b51e3d431459aac2213948f4fbf01c4ce974428d1ed745b2c06f8aa92b22dfede3b7ceb59aa4eb22467129f55b60037a1a2de9b37f25fda3fe40323fccc7c8bbdd4446096b37cef4138337ee9a04c2e3d5fd这看起来是一道RSA加密,特殊之处在于q,
q = next_prime(p ^ ((1<<512)-1))的意思是计算一个比p ^ ((1<<512)-1)大的最小素数,即p和q非常接近,可以从这里下手
n为512位,网站分解不了
那么由于两素数因子接近,那么我们可以使用平方差公式来分解n来得到两个因子,这道题目的解题与NumberTheory-FeeeeeMa(强壮密码人)非常相似
那么用类似的脚本看看
import gmpy2from Crypto.Util.number import *
c = 0x13a77e34f09b8a2291cdb397ea0e5cb9e86f691c6565b35c76e6bb6248b67db24315e22fe1dc5321a8820320cdd9b51e3d431459aac2213948f4fbf01c4ce974428d1ed745b2c06f8aa92b22dfede3b7ceb59aa4eb22467129f55b60037a1a2de9b37f25fda3fe40323fccc7c8bbdd4446096b37cef4138337ee9a04c2e3d5fdn = 0x308244e7a7de386723c92ba62e35bc22c3ec1b93023e1551408344a4ba31c6203da849aedee0cadf26a3442f9fd652f7d97c053a3f2c298eab6c0f0c2d0b4642a81765ddb00b690425eb212d5520327ac2d53a22922448399fecb54fbc04dbb68fa33fee7666cb9e05278b5f5f1330b3918d3a7def580fcc00f6f596f16eba3be = 65537
t = gmpy2.isqrt(n) + 1s = gmpy2.isqrt(t * t - n)p = t - sq = t + sassert p * q == n
phi = (p - 1) * (q - 1)d = int(gmpy2.invert(e, phi))m = pow(c, d, n)flag = long_to_bytes(m).decode()print(f"\n{flag}")发现报错
Traceback (most recent call last): assert p * q == nAssertionError那么用这个方法得出的p和q不能通过检验,需要换种方法。
重新看题发现不对劲,deepseek居然敢耍我,(1<<512)-1)是512位1再减1,那么p与q并不接近,上面的方法根本行不通。
那重新分析q,发现p ^ ((1<<512)-1)是对p进行按位异或,又因为全是一,那么这个式子就是对p按位取反。
由于按位取反 = 从全 1 数中减去该数
p ^ ((1<<512)-1) = (2^512 - 1) - pp + q = 2^512 - 1得出p + q的准确值,那么可以使用完全平方公式求出D = (p+q)² - 4n,检验D是否为完全平方数来验证求出的p和q的正确性
写了个简单的脚本发现验证并不正确,原来next_prime是取下一个素数,而两个相邻的素数一般不会差的太远,所以需要加一个偏移量来保证q的准确性
设 S = 2^512 - 1,遍历小偏移量 k,令 p+q = S + k
得到脚本:
import gmpy2from Crypto.Util.number import *
c = 0x13a77e34f09b8a2291cdb397ea0e5cb9e86f691c6565b35c76e6bb6248b67db24315e22fe1dc5321a8820320cdd9b51e3d431459aac2213948f4fbf01c4ce974428d1ed745b2c06f8aa92b22dfede3b7ceb59aa4eb22467129f55b60037a1a2de9b37f25fda3fe40323fccc7c8bbdd4446096b37cef4138337ee9a04c2e3d5fdn = 0x308244e7a7de386723c92ba62e35bc22c3ec1b93023e1551408344a4ba31c6203da849aedee0cadf26a3442f9fd652f7d97c053a3f2c298eab6c0f0c2d0b4642a81765ddb00b690425eb212d5520327ac2d53a22922448399fecb54fbc04dbb68fa33fee7666cb9e05278b5f5f1330b3918d3a7def580fcc00f6f596f16eba3be = 65537
S = (1 << 512) - 1
for k in range(100000): p_plus_q = S + k D = p_plus_q ** 2 - 4 * n if D < 0: continue if gmpy2.is_square(D): sqrt_D = int(gmpy2.isqrt(D)) p = (p_plus_q + sqrt_D) // 2 q = (p_plus_q - sqrt_D) // 2 if p * q == n: print(f"k = {k}") break
assert p * q == n
phi = (p - 1) * (q - 1)d = int(gmpy2.invert(e, phi))m = pow(c, d, n)flag = long_to_bytes(m).decode()print(f"{flag}")输出结果:
k = 45moectf{F3rm4t_i5_inde3d_7h3_k1ng_0f_@mat3ur_m4them@t1an5}ez_fermat
This rsa seems ez?
源码:
from Crypto.Util.number import bytes_to_long, getPrime
flag = open("flag.txt", "rb").read().strip()m = bytes_to_long(flag)
e = 65537p = getPrime(1024)q = getPrime(1024)n = p * qc = pow(m, e, n)
a, b = 0x2025, 0x2026hint = pow(a * p + b, q, n)
with open("cipher.txt", "w") as f: f.write(f"n = {n}\n") f.write(f"c = {c}\n") f.write(f"hint = {hint}\n")txt文件:
n = 15962603324053600624662899467930954606237037554051572101066176482327650464579501876010780067612122315010349894395035821617153081982429716234378979239633769779503395609504372912298014522344609346937078959678893005192469085672940628091531228571447786013091492389216237976408692434723698967667324605921896928717731731591488800520224411239128106892402734198486634978989319563704879076976307439748456769245703782902660583833942684475506024284563786886608632662629581206716450648212025254862836883526055933841863348546872473877054469843468332542834452213365868510001991952142536759384940216228753729399187149594010090304067c = 4281919068424886012214413435957379635517466043647380176350221253614623990599452495938689758001256045320061491376355750164051326734927905972267385611552894838140009898450267453653097466286838460889968896032216593811984942511165228143089457988630435810304672454147149556110542203084341901598519587611096893432307236558970264536719689039318023572680185246871600989317992399536729717958072642243834689061928667913495884682029212015071613730401843271641197254089900489231738518876415262001687002012972665662364940457678043829768126263211295258465202419686281997749861180041439407866197636369525192971926660559217573713931hint = 8894364790280693556124457666776501614023509475927164016497858629229215409128965020674642063592770935057209719989294548263833927477972267267951214184209046168773196349537926982123382473522039515862138755400978350128128233701795758152143272668945626119949376162344749760499165210224412751964322150037199401111258869439672762883792051818112500032192131948644557686621759867629440824691768395882797936616839572906841251000681054283164497419314938363871139253289134458466501698235039234455154975165111296198209091586550990873209534256929400689017034618807232508747449076486852786199340704419120538526208691066898035362296这是一道RSA,前面的都是经典RSA,而不一样的部分在a和b,p和q都是1024位,不能直接用网站一把梭。
hint是解出p和q的关键,hint = pow(a * p + b, q, n)换成数学式子可以看作hint = (a*p + b)^q mod n,而n = p*q,那么可以有hint = (a*p + b)^q mod p,由于a*p mod p = 0,那么上式可以等同于hint = b^q mod p,下一步的思路是消去p或者q,运用最小公约数得出n的一个因子
由费马小定理得b^n (mod p)= b^(p*q) (mod p)= (b^p)^q (mod p)≡ b^q (mod p)
代入hint中可以得出hint = b^n mod p,这样就只剩p是未知数了,并且p | (hint - b^n)
那么对p求最小公约数,即利用上式与n的最小公约数来求出p,但是b^n的体量太大根本运行不了,那么要利用同余来减少hint-b^n的体量,于是增加mod n作为限制条件
即(hint - b^n) mod n = (hint - (b^n mod n)) mod n,而(b^n mod n)可以用pow(b, n, n)
得到p = gcd((hint - pow(b, n , n)) mod n, n)。
得到脚本:
from math import gcdfrom Crypto.Util.number import long_to_bytes, inverse
n = 15962603324053600624662899467930954606237037554051572101066176482327650464579501876010780067612122315010349894395035821617153081982429716234378979239633769779503395609504372912298014522344609346937078959678893005192469085672940628091531228571447786013091492389216237976408692434723698967667324605921896928717731731591488800520224411239128106892402734198486634978989319563704879076976307439748456769245703782902660583833942684475506024284563786886608632662629581206716450648212025254862836883526055933841863348546872473877054469843468332542834452213365868510001991952142536759384940216228753729399187149594010090304067c = 4281919068424886012214413435957379635517466043647380176350221253614623990599452495938689758001256045320061491376355750164051326734927905972267385611552894838140009898450267453653097466286838460889968896032216593811984942511165228143089457988630435810304672454147149556110542203084341901598519587611096893432307236558970264536719689039318023572680185246871600989317992399536729717958072642243834689061928667913495884682029212015071613730401843271641197254089900489231738518876415262001687002012972665662364940457678043829768126263211295258465202419686281997749861180041439407866197636369525192971926660559217573713931hint = 8894364790280693556124457666776501614023509475927164016497858629229215409128965020674642063592770935057209719989294548263833927477972267267951214184209046168773196349537926982123382473522039515862138755400978350128128233701795758152143272668945626119949376162344749760499165210224412751964322150037199401111258869439672762883792051818112500032192131948644557686621759867629440824691768395882797936616839572906841251000681054283164497419314938363871139253289134458466501698235039234455154975165111296198209091586550990873209534256929400689017034618807232508747449076486852786199340704419120538526208691066898035362296
e = 65537a, b = 0x2025, 0x2026
bn_mod = pow(b, n, n)
diff = (hint - bn_mod) % n
p = gcd(diff, n)q = n // p
assert p * q == n
phi = (p - 1) * (q - 1)d = inverse(e, phi)m = pow(c, d, n)flag = long_to_bytes(m)
print(flag.decode())运行输出:
moectf{f3rmat_l1ttl3_th30r3m_l34ks_p_1n_2025}the_KEY_of_DES
Try to find a “collision” in DES.
源码:
from Crypto.Cipher import DESfrom random import randbytes
flag = b'moectf{???}'print("Do u know the length of DES'key? That may be the key of DES.")m = randbytes(8)print("Here is the message. Please enter two different keys to generate the same ciphertext.")print(m)try: key1 = bytes.fromhex(input("input the first key(hex): ")) key2 = bytes.fromhex(input("input the second key(hex): ")) des1 = DES.new(key=key1, mode=DES.MODE_ECB) c1 = des1.encrypt(m) des2 = DES.new(key=key2, mode=DES.MODE_ECB) c2 = des2.encrypt(m)except: print("something wrong.") exit()
if key1 == key2: print("no cheat!") exit()
if c1 == c2: print("You did it! Here is the flag") print(flag) exit()else: print("bye!") exit()XOR_revenge
这次你能求出通项吗?
源码:
from Crypto.Util.number import long_to_bytes
flag = b'moectf{???}'
# just a normal big primeM = 2**521 - 1
COEFF = [3, 5, 7, 11, 13, 17, 36]INIT = [1, 2, 3, 4, 5, 6, 7]
def gen_key(n): f = INIT[:] if n < 7: return f[n] for _ in range(7, n + 1): nxt = sum(COEFF[j] * f[6 - j] for j in range(7)) % M f = f[1:] + [nxt] return f[6]
n = 10**25key = gen_key(n)enc = bytes([x ^ y for x, y in zip(flag, long_to_bytes(key))])# b'lz\xb0T\xeb\x18\xec\xcfQ\x8bv#\x17t\xf0\xc7\xdaM\x87\xf5sr\xef\xd0\xfc3.\xdc%\xaaf\xf4\xd1\xc8\xb7\xef'这是XOR的对每个字符进行异或加密
可以发现n和m都是大数字,不可以直接逆运算
题目有提示找出通项,看加密代码
def gen_key(n): f = INIT[:] if n < 7: return f[n] for _ in range(7, n + 1): nxt = sum(COEFF[j] * f[6 - j] for j in range(7)) % M f = f[1:] + [nxt] return f[6]得出
f[k] = 3·f[k-1] + 5·f[k-2] + 7·f[k-3] + 11·f[k-4] + 13·f[k-5] + 17·f[k-6] + 36·f[k-7] (k ≥ 7)这里应该是要运用一些方法将n降幂,将O(25**10-6)降低
搜索一下发现一般用两种方法,矩阵快速幂和Kitamasa算法,对于我来说Kitamasa算法更加好理解一些
得出特征多项式
x^7 ≡ 3·x^6 + 5·x^5 + 7·x^4 + 11·x^3 + 13·x^2 + 17·x + 36因此可以将f[n]拆分成f[n-7]^f[7]即对多项式进行取余,一直降到最高次数小于7为止
并且运用二次幂,将n的幂拆成二进制,即
底数 x、x²、x⁴、x⁸、… (每步平方一次)指数二进制位:1 1 0 1 (只有二进制位是 1 才乘进结果)这样就log2(10^25)≈83,只要约 83 步。
那么总算法分为两步,一步是将f[n]分成多项式,另一步是对多项式进行快速幂运算
from Crypto.Util.number import long_to_bytes
M = 2**521 - 1COEFF = [3, 5, 7, 11, 13, 17, 36]INIT = [1, 2, 3, 4, 5, 6, 7]
def combine(a, b, c, k, mod): # a、b:两个多项式,用系数列表表示(下标 i 对应 x^i 的系数,i < k) # 功能:计算 a*b,再对特征多项式取余,返回一个次数 < k 的多项式 tmp = [0] * (2*k - 1) # 两个次数<k的多项式相乘,次数最高到 2k-2 for i in range(k): if a[i]: ai = a[i] for j in range(k): tmp[i+j] = (tmp[i+j] + ai * b[j]) % mod
# 从最高次 downto 第 k 次,逐项降次: # t·x^i = t·x^(i-k)·x^k ≡ t·x^(i-k)·Σ_j c[j]·x^(k-1-j) = Σ_j t·c[j]·x^(i-1-j) for i in range(2*k - 2, k - 1, -1): if tmp[i]: ti = tmp[i] for j in range(k): tmp[i-1-j] = (tmp[i-1-j] + ti * c[j]) % mod return tmp[:k]
def kitamasa(n, init, c, mod): k = len(c) pol = [0] * k pol[0] = 1 # 结果多项式,初始化为 1(乘法单位元) e = [0] * k e[1 if k > 1 else 0] = 1 # 底数多项式 = x,即系数列表 [0,1,0,0,0,0,0] while n: # 二进制快速幂 if n & 1: # n 的当前二进制位是 1 pol = combine(pol, e, c, k, mod) # 乘进结果 e = combine(e, e, c, k, mod) # 底数每次平方 n >>= 1 # 看下一位 # 循环结束:pol 就是 x^n 对特征多项式取余的结果,即 a[0]+a[1]x+...+a[6]x^6 return sum(pol[i] * init[i] for i in range(k)) % mod
key = kitamasa(10**25, INIT, COEFF, M)enc = b'lz\xb0T\xeb\x18\xec\xcfQ\x8bv#\x17t\xf0\xc7\xdaM\x87\xf5sr\xef\xd0\xfc3.\xdc%\xaaf\xf4\xd1\xc8\xb7\xef'flag = bytes([x ^ y for x, y in zip(enc, long_to_bytes(key))])print(flag)combine:多项式相乘 + 取余
-
第一步
tmp[i+j] += a[i]*b[j]:普通多项式乘法。两个次数 < 7 的多项式相乘,次数最高到7+7-2 = 12,所以数组长度2k-1 = 13。 -
第二步降次:从最高次
12往下扫到k=7,只要tmp[i]非零(系数是t),就把这一项”拆”成更低的项。 关键公式推导:t·x^i = t·x^(i-k)·x^k≡ t·x^(i-k)·(c[0]x^(k-1) + c[1]x^(k-2) + ... + c[k-1]) ← 用特征多项式换掉 x^k= t·c[0]·x^(i-1) + t·c[1]·x^(i-2) + ... + t·c[k-1]·x^(i-k)
kitamasa:对多项式做二进制快速幂
pol是”累计结果”,初始为1(乘法单位元,即只有常数项)。e是”底数”,初始为x(系数列表[0,1,0,0,0,0,0])。n的二进制位是 1 就pol = pol * e(在模特征多项式意义下),然后e = e * e(平方)。- 结束时
pol = x^n (mod 特征多项式)。
3)最后一步
x^n ≡ a[0] + a[1]x + ... + a[6]x^6,代入 B 节的对应关系,得到:
f[n] = a[0]·f[0] + a[1]·f[1] + ... + a[6]·f[6] = Σ pol[i]·INIT[i]就是 return sum(...) 那行。
得到
key : 3725129891528866480039250733345517340363920970037583787168208825011199634276657855793038655312920910100892325677030330013209440859443990621503014209963444464flag: moectf{m4tr1x_p0w3r_1s_4ll_y0u_n33d}后面用ai查看了一下用矩阵的做法
方法:矩阵快速幂
下面从零开始,把”矩阵”相关的每个概念都拆开讲清楚,配合纸笔跟着算效果更好。
A. 为什么线性递推适合用矩阵?
先回顾递推式(从 gen_key 的滑动窗口推出):
f[k] = 3·f[k-1] + 5·f[k-2] + 7·f[k-3] + 11·f[k-4] + 13·f[k-5] + 17·f[k-6] + 36·f[k-7] (k ≥ 7)核心观察:算新一项 f[k] 时,只需要”前面 7 个数”;而新项恰好是这 7 个数的线性组合(每个数乘一个固定系数再求和)。
如果把”当前窗口里的 7 个数”看成一个整体(状态),那么:
- 每算一步,就是一个旧状态 → 新状态 的变换;
- 这个变换是线性的(只做乘系数和相加);
- 而”线性变换”在数学里就用矩阵来表示。
于是”递推 10^25 步”就变成了”矩阵乘 10^25 次”,后面用快速幂解决。
B. 什么是矩阵 × 向量?(新手必看)
矩阵就是一张数的”表格”,几行几列。比如 2×2 矩阵:
[ a b ][ c d ]列向量就是只有一列的数,竖着写:
[ x ][ y ]矩阵 × 向量的规则:结果向量的每个分量 = 矩阵对应行与向量做”点乘”(对应位相乘再求和):
[ a b ] [ x ] [ a·x + b·y ][ c d ] · [ y ] = [ c·x + d·y ]为什么这个规则正好适合递推?因为点乘就是”乘系数再相加”——和递推式 3f[k-1] + 5f[k-2] + ... 是同一回事。
C. 把”滑动窗口”变成”状态向量”
因为算 f[k] 需要 f[k-1]…f[k-7] 共 7 个数,就把这 7 个数竖着排成一个列向量,称为状态向量:
s[k] = [ f[k] ] ← 最新的一项放最上面 [ f[k-1] ] [ f[k-2] ] ... [ f[k-6] ]初始状态(k=6,就是前 7 个初始值):
s[6] = [ f[6] ] [ 7 ] [ f[5] ] [ 6 ] [ f[4] ] [ 5 ] [ f[3] ] = [ 4 ] [ f[2] ] [ 3 ] [ f[1] ] [ 2 ] [ f[0] ] [ 1 ]D. 构造转移矩阵(伴随矩阵)
我们的目标是找一个 7×7 矩阵 A,满足:
s[k+1] = A · s[k]即”每乘一次 A,窗口就向前滑动一项”。现在一行一行地设计 A。
第一行 —— 负责算出新的第一项 f[k+1]:
f[k+1] = 3·f[k] + 5·f[k-1] + 7·f[k-2] + 11·f[k-3] + 13·f[k-4] + 17·f[k-5] + 36·f[k-6]
要让”第一行 点乘 s[k]“正好等于这个式子,第一行就必须是系数本身:
[ 3 5 7 11 13 17 36 ]其余 6 行 —— 负责”搬位”(滑动窗口):
s[k+1] 的第 2 个分量应该是 f[k](即 s[k] 的第 1 个分量),所以第 2 行应选中 s[k] 的第 1 个元素 → 第 2 行为 [1 0 0 0 0 0 0];
第 3 个分量应该是 f[k-1](s[k] 的第 2 个分量)→ 第 3 行为 [0 1 0 0 0 0 0];
依此类推。完整矩阵就是:
[ 3 5 7 11 13 17 36 ] [ 1 0 0 0 0 0 0 ] [ 0 1 0 0 0 0 0 ]A = [ 0 0 1 0 0 0 0 ] [ 0 0 0 1 0 0 0 ] [ 0 0 0 0 1 0 0 ] [ 0 0 0 0 0 1 0 ](对角线下面一条”斜线”上全是 1,这就是它叫”伴随矩阵 / companion matrix”的原因。)
动手验证一步(强烈建议拿纸笔跟算):
[ 3 5 7 11 13 17 36 ] [ 7 ] [ 3·7+5·6+7·5+11·4+13·3+17·2+36·1 ] [ f[7] ] [ 1 0 0 0 0 0 0 ] [ 6 ] [ 7 ] [ f[6] ]A·s[6] =[ 0 1 0 0 0 0 0 ] · [ 5 ] = [ 6 ] =[ f[5] ] [ 0 0 1 0 0 0 0 ] [ 4 ] [ 5 ] [ f[4] ] [ 0 0 0 1 0 0 0 ] [ 3 ] [ 4 ] [ f[3] ] [ 0 0 0 0 1 0 0 ] [ 2 ] [ 3 ] [ f[2] ] [ 0 0 0 0 0 1 0 ] [ 1 ] [ 2 ] [ f[1] ]第一行算出 f[7] = 3·7+5·6+7·5+11·4+13·3+17·2+36·1,其余行把窗口 [7,6,5,4,3,2,1] 搬成 [f7,7,6,5,4,3,2]——窗口正好滑动了一格,完美。
因此从初始状态开始乘 k 次 A,就得到 f[k] 及它前面的 6 个数:
s[n] = A·A·...·A · s[6] = A^(n-6) · s[6] (乘 n-6 次,从 k=6 推到 k=n)f[n] 就是 s[n] 的第一个分量。
E. 为什么用”快速幂”,不能直接乘 10^25 次?
- 直接算
A · A · ... · A(10^25 - 6 次)显然不可能。 - 快速幂的诀窍:把指数写成二进制,用”平方”大幅减少次数。
先看标量(普通数)版:算 a^13。13 的二进制是 1101₂,所以:
a^13 = a^8 · a^4 · a^1流程:
| 步骤 | 底数 | 指数二进制(13=1101) | 操作 |
|---|---|---|---|
| 1 | a | 末位 1 | 乘进结果:结果=a |
| 2 | a² | 次位 0 | 不乘 |
| 3 | a⁴ | 次位 1 | 乘进结果:结果=a⁵ |
| 4 | a⁸ | 最高位 1 | 乘进结果:结果=a^13 |
每步只做”平方”和”判断要不要乘”,13 次乘法变成 4 步。一般地,指数 n 只需 O(log n) 步。
矩阵也适用:矩阵乘法满足结合律,A·A=A²、A²·A²=A⁴……同样可以快速幂。本指数 n-6 ≈ 10^25,log2(10^25) ≈ 83,只需约 83 次 7×7 矩阵乘法,瞬间完成。
F. 代码逐行讲解
matmul:矩阵乘法
def matmul(A, B, mod): n, m, p = len(A), len(B[0]), len(B) C = [[0]*m for _ in range(n)] for i in range(n): # 结果矩阵 C 的第 i 行 for k in range(p): # 遍历 A 第 i 行的每个元素 A[i][k] if A[i][k]: for j in range(m): # 与 B 第 k 行的第 j 个元素相乘 C[i][j] = (C[i][j] + A[i][k]*B[k][j]) % mod return C- 矩阵乘法公式:
C[i][j] = Σ_k A[i][k]·B[k][j](A 的第 i 行 点乘 B 的第 j 列)。 - 代码里的三重循环就是按公式累加,只是把循环顺序调成 i,k,j:外层固定 A 的一行,内层累加,速度更快。
if A[i][k]:是优化:本矩阵非常稀疏(大量 0),为 0 就跳过,省很多计算。- 每次累加后立即
% mod:我们的数可能高达 500 多位,取模后保持数值不膨胀。
matpow:二进制快速幂
def matpow(A, e, mod): n = len(A) R = [[1 if i == j else 0 for j in range(n)] for i in range(n)] while e: if e & 1: R = matmul(R, A, mod) A = matmul(A, A, mod) e >>= 1 return RR初始化为单位矩阵(对角线上全是 1,其余为 0),它是矩阵乘法中的”1”:任何矩阵乘单位矩阵都等于它自己。if e & 1:判断 e 的二进制最低位是否为 1,是则把当前的 A 乘进结果 R;A = matmul(A, A, mod)每轮把底数平方(a → a² → a⁴ → a⁸…);e >>= 1把 e 右移一位,相当于看下一个二进制位。
主程序:求 key
A = [COEFF[:]] + [[1 if i == j+1 else 0 for j in range(7)] for i in range(1, 7)]s6 = [[7],[6],[5],[4],[3],[2],[1]]sn = matmul(matpow(A, 10**25 - 6, M), s6, M)key = sn[0][0]- 第一行:构造伴随矩阵 A。
[COEFF[:]]是第一行系数;后面的推导式生成”次对角线为 1”的 6 行(第 i 行只在列i-1处为 1)。 s6:初始状态列向量(用 7×1 矩阵表示,便于参与矩阵乘法)。sn = A^(10^25-6) · s6:一次快速幂 + 一次乘法,得到 s[10^25]。key = sn[0][0]:取第一个分量,就是 f[10^25] mod M。
(Python 原生支持任意大整数,521 位的模数运算毫无压力,这也是选 Python 解题的原因。)
解密
enc = b'lz\xb0T\xeb\x18\xec\xcfQ\x8bv#\x17t\xf0\xc7\xdaM\x87\xf5sr\xef\xd0\xfc3.\xdc%\xaaf\xf4\xd1\xc8\xb7\xef'flag = bytes([x ^ y for x, y in zip(enc, long_to_bytes(key))])逐字节异或:密文 和 key 的字节 相同时 得 0、不同得 1,因为 a^b^b = a,所以异或一次就还原出原文。long_to_bytes 把大整数按字节拆开。
G. 验证
运行上述代码输出:
flag: moectf{m4tr1x_p0w3r_1s_4ll_y0u_n33d}m1x3d_d1p
这个离散对数有力气
源码:
import osfrom hashlib import shake_256from operator import xorfrom Crypto.Random import get_random_bytesfrom Crypto.Util.number import getPrime, getRandomRange, isPrime
FLAG = os.environ.get("FLAG").encode()
LOWER = 2^72WIDTH = 2^50N = 2^521
def int_to_bytes(value, size): return int(value).to_bytes(size, "big")
def bytes_to_int(value): return int.from_bytes(value, "big")
def get_safeprime(): while True: q = getPrime(255) p = 2 * q + 1 if isPrime(p): assert p.bit_length() ==256 return p
def get_necessaryprime(): while True: p = getPrime(1024) q = getPrime(1024) if p != q: return p,q
def get_point(E): while True: P = E.random_point() if P != E(0) and (N // 2) * P != E(0): return P
def main(): g = 4 x = LOWER + getRandomRange(0, int(WIDTH)) p1=get_safeprime() h = pow(g, x, p1) salt = get_random_bytes(16) stream = shake_256(int_to_bytes(x, 16) + salt).digest(len(FLAG)) ct = bytes(xor(a, b) for a, b in zip(FLAG, stream))
p2, q2 = get_necessaryprime() n = p2*q2 msg = bytes_to_int(salt) assert 0 < msg < n c = pow(n - 1, msg, n^3)
p3 = 2^521 - 1 E = EllipticCurve(GF(p3), [1, 0]) assert p3.is_prime() assert E.cardinality() == N G = get_point(E) Q = h*G # ecc point multiplication
print("p1 = ",p1) print("ct = ",ct.hex()) print("n = ",n) print("c = ",c) print(f"G = {G}") print(f"Q = {Q}")
if __name__ == "__main__": main()
'''p1 = 73874304220966794859883564142670914442160584279512562772063275425872997290543ct = c73ebfd0d5f5306bf7b8c259f87a866c40cbb5ce5f8d326174f09d179df3a5d3906d1e6fbfn = 26348468790358661987445205133808481361435703130418513887705897278826308770033861923830274930436686278724254218616365814127895667201226946680165183008754547597474861303423910691551690444921386163168548614525381254405937849955507155314004007535604045061255553662727828051290832314449659484779308741293727707900900170235659862252073630553044526390301199317071679861165407076957146176561947990013484184202106200233399045641747234458307724661634973621282190792505046148325660314149396133509245559308317797985683006061032800023800961174212354544955109581064928578416971928807203749582669021458568452833766558770954438616497c = 18292208600418680625346183156783842167980266908746063547874782289594016023290780206333754396450393994395102007035855344054804193217359206463563433446472505727994901132977763787080203026950169369591501787480957627259013567994357936208362687606101415863993698014986685119064533855372082930021613298890333145509392421716752995306296461868488832964415358984329882937931905903464559143982263268269083480587788420877176573297769968153059118111876625064199454486635225619944093721820821165705764165249155126464874550914713055748639590002563670621007106376616325378189084111869509522388310189885502337810810215875286798134503525107415142540248231413068164395979470626299622797881264209250500723283329219227670112466101846529702624411174551568386938553935477994536587306540781433768299234258441555545749811160761849886388837032471652047716680527112246937059135097439738963306420040647197702553358014634206681392082294371653516056590167571665836297401855786357030195987426949879246135482587643313361147857381800085864146304896635173146062947294307468070616042354637632111985639047340422705751250339626174344904860600721556258009492692131360873608376680475870292729759624235570033630175014234069123392091032441528497624613784629834549504180502654304061001373814831486231477364902485785811049637463459384990001796639913357655607413053238839196073949185368693205326288736386552869873665539622247744841057721351869502628850114759676192404249955073513052265989954817453906473557788905111747827714893635800870166800765860714842566830570394408087721863150266141226191226570565273155340630477577238929069857526263693577191002065110637932256991574847955863906072356280613270856886191376520837492360166100997985166149831999620544245637744163660725702132833723949396909207374462649964703036145604767881029170114908642892746159599054748172266245422571025453930975860859362G = (6020778416720603517227477855097878740167361812994765487105307672032724334849113047243617528393766225587705066628296154430456880091295418013040021260550463027 : 691898700551711858905313841052959760588826940773891651738319555867728918193849130906719860297917423252790661716193289433324789017225426911052907111053843978 : 1)Q = (2129340065294857346243947364378019323540664200486037074516322333892314045157843753138708906788524972368828168216665115345570256093936157234360622885600492438 : 1817717059341975240621337553071398827917135577681647671729956862059289928752523593978226475321638296957956501967732700517974509739099606328777013339210950885 : 1)'''题目提示用离散对数与ECC,查看wiki发现是运用椭圆相关的知识 读题发现以下几点
flag加密
stream = shake_256(int_to_bytes(x, 16) + salt).digest(len(FLAG))ct = bytes(xor(a, b) for a, b in zip(FLAG, stream))换成数学式子:
ct[i] = FLAG[i] ⊕ stream[i]ct[i] ⊕ stream[i]= (FLAG[i] ⊕ stream[i]) ⊕ stream[i]= FLAG[i] ⊕ (stream[i] ⊕ stream[i])= FLAG[i] ⊕ 0= FLAG[i]flag = ct XOR shake_256(x‖salt)说明要得到flag需要x与salt
于是现在要做三件事:
- 从
Q = h*G恢复出h - 从
c = (n-1)^msg mod n³恢复出msg,即salt - 从
h = 4^x mod p1恢复出x
发现第二件事是最简单的先搞他
msg = bytes_to_int(salt)assert 0 < msg < nc = pow(n - 1, msg, n^3)即c = (n-1)^msg mod n³
利用二项式定理分解(n-1)^msg
令 :
的项都有因子 ,模 后全部消失 只剩 三项:
提取 :
还是有点难算,想办法将n^2的项也约掉,可以发现salt是 16 字节,所以 msg < 2^128,而 n ≈ 2^2048,即 msg < n,于是 msg·n < n·n = n^2,那么对整个式子模n^2不会对求msg有影响
现在分两种情况(取决于 msg 的奇偶):
-
msg是偶数:,所以 即 -
msg是奇数:,所以 即注意 (salt 是 16 字节),而 。所以 ,不需要再做模运算,直接整除就行:
因为 和 一定是 的倍数(这就是 的形式),所以 // n 是精确整除。
d = c % (n*n)msg1 = ((1 - d) % (n*n)) // n # 偶数情况msg2 = ((d + 1) % (n*n)) // n # 奇数情况两个候选值里,哪个满足 pow(n-1, msg, n^3) == c 哪个就是真的。然后 salt = msg 转成 16 字节。
这样就拿到了 salt = 4f753adb8b4aceb3dbe653ec49dcdc67。
接着恢复h
先看加密函数
p3 = 2^521 - 1E = EllipticCurve(GF(p3), [1,0])assert E.cardinality() == NG = get_point(E)Q = h*G这段加密函数是用了椭圆曲线,所以N = 2^521。
(已没招ai辅助一下) 是生成元,它的阶就是群阶 (因为题目保证了 ,即 的阶大于 ,而阶又是 的约数,所以正好是 )
要找 (),使得 。把 写成二进制:
定义”当前已知的低 位”:
定义”剩余点”:
因为 是 的倍数,可以写成 ,其中 。
关键技巧:把 再乘上 :
(这里用到了 。)
因为 的阶是 2,所以 只取决于 的奇偶:
- 如果 ( 是奇数),
- 如果 ( 是偶数),(无穷远点)
所以只要计算 ,看它是否等于 ,就能读出第 位! 一步一位,重复 521 次, 就出来了。
k = 521order2 = ec_mul(1 << 520, G) # 阶为 2 的点 Rh = 0P = Qpow2G = G # 维护 2^i * Gfor i in range(k): T = ec_mul(1 << (k - 1 - i), P) # T_i = 2^(520-i) * P if T == order2: h |= (1 << i) # 第 i 位是 1 P = ec_add(P, ec_neg(pow2G)) # 减去 2^i * G pow2G = ec_double(pow2G) # 2^i * G -> 2^(i+1) * G结果:h = 26125642122953389739954215687275690582838741899056500075443463368935197938621(254 位,符合 的预期)。
从 h = 4^x mod p1 恢复 x
现在我们有了 ,要求 ,满足:
x宽度为2^50,对于”已知 x 落在小区间”的离散对数可以使用袋鼠算法
- 原理:想象有两只袋鼠在”模 p1 的指数草地”上跳:
- **温顺袋鼠(tame)**:从区间**上端 $b$** 出发,按照一个固定规则往前跳,每跳一步就把跳的距离记下来。跳完一条路径后,它在**终点放一个陷阱**。- **野生袋鼠(wild)**:从 $y = g^x$ 出发($x$ 未知,它不知道自己位置),用**完全相同的跳跃规则**往前跳。
核心性质:**跳跃规则只由当前所在位置决定**。所以一旦野生袋鼠跳到温顺袋鼠曾经经过的任何一个点上,它们俩就会"合体",之后走完全相同的路,最终一起到达陷阱。
检测到"野生袋鼠到达陷阱"时:
$$x = (\text{温顺袋鼠走过的总距离}) - (\text{野生袋鼠走过的总距离})$$
因为温顺袋鼠从指数 $b$ 走到 $b+D_{tame}$,野生从指数 $x$ 走到 $x+D_{wild}$,它们在同一个点相遇意味着 $b+D_{tame} \equiv x+D_{wild}$,于是 $x = b+D_{tame}-D_{wild}$。区间宽度 ,取平均跳跃距离
温顺袋鼠跳 步(铺一条覆盖约 距离的路径)
野生袋鼠最多跳约 步
总步数约 ,几秒钟就完成
验证答案——只要 pow(4, x, p1) == h 就说明找到了正确的 ,不对就换随机参数重试。
得到结果:x = 4722366557208468302061,验证 4^x mod p1 == h 通过。
现在 和 都拿到了:
stream = shake_256(int_to_bytes(x, 16) + salt).digest(len(ct))flag = bytes(a ^ b for a, b in zip(ct, stream))得到:
moectf{K4ng4r00_Curv3_B1n0m14l_Ch41n}解题脚本:
from hashlib import shake_256from Crypto.Util.number import inverseimport random, math, gmpy2
LOWER, WIDTH = 2**72, 2**50p1 = 73874304220966794859883564142670914442160584279512562772063275425872997290543ct = bytes.fromhex("c73ebfd0d5f5306bf7b8c259f87a866c40cbb5ce5f8d326174f09d179df3a5d3906d1e6fbf")n = 26348468790358661987445205133808481361435703130418513887705897278826308770033861923830274930436686278724254218616365814127895667201226946680165183008754547597474861303423910691551690444921386163168548614525381254405937849955507155314004007535604045061255553662727828051290832314449659484779308741293727707900900170235659862252073630553044526390301199317071679861165407076957146176561947990013484184202106200233399045641747234458307724661634973621282190792505046148325660314149396133509245559308317797985683006061032800023800961174212354544955109581064928578416971928807203749582669021458568452833766558770954438616497c = 18292208600418680625346183156783842167980266908746063547874782289594016023290780206333754396450393994395102007035855344054804193217359206463563433446472505727994901132977763787080203026950169369591501787480957627259013567994357936208362687606101415863993698014986685119064533855372082930021613298890333145509392421716752995306296461868488832964415358984329882937931905903464559143982263268269083480587788420877176573297769968153059118111876625064199454486635225619944093721820821165705764165249155126464874550914713055748639590002563670621007106376616325378189084111869509522388310189885502337810810215875286798134503525107415142540248231413068164395979470626299622797881264209250500723283329219227670112466101846529702624411174551568386938553935477994536587306540781433768299234258441555545749811160761849886388837032471652047716680527112246937059135097439738963306420040647197702553358014634206681392082294371653516056590167571665836297401855786357030195987426949879246135482587643313361147857381800085864146304896635173146062947294307468070616042354637632111985639047340422705751250339626174344904860600721556258009492692131360873608376680475870292729759624235570033630175014234069123392091032441528497624613784629834549504180502654304061001373814831486231477364902485785811049637463459384990001796639913357655607413053238839196073949185368693205326288736386552869873665539622247744841057721351869502628850114759676192404249955073513052265989954817453906473557788905111747827714893635800870166800765860714842566830570394408087721863150266141226191226570565273155340630477577238929069857526263693577191002065110637932256991574847955863906072356280613270856886191376520837492360166100997985166149831999620544245637744163660725702132833723949396909207374462649964703036145604767881029170114908642892746159599054748172266245422571025453930975860859362Gx = 6020778416720603517227477855097878740167361812994765487105307672032724334849113047243617528393766225587705066628296154430456880091295418013040021260550463027Gy = 691898700551711858905313841052959760588826940773891651738319555867728918193849130906719860297917423252790661716193289433324789017225426911052907111053843978Qx = 2129340065294857346243947364378019323540664200486037074516322333892314045157843753138708906788524972368828168216665115345570256093936157234360622885600492438Qy = 1817717059341975240621337553071398827917135577681647671729956862059289928752523593978226475321638296957956501967732700517974509739099606328777013339210950885p3 = 2**521 - 1
def ec_add(P, Q): if P is None: return Q if Q is None: return P x1, y1 = P; x2, y2 = Q if x1 == x2: if (y1 + y2) % p3 == 0: return None return ec_double(P) lam = (y2 - y1) * inverse(x2 - x1, p3) % p3 x3 = (lam*lam - x1 - x2) % p3 y3 = (lam*(x1 - x3) - y1) % p3 return (x3, y3)
def ec_double(P): if P is None: return None x, y = P if y % p3 == 0: return None lam = (3*x*x + 1) * inverse(2*y, p3) % p3 x3 = (lam*lam - 2*x) % p3 y3 = (lam*(x - x3) - y) % p3 return (x3, y3)
def ec_neg(P): return None if P is None else (P[0], (-P[1]) % p3)
def ec_mul(k, P): R = None while k: if k & 1: R = ec_add(R, P) P = ec_double(P) k >>= 1 if P is None: break return R
G, Q = (Gx, Gy), (Qx, Qy)order2 = ec_mul(1 << 520, G)h, P, pow2G = 0, Q, Gfor i in range(521): if ec_mul(1 << (520 - i), P) == order2: h |= (1 << i) P = ec_add(P, ec_neg(pow2G)) pow2G = ec_double(pow2G)assert ec_mul(h, G) == Qprint("h =", h)
n2, n3 = n*n, n*n*nd = c % n2msg = next(v for v in (((1-d) % n2)//n, ((d+1) % n2)//n) if pow(n-1, v, n3) == c)salt = msg.to_bytes(16, "big")print("salt =", salt.hex())
def pollard_kangaroo(g, y, p, a, b, buckets=1 << 16): w = b - a m = math.isqrt(w) + 1 mask = buckets - 1 jumps = [random.randint(1, 2*m) for _ in range(buckets)] p2 = gmpy2.mpz(p) gj = [gmpy2.mpz(pow(g, j, p)) for j in jumps] xT, dT = gmpy2.mpz(pow(g, b, p)), b for _ in range(m): j = int(xT) & mask xT = (xT * gj[j]) % p2 dT += jumps[j] xW, dW = gmpy2.mpz(y), 0 while dW <= dT - a: j = int(xW) & mask xW = (xW * gj[j]) % p2 dW += jumps[j] if xW == xT: return (dT - dW) % (p - 1) return None
a, b = LOWER, LOWER + WIDTHx = Nonefor _ in range(20): cand = pollard_kangaroo(4, h, p1, a, b) if cand is not None and pow(4, cand, p1) == h: x = cand; breakprint("x =", x)
stream = shake_256(x.to_bytes(16, "big") + salt).digest(len(ct))print("FLAG =", bytes(a_ ^ b_ for a_, b_ in zip(ct, stream)))moeSign1n
只需要输入比赛的名称,就能得到 flag …吗?
源码:
from Crypto.Util.number import *from random import randint
flag = b'moectf{???}'p = getPrime(512)q = getPrime(512)n = p * qphi = (p - 1) * (q - 1)while 1: e = randint(2, n) if GCD(e, phi) == 1: d = pow(e, -1, phi) break
menu = '''1. get ciphertext.2. submit.3. quit.'''
while 1: print(menu) choice = int(input()) if choice == 1: m = bytes.fromhex(input("what message(hex form) do u want to encrtpt?\n")) if m == b'MoeCTF 2026': print('no cheat!') continue print(pow(bytes_to_long(m), e, n)) elif choice == 2: enc = int(input("plz sumbit the ciphertext.\n")) dec = long_to_bytes(pow(enc, d, n)) if dec == b'MoeCTF 2026': print("Welcome to MoeCTF 2026!") print(flag) exit() else: print("incorrect!") elif choice == 3: print("bye!") exit() else: print("invalid input!") continue这是一道rsa题目,n与e没给,应该要在服务器里找
从
if choice == 1: m = bytes.fromhex(input("what message(hex form) do u want to encrtpt?\n")) if m == b'MoeCTF 2026': print('no cheat!') continue print(pow(bytes_to_long(m), e, n))选项一给服务器传信息会返回密文 那么可以用
n = gcd( enc(2)^2 − enc(4), enc(2)^3 − enc(8), enc(2)^5 − enc(32), enc(2)^7 − enc(128) )求出n
从
elif choice == 2: enc = int(input("plz sumbit the ciphertext.\n")) dec = long_to_bytes(pow(enc, d, n)) if dec == b'MoeCTF 2026': print("Welcome to MoeCTF 2026!") print(flag) exit() else: print("incorrect!")可以发现需要用bytes_to_long(b'MoeCTF 2026')来求出flag,但是选项一给力黑名单,那么需要用一个数对bytes_to_long(b'MoeCTF 2026')进行加密绕过黑名单
设 t = bytes_to_long(b'MoeCTF 2026')(目标明文转成整数)。
我们想要 t^e(目标明文的密文),但服务器拒绝直接加密 t。怎么办?
取一个随机数 s,让服务器加密两个完全合法的消息:
利用乘法同态(第一节记住的那句话):
enc(t·s) = (t·s)^e = t^e · s^e两边同除以 enc(s) = s^e:
t^e = enc(t·s) · enc(s)^{-1} (mod n)enc(s)^{-1} 是 enc(s) 在模 n 下的逆元,Python 里一行:pow(cs, -1, n)。
版本 A:纯 socket(零依赖,理解交互原理用这个)
关键点:自己维护一个接收缓冲区 buf,recv_until() 一直收数据直到出现指定字符串。
import socket, randomfrom Crypto.Util.number import long_to_bytes, bytes_to_long, GCD
HOST, PORT = "127.0.0.1", 60497TARGET = b"MoeCTF 2026"
sk = socket.create_connection((HOST, PORT))sk.settimeout(8)buf = b""
def recv_until(tok): """一直收数据,直到缓冲区里出现 tok,把 tok 之前的内容(含 tok)返回并消费掉""" global buf while tok not in buf: data = sk.recv(4096) if not data: raise EOFError("connection closed") buf += data idx = buf.index(tok) + len(tok) out, buf = buf[:idx], buf[idx:] return out
def enc(hexstr): """选项 1:加密一个 hex 消息,返回密文""" sk.sendall(b"1\n") recv_until(b"encrtpt?\n") sk.sendall(hexstr.encode() + b"\n") chunk = recv_until(b"3. quit.") # 密文 + 下一轮菜单 for line in chunk.split(b"\n"): # 密文是纯数字长行,菜单行带文字 line = line.strip() if line.isdigit() and len(line) > 20: return int(line)
# ---- 第 1 步:GCD 恢复 n ----c2 = enc("02")samples = [enc(long_to_bytes(2**k).hex()) for k in (2, 3, 5, 7)]cand = 0for k, ck in zip((2, 3, 5, 7), samples): cand = GCD(cand, pow(c2, k) - ck) if cand else abs(pow(c2, k) - ck)for f in range(2, 100000): # 除掉残留的小因子 while cand % f == 0: cand //= fc3, c9 = enc("03"), enc("09")assert (c3 * c3 - c9) % cand == 0 # 换基数 3 验证n = cand
# ---- 第 2 步:乘法延展性构造 t^e ----t = bytes_to_long(TARGET)s = random.randrange(2**100, 2**101)assert t * s < n # 别忘了检查没超过模数cm = enc(long_to_bytes(t * s).hex()) # = t^e * s^ecs = enc(long_to_bytes(s).hex()) # = s^ec = cm * pow(cs, -1, n) % n # = t^e
# ---- 第 3 步:提交 ----sk.sendall(b"2\n")recv_until(b"ciphertext.\n")sk.sendall(str(c).encode() + b"\n")out = b""try: while True: d = sk.recv(4096) if not d: break out += dexcept socket.timeout: passprint(out.decode())版本 B:pwntools(实战推荐,代码短一半)
pwntools 把”等某个字符串出现 → 发送一行”封装成了一个函数 sendlineafter,交互逻辑一目了然:
from pwn import *from Crypto.Util.number import long_to_bytes, bytes_to_long, GCDimport random
HOST, PORT = "127.0.0.1", 60497TARGET = b"MoeCTF 2026"
io = remote(HOST, PORT)
def enc(hexstr): io.sendlineafter(b"quit.", b"1") # 等菜单出现,选 1 io.sendlineafter(b"encrtpt?\n", hexstr.encode()) # 等提示出现,发 hex return int(io.recvline()) # 密文独占一行
def submit(c): io.sendlineafter(b"quit.", b"2") io.sendlineafter(b"ciphertext.", str(c).encode()) return io.recvall(timeout=5)
# ---- 第 1 步:GCD 恢复 n ----c2 = enc("02")samples = [enc(long_to_bytes(2**k).hex()) for k in (2, 3, 5, 7)]cand = 0for k, ck in zip((2, 3, 5, 7), samples): cand = GCD(cand, pow(c2, k) - ck) if cand else abs(pow(c2, k) - ck)for f in range(2, 100000): while cand % f == 0: cand //= fc3, c9 = enc("03"), enc("09")assert (c3 * c3 - c9) % cand == 0n = candlog.success(f"recovered n ({n.bit_length()} bits)")
# ---- 第 2 步:乘法延展性构造 t^e ----t = bytes_to_long(TARGET)s = random.randrange(2**100, 2**101)cm = enc(long_to_bytes(t * s).hex())cs = enc(long_to_bytes(s).hex())c = cm * pow(cs, -1, n) % n
# ---- 第 3 步:提交 ----print(submit(c).decode())得到flag
moectf{0p3n_4_d00r_70_7h3_w0r1d_0f_m0d3rn_cryp706r4phy}文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!