ARA CTF Qual 2025 All Crypto Writeup
Pengen misuh, karena kekonyolan diri sendiri tidak lolos final, sad.
Jujur aja, chall tahun ini lebih gampang, cuma ya konyol aja gw kenapa ga lolos, lawak-lawak
IDK
Diberikan 2 file, chall.py dan out.txt, berikut ini isinya :
256084579755578542086218114125551851696556861593570039505266814472150395878124073152656973321432612003144066936748696264028836531055262757130008715084730883819241087427098304783995306535367534733304145108570867972800258098243859482322476642688960662237622617416182704984186000558550843048072667969693909457460433288472678049520222878986059183284526212678947818144261221015234212975218108988434750104221897690510909643000785039061676286339251000740701559583623131330615562434944630449204245703732378232440268218842380979930098451204379186073196818865288754028826412016718919661055361073803892996654255614341703973552434214690353439360094470658122060830888746150315689979059717648375863348048066762937533536365343157756588706348597583048596983736117880436305353954912296512342383274022448411248606056432927059528855960392713079667127767956297028547424422986362256747185079725100282718333011603255556269882239693370336170379132596784554724638130705877846123157852498059818857818750288084026416993535430979311127457051695804774275443852341594388987973489146842076451112004647710643951581665730372994225804955394591028139909689585488667786985783952428608974048888953446024160486816173283118444468250000061627095741330535010789084631380643987211428313705942146032852937784033011933867253067722735153766309842103066833140500431081204199978684824856672490683515669403409153235884644366865708468210622050158958459441825447471008285070220259796881210326268946836596353946878980187089097308731848261632
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from Crypto.Util.number import *
from sympy import nextprime
from Crypto.Util.Padding import pad
n = 8
flag = pad(SOMETHING ,n)
assert len(flag)%n == 0
n = len(flag)//n
flag = [flag[i:i+n] for i in range(0,len(flag),n)]
c = sum([nextprime(bytes_to_long(flag[i]))*2**(0x1337-158*(2*i+1)) for i in range(len(flag))])
print(c)
Analysis
dari chall.py bisa kita baca bahwa flag nya dibagi jadi 8 bagian, kemudian masing-masing bagian kita jadikan integer dan kita kalikan dengan $2^{0x1337 - 158(2i+1)}$. abis itu kita sum kan, atau notasi matematisnya
\[c = \sum_{i=0}^8 nextprime((flag[i])) \cdot 2^{0x1337 - 158(2i+1)}\]untuk recovernya tinggal kita modulo $2^{0x1337 - 158(2i-1)}$ lalu hasilnya kita bagi $2^{0x1337- 158(2i+1)}$, ya simpel nanti yang bakal kita dapat adalah nextprime((flag[i])). abis itu tinggal kita tebak bytes terakhirnya, karena disamarkan dengan next prime
Solution
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from Crypto.Util.number import *
out = 256084579755578542086218114125551851696556861593570039505266814472150395878124073152656973321432612003144066936748696264028836531055262757130008715084730883819241087427098304783995306535367534733304145108570867972800258098243859482322476642688960662237622617416182704984186000558550843048072667969693909457460433288472678049520222878986059183284526212678947818144261221015234212975218108988434750104221897690510909643000785039061676286339251000740701559583623131330615562434944630449204245703732378232440268218842380979930098451204379186073196818865288754028826412016718919661055361073803892996654255614341703973552434214690353439360094470658122060830888746150315689979059717648375863348048066762937533536365343157756588706348597583048596983736117880436305353954912296512342383274022448411248606056432927059528855960392713079667127767956297028547424422986362256747185079725100282718333011603255556269882239693370336170379132596784554724638130705877846123157852498059818857818750288084026416993535430979311127457051695804774275443852341594388987973489146842076451112004647710643951581665730372994225804955394591028139909689585488667786985783952428608974048888953446024160486816173283118444468250000061627095741330535010789084631380643987211428313705942146032852937784033011933867253067722735153766309842103066833140500431081204199978684824856672490683515669403409153235884644366865708468210622050158958459441825447471008285070220259796881210326268946836596353946878980187089097308731848261632
flag = []
for i in range(8):
if i == 0:
flag.append(long_to_bytes(out // 2**(0x1337-158*(2*i+1))))
else :
flag.append(long_to_bytes((out % 2**(0x1337-158*(2*i-1))) // 2**(0x1337-158*(2*i+1))))
print(b"".join(flag))
#output : b"ARA6{saya_terus_terang`]a_tahu_ini_tiba_tiba_te\xabus_terang_saya_tidak_di\x81eri_tahu_saya_tidak_tah\xcd_dan_saya_bahkan_bertan{a_tanya_kenapa_kok_saya\xb9tidak_diberi_tahu_sampb'_hari_ini_saya_ga_tahu}+"
# ARA6{saya_terus_terang_ga_tahu_ini_tiba_tiba_terus_terang_saya_tidak_diberi_tahu_saya_tidak_tahu_dan_saya_bahkan_bertanya_tanya_kenapa_kok_saya_tidak_diberi_tahu_sampai_hari_ini_saya_ga_tahu}
Flag
ARA6{saya_terus_terang_ga_tahu_ini_tiba_tiba_terus_terang_saya_tidak_diberi_tahu_saya_tidak_tahu_dan_saya_bahkan_bertanya_tanya_kenapa_kok_saya_tidak_diberi_tahu_sampai_hari_ini_saya_ga_tahu}
Currently In A Relationship (nope)
Diberikan cukup banyak file, ada chall.py, flag1.enc, flag2.enc dan out.txt
Analysis
Sub Chall 1.
Perhatikan isi dari chall.py sebagai berikut :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from Crypto.PublicKey import RSA
FLAG = open('flag.png', 'rb').read()
def encrypt_block(data, rsa_key):
num1 = int.from_bytes(data, "big")
num2 = 24 * num1 + 50
ct_arr = []
for num in [num1, num2]:
if num >= rsa_key.n:
raise ValueError("num >= n")
enc = pow(num, rsa_key.e, rsa_key.n)
ct = enc.to_bytes(1536 // 8, "big")
ct_arr.append(ct)
return ct_arr
def split_into_blocks(data, size):
return [data[i:i+size] for i in range(0, len(data), size)]
blocks = split_into_blocks(FLAG, (1536 // 8) - 1)
key = RSA.generate(1536, e=245)
ct1 = []
ct2 = []
# print(blocks)
for block in blocks:
enc = encrypt_block(block, key)
ct1.append(enc[0])
ct2.append(enc[1])
with open("flag1.enc", "wb") as file:
file.write(b''.join(ct1))
with open("flag2.enc", "wb") as file:
file.write(b''.join(ct2))
with open("out.txt", "w") as file:
file.write(str(key.n))
Di script ini, sebuah file png dibaca dalam bytes, kemudian di pecah menjadi berukuran 191 bytes sampai block terakhir, yang kemudian setiap block nya di enkripsi dengan fungsi encrypt_block() dan menghasilkan enc[0] dan enc[1] yang dikumpulkan di list ct1 dan ct2 lalu di join.
Nah, kita diberikan output dari ct1, ct2 dan out yang merupakan public_key.n, apa sebenarnya nilai dari enc[0] dan enc[1]? dilihat dari fungsi encrypt_block() flag tadi dijadikan 2 hal yakni num1 dan num2 yang selanjutnya akan di enkripsi dengan RSA, kemudian hasilnya disimpan lagi dalam bytes, jelas bahwa n nya 1536 bit, maka hasil enkripsinya masing-masing juga 1536 bit atau setara dengan 192 bytes. jelas bahwa num1 dan num2 berhubungan secara linear, maka kita bisa melakukan Franklin Reiter Attack.
Implementasinya kita bisa pakai ini.
Solusi
Sub Chall 1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from sage.all import *
from Crypto.Util.number import long_to_bytes, bytes_to_long
from tqdm import tqdm
def poly_gcd(f1, f2):
while f2:
f1, f2 = f2, f1 % f2
return f1.monic()
def franklin_reiter(f1, f2, c1, c2, e):
g1 = f1**e - c1
g2 = f2**e - c2
g = poly_gcd(g1, g2)
return -g.coefficients()[0]
n = 2006229242705761089244592364207487949361624338841743623343805892221493070924037058254018297047431317939813864991059916189920337729657360958791901498094872893965915056039447587256241362853002483335969409304627875490211093916970574649139997320618181046783486917322537376225003847401650196587662851173839236573632029215622660178722050004235739680197131963311215282046777269310276765557543166692460758171807832418320139197657628048720779839826970025196052649121618029
e = 245
ct1 = open('flag1.enc', 'rb').read()
print(len(ct1))
size = 1536 // 8
ct1 = [ct1[i : i + size] for i in range(0, len(ct1), size)]
ct1 = [int.from_bytes(x, 'big') for x in ct1]
print(len(ct1))
ct2 = open('flag2.enc', 'rb').read()
ct2 = [ct2[i : i + size] for i in range(0, len(ct2), size)]
ct2 = [int.from_bytes(x, 'big') for x in ct2]
P = PolynomialRing(Zmod(n), 'x')
x = P.gen()
f1 = x
f2 = 24*x + 50
flag = []
for i in tqdm(range(len(ct1))):
flag.append(franklin_reiter(f1, f2, ct1[i], ct2[i], e))
flag = [int(x).to_bytes((1536 // 8) - 1, "big") for x in flag]
with open("flag.png", "wb") as file:
file.write(b''.join(flag))
dari sini kita akan berhasil merecover file png nya sebagai berikut : 
Sub Chall 2.
Disini flag dienkripsi dengan AES, dan tidak ada yang aneh terkait dengan implementasi AES nya, namun ada sedikit keanehan bahwa password nya di pastikan panjangnya 16, 24, dan 32 dan istilah yang digunakan bukan key. So, download file nya lalu saya mencoba rockyou.txt dan boom dapatlah flagnya
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
with open('rockyou.txt', 'r', errors='ignore') as f:
lines = f.read().splitlines()
key_candidate = []
for i in lines:
if len(i) == 16 or len(i) == 24 or len(i) == 32:
key_candidate.append(i)
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
with open('flag2_.enc', 'rb') as f:
ct = f.read()
for password in key_candidate:
try:
cipher = AES.new(password.encode(), AES.MODE_ECB)
decrypted = cipher.decrypt(ct)
if b"ARA6" in decrypted :
print(decrypted)
print(password)
except:
pass
Flag
ARA6{fr4nkl1n_r3173r_70_r0cky0u_qu35710n_m4rk_0n_my_cryp706r4phy_ch4ll3n63_qu35710n_m4rk}
script-kiddies
Diberikan service netcat dan chall.py.
Analysis
Berikut isi chall.py :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
from Crypto.Cipher import Salsa20
from Crypto.Util.number import bytes_to_long, long_to_bytes
import ast
from secrets import token_bytes, token_hex
from zlib import crc32
import hashlib
import os
FLAG = b'ajdnkandkajsndkasndkandkandkandkandak'
KEY = token_bytes(32)
def verification(key,data):
return hashlib.sha1(key[:16] + data).hexdigest()
def encrypt_ticket(team_name):
cipher = Salsa20.new(key=KEY)
nonce = cipher.nonce
data={}
team_id = hex(crc32(team_name.encode()))[2:]
data['ticket'] = (team_name+'-'+team_id).encode()
checksum = verification(KEY,data['ticket'])
data = str(data).encode()
ciphertext = cipher.encrypt(data)
print('Your encrypted ticket is:', (nonce + bytes.fromhex(checksum) + ciphertext).hex())
def read_ticket(ticket):
packet = bytes.fromhex(ticket)
nonce = packet[:8]
checksum = packet[8:28].hex()
ciphertext = packet[28:]
try:
cipher = Salsa20.new(key=KEY, nonce=nonce)
plaintext = str(cipher.decrypt(ciphertext))[2:-1]
plaintext = ast.literal_eval(plaintext)
if verification(KEY[:16],plaintext['ticket']) != checksum:
print('Invalid checksum. Aborting!')
return
parsed = plaintext['ticket'].split(b'-')
if len(parsed) == 3 and parsed[-1] == b'invited':
print('We have invited and waited for you for a very long time, this for you')
print(FLAG.decode())
else:
print('sorry your team not invited yet, please wait next batch')
return 0
except:
print('Invalid data. Aborting!')
def menu():
print('[G]et ticket')
print('[I]nsert ticket')
print('[Q]uit')
def main():
print('Welcome to ARA 6.0, Get your ticket here!!\n')
while True:
menu()
option = input('\n>> ').upper()
if option == 'G':
team_name = input('Your team name: ')
encrypt_ticket(team_name)
elif option == 'I':
ticket = input('Your encrypted ticket (hex): ')
if(read_ticket(ticket) == 0):
exit(0)
elif option == 'Q':
exit(0)
else:
print('Invalid option!!\n')
if __name__ == '__main__':
main()
Jelas bahwa service memberikan hanya 2 pilihan yang bisa kita gunakan yakni Get Ticket dan Insert Ticket. Dari Get ticket service meminta input nama tim dari user dan kemudian mengenkripsinya mengikuti fungsi ini :
1
2
3
4
5
6
7
8
9
10
def encrypt_ticket(team_name):
cipher = Salsa20.new(key=KEY)
nonce = cipher.nonce
data={}
team_id = hex(crc32(team_name.encode()))[2:]
data['ticket'] = (team_name+'-'+team_id).encode()
checksum = verification(KEY,data['ticket'])
data = str(data).encode()
ciphertext = cipher.encrypt(data)
print('Your encrypted ticket is:', (nonce + bytes.fromhex(checksum) + ciphertext).hex())
Didalamnya digenerate crc32 nya berdasarkan nama timnya, lalu dibuat data ticket yang berisikan team_name-team_id yang dienkripsi kedalam Salsa20 dan juga digenerate checksum dari data ticket tersebut yang dimana kalau kita cek dia melakukan hashing dengan sha1 yang menggunakan panjang keynya itu 16.
1
2
def verification(key,data):
return hashlib.sha1(key[:16] + data).hexdigest()
Sementara itu pada opsi Insert Ticket user diminta untuk memberikan input ticket yang valid dan dilakukan validasi dengan fungsi read_ticket() yang isinya sebagai berikut :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def read_ticket(ticket):
packet = bytes.fromhex(ticket)
nonce = packet[:8]
checksum = packet[8:28].hex()
ciphertext = packet[28:]
try:
cipher = Salsa20.new(key=KEY, nonce=nonce)
plaintext = str(cipher.decrypt(ciphertext))[2:-1]
plaintext = ast.literal_eval(plaintext)
if verification(KEY[:16],plaintext['ticket']) != checksum:
print('Invalid checksum. Aborting!')
return
parsed = plaintext['ticket'].split(b'-')
if len(parsed) == 3 and parsed[-1] == b'invited':
print('We have invited and waited for you for a very long time, this for you')
print(FLAG.decode())
else:
print('sorry your team not invited yet, please wait next batch')
return 0
except:
print('Invalid data. Aborting!')
Jelas bahwa input user kemudian dipecah menjadi tiga yakni nonce nya checksum nya, dan ciphertextnya, kemudian ciphertext tadi di dekripsi degan nonce yang sama dan hasil plaintext nya akan dibaca sebagai dictionary, berikut sebagai ilustrasi
1
plaintext = {'ticket': b'ara_ara-aef234-......'}
kemudian plaintext[ticket] tadi bakal di split dan dicek apakah panjangnya 3 dan bagian akhirnya merupakann bytes invited, jika iya maka kita akan mendapatkan flag, jika tidak maka kita gagal.
Nah point yang perlu kita perhatikan adalah bahwa :
- Salsa20 adalah stream cipher
- Kita tahu nonce yang digunakan dan plaintext yang kita kirim
- checksum dibuat berdasarkan key random panjangnya 16 dan data ticket menggunakan sha1 Dari point 1, karena Salsa20 adalah stream cipher maka panjang ciphertext dipastikan sama dengan panjang plaintext, dan dari point 2 karena nonce yang digunakan sama dan kita bisa merekonstruksi plaintextnya di local maka kita bisa mendapatkan keystream yang digenerate berdasarkan nonce tersebut. Sehingga kita bisa beban mengirim ciphertext modifikasi yang hasil dekripsinya sesuai dengan kebutuhan kita. Selanjutnya dari point 3, ternyata sha1 rentan terhadap length extension attack, sehingga kita bisa menambah bytes
-invitedsupaya lolos validasi, source nya ada disini. Disini informasi yang kita butuhkan hanya panjang keynya plainitextnya sama string/ bytes yang ingin kita tambahkan di bagian akhir, yang nanti output dari implementasi ini adalah plaintext dan hasil hashing yang bersesuaian
Nah ternyata sha1 ini melakukan padding gitu ya gais, jadi ketika kita tambahkan dengan -invited di akhir, bagian team_id nya akan terkena padding sehingga menjadi sedikit lebih panjang. Nah dengan penyesuaian dan trial error, katika kita mencoba isi ticketnya yaitu team_name - team_id nya itu panjangnya 39, maka paddingnya menjadi minimum. karena kita tau panjang crc32 nya itu 8 dan ditambahkan - maka panjang team name yang kita kehendaki supaya minimum adalah 30. dan ketika dilakukan hash length extension panjang akhirnya menjadi 56, yang berarti supaya lolos dekripsi Salsa20, kita memerlukan keystream dengan panjangnya sama, sehingga nama tim yang perlu kita gunakan untuk dapatkan keystreamnya adalah dengan panjang 56 - 9 = 47. Langkah Exploitasi:
- Lakukan Get Ticket dengan panjang dari team_name nya 30, ambil checksumnya
- Lakukan Get Ticket dengan panjang dari team_name nya 47, ambil nonce dan ciphertextnya, lakukan xor antara plaintext aslinya dengan ciphertextnya maka kita peroleh keystream yang diinginkan.
- Lakukan Hash Length Extension Attack berdasarkan checksum dari langkah 1, dan tambahkan bytes
inviteddi akhir. - Kita xor antara keystream dengan plaintext yang kita inginkan untuk mendapatkan modified ciphertextnya
- Lakukan insert ticket dan kirimkan kombinasi nonce + checksum + ciphertext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from zlib import crc32
import hlextend
from pwn import *
p = remote("chall-ctf.ara-its.id", 5537)
data_ = {}
team_name = 'a' * 47
team_id = hex(crc32(team_name.encode()))[2:]
data_['ticket'] = (team_name+'-'+team_id).encode()
data_ = str(data_).encode()
msg = p.recvuntil(b"[Q]uit")
p.sendlineafter(b">>", b"G")
p.sendlineafter(b"Your team name:", team_name.encode())
ticket_enc = p.recvline_startswith(b" Your encrypted ticket is:").decode().split()
ticket_enc = ticket_enc[4]
packet = bytes.fromhex(ticket_enc)
nonce = packet[:8]
checksum = packet[8:28].hex()
ciphertext = packet[28:]
keystream = xor(ciphertext, data_)
data2_ = {}
team_name2 = 'a' * 30
team_id2 = hex(crc32(team_name2.encode()))[2:]
data2_['ticket'] = (team_name2+'-'+team_id2).encode()
msg = p.recvuntil(b"[Q]uit")
p.sendlineafter(b">>", b"G")
p.sendlineafter(b"Your team name:", team_name2.encode())
ticket_enc2 = p.recvline_startswith(b" Your encrypted ticket is:").decode().split()
ticket_enc2 = ticket_enc2[4]
packet2 = bytes.fromhex(ticket_enc2)
nonce2 = packet2[:8]
checksum2 = packet2[8:28].hex()
ciphertext2 = packet2[28:]
sha = hlextend.sha1()
print(data2_["ticket"])
data3_ = {}
data3_['ticket'] = b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-6bc1d3c1\x80\x00\x00\x00\x00\x00\x00\x01\xb8-invited'
print(data3_)
print(type(data3_["ticket"]))
print(len(data3_['ticket']))
print(sha.extend(b'-invited', data2_['ticket'], 16, checksum2))
data3_ = b"{'ticket': b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-6bc1d3c1" + b"\x80\x00\x00\x00\x00\x00\x00\x01\xb8" + b"-invited'}"
checksum3 = sha.hexdigest()
print(len(data3_))
print(len(keystream))
ciphertext3 = xor(data3_, keystream)
nonce = nonce
ticket3 = (nonce + bytes.fromhex(checksum3) + ciphertext3).hex()
msg = p.recvuntil(b"[Q]uit")
p.sendlineafter(b">>", b"I")
p.sendlineafter(b"Your encrypted ticket (hex):", ticket3.encode())
p.interactive()
Okey, kalau kita baca script nya ada sedikit keunikan bahwa ya kita tahu di chall.py data itu bentuknya adalah
1
b"{'ticket': b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-6bc1d3c1'}"
yang kalau di chall.py kita cukup melakukan data = str(data).encode(). namun data yang coba kita lakukan hal yang sama mengandung bytes yang tidak printable seperti \x00 dan \x01 yang mana kalau kita lakukan hal yang sama seperti di chall.py maka python kan membaca bukan sebagai \x00 1 bytes melainkan menjadi 4 bytes yakni \, x, 0 dan 0 sehingga alternatifnya kita bisa lakukan seperti yang dilakukan diatas dengan memecahnya menjadi
1
data3_ = b"{'ticket': b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-6bc1d3c1" + b"\x80\x00\x00\x00\x00\x00\x00\x01\xb8" + b"-invited'}"
Flag
ARA6{welcome_to_Radhit's_party!!waittt,you're_invited???}
Conclusion
Good game and Nice Try



