<center>
<h1>KCSC Recruitment 2026 </h1>
</center>

# Mini RSA
Bài khởi động
chall.py:
```py=
from Crypto.Util.number import getPrime, bytes_to_long
from hashlib import sha256
# fake flag, complete 4 phases below to get the real one
FLAG = b"KCSC{676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767676767}"
FLAG = FLAG[5:-1]
assert all(c in b"0123456789abcdef" for c in FLAG) # hex-only
assert len(FLAG) == 162
part1 = FLAG[0 : 37]
part2 = FLAG[37 : 91]
part3 = FLAG[91 : 155]
part4 = FLAG[155: 162]
assert part1 + part2 + part3 + part4 == FLAG
# Phase 1:
assert len(part1) == 37
n1 = getPrime(1024) * getPrime(1024)
flag1 = bytes_to_long(part1)
assert flag1 < n1
e1 = 7
c1 = pow(flag1, e1, n1)
print("Phase 1:")
print(f"n = {n1}")
print(f"e = {e1}")
print(f"c = {c1}\n")
# n = 24768551472661189646803798025735994729675923012939876254832364742560290611235358539402960915109731271068761785643095443054780548009656705453830110379444700418833089171817064649224391362714139414078789526050887157582406524961283991222714583389641534201051586959980157906704312629985265485739450352043789888921235279589151315468070396525574513463528110969355058846977730601522857753194783784154604698121483122903344163960138046879651018970211352779930792196553883676319989732719640639947477887444507816814644473808320811170489238150100695729301133978725734344966342539008392521368848055667562401810828901055904263751087
# e = 7
# c = 18095221808696767364482784106578665305597072892957003660200677125082480938590759416561596931362184836970227059449018023231775268023936196618166559695219893530882687916034256857200503922379924700258829452303164421775991854747192890323693977001251087329555558918620707751930767202265420598175188863386691087599780498257684594011697328525563218286665773793900960107693859824245088548197358663304146296383128902903029994359803481927306296829608298387122321098870479487155445811455194372706211665184810020606934406988332111485009183488421535000282713153719587883866210474448900196771807191538210239194200766493898209050827
# Phase 2:
assert len(part2) == 54
moduli = [getPrime(500) * getPrime(500) for _ in range(3)]
flag2 = bytes_to_long(part2)
assert flag2 < min(moduli)
e2 = 7
ciphertexts = [pow(flag2, e2, moduli[i]) for i in range(3)]
print("Phase 2:")
print(f"ns = {moduli}")
print(f"e = {e2}")
print(f"cs = {ciphertexts}\n")
# ns = [5776746251180954410501143373555071446651309076079023801239927839678110284298053445067843385733923949632510804583576555583870535387441316658685840687689240200699587579390668561563937127040444465254726625310777306158594065224927251973217783491441064524351035802071558210418452732784328906404928346757561, 6676568530944751773705274371029619538616953853214731842955709009914939432096974301535527889053677971564433108995378797418249594276219798862529871540959246847221996855818534102541008885671426716581485932426758973143450314953430892380665585863829711961246704347425347121483579347058369739426135845646693, 4192125436309381894285355753973720387617056931265026941800223776464084287332759678003685944684491135983930538940594703444960195450172411842880979937537165579234724095471901440019430531228826628339513666609406387657919179589286169600368614047778141005381080248940651068046222358006840623976659320922801]
# e = 7
# cs = [4162314281829070969983785868478817487122811901286983843325224202348302016796815815333452464672872912888701572281242789929104022254953168740731678111433971515437492093542686431714890537676381271531800973763109092820046704937574547000215411647046072684054960320518397352086234888330336947666810104929210, 50764199787126347289275452914702574991784396905310732416007230764340015964909418575803553215557208257135086801048181610281948813886172483845338416063595514468150336763299168160503561124717579857540938345665633478359717032027020484374804577083335729646127615429489801214833042281198632815600258496021, 2226866454269961169554477487076928558283277297558185848127271094461892302315098156482036398655353603737228883411877291393627455878900738358485648657420475192577890517627929950639476113316539806453385202418270139866437710517550467476876937820556775982947361645302515541452549533744585394098458949714224]
# Phase 3:
assert len(part3) == 64
p3 = getPrime(512)
q3 = getPrime(512)
n3 = p3 * q3
flag3 = bytes_to_long(part3)
assert flag3 < n3
phi3 = (p3 - 1) * (q3 - 1)
e3 = 0x10001
E = getPrime(16)
assert e3 != E
d3 = pow(E, -1, phi3) # I'm so generous that I will give you d directly :)
c3 = pow(flag3, e3, n3)
print("Phase 3:")
print(f"n = {n3}")
print(f"e = {e3}")
print(f"E = {E}")
print(f"d = {d3}")
print(f"c = {c3}\n")
# n = 58918298105520625812936595965949791679948485672063422501379350174567652137330835174097919449869935351283983373480427262310194532256798429690185381024326970315021350533262412067581291819891395690548362953546838296879197806387922608866341956665531154816197564814442845602714598090456294723302800178858587739713
# e = 65537
# E = 39119
# d = 47540992349005816963249153906419008010878957801546101159437592685401670280805946273432368604390309296333723642831097076480004356212731471488808547049589210685661344692631483342927181931551362472531714993510869363985573142465639097423475598977423653768735951445358570563443850030507199283981083278104055782079
# c = 37398307787309593195399039608335293998782975977344821375594074480912755773091240878727726167860952796427287138264423555320002390940336425049899775688041256572345243342480573394310574660551049821001839500881785836377907724697125451771009902048018563262095797500165548399273323153654585302502917216971632506342
# Phase 4:
assert len(part4) == 7
# hash of the flag
Flag_hash = sha256(FLAG).hexdigest()
print("Phase 4:")
print(f"{Flag_hash = }")
# Flag_hash = 6c65e2cc4142210eac7503a24a2d48205aeaf8686407c70fde01478336853281
```
Một bài RSA gồm nhiều lỗ hỏng khác nhau với flag được chia ra thành 4 phần khác nhau, nên ta sẽ đi tìm từng phần để thu được flag hoàn chỉnh.
Phase 1:
```py=
assert len(part1) == 37
n1 = getPrime(1024) * getPrime(1024)
flag1 = bytes_to_long(part1)
assert flag1 < n1
e1 = 7
c1 = pow(flag1, e1, n1)
print("Phase 1:")
print(f"n = {n1}")
print(f"e = {e1}")
print(f"c = {c1}\n")
# n = 24768551472661189646803798025735994729675923012939876254832364742560290611235358539402960915109731271068761785643095443054780548009656705453830110379444700418833089171817064649224391362714139414078789526050887157582406524961283991222714583389641534201051586959980157906704312629985265485739450352043789888921235279589151315468070396525574513463528110969355058846977730601522857753194783784154604698121483122903344163960138046879651018970211352779930792196553883676319989732719640639947477887444507816814644473808320811170489238150100695729301133978725734344966342539008392521368848055667562401810828901055904263751087
# e = 7
# c = 18095221808696767364482784106578665305597072892957003660200677125082480938590759416561596931362184836970227059449018023231775268023936196618166559695219893530882687916034256857200503922379924700258829452303164421775991854747192890323693977001251087329555558918620707751930767202265420598175188863386691087599780498257684594011697328525563218286665773793900960107693859824245088548197358663304146296383128902903029994359803481927306296829608298387122321098870479487155445811455194372706211665184810020606934406988332111485009183488421535000282713153719587883866210474448900196771807191538210239194200766493898209050827
```
$part_1$ được mã hóa theo công thức:
$$C \equiv part_1^{e_1} \pmod {N_1}$$
Độ dài của $part_1$ là 37 byte, vậy nó sẽ có 296 bit, khi mũ 7 lên thì sẽ có giá trị khoảng từ 2066 đến 2072 bit. Còn $N_1$ giá trị là 2048 bit.
Vậy ta có thể hoàn toàn brute-force tìm được k sao cho:
$$C_1 + k.N_1 = part_1^{e_1}$$
Tương đương
$$part_1 = \sqrt [e_1] {C_1 + k.N_1}$$
Ta sẽ brute-force với k bắt đầu là 1 và cứ tăng giá trị lên cho đến khi $part_1$ là một số nguyên.
```py=
#part 1
n1 = 24768551472661189646803798025735994729675923012939876254832364742560290611235358539402960915109731271068761785643095443054780548009656705453830110379444700418833089171817064649224391362714139414078789526050887157582406524961283991222714583389641534201051586959980157906704312629985265485739450352043789888921235279589151315468070396525574513463528110969355058846977730601522857753194783784154604698121483122903344163960138046879651018970211352779930792196553883676319989732719640639947477887444507816814644473808320811170489238150100695729301133978725734344966342539008392521368848055667562401810828901055904263751087
c1 = 18095221808696767364482784106578665305597072892957003660200677125082480938590759416561596931362184836970227059449018023231775268023936196618166559695219893530882687916034256857200503922379924700258829452303164421775991854747192890323693977001251087329555558918620707751930767202265420598175188863386691087599780498257684594011697328525563218286665773793900960107693859824245088548197358663304146296383128902903029994359803481927306296829608298387122321098870479487155445811455194372706211665184810020606934406988332111485009183488421535000282713153719587883866210474448900196771807191538210239194200766493898209050827
e1 = 7
k = 0
check_part1 = iroot(c1 + k*n1, e1)[1]
part1 = 0
while (check_part1 == False):
check_part1 = iroot(c1 + k*n1, e1)[1]
part1 = iroot(c1 + k*n1, e1)[0]
k += 1
```
Phase 2:
```py=
# Phase 2:
assert len(part2) == 54
moduli = [getPrime(500) * getPrime(500) for _ in range(3)]
flag2 = bytes_to_long(part2)
assert flag2 < min(moduli)
e2 = 7
ciphertexts = [pow(flag2, e2, moduli[i]) for i in range(3)]
print("Phase 2:")
print(f"ns = {moduli}")
print(f"e = {e2}")
print(f"cs = {ciphertexts}\n")
# ns = [5776746251180954410501143373555071446651309076079023801239927839678110284298053445067843385733923949632510804583576555583870535387441316658685840687689240200699587579390668561563937127040444465254726625310777306158594065224927251973217783491441064524351035802071558210418452732784328906404928346757561, 6676568530944751773705274371029619538616953853214731842955709009914939432096974301535527889053677971564433108995378797418249594276219798862529871540959246847221996855818534102541008885671426716581485932426758973143450314953430892380665585863829711961246704347425347121483579347058369739426135845646693, 4192125436309381894285355753973720387617056931265026941800223776464084287332759678003685944684491135983930538940594703444960195450172411842880979937537165579234724095471901440019430531228826628339513666609406387657919179589286169600368614047778141005381080248940651068046222358006840623976659320922801]
# e = 7
# cs = [4162314281829070969983785868478817487122811901286983843325224202348302016796815815333452464672872912888701572281242789929104022254953168740731678111433971515437492093542686431714890537676381271531800973763109092820046704937574547000215411647046072684054960320518397352086234888330336947666810104929210, 50764199787126347289275452914702574991784396905310732416007230764340015964909418575803553215557208257135086801048181610281948813886172483845338416063595514468150336763299168160503561124717579857540938345665633478359717032027020484374804577083335729646127615429489801214833042281198632815600258496021, 2226866454269961169554477487076928558283277297558185848127271094461892302315098156482036398655353603737228883411877291393627455878900738358485648657420475192577890517627929950639476113316539806453385202418270139866437710517550467476876937820556775982947361645302515541452549533744585394098458949714224]
```
$part_2$ được mã hóa cùng 1 e nhưng với nhiều modulo $N$ khác nhau (cụ thể là 3) , vậy ta có thể lập được hệ phương trình đồng dư như sau:
\begin{cases}
part_2^{e} \equiv C_1 \pmod{N_1}\\
part_2^{e} \equiv C_2 \pmod{N_2}\\
part_2^{e} \equiv C_3 \pmod{N_3}
\end{cases}
Giải hệ trên bằng thuật toán CRT (Chinese Remainder Theorem) sẽ thu được:
$$M \equiv part_2^e \pmod N$$
- với $N = N_1.N_2.N_3$
Nếu $part_2^e < N$ thì đến đây ta có thể rút căn 2 vế để thu được $part_2 = \sqrt[e]{M}$
Nhưng trong challenge này $part_2^e$ sẽ khoảng 3024 bit, lớn hơn $N$ (khoảng 3000 bit) nên vẫn phải áp dụng kỹ thuật brute-force tìm k thỏa $M + k.N = part_2^{e}$ như Phase 1 để tìm được $part_2$.
```py=
# part 2
ns2 = [5776746251180954410501143373555071446651309076079023801239927839678110284298053445067843385733923949632510804583576555583870535387441316658685840687689240200699587579390668561563937127040444465254726625310777306158594065224927251973217783491441064524351035802071558210418452732784328906404928346757561, 6676568530944751773705274371029619538616953853214731842955709009914939432096974301535527889053677971564433108995378797418249594276219798862529871540959246847221996855818534102541008885671426716581485932426758973143450314953430892380665585863829711961246704347425347121483579347058369739426135845646693, 4192125436309381894285355753973720387617056931265026941800223776464084287332759678003685944684491135983930538940594703444960195450172411842880979937537165579234724095471901440019430531228826628339513666609406387657919179589286169600368614047778141005381080248940651068046222358006840623976659320922801]
cs2 = [4162314281829070969983785868478817487122811901286983843325224202348302016796815815333452464672872912888701572281242789929104022254953168740731678111433971515437492093542686431714890537676381271531800973763109092820046704937574547000215411647046072684054960320518397352086234888330336947666810104929210, 50764199787126347289275452914702574991784396905310732416007230764340015964909418575803553215557208257135086801048181610281948813886172483845338416063595514468150336763299168160503561124717579857540938345665633478359717032027020484374804577083335729646127615429489801214833042281198632815600258496021, 2226866454269961169554477487076928558283277297558185848127271094461892302315098156482036398655353603737228883411877291393627455878900738358485648657420475192577890517627929950639476113316539806453385202418270139866437710517550467476876937820556775982947361645302515541452549533744585394098458949714224]
e2 = 7
c3 = 37398307787309593195399039608335293998782975977344821375594074480912755773091240878727726167860952796427287138264423555320002390940336425049899775688041256572345243342480573394310574660551049821001839500881785836377907724697125451771009902048018563262095797500165548399273323153654585302502917216971632506342
N = 1
for n in ns2:
N *= n
M = crt(ns2, cs2)[0]
k2 = 0
check_part1 = iroot(M + k2*N, e2)[1]
part2 = 0
while (check_part1 == False):
check_part1 = iroot(M + k2*N, e2)[1]
part2 = iroot(M + k2*N, e2)[0]
k2 += 1
```
Phase 3:
```py=
# Phase 3:
assert len(part3) == 64
p3 = getPrime(512)
q3 = getPrime(512)
n3 = p3 * q3
flag3 = bytes_to_long(part3)
assert flag3 < n3
phi3 = (p3 - 1) * (q3 - 1)
e3 = 0x10001
E = getPrime(16)
assert e3 != E
d3 = pow(E, -1, phi3) # I'm so generous that I will give you d directly :)
c3 = pow(flag3, e3, n3)
print("Phase 3:")
print(f"n = {n3}")
print(f"e = {e3}")
print(f"E = {E}")
print(f"d = {d3}")
print(f"c = {c3}\n")
# n = 58918298105520625812936595965949791679948485672063422501379350174567652137330835174097919449869935351283983373480427262310194532256798429690185381024326970315021350533262412067581291819891395690548362953546838296879197806387922608866341956665531154816197564814442845602714598090456294723302800178858587739713
# e = 65537
# E = 39119
# d = 47540992349005816963249153906419008010878957801546101159437592685401670280805946273432368604390309296333723642831097076480004356212731471488808547049589210685661344692631483342927181931551362472531714993510869363985573142465639097423475598977423653768735951445358570563443850030507199283981083278104055782079
# c = 37398307787309593195399039608335293998782975977344821375594074480912755773091240878727726167860952796427287138264423555320002390940336425049899775688041256572345243342480573394310574660551049821001839500881785836377907724697125451771009902048018563262095797500165548399273323153654585302502917216971632506342
```
Ta được cho khóa bí mật $d$ luôn, nhưng đừng nhầm lẫn, đó không phải là khóa bí mật thật sự, khóa $d$ thật sự là nghịch đảo của $e$ trong modulo $\phi(n)$, còn ở đây $d$ là nghịch đảo của $E$ trong $\phi(n)$.
Vậy ta phải tính được khóa $d$ thật sự từ giá trị $d$ fake này.
Ta biết rằng:
$$E.d_{fake} \equiv 1 \pmod{\phi(n)}$$
Vậy
$$E.d_{fake} - 1 = k \cdot \phi(n)$$
Do đó ta sẽ có thể tính được $\phi(n)$:
$$\phi(n) = \frac{E.d_{fake} - 1}{k}$$
Nhưng ta không biết $k$ nên chưa tính được (lúc diễn ra giải thì mình đi brute-force $k$ luôn)
Cụ thể là ta có thể tính $k$ như sau:
$$k = \frac{d_{fake} \cdot E - 1}{\phi(n)}$$
Vì $d_{fake}$ là nghịch đảo modulo $\phi(n)$, nên $d_{fake} < \phi(n)$.
Vậy thì $$k < \frac{\phi(n) \cdot E}{\phi(n)} $$
Do đó: $k < E$
Mà bài này cho $E = 39119$. Rất nhỏ nên brute-force bình thường.
Sau đó mình ngỗi ngẫm lại thì nhận ra có 1 cách tính trực tiếp, cụ thể:
Ta có:
$$\phi(n) = (p-1)(q-1) = n - p - q + 1$$
Vì $p, q \approx \sqrt{n}$, nên $p+q$ rất nhỏ so với $n$. Do đó:$$\phi(n) \approx n$$
Nên phương trình $E.d_{fake} - 1 = k \cdot \phi(n)$ sẽ trở thành:
$$E.d_{fake} - 1 \approx k \cdot n$$
Vậy ta có thể tính $k$ bằng phép chia nguyên: $$k = (E.d_{fake}) // n$$
Vì sai số rất nhỏ nên chỉ cần kiểm tra vài giá trị xung quanh $k$
```py=
# part 3
n3 = 58918298105520625812936595965949791679948485672063422501379350174567652137330835174097919449869935351283983373480427262310194532256798429690185381024326970315021350533262412067581291819891395690548362953546838296879197806387922608866341956665531154816197564814442845602714598090456294723302800178858587739713
e3 = 65537
E = 39119
d_fake = 47540992349005816963249153906419008010878957801546101159437592685401670280805946273432368604390309296333723642831097076480004356212731471488808547049589210685661344692631483342927181931551362472531714993510869363985573142465639097423475598977423653768735951445358570563443850030507199283981083278104055782079
c3 = 37398307787309593195399039608335293998782975977344821375594074480912755773091240878727726167860952796427287138264423555320002390940336425049899775688041256572345243342480573394310574660551049821001839500881785836377907724697125451771009902048018563262095797500165548399273323153654585302502917216971632506342
k_fake = (d_fake*E - 1) // n3
phi = (d_fake * E - 1) // k_fake
for k in range(k_fake - 2, k_fake + 3):
if k == 0: continue
if (d_fake * E - 1) % k == 0:
phi = (d_fake * E - 1) // k
d = pow(e3, -1, phi)
part3 = long_to_bytes(pow(c3, d, n3))
```
Phase 4:
```py=
# Phase 4:
assert len(part4) == 7
# hash of the flag
Flag_hash = sha256(FLAG).hexdigest()
print("Phase 4:")
print(f"{Flag_hash = }")
# Flag_hash = 6c65e2cc4142210eac7503a24a2d48205aeaf8686407c70fde01478336853281
```
Ta được biết giá trị của $part_4$ sau khi đi qua hàm băm `sha256`
Hàm này đương nhiên không có cách nào tính ngược lại rồi nên phải đi brute-force, vì $part_4$ chỉ dài 7 byte mà ta biết flag chỉ chứa các ký tự `0123456789abcdef` nên ta sẽ phải duyệt khoảng $16^7$ trường hợp (vẫn khả thi)
```py=
# part 4
hash = "6c65e2cc4142210eac7503a24a2d48205aeaf8686407c70fde01478336853281"
chars = "0123456789abcdef"
for p in product(chars, repeat=7):
flag = b"KCSC{" + part1 + part2 + part3 + "".join(p).encode() + b"}"
if(sha256(flag).hexdigest() == hash):
print(flag.decode())
break
```
Vậy ta đã tìm được đầy đủ flag, ghép chúng lại là có flag hoàn chỉnh.
full solve:
```py=
from Crypto.Util.number import *
from sympy.ntheory.modular import crt
from gmpy2 import iroot
from hashlib import sha256
from itertools import product
from tqdm import tqdm
#part 1
n1 = 24768551472661189646803798025735994729675923012939876254832364742560290611235358539402960915109731271068761785643095443054780548009656705453830110379444700418833089171817064649224391362714139414078789526050887157582406524961283991222714583389641534201051586959980157906704312629985265485739450352043789888921235279589151315468070396525574513463528110969355058846977730601522857753194783784154604698121483122903344163960138046879651018970211352779930792196553883676319989732719640639947477887444507816814644473808320811170489238150100695729301133978725734344966342539008392521368848055667562401810828901055904263751087
c1 = 18095221808696767364482784106578665305597072892957003660200677125082480938590759416561596931362184836970227059449018023231775268023936196618166559695219893530882687916034256857200503922379924700258829452303164421775991854747192890323693977001251087329555558918620707751930767202265420598175188863386691087599780498257684594011697328525563218286665773793900960107693859824245088548197358663304146296383128902903029994359803481927306296829608298387122321098870479487155445811455194372706211665184810020606934406988332111485009183488421535000282713153719587883866210474448900196771807191538210239194200766493898209050827
e1 = 7
k1 = 0
check_part1 = iroot(c1 + k1*n1, e1)[1]
part1 = 0
while (check_part1 == False):
check_part1 = iroot(c1 + k1*n1, e1)[1]
part1 = long_to_bytes(iroot(c1 + k1*n1, e1)[0])
k1 += 1
# part 2
ns2 = [5776746251180954410501143373555071446651309076079023801239927839678110284298053445067843385733923949632510804583576555583870535387441316658685840687689240200699587579390668561563937127040444465254726625310777306158594065224927251973217783491441064524351035802071558210418452732784328906404928346757561, 6676568530944751773705274371029619538616953853214731842955709009914939432096974301535527889053677971564433108995378797418249594276219798862529871540959246847221996855818534102541008885671426716581485932426758973143450314953430892380665585863829711961246704347425347121483579347058369739426135845646693, 4192125436309381894285355753973720387617056931265026941800223776464084287332759678003685944684491135983930538940594703444960195450172411842880979937537165579234724095471901440019430531228826628339513666609406387657919179589286169600368614047778141005381080248940651068046222358006840623976659320922801]
cs2 = [4162314281829070969983785868478817487122811901286983843325224202348302016796815815333452464672872912888701572281242789929104022254953168740731678111433971515437492093542686431714890537676381271531800973763109092820046704937574547000215411647046072684054960320518397352086234888330336947666810104929210, 50764199787126347289275452914702574991784396905310732416007230764340015964909418575803553215557208257135086801048181610281948813886172483845338416063595514468150336763299168160503561124717579857540938345665633478359717032027020484374804577083335729646127615429489801214833042281198632815600258496021, 2226866454269961169554477487076928558283277297558185848127271094461892302315098156482036398655353603737228883411877291393627455878900738358485648657420475192577890517627929950639476113316539806453385202418270139866437710517550467476876937820556775982947361645302515541452549533744585394098458949714224]
e2 = 7
c3 = 37398307787309593195399039608335293998782975977344821375594074480912755773091240878727726167860952796427287138264423555320002390940336425049899775688041256572345243342480573394310574660551049821001839500881785836377907724697125451771009902048018563262095797500165548399273323153654585302502917216971632506342
N = 1
for n in ns2:
N *= n
M = crt(ns2, cs2)[0]
k2 = 0
check_part1 = iroot(M + k2*N, e2)[1]
part2 = 0
while (check_part1 == False):
check_part1 = iroot(M + k2*N, e2)[1]
part2 = long_to_bytes(iroot(M + k2*N, e2)[0])
k2 += 1
# part 3
n3 = 58918298105520625812936595965949791679948485672063422501379350174567652137330835174097919449869935351283983373480427262310194532256798429690185381024326970315021350533262412067581291819891395690548362953546838296879197806387922608866341956665531154816197564814442845602714598090456294723302800178858587739713
e3 = 65537
E = 39119
d_fake = 47540992349005816963249153906419008010878957801546101159437592685401670280805946273432368604390309296333723642831097076480004356212731471488808547049589210685661344692631483342927181931551362472531714993510869363985573142465639097423475598977423653768735951445358570563443850030507199283981083278104055782079
c3 = 37398307787309593195399039608335293998782975977344821375594074480912755773091240878727726167860952796427287138264423555320002390940336425049899775688041256572345243342480573394310574660551049821001839500881785836377907724697125451771009902048018563262095797500165548399273323153654585302502917216971632506342
k_fake = (d_fake*E - 1) // n3
phi = (d_fake * E - 1) // k_fake
for k in range(k_fake - 2, k_fake + 3):
if k == 0: continue
if (d_fake * E - 1) % k == 0:
phi = (d_fake * E - 1) // k
d = pow(e3, -1, phi)
part3 = long_to_bytes(pow(c3, d, n3))
#print(part1, part2, part3)
# part 4
hash = "6c65e2cc4142210eac7503a24a2d48205aeaf8686407c70fde01478336853281"
chars = "0123456789abcdef"
for p in tqdm(product(chars, repeat=7)):
flag = b"KCSC{" + part1 + part2 + part3 + "".join(p).encode() + b"}"
if(sha256(flag).hexdigest() == hash):
print(flag.decode())
break
```
::: spoiler Flag
KCSC{f8fc7143e81ab9e2a4b8cd7b5893504c686199fea0ba22e7e9ffafae5aaba63a1a64b732debe30e2aaa9e9570403504c6861f68832310c7d569ae3ec1ecbfbba80d65a282127376ef656f9ce50ed2aa3fe}
:::
# Lost
Mình khá thích bài này
chall.py:
```py=
from Crypto.Util.number import getPrime, bytes_to_long
flag = "KCSC{████████████████████████████████████████l█e███████ha█████████████████████}"
assert len(flag) == 79
flag = bytes_to_long(flag.encode())
p = getPrime(512)
q = getPrime(512)
n = p * q
e = 0x10001
phi = (p-1) * (q-1)
# how many bits of phi do you want me to truncate <(") ?
HINT = phi & ((1 << (445)) - 1)
assert flag < n
c = pow(flag, e, n)
print(f'{n = }')
print(f"{e = }")
print(f'{c = }')
print(f'{HINT = }')
# n = 112654267531053465556136674746878649082311939063806818924462933263741875629394889661682552659024091908172480343659015574843946862042743125008337388884333221729927034358266152309599465123544533344748189013549990570426711270722167349548299576309337189468190071950365811726840694954345965264584467524018296958753
# e = 65537
# c = 8002944762099258567457615206256772306393999363093971354996328677238641774654391571386887016848224899250762255275947356988416616247761892841611709100843880889058667475786184996217958354468257211905339362523859724316921413272443004897703998073112581860169711839608791480826283429489201512747250548419782502867
# HINT = 58568716757057749176579698947278522097247638230381142002077590687430267388214398988361849858546815254992223558816558004613328562794052
```
Mình đọc qua thì chắc chắc 1000% là Coppersmith.
Vì dòng code:
```py=
HINT = phi & ((1 << (445)) - 1)
```
Nó tương đương với lấy $\phi(n)$ mod cho $2^{445}$. Tức là lấy 445 bit thấp nhất của $\phi(n)$. Do đó ta sẽ cố gắng liên hệ điều này với việc tìm bit của $p$ (hoặc $q$).
Ta có công thức:
$$\phi(n) = (p-1)(q-1) = p. q - (p + q) + 1 = n - (p + q) + 1$$
Vậy suy ra $$p + q = n + 1 - \phi(n)$$
Vì ta biết 445 bit thấp của $\phi(n)$ (thông qua `HINT`), ta có thể tính được 445 bit thấp của $(p+q)$:
Gọi $$S \equiv p + q \pmod{2^{445}} =n + 1 - \phi(n) \pmod{2^{445}}$$
Mà HINT $\equiv \phi(n) \pmod{2^{445}}$.
Nên:
$$S \equiv n + 1 - \text{HINT} \pmod{2^{445}}$$
Đến đây ta phải tinh ý nhận ra $S$ được biểu diễn theo các thành phần $p+q$ và $n = p.q$,
Nên ta sẽ lập phương trình:
$$x^2 - (p+q)x + (p \cdot q) = 0$$
Vì theo định lý Viet, $p$ và $q$ là hai nghiệm của phương trình đó.
Tới đây ta thay $S$ vào:
$$x^2 - S \cdot x + n \equiv 0 \pmod{2^{445}}$$
Giải phương trình này ta sẽ thu được 1 nghiệm x nằm trong khoảng $[0, 2^{445}-1]$, và nghiệm $x$ đó cũng là 445 bit thấp của $p$ luôn (gọi là $p_{low}$).
Giờ ta biết $p = 512$ bit, ta vừa tìm được $445$ bit thấp của nó, vậy thì còn thiếu $67$ bit.
Ta có thể biễu diễn $p$ như sau:
$$p = x \cdot 2^{445} + p_{low}$$
- Với x là $67$ bit cao của $p$
Phương trình trên tương đương với:
$$x \cdot 2^{445} + p_{low} \equiv 0 \pmod p$$
Đến đây ta tìm ra được phương trình chuẩn để dùng Coppersmith (mình sẽ dùng hàm `small_roots` trong Sage).
Ta xét đa thức $$f(x) = x \cdot 2^{445} + p_{low}$$
Mặc dù ta cần giải $f(x) \equiv 0 \pmod p$, nhưng vì chưa biết $p$, ta sẽ khởi tạo đa thức này trên modulo $n$ (ta làm được vì $n$ là bội của $p$). Thuật toán Coppersmith cho phép tìm nghiệm modulo ước số $p$ ngay cả khi ta tính toán trên vành $\mathbb{Z}_n$."
> Lưu ý là: Điều kiện Coppersmith cho bài toán tìm $p$ như thế này chỉ có thể tìm được nếu ta biết khoảng 50% số bit của $p$. Ở đây ta biết $445/512 \approx 86\%$, nên chắc chắn tìm được.
Và quan trọng là ở bài này nên đặt `multiplicities = False` ở hàm `.roots`. Vì nếu để `multiplicities=True` (mặc định): ngoài việc tìm nghiệm ra thì nó còn phải xác định xem nghiệm đó là nghiệm đơn hay nghiệm kép. Trong bài này khi tính nghiệm trên một vành số dư hợp số $\mathbb{Z}_M$ (như $\mathbb{Z}_{2^{445}}$), việc xác định nghiệm đó là nghiệm đơn hay kiếp trở nên phức tạp nên khi chạy sẽ báo lỗi nên tốt nhất là đặt nó = False.
solve.sage:
```py=
from Crypto.Util.number import long_to_bytes
from sage.all import *
import sys
n = 112654267531053465556136674746878649082311939063806818924462933263741875629394889661682552659024091908172480343659015574843946862042743125008337388884333221729927034358266152309599465123544533344748189013549990570426711270722167349548299576309337189468190071950365811726840694954345965264584467524018296958753
e = 65537
c = 8002944762099258567457615206256772306393999363093971354996328677238641774654391571386887016848224899250762255275947356988416616247761892841611709100843880889058667475786184996217958354468257211905339362523859724316921413272443004897703998073112581860169711839608791480826283429489201512747250548419782502867
HINT = 58568716757057749176579698947278522097247638230381142002077590687430267388214398988361849858546815254992223558816558004613328562794052
bit = 445
P = PolynomialRing(Zmod(2**bit), 'x')
x = P.gen()
S = (n + 1 - HINT)
roots = (x^2 - S*x + n).roots(multiplicities=False)
for r in roots:
p_low = Integer(r)
P2 = PolynomialRing(Zmod(n), 'y')
y = P2.gen()
f = y * (2**bit) + p_low
res = f.monic().small_roots(X=2**72, beta=0.45)
if res:
p_high = res[0]
p = int(p_high * (2**bit) + p_low)
if n % p == 0:
q = n // p
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
flag = long_to_bytes(pow(c, d, n))
print(flag.decode())
```
# Multi RSA
Một bài khá khó vì phải biết cách DLP và có kiến thức cơ bản về đại số tuyến tính.
chall.py:
```py=
from Crypto.Util.number import *
from random import randint
flag = b'KCSC{??????????????????????????????????????????????????????????????????????????????????????????}'
assert flag.startswith(b"KCSC{") and len(flag) == 96
# my prime generator
def get_prime(factor_bits, nbits):
while True:
p = 2
while p.bit_length() + factor_bits < nbits:
p *= getPrime(factor_bits)
current_len = p.bit_length()
needed_bits = nbits - current_len
if needed_bits < 2:
continue
try:
last_prime = getPrime(needed_bits)
except ValueError:
continue
p = p * last_prime + 1
if p.bit_length() == nbits and isPrime(p):
return p
p = get_prime(16, 128)
q = get_prime(16, 128)
n = p * q
phi = (p-1)*(q-1)
# 3 parts of the flag
m1, m2, m3 = [bytes_to_long(part) for part in [flag[i:i+32] for i in range(0, len(flag), 32)]]
assert all(part < n for part in [m1, m2, m3])
es = []
cs = []
for i in range(3):
# rsa encryption
e1, e2, e3 = [randint(1, phi-1) for _ in range(3)]
c1 = pow(m1, e1, n)
c2 = pow(m2, e2, n)
c3 = pow(m3, e3, n)
es.append((e1, e2, e3))
# cs.append((c1, c2, c3)) # nah, too easy
cs.append((c1 * c2 * c3) % n) # what if i multiply them together ?
# write outputs to file
with open("output.txt", "w") as f:
f.write(str(n) + "\n")
f.write(str(es) + "\n")
f.write(str(cs))
```
ouput.txt:
```
47671780042231704413040913594725353438536469505015682546574752585718301112921
[(23977172217622484168774759837385378732502237507920113310285233140285669132645, 7377494923066904273449829226282557300745689267275904429462158991949281414682, 18432112474217778047559883846454066035490987127796230294276735606306725549777), (3124743981175141708117586893234378699895764583129200589303495951740637999914, 22762044917281148499181453892958515527103600842740372832842228589873412009197, 17680895854953441923244596894140851738984314699825793211831691205085172067322), (37545236985478706251704483601220896064218212306586005333560412013908096438729, 20788835878098532955063747621021775806003893333625966206551591514326857932485, 32312926559646114857217655963694360470473973949883002194388252273197485497673)]
[42267207336603045334786451171189000378049455207827292631901571990305135126669, 33840845882520281865996502853093641414955191938495377599016576841954115079220, 18018251336838883503303580652480693144525524464022122529153696030852723761647]
```
Flag được chia thành 3 phần và được mã hóa bằng các e khác nhau. Nhưng ta không biết chính xác giá trị của ciphertext mà biết giá trị của các ciphertext đó nhân với nhau, tức là ta có hệ phương trình:
$$\begin{cases}
C_1 \equiv m_1^{e_{1,1}} \cdot m_2^{e_{1,2}} \cdot m_3^{e_{1,3}} \\
C_2 \equiv m_1^{e_{2,1}} \cdot m_2^{e_{2,2}} \cdot m_3^{e_{2,3}} \\
C_3 \equiv m_1^{e_{3,1}} \cdot m_2^{e_{3,2}} \cdot m_3^{e_{3,3}}
\end{cases} \pmod N$$
Trước khi giải cái hệ khủng bố này thì ta phân tính hàm sinh số nguyên tố trước:
```py=
def get_prime(factor_bits, nbits):
while True:
p = 2
while p.bit_length() + factor_bits < nbits:
p *= getPrime(factor_bits)
current_len = p.bit_length()
needed_bits = nbits - current_len
if needed_bits < 2:
continue
try:
last_prime = getPrime(needed_bits)
except ValueError:
continue
p = p * last_prime + 1
if p.bit_length() == nbits and isPrime(p):
return p
p = get_prime(16, 128)
q = get_prime(16, 128)
```
Hàm này tạo ra số nguyên tố $p$ theo dạng $p = k \cdot \Pi(p_i) + 1$. Điều này có nghĩa là $p-1$ là một số "smooth". Và trong các bài toán DLP, nếu $p-1$ là số smooth, ta có thể dùng thuật toán Pohlig-Hellman để tính DLP cực nhanh.
$N = p \cdot q \approx 256$ bit nên mình đã factor dễ dàng được $p, q$
```py=
p, q = 208241528391730184734449228964657137983, 228925423331290125959230566821710134887
```
Ta không thể tính DLP trên $N$ (vì nhóm $\mathbb{Z}_N^*$ không phải là nhóm cyclic). Do đó ta phải giải riêng trên $\mathbb{F}_p$ và $\mathbb{F}_q$, vì $p$ là số nguyên tố nên nhóm nhân $\mathbb{F}_p^*$ là nhóm cyclic cấp $p-1$.
Ta chọn $g$ là một căn nguyên thủy (primitive root) của $p$. Khi đó, mọi phần tử trong $\mathbb{F}_p^*$ đều có thể viết dưới dạng lũy thừa của $g$.
Xét trên modulo $p$:$$C_i \equiv m_1^{e_{i,1}} \cdot m_2^{e_{i,2}} \cdot m_3^{e_{i,3}} \pmod p$$
Từ đó ta đặt ẩn phụ cho các $m$ và $C$ theo cơ số $g$:
Đặt $x_1, x_2, x_3$ là logarit rời rạc của $m_1, m_2, m_3$:$$m_1 \equiv g^{x_1} \pmod p$$$$m_2 \equiv g^{x_2} \pmod p$$$$m_3 \equiv g^{x_3} \pmod p$$
Tính $Y_i$ là logarit rời rạc của $C_i$ (đây là giá trị ta có thể tính được vì $C_i, g, p$ đã biết):$$C_i \equiv g^{Y_i} \pmod p$$
Thay các giá trị $g$ vào phương trình ban đầu:$$g^{Y_i} \equiv (g^{x_1})^{e_{i,1}} \cdot (g^{x_2})^{e_{i,2}} \cdot (g^{x_3})^{e_{i,3}} \pmod p$$
Ta có thể thu gọn phương trình trên: $$g^{Y_i} \equiv g^{x_1 \cdot e_{i,1}} \cdot g^{x_2 \cdot e_{i,2}} \cdot g^{x_3 \cdot e_{i,3}} \pmod p$$$$g^{Y_i} \equiv g^{(e_{i,1}x_1 + e_{i,2}x_2 + e_{i,3}x_3)} \pmod p$$
Theo định lý Fermat nhỏ, nếu $g^A \equiv g^B \pmod p$, thì số mũ sẽ đồng dư theo modulo $\phi(p) = p-1$. Phương trình mũ trên trở thành phương trình tuyến tính bậc nhất:$$Y_i \equiv e_{i,1}x_1 + e_{i,2}x_2 + e_{i,3}x_3 \pmod{p-1}$$
Để tìm 3 ẩn số $x_1, x_2, x_3$, ta cần ít nhất 3 phương trình (tức là cần 3 cặp $C_i$ tương ứng với $i=1, 2, 3$).
Hệ phương trình sẽ có dạng ma trận:$$\begin{pmatrix}
Y_1 \\
Y_2 \\
Y_3
\end{pmatrix}
\equiv
\begin{pmatrix}
e_{1,1} & e_{1,2} & e_{1,3} \\
e_{2,1} & e_{2,2} & e_{2,3} \\
e_{3,1} & e_{3,2} & e_{3,3} \\
\end{pmatrix}
\cdot
\begin{pmatrix}
x_1 \\
x_2 \\
x_3
\end{pmatrix}
\pmod{p-1}$$Gọi ma trận các số mũ là $E$, vector của ciphertext là $Y$, và vector ẩn là $X$. Ta có:$$Y \equiv E \cdot X \pmod{p-1}$$
Thì lúc này ta giải hệ phương trình bằng cách tính $E^{-1} \pmod{p-1}$. (Điều kiện là định thức $\det(E)$ phải nguyên tố cùng nhau với $p-1$).
Tại sao lại phải như vậy? Vì trên trường số thực, nếu định thức $\det(E) \neq 0$ thì có 1 nghiệm duy nhất.
Nhưng trên vành modulo $p-1$. Để có nghiệm duy nhất thì: $\gcd(\det(E), p-1)$ phải bằng 1.
$p-1$ là số chẵn, và định thức của ma trận cũng thường chẵn (thưòng thôi).
Do đó có thể $\gcd(\det(E), p-1) > 1$. Nên hậu quả là hệ trên sẽ có nhiều nghiệm.
Do đó mình nghĩ ra giải pháp khác là thay vì tìm ma trận nghịch đảo, mình sử dụng phương pháp Khử Gauss, hàm `solve_right` trong SageMath. Cụ thể, nếu hệ có nghiệm, nghiệm tổng quát sẽ có dạng:$$X = X_{p} + K$$
Trong đó thì:
- $X_{p}$: Là một nghiệm riêng
- $K$: Là một vector thuộc không gian Kernel của ma trận $E$.
Nói cách khác, do $\gcd > 1$, ta sẽ tìm được một tập hợp các giá trị $x_j$. Từ mỗi $x_j$, ta tính lại được một tập hợp các giá trị cho $m_j \pmod p$:$$m_j^{(k)} \equiv g^{x_j + k \cdot \frac{p-1}{d}} \pmod p$$
Đó là lý thuyết mình đọc được chứ mình cũng không biết tìm Kernel trong không gian logarit kiểu gì, nên mình sẽ đi tìm cái gọi là "độ chêch lệnh" của nghiệm
Cụ thể:
Gọi $X_0$ là nghiệm riêng mà hàm `solve_right` tìm được. lúc này nghiệm tổng quát sẽ là:$$X = X_0 + \Delta x$$
Ta có công thức$m = g^x \pmod p$.
Vậy:$$m = g^{X} = g^{(X_0 + \Delta x)} = g^{X_0} \cdot g^{\Delta x} \pmod p$$
Vậy là ta vừa biến đổi phép cộng trong logarit trở thành phép nhân với một hệ số trong message thành công.
Giờ ta cứ đi tính xem cái $g^{\Delta x}$ này thực sự là gì.
Gọi số lượng nghiệm của hệ phương trình trên là $k$, và theo lý thuyết thì:
$$k = \gcd(\det(E), p-1)$$
Điều này có nghĩa là sẽ có đúng $k$ giá trị khác nhau của $g^{\Delta x}$ thỏa mãn phương trình. Tập hợp các giá trị này tạo thành một nhóm con cyclic cấp $k$ của $\mathbb{F}_p^*$.
Cụ thể, phần tử sinh của nhóm sai số này là:
$$u = g^{\frac{p-1}{k}} \pmod p$$
Khi đó, hệ số $g^{\Delta x}$ sẽ nằm trong tập hợp các lũy thừa của $u$, tức là thuộc {${ 1, u, u^2, \dots, u^{k-1}}$}
Như vậy, quy trình khôi phục message hoàn chỉnh sẽ là:
- Dùng `solve_right` của sage để tìm một nghiệm $X_0$ bất kỳ.
- Tính message cơ sở: $m_{0} \equiv g^{X_0} \pmod p$.
- Sinh tập hợp $k$ hệ số điều chỉnh (shifts)
- message thật sự $m$ sẽ là một trong các giá trị $m_{0}.\text{shift}[i] \pmod p$.
solve.sage:
```py=
from Crypto.Util.number import long_to_bytes
from sage.all import *
N = 47671780042231704413040913594725353438536469505015682546574752585718301112921
Es = [
(23977172217622484168774759837385378732502237507920113310285233140285669132645, 7377494923066904273449829226282557300745689267275904429462158991949281414682, 18432112474217778047559883846454066035490987127796230294276735606306725549777),
(3124743981175141708117586893234378699895764583129200589303495951740637999914, 22762044917281148499181453892958515527103600842740372832842228589873412009197, 17680895854953441923244596894140851738984314699825793211831691205085172067322),
(37545236985478706251704483601220896064218212306586005333560412013908096438729, 20788835878098532955063747621021775806003893333625966206551591514326857932485, 32312926559646114857217655963694360470473973949883002194388252273197485497673)
]
Cs = [42267207336603045334786451171189000378049455207827292631901571990305135126669, 33840845882520281865996502853093641414955191938495377599016576841954115079220, 18018251336838883503303580652480693144525524464022122529153696030852723761647]
p, q = 208241528391730184734449228964657137983, 228925423331290125959230566821710134887
res = []
for prime in [p, q]:
F = GF(prime)
g = F.multiplicative_generator()
R = Zmod(prime - 1)
Y = vector(R, [discrete_log(F(c), g) for c in Cs])
M = Matrix(R, Es)
X = M.solve_right(Y)
k = gcd(Integer(M.det()), prime - 1)
shifts = [pow(pow(g, (prime - 1) // k, prime), i, prime) for i in range(k)]
msgs = [pow(g, int(x), prime) for x in X]
res.append((msgs, shifts))
(msgs_p, shifts_p), (msgs_q, shifts_q) = res
flag = []
for i in range(3):
for sp in shifts_p:
for sq in shifts_q:
m = crt([int(msgs_p[i] * sp % p), int(msgs_q[i] * sq % q)], [p, q])
b = long_to_bytes(m)
if (i == 0 and b.startswith(b"KCSC")) or (idx > 0 and all(32 <= c <= 126 for c in b)):
flag.append(b)
break
else:
continue
break
print(b"".join(flag).decode())
```
Phải xử lý đoạn cuối khá rườm rà khi tìm ra được các $m$ đúng nữa vì có khá nhiều nghiệm đúng format nhưng mà không phải flag.
:::spoiler Flag
KCSC{I_hope_you_used_factor_and_discrete_log_to_solve_this_challenge_9f72b5a7608c076684b6ca6157}
:::
# rsabcde
Một bài nhiều cảm xúc
chall.py:
```py=
from Crypto.Util.number import bytes_to_long, long_to_bytes, getPrime
from secret import flag, f4k3_fl4g
import sys , random
sys.set_int_max_str_digits(678)
assert bytes_to_long(flag) < 2**456
try:
a, b, c, d, e = [ int(input(f"{x} = ")) for x in "abcde" ]
assert a > 0
assert 1111111111111111111 * a == c + 8888888888888888881 * e
assert 9999999999999999961 * b == 1258462969247482836585791373370000000000000000000000000000000000000000011896691036181988787986574538026426876 * a
assert d ** 2 == a + b
if a.bit_length() < 234:
prime = getPrime(512)
rand = random.choice([a, b, c, d, e])
n = prime * rand
ct = pow(bytes_to_long(flag), e, n)
print(f"n = {n}")
print(f"ct = {ct}")
else:
print("(👉゚ヮ゚)👉", f4k3_fl4g.decode())
except: print("Nope!")
```
Challenge cho phép ta nhập các giá trị a, b, c, d, e tùy ý. Sau đó đem flag đi mã hóa bằng RSA. Theo phản xạ thì mình thử nhập e = 1 ngay lập tức. Vì flag có giá trị nhỏ hơn $2^{456}$ nên chắc chắc nhỏ hơn $N$, vậy flag sẽ bằng ciphertext. Nên ta chỉ cần $a, b, c, d$ thỏa mãn điều kiện để server trả về flag là lụm.
Để gửi được $e=1$, ta phải vượt qua các `assert`.
Ta cần tìm $a, b, c, d$ nguyên dương thỏa mãng các ràng buộc
Ràng buộc 1: Phương trình: $999999...961.b = 12584629...76.a$.
- Đặt $K_3 = 99...961, K_4 = 125...76$.Ta sẽ có:$$b = a \cdot \frac{K_4}{K_3}$$
Ràng buộc 2:Phương trình: $d^2 = a + b$.
Để giải ta thay thế $b$ vào:$$d^2 = a + a .\frac{K_4}{K_3} = a \left( \frac{K_3 + K_4}{K_3} \right)$$
Mình sẽ gọi phân số rút gọn của $\frac{K_3 + K_4}{K_3}$ là $\frac{t}{m}$.
Ta có$$d^2 = a . \frac{t}{m}$$
Để $d^2$ là số nguyên, thì $a$ phải triệt tiêu được mẫu số $m$.
Đặt $a = k . m$ (với $k$ là số nguyên).
Thay vào phương trình:$$d^2 = k .m .\frac{t}{m} = k \cdot t$$
Vậy mục tiêu lúc này của ta là Tìm $k$ sao cho biểu thức $k .t$ là một số chính phương.
Nhưng cuộc đời nó đau đớn hơn nhiều... mình không brute-force ra $k$ thỏa mãn dù đã thử rất lâu. Sau đó mình tìm thấy ánh sáng. Cách đơn giản nhất để $k . t$ là số chính phương mà không cần suy nghĩ nhiều là chọn luôn $k = t$.
Khi đó: $d^2 = t . t = t^2$ (Luôn đúng!).
Nhưng làm gì dễ ăn vậy. Khi làm vậy thì nhận được fake flag:
```
(👉゚ヮ゚)👉 KCSC{if you_know: you_know; assert 125846296924748283658579137337_________________________________________11896691_36181988787986574538_26426876 == p*p*q}
```
Tại sao vậy? vì nếu chọn $k = t$:$$a = k .m = t . m$$Biến t có độ lớn xấp xỉ $K_4$ (~115 chữ số), nên a sẽ cực kỳ lớn, vượt xa giới hạn 234 bit. Server nhảy vào nhánh `else` và in ra Fake Flag
Nhưng cũng vì cái fake flag này mà mình lại nhìn ra hướng đi, cái chuỗi số `125...26876` trong đó chính là $K_4$. Vậy tức là $K_4 = p^2.q$
Mà $t \approx K_4$. Nên ta có thể viết $$t = p^2 . q$$ Vậy để $t$ chính phương thì chỉ cần chọn $k = q$ để nhân vô là ra một số chính phương có dạng $(p.q)^2$. sau khi factor thì $q$ khoảng 166 bit nên a = 166 + 64 = 230 bit. Vừa khớp!
solve.py:
```py=
from pwn import *
from Crypto.Util.number import long_to_bytes
import math
import sys
from sympy.ntheory import factorint
#sys.set_int_max_str_digits(10000)
r = remote("67.223.119.69", 5010)
K1 = 1111111111111111111
K2 = 8888888888888888881
K3 = 9999999999999999961
K4 = 1258462969247482836585791373370000000000000000000000000000000000000000011896691036181988787986574538026426876
g = math.gcd(K3, K4)
tu = (K3 + K4) // g
mau = K3 // g
factors = factorint(tu)
k = 1
for q, e in factors.items():
#print(q, e)
if e % 2 != 0:
k *= q
a = k * mau
e = 1
while (K1 * a - K2 * e) <= 0:
a *= 4
b = (K4 * a) // K3
c = K1 * a - K2 * e
d = math.isqrt(a + b)
for var, val in zip("abcde", [a, b, c, d, e]):
r.sendlineafter(f"{var} = ".encode(), str(val).encode())
r.recvuntil(b"ct = ")
ct = int(r.recvline().strip())
flag = long_to_bytes(ct)
print(flag.decode())
r.close()
```
> Nhớ phải tính trường hợp $c$ âm nữa, lúc đó phải tăng $a$ lên một khoảng là số chính phương vì như vậy $d$ mới là số nguyên
:::spoiler Flag
KCSC{f4c70rDB_15_4m4z1ng!}
:::
# to be continued
```py=
from sage.all import *
from Crypto.Util.number import getPrime, bytes_to_long, GCD
import random
flag = bytes_to_long(b"KCSC{???????????????????????????}")
rbits = lambda x: random.getrandbits(x)
p, q = getPrime(1024), getPrime(1024)
N = p*q
a, b = rbits(910), rbits(910)
assert GCD(a, b) == 1
phi = (p**3 - a) * (q**3 - b)
d = getPrime(1024)
e = pow(d, -1, phi)
c = pow(flag, e, N)
print("Welcome to KCSC's Recruiment")
print("Gifts: ")
print("a = ", a)
print("b = ", b)
print("N = ", N)
print("e = ", e)
print("c = ", c)
```
Challenge tính $\phi(n)$ theo cách khá dị với $\phi = (p^3 - a)(q^3 - b)$, rồi từ đó tạo $e$ dựa trên cái phi này.
Vậy giờ để giải mã $c$, ta cần tìm lại $p$ và $q$. Khi có $p, q$, ta có thể tính $d$ chuẩn $d = e^{-1} \pmod{(p-1)(q-1)}$ để lấy flag.
Mà ta nhận ra rằng $d$ chỉ khoảng 1024 bit, còn $N \approx 2^{2048}$, cụ thể thì $d < N^{3 . \frac{1}{6}}$, nên ta có thể áp dụng kỹ thuật liên phân số (Continued Fractions) để tìm $k$ và $d$.
Ta có:
$$e \cdot d \equiv 1 \pmod \phi$$
Tương đương:
$$e . d = 1 + k . \phi$$
Vậy $$\frac{e}{\phi} - \frac{k}{d} = \frac{1}{d \phi}$$
Mà ta biết $\phi$ xấp xỉ $N^3$ nên ta dùng $N^3$ để xấp xỉ:$$\frac{e}{N^3} \approx \frac{k}{d}$$
Điều kiện để $\frac{k}{d}$ là một hội tụ (convergent) của khai triển liên phân số $\frac{e}{N^3}$ là sai số phải đủ nhỏ. Với các kích thước trên, điều kiện này thỏa mãn.
Mà $$\phi = (p^3 - a)(q^3 - b) = p^3q^3 - b p^3 - a q^3 + ab$$
Nên $$\phi = N^3 - (b p^3 + a q^3 - ab)$$
Vậy ta có phương trình:
$$e . d = 1 + k(N^3 - b p^3 - a q^3 + ab)$$
$$e . d - 1 - k N^3 - k a b = -k (b p^3 + a q^3)$$
Mà $q = N/p$, nên:
$$e . d - 1 - k N^3 - k a b = -k \left( b p^3 + a \frac{N^3}{p^3} \right)$$
Tương đương
$$(e d - 1 - k N^3 - k a b) p^3 = -k b (p^3)^2 - k a N^3$$
Chuyển vế rồi đặt $X = p^3$ để tạo phương trình bậc 2:$$(k b) X^2 + (e d - k N^3 - k a b - 1) X + (k a N^3) = 0$$
Giải phương trình này ta sẽ tìm được $p_3$, từ đó tính được $\phi(n)$, bài toán kết thúc.
```py=
from sage.all import *
from Crypto.Util.number import long_to_bytes
a = 6304481321164246906567222989658765688045165275851071395205832715763220356466821410282955969161802841176445527338207576132114860045401544497249828909351039182182945402547214970223049123104061910807236829237608274748829008054249852723406591298824990323104235785926710097065678
b = 7999870125267969613672395287537327380867912777324346722357132031560297200201104156928904801090149293190711356687508094555199353750034650890008720565018729206913224039530232660373275511417192954417706309710650228654070214795786233986152367508554076009075490479812207855618459
N = 24081889277572802503659605826689926230027830719582767936363743502766482776099705960338212287859025560531973656731556156751481186678406820240771044210411774957806232749249860957895878890504459213322156810941197881824455880823042317347404688417969955356521338121436115650468863788924858088914981935069815044937563145675179199447446041774547448145057112430800054972872605835633576747583883285674457706771762988370139582185793239931790092003815695676798947934482380916291980085009315441662093814772584956990759521338343380951529181900596763825803370008245419318846198328641690031150992032307428945203841914049392112787033
e = 11050153015839883993161510135885787976039008082822382585575804877332676017418571820857988290309334210537161314550427816879212068985637953060885871602432684485487034375509379619789469792410393103838136020388501433382319198496971044588509979747359924647890043201124870325113772613514847295817375395027727381107822005050762060213230125451000959649050975147498113813336975434470468048553590716535948897232799351608157501900913776026264449516627219354423799349366099044820589167149211092110282765816684408405978492422462190786538301825408257216631933494464394828382376899323287315287973243876381980994428110571165021205398217777278825313093011741550183549639331123929016611159546502516316408659104164833466844920816898591821437607508224932520567325077419433023709911572576434857365505993059307741933102205523954940790045708534454784363255787058750009662319556437924605424640852945028220937879047842631235735134418121924586228479500537071667681030781582209438915903316316707019251330858088692416367956113483887988023423235436487473600194187120044620497634702200482106887128671850087321490311974939050362444303066126795966145296397377016134943620623131647797749418767732449622636419543202865842408588889412853603641329368785426334703041533454162178415167229266747016833148542628025339647321049686300229145201077528532971910019089492280243517721296838530796949829055923578715584168667973195521740594813691640838622696677680794584011285459260694486228084639063968847084787567170479050268786820660369450450781882574775405028286616546053143903706294406042325054355735859552547343917756174444977124800496069647281726872919803779970204586930394689812769802828483192801474434935520732430489482223645633398359373531176141794910809378994196853254737150627130658758517463777199549229895016654293830684067419466384677596715428945933720138801803468247516077733284618675
c = 8902578232624984411383722754874619024953250279805891829357397047274881749749711693192596092307853528587730589177614481442983584423981140117572924003770429665495872100397271876729790318687547951882889699514674500967917264418934982857003875266311412832039100198151364992410275296218572639061633726848133865363024117479523168920494186373486638535334075708542302849581459206163514282261518703853945190442837360895855359460519949894401579887910940646553403081219671212007000929291008947704019546704637268666663086394541789152216744224799090091941962088879750154156402189632368378336651988960439661260683085234566808439757
R = PolynomialRing(ZZ, 'x')
x = R.gen()
frac = Integer(e) / Integer(N)**3
convergents = frac.continued_fraction().convergents()
for conv in convergents:
k = int(conv.numerator())
d_fake = int(conv.denominator())
if k == 0: continue
if (e * d_fake - 1) % k != 0: continue
phi_fake = (e * d_fake - 1) // k
f = b * x**2 + (phi_fake - N**3 - a * b) * x + a * N**3
roots = f.roots()
for X, _ in roots:
p = X.nth_root(3)
if p**3 == X:
if N % p == 0:
q = N // p
phi_real = (p - 1) * (q - 1)
d_real = inverse_mod(e, phi_real)
m = power_mod(c, d_real, N)
print(long_to_bytes(int(m)).decode())
exit()
```
::: spoiler Flag
KCSC{Be_water!My_friend}
:::
# Sign v2
Trong lúc diễn ra giải thì mình thấy bài này quen quen, cảm giác như từng làm ở đâu đó, sau đó mới nhớ nó là ver 2 của câu trong đề TTV 2024
chall.py:
```py=
from hashlib import sha256
from Crypto.Util.number import bytes_to_long, long_to_bytes
from ecdsa import ellipticcurve
from ecdsa.ecdsa import curve_256, generator_256, Public_key, Private_key
from os import *
from random import randint
from pwn import xor
from flag import flag
from base64 import b64decode
secret = urandom(16)
G = generator_256
p = G.order()
def genKeyPair():
d = randint(1,p-1)
pubkey = Public_key(G, d*G)
privkey = Private_key(pubkey, d)
return pubkey, privkey
def ecdsa_sign(msg, nonce,privkey):
hsh = sha256(b64decode(msg)).digest()
nonce = sha256(xor(sha256(b64decode(nonce)).digest(), secret)).digest()
sig = privkey.sign(bytes_to_long(hsh), bytes_to_long(nonce))
return {"msg": msg, "r": hex(sig.r), "s": hex(sig.s)}
def challenge() :
pubkey , privkey = genKeyPair()
print('\nPublic key:', (int(pubkey.point.x()), int(pubkey.point.y())), '\n')
print("1 : Sign msg ")
print("2 : Submit private key")
nonces = []
for _ in range(3) :
try :
option = int(input("Option : "))
if option == 1 :
msg = (input("Enter your message: ")).encode()
nonce = (input("Enter your nonce: ")).encode()
if nonce in nonces :
print('Nope')
exit()
if len(nonces) > 0 :
for i in nonces :
if nonce in i or i in nonce:
print('Nope')
exit()
nonces.append(nonce)
print(ecdsa_sign(msg,nonce,privkey))
if option == 2 :
d = int(input("Private key : "))
print(nonces)
if d == int(privkey.secret_multiplier) :
print(f'Very nice , here is your flag : {flag}')
exit()
else :
print(f'Wrong key , correct is {privkey.secret_multiplier}')
exit()
except :
print("Not legal option")
exit()
print("Out of attemps !! ")
challenge()
```
Đây là challenge khai thác lỗi Nonce Reuse Attack trong thuật toán chữ ký số ECDSA.
> Về ECDSA, vì chưa học đến Elliptic Curve nên bài này mình chỉ là sử dụng các công thức có sẵn để tính chứ không thể hiểu về bản chất toán học đằng sau, may mắn là challenge này không cần hiểu quá sâu.
Nôm na là trong ECDSA, nếu bạn ký hai tin nhắn khác nhau ($msg_1, msg_2$) nhưng sử dụng cùng một số ngẫu nhiên $k$ (nonce), kẻ tấn công có thể tính toán ngược lại được khóa riêng tư (Private Key - $d$).
Tước tiên ta chú ý hàm tạo nonce của challenge như sau:
```py=
def ecdsa_sign(msg, nonce,privkey):
hsh = sha256(b64decode(msg)).digest()
nonce = sha256(xor(sha256(b64decode(nonce)).digest(), secret)).digest()
sig = privkey.sign(bytes_to_long(hsh), bytes_to_long(nonce))
return {"msg": msg, "r": hex(sig.r), "s": hex(sig.s)}
```
Tức là giá trị $k$ phụ thuộc vào kết quả của b64decode(nonce). Vậy ta hoàn toàn thao túng được $k$ bằng cách nhập nonce.
Nhưng ta không được phép nhập lại nonce cũ vì challenge có cơ chế kiểm tra:
```py=
for i in nonces :
if nonce in i or i in nonce:
print('Nope')
exit()
```
Tuy nhiên có một điều thú vị về hàm `base64.b64decode` trong Python: nó sẽ **bỏ qua các ký tự khoảng trắng** khi giải mã.
Vậy chiến thuật tấn công là:
Chúng ta sẽ gửi 2 nonce dạng chuỗi khác nhau để qua mặt sever, nhưng khi b64decode thì ra cùng một giá trị byte, nếu như vậy thì mới cùng một $k$.
Cụ thể
- Lần 1: Nonce string = "DQ== "
- Lần 2: Nonce string = "D Q=="
Hai chuỗi này không phải là con của nhau (thỏa mãn điều kiện), nhưng đều decode ra byte b'r' (do bỏ qua space), dẫn đến tạo ra cùng một $k$.
Và từ đây ta tính toán như sau:
Ta có 2 chữ ký $(r, s_1)$ và $(r, s_2)$ (lưu ý $r$ giống nhau vì $k$ giống nhau, $r$ là toạ độ x của điểm $k \times G$).
Công thức tạo chữ ký:$$s = k^{-1}(z + r \cdot d) \pmod n$$
Với 2 chữ ký:
- (1) $s_1 \cdot k \equiv z_1 + r \cdot d \pmod n$
- (2) $s_2 \cdot k \equiv z_2 + r \cdot d \pmod n$
Trừ (1) cho (2): $k(s_1 - s_2) \equiv z_1 - z_2 \pmod n$
Từ đó tìm được $k$:$$k \equiv (z_1 - z_2) \cdot (s_1 - s_2)^{-1} \pmod n$$
Sau khi có $k$, ta tính lại $d$ (Private Key):$$d \equiv (s_1 \cdot k - z_1) \cdot r^{-1} \pmod n$$
Có $d$ rồi thì gửi nó đi sẽ có flag.
solve.py:
```py=
from pwn import *
from hashlib import sha256
from ecdsa.ecdsa import generator_256
from Crypto.Util.number import bytes_to_long, inverse
from base64 import b64decode
HOST = '67.223.119.69'
PORT = 5009
p = remote(HOST, PORT)
n = generator_256.order()
def parse_signature(response):
start = response.find(b'{')
end = response.find(b'}') + 1
json = response[start:end].decode()
data = eval(json)
return int(data['r'], 16), int(data['s'], 16)
msg1 = "DQ=="
msg2 = "Dq=="
nonce1 = "DQ=="
nonce2 = "D Q=="
p.sendlineafter(b'Option : ', b'1')
p.sendlineafter(b'message: ', msg1.encode())
p.sendlineafter(b'nonce: ', nonce1.encode())
out1 = p.recvuntil(b'}')
r1, s1 = parse_signature(out1)
p.sendlineafter(b'Option : ', b'1')
p.sendlineafter(b'message: ', msg2.encode())
p.sendlineafter(b'nonce: ', nonce2.encode())
out2 = p.recvuntil(b'}')
r2, s2 = parse_signature(out2)
z1 = bytes_to_long(sha256(b64decode(msg1)).digest())
z2 = bytes_to_long(sha256(b64decode(msg2)).digest())
s = (s1 - s2) % n
z = (z1 - z2) % n
k = (z * inverse(s, n)) % n
d = ((s1 * k - z1) * inverse(r1, n)) % n
p.sendlineafter(b'Option : ', b'2')
p.sendlineafter(b'Private key : ', str(d).encode())
p.interactive()
```
::: spoiler Flag
KCSC{no_more_byte_\x00_today}
:::
# umama
Một challenge khá hàih
```py=
import socket
import threading
import time
import random
from flag import FLAG
# =========================================================================
# I. LCG - Linear Congruential Generator
# =========================================================================
class LCG:
def __init__(self, seed=None):
self.M = 3287099835290195791649721107570004318267582246774226300213811649423225012170212287749142020192530464259481640735325781154239321190173754611921808393447501266888056440801327249770117062004226214495561535681369100056816111927528166402933420016083242583498654107351130511963025469986264222148916890724787070272803001
self.A = 2894777803559679218352190554394546666288076241619396764815133803772055992385941101609673369526743001851311268818989398042025043757099501509707445437248873549911623395016719427960022056683203263457712425937702117698191301068614729419207159556006174347883149816514548460625672181804096655842375751872041822338728496
self.C = 122
self.seed = int(random.getrandbits(512)) % self.M if seed is None else seed % self.M
def _next_seed(self):
self.seed = (self.A * self.seed + self.C) % self.M
return self.seed
def uniform(self, min_val, max_val):
normalized = self._next_seed() / self.M
return min_val + normalized * (max_val - min_val)
# =========================================================================
# II. Uma Musume - Race Stats
# =========================================================================
class UmaMusume:
def __init__(self, name, speed=50, stamina=50, skill_factor=50):
self.name = name
self.speed = speed
self.stamina = stamina
self.skill_factor = skill_factor
self.odds = 0.0
self.current_pos = 0.0
self.current_speed = speed
def update_position(self, lcg_gen, track_length):
race_progress = self.current_pos / track_length
stamina_drain = max(0, race_progress - 0.7)
drain_reduction = self.stamina / 100.0
speed_modifier = 1 - (stamina_drain * (1.5 - drain_reduction))
self.current_speed = max(self.speed * 0.4, self.speed * speed_modifier)
skill_multiplier = 2.5 if race_progress > 0.7 else 1.0
random_boost = lcg_gen.uniform(0.95, 1.05 + self.skill_factor * 0.01 * skill_multiplier)
micro_random = lcg_gen.uniform(0.99, 1.01)
step_speed = self.current_speed * micro_random + random_boost
self.current_pos += step_speed
return step_speed
# =========================================================================
# III. Race Engine
# =========================================================================
class Race:
def __init__(self, uma_list, track_length, lcg_gen):
self.umas = uma_list
self.track_length = track_length
self.lcg_gen = lcg_gen
self.max_steps = 1000
def start_race(self):
for uma in self.umas:
uma.current_pos = 0.0
step_count = 0
race_over = False
while not race_over and step_count < self.max_steps:
step_count += 1
for uma in self.umas:
uma.update_position(self.lcg_gen, self.track_length)
if uma.current_pos >= self.track_length:
race_over = True
results = sorted(self.umas, key=lambda uma: uma.current_pos, reverse=True)
return results
# =========================================================================
# IV. Betting System
# =========================================================================
class Betting:
INITIAL_BALANCE = 10000.0
FLAG_PRICE = 100000.0
def __init__(self, umas, lcg_gen):
self.umas = umas
self.lcg_gen = lcg_gen
self.balance = self.INITIAL_BALANCE
self.current_bets = {}
self.current_odds = {}
def _calculate_odds(self):
total_power = sum(u.speed + u.stamina + u.skill_factor for u in self.umas)
self.current_odds = {}
for uma in self.umas:
uma_power = uma.speed + uma.stamina + uma.skill_factor
base_odds = total_power / uma_power
random_factor = self.lcg_gen.uniform(0.8, 1.2)
uma.odds = max(1.5, base_odds * random_factor * 0.5)
self.current_odds[uma.name] = uma.odds
def show_odds(self):
self._calculate_odds()
msg = "\n" + "="*60 + "\n"
msg += f"💰 WALLET: {self.balance:,.2f} Yen\n"
msg += "-"*60 + "\n"
msg += "📊 CURRENT ODDS:\n"
for i, uma in enumerate(self.umas):
msg += f" [{i + 1:2d}] {uma.name:25s} | 1 to {uma.odds:6.2f} x\n"
msg += "="*60 + "\n"
return msg
def place_bet(self, uma_index, amount):
self.current_bets = {}
try:
uma_index = int(uma_index) - 1
amount = float(amount)
except:
return False, "Invalid input"
if not (0 <= uma_index < len(self.umas)):
return False, f"Index must be 1 to {len(self.umas)}"
if amount <= 0 or amount > self.balance:
return False, "Invalid bet amount"
uma_name = self.umas[uma_index].name
self.balance -= amount
self.current_bets[uma_name] = amount
msg = f"\n✅ BET PLACED: {amount:,.2f} Yen on {uma_name}\n"
msg += f"📉 Remaining: {self.balance:,.2f} Yen\n"
return True, msg
def process_winnings(self, winning_uma_name):
total_winnings = 0.0
msg = "\n" + "="*60 + "\n"
msg += "🏆 RACE RESULTS 🏆\n"
msg += "-"*60 + "\n"
for uma_name, bet_amount in self.current_bets.items():
winning_odds = self.current_odds.get(uma_name, 0)
if uma_name == winning_uma_name:
winnings = bet_amount * winning_odds
self.balance += winnings
total_winnings += winnings
msg += f"🎉 WINNER! {uma_name}\n"
msg += f" Bet: {bet_amount:,.2f} Yen × {winning_odds:.2f} = +{winnings:,.2f} Yen\n"
else:
msg += f"❌ LOST: {uma_name} - {bet_amount:,.2f} Yen\n"
msg += "-"*60 + "\n"
msg += f"💵 TOTAL RETURN: +{total_winnings:,.2f} Yen\n"
msg += f"💰 NEW BALANCE: {self.balance:,.2f} Yen\n"
msg += "="*60 + "\n"
return msg
# =========================================================================
# V. Socket Server
# =========================================================================
class CTFServer:
def __init__(self, host='0.0.0.0', port=1337):
self.host = host
self.port = port
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
def send(self, conn, msg):
try:
conn.sendall((msg + '\n').encode())
except:
pass
def recv(self, conn, timeout=30):
conn.settimeout(timeout)
try:
return conn.recv(1024).decode().strip()
except:
return None
def handle_client(self, conn, addr):
print(f"[+] Client connected: {addr}")
umas = [
UmaMusume("Special Week"),
UmaMusume("Silence Suzuka"),
UmaMusume("Gold Ship"),
UmaMusume("Mejiro McQueen"),
UmaMusume("Tokai Teio"),
UmaMusume("Rice Shower"),
UmaMusume("Symboli Rudolf"),
UmaMusume("Kitasan Black"),
UmaMusume("Satono Diamond"),
UmaMusume("Daiwa Scarlet"),
]
lcg_gen = LCG(seed=int(time.time() * 1000))
betting_system = Betting(umas, lcg_gen)
track_length = 500.0
self.send(conn, "="*60)
self.send(conn, "🌸 UMA MUSUME - CTF BETTING GAME 🌸")
self.send(conn, f"Goal: Earn {betting_system.FLAG_PRICE:,.2f} Yen to buy the FLAG")
self.send(conn, f"Start: {betting_system.INITIAL_BALANCE:,.2f} Yen")
self.send(conn, "="*60)
while True:
self.send(conn, "\n" + "="*60)
self.send(conn, f"💰 CURRENT BALANCE: {betting_system.balance:,.2f} Yen")
self.send(conn, "-"*60)
self.send(conn, "[1] 🏇 Start Race (Place Bet)")
self.send(conn, "[2] 🏳️ Buy CTF Flag (100,000 Yen)")
self.send(conn, "[3] 🚪 Exit Game")
self.send(conn, "="*60)
choice = self.recv(conn)
if not choice:
break
if choice == '1':
self.send(conn, betting_system.show_odds())
self.send(conn, f"Uma Index [1-10] or 0 to cancel: ")
uma_index = self.recv(conn)
if not uma_index or uma_index == '0':
continue
self.send(conn, "Amount to bet: ")
amount = self.recv(conn)
if not amount:
continue
success, msg = betting_system.place_bet(uma_index, amount)
self.send(conn, msg)
if success:
race = Race(umas, track_length, lcg_gen)
results = race.start_race()
winning_name = results[0].name
self.send(conn, "\n" + "="*60)
self.send(conn, f"🥇 WINNER: {winning_name}")
self.send(conn, "🏁 RACE FINISHED - STANDINGS 🏁")
for rank, uma in enumerate(results):
if rank == 0:
self.send(conn, f" #{rank + 1}: **{uma.name}**")
else:
self.send(conn, f" #{rank + 1}: {uma.name}")
self.send(conn, "="*60)
self.send(conn, betting_system.process_winnings(winning_name))
elif choice == '2':
if betting_system.balance >= betting_system.FLAG_PRICE:
betting_system.balance -= betting_system.FLAG_PRICE
self.send(conn, "\n" + "="*60)
self.send(conn, "🎉 CONGRATULATIONS! 🎉")
self.send(conn, "="*60)
self.send(conn, f"🚩 FLAG: {FLAG}")
self.send(conn, f"💰 Remaining Balance: {betting_system.balance:,.2f} Yen")
self.send(conn, "="*60 + "\n")
break
else:
need = betting_system.FLAG_PRICE - betting_system.balance
self.send(conn, f"\n❌ NOT ENOUGH MONEY!")
self.send(conn, f" You need: {need:,.2f} more Yen")
self.send(conn, f" Current: {betting_system.balance:,.2f} Yen")
self.send(conn, f" Target: {betting_system.FLAG_PRICE:,.2f} Yen\n")
elif choice == '3':
self.send(conn, "\n👋 Thank you for playing! Sayonara!")
break
else:
self.send(conn, "⚠️ Invalid choice! Please try again.")
conn.close()
print(f"[-] Client disconnected: {addr}")
def start(self):
self.server.bind((self.host, self.port))
self.server.listen(5)
print(f"[*] Server listening on {self.host}:{self.port}")
try:
while True:
conn, addr = self.server.accept()
thread = threading.Thread(target=self.handle_client, args=(conn, addr))
thread.daemon = True
thread.start()
except KeyboardInterrupt:
print("\n[*] Server shutting down...")
finally:
self.server.close()
if __name__ == "__main__":
server = CTFServer(host='0.0.0.0', port=1337)
server.start()
```
Challenge liên quan đến PRNG (Bộ sinh số giả ngẫu nhiên).
Máy tính là thiết bị logic, nó không biết cách tạo ra sự ngẫu nhiên thực sự. Nó dùng các thuật toán toán học để tạo ra các dãy số trông có vẻ ngẫu nhiên, gọi là PRNG (Pseudo-Random Number Generator).
PRNG cần một giá trị đầu vào gọi là Seed .Nếu Seed giống nhau thì giá trị các số sinh ra hoàn toàn giống nhau.
Ta chú ý rằng seed ở challenge này được chọn như sau:
```py=
lcg_gen = LCG(seed=int(time.time() * 1000))
```
Tức là sinh seed ngẫu nhiên dựa trên thời gian, và thời gian thì là một đại lượng mà ta hoàn toàn có thể đoán được, nó sẽ loanh quanh khoảng thời gian mà ta kết nói tới sever.
Challenge tự viết một class LCG. Công thức của nó là:$$X_{i+1} = (A \cdot X_{i} + C) \pmod M$$$A, C, M$: Là các hằng số công khai.
$X$: seed.
Vì $A, C, M$ đều lộ, nếu ta biết $X$ seed hiện tại thì ta tính được toàn bộ tương lai của dãy số. Chỉ cần brute-force khoảng thời gian xung quanh thời gian ta kết nói với sever.
Trước đó ta phải lấy danh sách (s_odds) rồi mới tìm seed. Vì odds là giá trị đầu tiên được sinh ra từ seed, ta sẽ phải dùng dữ liệu này để đối chiếu xem seed mình đoán có đúng không.
Khi tìm được seed thì ta chạy hàm Race() để cái seed nó biến đổi sang trạng thái mới, mà ta biết con ngựa nào thắng luôn rồi thì cứ all in thôi =))), cứ lập lại cho đến khi đủ 1000000 Yên (chắc là code lỗi vì mình thấy trong source sever thì 100000 Yên là đủ).
> Lưu ý là phải copy y nguyên các class LCG, Uma, Race qua để đúng logic code
```py=
from pwn import *
import re
import time
class LCG:
def __init__(self, seed=None):
self.M = 3287099835290195791649721107570004318267582246774226300213811649423225012170212287749142020192530464259481640735325781154239321190173754611921808393447501266888056440801327249770117062004226214495561535681369100056816111927528166402933420016083242583498654107351130511963025469986264222148916890724787070272803001
self.A = 2894777803559679218352190554394546666288076241619396764815133803772055992385941101609673369526743001851311268818989398042025043757099501509707445437248873549911623395016719427960022056683203263457712425937702117698191301068614729419207159556006174347883149816514548460625672181804096655842375751872041822338728496
self.C = 122
self.seed = seed % self.M
def _next(self):
self.seed = (self.A * self.seed + self.C) % self.M
return self.seed
def uniform(self, min_v, max_v):
return min_v + (self._next() / self.M) * (max_v - min_v)
class Uma:
def __init__(self, name, spd=50, sta=50, sk=50):
self.name, self.spd, self.sta, self.sk = name, spd, sta, sk
self.pos, self.cur_spd = 0.0, spd
def update(self, lcg, length):
prog = self.pos / length
drain = max(0, prog - 0.7)
mod = 1 - (drain * (1.5 - self.sta / 100.0))
self.cur_spd = max(self.spd * 0.4, self.spd * mod)
sk_mul = 2.5 if prog > 0.7 else 1.0
bst = lcg.uniform(0.95, 1.05 + self.sk * 0.01 * sk_mul)
mic = lcg.uniform(0.99, 1.01)
self.pos += self.cur_spd * mic + bst
class Race:
def __init__(self, umas, length, lcg):
self.umas, self.len, self.lcg = umas, length, lcg
def run(self):
for u in self.umas: u.pos = 0.0
step = 0
while step < 1000:
step += 1
done = False
for u in self.umas:
u.update(self.lcg, self.len)
if u.pos >= self.len: done = True
if done: break
return sorted(self.umas, key=lambda x: x.pos, reverse=True)
r = remote('67.223.119.69', 5008)
NAMES = ["Special Week", "Silence Suzuka", "Gold Ship", "Mejiro McQueen", "Tokai Teio", "Rice Shower", "Symboli Rudolf", "Kitasan Black", "Satono Diamond", "Daiwa Scarlet"]
t0, gen = int(time.time()*1000), None
while True:
buf = r.recvuntil(b"Exit Game").decode()
bal = float(re.search(r"BALANCE:\s*([\d,]+\.\d+)", buf).group(1).replace(',', ''))
if bal >= 1000000:
r.sendline(b'2')
print(r.recvall().decode())
break
r.sendline(b'1')
dat = r.recvuntil(b"cancel: ").decode()
s_odds = re.findall(r"1 to\s+(\d+\.\d+)", dat)
umas = [Uma(n) for n in NAMES]
tot = sum(u.spd + u.sta + u.sk for u in umas)
if not gen:
for i in range(-5000, 5000):
l = LCG(t0+i)
if [f"{max(1.5, (tot/(u.spd+u.sta+u.sk))*l.uniform(0.8,1.2)*0.5):.2f}" for u in umas] == s_odds:
gen = l
break
else:
[gen.uniform(0.8, 1.2) for _ in umas]
win = Race([Uma(n) for n in NAMES], 500.0, gen).run()[0].name
r.sendline(str(NAMES.index(win)+1).encode())
r.sendlineafter(b"bet: ", str(int(bal)).encode())
```
:::spoiler Flag
KCSC{3m4m4}
:::
# theMidnIghTMan
chall.py:
```py=
from Crypto.Cipher import AES
import hashlib, os, time
class x3AES:
def __init__(self, key1, key2, key3):
self.key1 = hashlib.md5(key1).digest()
self.key2 = hashlib.md5(key2).digest()
self.key3 = hashlib.md5(key3).digest()
self.secret = os.urandom(16)
def pad(self, data, block_size):
length = len(data)
if length % block_size == 0:
return data
pad_len = block_size - (length % block_size)
return data + bytes([pad_len]) * pad_len
def encrypt(self, plaintext):
padded_data = self.pad(plaintext, AES.block_size)
iv = os.urandom(AES.block_size)
cipher1 = AES.new(self.key1, AES.MODE_OFB, iv)
enc1 = cipher1.encrypt(padded_data)
cipher2 = AES.new(self.key2, AES.MODE_CBC, self.secret)
enc2 = cipher2.encrypt(enc1)
cipher3 = AES.new(self.key3, AES.MODE_ECB)
enc3 = cipher3.encrypt(enc2)
return iv + enc3
def decrypt(self, ciphertext):
iv = ciphertext[:AES.block_size]
enc3 = ciphertext[AES.block_size:]
cipher3 = AES.new(self.key3, AES.MODE_ECB)
dec3 = cipher3.decrypt(enc3)
cipher2 = AES.new(self.key2, zAES.MODE_CBC, self.secret)
dec2 = cipher2.decrypt(dec3)
cipher1 = AES.new(self.key1, AES.MODE_OFB, iv)
dec1 = cipher1.decrypt(dec2)
return dec1
class Challenge:
def __init__(self):
self.flag = b"KCSC{??????????????????????????????????????????}"
self.key = os.urandom(9)
self.key1 = self.key[:3]
self.key2 = self.key[3:6]
self.key3 = self.key[6:]
self.cipher = x3AES(self.key1, self.key2, self.key3)
self.banner = """
;
ED.
E#Wi L.
t E###G. EW: ,ft t .Gt . .
.. : Ej E#fD#W; E##; t#E Ej j#W: Di Dt GEEEEEEEL
,W, .Et E#, E#t t##L E###t t#E E#, ;K#f E#i E#i ,;;L#K;;.
t##, ,W#t E#t E#t .E#K, E#fE#f t#E E#t .G#D. E#t E#t t#E
L###, j###t E#t E#t j##f E#t D#G t#E E#t j#K; E#t E#t t#E
.E#j##, G#fE#t E#t E#t :E#K:E#t f#E. t#E E#t ,K#f ,GD; E########f. t#E
;WW; ##,:K#i E#t E#t E#t t##L E#t t#K: t#E E#t j#Wi E#t E#j..K#j... t#E
j#E. ##f#W, E#t E#t E#t .D#W; E#t ;#W,t#E E#t .G#D: E#t E#t E#t t#E
.D#L ###K: E#t E#t E#tiW#G. E#t :K#D#E E#t ,K#fK#t E#t E#t t#E
:K#t ##D. E#t E#t E#K##i E#t .E##E E#t j###t f#t f#t t#E
... #G .. E#t E##D. .. G#E E#t .G#t ii ii fE
j ,;. E#t fE ,;. ;; :
L: ,
"""
def print_banner(self):
for line in self.banner.split("\n"):
print(line)
time.sleep(0.1)
def main(self):
prefix = b'Midnight is coming. Stay awake!!'
print(prefix)
while True:
option = input("Enter your option:\n1. Encrypt\n2. Decrypt\n3. Get Flag\n> ")
try:
if option == "1":
plaintext = bytes.fromhex(input("Enter plaintext (hex): "))
ciphertext = self.cipher.encrypt(plaintext)
print("Ciphertext: ", ciphertext.hex())
elif option == "2":
iv = bytes.fromhex(input("Enter IV (hex): "))
assert len(iv) == 16
ciphertext = bytes.fromhex(input("Enter ciphertext (hex): "))
plaintext = self.cipher.decrypt(iv + prefix + ciphertext)
print("Plaintext: ", plaintext.hex())
elif option == "3":
key_input = bytes.fromhex(input("Enter the key (hex): "))
if key_input == self.key:
print("Flag:", self.flag.decode())
else:
print("Wrong key!")
break
else:
break
except:
print("Don't pwn me!")
break
if __name__ == "__main__":
challenge = Challenge()
challenge.print_banner()
challenge.main()
```
Một challenge liên quan đến AES, (cụ thể là về MITM) vì khóa quá nhỏ.
Quá trình giải mã của server: Nhận đầu vào: IV (16 bytes) và Ciphertext (do ta gửi). Server thực hiện giải mã chuỗi IV + Prefix + Ciphertext qua 3 lớp:
- Lớp 3 (Key3 - ECB): AES-ECB.
- Lớp 2 (Key2 - CBC): AES-CBC (dùng secret làm IV riêng).
- Lớp 1 (Key1 - OFB): AES-OFB (dùng IV ta gửi).
ta sẽ tấn công ngược từ lớp trong cùng ra, theo thứ tự: Key1 $\rightarrow$ Key3 $\rightarrow$ Key2.
Key1: OFB: Đặc điểm của OFB là nó tạo ra một dòng Keystream chỉ dựa vào Key và IV.
$$P = C \oplus Keystream(Key, IV)$$
Lỗ hổng ở đây là hàm `decrypt` của server trả về Plaintext cuối cùng.
Nếu ta gửi 2 yêu cầu giải mã với cùng một phần Ciphertext nhưng IV khác nhau:
=> Dữ liệu đi vào Lớp 2 và Lớp 3 là giống hệt nhau => Đầu ra sau khi qua Lớp 3 và Lớp 2 là một chuỗi cố định (gọi là Interm).Sự khác biệt ở Plaintext cuối cùng chỉ do Keystream của Key1 tạo ra.
Vậy khi gửi `IV_A`, thì nhận được `Plaintext_A`.
Gửi `IV_B`, nhận được `Plain_B`.
Lấy (1) XOR (2), ta sẽ thu được: $$Plain\_A \oplus Plain\_B = Keystream(K1, IV\_A) \oplus Keystream(K1, IV\_B)$$
Mà Ta biết `Plain_A`, `Plain_B`, `IV_A`, `IV_B`.
Do đó ta brute-force từ 0 đến $2^{24}$ để thử mọi $K_1$. Nếu $K_1$ nào sinh ra hai keystream thỏa mãn phương trình trên thì dó là Key1 đúng.
Key 3:
Sau khi có key1, ta loại bỏ được lớp OFB.Giờ ta tấn công vào ECB và CBC.Ta chọn payload sao cho input đầu vào có cấu trúc lặp lại: `Block1 | Block2 | Block2`.
- `Prefix` = `Midnight is coming. Stay awake!! `(32 bytes).
- `Block1` (Prefix[0:16]): `Midnight is comi`
-`Block2` (Prefix[16:32]): `ng. Stay awake!!`
Vậy ta gửi payload là Prefix[16:32] (chính là Block2).
Sẽ nhận được p3. Dùng key1 tìm được ở bước 1 để tính `W` (Đây là output của lớp CBC.
$$W = P_3 \oplus Keystream(K1, IV_3)$$
Xét 2 block của `W` là `W2` (ứng với Prefix Block 2) và `W3` (ứng với Payload ta gửi):
Gọi $v_1 = D_{K3}(Block1)$ và $v_2 = D_{K3}(Block2)$.
Theo quy tắc giải mã CBC:
- Tại vị trí 2: $W_2 = D_{K2}(v_2) \oplus v_1$
- Tại vị trí 3: $W_3 = D_{K2}(v_2) \oplus v_2$ (Vì input của block 3 là $v_2$, và block trước đó (ciphertext) cũng là $v_2$)
Vậy giờ ta lấy $W_2 \oplus W_3$:
$$(D_{K2}(v_2) \oplus v_1) \oplus (D_{K2}(v_2) \oplus v_2) = v_1 \oplus v_2$$$$W_2 \oplus W_3 = D_{K3}(Block1) \oplus D_{K3}(Block2)$$
Đến đây Brute-force key3 sao cho phương trình đó bằng thôi
Key2:
Ta đã có key1 và key3.Ta xét CBC tại Block 2 để tìm key2.
Dùng key3 đã tìm được, tính giá trị `v1` và `v2` (đầu ra của lớp ECB).
$$\begin{aligned}
v_1 &= D_{K_3}(B_1) \\
v_2 &= D_{K_3}(B_2)
\end{aligned}$$
Thì lúc này :$$W_2 = D_{K2}(v_2) \oplus v_1$$Suy ra:$$D_{K2}(v_2) = W_2 \oplus v_1$$
Rồi ta brute-force tìm key2 sao cho khi giải mã AES (ECB) khối $v_2$ thì kết quả bằng $W_2 \oplus v_1$.
solve.py:
```py=
from pwn import *
from Crypto.Cipher import AES
from Crypto.Util.strxor import strxor as xor
import hashlib, os
from tqdm import tqdm
r = remote('67.223.119.69', 5011)
PREFIX = b'Midnight is coming. Stay awake!!'
B1, B2 = PREFIX[:16], PREFIX[16:32]
def oracle(iv, ct=b''):
r.sendlineafter(b'> ', b'2')
r.sendline(iv.hex().encode())
r.sendline(ct.hex().encode())
r.recvuntil(b': ')
return bytes.fromhex(r.recvline().strip().decode())
def crypt(k_bytes, data, iv=None):
key = hashlib.md5(k_bytes).digest()
if iv: return AES.new(key, AES.MODE_OFB, iv).decrypt(b'\x00' * len(data))
return AES.new(key, AES.MODE_ECB).decrypt(data)
iv1, iv2 = os.urandom(16), os.urandom(16)
p1, p2 = oracle(iv1), oracle(iv2)
tgt_k1 = xor(p1, p2)
k1 = next(k.to_bytes(3,'big') for k in tqdm(range(1<<24))
if xor(crypt(k.to_bytes(3,'big'), p1, iv1),
crypt(k.to_bytes(3,'big'), p2, iv2)) == tgt_k1)
iv3 = os.urandom(16)
p3 = oracle(iv3, B2)
w = xor(p3, crypt(k1, p3, iv3))
w2, w3 = w[16:32], w[32:48]
tgt_k3 = xor(w2, w3)
k3 = next(k.to_bytes(3,'big') for k in tqdm(range(1<<24))
if xor(crypt(k.to_bytes(3,'big'), B1),
crypt(k.to_bytes(3,'big'), B2)) == tgt_k3)
v1 = crypt(k3, B1)
v2 = crypt(k3, B2)
tgt_k2 = xor(w2, v1)
k2 = next(k.to_bytes(3,'big') for k in tqdm(range(1<<24))
if crypt(k.to_bytes(3,'big'), v2) == tgt_k2)
full_key = k1 + k2 + k3
print(f"[+] Full Key: {full_key.hex()}")
r.sendlineafter(b'> ', b'3')
r.sendline(full_key.hex().encode())
print(r.recvall().decode())
```
:::spoiler Flag
KCSC{a_formal_gift_to_honor_you_e5bb1bb281165c0}
:::
# 五六七
Một challenge quá ảo
chall.py:
```py=
from Crypto.Util.number import isPrime
import random
FLAG = open('flag.txt', 'rb').read()
p = int(input("Choose p > "))
if not isPrime(p) or p.bit_length() < 512:
print("Fail")
exit(1)
x = random.getrandbits(128)
def f(x):
res = 0
for i in range(7):
if i==5: continue
res += pow(i, x, p)
res %= p
return res
res = f(x)
print(res)
guess = int(input("guess x = "))
if guess == x:
print(f"Here is your flag: {FLAG}")
else:
print('false')
```
Ta được chọn một số nguyên tố $p$, ($p$ phải lớn hơn 512 bit)
Và ta có hàm số được định nghĩa là $f(x) = 1 + 2^x + 3^x + 4^x + 6^x \pmod p$.
Mục tiêu là tìm $x$ (128 bit) khi biết $f(x)$.
Ta sẽ dùng DLP để giải bài này:
Ta sẽ chọn số mũ $a$ sao cho $p = 2^a - 3$ là một số nguyên tố.Khi đó, ta có đẳng thức quan trọng:$$2^a \equiv 3 \pmod p$$
Điều này có nghĩa là trong trường $\mathbb{Z}_p$, số $3$ có thể được biểu diễn dưới dạng lũy thừa của $2$.
Nên ta thay $3 \equiv 2^a$ vào phương trình $f(x)$:
$$f(x) \equiv 1 + 2^x + (2^a)^x + (2^x)^2 + (2 \cdot 2^a)^x \pmod p$$
Đặt $Y = 2^x$, phương trình trở thành một đa thức theo biến $Y$:$$f(x) \equiv 1 + Y + Y^a + Y^2 + Y^{a+1} \pmod p$$
Hay:$$Y^{a+1} + Y^a + Y^2 + Y + (1 - \text{res}) \equiv 0 \pmod p$$.
Bậc của đa thức này chỉ khoảng 512. Nên có thể tìm nghiệm của đa thức này trong trường $\mathbb{Z}_p$ rất nhanh chóng.
Sau khi tìm được nghiệm $Y$, ta có bài toán Logarit rời rạc: $2^x \equiv Y \pmod p$.
Để giải bài toán này dễ dàng, ta cần $p-1$ có nhiều ước số nguyên tố nhỏ (smooth).
Ta có $p-1 = 2^a - 4 = 4(2^{a-2} - 1)$.
Dạng số $2^k - 1$ rất dễ phân tích thừa số. Nếu ta chọn $a-2$ là một số có nhiều ước số nhỏ, $p-1$ sẽ có nhiều thừa số nguyên tố nhỏ.
Ta các thừa số nguyên tố nhỏ sao cho tích của chúng lớn hơn $2^{128}$ (kích thước của $x$), sau đó dùng thuật toán Pohlig-Hellman và CRT để tìm $x$.
solve.py:
```py=
from pwn import *
from sage.all import *
HOST = '67.223.119.69'
PORT = 5012
def find_vulnerable_p():
for u in range(512, 1200):
target = 2**u - 3
if not is_pseudoprime(target):
continue
if is_prime(target):
p = target
order = p - 1
try:
factors_small = factor(order, limit=1000000)
smooth_part = 1
for f, e in factors_small:
smooth_part *= (f ** e)
if smooth_part > 2**128:
return p, u
except:
continue
exit(1)
def solve():
p, u = find_vulnerable_p()
r = remote(HOST, PORT)
print(r.recvuntil(b"Choose p > ").decode())
r.sendline(str(p).encode())
res = None
while True:
line = r.recvline(timeout=5).strip()
if not line:
continue
if b"Fail" in line:
return
if line.isdigit():
res = int(line)
break
F = GF(p)
P_ring = PolynomialRing(F, 'y')
y = P_ring.gen()
poly = y**(u+1) + y**u + y**2 + y + (1 - res)
roots = poly.roots()
real_x = None
def check_x(x_val):
ans = 0
for i in [0, 1, 2, 3, 4, 6]:
ans += pow(i, x_val, p)
return ans % p == res
for root, mult in roots:
val_Y = root
if val_Y == 0: continue
try:
x = discrete_log(val_Y, F(2))
if check_x(x):
real_x = x
break
except ValueError:
continue
if real_x is not None:
r.sendlineafter(b"guess x = ", str(real_x).encode())
print("\n" + r.recvall().decode())
if __name__ == "__main__":
solve()
```
:::spoiler Flag
KCSC{why_do_you_choose_crypto?}
:::
# e
Do chưa học về Latice nên có lẽ những kiến thức mình viết dưới đây sẽ có nhiều sai sót
chall.py:
```py=
flag = ???
def hash(msg: bytes) -> bytes:
msg = list( map( int, bin( int((msg).hex(), 16) )[2:] ) )
g = 35835915874844867368919076489095108449946327955754392558399825615420669938882575126094039892345713852759
n = 36
for b in msg:
n = (g * (n + int(3 if b else 6))) & ((1 << 512) - 1)
return (n)
print(hash(flag), len(flag))
# 4733050325257322080995332543647525767201053028813565191926008882515250832618947947748792937964480829322550895675027130098198538016461241391725598109040829 28
```
Một challenge quá ngắn gọn, nhưng độ khó thì là cao nhất, ta được cung cấp một hàm `hash`, và giá trị của flag sau khi đi qua hàm đó kèm theo với đó là ta biết độ dài flag = 28 byte.
Ta phân tích hàm `hash` trước:
```py=
def hash(msg: bytes) -> bytes:
msg = list( map( int, bin( int((msg).hex(), 16) )[2:] ) )
g = 35835915874844867368919076489095108449946327955754392558399825615420669938882575126094039892345713852759
n = 36
for b in msg:
n = (g * (n + int(3 if b else 6))) & ((1 << 512) - 1)
return (n)
```
Đầu tiên flag của chúng ta sẽ được chuyển đổi thành một chuỗi nhị phân. Vậy theo lý thuyết thì nếu flag là 28 byte, thì nó sẽ được chuyển thành một chuỗi bit có độ dài $28.8 = 224$ bit.
Đúng không? Mình đã sai lầm khi nghĩ như vậy và đã thất bại trong việc giải bài này trong lúc diễn ra giải.
Thật sự thì chuỗi bit chúng ta chỉ có 223 bit thôi, vì hàm `bin`() trong python nó loại bỏ đi chữ số 0 ở đầu. Ví dụ như ta biết byte đầu flag là chữ `K`, thì nhị phân của nó là `01001011`, khi qua `int()`, `bin()`. Nó sẽ trở thành `1001011`. Đây là lý do độ dài chuỗi bit là 223 thay vì 224 =))).
Kết quả: Flag bây giờ không còn là chuỗi ký tự, mà là một vector các biến nhị phân $\vec{b} = [b_0, b_1, \dots, b_{L-1}]$.
Giờ hãy phân tích công thức biến đổi
```py=
n = (g * (n + int(3 if b else 6))) & ((1 << 512) - 1)
```
Đoạn `int(3 if b else 6)` nó quyết định giá trị cộng thêm dựa trên bit $b$. Tức là:
- Nếu $b=1 \rightarrow$ cộng 3.
- Nếu $b=0 \rightarrow$ cộng 6.
Còn `& ((1 << 512) - 1)` tức là giữ lại 512 bit thấp, nó tương đương với việc đem mod cho $2^{512}$
Gọi:
- $n_k$: Giá trị hash ở bước thứ $k$.
- $b_k$: Bit thứ $k$ của input ($b_k \in \{0, 1\}$).
- $M = 2^{512}$: Modulo của phép toán.
- $\Delta_k$: Giá trị được cộng vào $n$.
- Nếu $b_k = 1 \implies \Delta_k = 3$.
- Nếu $b_k = 0 \implies \Delta_k = 6$.
Để tấn công bằng toán học, ta không thể dùng câu lệnh if/else. Ta phải chuyển nó một cái công thức chung tổng quát nào đó, sau khi mò mẫm, mình tìm ra được công thức tương đương:
$$\Delta_k = 6 - 3b_k$$
Thử lại:
$b=1: 6 - 3.1 = 3$ (Đúng).
$b=0: 6 - 3.0 = 6$ (Đúng).
Giờ ta phân tích xem trạng thái $n$ thay đổi thế nào qua từng vòng lặp. Gọi $L$ là tổng số bit.
Lần 1 ($b_0$):$$n_1 = g \cdot (n_0 + \Delta_0)$$
Lần 2 ($b_1$):$$n_2 = g \cdot (n_1 + \Delta_1)$$
Ta thay $n_1$ vào:$$n_2 = g \cdot ( [g(n_0 + \Delta_0)] + \Delta_1 ) = g^2 n_0 + g^2 \Delta_0 + g^1 \Delta_1$$
Và tương tự như vậy lần 3 sẽ là:
$$n_3 = g \cdot (n_2 + \Delta_2) = g^3 n_0 + g^3 \Delta_0 + g^2 \Delta_1 + g^1 \Delta_2$$
Tổng quát lên $L$ vòng:
$$H \equiv n_0 \cdot g^L + \sum_{i=0}^{L-1} \Delta_i \cdot g^{L-i} \pmod M$$
Giờ thay $\Delta_i = 6 - 3b_i$ vào:$$H \equiv n_0 \cdot g^L + \sum_{i=0}^{L-1} (6 - 3b_i) \cdot g^{L-i} \pmod M$$
Ta cần phải tách ẩn qua một bên, hằng số qua một bên:
$$H \equiv n_0 g^L + \sum_{i=0}^{L-1} 6 \cdot g^{L-i} - \sum_{i=0}^{L-1} 3 \cdot g^{L-i} \cdot b_i \pmod M$$
$$\sum_{i=0}^{L-1} (3 \cdot g^{L-i}) \cdot b_i \equiv C - H \pmod M$$
Đặt hệ số $c_i = 3 \cdot g^{L-i}$ và $Target = (C - H) \pmod M$, ta có phương trình tuyến tính có 2 nghiệm $0/1$:$$\sum_{i=0}^{L-1} c_i \cdot b_i \equiv Target \pmod M$$.
Và sau khi tham khảo wu của anh [Le Ha](https://hackmd.io/@at20n0118/Hy5LBsS51l#Knapsack-Problem) và anh [@nomorecaffeine](https://hackmd.io/@nomorecaffeine/r1xstVfxC#II-Knapsack-Problem), mình biết rằng. Phương trình trên chính là bài toán Knapsack (The Subset Sum Problem).
Tức là:
- Ta có một tập hợp các số $c_0, c_1, \dots, c_{222}$.
- Ta cần tìm cách chọn một số phần tử trong đó (tương ứng $b_i=1$ là chọn, $b_i=0$ là không chọn).
- Sao cho tổng của chúng bằng $Target$ (theo modulo $M$).
Bài toán này về lý thuyết là NP-Hard. Tuy nhiên, trong mật mã học, nó dễ bị giải nếu mật độ (density) thấp.
Mật độ được tính bằng:$$d = \frac{N}{\log_2(M)}$$
Trong bài này: $N = 223$, $\log_2(M) = 512$.
Thì cái mật độ này quá lớn rồi, phải tìm cách giảm số ẩn lại. Sau khi được các anh hint, thì có 2 cách để giảm số lượng ẩn lại, đầu tiên là biết format của flag là "KCSC{}" thì ta đã giảm được $47$ ẩn. Và chúng ta còn giảm thêm được 22 ẩn nữa nhờ giả định bit đầu của các byte ẩn là 0, vì nó là ký tự Ascii in được, ta đã đưa được tổng số ẩn xuống 154.
Vậy lúc này mật độ là 154/512 thì $d \approx 0.3$. Với mật độ này, thuật toán Lattice Reduction (LLL/BKZ) giải rất tốt.
Để giải bài toán Knapsack, ta chuyển nó về bài toán tìm vector ngắn nhất (Shortest Vector Problem - SVP) trong lưới.
Ta xây dựng một ma trận cơ sở $B$ để biểu diễn mối quan hệ tuyến tính này. Mục tiêu là tìm một vector $\mathbf{v}$ là tổ hợp tuyến tính của các hàng trong $B$, sao cho $\mathbf{v}$ thật ngắn.
Ma trận $B$ có kích thước $(N+2) \times (N+2)$:$$B = \begin{bmatrix}
1 & 0 & \dots & 0 & K \cdot c_0 \\
0 & 1 & \dots & 0 & K \cdot c_1 \\
\vdots & \vdots & \ddots & \vdots & \vdots \\
0 & 0 & \dots & 1 & K \cdot c_{N-1} \\
0 & 0 & \dots & 0 & K \cdot M \\
0 & 0 & \dots & 0 & -K \cdot Target
\end{bmatrix}$$
Trọng số $K$ ở đây Là một số lớn (ví dụ $2^{256}$). Ta nhân cột này với $K$ để ép nặng bất kỳ vector nào không thỏa mãn phương trình tổng. Nếu tổng $\sum c_i b_i - Target \neq 0$, thì giá trị ở cột cuối sẽ rất lớn (do nhân $K$), vector sẽ rất dài và thuật toán BKZ sẽ bỏ qua nó.
Khi nhân ma trận $B$ với vector nghiệm $\mathbf{x} = (b_0, b_1, \dots, b_{N-1}, k, 1)$, ta mong muốn thu được vector kết quả $\mathbf{v}_{short}$:$$\mathbf{v}_{short} = (b_0, b_1, \dots, b_{N-1}, 0)$$Phần đầu: Các bit $0$ hoặc $1$ của Flag.
Phần cuối: Số $0$ (Nghĩa là $\sum c_i b_i + kM - Target = 0$).
Và mình được hint là đùng BKZ để giải, vì BKZ mạnh hơn LLL. Nó chia lưới thành các khối nhỏ (blocks) kích thước $\beta$ và thực hiện tìm kiếm vector ngắn nhất (SVP) trên khối đó, sau khi chạy thử với nhiều trường hợp, mình thấy đặt block_size là 22 thì lý tưởng nhất (mình nghĩ vậy)
solve.sage:
```py=
from sage.all import *
from Crypto.Util.number import *
import binascii
import time
F = Zmod(2**512)
h_val = F(4733050325257322080995332543647525767201053028813565191926008882515250832618947947748792937964480829322550895675027130098198538016461241391725598109040829)
g = F(35835915874844867368919076489095108449946327955754392558399825615420669938882575126094039892345713852759)
n0 = F(36)
M = 2**512
total_bits = 223
PR = PolynomialRing(F, 'b', total_bits, implementation='generic')
VARS = PR.gens()
n = n0
for i in range(total_bits):
n = g * (n + (6 - 3*VARS[i]))
f = n - h_val
known_bits_format = bin(bytes_to_long(b"KCSC{"))[2:]
known_bits_end = bin(bytes_to_long(b"}"))[2:].zfill(8)
values = {}
for i in range(len(known_bits_format)):
values[VARS[i]] = int(known_bits_format[i])
for i in range(len(known_bits_end)):
values[VARS[-(i+1)]] = int(known_bits_end[-(i+1)])
start_unknown = len(known_bits_format)
len_unknown = total_bits - len(known_bits_format) - len(known_bits_end)
for i in range(0, len_unknown, 8):
values[VARS[start_unknown + i]] = 0
f_subs = f.subs(values)
unknown_vars = [v for v in VARS if v not in values]
poly_coeffs = [f_subs.coefficient(v) for v in unknown_vars]
constant_term = f_subs.constant_coefficient()
coeffs_int = [c.constant_coefficient().lift() for c in poly_coeffs]
target = (-constant_term).lift() % M
num_vars = len(coeffs_int)
B = Matrix(ZZ, num_vars + 2, num_vars + 2)
N = 2**256
lattice_target = target * N
for i in range(num_vars):
B[i, i] = 1
B[i, num_vars + 1] = coeffs_int[i] * N
B[num_vars, num_vars + 1] = M * N
B[num_vars+1, num_vars] = 1
B[num_vars+1, num_vars+1] = -lattice_target
bs = 22
B = B.BKZ(block_size=bs)
found_flag = False
for row in B:
vec = list(row)
if vec[-1] == 0:
potential_sol = vec[:num_vars]
solution_bits = []
is_valid = False
if all(x in [0, 1] for x in potential_sol):
solution_bits = potential_sol
is_valid = True
elif all(x in [0, -1] for x in potential_sol):
solution_bits = [-x for x in potential_sol]
is_valid = True
if is_valid:
final_bits = [0] * total_bits
for v, val in values.items():
idx = VARS.index(v)
final_bits[idx] = val
for i, val in enumerate(solution_bits):
var_name = unknown_vars[i]
idx = VARS.index(var_name)
final_bits[idx] = val
bit_str = "0" + "".join(str(b) for b in final_bits)
try:
flag_int = int(bit_str, 2)
flag_bytes = long_to_bytes(flag_int)
if b"KCSC" in flag_bytes:
print(flag_bytes.decode())
found_flag = True
break
except:
pass
```
:::spoiler Flag
KCSC{Th1s_1s_4_fl4g_f0r_y0u}
:::
---
# END
Em xin gửi lời cảm ơn đến anh Hào, anh Linh, anh Quân cũng như các anh chị trong clb thời gian qua đã training, hỗ trợ em.