# RaRCTF 2021 ## sRSA ```python from Crypto.Util.number import * p = getPrime(256) q = getPrime(256) n = p * q e = 0x69420 flag = bytes_to_long(open("flag.txt", "rb").read()) print("n =",n) print("e =", e) print("ct =",(flag * e) % n) ``` 注意到最後一行,它加密的方式乘以 $e$,並不是取 $pow(flag, e, n)$,所以直接乘以 $e^{-1}$ 即可。 ```python from output import * from Crypto.Util.number import long_to_bytes pt = ct * pow(e, -1, n) % n flag = long_to_bytes(pt) print(flag) ``` :::success rarctf{ST3GL0LS_ju5t_k1dd1ng_th1s_w4s_n0t_st3g_L0L!_83b7e829d9} ::: ## minigen ```python exec('def f(x):'+'yield((x:=-~x)*x+-~-x)%727;'*100) g=f(id(f));print(*map(lambda c:ord(c)^next(g),list(open('f').read()))) ``` 雖然我們不知道 $id(f)$ 的值,但是 $f(x)$ 只會回傳 $727$ 以內的數值,因此只要枚舉所有可能的起始值,然後看還原的 $flag$ 是否含有 $rarctf$ 。 ```python for i in range(727): g = f(i) test = "" try: for j in range(len(output)): test += chr(output[j] ^ next(g)) if "rar" in test: print(test) except: continue ``` :::success rarctf{pyg01f_1s_fun} ::: ## unrandompad ```python from random import getrandbits from Crypto.Util.number import getPrime, long_to_bytes, bytes_to_long def keygen(): # normal rsa key generation primes = [] e = 3 for _ in range(2): while True: p = getPrime(1024) if (p - 1) % 3: break primes.append(p) return e, primes[0] * primes[1] def pad(m, n): # pkcs#1 v1.5 ms = long_to_bytes(m) ns = long_to_bytes(n) if len(ms) >= len(ns) - 11: return -1 padlength = len(ns) - len(ms) - 3 ps = long_to_bytes(getrandbits(padlength * 8)).rjust(padlength, b"\x00") return int.from_bytes(b"\x00\x02" + ps + b"\x00" + ms, "big") def encrypt(m, e, n): # standard rsa res = pad(m, n) if res != -1: print(f"c: {pow(m, e, n)}") else: print("error :(", "message too long") menu = """ [1] enc() [2] enc(flag) [3] quit """[1:] e, n = keygen() print(f"e: {e}") print(f"n: {n}") while True: try: print(menu) opt = input("opt: ") if opt == "1": encrypt(int(input("msg: ")), e, n) elif opt == "2": encrypt(bytes_to_long(open("/challenge/flag.txt", "rb").read()), e, n) elif opt == "3": print("bye") exit(0) else: print("idk") except Exception as e: print("error :(", e) ``` 不難發現這隻程式還是使用原始的 $m$ 去加密,再加上 public exponent 只有 $3$,那肯定是直接用 Hastad boardcast attack。 ```python flag_cube = crt([c1,c2,c3], [n1,n2,n3]) flag = flag_cube.nth_root(3) from Crypto.Util.number import * print(long_to_bytes(flag)) ``` :::success rarctf{https://cdn.discordapp.com/attachments/751845431063085160/866641917714235392/unknown.png_8538853c64} ::: ## babycrypt ```python from Crypto.Util.number import getPrime, bytes_to_long flag = bytes_to_long(open("/challenge/flag.txt", "rb").read()) def genkey(): e = 0x10001 p, q = getPrime(256), getPrime(256) if p <= q: p, q = q, p n = p * q pubkey = (e, n) privkey = (p, q) return pubkey, privkey def encrypt(m, pubkey): e, n = pubkey c = pow(m, e, n) return c pubkey, privkey = genkey() c = encrypt(flag, pubkey) hint = pubkey[1] % (privkey[1] - 1) print('pubkey:', pubkey) print('hint:', hint) print('c:', c) ``` 我們先來看一下 hint,$$hint = n \pmod{q-1} = p \pmod{q-1}$$ 注意到 $genkey()$ 中有確保 $2q>p>q$,所以上式其實是 $p-(q-1)$。接著就是解一元二次方程式... ```python from output import * from decimal import * ### hint = p-(q-1) diff = hint - 1 getcontext().prec = 1000 pq_sum = int(Decimal(diff ** 2 + 4 * n) ** (Decimal(1)/Decimal(2))) p, q = (pq_sum + diff) // 2, (pq_sum - diff) // 2 assert p*q == n pt = pow(c, pow(e, -1, (p-1)*(q-1)), n) from Crypto.Util.number import long_to_bytes flag = long_to_bytes(pt) print(flag) ``` :::success rarctf{g3n3r1c_m4th5_equ4t10n_th1ng_ch4ll3ng3_5a174f54e6} ::: ## Shamir's Stingy Sharing ```python import random, sys from Crypto.Util.number import long_to_bytes def bxor(ba1,ba2): return bytes([_a ^ _b for _a, _b in zip(ba1, ba2)]) BITS = 128 SHARES = 30 poly = [random.getrandbits(BITS) for _ in range(SHARES)] flag = open("/challenge/flag.txt","rb").read() random.seed(poly[0]) print(bxor(flag, long_to_bytes(random.getrandbits(len(flag)*8))).hex()) try: x = int(input('Take a share... BUT ONLY ONE. ')) except: print('Do you know what an integer is?') sys.exit(1) if abs(x) < 1: print('No.') else: print(sum(map(lambda i: poly[i] * pow(x, i), range(len(poly))))) ``` 我們的目標很明確,就是將透過一次詢問將 $poly[0]$ 給找出來,但是不可以直接取 $x=0$。這裡就要用到 $$P(x) \equiv poly[0] \pmod{x}$$ 當 $|x|>|poly[0]|$ 時,餘數就恰恰好是 $poly[0]$。 ```python import random from Crypto.Util.number import long_to_bytes x = 10889035741470030830827987437816582766592 def bxor(ba1,ba2): return bytes([_a ^ _b for _a, _b in zip(ba1, ba2)]) enc_flag = bytes.fromhex("88e39f174c26080b373b9bc7ae7d8cad95d56a7475ec035442f0ec51696016") a = 190534637132143375685957506679310680480863339405938582444449143962690405859986472143460034693369773025504626732911418966373114939017878294194553640632792917591210219147119959461183027499174184467045117639864214986271793981241500737756074184720483905141278453930914741353266433395425726425312520994331518638359405458542281881551701324480451365675465360290750303522693184805519163377448364461291988285127560844142909629092296463360184200228478431172050187457961296417093565813624582905576545060659982135127710432555195640298858960426436887536779848225975212943082796344524171290042714607028901773307317828897257565965508196026690579778055479348407289264307979835906872293456477803249926363388887589801349394462859660332793967892610193387774340142111732514239655989994304072228568306509018656495390081679320916480128706930568302005718655546817028961367313499638100665301474421779493876442295678063661088730186043737560634757185231611152176808509882704542114991507666095914567522012795497740657890285438897345292418346804580502848802809140739940672650603206760402834276433520993036614693214507001908851596185356443242027867671634685155397744771590345162441239198620184835568444616300859896343442483468582 poly0 = a % x random.seed(poly0) flag = bxor(enc_flag, long_to_bytes(random.getrandbits(len(enc_flag)*8))) print(flag) ``` :::success rarctf{n3v3r_trust_4n_1nt3g3r} ::: ## rotoRSA ```python from sympy import poly, symbols from collections import deque import Crypto.Random.random as random from Crypto.Util.number import getPrime, bytes_to_long, long_to_bytes def build_poly(coeffs): x = symbols('x') return poly(sum(coeff * x ** i for i, coeff in enumerate(coeffs))) def encrypt_msg(msg, poly, e, N): return long_to_bytes(pow(poly(msg), e, N)).hex() p = getPrime(256) q = getPrime(256) N = p * q e = 11 flag = bytes_to_long(open("/challenge/flag.txt", "rb").read()) coeffs = deque([random.randint(0, 128) for _ in range(16)]) welcome_message = f""" Welcome to RotorSA! With our state of the art encryption system, you have two options: 1. Encrypt a message 2. Get the encrypted flag The current public key is n = {N} e = {e} """ print(welcome_message) while True: padding = build_poly(coeffs) choice = int(input('What is your choice? ')) if choice == 1: message = int(input('What is your message? '), 16) encrypted = encrypt_msg(message, padding, e, N) print(f'The encrypted message is {encrypted}') elif choice == 2: encrypted_flag = encrypt_msg(flag, padding, e, N) print(f'The encrypted flag is {encrypted_flag}') coeffs.rotate(1) ``` 不管三七二十一,我們先傳 $16$ 次 $0$ 把多項式的係數拿出來。但是在 $GF(p)$ 下求解多項式的根是很難的,參考 Frenklin related message attack,看以下兩個多項式的公因式:$$a_{15}x^{15}+a_{14}x^{14}+\cdots+a_1x+a_0 - C_{1}$$ 和 $$a_{14}x^{15}+a_{13}x^{14}+\cdots+a_0x+a_{15} - C_{2}$$ 它有高機率是 $x-m$。 ```python from collections import deque weight = deque([2 ** i for i in range(16)]) A = Matrix(ZZ, 16, 16) for i in range(16): for j in range(16): A[i, j] = weight[j] weight.rotate(15) Y = vector([4978732, 3141824, 5169553, 5423981, 6129442, 4918964, 3612103, 2767826, 4224952, 5959574, 4448158, 3981191, 5734192, 6225584, 4652503, 5503976]) coef = A\Y print(coef) N = 7661390097649734971645170813670315640515367193176661723124492523771787060503737006824444244709748736037802701619994549056559846309506086614565557453133947 e = 11 enc_flags = [6391979643894258282207873662949703091852978151728933548794699743197774742341510199981080536714753760857390702790824914789875604581308419193796521148656341, 2719916709375804370737303381943697780007800261847823047269370734500397621436239194565538325800843288778974187794784857624279205096393510810624369587402213, 5891393159782000403710586923779949556733942113908057101500722744110330393067648912056279355064792741092819457229606542426336472104307098678479775126566512, 1089791603259975036616907669338490891370492565083613602678894020054081728713700968306345420684642209456219024639181124438391555765460813662182355455768002, 7382618063883456361096298367293576656463193178887809700357322971305678170935768941963148819310675853679763627308918129298402959020288518329382446263764355] ZmodN = Zmod(N) R.<x> = ZmodN[] f, g = 0, 0 for i in range(16): f += Zmod(N)(coef[i]) * x^i g += Zmod(N)(coef[(i-1) % 16]) * x^i f, g = f^e-enc_flags[0], g^e-enc_flags[1] def polyGCD(a, b): if b == 0: return a.monic() else: return polyGCD(b, a%b) _ = N - (polyGCD(f, g).coefficients()[0]) print(_) ``` :::success rarctf{sp1n_spin_sp33n_sp00n_sp1n-b8765b7c3b} ::: ## PsychECC ```python from collections import namedtuple import random def moddiv(x,y,p): return (x * pow(y, -1, p)) %p Point = namedtuple("Point","x y") class EllipticCurve: INF = Point(0,0) def __init__(self, a, b, p): self.a = a self.b = b self.p = p def add(self,P,Q): if P == self.INF: return Q elif Q == self.INF: return P if P.x == Q.x and P.y == (-Q.y % self.p): return self.INF if P != Q: Lambda = moddiv(Q.y - P.y, Q.x - P.x, self.p) else: Lambda = moddiv(3 * P.x**2 + self.a,2 * P.y , self.p) Rx = (Lambda**2 - P.x - Q.x) % self.p Ry = (Lambda * (P.x - Rx) - P.y) % self.p return Point(Rx,Ry) def multiply(self,P,n): n %= self.p if n != abs(n): ans = self.multiply(P,abs(n)) return Point(ans.x, -ans.y % p) R = self.INF while n > 0: if n % 2 == 1: R = self.add(R,P) P = self.add(P,P) n = n // 2 return R # P256 parameters, secure. p = 115792089210356248762697446949407573530086143415290314195533631308867097853951 order = 115792089210356248762697446949407573529996955224135760342422259061068512044369 a = -3 b = 41058363725152142129326129780047268409114441015993725554835256314039467401291 E = EllipticCurve(a,b,p) print("""Welcome to my prediction centre! We're always looking out for psychics! We're gonna choose a random number. You get to choose a point. We'll multiply that point by our random number. Since this curve is of perfect and prime order, it'll be impossible to break this test. Only a psychic could know! Be psychic, get the flag.""") x = int(input("Enter point x: ")) y = int(input("Enter point y: ")) P = Point(x,y) n = random.randint(1,order) Q = E.multiply(P,n) print("Ok, where do you think the point will go?") px = int(input("Enter point x: ")) py = int(input("Enter point y: ")) prediction = Point(px,py) if prediction == E.INF or prediction == P: print("Psychics don't use dirty tricks.") quit() if prediction == Q: print("Wow! You're truly psychic!") print(open("/challenge/flag.txt").read()) quit() print("Better luck next time.") print(f"Point was {Q}") ``` 題目要求我們猜中 $nP$ 的座標,其中 $n$ 是一個隨機的數字。直覺的想法是想要找 $P$ 使得 $ord(P)$ 很小,那麼只要多猜幾次就會中了!然而 P256 Curve 的 order 是個大質數,所以不存在這樣的 $P$,還好上禮拜解 Crypto CTF 的時候,不小心瞄到 Invalid curve attack,因此目標變成找一個不在 P256 Curve 上的 P,滿足它的 $ord(P)$ 很小。 :::success rarctf{w0ah_str4ight_cl41r0v0y4nc3!!_8119733d69} ::: ## snore ```python from Crypto.Util.number import * from Crypto.Util.Padding import pad from Crypto.Cipher import AES from hashlib import sha224 from random import randrange import os p = 148982911401264734500617017580518449923542719532318121475997727602675813514863 g = 2 assert isPrime(p//2) # safe prime x = randrange(p) y = pow(g, x, p) def verify(s, e, msg): r_v = (pow(g, s, p) * pow(y, e, p)) % p return bytes_to_long(sha224(long_to_bytes(r_v) + msg).digest()) == e def sign(msg, k): r = pow(g, k, p) e = bytes_to_long(sha224(long_to_bytes(r) + msg).digest()) % p s = (k - (x * e)) % (p - 1) return (s, e) def xor(ba1,ba2): return bytes([_a ^ _b for _a, _b in zip(ba1, ba2)]) otp = os.urandom(32) messages = [ b"Never gonna give you up", b"Never gonna let you down", b"Never gonna run around and desert you", b"Never gonna make you cry", b"Never gonna say goodbye", b"Never gonna tell a lie and hurt you" ] print("p = ", p) print("g = ", g) print("y = ", y) for message in messages: k = bytes_to_long(xor(pad(message, 32)[::-1], otp)) # OTP secure s, e = sign(message, k % p) assert (verify(s, e, message)) print(message, sign(message, k % p)) flag = open("flag.txt","rb").read() key = sha224(long_to_bytes(x)).digest()[:16] iv = os.urandom(16) cipher = AES.new(key, AES.MODE_CBC, iv) ct = cipher.encrypt(pad(flag, 16)).hex() print("ct = ", ct) print("iv = ", iv.hex()) ``` 本來想說直接在 $GF(p)$ 上解 discrete log,可是 $p$ 不夠 smooth,所以只好從前面六個 Schnorr signatures 試著去將 $x$ 求出。查了不少的 google,感覺就是把它當成 Hidden number problem(HNP) 去解, 一般來說,HNP是有好幾個型如 $$\alpha_i x \equiv b_i+c_i \pmod{p_i}$$ 未知的東西是 $x, c_i$,其中 $c_i$ 相對於 $p_i$ 是小的。在這題,我們未知的 $k$ 是 message 跟一個固定的 random bytes 去做 XOR,所以高機率不會比 $q$ 明顯小。這邊用到的是 $$(e_i-e_j)x \equiv (k_i-k_j)+(s_i-s_j) \pmod{q}$$ 然後確保 $k_i-k_j$ 很小。這裡不難發現有兩組 message 的長度和前綴一樣,所以做完 XOR 後,對應的 $k$ 只有中間的 88bits 不一樣。如此拿去直接跑 HNP 沒結果,因此乘以 $pow(2, -96, q)$ 讓 $k_i-k_j$ 中間的 bits 跑去後面。這樣跑 HNP 就會有答案~ ```python p = 148982911401264734500617017580518449923542719532318121475997727602675813514863 q = (p-1) // 2 g = 2 y = 99943368625476277151400768907519717344447758596311103260066302523387843692499 s = [82164720827627951718117576622367372918842412631288684063666489980382312886875, 121728190859093179709167853051428045020048650314914045286511335302789797110644, 146082371876690961814167471199278995325899717136850705507399907858041424152875, 70503066417308377066271947367911829721247208157460892633371511382189117698027, 129356717302185231616252962266443899346987025366769583013987552032290057284641, 12183293984655719933097345580162258768878646698567137931824149359927592074910] e = [20555462814568596793812771425415543791560033744700837082533238767135, 18832686601255134631820635660734300367214611070497673143677605724980, 17280327447912166881602972638784747375738574870164428834607749483679, 18679076989831101699209257375687089051054511859966345809079812661627, 2084781842220461075274126508657531826108703724816608320266110772897, 15768525934046641405375930988120401106067516205761039338919748323087] def solve_HNP(a, b, k): # http://www.isg.rhul.ac.uk/~sdg/igor-slides.pdf N = len(a) M = Matrix(RationalField(), N+1, N+1) for i in range(N): M[i, i] = q M[N, i] = b[i] M[N, N] = 1 / (2 ** (k + 1)) def babai(A, w): A = A.LLL(delta=0.75) G = A.gram_schmidt()[0] t = w for i in reversed(range(A.nrows())): c = ((t * G[i]) / (G[i] * G[i])).round() t -= A[i] * c return w - t closest = babai(M, vector(RationalField(), a + [0])) return (closest[-1] * (2 ** (k + 1))) % p s_bar = [((s[4]-s[0]) * pow(2, -96, q)) % q, ((s[3]-s[1]) * pow(2, -96, q)) % q] e_bar = [((e[0]-e[4]) * pow(2, -96, q)) % q, ((e[1]-e[3]) * pow(2, -96, q)) % q] for i in range(250): x = solve_HNP(s_bar, e_bar, i) if pow(g, x, p) == y: print(x) break ``` :::success rarctf{zZZzZZZZzzZZZzzZZZZZZzZzzzZzzZZzZzzZZzzzZZZZzZZz_s0rry_1_w4s_t00_t1r3d_t0_c0me-up_w1th_4n_4ctual-fl4g_7686f36b65} :::