tadpole
tadpoles only know the alphabet up to b... how will they ever know what p is?
添付ファイル:tadpole.py, output.txt
#tadpole.py from Crypto.Util.number import bytes_to_long, isPrime from secrets import randbelow p = bytes_to_long(open("flag.txt", "rb").read()) assert isPrime(p) a = randbelow(p) b = randbelow(p) def f(s): return (a * s + b) % p print("a = ", a) print("b = ", b) print("f(31337) = ", f(31337)) print("f(f(31337)) = ", f(f(31337)))
a = 7904681699700731398014734140051852539595806699214201704996640156917030632322659247608208994194840235514587046537148300460058962186080655943804500265088604049870276334033409850015651340974377752209566343260236095126079946537115705967909011471361527517536608234561184232228641232031445095605905800675590040729 b = 16276123569406561065481657801212560821090379741833362117064628294630146690975007397274564762071994252430611109538448562330994891595998956302505598671868738461167036849263008183930906881997588494441620076078667417828837239330797541019054284027314592321358909551790371565447129285494856611848340083448507929914 f(31337) = 52926479498929750044944450970022719277159248911867759992013481774911823190312079157541825423250020665153531167070545276398175787563829542933394906173782217836783565154742242903537987641141610732290449825336292689379131350316072955262065808081711030055841841406454441280215520187695501682433223390854051207100 f(f(31337)) = 65547980822717919074991147621216627925232640728803041128894527143789172030203362875900831296779973655308791371486165705460914922484808659375299900737148358509883361622225046840011907835671004704947767016613458301891561318029714351016012481309583866288472491239769813776978841785764693181622804797533665463949
modulusを求める問題です。0 mod pとなる式を二つ作りgcdを取ります。今回はgcdがそのままmodulusとなりました。
#solver.py import math from Crypto.Util.number import * a = (中略) b = (中略) f = (中略) #f(31337) ff = (中略) #f(f(31337)) print(long_to_bytes(math.gcd(a*31337+b-f,a*f+b-ff)))
corctf{1n_m4th3m4t1c5,_th3_3ucl1d14n_4lg0r1thm_1s_4n_3ff1c13nt_m3th0d_
f0r_c0mput1ng_th3_GCD_0f_tw0_1nt3g3rs}
luckyguess
i hope you're feeling lucky today
nc be.ax 31800
添付ファイル:luckyguess.py
#luckyguess.py #!/usr/local/bin/python from random import getrandbits p = 2**521 - 1 a = getrandbits(521) b = getrandbits(521) print("a =", a) print("b =", b) try: x = int(input("enter your starting point: ")) y = int(input("alright, what's your guess? ")) except: print("?") exit(-1) r = getrandbits(20) for _ in range(r): x = (x * a + b) % p if x == y: print("wow, you are truly psychic! here, have a flag:", open("flag.txt").read()) else: print("sorry, you are not a true psychic... better luck next time")
線形合同法のような形でr回計算した値を予想します。rが20bitと小さい値だ、と思いましたが1/rを引くまで回すのは…。
x = ax + b mod p となるxを渡せば何回計算しようとも同じ値になります。
x = ax + b mod p (a-1)x = -b mod p x = -b/(a-1) mod p
#solver.py from Crypto.Util.number import * import socket # --- common funcs --- def sock(remoteip, remoteport): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((remoteip, remoteport)) return s, s.makefile('rw') def read_until(f, delim='\n'): data = '' while not data.endswith(delim): data += f.read(1) return data HOST, PORT = "be.ax", 31800 p = 2**521 - 1 s, f = sock(HOST, PORT) a = int(read_until(f).split()[-1]) b = int(read_until(f).split()[-1]) x = ((p-b) * inverse(a-1,p))%p read_until(f,"point: ") s.send(str(x).encode()+b"\n") read_until(f,"guess? ") s.send(str(x).encode()+b"\n") flag = read_until(f).strip() print(flag)
corctf{r34l_psych1c5_d0nt_n33d_f1x3d_p01nt5_t0_tr1ck_th15_lcg!}
exchanged
you could make an exchange out of this
添付ファイル:exchanged.py, output.txt
#exchanged.py from Crypto.Util.number import * from Crypto.Cipher import AES from Crypto.Util.Padding import pad from hashlib import sha256 from secrets import randbelow p = 142031099029600410074857132245225995042133907174773113428619183542435280521982827908693709967174895346639746117298434598064909317599742674575275028013832939859778024440938714958561951083471842387497181706195805000375824824688304388119038321175358608957437054475286727321806430701729130544065757189542110211847 a = randbelow(p) b = randbelow(p) s = randbelow(p) print("p =", p) print("a =", a) print("b =", b) print("s =", s) a_priv = randbelow(p) b_priv = randbelow(p) def f(s): return (a * s + b) % p def mult(s, n): for _ in range(n): s = f(s) return s A = mult(s, a_priv) B = mult(s, b_priv) print("A =", A) print("B =", B) shared = mult(A, b_priv) assert mult(B, a_priv) == shared flag = open("flag.txt", "rb").read() key = sha256(long_to_bytes(shared)).digest()[:16] iv = long_to_bytes(randint(0, 2**128)) cipher = AES.new(key, AES.MODE_CBC, iv=iv) print(iv.hex() + cipher.encrypt(pad(flag, 16)).hex())
p = 142031099029600410074857132245225995042133907174773113428619183542435280521982827908693709967174895346639746117298434598064909317599742674575275028013832939859778024440938714958561951083471842387497181706195805000375824824688304388119038321175358608957437054475286727321806430701729130544065757189542110211847 a = 118090659823726532118457015460393501353551257181901234830868805299366725758012165845638977878322282762929021570278435511082796994178870962500440332899721398426189888618654464380851733007647761349698218193871563040337609238025971961729401986114391957513108804134147523112841191971447906617102015540889276702905 b = 57950149871006152434673020146375196555892205626959676251724410016184935825712508121123309360222777559827093965468965268147720027647842492655071706063669328135127202250040935414836416360350924218462798003878266563205893267635176851677889275076622582116735064397099811275094311855310291134721254402338711815917 s = 35701581351111604654913348867007078339402691770410368133625030427202791057766853103510974089592411344065769957370802617378495161837442670157827768677411871042401500071366317439681461271483880858007469502453361706001973441902698612564888892738986839322028935932565866492285930239231621460094395437739108335763 A = 27055699502555282613679205402426727304359886337822675232856463708560598772666004663660052528328692282077165590259495090388216629240053397041429587052611133163886938471164829537589711598253115270161090086180001501227164925199272064309777701514693535680247097233110602308486009083412543129797852747444605837628 B = 132178320037112737009726468367471898242195923568158234871773607005424001152694338993978703689030147215843125095282272730052868843423659165019475476788785426513627877574198334376818205173785102362137159225281640301442638067549414775820844039938433118586793458501467811405967773962568614238426424346683176754273 e0364f9f55fc27fc46f3ab1dc9db48fa482eae28750eaba12f4f76091b099b01fdb64212f66caa6f366934c3b9929bad37997b3f9d071ce3c74d3e36acb26d6efc9caa2508ed023828583a236400d64e
Diffie-Hellman鍵共有のような形で、shared = mult(A, b_priv) = mult(B, a_priv) となっており、これが分かればkeyを導出できAES暗号化されたflagを復号できます。A, Bは与えられていることから、a_priv or b_privを求めていきます。
mult関数を見ていきます。線形合同法のような計算をn(=a_priv or b_priv)回行います。nはかなり大きい数字のため、O(n)となる逐次処理では計算が終わりません。ゆえに、mult関数を高速化最適化を行います。
mult(s, 1) = as+b mod p mult(s, 2) = a(as+b) + b = (a^2)s +ab + b mod p mult(s, 3) = a((a^2)s+ab+b) + b = (a^3)s + (a^2)b +ab + b mod p -> mult(s, n) = (a^n)s + (a^(n-1))b + (a^(n-2))b + ... + ab + b = (a^n)s + b(((a^n)-1) / (a-1)) mod p
上記のように、規則性と等比数列の和の公式を使って高速化できる式となりました。ここからn(=a_priv or b_priv)を求めていきます。つまりnの式を作るよう変形すると、
mult(s, n) = A = (a^n)s + b(((a^n)-1) / (a-1)) mod p (a-1)A = (a^n)(a-1)s + (a^n)b - b mod p (as-a+b)(a^n) = (a-1)A + b mod p (a^n) = ((a-1)A+b) / (as-a+b) mod p
右辺は既知の変数のみで構成されているので計算できます。あとは、(右辺の値) = an mod pとなる値を求める過程だけなのですが、離散対数問題を解くということとなります。
p-1が素因数分解可能かつ小さい素数のみで構成されている場合、Pohlig-Hellman algorithmを用いて離散対数問題を解くことができます。
p-1を素因数分解してみると、小さい素数のみで構成されていたので、Pohlig-Hellman algorithmを使えそうです。
sageのdiscrete_logにはこのアルゴリズムが搭載されているようなので、あとはsageに任せます。
#sage 離散対数問題を解く print(discrete_log( Mod(右辺値, p), Mod(a, p) )
from Crypto.Util.number import * import math import gmpy2 from Crypto.Cipher import AES from Crypto.Util.Padding import pad from hashlib import sha256 p = (中略) a = (中略) b = (中略) s = (中略) A = (中略) B = (中略) enc = (中略) def mult(s, n): deno = inverse(a-1,p) t = ((pow(a,n,p)-1)*deno)%p return (pow(a,n,p)*s + b*t)%p An = ((A*(a-1)+b) * inverse((a-1)*s+b,p))%p Bn = ((B*(a-1)+b) * inverse((a-1)*s+b,p))%p print(An) print(Bn) #--- b_priv = 72294308363142635191285137067271613704518381689222403756427895774621470359587678998089163134800904011887080683864659561441585487866597353332892511005327144670737921076740544522591181991307617152388962472418690715521378068114703790176987564145359661515790454979281591952064634314009219343525018858317272168846 assert mult(s,b_priv) == B shared2 = mult(A, b_priv) print(shared2) key = sha256(long_to_bytes(shared2)).digest()[:16] iv = bytes.fromhex(enc[:32]) cipher = AES.new(key, AES.MODE_CBC, iv=iv) print(cipher.decrypt(bytes.fromhex(enc[32:])))
corctf{th1s_lcg_3xch4ng3_1s_4_l1ttl3_1ns3cur3_f0r_n0w}
hidE
This RSA encryption service is so secure we're not even going to tell you how we encrypted it
nc be.ax 31124
添付ファイル:main.py
#main.py #!/usr/local/bin/python import random import time import math import binascii from Crypto.Util.number import * p, q = getPrime(512), getPrime(512) n = p * q phi = (p - 1) * (q - 1) flag = open('./flag.txt').read().encode() random.seed(int(time.time())) def encrypt(msg): e = random.randint(1, n) while math.gcd(e, phi) != 1: e = random.randint(1, n) pt = bytes_to_long(msg) ct = pow(pt, e, n) return binascii.hexlify(long_to_bytes(ct)).decode() def main(): print('Secure Encryption Service') print('Your modulus is:', n) while True: print('Options') print('-------') print('(1) Encrypt flag') print('(2) Encrypt message') print('(3) Quit') x = input('Choose an option: ') if x not in '123': print('Unrecognized option.') exit() elif x == '1': print('Here is your encrypted flag:', encrypt(flag)) elif x == '2': msg = input('Enter your message in hex: ') print('Here is your encrypted message:', encrypt(binascii.unhexlify(msg))) elif x == '3': print('Bye') exit() if __name__ == '__main__': main()
UNIXTIMEの小数点以下を切り捨てた整数をシードとし、暗号化の要求があるたびに生成した乱数をeとしています(phiと互いに素でなければ、互いに素になるまで生成する)。flagの暗号文と任意の整数の暗号化を何回でも取得できます。
まず、シードは予測できそうです。ncするタイミングで±10秒あたりを見れば問題なさそうです。
eが1~nの間なので、かなり大きくなる可能性があるのでWiener's Attackかと思って進めていきましたが、e/nが0.96の場合でもうまくいかずdが小さくなってないのでしょう…。何かないかといつもの資料を見ていると、Common Modulus Attackなのでは?となりました。
www.slideshare.net
Common Modulus Attackは前半で出てくるイメージでしたが、だからといって気付くのが遅いなぁと反省。flagの暗号文を2つ取得し、シードをbrute forceして乱数を得てこの攻撃を行います。
#solver.py from Crypto.Util.number import * import socket import time import random import gmpy2 # --- common funcs --- def sock(remoteip, remoteport): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((remoteip, remoteport)) return s, s.makefile('rw') def read_until(f, delim='\n'): data = '' while not data.endswith(delim): data += f.read(1) return data HOST, PORT = "be.ax", 31124 while True: try: tim = int(time.time()) s, f = sock(HOST, PORT) read_until(f) n = int(read_until(f).split()[-1]) print(f"n = {n}") for _ in range(5): read_until(f) read_until(f,"option: ") s.send(b"1\n") c1 = int(read_until(f).split()[-1],16) for _ in range(5): read_until(f) read_until(f,"option: ") s.send(b"1\n") c2 = int(read_until(f).split()[-1],16) for i in range(20): random.seed(tim) e_para = [] for k in range(100): e = random.randint(1, n) e_para.append(e) for i1 in range(len(e_para)-1): for i2 in range(i1+1,len(e_para)): e1 = e_para[i1] e2 = e_para[i2] g,s1,s2 = gmpy2.gcdext(e1,e2) g,s1,s2 = int(g), int(s1), int(s2) assert e1*s1 + e2*s2 == g if g == 1: m = (pow(c1,s1,n)*pow(c2,s2,n))%n flag = long_to_bytes(m) if b"corctf" in flag: print(flag) exit() tim += 1 except: pass
Wiener's Attackじゃないかと模索している間、この攻撃がpypiで実装されているのを発見して驚いてました(笑)
corctf{y34h_th4t_w4snt_v3ry_h1dd3n_tbh_l0l}
generous
Let me introduce you to this nice oracle I found...
nc be.ax 31244
添付ファイル:generous.py
#generous.py #!/usr/local/bin/python from Crypto.Util.number import getPrime, inverse, bytes_to_long from random import randrange with open("flag.txt", "rb") as f: flag = f.read().strip() def gen_keypair(): p, q = getPrime(512), getPrime(512) n = (p**2) * q while True: g = randrange(2, n) if pow(g, p-1, p**2) != 1: break h = pow(g, n, n) return (n, g, h), (g, p, q) def encrypt(pubkey, m): n, g, h = pubkey r = randrange(1, n) c = pow(g, m, n) * pow(h, r, n) % n return c def decrypt(privkey, c): g, p, q = privkey a = (pow(c, p-1, p**2) - 1) // p b = (pow(g, p-1, p**2) - 1) // p m = a * inverse(b, p) % p return m def oracle(privkey, c): m = decrypt(privkey, c) return m % 2 pub, priv = gen_keypair() n, g, h = pub print(f"Public Key:\n{n = }\n{g = }\n{h = }") print(f"Encrypted Flag: {encrypt(pub, bytes_to_long(flag))}") while True: inp = int(input("Enter ciphertext> ")) print(f"Oracle result: {oracle(priv, inp)}")
まずは中のアルゴリズムをまとめていきます。
暗号化 n = ppq h = g^n mod n c = g^m × h^r = g^m × (g^n)^r = g^(m+nr) mod n 復号 a = ((c^(p-1) mod p^2)-1) / p b = ((g^(p-1) mod p^2)-1) / p m = a × b^(-1) mod p oracle 公開鍵をくれる flagの暗号文をくれる 任意の暗号文を復号し、その下位1bitだけ教えてくれる
おそらくflagを1bitずつ求めていって復元するという形になると予想されます。
どっかで見たことあるなぁなんか名前あったなぁという暗号化方式で、自分の記憶を頼りにWaniCTFだったかなとはてなブログを遡ると…見つけました!
WaniCTF'21-spring Crypto Writeup - Attack All Around
暗号化方式は同じで復号oracleも存在しているのですが、この問題はflag以外の暗号文は全て復号してくれるという問題でした。cをflagの暗号文とすると、c' = c2 mod nを復号してもらうと2×flagとなり、半分で割ってlong_to_bytes()していました。それにしても、1年前の自分はちゃんと復号される証明までできていたのに今は分からなくて退化してる…。
この問題のflagより、この暗号化方式はOkamoto-Uchiyama cryptosystem(以後、OU)という名前のようです。
Okamoto–Uchiyama cryptosystem - Wikipedia
この名前でググるとほとんどCTFのwriteupなのですが、n=pppとしている問題かこのWaniCTFのものでした。
こちらのwriteupを参考にすると、OUは準同型暗号のようで平文x+yの暗号文はxの暗号文とyの暗号文の積となるようです。
それで2mの暗号文は、f(m+m) = f(m)×f(m) = c×c = c2 mod n なのかとようやく理解しました。OUCSはほとんどの方が、m+1の暗号文を作成してサーバに復号させていたようです。
以上より、m' = m//2 の暗号文をどうにか作れないかと式をたくさん書いた紙とにらめっこします。
m / 2 = m × 2^(-1) = m + m + ... + m mod n c' = c^(2^(-1)) mod n
このようにして、復号するとm/2となる暗号文c'を作成できました。しかしmが奇数である場合についても考える必要があります。mの下位1bitが分かれば次は2bit目です。1bit目が1である場合、(m-1)/2の暗号文を導出すれば2bit目が分かります。
(m-1)/2 = (m-1) × 2^(-1) = (m+(n-1)) × 2^(-1) = (m+(n-1)) + (m+(n-1)) + ... + (m+(n-1)) mod n c'' : (n-1)の暗号文 c'' = g^(n-1+nr) mod n c' = (c × c'')^(2^(-1)) mod n
cを復号して下位1bitが0ならm/2の暗号文を作り、下位1bitが1なら(m-1)/2の暗号文を作り、サーバによって暗号文を復号して下位1bitを得る。というのを繰り返してflagを復元していきます。
#solver.py from Crypto.Util.number import * import socket # --- common funcs --- def sock(remoteip, remoteport): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((remoteip, remoteport)) return s, s.makefile('rw') def read_until(f, delim='\n'): data = '' while not data.endswith(delim): data += f.read(1) return data def make_flag(m): while len(m)%8: m += "0" m = int(m[::-1],2) print(long_to_bytes(m)) HOST, PORT = "be.ax", 31244 s, f = sock(HOST, PORT) read_until(f) n = int(read_until(f).split()[-1]) g = int(read_until(f).split()[-1]) h = int(read_until(f).split()[-1]) enc = int(read_until(f).split()[-1]) inv2 = inverse(2,n) n1enc = (pow(g,n-1,n)*pow(h,35,n))%n enc2 = (pow(g,inv2,n)*pow(h,35,n))%n flag = "" for i in range(1000): read_until(f,"ciphertext> ") s.send(str(enc).encode()+b"\n") ru = read_until(f) bits = ru.split()[-1] flag += bits if bits == "1": enc = (enc*n1enc)%n enc = pow(enc,inv2,n) make_flag(flag)
corctf{see?1_bit_is_very_generous_of_me}
corCTF 2021 : corCTF 2021 writeup - Attack All Around
leapfrog面白い問題なのですが、解けなくて悔しい…。