MeepwnCTF2018-Still old school

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 曾经去一家公司参观考察,前台的招待人员是一个年轻漂亮的小姑娘,明眸皓齿、唇色红艳,站在那里笑盈盈地迎着我们一行人,...
    Amay梅阅读 326评论 0 3
  • 重阳节就这样悄无声息过去了,我原来并不觉得它可以跟春节,中秋节,端午节这样大众的节日相媲美,对它的印象只有小时候学...
    刘取丹昕赵汗情阅读 467评论 0 1
  • 一个女人脱着一副沉重的棺材走在漫天的星空中,每一步都很小心,他对着身边的儿子说:“跟进妈妈的脚步,不要被主神那个...
    小想子阅读 991评论 0 0