这题涉及仿射密码,但我发现没有人愿意详尽透露 仿射密码加解密公式中的a和b 是怎么求出来的,于是研究并撰写此文。(顺带解决问题“fanfie”)
首先了解仿射密码:
仿射密码
一种单表加密密码(这代表用同一张加密表加密的单个字母的加密结果是一样的)。用来加密的表(我个人习惯称其为“仿射表”)是你自己设定的,体现某些字符对应的数值(不再是ASCII值!)。这张表能把一堆字符(明文)转成数值,然后套公式加密成另外一些数值,然后再转成另一堆字符(密文),反之也可行。仿射表大概长这样:
A B C D E F G H I J K L M 0 1 2 3 4 5 6 7 8 9 10 11 12 N O P Q R S T U V W X Y Z 13 14 15 16 17 18 19 20 21 22 23 24 25 加密函数:
(此处表述可能和严谨定义有出入,但是应该好理解一些,权衡好即可)
此处“y1”是你最终得到的单个密文字符的对应数值,我认为"a"和"b"是两个普通的加密参数,"x1"是你输入的单个明文字符的对应数值,“m”也是一个普通的加密参数,但它有两个特点:
m的数值即是你列出的“仿射表”的字符对数;
m与a互质(即“gcd(a, m)==1”。这在穷举法中很有用,能大大减小运算量)
解密函数:
此处“y2”是你最终得到的单个明文字符的对应数值,“
”是a关于m的乘法逆元,“x2”是你输入的单个密文字符的对应数值。
现在复现“fanfie”的解题过程:
首先拿到文件的时候我就感到不知所措:这是什么!???尝试了不少解密工具都没解出来。
最后去看别人的题解了,发现这道题少了两个关键信息:
明文被base32加密,然后以某种方式加密;
(分析后)存在部分密文对应base32加密后明文的开头一部分,这部分是“IJEVIU2DKRDA====”。
此处还有大佬通过对比“IJEVIU2DKRDA====”和文件内容(即密文)“MZYVMIWLGBL7CIJOGJQVOA3IN5BLYC3NHI” ,发现字符“I”总会被加密成字符“M”,并由此推断出此处的“以某种方式加密”指的是一种单字母替换加密,佩服!
这里我还发现大佬们一旦知道存在单字母替换加密,就会对明文中以及原本加密时可能用到的字符进行“编码”,相当于列“仿射表”(看来“仿射表”并非是仿射密码专属的......)。
这里我发现了此题大佬们列出来的仿射表不是常规的26对字符,而是32对!莫名其妙地多出了字符‘2’~‘7’的对应,这曾一度让我百思不得其解。但是发现异常就要去解决,不该把一切想做理所当然。于是我不断搜寻相关信息,直到我被某位大佬的关键提醒点醒,霎时醍醐灌顶:
base64 :由
0-9、a-z、A-Z、+、/
及后缀“=”组成(不包括”=“则有64种字符);base32: 由
A~Z 和 2~7
组成(32种字符)。
明文是被base32处理过的,处理过后的字符串中的任意一个字符只能是base32的32种字符中的其中一种。因此这32种字符都是“可能用到的字符”,所以列表会列出32种对应。(由于字符‘1’不包含在这32种字符中,不可能“用到”,所以表中没有这个)
现在有表了(此处大佬们发现了表中的一些对应关系,于是开始尝试用仿射密码来解密(可能性很大)),应该还有解密函数才行。但是我们并不知道a和b的值,所以目前这个函数还无法使用。
最后我认识到:可以先从加密函数切入,拿到a和b,然后再求出a关于m的乘法逆元,我们就能用解密函数了。
我们目前有了几组可用的明文字符和密文字符的对应,但是我本是想不到如何求a和b的。最后走投无路之下只能这样做,结果发现有奇效:
求解仿射密码加解密公式中的a和b值的思路与方法:
a既然和m互质,那么a的取值就只能是那几个(这些值都不大于m);
b最后加在a上让m取余,那如果b的数值等于m,那就相当于b是0;如果b的数值是(m+1),就就相当于b是1......总之,取余会限制b,让b的数值大小根本不必大于等于m,所以b的取值范围也很有限。
a和b的可能取值都很有限,于是穷举法是具备可行性的:我穷举a和b的各种组合,然后拿其中一组明密文字符的对应来防入加密公式,然后依次判断某一组a和b是否会让公式成立,然后筛选出符合公式的a和b的组合,然后再取一组明密文字符的对应,重复上述操作,直到最后的可能的a和b只剩下一组,那么到这里就是求出a和b的值了。
现在既然已经得到了a和b的值,那么可以用函数“gmpy2.invert()”求a关于m的乘法逆元,即“”。
至此,我们得到了完整的解密函数。
然后把密文拿出来解密即可。这里附上我写的能半自动求出a、b,然后据此解密出明文的python脚本:
# 首先,根据一对一单字母加密,预先知道完整密文和一小部分明文,则很可能是仿射密码
# 这个脚本拿来依据仿射密码加密公式穷举得出仿射密码加解密公式中的a与b(由于a和b的取值范围极有限,故可穷举),然后按照仿射密码解密公式由密文解出明文
# 首先要自己列出仿射表,明确模数m的值。比如:在题目“fanfie”中,仿射表有32列,故m=32。但是很多时候听说是26位,那就是m=26
'''
总结一下,要使用此脚本,应该先根据自己的情况调整映射表affine_table和re_affine_table(一般手动调整即可),
并且你得知道2~4组加密前后的对应字符来求a和b,
并且还得有待解密的密文(这句算是废话)
'''
from Crypto.Util.number import *
import gmpy2
import sys
affine_table = { # 这是32位的映射表,如果是其他位数的要自己调整(比如:如果是26位,就删掉'2'及其后面的键与值)
'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6, 'H': 7,
'I': 8, 'J': 9, 'K': 10, 'L': 11, 'M': 12, 'N': 13, 'O': 14, 'P': 15,
'Q': 16, 'R': 17, 'S': 18, 'T': 19, 'U': 20, 'V': 21, 'W': 22, 'X': 23,
'Y': 24, 'Z': 25, '2': 26, '3': 27, '4': 28, '5': 29, '6': 30, '7': 31
}
re_affine_table = { # 这是反过来后的仿射表,可以从数字转到字符,最终解明文的时候有用。如果是其他位数也需要自己调整
0: 'A', 1: 'B', 2: 'C', 3: 'D', 4: 'E', 5: 'F', 6: 'G', 7: 'H',
8: 'I', 9: 'J', 10: 'K', 11: 'L', 12: 'M', 13: 'N', 14: 'O', 15: 'P',
16: 'Q', 17: 'R', 18: 'S', 19: 'T', 20: 'U', 21: 'V', 22: 'W', 23: 'X',
24: 'Y', 25: 'Z', 26: '2', 27: '3', 28: '4', 29: '5', 30: '6', 31: '7'
}
# 从此处开始是在算a和b的值
run_num = 0 # 拿来记录做出的筛查运算次数
a = 0 # 这边先定义一下可以防止后面使用的时候出现不必要的报错
b = 0
# 第一次筛查运算
m = int(input("请输入模数m:"))
x = input("请输入加密前的字符:").upper() # 这里自动把你输入的字符处理成大写的,这才和字典对得上
y = input("请输入加密后的字符:").upper()
pos_list = []
for a in range(1, m): # 穷举a
if GCD(a, m) == 1: # 这是仿射密码的一个特征:a与m互质。根据这个可以筛掉一部分a
for b in range(1, m): # 穷举b
if (a * affine_table[x] + b) % m == affine_table[y]: # 将符合加密公式的a和b存到一个数组里
pos_list.append([a, b])
run_num += 1 # 更新一下筛查运算次数
print("第%d次筛查运算结果:" % run_num, pos_list)
# 之后的第n次筛查运算(这时候可能的a和b组合应该不止一个)
temp_list = pos_list # 这里赋初值只是为了能进到while循环里
while len(temp_list) > 1: # 反复筛查,直到a和b的组合只剩下1个或者0个
x = input("请再输入加密前的字符:").upper() # 再输入解密前后的一组字符
y = input("请再输入加密后的字符:").upper()
temp_list = [] # 这个是拿来暂存每一次筛除后的a和b组合的数组
for n in range(0, len(pos_list)): # 对现在的所有a和b做进一步筛查
if (pos_list[n][0] * affine_table[x] + pos_list[n][1]) % m == affine_table[y]: # 将符合加密公式的a和b存到一个数组里
temp_list.append(pos_list[n])
pos_list = temp_list # 将这一次筛除后的结果重新放到列表中(相当于把不符合条件的a和b组合给去掉了)
run_num += 1 # 更新一下筛查运算次数
print("第%d次筛查运算结果:" % run_num, pos_list)
if len(temp_list) == 1: # 如果反复处理后只剩下最后一组符合条件的a和b,那就算是求出了a和b的值
a = pos_list[0][0]
b = pos_list[0][1]
print("a与b的值已求出:a=%d, b=%d" % (a, b))
elif len(temp_list) == 0: # 如果处理过了反而发现所有的a和b组合都不符合,就直接结束程序
print("符合条件的a和b根本不存在!你是不是x、y或m输错了?")
sys.exit()
# 从此处开始是在求解密文为明文
print("接下来可以根据得到的a、b和密文来求明文:")
c = input("请输入你的密文:").upper() # 虽然我知道密文一般默认是大写的,但我还是
# 根据公式,应该先求a关于m的乘法逆元
re_a = gmpy2.invert(a, m) # 求乘法逆元
print("最终得到明文:", end='')
for n in c: # 将处理密文和输出明文合成一步来做
print(re_affine_table[re_a * (affine_table[n] - b) % m], end='')
附上一段运行过程:
最终做base32解密,即可得到flag。