翻譯自:http://blog.redrocket.club/2018/07/18/meepwn-quals-2018-StillOldSchool/
題目給了服務器的腳本,和一個服務。
腳本如下:
from secret import flag, mask1, mask2
import string
import random
import sys
import os
import signal
import hashlib
from Crypto.Cipher import AES
menu = """
CHOOSE 1 OPTION
1. Encrypt message
2. Decrypt message
3. Get encrypted flag
4. Exit\n
"""
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
bs = 16
def to_string(num, max_len = 128):
tmp = bin(num).lstrip('0b')[-max_len:].rjust(max_len, '0')
return "".join(chr(int(tmp[i:i+8], 2)) for i in range(0, max_len, 8))
def pad(s):
padnum = bs - len(s) % bs
return s + padnum * chr(padnum)
def unpad(s):
return s[:-ord(s[-1])]
def gen_key(mask):
tmp1 = random.random()
tmp2 = random.random()
key = int(tmp1 * 2**128) | int(tmp2 * 2**75) | (mask & 0x3fffff)
key = to_string(key)
return key
def encrypt_msg(msg, key1, key2):
iv = to_string(random.getrandbits(128))
aes1 = AES.new(key1, AES.MODE_CBC, iv)
aes2 = AES.new(key2, AES.MODE_CBC, iv)
enc = aes1.encrypt(aes2.encrypt(pad(msg)))
return (iv + enc).encode("hex")
def proof_of_work():
"""
This function has very special purpose
:)) Simply to screw you up
"""
prefix = to_string(random.getrandbits(64), 64)
print 'prefix = {}'.format(prefix.encode('hex'))
challenge = raw_input('> ')
tmp = hashlib.sha256(prefix + challenge).hexdigest()
if tmp.startswith('00000'):
return True
else:
return False
key1 = gen_key(mask1)
key2 = gen_key(mask2)
signal.alarm(300)
if not proof_of_work():
exit(0)
for _ in range(256):
print menu
try:
choice = int(raw_input("> "))
except:
print "wrong option"
exit(-1)
if choice == 1:
msg = raw_input("give me a string: ")
print encrypt_msg(msg, key1, key2)
elif choice == 2:
print "Not implement yet..."
elif choice == 3:
print encrypt_msg(flag, key1, key2)
elif choice == 4:
exit(-1)
else:
print "wrong option"
exit(-1)
通過這個服務我們可以得到加密過的flag,或者讓其加密我們輸入的內容,服務 會用不同的key做兩次AES加密:
def encrypt_msg(msg, key1, key2):
iv = to_string(random.getrandbits(128))
aes1 = AES.new(key1, AES.MODE_CBC, iv)
aes2 = AES.new(key2, AES.MODE_CBC, iv)
enc = aes1.encrypt(aes2.encrypt(pad(msg)))
return (iv + enc).encode("hex")
加密用的key通過如下的隨機算法生成:
def gen_key(mask):
tmp1 = random.random()
tmp2 = random.random()
key = int(tmp1 * 2**128) | int(tmp2 * 2**75) | (mask & 0x3fffff)
key = to_string(key)
return key
每個key都包含一個未知的mask,mask最長為22bit
python 的random.random函數并不是一個密碼學安全的隨機數生成器(RNG),一旦我們能夠獲得足夠的輸出,我們可以還原出tmp1,tmp2。
同時我們可以注意到AES CBC模式使用的iv唄提供給了我們,而且iv是使用相同的RNG生成的:
iv = to_string(random.getrandbits(128))
當我們還原出tmp1,tmp2之后,可以通過爆破的方式得到對應的mask。
梅森旋轉算法(Mersenne Twister)
通過print語句,我們可以知道該腳本是用python2編寫的。CPython2.7使用的是梅森旋轉算法偽隨機數發生器( Mersenne Twister Pseudo Random Number Generator)。https://en.wikipedia.org/wiki/Mersenne_Twister
該算法依賴于624個內部狀態(每個狀態為int32值),這些內部狀態遵循如下的關系:
即每個狀態會依賴于之前的三個數字。
當生成當前輸出的隨機數時,會進行一定的數學變換來滿足一些性質:
[...]
y = mt[self->index++];
y ^= (y >> 11);
y ^= (y << 7) & 0x9d2c5680UL;
y ^= (y << 15) & 0xefc60000UL;
y ^= (y >> 18);
return y;
如果我們想要恢復tmp1,需要:
- 請求156個隨機的iv(生成一個iv需要4個int32)
- 根據輸出的隨機數恢復內部狀態 Yi
- 計算在生成key時用到的內部狀態Yi-N+1
- 重現gen_key函數中生成的隨機數
但還存在一些問題,在更新內部狀態的過程中,之前狀態的最低bit位丟失了。因此在根據后面的內部狀態倒推之前的狀態的過程中,我們會得到兩個可能的結果。
再仔細看random.random函數的實現,需要兩個32bit的內部狀態:
static PyObject * random_random(RandomObject *self)
{
unsigned long a=genrand_int32(self)>>5, b=genrand_int32(self)>>6;
return PyFloat_FromDouble((a*67108864.0+b)*(1.0/9007199254740992.0));
}
又由于我們需要得到兩個key,因此一共有 (22)2=16種可能。
通過如下的代碼計算可能的內部狀態:
def inv(x):
x ^= (x >> 18)
# Lowest 16 bit stay how they are, so we can just repeat...
x ^= (x << 15) & 0xEFC60000
# Do it step by step
x ^= (x << 7) & 0x1680
x ^= (x << 7) & 0xC4000
x ^= (x << 7) & 0xD200000
x ^= (x << 7) & 0x90000000
# Only highest 11 bits are untouched
x ^= (x >> 11) & 0xFFC00000
# Do step by step again
x ^= (x >> 11) & 0x3FF800
x ^= (x >> 11) & 0x7FF
return x
def recover_state(i, outputs32):
"""
return all possible candidates for state how it was (i-624) iterations ago!
"""
Y = inv(outputs32[i - 1])
h_1 = Y ^ inv(outputs32[i - 227 - 1])
Y_old = inv(outputs32[i])
h_1_msb = ((Y_old ^ inv(outputs32[i - 227]))>>30) & 1
h_2 = h_1
h_2_alt = h_1 ^ 0x9908B0DF
# even case
h_2 = (h_2 << 1) & 0x7fffffff
# odd case
h_2_alt = ((h_2_alt << 1)|1) & 0x7fffffff
# Add the missing highest bit (recovered from successive output)
h_2 = (h_1_msb<<31)|h_2
h_2_alt = (h_1_msb<<31)|h_2_alt
candidates = [h_2, h_2_alt]
return candidates
再窮舉16種可能的情況,推導random.random計算出的16個可能的浮點數:
def float_magic(a, b):
"""
Rebuild of random_rancom from randommodule.c
uses two outsputs!
"""
a = a >> 5
b = b >> 6
return (a*67108864.0+b)*(1.0/9007199254740992.0)
def floats_for_cands(a_cs, b_cs):
"""
Applies float_magic to all candidate combinations
"""
floats = []
for a_c in a_cs:
for b_c in b_cs:
floats.append(float_magic(a_c, b_c))
return floats
還需要注意,在key_gen和得到第一個iv見的proof of work也用到了2個內在狀態。
當我們逆向推導出所有的16種可能后,我們還需要找出key對應的mask。
如果我們采用純爆破的方法,一個mask最多有22bit長,計算量為
222 * 222=244
顯然計算量過大。
一個替代做法是:
- 發送一個任意的明文M給服務器,存下對應的密文C
- 用所有可能的 16 * 222 個key2 解密密文C得到 D
- 用哈希表存儲所有 的key2i 和對應的 Di
- 用所有可能的16 * 222個key1 加密明文M得到E
- 在哈希表中查找和Ei相同的Di,即能確定最終的key1,key2以及mask1,mask2
通過這種方法,總的計算量為 2*16 * 222=227 在可行范圍內??梢允褂枚嗑€程在多核機器上運行,加快窮舉速度。