moectf-2026密码学方向writeup-牢大学密码真的有出路吗

11396 字
57 分钟
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 和做学术是不一样的,前者更需要的是你在面对具体题目的时候,即时学习并且运用的能力。

基础知识#

这里列举一些可能会用到的知识:

  1. 初等数论(数竞生狂喜):整除、gcd、同余、剩余系等
  2. 现代密码学:RSA、DES 等等
  3. 线性代数、近世代数:向量、矩阵、群、环、域等
  4. 算法基础:复杂度计算、二分、DP、快速幂等

关于数学知识和算法知识,大家可以从数学部分简介 - OI Wiki以及基础数学知识 - CTF Wiki上学习。但笔者建议还是多看看书,这里给大家推荐一些我入门的书籍:

  1. 密码学数学知识一本通:《公钥密码学的数学基础》(第二版)王小云
  2. 进阶学习:《Mathematics of Public Key Cryptography》(Steven D. Galbraith)
  3. 数论深入学习:《数论讲义 上册》(第二版)柯召、孙琦
  4. 密码原语学习:《现代密码学》(第5版)杨波

但是但是但是,千万不要被数学给劝退!

眼下你只需了解个大概即可(甚至不怎么了解也无所谓),正如一开始所说的 CTF 更需要的是在面对具体题目的时候,即时学习并且运用的能力,结合题目慢慢来或许更轻松一些。

相关工具#

Python#

就像中学时代的习题,需要用数学符号和语言来描述,CTF 题目也需要一个载体,而大多数题目基于 Python 这一编程语言。建议先完成赛事中”从此开始”部分的 Python 相关内容。

下面仅介绍几个建议安装的 Python 包:

1. pycryptodome

提供了各种密码学算法的实现,最重要,基本上所有题目都会用到:

Terminal window
pip install pycryptodome

2. pwntools

交互题好帮手,但是只能在 Linux 环境下使用。推荐阅读题目 [wsl_Sign1n] 提供的附件后安装使用:

Terminal window
pip install pwntools

Markdown#

Markdown 是一种轻量级标记语言,排版语法简洁。平常我们见到的公式、图片、代码块等富文本,就可以通过 Markdown 来编写。

有些时候,题目或 writeup 中会给出 .md 文件,这就是用 Markdown 编写的文档。当然,你可以用记事本打开或编辑 .md 文件,但它不能渲染。你可以使用在线 Markdown 编辑器进行预览,也可以使用 VS Code 的插件——Markdown All in One,也可以使用轻量的编辑器 Typora。在这里,笔者推荐 Obsidian,有着更强大的知识库管理和插件支持:Obsidian - Sharpen your thinking

另附:Markdown教程 — 最简明的Markdown语法入门指南

SageMath#

SageMath 是一个基于 Python 语言的强大数学工具,在 Crypto in CTF 中有着广泛的应用。

你可以将其简单地理解为集成了许多第三方库的 Python,因此其基础语法和 Python 是一样的,很好上手!

有关它的安装与使用,我在 Moe 2026 出了一道题 [wsl_Sign1n],其中给出了详细的安装指南,只要你在 WSL 里成功安装就能获得 flag。

另附:Sage 官方文档 | Sage 教程

题目连接#

比赛中有不少题目都需要连接远程环境,因此同样建议先完成”从此开始”中的相关内容。

总之,CTF 无论什么方向,第一步肯定都绕不过工具安装和环境配置,希望你能克服困难,迈出这一步!


四、如何学习#

在这里笔者会提供一些有用的学习渠道,有效弥补信息差。

1. LLM & Agent#

AI 可以说是这个时代最重要的学习工具。首先,在大模型的选择上,你要做的第一件事就是扔掉你的豆包(它的幻觉会坑死你),可以选择 DeepSeek/GLM/MiniMax 等国产模型,感兴趣的话也可以试试 GPT、Claude Opus 等国外模型。当然,如果你从未尝试过,那么我们强烈推荐 DeepSeek | 深度求索,登录即用,便捷且强大!

**Agent(智能体)**的本质是自动执行任务的程序,核心在于让大模型不只回答问题,而是按步骤完成电脑操作。通过接入大模型,Agent 可以在你的电脑上完成它能做到的任意操作,比如读取本地的资料并教你学习知识,帮你下载东西/配置环境,在本地编写代码并测试。以上任务只要和 Agent 说”一些”话就能完成。

比较实用的 Agent 有 Claude Code、Codex 等,你可以在网上搜索这些强大工具的使用方法!

2. CTF 练习#

在实践中学习,切忌纸上谈兵!在学习知识的基础之上,多去训练场练习,多参加比赛。

  1. 训练 - 西电 CTF 终端 xdu练习场,推荐在这里练习往年 MoeCTF 的题目,在互联网上查找题解,加速入门!
  2. CTF²——一个练习场
  3. CryptoHack——题目种类多,难度平缓,非常不错的练习网站

3. Blogs#

多读大佬的博客和 WP 进行学习:

4. 其它有用的#

  1. Z-Library——电子书下载
  2. 维基百科——自由的百科全书,关于密码学的内容收录很详细
  3. 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)的困难性——即给定 ga mod pg^a \bmod p,在已知 gg 和 pp 的情况下,计算 aa 是极其困难的(当 pp 很大时)。

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 可以截获 pp, gg, AA, BB,但无法在合理时间内从 A=ga mod pA = g^a \bmod p 计算出私钥 aa
  • 攻击者无法从 B=gb mod pB = g^b \bmod p 计算出私钥 bb
  • 因此,攻击者无法计算出共享密钥 gab mod pg^{ab} \bmod p

DH 的经典攻击场景:

攻击方式描述
离散对数攻击当 pp 太小时,可直接求解 a=log⁡g(A) mod pa = \log_g(A) \bmod p
中间人攻击(MITM)攻击者在 Alice 和 Bob 之间截获并替换公钥
私钥泄露若任何一方的私钥泄露,共享密钥即可被公开计算(本题的攻击点)
小子群攻击当 p−1p-1 有小的素因子时,可降维恢复私钥

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 % 256
flag = 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 / 7
p = 170141183460469231731687303715884105727
g = 3
alice_public = 42348823944539164637318232035973172471
bob_public = 101533001028006636636416596392258549313
alice_private = 20260704
ciphertext = [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 = 170141183460469231731687303715884105727
bob_public = 101533001028006636636416596392258549313
alice_private = 20260704
ciphertext = [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 % 256
flag = 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 * q
phi = (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

justXOR#

真正的大高衔接 belike…#

源码:#

from Crypto.Util.number import long_to_bytes
flag = b'moectf{???}'
# just a normal big prime
M = 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**25
key = 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]

发现ai+1=3ai+2(modM)a_{i+1} = 3a_i + 2 \pmod{M} 迭代次数 n=1025n = 10^{25},不可能直接循环计算。 因此一定能算出一个递推公式 首先发现 key=an−1key = a_{n-1} 根据加密先用待定系数法可以设出an=c⋅an−1+da_n = c \cdot a_{n-1} + d 首先 a1=c⋅a0a_1 = c \cdot a_0 a2=c⋅a1=c2⋅a0a_2 = c \cdot a_1 = c^2 \cdot a_0 a3=c⋅a2=c3⋅a0a_3 = c \cdot a_2 = c^3 \cdot a_0 ⋯\cdots an=cn⋅a0=C⋅cna_n = c^n \cdot a_0 = C \cdot c^n 可以先推出指数,这里的c = 3。

接着设定特殊常数解an=ka_n = k: k=3k+2 ⇒ −2k=2 ⇒ k=−1k = 3k + 2 \ \Rightarrow\ -2k = 2 \ \Rightarrow\ k = -1

再代入已知的值有 a0=1a_0 = 1: 1=C⋅30−1 ⇒ 1=C−1 ⇒ C=21 = C \cdot 3^0 - 1 \ \Rightarrow\ 1 = C - 1 \ \Rightarrow\ C = 2

那么 ai=(2⋅3i−1) mod Ma_i = (2 \cdot 3^i - 1) \bmod M key=an−1=(2⋅3n−1−1) mod Mkey = a_{n-1} = (2 \cdot 3^{n-1} - 1) \bmod M

得到脚本:

from Crypto.Util.number import long_to_bytes
n = 10**25
M = 2039129633208009090414901212304234091626233923923301042398416489123719065081065776033127561876033127924471
enc = 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) % M
flag = 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_prime
from secret import flag
p = getStrongPrime(512)
q = next_prime(p ^ ((1<<512)-1))
n = p * q
e = 65537
m = long_to_bytes(flag)
assert m < n
c = 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 gmpy2
from Crypto.Util.number import *
c = 0x13a77e34f09b8a2291cdb397ea0e5cb9e86f691c6565b35c76e6bb6248b67db24315e22fe1dc5321a8820320cdd9b51e3d431459aac2213948f4fbf01c4ce974428d1ed745b2c06f8aa92b22dfede3b7ceb59aa4eb22467129f55b60037a1a2de9b37f25fda3fe40323fccc7c8bbdd4446096b37cef4138337ee9a04c2e3d5fd
n = 0x308244e7a7de386723c92ba62e35bc22c3ec1b93023e1551408344a4ba31c6203da849aedee0cadf26a3442f9fd652f7d97c053a3f2c298eab6c0f0c2d0b4642a81765ddb00b690425eb212d5520327ac2d53a22922448399fecb54fbc04dbb68fa33fee7666cb9e05278b5f5f1330b3918d3a7def580fcc00f6f596f16eba3b
e = 65537
t = gmpy2.isqrt(n) + 1
s = gmpy2.isqrt(t * t - n)
p = t - s
q = t + s
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"\n{flag}")

发现报错

Traceback (most recent call last):
assert p * q == n
AssertionError

那么用这个方法得出的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) - p
p + 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 gmpy2
from Crypto.Util.number import *
c = 0x13a77e34f09b8a2291cdb397ea0e5cb9e86f691c6565b35c76e6bb6248b67db24315e22fe1dc5321a8820320cdd9b51e3d431459aac2213948f4fbf01c4ce974428d1ed745b2c06f8aa92b22dfede3b7ceb59aa4eb22467129f55b60037a1a2de9b37f25fda3fe40323fccc7c8bbdd4446096b37cef4138337ee9a04c2e3d5fd
n = 0x308244e7a7de386723c92ba62e35bc22c3ec1b93023e1551408344a4ba31c6203da849aedee0cadf26a3442f9fd652f7d97c053a3f2c298eab6c0f0c2d0b4642a81765ddb00b690425eb212d5520327ac2d53a22922448399fecb54fbc04dbb68fa33fee7666cb9e05278b5f5f1330b3918d3a7def580fcc00f6f596f16eba3b
e = 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 = 45
moectf{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 = 65537
p = getPrime(1024)
q = getPrime(1024)
n = p * q
c = pow(m, e, n)
a, b = 0x2025, 0x2026
hint = 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 = 15962603324053600624662899467930954606237037554051572101066176482327650464579501876010780067612122315010349894395035821617153081982429716234378979239633769779503395609504372912298014522344609346937078959678893005192469085672940628091531228571447786013091492389216237976408692434723698967667324605921896928717731731591488800520224411239128106892402734198486634978989319563704879076976307439748456769245703782902660583833942684475506024284563786886608632662629581206716450648212025254862836883526055933841863348546872473877054469843468332542834452213365868510001991952142536759384940216228753729399187149594010090304067
c = 4281919068424886012214413435957379635517466043647380176350221253614623990599452495938689758001256045320061491376355750164051326734927905972267385611552894838140009898450267453653097466286838460889968896032216593811984942511165228143089457988630435810304672454147149556110542203084341901598519587611096893432307236558970264536719689039318023572680185246871600989317992399536729717958072642243834689061928667913495884682029212015071613730401843271641197254089900489231738518876415262001687002012972665662364940457678043829768126263211295258465202419686281997749861180041439407866197636369525192971926660559217573713931
hint = 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 gcd
from Crypto.Util.number import long_to_bytes, inverse
n = 15962603324053600624662899467930954606237037554051572101066176482327650464579501876010780067612122315010349894395035821617153081982429716234378979239633769779503395609504372912298014522344609346937078959678893005192469085672940628091531228571447786013091492389216237976408692434723698967667324605921896928717731731591488800520224411239128106892402734198486634978989319563704879076976307439748456769245703782902660583833942684475506024284563786886608632662629581206716450648212025254862836883526055933841863348546872473877054469843468332542834452213365868510001991952142536759384940216228753729399187149594010090304067
c = 4281919068424886012214413435957379635517466043647380176350221253614623990599452495938689758001256045320061491376355750164051326734927905972267385611552894838140009898450267453653097466286838460889968896032216593811984942511165228143089457988630435810304672454147149556110542203084341901598519587611096893432307236558970264536719689039318023572680185246871600989317992399536729717958072642243834689061928667913495884682029212015071613730401843271641197254089900489231738518876415262001687002012972665662364940457678043829768126263211295258465202419686281997749861180041439407866197636369525192971926660559217573713931
hint = 8894364790280693556124457666776501614023509475927164016497858629229215409128965020674642063592770935057209719989294548263833927477972267267951214184209046168773196349537926982123382473522039515862138755400978350128128233701795758152143272668945626119949376162344749760499165210224412751964322150037199401111258869439672762883792051818112500032192131948644557686621759867629440824691768395882797936616839572906841251000681054283164497419314938363871139253289134458466501698235039234455154975165111296198209091586550990873209534256929400689017034618807232508747449076486852786199340704419120538526208691066898035362296
e = 65537
a, 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 DES
from 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 prime
M = 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**25
key = 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 - 1
COEFF = [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 : 3725129891528866480039250733345517340363920970037583787168208825011199634276657855793038655312920910100892325677030330013209440859443990621503014209963444464
flag: 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)操作
1a末位 1乘进结果:结果=a
2a²次位 0不乘
3a⁴次位 1乘进结果:结果=a⁵
4a⁸最高位 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 R
  • R 初始化为单位矩阵(对角线上全是 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 os
from hashlib import shake_256
from operator import xor
from Crypto.Random import get_random_bytes
from Crypto.Util.number import getPrime, getRandomRange, isPrime
FLAG = os.environ.get("FLAG").encode()
LOWER = 2^72
WIDTH = 2^50
N = 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 = 73874304220966794859883564142670914442160584279512562772063275425872997290543
ct = c73ebfd0d5f5306bf7b8c259f87a866c40cbb5ce5f8d326174f09d179df3a5d3906d1e6fbf
n = 26348468790358661987445205133808481361435703130418513887705897278826308770033861923830274930436686278724254218616365814127895667201226946680165183008754547597474861303423910691551690444921386163168548614525381254405937849955507155314004007535604045061255553662727828051290832314449659484779308741293727707900900170235659862252073630553044526390301199317071679861165407076957146176561947990013484184202106200233399045641747234458307724661634973621282190792505046148325660314149396133509245559308317797985683006061032800023800961174212354544955109581064928578416971928807203749582669021458568452833766558770954438616497
c = 18292208600418680625346183156783842167980266908746063547874782289594016023290780206333754396450393994395102007035855344054804193217359206463563433446472505727994901132977763787080203026950169369591501787480957627259013567994357936208362687606101415863993698014986685119064533855372082930021613298890333145509392421716752995306296461868488832964415358984329882937931905903464559143982263268269083480587788420877176573297769968153059118111876625064199454486635225619944093721820821165705764165249155126464874550914713055748639590002563670621007106376616325378189084111869509522388310189885502337810810215875286798134503525107415142540248231413068164395979470626299622797881264209250500723283329219227670112466101846529702624411174551568386938553935477994536587306540781433768299234258441555545749811160761849886388837032471652047716680527112246937059135097439738963306420040647197702553358014634206681392082294371653516056590167571665836297401855786357030195987426949879246135482587643313361147857381800085864146304896635173146062947294307468070616042354637632111985639047340422705751250339626174344904860600721556258009492692131360873608376680475870292729759624235570033630175014234069123392091032441528497624613784629834549504180502654304061001373814831486231477364902485785811049637463459384990001796639913357655607413053238839196073949185368693205326288736386552869873665539622247744841057721351869502628850114759676192404249955073513052265989954817453906473557788905111747827714893635800870166800765860714842566830570394408087721863150266141226191226570565273155340630477577238929069857526263693577191002065110637932256991574847955863906072356280613270856886191376520837492360166100997985166149831999620544245637744163660725702132833723949396909207374462649964703036145604767881029170114908642892746159599054748172266245422571025453930975860859362
G = (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 于是现在要做三件事:

  1. 从 Q = h*G 恢复出 h
  2. 从 c = (n-1)^msg mod n³ 恢复出 msg,即 salt
  3. 从 h = 4^x mod p1 恢复出 x

发现第二件事是最简单的先搞他

msg = bytes_to_int(salt)
assert 0 < msg < n
c = pow(n - 1, msg, n^3)

即c = (n-1)^msg mod n³ 利用二项式定理分解(n-1)^msg (a+b)m=∑k=0m(mk)am−kbk(a+b)^m = \sum_{k=0}^{m} \binom{m}{k} a^{m-k} b^k

令 a=−1, b=na = -1,\ b = n:

(n−1)msg=∑k=0msg(msgk)nk(−1)msg−k(n-1)^{msg} = \sum_{k=0}^{msg} \binom{msg}{k} n^k (-1)^{msg-k}

k≥3k \ge 3 的项都有因子 n3n^3,模 n3n^3 后全部消失 只剩 k=0,1,2k=0,1,2 三项:

(n−1)msg≡(−1)msg+(−1)msg−1 msg⋅n+(−1)msg−2(msg2)n2(modn3)(n-1)^{msg} \equiv (-1)^{msg} + (-1)^{msg-1}\,msg \cdot n + (-1)^{msg-2}\binom{msg}{2} n^2 \pmod{n^3}

提取 (−1)msg(-1)^{msg}:

(n−1)msg≡(−1)msg(1−msg⋅n+(msg2)n2)(modn3)(n-1)^{msg} \equiv (-1)^{msg}\left(1 - msg\cdot n + \binom{msg}{2}n^2\right) \pmod{n^3}

还是有点难算,想办法将n^2的项也约掉,可以发现salt是 16 字节,所以 msg < 2^128,而 n ≈ 2^2048,即 msg < n,于是 msg·n < n·n = n^2,那么对整个式子模n^2不会对求msg有影响

现在分两种情况(取决于 msg 的奇偶):

  • msg 是偶数:(−1)msg=1(-1)^{msg} = 1,所以 c≡1−msg⋅n(modn2)c \equiv 1 - msg\cdot n \pmod{n^2} 即 msg⋅n≡1−c(modn2)msg\cdot n \equiv 1 - c \pmod{n^2}

  • msg 是奇数:(−1)msg=−1(-1)^{msg} = -1,所以 c≡−1+msg⋅n(modn2)c \equiv -1 + msg\cdot n \pmod{n^2} 即 msg⋅n≡c+1(modn2)msg\cdot n \equiv c + 1 \pmod{n^2}

    注意 msg<2128msg < 2^{128}(salt 是 16 字节),而 n≈22048n \approx 2^{2048}。所以 msg⋅n<n2msg\cdot n < n^2,不需要再做模运算,直接整除就行:

因为 1−c(modn2)1 - c \pmod{n^2} 和 c+1(modn2)c + 1 \pmod{n^2} 一定是 nn 的倍数(这就是 msg⋅nmsg\cdot n 的形式),所以 // 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 - 1
E = EllipticCurve(GF(p3), [1,0])
assert E.cardinality() == N
G = get_point(E)
Q = h*G

这段加密函数是用了椭圆曲线y2=x3+xy^2 = x^3 + x,所以N = 2^521。

(已没招ai辅助一下) GG 是生成元,它的阶就是群阶 25212^{521}(因为题目保证了 (2520)G≠O(2^{520})G \ne O,即 GG 的阶大于 25202^{520},而阶又是 25212^{521} 的约数,所以正好是 25212^{521})

要找 hh(0≤h<25210 \le h < 2^{521}),使得 Q=hGQ = hG。把 hh 写成二进制:

h=b0+b1⋅2+b2⋅22+⋯+b520⋅2520h = b_0 + b_1\cdot 2 + b_2\cdot 2^2 + \cdots + b_{520}\cdot 2^{520} 定义”当前已知的低 ii 位”:

hi=h mod 2i=b0+b1⋅2+⋯+bi−1⋅2i−1h_i = h \bmod 2^i = b_0 + b_1\cdot 2 + \cdots + b_{i-1}\cdot 2^{i-1}

定义”剩余点”:

Pi=Q−hiG=(h−hi)GP_i = Q - h_i G = (h - h_i)G

因为 h−hih - h_i 是 2i2^i 的倍数,可以写成 h−hi=2i⋅tih - h_i = 2^i \cdot t_i,其中 ti=bi+2⋅(更高位的组合)t_i = b_i + 2\cdot(\text{更高位的组合})。

关键技巧:把 PiP_i 再乘上 2520−i2^{520-i}:

Ti=2520−i⋅Pi=2520−i⋅2i⋅ti⋅G=2520⋅ti⋅G=ti⋅RT_i = 2^{520-i}\cdot P_i = 2^{520-i}\cdot 2^i \cdot t_i \cdot G = 2^{520}\cdot t_i \cdot G = t_i \cdot R

(这里用到了 R=2520GR = 2^{520}G。)

因为 RR 的阶是 2,所以 ti⋅Rt_i \cdot R 只取决于 tit_i 的奇偶:

  • 如果 bi=1b_i = 1(tit_i 是奇数),Ti=RT_i = R
  • 如果 bi=0b_i = 0(tit_i 是偶数),Ti=OT_i = O(无穷远点)

所以只要计算 TiT_i,看它是否等于 RR,就能读出第 ii 位! 一步一位,重复 521 次,hh 就出来了。

k = 521
order2 = ec_mul(1 << 520, G) # 阶为 2 的点 R
h = 0
P = Q
pow2G = G # 维护 2^i * G
for 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<p1<2256h < p_1 < 2^{256} 的预期)。

从 h = 4^x mod p1 恢复 x

现在我们有了 hh,要求 xx,满足:

4x≡h(modp1),x∈[272, 272+250)4^x \equiv h \pmod{p_1}, \quad x \in [2^{72},\, 2^{72}+2^{50})

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}$。

区间宽度 w=250w = 2^{50},取平均跳跃距离 m≈w=225m \approx \sqrt{w} = 2^{25} 温顺袋鼠跳 mm 步(铺一条覆盖约 ww 距离的路径) 野生袋鼠最多跳约 2m2m 步 总步数约 2w≈2262\sqrt{w} \approx 2^{26},几秒钟就完成 验证答案——只要 pow(4, x, p1) == h 就说明找到了正确的 xx,不对就换随机参数重试。

得到结果:x = 4722366557208468302061,验证 4^x mod p1 == h 通过。

现在 xx 和 saltsalt 都拿到了:

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_256
from Crypto.Util.number import inverse
import random, math, gmpy2
LOWER, WIDTH = 2**72, 2**50
p1 = 73874304220966794859883564142670914442160584279512562772063275425872997290543
ct = bytes.fromhex("c73ebfd0d5f5306bf7b8c259f87a866c40cbb5ce5f8d326174f09d179df3a5d3906d1e6fbf")
n = 26348468790358661987445205133808481361435703130418513887705897278826308770033861923830274930436686278724254218616365814127895667201226946680165183008754547597474861303423910691551690444921386163168548614525381254405937849955507155314004007535604045061255553662727828051290832314449659484779308741293727707900900170235659862252073630553044526390301199317071679861165407076957146176561947990013484184202106200233399045641747234458307724661634973621282190792505046148325660314149396133509245559308317797985683006061032800023800961174212354544955109581064928578416971928807203749582669021458568452833766558770954438616497
c = 18292208600418680625346183156783842167980266908746063547874782289594016023290780206333754396450393994395102007035855344054804193217359206463563433446472505727994901132977763787080203026950169369591501787480957627259013567994357936208362687606101415863993698014986685119064533855372082930021613298890333145509392421716752995306296461868488832964415358984329882937931905903464559143982263268269083480587788420877176573297769968153059118111876625064199454486635225619944093721820821165705764165249155126464874550914713055748639590002563670621007106376616325378189084111869509522388310189885502337810810215875286798134503525107415142540248231413068164395979470626299622797881264209250500723283329219227670112466101846529702624411174551568386938553935477994536587306540781433768299234258441555545749811160761849886388837032471652047716680527112246937059135097439738963306420040647197702553358014634206681392082294371653516056590167571665836297401855786357030195987426949879246135482587643313361147857381800085864146304896635173146062947294307468070616042354637632111985639047340422705751250339626174344904860600721556258009492692131360873608376680475870292729759624235570033630175014234069123392091032441528497624613784629834549504180502654304061001373814831486231477364902485785811049637463459384990001796639913357655607413053238839196073949185368693205326288736386552869873665539622247744841057721351869502628850114759676192404249955073513052265989954817453906473557788905111747827714893635800870166800765860714842566830570394408087721863150266141226191226570565273155340630477577238929069857526263693577191002065110637932256991574847955863906072356280613270856886191376520837492360166100997985166149831999620544245637744163660725702132833723949396909207374462649964703036145604767881029170114908642892746159599054748172266245422571025453930975860859362
Gx = 6020778416720603517227477855097878740167361812994765487105307672032724334849113047243617528393766225587705066628296154430456880091295418013040021260550463027
Gy = 691898700551711858905313841052959760588826940773891651738319555867728918193849130906719860297917423252790661716193289433324789017225426911052907111053843978
Qx = 2129340065294857346243947364378019323540664200486037074516322333892314045157843753138708906788524972368828168216665115345570256093936157234360622885600492438
Qy = 1817717059341975240621337553071398827917135577681647671729956862059289928752523593978226475321638296957956501967732700517974509739099606328777013339210950885
p3 = 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, G
for 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) == Q
print("h =", h)
n2, n3 = n*n, n*n*n
d = c % n2
msg = 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 + WIDTH
x = None
for _ in range(20):
cand = pollard_kangaroo(4, h, p1, a, b)
if cand is not None and pow(4, cand, p1) == h:
x = cand; break
print("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 * q
phi = (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, random
from Crypto.Util.number import long_to_bytes, bytes_to_long, GCD
HOST, PORT = "127.0.0.1", 60497
TARGET = 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 = 0
for 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 //= f
c3, 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^e
cs = enc(long_to_bytes(s).hex()) # = s^e
c = 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 += d
except socket.timeout:
pass
print(out.decode())

版本 B:pwntools(实战推荐,代码短一半)#

pwntools 把”等某个字符串出现 → 发送一行”封装成了一个函数 sendlineafter,交互逻辑一目了然:

from pwn import *
from Crypto.Util.number import long_to_bytes, bytes_to_long, GCD
import random
HOST, PORT = "127.0.0.1", 60497
TARGET = 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 = 0
for 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 //= f
c3, c9 = enc("03"), enc("09")
assert (c3 * c3 - c9) % cand == 0
n = cand
log.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}

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Profile Image of the Author
LYH
LYH,安全方向的在校学生。方向集中在 Web 安全和密码学——Web 这边从 webshell 识别、文件包含到注入类题目都在练,密码学在啃 RSA 和格密码相关的内容,逆向和网络也有涉猎。日常工具链是 Mac + Docker。欢迎交流学习!
公告
欢迎来到我的博客!这里展示我的学习过程与结果。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
站点统计
文章
26
分类
13
标签
19
总字数
180,600
运行时长
0 天
最后活动
0 天前

文章目录