Diberikan source code 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 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 |
#!/usr/local/bin/python from Crypto.Cipher import AES from Crypto.Util.number import long_to_bytes from random import randint from binascii import hexlify with open('flag.txt','r') as f: flag = f.read().strip() with open('keyfile','rb') as f: key = f.read() assert len(key)==32 ''' Pseudorandom number generators are weak! True randomness comes from phyisical objects, like dice! ''' class TrueRNG: @staticmethod def die(): return randint(1, 6) @staticmethod def yahtzee(N): dice = [TrueRNG.die() for n in range(N)] return sum(dice) def __init__(self, num_dice): self.rolls = num_dice def next(self): return TrueRNG.yahtzee(self.rolls) def encrypt(message, key, true_rng): nonce = true_rng.next() cipher = AES.new(key, AES.MODE_CTR, nonce = long_to_bytes(nonce)) return cipher.encrypt(message) ''' Stick the flag in a random quote! ''' def random_message(): NUM_QUOTES = 25 quote_idx = randint(0,NUM_QUOTES-1) with open('quotes.txt','r') as f: for idx, line in enumerate(f): if idx == quote_idx: quote = line.strip().split() break quote.insert(randint(0, len(quote)), flag) return ' '.join(quote) banner = ''' ============================================================================ = Welcome to the yahtzee message encryption service. = = We use top-of-the-line TRUE random number generators... dice in a cup! = ============================================================================ Would you like some samples? ''' prompt = "Would you like some more samples, or are you ready to 'quit'?\n" if __name__ == '__main__': NUM_DICE = 2 true_rng = TrueRNG(NUM_DICE) inp = input(banner) while 'quit' not in inp.lower(): message = random_message().encode() encrypted = encrypt(message, key, true_rng) print('Ciphertext:', hexlify(encrypted).decode()) inp = input(prompt |
Jadi code diatas akan membaca quotes.txt dan ambil sebuah random quote. Di dalam quote tersebut terdapat flag, akan tetapi flag diletakkan diantara sebuah quote secara random. Jadi misalnya ada quote “ini quote 100”, maka flag bisa berada di “flag ini quote 100” atau “ini flag quote 100” atau “ini quote flag 100”, atau “ini quote 100 flag”. Disitu nonce dari AES CTR diambil dari penjumlahan 2 buah angka random dari 1-6 (mirip dengan pelemparan 2 buah dadu), sehingga kemungkinan noncenya adalah : [2,3,4,5,6,7,8,9,10,11,12]

Dari ilustrasi dan source code diatas, misalnya kita coba ambil 300 sample ciphertext. Ada kemungkinan dari 300 ciphertext tersebut, kita bisa mendapatkan beberapa ciphertext yang dienkrip dengan key dan nonce yang sama serta string flag berada di paling depan. Karena ada kemungkinan reusing key dan nonce, maka AES CTR menjadi vulnerable. Berikut adalah ilustrasi dari proses enkripsi dengan AES CTR :

Dari flow diatas, misalnya ada 2 plaintext yang dienkrip dengan key dan nonce yang sama. Berarti :
C1 = EncryptedKey ^ P1
C2 = EncryptedKey ^ P2
C1 ^ C2 = EncryptedKey ^ P1 ^ EncryptedKey ^ P2
C1 ^ C2 = P1 ^ P2
C1 ^ C2 ^ P1 = P2
Karena kita tahu format awal dari flag adalah “flag{“ maka kita bisa coba dapetin plaintext lainnya. Pertama, kita kumpulin 300 sample dari ciphertext dengan source code sebagai berikut :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
from pwn import * import subprocess con = remote('mc.ax', 31076) proof_of_work = con.recvline().decode().strip().split(': ')[1] res = subprocess.check_output(proof_of_work, shell=True) con.sendline(res) con.recvuntil("samples?\n") con.sendline("y") list_of_ciphers = [] #kumpulin 300 sample for i in range(300): cipher = con.recvline().decode().strip().split(': ')[1] list_of_ciphers.append(cipher) con.recvline() con.sendline("y") print(list_of_ciphers) |
Selanjutnya, dari persamaan C1 ^ C2 ^ P1 = P2, kita ambil setiap ciphertext sebanyak 5 karakter (karena known plaintext “flag{“ terdiri dari 5 karakter) dan di xorkan dengan ciphertext lainnya sebanyak 5 karakter juga. Codenya seperti berikut :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
from binascii import hexlify from Crypto.Util.number import long_to_bytes as l2b known = hexlify(b"flag{") #10 list_of_ciphers = ['9c2880708f5e6abc0ebda6c82da626dd5908818428785598de4a97326bca20db4ec61a5968a6b6edbb2479bb5d02770eb3c17253753bad1455abff0aa6c4b6ab3a5a9493d5b717ac998839119926d279debf12d3be5e173541d972', '911450a898c861fc73fc7f9436a5980e9b4d0ca1e24aa4e1b9de34defcfbfe55764db9121e773e4cdcaf6302cc18c2d4d4673317f0b667f004248fe6dba46709bb34373ee016d5b03b9169676f4c5c837c8354901c768b5c9e86f93ae7aad792123c373c92a0960b0e', ...] for i in range(0,len(list_of_ciphers)): for j in range(i+1,len(list_of_ciphers)): try : p = l2b(int(list_of_ciphers[i][:10],16) ^ int(list_of_ciphers[j][:10],16) ^ int(known,16)).decode() if p.isalnum : print(f"Index {i} {j} => {p}") except : pass |
Ciphertext pada index 68 dixor dengan ciphertext pada index 88 dixor dengan “flag{” menghasilkan Every. Ciphertext pada index 68 dixor dengan ciphertext pada index 133 dixor dengan “flag{” juga menghasilkan Every. Karena plaintext dalam bahasa inggris, saya tebak katanya adalah Everything.

Sekarang kita memperoleh known plaintext baru, yaitu Everything. Ganti knownnya menjadi Everything, dan sekarang ambil ciphertextnya 10 karakter, karena jumlah karakter pada string “Everything” ada 10, yang berarti dalam hex adalah 20 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
from binascii import hexlify from Crypto.Util.number import long_to_bytes as l2b known = hexlify(b"Everything") #20 list_of_ciphers = ['9c2880708f5e6abc0ebda6c82da626dd5908818428785598de4a97326bca20db4ec61a5968a6b6edbb2479bb5d02770eb3c17253753bad1455abff0aa6c4b6ab3a5a9493d5b717ac998839119926d279debf12d3be5e173541d972', '911450a898c861fc73fc7f9436a5980e9b4d0ca1e24aa4e1b9de34defcfbfe55764db9121e773e4cdcaf6302cc18c2d4d4673317f0b667f004248fe6dba46709bb34373ee016d5b03b9169676f4c5c837c8354901c768b5c9e86f93ae7aad792123c373c92a0960b0e', ...] for i in range(0,len(list_of_ciphers)): for j in range(i+1,len(list_of_ciphers)): try : p = l2b(int(list_of_ciphers[i][:20],16) ^ int(list_of_ciphers[j][:20],16) ^ int(known,16)).decode() if p.isalnum : print(f"Index {i} {j} => {p}") except : pass |
Jalanin script diatas, kita dapat ciphertext pada index 208 dixor dengan ciphertext pada index 213 dixor dengan “Everything” menghasilkan A truly ri.

Searching di google dan disitu ada autocomplete berupa quote yang dimulai dengan a truly rich man. Terus coba liat google image, disitu kita dapat quote “A truly rich man is one whose children …”.

Saya coba gunakan string diatas dan coba cari string yang terdapat substring “flag{“. Sekarang kita memperoleh known plaintext baru, yaitu A truly rich man is one whose children. Ganti knownnya menjadi A truly rich man is one whose children, dan sekarang ambil ciphertextnya 38 karakter, karena jumlah karakter pada string “A truly rich man is one whose children” ada 38, yang berarti dalam hex adalah 76 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
from binascii import hexlify from Crypto.Util.number import long_to_bytes as l2b known = hexlify(b"A truly rich man is one whose children") #76 list_of_ciphers = ['9c2880708f5e6abc0ebda6c82da626dd5908818428785598de4a97326bca20db4ec61a5968a6b6edbb2479bb5d02770eb3c17253753bad1455abff0aa6c4b6ab3a5a9493d5b717ac998839119926d279debf12d3be5e173541d972', '911450a898c861fc73fc7f9436a5980e9b4d0ca1e24aa4e1b9de34defcfbfe55764db9121e773e4cdcaf6302cc18c2d4d4673317f0b667f004248fe6dba46709bb34373ee016d5b03b9169676f4c5c837c8354901c768b5c9e86f93ae7aad792123c373c92a0960b0e', ...] for i in range(0,len(list_of_ciphers)): for j in range(i+1,len(list_of_ciphers)): try : p = l2b(int(list_of_ciphers[i][:76],16) ^ int(list_of_ciphers[j][:76],16) ^ int(known,16)).decode() if p.isalnum and "flag{" in p: print(f"Index {i} {j} => {p}") except : pass |
Jalanin script diatas dan berhasil dapetin flag:

Flag : flag{0h_W41t_ther3s_nO_3ntr0py}