这是近期参加HTB夺旗战时遇到的一道难度为简单的密码学Crypto题目。但是我觉得挺有意思,就做下记录。
1. 题目:
题干没有太多的内容,就是一段python程序,和一个output的加密结果,如下。
Python:
import os
flag = open("flag.txt", "rb").read()
def genkeys(n):
keys = [os.urandom(5) for _ in range(n)]
return keys
def encrypt(keys, flag):
for key in keys:
flag = bytes([a ^ b for a, b in zip(flag, key * (len(flag)+5//5))])
print(fl)
return flag
keys = genkeys(1955) # with this many keys, this is totally secure
print(encrypt(keys, flag).hex())
Output.txt:
cf7bfbb7e9b770ceffd2f51c8da0e1fe70d7ffbee370cda4bcf470d4f8e3fe70daf8ffd8448ab5b8d84989bed2b370cafde1eb56e6b8bcea4ae6a1b9e44788a2beb810c4
有兴趣的朋友可以自己先做一做,flag的固定格式是(只是格式,flag长度未知):HTB{f4k3_fl4g_f0r_t3st1ng}
2. 通读程序:
首先通读python程序,简单易懂。
第一步,从flag.txt里读取flag原文。
第二步,genkeys( )是一个利用urandom每次生成5个随机字符串作为key的函数。程序中 n = 1955,表示 keys 为一个拥有1955组 key 的 list, 而每组都有5个key。 (这里如果难以理解可以自己打印一下keys,或者将n改为一个较小值)
第三步,encrypt( )是一个利用XOR生成的keys对flag原文进行加密的函数。原理是将flag的每位不断和5位key做XOR运算进行加密,直到做完1955组随机生成的key。
注意: 程序中的 key * (len(flag)+5//5)) 这一步只是为了确保 key的长度永远比flag长,每一位flag都能与keys加密。所做的操作就是不断重复5位key。
例如:生成的5位key为 abcde,在经历 key * (len(flag)+5//5))之后, key 变为 abcdeabcdeabcde....
总结,这就是对flag进行加密的源程序。我们的目标就是将output.txt里的密文根据python加密程序逆向求出原文。
3. 解题思路
这题的核心思路就是他的加密算法 XOR。有两个核心的算法基础:
3.1 核心一:
XOR运算符为 ,Flag的原文是 ,加密的1955组密钥为:。加密的运算过程可以表示为:
看起来 Flag经历了如此多的XOR运算,几乎无法进行还原。事实上,XOR具有结合律的特性,即:
因此,加密过程可简化为:
整个加密过程可以看为,所有的key先做XOR运算,得出一个结果后,再与flag(P)进行一次XOR运算。因此,看起来flag和key做了1955次XOR运算。实际上如果把所有key的XOR运算看成一个整体,作为一个终极无敌key,flag将只会和这个终极无敌key进行 一次 XOR运算。
3.2 核心二:
XOR运算的逆运算是它本身,也就是说两次异或同一个数最后结果不变,即 :
由核心一可知,密文(C)可以由明文(P)与终极无敌 Key 做一次 XOR得到,即(1):
可得到:
整理一下思路,现在我们有 flag 的密文,以及HTB{ } 几位明文。我们可以通过已知的几位明文(HTB{ })和密文的对应关系求出密钥(一共五位)即(2)。然后才能解密密文(3)。
密文一共是136位,由于密文用bytes( ) 和hex( ) 被转换为十六进制,两位十六进制对应一位ASCII码,所以相对应的ASCII码明文为68位。假设68位明文全部为Flag的内容,我们会发现一个问题,根据 HTB{ 四位明文字符和他们的密文,我们可以通过XOR运算得出前四位的密钥(key),而 } 是第68位,对应的是第三位密钥而不是第五位密钥。因此我们只能得出四位密钥。
但是4位总比1位没有好吧,走一步看一步。
我们先把四位密钥算出来。
4. 操作
4.1 将16进制密文转化为10进制
因为过程涉及16进制和ASCII码还有XOR的运算,我选择使用人类熟悉的十进制,当然直接用16进制也是可以的。这一步,是将output中每一位对应的16进制分开(并转换成10进制)。
out = "cf7bfbb7e9b770ceffd2f51c8da0e1fe70d7ffbee370cda4bcf470d4f8e3fe70daf8ffd8448ab5b8d84989bed2b370cafde1eb56e6b8bcea4ae6a1b9e44788a2beb810c4"
d = []
for i in range(1,136,2):
h = out[i-1]+out[i]
d.append (int(h, base=16))
print(d)
---------------------------------------------------------------------------------
Result:
[207, 123, 251, 183, 233, 183, 112, 206, 255, 210, 245, 28, 141, 160, 225, 254, 112, 215, 255, 190, 227, 112, 205, 164, 188, 244, 112, 212, 248, 227, 254, 112, 218, 248, 255, 216, 68, 138, 181, 184, 216, 73, 137, 190, 210, 179, 112, 202, 253, 225, 235, 86, 230, 184, 188, 234, 74, 230, 161, 185, 228, 71, 136, 162, 190, 184, 16, 196]
这里输出的结果是10进制的密文,每一个元素对应明文的一个字符。例如:明文里的 H 对应 207,T 对应 123,B 对应 251,{ 对应 183,} 对应 196。
4.2. 计算超级无敌Key
将明文 的每一位 和 对应的密文的每一位做 XOR 运算,将得到超级无敌Key的前四位。
flag = [72, 84, 66, 123, 125] ## HTB{}的十进制ASCII码
out = [207, 123, 251, 183, 196] ## HTB{}的十进制对应密文
FakeSuperkey = [a ^ b for a, b in zip(flag,out)]
print(FakeSuperkey)
---------------------------------------------------------------------------------
Result:
[135, 47, 185, 204, 185]
我们注意到 超级无敌Key中有两个185,原因也很简单,因为 明文 B 和 明文 } 都是用超级无敌Key中的第三位进行加密的。
现在我们有了四位 超级无敌Key,按理来说可以还原 的明文了。说不定可以从还原的明文中找到规律,得到最后一位Key。
4.3. 还原个明文
我们假设超级无敌Key的最后一位和第三位一模一样,即都为185。先假设的超级无敌Key为步骤二中的 [135, 47, 185, 204, 185],但你我都清楚,这个第五位是假的,那么还原的时候明文中所有是5的倍数为的字符都是假的。
但是,我们就是要这么干!
out = [207, 123, 251, 183, 233, 183, 112, 206, 255, 210, 245, 28, 141, 160, 225, 254, 112, 215, 255, 190, 227, 112, 205, 164, 188, 244, 112, 212, 248, 227, 254, 112, 218, 248, 255, 216, 68, 138, 181, 184, 216, 73, 137, 190, 210, 179, 112, 202, 253, 225, 235, 86, 230, 184, 188, 234, 74, 230, 161, 185, 228, 71, 136, 162, 190, 184, 16, 196]
## 68 位密文
FakeSuperkey = [135, 47, 185, 204, 185, 135, 47, 185, 204, 185, 135, 47, 185, 204, 185, 135, 47, 185, 204, 185, 135, 47, 185, 204, 185, 135, 47, 185, 204, 185, 135, 47, 185, 204, 185, 135, 47, 185, 204, 185, 135, 47, 185, 204, 185, 135, 47, 185, 204, 185, 135, 47, 185, 204, 185, 135, 47, 185, 204, 185, 135, 47, 185, 204, 185, 135, 47, 185, 204, 185, 135, 47, 185, 204, 185, 135, 47, 185, 204, 185, 135, 47, 185, 204, 185, 135, 47, 185, 204, 185]
## 五位密钥被key * (len(flag)+5//5))处理过后将变得比密文长,长多少无所谓,因为会被zip掉。
out = [a ^ b for a, b in zip(out,FakeSuperkey)]
ass = ""
j = 0
for item in out:
j += 1
if j % 5 == 0:
ass += '*'
else:
ass += chr(item)
print(ass)
---------------------------------------------------------------------------------
Result:
HTB{*0_w3*r34l*y_n3*d_th*s_m4*y_c4*_k3y*_f0r*4_s1*ly_t*me_m*ch1n*??}
芜湖!个flag以及拿到手了。观察一下旗子的明文,发现是用数字替换单词字幕的惯用手法。比如 i 替换为 1。而最后三个单词很有辨识度,是 silly time machine 的变形。我们只需要算出任意一个 * 号处的 Key 即可。
5. 夺旗!
这里我们使用time中的 i,在明文中应该是 1 (ASCII 码为 49),数出他在密文中对应的位置 (188)。因此我们现在使用 HTB{1 作为明文 (72, 84, 66, 123 ,49是他们的ASCII码)以及他们对应的十进制密文 (207, 123, 251, 183, 188),来制作真正的终极无敌Key。
flag = [72, 84, 66, 123 ,49]
out = [207, 123, 251, 183, 188]
RealSuperKey = [a ^ b for a, b in zip(flag,out)]
print(RealSuperKey)
---------------------------------------------------------------------------------
Result:
[135, 47, 185, 204, 141]
拿到终极无敌Key的我双手热得发烫!!!
将终极无敌Key的五个密钥复制成比flag长后,开始最终揭秘!!!!!
out = [207, 123, 251, 183, 233, 183, 112, 206, 255, 210, 245, 28, 141, 160, 225, 254, 112, 215, 255, 190, 227, 112, 205, 164, 188, 244, 112, 212, 248, 227, 254, 112, 218, 248, 255, 216, 68, 138, 181, 184, 216, 73, 137, 190, 210, 179, 112, 202, 253, 225, 235, 86, 230, 184, 188, 234, 74, 230, 161, 185, 228, 71, 136, 162, 190, 184, 16, 196]
key = [135, 47, 185, 204, 141, 135, 47, 185, 204, 141, 135, 47, 185, 204, 141, 135, 47, 185, 204, 141, 135, 47, 185, 204, 141, 135, 47, 185, 204, 141, 135, 47, 185, 204, 141, 135, 47, 185, 204, 141, 135, 47, 185, 204, 141, 135, 47, 185, 204, 141, 135, 47, 185, 204, 141, 135, 47, 185, 204, 141, 135, 47, 185, 204, 141, 135, 47, 185, 204, 141, 135, 47, 185, 204, 141, 135, 47, 185, 204, 141, 135, 47, 185, 204, 141, 135, 47, 185, 204, 141]
out = [a ^ b for a, b in zip(out,key)]
ass = ""
for item in out:
ass += chr(item)
print(ass)
---------------------------------------------------------------------------------
Result:
HTB{d0_w3_r34lly_n33d_th1s_m4ny_c4r_k3y5_f0r_4_s1lly_t1me_m4ch1n3??}
6. 结语:
Marty McFly: Do we really need this many car keys for a silly time machine??
Marty McFly: No, we don't.
以上。