Đây là bài mình lấy ý tưởng từ 1 challenge reversing trong 1 CTF mà mình từng chơi. Đại loại là bài đó cho các output của hàm hash FNV, và bắt mình phải guess ra input bằng cách thử hash các từ trong tiếng Anh cho đến khi nào nó match với cái output của nó thì thôi...
Tuy nhiên thì, lúc đó mình méo nghĩ đến chuyện dùng từ điển với cả cái challenge đó cũng méo có hint gì cả về việc dùng cái đó cả :/
Với một đầu óc non nớt tràn ngập đam mê với Crypto, mình đã quyết định làm điều tưởng như không tưởng, đó là viết ra 1 thuật toán dịch ngược cmnl cái hàm đó 😗
Tuy thuật toán đó chỉ giải ra được đúng 2 từ, nhưng may mà nó cũng đủ để hint cho mình là phải dùng cái từ điển, và quan trọng là, từ đó, một ý tưởng cho 1 challenge Crypto thú dzị đã được ra đời ~.~
Lí do mình khá thích challenge này là bởi vì:
FNV encryptionMở đầuMục lụcFilesmain.py
output.txt
Phân tíchNguyên lí hoạt độngVì sao nó khó hơn bạn nghĩCách giải- Hint 1: Thay đổi góc nhìn -&
-> %
^
-> +
Viết lại dãy sốNghỉ giải lao 1 chút...- Hint 2: Giải hệ bất phương trình -LatticeCVPCVP của rkm0959Quay trở về với bài toánCode giảisolve.sage
Một chút lưu ý nhỏFlagChallenge phụ: blocksize=32 thì sao nhỉ...main32.py
output32.txt
main.py
xxxxxxxxxx
from Crypto.Util.number import long_to_bytes
##### encryption #####
A = 0x0000000000000000000001000000000000000000000000000000000000000163
S = 0xdd268dbcaac550362d98c384c4e576ccc8b1536847b6bbb31023b4c8caee0535
n = 256
def encrypt_block(block: bytes):
enc_block = S
for byte in block:
enc_block = ((A*enc_block) ^ byte) & ((1<<n) - 1)
return long_to_bytes(enc_block, n//8)
BLOCKSIZE = n//9
def encrypt(data: bytes):
enc = b''
for i in range(0, len(data), BLOCKSIZE):
enc += encrypt_block(data[i:i+BLOCKSIZE])
return enc
##### challenge #####
print(encrypt(open("flag.txt", "rb").read()).hex())
output.txt
xxxxxxxxxx
72a842f55fe95c7d0a494bf3da149c5e86fa86fc2c83bd1c0bd8e0c5f04d2ab9f75efedb902fb62d97919606cf34b8c743721957f02b58dcdefa83b47e6a3785
Challenge encrypt data bằng cách chia flag
cần tìm thành các blocks có độ dài là 256//9 = 28
bytes. Mỗi block sẽ được đưa vào hàm encrypt_block
mà thực chất là một hàm hash tên là FNV-1 và cho ra một cipherblock. Ghép tất cả các cipherblocks lại với nhau và chúng ta có output.txt :3
Mỗi cipherblock được hash riêng biệt và không liên quan gì tới cipherblock ở phía trước hay phía sau gì cả, vì vậy hướng đi của bài này sẽ là tìm cách giải từng cipherblock một bằng cách tìm hàm reverse của hàm encrypt_block
.
Cách hàm hash này hoạt động khá là đơn giản, đó là khởi tạo state (trong code là enc_block
) bằng 1 giá trị S đã biết trước. Và với mỗi byte trong block
, ta nhân cái state đó với một số A đã biết trước, XOR với cái byte đó và lấy n
bit cuối bằng cách AND
với
Nghe thì có vẻ đơn giản, nhưng đây lại là một trong những ví dụ điển hình của 1 loại đặc sản trong Crypto, đó chính là trap-door function, đi từ input ra output thì dễ nhưng ngược lại thì khó vkl (==')
Gọi các state (giá trị của enc_block
) qua mỗi vòng lặp trong encrypt_block
là block
là
Bài toán yêu cầu cho biết
Một trong những lí do hàm này khó có thể quay output ngược trở lại input, là bởi vì mình không biết được các state ở giữa như thế nào ngoài trừ
Và cũng có nghĩa là chẳng có 1 cách nào đoán đúng ra được state ở giữa mà không thử hết tất cả các bytes ở tất cả các vị trí (hoặc có mà mình méo biết...), trong trường hợp này là tầm 256^28=2^224
trường hợp 😗 Cũng nhanh thôi, thử tầm vài triệu năm là xong ý mà :3
Lí do thứ hai nó khó vkl ra là bởi vì mỗi vòng lặp lại là một sự trộn lẫn giữa phép toán nhân dùng giữa các số nguyên, và phép toán XOR, vốn được dùng để làm phép cộng 2 đa thức trong GF(2), như ta đã thấy trong bài Mixed Signal. 2 cái không liên quan với nhau giờ lại đi ôm nhau, không thành thảm họa mới là lạ... Đã thế lại còn có AND nữa, má nó chứ... ://
Để giải được bài này, mình sẽ không giải
Bài này mình có cho 2 hints.
Hint thứ nhất là dùng +
và %
thay vì ^
và &
. Điều này ám chỉ là mình phải bằng cách nào đó viết lại công thức của hàm encrypt_block
dưới dạng +
và %
.
Còn hint thứ 2 là để dùng 1 tool mà mình chưa đề cập đến vội ~.~
Mục đích đưa về +
và %
là bởi vì sau đó chúng ta có thể đưa cái dãy số ở bên trên về thành 1 phương trình ngắn gọn. Còn sau đó ta sẽ làm gì với nó, thì sẽ được đề cập sau :3
&
-> %
Đầu tiên mình sẽ bàn đến cách thay từ &
thành %
.
May sao bài này mình lại AND
với số dạng
Khi ta AND
1 số với 111111...11111
, nó sẽ tương đương với việc mình mod
số đó với
Để hiểu được tại sao điều này lại đúng, chúng ta có thể so sánh việc này với việc lấy n
chữ số cuối của 1 số dưới dạng thập phân.
Nếu ta chỉ cần lấy hàng đơn vị, chỉ cần chia cho 10
và lấy phần dư là xong. Điều này cũng giống với việc mod 10
nhỉ.
Nếu muốn lấy thêm chữ số hàng chục, ta có thể chia cho 100
, tức là 10^2
và lấy phần dư.
Theo quy luật đó, để lấy n
chữ số thập phân cuối cùng, ta sẽ mod
số đó với
Khi ta AND
1 số với 1 số toàn 111111...11111
, tức là ta đang lấy n
chữ số nhị phân cuối cùng của nó, nhị phân là base 2, nên sẽ tương đương với mod
^
-> +
Khi chúng ta XOR một giá trị
Gọi số_bit(X) - 8 bit đầu trong
Nếu xét
Nói cách khác, XOR với 1 giá trị bất kì từ 0 đến 255 cũng đồng nghĩa với việc cộng thêm với 1 giá trị nào đó trong khoảng từ -255 đến 255.
Lưu ý là giá trị được XOR và giá trị được cộng thêm tương đương có thể là 2 giá trị hoàn toàn khác nhau, không liên quan gì hết đến nhau cả.
Điều thứ 2 mình cần để ý, đó là vì mình có
Từ suy luận ở bên trên, ta sẽ viết lại dãy số như sau:
Tuy rằng
Và từ đó ta sẽ quy về được một phương trình mod 2^256
gồm 28 ẩn
Đưa
Vì
Việc có thể đưa phuơng trình về dạng tích và tổng như thế này sẽ rất giúp ích cho chúng ta khi sử dụng tool mà mình sắp tới đề cập đến.
Mình sẽ để hình con mèo mà mình kiếm trên Google để ở đây để các bạn dừng chân một chút nhé :3 Chắc đọc đến đây các bạn cũng đã hơi mệt rùii, mà chặng đường thì vẫn còn dài ~.~
Đương nhiên thì, nếu có 29 ẩn mà lại chỉ có 1 phương trình, hẳn nhiên sẽ có vô số trường hợp sẽ cùng thỏa mãn phương trình đó rồi. Tuy nhiên, các ẩn này lại có một giới hạn, đó là chúng
Nếu mình coi như đây là 28 bất phương trình, kèm thêm 1 phương trình ở bên trên nữa ta sẽ có 1 hệ 29 bất phương trình 29 ẩn :3
rkm0959 (ông này siêu đỉnh Crypto luôn :33) có một tool tên là "Inequality Solving with CVP", tức là giải hệ phương trình bằng phương pháp CVP, chính là dùng để giải cái hệ trên này đó ~.~
Để hiểu đại ý được về cách thức và mục tiêu của thuật toán này, và cách nó kết nối với hệ bất phương trình của chúng ta, mình xin phép tản mạn về 1 lí thuyết có liên quan, đó chính là lattice và CVP.
Nếu mình có n
vector n
vector đó, có dạng:
với
Thuật toán CVP là một thuật toán tìm kiếm vector trong lattice được tạo bởi
Nói cách khác, việc nó làm là cố gắng đi tìm 1 vector có dạng
Vector
Còn cái ông rkm0959 mà mình bảo lúc nãy ý, đã custom cái thuật toán đó và tìm ra được một cách để tìm được một vector trong cái lattice nằm giữa 2 vectors mình chọn là
Định nghĩa "nằm giữa" ở đây là giả sử mình dùng thuật toán trên và tìm được 1 vector
Theo như hình minh họa thì tức là thuật toán của ông ý sẽ đi tìm ra được vector trong khoảng này:
À, thuật toán sẽ không chạy được nếu có bất kì
"Hmm... Nhưng mà mấy cái này thì giúp được gì cho bài toán của chúng ta nhỉ?" - bạn có thể sẽ hỏi mình như vậy.
Tuy nhiên, khi mình xét các vector như thế này, có thể các bạn sẽ bắt đầu nhận ra được quy luật:
Vì
Và vector đó bằng:
Cuối cùng chúng ta có thể dùng tool trên để tìm ra được vector nằm trong lattice tạo bởi
Vector mình tìm được sẽ có dạng là
Thế là xong ^.^ Khá là căng não phải không nào :3
solve.sage
x
from Crypto.Util.number import bytes_to_long
load("solver.sage") # Load rkm0959's CVP solver into code
"""
---------------------------- CONSTS ----------------------------
"""
A = 0x0000000000000000000001000000000000000000000000000000000000000163
S = 0xdd268dbcaac550362d98c384c4e576ccc8b1536847b6bbb31023b4c8caee0535
n = 256
"""
------------------------- FNV CRACK AREA -------------------------
"""
def try_solve_fnv1(target_hash, blocklen):
try:
# List of vectors
M = []
# ----- Create vectors (0, ..., 1, 0, 0, ..., 0, A^(length - 1 - i)) -----
for i in range(blocklen):
v = zero_vector(ZZ, blocklen + 1)
v[ i] = 1
v[-1] = A^(blocklen - i - 1)
M.append(v)
# ----- Append (0, 0, ..., 0, 2**n) to M -----
M.append(vector([0] * blocklen + [2^n]))
# ----- Convert to matrix because CVP works with set of vectors as matrix :v -----
M = Matrix(M)
# ----- CVP! -----
d = solve(
M,
lb=[-255]*(blocklen) + [ target_hash - A^blocklen * S ],
ub=[ 255]*(blocklen) + [ target_hash - A^blocklen * S ],
)[0][:-1]
# ----- Recover byte values from diffs -----
s = S
w = []
for i in range(blocklen):
t = (A*s) % (2^n)
s = ((A*s) + d[i]) % (2^n)
b = t ^^ s
w.append(b)
# ----- Return array of numbers as bytes -----
return bytes(w)
except Exception as e:
# print(e)
pass
"""
----------------------------- MAIN -----------------------------
"""
encrypted = bytes.fromhex(open("output.txt", "r").read())
target_hashes = [bytes_to_long(encrypted[i:i+n//8]) for i in range(0, len(encrypted), n//8)]
if __name__ == '__main__':
BLOCKSIZE = n//9
msg = b''
for target_hash in target_hashes:
if (block := try_solve_fnv1(target_hash, BLOCKSIZE)):
print(f'{hex(target_hash)} => {block}')
msg += block
else:
print(f'{hex(target_hash)} => {b"?" * BLOCKSIZE}')
msg += b"?" * BLOCKSIZE
print(msg.decode())
Trong code này khi tạo các vectors, mình để
là sẽ ra được vector
Điểm mình cần lưu ý thứ 2 là trong input của hàm solve
, thay vì chúng ta cho 1 list gồm 29 vector, thì mình phải convert cái list đó thành 1 ma trận 29x29, mỗi hàng tương đương với 1 vector, trước khi nhét nó vào argument đầu tiên của hàm. Lí do mình phải làm thế là vì thằng kia thích để thế 😗 (hoặc có nguyên nhân sâu xa gì đó mà mình cũng chưa biết...)
Và cuối cùng là về cái code này: Code sẽ giải ra được tất cả các block trừ block cuối cùng. Bạn có thể đoán xem vì sao không nhỉ, và làm cách nào để có thể giải ra được nó ;)
xxxxxxxxxx
BKSEC{1-th0ught-tH4t#X0r+4ND$mulTiPLY!D0n,t@m1X????}
Sau khi thử nghiệm với vô vàn các loại blocksize
trong đề bài thì mình nhận thấy rằng nếu blocksize >= 32
thì sẽ có nhiều giá trị
Từ đó dẫn đến việc cái vector mà thuật toán CVP tìm được có thể không mang giá trị đúng của
Tuy nhiên, không có gì là không thể hết ;) Chi ít là với blocksize = 32
.
Dưới đây sẽ là một challenge tương tự dùng blocksize = 32
(là giá trị mình định đặt cho bài này cơ, nhưng sợ làm khó các bạn quá nên thôi :v). Nếu bạn nào vẫn thấy hứng thú sau khi đọc đống hổ lốn này, và vẫn muốn bị ăn hành Crypto tiếp, có thể giải thử ra xem flag
là gì nhé :)
main32.py
xxxxxxxxxx
from Crypto.Util.number import long_to_bytes
##### encryption #####
A = 0x0000000000000000000001000000000000000000000000000000000000000163
S = 0xdd268dbcaac550362d98c384c4e576ccc8b1536847b6bbb31023b4c8caee0535
n = 256
def encrypt_block(block: bytes):
enc_block = S
for byte in block:
enc_block = ((A*enc_block) ^ byte) & ((1<<n) - 1)
return long_to_bytes(enc_block, n//8)
BLOCKSIZE = n//8
def encrypt(data: bytes):
enc = b''
for i in range(0, len(data), BLOCKSIZE):
enc += encrypt_block(data[i:i+BLOCKSIZE])
return enc
##### challenge #####
print(encrypt(open("flag32.txt", "rb").read()).hex())
output32.txt
xxxxxxxxxx
a4d0ceb24a6bba81952fb954bc0b3a6f8cfbfbfa8e267ddc002bbc24277e7f6d1cc9330077201459cf39795bf5927062527c111d851b3184a15e83d1027fa9498ee13591e50f38e6f6abfe8051c6c3fae71010129de81c0a26e2581500f6db52175dbf69e60b266cba0b56d5af81839b5dbf1f4a6087a533a7d887d8260de641446afe6d8df397dadb53db63bae8fd923c1ad25e2200135d239bae2a2e84a04d
Hint nhẹ: flag
KHÔNG bắt đầu bằng BKSEC{
.