该文章主要记录一些古典密码相关的趣题
Ex Viginere?
题目来源:MoeCTF 2021
加密流程解读
题目描述:
1 | 这难道是维吉尼亚吗? |
题目:
1 | from random import randint |
并且给定cipher文件,其中内容为长度为108361的字母文本:
把加密程序一点一点读完,可以提取出以下信息:
- 最终 flag 由 key 组成,其中 key 是被加密的原文中的某个字符串
- key 在原文中的位置未知,且长度未知
- 生成一个随机数 n,作为之后生成 sequence 序列的模数
- sequence 序列其实就是模 n 意义下的斐波那契数列,共生成 65536 项
- 从 sequence 序列中随机选择 a 项并模 26,作为加密所需的 k1 序列
- 从 sequence 序列中随机选择 b 项并模 26,作为加密所需的 k2 序列
- 对原文中的每一个字符 m 进行仿射加密得到密文,其中仿射加密的 A 为 k1[i % a],B 为 k2[i % b]
此外还有一些额外信息:
- 一段有关 key 的md5值
- a*b < 100
引入重合指数
因为密文很长,所以可以用统计方面的思想来解决题目,因此引入重合指数的概念:重合指数是指字符串中两个随机元素相同的概率。因此,假如我们仅考虑完全由英文字符组成的文本,那么一个文本的重合指数就按如下方式计算:(重合指数以 In 表示,p(a)表示 a 在文本中出现的频率)
1 | In = p(a)*p(a) + p(b)*p(b) + p(c)*p(c) + ... + p(z)*p(z) |
代码实现如下:
1 | table = ascii_lowercase |
那么对于下列两种文本分别计算重合指数,就能看出它们的区别:(文本字符均足够多,符合统计概念)
1、完全随机的英文文本(26个字母均随机生成,因此出现频率相当,均为 1/26)
2、正常的英文文本(26个字符有使用频率上的差别),一般来说如下:
1、
2、
由此,我们就有了一个区分随机文本与英文文本的重要依据。
解题
part1:找出密钥长度
重合指数如何应用到题目里呢?首先想明白一点:被同一个密钥(a、b均相同)加密的明文,一定会变成同一个密文。因此我们如果能够找到所有被同一个密钥加密的密文组,那么该密文组的重合指数是符合正常英文文本的(可以仔细想想)
所以我们就需要先由重合指数找到哪些密文是被同一个密钥加密的,实际上这就是在求 gcd(a,b) ,而由题目条件 a*b < 100,因此我们可以很快爆破出来:
1 | #part1 依据重合指数找出a*b=77 |
爆破得到a*b = 77,因此a=7,b=11或a=11,b=7
part2:确定密钥内容
确定了密钥长度后,现在就需要确定密钥的内容具体是什么,也就是 k1、k2里究竟是什么数字。
首先会发现一个 trick,那就是 sequence 序列的生成根本没什么用,由于最终是在mod 26 下进行计算,因此直接把sequence 当成一个由 0-25 组成的随机序列就可以。那么怎么确定呢,依然是利用重合指数,只是要换一种用法。
先引入一个结论:
假设 $ p1, p2 , p3…, pn$为递减的概率分布( $ p1\geq p2 \geq p3…\geq pn$ ),令$ q1’, q2’ , q3’…, qn’$为 $ q1, q2 , q3…, qn$的任意置换,则当 $ q1’, q2’ , q3’…, qn’$ 恰好也为递减的概率分布时,下式取得最大值:
这个结论的证明并不复杂,可以自行尝试。
这个结论有什么用呢?我们现在先把密文按长度为 77 分组,并取出每组的第一个字母组成一个新的密文串,那么这个新的密文串的所有字符,都是被k1[0]、k2[0]这个密钥加密的。那么我们爆破的密钥空间大小一共就只有12*26 = 312(参考仿射密码)。
爆破的依据是什么?仍然是重合指数,对于现在被加密的密文,它自身的重合指数是符合0.065的,因为是单表代换;但是加密过后,各个字符的频率发生了错位。比如正常英文文本中频率最高的是 e,而加密后可能变成了 z。那么把正常英文文本的概率分布当作p,把加密文本的概率分布当作q,就能发现可以利用刚才引入的结论来进行爆破了:
1 | #part2 依据与英语字频吻合指数找出具体的k1、k2 |
之所以只爆破11个而不爆破全部77个,道理也很简单,爆破11个已经能够找出全部的k1、k2了。
part3:爆破md5
知道了密钥后就很轻松,就是个纯粹的爆破问题了
1 | #part3 爆破密钥md5 |
完整exp:(记得改一下if条件)
1 | from string import * |
一个很纯粹的古典密码分析过程,其实是非常有意思也非常有价值的。
Unstable_time_machine
题目来源:暂不明确
题目:
1 | from sympy import totient,mobius |
题目内容很简单,给了一个Time_Machine()函数,然后对secret的每个字节乘上9999,再进行该函数运算,并给出函数值,要求还原secret。
预期解是拟合一个多项式去拟合,从而将函数内部的三层循环用一个拟合后的表达式直接算出,并与secret做对照得到flag。但是既然我将他分类到古典密码中,说明肯定有古典密码的做法,接下来就讲一讲这个做法。
首先要注意到一点,仔细观察这个函数:
1 | def Time_Machine(n): |
三层循环,每层循环最内部其实都可以看作加上一个正数(mobius函数有可能为-1,但对加和的影响基本可以忽略不计)。也就是说,如果我们只关注Time_Machine()函数的自变量和返回值:
那么这个函数有一个很重要的性质:单调递增
这也就是说,对于secret中的每两个字符,如果第一个字符的ASCII码大于第二个字符,那么其乘9999后的函数值也会大于第二个字符。特殊一点,举几个在这题里面将会用到的一些例子就是:
那么接下来我们就根据这一点对secret进行分析。
统计频率
我们首先用如下程序统计出密文中每个数值出现次数:
1 | tt = [11827965311995932558558, 436740119853412116456, 41650252425798979676175, 50626127671556204573190, 41650252425798979676175, 60980101311350323854180, 963563059779422880492, 75413523978530416956915, 436740119853412116456, 43341417116799148415475, 86365877055447093121710, 65537436405378983697036, 43341417116799148415475, 40009068720753857066727, 75413523978530416956915, 436740119853412116456, 89280922053913796530170, 63227978177791478672610, 78047807265358355987991, 436740119853412116456, 75413523978530416956915, 63227978177791478672610, 436740119853412116456, 72846494790687055657395, 63227978177791478672610, 56664683419644282294873, 80750509186740318135738, 43341417116799148415475, 436740119853412116456, 58792701246489673616637, 89280922053913796530170, 436740119853412116456, 65537436405378983697036, 78047807265358355987991, 92269143789218304195771, 92269143789218304195771, 56664683419644282294873, 43341417116799148415475, 436740119853412116456, 36872675850292365936996, 48724893081131391463482, 43341417116799148415475, 36872675850292365936996, 41650252425798979676175, 436740119853412116456, 63227978177791478672610, 45083567393314209309726, 436740119853412116456, 75413523978530416956915, 50626127671556204573190, 58792701246489673616637, 43341417116799148415475, 1864884862832210879220, 436740119853412116456, 11827965311995932558558, 436740119853412116456, 43341417116799148415475, 86365877055447093121710, 65537436405378983697036, 43341417116799148415475, 40009068720753857066727, 75413523978530416956915, 436740119853412116456, 75413523978530416956915, 48724893081131391463482, 43341417116799148415475, 436740119853412116456, 65537436405378983697036, 70345565162259426664317, 63227978177791478672610, 46877717850904608344028, 70345565162259426664317, 36872675850292365936996, 58792701246489673616637, 436740119853412116456, 75413523978530416956915, 63227978177791478672610, 436740119853412116456, 70345565162259426664317, 78047807265358355987991, 60980101311350323854180, 436740119853412116456, 45083567393314209309726, 63227978177791478672610, 70345565162259426664317, 436740119853412116456, 89280922053913796530170, 43341417116799148415475, 36872675850292365936996, 70345565162259426664317, 72846494790687055657395, 1561103367493488746697, 8905422223361768646663, 7434854394795874666620, 19766534787409257458523, 8393017568869581586191, 20736491520823134042987, 10000251324957046694310, 95331756776933061303633, 89280922053913796530170, 2210983036628738475708, 78047807265358355987991, 33924401628947906025510, 54594963263249504183781, 60980101311350323854180, 2210983036628738475708, 83522804274246348745272, 33924401628947906025510, 75413523978530416956915, 50626127671556204573190, 58792701246489673616637, 43341417116799148415475, 33924401628947906025510, 40009068720753857066727, 2210983036628738475708, 58792701246489673616637, 65537436405378983697036, 56664683419644282294873, 43341417116799148415475, 86365877055447093121710, 50626127671556204573190, 75413523978530416956915, 89280922053913796530170, 33924401628947906025510, 80750509186740318135738, 43341417116799148415475, 70345565162259426664317, 89280922053913796530170, 33924401628947906025510, 83522804274246348745272, 43341417116799148415475, 2401069164267674760552, 2401069164267674760552, 493945107805384908948, 101685064551886104386625] |
打印出的结果为:
1 | {11827965311995932558558: 2, 436740119853412116456: 18, 41650252425798979676175: 3, 50626127671556204573190: 4, 60980101311350323854180: 3, 963563059779422880492: 1, 75413523978530416956915: 9, 43341417116799148415475: 14, 86365877055447093121710: 3, 65537436405378983697036: 5, 40009068720753857066727: 3, 89280922053913796530170: 6, 63227978177791478672610: 7, 78047807265358355987991: 4, 72846494790687055657395: 2, 56664683419644282294873: 3, 80750509186740318135738: 2, 58792701246489673616637: 5, 92269143789218304195771: |
同时,打印出字典长度可以看到只有40个可见字符,然后secret长度却为135,说明有多个字符重复出现了,应该可以进行一点语义分析。
数值排序
然后,为了利用单调性,我们将字典中的键值对按数值顺序排好,如:
1 | sequence = [] |
每四个一行打印,主要是为了观察的方便,结果如下:
1 | (436740119853412116456, 18),(493945107805384908948, 1),(963563059779422880492, 1),(1561103367493488746697, 1), |
词频分析
根据刚才的数值排序及频率列表,我们很快能确定以下几个字符:
1 | " ","e","{","}" |
这分别是因为:
- 出现频率最高的值在数值排序中也在第一,所以应该是空格
- 出现频率第二的值根据英语词频,应该是e
- 大括号的ASCII码很靠后,因此最后两个应该就是左右大括号
所以我们第一步可以先手动赋一下这几个字符的初值:
1 | dic_secret[436740119853412116456] = " " |
然后,我们可以用以下函数将已知的字符代回原secret观察:
1 | def output(dic_secret): |
原secret此时如下:
1 | * ****** e**e** *** ** ****e ** *****e **e** ** ***e* * e**e** **e ******* ** *** *** *e**********{************e******e******e****e***} |
同时,我们把已知的几个字符插入数值序列中,就可以直观的看到数值大小顺序,函数如下:
1 | def out_unknown(sequence,dic_secret): |
结果如下:
1 | *****************e*******************{} |
进一步分析
首先,由于知道flag头是DASCTF,那么我们其实就又可以确定六个字符,其对应结果如下:
1 | * ****** e**e** *** ** ****e ** *****e **e** ** ***e* * e**e** **e ******* ** *** *** *e****DASCTF{************e******e******e****e***} |
然后我们又已知,e后面应该全是小写字母了,我们不妨按倒序zyx….给他先排好,函数如下:
1 | init = ord("z") |
其中,not_in_list是为了去除接下来观察发现的多余字母,然后查看打印结果进行微调:
1 | * *k*o*t exqe*t ypu tp spmve ny quzzme *je** ph tkne* * exqe*t tje qrpir*n tp ruo hpr ye*rs*DASCTF{y*u*lo*w*tkne***nqmexkty*very*we***} |
可以看出有几个单词明显不对:
1 | exqe*t ypu spmve tje... |
那么就去除q,j这两个不应该出现的字母,重新打印:
1 | init = ord("z") |
可以发现这下明显合理多了,我们继续假设e前面的四个是小写字母(其实通过词频也可以看出这四个不会是大写字母),继续打印观察结果,此时我们把break点调为大写T对应的数字:
1 | init = ord("z") |
显然可以判断出b不应该出现,加入到not_in_list中:
1 | * didn*t expect you to solve my puzzle ahead of time* * expect the program to run for years*DASCTF{y*u`kn*w`time`c*mplexity`very`we***} |
此时基本上句子已经成型了,从语义已经可以推断剩下的字母,比如:
1 | * didn*t |
三个符号分别对应的是大写I,单引号’,数字0,然后进一步可以由ASCII码表确定最后几个字符,就能得到完整的secret了。
1 | I didn't expect you to solve my puzzle ahead of time. I expect the program to run for years,DASCTF{y0u_kn0w_time_c0mplexity_very_we11!} |
完整代码如下:
1 | def out_sequence(sequence): |