FNV encryption

Mở đầu

Đâ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ì:

Mục lục

 

Files

main.py

output.txt

Phân tích

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.

Nguyên lí hoạt động

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 2n1.

 

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_blockS0,S1,,S28 và các bytes trong 1 blockb0,b1,,b27, ta có dãy số như sau:

S0=SS1=(AS0b0)&(22561),0b0255S2=(AS1b1)&(22561),0b1255S28=(AS27b27)&(22561),0b27255

Bài toán yêu cầu cho biết A,SS28, tìm cách để khôi phục lại được các bytes được XOR ở giữa 😗

Vì sao nó khó hơn bạn nghĩ

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ừ S0S28. Hàm này được thiết kế để nó trông ngẫu nhiên hết sức có thể, vì vậy state nào ở giữa cũng có khả năng xảy ra.

 

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ứ... ://

 

Cách giải

Để giải được bài này, mình sẽ không giải b0,b1,,b27 một cách trực tiếp, mà là cần phải tạo 1 phương trình 29 ẩn d0,d1,,d27,k, và giải ra bi từ di. Tuy nhiên di hay k là gì, thì mình sẽ không đề cập đến nó vội, mà mình muốn giải thích cho các bạn về từng bước để tạo ra được phương trình đó đã ~.~

 

Bài này mình có cho 2 hints.

 

Hint thứ nhất là dùng +% thay 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 +%.

 

Còn hint thứ 2 là để dùng 1 tool mà mình chưa đề cập đến vội ~.~

- Hint 1: Thay đổi góc nhìn -

Mục đích đưa 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 2n1 chứ không phải số nào khác :333

Khi ta AND 1 số với 2n1, tức là 1 số mà dưới dạng nhị phân của nó toàn là 111111...11111, nó sẽ tương đương với việc mình mod số đó với 2n.


Để 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ỉ.

12345678mod10=8

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ư.

12345678mod102=78

Theo quy luật đó, để lấy n chữ số thập phân cuối cùng, ta sẽ mod số đó với 10n và lấy kết quả.


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 2n mà thôi 😗

 

^ -> +

Khi chúng ta XOR một giá trị X nào đó với một byte b, thì nhiều nhất là 8 bit cuối của giá trị đó bị thay đổi.

1001100011100101011011110110

Gọi số_bit(X) - 8 bit đầu trong XU, 8 bit cuối trước khi và sau khi bị XORLaLb, ta có:

X=28U+LaXb=28U+Lb

Nếu xét d=(Xb)X, mình sẽ có được tính chất thú vị như sau về nó:

255d=(Xb)X=LbLa255

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.

Xb=X+d0b255255d255

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ả.

1337101=1337+3510135

Điều thứ 2 mình cần để ý, đó là vì mình có d=(Xb)X nên mình cũng có thể suy ra b=(X+d)X.

Viết lại dãy số

Từ suy luận ở bên trên, ta sẽ viết lại dãy số như sau:

S0=SS1=(AS0+d0)mod2256,255d0255S2=(AS1+d1)mod2256,255d1255S28=(AS27+d27)mod2256,255d27255

Tuy rằng d0,d1,,d27 có thể không hề bằng b0,b1,,b27 một tẹo nào, một cái là cộng một cái là XOR mà. Nhưng nếu ta giải ra được các di, thì có thể khôi phục được các Si bằng cách thay số dần dần lên dãy số trên, và tính lại được bi từ di bằng cách lấy:

ASi(ASi+di)=bi

Và từ đó ta sẽ quy về được một phương trình mod 2^256 gồm 28 ẩn d0,d1,,d27.

S28=(AS27+d27)mod2256=(A(AS26+d26)+d27)mod2256=A2S26+Ad26+d27mod2256=A3S25+A2d25+Ad26+d27mod2256=A28S0+A27d0+A26d1+Ad26+d27mod2256=A28S+A27d0+A26d1+Ad26+d27mod2256

Đưa A28S sang vế trái, mình sẽ có được:

S28A28S=A27d0+A26d1+Ad26+d27mod2256

M=NmodO cũng đồng nghĩa với việc M=N+kO với kZ, nên ta có thể suy ra:

S28A28S=A27d0+A26d1+Ad26+d27+2256k

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.

 

Nghỉ giải lao 1 chút...

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 ~.~

 

- Hint 2: Giải hệ bất phương trình -

Đươ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 Z, và:

255di255,d[0,27]

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

S28A28SA27d0+A26d1+Ad26+d27+2256kS28A28S255d0255255d1255255d2255255d27255

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à latticeCVP.

Lattice

Nếu mình có n vector v0,v1,,vn1 bất kì, thì lattice là tập hợp tất cả các vector được tạo bởi n vector đó, có dạng:

c0v0+c1v1++cn1vn1

với ciZ,i[0,n1].

CVP

Thuật toán CVP là một thuật toán tìm kiếm vector trong lattice được tạo bởi v0,v1,,vn1, có khoảng cách gần nhất với một vector target w mà mình cho trước.

Nói cách khác, việc nó làm là cố gắng đi tìm 1 vector có dạng c0v0+c1v1++cn1vn1 với cZ sao cho nó nằm sát với vector w nhất có thể.

Vector w là một vector mình sẽ được chọn, và w có thể nằm hoặc không nằm trong cái lattice đó cũng được.

CVP của rkm0959

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à lr thay vì chỉ nằm sát w như thuật toán ban đầu.

Đị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 m=(m0,m1,,mn1), với l=(l0,l1,,ln1)r=(r0,r1,,rn1) thì:

l0m0r0l1m1r1ln1mn1rn1

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ì li nào >ri nhé :33

Quay trở về với bài toán

"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:

v0=(A27,1,0,0,0,,0,0)v1=(A26,0,1,0,0,,0,0)v2=(A25,0,0,1,0,,0,0)v3=(A24,0,0,0,1,,0,0)v26=(A1,0,0,0,0,,1,0)v27=(A0,0,0,0,0,,0,1)v28=(2256,0,0,0,0,,0,0)

d0,d1,,d27,k cần tìm đều thuộc Z, nên d0v0+d1v1++d27v27+kv28 chắc chắn sẽ nằm trong lattice tạo bởi v0,v1,,v28.

Và vector đó bằng:

d0v0=(d0A27d000000)+d1v1=(d1A260d10000)+d2v2=(d2A2500d2000)+d3v3=(d3A24000d300)+d26v26=(d26A10000d260)+d27v27=(d27A000000d27)+kv28=(k2256000000)d0v0+d1v1++d27v27+kv28=(S28A28Sd0d1d2d3d26d27)

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 v0,v1,,v28, với

l=(S28A28S,255,255,255,,255,255)vàr=(S28A28S,255,255,255,,255,255)

Vector mình tìm được sẽ có dạng là (S28A28S,d0,d1,d2,d3,,d26,d27). Sau đó mình chỉ cần lấy 28 cột cuối của vector trên và rút ra được các giá trị d0,d1,,d27. Từ đây mình có thể giải được b0,b1,,b27, thành công khôi phục lại được 1 block từ 1 cipherblock :3

 

Thế là xong ^.^ Khá là căng não phải không nào :3

Code giải

solve.sage

Một chút lưu ý nhỏ

Trong code này khi tạo các vectors, mình để Ai ở cột cuối thay vì ở cột đầu như trong giải thích bên trên, khi đó thì mình vẫn có thể dùng CVP để tìm 1 vector nằm trong khoảng giữa

l=(255,255,255,,255,255,S28A28S)vàr=(255,255,255,,255,255,S28A28S)

là sẽ ra được vector (d0,d1,d2,d3,,d26,d27,S28A28S) và thay vì lấy 28 cột cuối của nó, thì mình lấy 28 cột đầu để trích xuất d0,d1,d2,d3,,d26,d27.

 

Đ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ó ;)

Flag

 

Challenge phụ: blocksize=32 thì sao nhỉ...

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ị d0,d1,d2,d3,,d26,d27 thỏa mãn hệ bất phương trình

S28A28SA27d0+A26d1+Ad26+d27+2256kS28A28S255d0255255d1255255d2255255d27255

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 d0,d1,d2,d3,,d26,d27, và các giá trị b0,b1,,b27 được khôi phục sẽ bị sai.

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

output32.txt

Hint nhẹ: flag KHÔNG bắt đầu bằng BKSEC{.