Kungfu RE
Lời giải cho một số bài reverse mình cảm thấy thích thú.
Kungfu RE
[VCS Training] crackme_1
Decompile bằng IDA32, ta thu được pseudo-code của hàm main()
như sau
|
|
Chương trình cho phép nhập vào tối đa 300 ký tự (không tính \n
) và bắt đầu check input nếu độ dài ≥ 294. Ngược lại, chương trình sẽ in ra oh, no!
Nếu input của chúng ta hợp lệ, flag sẽ được in ra tại đoạn mã này.
|
|
Chúng ta bắt đầu đi vào hàm checkInput()
để phân tích chức năng của nó.
|
|
Chương trình khởi tạo một byteStr
, kiểm tra độ dài input
một lần nữa và bắt đầu thực hiện 122 round. Sau mỗi round, byteStr
sẽ tăng thêm 3 đơn vị, mỗi DWORD có kích thước 4 byte. Vậy mỗi round cần dùng 12 byte → kích thước của byteStr
sẽ là 12 * 122 = 1464 byte.
Trong mỗi round, hàm execCheck()
sẽ được gọi với 3 đối số.
|
|
Với kiểu dữ liệu DWORD trong IDA, mỗi đối số sẽ có kích thước 4 byte. Đây là một mẩu chuỗi byte nhỏ trong byteStr
Trong IDA,
db
đại diện cho 1 byte,dw
đại diện cho 2 byte vàdd
đại diện cho 4 byte
Để biết chính xác các đối số đó làm nhiệm vụ gì, chúng ta sẽ đi tiếp vào trong hàm execCheck()
|
|
Sau một hồi quan sát, ta biết được 3 đối số đó lần lượt là:
caseIndex: Có tổng cộng 4 caseIndex. Dưới đây là case 1, 1 và 2.
subInput: Lấy ví dụ hình ảnh ở trên với case 2. Index của
subInput
là 88h, nghĩa làsubInput
sẽ được lấy từinput[0x88]
đến hết.initString: Đây là chuỗi byte để so sánh kết quả của từng case.
Chúng ta thấy switch xử lý từng case một nhưng đều theo 1 format là
|
|
Hàm sub_861000()
thực hiện một số biến đổi và trả về cho ta là địa chỉ của hàm xử lý từng case đó tại biến lpAddress
. Và đây là dòng lệnh khi hàm đó được gọi
|
|
Nhiệm vụ của chúng ta là phải lấy được code hàm xử lý 4 case trên. Đặt breakpoint ngay tại dòng code trên, ấn F7
để đi vào trong hàm xử lý từng case. Dưới đây là một đoạn code của hàm xử lý case 1.
Ấn phím p
để IDA tạo function, ấn F5
để xem mã giả của hàm xử lý.
Thực hiện tương tự cho các case còn lại. Lưu ý là qua từng round, do input ban đầu chúng ta nhập chưa chính xác, chương trình sẽ end khi gọi hàm execCheck()
.
Tới câu lệnh jnz
, mình chỉ cần sửa lại giá trị cờ ZF
từ 1 thành 0 là có thể đi tiếp vào round sau.
Bây giờ, chúng ta sẽ đi phân tích cụ thể chức năng của từng hàm.
Hàm checkCase1()
sẽ đem xor ký tự subInput[0]
với 0x20 nếu mã ASCII của nó là chẵn và xor với 0x52 nếu ngược lại.
|
|
Để tìm được kí tự thỏa mãn checkCase1()
khá đơn giản bằng cách brute force.
|
|
Lưu ý rằng,
block
của mình là một mảng có kích thước 12 byte.
Hàm checkCase2()
thực hiện biến đổi 2 kí tự subInput[0]
và subInput[1]
qua 5 round.
|
|
Chúng ta hoàn toàn brute force được để tìm được 2 kí tự thỏa mãn. Lưu ý là v4
chỉ là số 2 byte, trong khi các phép biến đổi có thể làm v4
vượt qua khoảng giới hạn. Vì vậy sau mỗi vòng brute force, ta phải v4 &= 0xffff
để nó luôn là số 2 byte.
|
|
Đối với checkCase3()
, chương trình biến đổi 3 kí tự đầu của subInput
và đi so sánh với initString
. Lưu ý rằng, dưới đây là toàn bộ mã giả do IDA sinh ra. Nếu chú ý sẽ thấy đoạn mã hóa giống y hệt như đoạn code mình đã comment. Mình đoán IDA đã gặp lỗi gì đó trong phân tích / do mình đoán bừa, nhưng không ảnh hưởng đến bài toán nên mình đã comment lại.
|
|
Chúng ta tiếp tục brute force 3 kí tự để tìm ra đáp án.
|
|
Đây là mã giả của hàm checkCase4()
. Ta thấy createTable
và checkFlag
là 2 con trỏ hàm. Vậy nên ta sẽ không double-click để xem sourcecode. Lúc debug, ta chỉ cần ấn F7
là sẽ nhảy được vào trong từng hàm.
|
|
Hàm createTable()
tạo cho chúng ta một bảng table
có kích thước 256 byte với giá trị các ô nằm trong khoảng 0 → 255. Lưu ý là giá trị các ô đã bị hoán đổi sau vòng for kia. Nhưng do hàm luôn nhận string susan
làm đối số, vì vậy giá trị của table
sẽ luôn được cố định.
|
|
Sau khi có table
ở trên, hàm checkFlag()
bắt đầu đi thực hiện biến đổi và kiểm tra.
|
|
Ta dễ dàng viết script brute force để lấy được kết quả
|
|
Trong IDA có chức năng tạo mảng với chuỗi byte. Đầu tiên ta sẽ select đoạn byte cần tạo mảng, ấn
Shift E
và chọn các option phù hợp.
Sau khi hoàn thiện việc giải mã 4 hàm rồi, ta sẽ lấy toàn bộ chuỗi byte ban đầu, chia thành từng block và xử lý theo từng case một.
|
|
Kết quả message thu được là
|
|
Chạy lại chương trình và nhập message trên, ta được flag là vcstraining{Aw3s0me_D4ta_tran5Form4t1oN_Kak4}
[KCSC CTF 2022] FlagChecker
Đề bài cho chúng ta một file PE32 được pack bởi UPX
và viết bằng AutoIt.
Sau khi unpack, mình sử dụng tool unautoit
để extract script autoit nhờ bài viết này https://sec.vnpt.vn/2022/03/zer0pts22-ctf-writeup-a-part-flag-checker.
Có lẽ tác giả đã đổi tên nên sẽ không tìm được tool này trên Github. Các bạn có thể vào folder CTF của mình để tải công cụ này xuống.
Hàm checker()
gọi CallWindowProcA
với đống shellcode phía trên cùng với input
nhập vào.
|
|
Thử debug với IDA, ta thấy chương trình thông báo lỗi, chứng tỏ có antidebug ở đây
Sử dụng tab strings, ta dễ dàng tìm ra đoạn check debug và bypass nó. Đặt breakpoint tại CallWindowProcA
trong user32.dll
, F9 để chương trình chạy tới đoạn gọi shellcode và input.
Đây là hình minh họa một đoạn shellcode ngắn của chương trình.
Ta thấy string user32.dll
nằm ngay ở shellcode, sau khi makecode trừ các chuỗi như trên, ta thu được kết quả như sau
Đối với file 32 bit, khi gọi hàm loc_47003B3()
, chương trình sẽ push chuỗi user32.dll
lên stack, khi đó hàm được gọi có thể sử dụng chuỗi này như một đối số được truyền vào của hàm.
Thực hiện makecode tương tự cho các bytecode còn lại, chúng ta sẽ vừa debug trâu bò vừa ghi các hàm được gọi ra bản nháp để hiểu sâu hơn về chương trình.
Hàm loc_4700939
Hàm có sự xuất hiện của những strings như:
- kernel32.dll
- LoadLibraryA
- GetProcAddress
- VirtualAlloc
- VirtualFree
- lstrlenA
nên mình nghĩ khả năng cao nó sẽ load các hàm từ kernell32.dll
.
Hàm loc_470076A
Tương tự như trên, ở hàm này, chương trình sẽ load các hàm thực hiện mã hóa từ advapi32.dll
.
- CryptAcquireContextA
- CryptCreateHash
- CryptImportKey
- CryptDeriveKey
- CryptHashData
- CryptEncrypt
- CryptGetHashParam
- CryptDestroyHash
- CryptDestroyKey
- CryptReleaseContext
Hàm loc_47003B3
Phần thực thi chính shellcode sẽ nằm ở đây. Như cách giải thích việc gọi hàm ở phía trên,
việc gọi hàm như vậy sẽ tương đương với
|
|
Tiếp theo, chương trình có load hàm MessageBoxA()
, kiểm tra độ dài input
với 28. Đoạn mã phía dưới theo phỏng đoán của mình chính là ciphertext
để check flag.
Tới đây, mình dành vài tiếng để vừa makecode, debug và ghi chép mô phỏng lại toàn bộ các hàm được gọi. Kết quả được clean bởi ChatGPT như sau
|
|
Tới đây thì ta hoàn toàn hình dung được luồng thực thi của chương trình. Trước tiên sẽ cho phép nhập input mã hóa RC4 với key là SHA1 của youtube_url
. Cuối cùng sẽ so sánh với mảng ciphertext
.
Mình sẽ lấy luôn các hàm decrypt bằng cryptoAPI để tránh những sai sót không mong muốn.
|
|
Flag thu được là KCSC{rC4_8uT_1T_L00k2_W31Rd}