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}

最終成績

最終成績


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM