Flare-on 10 WriteUp (Part 3)
Phần này mình sẽ trình bày các challenge còn lại sau 2 bài trước đó (Part 1, Part 2).
Challenge 10: Kupo
Đề cung cấp các file gồm:
- tap: đĩa mềm ở dạng tape.
- cfg: config để chạy pdp11 simulator.
- dsk: ổ đĩa để chạy simulator.
- md hướng dẫn các bước đầu để thực hiện.
Đầu tiên là việc setup simh và mount file tap khá đơn giản chỉ cần làm theo hướng dẫn trong file readme.md và thêm câu lệnh “attach ts0 forth.tap” sau dòng “set ts en” là mount được file tap. Sau đó thì trong 1 thời gian dài, mình không thể móc được cái binary pdp11 ra để chạy mặc dù biết nó mount tại /dev/rmt12, do đó mình đã dùng phương pháp khác để thử:
- Format của file forth.tap có nhiều file, mỗi file được chia thành từng block gồm:
- Size1 4 byte (có giá trị là 0x200 hoặc < 0x200 nếu là block cuối của file.)
- Data 0x200 byte, nếu size < 0x200 thì là endoffile và sẽ có thêm 1 byte marker
- Size2 4 byte (có giá trị giống với size1)
- Khi kết thúc 1 file, sẽ có thêm 4 byte \x00 nữa và 8 byte \x00 sẽ là đánh dấu kết thúc của file tap.
Dựa vào kiến thức tự đoán mò ra và chưa hề được xác thực đó, mình extract được các file text và compressed để phân tích tiếp.
File text sẽ có thêm thông tin hướng dẫn và một gợi ý khác là cần dùng mật khẩu của Ken Thompson (“p/q2-q4!”). Còn file compress sau khi uncompress sẽ thu được binary của pdp11.
Cũng mất một khoảng thời gian nữa mình mới hỏi được công cụ đọc code cái file pdp11 này khi các tool hay dùng là ghidra, ida đều bó tay và pdp11disam thì output rất khó theo dõi. Có thể dùng objdump build từ docker ở đây: https://hub.docker.com/r/jameshagerman/gcc-pdp11-aout để trích xuất thông tin cũng như disasembly. Sau khi disasembly thì thấy có khá nhiều symbol, nổi bật là các symbol secret, decrypt, decode,…
Trong đống code dị này, hàm duy nhất mà mình có thể nắm được là hàm decrypt vì nó khá giống code asm bên x86 + một vài sytax có thể đoán. Hàm này thực hiện mã hóa dữ liệu bằng mã hóa xor với key có độ dài > 1. Code python dạng:
def decrypt(key, data):
odata = bytearray()
for i in range(len(data)):
odata.append(data[i] ^ ord(key[i%len(key)]))
return odata
Khi tờ mờ hiểu được hàm này, mình đã thử decrypt secret với password để xem nhưng không thu được gì, vì còn hàm decode vẫn chưa biết làm sao với nó. Tiếp tục nhận được sự trợ giúp, mình tiến hành thay hết các address của các hàm vào và thay luôn cả code assembly từng hàm thì thu được hàm decode đẹp hơn. Cái hàm decode của mình mò ra nó dạng như này:
def decode(data):
r = ”
for i in range(len(data)):
r += chr(i+data[i])
return r
Cuối cùng, sau rất nhiều lần được hỗ trợ thì mình cũng tìm được đoạn code decode để có thể lấy flag bài này và tiếp tục làm các bài sau, ơn giời:
Flag: ken_and_dennis_and_brian_and_doug@flare-on.com
Challenge 11: Overtherainbow
Đề bài cho 1 tập tin PE 64 bit và 1 file very_important_file.d3crypt_m3 cần phải giải mã. Phân tích tập tin trên IDA, tìm được một vài đặc điểm sau:
- Chương trình yêu cầu nhập tham số là một đường dẫn tới thư mục, sau đó sẽ tìm kiếm và mã hóa các file có đuôi “.3ncrypt_m3”.
Hàm mã hóa file tại địa chỉ 0x14007EE60 thực hiện các công việc sau:
- Giải mã public key hardcode
- Tạo ra một key có độ dài 0x100 bytes và mã hóa key đó bằng thuật toán rsa với public key ở trên. Trong đó key bao gồm 3 phần
- Phần 1: 0xa8 byte \x00
- Phần 2: 0x48 byte được tạo ngẫu nhiên là phần cần tìm
- Phần 3: 16 byte “expand 32-byte k”
- Mã hóa dữ liệu của tập tin . 3ncrypt_m3 bằng cách tạo key stream và sử dụng thuật toán xor để mã hóa với keystream đó. Nội dung của file sau khi bị mã hóa sẽ bao gồm bản mã của data và bản mã của key sử dụng để mã hóa. File mã hóa sẽ được lưu cùng tên gốc nhưng đổi đuôi thành “.d3crypt_m3”.
Vì quá trình mã hóa xor ở đây là đối xứng nên để giải mã, ta chỉ cần tìm đúng key và setup key đó cho quá trình mã hóa dữ liệu ở file “.d3crypt_m3” này sẽ thu được bản rõ. Các dữ liệu đã biêt trong quá trình mã hóa key bằng rsa bao gồm:
- publickey(e,n)
- 0xa8 byte đầu của key
- 16 byte cuối của key
Sau một hồi google, tìm thấy bài viết liên quan đến dạng tấn công key rsa khi biết 1 phần của key có sử dụng thư việ sagematch để giải: https://github.com/incertia/uiuctf2017/blob/master/babyrsa/solve.sage.
Chỉnh sửa script với các dữ liệu trên, ta giải mã được phần còn lại của key dùng để mã hóa file “very_important_file.d3crypt_m3”. Buffer cần tìm có giá trị là “06f7768ff2b963f356fc25b3443f7b729f68bcbdd65f22de685c3cb5c8a2697224368530e264fd388dc962f5d737cb873e24f39709d294224a5268c3512ddb6b3e54419b41c810cf”
Tiến hành đổi tên file very_important_file.d3crypt_m3 thành very_important_file.d3crypt_m3, debug cho chường trình bắt đầu mã hóa và thay thế các buffer ngẫu nhiên bằng dữ liệu đã tìm được sẽ có flag:
R1 = bytearray.fromhex(“685c3cb5c8a2697224368530e264fd388dc962f5d737cb873e24f39709d294224a5268c3512ddb6b3e54419b41c810cf”)
Kdata[168:192] = bytearray.fromhex(“06f7768ff2b963f356fc25b3443f7b729f68bcbdd65f22de”)=
Flag: Wa5nt_th1s_Supp0s3d_t0_b3_4_r3vers1nG_ch4l1eng3@flare-on.com
Challenge 12: HVM
Chương trình được thiết kế khá đơn giản, ta dễ dàng thấy nó yêu cầu 2 tham số đầu vào, và đọc shellcode từ resource:
Sau đó, một VM được tạo để thực thi shellcode. Chương trình giao tiếp với VM bởi IO port:
Trước và sau khi gọi 1 hàm, chương trình thực hiện giải mã và mã hóa shellcode bằng thuật toán RC4 với key là giá trị được lấy từ R8 của VM. Nếu kết quả sau khi VM exit bằng 0x1337, chương trình sẽ in ra flag bằng cách XOR tham số argv[2] với key hardcode.
Để giải mã toàn bộ hàm trong shellcode, ta có thể sử dụng python script:
Tham số argv1 được xor với key hardcode, kết quả sau khi xor ta thu được argv[1] = “b’FLARE2023FLARE2023FLARE2023FLARE2023\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00’”
Sau khi kiểm tra tham số argv[1], tham số argv[2] được base64 và giải mã. Cuối cùng nó được so sánh với argv[1], nếu bằng chương trình trả về kết quả mong muốn giúp ta có được flag.
Hàm decrypt b64 tạo keysteam sử dụng thuật toán salsa20 với key “FLARFLARFLAR FLAR”.
Sau khi gen slasa20 keystream, dữ liệu base64 của argv[2] được chia làm các khối 16 bytes, mỗi khối chia làm 2 phần 8bytes data1, data2 và giải mã bằng xor loop:
Để đánh bại thuật toán này ta có thể sử dụng python script sau:
Flag: c4n_i_sh1p_a_vm_as_an_exe_ask1ng_4_a_frnd@flare-on.com
Challenge 13: y0da (Final challenge)
Sử dụng floss kiểm tra strings của chương trình, ta thấy một số string đáng chú ý trong các hình sau
Thử chạy chương trình, ta thấy nó listen trên port 1337. Sử dụng netcat kết nối, chương trình spawn tiến trình cmd.exe. Mình thử nhập một số command xuất hiện trong string như M4st3r, Y0d4, gimmie_advic3… thì gimmie_advic3 nhận lại được phản hồi “Y0da ‘s life tip #0x10d4”
Kiểm tra tiến trình đang chạy bằng process hacker2, ta thấy tiến trình có 3 thread bắt đầu với các RVA 0x4928c, 0x4e0e7, 0x32701
Sử dụng ghidra phân tích y0da.exe. Follow entry từ 0x180032701 ta đến được hàm 180036cd4(), hàm này tạo 2 thread có entry tương tự như ta đã xem bằng công cụ process hacker 2.
Tiến hành hàm phân tích FUN_18004e0e7(), hàm này chứa string gimmie_advic3 mà ta đã phân tích bằng floss. Ngoài ra, ta sẽ thấy chuỗi gimmie_s3cr3t nữa. 2 chuỗi string tương ứng với 2 command và được xử lý bởi 2 hàm 18003c5e2() và 1800216f6().
Chạy command gimmie_s3cr3t, chương trình yêu cầu ta nhập password và hiển thị như hình ảnh bên dưới nếu chúng ta nhập sai password:
Hàm 1800216f6() gọi đến hàm 18001bb76() thực hiện hai nhiệm vụ là kiểm tra password và giải mã resource:
Hàm 18004ebc7() kiểm tra password bằng cách tách các từ trong password bởi ký tự ‘_’ và kiểm tra hash md5 cho từng chữ. Ta có thể dùng các công cụ tìm hash md5 online để xác định password:
- 4C8476DB197A1039153CA724674F7E13 – patience
- 627FE11EEEF8994B7254FC1DA4A0A3C7 – y0u
- D0E6EF34E76C41B0FAC84F608289D013 – must
- 48367C670F6189CF3F413BE394F4F335 – h4v3
Cuối cùng ta có password: patience_y0u_must_h4v3
Thử chạy lại command gimmie_s3cr3t với password patience_y0u_must_h4v3, ta thấy chương trình phản hồi dữ liệu “M4st3r Y0d4 says…” trông khá giống base64:
Password đúng patience_y0u_must_h4v3 được sử dụng làm key thuật toán RC4 để giải mã resource y0da. Kết quả sau khi giải mã, ta thu được file ảnh jpeg chứa flag giả:
Như vậy, chúng ta cần quan tâm đến dữ liệu trông giống base64 được in ra màn hình và thread còn lại có entry từ hàm 18004928c(). Kiểm tra hàm 18004928c(), ta thấy hàm 180050e82() có giá trị. Ban đầu, nó kiểm tra header và sau đó tìm vùng dữ liệu bắt đầu bằng 0xFFE1AA3B, 0xFFE2A1C5 của file ảnh jpeg.
Nhìn sơ bộ, ta thấy dữ liệu buffer1 bắt đầu bởi 0xffe1aa3b có kích thước 0x39 bytes và dường như bị mã hóa. Còn dữ liệu buffer2 bắt đầu từ 0xffe2a1c5 có kích thước 0x1c3 trông khá giống shellcode.
Tiếp tục phân tích hàm này, ta dựa vào một số constant như 0x9908b0df để xác định được hàm 180063054 tạo một buffet ngẫu nhiên bởi Mersenne Twister PRNG.
Tuy nhiên điều chúng ta đang quan tâm ở đây là chuỗi trông khá giống base64 bắt đầu từ
“M4st3r Y0d4 says…”. Kết hợp debug ta xác định được 2 hàm 18004936e và 18002bddf lần lượt giả mã dữ liệu buffer1 phía sau vùng dữ liệu bắt đầu từ 0xffe1aa3b của file ảnh và sau đó mã hóa vùng buffer1 vừa giải mã bằng base32 custom ký tự Q4T23aSwLnUgHPOIfyKBJVM5+DXZC/Re= mà ta cũng đã thấy khi phân tích với công cụ floss.
Phân tích hàm giải mã buffer 18004936e. Rất tiếc là hàm này ghidra phân tích lỗi vì kỹ thuật obfuscate. Vì vậy, ta có thể debug để xem hàm này làm gì. Hàm này có 4 tham số, buffer1, sizeof buffer1, MersenseTwisterBuffer, buffer2. 18004936e gọi đến hàm 18001d361 thực hiện sắp xếp lại buffer2 và giải mã từng byte flag, lúc này ta thấy buffer2 là ROP chain.
Thứ tự các lệnh của ROP chain này được đẩy vào stack và follow bởi lệnh ret:
Ta có thể sử dụng tính năng tracing trong xdbg để log lại lịch sử lệnh thực thi.
Trích xuất ROP chain, ta nhận được hơn 1000 dòng mã asm bao gồm lệnh ret. Việc phân tích nó khá là khó, để ý thì mình thấy nó có những đoạn mã lặp lại, và ở cuối ROP chain ta thấy có 1 phép XOR nên mình đã thử mô phỏng XOR ngược lại output với mersenseTwisterBuffer đơn giản vì XOR là hàm 2 chiều. Thử 1 vài ký tự đầu thấy xuất hiện các ký tự P0w nên mình đoán khả năng, phần phía trên của ROP chain kết hợp buffer1 thực hiện gen Flag sau đó, Flag được XOR với mersenseTwisterBuffer thu được output. Sau đó output này được mã hóa base32 phản hồi command gimmie_s3cr3t:
Thực hiện chuyển đoạn ROP chain này thành python:
Như vậy, ta có được flag
Flag: P0w3rfu1_y0u_h4v3_b3c0m3_my_y0ung_flareaw4n@flare-on.com
Tổng kết
Vậy là mình đã trình bày cách bọn mình giải các challenge Flare-on 10 này. Đợt này mình cảm thấy đề khó hơn năm ngoái, ngốn nhiều time của bọn mình hơn nhưng cũng rất thú vị. Mong bài chia sẻ của mình sẽ hữu ích với AE nào quan tâm, hứng thú với RE và các cuộc thi ctf như Flare-on!!!
Cảm ơn các bạn đã đón đọc!
By,
Blue_Team