REVERSING WITH IDA FROM SCRATCH (P9)

m4n0w4r
tradahacking
Published in
12 min readMar 22, 2019

Ở phần 8, tôi và các bạn đã tìm hiểu cơ bản cách thức xử lý của IDA Loader. Chúng ta sẽ khám phá dần các tính năng khác của IDA qua từng bài viết, ví dụ cách sử dụng Debugger để quan sát các cờ thay đổi khi thực hiện các lệnh, v..v…

Để tìm hiểu các tính năng của IDA, ta sẽ tiếp tục thực hành qua các ví dụ rất đơn giản. Trong phần 9 này là một crackme nhỏ nhẹ, được biên dịch bằng Visual Studio 2015. Để thực thi crackme này có thể bạn cần phải cài đặt phiên bản mới nhất của Visual Studio 2015 C++ runtimes ( https://www.microsoft.com/en-us/download/details.aspx?id=48145: The Visual C++ Redistributable packages install the runtime components to execute compiled C++ Visual Studio 2015 programs).

Sau khi cài đặt xong VC++ runtimes, chạy thử crackme (https://mega.nz/#!LHAGkKRD!h9PJX9kftBW4z3Ykf3Gwpa3LW6H_FCjIv7FZZnGux6s) là HOLA_REVERSER.exe:

Khi nhập vào một số bất kì, crackme sẽ thông báo bạn là một “good” hay “bad” reverser, hehe:

Có thể thấy crackme hoạt động rất đơn giản. Ta sẽ mở nó trong IDA để phân tích.

Bên lề: trong trường hợp của thầy Ricardo Narvaja (không phải của tôi & bạn), sau khi IDA tiến hành phân tích xong, hàm main() của crackme xuất hiện giữa các hàm tại cửa sổ Functions. Có thể dễ dàng tìm kiếm bằng tổ hợp phím CTRL + F tại cửa sổ đó. Điều này xảy ra bởi vì ông ấy là chủ của crackme này, khi biên dịch bằng Visual Studio thì trình biên dịch đã tạo ra thêm một tập tin symbol là .pdb (https://docs.microsoft.com/en-us/windows/desktop/debug/symbol-files). Dựa vào file này, IDA có thể nhận diện và nạp tên của các hàm và các biến mà Ricardo Narvaja sử dụng khi lập trình, ví dụ như trong hàm main() gọi tới hàm printf():

Giờ hãy xem những gì sẽ xảy ra đối với IDA của chúng ta khi phân tích xong crackme.

Tại màn hình IDA của bạn, bạn sẽ không thấy tên hàm main xuất hiện tại cửa sổ Functions bởi IDA không có tập tin .pdb để load cùng. Điều này hoàn toàn bình thường, bởi không coder nào lại cung cấp một chương trình kèm theo symbols cả :).

Thông thường, chúng ta chỉ có được các tập tin symbols của các modules hệ thống (kernel32.dll, ntdll.dll, …) chứ không thể có của các chương trình, ngoại trừ những trường hợp rất hiếm hoi. Trong ví dụ này, Ricardo Narvaja có symbols file là bởi vì ông là người code ra crackme này. Tuy nhiên, để đúng như thực tế, ta sẽ phân tích crackme mà không có symbol đi kèm giống như sẽ gặp với rất nhiều các chương trình khác.

Khi load file không có symbol ta sẽ dừng tại đây:

Như trên hình, code tại đây không cung cấp nhiều thông tin, do vậy ta phải tìm kiếm thông tin tại cửa sổ Strings:

Tại cửa sổ Strings, ta có được thông tin các strings được sử dụng bởi crackme. Qua các strings này ta thấy crackme sẽ hiển thị thông báo nếu như chúng ta nhập vào một số không hợp lệ. Nhấp đúp chuột tại chuỗi “Pone un numerito\n”, chuỗi này trong tiếng anh có nghĩa là “Enter a serial”:

Như kết quả trên hình, ta thấy rằng địa chỉ 0x402108 là con trỏ đến chuỗ. Bên cạnh địa chỉ có một tag mà IDA đặt cho địa chỉ này, bắt đầu bằng “sz” (ở máy tôi) hay “a” (trên máy bạn), hàm ý đây là một chuỗi ASCII và phần còn lại tương ứng với nội dung của chuỗi. Với thông tin này sẽ giúp ta dễ dàng để nhận ra đó là một chuỗi ASCII “szPoneUnNumerit”, từ khóa db ở bên cạnh hàm ý đây là một chuỗi byte.

Nếu nhấn phím “D” chúng ta sẽ chuyển chuỗi này thành các bytes như hình:

Để khôi phục lại chuỗi ban đầu, nhấn phím “A”. Di chuyển con chuột tới biểu tượng mũi tên nhỏ “↑”, IDA sẽ hiển thị thông tin về đoạn code sử dụng tới chuỗi này:

Tuy nhiên, tốt hơn nên sử dụng phím “X” để tìm danh sách tất các các vùng code tham chiếu tới chuỗi:

Kết quả có được như trên hình, nhấn OK ta sẽ tới hàm chính (main), nhưng ở đây hàm không được đặt tên là main như các bạn đã thấy khi load file có kèm .pdb. Ở đây, ta thấy có môt buffer được IDA đặt một cái tên khá chung chung là Buf:

Chúng ta không biết mã nguồn của crackme này, nhưng giả sử cung cấp một đoạn của mã nguồn như sau:

Các bạn có thể thấy trình biên dịch đã tối ưu hóa một số biến đã khai báo trong chương trình, ví dụ như các biến cookie không được sử dụng sẽ bị loại bỏ và max đã được thay thế bằng hằng số. Bên cạnh đó ta có thông tin về vùng buffer trong mã nguồn của crackme có độ dài là 120 bytes. Buffer là một vùng nhớ dành riêng được sử dụng cho việc lưu trữ dữ liệu. Vậy khi chúng ta không có mã nguồn, làm thế nào trong IDA chúng ta có thể biết được kích thước của một buffer trên stack?

Như trên hình, tại phần đầu của mỗi hàm chúng ta sẽ thấy một danh sách các biến và các tham số của hàm. Nhấp đúp chuột vào bất kỳ một cấu phần nào cũng sẽ đưa chúng ta đến chế độ xem tĩnh của Stack, nơi chứa thông tin về vị trí của các biến, các tham số và buffer, … kèm theo đó là khoảng cách giữa chúng:

Tại cửa số Stack của hàm, chúng ta thấy biến Buf nhưng nó đang được định nghĩa là các byte “db”. Để thay đổi nó thành mảng các ký tự hoặc buffer như khai báo trong mã nguồn, chúng ta nhấn chuột phải tại Buf và chọn tùy chọn Array:

Ta thấy rằng IDA đã tự động nhận biết được mảng này tối đa gồm 120 phần tử (khoảng cách của Buf so với biến tiếp theo), mỗi phần tử trong mảng có kích thước là 1 byte. Nhấn OK để chấp nhận thông tin chuyển đổi mà IDA cung cấp, kết quả có được như hình:

Như vậy, tôi vừa tạo được một buffer gồm 120 bytes trong IDA, hoàn toàn phù hợp với thông tin khai báo trong mã nguồn. Từ khóa “dup” có nghĩa là lặp lại các dữ liệu trong ngoặc 120 lần (Nó tương đương với việc viết ?,?,?,?, … (120 lần)). Vì giá trị là chưa được xác định nên nó chỉ là một buffer rỗng.

Tôi sẽ làm rõ dần các thông tin tại cửa sổ Stack trong các phần sau, nhưng bên dưới biến Buf là một biến dword (dw) được đặt tên là var_4. “s” cung cấp thông tin giá trị thanh ghi EBP đã lưu của hàm gọi (old frame) và “r” cung cấp thông tin về return address trước khi truy cập vào hàm.

Các tham số của hàm sẽ được truyền vào thông qua lệnh PUSH, sau đó sử dụng một lệnh CALL để gọi hàm, lưu return address (r), và phía trên s là vùng dành cho các biến cục bộ khai báo trong hàm.

VARIABLES

S (stored ebp — thường xuất phát từ PUSH EBP là lệnh đầu tiên của hàm)

R (return address)

ARGUMENTS

Do các tham số được truyền vào ngăn xếp trước khi lưu địa chỉ trở về nên chúng sẽ nằm ở dưới và ở phía trên địa chỉ trở về là thanh EBP được lưu (tạo ra bởi lệnh PUSH EBP — thường là câu lệnh đầu tiên của hàm). Sau đó, ở trên là không gian dành cho các biến cục bộ. Chúng ta sẽ quan sát chi tiết hơn ở những phần tiếp sau.

Để xem nơi nào gọi tới hàm này ta nhấn “X”:

Nhấn OK sẽ đưa ta tới nơi gọi hàm call sub_401040. Trước lời gọi hàm ta thấy có một số lệnh PUSH truyền tham số cho hàm. Tuy nhiên, nếu chưa từng lập trình thì ta sẽ không rõ thông tin về các tham số này:

Trong trường hợp nạp crackme kèm theo symbol thì IDA nhận diện được đầy đủ các tham số truyền cho hàm như hình dưới đây:

IDA phát hiện ra rằng các tham số này không bao giờ được sử dụng, chúng là các tham số argc, argvenvp mà theo mặc định là các tham số của hàm main(). Nhưng vì không có bất kỳ tham chiếu hoặc sử dụng nào trong hàm, do vậy chức năng IDA sẽ loại bỏ chúng để tối ưu:

Bên cạnh đó, như các bạn thấy trong mã nguồn của crackme, thầy Ricardo cũng không sử dụng/ khai báo các tham số này cho hàm main(), nhưng khi load cùng với symbol thì chúng được sử dụng một cách mặc định. Quay trở lại với crackme, khi chúng ta muốn xem nơi mà một biến được truy cập, chúng ta chọn biến đó và nhấn “X”. Ví dụ, tôi chọn var_4:

Các bạn thấy rằng, biến var_4 sẽ được sử dụng ở hai vị trí.

Bên lề: thông tin bổ sung thêm dành cho những ai chưa biết, đó là về giá trị cookie mà trong code chương trình không hề có. Đây là cơ chế bảo vệ của chương trình tránh khỏi lỗi stack overflow hay còn được gọi theo thuật ngữ chuyên ngành là Stack Canary. Giá trị này được lưu ở đầu của mỗi hàm và sẽ được kiểm tra trước khi thoát khỏi hàm. Chúng ta sẽ đặt tên cho nó là SECURITY_COOKIE:

Khi bạn chạy crackme, bạn để ý nó sẽ in các chuỗi ra màn hình. Điều này có nghĩa khi in các chuỗi, nó gọi tới một lệnh CALL như hình dưới. Nếu bạn đi sâu vào lệnh call này bạn sẽ thấy tác giả không lập trình ra chúng, nhưng chắc chắn cuối cùng nó cũng sẽ gọi tới hàm printf() để in các chuỗi ra màn hình.

Nếu load crackme cùng với symbols, thì IDA sẽ nhận diện được đó là hàm printf() như hình:

Khi đào sâu vào bên trong lệnh CALL, ta sẽ thấy rằng các tham số là các chuỗi sẽ được in ra màn hình console thì ta hoàn toàn có thể suy luận rằng nó là hàm printf().

Chúng ta thấy bên trong hàm kết thúc bằng hàm vfprintf, do đó đổi lại tên hàm như sau:

Như đã phân tích ở trên, chương trình có mảng Buf với kích thước 120-bytes, hãy phân tích xem code sẽ làm gì với Buf này. Ta thấy rằng nó được truyền vào cho hàm gets_s(), đó là hàm nhận những gì chúng ta nhập vào từ màn hình console.

Hàm này nhận hai tham số truyền vào:

  • Tham số thứ nhất là một con trỏ trỏ đến buffer.
  • Tham số thứ hai là kích thước tối đa cho phép chúng ta gõ.

Hãy xem trong trường hợp ví dụ của chúng ta.

Lệnh LEA được sử dụng để lấy ra địa chỉ của một biến, trong trường hợp này là con trỏ trỏ tới Buf, và buffer được truyền cho hàm thông qua lệnh PUSH EAX. Tiếp theo là PUSH 0x14 để quy định số lượng kí tự tối đa được gõ trên màn hình:

Chúng ta thấy trong mã nguồn của crackme, thầy Ricardo gọi hàm gets_s() với hai đối số là buf và kích thước tối đa mà thầy đã khai báo thông qua một biến được gọi là max, có giá trị 20. Tuy nhiên, trình biên dịch, để tiết kiệm không gian đã sử dụng luôn số 20 (ở hệ hexa là 0x14) như là một tham số cho hàm vì sau đó code không còn sử dụng đến biến max nữa.

Vì vậy, không cần phải thực thi crackme, tôi hoàn toàn biết được buffer của tôi sẽ dùng để chứa các ký tự mà tôi gõ tại màn hình console. Sau đó, ở đoạn code bên dưới, ta thấy cùng một con trỏ tới bộ đệm được truyền là một tham số cho hàm atoi():

Thông tin hàm như sau:

Hàm atoi() thực hiện chuyển đổi một string (kiểu char*) thành số nguyên int. Hàm sẽ trả về 0 nếu convert không thành công. Do vậy, ý tưởng ở đây là những gì bạn nhập trên màn hình sẽ được chuyển đổi sang số. Ví dụ nếu bạn nhập 41424344, nó sẽ chuyển đổi sang dạng hexa và lưu kết quả chuyển đổi vào thanh ghi EAX.

Như trên hình, hàm atoi() sau khi thực hiện sẽ lưu kết quả trả về trong thanh ghi EAX, tiếp theo gán vào thanh ghi ESI và sau khi được in ra màn hình sẽ tới đoạn code thực hiện so sánh giá trị của ESI với một số mặc định 0x124578.

Tóm lại, số ta gõ được chuyển đổi từ một chuỗi số thập phân sang số ở hệ thập lục phân và so sánh với hằng số trên. Nếu so sánh không bằng nhau (JNZ), ta sẽ phải nhận thông báo “Bad Reverser”, còn ngược lại nếu bằng thì sẽ nhận thông báo “Good Reverser”. Sử dụng Python bar của IDA để chuyển đổi số 0x124578 về dạng thập phân:

Kết quả sau khi chuyển đổi ta được một số thập phân là 1197432:

Thực thi crackme và nhập số vừa tìm được:

Như các bạn thấy đây là một ví dụ đơn giản của quá trình static reversing, qua đó giúp bạn làm quen hơn với LOADER của IDA.

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

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

--

--