REVERSING WITH IDA FROM SCRATCH (P4)

m4n0w4r
tradahacking
Published in
12 min readMar 4, 2019

--

Lệnh XCHG

Trong phần này, chúng ta tiếp tục thực hành với các lệnh chuyển dữ liệu trong IDA. Lệnh tiếp theo là XCHG, cú pháp của lệnh như sau:

XCHG A, B; Hoán đổi giá trị của A với giá trị của B. A và B có thể là hai thanh ghi, thanh ghi và ô nhớ, nhưng không được phép đồng thời là 2 ô nhớ.

Ta sẽ xem xét một vài ví dụ bên dưới. Do trong file Veviewer không có lệnh XCHG nên tôi sẽ dùng lại crackme của Cruehead và thực hiện thay đổi lệnh tại địa chỉ 0x4013d8.

Đặt chuột tại lệnh xor eax, eax, chọn Edit > Patch Program > Assemble trong menu của IDA và thay đổi như hình:

Nhấn OK để chấp nhận thay đổi. Kết quả ta thấy rằng hàm ban đầu đã bị hủy sau khi chúng ta thực hiện thay đổi lệnh:

Như trên hình, tại vị trí 004013D9, do không phải là lệnh mà IDA có thể nhận biết được nên nó sẽ hiển thị ở dạng dữ liệu. Trong trường hợp này, nó chỉ là một byte — căn cứ vào thông tin biểu diễn kiểu dữ liệu “db, và giá trị tại đó là 0xC0. Nếu ta thay thế byte này bằng một lệnh NOP (có nghĩa là KHÔNG LÀM GÌ CẢ):

Kết quả sau khi thay bằng lệnh NOP (opcode là 0x90):

Các bạn thấy mọi thứ trông có vẻ ổn hơn, nhưng tuy nhiên cấu trúc hàm vẫn bị phá vỡ. Như ở trên, khi ta thay lệnh XOR bằng lệnh XCHG thì phần không được nhận biết là mã lệnh nằm ở giữa và đồng thời cũng hủy luôn cấu trúc của hàm ban đầu. Tuy nhiên, khi đã thay bằng lệnh NOP, tức là ta đã loại bỏ byte mà IDA không nhận biết được nhưng IDA lại không tự động nhận diện lại cấu trúc của hàm. Để buộc IDA nhận biết đây là hàm thì phải thực hiện bằng tay. Nhấn chuột phải tại nơi hàm bắt đầu — tại địa chỉ 0x4013d8 và chọn Create Function:

IDA sẽ thay tiền tố loc_ (đứng trước của một địa chỉ hàm ý cho biết rằng đó là một vị trí thông thường) bằng tiền tố sub_ (thông báo rằng đó là bắt đầu của một chương trình con hoặc một hàm) và tự động nhận biết các tham số cũng như biến cục bộ tương ứng của hàm:

Sau khi thực hiện như trên các bạn thấy hàm đã được tạo lại chính xác. Để chuyển về chế độ đồ họa, nhấn phím cách (spacebar):

OK đẹp hơn rồi!

Quay lại với lệnh XCHG, giả sử nếu EAX có giá trị 0x12345678 và ESI có giá trị 0x55, khi ta thực hiện lệnh XCHG thì thanh ghi EAX sẽ được gán giá trị 0x55 và thanh ghi ESI là 0x12345678. Như vậy, sau khi thực hiện lệnh XCHG thì giá trị của hai thanh ghi được hoán đổi cho nhau.

Bên lề: Quan sát trong menu View > Open subviews, bạn sẽ thấy có một lựa chọn là Patched Bytes (Ctrl+Alt+P). Cửa sổ này cho ta biết địa chỉ nào có lệnh đã bị thay đổi và có thể khôi phục lại được các giá trị bau đầu thông qua lựa chọn Revert:

Lệnh XCHG cũng có thể được sử dụng để hoán đổi giữa giá trị thanh ghi với nội dung bộ nhớ được trỏ bởi thanh ghi:

Trong ví dụ trên, [ESI] có nghĩa là tìm kiếm nội dung tại vị trí trong bộ nhớ được trỏ bởi giá trị của thanh ghi ESI và hoán đổi cho giá trị của EAX. Giá trị của thanh ghi EAX sẽ được lưu vào vị trí bộ nhớ mà thanh ghi ESI trỏ tới nếu như vùng nhớ đó có quyền ghi.

Giả sử, nếu EAX có giá trị 0x55 và ESI có giá trị 0x10000. Lệnh XCHG lúc này sẽ kiểm tra hiện đang có gì lưu tại vị trí bộ nhớ 0x10000 và vùng nhớ này có thể ghi được không, nó sẽ lưu giá trị 0x55 ở đó và sẽ đọc giá trị mà nó đã có và lưu vào thanh ghi EAX.

Điều gì xảy ra nếu chúng ta thực hiện tương tự, nhưng thay vì sử dụng thanh ghi chúng ta sử dụng một địa chỉ bộ nhớ là một giá trị số cụ thể như chúng ta đã thực hiện với lệnh MOV ở phần trước?

Do chức năng Assemble của IDA không thể áp dụng đầy đủ cho tất cả các lệnh, chúng ta phải thay đổi các bytes thông qua chức năng Patch Bytes. Tuy nhiên, tốt hơn là nên sử dụng plugin Keypatch (một plugin được viết bởi hai anh Nguyen Anh Quynh (aka aquynh) & Thanh Nguyen (aka rd) đã giành được giải thưởng plugin contest của Hexrays năm 2016 — https://www.hex-rays.com/contests/2016/index.shtml), cho phép patch trực tiếp các lệnh ASM vào binary. Để cài đặt và sử dụng plugin này các bạn xem tại đây: https://github.com/keystone-engine/keypatch Khi cài đặt thành công, tại màn hình của IDA, nhấn chuột phải tại một lệnh bất kỳ sẽ xuất hiện như hình:

Lựa chọn Patcher (Ctrl-Alt-K), ta thấy rằng việc viết lệnh trong Keypatch rất đơn giản và sau đó lệnh sẽ được chuyển đổi thành cú pháp của IDA khi nhấn Patch:

Dưới đây là kết quả sau khi sửa lệnh:

Cũng giống như với lệnh MOV, khi xuất hiện tiền tố dword_ mà không phải là offset_ ở phía trước, nó có nghĩa là nó hoán đổi nội dung của 0x4020DC với giá trị của EAX.

Phew, với lệnh XCHG như thế là quá đủ, chúng ta chuyển sang các câu lệnh khác.

Các câu lệnh tương tác với Stack

Stack là gì?

Stack (Ngăn xếp) là một phần của bộ nhớ và là cấu trúc dữ liệu một chiều (các phần tử được cất vào và lấy ra từ cùng một đầu của cấu trúc). Việc truy cập vào stack sẽ tuân theo cơ chế FILO, nghĩa là “Vào trước, ra sau” hay LIFO, “Vào sau, ra trước”. Các bạn có thể hình dung Stack như một chồng đĩa, chiếc đĩa cuối cùng được xếp vào sẽ nằm trên đỉnh và chỉ có nó mới có thể được lấy ra đầu tiên. Theo quy ước, Stack hướng về phía địa chỉ bộ nhớ thấp hơn.

Stack cho phép lưu trữ và khôi phục lại dữ liệu. Đối với việc xử lý dữ liệu trên stack, có hai thao tác lệnh cơ bản: PUSH, đẩy/lưu một phần tử vào đỉnh ngăn xếp và thao tác ngược lại của nó POP, loại bỏ/khôi phục một phần từ được đẩy vào cuối cùng ra khỏi ngăn xếp.

Tại mỗi thời điểm, ta chỉ có quyền truy cập tới đỉnh của stack, nghĩa là, phần tử được đẩy vào cuối cùng. Thao tác POP cho phép lấy được phần tử này ra khỏi ngăn xếp và cho phép truy cập tới phần tử tiếp theo bên dưới (phần tử được đẩy vào trước đó) — trở thành phần tử được xếp vào cuối cùng. Trong crackme.exe, tôi sẽ lấy ví dụ về cả hai lệnh PUSH và POP.

Lệnh PUSH

Lệnh này được dùng để thêm/ lưu dữ liệu vào trong ngăn xếp. Toán hạng nguồn có thể là các thanh ghi dùng chung hoặc ô nhớ. Sau mỗi lần thực hiện lệnh Push thì giá trị của thanh ghi ESP sẽ được giảm đi.

Thông thường, trong kiến trúc 32 bit, lệnh PUSH thường được sử dụng để truyền các tham số của một hàm vào ngăn xếp trước khi thực hiện lời gọi hàm bằng một lệnh CALL.

Quan sát ví dụ tại địa chỉ 0x40104f trong hình minh họa ở trên. Lệnh PUSH 64 đặt giá trị dword 0x64 vào đỉnh của stack, sau đó lệnh tiếp theo PUSH EAX đặt giá trị EAX lên trên giá trị dword 64 đã lưu trước đó. Như vậy, lúc này giá trị của EAX sẽ nằm tại đỉnh của ngăn xếp:

Cũng trong hình trên, ta còn thấy một kiểu lệnh PUSH khác. Thay vì lưu các giá trị hằng số thì lưu các địa chỉ bộ nhớ vào Stack, như trong trường hợp sau:

Ở đây có tiền tố offset ở phía trước của TAG tương ứng với một chuỗi, như vậy có nghĩa là sẽ push một địa chỉ có nội dung là một chuỗi hay mảng ký tự vào đỉnh của Stack. Chúng ta nhấp đúp vào thẻ đại diện cho tên chuỗi là WindowName. Trong mã nguồn C, việc khai báo một mảng kí tự sẽ như sau:

char mystring[] = “Hello”;

Trong trường hợp này, IDA sử dụng hai dòng để mô tả biến, char WindowName[] xuất hiện như hình là vì IDA nhận biết được nó thuộc API là CreateWindow(). Hàm API này nhận tham số truyền vào phải là một LPCTSTR, đó là một mảng char[] và tham số đó là một chuỗi được gọi là WindowName.

Dù bằng cách nào, là một mảng các ký tự hay các bytes thì IDA sẽ bổ sung thêm thông tin chi tiết hơn khi nó nhận diện được qua hàm API. Bên dưới 0x4020e7, địa chỉ tiếp theo sẽ là 0x4020f4, ở giữa hai địa chỉ này sẽ là một loạt các bytes liên tiếp tương ứng với các kí tự của chuỗi “Crackme v1.0” và phân định bởi số 0 ở cuối, biểu diễn cho việc kết thúc một chuỗi (hay còn gọi là null byte).

Nếu chúng ta nhấn phím D để thay đổi kiểu dữ liệu trên WindowName, bằng cách này ta sẽ ép IDA chuyển đổi thành các bytes (db) thay vì để cho IDA tự động nhận biết đó là một mảng các ký tự:

Trên hình là những byte tương ứng với chuỗi “Crackme v1.0

Tại vị trí tham chiếu tới chuỗi, câu lệnh gốc lúc này sẽ bị thay đổi. Tiền tố offset ở phía trước có nghĩa là sẽ đẩy giá trị 0x4020E7, nhưng giờ đây nội dung không còn là một mảng các ký tự nữa mà là một byte, lệnh lúc này đã được thay đổi thành:

Bởi vì khi IDA tìm kiếm nội dung của 0x4020e7 để thông báo cho chúng ta giá trị đó là gì, do tại đó đã được chuyển thành một db, điều này có nghĩa là biến lúc này đã bị đổi thành một byte duy nhất do thao tác ta đã thực hiện ở trên.

Để lấy lại chuỗi ban đầu, ta nhấn phím A, IDA sẽ tự động chuyển đổi lại thành chuỗi ASCII:

Tương tự như vậy, khi trong quá trình phân tích nế ta phát hiện bất kì chuỗi nào được biểu diễn dưới dạng các byte rời rạc như hình dưới đây:

Hãy chuyển tới vị trí bắt đầu và nhấn A, nó sẽ được chuyển thành chuỗi tương ứng:

Trong trường hợp này, ta thấy chuỗi này không được định nghĩa bằng hai dòng giống như chuỗi trước đó và cũng không cho biết nó là một CHAR [], tuy nhiên nó lại được định nghĩa bằng một thẻ bắt đầu bằng sz hoặc a (trên máy của bạn) vì nó là một chuỗi ASCII. Ở ví dụ trước, IDA cung cấp thông tin bổ xung rõ hơn bởi nó nhận diện được đó là một tham số của hàm API và thông báo rằng tham số này phải là một char[]. Đó là lý do tại sao IDA cũng cấp thêm thông tin bổ sung như vậy, còn đối với một chuỗi bình thường sẽ giống như các bạn thấy ở trên.

Ở đây chúng ta thấy một chuỗi khác:

Tại địa chỉ 0x402110, ta có thể phân tách nó thành các bytes bằng cách nhấn phím tắt D tại szMenu:

Nếu nhấn A, ta sẽ có lại chuỗi ban đầu. Nhấn X để tìm các tham chiếu tới chuỗi này, kết quả sẽ tìm được nơi mà chuỗi này được sử dụng:

Chúng ta thấy rằng lệnh Mov sẽ lấy địa chỉ 0x402110 vì có tiền tố offset ở phía trước. Thông thường, khi truyền các tham số cho một hàm, chúng ta sẽ luôn thấy lệnh có dạng PUSH offset xxxxx, bởi vì cái ta cần là truyền địa chỉ nơi mà là chuỗi, còn nếu như không có tiền tố offset mà thay vào đó là dword thì sẽ đẩy nội dung của địa chỉ 0x402110 là các bytes 55 4e 45 4d (của cùng một chuỗi). Nhưng các hàm APIs lại không hoạt động theo cách này, chúng luôn nhận con trỏ hoặc địa chỉ bắt đầu hoặc nơi mà chuỗi bắt đầu.

Trong câu lệnh bên trên, tiền tố DS: TAG chỉ ra rằng nó sẽ lưu vào một địa chỉ bộ nhớ của đoạn dữ liệu (DS = DATA). Khi làm việc với struct, chúng ta sẽ tìm hiểu về trường hợp đó. Bây giờ, vấn đề quan trọng là nó lưu địa chỉ trỏ đến đầu chuỗi vào section DATA.

Lệnh POP

Lệnh này được dùng để lấy ra giá trị từ đỉnh của ngăn xếp, sau khi thực hiện lệnh thì giá trị của thanh ghi ESP sẽ được tăng lên để trỏ tới phần tử tiếp theo.

Ở ví dụ trên hình, lệnh POP thực hiện thao tác đọc giá trị trên đỉnh của ngăn xếp và chuyển nó đến thanh ghi đích, trong trường hợp này, câu lệnh POP EDI sẽ đọc giá trị đầu tiên hay giá trị trên đỉnh của ngăn xếp và sao chép nó vào thanh ghi EDI, sau đó trỏ thanh ghi ESP vào giá trị bên dưới và giá trị mới này sẽ trở thành đỉnh của ngăn xếp.

Thử tìm kiếm các lệnh POP, chúng ta thấy rằng không có nhiều biến thể của lệnh mặc dù có khả năng thực hiện POP giá trị vào một địa chỉ bộ nhớ thay vì một thanh ghi, nhưng tùy chọn này không được sử dụng rộng rãi.

Phần 4 xin được tạm dừng tại đây. Chúng ta sẽ tiếp tục trong phần 5 với câu lệnh khác để có thể tìm hiểu thêm về LOADER của IDA.

Xin chào và hẹn gặp lại ở phần tiếp theo!

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

--

--