分享picoCTF中一道数据加解密题目
分享picoCTF中一道数据加解密题目
最近开始刷 picoCTF 2024,给自己定了个小目标:不论难易每天解出1题,如果查看提示或别人的答案,则不算数,并且还要加罚1题。
picoCTF比较适合新手入门,题目难度是循序渐进的,picoCTF2024也不例外,前几道题都比较简单,没什么好说的。今天分享一下第15题,这道题不算太难但是有点意思。该题的解出人数是前15题里最少的,但是点赞率(Liked)并不低,说明它得到了CTFer们的认可,有兴趣的同学可以自己先试着解一下。
1. 题目
这道题是关于数据加解密的,题中给出了密文数据(enc_flag)以及加密代码(custom_encryption.py),CTFer需要通过编写解密代码来获取Flag,分值100分。
2. 分析
首先阅读加密代码,可以得出核心的加密过程为:
- 输入为一串明文和一个text_key,其中text_key在代码中硬编码为"trudeau"
- 通过一系列的运算得到一个整数型的shared_key
- 通过dynamic_xor_encrypt函数用text_key将原始明文加密成一串半密文
- 通过encrypt函数用shared_key将半密文加密成最终密文
上述加密过程可以描述为下图:
容易想到,逆转上述过程即可实现解密,过程如下图所示:
其中,decrypt函数为encrypt函数的逆操作,dynamic_xor_decrypt函数为dynamic_xor_encrypt函数的逆操作,text_key和shared_key既可用于加密也可用于解密(对称密钥)。
3. 解答
根据上节的分析可知,解密有4个关键要素:
- text_key:为已知
- shared_key:需破解
- decrypt函数:可根据encrypt函数实现
- dynamic_xor_decrypt函数:可根据dynamic_xor_encrypt函数实现
破解shared_key
在test函数中,shared_key是通过一系列的计算得到的整数,其中用到两个中间随机数a和b。在enc_flag文件中记录了a和b,因此只需要在源码中对a和b直接赋值,运行代码即可获得shared_key = 12。
def test(plain_text, text_key):
p = 97
g = 31
if not is_prime(p) and not is_prime(g):
print("Enter prime numbers")
return
a = randint(p-10, p)
b = randint(g-10, g)
print(f"a = {a}")
print(f"b = {b}")
u = generator(g, a, p)
v = generator(g, b, p)
key = generator(v, a, p)
b_key = generator(u, b, p)
shared_key = None
if key == b_key:
shared_key = key
else:
print("Invalid key")
return
......
实现decrypt函数
encrypt函数是将输入字符串的每个字符转换成unicode编码后乘以key*311,将该数学运算逆向后很容易得到decrypt函数。
def encrypt(plaintext, key):
cipher = []
for char in plaintext:
cipher.append(((ord(char) * key*311)))
return cipher
def decrypt(cipher, key):
plain = ""
for code in cipher:
plain += chr(int(code/311/key))
return plain
实现dynamic_xor_decrypt函数
dynamic_xor_encrypt函数是将输入字符串中每个字符的编码与text_key字符串中对应字符的编码进行异或运算,再转换成新的字符。因异或运算本身是可逆的(若A^k=A',则A=A'^k),推测该函数可以直接用于解密。
def dynamic_xor_encrypt(plaintext, text_key):
cipher_text = ""
key_length = len(text_key)
for i, char in enumerate(plaintext[::-1]):
key_char = text_key[i % key_length]
encrypted_char = chr(ord(char) ^ ord(key_char))
cipher_text += encrypted_char
return cipher_text
def dynamic_xor_decrypt(cipher, text_key):
return dynamic_xor_encrypt(cipher, text_key)
获取Flag
综合以上,用Python写出解密代码如下:
if __name__ == "__main__":
# 待解密的密文,来自enc_flag文件
cipher = [33588, 276168, ..., 63444]
# 解密成半密文
shared_key = 12
semi_cipher = decrypt(cipher, shared_key)
# 解密成明文
text_key = "trudeau"
plain = dynamic_xor_decrypt(semi_cipher, text_key)
# 打印明文
print(f"plain is: {plain}")
运行上述Python代码得到一串字符:
plain is: e~r~TAFntdbczmJs#re%pa!uNsv4,.$"/h
WTF,怎么回事?picoCTF的Flag应该是picoCTF{......}
这种形式的,很明显答案不正确。
4. 修正
那么问题出在哪里呢?仔细分析发现,dynamic_xor_encrypt函数在进行异或运算前先对明文做了一次反转操作(plaintext[::-1]),关键代码如下。当直接用该函数进行解密时,会出现异或运算所用的key_char与加密时的不对应,解密结果当然不正确。
for i, char in enumerate(plaintext[::-1]):
key_char = text_key[i % key_length]
encrypted_char = chr(ord(char) ^ ord(key_char))
......
为了便于理解这个过程,我重新画了一个加密过程的图,图中虚线箭头的方向为执行异或运算的顺序:
为保证加解密的异或运算所用的key_char保持一致,dynamic_xor_decrypt函数需要做些调整:在异或运算前不要对半密文(c'b'a')做反转操作(cipher[::]),而在异或运算完成后对明文做一次反转操作(plain[::-1])。
def dynamic_xor_decrypt(cipher, text_key):
plain = ""
key_length = len(text_key)
for i, char in enumerate(cipher[::]):
key_char = text_key[i % key_length]
decrypted_char = chr(ord(char) ^ ord(key_char))
plain += decrypted_char
return plain[::-1]
修正dynamic_xor_decrypt函数后再次执行解密,便可得到正确答案:
plain is: picoCTF{custom_d2cr0pt6d_dc499538}
这算是题目的一个小坑,容易被忽视。
5. 总结
picoCTF比较适合新手入门,题目难度是循序渐进的,本文分享了picoCTF-2024中一道数据加解密相关的问题,这道题不算太难,但是容易踩坑,值得CTFer动手做一下。
转载自:https://juejin.cn/post/7375532797383393289