micromicromicropython Write-up (Bản tiếng Việt)

1. Phân tích lỗ hổng & Cú lừa (The Decoy)

Thử thách cung cấp một môi trường MicroPython bị cắt giảm nặng nề (bản minimal và module os đã bị vô hiệu hóa). Kiểm tra mã nguồn, có một file catflag.c chứa một cờ (flag) được hardcode: ctf{ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL_1FAEFB6177B4672DEE07F9D3AFC62588CCD2631EDCF22E8CCC1FB35B501C9C86}.

Tuy nhiên, đây chỉ là một cờ mồi (decoy)! Cờ thật chỉ có thể lấy được trên môi trường remote bằng cách thực thi thành công file binary /catflag. Thử thách yêu cầu chúng ta tìm một lỗ hổng trong MicroPython để đạt được khả năng thực thi mã (RCE).

Lỗ hổng cốt lõi nằm ở hàm mp_import_all (được kích hoạt bởi from module import *). Khi một đối tượng staticmethod bọc một đối tượng bytes được chèn vào sys.modules, mp_import_all sẽ coi raw bytes đó như một cấu trúc mp_map_t và duyệt qua nó. Mặc dù việc đọc quá giới hạn của mảng byte ban đầu cuối cùng sẽ gây ra lỗi out-of-bounds read và crash (segfault/TypeError), nhưng entry đầu tiên trong map giả mạo này đã kịp được phân tích cú pháp thành công và import vào dictionary globals() trước khi crash xảy ra!

Điều này mang lại cho chúng ta một primitive import raw pointer: chúng ta có thể đặt một địa chỉ bộ nhớ bất kỳ vào đối tượng bytes và ép interpreter (trình thông dịch) hiển thị nó như một đối tượng Python.

2. Chiến thuật khai thác

Quá trình khai thác diễn ra qua nhiều giai đoạn bằng cách sử dụng primitive type confusion (nhầm lẫn kiểu dữ liệu) này.

Bước 1: Khôi phục VfsPosix

Mặc dù module os đã bị loại bỏ, các lớp Virtual File System (VFS) nội bộ, chẳng hạn như VfsPosix, vẫn được biên dịch trong binary. Bằng cách leak địa chỉ của một hàm đã biết (ví dụ: id(exec)) và cộng/trừ với một offset PIE cố định, chúng ta có thể tính toán được địa chỉ của type object VfsPosix. Sử dụng primitive import thông qua staticmethod(bytes), chúng ta import lớp VfsPosix vào namespace của mình.

Bước 2: Đọc file tùy ý (/proc/self/maps & /proc/self/mem)

Với việc có trong tay lớp VfsPosix, chúng ta có thể khởi tạo nó và sử dụng để mở các file trên hệ thống:

fs = VfsPosix('/')
maps = fs.open('/proc/self/maps', 'r')
mem = fs.open('/proc/self/mem', 'rb')

Đọc file /proc/self/maps cho phép chúng ta leak base address của thư viện ld-musl. Từ base này, chúng ta có thể tính toán địa chỉ chính xác của hàm system().

Bước 3: Làm giả một Built-in Function (Hàm tích hợp sẵn)

Để gọi system(), chúng ta tạo một struct mp_obj_fun_builtin_fixed_t giả (một built-in function nhận 1 tham số) trong bộ nhớ. Một bước quan trọng ở đây là bypass ASLR cho các con trỏ type nội bộ. Thay vì hardcode type pointer của một builtin function, chúng ta sử dụng primitive đọc bộ nhớ tùy ý (thông qua /proc/self/mem) để đọc trực tiếp header của một builtin function đang "sống" như hàm len. Chúng ta sao chép header hợp lệ này để tạo ra một đối tượng function giả mạo hoàn hảo, với function pointer đã bị ghi đè bằng địa chỉ của system().

Bước 4: Thực thi /catflag

Chúng ta cũng cần một tham số cho system(). Chúng ta tạo một đối tượng bytes b"/catflag\x00" và dùng /proc/self/mem để tìm ra con trỏ dữ liệu thô (raw data pointer) của nó. Sau đó, chúng ta sử dụng primitive import một lần cuối để import con trỏ chuỗi C thô này dưới dạng một biến Python.

Cuối cùng, chúng ta gọi hàm giả mạo với con trỏ chuỗi giả mạo:

S(C) # Tương đương với system("/catflag")

3. Bài học rút ra

  • Mồi nhử (Decoys): Đừng tin tưởng mù quáng vào các file source (như catflag.c) trong file zip nếu thử thách có gợi ý về "heavy stripping" (cắt giảm nặng) hoặc 0-days. Flag thực sự có thể được tạo động hoặc hoàn toàn khác biệt trên server remote.
  • Thực thi một phần vẫn là Thực thi: Ban đầu, crash do type confusion trong mp_import_all có vẻ giống như đi vào ngõ cụt vì chắc chắn sẽ bị segfault. Tuy nhiên, việc hiểu rằng vòng lặp đầu tiên thành công và kịp thay đổi globals() trước khi crash chính là chìa khóa. Bạn không cần một đối tượng fake hoàn hảo từ đầu đến cuối; bạn chỉ cần nó "sống" đủ lâu để thực hiện được mục đích phá hoại.
  • Clone type động: Việc hardcode các type pointer nội bộ (như &mp_type_fun_builtin_1) rất dễ vỡ (brittle) và dễ gặp lỗi trên các connection khác nhau. Việc sao chép (clone) động header của một đối tượng đang sống (như len) thông qua primitive đọc tùy ý giúp cho exploit hoạt động ổn định 100%.

4. Flag

bctf{this_was_originally_going_to_be_a_0day_but_someone_reported_it}