Rootme challenge - SEHVEH
Kể ra thì từ khi tạo blog đến giờ mình chưa viết được bài nào ra hồn cả. Mấy tuần trước mình đã có dự định là sau khi thi xong sẽ bắt đầu việc viết lách, thế nhưng rốt cục bệnh lười lại tái phát. Mãi đến hôm nay, sau một hồi đấu tranh với cái tính nhác việc, mình mới chịu mở lap lên và ngồi lọc cọc gõ ra bài này.
Dạo này việc học của mình không có tiến bộ gì mấy (khá thất vọng vì điều này), tất cả những gì mình làm trong vài tuần vừa qua chỉ là giải bài trên Root-me mà thôi. Root-me là một trang web rất thích hợp cho những newbie mới tập chơi CTF như mình. Những chall mình giải được cung cấp cho mình một vài kiến thức mới mà mình thấy đáng phải ghi lại, và sau đây là một trong số đó.
Dài dòng thế là đủ rồi, chào mừng đến với bài viết của mình về Structured Exception Handling. Trong bài viết này mình sẽ trình bày writeup cho challenge SEHVEH của Root-me, đồng thời nêu những khái niệm mới mà mình học được trong quá trình giải bài.
1. Exception:
Trước khi đi vào writeup, mình muốn nói sơ qua một chút về exception. Theo định nghĩa từ https://docs.microsoft.com/, exception là một sự kiện xảy ra trong quá trình chạy chương trình. Những sự kiện này buộc chương trình phải thực hiện các đoạn code nằm bên ngoài luồng thực thi thông thường.
Ta lấy một ví dụ như sau:
|
|
Ở chương trình trên, nếu nhập input là 0, ta dễ dàng nhận thấy có một phép toán không hợp lệ (phép chia cho 0). Lúc này chương trình sẽ thông báo lỗi và dừng lại. Vậy tức là trong quá trình thực thi, đã có một exception khiến cho chương trình không chạy như bình thường mà phải thực hiện một đoạn code khác có nhiệm vụ in lỗi và thoát chương trình.
2. SEH và VEH:
Structure Exception Handling (hay SEH) là một cơ chế để xử lý các exception. Nhờ cơ chế này mà chúng ta có thể kiểm soát được cách mà chương trình của mình giải quyết các exception. Còn Vectored Exception Handling (hay VEH) thực chất là một phần của SEH. VEH cho phép chúng ta chỉ định một hàm để xử lý toàn bộ các exception xảy ra trong quá trình thực thi. Trong Windows, ta có thể sử dụng hàm AddVectoredExceptionHandler để thêm handler cho chương trình.
Đề bài tên là SEHVEH nên chắc hẳn sẽ dính đến hai khái niệm này rồi. Bây giờ thì bắt tay vào giải thôi.
3. Writeup:
OK, bây giờ chúng ta cũng đi vào writeup nào. {: .mx-auto.d-block :}
Nhìn vào hàm start, ta nhận thấy sau khi nhận input bằng scanf, chương trình đưa địa chỉ của một hàm nào đó vào ecx, sau đó tiến hành gọi lệnh call ecx (trong hình mình có đặt tên cho nó là check)
Hàm check đương nhiên có nhiệm vụ là… kiểm tra input của chúng ta rồi. Vậy giờ hãy đi vào phân tích hàm này.
Dựa vào graph của ida, có vẻ như hàm check có 4 phần kiểm tra riêng biệt. Nếu input của ta rớt test nào thì lập tức ebp sẽ nhảy lên 1. Vậy mục tiêu của ta là giữ cho ebp nằm ở số 0 thôi!
{: .mx-auto.d-block :}
Sử dụng debugger (hay phân tích tĩnh cũng được), ta nhận thấy đoạn code trên dùng để kiểm tra độ dài của input. Phần check này khá dễ nên mình sẽ không ghi ở đây.
{: .mx-auto.d-block :}
Đoạn này cũng đơn giản luôn nè, chỉ là xor thôi.
{: .mx-auto.d-block :}
Ở đây xuất hiện 3 instruction khá lạ như sau
|
|
Đối với các chương trình trên Windows, thanh ghi fs dùng để chỉ tới Thread Information Block (TIB). Từ tên gọi, ta cũng đoán được đây là một cấu trúc dữ liệu lưu những thông tin về thread đang chạy, và một trong số những thông tin đó chính là SEH frame (tham khảo tại [đây] (https://en.wikipedia.org/wiki/Win32_Thread_Information_Block#Accessing_the_TIB)).
Nói về SEH frame thì đó là nơi lưu trữ địa chỉ của các exception handler, cách mà chương trình tìm đến handler phù hợp được mô tả theo lưu đồ sau (quá trình này được gọi là Stack Unwinding {: .mx-auto.d-block :}
Dựa vào cấu trúc của TIB, ta nhận thấy fs:0 chính là pointer trỏ đến SEH frame. Mỗi địa chỉ của một handler được lưu dưới dạng struct EXCEPTION_REGISTRATION_RECORD (tham khảo thêm về cấu trúc tại đây). Chính vì thế, 2 lệnh push ở trên thực chất là đưa thêm một struct EXCEPTION_REGISTRATION_RECORD, hay nói cách khác, địa chỉ của một handler vào stack. Tiếp đó, lệnh mov sẽ thay đổi địa chỉ mà fs:0 trỏ tới, tức là thay đổi cái handler đầu tiên được gọi.
Nói tóm gọn lại, khi gặp exception trong chương trình, đầu tiên OS sẽ đến địa chỉ nằm trong fs:0 và gọi hàm ở đó. Nếu exception chưa được giải quyết, nó sẽ tìm đến hàm tiếp theo được trỏ đến bởi phần tử next trong struct EXCEPTION_REGISTRATION_RECORD và cứ tiếp tục như thế cho đến khi thỏa mãn exception (nhắc lại một lần nữa đây là stack unwinding nha :D ).
Sau khi thêm handler vô rồi thì ta có thể thấy chương trình tạo exception ngay phía dưới với lệnh int 1, với kiến thức đã nói ở trên thì ta có thể dễ dàng xử lý điều này (bằng cách debug hay tìm tới địa chỉ của hàm handler vừa được thêm vô rồi đọc code đều được)
Đoạn check cuối cũng thêm một handler vào, nhưng thêm bằng hàm AddVectoredExceptionHandler của WinApi. Về cơ bản thì hàm này làm nhiệm vụ y hệt với 3 instruction ở trên, vậy nên ta cũng xử lý giống ở trên luôn. (Một lưu ý nhỏ, hãy nhớ rằng nếu exception chưa được giải quyết, chương trình sẽ tìm đến hàm tiếp theo trong danh sách handler, do đó nếu phân tích tĩnh thì nên chú ý xem hàm này đã làm thỏa exception chưa)
Thôi viết thế đủ rồi nhỉ, mình cũng lười quá rôi. Bắt đầu viết từ đầu tháng mà ngâm đến cuối tháng mới xong :(