Cryptoverse CTF 2023 WriteUps

發表於
分類於 CTF

This article is automatically translated by LLM, so the translation may be inaccurate or incomplete. If you find any mistake, please let me know.
You can find the original article here .

This time, I solved a few problems in the nyahello team, mainly including some relatively difficult problems in crypto.

Crypto

LFSR Explorer

from Crypto.Util.number import *
from secret import flag

assert flag.startswith("cvctf{")
assert flag.endswith("}")

flag = flag[6:-1].encode()
assert len(flag) == 8

def explore(state, mask):
    curr = (state << 1) & 0xffffffff
    i = state & mask & 0xffffffff
    last = 0
    while i != 0:
        last ^= (i & 1)
        i >>= 1
    curr ^= last
    return (curr, last)

states = [bytes_to_long(flag[4:]), bytes_to_long(flag[:4])]
mask = 0b10000100010010001000100000010101

output = []
for i in range(8):
    tmp = 0
    for j in range(8):
        (states[i // 4], out) = explore(states[i // 4], mask)
        tmp = (tmp << 1) ^ out
    output.append(tmp)

with open("output.txt", "wb") as f:
    f.write(bytes(output))

There are two LFSRs, just reverse them directly.

with open("output.txt", "rb") as f:
    ct = f.read()


def rev(state, mask):
    curr = state >> 1
    b = state & 1
    state >>= 1
    while mask:
        if mask & 1:
            b ^= state & 1
        state >>= 1
        mask >>= 1
    return curr | (b << 31)


mask = 0b10000100010010001000100000010101
s0 = int.from_bytes(ct[:4], "big")
s1 = int.from_bytes(ct[4:], "big")
for _ in range(32):
    s0 = rev(s0, mask)
    s1 = rev(s1, mask)
print(s1.to_bytes(4, "big") + s0.to_bytes(4, "big"))
# cvctf{G@@d_j0b}

Knapsack vs. Backpack

from Crypto.Util.number import *
from math import gcd
from secret import flag
import random

NBITS = 32

print("=== Knapsack vs. Backpack ===")

class Knapsack:
    def __init__(self, nbits):
        W, P = [], []
        for _ in range(nbits):
            W.append(random.randint(1, 10))
            P.append(random.randint(1, 100))
        
        self.W, self.P = W, P

    def fill(self, nbits):
        r = getRandomNBitInteger(nbits)
        pt = [int(i) for i in bin(r)[2:].zfill(nbits)]
        self.A = sum([x * y for x, y in zip(pt, self.W)])
        self.B = sum([x * y for x, y in zip(pt, self.P)])

try:
    for _ in range(10):
        challenge1 = Knapsack(NBITS*4)
        challenge1.fill(NBITS*4)
        print(challenge1.W)
        print(challenge1.P)
        print(f"Knapsack Capacity: {challenge1.A}")

        inp = list(map(int, input("Items: ").strip().split()))
        for i in inp:
            if i < 0 or i >= len(challenge1.W):
                print("Nope.")
                exit(1)
        if len(inp) != len(set(inp)):
            print("Nope.")
            exit(1)
        weight = sum([challenge1.W[i] for i in inp])
        profit = sum([challenge1.P[i] for i in inp])
        if weight <= challenge1.A and profit >= challenge1.B:
            print("Correct!")
        else:
            print("Nope.")
            exit(1)
except:
    exit(1)

print(flag[:len(flag)//2])

class Backpack:
    def __init__(self, nbits):
        r = [42]
        for _ in range(nbits - 1):
            r.append(random.randint(2*r[-1], 4*r[-1]))
        
        B = random.randint(3*r[-1] + 1, 4*r[-1])
        A = random.randint(2*r[-1] + 1, B - 1)
        while gcd(A, B) != 1:
            A = random.randint(2*r[-1] + 1, B - 1)

        self.M = [A * _ % B for _ in r]

    def fill(self, inp):
        return sum([x * y for x, y in zip(inp, self.M)])

try:
    for _ in range(10):
        challenge2 = Backpack(NBITS)
        r = getRandomNBitInteger(NBITS)
        pt = [int(i) for i in bin(r)[2:].zfill(NBITS)]
        ct = challenge2.fill(pt)
        print(challenge2.M)
        print(f"In your Knapsack: {ct}")

        inp = int(input("Secret: ").strip())
        if inp == r:
            print("Correct!")
        else:
            print("Nope.")
            exit(1)
except:
    exit(1)

print(flag[len(flag)//2:])

The first half is a 0/1 knapsack, let copilot write it directly. The second half is a Merkle-Hellman knapsack, directly use LLL to solve the subset sum for the answer.

from sage.all import *
from pwn import process, remote
import ast


def solve_01_knapsack(W, P, A):
    # thanks copilot!
    # W: weight of items
    # P: profit of items
    # A: capacity of knapsack
    n = len(W)

    # dp[i][j]: maximum profit of items 0..i with capacity j
    dp = [[0 for _ in range(A + 1)] for _ in range(n)]
    for i in range(n):
        for j in range(A + 1):
            if j >= W[i]:
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - W[i]] + P[i])
            else:
                dp[i][j] = dp[i - 1][j]

    # reconstruct solution
    i, j = n - 1, A
    items = []
    while i >= 0 and j >= 0:
        if i == 0:
            if dp[i][j] == P[i]:
                items.append(i)
            break
        if dp[i][j] != dp[i - 1][j]:
            items.append(i)
            j -= W[i]
        i -= 1

    return items


def solve_ssp(w, t):
    L = block_matrix([[matrix(w).T, ZZ(1), ZZ(0)], [matrix([-t]), ZZ(0), ZZ(1)]])
    for row in L.LLL():
        if row[-1] != 0:
            if row[-1] < 0:
                row = -row
            if all([0 <= x and x <= 1 for x in row[1:-1]]):
                if vector(row[1:-1]) * vector(w) == t:
                    return row[1:-1]


# io = process(["python", "challenge.py"])
io = remote("67.205.179.135", 7272)
io.recvline()
for _ in range(10):
    W = ast.literal_eval(io.recvlineS().strip())
    P = ast.literal_eval(io.recvlineS().strip())
    io.recvuntil(b"Knapsack Capacity: ")
    A = int(io.recvlineS().strip())
    items = solve_01_knapsack(W, P, A)
    io.sendline(" ".join(map(str, items)).encode())
    io.recvline()
flag1 = io.recvlineS().strip()
for _ in range(10):
    W = ast.literal_eval(io.recvlineS().strip())
    io.recvuntil(b"In your Knapsack: ")
    t = int(io.recvlineS().strip())
    sol = solve_ssp(W, t)
    secret = int("".join(map(str, sol)), 2)
    io.sendline(str(secret).encode())
    io.recvline()
flag2 = io.recvlineS().strip()
print(flag1 + flag2)
# cvctf{knap5ack_0/1_15_for_the_noob5_BUT_backp@ck_M3rkl3_h3llm4n_15_for_the_pro5}

Fractional Flag

from Crypto.Util.number import getPrime, inverse, bytes_to_long
from secret import flag

assert len(flag) == 99
assert flag.startswith(b"cvctf{") and flag.endswith(b"}")

class MyRSA:
    def __init__(self, n = 2, nbits = 512):
        self.p = getPrime(nbits)
        self.q = getPrime(nbits)
        self.N = self.p * self.q
        self.d = getPrime(nbits//2 - 1)
        self.my_phi = self.gen_phi(n)
        self.e = inverse(self.d, self.my_phi)

    def gen_phi(self, n):
        return sum([self.p**i for i in range(n)]) * sum([self.q**i for i in range(n)])

    def encrypt(self, m):
        print("I am not going to encrypt anything...")
        return m

    def get_public(self):
        print(f"N = {self.N}")
        print(f"e = {self.e}\n")
    
    def get_private(self):
        # print(f"d = {self.d}")
        return self.d

NPARTS = 3
fractions = [bytes_to_long(flag[len(flag)//NPARTS*i:len(flag)//NPARTS*(i+1)]) for i in range(NPARTS)]

print("[+] Here are my public keys:")
ns = [2, 3, 6]
rsas = [MyRSA(n) for n in ns]
private_exponents = [rsa.get_private() for rsa in rsas]

for rsa in rsas:
    rsa.get_public()

print("[+] Here are my flag fractions:")
for i in range(NPARTS):
    f = sum(fractions[j] * private_exponents[i]**(NPARTS-1-j) for j in range(NPARTS))
    print(f"[!] Fraction {i+1}: {f}")

There are three types of RSA public keys, for n{2,3,6}n \in \{2,3,6\}, there is endn1(modφi)e_n d_n \equiv 1 \pmod{\varphi_i'}, where φn=(j=0n1pnj)j=0n1qnj\varphi_n'=(\sum_{j=0}^{n-1}p_n^j)\sum_{j=0}^{n-1}q_n^j.

For n=2n=2, φ=(1+p)(1+q)N=pq\varphi'=(1+p)(1+q) \approx N = pq.

For n=3n=3, φ=(1+p+p2)(1+q+q2)N2=p2q2\varphi'=(1+p+p^2)(1+q+q^2) \approx N^2 = p^2q^2.

So we can get the approximate relationship φnNn1\varphi_n' \approx N^{n-1}.

Using ed1(modφ)ed \equiv 1 \pmod{\varphi'}, we get ed=kφ+1ed = k\varphi' + 1, where kdk \approx d, because dd is not large, we can use a method similar to the Wiener attack to solve for dd.

eφkd\frac{e}{\varphi'} \approx \frac{k}{d}

So using Nn1N^{n-1} to approximate φ\varphi', we can solve for dd, and after interpolation, the answer will come out.

from sage.all import *
from Crypto.Util.number import isPrime, long_to_bytes

pubs = [
    (
        2,
        97612005002255328088089410975523983681740354800179039894937915732930244664586629120094932234362725524185511994009850035922533559839893979074939037832668549750255468381192887812469917667115839541015495242839376266431038121426628287675447989631207259597586333251571969522037989427555616133183042678577218636039,
        76116130167787561359953887266200869082448529331107937460219287530308978477708991999616543343406382355689250048669403437942401267295685636067522489421767819007148067769751457394505797110602169754511939818788483430730012575308299189299157812591392734160576417069436538119185074252625496946207312639052459476691,
    ),
    (
        3,
        82898492840033848499679066573599199466262845944574446435099699953454086638324386416129279494828609729766998439132172194894188387716844097335523066318666261933348513791114624155336437054163128912934864480178839237943154511986161169068625070999701692457965233641625122455113801192492673037347038351956273261209,
        4209437149177720414392052396995336370571472638739290885909782621676928212352728218779571626530884770305850882606520062302514835362331092897621059096111410844919671863579044484955054277159466422627065939698416080746679841734383574957303171143150437215717944527004524163060734647488552780385109395365103554265493136383680197786179491825415327877829363932241445886199163804911851983235568877189260370900147226881457732043676609664875472514523869518158935427849891304397052900751093562294098001282747462255107311383605819243052786444806092296448159814920695343700003324553167747140120222023780385726663202175975632841101,
    ),
    (
        6,
        74994178474501705271671940639744064973727732038326591115828216310018498822567967944888584927591599150655580137956560356301728244890598527629795727836492659456865084635190916504902969902122979161893704294891200235692036639700141954229504462610034653592940594511895628524050995435988759181763969824436031245313,
        1489343297993123282242703585767862889532966168614466114229659752495521741344828171911376172581419167249138037648594483979411817592040147160238477726426615905245322859547191851844862266610905034585090032979459885344332508376146458008432418485138745167332564668471655885462387844870978937353124271354756749987557641173034038037138552554286596191926276854019397533360181392329678995160770713221738607291949604788483647158238145885370267789175042383720834228546234687195524802723091258548142868749009819341250311783811669486498714774091479332840123020791916467567722826634424992913028930995948675696110790085249624326162007313472329046629836566951986389110657545671708876118803228632059850860536273457909861781873059134865932188230401277356396602494704882757454481213539839098539947582560234411030973307998825870210215033177749651750411682404527804606701340350999128273688467096980925538961234964285918052638858734125375270072587641505250814286217419987349217586276836222849825473745401814287955844160842207855608228330415022147681694596413651097936005515497301045029721788331269208281455180641244748905469345705931987679143863830321305138722585532495234826613572876602582994268211475793765951624522296606385747914811802020368502982841323574845193182348705392733364068634167943152196883988138300398486795444947317920558342244022638552596544997423504070874736567161433560622253197613565603458736833008516912312579020461040855359316882678726669756450813443842584222951463074601799430267187315529031340358497011459213690622999701314023178557960651,
    ),
]

ds = []
for n, N, e in pubs:
    cf = continued_fraction(ZZ(e) / ZZ(N ** (n - 1)))
    for c in cf.convergents():
        d = c.denominator()
        if d.bit_length() == 255 and isPrime(d):
            print(d)
            ds.append(d)
            break

fs = [
    12885959655953139374785717692048211268233398655222955130228721746869891559409080848367142483157517829612928194235174347299097615553919212787225377729454255371358309685093996459461793296183459238094074717707949401000365552082820354011,
    23144924364202496361036242964551729244108242071387074122924446048219898057065538815277890013860024253422666710259842325295228086904295846675276536535237894072110755426259617054492417613772645109337095876879440959135444974146373938103,
    10216090816848970230135050433869173852319169856151418349231810820430643701522230150158652915588855848669985626642166987135416343716291162149831827545892183798838168834007800941773301995075510960084628367588300016798178754792796044603,
]
ms = map(int, QQ['x'].lagrange_polynomial(zip(ds, fs)))
print(b"".join([long_to_bytes(m) for m in ms][::-1]))
# cvctf{th15_fam1Ly_0f_RSA-like_crypt0syst3m_c4n_b3_4tt4ck3d_wh3n_E_15_cL053_t0_N^(n-1)_4nd_d<N^0.25}

PicoChip 1

from itertools import groupby
from Crypto.Util.number import *
from enum import Enum
from base64 import b64encode
from secret import flag

import plotly.express as px
import random


class State(Enum):
    # PicoChip power states for hardware simulation.
    FALLBACK = 0x0
    SUCCESS = 0x1
    ERROR = 0x2


class PicoChip:
    def __init__(self, k: int, N: int, t: int):
        self.states = []
        self.k = k
        self.N = N
        self.t = t
        self.r = self.gen_op()

    def rec(self, s: State):
        self.states.append(s)

    def gen_op(self) -> list:
        rs = []
        i = 3
        while len(rs) < self.N:
            if isPrime(i):
                self.rec(State.SUCCESS)
                rs.append(i)
            i += 2
        return rs
    
    def gen_secure_prime(self) -> int:
        # More secure than getPrime() as indicated from my latest paper.
        while True:
            v0 = random.randint(2**(self.k-1), 2**self.k)
            v = v0 + 1 - v0 % 2
            while True:
                is_prime = True
                i = 0
                while i < self.N:
                    if v % self.r[i] == 0:
                        v = v + 2
                        self.rec(State.FALLBACK)
                        break
                    i += 1
                    self.rec(State.SUCCESS)

                if i == self.N:
                    for m in range(1, self.t+1):
                        if not miller_rabin(v):
                            is_prime = False
                            v = v + 2
                            self.rec(State.FALLBACK)
                            break
                        self.rec(State.SUCCESS)
                    if is_prime:
                        return v
    
    def plot(self):
        # For transparency, I will show you the internals of PicoChip.
        power_states(self.states)


def power_states(states: list):
    if State.ERROR in states:
        raise Exception("PicoChip is broken!")
    
    power = []
    for k, g in groupby(states):
        if k != State.FALLBACK:
            power.append(sum(1 for _ in g))
        else:
            power.extend([0 for _ in g])

    gx, gy = [], []
    for i in range(len(power)):
        gx.append(i)
        gy.append(power[i])
        gx.append(i + 1)
        gy.append(power[i])

    fig = px.line(x=gx, y=gy, title="PicoChip Power States")
    fig.write_html("power_states.html")


def miller_rabin(n: int) -> bool:
    if n <= 1 or n == 4:
        return False
    if n <= 3:
        return True
    
    d = n - 1
    while d % 2 == 0:
        d //= 2
    a = 2 + random.randint(1, n - 4)
    x = pow(a, d, n)
    if x == 1 or x == n - 1:
        return True
    
    while d != n-1:
        x = (x * x) % n
        d *= 2
        if x == 1:
            return False
        if x == n - 1:
            return True
    return False


if __name__ == "__main__":
    pico = PicoChip(36, 60, 1)
    p = pico.gen_secure_prime()
    q = pico.gen_secure_prime()
    assert p != q and isPrime(p) and isPrime(q)

    assert flag.startswith(b"cvctf{") and flag.endswith(b"}")
    flag = flag[6:-1]
    
    N = p * q
    e = 0x10001
    m = bytes_to_long(flag)
    ct = pow(m, e, N)

    print("[+] PicoChip Encryption Parameters:")
    # print(f"N = {N}")
    print(f"e = {e}")
    print(f"ct = {ct}")

    pico.plot()
    print("[+] PicoChip Power States saved.")

This problem has a prime generator that leaks side channel information. By tracing back the power from the HTML it generates, and then tracing back the states from power, we can determine some information about pkp-k mod some small primes, and then use CRT to recover it.

Recovering qq follows the same concept.

from sage.all import crt, lcm, reduce
from Crypto.Util.number import *

# fmt: off
pico_r = [3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283]
# fmt: on


def solve_eqs(eqs):
    if len(eqs) < 2:
        return 0, 0
    ms = [-k for k, _ in eqs]
    ps = [p for _, p in eqs]
    L = reduce(lcm, ps)
    res = crt(ms, ps)
    return L, res


def solve(st, N, t):
    eqs = []
    k = 0
    idx = 0
    while idx < len(st):
        for i, s in enumerate(st[idx : idx + N]):
            idx += 1
            if s == 0:
                eqs.append((k, pico_r[i]))
                # assert (vp + k) % pico.r[i] == 0
                k += 2
                break
        else:
            L, cur_vv = solve_eqs(eqs)
            print(f"{L = }")
            print(f"{cur_vv = }")
            for s in st[idx : idx + t]:
                idx += 1
                if s == 0:
                    k += 2
                    break
                if isPrime(cur_vv + k):
                    print("FOUND1", cur_vv + k)
                    return cur_vv + k, st[idx:]
                if L and (2**37 // L) < 10000:
                    for u in range(2**37 // L):
                        if isPrime(cur_vv + k + u * L):
                            print("FOUND2", cur_vv + k + u * L)
                            return cur_vv + k + u * L, st[idx:]


# fmt: off
gy = [66,66,0,0,10,10,0,0,0,0,1,1,0,0,60,60,0,0,0,0,2,2,0,0,3,3,0,0,0,0,17,17,0,0,60,60,0,0,0,0,12,12,0,0,1,1,0,0,0,0,4,4,0,0,41,41,0,0,0,0,1,1,0,0,6,6,0,0,0,0,65,65,0,0,0,0,2,2,0,0,1,1,0,0,0,0,3,3,0,0,48,48,0,0,0,0,1,1,0,0,2,2,0,0,0,0,29,29,0,0,28,28,0,0,0,0,60,60,0,0,5,5,0,0,0,0,16,16,0,0,1,1,0,0,0,0,25,25,0,0,61,61]
# fmt: on
power = gy[::2]

st = sum([[0] if v == 0 else [1] * v for v in power], [])
print(st)
st = st[60:]
p, stt = solve(st, 60, 1)
print(stt)
q, _ = solve(stt, 60, 1)

e = 65537
ct = 3059648962482294740345
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
m = pow(ct, d, p * q)
print(int(m).to_bytes(100, "big").strip(b"\x00"))
# cvctf{p1C@-_}

PicoChip 2

from Crypto.Util.number import *
from enum import Enum
from base64 import b64encode
from secret import flag

import random
import csv


class State(Enum):
    # PicoChip power states for hardware simulation.
    FALLBACK = 0x0
    SUCCESS = 0x1
    ERROR = 0x2


class PicoChip:
    def __init__(self, k: int, N: int, t: int):
        self.states = []
        self.k = k
        self.N = N
        self.t = t
        self.r = self.gen_op()

    def rec(self, s: State):
        self.states.append(s)

    def gen_op(self) -> list:
        rs = []
        i = 3
        while len(rs) < self.N:
            if isPrime(i):
                self.rec(State.SUCCESS)
                rs.append(i)
            i += 2
        return rs
    
    def gen_secure_prime(self) -> int:
        # More secure than getPrime() as indicated from my latest paper.
        while True:
            v0 = random.randint(2**(self.k-1), 2**self.k)
            v = v0 + 1 - v0 % 2
            
            while True:
                is_prime = True
                i = 0
                while i < self.N:
                    if v % self.r[i] == 0:
                        v = v + 2
                        self.rec(State.FALLBACK)
                        break
                    i += 1
                    self.rec(State.SUCCESS)

                if i == self.N:
                    for m in range(1, self.t+1):
                        if not miller_rabin(v):
                            is_prime = False
                            v = v + 2
                            self.rec(State.FALLBACK)
                            break
                        self.rec(State.SUCCESS)
                    if is_prime:
                        return v
    
    def save(self):
        # For transparency, I will show you the internals of PicoChip.
        power_states(self.states)


def power_states(states: list):
    if State.ERROR in states:
        raise Exception("PicoChip is broken!")
    
    states = [s.value for s in states] 
    fields = ['Time', 'Signal']
    rows = [[i, s] for i, s in enumerate(states)]
  
    with open('power_states.csv', 'w') as f:
        write = csv.writer(f)
        write.writerow(fields)
        write.writerows(rows)


def miller_rabin(n: int) -> bool:
    if n <= 1 or n == 4:
        return False
    if n <= 3:
        return True
    
    d = n - 1
    while d % 2 == 0:
        d //= 2
    a = 2 + random.randint(1, n - 4)
    x = pow(a, d, n)
    if x == 1 or x == n - 1:
        return True
    
    while d != n-1:
        x = (x * x) % n
        d *= 2
        if x == 1:
            return False
        if x == n - 1:
            return True
    return False


if __name__ == "__main__":
    pico = PicoChip(512, 100, 10)
    p = pico.gen_secure_prime()
    q = pico.gen_secure_prime()
    assert p != q and isPrime(p) and isPrime(q)

    assert flag.startswith(b"cvctf{") and flag.endswith(b"}")
    flag = flag[6:-1]
    
    N = p * q
    e = 0x10001
    m = bytes_to_long(flag)
    ct = pow(m, e, N)
    
    print("[+] PicoChip Encryption Parameters:")
    print(f"N = {N}")
    print(f"e = {e}")
    print(f"ct = {ct}")

    pico.save()
    print("[+] PicoChip Power States saved.")

Almost the same as before, but this time NN is known and both pp and qq are 512 bits, so we use the same method to recover and then use Coppersmith or similar methods.

I found that I can get pkpmodLpp-k_p \bmod{L_p} and qkqmodLqq-k_q \bmod{L_q}, where log2Lp=220\log_2 L_p = 220 and log2Lq=256\log_2 L_q = 256. However, there are still a few issues to handle:

  1. 256 bits is a bit tight for Coppersmith.
  2. My method cannot determine which part of the power trace is pp and which part is qq, so the parameters sent into CRT need to be correct, and we need to find a way to handle errors.
  3. Similarly, I cannot be sure about kp,kqk_p, k_q.

The main point is to notice that Lp,LqL_p, L_q are products of small primes, so after taking L=gcd(Lp,Lq)L=\gcd(L_p,L_q), we can use small_roots to solve (p+x)(q+y)N(modL)(p'+x)(q'+y) \equiv N \pmod{L} to get kp,kqk_p, k_q.

Next, since Lp/L,Lq/LL_p/L, L_q/L are not 1, there is some additional information that can be used. After some calculations, using CRT, we can find the value of pp or qq modulo lcm(Lp,Lq)\operatorname{lcm}(L_p, L_q), which is about 340 bits.

Finally, for the error part, I decomposed lcm(Lp,Lq)\operatorname{lcm}(L_p, L_q) and randomly removed a few primes, then used Coppersmith to solve until a solution was found.

from Crypto.Util.number import *
from tqdm import tqdm
import random
from lbc_toolkit import small_roots

N = 130242563655857624894400945577596726349342500250110028576899185041437457270571385988775800707021082684571500938230335838572343683077030043170929464612371476956037213483889197399888953059037490955052374761877827022364642118001281744384383425614733953384975007872647643383223846078326219496054222658506664194809
e = 65537
ct = 105907761990378066226415220707780642346253492128869807523331608442629667283774856771483475721417798409391047173653877895381454531056957845466445503946203117753779878926970882151071816334395199925843076844258607834225532144320120475312185170491128333893223806872578843356036259570319998389877610989110147615590

# get this with `get_mod.py`
Lq = 110771048542294008655567252323407263021635384906342218910220956868946703113415
qmLq = 46507139167438726023448101260439823591848326066022505845258905249241953906242
Lp = 1487051625896473568251626796657514782188841738792655155038905673165
pmLp = 72663085590708489175312897469020819802613550944383864132672454215

L = gcd(Lq, Lp)
qL = qmLq % L
pL = pmLp % L

# qmLq, pmLq is not actually q (mod Lq) and p (mod Lp)
# instead, it is the initial output of `v0 = random.randint(2**(self.k-1), 2**self.k); v = v0 + 1 - v0 % 2`
# I am not sure the `k` value found in `get_mod.py` is correct
# so just test it with coppermsith
x, y = Zmod(L)["x,y"].gens()
f = (qL + x) * (pL + y) - N
x, y = small_roots(f, (2**10, 2**10))[0]
qmLq += x
pmLp += y

# unfortunately, neither Lp and Lq are big enough to apply coppersmith here
# but we can use the fact that n=pq to get more bits of q (mod Lq/gcd(Lq,Lp)) and p (mod Lp/gcd(Lq,Lp))
L2 = Lp // L
qmL2 = N / pmLp % L2
L3 = Lq // L
pmL3 = N / qmLq % L3
LL = Lq * L2
qmLL = crt([qmLq, qmL2], [Lq, L2])
pmLL = crt([pmLp, pmL3], [Lp, L3])
assert qmLL * pmLL % LL == N % LL

# let's coppersmith
x = Zmod(N)["x"].gen()
for _ in range(100):
    ps = [p for p, _ in factor(LL)]
    # we are not sure it is always correct
    # try remove some primes and apply coppersmith until it works
    pps = set(ps) - set(random.sample(ps, 8))
    Lsub = prod(pps)
    print("trying", Lsub, Lsub.bit_length())
    qmLsub = qmLL % Lsub
    f = (qmLsub + x * Lsub).monic()
    rs = f.small_roots(X=2 ** (512 - Lsub.bit_length()), beta=0.49, epsilon=0.1)
    if rs:
        sol = rs[0]
        print("yay", sol)
        break

q = ZZ(qmLsub + sol * Lsub)
p = ZZ(N // q)
assert p * q == N
phi = (p - 1) * (q - 1)
d = inverse_mod(e, phi)
m = pow(ct, d, N)
print(long_to_bytes(m))
# cvctf{jU57_4_s1MpL3_s1d3_ch4nn3L_ch1p@_@}

Misc

OJail

An OCaml jail (?), connect and directly Sys.command "sh";; to get a shell.

*Minecraft's Deadly Dilemma

Couldn't solve this problem during the competition

from io import BytesIO
from PIL import Image
import base64
import numpy as np

def load_image(img: str):
    with open(img, 'rb') as f:
        img = Image.open(f)
        img = img.convert('L')
        return np.asarray(img)

def load_image_b64(img: str):
    img = Image.open(BytesIO(base64.b64decode(img)))
    img = img.convert('L')
    return np.asarray(img)

def distance(x, y):
    return np.linalg.norm(x - y)

def is_close(inp, x):
    if inp.shape != x.shape:
        return False
    return np.sqrt(np.sum(np.abs(inp - x))) < 474


if __name__ == '__main__':
    sword = load_image("./sword.png")
    pickaxe = load_image("./pickaxe.png")
    try:
        test = load_image_b64(input().strip())
        if is_close(test, pickaxe) and distance(test, pickaxe) > distance(test, sword):
            from secret import flag
            print(flag)
        else:
            print("Nope!")
    except:
        exit(0)

In short, there are two 64x64 images pickaxe and sword, and we need to find an image such that L2Norm(img - pickaxe) > L2Norm(img - sword) and sqrt(L1Norm(img - pickaxe)) < 474.

My approach was to use PyTorch with backpropagation to find it, and the loss part was adjusted randomly until it worked.

from io import BytesIO
from PIL import Image
import base64
import numpy as np
import torch


def load_image(img: str):
    with open(img, "rb") as f:
        img = Image.open(f)
        img = img.convert("L")
        return np.asarray(img)


def load_image_b64(img: str):
    img = Image.open(BytesIO(base64.b64decode(img)))
    img = img.convert("L")
    return np.asarray(img)


def distance(x, y):
    return np.linalg.norm(x - y)


def is_close(inp, x):
    if inp.shape != x.shape:
        return False

    return np.sqrt(np.sum(np.abs(inp - x))) < 474


sword = load_image("./sword.png")
pickaxe = load_image("./pickaxe.png")

sword_tensor = torch.tensor(sword).float()
pickaxe_tensor = torch.tensor(pickaxe).float()
x = torch.nn.Parameter(torch.rand((1, 1, 64, 64)), requires_grad=True)
optim = torch.optim.SGD([x], lr=3e-1)


def loss_fn(x):
    dist = torch.sqrt(torch.sum(torch.abs(x - pickaxe_tensor)))
    cond2 = torch.norm(x - pickaxe_tensor) - torch.norm(x - sword_tensor)
    total_loss = dist - cond2 * 2
    return total_loss, dist, -cond2 * 2


i = 0
while True:
    optim.zero_grad()
    loss, a, b = loss_fn(torch.clamp(x, 0, 254))
    if i % 1000 == 0:
        print(i, loss, a, b)
    if a < 474 and b < 0:
        xxx = torch.clamp(x, 0, 254).detach().numpy().reshape(64, 64).round()
        img = Image.fromarray(xxx)
        img = img.convert("L")
        img.save("result.png")
        yyy = load_image("result.png")
        c1 = is_close(yyy, pickaxe)
        c2 = distance(yyy, pickaxe) > distance(yyy, sword)
        print(
            c1,
            c2,
            distance(yyy, pickaxe),
            distance(yyy, sword),
            np.sqrt(np.sum(np.abs(yyy - pickaxe))),
            np.sqrt(np.sum(np.abs(xxx - pickaxe))),
        )
        if c1 and c2:
            break
    loss.backward()
    optim.step()
    i += 1

with open("result.png", "rb") as f:
    print(base64.b64encode(f.read()))

"""
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
"""
# cvctf{chatGPT_isn't_wrong_but_u_n33d_more_s@mples}