CTF密码学题目深度解析:从复杂迭代到仿射密码的突破

admin 2026-02-04 17:55:47 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文解析了CTF题目easyCaesar,揭示其看似复杂的迭代加密本质是模67的仿射变换。利用Flag格式已知明文攻击,将密钥空间大幅缩减,通过暴力枚举出参数a=23和b=63,最终成功解密获取Flag。文章详细论证了数学原理并给出算法改进建议。 综合评分: 90 文章分类: CTF,漏洞分析


cover_image

CTF密码学题目深度解析:从复杂迭代到仿射密码的突破

原创

破镜安全 破镜安全

破镜安全

2026年2月4日 08:03 四川

CTF密码学题目深度解析:从复杂迭代到仿射密码的突破

前言

在CTF竞赛中,密码学题目往往会通过看似复杂的算法设计来增加破解难度。本文将详细分析一道名为”easyCaesar”的密码学题目,该题目结合了凯撒密码、仿射变换和随机数生成,看似难以破解,但通过深入的数学分析,我们将揭示其本质上的脆弱性,并最终完成破解。

本文适合密码学初学者阅读,我们将从零开始,一步步分析题目,揭示破解思路,不省略任何关键细节。

一、题目初探

1.1 题目文件

题目提供了一个Python加密脚本easyCaesar.py,以及脚本运行后输出的密文。让我们先查看这个脚本:

from secret import flag
import random

assert flag.startswith('hsctf{')

N = 128
m1 = flag[4:-1]
key = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890{}_#&'

def encrypt(m, a, b):
    c = ''
    kt = random.getrandbits(len(m))
    for mt in m:
        tmp = key.find(mt)
        k = kt
        for _ in range(len(m)):
            tmp = tmp * a if k % 2 == 0 else tmp + b
            k = k // 2
        c += key[tmp % len(key)]
    return c

print(encrypt(m1, random.getrandbits(N), random.getrandbits(N)))
# WhcuU0o4Vc0VUasJc08W04uJ0qd2IJpVJ02V04p

脚本输出的密文是:

WhcuU0o4Vc0VUasJc08W04uJ0qd2IJpVJ02V04p

1.2 代码结构分析

在开始破解之前,我们需要理解这个加密算法的结构。让我们逐行分析:

全局变量部分:

  • flag:从外部文件导入的flag,格式以hsctf{开头
  • N = 128:用于生成128位随机数
  • m1 = flag[4:-1]:这是实际被加密的内容,去掉了flag的前4个字符和最后1个字符
  • key:一个67字符的字符串,用作替换密码的字符集

让我们统计一下这个key的组成:

key = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890{}_#&'
  • 小写字母26个:qwertyuiopasdfghjklzxcvbnm
  • 大写字母26个:QWERTYUIOPASDFGHJKLZXCVBNM
  • 数字10个:1234567890
  • 特殊字符5个:{}_#&
  • 总计:67个字符

加密函数分析:

encrypt(m, a, b)函数接收三个参数:

  • m:明文字符串
  • a:第一个128位随机数
  • b:第二个128位随机数

函数内部的工作流程:

  1. 生成一个随机数kt,位数等于明文长度
  2. 外层循环遍历明文的每个字符mt
  3. 对每个字符,找到它在key中的位置索引,存入tmp
  4. 内层循环执行len(m)次迭代
  5. 每次迭代根据k的最低位决定操作:乘a或加b
  6. k右移一位
  7. 最终将tmplen(key)取模,作为密文字符在key中的索引

二、寻找突破口

2.1 加密参数分析

乍看之下,这个加密算法似乎很复杂:

  • 使用了128位的大随机数ab
  • 每次加密都会生成新的随机数kt
  • 内层循环进行了多次迭代运算

但作为密码分析者,我们需要寻找算法中的薄弱环节。让我们重点关注内层循环:

for _ in range(len(m)):
    tmp = tmp * a if k % 2 == 0 else tmp + b
    k = k // 2

这里有一个关键观察:在内层循环的整个执行过程中,变量abk(初始化为kt)都是常数,唯一变化的只有tmp

2.2 数学本质探究

让我们深入分析内层循环到底在做什么。假设明文字符在key中的位置是x,内层循环执行前tmp = x

内层循环根据k(即kt)的二进制位进行不同操作。让我们举一个简单的例子来理解这个过程。

示例:假设len(m) = 3kt的二进制表示为101

初始状态:

  • tmp = x(明文字符位置)
  • k = 101(二进制)= 5(十进制)

第1次迭代:

  • k % 2 = 1(最低位为1),执行tmp = tmp + b,即tmp = x + b
  • k = k // 2 = 2(二进制10

第2次迭代:

  • k % 2 = 0(最低位为0),执行tmp = tmp * a,即tmp = (x + b) * a = a*x + a*b
  • k = k // 2 = 1(二进制1

第3次迭代:

  • k % 2 = 1(最低位为1),执行tmp = tmp + b,即tmp = a*x + a*b + b = a*x + b(a+1)
  • k = k // 2 = 0

最终结果:tmp = a*x + b(a+1)

最后对67取模:tmp % 67 = (a*x + b(a+1)) % 67

2.3 仿射变换的揭示

从上面的例子可以看出,无论kt的二进制表示如何,无论内层循环执行多少次,最终的结果都可以表示为:

tmp = (A * x + B) mod 67

其中:

  • AB是由原始参数abkt共同决定的常数
  • x是明文字符在key中的位置
  • 67是len(key)

这是一个标准的仿射变换(Affine Transformation)!

仿射密码是古典密码学中的一种,其加密公式为:

E(x) = (a*x + b) mod m

解密公式为:

D(y) = a^(-1) * (y - b) mod m

其中a^(-1)a在模m意义下的乘法逆元。

2.4 参数空间的缩减

这里有一个至关重要的发现:虽然原始的ab是128位的大整数(范围可达2^128,约10^38),但由于所有运算最终都要对67取模,等效的仿射变换参数AB必然是小于67的整数

这意味着:

  • 原本看似有2^128 × 2^128个可能的参数组合
  • 实际上只等价于最多67 × 67 = 4489种不同的变换

参数空间从天文数字级别骤降到不足5000种,这为暴力破解提供了可能!

2.5 加密过程的统一性

还有一个关键观察:在encrypt函数中,kt在函数开始时生成一次后就不再改变。这意味着:

明文的每个字符都经过完全相同的仿射变换!

只是因为不同字符在key中的位置(x值)不同,所以得到的密文也不同。但变换的参数AB对所有字符都是相同的。

三、利用已知信息构造攻击

3.1 Flag格式解析

密码分析的一个重要原则是充分利用已知信息。在这个题目中,我们有一个重要的已知信息:

assert flag.startswith('hsctf{')
m1 = flag[4:-1]

让我们详细分析flag的格式。假设完整的flag是:

hsctf{xxxxxxxxxxxxx}

那么:

  • flag[0:4] = 'hsct'
  • flag[4] = 'f'
  • flag[5] = '{'
  • flag[6:-1] = 实际的flag内容
  • flag[-1] = '}'

因此,m1 = flag[4:-1]的意思是:

  • 去掉前4个字符'hsct'
  • 去掉最后1个字符'}'
  • 剩余部分就是'f{xxxxxxxxxxxxx'

这给我们提供了关键信息:加密的明文必然以'f{'开头!

3.2 构造已知明密文对

既然我们知道明文以'f{'开头,而密文是'WhcuU0o4Vc0VUasJc08W04uJ0qd2IJpVJ02V04p',那么:

  • 明文第1个字符:'f' → 密文第1个字符:'W'
  • 明文第2个字符:'{' → 密文第2个字符:'h'

现在我们需要找到这些字符在key中的位置。让我们写一个简单的查询:

key = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890{}_#&'

print(f"'f' 在 key 中的位置: {key.find('f')}")  # 13
print(f"'{{' 在 key 中的位置: {key.find('{')}")  # 62
print(f"'W' 在 key 中的位置: {key.find('W')}")  # 27
print(f"'h' 在 key 中的位置: {key.find('h')}")  # 15

运行结果:

'f' 在 key 中的位置: 13
'{' 在 key 中的位置: 62
'W' 在 key 中的位置: 27
'h' 在 key 中的位置: 15

3.3 建立方程组

现在我们有了两组已知的明密文对应关系。根据仿射变换公式:

cipher_pos = (A * plain_pos + B) mod 67

我们可以建立两个方程:

方程1(f → W):

(A * 13 + B) mod 67 = 27

方程2({ → h):

(A * 62 + B) mod 67 = 15

这是一个二元一次同余方程组,有两个未知数AB,以及两个方程,理论上可以求解。

四、参数破解

4.1 暴力枚举方法

虽然同余方程组有数学方法可以求解,但考虑到参数空间只有67×67=4489种可能,最简单直接的方法就是暴力枚举。

我们可以遍历所有可能的AB值(范围1到66),检查哪一组同时满足两个方程:

MOD = 67
f_pos = 13
brace_pos = 62
W_pos = 27
h_pos = 15

found = False
for a in range(1, MOD):
    for b in range(1, MOD):
        if (a * f_pos + b) % MOD == W_pos and (a * brace_pos + b) % MOD == h_pos:
            print(f"找到解: a = {a}, b = {b}")
            found = True
            break
    if found:
        break

运行这段代码,我们得到:

找到解: a = 23, b = 63

让我们验证这组解:

print(f"验证方程1: (23 * 13 + 63) % 67 = {(23 * 13 + 63) % 67}")
print(f"验证方程2: (23 * 62 + 63) % 67 = {(23 * 62 + 63) % 67}")

输出:

验证方程1: (23 * 13 + 63) % 67 = 27  ✓
验证方程2: (23 * 62 + 63) % 67 = 15  ✓

完美!我们找到了正确的仿射变换参数。

4.2 为什么参数从1开始

细心的读者可能注意到,我们的枚举范围是range(1, MOD)而不是range(0, MOD)。这是因为:

  1. 如果a = 0,则仿射变换退化为y = b mod 67,所有不同的明文字符都会映射到同一个密文字符,这显然不符合题目情况。
  2. 在仿射密码中,参数a必须与模数m互质(即gcd(a, m) = 1),才能保证变换是可逆的。虽然67是质数,所以除了0以外的所有数都与67互质,但为了保险起见,我们从1开始枚举。

五、解密实现

5.1 仿射密码解密原理

既然加密是:

y = (a * x + b) mod m

那么解密就是求解:

x = (y - b) * a^(-1) mod m

其中a^(-1)a在模m意义下的乘法逆元,满足:

a * a^(-1) ≡ 1 (mod m)

5.2 计算模逆元

模逆元可以使用扩展欧几里得算法计算。在Python中,我们可以使用gmpy2库来快速计算:

import gmpy2

a = 23
MOD = 67
a_inv = int(gmpy2.invert(a, MOD))
print(f"a 的模逆元: {a_inv}")
print(f"验证: (23 * {a_inv}) % 67 = {(23 * a_inv) % 67}")

输出:

a 的模逆元: 35
验证: (23 * 35) % 67 = 1  ✓

如果不使用gmpy2库,我们也可以手动实现扩展欧几里得算法:

def extended_gcd(a, b):
    if a == 0:
        return b, 0, 1
    gcd, x1, y1 = extended_gcd(b % a, a)
    x = y1 - (b // a) * x1
    y = x1
    return gcd, x, y

def mod_inverse(a, m):
    gcd, x, _ = extended_gcd(a, m)
    if gcd != 1:
        return None  # 模逆元不存在
    return (x % m + m) % m

a_inv = mod_inverse(23, 67)
print(f"a 的模逆元: {a_inv}")

5.3 完整解密过程

现在我们有了所有必要的参数:

  • 仿射变换参数:a = 23, b = 63
  • 模逆元:a_inv = 35
  • 模数:MOD = 67

让我们编写完整的解密程序:

import gmpy2

key = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890{}_#&'
ciphertext = "WhcuU0o4Vc0VUasJc08W04uJ0qd2IJpVJ02V04p"

# 参数
a = 23
b = 63
MOD = 67
a_inv = int(gmpy2.invert(a, MOD))  # 35

# 解密
plaintext = ""
for ch in ciphertext:
    cipher_pos = key.find(ch)  # 密文字符在key中的位置
    plain_pos = ((cipher_pos - b) * a_inv) % MOD  # 逆仿射变换
    plaintext += key[plain_pos]

print(f"解密得到的明文: {plaintext}")

# 恢复完整flag
flag = 'hsct' + plaintext + '}'
print(f"完整FLAG: {flag}")

让我们逐步展示解密的前几个字符:

密文 'W' (位置27) -> (27-63)*35 % 67 -> (-36)*35 % 67 -> 位置13 -> 明文 'f'
密文 'h' (位置15) -> (15-63)*35 % 67 -> (-48)*35 % 67 -> 位置62 -> 明文 '{'
密文 'c' (位置21) -> (21-63)*35 % 67 -> (-42)*35 % 67 -> 位置4  -> 明文 't'
密文 'u' (位置6)  -> (6-63)*35 % 67  -> (-57)*35 % 67 -> 位置15 -> 明文 'h'
密文 'U' (位置32) -> (32-63)*35 % 67 -> (-31)*35 % 67 -> 位置54 -> 明文 '3'

可以看到,前两个字符正是我们预期的'f{',验证了我们的分析!

运行完整程序,得到最终结果:

解密得到的明文: f{th3_l4st_s3c5et_0f_4he_un1ve2se_1s_42
完整FLAG: hsctf{th3_l4st_s3c5et_0f_4he_un1ve2se_1s_42}

六、验证与确认

6.1 反向验证

为了确保我们的分析完全正确,让我们进行反向验证:使用找到的参数a=23, b=63对明文进行加密,看是否能得到原始密文。

key = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890{}_#&'
plaintext = "f{th3_l4st_s3c5et_0f_4he_un1ve2se_1s_42"
expected_cipher = "WhcuU0o4Vc0VUasJc08W04uJ0qd2IJpVJ02V04p"

a = 23
b = 63
MOD = 67

# 加密
encrypted = ""
for ch in plaintext:
    plain_pos = key.find(ch)
    cipher_pos = (a * plain_pos + b) % MOD
    encrypted += key[cipher_pos]

print(f"加密结果: {encrypted}")
print(f"期望密文: {expected_cipher}")
print(f"是否匹配: {encrypted == expected_cipher}")

运行结果:

加密结果: WhcuU0o4Vc0VUasJc08W04uJ0qd2IJpVJ02V04p
期望密文: WhcuU0o4Vc0VUasJc08W04uJ0qd2IJpVJ02V04p
是否匹配: True

完美匹配!这证明我们的分析完全正确。

6.2 逐字符对比验证

让我们详细验证每个字符的加密过程:

print(f"{'序号':<4}&nbsp;{'明文':<4}&nbsp;{'明文位置':<8}&nbsp;{'计算':<20}&nbsp;{'密文位置':<8}&nbsp;{'密文':<4}&nbsp;{'匹配':<4}")
print("-"&nbsp;*&nbsp;60)

match_count =&nbsp;0
for&nbsp;i, (p_ch, c_ch)&nbsp;in&nbsp;enumerate(zip(plaintext, expected_cipher)):
&nbsp; &nbsp; p_pos = key.find(p_ch)
&nbsp; &nbsp; c_pos_calc = (a * p_pos + b) % MOD
&nbsp; &nbsp; c_ch_calc = key[c_pos_calc]
&nbsp; &nbsp; c_pos_real = key.find(c_ch)

&nbsp; &nbsp; match =&nbsp;"✓"&nbsp;if&nbsp;c_ch_calc == c_ch&nbsp;else&nbsp;"✗"
&nbsp; &nbsp;&nbsp;if&nbsp;c_ch_calc == c_ch:
&nbsp; &nbsp; &nbsp; &nbsp; match_count +=&nbsp;1

&nbsp; &nbsp;&nbsp;if&nbsp;i <&nbsp;5&nbsp;or&nbsp;i >= len(plaintext) -&nbsp;3: &nbsp;# 显示前5个和后3个
&nbsp; &nbsp; &nbsp; &nbsp; calc_str =&nbsp;f"({a}*{p_pos}+{b})%{MOD}"
&nbsp; &nbsp; &nbsp; &nbsp; print(f"{i+1:<4}&nbsp;{p_ch:<4}&nbsp;{p_pos:<8}&nbsp;{calc_str:<20}&nbsp;{c_pos_calc:<8}&nbsp;{c_ch_calc:<4}&nbsp;{match:<4}")
&nbsp; &nbsp;&nbsp;elif&nbsp;i ==&nbsp;5:
&nbsp; &nbsp; &nbsp; &nbsp; print(f"{'...':<4}&nbsp;{'...':<4}&nbsp;{'...':<8}&nbsp;{'...':<20}&nbsp;{'...':<8}&nbsp;{'...':<4}&nbsp;{'...':<4}")

print("-"&nbsp;*&nbsp;60)
print(f"匹配度:&nbsp;{match_count}/{len(plaintext)}&nbsp;(100%)")

输出:

序号 明文 明文位置 &nbsp; 计算 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 密文位置 &nbsp; 密文 匹配
------------------------------------------------------------
1 &nbsp; &nbsp;f &nbsp; &nbsp;13 &nbsp; &nbsp; &nbsp; (23*13+63)%67 &nbsp; &nbsp; &nbsp; &nbsp;27 &nbsp; &nbsp; &nbsp; W &nbsp; &nbsp;✓
2 &nbsp; &nbsp;{ &nbsp; &nbsp;62 &nbsp; &nbsp; &nbsp; (23*62+63)%67 &nbsp; &nbsp; &nbsp; &nbsp;15 &nbsp; &nbsp; &nbsp; h &nbsp; &nbsp;✓
3 &nbsp; &nbsp;t &nbsp; &nbsp;4 &nbsp; &nbsp; &nbsp; &nbsp;(23*4+63)%67 &nbsp; &nbsp; &nbsp; &nbsp; 21 &nbsp; &nbsp; &nbsp; c &nbsp; &nbsp;✓
4 &nbsp; &nbsp;h &nbsp; &nbsp;15 &nbsp; &nbsp; &nbsp; (23*15+63)%67 &nbsp; &nbsp; &nbsp; &nbsp;6 &nbsp; &nbsp; &nbsp; &nbsp;u &nbsp; &nbsp;✓
5 &nbsp; &nbsp;3 &nbsp; &nbsp;54 &nbsp; &nbsp; &nbsp; (23*54+63)%67 &nbsp; &nbsp; &nbsp; &nbsp;32 &nbsp; &nbsp; &nbsp; U &nbsp; &nbsp;✓
... &nbsp;... &nbsp;... &nbsp; &nbsp; &nbsp;... &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;... &nbsp; &nbsp; &nbsp;... &nbsp;...
37 &nbsp; _ &nbsp; &nbsp;64 &nbsp; &nbsp; &nbsp; (23*64+63)%67 &nbsp; &nbsp; &nbsp; &nbsp;61 &nbsp; &nbsp; &nbsp; 0 &nbsp; &nbsp;✓
38 &nbsp; 4 &nbsp; &nbsp;55 &nbsp; &nbsp; &nbsp; (23*55+63)%67 &nbsp; &nbsp; &nbsp; &nbsp;55 &nbsp; &nbsp; &nbsp; 4 &nbsp; &nbsp;✓
39 &nbsp; 2 &nbsp; &nbsp;53 &nbsp; &nbsp; &nbsp; (23*53+63)%67 &nbsp; &nbsp; &nbsp; &nbsp;9 &nbsp; &nbsp; &nbsp; &nbsp;p &nbsp; &nbsp;✓
------------------------------------------------------------
匹配度: 39/39 (100%)

所有39个字符全部匹配,验证完全成功!

七、完整解题脚本

综合以上分析,我们可以编写一个完整的解题脚本:

import&nbsp;gmpy2

def&nbsp;solve():
&nbsp; &nbsp;&nbsp;"""完整的解题函数"""
&nbsp; &nbsp;&nbsp;# 题目数据
&nbsp; &nbsp; key =&nbsp;'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890{}_#&'
&nbsp; &nbsp; ciphertext =&nbsp;"WhcuU0o4Vc0VUasJc08W04uJ0qd2IJpVJ02V04p"
&nbsp; &nbsp; MOD = len(key) &nbsp;# 67

&nbsp; &nbsp; print("="&nbsp;*&nbsp;70)
&nbsp; &nbsp; print("步骤1: 利用flag格式构造已知明密文对")
&nbsp; &nbsp; print("="&nbsp;*&nbsp;70)

&nbsp; &nbsp;&nbsp;# flag格式为 hsctf{...},m1 = flag[4:-1] = f{...
&nbsp; &nbsp;&nbsp;# 因此明文前两个字符是 'f' 和 '{'
&nbsp; &nbsp; f_pos = key.find('f')
&nbsp; &nbsp; brace_pos = key.find('{')
&nbsp; &nbsp; W_pos = key.find('W') &nbsp;# 密文第一个字符
&nbsp; &nbsp; h_pos = key.find('h') &nbsp;# 密文第二个字符

&nbsp; &nbsp; print(f"已知: 'f'(位置{f_pos}) -> 'W'(位置{W_pos})")
&nbsp; &nbsp; print(f"已知: '{{'(位置{brace_pos}) -> 'h'(位置{h_pos})")

&nbsp; &nbsp; print("\n"&nbsp;+&nbsp;"="&nbsp;*&nbsp;70)
&nbsp; &nbsp; print("步骤2: 暴力枚举仿射变换参数")
&nbsp; &nbsp; print("="&nbsp;*&nbsp;70)

&nbsp; &nbsp;&nbsp;# 建立方程组并暴力求解
&nbsp; &nbsp;&nbsp;# (a * f_pos + b) % MOD = W_pos
&nbsp; &nbsp;&nbsp;# (a * brace_pos + b) % MOD = h_pos

&nbsp; &nbsp; a, b =&nbsp;0,&nbsp;0
&nbsp; &nbsp;&nbsp;for&nbsp;i&nbsp;in&nbsp;range(1, MOD):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;j&nbsp;in&nbsp;range(1, MOD):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(i * f_pos + j) % MOD == W_pos&nbsp;and&nbsp;\
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(i * brace_pos + j) % MOD == h_pos:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; a, b = i, j
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;a !=&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break

&nbsp; &nbsp; print(f"找到参数: a =&nbsp;{a}, b =&nbsp;{b}")
&nbsp; &nbsp; print(f"验证: ({a}&nbsp;*&nbsp;{f_pos}&nbsp;+&nbsp;{b}) %&nbsp;{MOD}&nbsp;=&nbsp;{(a * f_pos + b) % MOD}&nbsp;(应为{W_pos}) ✓")
&nbsp; &nbsp; print(f"验证: ({a}&nbsp;*&nbsp;{brace_pos}&nbsp;+&nbsp;{b}) %&nbsp;{MOD}&nbsp;=&nbsp;{(a * brace_pos + b) % MOD}&nbsp;(应为{h_pos}) ✓")

&nbsp; &nbsp; print("\n"&nbsp;+&nbsp;"="&nbsp;*&nbsp;70)
&nbsp; &nbsp; print("步骤3: 计算模逆元")
&nbsp; &nbsp; print("="&nbsp;*&nbsp;70)

&nbsp; &nbsp; a_inv = int(gmpy2.invert(a, MOD))
&nbsp; &nbsp; print(f"a^(-1) =&nbsp;{a_inv}")
&nbsp; &nbsp; print(f"验证: ({a}&nbsp;*&nbsp;{a_inv}) %&nbsp;{MOD}&nbsp;=&nbsp;{(a * a_inv) % MOD}&nbsp;✓")

&nbsp; &nbsp; print("\n"&nbsp;+&nbsp;"="&nbsp;*&nbsp;70)
&nbsp; &nbsp; print("步骤4: 解密密文")
&nbsp; &nbsp; print("="&nbsp;*&nbsp;70)

&nbsp; &nbsp; plaintext =&nbsp;""
&nbsp; &nbsp;&nbsp;for&nbsp;ch&nbsp;in&nbsp;ciphertext:
&nbsp; &nbsp; &nbsp; &nbsp; cipher_pos = key.find(ch)
&nbsp; &nbsp; &nbsp; &nbsp; plain_pos = ((cipher_pos - b) * a_inv) % MOD
&nbsp; &nbsp; &nbsp; &nbsp; plaintext += key[plain_pos]

&nbsp; &nbsp; print(f"解密结果:&nbsp;{plaintext}")

&nbsp; &nbsp;&nbsp;# 恢复完整flag
&nbsp; &nbsp; flag =&nbsp;'hsct'&nbsp;+ plaintext +&nbsp;'}'

&nbsp; &nbsp; print("\n"&nbsp;+&nbsp;"="&nbsp;*&nbsp;70)
&nbsp; &nbsp; print("最终结果")
&nbsp; &nbsp; print("="&nbsp;*&nbsp;70)
&nbsp; &nbsp; print(f"FLAG:&nbsp;{flag}")

&nbsp; &nbsp;&nbsp;return&nbsp;flag

if&nbsp;__name__ ==&nbsp;"__main__":
&nbsp; &nbsp; flag = solve()

运行这个脚本,输出:

======================================================================
步骤1: 利用flag格式构造已知明密文对
======================================================================
已知: 'f'(位置13) -> 'W'(位置27)
已知: '{'(位置62) -> 'h'(位置15)

======================================================================
步骤2: 暴力枚举仿射变换参数
======================================================================
找到参数: a = 23, b = 63
验证: (23 * 13 + 63) % 67 = 27 (应为27) ✓
验证: (23 * 62 + 63) % 67 = 15 (应为15) ✓

======================================================================
步骤3: 计算模逆元
======================================================================
a^(-1) = 35
验证: (23 * 35) % 67 = 1 ✓

======================================================================
步骤4: 解密密文
======================================================================
解密结果: f{th3_l4st_s3c5et_0f_4he_un1ve2se_1s_42

======================================================================
最终结果
======================================================================
FLAG: hsctf{th3_l4st_s3c5et_0f_4he_un1ve2se_1s_42}

八、深层技术分析

8.1 为什么内层循环等价于仿射变换

让我们从数学角度严格证明这个结论。

定理:对于任意常数abk和变量x,执行以下操作序列:

for i in range(n):
&nbsp; &nbsp; x = x * a if (k >> i) & 1 == 0 else x + b

等价于一个仿射变换:x' = (A * x + B) mod m

证明: 我们使用数学归纳法。设k的二进制表示为b_{n-1}...b_1b_0

基础情况(n=1):

  • 如果b_0 = 0x' = x * a = a * x + 0,是仿射变换
  • 如果b_0 = 1x' = x + b = 1 * x + b,是仿射变换

归纳步骤:假设执行k步后,结果可表示为x_k = A_k * x + B_k

第k+1步:

  • 如果b_k = 0x_{k+1} = x_k * a = (A_k * x + B_k) * a = (a*A_k) * x + (a*B_k)
  • 如果b_k = 1x_{k+1} = x_k + b = (A_k * x + B_k) + b = A_k * x + (B_k + b)

两种情况下,x_{k+1}仍然是x的一次函数,即仿射变换。

因此,无论执行多少步,无论k的二进制表示如何,结果始终是仿射变换。

8.2 模运算的简化效应

原始参数ab是128位整数,范围高达2^128 ≈ 3.4×10^38。但为什么最终等效参数只需要在[0, 66]范围内呢?

这是由于模运算的周期性。根据同余的性质:

如果 a ≡ a' (mod m),则 a * x ≡ a' * x (mod m)
如果 b ≡ b' (mod m),则 x + b ≡ x + b' (mod m)

因此,对于仿射变换y = (a*x + b) mod m

y = (a*x + b) mod m = ((a mod m)*x + (b mod m)) mod m

a' = a mod 67b' = b mod 67,则:

  • aa'在模67意义下完全等价
  • bb'在模67意义下完全等价

这就是为什么128位的大数可以简化为不超过67的小数。

8.3 仿射密码的安全性分析

仿射密码是古典密码学中的一种,其安全性非常低,主要弱点包括:

1. 小的密钥空间对于模数m,仿射密码的密钥空间大小为φ(m) * m,其中φ(m)是欧拉函数。对于m=67(质数),密钥空间为66 * 67 = 4422,可以轻易暴力破解。

2. 易受频率分析如果有足够的密文,可以通过频率分析找出字符映射关系。

3. 已知明文攻击只需要2个已知明密文对就可以建立方程组求解参数,这正是本题的破解方法。

4. 线性特性仿射变换是线性的,容易被数学方法攻破。现代密码学强调非线性,例如AES使用S盒引入非线性。

8.4 题目中的设计缺陷

虽然题目试图通过以下方式增加复杂度:

  1. 使用128位大随机数
  2. 引入随机数kt
  3. 设计复杂的内层循环

但由于以下设计缺陷,这些措施都失效了:

缺陷1:模数太小len(key) = 67太小,导致大随机数的优势完全丧失。

缺陷2:参数固定所有字符使用相同的abkt,导致变换参数统一,易于攻击。

缺陷3:可预测的明文格式Flag格式hsctf{...}提供了已知明文,直接暴露了2个已知明密文对。

缺陷4:本质的线性无论内层循环多复杂,本质仍是线性变换,可以被数学方法化简。

九、安全改进建议

如果要提高这个加密算法的安全性,可以考虑:

9.1 增大密钥空间

将密钥表扩展到至少256个字符(或使用字节值0-255),使模数达到256以上。这样可以:

  • 增大参数空间
  • 提高暴力破解难度
  • 更好地利用大随机数的熵

9.2 引入非线性

在仿射变换基础上添加非线性变换,例如:

tmp = (a * tmp + b) % MOD
tmp = S_box[tmp] &nbsp;# S盒替换,引入非线性

9.3 使用不同的变换参数

为每个字符或每个位置使用不同的变换参数:

for&nbsp;i, mt&nbsp;in&nbsp;enumerate(m):
&nbsp; &nbsp; a_i = hash(kt + i) % MOD
&nbsp; &nbsp; b_i = hash(kt - i) % MOD
&nbsp; &nbsp;&nbsp;# 使用 a_i, b_i 进行变换

9.4 加入轮函数

参考分组密码的设计,进行多轮变换:

for&nbsp;round&nbsp;in&nbsp;range(16):
&nbsp; &nbsp;&nbsp;# 每轮使用不同的轮密钥
&nbsp; &nbsp;&nbsp;# 进行替换、置换、混淆等操作

9.5 使用标准算法

最根本的改进是:不要自创加密算法。使用经过严格审查的标准算法,如:

  • 对称加密:AES、ChaCha20
  • 非对称加密:RSA、ECC
  • 哈希函数:SHA-256、SHA-3

十、总结与启示

10.1 破解过程回顾

本题的破解过程可以总结为以下步骤:

  1. 代码审计:仔细分析加密算法的实现细节
  2. 数学建模:发现内层循环本质上是仿射变换
  3. 参数简化:认识到模运算导致的参数空间缩减
  4. 信息利用:利用flag格式构造已知明密文对
  5. 方程求解:建立同余方程组并暴力枚举求解
  6. 逆变换:使用模逆元实现解密
  7. 验证确认:通过正反向验证确保正确性

10.2 密码学教训

这道题目给我们带来的密码学教训:

1. 不要自创加密算法历史上无数自创的加密算法被证明是脆弱的。密码学是一门深奥的科学,需要经过严格的理论分析和实践检验。

2. 复杂不等于安全表面复杂的算法可能隐藏着简单的数学本质。真正的安全来自于数学上的困难问题,而不是代码的复杂度。

3. 小参数空间是致命的无论使用多大的随机数,如果最终要对小的模数取模,参数空间就会被压缩,导致易受暴力攻击。

4. 已知明文是大忌任何可预测的明文格式都可能成为攻击的突破口。现代密码学中,即使攻击者知道部分明文,也应该无法破解。

5. 线性变换易被破解线性代数提供了强大的工具来分析和破解线性密码。现代密码算法都强调非线性,如AES的S盒。

10.3 CTF学习价值

作为一道CTF题目,本题具有很高的教育价值:

对初学者

  • 学习仿射密码的原理和破解方法
  • 理解模运算和模逆元的概念
  • 掌握已知明文攻击的思路
  • 培养从复杂问题中抽象出数学模型的能力

对进阶者

  • 深入理解算法的数学本质
  • 学习密码分析的系统方法
  • 认识密码设计中的常见陷阱
  • 提升代码审计和漏洞挖掘能力

10.4 实战技巧

在解决类似问题时,可以参考以下实战技巧:

  1. 从简单情况入手:先用小的参数手动模拟算法,理解其工作原理
  2. 寻找不变量:观察哪些变量在循环中保持不变
  3. 数学化简:尝试将复杂的迭代运算化简为简单的代数形式
  4. 利用已知信息:充分挖掘题目给出的所有信息(格式、约束等)
  5. 编写验证代码:用实际代码验证理论分析的正确性
  6. 正反向验证:既要能解密,也要验证加密,确保方法可靠

10.5 延伸阅读

如果对本题涉及的知识感兴趣,可以进一步学习:

基础理论

  • 数论基础:同余理论、模运算、欧几里得算法
  • 抽象代数:群、环、域的概念
  • 古典密码学:凯撒密码、仿射密码、维吉尼亚密码等

现代密码学

  • 分组密码:DES、AES的设计原理
  • 密码分析:差分分析、线性分析、代数攻击
  • 密码协议:密钥交换、数字签名、认证协议

实践技能

  • Python密码学库:PyCrypto、cryptography
  • 密码分析工具:CyberChef、CrypTool
  • CTF平台:CryptoHack、OverTheWire等

结语

通过对这道”easyCaesar”题目的深入分析,我们不仅成功破解了密文,更重要的是学习了密码分析的思维方法。我们看到,看似复杂的加密算法,通过数学分析可以揭示其本质的脆弱性;而真正的密码安全,需要建立在坚实的数学基础和严格的设计原则之上。

希望本文能帮助读者理解仿射密码的原理与破解方法,掌握密码分析的基本技巧,并在今后的CTF竞赛和密码学学习中取得更好的成绩。

最终FLAG:

hsctf{th3_l4st_s3c5et_0f_4he_un1ve2se_1s_42}

免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:破镜安全 破镜安全 破镜安全《CTF密码学题目深度解析:从复杂迭代到仿射密码的突破》

工具|ShiroExploit 网络安全文章

工具|ShiroExploit

文章总结: ShiroExploit是一款针对ApacheShiro框架的反序列化综合利用工具。该工具具备密钥爆破、利用链探测、命令执行及内存马注入等核心功能。
评论:0   参与:  0