REVERSING WITH IDA FROM SCRATCH (P37)

m4n0w4r
tradahacking
Published in
18 min readDec 25, 2021

--

Qua các bài viết trước, các bạn cũng đã biết CANARY là gì rồi, nó là một giá trị ngẫu nhiên được lưu vào bộ nhớ ( biến) trên Stack, ngay phía trên giá trị của EBP ( old frame) và địa chỉ trở về. Vì vậy, khi thực hiện overflow để ghi đè lên địa chỉ trở về thì giá trị này cũng sẽ bị ghi đè. Lúc đó, cuối chương trình sẽ có đoạn code thực hiện kiểm tra lại giá trị của CANARY, nếu như vẫn là giá trị ban đầu đã lưu thì chương trình sẽ tiếp tục thực thi bình thường, còn ngược lại nếu là một giá trị khác giá trị ban đầu thì chương trình sẽ chặn việc thực thi mã.

Gửi kèm theo bài viết này là ví dụ CANARY_sin_DEP.exe ( hay CANARY_without_DEP) do thầy Ricardo Narvaja biên soạn để tôi và các bạn thực hành. Trong phần sau, chúng ta sẽ thực hành với một ví dụ có DEP và phải xây dựng ROP để vượt qua được CANARY. Code trong hai ví dụ là giống nhau, khác nhau chỉ là ở chỗ có cơ chế Stack CANARY được thêm vào nhằm ngăn không cho nó bị khai thác bằng cách ghi đè lên địa chỉ trở về.

Ta sẽ sử dụng lại script đã viết cho file NO_DEP.exephần 34 để trace code và xem tại sao script đó lại không áp dụng được với CANARY_sin_DEP.exe, từ đó cố gắng tìm hiểu cách thức để vượt qua cơ chế bảo vệ CANARY.

Thay lại tên target trong script:

Sau đó, chạy thử script:

Tôi thấy script chạy bình thường nhưng không thấy thực thi được ứng dụng calculator. Kiểm tra thì thấy xuất hiện dump file, chứng tỏ chương trình đã bị crash:

Để biết được nguyên nhân ta phải trace code để xem điều gì sẽ xảy ra. Trước tiên, load chương trình vào IDA để phân tích đã. Khi đi vào hàm saluda(), bạn sẽ thấy có sự khác biệt so với ví dụ đã thấy ở phần trước đó.

Ta thấy rằng nó đọc ra một giá trị ngẫu nhiên _security_cookie (được lưu ở data section) và gán vào thanh ghi EAX, sau đó thực hiện phép XOR giá trị này ở EAX với thanh ghi EBP. Cách này giúp tạo ra một giá trị hoàn toàn ngẫu nhiên, vì EBP là địa chỉ base frame của hàm saluda và nếu file thực thi áp dụng cơ chế địa chỉ ngẫu nhiên ( ASLR) thì giá trị của EBP cũng sẽ là ngẫu nhiên mà không phải là một hằng số cụ thể.

Kết quả tính toán của lệnh XOR sẽ được lưu vào trong biến CANARY. Như vậy, nếu cố thực hiện overflow nhằm ghi đè lên địa chỉ trở về thì đồng nghĩa giá trị lưu ở biến CANARY cũng sẽ bị thay đổi. Bên cạnh đó, không ai có thể biết được chính xác giá trị ngẫu nhiên nào sẽ lưu vào biến để tìm cách ghi đè vào biến với cùng giá trị đó.

Khi chuẩn bị thoát khỏi hàm, giá trị này sẽ được lấy ra, đem XOR một lần nữa với cùng giá trị của EBP để có được _security_cookie ban đầu. Tiếp theo, gọi hàm CALL để thực hiện kiểm tra so sánh. Nếu không bằng, nó sẽ đưa ra lỗi hoặc đóng chương trình tuy theo theo trường hợp.

Đặt một BP bên dưới hàm gets_s() để có thể dừng sau khi attach process:

Chạy script, attach process vào IDA, ta sẽ dừng tại BP đã đặt:

Lúc này biến CANARY đã bị ghi đè và có giá trị là 0x41414141 mà ta truyền vào bằng script:

Các bạn có thể tính toán để có được giá trị của của CANARY tại thời điểm thực thi này theo công thức CANARY = _security_cookie xor EBP, tuy nhiên như đã nói, giá trị này sẽ thay đổi mỗi lần bạn chạy chương trình. Ví dụ:

CANARY = 0x54A7D493 xor 0x008FF91C

Nếu chạy lại lần nữa và tính toán thì thấy CANARY sẽ thay đổi. Chính vì vậy, chúng ta không thể được dự đoán chính xác giá trị để có thể ghi đè vào giá trị mà chương trình đã tính toán khi thực thi.

Ta trace tiếp code từ bp đang dừng ở trên:

Các bạn sẽ thấy chương trình lấy ra 0x41414141 và XOR với EBP, kết quả có được lưu vào thanh ghi ECX để đem kiểm tra tại hàm __security_check_cookie(x):

Trace vào trong hàm để xem code tại đó thực hiện nhiệm vụ gì:

Ta thấy nó so sánh giá trị tại ECX với _security_cookie đã lưu, và vì ở đây hai giá trị này không bằng nhau nên chương trình sẽ rẽ sang nhánh failure. Ngược lại, nếu bằng nhau ta sẽ tới nhánh có lệnh RET và tiếp tục thực hiện cho tới Return Address vì địa chỉ này không bị ghi đè.

Ok, do chúng ta đã thực hiện overflow do đó toàn bộ các giá trị ( CANARY và RET) đã bị ghi đè, nên hiển nhiên chúng ta sẽ tiếp tục tới nhánh màu đỏ trên hình. Cứ tiếp tục trace code để xem thế nào:

Chúng ta sẽ không phân tích toàn bộ đoạn code trên, nhưng nếu ta tiếp tục RUN chương tình, nó sẽ bị treo hoặc đóng luôn.

Vậy, câu hỏi đặt ra là làm thế nào để vượt qua cơ chế này? Cách đầu tiên đã được áp dụng dù rằng có một số hạn chế nhưng vẫn hoạt động được là sử dụng SEH ( https://msdn.microsoft.com/en-us/library/swezty51.aspx).

Ngoại lệ ( exception) là sự kiện xảy ra trong quá trình thực thi của một chương trình, và yêu cầu thực thi mã bên ngoài luồng thực thi bình thường của chương trình đó. Có hai kiểu ngoại lệ là phần cứng ( hardware exception) và phần mềm ( software exception). Ngoại lệ phần cứng được sinh ra bởi CPU và thường xảy ra do câu lệnh như: thực hiện phép chia cho 0 hoặc cố gắng truy cập vào bộ nhớ không hợp lệ. Ngoại lệ phần mềm xảy ra bởi OS, ví dụ như hệ thống có thể phát hiện nếu một tham số không hợp lệ được sử dụng.

SEH ( Structural Exception Handling) là một cơ chế trong Windows, sử dụng một cấu trúc dữ liệu được gọi là “ danh sách liên kết”, chứa một chuỗi các bản ghi dữ liệu. Khi một ngoại lệ được kích hoạt, hệ điều hành sẽ duyệt tới danh sách này. Trình xử lý ngoại lệ ( exception handler) có thể, một là đánh giá xem nó khả năng để xử lý ngoại lệ không hoặc là thông báo cho hệ điều hành tiếp tục kiểm tra danh sách và đánh giá các hàm ngoại lệ khác. Windows lưu “ danh sách liên kết “ trên bộ nhớ Stack của chương trình cùng với con trỏ đến nơi chương trình nên nhảy tới khi nó phát hiện một ngoại lệ.

Và để kiểm soát ngoại lệ, trình xử lý ngoại lệ cần có hai phần tử:

  • (1) một con trỏ tới “Bản ghi đăng ký ngoại lệ (Exception Registration Record)” hiện tại (SEH)
  • (2) một con trỏ tới “Bản ghi đăng ký ngoại lệ tiếp theo (Next Exception Registration Record)” (nSEH).

Theo quy định, bộ nhớ Stack của Windows sẽ hướng về phía địa chỉ thấp hơn ( grows downward) cho nên chúng ta sẽ thấy rằng trình tự của những bản ghi này được sắp xếp theo thứ tự đảo ngược [nSEH] -> [SEH].

Với những người có nhiều kinh nghiệm trong lập trình thì sẽ biết có các cấu trúc xử lý ngoại lệ như TRY-EXCEPT hoặc TRY-CATCH, trong đó mã lệnh đặt trong khối được thực thi và nếu có bất kỳ ngoại lệ nào xảy ra thì nhảy tới khối .

Như tôi đã đề cập ở trên, có các cấu trúc được gọi là _EXCEPTION_REGISTRATION_RECORD, bao gồm cấu trúc bên trong với hai trường; trường thứ nhất — là một con trỏ tới một _EXCEPTION_REGISTRATION_RECORD khác và trường thứ hai — có kiểu PEXCEPTION ROUTINE.

Vì vậy, cách hành xử khi một ngoại lệ xảy ra, OS sẽ đi từ đầu danh sách và duyệt _EXCEPTION_REGISTRATION_RECORD để kiểm tra xem nó có thể xử lý lỗi hay không, nếu không sẽ chuyển sang bản ghi tiếp theo và cứ tiếp tục như thế thông qua . Exception handler cuối cùng được đặt tại 0xFFFFFFFF nhằm kết thúc quá trình xử lý ngoại lệ và gửi một thông báo “Access Violation Error in the application”. Hình minh họa dưới đây sẽ cung cấp cho bạn cái nhìn dễ hiểu hơn:

Nguồn: https://www.vulnerableghost.com/2016/01/vanila-buffer-overflow-attack-on-windows.html

Các bạn nào đã làm việc nhiều với OllyDbg sẽ thấy được hình ảnh quen thuộc sau:

Quay trở lại với file CANARY_sin_DEP.exe của chúng ta, thực hiện attach lại process và kiểm tra thông tin liên quan tới SEH của nó. Tại IDA, truy cập Debugger > Debugger windows > SEH list ta sẽ thấy SEH của từng cấu trúc trong Stack, tiếc là nó không hiển thị địa chỉ của Stack ở đâu, nhưng ta có thể tạo được danh sách liên kết một cách dễ dàng.

Hãy xem các tham chiếu tới địa chỉ đầu tiên:

Nhấn X để tìm các tham chiếu tới handler này.

Thông tin mà xrefs cung cấp không có giá trị nhiều vì ta biết rằng các SEH được sắp xếp trên bộ nhớ stack. Ở đây nó không hiển thị thông tin tham chiếu vì stack không phải là code section, do đó ta phải tìm kiếm nó thông qua tính năng search immediate value. Thực hiện tìm kiếm toàn bộ nhớ với địa chỉ SEH trên:

Kết quả có được như hình:

Nhấn đúp chuột vào địa chỉ Stack ta sẽ tới vị trí chứa địa chỉ của handler, sau đó nhấn phím D để chuyển đối giá trị sang dword. Kết quả có được như sau:

Như vậy, địa chỉ ở bên trên sẽ là trỏ tới Next SEH, tiếp tục nhấn phím D:

Quan sát trên hình, Next SEH trong trường hợp của tôi là tại 0xCFFEE0. Đi tới địa chỉ này:

Tiếp tục, ta sẽ tới SEH cuối cùng ( 0xFFFFFFFF):

Vậy ở đây, chúng ta có ba cấu trúc liên quan SEH, do đó ta phải cố gắng lấp đầy ngăn xếp để sinh ra một ngoại lệ khi không được ghi vượt quá không gian của stack section. Sửa script và chạy lại để script thực hiện phá vỡ toàn bộ stack:

Chạy script để kiểm tra, ta sẽ nhận được thông báo:

Nhấn OK để tiếp tục sẽ dừng lại tại lệnh sau:

Chương trình đang cố ghi dữ liệu tại AL ( 0x41) vào vùng nhớ trỏ bởi ESI ( 0x00500000). Follow theo địa chỉ này tại ESI sẽ tới đây:

Như trên hình, ta thấy vị trí kết thúc của stack tại 0x004FFFFF và chương trình đang cố gắng ghi giá trị vượt ra ngoài không gian của Stack. Chương trình bị crash do ESI — trong trường hợp của tôi đang trỏ tới 0x00500000, là vùng nhớ không thuộc stack.

Thử kiểm tra danh sách các SEH.

Với kết quả trên, SEH đã bị ghi đè rồi. Do đó, nếu tôi cho tiếp tục chương trình thì có thể sẽ nhảy đến 0x41414141, và rõ ràng điều này có một số hạn chế. Ta cần tìm một mô-đun không sử dụng ASLR để phục vụ việc nhảy tới nó, và cũng chỉ có thể nhảy tới mô-đun mà có SafeSEH OFF ( là một tùy chọn khi cấu hình linker của Visual Studio, nó bảo vệ exceptional handler chain bằng cách bổ sung thêm một bảng xử lý ngoại lệ an toàn (safe exceptional handler table), bảng này chứa địa chỉ có thể được sử dụng làm hàm xử lý ngoại lệ. Nếu một ngoại lệ xảy ra hoặc luồng thực hiện của hander chain thay đổi và địa chỉ của SEH handler không có trong bảng này, ứng dụng sẽ tự kết thúc thực hiện, qua đó sẽ ngăn chặn việc thực thi code thông qua các khai thác dựa trên SEH).

Lấy thông tin về danh sách các module đã được nạp với hỗ trợ của idasploiter.

Như trên hình, chỉ có Mypepe.dll là không có ASLRSafeSEHOFF. Do đó, ta lựa chọn module này để nhảy đến đó.

Hãy tìm vị trí của SEH trong Stack:

Tới địa chỉ 0x74CCD3F0, nhấn chuột phải và chọn Create function:

Kiểm tra thử xem có xrefs nào tại Stack không, nếu không thì thực hiện tìm kiếm tương tự như đã làm ở trên.

Không có, vậy là phải tìm rồi. Chọn Search > Inmediate Value:

Kết quả tôi có được như sau:

Như vậy, có hai nơi trong ngăn xếp sử dụng nó, tiến hành kiểm tra:

Địa chỉ trên hình trỏ tới Next SEH, tuy nhiên tại đó đã bị ghi đè bởi dữ liệu do ta truyền vào:

Bây giờ, chúng ta sẽ tính toán khoảng cách từ đầu của buffer cho đến giá trị trước địa chỉ trỏ tới Next SEH, trên máy của tôi là 0x4FFC1B.

Thanh ghi ESI trỏ tới cuối buffer thì thanh ghi EDI sẽ trỏ tới đầu của buffer. Tôi tới địa chỉ lưu tại thanh ghi EDI:

Tại địa chỉ đó, nhấn Alt+L để đánh dấu điểm đầu tiên ( Begin selection):

Để lựa chọn thêm các giá trị tiếp theo ta cuộn chuột đồng thời giữ phím SHIFT, nhưng làm vậy sẽ rất lâu vì khoảng cách giữa 2 địa chỉ ( EDI vs ESI) là khá xa nhau, để nhanh hơn ta nhấn phím G và nhập vào địa chỉ cuối cùng địa chỉ cuối cùng 0x4FFC1B. Khi tới địa chỉ, trước khi nhấn chuột, nhấn giữ SHIFT để đảm bảo toàn bộ các bytes nằm nữa EDI và ESI được lựa chọn. Tương tự như hình dưới đây:

Sau đó vào Edit > Array, IDA sẽ tính toán cho ta độ dài của buffer là 844 ở hệ thập phân.

Như vậy, để ghi đè lên SEH, chúng ta cần truyền input cho chương trình tương tự như sau:

fruit =844 * “A” + NEXT_SEH + SEH + 6000 * “B”

Với đoạn input trên, ta sẽ có thể ghi đè lên NEXT SEHSEH. Việc giá trị nào sẽ được ghi đè vào thì ta sẽ xem xét sau. Sau đó, tôi vẫn tiếp tục gửi dữ liệu để làm crash chương trình và đè lên toàn bộ Stack ( 6000 * “B”). Tôi chỉnh script như sau:

Ta kiểm tra xem liệu tính toán này có hợp lý hay không và nó có ghi đè được lên SEH với giá trị 0x46474849 không? Chạy thử script trên:

Vậy là đã ghi đè được đúng như ý muốn. Chương trình vẫn tiếp tục bị crash vì Stack đã bị ghi đè hoàn toàn, nhưng ta đã có điều mình cần và chứng tỏ việc tính toán ở trên là chuẩn xác. Tôi nhấn F9 để cho chương trình tiếp tục thực thi, ngay lập tực SEH handler sẽ xử lý ngoại lệ và nhảy tới 0x46474849 đúng như mong đợi:

OK việc tính toán thì đã ổn nhưng bây giờ chúng ta sẽ nhảy tới đâu? Theo thông tin tại https://msdn.microsoft.com/en-us/library/b6sf5kbd.aspx thì khi gọi handler, theo thiết kế trên Stack, đầu tiên sẽ là địa chỉ trở về và sau đó ở vị trí thứ hai của cấu trúc này ( thứ ba trên stack) sẽ là EstablisherFrame:

Sau khi ngoại lệ xảy ra, hệ thống sẽ cố gắng xử lý ngoại lệ đã xảy ra này, nó thiết lập cấu trúc handler EXCEPTION_DISPOSITION trên stack. Giá trị EstablisherFrame của cấu trúc này trỏ tới bản ghi handler đầu tiên. Bản ghi handler đầu tiên chứa địa chỉ của và địa chỉ của SEH Handler. Hệ thống đặt ngăn xếp theo cách ESP trỏ tới đầu của cấu trúc EXCEPTION_DISPOSITION.

Như kết quả trên hình, ta có EstablisherFrame hiện đang trỏ tới Next SEH, là nơi có SEH handler tiếp theo sẽ xử lý ngoại lệ nếu handler hiện tại ( 0x46474849) không xử lý được.

Do target này không có DEP nên nếu chúng ta nhảy tới một gadget có dạng POP r32, POP r32, RET thì sẽ nhảy tới Next SEH. Vì sau hai lệnh POP ta sẽ lấy ra 2 giá trị đầu tiên và nhảy tới giá trị thứ ba bằng lệnh RET. Thực hiện tìm kiếm gadget trên trong Mypepe.dll, không quan trọng thanh ghi nào được sử dụng trong các lệnh. Kết quả trên máy tôi như sau:

Thay địa chỉ tìm được ( 78001075 pop esi # pop ebp # retn) vào SEH handler để chương trình nhảy tới đó khi xử lý ngoại lệ:

Tiếp tục thực hiện lại script để kiểm tra SEH:

Chuyển tới địa chỉ 0x78001075 và đặt Breakpoint tại đó:

Nhấn F9 để cho chương trình tiếp tục thực thi và và chấp nhận ngoại lệ (Yes (pass to app)):

Chương trình sẽ chuyển tới exception handler tại 0x78001075 để thực hiện xử lý:

Nhấn F7 để trace code, ta sẽ đi tới địa chỉ để thực hiện Next SEH:

Các mã hexa tại đây đã được IDA tự động chuyển đổi sang code, để dễ quan sát hơn bạn có thể xem tại cửa sổ Hex Dump:

Như trên hình, ta thấy đây là Next SEHSEH handler, tiếp theo đó là shellcode của chúng ta. Bây giờ những gì chúng ta cần thực hiện là thay thế Next SEH bằng opcode EB 06 90 90 (lệnh nhảy qua 6 bytes) để nó nhảy qua SEH và thực hiện shellcode mà ta mong muốn. Do vậy, ta sẽ đặt shellcode vào trước vị trí bắt đầu của các chữ “ B “.

Chạy lại script và nhận kết quả sau một hồi “try hard”:

Tuy nhiên, có một điều không hay xảy ra là nó sẽ liên tục sinh ra rất nhiều instance của calculator, ta cảm giác như kiểu gặp vòng lặp vô tận, đó là bởi vì mỗi lần chương trình bị crash, nó lại quay lại SEH để xử lý ngoại lệ và do vậy lại gọi calculator. Để sửa lỗi này, ta sửa lại shellcode bằng cách sau khi hiện thành công sẽ thoát luôn và chương trình sẽ đóng lại.

Phân tích Mypepe.dll ta có được địa chỉ gọi tới hàm API ExitProcess. Do Mypepe.dll không có ASLR nên địa chỉ này sẽ không bị thay đổi sau mỗi lần load:

Bổ sung thêm vào cuối của shellcode lệnh push offset loc_780039AB # retn, tương ứng với opcode là: \x68\xAB\x39\x00\x78\xC3

Kiểm tra lại script:

Quá ổn, script chỉ gọi duy nhất một trình calculator mà thôi! Trong phần tiếp theo, tôi và các bạn sẽ tìm hiểu cách thực hiện ROP khi khai thác một SEH là như thế nào.

Bên cạnh SafeSEH thì có một cơ chế bảo vệ đặc biệt khác gọi là SEHOP (SEH Overwrite Protection) ( https://support.microsoft.com/en-us/help/956607/how-to-enable-structured-exception-handling-overwrite-protection-sehop), cơ chế này được kích hoạt mặc định trên Windows Server 2008 R2 trở đi, trong một số chương trình, trên trình duyệt hiện đại hoặc một số services. Do khi ta ghi đè lên một exception handler frame, SEH chain sẽ bị phá vỡ và sẽ không thể tới được FinalExceptionHandler. Cơ chế bảo vệ SEHOP sẽ kiểm tra tính toàn vẹn của SEH chain trước khi nhảy và xác minh có thể tới được FinalExceptionHandler function trong ntdll.dll hay không, nếu kết hợp với ASLR thì sẽ vô cùng khó khăn và gần như không thể khai thác.

Hẹn gặp lại các bạn ở phần 38!

Xin gửi lời cảm ơn chân thành tới thầy Ricardo Narvaja!

m4n0w4r

Nguồn: https://hex-rays.com/ida-pro/

Ủng hộ tác giả

Nếu bạn cảm thấy những gì tôi chia sẻ trong bài viết là hữu ích, bạn có thể ủng hộ bằng “bỉm sữa” hoặc “quân huy” qua địa chỉ:

Tên tài khoản: TRAN TRUNG KIEN
Số tài khoản: 0021001560963
Ngân hàng: Vietcombank

--

--