Post

ARA CTF Qual 2025 All Crypto Writeup

Pengen misuh, karena kekonyolan diri sendiri tidak lolos final, sad.

ARA CTF Qual 2025 All Crypto Writeup

Jujur aja, chall tahun ini lebih gampang, cuma ya konyol aja gw kenapa ga lolos, lawak-lawak

IDK

Desktop View

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)

Desktop View

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.

\[f(x) = 24x + 50\]

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 : Desktop View

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

Desktop View

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 :

  1. Salsa20 adalah stream cipher
  2. Kita tahu nonce yang digunakan dan plaintext yang kita kirim
  3. 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 -invited supaya 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:

  1. Lakukan Get Ticket dengan panjang dari team_name nya 30, ambil checksumnya
  2. 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.
  3. Lakukan Hash Length Extension Attack berdasarkan checksum dari langkah 1, dan tambahkan bytes invited di akhir.
  4. Kita xor antara keystream dengan plaintext yang kita inginkan untuk mendapatkan modified ciphertextnya
  5. 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

This post is licensed under CC BY 4.0 by the author.