⚠ Spoiler: Đây là write-up cho các challenge của Flare-on 9 tổ chức vào khoảng tháng 11/2022 tại Website.

[04] darn_mice

"If it crashes its user error." -Flare Team

7-zip password: flare

Công cụ sử dụng:

  • IDA Pro
  • x96dbg
  • Detect It Easy v3.06

Thử scan với Detect It Easy v3.06 chúng ta biết được darn_mice.exe là một ứng dụng console viết bằng C++.

Result from Detect It Easy

và nếu double-click vào chạy thử, ta thấy chương trình quit luôn. Phải chăng là do lỗi người dùng thật?. Nope, đơn giản là chương trình yêu cầu tham số nhập vào (nhìn vào hàm main trong IDA ta thấy ngay điều này ở đoạn so sánh argc với 2):

Main function

Chạy lại với tham số, lần này có khá khẩm hơn 1 chút, chương trình in ra vài dòng nhưng rồi cũng exit luôn:

Run with params

Lần theo chuỗi được in ra trong IDA, đưa chúng ta đến với sub_6E1000 là nơi chuỗi này được dùng (cũng chính là sub được gọi đến trong hàm main). Tham số được truyền vào ở đây chính là chuỗi chúng ta nhập vào:

sub_6E1000

Ở đây mình đã rename các hàm cùng các biến để cho dễ hiểu:

  • sub_6E1280 có chuỗi BCryptDeriveKeyPBKDF2 failed chúng ta tạm gọi là bcrypt_magic. Trong đoạn này còn gọi đến hàm gì đó liên quan đến RC4, tạm thời chúng ta chưa cần quan tâm vội.
  • Chuỗi nhập vào chúng ta sẽ gọi là pbPassword.
  • Ở đây if ( !v3 || v3 > 35 ) có kiểm tra độ dài password nhập vào. Cùng với đoạn for ngay tiếp theo, ta đoán password có độ dài 35 ký tự (hay 0x23 = 0x24 - 1).
  • Đoạn for phía sau khá là lạ:
    1. Đầu tiên, thực hiện allocation một vùng nhớ, kích thước 0x1000 lưu vào v2.
    2. Sau đó là lấy từng ký tự của password, sau đó đem cộng với giá trị tương ứng ở vị trí đó của biến magic được setup ở đầu function.
    3. Gán giá trị này cho byte đầu tiên của vùng nhớ vừa khởi tạo.
    4. Nhảy đến và chạy code tại vùng nhớ đó (tương ứng với lệnh gọi v2(v2)).
    5. In ra chuỗi Nibble...
    6. Tiếp tục vòng lặp
  • Qua đoạn for này, nếu nhập đúng password, chương trình sẽ dùng password này để decrypt ra flag.

Giả sử với chuỗi nhập vào là AAAAAAAAA, thì ở lần lặp đầu tiên của vòng for, giá trị đầu tiên mảng magicP (code qmemcpy(magic, "P^^", 3);) chúng ta sẽ tính toán như sau:

>>> hex(ord('A') + ord('P'))
'0x91'

để ra giá trị tại vùng nhớ địa chỉ 010F0000 là 0x91` như trong screen shot dưới đây, khi debug bằng x96dbg:

First call

Vậy bài toán của chúng ta là xây dựng code làm sao để thực hiện đúng 1 lệnh assembly (opcode) mà:

  1. Chương trình không bị crash.
  2. Sau khi thực hiện xong thì quay về đúng với flow cũ của chương trình (để tiếp tục in ra dòng Nibble...).

Câu trả lời của chúng ta không phải là opcode 0x90 (NOP) mà chính là opcode 0xC3 (RET), thường xuất hiện ở cuối của function:

ret opcode

Khi call vào vùng nhớ, gặp opcode ret thì ngay lập tức flow code sẽ quay về và tiếp tục chạy như bình thường.

Viết một đoạn python để re-build lại password từ magic:

# from setup part of sub_6E1000
>>> magic = [0x50, 0x5E, 0x5E, 0xA3, 0x4F, 0x5B, 0x51, 0x5E, 0x5E, 0x97, 0xA3, 0x80, 0x90, 0xA3, 0x80, 0x90, 0xA3, 0x80, 0x90, 0xA3, 0x80, 0x90, 0xA3, 0x80, 0x90, 0xA3, 0x80, 0x90, 0xA3, 0x80, 0x90, 0xA2, 0xA3, 0x6B, 0x7F]
>>> "".join([chr(0xC3 - m) for m in magic])
'see three, C3 C3 C3 C3 C3 C3 C3! XD'

Vậy password đúng là see three, C3 C3 C3 C3 C3 C3 C3! XD. Chạy lại chương trình với password này sẽ in ra flag:

Final flag

i_w0uld_l1k3_to_RETurn_this_joke@flare-on.com