翻译自: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 在可行范围内。可以使用多线程在多核机器上运行,加快穷举速度。