<!-- Visited links are known to be leaky, see https://bugs.chromium.org/p/chromium/issues/detail?id=713521 https://bugs.chromium.org/p/chromium/issues/detail?id=1247907 Can use requestAnimationFrame and requestIdleCallback to reliably check when a links :visited status changes --> <head> <style> #link { position: absolute; top: 0; opacity: 0.001; pointer-events: none; } a { display: block; } .container { display: grid; grid-template-columns: 1fr 1fr; } .links { width: 30vw; } </style> </head>
defkeygen(ln): # Generate a linearly independent key arr = [ 1 << i for i inrange(ln) ]
for i inrange(ln): for j inrange(i): if random.getrandbits(1): arr[j] ^= arr[i] for i inrange(ln): for j inrange(i): if random.getrandbits(1): arr[ln - 1 - j] ^= arr[ln - 1 - i]
return arr
defgen_keystream(key): ln = len(key) # Generate some fake values based on the given key... fake = [0] * ln for i inrange(ln): for j inrange(ln // 3): if i + j + 1 >= ln: break fake[i] ^= key[i + j + 1]
# Generate the keystream res = [] for i inrange(ln): t = random.getrandbits(1) if t: res.append((t, [fake[i], key[i]])) else: res.append((t, [key[i], fake[i]]))
# Shuffle! random.shuffle(res)
keystream = [v[0] for v in res] public = [v[1] for v in res] return keystream, public
defxor(a, b): return [x ^ y for x, y inzip(a, b)]
defrecover_keystream(key, public): st = set(key) keystream = [] for v0, v1 in public: if v0 in st: keystream.append(0) elif v1 in st: keystream.append(1) else: assertFalse, "Failed to recover the keystream" return keystream
defbytes_to_bits(inp): res = [] for v in inp: res.extend(list(map(int, format(v, '08b')))) return res
defbits_to_bytes(inp): res = [] for i inrange(0, len(inp), 8): res.append(int(''.join(map(str, inp[i:i+8])), 2)) returnbytes(res)
fakei = -1 xk = 0# the last element of fake is always 0
whilelen(rkey) != len(public): if xk in PT[0] and PT[0].index(xk) != fakei: ROW = False else: ROW = True fakei = PT[ROW].index(xk) k = PT[not ROW][fakei] rkey.append(k) print(fakei) xk = reduce(xor, rkey[-window:])
key = rkey[::-1]
defrecover_keystream(key, public): st = set(key) keystream = [] for v0, v1 in public: if v0 in st: keystream.append(0) elif v1 in st: keystream.append(1) else: assertFalse, "Failed to recover the keystream" return keystream
defbits_to_bytes(inp): res = [] for i inrange(0, len(inp), 8): res.append(int("".join(map(str, inp[i : i + 8])), 2)) returnbytes(res)
ks = recover_keystream(key, public) print(bytes(a ^ b for a, b inzip(ct, bits_to_bytes(ks))))
defkeygen(ln): # Generate a linearly independent key arr = [ 1 << i for i inrange(ln) ]
for i inrange(ln): for j inrange(i): if random.getrandbits(1): arr[j] ^= arr[i] for i inrange(ln): for j inrange(i): if random.getrandbits(1): arr[ln - 1 - j] ^= arr[ln - 1 - i]
return arr
defgen_keystream(key): ln = len(key) assert ln > 50 # Generate some fake values based on the given key... fake = [0] * ln for i inrange(ln - ln // 3): arr = list(range(i + 1, ln)) random.shuffle(arr) for j in arr[:ln // 3]: fake[i] ^= key[j]
# Generate the keystream res = [] for i inrange(ln): t = random.getrandbits(1) if t: res.append((t, [fake[i], key[i]])) else: res.append((t, [key[i], fake[i]]))
# Shuffle! random.shuffle(res)
keystream = [v[0] for v in res] public = [v[1] for v in res] return keystream, public
defxor(a, b): return [x ^ y for x, y inzip(a, b)]
defrecover_keystream(key, public): st = set(key) keystream = [] for v0, v1 in public: if v0 in st: keystream.append(0) elif v1 in st: keystream.append(1) else: assertFalse, "Failed to recover the keystream" return keystream
defbytes_to_bits(inp): res = [] for v in inp: res.extend(list(map(int, format(v, '08b')))) return res
defbits_to_bytes(inp): res = [] for i inrange(0, len(inp), 8): res.append(int(''.join(map(str, inp[i:i+8])), 2)) returnbytes(res)
for r, i in findpt(check): rkey.add(int2vec(PT[not r][i])) pbar.n = len(rkey) pbar.refresh() print(len(rkey))
rkey = set(vec2int(v) for v in rkey)
key = list(rkey)
defrecover_keystream(key, public): st = set(key) keystream = [] for v0, v1 in public: if v0 in st: keystream.append(0) elif v1 in st: keystream.append(1) else: assertFalse, "Failed to recover the keystream" return keystream
defbits_to_bytes(inp): res = [] for i inrange(0, len(inp), 8): res.append(int("".join(map(str, inp[i : i + 8])), 2)) returnbytes(res)
ks = recover_keystream(key, public) print(bytes(a ^ b for a, b inzip(ct, bits_to_bytes(ks))))
# takes ~1hr to run... # pbctf{I_hope_you_enjoyed_this_challenge_now_how_about_playing_Metroid_Dread?}
inp = input("> ") iflen(inp) > 64orany(v notin ACCEPTABLE for v in inp): print("Invalid input :(") exit(0)
inp_hash = GoodHash(inp.encode()).hexdigest()
if token_hash == inp_hash: try: token = json.loads(inp) if token["admin"] == True: print("Wow, how did you find a collision?") print(f"Here's the flag: {flag}") else: print("Nice try.") print("Now you need to set the admin value to True") except: print("Invalid input :(") else: print("Invalid input :(")
from Crypto.Cipher import AES from Crypto.Util.number import * from sage.allimport GF, PolynomialRing import json import os import string import random
defghash(H, XS): # H: ele # XS: ele[] Y = int2ele(0) for X in XS: Y = (Y + X) * H return Y
defpad(b: bytes): fill = (16 - (len(b) % 16)) % 16 + 8 ghash_in = b + b"\x00" * fill + long_to_bytes(8 * len(b), 8) return ghash_in
defto_blocks(b: bytes, sz=16): return [b[i : i + sz] for i inrange(0, len(b), 16)]
deffind_collision(token, max_attempt=2 ** 14): H = bytes2ele(subkey) blocks = to_blocks(pad(token)) assertlen(blocks) == 5 C = bytes2ele(b'dmin": true }\x00\x00\x00') + bytes2ele(blocks[3]) new_blocks = list(blocks) new_blocks[3] = ele2bytes(bytes2ele(new_blocks[3]) + C) ele2 = bytes2ele(new_blocks[2]) ele1 = bytes2ele(new_blocks[1]) H2 = H * H for _ inrange(max_attempt): B = bytes2ele(bytes(random.sample(ACCEPTABLE, 12)) + b'","a') - ele2 A = (C - B * H) / H2 bA = ele2bytes(ele1 + A) ifall(x in ACCEPTABLE for x in bA): break else: return assert A * H ** 2 + B * H == C new_blocks[1] = ele2bytes(bytes2ele(new_blocks[1]) + A) new_blocks[2] = ele2bytes(bytes2ele(new_blocks[2]) + B) fake_token = b"".join(new_blocks)[:-2].strip(b"\0") assert GoodHash(fake_token).digest() == GoodHash(token).digest() return fake_token
# run this script in 8 tmux windows, then sit down and pray one of them can find the correct one in a short time... # pbctf{GHASH_is_short_for_GoodHash_:joy:}
System.out.println("Welcome to the 'Lucky Crystal Game'!"); System.out.println("Please provide a lucky seed:"); Scannerscr=newScanner(System.in); longseed= scr.nextLong(); Randomrng=newRandom(seed);
lb = [0] + [v - b for v, b inzip(vlb, bb)] ub = [2 ** 48] + [v - b for v, b inzip(vub, bb)] result, applied_weights, fin = solve( matrix(B), list(lb), list(ub) ) # note, this function will mutate your B, lb and ub by default
s = ZZ(fin[0]) for z in zz: r = ZZ(z(s)) o = ((r >> 24) / (1 << 24)).n() print(o, o > 7.331 * 0.1337)
print( xor(s, 0x5DEECE66D) ) # Java RNG will xor your seed with 0x5DEECE66D when setting seed
# one of the good seeds = 272404351039795 # pbctf{intended_is_LLL_with_branch_and_bound_9ad09becbc4c7685}
self.x = self.x[1:] + [sum(x * y for x, y inzip(self.x, self.a1)) % self.m1] self.y = self.y[1:] + [sum(x * y for x, y inzip(self.y, self.a2)) % self.m2] self.z = self.z[1:] + [sum(x * y for x, y inzip(self.z, self.a3)) % self.m3]
return o.to_bytes(8, byteorder='big')
if __name__ == "__main__": prng = PRNG()
hint = b'' for i inrange(12): hint += prng.out() print(hint.hex())
assertlen(flag) % 8 == 0 stream = b'' for i inrange(len(flag) // 8): stream += prng.out() out = bytes([x ^ y for x, y inzip(flag, stream)]) print(out.hex())
xs = [x0, x1, x2] whilelen(xs) != 12: xs += [sum(x * y for x, y inzip(a1, xs[-3:])) - next(iks) * m1]
ys = [y0, y1, y2] whilelen(ys) != 12: ys += [sum(x * y for x, y inzip(a2, ys[-3:])) - next(iks) * m2]
zs = [z0, z1, z2] whilelen(zs) != 12: zs += [sum(x * y for x, y inzip(a3, zs[-3:])) - next(iks) * m3]
sts = [2 * m1 * x - m3 * y - m2 * z - next(iks) * M for x, y, z inzip(xs, ys, zs)]
M, _ = Sequence(sts).coefficient_matrix() print(vector(_)) M = M.dense_matrix().T A = matrix.identity(24) A = A.augment(matrix(24, 12)) B = matrix(12, 36) C = matrix(12, 24) C = C.augment(matrix.identity(12)) A = A.stack(B).stack(C) M = M.augment(A)
result, applied_weights, fin = solve(M, list(lb), list(ub)) # `solve` will mutate M, lb and ub
# M is already rescaled, although it is zero determinant, but the freedom seems to only affects the ks parts # so the first 9 elements are correct based on my testing script init_states = list(M.solve_left(result)[:9])
self.x = self.x[1:] + [sum(x * y for x, y inzip(self.x, self.a1)) % self.m1] self.y = self.y[1:] + [sum(x * y for x, y inzip(self.y, self.a2)) % self.m2] self.z = self.z[1:] + [sum(x * y for x, y inzip(self.z, self.a3)) % self.m3]
returnint(o).to_bytes(8, byteorder="big")
prng = PRNG(init_states)
for o in states: assertint(o).to_bytes(8, "big") == prng.out()
flag = b""
for blk in [ct[i : i + 8] for i inrange(0, len(ct), 8)]: flag += bytes(a ^^ b for a, b inzip(blk, prng.out())) print(flag)
defgenPrime(): whileTrue: a = random.getrandbits(256) b = random.getrandbits(256)
if b % 3 == 0: continue
p = a ** 2 + 3 * b ** 2 if p.bit_length() == 512and p % 3 == 1and isPrime(p): return p
defadd(P, Q, mod): m, n = P p, q = Q
if p isNone: return P if m isNone: return Q
if n isNoneand q isNone: x = m * p % mod y = (m + p) % mod return (x, y)
if n isNoneand q isnotNone: m, n, p, q = p, q, m, n
if q isNone: if (n + p) % mod != 0: x = (m * p + 2) * inverse(n + p, mod) % mod y = (m + n * p) * inverse(n + p, mod) % mod return (x, y) elif (m - n ** 2) % mod != 0: x = (m * p + 2) * inverse(m - n ** 2, mod) % mod return (x, None) else: return (None, None) else: if (m + p + n * q) % mod != 0: x = (m * p + (n + q) * 2) * inverse(m + p + n * q, mod) % mod y = (n * p + m * q + 2) * inverse(m + p + n * q, mod) % mod return (x, y) elif (n * p + m * q + 2) % mod != 0: x = (m * p + (n + q) * 2) * inverse(n * p + m * q + 2, mod) % mod return (x, None) else: return (None, None)
defpower(P, a, mod): res = (None, None) t = P while a > 0: if a % 2: res = add(res, t, mod) t = add(t, t, mod) a >>= 1 return res
defrandom_pad(msg, ln): pad = bytes([random.getrandbits(8) for _ inrange(ln - len(msg))]) return msg + pad
p, q = genPrime(), genPrime() N = p * q phi = (p ** 2 + p + 1) * (q ** 2 + q + 1)
print(f"N: {N}")
d = getPrime(400) e = inverse(d, phi) k = (e * d - 1) // phi
n = 144256630216944187431924086433849812983170198570608223980477643981288411926131676443308287340096924135462056948517281752227869929565308903867074862500573343002983355175153511114217974621808611898769986483079574834711126000758573854535492719555861644441486111787481991437034260519794550956436261351981910433997 e = 3707368479220744733571726540750753259445405727899482801808488969163282955043784626015661045208791445735104324971078077159704483273669299425140997283764223932182226369662807288034870448194924788578324330400316512624353654098480234449948104235411615925382583281250119023549314211844514770152528313431629816760072652712779256593182979385499602121142246388146708842518881888087812525877628088241817693653010042696818501996803568328076434256134092327939489753162277188254738521227525878768762350427661065365503303990620895441197813594863830379759714354078526269966835756517333300191015795169546996325254857519128137848289
P = PolynomialRing(Zmod(e), "k,s") kk, ss = P.gens() f = 1 + kk * (n ^ 2 + ss * (ss + n + 1) - n + 1) load("~/workspace/coppersmith.sage") k, s = small_roots(f, (2 ** 400, 2 ** 513), m=3, d=4)[0] # take ~1min k, s = ZZ(k), ZZ(s) print(f"{k = }") print(f"{s = }")
sol = solve(x ^ 2 - s * x + n, (x,)) p = ZZ(sol[0].rhs()) q = ZZ(sol[1].rhs()) print(f"{p = }") print(f"{q = }") assert p * q == n d = inverse_mod(e, (p ^ 2 + p + 1) * (q ^ 2 + q + 1)) print(f"{d = }")
E = ( 123436198430194873732325455542939262925442894550254585187959633871500308906724541691939878155254576256828668497797665133666948295292931357138084736429120687210965244607624309318401630252879390876690703745923686523066858970889657405936739693579856446294147129278925763917931193355009144768735837045099705643710, 47541273409968525787215157367345217799670962322365266620205138560673682435124261201490399745911107194221278578548027762350505803895402642361588218984675152068555850664489960683700557733290322575811666851008831807845676036420822212108895321189197516787046785751929952668898176501871898974249100844515501819117, )
defadd(P, Q, mod): m, n = P p, q = Q
if p isNone: return P if m isNone: return Q
if n isNoneand q isNone: x = m * p % mod y = (m + p) % mod return (x, y)
if n isNoneand q isnotNone: m, n, p, q = p, q, m, n
if q isNone: if (n + p) % mod != 0: x = (m * p + 2) * inverse_mod(n + p, mod) % mod y = (m + n * p) * inverse_mod(n + p, mod) % mod return (x, y) elif (m - n ** 2) % mod != 0: x = (m * p + 2) * inverse_mod(m - n ** 2, mod) % mod return (x, None) else: return (None, None) else: if (m + p + n * q) % mod != 0: x = (m * p + (n + q) * 2) * inverse_mod(m + p + n * q, mod) % mod y = (n * p + m * q + 2) * inverse_mod(m + p + n * q, mod) % mod return (x, y) elif (n * p + m * q + 2) % mod != 0: x = (m * p + (n + q) * 2) * inverse_mod(n * p + m * q + 2, mod) % mod return (x, None) else: return (None, None)
defpower(P, a, mod): res = (None, None) t = P while a > 0: if a % 2: res = add(res, t, mod) t = add(t, t, mod) a >>= 1 return res
a, b = power(E, d, n) print(long_to_bytes(a)) print(long_to_bytes(b))
有個 pcap 檔,wireshark 打開看到有很多 Bluetooth Low Energy 的 PDU
在,可以看出它是一端的 client 向另一方進行寫入。仔細觀察一下一些 BTATT
的 PDU 可以看到寫入的時候都有一些 index 和
data,所以我寫了個腳本試著把傳送的 flag 還原出來就解掉了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
import pyshark
cap = pyshark.FileCapture("btle.pcap")
buf = bytearray(50)
for pdu in cap: ifhasattr(pdu, "btatt") and"Prepare Write Request"instr(pdu): btatt = pdu.btatt b = btatt.value.binary_value i = int(btatt.offset) buf[i : i + len(b)] = b print(buf)