Passwords as Keys

题目描述

在对称加密算法当中,采用随机字节作为密钥而不是其他的可以预测的数据是非常重要的。这个随机密钥应该借助密码学上安全的伪随机数生成算法来实现。如果密钥在一定程度上是可以预测的,那么这个密码的安全程度,就会大打折扣。

但是如果密钥只是在形式上看起来是随机比特,并不代表他就是真正的随机比特。在本题目当中密钥由一些简单的口令的哈希值生成,那么这使得密码是可以被攻击的。

题目代码

from Crypto.Cipher import AES
import hashlib
import random


# /usr/share/dict/words from
# https://gist.githubusercontent.com/wchargin/8927565/raw/d9783627c731268fb2935a731a618aa8e95cf465/words
with open("/usr/share/dict/words") as f:
words = [w.strip() for w in f.readlines()]
keyword = random.choice(words)

KEY = hashlib.md5(keyword.encode()).digest()
FLAG = ?


@chal.route('/passwords_as_keys/decrypt/<ciphertext>/<password_hash>/')
def decrypt(ciphertext, password_hash):
ciphertext = bytes.fromhex(ciphertext)
key = bytes.fromhex(password_hash)

cipher = AES.new(key, AES.MODE_ECB)
try:
decrypted = cipher.decrypt(ciphertext)
except ValueError as e:
return {"error": str(e)}

return {"plaintext": decrypted.hex()}


@chal.route('/passwords_as_keys/encrypt_flag/')
def encrypt_flag():
cipher = AES.new(KEY, AES.MODE_ECB)
encrypted = cipher.encrypt(FLAG.encode())

return {"ciphertext": encrypted.hex()}

解题代码

import requests
from Crypto.Cipher import AES
import hashlib
# 获取密钥字典
url="https://gist.githubusercontent.com/wchargin/8927565/raw/d9783627c731268fb2935a731a618aa8e95cf465/words"
r=requests.get(url)
words=r.text.split("\n")

ciphertext=bytes.fromhex("c92b7734070205bdf6c0087a751466ec13ae15e6f1bcdd3f3a535ec0f4bbae66")
for i in words:
key = hashlib.md5(i.encode()).digest() #只有这样才能保住key是byte,才能进行解密
cipher = AES.new(key, AES.MODE_ECB)
plaintext=cipher.decrypt(ciphertext)
if b'crypto' in plaintext:
print(plaintext)
break
#b'crypto{k3y5__r__n07__p455w0rdz?}'

ECB Oracle

题目描述

ECB是一种简单的加密方式,每个明文块都是完全独立被加密的,在本题当中,你的输入会被拼接到flag的前边然后作为一个整体进行加密。我们没有提供解密功能,可能在ECB oracle的前提下,你不需要解密。

题目代码

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad


KEY = ?
FLAG = ?


@chal.route('/ecb_oracle/encrypt/<plaintext>/')
def encrypt(plaintext):
plaintext = bytes.fromhex(plaintext)

padded = pad(plaintext + FLAG.encode(), 16)
cipher = AES.new(KEY, AES.MODE_ECB)
try:
encrypted = cipher.encrypt(padded)
except ValueError as e:
return {"error": str(e)}

return {"ciphertext": encrypted.hex()}

解题思路

ECB加密模式

padding 模式

加密时,将明文分为16个字节的明文块,对于不满16字节的明文块进行填充,如果缺少一个字节则填充一个0x01,如果缺少两个字节则将后两个字节填充为0x02,依次类推。可别注意,如果最后一个明文块恰巧为16个字节,那么需要将最后16个字节填充为0x10,我们根据这个特性,可以对ECB加密模式进行攻击。

解决思路

1.推测flag的长度。

a="aa"
def get_flaglen():
print("Try to get the length of flag:")
for i in range(1,16):
urls=url+a*i
r=requests.get(urls)
print(len(r.text[15:-3])) #打印密文的长度
if i==1:
t=len(r.text[15:-3])
if "ciphertext" in r.text:
#print(t,len(r.text[15:-3]))
if len(r.text[15:-3])>t:
print(f"The length of flag is {(64-i*2)//2}")
return (64-i*2)//2

我们逐字节对明文块进行填充,发现当输入字节数小于等6的时候,密文长度均为64bit,也就是说输入长度+flag长度小于等于两个块的长度,也就是小于等于32个字节。当我们输入7个字节的时候,密文长度变化为96,相较于之前增加了一个密文块的长度,说明此时输入+flag的长度刚好为两个块的长度,也就是32个字节,(由于填充规则,算法填充了最后一个块),推测flag的长度为25个字节。

2.推测前十五个字节

我们用a表示输入的字节,p表示已经确定的明文的字节,?表示待确定的明文字节。 当我们输入15个字节时,第一个要加密的明文块,我们可以表示为:

aaaaaaaaaaaaaaa?

我们此时可以得到,整个信息加密的密文,而密文当中的前64bit,对应的就是该明文块的加密结果。

此时,我们可以不断调整最后一个字节,也可说是对最后一个字节进行爆破。我们不断构造:

aaaaaaaaaaaaaaax

对此进行加密,然后观察密文输出的前64bit,直到得到与先前密文前64bit相同的时候停止,那么此刻aaaaaaaaaaaaaaax=aaaaaaaaaaaaaaa?,也就确定了flag第一位的值。

同理,我们输入14个字节,此时第一个加密的明文块:

aaaaaaaaaaaaaap? (第一字节已知)

我们按照同样的方法,爆破明文块的最后一个字节,这时可以得到第二个字节的值,以此类推,我们可以借助第一个明文块得到flag前15个字节。

3.推测后续字节

在第二步当中,我们已经得到了flag的前15个字节,但是我们剩余10个未知字节。 此时我们需要借助第二个明文块。我们构造输入:

aaaaaaaaaaaaaaaa ????????????????

我们输入16个字节,对上述明文块进行加密,保存密文32~64bit的加密结果,也就是对应第二个明文块的密文。

aaaaaaaaaaaaaaaa pppppppppppppppx

然后我们输入对上边的内容进行加密,并且不断调整最后一个字节x,使得此加密的结果32~64bit的内容,与之前保存的密文相同。此时

aaaaaaaaaaaaaaaa pppppppppppppppx=aaaaaaaaaaaaaaaa ????????????????

那么我们就得到了flag第十六x的值。 按照步骤2中的方法不断调整,我们可以得到后续flag字节。

解题代码

import requests
from tqdm import tqdm
url="https://aes.cryptohack.org/ecb_oracle/encrypt/"
a="aa"
t=0
def get_flaglen():
print("Try to get the length of flag:")
for i in range(1,16):
urls=url+a*i
r=requests.get(urls)
print(len(r.text[15:-3]))
if i==1:
t=len(r.text[15:-3])
if "ciphertext" in r.text:
#print(t,len(r.text[15:-3]))
if len(r.text[15:-3])>t:
print(f"The length of flag is {(64-i*2)//2}")
return (64-i*2)//2

def get_flag(lens):
flag = "crypto{p3n6u1n5"
flag_hex = "63727970746f7b70336e3675316e35"
#flag=""
#flag_hex=""
if lens<=15:
for i in range(0,15):
urls = url + a * (15-i)
r = requests.get(urls)
target = r.text[15:-3][0:32]
for _ in tqdm(range(94,128)):
x=str(hex(_))[2:]
urls=url+a*(15-i)+flag_hex+x
#print(urls)
rs=requests.get(urls)
if(rs.text[15:-3][0:32]==target):
print(chr(_))
flag+=chr(_)
flag_hex+=str(hex(_)[2:])
break
else:
flag="crypto{p3n6u1n5"
flag_hex="63727970746f7b70336e3675316e35"
for i in (range(0,lens-15)):
urls=url+a*(16-i)
r = requests.get(urls)
target = r.text[15:-3][32:64]
for _ in tqdm(range(32,128)):
x = str(hex(_))[2:]
urls = url + a * (16 - i) + flag_hex + x
rs = requests.get(urls)
if(rs.text[15:-3][32:64]==target):
print(chr(_))
flag+=chr(_)
print(flag)
flag_hex+=str(hex(_)[2:])
break
return flag

flen=get_flaglen()
print(get_flag(flen)) #crypto{p3n6u1n5
#crypto{p3n6u1n5_h473_3cb}

#此代码运行较慢。。。。