REVERSING WITH IDA FROM SCRATCH (P35)

m4n0w4r
tradahacking
Published in
18 min readMay 21, 2021

--

Lời tựa: Con người là loại động vật cấp cao và cũng đầy lòng trắc ẩn. Tôi không muốn một ngày nào đó, có người còm vào blog của tôi để hỏi đại loại: “Tiền anh được donate anh đã làm gì…? “. Kiểu như bài này: https://vtc.vn/hoai-linh-chua-chuyen-13-ty-dong-keu-goi-ung-ho-mien-trung-ar613537.html. Sáng nay đọc bài báo này https://vtc.vn/bo-la-benh-nhan-covid-19-vua-qua-doi-hai-con-nho-tu-cham-nhau-trong-khu-cach-ly-ar613273.html, tôi quyết định dùng số tiền mà những bạn đã donate cho tôi để ủng hộ hai cháu nhỏ.

Tiếp tục với phần 35, chúng ta sẽ làm việc với phiên bản có DEP. Như các bạn đã biết, code của ví dụ này tương tự như ở NO_DEP. Vậy điều gì sẽ xảy ra nếu tôi sử dụng lại script đã tạo cho file không có DEP ở phần trước?

Thay đổi lại tên file trong script để trỏ tới DEP.exe.

Load DEP.exe vào IDA, sau đó đặt một breakpoint trong hàm saluda() để xem những gì xảy ra ngay sau khi chương trình thực hiện hàm gets_s():

Tiếp theo mở cmd, chạy script và attach tiến trình vào IDA ( hiện đã thiết lập BP như trên). Lúc này, tiến trình đang đợi để nhận dữ liệu truyền vào:

Sau khi attach xong, quay trở lại màn hình cmd để nhấn Enter:

Trở lại IDA và nhấn F9 để thực thi, IDA sẽ dừng lại tai BP đã đặt:

Nhấn F8 để trace code cho tới khi dừng lại tại lệnh RET:

Quan sát cửa sổ Stack, ta thấy địa chỉ trở về lúc này đã bị ghi đè bởi địa chỉ của ROP gadget, do đó khi thực hiện lệnh RET chương trình sẽ nhảy tới địa chỉ 0x7800f7c1 — nơi chứa cặp lệnh PUSH ESP — RET. Như vậy, vẫn chưa có vấn đề gì ở đây vì địa chỉ này nằm ở code section của file dll (Mypepe.dll) và section này luôn có quyền thực thi. Tiếp tục nhấn F7 để trace qua lệnh RET, ta sẽ tới đây:

Tiếp tục trace tới lệnh RET:

Lệnh PUSH đã đẩy giá trị của thanh ghi ESP vào Stack, do đó giá trị này trở thành địa trỉ trở về, lệnh RET lúc này sẽ thực hiện lấy địa chỉ tại đỉnh Stack và nhảy tới để thực thi mã lệnh tại 0x4FF870tại đây chứa shellcode mà chúng ta đã nhồi lên Stack. Trong ví dụ với NO_DEP ở phần trước, khi nhảy được tới thì cũng thực thi được shellcode này. Nhưng với ví dụ này thì điều gì sẽ xảy ra? Ta nhấn tiếp F7 để kiểm tra:

Như vậy, ở NO_DEP thì đoạn shellcode trên hình được thực hiện bình thường mà không có vấn đề, nhưng ở đây nó không cho phép chúng ta làm điều đó bởi vì DEP không cấp quyền thực thi code tại Stack ( Heap, v..v…), ta chỉ còn quyền đọc và ghi mà thôi.

Vậy chúng ta có thể làm gì?

Các bạn thấy rằng, ta có thể nhảy tới code của một DLL như đã làm và thực thi các lệnh trong code của DLL là PUSH ESP-RET; đó là ý tưởng của ROP. Đặt các gadgets là các khối code nhỏ cùng nhau và kết thúc bởi lệnh RET, để cuối cùng cho phép thực thi tại Stack, Heap hoặc bất kì vùng nào cho phép thực thi.

Ví dụ nếu tôi muốn gán một giá trị vào EAX, thay vì sử dụng PUSH ESP-RET, tôi sẽ tới đoạn code có lệnh POP EAX-RET. Thử tìm POP EAX-RET trong mypepe.dll bằng idasploiter vì dll này không có ASLR.

Nhấn Ctrl+F để tìm kiếm POP EAX — RET, kết quả có được như sau:

Ta có được một gadget tại địa chỉ 0x78003d08 như trên hình. Sửa lại script như sau:

Trong script trên, tôi thay thế việc nhảy tới PUSH ESP — RET bằng một mới, bắt đầu bằng lệnh POP EAX để gán giá trị bên dưới là 0x41424344 vào thanh ghi EAX và sau thực hiện lệnh RET ( các bạn lưu ý rằng tất cả hoặc gần như tất cả các gadgets đều kết thúc bằng lệnh RET). Như vậy, chương trình sẽ nhảy tới gadget tiếp theo để thực thi lệnh, ở đây là 0xCCCCCCCC. Ta sẽ thiết lập lại gadget này sau, trước tiên cứ thử thực hiện script đã để xem điều gì xảy ra. Làm tương tự như trên và attach vào IDA để trace code.

Ta có kết quả như hình dưới đây:

Khi đến lệnh RET, chương trình sẽ nhảy tới địa chỉ chứa đoạn ROP của chúng ta là POP EAX-RET. Khi thực hiện POP EAX thì giá trị 0x41424344 lúc này đang nằm trên đỉnh của Stack sẽ được chuyển vào thanh ghi EAX, đồng thời giá trị 0xCCCCCCCC sẽ nằm trên đỉnh Stack. Khi thực hiện RET ta sẽ nhảy tới địa chỉ của gadget tiếp theo này. Tiến hành trace với F7:

Ta đang dừng tại gadget đầu tiên là POP EAX-RET, quan sát trên hình ta biết được lệnh POP lấy giá trị tại đỉnh Stack và gán vào EAX. Nhấn F7 để trace qua lệnh và kiểm tra:

Như trên hình, ta thấy EAX đã được gán giá trị là 0x41424344 và ta đang dừng tại lệnh RET để nhảy tới gadget tiếp theo. Trong trường hợp này đó chính là 0xCCCCCCCC, giá trị gadget này không tồn tại, nhưng chúng ta sẽ tìm các gadget phù hợp để thay thế.

Nhắc lại một lần nữa, đó chính là lý do kĩ thuật này được gọi là ROP ( Return Oriented Programming), đặt lần lượt các gadgets khác nhau và cạnh nhau để thực hiện những mục đích mà chúng ta muốn. Như vậy, ta đang thực thi các mã lệnh không phải là các lệnh do chúng ta đưa vào, ta chỉ đưa ra một danh sách các địa chỉ mã để phục vụ mục tiêu cuối cùng của mình.

ROP cũng có cách tự động và cách thủ công (manual). Đầu tiên chúng ta sẽ thực hiện làm thủ công trước, sau đó sẽ tìm hiểu về cách thức làm tự động là như thế nào.

Phương pháp chung là sắp xếp các giá trị nhất định trong các thanh ghi và sau đó nhảy tới một gadget PUSHAD — RET. Chúng ta thấy rằng nó sẽ sắp xếp lại mọi thứ, mặc dù có rất nhiều cách để thực hiện ROP, tuy nhiên đây là phương pháp phổ biến nhất.

Điều đầu tiên ta cần quyết định hàm API nào sẽ được sử dụng để loại bỏ việc bảo vệ Stack ( unprotect the stack); trong trường hợp này, có thể là API VirtualAlloc hoặc VirtualProtect, đây là các API thường hay được sử dụng nhất mặc dù còn có những hàm khác nữa.

Mở danh sách các module đã được nạp thông qua Debugger-Debugger windows-Module list, tại đó ta nhấn chuột phải vào Mypepe.dll và phân tích nó, sau đó thực hiện load debug symbols của module này nếu nó có.

Vì chúng ta không có symbols của Mypepe, nên mặc dù code phân tích được, nhưng nó không hiển thị thêm bất kỳ thông tin nào như các hàm được import được sử dụng bởi Mypepe.

Như các bạn đã biết khi đọc các phần liên quan đến Unpacking, để call tới API thường sẽ thông qua offset (off_). Do đó, đứng tại code của Mypepe, ta thực hiện tìm kiếm theo chuỗi “off_”:

Kết quả có được như hình dưới, ta không cần phải tìm kiếm toàn bộ, chỉ cần một lần duy nhất thôi:

Lệnh call tại địa chỉ 0x7800CDD1 là lệnh nhảy điển hình tới hàm được imported, vì tiền tố off_ ( đừng nhầm lẫn với OFFSET là địa chỉ) trong trường hợp này có nghĩa là nội dung của địa chỉ chính là một con trỏ tới hàm API. Ta đến địa chỉ đó như trong hình minh họa dưới đây:

Quan sát một chút ở bên dưới ta sẽ thấy được hàm VirtualAlloc:

Như trên hình, ta đã có được VA (Virtual Address) chứa con trỏ của hàm VirtualAlloc trong Mypepe tại 0x7802E0B0. Do vậy, chúng ta sẽ chuẩn bị một ROP cho API này. Địa chỉ 0x7802e0b0 này chính là đầu vào của IAT, từ bây giờ ta sẽ gọi nói là VA.

Để tạo ROP cho hàm VirtualAlloc thì ý tưởng là bố trí các giá trị cần sử dụng vào các thanh ghi và sau đó sử dụng PUSHAD — RET để đẩy các giá trị vào ngăn xếp, lúc đó các giá trị này sẽ được bố trí như là các tham số của hàm VirtualAlloc. Các bạn đừng nghĩ nó là một kĩ thuật gì cao siêu mà đơn giản chỉ là một cách sắp xếp khéo léo mà thôi lolz.

Lệnh PUSHAD thì như các bạn đã biết, nó sẽ lưu giá trị của các thanh ghi lên stack theo thứ tự sau: EAX, ECX, EDX, EBX, original ESP, EBP, ESI, và EDI. Trong khi đó, hàm VirtualAlloc nhận các tham số như sau:

Khi truyền tham số cho hàm sẽ theo chiều từ phải quá trái, có nghĩa là:

push flProtect
push flAllocationType
push dwSize
push lpAddress
call VirtualAlloc

Do vậy, ý tưởng chúng ta sẽ bố trí giá trị của các thanh ghi như sau:

Ta cần tìm cách gán giá trị 0x90909090 vào thanh EAX thông qua gadget POP EAX-RET, nhưng giá trị này cần được gán sau bởi vì ta cần thanh ghi EAX để thực hiên một số nhiệm vụ khác trước.

Trong khi đó thanh ghi ESI nên trỏ tới địa chỉ VirtualAlloc và thanh ghi EDI nên chứa địa chỉ của một ROP RET. Lý do là vì khi ta thực hiện PUSHAD ( của ROP PUSHAD — RET) thì thanh ghi EDI sẽ nằm ở đỉnh của Stack, thanh ghi ESI nằm bên dưới và trỏ vào hàm VirtualAlloc. Khi lệnh RET ( của PUSHAD-RET) thực thiện sẽ nhảy tới EDI để thực hiện RET tại EDI, RET ( tại EDI thực hiện) sẽ nhảy tới ESI ( đang trỏ tới hàm VirtualAlloc); Lúc này, các tham số của hàm VirtualAlloc đã được bố trí trên Stack nhờ lệnh PUSHAD trước đó. Như vậy, dựa vào thông tin địa chỉ của API mà ta đã tìm ở trên, ta sẽ tìm cách đẩy nó vào ESI.

Cách nhanh nhất là ta tìm một ROP dạng MOV ESI, [register] — RET (vì nếu ta gán được giá trị 0x7802E0B0 vào một thanh ghi bất kì, thì qua lệnh mov ta sẽ lấy được pointer tới API VirtualAlloc). Ta thử tìm kiếm xem có gadgets này trong Mypepe.dll không?

Rất tiếc là không có:

Nhưng bù lại ta lại tìm được một gadget khác phù hợp với mục tiêu:

Với ROP có được ở trên, trước đó ta sẽ tìm cách gán địa chỉ của hàm bên dưới hàm VirtualAlloc vào EAX; cụ thể ở đây là địa chỉ off_7802E0B4dd offset ntdll_RtlReAllocateHeap, để khi thực hiện lệnh mov eax, [eax-4] ta sẽ có địa chỉ của VirtualAlloc tại EAX. Sau đó, sử dụng một gadget khác để qua đó gán giá trị của EAX vào ESI. Ta sẽ sắp xếp các gadgets để đạt được mục đích này.

Theo phân tích ở trên, cộng 4 vào địa chỉ chứa con trỏ tới VirtualAlloc và gán vào EAX bằng gadget POP EAX-RET mà chúng ta đã có. Ta sửa lại script như sau:

Với việc sửa script như trên, tôi sẽ có được giá trị của (IAT location + 4) được gán vào EAX. Tiếp theo sẽ là gadget lấy ra địa chỉ chính xác của hàm VirtualAlloc:

780022DE mov eax, [eax-4] # retn Mypepe.dll 2 0 mem-to-reg eax

Sau khi sắp xếp các gadgets như trên, chúng ta hãy kiểm tra hoạt động của script để xem nó có thực hiện đúng như những gì chúng ta đã tưởng tượng không và cuối cùng thanh ghi EAX có lấy được địa chỉ của hàm VirtualAlloc hay không?

Một điều khá bất tiện mà các bạn sẽ thấy là idasploiter chỉ hoạt trong chế độ gỡ lỗi, do đó nó sẽ khởi động lại mỗi khi bạn debug chương trình.

Theo dõi các ROP mà ta đã sắp xếp bằng cách trace với F7:

Lệnh POP EAX sẽ gán giá trị tại đỉnh Stack vào EAX:

Lệnh ret sẽ thực hiện nhảy tới gadget thứ hai, tiếp tục nhấn F7:

Như trên hình, các bạn thấy lệnh mov sẽ lấy nội dung tại địa chỉ [eax-4] để gán vào eax; đó chính là địa chỉ của hàm API VirtualAlloc. Tiếp tục nhấn F7 để kiểm tra:

Kết quả như bạn thấy trên hình, chúng ta đã đạt được mục tiêu là lấy được địa chỉ API của hàm VirtualAlloc và gán vào thanh ghi EAX. Với sự bố trí các gadgets như trên thì nó sẽ hoạt động trên bất kỳ máy nào. Tiếp theo, ta sẽ tìm các gadgets thực hiện lấy giá trị của EAX gán vào ESI như đã phân tích lý do ở trên, vì ta sẽ sử dụng ESI để gọi thực thi hàm VirtualAlloc với các tham số đã được sắp xếp trên Stack.

Do không có gadget nào thực hiện MOV ESI, EAX nên chúng ta phải hình dung và suy nghĩ một cách khác. Mặc dù thông thường các gadgets sẽ kết thúc với lệnh RET, tuy nhiên thì không nhất thiết phải lúc nào cũng phải như vậy. Ta sẽ tìm kiếm một cách khác để không theo cách thông thường, nhưng mục tiêu là kiểm soát được việc gán cho thanh ghi ESI địa chỉ của hàm VirtualAlloc. Tôi có một gadget như sau:

Với gadget này, nếu tôi đặt giá trị của EAX lên đỉnh Stack thông qua PUSH EAX-CALL ESI và chuẩn bị thanh ghi ESI để có một POP ESI-RET, tôi có thể chuyển giá trị của EAX sang ESI bằng cách sử dụng Stack. Hãy xem tiếp một gadget nữa:

Thử thêm gadget POP ESI-RETPUSH EAX-CALL ESI vào script để xem thế nào:

Kiểm tra script vừa chỉnh sửa, sau khi trace qua gadget mov eax, [eax-4] # retn ta sẽ tới đây:

Các bạn thấy lệnh POP ESI lúc này sẽ gán lại địa chỉ của cùng gadget vào thanh ghi ESI ( trỏ tới POP ESI-RET) để sau đó sử dụng gadget tiếp theo nhằm lấy lại quyền kiểm soát.

Lệnh PUSH sẽ đẩy địa chỉ của VA lên Stack, lệnh CALL ESI thực hiện và lại nhảy tới gadget POP ESI-RET một lần nữa vì địa chỉ của gadget này đã được gán vào ESI trước đó :(

Trace tới đây, tôi nhận thấy rằng các gadgets trên đã không thành công, vì khi thực hiện lệnh POP ESI như trên hình, nó sẽ gán địa chỉ trở về ( bên dưới lệnh CALL ESI) vào ESI còn địa chỉ VA nằm ngay bên dưới. Do vậy, thay vì sử dụng POP ESI-RET, ta cần phải POP tạm địa chỉ trở về vào một thanh ghi khác, rồi POP tiếp địa chỉ của VA và ESI … cho nên tìm một gadget có dạng POP XXX, POP ESI — RET sẽ là hợp lý nhất trong trường hợp này. Tìm kiếm gadget mong muốn, ta có được kết quả như sau:

Với thông tin về gadget mới, ta sửa lại script để chỉnh lại giá trị sẽ gán vào thanh ghi ESI sau khi thực hiện lệnh POP ESI:

Kiểm tra lại script vừa sửa, ta dừng lại trước lệnh call esi:

Nhấn F7 để trace vào lệnh call và trace tới lệnh ret, ta có được kết quả đúng như mong muốn:

Vậy là ta đã đạt được mục tiêu, ESI lúc này đang có VA của API VirtualAlloc, gadget tiếp theo sẽ nhảy tới là 0xCCCCCCCC.

Nhiệm vụ tiếp theo sẽ dễ dàng hơn, ta cần gán cho EBP một địa chỉ để làm sao có thể JMP ESP. Có thể sử dụng gadget CALL ESP hoặc PUSH ESP-RET. Chúng ta đã có một PUSH ESP-RET (ở phần trước), chỉ cần tìm POP EBP-RET để gán giá trị cho EBP.

Tiếp tục sửa script như sau:

Với gadget như trên, ta sẽ gán cho thanh ghi EBP trỏ tới PUSH ESP-RET. Ta tiếp tục:

Như đã phân tích ở đầu bài viết, khi thực hiện lệnh PUSHAD, thanh ghi EDI sẽ nằm ở đỉnh của Stack. Do đó, EDI = ROP NOP có nghĩa là nó phải trỏ đến RET là NOP trong lập trình ROP. Tìm kiếm một POP EDI-RET để gán giá trị cho EDI. Địa chỉ lệnh của RET để gán vào EDI có thể là một địa chỉ bất kì và sửa lại script:

Tiếp theo chúng ta cần thiết lập các tham số sử dụng cho hàm VirtualAlloc:

Ta cần gán 4 giá trị sau: 0x90909090 ( nop bytes) vào EAX, 0x40 (PAGE_EXECUTE_READWRITE) vào ECX, 0x1000 ( MEM_COMMIT) vào EDX0x1 ( dwSize) vào EBX. Tìm kiếm các gaggets POP tương ứng để thực hiện việc gán các giá trị này:

Bổ sung các gadgets này vào ROP trong script:

Mọi thứ đã sẵn sàng, bây giờ chúng cần tìm gadget cuối cùng là PUSHAD-RET để sắp xếp toàn bộ thông tin lên Stack:

Tôi có được kết quả một gadget như trên hình, lệnh ADD AL, XX không làm ảnh hưởng tới mục đích của chúng ta vì câu lệnh PUSHAD trước đó đã thực hiện được ý đồ là đẩy EAX=0x90909090 lên Stack rồi, do đó ta sẽ sử dụng gadget này và bổ sung vào script:

Toàn bộ rop được bố trí như trên hình, tiến hành kiểm tra script và trace cho tới khi tới lệnh call đến hàm VirtualAlloc. Ta đang dừng tại lệnh PUSHAD:

Hehe, mọi thứ có vẻ tốt và đang đi đúng hướng, nhấn F7 để trace qua lệnh PUSHAD, giá trị của các thanh ghi được đẩy lên Stack theo đúng thứ tự:

Giá trị của thanh ghi EDI lúc này đang nằm tại đỉnh của Stack và địa chỉ này trỏ tới lệnh 780015C9 ret. Khi trace qua lệnh ret78009794 thì địa chỉ 780015C9 sẽ được lấy ra và gán cho EIP để thực hiện câu lệnh tiếp theo. Do 780015C9 tiếp tục là lệnh ret nên giá trị tiếp theo là 777666A0 (kernel32_VirtualAlloc) được gán vào cho EIP để thực hiện lệnh tại đây:

Chúng ta đã tới được hàm VirtualAlloc để thực hiện nhiệm vụ cho phép vùng nhớ trên stack hiện đang chứa shellcode có khả năng thực thi được. Các tham số của hàm đã được truyền vào stack theo chiều từ phải qua trái nhờ sự hỗ trợ của lệnh PUSHAD, và tại đỉnh của Stack chính là địa chỉ trở về khi mà chương trình thực hiện xong hàm API VirtualAlloc, địa chỉ này chính là nơi chứa lệnh PUSH ESP-RET.

Bên dưới địa chỉ trỏ tới shellcode chính là ( kích thước của vùng nhớ, tính theo bytes). Ở đây, ta thiết lập giá trị là 0x1 — block tối thiểu cần được unprotect, tiếp theo là MEM_COMMIT ( 0x00001000) và PAGE_EXECUTE_READWRITE ( 0x40). Nhấn CTRL + F7 để thực thi hàm VirtualAlloc và lệnh RET của hàm, ta thấy chương trình được chuyển hướng tới gadget PUSH ESP-RET:

Các hàm API sau khi thực hiện xong, kết quả trả về sẽ lưu tại thanh ghi EAX. Ở đây ta có EAX nhận kết quả trả về là địa chỉ base address của vùng nhớ đã được thay đổi cơ chế bảo vệ. Như vậy, hàm VirtualAlloc đã thực hiện chính xác và kể từ bây giờ ta có thể thực thi mã của mình:

Tiếp tục nhấn F7 để trace qua đoạn code PUSH ESP-RET, ta sẽ tới NOP Sled + Shellcode:

Như vậy, ta đã tới được shellcode của mình mà không gặp vấn đề gì. Cho shellcode thực thi, kết quả nhận được như sau:

Giao diện quen thuộc của calculator đã hiện ra, tất nhiên toàn bộ cộng việc trên có thể được thực hiện tự động hóa nhờ vào sự hỗ trợ của mona. Trong phần tiếp theo tôi và các bạn sẽ tìm hiểu xem cách thức thực hiện tự động là như thế nào!

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

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

m4n0w4r

Ủ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

--

--