This year, I had the opportunity to create all cryptography challenges for Cyber Jawara 2021 competition. Here’s a summary of the challenges I created:
- 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
This writeup only contains the solution for sha256plus. You can find writeups for all the cryptography challenges from the qual round here (by team Ainge CTF) and the final round editorial here.
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
- Find two different plaintexts that produce the same hash value (hash collision)
- Each plaintext is limited by regex (must be word chars and no more than 10 chars)
- Plaintext is converted to hex, then each hex character is hashed using SHA256 and multiplied by a constant from $ 2021^i $ modulo $ 2069 $
- The above calculation is summed and taken modulo $ 2^{256} $
- Only a random 10-byte chunk of the flag is hashed, so we need to connect to the service multiple times
Part 1: Find hash collision
The plaintext length is limited to 10 characters. When the plaintext is 10 characters long, the multiplier constant is $ 2021^i $ modulo $ 2069 $ for $ 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]
To form a hash collision, we need to find an element in the list above that is the sum of two or more other elements. Here’s the code.
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
There are 3 solutions in the above case. We’ll use the first solution to form our plaintexts, where 1658 is used for the first plaintext and 601 + 543 + 279 + 235 for the second plaintext.
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
Let’s denote the SHA256 hash function as $ H $ and the target hash as $ T $. The idea is to create an 18x18 matrix containing the following elements:
\[\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}\]We hope that LLL will return the weight values for each element in the subset sum solution. However, we need to reduce redundant elements because the weight values in this case are not just 0 or 1 like in the usual or common knapsack problem. We’ll reduce one redundant element from the matrix, so the matrix we’ll form will be 17x17.
Proving redundant element exists
Remember, the flag consists of 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]
The max
value is 14, not 16. We can conclude that in most cases, there are at least two redundant elements in the 18x18 matrix.
Finding complete subset
After successfully getting the weights for each element, we also need to arrange the plaintext that matches the set of weights. In this case, I calculated the product against several subsets, then made sure no element was used more than once in the 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}
The flag was inspired by Dragon CTF 2021: CRC Recursive Challenge