[SECCON CTF 2022] crypto 部分复现

目录

janken vs kurenaif

pqpq 以前写过了,官方方法一样

witches_symmetric_exam

this is not lsb

insufficient CVP

BBB


当时仅pqpq差点完成,有个小错。其它都无从下手。终于看到wp一一试一下。原文

janken vs kurenaif

这个情况第一回见,题目给出一个种子,并通过种子生成666个[0,1,2]的随机数,来玩石头-剪刀-布,要求给出一个种子,生成的随机数都是对应的[1,2,0]。

python的随机数使用梅森旋转MT19937来生成,以前的题基本都是复现序列,通过已知的随机数部分来恢复state,从而得到与原题相同的伪随机数。这个题是在一个不满秩的情况下求种子。

import os
import signal
import random
import secrets

FLAG = os.getenv("FLAG", "fake{cast a special spell}")


def janken(a, b):
    return (a - b + 3) % 3


signal.alarm(1000)
print("kurenaif: Hi, I'm a crypto witch. Let's a spell battle with me.")

witch_spell = secrets.token_hex(16)
witch_rand = random.Random()
witch_rand.seed(int(witch_spell, 16))
print(f"kurenaif: My spell is {witch_spell}. What about your spell?")

your_spell = input("your spell: ")
your_random = random.Random()
your_random.seed(int(your_spell, 16))

for _ in range(666):
    witch_hand = witch_rand.randint(0, 2)
    your_hand = your_random.randint(0, 2)

    if janken(your_hand, witch_hand) != 1:
        print("kurenaif: Could you come here the day before yesterday?")
        quit()

print("kurenaif: Amazing! Your spell is very powerful!!")
print(f"kurenaif: OK. The flag is here. {FLAG}")

第一步就是先求 state

from pwn import *
import random


io = remote("janken-vs-kurenaif.seccon.games", 8080)
context.log_level = 'debug'

io.recvuntil(b"kurenaif: My spell is ")
u_spell = io.recvuntil(b'.', drop= True).decode()

print(f"{u_spell = }")
#u_spell = 'df74f6c5c096355a87ec544c1ac865e8'

u_rand = random.Random()
u_rand.seed(int(u_spell, 16))

u_hand = [u_rand.randint(0,2) for _ in range(666)]
i_hand = [(i+1)%3 for i in u_hand]

#solve prng
from Untwister import Untwister as _Untwister
import logging
logging.disable()

# Superclass Untwister to obtain PRNG state after seeding
# instead of PRNG state after outputs generated
class Untwister(_Untwister):
    def __init__(self):
        super().__init__()
        self.first_MT = self.MT

        # https://github.com/python/cpython/blob/main/Modules/_randommodule.c#L201
        # Index is 624 after seeding
        self.index = 624

    def get_random(self):
        # https://github.com/python/cpython/blob/main/Modules/_randommodule.c#L232
        # First MT state is set to 0x80000000 after seeding
        self.solver.add(self.first_MT[0] == 0x80000000)

        print(self.solver.check())
        model = self.solver.model()
        state = [
            model[x].as_long() if model[x] is not None else 0
            for x in self.first_MT
        ]

        result_state = (3, tuple(state+[624]), None)
        rand = random.Random()
        rand.setstate(result_state)
        return rand

ut = Untwister()
for me in i_hand:
    res = bin(me)[2:].zfill(2) + "?" * 30  #fill to 32bits
    ut.submit(res)
ut_rand = ut.get_random()
ut_rand_state = ut_rand.getstate()[1][:-1]

# sanity check
for me in i_hand:
    assert me == ut_rand.randint(0, 2)

这里边用到一个模块Untwister 可以到github下载,其实本身很短,直接放在这。

#https://raw.githubusercontent.com/icemonster/symbolic_mersenne_cracker/main/main.py
from z3 import *
from random import Random
from itertools import count
from time import time
import logging

logging.basicConfig(format='STT> %(message)s')
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

SYMBOLIC_COUNTER = count()

class Untwister:
    def __init__(self):
        name = next(SYMBOLIC_COUNTER)
        self.MT = [BitVec(f'MT_{i}_{name}', 32) for i in range(624)]
        self.index = 0
        self.solver = Solver()

    #This particular method was adapted from https://www.schutzwerk.com/en/43/posts/attacking_a_random_number_generator/
    def symbolic_untamper(self, solver, y):
        name = next(SYMBOLIC_COUNTER)

        y1 = BitVec(f'y1_{name}', 32)
        y2 = BitVec(f'y2_{name}' , 32)
        y3 = BitVec(f'y3_{name}', 32)
        y4 = BitVec(f'y4_{name}', 32)

        equations = [
            y2 == y1 ^ (LShR(y1, 11)),
            y3 == y2 ^ ((y2 << 7) & 0x9D2C5680),
            y4 == y3 ^ ((y3 << 15) & 0xEFC60000),
            y == y4 ^ (LShR(y4, 18))
        ]

        solver.add(equations)
        return y1

    def symbolic_twist(self, MT, n=624, upper_mask=0x80000000, lower_mask=0x7FFFFFFF, a=0x9908B0DF, m=397):
        '''
            This method models MT19937 function as a Z3 program
        '''
        MT = [i for i in MT] #Just a shallow copy of the state

        for i in range(n):
            x = (MT[i] & upper_mask) + (MT[(i+1) % n] & lower_mask)
            xA = LShR(x, 1)
            xB = If(x & 1 == 0, xA, xA ^ a) #Possible Z3 optimization here by declaring auxiliary symbolic variables
            MT[i] = MT[(i + m) % n] ^ xB

        return MT

    def get_symbolic(self, guess):
        name = next(SYMBOLIC_COUNTER)
        ERROR = 'Must pass a string like "?1100???1001000??0?100?10??10010" where ? represents an unknown bit'

        assert type(guess) == str, ERROR
        assert all(map(lambda x: x in '01?', guess)), ERROR
        assert len(guess) <= 32, "One 32-bit number at a time please"
        guess = guess.zfill(32)

        self.symbolic_guess = BitVec(f'symbolic_guess_{name}', 32)
        guess = guess[::-1]

        for i, bit in enumerate(guess):
            if bit != '?':
                self.solver.add(Extract(i, i, self.symbolic_guess) == bit)

        return self.symbolic_guess


    def submit(self, guess):
        '''
            You need 624 numbers to completely clone the state.
                You can input less than that though and this will give you the best guess for the state
        '''
        if self.index >= 624:
            name = next(SYMBOLIC_COUNTER)
            next_mt = self.symbolic_twist(self.MT)
            self.MT = [BitVec(f'MT_{i}_{name}', 32) for i in range(624)]
            for i in range(624):
                self.solver.add(self.MT[i] == next_mt[i])
            self.index = 0

        symbolic_guess = self.get_symbolic(guess)
        symbolic_guess = self.symbolic_untamper(self.solver, symbolic_guess)
        self.solver.add(self.MT[self.index] == symbolic_guess)
        self.index += 1

    def get_random(self):
        '''
            This will give you a random.Random() instance with the cloned state.
        '''
        logger.debug('Solving...')
        start = time()
        self.solver.check()
        model = self.solver.model()
        end = time()
        logger.debug(f'Solved! (in {round(end-start,3)}s)')

        #Compute best guess for state
        state = list(map(lambda x: model[x].as_long(), self.MT))
        result_state = (3, tuple(state+[self.index]), None)
        r = Random()
        r.setstate(result_state)
        return r

得到state后可以验证真能生成指定的序列。

第二步是根据这个state计算种子,这个用z3来实现,在复现过程中发现计算这东西不是每次都成功,有的数据可能会运算很久也不出结果。所以不行的时候可以重来换个spell。这块也是没有能力写的,只能原文复制下来。然后保存后用。

#2.get seed
from z3 import *

prng_N = 624

def u32(x):
    return x & 0xffffffff

# Note that LShR is used instead of ">>" operator
# unsigned 32 bit integers use logical bitshift, not arithmetic bitshift.

# https://github.com/python/cpython/blob/main/Modules/_randommodule.c#L186
def init_genrand(s):
    mt = [0 for _ in range(prng_N)]

    mt[0] = BitVecVal(s, 32)
    mti = 1
    while mti < prng_N:
        mt[mti] = u32(
            1812433253 * (mt[mti-1] ^ LShR(mt[mti-1], 30)) + mti
            # 1812433253 * (mt[mti-1] ^ (mt[mti-1] >> 30)) + mti
        )
        mti += 1
    return mt, mti

# https://github.com/python/cpython/blob/main/Modules/_randommodule.c#L209
def init_by_array(init_key):
    key_length = len(init_key)
    mt, mti = init_genrand(19650218)

    i, j = 1, 0
    k = prng_N if prng_N > key_length else key_length
    while k:
        mt[i] = u32(
            (mt[i] ^ ((mt[i-1] ^ LShR(mt[i-1], 30)) * 1664525)) + init_key[j] + j
        )
        i, j = i + 1, j + 1
        if i >= prng_N:
            mt[0] = mt[prng_N-1]
            i = 1
        if j >= key_length:
            j = 0
        k -= 1

    k = prng_N - 1
    while k:
        mt[i] = u32(
            (mt[i] ^ ((mt[i-1] ^ LShR(mt[i-1], 30)) * 1566083941)) - i
        )
        i += 1
        if i >= prng_N:
            mt[0] = mt[prng_N-1]
            i = 1
        k -= 1

    mt[0] = 0x80000000;

    return mt

seed_vars = [
    BitVec(f"seed_{i}", 32) for i in range(624)
]
seed_rand_state = init_by_array(seed_vars)

s = Solver()
for a, b in zip(seed_rand_state, ut_rand_state):
    s.add(a == b)
print('check:',s.check())

model = s.model()
print(f"{model = }")
seed_init_key = [
    model[x].as_long() if model[x] is not None else 0
    for x in seed_vars
]
print(f"{seed_init_key[:10] = }")

最后把所有计算得到的key连起来(每个32位,4字节,一个非常长的数)

#3,get my spell 
seed_rand_seed = sum([x * (2 ** (idx * 32))for idx, x in enumerate(seed_init_key)])
print("seed_rand_seed =", hex(seed_rand_seed)[2:52], "...")

seed_rand = random.Random()
seed_rand.seed(seed_rand_seed)

# sanity check
assert seed_rand.getstate()[1][:-1] == ut_rand_state
for me in i_hand:
    assert me == seed_rand.randint(0, 2)

print(f"I spell:{hex(seed_rand_seed)[2:]}")

io.sendline(f"{hex(seed_rand_seed)[2:]}".encode())
print(io.recvall().decode())

pqpq 以前写过了,官方方法一样

witches_symmetric_exam

题目很明了,题目先将secret_spell用AES_GCM加密,然后将tag,nonce与cipher连一起,再进行AES_OFB方式加密,返回iv+cipher。

后台提供解密功能,但仅返回错在哪。

from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
from flag import flag, secret_spell

key = get_random_bytes(16)
nonce = get_random_bytes(16)


def encrypt():
    data = secret_spell
    gcm_cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
    gcm_ciphertext, gcm_tag = gcm_cipher.encrypt_and_digest(data)  #CTR+消息验证码(?)

    ofb_input = pad(gcm_tag + gcm_cipher.nonce + gcm_ciphertext, 16)

    ofb_iv = get_random_bytes(16)
    ofb_cipher = AES.new(key, AES.MODE_OFB, iv=ofb_iv)  #OFB 
    ciphertext = ofb_cipher.encrypt(ofb_input)
    return ofb_iv + ciphertext


def decrypt(data):
    ofb_iv = data[:16]
    ofb_ciphertext = data[16:]
    ofb_cipher = AES.new(key, AES.MODE_OFB, iv=ofb_iv)

    try:
        m = ofb_cipher.decrypt(ofb_ciphertext)
        temp = unpad(m, 16)   #从最后一位padding
    except:
        return b"ofb error"

    try:
        gcm_tag = temp[:16]
        gcm_nonce = temp[16:32]
        gcm_ciphertext = temp[32:]
        gcm_cipher = AES.new(key, AES.MODE_GCM, nonce=gcm_nonce)

        plaintext = gcm_cipher.decrypt_and_verify(gcm_ciphertext, gcm_tag)
    except:
        return b"gcm error"

    if b"give me key" == plaintext:
        your_spell = input("ok, please say secret spell:").encode()
        if your_spell == secret_spell:
            return flag
        else:
            return b"Try Harder"

    return b"ok"


print(f"ciphertext: {encrypt().hex()}")
while True:
    c = input("ciphertext: ")
    print(decrypt(bytes.fromhex(c)))

这显然是个padding oracle的题,ofb和gcm都是通过与固定的流异或来得到密文。通过尾部爆破看解密状态得到加密用的流。

如果padding为1字节那么尾字节就是01,当用0-255爆破时,恰好没报错的那个数与1异或就是加密流的最后一个字节,依次向前两字节就是0202,直到16。

第一段我还是明白的,leak_enc用来爆破加密流,每次发送256种情况,然后从返回中取状态,这样可以加快爆破速度,每次recvuntil是非常费时的。

from pwn import *

context.log_level = "error"

r = remote("witches-symmetric-exam.seccon.games", 8080)
context.log_level = 'debug'

r.recvuntil(b"ciphertext: ")
enc = bytes.fromhex(r.recvline().decode())
print(f"{len(enc)  = }")
print(f"{enc.hex() = }")

#get ofb stream
def bxor(*args):
    if len(args) == 1:
        return args[0]
    a, b = args[0], args[1:]
    return bytes([i ^ j for i, j in zip(a, bxor(*b))])

def query(xs):
    r.sendline("\n".join([x.hex() for x in xs]).encode())
    ress = []
    for _ in xs:
        r.recvuntil(b"ciphertext: ")
        res = r.recv(5).strip().decode()
        if "b'ofb" in res:
            ress.append("pad")
        elif "b'gcm" in res:
            ress.append("gcm")
        elif "o', p" in res:
            ress.append("spell")
        elif "b'ok'" in res:
            ress.append("ok")
        else:
            raise ValueError(res)
    return ress

def leak_enc(pln):
    enc = [0 for _ in range(16)]

    for pos in range(16):
        print(".", end="", flush=True)
        expected_padding = ([0] * 16 + [pos+1] * (pos+1))[-16:]

        found = []
        probe = [0] * 16

        xs = []
        for i in range(256):
            probe[15-pos] = i
            try_block_ofb = pln + bxor(enc, expected_padding, probe)
            xs.append(try_block_ofb)
        #每次发送256个,然后再取返回信息,加快爆破速度
        ress = query(xs)
        for i, res in enumerate(ress):
            if res == "gcm":
                found.append(i)
        if len(found) != 1:
            raise ValueError("???", pos, found)

        enc[15-pos] = found[0]
    print()

    return bytes(enc)

然后把4段的加密流得到,异或得到gcm的tag,nonce,cipher_gcm

#get ofb
from Crypto.Util.Padding import unpad

ofb_iv = enc[:16]
ofb_enc = enc[16:]

print(f"{ofb_iv.hex()     = }")

ofb_stream = ofb_iv
for block in range(4):
    ofb_stream += leak_enc(ofb_stream[-16:])
ofb_stream = ofb_stream[16:]

print(f"{ofb_stream.hex() = }")

ofb_dec = bxor(ofb_enc, ofb_stream)
gcm_tag, gcm_nonce, gcm_enc = ofb_dec[:16], ofb_dec[16:32], unpad(ofb_dec[32:], 16)

print(f"{gcm_tag.hex()    = }")
print(f"{gcm_nonce.hex()  = }")
print(f"{gcm_enc.hex()    = }")

第三步是爆破 gcm的流,这块目前还不大明白,慢慢来,GCM需要计算tag,tag是通过h0和密文通过_ghash_clmul方法运算得到,除了这个ofb,gcm基本上一样。

#GCM parameters
from Crypto.Util.number import long_to_bytes, bytes_to_long
from Crypto.Cipher._mode_gcm import _GHASH as GHASH, _ghash_clmul as ghash_c

# https://github.com/Legrandin/pycryptodome/blob/master/lib/Crypto/Cipher/_mode_gcm.py#L223-L226
h0 = leak_enc(b"\x00" * 16)
print(f"{h0.hex() = }")

# https://github.com/Legrandin/pycryptodome/blob/master/lib/Crypto/Cipher/_mode_gcm.py#L232-L236
nonce = gcm_nonce
fill = (16 - (len(nonce) % 16)) % 16 + 8
ghash_in = (nonce + b'\x00' * fill + long_to_bytes(8 * len(nonce), 8))
j0 = GHASH(h0, ghash_c).update(ghash_in).digest()
print(f"{j0.hex() = }")

#Decrypt GCM
# https://github.com/Legrandin/pycryptodome/blob/master/lib/Crypto/Cipher/_mode_gcm.py#L239-L245
# nonce_ctr = j0[:12]
# iv_ctr = (bytes_to_long(j0) + 1) & 0xFFFFFFFF
# nonce_iv_bytes = nonce_ctr + bytes.fromhex(hex(iv_ctr)[2:].zfill(8))
block_one_ctr = bytes.fromhex(hex(int(j0.hex(), 16) + 1)[2:].zfill(32))
block_two_ctr = bytes.fromhex(hex(int(j0.hex(), 16) + 2)[2:].zfill(32))

gcm_stream = leak_enc(block_one_ctr) + leak_enc(block_two_ctr)
gcm_pln = bxor(gcm_stream, gcm_enc)
secret_spell = gcm_pln
print(f"{secret_spell = }")

得到secret_spell就可以包装gcm,ofb包了,发送即返回flag

#3, encode 'give me key'

from Crypto.Util.Padding import pad

signer = GHASH(h0, ghash_c)
token_dec = b"give me key"

# https://github.com/Legrandin/pycryptodome/blob/master/lib/Crypto/Cipher/_mode_gcm.py#L372-L379
# > encrypt and digest > encrypt
token_enc = bxor(token_dec, gcm_stream)
msg_len = len(token_enc)
# signer.update(token_enc) # defer to later for block alignment

# https://github.com/Legrandin/pycryptodome/blob/master/lib/Crypto/Cipher/_mode_gcm.py#L459-L462
# > encrypt and digest > digest
signer.update(token_enc + b"\x00" * (16 - msg_len))
signer.update(long_to_bytes(8 * 0, 8) + long_to_bytes(8 * msg_len, 8))
tag_dec = signer.digest()

# https://github.com/Legrandin/pycryptodome/blob/master/lib/Crypto/Cipher/_mode_gcm.py#L465
# > encrypt and digest > digest > self._tag_cipher.encrypt
j0_stream = leak_enc(j0)
tag_enc = bxor(tag_dec, j0_stream)

print(f"{tag_enc.hex() = }")

payload = ofb_iv + bxor(pad(tag_enc + gcm_nonce + token_enc, 16), ofb_stream)
print(f"{payload.hex() = }")

r.sendline(payload.hex().encode())
r.sendline(secret_spell)
flag = r.recvuntil(b"}").decode().strip()
print(flag)

#SECCON{you_solved_this!?I_g1ve_y0u_symmetr1c_cipher_mage_certificate}

this is not lsb

LSB都整来,对于c = m^e mod n 来说,不断的将m乘2(c*2^e mod n)判断末位是0还是1,以确定2m与n的关系。这个题有点类似,返回的不是末位而是第1位是不是0xff

from Crypto.Util.number import *
#from flag import flag
flag = bytes_to_long(b'A'*55)
p = getStrongPrime(512)
q = getStrongPrime(512)
print(p,q)
e = 65537
n = p * q
phi = (p - 1) * (q - 1)

d = pow(e, -1, phi)

print(f"n = {n}")
print(f"e = {e}")
print(f"flag_length = {flag.bit_length()}")

# Oops! encrypt without padding!
c = pow(flag, e, n)
print(f"c = {c}")

# padding format: 0b0011111111........
def check_padding(c):
    padding_pos = n.bit_length() - 2
    m = pow(c, d, n)

    return (m >> (padding_pos - 8)) == 0xFF


while True:
    c = int(input("c = "))
    print(check_padding(c))

 看了WP,感觉啊,还可以这样。方法依然是二分法。

先确定flag的上下边界(已知flag以SECCON{开头,后补全0和全1),然后确定一个除法的最小值:int('00'+'1'*8+'0'*(b.bit_length()-10),2),然后每发送 dec//flag_md(上下边界的中间值)的e 次幂再乘c ,当返回是0xff时则说明flag在上半部(比一半大)。修改边界循环即可。

from pwn import *

context.log_level = "error"

# ====== Get parameters ======

r = remote("this-is-not-lsb.seccon.games", 8080)
r.recvuntil(b"n = ")
n = int(r.recvline().strip().decode())
r.recvuntil(b"e = ")
e = int(r.recvline().strip().decode())
r.recvuntil(b"flag_length = ")
flag_length = int(r.recvline().strip().decode())
r.recvuntil(b"c = ")
c = int(r.recvline().strip().decode())

print(f"{n           = }")
print(f"{e           = }")
print(f"{flag_length = }")
print(f"{c           = }")

def query(k):
    y = (pow(k, e, n) * c) % n
    r.recvuntil(b"c = ")
    r.sendline(f"{y}".encode())
    res = r.recvline().decode().strip()
    if res == "True":
        return True
    elif res == "False":
        return False
    else:
        raise ValueError(res)

dec_lb = int("0011111111".ljust(n.bit_length(), "0"), 2)
dec_ub = int("0011111111".ljust(n.bit_length(), "1"), 2)
flag_lb = int((b"SECCON{"+ b"\x00" * 48).hex(), 16)
flag_ub = int((b"SECCON{"+ b"\xff" * 48).hex(), 16)

# ====== Binary search ======

while flag_ub - flag_lb > 10:
    flag_md = (flag_lb + flag_ub) // 2
    coef = dec_lb // flag_md
    if query(coef):
        print("-", end="", flush=True)
        flag_lb = flag_md
    else:
        print("+", end="", flush=True)
        flag_ub = flag_md
print()
flag = bytes.fromhex(hex(flag_lb)[2:])
print(f"{flag = }")

insufficient CVP

又是一个看着大概认识又不会的题。求系数用格基约减。但这里差一点,就是有两个未知的情况。

from random import randint
from Crypto.Util.number import getPrime, bytes_to_long
from secret import FLAG


# f(x,y,z) = a1*x + a2*x^2 + a3*x^3
#          + b1*y + b2*y^2 + b3*y^3
#          + c*z + s mod p
def calc_f(coeffs, x, y, z, p):
    ret = 0
    ret += x * coeffs[0] + pow(x, 2, p) * coeffs[1] + pow(x, 3, p)*coeffs[2]
    ret += y * coeffs[3] + pow(y, 2, p) * coeffs[4] + pow(y, 3, p)*coeffs[5]
    ret += z * coeffs[6]
    ret += coeffs[7]

    return ret % p


p = getPrime(512)


# [a1, a2, a3, b1, b2, b3, c, s]
coeffs = [randint(0, 2**128) for _ in range(8)]

key = 0
for coeff in coeffs:
    key <<= 128
    key ^= coeff

cipher_text = bytes_to_long(FLAG) ^ key
print(cipher_text)

shares = []
for _ in range(4):
    x = randint(0, p)
    y = randint(0, p)
    z = randint(0, 2**128)

    w = calc_f(coeffs, x, y, z, p)
    packed_share = ((x,y), w)
    shares.append(packed_share)

print(p)
print(shares)

这里4个x,y已经给出,但z没给,假设没有6,7两个系数的情况下用

B = \begin{bmatrix} x_{0} & x_{1} & ... & x_{3} & 1 & & & \\ x_{0}^{2} & x_{1}^{2} & ... & x_{3}^{2} & & 1 & & \\ ... & ... & ... & ... & & & ... & \\ y_{0}^{3} & y_{1}^{3} & ... & y_{3}^{3} & & & & 1\\ -w_{0} & -w_{1} & ... & -w_{3} & & & & \end{bmatrix}

这里多的c7,c7,z都只有128位,与已知的x,y,p的512比很小,这里先将其忽略求出近似值,将右侧的单位对角阵乘2^128,在左上与w之间补一个对角阵p,并在最后加一列写一个极大值,这样可以用CVP算法求得前6个参数*2^128的挖值,再除去2^128即可得到前6个参数。

#read msg
f = open('output.txt')
cipher_text = int(f.readline())
p = int(f.readline())
shares = eval(f.readline())

'''
    | A  B  O |    A= x0,x1,... y0^3,y1^3,...
M = | C  O  O |    B= I*2^128    C= I*p
    | D  E  L |    D= -w0,-w1..  L= large
                   E= O,  对M进行格基约减后,得到的前6个参数
'''
r, c = 11, 11
two_127 = 2**127
two_128 = 2**128
large = 2**1024

A = matrix(ZZ, 6,4)
B = identity_matrix(ZZ,6)*2^128
C = identity_matrix(ZZ,4)*p 
D = matrix(ZZ, 1,4)
L = matrix(ZZ, 1,1, [large])
for i in range(4):
    xi,yi = shares[i][0]
    A[0,i],A[1,i],A[2,i] = xi, xi**2, xi**3
    A[3,i],A[4,i],A[5,i] = yi, yi**2, yi**3  
    D[0,i] = -shares[i][1]

M = block_matrix(3, [[A,B,zero_matrix(ZZ,6,1)],
                     [C,zero_matrix(ZZ,4,6),zero_matrix(ZZ,4,1)],
                     [D,zero_matrix(ZZ,1,6),L]])
                     
vec = vector(ZZ,11)
for i in range(6):
    vec[i] = two_127*two_128
vec[10] = large 

# https://jgeralnik.github.io/writeups/2021/08/12/Lattices/
def closest_vector(B, target):
    # Babai's Nearest Plane algorithm
    M = B.LLL()
    G = M.gram_schmidt()[0]
    small = target
    for _ in range(1):
        for i in reversed(range(M.nrows())):
            c = ((small * G[i]) / (G[i] * G[i])).round()
            small -= M[i] * c
    return target - small

cv = closest_vector(M, vec)
assert cv[10] == large

for idx, cvi in enumerate(cv):
    print(f"cv[{idx}] = {cvi}")

coeffs = [i//two_128 for i in cv[4:10]]
czs = [int((w - (x * coeffs[0] + pow(x, 2, p) * coeffs[1] + pow(x, 3, p)*coeffs[2]
           + y * coeffs[3] + pow(y, 2, p) * coeffs[4] + pow(y, 3, p)*coeffs[5]))%p) for (x,y),w in shares]  #模值需要转整

c6 = gcd(czs[1]-czs[0], czs[2]-czs[0])
c7 = czs[0]%c6 

coeffs += [c6,c7]

key = 0
for coeff in coeffs:
    key <<= 128
    key ^^= coeff
flag = cipher_text ^^ key
flag = bytes.fromhex(hex(flag)[2:])

#SECCON{Unfortunately_I_could_not_come_up_with_a_more_difficult_problem_than_last_year_sorry...-6fc18307d3ed2e7673a249abc2e0e22c}

这里在求czs的时候没有用WP里的方法,而是直接用的式子,在sage里由于作了取模,所以czs[1]-czs[0]会自动取模得到模p的值,从而无法用gcd,所以在取模后需要用int对数字进行转换,转成数字型再运算。看别人几句话就写出来,自己一练才发现功夫还着得远。

BBB

最后一个BBB就相对简单点了,远端是个RNG,a,p已知,b可以输入,那这个RNG就可以控制了,但运算次数不定,在10-100之间。要求输入5个种子,用生成的值作为e,并且每次都生成n,并对flag加密后返回。

from Crypto.Util.number import bytes_to_long, getPrime
from random import randint
from math import gcd
from secret import FLAG
from os import urandom


assert len(FLAG) < 100


def generate_key(rng, seed):
    e = rng(seed)
    while True:
        for _ in range(randint(10,100)):
            e = rng(e)
        p = getPrime(1024)
        q = getPrime(1024)
        phi = (p-1)*(q-1)
        if gcd(e, phi) == 1:
            break

    n = p*q
    return (n, e)


def generate_params():
    p = getPrime(1024)
    a = randint(0, p-1)

    return (p,a)


def main():
    p,a = generate_params()
    print("[+] The parameters of RNG:")
    print(f"{a=}")
    print(f"{p=}")
    b = int(input("[+] Inject [b]ackdoor!!: "))
    rng = lambda x: (x**2 + a*x + b) % p

    keys = []
    seeds = []
    for i in range(5):
        seed = int(input("[+] Please input seed: "))
        seed %= p
        if seed in seeds:
            print("[!] Same seeds are not allowed!!")
            exit()
        seeds.append(seed)
        n, e = generate_key(rng, seed)
        if e <= 10:
            print("[!] `e` is so small!!")
            exit()

        keys.append((n,e))

    flag = bytes_to_long(FLAG + urandom(16))
    for n,e in keys:
        c = pow(flag, e, n)
        print("[+] Public Key:")
        print(f"{n=}")
        print(f"{e=}")
        print("[+] Cipher Text:", c)


if __name__ == "__main__":
    main()

思路也简单,首先就是这个题n肯定是没法分解,给5组值也就是用中国剩余定理了,这就要求e要相同,当然e必需相同,不然rng会出来不确定的值。

首先通过输入b来确定e,也就是让f(x)==x 由于有要求e必需大于10所以这里e选11,先求得b

e == e^{2} + a*e + b (mod p)

这样不管运行多少次都能保证结果是11也就能确定e

然后要求输入5组x那就要算出 f(x1) == e,f(x2)== x1,...

因为前边有10次运算,并且大概率这个运算会超过20次,所以空间很大,不行就重爆破。

当然这里因为有x^2所以每步都可能有两个解,凑够4个即可。

最后将得到的c,n用 crt得到m再开11次方。

from pwn import *

io = remote('BBB.seccon.games', 8080)
context.log_level = 'debug'

io.recvuntil(b'a=')
a = int(io.recvline().strip())

io.recvuntil(b'p=')
p = int(io.recvline().strip())

#rng(11) = 11^2 + a*11 + b mod p
P.<x> = PolynomialRing(Zmod(p), 'x')
e = 11
eq = e^2 + a*e + x - e
b = eq.roots()[0][0]
print(f'{b=}')

#rng(a1) = a2 ;rng(a2) = a3 ; ... rng(a4) = 11; rng(11) = 11
ra = [e]
while len(ra)<5:
    eq = x^2 + a*x + b - ra[-1]
    d = eq.roots()
    for j,_ in d:
        if j not in ra:
            ra.append(j)
            print(f"{j = }")

io.sendlineafter(b'[+] Inject [b]ackdoor!!:', str(b).encode())
for i in range(5):
    io.sendlineafter(b"[+] Please input seed: ", str(ra[i]).encode())

n_a ,e_a ,c_a = [], [], []
for i in range(5):
    io.recvuntil(b'n=')
    n_a.append(int(io.recvline().strip()))
    io.recvuntil(b'[+] Cipher Text: ')
    c_a.append(int(io.recvline().strip()))

f11 = crt(c_a,n_a)
m = f11.nth_root(11)
flag = bytes.fromhex(hex(m)[2:])
print(flag)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值