TCTF/0CTF决赛2020 writeup

2020年的0CTF/TCTF Finals总决赛,居然没有web题,只能做做crypto了。

本writeup包含Oblivious、MEF。

Oblivious

题目描述nc chall.0ops.sjtu.edu.cn 10002

题目

import os
from hashlib import sha256
import SocketServer
from random import seed,randint,choice
from Crypto.Util.number import getStrongPrime, inverse
from flag import flag
import hashlib
import string

BITS = 2048
assert flag.startswith('flag{') and flag.endswith('}')
assert len(flag) < BITS/8
padding = os.urandom(BITS/8-len(flag))
flagnum = int((flag+padding).encode('hex'), 16)

class Task(SocketServer.BaseRequestHandler):
    def pow(self):
        res = "".join([choice(string.ascii_letters) for i in range(20)])
        self.request.sendall("md5(??????+%s).startswith('000000')" % (res))
        pre = self.recvn(6)
        return hashlib.md5(pre+res).hexdigest().startswith("000000")

    def genkey(self):
        '''
        NOTICE: In remote server this key is generated like below but hardcoded, since genkey is time/resource consuming
        and I don't want to add annoying PoW, especially for a final event.
        This function is kept for your local testing.
        '''
        p = getStrongPrime(BITS/2)
        q = getStrongPrime(BITS/2)
        self.p = p
        self.q = q
        self.n = p*q
        self.e = 0x10001
        self.d = inverse(self.e, (p-1)*(q-1))

    def genmsg(self):
        '''
        simply xor looks not safe enough. what if we mix adjacent columns?
        '''
        m0 = randint(1, self.n-1)
        m0r = (((m0&1)<<(BITS-1)) | (m0>>1))
        m1 = m0^m0r^flagnum
        return m0, m1

    def recvn(self, sz):
        '''
        add a loop in recv to avoid truncation by network issues
        '''
        r = sz
        res = ''
        while r>0:
            res += self.request.recv(r)
            if res.endswith('\n'):
                r = 0
            else:
                r = sz - len(res)
        res = res.strip()
        return res

    def handle(self):
        seed(os.urandom(0x20))
        if not self.pow():
            self.request.close()
            return
        self.genkey()
        self.request.sendall("n = %d\ne = %d\n" % (self.n, self.e))
        try:
            while True:
                self.request.sendall("--------\n")
                m0, m1 = self.genmsg()
                x0 = randint(1, self.n-1)
                x1 = randint(1, self.n-1)
                self.request.sendall("x0 = %d\nx1 = %d\n" % (x0, x1))
                v = int(self.recvn(BITS/3))
                k0 = pow(v^x0, self.d, self.n)
                k1 = pow(v^x1, self.d, self.n)
                self.request.sendall("m0p = %d\nm1p = %d\n" % (m0^k0, m1^k1))
        finally:
            self.request.close()

class ForkedServer(SocketServer.ForkingTCPServer, SocketServer.TCPServer):
    pass

if __name__ == "__main__":
    HOST, PORT = '0.0.0.0', 10002
    server = ForkedServer((HOST, PORT), Task)
    server.allow_reuse_address = True
    server.serve_forever()

题目主要逻辑是先随机生成一个m0,然后经过位移生成m0r,计算m1=m0^m0r^flagnum,然后再随机生成x0x1,告诉你e,n,x0,x1。要求输入一个数v,通过公钥d分别加密v^x0v^x1得到k0k1,计算m0p=m0^k0m1p=m1^k0,返回m0pm1p的值。

因为我们可以控制输入v,当v的值为x0时,v^x0就等于0,公钥加密后的k0也等于0,m0p=m0^0就得到了随机数m0的值。

随机数种子是seed(os.urandom(0x20)),生成后就不再改变,os.urandom(0x20)是通过系统的/dev/urandom获得的,无法预测。

但三个随机数m0,x0,x1都是通过random.randint(1,n-1)生成的,且可以获取到的,所以有机会预测出下一个随机数。

python中的random库是使用Mersenne Twister梅森旋转来生成伪随机数,只要获得前624个,就可以预测之后的随机数。

在Github上搜索到两个库Python-random-module-crackermersenne-twister-predictor,可以用来预测python的伪随机数。

首先尝试的是Python-random-module-cracker库,虽然主页上的示例写着可以预测randint,但实际测试发现只能预测randint(0,4294967294)

接着尝试mersenne-twister-predictor库,主页上这些可以预测getrandbits没有写可以预测randint.

我翻了python的random库,发现实际上randint(a,b)函数返回值的生成过程,是调用randrange(a,b+1)函数。而randrange(a,b+1)函数是先获取宽度width=b+1-a,然后当宽度大于最大宽度时,调用a+_randbelow(width)函数。而_randbelow(width)函数,首先计算k = _int(1.00001 + _log(n-1, 2.0)),相当于计算width转化成二进制位有多少位,然后在调用getrandbits(k)

整个函数调用链条看下来,randint实际上还是在用getrandbits。而题目给的n是2048位,Python-random-module-cracker库的预测getrandbits,可以支持k为32的倍数,也就是说2048位也是可以的。

先本地测试一下Python-random-module-cracker库预测randint(1,n-1)的可行性,就是将randint(1,n-1)按照生成规则还原回调用getrandbits(2048)时实际产生的随机数。

先用pip install mersenne-twister-predictor安装好这个库,测试代码如下

import random, time
import os

BITS = 2048

n = 19537672993921510910953800210784804463906011801348944134382259677098515591468020354186917058659291508782207012207322759124661039955163907358060182234684997838303129553612091765074441858018987479764884871179221087985572587060253497705505070405152688906445392906317500619032806029443372743631700328868047923922273766615053104519261361069287938437682793053653603535934093590530631032737414606160770584158459833468735707963661279153502660376802573242852076645275762942169376811866451825822378845156284080472507828885812988167574335939801962133577967403542809570426652088681810875263525518970234197229449528868110799345007

from mt19937predictor import MT19937Predictor

sum = 0
for _ in range(1000):
    random.seed(os.urandom(0x20))

    k=2048

    predictor = MT19937Predictor()
    for _ in range(624):
        # x = random.getrandbits(k)
        x = random.randint(1,n-1)-1
        predictor.setrandbits(x, k)

    # assert random.getrandbits(32) == predictor.getrandbits(32)
    ran = random.randint(1,n-1)-1
    pre = predictor.getrandbits(k)
    # print('rand:'+str(ran))
    # print('pred:'+str(pre))
    if ran==pre:
        sum+=1
print(sum)

实际预测准确率大概为1%,但总归还是可行了。

所以整个解题思路就是,输入前624/3=208次v为x0的值,得到m0,x0,x1这三个随机数,也就是前624个随机数,然后传入mersenne-twister-predictor库,预测出下一个m0值。此时再输入v为x1的值,则得到的k1为0,得到的m1p=m1^k1=m1=m0^m0r^flagnum,将预测的m0位移得到m0r,则flagnum=m1p^m0^m0r,再转换为字符串即可得到flag。

完整解题脚本如下

from pwn import *
import time
import hashlib
import string
import os
from Crypto.Util.number import long_to_bytes

BITS = 2048
k = BITS
n = 19537672993921510910953800210784804463906011801348944134382259677098515591468020354186917058659291508782207012207322759124661039955163907358060182234684997838303129553612091765074441858018987479764884871179221087985572587060253497705505070405152688906445392906317500619032806029443372743631700328868047923922273766615053104519261361069287938437682793053653603535934093590530631032737414606160770584158459833468735707963661279153502660376802573242852076645275762942169376811866451825822378845156284080472507828885812988167574335939801962133577967403542809570426652088681810875263525518970234197229449528868110799345007
e = 65537

from mt19937predictor import MT19937Predictor

def md5(candidate):
    return hashlib.md5(str(candidate).encode('ascii')).hexdigest()

def md5pow(suffix):
    for i in string.ascii_letters:
        for j in string.ascii_letters:
            for k in string.ascii_letters:
                for l in string.ascii_letters:
                    for m in string.ascii_letters:
                        for n in string.ascii_letters:
                            if (md5(i+j+k+l+m+n+suffix)[:6] == '000000'):
                                return i+j+k+l+m+n

for _ in range(512):
    predictor = MT19937Predictor()
    sh = remote("chall.0ops.sjtu.edu.cn",10002)

    # md5(??????+TNIdqVwjSqAmJdanUPIm).startswith('000000')
    has = str(sh.recvuntil("startswith('000000')"),encoding='ascii').strip()
    
    print(has)

    suffix = has[11:31]

    md5p = md5pow(suffix)
    sh.sendline(md5p)
    n = str(sh.recvline(),encoding='ascii').strip()
    e = str(sh.recvline(),encoding='ascii').strip()
    
    for i in range(int(624/3)):
        sh.recvline()
        x0 = str(sh.recvline(),encoding='ascii').strip()
        x1 = str(sh.recvline(),encoding='ascii').strip()

        x0 = int(x0[5:])
        x1 = int(x1[5:])
        v = x0
        sh.sendline(str(v))

        m0p = str(sh.recvline(),encoding='ascii').strip()
        m1p = str(sh.recvline(),encoding='ascii').strip()

        m0 = int(m0p[6:])

        predictor.setrandbits(x0-1, k)
        predictor.setrandbits(x1-1, k)
        predictor.setrandbits(m0-1, k)

    pre = predictor.getrandbits(k)+1
    m0 = pre
    m0r = (((m0&1)<<(BITS-1)) | (m0>>1))

    sh.recvline()
    x0 = str(sh.recvline(),encoding='ascii').strip()
    x1 = str(sh.recvline(),encoding='ascii').strip()

    x0 = int(x0[5:])
    x1 = int(x1[5:])
    v = x1
    sh.sendline(str(v))

    pre = predictor.getrandbits(k)+1
    if pre == x0:
        print('x0 predict')
    pre = predictor.getrandbits(k)+1
    if pre == x1:
        print('x1 predict')

    m0p = str(sh.recvline(),encoding='ascii').strip()
    m1p = str(sh.recvline(),encoding='ascii').strip()

    m1p = int(m1p[6:])

    flagnum = m1p^m0^m0r
    flag = long_to_bytes(flagnum)
    print(flag)

    sh.close()
    time.sleep(1)
    print(_)

跑到大约第80轮,得到flag为flag{Hav3_YoU_reCongn1z3D_tHAt_I_m_uS1Ng_pypy_0n_sErvEr},可惜是赛后才跑出来的。

有不用预测随机数的做法,过两天看了其他人的writeup再总结一下。

看到了其他做法
https://cr0wn.uk/2020/0ctf-oblivious/
https://github.com/Septyem/My-Public-CTF-Challenges/tree/master/0ctf-tctf-2020-final/Oblivious

MEF

题目如下

flag = '[REDACTED]'
flagnum = int(flag.encode('hex'),16)
h = [1]
p = 374144419156711147060143317175368453031918731002211
m = 16077898348258654514826613220527141251832530996721392570130087971041029999399
assert flagnum < m

def listhash(l):
    fmt = "{} "*len(l)
    s = fmt.format(*l)
    return reduce(lambda x,y:x*y,map(hash,s.split(' ')))

num = 0x142857142857142857
for i in range(num):
    x = h[i]*p%m
    x += h[listhash(h)%len(h)]
    x %= m
    h.append(x)

encflag = h[num] ^ flagnum
print encflag

# encflag = 11804007143439251849628349629375460277798651136608332038133488180610375813979

题目主要逻辑是生成一个长度为0x142857142857142857的数组,生成的方法是第0位为1,第i位是h[i-1]*p%m,然后加上i位前的某一位,然后再对m取模,最后一个数与flagnum异或。

至于加上哪一位,需要通过listhash(h)函数计算。实际计算发现,计算前100位,该函数返回的结果每次都是0,所以实际加上的都是第0位,也就是加1。因为s.split(' ')是以空格分隔,而生成s的时候最后加上了一个空格,所以最后一位就是哈希一个空值hash(),结果为0。再进入reduce相乘,结果必为0。

整体算下来,h就是

h=[1,(p+1)%m, (((p+1)%m+1)*p)%m, (((((p+1)%m+1)*p)%m)*p+1)%m...]
# 化简
h=[1%m,(p+1)%m,(p**2+p+1)%m,(p**3+p**2+p+1)%m...]

根据取模运算规则,取模后乘法和加法都可以转为先乘先加最后取模。

最后一个可以用求和公式,符合等比数列求和公式

\[1+p^1+p^2+p^3+...+p^{n-1}+p^n=\sum_{i=0}^{n}{p^i}=\frac{p^{n+1}-1}{p-1} \]

然后再计算取模,根据取模运算规则,取模后除法不能转为先除再取模,所以应该用逆元运算,前提是模数与除数互质,通过factordb.com可知模数m是质数,则一定互质。可以使用以下公式

\[(a/b)\%m=((a\%(b*m))/b \]

\[\sum_{i=0}^{n}{p^i}\%m=((p^{n+1}-1)\%((p-1)*m))/(p-1) \]

最后解密脚本为

from Crypto.Util.number import long_to_bytes

p = 374144419156711147060143317175368453031918731002211
m = 16077898348258654514826613220527141251832530996721392570130087971041029999399
output = 11804007143439251849628349629375460277798651136608332038133488180610375813979

num = 0x142857142857142857

# num = 9999
# print(((p**(num+1)-1)/(p-1))%m)
# print( (p**(num+1)-1) % ((p-1)*m) ) / (p-1)
x = (pow(p, num+1, (p-1)*m)) / (p-1)

print( x )
flagnum = output^x
print( flagnum )
print( long_to_bytes( flagnum ) )
# flag{not_(mem)_hard_at_alllll}

最终成绩

最终成绩

posted @ 2020-09-27 23:50  Ashen。  阅读(1176)  评论(0编辑  收藏  举报