帮我非常详细的讲解这道ctf题目中的每一个知识点,具体是怎样证明怎样利用,把每一个知识点与解决的代码...

बनाया गया: 4 मार्च 2026

का उपयोग करके उत्तर दिया गया GPT-5.2 Thinking द्वारा Chat01

प्रश्न

offical.py
server.py

帮我非常详细的讲解这道ctf题目中的每一个知识点,具体是怎样证明怎样利用,把每一个知识点与解决的代码对应起来,并给出学习的相关路径,方法

विचार

Handling file citation details

Looks like the file search results didn’t include line markers, which is odd because I’m supposed to cite with line ranges. But maybe in this case it's fine to cite without them. The instructions do say to include "(website)." I might still check if mclick gives line numbers for clarity.

Following citation instructions and explaining encryption

The instruction is to include citations like "(website)," although without line ranges, so I’ll follow that and cite both files entirely.

Now, the encryption process involves generating an RSA modulus, using an exponent to flip specific bits, and safely disguising responses after a certain step to prevent direct reading.

Understanding RSA LSB attack with disguise

The solution involves using oracles to recover parameters before triggering the flag, at which point the system switches to "safe: False" and begins disguising the ciphertext output. The trick relies on using the LSB (least significant byte) attack to extract the necessary byte, despite the disguise applied with an XOR mask.

Understanding the disguise function and its effect on modulo 256

The disguise function XORs the message bytes in two loops with random masks. The first loop applies the same mask to all bytes, while the second loop iterates backward and uses a new random mask for each byte. The last byte ends up being XORed twice with the same mask, canceling out the disguise.

Understanding RSA byte-wise LSB attack and recovery process

So, the disguise function ultimately leaves the original least significant byte untouched, letting the attacker use an RSA byte oracle. The exploit involves repeatedly multiplying ciphertext and querying to learn the last byte. Additionally, the attacker recovers the RSA modulus (n) through GCD calculations and cipher manipulations, gradually reconstructing the plaintext.

Exploring RSA byte-wise LSB attack using oracle

The attacker queries with encrypt(2, i) to toggle specific bits of the exponent, adjusting the ciphertext accordingly. They check if the modified ciphertext equals the original by using pow(2, 1<<i, n). Through this, they deduce the bits of the exponent, eventually recovering the original plaintext in an iterative manner.

Exploring RSA decryption in modular arithmetic

The decryption process, where ciphertext is multiplied by 256^e mod n, results in plain text modification by 256 modulo n. The oracle reveals only the least significant byte each time, allowing recovery of the original message through iterative expansion using modular arithmetic, iterating over each step to approximate the plain text value.

Reconstructing digits of t_i with oracle feedback

We can express mB^i as t_i * n + p_i. By taking p_i mod B, we deduce t_i mod B iteratively. The algorithm reveals digits of t_i one by one, ultimately reconstructing the full value of t_i in base B by iterating, extracting the least significant digits from each step using modular relationships.

Building t_i step by step through digit extraction

The algorithm builds t_i by appending digits from most significant to least. Starting with k = 0, it accumulates digits by shifting left each time. At each step, the algorithm computes t_i’s digits, iterating through p_i mod B to construct t_i in base B, with the final value of k representing t_{127}.

Iterative construction of t_i and recursive updates

To build t_i iteratively, we start from t_0 and add successive digits based on p_i. Each update involves calculating d_i, which relates to t_i. The recursion formula k = k * B + d_i aims to append digits to t_i by updating from previous steps. However, there's a subtle relationship between d_i and carry that needs careful tracking!

Thought for 4m 6s

उत्तर

下面我按“题目服务端逻辑 → 漏洞点(知识点)→ 证明/推导 → 如何利用 → 对应解题代码”的顺序,把这题从零到一拆开讲清楚,并在最后给一条学习路径+训练方法。我会直接对照你上传的 server.pyoffical.py 来讲。 (server.py) (offical.py)


0. 先把题目“提供了什么能力”读明白

服务端关键逻辑(server.py)

  1. 生成 RSA 参数:
  • p,q 各 512 bit,n=pq
  • phi=(p-1)(q-1)
  • 随机 50 bit 的 e,直到 gcd(e,phi)=1
  • d = e^{-1} mod phi(Python 里用 pow(e,-1,phi)) (server.py)
  1. 菜单能力:
  • Encrypt:你给 plain 和一个位 x,它不是用标准 e,而是用 e ^ (1<<x)(把 e 的第 x 位翻转)去做 pow(plain, e^(1<<x), n) (server.py)
  • Decrypt:你给一个整数 cipher,它做 pow(cipher, d, n) 返回明文(字节形式再 base64) (server.py)
  • Get flag:返回 secret = flag^e mod n,并把 safe=False(之后 encrypt/decrypt 输出会被 disguise 处理) (server.py)

关键点:

  • 你能在 safe=True 阶段拿到“干净”的 encrypt 输出(没 disguise)。
  • 一旦调用 Get flag,之后所有输出都会 disguise,但 Decrypt 仍然存在。这通常意味着“密文→明文”某些信息还能泄漏。

1) 知识点:RSA 基础与符号(必须掌握)

1.1 RSA 的核心等式

  • 加密:c = m^e mod n
  • 解密:m = c^d mod n,其中 ed ≡ 1 (mod phi(n))phi(n)=(p-1)(q-1) (server.py)

1.2 “同余”与“整除”的桥梁

如果

AB(modn)A \equiv B \pmod n

那么等价于

n(AB)n \mid (A-B)

(“同余 ⇔ 差是 n 的倍数”)

这句是本题 GCD 求 n 的根本原因。


2) 漏洞点 1:为什么翻转第 0 位(LSB)能让指数变成 e−1

服务端给你:指数 e 的第 x 位翻转:e' = e ^ (1<<x)。(server.py)

2.1 证明:e 一定是奇数

  • phi=(p-1)(q-1) 一定是偶数(因为 p,q 都是奇素数,p-1,q-1 都是偶数)
  • 服务端强制 gcd(e,phi)=1
  • 如果 e 是偶数,那 gcd(e,phi) 至少包含 2,不可能为 1
    所以 e 必为奇数,最低位 bit0 = 1。(server.py)

2.2 翻转 bit0 的效果

当 bit0=1 时:

e1=e1e \oplus 1 = e-1

因为奇数的二进制最低位是 1,异或 1 就变成 0,相当于减 1。

这就是 offical.py 注释里那段“phi 偶数→e 奇数→翻转最低位相当于 e-1”的来源。(offical.py)


3) 利用 1:用 GCD 直接把 n 算出来(最关键的一步)

3.1 思路:构造两个“差值一定被 n 整除”的数

在 safe=True 阶段我们能得到真实的模 n 结果。offical.py 先做:(offical.py)

python
c1 = encrypt(2, 0) # 2^(e-1) mod n c1_ = encrypt(4, 0) # 4^(e-1) mod n c2 = encrypt(3, 0) # 3^(e-1) mod n c2_ = encrypt(9, 0) # 9^(e-1) mod n

3.2 证明:c1^2 - c1_ 一定是 n 的倍数

因为:

  • c1 = 2^(e-1) mod n
  • c1_ = 4^(e-1) mod n = (2^2)^(e-1) = 2^(2(e-1)) mod n

而:

c12(2e1)2=22(e1)c1(modn)c1^2 \equiv (2^{e-1})^2 = 2^{2(e-1)} \equiv c1_ \pmod n

所以:

n(c12c1)n \mid (c1^2 - c1_)

同理:

n(c22c2)n \mid (c2^2 - c2_)

因此:

ngcd(c12c1, c22c2)n \mid \gcd(c1^2-c1_,\ c2^2-c2_)

3.3 为什么要用两组 (2,4) 和 (3,9)

一组差值可能是 k*n(带倍数),两组取 gcd 通常能把倍数消掉,逼近 n。

于是 offical.py 做:(offical.py)

python
n = GCD(c1**2-c1_, c2**2-c2_)

3.4 “去掉小素数因子”的原因

脚本随后把 n 里 2~999 的小因子除掉:(offical.py)

python
for i in range(2, 1000): if n%i==0: while n%i==0: n=n//i

原因:你得到的 gcd 可能是 n * t,其中 t 往往是一些小因子(比如差值里额外带出来的公共小因子),把它们剥掉更稳。严格来说不保证只含小因子,但 CTF 中常这么处理。


4) 利用 2:逐位恢复 e(50 bit),把“翻转任意位指数”变成“指数泄漏侧信道”

这一块是本题最巧妙的利用:你并不知道 e,但你能查询 2^(e xor 2^i)

4.1 先得到“正确的 2^e mod n”

我们有:

  • encrypt(2,0)2^(e-1) mod n(因为 bit0 翻转)
    那乘以 2 就是:
22e12e(modn)2 \cdot 2^{e-1} \equiv 2^e \pmod n

代码对应:(offical.py)

python
c = c1 * 2 % n # c = 2^e mod n

4.2 关键判别:第 i 位到底是 0 还是 1?

你查询:

python
c_ = encrypt(2, i) # 2^(e xor 2^i) mod n

分两种情况:

情况 A:e 的第 i 位是 1
翻转会把该位从 1→0,相当于减去 2^i

e=e2ie' = e - 2^i

所以

c=2e2ic_ = 2^{e-2^i}

两边乘上 22i2^{2^i}

c22i2ec(modn)c_ \cdot 2^{2^i} \equiv 2^e \equiv c \pmod n

也就是会“对上”你之前算出的 c=2^e mod n

情况 B:e 的第 i 位是 0
翻转会变成 0→1,相当于加上 2^i

e=e+2ie' = e + 2^i

则:

c22i2e+2i22i=2e+2i+1≢2ec_ \cdot 2^{2^i} \equiv 2^{e+2^i} \cdot 2^{2^i} = 2^{e+2^{i+1}} \not\equiv 2^e

(除非出现极小概率的巧合:22i+11modn2^{2^{i+1}} \equiv 1 \mod n,可忽略)

4.3 代码如何实现判别

(offical.py)

python
for i in range(1, 50): c_ = encrypt(2, i) if c_ * pow(2, 1<<i, n) % n == c: e_[i] = 1 else: e_[i] = 0
  • pow(2, 1<<i, n) 计算 22imodn2^{2^i} \mod n
  • 若相乘后等于 c,判定位为 1,否则为 0。
  • e_[0]=1 是因为我们证明了 e 必为奇数。(offical.py)

最后拼成整数 e:(offical.py)

python
e_ = e_[::-1] e = int(''.join(map(str, e_)), 2)

到这里你已经完全恢复了公钥 (n,e)。


5) 漏洞点 2:Get flag 后的 disguise “看似加密输出”,但泄漏了最后一个字节

Get flag 之后 safe=False,encrypt/decrypt 输出会走:(server.py)

python
if not safe: plain = self.disguise(plain)

5.1 证明:disguise 会原样保留最后一字节

disguise 做了两轮 XOR:(server.py)

python
mask = os.urandom(1) for i in range(len(res)): res[i] ^= mask0 for i in range(len(msg)-1, -1, -1): res[i] ^= mask # 第一轮这里的 mask 还是 mask0 mask = os.urandom(1)

看最后一个字节 res[-1]

  • 第一轮:res[-1] ^= mask0
  • 第二轮第一次迭代(i = len-1):res[-1] ^= mask,此时 mask == mask0

所以最后字节变成:

bmask0mask0=bb \oplus mask0 \oplus mask0 = b

完全抵消,最后一个字节保持原值。

offical.py 在 decrypt 输出后做 bytes_to_long,这个整数的 mod 256 正好取的是最后一个字节(大端 bytes 的最后一位是最低有效字节)。因此即使被 disguise,p % 256 仍然是真实明文的最低字节。(offical.py) (server.py)


6) 利用 3:RSA 的“乘法同态” + “最低字节 oracle”恢复 127 字节明文

6.1 知识点:RSA 的乘法同态(malleability)

对 RSA:

Enc(m)=memodnEnc(m) = m^e \bmod n

则:

Enc(ms)=(ms)emeseEnc(m)se(modn)Enc(m\cdot s) = (m\cdot s)^e \equiv m^e \cdot s^e \equiv Enc(m)\cdot s^e \pmod n

所以你能在密文层面乘上 ses^e,解密后明文会乘上 ss

6.2 本题怎么用:让明文不断乘 256

B=256B=256。如果 flag 明文是 mm,题目给你:

c0=memodnc_0 = m^e \bmod n

你把密文迭代更新:

ci=ci1Bemodnc_{i} = c_{i-1} \cdot B^e \bmod n

则解密得到:

Dec(ci)=mBimodnDec(c_i) = m \cdot B^i \bmod n

代码对应(每轮都做一次乘法):(offical.py)

python
c = c * pow(256, e, n) % n p = decrypt(c) # 这里p的最后一字节是真实的 (m*256^i mod n) 的最后一字节

6.3 关键推导:如何从 “(m·B^i mod n) 的最低字节” 推出 m?

设:

mBi=tin+pi,0pi<nmB^i = t_i n + p_i,\quad 0 \le p_i < n

其中:

  • pi=mBimodnp_i = mB^i \bmod n
  • ti=mBint_i = \left\lfloor \frac{mB^i}{n} \right\rfloor

对两边取 mod BB(即看最低字节):

  • 因为 i1i\ge 1mBi0(modB)mB^i \equiv 0 \pmod B
  • 所以:
pitin(modB)p_i \equiv -t_i n \pmod B

因为 gcd(n,256)=1(n 是奇数),所以 n 在 mod 256 下可逆:

tipin1(modB)t_i \equiv -p_i \cdot n^{-1} \pmod B

也就是:

timod256 可以从 pimod256 算出来t_i \bmod 256\ \text{可以从}\ p_i \bmod 256\ \text{算出来}

这正是脚本里的:(offical.py)

python
(-inverse(n,256) * p % 256)

6.4 为什么能“逐字节”拼出 t_i(进而还原 m)

还要一个递推关系:

mBi1=ti1n+pi1mB^{i-1} = t_{i-1}n + p_{i-1}

乘 B:

mBi=(Bti1)n+(Bpi1)mB^i = (B t_{i-1})n + (B p_{i-1})

再除以 n,商为:

ti=Bti1+Bpi1nt_i = B t_{i-1} + \left\lfloor \frac{B p_{i-1}}{n} \right\rfloor

注意 Bpi1n[0,B1]\left\lfloor \frac{B p_{i-1}}{n} \right\rfloor \in [0,B-1],它正好是 t_i 的最低一个 base-256“数字”,因为 Bti1B t_{i-1} 一定是 256 的倍数。

而我们上一节算出的 timod256t_i \bmod 256 就是这个新“数字”。因此可以用 base-256 累积:

ti=256ti1+(timod256)t_i = 256\cdot t_{i-1} + (t_i \bmod 256)

代码对应:(offical.py)

python
k = k*256 + (-inverse(n,256)*p % 256)

这里的 k 就是在迭代维护 t_i

6.5 最后从 t_127 反推出 m

因为 flag 被 pad 到 127 字节(即小于 256127256^{127}),脚本迭代 127 次得到 t127t_{127}。(server.py) (offical.py)

由:

mB127=t127n+p127m=t127n+p127B127mB^{127} = t_{127}n + p_{127} \Rightarrow m = \frac{t_{127}n + p_{127}}{B^{127}}

脚本用:

python
long_to_bytes(k*n//256**127)

相当于取 t127n256127\left\lfloor \frac{t_{127}n}{256^{127}}\right\rfloor 当作 m 的恢复值。(offical.py)

实战里更稳的做法:通常会再用 padding 结构(PKCS#7)验证/微调(比如尝试 +0..255),但这题很多时候直接就能出可读 flag(脚本也用 decode(errors='ignore') 容错)。(offical.py) (server.py)


7) 把“每个知识点”与 offical.py 的关键代码一一对应

7.1 交互与编码知识点:base64、bytes↔int、大数

  • 服务端输出 base64( long_to_bytes(x) ),所以脚本必须 b64decode → bytes_to_long (offical.py) (server.py)
  • encrypt/decrypt/getFlag 三个函数做的就是“菜单交互 + 编解码”。(offical.py)

7.2 知识点:e 必为奇数 → 翻转 LSB 得到 e−1

  • 注释解释 + e_[0]=1 + 用 encrypt(2,0) 作为 2e12^{e-1} (offical.py) (server.py)

7.3 知识点:同余差值被 n 整除 → GCD 求模数 n

  • n = GCD(c1**2-c1_, c2**2-c2_) (offical.py)
  • “去小因子”是工程化稳定性处理 (offical.py)

7.4 知识点:翻转第 i 位指数 → 通过比较恢复 e 的每一位

  • c = c1*2%n 得到 2e2^e (offical.py)
  • if c_ * 2^{2^i} == c 判定位 i 是否为 1 (offical.py)

7.5 知识点:disguise 保留最后字节 → 形成“最低字节 oracle”

  • 证明来自 disguise 的“最后字节 XOR 两次同一 mask”抵消 (server.py)
  • 代码里只用 p % 256 就是只取最后字节 (offical.py)

7.6 知识点:RSA 乘法同态 + 逐字节恢复商 t_i → 还原 m

  • c=c*pow(256,e,n)%n 把明文乘 256 (offical.py)
  • (-inverse(n,256)*p%256) 恢复 t_i mod 256 (offical.py)
  • k=k*256+... base-256 递推构造 t_i (offical.py)
  • k*n//256**127 近似求回 m (offical.py)

8) 学习路径(从不会到能独立做出这题)

我建议按“数学 → RSA → 典型攻击 → CTF工程实现”四层走,每层都要配练习。

第 1 层:模运算与数论基本功(1~2 周)

要做到看到公式就能变形:

  • 同余、整除、最大公约数(欧几里得算法)
  • 模逆元(扩展欧几里得、pow(a,-1,m) 的意义)
  • 快速幂(为何 pow(x,e,n) 快)

练习方法:

  • 手推 5~10 个小例子:自己选 n,验证 “同余差值可被 n 整除”“gcd 能提取公共因子”。

第 2 层:RSA 结构与安全条件(1 周)

要掌握:

  • phi(n)ed≡1 (mod phi) 的由来
  • 为什么 e 通常取 65537、为什么必须 gcd(e,phi)=1
  • RSA 的乘法同态(这题后半段的核心)

练习方法:

  • 自己写一个小 RSA:生成 p,q,验证 Dec(Enc(m))==m
  • 验证同态:Enc(m)*Enc(s) == Enc(m*s)(注意是同一个 e 同一个 n)

第 3 层:CTF 常见 RSA 侧信道/Oracle 攻击(2~4 周)

建议顺序:

  1. GCD 恢复 n:从多个同余关系里捞出模数
  2. 指数泄漏/指数操纵:像本题这样“能查询不同指数的幂”时怎么把 e 拆出来
  3. LSB/Byte Oracle
    • 先做“奇偶 oracle(LSB 1 bit)”的经典攻击
    • 再做“最低字节 oracle(8 bit)”的更快版本(本题就是这个)
  4. 最后再看更复杂的 padding oracle / Bleichenbacher(不要求,但很有帮助)

练习方法:

  • 每学一个攻击,自己写一个最小化 demo:服务端只保留 20 行逻辑,客户端攻击脚本 30 行以内跑通。

第 4 层:工程化(pwntools + 编解码 + 大整数)(持续)

必须熟练:

  • pwntools 的 remotesendline/recvline、交互稳定性
  • bytes_to_long/long_to_bytes、端序与“最低字节”的关系
  • base64、字节长度、前导 0 的坑(long_to_bytes 会丢前导 0)

练习方法:

  • 刻意把服务端输出改成不同形式(十六进制/base64/直接整数),自己改客户端适配。

9) 建议你“复盘这题”的最佳方法(能真正长本事)

  1. server.py 本地跑起来(固定随机种子/打印 e,n),先验证每一步推导都正确。(server.py)
  2. 分三阶段调试脚本:
    • 只做 gcd 求 n(打印 n 比对)
    • 在已知 n 的情况下做 e 位恢复(打印二进制比对)
    • 最后再开 safe=False 验证 “p%256 不变” 的性质(这一步能让你对 disguise 的漏洞理解非常牢)(offical.py) (server.py)

如果你愿意,我也可以把 offical.py 重新写成一份**带“推导式注释 + 每一步断言检查 + 更稳的末尾校验(利用 PKCS#7 padding 验证/微调)”**的版本,让你对每个变量(c、p、k、t_i)到底代表什么更直观。

इस प्रश्नोत्तर को साझा करें