Cryptoverse CTF 2023 WriteUps

這次自己在 nyahello 隊伍中稍微解了幾題,主要包含了 crypto 中幾個相對難的題目。

Crypto

LFSR Explorer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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))

有兩個 LFSR,直接逆回去即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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:])

前半是 0/1 背包,讓 copilot 直接幫我寫。後半是 merkle-hellman backpack,直接用 LLL 解 subset sum 出答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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}")

有三個類 RSA public key,對於 ,其中

來說

來說

所以可以得到 的近似關係。

而拿 可得 ,其中 ,因為 不大所以可以用類似 wiener attack 的方法解出

所以拿 去近似 ,就可以求出 ,解出來之後再插值一下就出來了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
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.")

這題有個 prime generator 會 leak side channel 的資訊,可以透過它生出的 html 回推 power,而 power 又能回推 states,因此可以透過什麼時候在 FALLBACK 來判斷一些 mod 一些小質數的資訊,然後 crt 復原即可。

還原 也是一樣的概念。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
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.")

和前面幾乎一樣,不過這次 N 已知且 p,q 都是 512 bits 的,所以大概是用一樣的方法還原然後 coppersmith 之類的。

我拿實際參數來側發現我能得到 ,其中 。不過我這樣還有個幾個問題要處理

  1. 256 bits 對於 coppersmith 有點緊
  2. 因為我的方法無法判斷 power trace 中哪段是 p 哪段是 q,所以 crt 送進去的參數是對所以還要想辦法去 error
  3. 類似上面的原因,我也不能確定

主要是要先注意到 都是一些小質數的乘積,所以取 之後用 small_roots 解 可得

再來是 都非 1,所以其實還有些額外的資訊可以用,稍微算一下之後用 CRT 就能算出 之下的值了,大概 340 bits 左右。

最後 error 的部分我是透過把 分解然後隨機移除幾個 prime,然後用 coppersmith 解解看直到有解為止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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

ocaml 的 jail (?),連上去直接 Sys.command "sh";; 就拿 shell 了。

*Minecraft's Deadly Dilemma

賽中沒能解掉這題

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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)

簡單來說他有兩張 64x64 的圖片 pickaxesword,然後要找到一張圖讓 L2Norm(img - pickaxe) > L2Norm(img - sword),並且 sqrt(L1Norm(img - pickaxe)) < 474

我的作法是直接上 pytorch 用 backpropagation 來找,loss 部分亂弄一下就出來了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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}