前言
仿射密码和希尔密码是两个很有趣的古典密码,两者之间还有一定的联系。
本文想对仿射密码和希尔密码的加密解密进行深度解析,并附有精彩例题。之所以厚颜无耻地说是“深度解析”,是因为网络上的文章 🙂🙂🙂。
环境要求:
- gmpy2 (gmpy2 安装教程)
- numpy (pip install numpy)
仿射密码
仿射密码在 CTF-Wiki 中写得已经很精彩了,这里简练地回顾一下:
仿射密码原理
加密函数:$E(x)=(ax+b) \pmod m$
解密函数: $D(x)=a^{-1} (x-b) \pmod m$
密钥空间
字符集中 $m$ 往往是 26,(其实经常不是 🙂 )
$(a, m) = 1$, 所以 $a \text{ 取值空间大小为 } \varphi (m) = 12$,
$b \in [0,26)$
密钥空间是 $26 \times 12 = 312 $
故,在已知部分明文的情况下才可以攻击!
破解方法
- 如果我们知道其中任意一个参数,那我们便可以很容易地快速枚举另外一个参数得到答案;
- 如果知道两个加密前后的字母也可以解密。
$$
y_1=(ax_1+b) \pmod {m}
\\
y_2=(ax_2+b) \pmod {m}
$$
两式相减:$y_1−y_2=a(x_1−x_2) \mod {26}$ 求得
$$
a = (y_1−y_2) * (x_1−x_2)^{-1} \mod{m}
\\
b = y_1 - ax_1 \mod {m}
$$
这样就求得了参数 a, b,然后用解密函数还原明文:
$$
Message = D(x)=a^{-1} (x-b) \pmod m
$$
字符集和模数的问题
刚刚其实偷偷说了,很多情况(指题目)字符集不是 “a-z” 或者 “A-Z” 这么简单,a/ A 并不一定代表 0,只是在教学的时候常常用这样的方式,毕竟 26 这个数字还是比较小的,爆破也很容易。
模数 m 很多时候也可以任性,不需要恰巧等于字符集的大小。比如,如果字符集的大小为 256,这个时候参数 a 的取值会比较困难。(之前说了,要满足 $(a, m) = 1$,否则无法求逆元,仿射密码本身存在问题)
而这也是密码学的一个小门槛,只会使用工具的人,不擅长也不喜欢处理这些细节。一旦题目更改,原先的工具就失效了。
所以在处理密码的时候需要关注字符集,对应编码。
当然下面也介绍一个一劳永逸的方法。
例题赏析
TWCTF 2016 的 super_express 虽然在 Wiki 里面有了,讲解也很到位,但是还是觉得太精彩了,所以这里引用一下,也为后面统一的解密方法做铺垫。
仿射密码例题
TWCTF 2016 的 super_express
文件一:problem.py
1 | import sys |
文件二:encrypted
1 | 805eed80cbbccb94c36413275780ec94a857dfec8da8ca94a8c313a8ccf9 |
分析思路
可以发现,虽然对于 flag 中的每个字母都加密了 n 次,如果我们仔细分析的话,我们可以发现
$$
\begin{align*}
c_1&=a_1c+b_1 \\
c_2&=a_2c_1+b_2 \\
&=a_1a_2c+a_2b_1+b_2 \\
&=kc+d
\end{align*}
$$
根据第二行的推导,我们可以得到其实 $c_n$ 也是这样的形式,可以看成 $c_n=xc+y$ ,
由于是线性变化,所以依然是仿射变换的形式。
这里使用选择明文攻击:
251 < 256 = 16^2
加上源码中 encrypted += '%02x' % c
易知 encrypted 中两个十六进制字母表示一个数字
破解脚本
1 | # 计算 a, b |
一道让人感到酣畅淋漓的密码学题目就此解决。
统一攻击脚本
下面是我写的攻击脚本
1 | # -*- coding: utf-8 -*- |
在函数 Affine_attack 中分离了字符集处理和选择明文攻击的代码实现,用 str2num 函数将字符集映射到数字集;用 ordertype 函数将字符集返回用到的字符集的基,在还原密文的时候使用。
函数 Affine_attack 就此可以根据参数 字符串 message, 字符串cipher, 模数n, 字符集type 直接破解密码。
脚本里面包含了两个样例,一个是字符集上的,一个是 TWCTF 2016 super_express 这道题目,super_express 需要对密文进行简单地处理,把它转化成字符串的格式,然后只用调用函数即可。:happy:
希尔密码
希尔密码更精彩一些,和仿射密码一样,是域上的线性运算。和仿射密码不同的是,这里的线性运算是矩阵乘法,是多表替换的古典密码。
这部分 CTF-Wiki 写得很少,只有寥寥几句。推荐的工具也不是特别好用的感觉。各路博客也质量也一般般,近乎是直接上代码,代码质量也一般(无意冒犯无意冒犯)。
笔者在华东师范大学数学系课件里面找到了相关的描述。
原理
基础数学知识
定义1:设 $A$ 为定义在集合 $Z_m $ 上的 $n$ 阶方阵,若存在一个定
义在 $Z_m$ 上的方阵B,使得
$$
AB = BA = E \mod {m}
$$
则称 $A$ 模 $m$ 可逆,$B$ 为 $A$ 的模 m 逆矩阵,记为
$$
B = A^{−1} \mod m
$$
和 $Z_m$ 上的整数运算一样,需要满足一定条件才能得到逆运算的结果(这里是逆矩阵)。
定义在集合 $Z_m $上的 $n$ 阶方阵 $A$ 模 $m$ 可逆 $\Leftrightarrow$ $m$ 与 $det(A)$ 互素。
求逆矩阵的方法和大学线性代数里面的类似,但是不一样。
$$
A^{-1}=\frac{1}{|A|} A^{\star} \mod m
\\
\text{其中}\ \frac{1}{|A|} = |A|^{-1} \mod m
$$
以上是基础知识。
加密解密流程
加密:
- 将明文中的字符映射到数字上。(和仿射密码类似)
- 选择一个 n 阶可逆整数方阵 $A$ ,称为 $Hill_n$ 密码的加密矩阵。
- 根据加密矩阵,将明文字母分组。
比如,加密矩阵如果是 2 × 2 的方阵,明文就要每两个为列,组成 2 × n 的矩阵 $X$ - $A \times X = C$ 加密得到 2 × n 的密文矩阵 $C$.
- 密文矩阵再逐列拼起来,再从数字映射到字母,得到密文串。
解密:
解密和加密类似,先通过刚刚的方法计算出 模 m 的逆矩阵,然后用 $A^{-1}$ 再进行解密:
$$
A^{-1} \times C = X
$$
然后用上述相同的方法转换成明文串。
加解密脚本
1 | # -*- coding: utf-8 -*- |
核心代码
矩阵逆元
Hill 密码解密的关键在于求出 矩阵A 的逆元(模 n 情况下的);
理论如下:
$$
A^{-1} = \frac{1}{|A|} A^{\star}
\\
\frac{1}{|A|} = |A|^{-1} \pmod n
\\
\therefore A^{-1} = |A|^{-1} · A^{\star} \pmod n
$$
需要注意的是逆矩阵会有非整数,但是伴随矩阵一定是整数(整数矩阵),所以依旧可以 $A^{\star}=|A| · A^{-1}$ 的方式求出伴随矩阵。
求法:
$$
A^{\star} = |A| · A^{-1}
\\
A^{-1} = |A|^{-1} · A^{\star} \pmod n
$$
Numpy
这段代码大量使用了 numpy 这个第三方库里的对象、函数、方法。
其中和线性代数相关的函数有:
| 函数 | 描述 | 助记 |
|---|---|---|
dot |
如果一维数组,得到两数组的內积 如果二维数组(矩阵)之间的运算,得到矩阵积; |
dot - · |
linalg.det |
数组的行列式 | hanglie |
linalg.solve(A, B) |
求解线性矩阵方程 | |
inv |
计算矩阵的乘法逆矩阵 | invert |
当然代码中还用到了诸如 Cn.flatten('F') 这样的宝藏方法。
还需要注意的是,Numpy 默认的矩阵元素是 np.float 类型,且求逆矩阵、一些运算之后,矩阵类型都会变成 np.float 类型,这对解密的正确性的打击是毁灭性的。
3.9999 会被直接当成 3,而且如果不够注意的话,会导致连环的效应😢 。
其他
这里也兼容了各种字符集。
题目示例
题目选自 Viking 战队2018级的招新题:被线性代数支配的恐惧。
文件
1 | HNBSIRVQQBRFGPS |
1 |
|
分析
这道题不是原生的希尔密码,有希尔密码+仿射密码的味道。(Exciting )
我用 Python 重写代码如下:
1 | import numpy as np |
这就很清晰了,加密方案是:
$$
A_n \times X_n + B_n = C_n
$$
换汤不换药,打走小白工具侠。解密方法是:
$$
X_n = (C_n - B_n) \times A_n^{-1}
$$
用上之前的脚本(调用 || ctrl + c && ctrl +v):
1 | # 被线代支配的恐惧 |
message : XIANDAIHENYOUQU.