Tahun ini, saya berkesempatan untuk membuat seluruh soal pada kategori Cryptography di kompetisi Cyber Jawara 2021. Berikut rangkuman soal yang telah saya buat:
- Qual
- shuvvler (300 points, 32 solves): Classic substitute and shift cipher
- RSASS (937 points, 9 solves): Common RSA modulus attack
- babynext (962 points, 7 solves): Dependent RSA primes (predictable $ p + q $)
- burve (981 points, 6 solves): Improper ECC private key generation
- gluckstag (1000 points, 1 solve): Improper AES-CFB implementation
- Final
- halftwin (699 points, 2 solves): Dependent RSA primes (share 50% LSB)
- anschlag (700 points, 1 solve): Zerologon Vulnerability in AES-CFB8
- sha256plus (700 points, 0 solves): Hash collision in custom hash function
Kali ini saya hanya akan menuliskan write-up sha256plus. Write-up seluruh soal Cryptography pada babak penyisihan dapat dilihat di sini (write-up tim Ainge CTF) dan editorial babak final dapat dilihat di sini.
sha256plus (700 points, 0 solves)
Termotivasi dari Bitcoin, Bob mencoba untuk mengembangkan hash function SHA256plus berbasis SHA256 yang nantinya digunakan untuk mekanisme PoW cryptocurrency miliknya. Karena Bob hanya mengenal dunia cryptocurrency dan tidak mempelajari basic cryptography, Bob butuh bantuanmu untuk melakukan pengecekan apakah hash function yang telah ia kembangkan sudah aman atau belum.
nc 178.128.96.125 33303
Author: deomkicer#3362
#!/usr/bin/env python3
import hashlib
import random
import re
import sys
class Unbuffered(object):
def __init__(self, stream):
self.stream = stream
def write(self, data):
self.stream.write(data)
self.stream.flush()
def writelines(self, datas):
self.stream.writelines(datas)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
sys.stdout = Unbuffered(sys.stdout)
FLAG = re.findall(br'^CJ2021{(\w{40})}$', open('flag.txt', 'rb').read())[0]
def user_input(s):
inp = input(s).strip()
assert len(inp) < 256
return inp
def sha256plus(s):
res = 0
mod = 2**256
for i, c in enumerate(s.hex()):
res += pow(2021, i, 2069) * int.from_bytes(hashlib.sha256(c.encode()).digest(), 'big')
res %= mod
return int.to_bytes(res, 32, 'big')
def main():
print('Is there any collision on my hash function?')
m1 = user_input('Message #1: ').encode(); assert re.fullmatch(br'[\w]{0,10}', m1), '#$!@'
m2 = user_input('Message #2: ').encode(); assert re.fullmatch(br'[\w]{0,10}', m2), '#$!@'
if m1 != m2 and sha256plus(m1) == sha256plus(m2):
r = random.randrange(0, len(FLAG)-5, 5)
print(f'Oh no, you found the collision!')
print(f'Here take my flag(s): {sha256plus(FLAG[r:r+10]).hex()}')
else:
print('Ok, I guess it is secure enough.')
if __name__ == '__main__':
main()
TL;DR
- Mencari dua plainteks yang berbeda namun memiliki hash value yang sama (hash collision)
- Tiap plainteks dibatasi oleh regex (plainteks harus berupa word chars dan tidak lebih dari 10 chars)
- Plainteks diubah ke bentuk hex, lalu tiap karakter hex tersebut di-hash menggunakan SHA256 dan dikalikan dengan konstanta dari $ 2021^i $ modulo $ 2069 $
- Perhitungan di atas dijumlahkan dan di-modulo $ 2^{256} $
- Hanya potongan 10 random bytes flag yang di-hash, sehingga perlu melakukan beberapa kali koneksi ke servis yang diberikan
Part 1: Find hash collision
Panjang plainteks dibatasi maksimal 10 karakter. Ketika plainteks memiliki panjang 10 karakter, maka konstanta pengalinya adalah $ 2021^i $ modulo $ 2069 $ untuk $ i={0,…,19} $.
>>> [pow(2021, i, 2069) for i in range(20)]
[1, 2021, 235, 1134, 1431, 1658, 1107, 658, 1520, 1524, 1332, 203, 601, 118, 543, 833, 1396, 1269, 1158, 279]
Untuk membentuk hash collision, kita perlu mencari suatu elemen pada list di atas yang merupakan penjumlahan dari dua atau lebih elemen lainnya. Berikut kodenya.
def cari_subsets(s, a):
result = []
def cari(target, arr, backpack=[]):
if arr: # arr tidak kosong
if arr[-1] == target:
tmp = backpack + [arr[-1]]
result.append(tmp)
elif arr[-1] < target:
cari(target - arr[-1], arr[:-1], backpack + [arr[-1]])
cari(target, arr[:-1], backpack)
cari(s, sorted(a))
return result
potongan = 10
a = [pow(2021, i, 2069) for i in range(potongan*2)]
for s in a:
print(s, cari_subsets(s, a))
1 [[1]]
2021 [[2021]]
235 [[235]]
1134 [[1134]]
1431 [[1431]]
1658 [[1658], [601, 543, 279, 235]]
1107 [[1107]]
658 [[658]]
1520 [[1520]]
1524 [[1524]]
1332 [[1332]]
203 [[203]]
601 [[601], [279, 203, 118, 1]]
118 [[118]]
543 [[543]]
833 [[833]]
1396 [[1396]]
1269 [[1269]]
1158 [[1158], [601, 235, 203, 118, 1]]
279 [[279]]
- 1658 = 601 + 543 + 279 + 235
- 601 = 279 + 203 + 118 + 1
- 1158 = 601 + 235 + 203 + 118 + 1
Terdapat 3 solusi pada kasus di atas. Kita akan menggunakan solusi pertama sebagai pembentukan plainteks, dimana angka 1658 untuk membentuk plainteks pertama dan angka 601 + 543 + 279 + 235 untuk plainteks kedua.
import re
potongan = 10
a = [pow(2021, i, 2069) for i in range(potongan*2)]
charset = '0123456789abcdef'
b1 = ''.join(['1' if i == 1658 else '0' for i in a])
b2 = ''.join(['1' if i in [601, 543, 279, 235] else '0' for i in a])
for i in charset:
for j in charset:
t1 = b1.replace('0', i).replace('1', j)
t2 = b2.replace('0', i).replace('1', j)
m1 = bytes.fromhex(t1)
m2 = bytes.fromhex(t2)
if re.fullmatch(br'[\w]{0,10}', m1) and re.fullmatch(br'[\w]{0,10}', m2) and m1 != m2:
print(m1.decode())
print(m2.decode())
exit()
3343333333
3C3333CC34
$ nc 178.128.96.165 33303
Is there any collision on my hash function?
Message #1: 3343333333
Message #2: 3C3333CC34
Oh no, you found the collision!
Here take my flag(s): 2aaaf3169ed22072b7a32e2470c800142893bda1e2ed7a1d88f920d515b35ed6
Part 2: Reverse target hash
Notasikan fungsi hash SHA256 sebagai $ H $ dan target hash sebagai $ T $. Idenya, kita akan membuat matrix berukuran 18x18 yang berisikan elemen-elemen berikut:
\[\begin{bmatrix} 1 & 0 & 0 & \cdots & 0 & H(\textbf{0}) \\ 0 & 1 & 0 & & 0 & H(\textbf{1}) \\ 0 & 0 & 1 & & 0 & H(\textbf{2}) \\ \vdots & & & \ddots & & \\ 0 & 0 & 0 & 0 & 1 & -2^{256} \\ 0 & 0 & 0 & 0 & 0 & -T \end{bmatrix}\]Dengan harapan, LLL mengembalikan nilai weight tiap masing-masing elemen solusi dari subset sum. Namun kita perlu mengurangi elemen yang redundant karena nilai weight pada kasus ini tidak hanya bernilai 0 atau 1 seperti pada knapsack problem. Kita akan mengurangi satu elemen redundant pada matrix, sehingga matrix yang akan kita bentuk adalah matrix berukuran 17x17.
Proving redundant element exists
Ingat, flag merupakan word chars.
import random
charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz'
test = random.choices(charset, k=1000)
test = ''.join(test).encode()
a = [test[i:i+10] for i in range(0, len(test), 10)]
b = [len(set(x.hex())) for x in a]
print(b)
print(f'min: {min(b)}')
print(f'max: {max(b)}')
potongan = 10
print([pow(2021, i, 2069) for i in range(potongan*2)])
[9, 9, 10, 7, 10, 9, 9, 8, 11, 11, 9, 9, 9, 13, 9, 11, 9, 11, 9, 9, 8, 10, 9, 8, 7, 10, 8, 9, 10, 8, 11, 11, 7, 9, 9, 10, 6, 9, 9, 10, 9, 10, 9, 9, 8, 11, 10, 9, 10, 10, 10, 10, 9, 8, 11, 9, 10, 10, 8, 11, 9, 10, 10, 9, 11, 8, 10, 9, 7, 8, 10, 10, 10, 8, 8, 12, 9, 8, 9, 14, 8, 9, 9, 10, 11, 10, 11, 9, 9, 7, 11, 9, 9, 9, 11, 10, 11, 11, 8, 9]
min: 6
max: 14
[1, 2021, 235, 1134, 1431, 1658, 1107, 658, 1520, 1524, 1332, 203, 601, 118, 543, 833, 1396, 1269, 1158, 279]
Nilai max
adalah 14, bukan 16. Dapat disimpulkan, dalam sebagai besar kasus, setidaknya terdapat dua elemen yang redundant pada matrix berukuran 18x18.
Finding complete subset
Setelah berhasil mendapatkan weight dari tiap elemen, kita juga perlu menyusun plainteks yang cocok dari himpunan weight tersebut. Pada kasus ini, saya menghitung product terhadap beberapa subset, lalu memastikan tidak ada suatu elemen yang digunakan lebih dari satu kali pada complete subset.
Implementation
#!/usr/bin/sage
from Crypto.Hash import SHA256
from functools import reduce
from itertools import combinations, product
import os, time
t0 = time.time()
sha256 = lambda s: SHA256.new(s.encode()).digest()
charsets = list(combinations('0123456789abcdef', 15))
dicc = {}
potongan = 10
message_1 = '3343333333'
message_2 = '3C3333CC34'
cmd = f'printf "{message_1}\n{message_2}\n" | ./server.py | tail -n 1 | cut -d " " -f 5'
# cmd = cmd.replace('./server.py', 'nc localhost 33303')
cmd = cmd.replace('./server.py', 'nc 178.128.96.165 33303')
################################################################
while len(dicc.keys()) != 7:
hashnya = os.popen(cmd).read().strip()
if hashnya in dicc.keys(): continue
s = Integer(hashnya, 16)
found = False
for charset in charsets:
a = [int.from_bytes(sha256(i), 'big') for i in charset] + [-1 * 2^256]
n = len(a)
################################################################
b = []
for i in range(n):
vec = [0 for _ in range(n + 1)]
vec[i] = 1
vec[-1] = a[i]
b.append(vec)
b.append([0 for _ in range(n)] + [-s])
B = matrix(ZZ, b)
################################################################
res = B.LLL()
for e in res:
e = list(e)
if e[-1] == 0 and e.count(0) >= 2 and all(x >= 0 for x in e):
dicc[hashnya] = [''.join(charset), e]
print(f'{len(dicc.keys())}: {hashnya[:8]}...{hashnya[-8:]} -> {dicc[hashnya]}')
found = True
break
if found: break
################################################################
def cari_depan(flags):
depan = []
for kiri in flags:
if all(kiri[:5] != kanan[-5:] for kanan in flags):
depan.append(kiri)
if len(depan) == 1:
return depan[0]
else:
return False
def susun_flag(flags, target=40):
kiri = cari_depan(flags)
flag = kiri
while len(flag) != target:
for kanan in flags:
if kiri[-5:] == kanan[:5]:
flag += kanan[5:]
kiri = kanan
break
return flag
def cari_subsets(s, a):
result = []
def cari(target, arr, backpack=[]):
if arr: # arr tidak kosong
if arr[-1] == target:
tmp = backpack + [arr[-1]]
result.append(tmp)
elif arr[-1] < target:
cari(target - arr[-1], arr[:-1], backpack + [arr[-1]])
cari(target, arr[:-1], backpack)
cari(s, sorted(a))
return result
def cari_complete_subset(kamus):
result = []
box = []
for key in kamus.keys():
box.append(kamus[key])
produc = list(product(*box))
for produ in produc:
t1 = reduce(lambda a, b: a + b, produ)
if len(set(t1)) == len(t1):
result.append(produ)
return result
################################################################
box = []
for hashnya in dicc.keys():
charset, arr = dicc[hashnya]
arr = arr[:-2] # 2 paling belakang itu weight dari 2^256 dan hash valuenya
naon = [Integer(pow(2021, i, 2069)) for i in range(potongan*2)]
ssss = {i: cari_subsets(i, naon) for i in arr if i}
cccc = cari_complete_subset(ssss)
box.append([])
for ccc in cccc:
res = ['_' for _ in range(potongan*2)]
for cc in ccc:
total = sum(cc)
char = charset[arr.index(total)]
for c in cc:
idx = naon.index(c)
res[idx] = char
res = ''.join(res)
res = bytes.fromhex(res)
box[-1].append(res)
print([len(bo) for bo in box])
probox = list(product(*box))
for v in probox:
tmp = cari_depan(v)
if tmp:
flags = [i.decode() for i in v]
flag = 'CJ2021{' + susun_flag(flags) + '}'
print(f'flag = {flag}')
print(f'time = {time.time()-t0} seconds')
$ sage solve.sage
1: 6306d8b2...331c887e -> ['0123456789abcdf', [0, 0, 0, 1134, 6380, 6733, 1158, 1, 279, 0, 0, 0, 1845, 0, 1491, 10158, 0]]
2: a594946b...69744c53 -> ['0123456789abdef', [0, 0, 0, 1134, 0, 3754, 6534, 1672, 0, 0, 0, 0, 658, 118, 5151, 11404, 0]]
3: aa4dbd37...0cd45768 -> ['0123456789abcdf', [0, 279, 0, 118, 894, 4951, 5406, 0, 1524, 203, 0, 0, 3155, 0, 2491, 11158, 0]]
4: 2aaaf316...15b35ed6 -> ['0123456789abcdf', [0, 0, 1658, 1883, 1525, 1846, 4528, 2121, 0, 2021, 0, 0, 0, 1269, 2170, 9865, 0]]
5: 50588764...4775b367 -> ['0123456789abcdf', [0, 2792, 0, 1784, 1991, 3850, 1521, 1332, 0, 2021, 1524, 0, 279, 0, 1927, 9642, 0]]
6: 9650b408...11db443d -> ['0123456789abcdf', [0, 2357, 279, 3674, 118, 2324, 5321, 0, 1269, 2021, 0, 0, 0, 0, 1658, 10029, 0]]
7: c2f04f2c...4730b007 -> ['0123456789abcef', [0, 951, 0, 1144, 0, 4161, 4157, 3044, 0, 203, 279, 0, 0, 1134, 3948, 10936, 0]]
[1, 1, 1, 1, 1, 1, 1]
flag = CJ2021{I_b3t_someone_wi11_juST_LLL_ThiS_eaf418b}
time = 4.9704320430755615 seconds
Flag
CJ2021{I_b3t_someone_wi11_juST_LLL_ThiS_eaf418b}
Flagnya terinspirasi dari Dragon CTF 2021: CRC Recursive Challenge