Ở phần 13 này, chúng ta sẽ thư giãn một chút trước khi tiếp tục với các bài thực hành khác để tìm hiểu sâu hơn về cách sử dụng IDA. Phần này tôi sẽ giới thiệu tới các bạn một plugin khá tiện lợi và thú vị, cho phép chúng ta có thể xử lý tốt hơn với Python.

Plugin có tên là IpyIDA, được phát triển bởi Marc-Etienne M.Léveillé (https://twitter.com/marc_etienne_), một chuyên gia hiện làm việc tại hãng ESET. Việc cài đặt plugin này khá dễ dàng, chỉ bằng cách sao chép và dán dòng lệnh sau vào thanh Python của IDA:

import urllib2; exec urllib2.urlopen('https://github.com/eset/ipyida/raw/stable/install_from_ida.py').read()

Trong trường hợp có vấn đề hoặc gặp lỗi, bạn có thể truy cập trang chủ của plugin tại đây: https://github.com/eset/ipyida

Câu lệnh trên sẽ tiến hành cài đặt một cách hoàn toàn tự động trong vòng vài phút. Sau khi cài đặt xong, ta có thể sử dụng nó thông qua Edit > Plugins > IpyIDA, cửa sổ Ipython Console xuất hiện tương tự như hình minh họa bên dưới:

Ta thấy rõ ràng nó có nhiều tính năng mạnh hơn thanh Python của IDA. Để tra cứu các tính năng của plugin này, ta nhấn phím ?:

IPython -- An enhanced Interactive Python

=========================================

IPython offers a combination of convenient shell features, special commands and a history mechanism for both input (command history) and output (results caching, similar to Mathematica). It is intended to be a fully compatible replacement for the standard Python interpreter, while offering vastly improved functionality and flexibility.

At your system command line, type 'ipython -h' to see the command line options available. This document only describes interactive features.

MAIN FEATURES

-------------

* Access to the standard Python help. As of Python 2.1, a help system is available with access to object docstrings and the Python manuals. Simply type 'help' (no quotes) to access it.

* Magic commands: type %magic for information on the magic subsystem.

* System command aliases, via the %alias command or the configuration file(s).

* Dynamic object information:

Typing ?word or word? prints detailed information about an object. If certain strings in the object are too long (docstrings, code, etc.) they get snipped in the center for brevity.

Typing ??word or word?? gives access to the full information without snipping long strings. Long strings are sent to the screen through the less pager if longer than the screen, printed otherwise.

The ?/?? system gives access to the full source code for any object (if available), shows function prototypes and other useful information.

If you just want to see an object's docstring, type '%pdoc object' (without quotes, and without % if you have auto magic on).

* Completion in the local namespace, by typing TAB at the prompt.

At any time, hitting tab will complete any available python commands or variable names, and show you a list of the possible completions if there's no unambiguous one. It will also complete filenames in the current directory.

This feature requires the readline and rlcomplete modules, so it won't work if your Python lacks readline support (such as under Windows).

* Search previous command history in two ways (also requires readline):

- Start typing, and then use Ctrl-p (previous,up) and Ctrl-n (next,down) to search through only the history items that match what you've typed so far. If you use Ctrl-p/Ctrl-n at a blank prompt, they just behave like normal arrow keys.

- Hit Ctrl-r: opens a search prompt. Begin typing and the system searches your history for lines that match what you've typed so far, completing as much as it can.

- %hist: search history by index (this does *not* require readline).

* Persistent command history across sessions.

* Logging of input with the ability to save and restore a working session.

* System escape with !. Typing !ls will run 'ls' in the current directory.

* The reload command does a 'deep' reload of a module: changes made to the module since you imported will actually be available without having to exit.

* Verbose and colored exception traceback printouts. See the magic xmode and xcolor functions for details (just type %magic).

* Input caching system:

IPython offers numbered prompts (In/Out) with input and output caching. All input is saved and can be retrieved as variables (besides the usual arrow key recall).

The following GLOBAL variables always exist (so don't overwrite them!):

_i: stores previous input.

_ii: next previous.

_iii: next-next previous.

_ih : a list of all input _ih[n] is the input from line n.

Additionally, global variables named _i are dynamically created ( being the prompt counter), such that _i == _ih[]

For example, what you typed at prompt 14 is available as _i14 and _ih[14].

You can create macros which contain multiple input lines from this history, for later re-execution, with the %macro function.

The history function %hist allows you to see any part of your input history by printing a range of the _i variables. Note that inputs which contain magic functions (%) appear in the history with a prepended comment. This is because they aren't really valid Python code, so you can't exec them.

* Output caching system:

For output that is returned from actions, a system similar to the input cache exists but using _ instead of _i. Only actions that produce a result (NOT assignments, for example) are cached. If you are familiar with Mathematica, IPython's _ variables behave exactly like Mathematica's % variables.

The following GLOBAL variables always exist (so don't overwrite them!):

_ (one underscore): previous output.

__ (two underscores): next previous.

___ (three underscores): next-next previous.

Global variables named _ are dynamically created ( being the prompt counter), such that the result of output is always available as _.

Finally, a global dictionary named _oh exists with entries for all lines which generated output.

* Directory history:

Your history of visited directories is kept in the global list _dh, and the magic %cd command can be used to go to any entry in that list.

* Auto-parentheses and auto-quotes (adapted from Nathan Gray's LazyPython)

1. Auto-parentheses Callable objects (i.e. functions, methods, etc) can be invoked like this (notice the commas between the arguments)::

In [1]: callable_ob arg1, arg2, arg3

and the input will be translated to this::callable_ob(arg1, arg2, arg3)

This feature is off by default (in rare cases it can produce undesirable side-effects), but you can activate it at the command-line by starting IPython with `--autocall 1`, set it permanently in your configuration file, or turn on at runtime with `%autocall 1`.

You can force auto-parentheses by using '/' as the first character of a line. For example::

In [1]: /globals # becomes 'globals()'

Note that the '/' MUST be the first character on the line! This won't work::

In [2]: print /globals # syntax error

In most cases the automatic algorithm should work, so you should rarely need to explicitly invoke /. One notable exception is if you are trying to call a function with a list of tuples as arguments (the parenthesis will confuse IPython)::

In [1]: zip (1,2,3),(4,5,6) # won't work

but this will work::

In [2]: /zip (1,2,3),(4,5,6)

------> zip ((1,2,3),(4,5,6))

Out[2]= [(1, 4), (2, 5), (3, 6)]

IPython tells you that it has altered your command line by displaying the new command line preceded by -->. e.g.::

In [18]: callable list

-------> callable (list)

2. Auto-Quoting You can force auto-quoting of a function's arguments by using ',' as the first character of a line. For example::

In [1]: ,my_function /home/me # becomes my_function("/home/me")

If you use ';' instead, the whole argument is quoted as a single string (while ',' splits on whitespace)::

In [2]: ,my_function a b c # becomes my_function("a","b","c")

In [3]: ;my_function a b c # becomes my_function("a b c")

Note that the ',' MUST be the first character on the line! This won't work::

In [4]: x = ,my_function /home/me # syntax error

____________________________________________________________________

OK, như các bạn thấy IPyIDA có khá nhiều tính năng và cần phải có thời gian để tìm hiểu dần. Để xóa các thông tin đã hiển thị ở trên, nhấn phím ESC.

Một tính năng hay của plugin này là nó cũng cấp khả năng tự động hoàn tất lệnh bằng phím TAB giống như bạn gõ lệnh ở terminal trên Linux (tính năng này không có ở thanh Python mặc định của IDA). Ví dụ: nếu tôi gõ imp và nhấn TAB, nó sẽ tự động autocomplete thành import. Sau đó tôi nhấn phím cách và nhấn TAB một lần nữa:

Như trên hình, ta thấy được toàn bộ các modules có thể import, sau đó sử dụng các mũi tên điều hướng  để lựa chọn modules cần import và thoát ra bằng cách nhấn ESC.

Khi tôi nhập dấu “?” một lần, nó cung cấp cho ta thông tin nhanh về module đó:

Và nếu tôi nhập dấu “?” hai lần nó sẽ hiển thị code như hình dưới đây:

Để thoát ta chỉ việc nhấn phím ESC. Bằng việc sử dụng các mũi tên lên và xuống, ta có thể đi đến các lệnh trước đó đã sử dụng. Hoặc sử dụng %hist để hiện thị thông tin lịch sử về các lệnh:

%edit sẽ mở ứng dụng notepad của Windows. Còn %edit x-y sẽ mở một notepad chứa các lệnh đã gõ nằm trong khoảng đó, tương tự như hình sau:

%history -n thêm số dòng bên cạnh các câu lệnh đã sử dụng:

Rõ ràng IPython khá mạnh và nó có rất nhiều câu lệnh mà bạn có thể tìm thấy ở đây: http://ipython.org/ipython-doc/3/index.html

Chúng ta sẽ làm một vài ví dụ đơn giản với plugin mới này.

Câu lệnh trên thực hiện lấy địa chỉ hiện tại của con trỏ. Nếu tôi sử dụng lệnh %edit như dưới đây, trình notepad của Windows sẽ được gọi lên để lưu các lệnh thành python script:

Sau đó, ta cho thực thi thông qua menu File-Script file, script sẽ cho kết quả tương tự:

Ngoài ra, lệnh idc.GetDisasm (ea) sẽ cung cấp cho chúng ta lệnh ASM tại nơi con trỏ đang đứng:

Nếu thay đổi con trỏ sang câu lệnh khác, ta sẽ phải tìm lại giá trị ea một lần nữa. Với câu lệnh idc.GetOpnd, ta có thể lấy được thông tin về toán hạng đầu tiên hoặc thứ hai của câu lệnh. Ví dụ như sau:

Đoạn code dưới đây thực hiện in ra tên của hàm hiện tại:

Tên của tất cả các hàm được liệt kê thông qua đoạn code sau:

Các câu lệnh bên trong hàm:

Tìm tham chiếu đến hàm. Nếu ta đặt con trỏ tại đầu của một hàm, sau đó nhấn X để tìm các xrefs đến hàm này thì có được kết quả như sau:

Ta hoàn toàn có thể sử dụng câu lệnh để làm được việc tương tự như trên, ví dụ:

Có thể thấy plugin này mang lại cho chúng ta rất nhiều tiện lợi và IDApython có hàng ngàn câu lệnh phục vụ để thiết lập các breakpoints, log, thực thi debugger, v..v…

Phần 13 đến đây là kết thúc. Hẹn gặp lại các bạn ở phần 14!

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


Để tránh tình trạng nhàm chán khi học một mớ lý thuyết, tôi sẽ cố gắng xen kẽ các bài tập để thực hành. Trong phần này tôi gửi kèm file TEST_REVERSER.exe mà thầy Ricardo đã code. Nó rất đơn giản! Tuy nhiên, thông qua ví dụ này sẽ giúp chúng ta nhìn thấy một số điều mới trong việc thực hiện static reversing cũng như áp dụng debugging.

Khi thực thi file bên ngoài IDA, các bạn sẽ thấy tương tự như sau:

Chương trình yêu cầu ta cung cấp tên của người dùng và một mật khẩu tương ứng. Nếu mật khẩu nhập vào không đúng sẽ hiển thị thông báo “Bad reverser” như trên hình.

Tiến hành mở file trong IDA để phân tích ở chế độ static. Do ta nạp file vào IDA không kèm theo symbol, nên sau khi IDA phân tích xong, code dòm khá tệ

Quan sát trên hình, các bạn thấy rằng IDA không nhận diện được hàm main() của chương trình. Tuy nhiên, IDA đang dừng lại tại Entry Point … như vậy là cũng tốt rồi. Thực tế, khi các bạn phân tích các ứng dụng khác cũng sẽ luôn luôn như thế và chúng ta phải tìm cách để giải quyết vấn đề nhỏ này.

Một trong những cách phổ biến mà các bạn đã thấy khi tìm đến phần code chính của chương trình là thực hiện tìm kiếm theo các chuỗi. Cách làm thì tôi đã giới thiệu thông qua các phần trước. Đối với các chương trình C/C++ kiểu này, có một cách để tìm ra hàm main() mà hầu như luôn cho kết quả chính xác như sau.

Chúng ta biết rằng hàm main() được truyền vào các tham số như argc, argv, v..v. Hay còn gọi là các console arguments: int main(int argc, char *argv[])

Trong ví dụ trước, các bạn có thể thấy thậm chí khi ta không sử dụng các tham số trong main() thì luôn luôn có các lệnh PUSH làm nhiệm vụ truyền các tham số này cho main(). Do vậy, các tham số này là mặc định, nên chúng ta có thể tìm kiếm trong tab NAMES để xem chúng có ở đó không:

Để không phải lặp lại quá nhiều, từ giờ trở đi, khi tôi đề cập thực hiện trên tab XXX, bạn đã biết rằng bạn phải mở tab này tại View-Open Subview-XXX.

Tại tab Names như trên hình, nhấn CTRL + F để thực hiện tìm kiếm theo điều kiện, ví dụ để tìm các tham số truyền cho hàm main() tôi nhập arg và sau khi có được kết quả, tôi nhấp đúp vào _p_argc. IDA sẽ đưa chúng ta đến đoạn code sau:

Sau đó nhấn X để tìm các references tới đoạn code trên. Ta sẽ tới đây:

Tại đoạn code trên hình, các bạn thấy cách ứng dụng gọi các hàm _p_argv và _p_argc và trả về kết quả, sau đó truyền các tham số đó cho hàm main() – trong trường hợp này là 0x401070.

Nếu xem xét vùng code này trong IDA khi được load kèm theo symbol:

Và reference:

Ở đây không phải là tôi chơi cheat lolz, tôi chỉ là thực hiện kiểm tra xem phương pháp tìm kiếm hàm main() như trên có chính xác không và như các bạn thấy nó hoạt động khá chuẩn. Bằng cách tìm kiếm các reference của các tham số được truyền qua giao diện console, ta có thể tìm được hàm main() của chương trình.

Khi đã biết được vị trí của hàm main(), thực hiện đổi lại tên hàm như sau:

Ngay lập tức, IDA tự động đổi tên các args sau khi chúng ta chỉ rõ đó chính là hàm main() của chương trình.

Bây giờ, code của chúng ta trông giống như phiên bản load kèm theo symbol:

Các bạn thấy trong trường hợp này các biến và các tham số nhiều hơn trong ví dụ trước. Nếu nhấn đúp vào bất kỳ biến hoặc tham số nào, chúng ta sẽ chuyển tới của sổ Stacknơi cung cấp thông tin về stack layout của hàm main().

Chúng ta quan sát từ phía dưới lên, về mặt logic đầu tiên sẽ là các tham số được truyền vào cho hàm. Các tham số này luôn luôn nằm dưới địa chỉ trở về (return address (r)), vì chúng được truyền vào bằng lệnh PUSH và được lưu trong ngăn xếp (Stack) trước khi gọi hàm bằng lệnh CALL. Tiếp theo đó, địa chỉ trở về (r) sẽ được lưu vào Stack.

Sau đó, chương trình sẽ lưu thanh ghi EBP (s), đó là giá trị EBP của hàm đã gọi hàm main(). Giá trị này được lưu trong ngăn xếp khi hàm được thực thi thông qua câu lệnh push ebp như trong hình minh họa bên dưới:

Tiếp theo, chương trình thực hiện copy thanh ghi ESP vào EBP. Bằng cách này sẽ đặt EBP vào trong hàm hay còn gọi là BASE, từ đó sử dụng thanh ghi này để truy cập các tham số (EBP + XXX) và các biến cục bộ (EBP – XXX) của hàm. Cuối cùng là lệnh SUB ESP, 0x94, lệnh này sẽ dịch chuyển thanh ghi ESP để tạo ra không gian trống dành cho các biến cục bộ và và các buffer, đó là lý do tại sao thanh ghi EBP phải – XXX để truy cập tới các biến này. Ở chương trình này có giá trị là 0x94, là do trình biên dịch (compiler) đã tự động tính toán cần dành bao nhiêu không gian là đủ cho các biến, tùy theo cách chúng ta lập trình.

Thanh ghi ESP có giá trị nằm trên không gian dành riêng cho các biến cục bộ (đỉnh của Stack) và thanh ghi EBP trỏ tới BASE, phân chia các biến ở trên và ở dưới là Return address và các Args.

Đây là lý do tại sao các hàm dựa vào thanh ghi EBP, một khi giá trị EBP của hàm mà tôi gọi được lưu bằng lệnh PUSH EBP, và sau đó copy ESP sang EBP thì ta thấy trong chế độ xem tĩnh của stack, nó hiển thị giá trị 000000000 như một ranh giới để phân tách giữa các biến cục bộ và các tham số của hàm.

Như vậy, các bạn đã hiểu tại sao var_4 có thông tin là -00000004, vì dùng thanh ghi EBP làm BASE nên địa chỉ tính toán cho biến sẽ là EBP-4. Bên dưới, argc sẽ tương ứng là EBP + 8 (quan sát cột bên trái):

Điều này có thể được xác minh tại màn hình disassembly của hàm main(), nơi var_4 được sử dụng. Khi nhấp chuột phải, chúng ta sẽ thấy như sau:

Quay trở lại với cửa sổ Stack của hàm main(). Khi nhìn thấy có một khoảng không gian trống, nơi không có các biến tiếp giáp, thì đó có thể bên trên là một buffer (sau này, các bạn sẽ thấy các trường hợp trong đó không gian trống lại là một structure). Bây giờ, cuộn chuột lên một chút:

Ở đó ta thấy Buf (hoặc var_7C) là biến đầu tiên ở trên vùng trống, nhấn phải chuột và chọn Array:

Các bạn sẽ thấy IDA tự động phát hiện kích thước của Array = 120, tức là nó bao gồm 120 phần tử có kích thước 1-byte:

Sau khi chuyển đổi xong ta thấy biểu diễn của Stack lúc này trông tốt hơn trước:

Thanh ghi EBP được dùng làm Base, và nhớ rằng khi EBPESP bằng nhau thông qua lệnh MOV EBP, ESP, thì thanh ghi ESP sẽ được trừ đi giá trị 0x94 để dành không gian cho các biến được khai báo trong hàm và ESP lúc đó sẽ hoạt động ở phía trên khu vực các biến, tức là đỉnh của Stack.

Ta thấy khu vực làm việc của thanh ghi ESP vẫn còn sau khi thực hiện SUB ESP, 0x94.

Ở cột bên trái là -00000094, vì vậy nó là ESP = EBP-094. Rõ ràng sau đó nó sẽ tiếp tục tăng lên khi hoạt động giữa các hàm với nhau. Nhưng khi nó hoạt động bên trong hàm main() này và cho đến khi thoát khỏi hàm thì ESP sẽ làm việc từ 0x94 trở lên, bởi vì nó không can thiệp tới phần dành riêng cho các biến.

Tại chương trình này, khi chúng ta phân tích thông tin tại cửa sổ Stack của hàm main(), ta đang xem xét các biến trong hàm vì các tham số của hàm (argc, argv, vv) là đã biết:

Các bạn sẽ nhận thấy rằng var_4 là biến lưu COOKIE_SECURITY. Nó nhận giá trị đã được XOR với thanh ghi EBP và lưu lại vào biến trên Stack, mục đích là để bảo vệ chương trình khỏi lỗi Overflows. Vì vậy chúng ta tiến hành đổi lại tên biến này:

Quan sát sub_0x4011b0 tại địa chỉ 0x4010A0 bên dưới, ta có thể đoán được đây là hàm API printf() vì có một string được truyền vào cho hàm, cũng như trong quá trình thực thi chương trình ta đã thấy chuỗi này được in ra tại màn hình console:

Và đi sâu vào trong hàm sub_0x401040, ta có được thông tin sau:

Như vậy, chúng ta sẽ đổi tên sub_0x4011b0 thành printf():

Tiếp tục phân tích tiếp chương trình:

Như trên hình, ta thấy rằng biến Size được khởi tạo giá trị là 8 và không bao giờ thay đổi trong chương trình. Quan sát cụ thể hơn tại màn hình xrefs, biến này chỉ được đọc ra có hai lần, vì vậy chúng ta sẽ đổi tên Size thành Size_CONST_8:

Tiếp theo, bên dưới ta thấy lời gọi tới hàm gets_s() (là cải tiến của hàm gets()). Hàm này giới hạn ký tự tối đa mà bạn có thể nhập vào. Trong trường hợp này tối đa là 8 kí tự, được truyền qua lệnh PUSH EAX và sau đó là lệnh LEA để lấy địa chỉ của biến Buf hay Buffer mà ta đã tìm hiểu ở trước.

Thông tin về hàm các bạn có thể xem tại đường link sau https://msdn.microsoft.com/en-us/library/5b5x9wc7.aspx?f=255&MSPPError=-2147217396

Như vậy, ta biết rằng biến Buf sẽ lưu thông tin về tên User chúng ta nhập vào từ bàn phím và tối đa chỉ được 8 kí tự:

Địa chỉ của Buf sau đó sẽ được đưa vào thanh ghi EDX. lệnh PUSH EDX sau đó truyền địa chỉ của Buf như là tham số cho hàm API strlen(). Hàm API này sẽ lấy độ dài của chuỗi trong Buf tương ứng với chuỗi người dùng vừa nhập. Độ dài có được sẽ lưu vào biến var_90, do đó, chúng ta đổi tên var_90 thành len_USER:

Mũi tên màu xanh trên hình cho thấy một bước nhảy lùi, vậy có thể đây là một vòng lặp (Loop). Ta thấy biến var_84 được khởi tạo trước khi sử dụng, và được dùng để so sành với độ dài chuỗi tại địa chỉ 0x4010ef, bên dưới là lệnh nhảy có điều kiện để xem xét việc thoát khỏi vòng lặp. Thông thường, bộ đếm của một vòng lặp sẽ được khởi tạo bằng 0 và sẽ chỉ thoát khỏi vòng lặp khi bộ đếm này lớn hơn hoặc bằng với độ dài của len_USER đã gõ. Như vậy, có thể khẳng định biếnvar_84 chính là bộ đếm của vòng lặp, ta đổi tên nó thành COUNTER.

Bộ đếm này được tăng lên ở cuối vòng Loop:

Tại khối lệnh trên, nó copy giá trị của COUNTER vào thanh ghi EAX, sau đó tăng EAX lên 1 và lưu lại vào biến một lần nữa. Việc làm này tương đương với một lệnh ở ngôn ngữ bậc cao là COUNTER++

Trong khối lệnh bên trên, ta thấy nó thực hiện chuyển byte đầu tiên EBP + EDX + BUF của buffer vào EAX, bởi vì EBP + BUF được được công thêm với biến COUNTER hiện đang bằng 0 (nhưng sẽ tăng lên theo chu kỳ của vòng lặp). Thanh ghi EAX sau khi nhận từng giá trị kí tự trong Buf sẽ được cộng với biến var_88. Qua đó, ta kết luận đoạn code trên thực hiện việc cộng toàn bộ giá trị các kí tự ta nhập vào và lưu vào biến var_88 (biến này ban đầu được khởi tạo bằng 0). Nên nhớ rằng biến var_88 cuối cùng chỉ lưu kết quả ở dạng hexa.

Ở đây chúng ta gặp một câu lệnh mới là: MOVSX

Lệnh MOVSXMOVZX, hai lệnh này đều lấy 1 byte và chuyển vào một thanh ghi. MOVZX sẽ điền 0 vào bytes cao. MOVSX sẽ xem xét bit dấu, nếu là số dương, nhỏ hơn hoặc bằng 0x77 thì nó sẽ điền 0; còn nếu là số âm, 0x80 hoặc lớn hơn, thì nó sẽ điền 0xFF.

Xem xét các ví dụ:

MOVZX EAX, [XXXX]

Nếu giá trị lưu tại XXXX là 0x40, thanh ghi EAX sẽ có giá trị là 0x00000040.

Cũng có thể sử dụng MOVZX EAX, CL. Nó cũng tương tự như trên, chuyển giá trị tại CL vào EAX và điền 0 vào các byte cao.

MOVSX EAX, CL

Lệnh này sẽ quan tâm tới bit dấu, nếu CL là 0x40, EAX sẽ là 0x00000040 và nếu là 0x85 thì là số âm, EAX sẽ là 0xFFFFFF85.

Do chúng ta nhập các kí tự và số tại màn hình console nên chúng sẽ là các positive hex values, nên sẽ không gặp vấn đề gì. Đoạn code sẽ thực hiện cộng dồn lần lượt từng kí tự một, do đó ta đổi tên biến var_88 thành SUMMARY:

Tóm lại, vòng lặp ở đây làm nhiệm vụ tính tổng các kí tự ta đã nhập vào. Thực hiện chuyển các khối lệnh của vòng lặp này về cùng màu để dễ nhận biết:

Hoặc để cho gọn, bạn có thể thực hiện nhóm các khối lệnh này lại bằng cách nhấn Ctrl và chọn lần lượt các khối cần nhóm. Sau đó, nhấn chuột phải trên một khối và chọn Group Nodes. Cuối cùng đặt tên cho node sau khi nhóm lại. Ví dụ:

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

Trong quá trình phân tích, nếu chúng ta cần kiểm tra lại thông tin, ta có thể lựa chọn để Ungroup Nodes.

Sau khi in ra màn hình chuỗi USER đã nhập vào, chương trình tiếp tục yêu cầu ta cung cấp một PASSWORD:

Sau đó, chương trình lại gọi hàm gets_s() một lần nữa bằng cách sử dụng lại cùng biến BufSize ở trên:

Ta hoàn toàn có thể sử dụng lại cùng Buffer để lưu thông tin Password, vì sau khi thực hiện tính toán xong thì ta không còn sử dụng tới chuỗi User nữa.

Sau khi có được Password nhập vào và lưu tại Buf, Password này sẽ được chuyển đổi sang dạng Hexadecimal như đã thấy ở ví dụ trước thông qua hàm atoi() và lưu vào biến var_94. Do vậy, tôi đổi tên biến var_94 thành PASSWORD_HEX:

Tiếp theo, gặp hàm sub_401010. Hàm này sẽ nhận hai tham số truyền vào, một là PASSWORD_HEX thông qua lệnh PUSH EDX và hai là SUMMARY (tổng các kí tự trong chuỗi User) thông qua lệnh PUSH EAX. Đi sâu vào trong hàm này để phân tích:

Khi vào trong hàm, các bạn thấy hàm có hai tham số, rõ ràng là tham số bên dưới sẽ là PASSWORD_HEX vì nó được truyền vào đầu tiên được thông qua lệnh PUSH và tham số còn lại sẽ là SUMMARY. Ta đổi tên lại các tham số cho phù hợp như sau:

Sau khi đổi tên các tham số, ta chọn sub_0x401010, nhấn chuột phải và chọn Set Type (hoặc nhấn phím tắt là Y).

Theo đó, IDA sẽ cố gắng khai báo lại hàm cùng với các tham số của hàm sao cho tường minh nhất, đồng thời ta cũng đổi luôn tên hàm thành Check() như hình:

Nếu quay ngược trở lại hàm main(), các bạn sẽ thấy IDA bổ sung thêm thông tin tại các lệnh PUSH như sau:

Vậy hàm Check chúng ta vừa đổi tên ở trên thực hiện công việc gì?

Để ý thấy rằng hàm có sử dụng lệnh so sánh CMP, và trước khi thực hiện so sánh nó copy PASSWORD vào thanh ghi EAX và thực hiện lệnh SHL EAX, 1

Lệnh SHL là lệnh dịch bit sang trái đi n bit. Trong trường hơp này của chúng ta là dịch 1 bit, tương đương với việc lấy giá trị của EAX nhân với 2 và lưu lại vào EAX.

Tổng hợp lại, toàn bộ hàm Check thực hiện việc lấy giá trị PASSWORD nhập vào, đem nhân với 2, được bao nhiêu đem so sánh với tổng các ký tự của USER.

Để chuyển đổi một kí tự sang hệ thập phân, trong IDA ta làm như sau:

Dựa vào đó, ta thực hiện việc tính tổng cho tất cả các ký tự của chuỗi old_man mà ta sẽ sử dụng nó như là USER nhập vào:

Tổng có được là 0x2da. Như đã tổng kết ở trên, password nhập vào sẽ được nhân 2 trước khi đem đi so sánh với giá trị tổng (ví dụ là: 0x2da vừa tính). Do đó, mật khẩu chính xác mà ta cần phải nhập vào là một giá trị sau khi nhân 2 phải bằng 0x2da. Ta có biểu thức như sau:

X*2=0x2da; X is password

Giải phương trình trên: X=0x2da/2, kết quả có được X là một số ở dạng thập phân (Số này sẽ được chuyển đổi thành Hexa khi đi qua hàm atoi trong hàm main())

Vì vậy, nếu tôi gõ tên của người dùng là old_man và password là 365, điều gì sẽ xảy ra?

Ta thấy, trong hàm Check sử dụng lệnh so sánh không bằng để đưa ra quyết định rẽ nhánh thực hiện:

Nếu không bằng nhau ta sẽ đi đến khối màu đỏ, thực hiện xóa thanh ghi AL về 0. Còn nếu bằng nhau sẽ đi tới khối màu xanh lá cây và thiết lập thanh ghi AL là 1. Quan sát xem với thiết lập kết quả trả về ở thanh ghi AL thì chương trình sẽ làm gì:

Như trên hình, giá trị của AL được lưu vào biến var_7D, sau đó giá trị biến này được gán cho thanh ghi ECX để kiểm tra. Ta đổi tên biến này thành SUCCESS_FLAG:

Nếu biến này bằng 0 thì thông báo “Bad reverser..” sẽ hiển thị. Ngược lại nếu biến này là 1, thông báo “Good Reverser” sẽ hiển thị.

Như vậy, toàn bộ phần 12 đến đây là kết thúc. Tôi muốn các bạn dành thời gian thực hành thử debug chương trình này và kiểm tra mọi thứ chúng ta đã reversed thông qua việc đặt các breakpoints, quan sát các giá trị trong từng trường hợp cho đến khi tới bước so sánh cuối cùng.

Image result for too long and crazy funny

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

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


Tôi không muốn nhồi nhét quá nhiều lý thuyết ngay từ đầu, vì vậy tôi đã lồng ghép và xen kẽ một số bài thực hành để các bạn không có cảm giác nhàm chán. Tuy nhiên, không vì thế mà chúng ta bỏ qua các kiến thức cơ bản, do đó trước khi tiếp tục các phần nâng cao hơn, phần này tôi sẽ cùng với các bạn xem xét một số cờ quan trọng trong ASM.

FLAGS

CARRY FLAG (CF)

Chúng ta đã tìm hiểu hoạt động của cờ CF (cờ nhớ) trong phần trước. Cờ này được kích hoạt trong quá trình tính toán của các số unsigned. Khi kết quả là số âm như ta đã gặp ở bài trước hoặc vượt quá mức biểu diễn tối đa trong trường hợp phép cộng. Hay nói cách khác, cờ CF được thiết lập là 1 khi có nhớ từ bit msb trong phép cộng hoặc có vay vào bit msb trong phép trừ.

Hãy quan sát các ví dụ sau thông qua trình debugger. Thực thi crackme.exe trong IDA thông qua debugger, ta dừng lại tại Entry Point của crackme (như đã cấu hình ở phần trước). Tại đây ta tiến hành patch lệnh như sau:

Khi patch như trên, chế độ graph của IDA sẽ bị lỗi, IDA tự động chuyển đổi về chế độ text. Để chuyển lại về chế độ graph, ta nhấn chuột phải tại địa chỉ entry point và chọn Create Function, sau đó nhấn phím space bar để chuyển lại về chế độ graph ban đầu. Tiếp theo, đặt lại giá trị cho thanh ghi EAX = 0xffffffff bằng cách nhấn chuột phải tại thanh ghi này và chọn Modify Value (E):

Sau khi thay đổi xong, nhấn F8 trace qua lệnh đã patch để thực hiện lệnh add này. Quan sát ta sẽ thấy cờ CF được kích hoạt do phép cộng bị tràn vượt quá số dương lớn nhất:

Điều tương tự xảy ra nếu chúng ta thực hiện patch lệnh bên dưới thành SUB EAX, EDX:

Sau đó, thay giá trị của EAX thành 0x25EDX thành 0x40. Nhấn F8 để thực hiện lệnh và quan sát xem liệu CF có được kích hoạt hay không? Chúng ta đã thấy rằng, nếu trừ hai số dương cho nhau và kết quả là số âm (Signed = -27 ~ 0xFFFFFFE5), cờ CF sẽ được bật lên:

Như vậy là cờ CF đã được kích hoạt. Nếu tôi tiếp tục cho thực hiện phép trừ này một lần nữa bằng cách nhấn chuột phải tại lệnh SUB và chọn Set IP, nhưng lần này tôi thay giá trị của thanh ghi EAX thành 0x100, giá trị của EDX vẫn giữ nguyên.

Tiếp theo F8 để trace qua lệnh:

Ta thấy cờ CF không được kích hoạt.

Như vậy, các quy tắc để bật cờ CF (carry) trong phép toán nhị phân/số nguyên là:

  • Cờ carry được bật nếu phép cộng hai số dẫn đến bit có trọng số lớn nhất bị đẩy ra ngoài – vượt quá khả năng biểu diễn. Ví dụ: 1111 + 0001 = 0000 (CF được bật)
  • Cờ carry được bật nếu phép trừ hai số dẫn tới việc cần phải vay vào bit có trọng số lớn nhất để trừ. Ví dụ: 0000 – 0001 = 1111 (CF được bật)

Trường hợp ngược lại, cờ CF không được bật (bằng 0):

  • 0111 + 0001 = 1000 (carry flag thiết lập bằng 0)
  • 1000 – 0001 = 0111 (carry flag thiết lập bằng 0)

OVERFLOW FLAG (OF)

Cờ OF (cờ tràn) cũng tương tự như cờ CF, nhưng đối với các tính toán liên quan đến số có dấu (signed). Cờ OF được thiết lập 1 khi xảy ra tràn, ngược lại nó bằng 0. Hiện tượng tràn gắn liền với một sự thật là phạm vi của các số biểu diễn trong máy tính có giới hạn. Ví dụ, phạm vi của các số thập phân có dấu có thể biểu diễn bằng một word 16 bit là từ -32768 đến 32767, với một byte 8 bit thì phạm vi là từ -128 đến 127. Đối với các số không dấu thì phạm vi từ 0 tới 65535 cho một word và từ 0 đến 255 cho một byte. Nếu kết quả của một phép tính nằm ngoài phạm vi thì hiện tượng tràn sẽ xảy ra và kết quả nhận được bị cắt bớt sẽ không phải là kết quả đúng. Xem xét một vài ví dụ dưới đây.

Giờ ta thay đổi EIP về lại câu lệnh ADD EAX, 1 và đặt lại giá trị của thanh ghi EAX thành 0x7fffffff (signed: 2147483647):

Sau đó nhấn F8 để thực hiện lệnh:

Chúng ta thấy rằng cờ OF đã được kích hoạt sau khi thực hiện lệnh. Đó là vì khi cộng 1 vào số dương 0x7fffffff, nếu xem xét đây là thao tác tính toán với số có dấu (signed) thì sẽ khiến kết quả sau khi thực hiện là số âm nhỏ nhất (signed: -2147483648) và dẫn đến kết quả sai:

Nếu thực hiện phép trừ EAX cho EDX với các giá trị như trên:

Cờ OF vẫn sẽ được kích hoạt bởi vì khi lấy số âm nhỏ nhất là 0x80000000 trừ đi 0x40 cho kết quả là một giá trị dương rất lớn (0x7FFFFFC0) và khiến cho kết quả của phép toán sai. Do đó, chúng ta có thể kết luận rằng cờ OF được kích hoạt khi có lỗi xảy ra trong quá trình tính toán với dấu. OF được bật khi bit có trọng số cao nhất (được xem là bit dấu) bị thay đổi bằng cách cộng hai số có cùng dấu (hoặc trừ hai số có dấu ngược nhau). Tràn không bao giờ xảy ra khi các dấu của hai toán hạng cộng là khác nhau (hoặc dấu của hai toán hạng trừ là giống nhau).

Như vậy, một số quy tắc để bật OF (overflow) trong phép toán nhị phân/số nguyên là:

  • Nếu tổng của hai số với bit dấu tắt tạo ra kết quả là một số với bit dấu bật, cờ “overflow” sẽ được bật. Ví dụ: 0100 + 0100 = 1000 (OF được bật)
  • Nếu tổng của hai số với bit dấu bật tạo ra kết quả là một số với bit dấu tắt, cờ “overflow” sẽ được bật. Ví dụ: 1000 + 1000 = 0000 (OF được bật)

SIGN FLAG (SF)

Cờ này khá đơn giản, nó được kích hoạt khi kết quả của việc tính toán là số âm, trong mọi trường hợp. Nó chỉ quan tâm tới kết quả của dấu mà không cần quan tâm kết quả tính toán đúng hay sai. Hay nói cách khác, cờ SF (cờ dấu) được thiết lập 1 khi bit msb của kết quả bằng 1, có nghĩa là kết quả là âm nếu ta làm việc vơi số có dấu.

Ví dụ như sau:

Kết quả của0x8000000 cộng 0x1 vẫn nằm trong dải số âm, là 0x8000001, vì vậy SF được kích hoạt. Chúng ta cũng thấy rằng OFCF không được kích hoạt vì không có lỗi trong quá trình tính toán của cả signed hoặc unsigned.

Rõ ràng, bộ xử lý khi thực hiện một lệnh liên quan tới tính toán hai thanh ghi, nó không hề biết các thanh ghi này là signed hay unsigned. Còn chúng ta có thể biết được là bởi vì ta thấy các lệnh nhảy có điều kiện ở phía dưới, ngược lại bộ xử lý không biết, do đó, trong bất kỳ hoạt động nào nó cũng sẽ xem xét các lệnh như thể là signed hoặc unsigned tại cùng một thời điểm và thay đổi các cờ cần thiết.

Vì các lệnh nhảy có điều kiện phụ thuộc vào cờ, chương trình sẽ nhìn vào kết quả của các cờ CF (trong phép tính unsigned) hoặc OF (trong phép tính signed) để từ đó đưa ra quyết định nhảy. Ví dụ, nếu có một lệnh JB (lệnh này là unsigned), do đó nó sẽ chỉ nhìn vào cờ CF và không quan tâm đến cờ OF ngay cả khi cả hai cờ này đều được kích hoạt.

Như vậy, trách nhiệm thuộc về người lập trình, người có quy ước về kết quả. Nếu đang làm việc với số có dấu thì chỉ có cờ OF đáng quan tâm trong khi cờ CF có thể bỏ qua, ngược lại khi làm việc với số không dấu thì cờ quan trọng là CF chứ không phải là OF.

ZERO FLAG (ZF)

Cờ này không phụ thuộc vào dấu

Nó được kích hoạt khi:

  • Phép so sánh (sử dụng một phép trừ) khi cả hai toán hạng đều bằng nhau.
  • Khi tăng hoặc giảm và kết quả là bằng không, hoặc trong một phép trừ mà kết quả có được bằng 0.

Chúng ta có thể chứng minh điều đó:

Ta thiết lập EAX có giá trị 0xffffffff và cộng thêm 1 vào EAX. Điều gì sẽ xảy ra?

Chúng ta thấy rằng cờ ZF được kích hoạt vì kết quả bằng 0 và nếu ta xem xét cả số unsigned thì cờ CF cũng được kích hoạt vì có tràn khi cộng 1 vào số dương lớn nhất. Trong khi đó, cờ OF không được kích hoạt bởi vì cả hai đều là số có dấu, -1 + 1 = 0 và không có lỗi. Cờ SF cũng không kích hoạt vì kết quả không phải là số âm.

Chúng ta thấy trạng thái các cờ rất quan trọng. Hãy xem liệu lệnh nhảy có điều kiện kế tiếp có xảy ra hay không? Ta patch lệnh SUB EAX, EDX như trên và bên dưới là lệnh nhảy JB 0x401018:

Sau đó gán EAX = 0x40EDX = 0x2, nhấn F8 để thực hiện lệnh SUB:

Mũi tên màu đỏ sẽ nhấp nháy vì thanh ghi EAX lớn hơn thanh ghi EDX, do đó lệnh nhảy sẽ không thực hiện. Ta quan sát các cờ.

JB là một lệnh nhảy unsigned và chỉ nhảy nếu cờ CF được kích hoạt. Rõ ràng ở đây cờ CF không được kích hoạt vì quá trính tính toán là chính xác giữa hai số dương cho ra kết quả là số dương, có nghĩa số đầu tiên lớn hơn số thứ hai, như vậy sẽ không thực hiện nhảy.

Nhưng nếu chúng ta thay đổi EAX thành 0x40EDX thành 0x80 và thử lặp lại lệnh trừ một lần nữa:

Trong trường hợp này, vì EAX nhỏ hơn EDX, lệnh nhảy JB sẽ được thực hiện và đi theo hướng của mũi tên màu xanh lá cây.

Vì khi lệnh JB nhìn vào cờ CF, nó sẽ nhảy vì cờ CF đã được bật. Vì kết quả của một thao tác unsigned là số âm và đã gây ra lỗi. Cờ SF cũng được kích hoạt vì kết quả là âm, còn cờ OF không được kích hoạt.

Lệnh JB sẽ nhảy căn cứ theo trạng thái của cờ CF nhưng nếu tôi thay đổi thành lệnh JL.

Trong trường hợp này, hướng thực hiện thay đổi và đi theo mũi tên màu xanh lá cây bởi vì toán hạng đầu tiên nhỏ hơn toán hạng thứ hai, nhưng lệnh nhảy JL căn cứ vào cờ nào?

Chúng ta thấy rằng lệnh JL sẽ thực hiện nếu cờ SF khác 0. Trong trường hợp này cờ SF = 1, do vậy sẽ nhảy và cũng là logic vì toán hạng đầu tiên nhỏ hơn toán hạng thứ hai. Lệnh SUB tương tự cách hoạt động của lệnh so sánh CMP, chỉ khác là SUB lưu kết quả còn CMP thì không.

Kết luận của bài viết này là không nhất thiết phải nhìn vào cờ để biết điều gì sẽ xảy ra với các nhảy có điều kiện, nó thuộc về hoạt động nội bộ bên trong. Chúng ta chỉ cần nhớ rằng, nếu hai toán hạng bằng nhau, lệnh JZ sẽ thực hiện. Nếu toán hạng đầu nhỏ hơn và là unsigned, thì sẽ nhảy nếu nó là lệnh JB. Còn nếu toán hạng đầu nhỏ hơn nhưng ở kiểu signed thì sẽ nhảy nếu là lệnh JL. Như vậy, ta chỉ cần quan sát cột thứ ba trong bảng signedunsigned là đủ. Tuy nhiên, sẽ vẫn là tốt hơn nếu chúng ta có cái nhìn chi tiết hơn :P.

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

Image result for we dig deeper funny

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


Trong phần này tôi và các bạn sẽ tìm hiểu về tính năng Debugger của IDA. Bản thân IDA hỗ trợ nhiều kiểu Debuggers, để xem cách hoạt động của tính năng này chúng ta mở Crackme Cruehead gốc trong IDA.

Bên lề: Trường hợp IDA hiển thị như hình dưới có nghĩa là đã có một file database trước đó được lưu rồi, ta chọn Overwrite nếu muốn tạo ra một cơ sở dữ liệu mới và ghi đè lên file cũ, lúc đó IDA sẽ tiến hành phân tích toàn bộ file lại từ đầu và tạo một file .idb mới.

Tại màn hình Load a new file… của IDA bỏ chọn Manual load, nếu xuất hiện các cửa sổ khác thì cứ nhấn OK cho đến khi IDA load xong file. Sau khi IDA phân tích xong crackme, lựa chọn Debuggers bằng cách truy cập menu Debugger > Select Debugger như hình:

Các bạn sẽ thấy có nhiều tùy chọn Debugger khác nhau, ta sẽ tìm hiểu dần dần khi có điều kiện. Ở bài viết này chúng ta sẽ lựa chọn Local Win32 debugger để bắt đầu (có thể lựa chọn trình debugger mặc định mỗi khi load một file mới bằng các tích chọn Set as default debugger).

Qua đây, các bạn có thể thấy rằng, IDA không chỉ là một Loader với khả năng static disassembler rất tốt, cung cấp khả năng tương tác mạnh mẽ, mang lại hiệu qủa trong quá trình dịch ngược, mà còn hỗ trợ thêm cả việc debug nhằm hoàn thiện hơn. Tuy nhiên, cách hoạt động của debugger trong IDA sẽ khác một chút so với những gì các bạn đã làm quen với OllyDbg/ x64dbg.

Trong Debuggers > Debuggers options chúng ta có các thiết lập như sau:

Lựa chọn Suspend on process entry point để trình debugger dừng lại tại entry point của crackme. Chúng ta sẽ thực hiện những thay đổi như đã thực hiện trong phần trước. Trước tiên ta sẽ đổi màu các khối và đặt lại tên cho các sub_ như sau:

Sau đó, tại địa chỉ 0x401243 chúng ta đặt một Breakpoint tại lệnh nhảy (JZ) bằng cách nhấn F2 tại địa chỉ này. Tiếp theo, ta chuyển tới một lệnh nhảy khác mà chúng ta cũng đã phân tích trong phần trước, đặt một breakpoint tại lệnh nhảy này đồng thời đổi màu các khối tương tự như hình dưới đây:

Sau khi thiết lập xong, ta thực hiện debug bằng cách chọn Debugger > Start Process hoặc nhấn phím tắt là F9, cửa sổ cảnh báo sẽ xuất hiện như hình:

Cửa sổ cảnh báo trên sẽ luôn luôn xuất hiện khi chúng ta thực hiện debug một tệp thực thi trên máy local của mình. Bởi khi phân tích file bằng Loader của IDA, file đó sẽ không bao giờ được thực thi trên máy chúng ta, nhưng bây giờ nếu file được chạy để phục vụ debug, thì IDA sẽ đưa ra cảnh báo nhắc nhở phải cẩn thận khi thực hiện, vì có khả năng là đó một Virus hay cái gì đó có có thể gây nguy hiểm tới máy tính của chúng ta. Khi đó, ta cần sử dụng tính năng Remote Debugger và thực thi file trong một môi trường ảo hóa hoặc debug hoàn toàn bằng máy ảo. Việc sử dụng tính năng remote debugging như thế nào tôi sẽ cố gắng đề cập trong một bài viết khác.

Do ta đã biết Crackne này hoàn toàn sạch nên nhấn YES để tiếp tục quá trình debug:

Vì chúng ta lựa chọn để dừng lại tại Entry Point, do vậy debugger đã dừng lại tại EP của crackme, tại địa chỉ 0x401000. Nếu bạn nhấn space bar, IDA sẽ chuyển qua chế độ đồ họa như ta đã làm việc ở LOADER. Tiếp theo, các bạn có thể bố trí lại các cửa sổ theo ý thích, thu hẹp lại cửa sổ Output Window, mở rộng cửa số Stack view, thiết lập cửa sổ General registers để quan sát được các thanh ghi cũng như trạng thái của các cờ như hình dưới:

Khi có được một màn hình bố trí hợp lý và phù hợp, ta sẽ lưu nó lại như một thiết lập mặc định. Tại IDA, vào Windows > Save Desktop, tích chọn Default như hình dưới. Làm như thế này, bất cứ khi nào ta thực hiện Debugger, IDA sẽ luôn nạp các thiết lập ta đã đặt mặc định và nếu chúng ta muốn thay đổi nó một lần nữa, ta có thể làm lại mà không có vấn đề gì:

Bên dưới cửa sổ thanh ghi là cửa sổ chứa thông tin về Stack của chương trình:

Ta có màn hình IDA-View EIP là nơi hiển thị toàn bộ các lệnh của chương trình và bên dưới là cửa sổ Hex View hay Hex Dump, hiển thị thông tin của bộ nhớ ở chế độ xem hexa:

Quan sát tại phần dưới của màn hinh disassembly chúng ta thấy các thông tin sau:

Đây là các thông tin về File Offset (Offset của file thực thi khi lưu trên disk) và địa chỉ bộ nhớ (Virtual Address khi file được load vào memory). Nếu chúng ta mở crackme bằng một trình HexEditor, ví dụ như HxD, ta sẽ thấy tại File Offset là 0x600 có cùng opcode như đã thấy tại màn hình disassembly:

Chúng ta đã biết rằng, phím tắt G được sử dụng để đi đến một địa chỉ bộ nhớ bất kỳ, nếu bạn ấn G và nhập vào địa chỉ 0x401389:

Ta sẽ đi đến chỗ đã thiết lập breakpoint. Vào Options > General, cấu hình lại Number of opcode bytes (graph)0:

Trong cửa sổ View > Open Subview > Segments, chúng ta thấy có ba segments được nạp bởi Loader, trong đó CODE segment được nạp vào 0x401000, tiếp theo là DATA segment và .idata segment:

Bất kỳ sự thay đổi nào chúng ta thực hiện với ba segments này đều được lưu lại vì chúng được nạp bởi IDA Loader, nhưng các thay đổi bên ngoài ba segments này sẽ bị mất bởi vì chúng chỉ là các mô-đun nạp bởi Debugger và sẽ không được lưu vào cơ sở dữ liệu của IDA. Như vậy, các mô-đun mà chúng ta đang phân tích và muốn debug phải nằm trong Loader hoặc có xuất hiện chữ L ở đó, rõ ràng chúng cũng sẽ được tải trong Debugger, nhưng có L đồng nghĩa là chúng sẽ ở trong cả hai chế độ và ta vừa có thể phân tích tĩnh tại Loader và vừa debug được bằng Debugger mà không bị mất thông tin.

Một trong những thanh công cụ mà tôi thấy luôn luôn hữu ích là Jump:

IDA sẽ bổ sung thêm hai nút như hình dưới, tương tự như ta thấy ở các trình web browser. Sau khi cho hiển thị toolbar này, ta lưu lại màn hình Windows > Save Desktop.

Thanh công cụ này cho ta khả năng quay trở lại hoặc chuyển tới màn hình đã làm việc lúc trước một cách rất thuận tiện. Nhấn mũi tên , ta sẽ quay lại màn hình entry point của crackme mà ta đã break trước đó:

Cũng ngay tại menu Debugger, ta có thể xem được danh sách các Breakpoints đã đặt thông qua Breakpoint list (Ctrl+Alt+B):

Và đi tới bất kỳ bp nào mà ta muốn bằng cách nhấp đúp vào bp đó, ví dụ:

Lúc này, ta đang dừng lại tại Entry Point và có hai Breakpoints đã thiết lập trước đó, do vậy ta có thể nhấn F9 để thực thi crackme:

Khi crackme đã thực thi hoàn toàn, vào Help > Register và nhập thông tin bất kì, ví dụ:

Sau đó nhấn OK, ta sẽ dừng lại ở bp tại địa chỉ 0x40138B:

Mũi tên bên trái lúc này đang nhấp nháy, nó báo hiệu cho ta biết nơi vùng code tiếp theo sẽ tiếp tục thực hiện. Quan sát tại cửa sổ Registers, chúng ta thấy rằng thanh ghi EAX đang có giá trị 0x6D:

Nếu tôi gõ chr (0x6D) tại thanh Python của IDA, kết quả trả về cho tôi chữ cái m trong chuỗi “manowar”.

Cũng tại thanh Python, làm tương tự như trên ta thấy với chr(0x41) sẽ cho kết quả là chữ A. Như vậy, kí tự này sẽ được so sánh xem liệu nó có thấp hơn giá trị 0x41 (‘A’) không?

Bên cạnh đó, IDA cũng cung cấp cách biểu diễn khác hiển thị rõ ràng hơn, bằng cách một nhấp chuột phải vào giá trị 41h tại màn hình disassembly, trong số các tùy chọn mà chúng ta có thể lựa chọn giống hình dưới, ta thấy có xuất hiện ‘A’:

Làm tương tự với 0x5A, ta có được chữ cái ‘Z’. Chúng ta thấy nó so sánh giá trị 0x6D với AZ. Tại thời điểm này, chưa phải lúc chúng ta tìm cách giải quyết hoàn toàn crackme này, nhưng các bạn thấy rằng 0x6D lớn hơn 0x41, do vậy ta sẽ không đi đến khối màu đỏ – thực hiện hiển thị thông báo lỗi. Rõ ràng, nó sẽ nhảy vào khối đỏ nếu nó thấp hơn, nhưng nó cũng có thể được đánh giá bằng cách quan sát trạng thái của các cờ.

Trong hình trên, chúng ta thấy lệnh nhảy JB nhảy theo mũi tên màu xanh lá cây trong IDA nếu nhỏ hơn. Khi thực hiện lệnh so sánh, cờ C (còn gọi là CF hoặc C) sẽ được kích hoạt, theo đó lệnh nhảy sẽ nhảy nếu cờ C = 1. Quan sát các cờ trong IDA:

Do cờ C = 0 nên lệnh nhảy sẽ không được thực hiện và nó sẽ theo hướng của mũi tên đỏ. Vậy điều kiện tính toán sẽ thế nào để cho cờ Carry Flag được kích hoạt?

Carry Flag cung cấp cho chúng ta thông tin rằng có gì đó đã sai trong một phép toán giữa các số nguyên unsigned. Nếu tôi làm phép trừ giống như cách thực hiện của lệnh CMP (không lưu lại kết quả), phép tính 0x6D-0x41 cho kết quả là 0x2C, đây là một số dương và sẽ không có vấn đề gì. Tuy nhiên, nếu ví dụ, giá trị của tôi là 0x30, bằng cách trừ đi 0x41, kết quả sẽ là -0x11:

Đây là một giá trị âm và không được chấp nhận là kết quả của một phép toán số dương, vì nếu bạn tiếp tục làm việc ở hệ thập lục phân:

Kết quả có được là 0xFFFFFFEF và đây sẽ là một số dương có giá trị rất lớn 4294967279 và không cách nào để thực hiện phép trừ 0x30 – 0x41 ra kết quả bằng 0xFFFFFFEF J.

Làm thế nào để chúng ta biết được cần phải quan tâm đến dấu của kết quả khi thực hiện một phép toán?

Điều này phụ thuộc vào lệnh nhảy, trong trường hợp này JB là một lệnh nhảy được sử dụng sau khi so sánh các số nguyên không dấu (unsigned). Đối với các phép toán giữa các số nguyên có dấu (signed) sẽ sử dụng lệnh JL. Ví dụ, nếu tôi so sánh số 0xFFFFFFFF với 0x40. Trong một lệnh nhảy không quan tâm đến dấu thì rõ ràng là số này lớn hơn, nhưng nếu nó là một bước nhảy nơi mà dấu cần được xem xét, lúc đó 0xFFFFFFFF sẽ là -1 và nó sẽ nhỏ hơn 0x40.

Vì vậy, để đánh giá liệu so sánh sử dụng dấu hay không, chúng ta phải xem xét lệnh nhảy tiếp theo để đưa ra quyết định.

Nếu lệnh nhảy là bất kỳ trong số trên, nó được xem xét là Without Sign, còn nếu thuộc danh sách trong bảng dưới thì được xem xét là With Sign.

Các bạn thấy lệnh nhảy JE (nhảy nếu hai toán hạng bằng nhau) đều xuất hiện ở cả hai bảng vì trong trường hợp đó dấu không còn quan trọng nữa. Nếu hai toán hạng bằng nhau, nó sẽ được kích hoạt bằng cách đặt cờ ZF là 1. Chúng ta cũng thấy rằng lệnh JG (nhảy nếu lớn hơn) trong bảng tổng hợp nhảy có dấu. Cùng một mục đích nhảy như thế là lệnh JA, nhưng trong bảng tổng hợp nhảy không dấu.

Trong quá trình phân tích hàng ngày chúng ta sẽ thường xuyên phải quan sát trạng thái của các cờ, và nếu thấy một lệnh nhảy JB thì ta sẽ biết rằng đó là một so sánh giữa các số dương hay các số nguyên không dấu, và nếu toán hạng đầu tiên là nhỏ hơn thì nó sẽ nhảy.

Nếu tiếp tục thực thi chương trình, bạn sẽ thấy nó liên tục dừng lại ở breakpoint đã đặt, từ đó sẽ thấy rằng ta đang ở một vòng lặp thực hiện đọc từng kí tự của chuỗi tên và so sánh với 0x41, nếu có một kí tự nhập vào thấp hơn thì crackme sẽ hiển thị thông báo lỗi. Tuy nhiên, vừa rồi tôi nhập đều là các chữ cái (manowar) trong ô Name nên sẽ không xảy ra việc hiển thị thông báo lỗi này. Nhưng thử dừng quá trình debug lại và tiến hành debug lại từ đầu, lúc này tôi nhập tên là 22ricnar và key là 98989898:

Nhấn OK, ta lại dừng lại tại breakpoint tại địa chỉ 0x40138B:

Bây giờ, ta thấy rằng kí tự đầu tiên là 0x32 tương ứng với số 2 trong chuỗi 22ricnar. Vì 0x32 nhỏ hơn 0x41, mũi tên màu xanh lá cây sẽ nhấp nháy, tức là lệnh nhảy sẽ được thực hiện, cờ C được kích hoạt bởi vì khi lấy 0x32 trừ đi 0x41 trong một phép trừ unsigned, kết quả có được là số âm, đó là một lỗi sẽ kích hoạt cờ C.

Nếu nhấn chuột phải tại cờ C, ta có thể thiết lập nó về 0:

Khi thiết lập xong thì đồng thời mũi tên đỏ cũng nhấp nháy vì chúng ta vừa mới thay đổi lại điều kiện nhảy.

Nếu chúng ta cho Run tiếp, nó sẽ lại dừng lại tại breakpoint khi kiểm tra kí tự tiếp theo cũng là 2 trong chuỗi 22ricnar, và mũi tên màu xanh lá cây sẽ lại nhấp nháy một lần nữa. Làm tương tự như trên, ta đảo ngược lại cờ CF, thiết lập nó là 0. Những lần break tiếp theo tại lệnh nhảy này tương ứng với chuỗi ricnar, các kí tự từ lúc này trở đi đều lớn hơn 0x41, do đó sau phép so sánh sẽ không kích hoạt cờ CF và crackme sẽ rẽ nhánh theo mũi tên đỏ.

Sau khi vượt qua được quá trình kiểm tra từng kí tự trong chuỗi tên chúng ta dừng lại tại lệnh nhảy thứ hai (đã đặt breakpoint):

Tại đây, ta thấy crackme thực hiện so sánh xem EAX và EBX có bằng nhau không? Chuyển qua cửa sổ Registers, quan sát thấy giá trị của hai thanh ghi là khác nhau, do đó mũi tên đỏ nhấp nháy để báo hiệu sẽ rẽ nhánh vào đoạn code thông báo lỗi:

Do giá trị của hai thanh ghi EAXEBX không bằng nhau cho nên cờ Z không được bật:

Nếu ta thay đổi giá trị của cờ ZF, crackme sẽ rẽ nhánh theo hướng mũi tên màu xanh lá cây để tới vùng code hiển thị Good Work. Nhấn chuột phải tại cờ ZF và chọn Increment Value:

Nhấn F9 để thực thi, ta có được kết quả như sau:

Toàn bộ quá trình thực hiện ở trên cùng đạt một mục đích tương tự như khi chúng ta thực hiện patch crackme này, chỉ khác ở chỗ là ta không thay đổi code của chương trình, hoàn toàn công việc chỉ là thay đổi trạng thái các cờ trong quá trình debug.

Ngoài ra, có một cách khác là thay đổi thanh ghi EIP để nó trỏ tới lệnh tiếp theo được thực hiện. Ví dụ, giả sử ta đang dừng tại lệnh nhảy tại địa chỉ 0x401243, lúc này lựa chọn địa chỉ 0x40124c và nhấn chuột phải và chọn Set IP (hay nhấn phím tắt là CTRL + N):

Lúc đó, chương trình sẽ tiếp tục thực hiện lệnh từ địa chỉ 0x40124c giống như ta đã làm khi thực hiện thay đổi các cờ.

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

Image result for it's time to stop posting

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


Ở 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 xut phát t PUSH EBP là lnh đu tiên ca 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


Bên lề: Trong các phần trước, tôi đã giới thiệu tới các bạn nhưng câu lệnh Asm chính để các bạn hiểu được vì sao Assembly là ngôn ngữ của “Reverse Engineering”. Đây không phải là khóa học về Asm nên tôi không thể và cũng không đủ khả năng để đề cập được hết tất cả các lệnh. Trong quá trình tìm hiểu, không biết lệnh nào các bạn hãy chủ động tìm kiếm thêm. Còn ở phần này, chúng ta sẽ tìm hiểu tiếp về LOADER của IDA.

Với nhiều người, thông thường khi chưa có nhiều kinh nghiệm về static reversing, khi phân tích một chương trình chúng ta sẽ bổ sung thêm các ghi chú, gán nhãn hay đổi tên một số thứ (hàm, biến, tham số) để có được một cái nhìn tốt hơn và phần lớn chúng ta đều nhờ tới sự hỗ trợ của trình gỡ lỗi (debugger). Tuy nhiên, việc thực hành nhiều hơn với static reversing có thể sẽ giúp ta nâng cao khả năng đọc code và phân tích các chương trình mà không cần hoặc gần như ít khi phải sử dụng tới việc debug.

Nói chung, các chương trình sẽ rất lớn và phức tạp, do vậy chúng ta có thể sẽ không reverse hoàn toàn chương trình mà chỉ là một hoặc vài hàm mà ta quan tâm và cần cho mục đích của mình. IDA là một công cụ cho phép ta tương tác để có được kết quả reversing tốt nhất mà gần như không có công cụ nào làm được hoặc sẽ gặp nhiều khó khăn hơn (đây cũng chỉ là một ý kiến nhận định chủ quan mà thôi). Tuy nhiên, để thực hiện được điều này còn phụ thuộc vào kinh nghiệm riêng của từng người và có nhiều người cũng không cần phải sử dụng đến IDA.

Đầu tiên, không có cách nào khác ngoài việc cần phải thực hành thường xuyên, liên tục và nâng cao hiệu quả trong quá trình sử dụng IDA. Nếu bạn thất bại hoặc không đạt được kết quả tốt thì cần phải thực hành nhiều hơn nữa. IDA vốn đã không phải là một công cụ dễ sử dụng, nó có rất nhiều tính năng, có nghĩa là mỗi ngày chúng ta có thể học những thứ mới hơn khi chúng ta sử dụng nó.

Phân tích crackme CrueHead

Trong phần này, chúng ta sẽ bắt đầu quá trình phân tích tĩnh với crackme của CRUEHEAD. Điều quan trọng không phải là ngay lập tức giải quyết crackme này mà là cách ta làm quen với LOADER của IDA và nâng cao sự tự tin của chúng ta trong quá trình phân tích.

Sau khi để IDA phân tích xong crackme, truy xuất menu View > Open Subview > Segments, chúng ta sẽ thấy có các segments được nạp tự động bởi LOADER. Nhìn vào hình dưới, ta có thể suy đoán rằng nội dung HEADER của file sẽ được nạp tại địa 0x400000, phía trước các sections. Tuy nhiên, ở thời điểm này nó không được nạp vào vì lúc ta load file lần đầu tiên và nhấn OK, IDA sẽ tự động chỉ tải một số sections của file thực thi mà không kèm theo HEADER.

Chúng ta thấy rằng ứng với tên của từng sections sẽ có địa chỉ bắt đầu và kết thúc tương ứng, kèm theo đó là các cột RWX thông báo cho ta biết section đó có quyền đọc (READ (R)), quyền ghi (WRITE (W)) hay quyền thực thi (Execute (X)). Tiếp theo đó, ta thấy hai cột có tên DL tương ứng với DEBUGGERLOADER. Cột D trống vì nó được gọi khi chúng ta tải chương trình ở chế độ DEBUGGER và hiển thị cho chúng ta các segments được nạp vào nó. Cột L cho ta thông tin là các segments này được nạp bởi trình LOADER của IDA. Các cột phía sau không quan trọng nên tôi sẽ không đề cập chi tiết thêm ở đây.

Ở ví dụ này, không cần thiết phải nạp thêm Header. Trong phần trước chúng ta đã thấy rằng, khi ta thay đổi một lệnh bằng một lệnh nhảy tới địa chỉ 0x400000, nó sẽ được đánh dấu bằng màu đỏ như là một địa chỉ không được nạp trong LOADER.

Nếu muốn nạp thêm HEADER, mở lại CRACKME.exe một lần nữa:

Lựa chọn Overwrite để ghi đè và tạo lại một bản phân tích mới:

Đánh dấu vào lựa chọn Manual Load và nhấn OK:

IDA sẽ hỏi xem ta có muốn tự thay đổi địa chỉ base – nơi mà file thực thi sẽ được nạp. Ở đây, ta giữ nguyên giá trị mà IDA cung cấp và nhấn OK.

Nhấn Yes trên tất cả các thông báo để cho phép IDA nạp toàn bộ các thông tin:

Với việc thực hiện như trên, ta đã để cho LOADER của IDA nạp tất cả các sections bao gồm cả HEADER. Tuy nhiên, việc làm như vậy thường không cần thiết. Bây giờ, tôi sẽ lưu một bản Snapshot bằng cách chọn File > Take Database Snapshot và sau đó thay đổi bằng một lệnh nhảy JMP 0x400000 như đã làm ở phần trước:

Kết quả sẽ như hình dưới đây:

Ta thấy rằng nó không còn đánh dấu địa chỉ 0x400000 bằng màu đỏ mà thay vào đó là thông tin ImageBase. Nhấp đúp vào đó IDA sẽ đi tới địa chỉ ImageBase này:

Như trên hình, chúng ta sẽ thấy Header của file được nạp tại địa chỉ 0x400000 và được gắn một tag là ImageBase, cùng với đó là những thông tin liên quan tới các trường của PE Header mà trình Loader của IDA nhận diện được. Giờ quay trở lại bản snapshot mà ta đã chụp trước khi sửa file, vào View > Database Snapshot Manager (Ctrl+Shift+T), chọn bản snapshot và nhấn Restore:

Nếu các bạn đã từng thực hiện crack hay reverse một ứng dụng, bạn sẽ thấy các strings trong chương trình đó là rất quan trọng và thường sẽ là dấu hiệu để tìm kiếm đầu tiên. Để tìm kiếm các strings trong IDA, chọn View > Open Subview > Strings (phím tắt là Shift+F12):

Kết quả, ta thấy có rất nhiều chuỗi được tìm thấy ở nhiều sections khác nhau. Vậy đâu sẽ là string mà chúng ta cần quan tâm? Ta chạy thử crackme.exe bên ngoài IDA, vào Help chọn Register, crackme yêu cầu nhập tên và mật khẩu. Nếu nhập thông tin bất kỳ, tôi nhận được thông báo sau “No luck there, mate!”:

Ghi nhớ thông tin này, quay trở lại IDA và tìm kiếm chuỗi trong danh sách kết quả các chuỗi có được:

Nhấn đúp chuột vào chuỗi này sẽ tới đây:

Tại địa chỉ 0x402169 có chứa nội dung của chuỗi cần tìm, nếu chúng ta nhấn D tại địa chỉ này sẽ chuyển đổi nó về dạng data, lúc đó IDA sẽ hiển thị chuỗi theo dạng các bytes rời rạc như hình dưới đây:

Nếu nhấn A tại địa chỉ đó, chúng ta sẽ khôi phục lại được chuỗi ban đầu. Tiếp theo nhấn X để tìm kiếm các đoạn code có sử dụng tới chuỗi này. Kết quả có được như hình dưới đây:

Ta thấy rằng chuỗi trên được sử dụng bởi hai hàm khác nhau: tại sub_401362sub_40137E. Cả hai hàm này đều nằm ở phía trên địa chỉ mà chúng ta đang đứng. Đó là lý do tại sao ở cột Direction hiển thị thông tin là Up (tức là ở phía trên). Như vậy, trong trường hợp crackme mà ta đang phân tích thì đây là hai hàm khác nhau bởi IDA cung cấp cho chúng ta địa chỉ các tham chiếu là hàm + XXXX.

Hình trên là code của hàm đầu tiên và hình dưới là code của hàm thứ hai:

Như vậy, ta đã có được thông tin của các đoạn code sẽ hiển thị thông báo “No luck!”. Tới đây, ta có thể sử dụng một trình debugger khác như OllyDbg/ x64dbg, đặt các breakpoint tại các hàm trên và quan sát việc dừng chương trình khi nhập các thông tin về usernamepassword hoặc là debug chương trình bằng cách sử dụng chính trình Debugger của IDA. Tuy nhiên, ở phần này tôi và các bạn sẽ tập trung chính vào quá trình phân tích tĩnh để tìm hiểu code của crackme.

Phân tích và Patch thông báo thứ nhất

Phân tích hàm đầu tiên trước:

Như quan sát trên hình, hàm này gọi đến hàm API là MessageBoxA để hiển thị thông báo “No luck there, mate!” mà ta đã nhìn thấy khi thực thi crackme. Hàm API này nhận các tham số truyền vào gồm các chuỗi “No luck!” – ứng với tiêu đề của cửa sổ và “No luck there, mate!” – ứng với nội dung của thông báo. Như vậy, ta kết luận hàm này chỉ làm nhiệm vụ hiển thị thông báo lỗi, ngoài ra không làm thêm công việc gì khác.

Qua đó, ta thấy rằng đoạn code ở địa chỉ kia cũng thực hiện công việc tương tự là hiển thị thông báo lỗi, nhưng có thể ở ngữ cảnh của một quá trình kiểm tra khác. Do đó, để có thể nhận được thông báo thành công, ta cần phải tránh được hai vùng code gọi tới thông báo này. Nhấn X để tìm kiếm các tham chiếu đến hàm 0x401362, ta có được một kết quả duy nhất như hình dưới. Trước khi đi đến địa chỉ đó, ta đổi tên hàm 0x401362 bằng một cái tên nào đó gợi nhớ cho chúng ta, ví dụ như SHOW_ERROR.

Để đổi tên hàm, nhấn phím tắt N:

Sau khi đổi tên xong, ta chuyển tới vị trí tham chiếu thực hiện lời gọi tới hàm trên:

Tại đây, ta thấy khối lệnh đưa chúng ta tới SHOW_ERROR là một khối lệnh thực hiện so sánh trước đó nhằm đưa ra quyết định rẽ nhánh. Thông thường, như tôi khi thực hiện reversing, tôi muốn quan sát thấy mọi thứ một cách trực quan và rõ ràng. Rất may mắn là IDA cung cấp cho chúng ta tính năng thay đổi màu các khối lệnh, với các khối lệnh không mong muốn hay là “bad block” tôi thường dùng màu đỏ, với các khối lệnh mong muốn hay “good block” tôi thường để màu xanh.

Tại mỗi block sẽ có biểu tượng để thay đổi màu sắc tại từng khối như trên hình minh họa. Với nhiều người có thể công việc này là không cần thiết, nhưng khi phân tích các hàm phức tạp, nó sẽ hỗ trợ rất nhiều.

Như trên hình, ta thấy có một lệnh nhảy có điều kiện, tuy nhiên tạm thời ta sẽ không phân tích vì nó nhảy sang một khối lệnh khác. Quan sát khối lệnh tại địa chỉ 0x40124c, ta thấy có một lệnh call, đi vào code bên trong lệnh call này bằng cách chọn 0x40134d và nhấn Enter:

Căn cứ vào code tại đó, ta có thể biết được đây là hàm cho hiển thị thông báo “Good work!” khi ta nhập đúng thông tin nào đó. Như vậy, rõ ràng là chương trình sẽ kiểm tra và đưa ra quyết định đi tới đây hay rẽ nhánh sang khối lệnh hiển thị thông báo “No luck!”.

Tiến hành đổi tên hàm này thành SHOW_GREAT:

Sau đó quay trở lại đoạn code rẽ nhánh, thực hiện thay đổi màu cho khối lệnh này sang màu xanh, tương tự như hình minh họa dưới đây:

Và cũng như chúng ta sẽ làm cho các phần khác của chương trình, để có thể dễ dàng quay trở lại ở đây, ta chọn lệnh JZ tại địa chỉ 0x401243, sau đó vào menu Jump và chọn Mark Position (Alt+M). Đặt một tên sao cho dễ nhớ, ví dụ DECISION_FINAL:

Sau khi thực hiện xong, truy xuất Jump > Jump To Marked Position, chúng ta sẽ thấy danh sách các ví trí mà đã đánh dấu và ta có thể đi đến vị trí mong muốn trong trường hợp chúng ta bị lạc đâu đó trong quá trình phân tích chương trình:

Với thông tin về lệnh nhảy có được thì có nghĩa là về mặt lý thuyết, nếu ta thay lệnh JZ bằng lệnh JNZ, chương trình sẽ đưa chúng ta đến block Good Work ngay cả khi thông tin ta nhập vào là không hợp hệ. Tôi sẽ thử patch bằng plug-in Keypatch xem kết quả thế nào:

Sau đó lưu lại thay đổi bằng Edit > Patch program > Apply Patches To Input File:

Chạy và kiểm tra file đã patched:

Ok, như trên hình thì ta đã patch thành công rồi 🙂 .

Phân tích và Patch thông báo thứ hai

Tuy nhiên, trong trường hợp người dùng lại nhập thông tin theo kiểu khác như hình trên (nhập vào tên là có kèm theo số), khi nhấn OK thì chương trình vẫn sẽ văng ra thông báo lỗi. Như vậy, ta phải tiếp tục phân tích và patch tiếp để hoạt động đúng như ta muốn.

Ta tới đoạn code hiển thị thông báo lỗi tương tự tại địa chỉ 0x004013AC, là khối được tôi đổi thành màu đỏ như hình dưới đây:

Rõ ràng, phía trước khối lệnh này là lệnh thực hiện so sánh với 0x41 tương ứng với chữ cái ‘A‘ trong bảng mã ASCII và nếu thấp hơn (JB) sẽ hiển thị thông báo lỗi. Do vậy, nếu chúng ta nhập vào số thay vì chữ cái tại textbox Name, rõ ràng là các số có mã ASCII thấp hơn 0x41 (số 0 là 0x30, số 1 là 0x31, v..v..). Chính vì vậy, khi kiểm tra trong tên có chứa số thì crackme sẽ hiển thị thông báo lỗi. Ta sẽ patch tại đây, nhưng ta không thể thay lệnh JB bằng lệnh JNB được, bởi vì nếu làm thế ta sẽ nhận được thông báo lỗi nếu ta chỉ nhập chữ cái trong ô textbox Name. Trong bài viết sau, tôi sẽ phân tích crackme này một cách hoàn chỉnh.

Ta chuyển sang chế độ Text bằng cách nhấn phím Space bar:

Chúng ta thấy nó sẽ nhảy tới đoạn code hiển thị thông báo lỗi (đường chấm chấm ở bên trái cho biết đích của lệnh nhảy), do đó, nếu tôi thay bằng lệnh NOP thì sẽ không nhảy và tiếp tục thực hiện các lệnh tiếp theo mà không tới đoạn code hiển thị lỗi.

Quay trở lại với chế độ đồ họa bằng cách nhấn Space bar.

Sử dụng Keypatch để thực hiện patch lệnh như hình minh họa trên. Các bytes của lệnh nhảy sẽ được thay bằng opcode 0x90 của lệnh NOP:

Sau khi patch xong save lại như đã thực hiện ở bước trước:

Cuối cùng kiểm tra thành quả sau khi patch:

Ok, đây mới chỉ là phần khởi động để các bạn làm quen với việc phân tích tĩnh và thực hiện sửa lại code của chương trình thông qua việc patch lệnh. Trong các phần sau, chúng ta sẽ thực hiện reverse hoàn toàn crackme này và tạo một keygen. Cứ đi từ từ từng bước một, chậm và chắc!

Bên lề: Như các bạn đã thực hành theo bài viết, toàn bộ quá trình thực hiện về bản chất là thay đổi luồng thực thi của chương trình hay nhánh thực hiện trong chương trình. Việc phân nhánh thực hiện này được quyết định vào điều kiện so sánh trước đó. Các bạn có kiến thức về lập trình cũng biết có hai cấu trúc lệnh rẽ nhánh phổ biến hay dùng là ifif-else:

Với các khối lệnh trên thì ở mức bên dưới sẽ sử dụng các câu lệnh assembly là testcmp để kiểm tra điều kiện, từ đó bật các cờ của thanh ghi EFLAGS. Dựa vào trạng thái của cờ thì các lệnh nhảy jcc sẽ nhảy tới các block code tương ứng. Tuy nhiên có một lưu ý quan trọng khi các bạn đọc mã asm là điều kiện sẽ được đảo ngược lại. Tức là, ở mã nguồn của chương trình, bạn viết lệnh kiểm tra một biến var_1 bằng 0, nhưng sau khi biên dịch chương trình thì trình compiler sẽ đảo ngược lại điều kiện này, do đó lệnh điều kiện (ở đây là lệnh nhảy) sẽ kiểm tra biến var_1 khác 0.

Để có cái nhìn trực quan hơn tôi sẽ tổng kết bằng hai hình minh họa được trích ra từ cuốn sách Secrets of Reverse Engineering của Eldad Eilam:


Hẹn các bạn ở phần 9.

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


Phần này sẽ là một phần khá thú vị về các lệnh liên quan đến việc kiểm soát luồng thực thi của chương trình hay còn gọi với một thuật ngữ chuyên ngành là flow control. Như các bạn đã biết rằng thanh ghi EIP luôn trỏ vào lệnh tiếp theo sẽ được thực hiện và khi được thực thi thì EIP sẽ trỏ tới lệnh kế tiếp.

Nhưng bản thân các chương trình cũng có các lệnh dùng để kiểm soát luồng thực thi, từ đó có thể chuyển hướng thực hiện đến một lệnh mong muốn. Trong phần này, chúng ta sẽ xem xét các trường hợp cụ thể của lệnh.

Lệnh nhảy không điều kiện

Lệnh JMP

JMP A; giống như lệnh goto trong lập trình bậc cao Lệnh JMP là một lệnh nhảy không phụ thuộc vào điều kiện và A sẽ là một địa chỉ bộ nhớ mà chúng ta muốn chương trình nhảy tới.

JMP SHORT là một lệnh nhảy ngắn gồm có 2 bytes, có khả năng nhảy về phía trước và ngược lại. Hướng nhảy được chỉ định bởi giá trị của byte thứ hai vì byte đầu tiên là opcode (0xEB) của lệnh. Lệnh này không thể nhảy quá xa.

Nếu thiết lập tùy chọn trong IDA để hiển thị opcode của các lệnh, chúng ta sẽ thấy opcode EB tương ứng với lệnh JMP và lệnh này sẽ nhảy 5 bước về phía trước kể từ vị trí kết thúc lệnh. Nghĩa là địa chỉ đích của lệnh được tính như sau:

Lấy địa chỉ bắt đầu của lệnh cộng với 2 là số bytes chiếm bởi lệnh và sau đó cộng thêm 5 (byte thứ hai – bước nhảy). Rõ ràng, việc nhảy tới hoặc lùi với một byte duy nhất không cho chúng ta đạt được bước nhảy xa. Bước nhảy cao nhất sẽ là 0x7f, chúng ta sẽ lấy một ví dụ bên dưới.

Bên lề: Khi chúng ta thực hiện một số thay đổi sẽ phá vỡ cấu trúc của hàm, để thuận tiện ta nên tiến hành tạo một bản chụp (snapshot) cơ sở dữ liệu nhằm giúp ta quay trở lại trạng thái trước khi thay đổi. Hãy làm việc này vào bất cứ lúc nào bạn nghi ngờ về việc thay đổi sẽ có thể làm phá vỡ cấu trúc hàm và không biết cách nào để khôi phục lại.

IDA sẽ yêu cầu chúng ta đặt tên cho database được snapshot. Có thể quản lý snapshot này thông qua View > Database Snapshot Manager:

Tại đây, chúng ta có thể xem danh sách tất cả các snapshot và ngày chụp, cùng với nút Restore cho phép chúng ta có thể trở về trạng thái chúng ta muốn từ những bản snapshot mà chúng ta đã lưu.

Hãy xem điều gì sẽ xảy ra nếu tôi thay 5 thành 7F:

Sử dụng tính năng Patch Bytes của IDA để thực hiện thay đổi lệnh như trên hình. Sau khi patch xong, ta thấy bước nhảy sẽ dài hơn và vượt ra ngoài hàm. Nhấn phím space bar để thoát khỏi chế độ đồ hoạ, chuyển sang chế độ Text:

Chúng ta thấy rằng lệnh nhảy của chúng ta vẫn OK và nhảy tới địa chỉ là 0x4013a5. Tiếp theo, nếu tôi thay 0x7f bằng 0x80 thì sẽ thế nào? Quay về chế độ đồ họa và thực hiện thay đổi thành 0x80. Sau đó lại chuyển về chế độ text, ta có kết quả như hình:

Chúng ta thấy rằng bây giờ lệnh nhảy đã chuyển thành lệnh có bước nhảy lùi lớn nhất.

Trong trường hợp này, do ta thực hiện bước nhảy lùi, để đảm bảo cho công thức tính toán và bởi Python không biết được đây là bước nhảy tiến hay nhảy lùi từ giá trị này, ta phải sử dụng giá trị -0x80 (được biểu diễn bằng một dword ở hệ thập lục phân là 0xFFFFFF80) và sau đó thực hiện AND kết quả tính toán được với 0xFFFFFFFF nhằm xóa toàn bộ các bit lớn hơn một số 32 bit. Kết quả ta có được địa chỉ nhảy đến là 0x4012a6.

Nếu tôi sử dụng giá trị 0xFF thì ta sẽ có một bước nhảy tối thiểu vì giá trị hex 0xFF là biểu diễn của -1. Ở đây, tôi thay bằng 0xFFFFFFff. Luôn nhớ rằng ta cần cộng thêm 2 bytes (đó là độ lớn của lệnh), do đó, kết quả tính toán sẽ có được địa chỉ cần nhảy tới là 0x401325.

Nếu chúng ta tiếp tục với một giá trị khác, ví dụ 0xFE, tức là nhảy ngược -2, vậy theo công thức sẽ cộng thêm 0xFFFFFFFE.

Với giá trị này thì lệnh nhảy sẽ nhảy tới chính câu lệnh đó hay còn được gọi là Infinite Loop, bởi vì nó luôn luôn lặp đi lặp lại chính nó và không thể thoát được.

Bên lề: 2 bytes “0xEB 0xFE” được gọi là 2 bytes “thần thánh”. Chúng được sử dụng trong quá trình Unpacking, Debug Malware. Thông thường malware sẽ tạo ra các thread hoặc bằng các kĩ thuật Process Hollowing/ RunPE để thực thi malicious code, lúc này ta sẽ tìm cách patch bytes tại entry point thành 0xEB 0xFE để tạo infinite loop (lưu ý nhớ lại byte gốc của EP), sau khi patch xong để process thực thi bình thường và rơi vào vòng lặp vô tận, tiến hành attach tiến trình mới vào một trình debugger khác để debug tiếp.

Cứ như vậy, nhảy -3 sẽ là 0xFD, vì vậy nó sẽ nhảy đến địa chỉ 0x401323.

Rõ ràng với những bước nhảy ngắn, chúng ta không thể nhảy tới bất kỳ địa chỉ nào vì bị giới hạn ở một vài byte xung quanh nơi chúng ta đang sử dụng nó, do đó ta cần sử dụng bước nhảy dài.

Như trên hình, ta thấy một vài lệnh nhảy dài. Tiền tố loc_ đứng đằng trước hàm ý rằng đó là địa chỉ cần nhảy tới:

Trong hình, ta thấy một lệnh nhảy dài, khoảng cách giữa 0x4026ae0x4029b3 là lớn hơn nhiều so với những gì chúng ta có thể đạt được với một bước nhảy ngắn.

Khoảng cách sẽ được tính bằng công thức lấy địa chỉ cuối cùng – địa chỉ ban đầu – 5 (là chiều dài của lệnh) (Final address – start address – 5), kết quả có được là 0x300. Đó chính là dword đứng cạnh opcode của bước nhảy dài 0xe9.

Nếu tôi sử dụng plugin Keypatch để thay đổi địa chỉ đích của lệnh nhảy đến một hướng ngược lại, ví dụ 0x400000:

Tuy nhiên, nó được đánh dấu màu đỏ bởi đó không phải là một địa chỉ hợp lệ. Thử áp dụng công thức tính toán trong Python:

Kết quả có được là -0x26b3:

Chuyển sang hexa là FFFFD94D, đó là các bytes đứng cạnh opcode 0xe9, được bố trí theo kiểu Little-endian:

Lệnh nhảy có điều kiện (Jcc)

Thông thường, các chương trình phải đưa ra các quyết định rẽ nhánh thực thi chương trình, điều này sẽ căn cứ vào việc so sánh các giá trị để chuyển hướng thực hiện chương trình sang một điểm khác.

Ta có lệnh so sánh:

CMP A, B; so sánh toán hạng thứ nhất với toán hạng thứ hai và bật các cờ trên thanh ghi EFLAGS dựa theo kết quả tính toán (việc tính toán tương tự như lệnh SUB, nhưng khác ở chỗ kết quả tính toán không được lưu lại).

Tôi cần chương trình thực hiện so sánh giữa A và B. Dựa vào mối quan hệ giữa chúng, chương trình sẽ thực hiện một công việc nào đó nếu thỏa mãn, còn không chương trình sẽ thực hiện một công việc khác. Vì vậy, bình thường sau khi so sánh sẽ làm thay đổi các FLAGS, căn cứ vào trạng thái của cờ, lệnh nhảy có điều kiện sẽ quyết định có thực hiện hay không.

Trong hình trên, chúng ta thấy một ví dụ về lệnh nhảy có điều kiện là JZ. Lệnh này sẽ thực hiện nếu cờ ZF được bật. Điều này được quyết định bởi lệnh CMP trước đó. Khi hai thanh ghi EAX và EBX bằng nhau, CMP thực hiện phép trừ hai thanh ghi, kết quả sẽ bằng 0 và do đó cờ ZF được bật thành 1. Khi cờ ZF được kích hoạt thì sẽ thực hiện lệnh nhảy theo hướng mũi tên màu xanh lá cây, còn nếu hai thanh ghi khác nhau thì sẽ đi theo hướng của mũi tên đỏ.

Nếu sử dụng debugger để debug chương trình, chúng ta có thể tương tác để thay đổi kết quả trên các cờ. Tuy nhiên, điều quan trọng bây giờ cần phải nhớ có những lệnh nhảy khác nhau có thể xuất hiện trong một chương trình như hình minh họa dưới đây:

Ngoại trừ các lệnh JMPNOP được liệt kê trong bảng, các lệnh còn lại đều là các lệnh nhảy có điều kiện. Các lệnh nhảy này đều căn cứ vào kết quả của câu lệnh so sánh trước đó.

  • Các lệnh nhảy Above / Below được sử dụng cho so sánh số không dấu (unsinged comparison)
  • Các lệnh nhảy Greater than / Less than được sử dụng cho so sánh số có dấu (singed comparison)
  • Mặc dù có nhiều lệnh nhảy có điều kiện nhưng may mắn là trong số đó có các lệnh cùng một mục đích. Ví dụ: JNE == JNZ (Nhảy nếu không bằng nhau, Nhảy nếu không bằng 0, cả hai cùng kiểm tra cờ (ZF == 0)).
Instruction Description Aliases Flags
jz Jump if zero je zf = 1
jnz Jump if not zero jne zf = 0
jl Jump if less jnge sf = 1
jle jump if less or equal jng zf=1 or sf=1
jg jump if greater jnle zf=0 and sf=0
jge jump if greater or equal jnl sf = 0
jc jump if carry jb, jnae cf = 1
jnc jump if not carry jnb, jae cf = 0

Bên lề: bên cạnh việc so sánh sử dụng câu lệnh CMP, một câu lệnh khác cũng rất hay được sử dụng là TEST. Bản chất của lệnh TEST là tính toán logic thông qua việc AND hai toán hạng, căn cứ trên kết quả để bật cờ. Kết quả tính toán sẽ không được lưu lại.

Lệnh CALL và RET

Các lệnh tiếp theo mà tôi sẽ đề cập đến là lệnh CALL, dùng để gọi một hàm và lệnh RET, dùng để trở quay trở về lệnh tiếp theo sẽ được thực hiện sau lệnh Call.

Trên hình, chúng ta thấy một ví dụ của lệnh CALL, lệnh này sẽ nhảy đến địa chỉ 0x4013d8 để thực hiện hàm tại đó (chúng ta thấy tiền tố sub_ ở phía trước của địa chỉ 0x4013D8 thông báo cho ta biết đây là một hàm).

Lệnh CALL thực hiện sẽ lưu vào đỉnh của ngăn xếp (Stack) đỉa chỉ trở về sau khi thực hiện xong hàm, tức là địa chỉ bên dưới của lệnh Call mà trong trường hợp này là 0x40123d. Sau đó, nó sẽ thay đổi địa chỉ của thanh ghi EIP bằng địa chỉ đã chỉ định trong câu lệnh. Địa chỉ đích có thể được chỉ định theo nhiều cách:

  • Giá trị trực tiếp (immediate value): call   0x401da8
  • Thanh ghi dùng chung: call eax
  • Vị trí bộ nhớ: call    dword ptr [0x40202c]

Ta có thể truy cập các lệnh bên trong CALL bằng cách nhấn Enter tại lệnh CALL đó:

Khi kết thúc hàm, sẽ thực hiện một lệnh RET, lệnh này có nhiệm vụ lấy địa chỉ trở về được lưu tại đỉnh của stack là 0x40123d, đưa vào thanh ghi EIP và nhảy tới địa chỉ này để tiếp tục thực hiện lệnh sau CALL.

Bên lề: liên quan đến hai lệnh Call & Ret chúng ta cần biết thêm về tập quán gọi hàm (hay từ chuyên môn là calling convention), bởi vì việc gọi hàm có thể khác nhau trong mã lệnh asm. Nó bao gồm thứ tự các tham số được đẩy vào ngăn xếp hay các thanh ghi, hàm gọi (caller) hay hàm được gọi (callee) chịu trách nhiệm dọn dẹp ngăn xếp khi hoàn tất hàm. Tùy thuộc vào trình biên dịch, có hai tập quán gọi hàm hay gặp nhất là cdecl (C declaration) và và stdcall. Ngoài ra, còn rất nhiều tập quán gọi hàm khác mà các bạn có thể xem thêm tại đây: https://en.wikipedia.org/wiki/X86_calling_conventions

cdecl – tập quán gọi hàm phổ biến nhất:

  • Tham số của hàm được đẩy vào stack theo chiều từ phải qua trái.
  • Kết quả trả về của hàm thường lưu vào thanh ghi EAX.
  • Hàm gọi (caller) chịu trách nhiệm dọn dẹp stack.

Ví dụ:

_cdecl int function(int arg1, int arg2, int arg3)

Stdcall – thường được sử dụng bởi Microsoft C++, đặc biệt là Win32 API functions:

  • Tham số của hàm được đẩy vào stack theo chiều từ phải qua trái.
  • Kết quả trả về của hàm thường lưu vào thanh ghi EAX.
  • Hàm được gọi (callee) chịu trách nhiệm dọn dẹp stack.

Ví dụ:

_stdcall int function(int a, int b, int c)

Như vậy, tới phần này chúng ta đã tìm hiểu một số câu lệnh ASM hay sử dụng nhất. Nó là bước đệm để các tôi và các bạn đi tiếp các phần sau.

Phần 7 xin được dừng lại tại đây. Hẹn các bạn gặp lại ở phần 8 sẽ thú vị hơn!

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


Tiếp tục với các lệnh assembly cơ bản, phần này sẽ là các lệnh tính toán và logic.

Các lệnh tính toán

Lệnh ADD

Cú pháp của lệnh như sau:

ADD A, B ; A = A + B

Câu lệnh ADD thực hiện cộng giá trị của B với A, kết quả tính toán sẽ lưu vào A. Tức là A = A + B. A ở đây có thể là một thanh ghi hoặc là nội dung của một ô nhớ, B có thể là một thanh ghi, một hằng số hoặc nội dung của một ô nhớ. Tuy nhiên, trong câu lệnh ADD thì cả AB không thể đồng thời là nội dung của ô nhớ.

Quan sát một số ví dụ về lệnh ADD có được bằng việc tìm kiếm chuỗi ADD trong Veviewer.

Ta thấy có khá nhiều câu lệnh ADD với toán hạng đầu tiên là một thanh ghi và toán hạng thứ hai là một hằng số. Như chúng ta biết, nó sẽ cộng vào giá trị mà thanh ghi đang có tại thời điểm đó với giá trị hằng số, kết quả tính toán sẽ được lưu lại vào thanh ghi.

Trong ví dụ này, nếu thanh ghi ECX có giá trị là 0x10000, nó sẽ được cộng thêm 4, kết quả thu được là 0x10004 sẽ được lưu vào chính thanh ghi ECX.

Còn trong trường hợp trên, lệnh ADD sẽ thực hiện cộng giá trị 0xFFFFFFFF vào giá trị có được tại địa chỉ được trỏ bởi ECX+30, và nếu địa chỉ đó có quyền ghi, nó sẽ thực hiện cộng thêm và lưu lại kết quả ở đó.

Giả sử, nếu thanh ghi ECX đang có giá trị 0x10000 thì kết quả phép tính trong [] cho ta một địa chỉ là 0x10030. Giả sử nội dung tại địa chỉ này có giá trị 1, thì khi được cộng thêm 0xFFFFFFFF (bằng -1) sẽ có kết quả sẽ bằng 0 và được lưu lại vào địa chỉ 0x10030.

Còn ở crackme.exe, ta có thể gặp ví dụ của lệnh ADD sử dụng các toán hạng là các thanh ghi như sau:

Trong trường hợp này, giá trị của cả hai thanh ghi sẽ được cộng với nhau và lưu vào EDI. Tất nhiên ta cũng có thể sử dụng các thanh ghi 16-bit và 8-bit. Ví dụ:

ADD AL, 8

ADD AX, 8

ADD BX, AX

ADD byte ptr ds: [EAX], 7

Lệnh trên cộng vào byte nội dung mà EAX trỏ đến với giá trị hằng số là 7 và lưu lại tại cùng một vị trí.

Lệnh SUB

Cú pháp của lệnh như sau:

SUB A, B ; A = A – B

Lệnh SUB cũng tương tự như lệnh ADD, ngoại trừ thay vì thực hiện cộng thì nó thực hiện trừ số nguyên và lưu kết quả vào A. Các kết hợp của lệnh có thể như sau:

Lệnh INC và DEC

INC A; A++

DEC A; A–

Các lệnh trên thực hiện tăng hoặc giảm giá trị thanh ghi hoặc nội dung của một địa chỉ bộ nhớ đi 1. Trên thực tế đây có thể xem là một trường hợp đặc biệt của phép cộng và trừ.

Bên lề: Cả hai lệnh này thường hay được sử dụng trong các vòng lặp để tăng hoặc giảm biến đếm.

Lệnh IMUL

Đây là lệnh thực hiện phép tính nhân số có dấu và có hai dạng như sau:

IMUL A, B    ; A = A * B

IMUL A, B, C ; A = B * C

Bên lề: Tại sao lại là câu lệnh imul (signed multiply) mà không phải là câu lệnh mul (unsigned multiply)? Đó là bởi trình Visual Studio dường như có một sự ưa thích đối với lệnh imul. Kể cả khi bạn khai báo biến có kiểu unsigned trong chương trình, khi compile code và chuyển qua assembly thì sẽ thấy chương trình sử dụng câu lệnh imul.

Quay trở lại với cú pháp của lệnh:

  • Câu lệnh đầu tiên sẽ thực hiện nhân A với B, kết quả được bao nhiêu sẽ được lưu lại vào A.
  • Câu lệnh thứ hai thì B và C được nhân với nhau và kết quả được lưu vào A.

Trong cả hai trường hợp A chỉ có thể là một thanh ghi, B chỉ có thể là một thanh ghi hoặc nội dung của một vị trí bộ nhớ và C chỉ có thể là một hằng số.

imul eax, [ecx] 

imul esi, edi, 25 

Một vài ví dụ khác của lệnh IMUL tìm thấy trong file Veviewer

Trong hình minh họa trên, ta thấy hầu như chỉ có các lệnh imul theo dạng 1 (imul A, B). Với trường hợp lệnh chỉ có một toán hạng (ví dụ: imul ecx), thì tùy theo độ dài của toán hạng mà sẽ lấy giá trị trong các thanh ghi AL, AX, hoặc EAX để nhân và kết quả của phép nhân sẽ được lưu vào AX, DX:AX, hoặc EDX:EAX.

Lệnh DIV/ IDIV

Cú pháp của lệnh như sau:

DIV/ IDIV A

Trong câu lệnh này, A được hiểu là số chia. Số bị chia và thương số không được chỉ định bởi vì chúng luôn giống nhau. Tức là có 3 dạng như sau:

  • Nếu A có kiểu byte, lấy giá trị của thanh ghi AX chia cho A, kết quả thương số lưu vào thanh ghi AL, phần dư lưu vào thanh ghi AH.
  • Nếu A có kiểu word, lấy giá trị của cặp thanh ghi DX:AX chia cho A, kết quả thương số lưu vào thanh ghi AX, phần dư lưu vào thanh ghi DX.
  • Nếu A có kiểu dword, lấy giá trị của cặp thanh ghi EDX:EAX chia cho A, kết quả thương số lưu vào thanh ghi EAX, phần dư lưu vào thanh ghi EDX.

Xem xét ví dụ:

Với lệnh trên, ví dụ nếu EAX = 5, EDX = 0 và ECX = 2, nó sẽ thực hiện phép chia số nguyên. Kết quả của phép chia 5 / 2 sẽ được 2 và dư 1. Khi đó kết quả là 2 được lưu vào thanh ghi EAX và số dư 1 sẽ được lưu vào thanh ghi EDX.

Bên lề: Thông thường khi thực hiện phép chia, do thanh ghi EDX được sử dụng để lưu phần dư nên nó sẽ được thiết lập về 0 trước khi thực hiện phép tính. Để xóa EDX về 0 có hai cách:

  • Sử dụng câu lệnh XOR (chi tiết bên dưới): XOR EDX, EDX
  • Sử dụng câu lệnh CDQ (như trên hình minh họa): Câu lệnh này thực hiện mở rộng bit dấu (bit 31) của thanh ghi EAX sang thanh ghi EDX. Nếu bit này có giá trị 0 thì EDX sẽ bằng 0.

Điều tương tự sẽ xảy ra nếu như A là nội dung của một ô nhớ (dword ptr ds:[402000]), EDX:EAX sẽ được chia cho giá trị đó và kết quả sẽ được lưu trong EAX và phần dư trong EDX.

Các lệnh Logic

Lệnh AND, OR và XOR

AND A, B ; A = A & B

OR A, B  ; A = A | B

XOR A, B ; A = A ^ B

Lệnh đầu tiên thực hiện phép AND giữa hai giá trị và lưu lại kết quả vào A, tương tự với các lệnh OR hoặc XOR. Mỗi phép tính đều sử dụng một bảng thật tương ứng của nó. A và B có thể là thanh ghi hoặc nội dung của địa chỉ bộ nhớ, tuy nhiên các thao tác giữa hai ô nhớ là không hợp lệ.

Lệnh hay được sử dụng nhiều nhất là XOR cùng một thanh ghi để dễ dàng xóa thanh ghi đó về 0. Ví dụ: XOR EAX,EAX. Dưới đây là bảng thật hay bảng chân lý (như ở các trường đại học hay dạy) tương ứng cho từng lệnh:

Trong bảng trên chúng ta thấy rằng nếu XOR một số với chính nó thì kết quả sẽ luôn bằng không. Các phép tính này được thực hiện ở chế độ nhị phân (binary):

  • Lệnh AND có thể sử dụng để che đi/ giữ lại các bit nhất định của toán hạng đích. Bit 0 của mặt nạ sẽ xóa bit tương ứng, còn bit 1 của mặt nạ sẽ giữ nguyên bit tương ứng của toán hạng đích.
  • Lênh OR có thể được sử dụng để thiết lập các bit xác định của toán hạng đích trong khi vẫn giữ nguyên các bit còn lại. Bit 1 cua mặt nạ sẽ thiết lập bit tương ứng còn bit 0 của mặt nạ sẽ giữ nguyên bit tương ứng của toán hạng đích.
  • Lệnh XOR dùng để đảo các bit xác định của toán hạng đích trong khi vẫn giữ nguyên các bit còn lại. Bit 1 của mặt nạ làm đảo bit tương ứng còn bit 0 giữ nguyên bit tương ứng của toán hạng đích.

Như đã nói, lệnh XOR dùng để xóa một thanh ghi về 0, bằng cách này sẽ thực hiện nhanh hơn lệnh MOV.

Để kiểm tra ta có thể viết một lệnh xor hai số giống nhau ở dạng binary trong khung Python. Kết quả trả về luôn là 0:

Tất nhiên, ta hoàn toàn có thể áp dụng với các số thập phân và thập lục phân. Ở trên tôi để ở dạng nhị phân để quan sát kết quả cụ thể ứng với từng bit. Còn trong ví dụ dưới đây tôi để ở dạng hexa:

Một ví dụ đơn giản của lệnh AND:

AND EAX, 0F

Biểu diễn ở dạng binary thì 0F sẽ là 1111:

Dựa vào bảng thật, chúng ta thấy rằng nếu cả hai bit là 1 thì kết quả sẽ không thay đổi, trong khi các cặp bit khác sẽ cho kết quả là 0. Bằng cách này tôi dễ dàng thiết lập lại tất cả các bit của một số là 0 và giữ nguyên 4 bit cuối cùng không thay đổi. Ví dụ:

Như đã biết phép AND được biểu diễn bằng dấu “&”. Với câu lệnh như trên, ta sẽ giữ lại được 4 bit cuối.

Lệnh OR được biểu diễn bằng dấu “|”, ví dụ như sau:

Lệnh NOT

NOT A

Lệnh NOT thực hiện đảo ngược tất cả các bit của A và lưu lại kết quả vào A. Trong Python không có lệnh NOT, nhưng nó rất đơn giản nếu bạn có một số nhị phân, ví dụ 0101 và bạn áp dụng lệnh NOT với số này:

Kết quả có được sau khi thực hiện đảo ngược từng bit một. Toàn bộ các bit 0 sẽ được thay bằng 1 và ngược lại.

Lệnh NEG

NEG A ; chuyển đổi A thành –A (reg = 0 – reg). Trên thực tế, lệnh neg là kết quả của một lệnh notadd 1.

Nó không giống như cú pháp ~ trong Python vì lệnh này chỉ là phép trừ đi 1.

Nói cách khác, để thực hiện lệnh NEG bằng Python, bạn cần cộng thêm 1 vào kết quả.

Các lệnh dịch bit SHL, SHR

SHL A, B; Dịch trái A đi B bit

SHR A, B; Dịch phải A đi B bit

A có thể là một thanh ghi hoặc một vị trí bộ nhớ và B là một hằng số hay một thanh ghi 8-bit. Các lệnh này thực hiên phép dịch bit sang trái (SHL) và sang phải (SHR), các bit bên phải/trái được thay thế bằng các số 0, chúng ta hãy xem ví dụ.

Nếu tôi có -1:

Khi thực hiện SHL 2, nó sẽ có kết quả:

Khi di chuyển các bit sang trái, mỗi lần dịch thì MSB sẽ được đưa qua cờ CF và 0 đưa vào LSB. Vì dịch đi 2, nên hai bit cuối cùng ở phía bên phải nhất sẽ được thay thế bằng 0.

Tương tự khi ta thực hiện lệnh SHR. Các bit sẽ di chuyển sang phải, sau mỗi lần dịch thì LSB sẽ được đưa qua cờ CF còn 0 đưa vào MSB.

Lưu ý: Việc dịch bit trái (phải) tương ứng với phép nhân (chia) cho lũy thừa 2.

  • shl eax, 0x2 à EAX << 2 or EAX = EAX * 4
  • shr eax, 0x2 à EAX >> 2 or EAX = EAX / 4

Phần 6 xin được dừng lại tại đây. Hẹn các bạn gặp lại ở phần 7!

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


LEA (LOAD EFFECTIVE ADDRESS)

Trong phần tiếp theo này chúng ta sẽ tiếp tục với lệnh LEA và cách sử dụng của lệnh này trong code của chương trình.

Cú pháp của lệnh như sau:

LEA A, B

Lệnh LEA (nạp địa chỉ hiệu dung vào thanh ghi) thực hiện chuyển một địa chỉ được chỉ định trong B (nguồn) vào A (đích). Nội dung của B không bao giờ được truy cập, nó sẽ luôn là một địa chỉ hoặc là kết quả của phép tính toán nằm trong dấu ngoặc vuông [] của toán hạng thứ hai. Lệnh LEA được sử dụng rất nhiều để lấy địa chỉ bộ nhớ của các biến hoặc các tham số.

Quan sát một ví dụ dưới đây:

Các bạn để ý, tại các hàm mà IDA phân tích ta sẽ thấy có các tham số được truyền vào cho hàm. Các tham số này được kí hiệu là arg_x. Thường thì việc truyền tham số cho một hàm sẽ thông qua lệnh PUSH trước khi thực thi hàm đó. Như chúng ta đã thấy trên hình, lệnh PUSH thực hiện lưu các giá trị vào trong ngăn xếp, các giá trị này được gọi là đối số hay tham số.

Quan sát hình, ở phần đầu của mỗi hàm, ta sẽ thấy có một danh sách liệt kê gồm có các biến cục bộ và các tham số mà hàm sử dụng. Cụ thể, tại hàm sub_401170 trên hình, ta thấy có một tham số trong ngăn xếp bởi hàm chỉ nhận duy nhất một arg, trong trường hợp này IDA đặt tên là arg_0. Bên cạnh đó, hàm cũng dành riêng không gian trong ngăn xếp để phục vụ lưu các biến cục bộ, các biến cục bộ này nằm phía trên các tham số như quan sát trên hình và bắt đầu bằng var_x.

Tôi sẽ giải thích sau về vị trí chính xác của các tham số và các biến trong stack. Còn tại thời điểm này bạn chỉ cần nhớ rằng mỗi tham số hoặc biến mà một hàm sử dụng sẽ có một địa chỉ do hệ thống cấp phát và một giá trị được lưu tại địa chỉ đó.

Quay trở lại với lệnh LEA, chương trình sử dụng một lệnh LEA tại địa chỉ 0x401191 lea     eax, [ebp+var_C], câu lệnh này sẽ thực hiện lấy địa chỉ của biến trên Stack và gán vào thanh ghi EAX. Nếu ngược lại, đây là một câu lệnh MOV thì sẽ chuyển nội dung hoặc giá trị đang được lưu giữ trong biến vào EAX.

Hay nói cách khác, lệnh LEA, mặc dù sử dụng cặp ngoặc [], nhưng nó chỉ chuyển địa chỉ đã được thực hiện tính toán bên trong ngoặc mà không truy cập vào nội dung của ô nhớ và thanh ghi EBP thường được sử dụng làm thanh ghi cơ sở cho việc truy xuất các biến và các tham số trên stack của mỗi hàm. Về bản chất, quá trình thực hiện thực ra chỉ là cộng hoặc trừ thanh ghi EBP với một giá trị hằng số để trỏ tới một địa chỉ nằm trên Stack mà thôi, tuy nhiên do IDA là công cụ hỗ trợ khả năng tương tác cao, nên nó sẽ đánh nhãn cho từng biến để ta có thể dễ dàng đặt lại tên khi phân tích.

Tại IDA, nếu ta nhấn phải chuột vào biến đó, ta sẽ thấy được cách biểu diễn toán học thuần túy là [EBP-0Ch] và ta có thể thay đổi lệnh bằng cách nhấn phím Q. Đó là lý do tại sao lệnh LEA chỉ thực hiện việc tính toán EBP-0C, vì đơn giản thanh ghi EBP đang lưu một địa chỉ trên ngăn xếp làm base (cơ sở) cho hàm, sau khi trừ đi giá trị 0C ta có được địa chỉ của biến nói trên.

Ở đây, nhiều người có thể tự hỏi sẽ dễ dàng hơn cho IDA nếu sử dụng ký hiệu toán học thuần túy cho các biến và các tham số thay vì sử dụng [EBP – hoặc + một tag]. Vấn đề là trong reversing, việc ta có thể đổi tên các biến và tham số với tên mà ta mường tượng ra khi tác giả code chương trình là rất quan trọng, như vậy sẽ giúp chúng ta dễ dàng hơn trong quá trình phân tích. Lúc đó, sẽ không còn là một biến được gọi là EBP – 0C mà có thể đổi thành EBP + SIZE, nếu lúc phân tích, tôi biết lệnh này sẽ lưu một size (Việc đổi tên được thực hiện bằng cách sử dụng phím tắt N). Nếu cần quay về câu lệnh gốc tôi có thể nhấn chuột phải lên nó để thay đổi.

Vì lý do trên, lệnh LEA cũng được sử dụng để thực hiện các tính toán nằm trong [], sau đó chuyển kết quả tính toán vào thanh ghi đích, mà không cần truy cập nội dung.

Xem xét các ví dụ:

LEA EAX, [4+5]

Lệnh trên sẽ tính toán và gán 9 vào thanh ghi EAX, nó sẽ không chuyển nội dung của địa chỉ 0x9 như lệnh MOV sẽ thực hiện: MOV EAX, [4+5]

LEA EAX, [EBP – 0C]

Lệnh trên chuyển kết quả của EBP – 0C, là địa chỉ bộ nhớ của biến thu được khi thực hiện phép tính EBP – 0C và gán cho thanh ghi EAX. Khác với lệnh này MOV EAX, [EBP – 0C], ngoài việc tính toán EBP-0C để có được kết quả địa chỉ giống như lệnh LEA, lệnh này còn tìm kiếm nội dung của một giá trị được lưu trữ tại địa chỉ đã tính toán và gán giá trị đó cho EAX.

Chốt lại vấn đề, điều rất quan trọng giúp chúng ta nhận ra sự khác biệt giữa lệnh LEA và lệnh MOV là:

  • Lệnh LEA thực hiện lấy địa chỉ biến. Tương ứng mã giả là a = &b
  • Lệnh MOV thực hiện lấy giá trị được lưu tại địa chỉ biến. Tương ứng với mã giả là a = *b

Khi phân tích ứng dụng Vewiever, chúng ta thấy trong kết quả của việc tìm kiếm lệnh LEA thì hầu như đều sử dụng lệnh này để có được địa chỉ của các biến hoặc tham số trên ngăn xếp. Có rất nhiều lệnh sử dụng [EBP + something].

Một ứng dụng khác của lệnh LEA là các phép toán kết hợp giữa các thanh ghi và các hằng số, kết quả tính được gán cho toán hạng đầu tiên có thể một số hoặc một địa chỉ phụ thuộc vào giá trị của các thanh ghi.

Trong hình trên, tại thời điểm thực hiện tính toán, giả sử nếu ESI có giá trị 400000 và EAX bằng 2, thì giá trị của thanh ghi EDX sẽ là kết quả tính toán của biểu thức 0x400000 + 2*4+0x14:

Nói cách khác, nó sẽ gán giá trị 0x40001c vào thanh ghi EDX. Cơ bản về lệnh LEA đến đây là kết thúc và cũng là kết thúc của phần 5.

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

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


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.

Image result for stack lower memory addr

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


IDA Loader

Các bạn đã thấy rằng khi mở một file thực thi trong IDA, nó sẽ sử dụng bộ phân tích tĩnh để phân tích file hay còn được gọi là Loader. Ở chế độ Loader này, chương trình sẽ không được thực thi, nhưng nó được IDA phân tích và sau cùng sẽ tạo ra một file .idblà cơ sở dữ liệu lưu các thông tin trong quá trình phân tích, bao gồm đổi tên biến, tên hàm, các chú thích…. Trên thực tế, file .idb sẽ là tổng hợp của 5 files(.id0, .id1, .nam, .id2, and .til) được sinh ra trong quá trình phân tích:

Bên lề: IDA không có tính năng Undo như các bạn hay làm việc với các trình soạn thảo văn bản, cho nên bất kì những thay đổi nào mà bạn thực hiện trong quá trình phân tích sẽ không quay lại được và sẽ lưu thẳng vào database. Tuy nhiên, những thay đổi này chỉ là ở phía database mà thôi, nó sẽ không tác động trực tiếp lên binary gốc mà bạn đang phân tích.

Ở chế độ Loader đương nhiên sẽ không xuất hiện các cửa sổ Registers, cửa sổ Stack và danh sách các Module được nạp vào bộ nhớ mà chương trình sử dụng. Các thông tin này chỉ xuất hiện khi ta cho thực thi và debug chương trình ở chế độ Debugger. Chúng ta sẽ làm quen với tính năng debug của IDA ở các phần sau.

Sau khi nạp crackme của Cruehead vào IDA, quan sát trong danh sách các tiến trình (process) thông qua trình Task Manager, ta thấy không xuất hiện process của crackme này. Như vậy, có thể hiểu crackme không được thực thi trừ khi chúng ta sử dụng Debugger của IDA. Việc này cực kì hữu ích cho một số công việc nhất định như phân tích mã độc… Ở chế độ Loader, chúng ta có thể phân tích bất kỳ hàm nào của chương trình, tuy nhiên không phải lúc nào chúng ta cũng có thể truy xuất vào hàm mà ta cần tìm hiểu, lúc đó ta phải học cách để debug. Tất nhiên, để tìm hiểu cách phân tích các hàm, chúng ta cần phải trang bị kiến thức cơ bản về các thanh ghi và các câu lệnh asm cơ bản. Bởi vì mặc dù không debug, không có cửa sổ thanh ghi với các giá trị tại từng thời điểm, các câu lệnh sử dụng chúng thì dựa vào các kiến thức cơ bản này, ta có thể hiểu mục đích của hàm hoặc chương trình làm gì.

Các thanh ghi là gì và chúng được sử dụng cho những mục đích nào?

Nôm na các bạn có thể hiểu rằng, bộ vi xử lý khi thực thi các chương trình cần có “trợ lý” phục vụ cho nó. Các thanh ghi lúc này sẽ hỗ trợ bộ vi xử lý trong quá trình thực thi chương trình. Chúng được xem như các vùng lưu trữ nhỏ được tích hợp sẵn trong bộ xử lý (volatile memory – chỉ giữ được dữ liệu khi máy tính còn hoạt động). Khi CPU thực thi một lệnh, nó phải lấy lệnh từ bộ nhớ, giải mã lệnh, và sau đó thực hiện hành động tương ứng với mục đích của lệnh. Các hành động mà CPU thực hiện có thể thao tác thông tin trong các thanh ghi hoặc trong bộ nhớ.

Khi tìm hiểu về các lệnh ASM, các bạn sẽ biết được nội dung của hai vị trí bộ nhớ không thể cộng trực tiếp với nhau. Bộ vi xử lý sẽ phải chuyển một trong số chúng vào thanh ghi và sau đó cộng nó với vị trí bộ nhớ còn lại. Đây chỉ là một ví dụ, dĩ nhiên các thanh ghi có thể sử dụng cho những mục đích nhất định, cụ thể hơn chúng ta sẽ đi chi tiết bên dưới. Trong kiến trúc 32 bit các thanh ghi được sử dụng là EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI và EIP. Vào cuối bài, sẽ có một phần nhỏ dành cho 64 bit.

Các thanh ghi dùng chung

Chúng ta có 8 thanh ghi dùng chung, gồm:

EAX (thanh ghi chứa – accumulator): được sử dụng nhiều nhất trong các lệnh số học, logic, và chuyển dữ liệu. Các thao tác nhân, chia sử dụng thanh ghi này. Với các hàm API của Windows, kết quả trả về của hàm thường sẽ lưu vào thanh ghi EAX.

EBX (thanh ghi cơ sở – base): thanh ghi EBX có thể truy cập trực tiếp dữ liệu bộ nhớ và nó cũng là một thanh ghi dùng chung.

ECX (thanh ghi đếm – count): ECX là một thanh ghi dùng chung có thể được sử dụng như là một bộ đếm cho các lệnh khác nhau. Nó cũng có thể chứa địa chỉ lệch của dữ liệu trong bộ nhớ. Các lệnh sử dụng bộ đếm là các lệnh liên quan lặp chuỗi, các lệnh chuyển, xoay và LOOP / LOOPD.

EDX (thanh ghi dữ liệu – data): là một thanh ghi dùng chung dùng để chứa một phần kết quả của phép nhân hoặc một phần của phép chia. Nó cũng có thể truy cập địa chỉ dữ liệu trong bộ nhớ trực tiếp.

EDI (chỉ số đích – destination): EDI thường được sử dụng trong các thao tác làm việc với chuỗi hoặc mảng. Thanh ghi này sẽ trỏ tới chuỗi đích. Bên cạnh đó nó cũng là một thanh ghi dùng chung.

ESI (chỉ số nguồn – source): Giống như EDI, ESI cũng thường được sử dụng trong các thao tác làm việc với chuỗi hoặc mảng. Thanh ghi này sẽ trỏ tới chuỗi nguồn.

EBP (con trỏ cơ sở – base): EBP trỏ tới vị trí bộ nhớ, bên cạnh mục đích dùng chung thì nó được sử dụng làm frame pointer để truy xuất các tham số và các biến cục bộ trong ngăn xếp của một hàm.

ESP (con trỏ ngăn xếp – stack): thanh ghi này luôn trỏ đến đỉnh hiện thời của Stack. Theo nguyên tắc làm việc của Stack thì thanh ghi này sẽ hướng về phía địa chỉ thấp hơn.

Như vậy, có tổng cộng 8 thanh ghi 32 bit dùng chung là EAX, EBX, ECX, EDX, ESP, EBP, ESI và EDI. Ngoài ra, các thanh ghi này còn có thể chia nhỏ thành các thanh ghi 16-bit và 8-bit như hình dưới đây:

Ví dụ, nếu thanh ghi EAX có giá trị là 0x12345678 thì AX là thanh ghi 16 bit chứa bốn chữ số cuối cùng:

Thanh ghi AX có thể được tách thành 2 thanh ghi 8 bit, đó là cặp thanh ghi: AH chứa hai số 5 và 6 và AL chứa hai số cuối cùng là 7 và 8:

Như vậy các bạn có thể hình dung, thanh ghi 32 bit EAX được tách thành một thanh ghi 16 bit gọi là AX; AX được tách thành hai thanh ghi 8 bit được gọi là AHAL. Tương tự đối với các thanh ghi EBX (BX, BH và BL), ECX (CX, CH và CL) và EDX (DX, DH và DL), riêng các thanh ghi còn lại chỉ được tách thành một thanh ghi 16 bit, không chia nhỏ thêm thành các thanh ghi 8 bit:

Các thanh ghi đặc biệt

Bên cạnh các thanh ghi dùng chung ở trên, các bạn sẽ gặp các thanh ghi đặc biệt khác nữa, bao gồm:

EIP (con trỏ lệnh – instruction): đây là một thanh ghi đặc biệt, nó luôn trỏ đến lệnh tiếp theo sẽ được thực hiện. Khác với các thanh ghi khác, EIP không thể bị tác động trực tiếp bởi các lệnh.

Một thanh ghi quan trọng khác là EFLAGS (thanh ghi cờ), mỗi bit của nó được dùng để phản ánh một trạng thái nhất định của phép toán. Dựa theo kết quả tính toán mà các cờ sẽ được bật và căn cứ trên các cờ này để thực hiện rẽ nhánh thực thi của chương trình, chúng ta sẽ tìm hiểu thêm sau.

Bên lề: Zero Flag (ZF) là cờ phổ biến nhất được sử dụng trong reversing. Chủ yếu được sử dụng trong các lệnh rẽ nhánh có điều kiện, làm thay đổi luồng thực thi dựa trên các kết quả lệnh trước đó.

Tiếp theo là các thanh ghi đoạn, các thanh ghi này trỏ tới các phần khác nhau của file thực thi như CS = CODE, DS = DATA v..v…

Trong quá trình làm việc với thanh ghi và bộ nhớ thì có một chi tiết quan trọng khác là kích thước của các kiểu dữ liệu thường được sử dụng nhiều nhất:

IDA hỗ trợ xử lý nhiều loại dữ liệu mà chúng ta sẽ thấy qua từng phần một. Điều quan trọng là phải ghi nhớ rằng BYTE là 1 byte, WORD là 2 bytes và DWORD 4 bytes trong bộ nhớ.

Các lệnh ASM cơ bản

Lý do các bạn cần biết về Assembly language đó là bởi nó nằm ở tầng thấp nhất trong Software chain. Khi một phần mềm thực hiện bất kỳ hành động nào, nó cũng sẽ được biểu diễn bằng các lệnh ASM. Người ta xem Assembly là ngôn ngữ của “Reverse Engineering”, do đó để có thể bước vào con đường của một người làm về dịch ngược, bạn phải tự mình rèn luyện để có kiến thức vững về ngôn ngữ assembly trên nền tảng mà bạn muốn nghiên cứu, bởi mỗi hệ thống/ nền tảng khác nhau sẽ có tâp lệnh Asm riêng.

IDA phân rã các lệnh ASM với một số cú pháp khác so với các trình debugger như OllyDbg/x64dbg, cho nên bạn nào đang quen với cách đọc lệnh trên OllyDbg sẽ cảm thấy hơi rối một chút.

Các lệnh chuyển dữ liệu

MOV

MOV dest, srcSao chép nội dung của toán hạng nguồn (src) tới đích (dest). Thao tác: dest <- src. Lệnh này được sử dụng để chuyển dữ liệu giữa các thanh ghi, giữa một thanh ghi và một ô nhớ hoặc chuyển trực tiếp một số vào một thanh ghi hay ô nhớ. Hiểu cơ bản thì lệnh mov này có thể tương ứng với lệnh gán ở ngôn ngữ bậc cao.

Lấy một số ví dụ, đầu tiên là chuyển giá trị từ một thanh ghi này vào thanh ghi khác.

Câu lệnh như sau:MOV EAX, EDI; EAX nhận giá trị của EDI; còn EDI giữ nguyên giá trị, không bị thay đổi.

Nói chung, chỉ có thể chuyển trực tiếp dữ liệu từ hoặc đến thanh ghi, ngoại trừ thanh ghi EIP không thể là Destination hoặc Source của bất kỳ hoạt động nào. Chúng ta không thể thực hiện câu lệnh sau:

MOV EIP, EAX; Câu lệnh hày hoàn toàn không hợp lệ.

Ví dụ tiếp theo thực hiện chuyển một hằng số vào một thanh ghi như sau:

MOV EAX, 1; chuyển số 1 vào thanh ghi EAX, giá trị trước đó của thanh ghi EAX bị ghi đè lên (thay bằng giá trị mới).

Tiếp theo là câu lệnh thực hiện chuyển giá trị của một địa chỉ ô nhớ không phải là nội dung của ô nhớ đó. Các hình minh họa dưới đây là lệnh trong file VEViewer.exe (download tại đây: https://mega.nz/#!CLIgmS5S!s5qrzoxbRP5W9xblRf8bgVz5UWRcR9yGoICF-PpJbR4 )

Ở ví dụ trên, thanh ghi EAX lúc này sẽ nhận giá trị là một địa chỉ bộ nhớ. Tiền tố offset ở phía trước chỉ ra rằng phải lấy địa chỉ chứ không phải nội dung của ô nhớ đó. Vì vậy, nếu tôi nhấn Q, IDA sẽ chuyển đối câu lệnh này thành dạng:

MOV EAX, 46f038h; đây là một lệnh giống như ở OllyDbg, nhưng không cung cấp cho ta bất kỳ thông tin gì về nội dung của địa chỉ đó. Nếu nhấp chuột phải vào địa chỉ 46f038, ta có thể quay về lệnh gốc ban đầu mà IDA đã hiển thị:

Có một câu hỏi đặt ra: Liệu IDA có thể cho tôi biết về các thông tin bổ sung liên quan tới địa chỉ bộ nhớ đó không?

Nếu tôi chuyển qua cửa sổ Hex View và tìm địa chỉ trên bằng cách nhấn phím tắt G và nhập vào địa chỉ cần đến:

Ta thấy rằng, tại địa chỉ này đang lưu các bytes có giá trị 0x00. Theo mô tả địa chỉ trong IDA, tôi biết đó là một DWORD:

Nếu trở lại màn hình disassembly và nhấp đúp vào địa chỉ này, ta sẽ tới đây:

Tiền tố dword đứng đằng trước một địa chỉ, có nghĩa là nội dung của địa chỉ đó là một DWORD, sau đó có kiểu dữ liệu dd tương ứng với DWORD và tiếp theo là giá trị 0 lưu tại vị trí ô nhớ đó. Như vậy, IDA đang nói với tôi rằng chương trình sử dụng địa chỉ đó để lưu DWORD và hơn thế nữa, ở bên phải tôi thấy các tham chiếu đến vùng code mà DWORD này sẽ được sử dụng.

Như trên hình, ta thấy có hai chỗ tham chiếu. Mỗi mũi tên là một vị trí và khi đặt con trỏ chuột tại đó ta có thể xem trước được mã lệnh tại từng vị trí này.

Nếu tôi nhấn phím X trên đầu của địa chỉ, IDA sẽ hiển thị cho ta thấy các lệnh sử dụng tới địa chỉ đó:

Lệnh đầu tiên trong hình sẽ thực hiện đọc địa chỉ mà chúng ta đã thấy ở trên và lưu vào thanh ghi eax. Câu lệnh thứ hai sẽ ghi một DWORD (phụ thuộc vào giá trị của eax) vào nội dung bộ nhớ tại địa chỉ 0x46F038.

Vì vậy, trong IDA lệnh đầu tiên không chỉ thông báo cho ta rằng nó sẽ chuyển một địa chỉ vào một thanh ghi mà nó còn cho biết địa chỉ đó chứa một DWORD, đó chính là thông tin bổ sung thêm mà IDA cung cấp. Một điểm cộng cho IDA. Vậy nên, chúng ta thấy rằng khi đề cập đến các địa chỉ, IDA sẽ kèm theo tiền tố là offset và khi chúng ta đi tìm nội dung của địa chỉ đó, như trong trường hợp này sẽ có giá trị là 0. IDA mặc định không sử dụng dấu ngoặc [] như OllyDbg nếu đó là một địa chỉ.

Tổng kết lại những gì đã viết dài dòng ở trên 🙂 :

mov     eax, offset dword_46F908

Chuyển địa chỉ 0x46F908 vào thanh ghi EAX, tức là EAX = 0x46F908

mov     eax, dword_46F908

Chuyển nội dung hoặc giá trị tại địa chỉ đó vào thanh ghi EAX, tức là EAX = 0x0

Với những ai đã quen với việc sử dụng OllyDbg thì lệnh này sẽ được hiển thị trong OLLY với cặp ngoặc vuông: MOV EAX, DWORD PTR DS:[46f908]. Còn trong IDA, khi một địa chỉ có tiền tố offset ở phía trước thì có nghĩa là đang nói đến giá trị số của địa chỉ, và khi thay bằng tiền tố dword, thì có nghĩa là sử dụng đến nội dung / giá trị tại địa chỉ đó. Điều này chỉ xảy ra khi đề cập đến các địa chỉ là số, nếu ta làm việc với các thanh ghi thì sẽ thế nào? Quan sát hình dưới đây:

Lệnh trong hình sử dụng dấu ngoặc [] vì rõ ràng bạn không biết giá trị tĩnh mà thanh ghi có thể có được tại thời điểm đó và bạn không thể biết hướng nào bạn sẽ trỏ đến để lấy thêm thông tin từ đó. Tất nhiên trong trường hợp này, ví dụ nếu thanh ghi EDI trỏ tới 0x10000, lệnh này sẽ tìm nội dung trong địa chỉ bộ nhớ đó và sao chép nó vào thanh ghi ECX.

Vì vậy, điều quan trọng là phải hiểu rằng khi IDA sử dụng tiền tố offset ở phía trước của một địa chỉ, nó hàm ý đề cập đến địa chỉ ô nhớ mà không liên quan đến nội dung tại địa chỉ ô nhớ đó.

Lấy một ví dụ khác:

Trong hình trên, chúng ta thấy rằng EAX sẽ nhận giá trị 0x45f4d0 vì nó có tiền tố offset ở phía trước và nó cho ta biết rằng IDA không thể nhận biết được kiểu dữ liệu là gì nên có thêm tiền tố unk (unknown).

Trong lệnh được đánh dấu trên hình, lệnh đầu tiên thực hiện chuyển nội dung của 0x46fc50 là một giá trị DWORD và với lệnh ở dưới nó sẽ chuyển địa chỉ chính nó, nghĩa là giá trị số 0x46FC50.

Chúng ta có thể thấy giá trị đang lưu tại địa chỉ để xem giá trị nào sẽ được chuyển vào EAX tại địa chỉ 0x42f302, nhấp đúp tại 0x46fc50 ta sẽ thấy như sau:

Chúng ta thấy rằng lệnh sẽ thực hiện gán giá trị 0 cho thanh ghi EAX, nếu như một lệnh khác trong danh sách các tham chiếu không được thực hiện và lưu giá trị khác vào địa chỉ này.

Câu lệnh được đánh dấu ở trong hình sẽ lưu một giá trị DWORD vào địa chỉ bộ nhớ, trong khi những lệnh khác chỉ đọc địa chỉ với tiền tố offset hoặc đọc ra giá trị tại địa chỉ.

Ngoài các ví dụ trên, tất nhiên ta cũng hoàn toàn có thể gán hằng số vào thanh ghi 16-bit và 8-bit như chúng ta đã thấy trước đó:

Lệnh trên thực hiện gán giá trị 1 vào thanh ghi AL và giữ nguyên giá trị ban đầu đã có trước đó của thanh ghi EAX. Trường hợp này chỉ có các byte thấp của thanh ghi là bị thay đổi.

Lệnh trên gán nội dung của địa chỉ bộ nhớ 0x459c24 vào thanh ghi AX và cho chúng ta biết giá trị lấy được có kích thước là một WORD.

Và như trong hình chúng ta thấy rằng giá trị ban đầu đang là 0, có thể sau đó khi thực thi chương trình giá trị này sẽ bị thay đổi.

Trong hình minh họa trên, giá trị của thanh ghi AX được gán vào nội dung của địa chỉ đang trỏ bởi thanh ghi EBX. Vì nó là một thanh ghi và không biết đang lưu giá trị nào vào thời điểm đó, nó sử dụng cặp ngoặc [ ] để chỉ ra rằng nó ghi vào nội dung của EBX.

Thêm một ví dụ khác, lệnh trên sẽ thực hiện sẽ gán giá trị của thanh ghi AX vào nội dung của ESI + 8. Tiếp tục, tôi có lệnh như sau:

Nếu tôi nhấp đúp vào tên dài loằng ngoằng như trong hình, IDA sẽ dẫn tới đây:

Như đã biết, bảng IAT là bảng lưu thông tin địa chỉ của các hàm được import khi thực thi file, hầu như luôn luôn nằm tại section .idata hoặc .rdata. Nếu tôi quan sát địa chỉ đó tại màn hình Hex View, nó vẫn chưa có giá trị của hàm bởi vì IAT chỉ được điền đầy đủ khi thực thi chương trình, còn hiện tại thì vẫn chưa.

Có thể IDA của các bạn sẽ không hiển thị tên giống như ở màn hình của tôi. Để chuyển đổi, bạn vào menu Options – Demangle Names và chọn Names:

Để biết hàm này được lấy từ thư viện nào ta cuộn chuột lên trên một chút, kết quả là các hàm được import từ thư viện QtCoreX.dll và ở trên còn có nhiều dlls khác nữa:

Như vậy, qua bài viết này, các bạn đã thấy được rất nhiều ví dụ khác nhau của lệnh MOV. Bạn có thể thực hành và xem tại IDA với tập tin thực thi tôi đã gửi kèm. Trong phần 4, chúng ta sẽ tiếp tục với các câu lệnh khác.

Bên lề: kiến trúc x64 được thiết kế như một phần mở rộng cho x86 và có sự tương đồng mạnh mẽ với các tập lệnh x86. Có một vài sự khác biệt từ góc độ phân tích mã lệnh:

  • Các thanh ghi dùng chung 32 bit (4 byte) eax, ebx, ecx, edx, esi, edi, ebp và esp được mở rộng thành 64 bit (8 byte); các thanh ghi này được đặt tên là rax, rbx, rcx, rdx, rsi, rdi, rbp và rsp.
  • 8 thanh ghi mới được bổ sung thêm là r8, r9, r10, r11, r12, r13, r14, và r15.
  • Một chương trình có thể truy cập vào thanh ghi dưới dạng 64 bit (rax, rbx, v.v.), 32 bit (eax, ebx, v.v.), 16 bit (ax, bx, v.v.) hoặc 8 bit (al, bl, …).
  • Truy cập các thanh ghi r8 – r15 dưới dạng byte, word, dword hoặc qword bằng cách bổ sung thêm b, w, d hoặc q vào sau tên thanh ghi.
  • Trong kiến trúc x86, các tham số của hàm sẽ được đẩy vào ngăn xếp trước khi gọi hàm, trong khi ở kiến trúc x64, bốn tham số đầu tiên được truyền vào các thanh ghi rcx, rdx, r8 và r9 và nếu chương trình còn các tham số khác nữa, chúng sẽ được lưu vào stack. Điều này sẽ khiến cho khó xác định được xem địa chỉ bộ nhớ nào là biến cục bộ hay tham số của hàm.

Hẹn gặp lại các bạn!

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


Lời tựa

Như tôi đã đề cập ở phần 1, mục tiêu của bộ tutorial này là dành cho những người mới bắt đầu, do vậy có những kiến thức mà có thể nhiều bạn đã biết rồi. Bạn có thể bỏ qua nếu muốn, nhưng đối với đa số những người chưa biết, những kiến thức này sẽ rất quan trọng và đó là lý do tại sao tôi vẫn dành thời gian để hệ thống hóa lại những kiến thức cơ bản này.

Hệ thống số

Ba hệ thống số học được sử dụng nhiều nhất là số nhị phân, thập phân và thập lục phân.

Các khái niệm cơ bản của từng hệ thống số học này như sau:

BINARY (Số nhị phân): các số được biểu diễn bằng hai chữ số là 0 và 1, đó là lý do tại sao nó được gọi là nhị phân. Máy tính chỉ hiểu được số nhị phân.

DECIMAL (Số thập phân): các số được biểu diễn bằng 10 chữ số (từ 0 đến 9), đó là lý do tại sao nó được gọi là số thập phân. Con người chúng ta từ lúc sinh ra mặc định sẽ được làm quen với hệ thống số này.

HEXADECIMAL (Số thập lục phân): Tất cả các số được biểu diễn với các kí tự từ 0 đến F (từ 0 đến 9, cộng với A, B, C, D, E và F (không phân biệt chữ hoa và chữ thường), có nghĩa là sẽ có tổng cộng 16 kí tự). Trong đó: A = 10, B = 11, C = 12, D = 13, E = 14, F = 15.

Bên lề: Hệ nhị phân là cách mà một máy tính “hiểu” và “hành xử”, nhưng với con người thì nó thực sự rất bất tiện. Nó tốn quá nhiều chữ số để biểu diễn cho một số, dẫn đến khó viết và khó ghi nhớ. Chính vì lý do này, chúng ta mới cần đến hệ thập lục phân để khắc phục những vấn đề đó. Khi tìm hiểu về reversing/cracking và kể cả khi xây dựng mã khai thác (exploits), các bạn sẽ liên tục phải làm việc với các giá trị hexa, chính vì thế các bạn cần phải nắm vững nó.

Quan sát tại giao diện của IDA, nếu như bạn đã cài đặt chuẩn thì ở dưới cùng sẽ thấy có một thanh dùng để thực hiện các câu lệnh Python. Điều này sẽ hỗ trợ chúng ta chuyển đổi giữa các hệ số một cách rất dễ dàng:

Nếu tôi gõ, ví dụ 0x45, giá trị này được diễn giải như là một số thập lục phân bởi vì có tiền tố 0x ở phía trước. Chúng ta có thể chuyển đổi từ thập lục phân sang thập phân chỉ bằng cách nhấn Enter.

Kết quả có được là:

Ta nhận được số 69, đó chính là 0x45 ở hệ thập lục phân chuyển đổi sang hệ thập phân. Nếu muốn chuyển đổi ngược lại, chúng ta phải sử dụng hàm hex():

Để chuyển đổi sang hệ nhị phân ta sử dụng bin():


Kết quả có được là 1000101, tiền tố 0b ở đằng trước có nghĩa đây là số nhị phân. Chúng ta cũng có thể chuyển đổi từ hệ nhị phân sang thập phân hoặc thập lục phân.

Qua đây có thể thấy, bất kỳ số nào ta nhập trực tiếp vào thanh Python, sau khi nhấn Enter sẽ hiển thị kết quả ở hệ thập phân, để chuyển nó sang hệ Hexa hoặc Binary tương ứng, chúng ta phải sử dụng các hàm hex() hoặc bin() của Python.

Số âm trong hệ thập lục phân

Hầu như chúng ta sẽ làm việc nhiều ở hệ thập lục phân, nhưng câu hỏi đặt ra là làm thế nào để biểu diễn một số âm (32 bit) ở hệ thập lục phân? Khi các bạn học về lập trình, các bạn được học về số nguyên (integer). Để thông báo cho máy tính biết các bạn sử dụng số âm thì các bạn sẽ khai báo kiểu biến là signed integer. Lúc này, phía máy tính, nó cần phải có một phương thức để biểu diễn cho các số âm. Nếu máy tính được yêu cầu biểu diễn số âm thì sẽ nó bỏ qua bit có trọng số cao nhất (MSB) – tức là bit đầu tiên và xem bit này như là bit dấu. Nếu bit dấu là 0 đó là số dương, ngược lại nếu là 1 thì đó là số âm.

Tóm lại, với một số nhị phân 32 bit, người ta quy định sử dụng bit đầu tiên để biểu diễn dấu của số, là 0 thì đó là số dương và ngược lại, là 1 thì đó là số âm. Ví dụ:

Chúng ta thấy rằng giá trị -0x45 ở hệ thập lục phân có thể được biểu diễn là 0xffffffbb và bit đầu tiên của nó ở hệ nhị phân là 1. Bằng cách này, số dương nhỏ nhất rõ ràng là 0 nhưng số dương lớn nhất mà chúng ta có thể biểu diễn là bao nhiêu?

Chúng ta thấy rằng 0x7fffffff sẽ là số dương lớn nhất nếu ta quan tâm tới bit dấu. Nếu cộng thêm 1 vào giá trị này:

Khi đó, bit đầu tiên thay đổi thành 1 và tất cả các bit còn lại là 0. Vấn đề là trình Calculator của IDA xem xét tất cả các số đều là số dương trừ khi chúng ta thêm dấu ““ vào trước số, ví dụ:

Như vậy, giá trị âm lớn nhất là -1 tương ứng với 0xFFFFFFFF và giá trị âm nhỏ nhất sẽ là 0x80000000. Trường hợp, ta không quan tâm đến dấu, thì tất cả các giá trị sẽ là số dương nằm trong khoảng từ 0 đến 0xFFFFFFFF.

Còn nếu xem xét đến sử dụng bit dấu, chúng ta sẽ có các số dương nằm trong khoảng từ 0x0 đến 0x7FFFFFFF và các số âm nằm trong khoảng từ 0xFFFFFFFF đến 0x80000000.

Số dương

000000000 bằng 0

000000001 bằng 1

…………………………

…………………………

7FFFFFFF bằng 2147483647 thập phân (sẽ là số dương lớn nhất)

Số âm

FFFFFFFF bằng -1

FFFFFFFE bằng -2

…………………………

…………………………

80000000 bằng -2147483648 (sẽ là số âm nhỏ nhất)

Bảng mã ASCII

Một trong những vấn đề nữa mà chúng ta cần phải biết là cách mà hệ thống in/hiển thị dữ liệu trên màn hình, mỗi kí tự được gán tương ứng với một giá trị thập lục phân để biểu diễn chúng là các chữ cái, chữ số, hay biểu tượng, v.v …

Quan sát bảng ở bên dưới, chúng ta có cột đầu tiên là các giá trị thập phân, cột thứ hai là các giá trị thập lục phân và cột thứ ba biểu diễn các kí tự tương ứng. Lấy ví dụ, nếu tôi muốn viết một khoảng trống, tôi phải sử dụng 0x20 hoặc 32 ở hệ thập phân. Do đó, để biểu diễn bất kỳ kí tự nào mà chúng ta cần là số hay chữ cái, ta có thể sử dụng bảng mã này, và nó được gọi là bảng mã ASCII (American Standard Code for Information Interchange). Bảng mã ASCII được đùng để hiển thị văn bản trong máy tính và các thiết bị thông tin khác, nó cũng được dùng bởi các thiết bị điều khiển làm việc với văn bản.

Tại IDA calculator, nó sẽ xem xét các biểu thức và hiển thị các kí tự tương ứng như chúng ta đã thấy trong trường hợp 0x45 là kí tự E.

Một cách khác để chuyển đổi là sử dụng hàm chr() của Python:

Trong cửa sổ Hex Dump của IDA chúng ta cũng có một cột để hiển thị các kí tự tương ứng:

Như trên hình, chúng ta thấy 45 được biểu diễn là chữ E.

Chức năng tìm kiếm trong IDA

Các bạn để ý sẽ thấy có tùy chọn Search trên menu của IDA và nếu ở màn hình disassembly hoặc tab IDA – View, IDA sẽ cung cấp cho chúng ta nhiều lựa chọn tìm kiếm rất dễ hiểu. Khi chuyển sang các tab khác (trừ tab Hex-View) thì các tùy chọn của Search sẽ được lược bớt.

Các tùy chọn được hiển thị trong hình dưới đây có thể sẽ khác trên máy của các bạn, bởi bản IDA của tôi có bổ sung thêm một số plugin.

Next Code

Tùy chọn này sẽ tìm kiếm lệnh kế tiếp đã được hiểu là Code. Nếu như không được nhận diện là Code, nó sẽ bỏ qua.

Next Data

Tùy chọn này sẽ tìm kiếm các địa chỉ tiếp theo, nơi được IDA hiểu là Data (đã được định nghĩa) hoặc là xử lý dữ liệu tại bất kỳ section nào.

Như trong trường hợp trên, IDA phát hiện một dword (dd) tại địa chỉ không tương ứng với bất kỳ lệnh nào, rõ ràng là, nếu chúng ta tiếp tục tìm kiếm, nó sẽ tìm kiếm dữ liệu tiếp theo. Ví dụ, quan sát phần dữ liệu bên dưới khi tôi thực hiện tìm kiếm một lần nữa:

IDA dừng ở địa chỉ 00402004, tại đó thông tin ở bên phải hàm ý rằng đó là nơi mà bạn sẽ làm việc với dữ liệu, thuộc section DATA. IDA sẽ bỏ qua các địa chỉ chỉ chứa số 0, không có bất kỳ tham chiếu nào. Tóm lại, IDA chỉ tìm kiếm những dữ liệu mà chương trình có thể sử dụng tới.

Search completed. Found at 00402004.

Search completed. Found at 00402048.

Search Explored và Unexplored

Tính năng đầu tiên sẽ tìm code hoặc data đã được định nghĩa (lệnh hoặc dữ liệu) và tính năng thứ hai áp dụng với các vùng không được phát hiện là lệnh hoặc dữ liệu hợp lệ:

Khu vực với các giá trị 0 tại 0x402000 được tìm thấy với tính năng Search Unexplored.

Search completed. Found at 00402000.

Search completed. Found at 00402000.

Search completed. Found at 00402001.

Search completed. Found at 00402001.

Search completed. Found at 00402002.

Search completed. Found at 00402003.

Search completed. Found at 00402008.

Lặp lại việc tìm kiếm này, chúng ta thấy rằng nó bỏ qua dữ liệu tại 0x402004 vì dữ liệu đó được coi là Explored.

Search Immediate Value – Search Next Immediate Value

Lệnh này tìm kiếm lệnh đầu tiên hoặc byte dữ liệu có chứa giá trị hằng số được chỉ định. Quá trình tìm kiếm tương đối chậm (nhưng nhanh hơn nhiều so với tìm kiếm text):

Nếu chọn Find all occurrences, IDA sẽ tìm tất cả. Còn nếu không chọn, IDA sẽ tìm kiếm từng lần một, với trường hợp này, để lặp lại việc tìm kiếm, ta sẽ sử dụng Search next immediate value.

Search Text – Search Next Text

Tìm kiếm các chuỗi mà chúng ta nhập vào, bao gồm cả biểu thức chính quy nếu chúng ta muốn.

Nếu chúng ta chỉ tìm kiếm một lần, để tìm tiếp thì sử dụng Search Next Text.

Search Sequence Of Bytes

Tùy chọn này cho phép ta tìm kiếm các chuỗi các bytes ở hệ thập lục phân:

Nếu nhấp đúp vào kết quả đầu tiên và tại tùy chọn của IDA, ta thiết lập giá trị 6 tại Number of opcode bytes để hiển thị tối đa 6 bytes tương ứng với mỗi lệnh.

Kết quả IDA sẽ hiển thị opcode 90 90 mà ta đã yêu cầu tìm kiếm:

Search Not Function

Chức năng này cho phép tìm kiếm byte đầu tiên không thuộc về bất kỳ hàm nào:

Search completed. Found at 004013D7.

Trên hình, ta thấy có một lệnh RET đứng độc lập, như vậy nó không thuộc bất kỳ một hàm nào. Đôi khi có những hàm mà IDA không thể xác định đó là hàm nhưng chúng lại có các lệnh hợp lệ.

Trên đây, các bạn đã tìm hiểu những chức năng tìm kiếm quan trọng nhất mà IDA cung cấp thông qua Search menu, tất nhiên, bên cạnh đó IDA còn hỗ trợ khả năng xử lý các Python script, từ đó bạn có thể thực hiện nhiều tác vụ hơn.

Tôi sẽ cố gắng đi thật chậm, từng bước một, không vội vã để các đều có thể dễ dàng hiểu và nắm bắt được.

Phần 2 đến đây kết thúc. Hẹn các bạn trong phần 3!

m4n0w4r

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

Ủ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


Lời tựa

Loạt bài viết này không phải tôi nghĩ ra, tôi đã xin phép viết lại từ bộ tuts của thầy Ricardo Narvaja – ông là linh hồn và là người sáng lập ra nhóm Crackslatinos, hiện ông đang làm việc tại Core Security dưới vai trò là Exploit Writer. Bộ OllyDbg tôi đã viết trước đây (vẫn còn đang dở ) cũng là dựa vào các bài viết mà ông chia sẻ. Tôi ngưỡng mộ ông bởi trong thời gian nhiều năm sinh hoạt ở CLS, Ricardo đã viết và chia sẻ vô số các bài viết về Cracking và Reverse Engineering.

Ricardo Narvaja

Ý tưởng ra đời của loạt bài viết này rất đơn giản, đó là cập nhật bộ tài liệu về Cracking và Reversing với OllyDbg nhưng thay vào đó sẽ sử dụng công cụ nổi tiếng là IDA. Các bạn sẽ làm quen với IDA trên môi trường hệ điều hành Windows, từ những phần cơ bản nhất cho tới những kiến thức nâng cao hơn.

Bộ này tôi nghĩ dành cho những người mới bắt đầu tìm hiểu về IDA, bạn nào đã “expert” rồi thì có thể đọc cho vui và góp ý để tôi chỉnh lại những chỗ còn thiếu sót. Bạn nào cảm thấy mất thời gian khi đọc những gì tôi viết, hãy chuyển sang làm những việc khác khiến bạn thấy hứng thú hơn.

Tại sao lại là IDA Pro?

Vậy lý do gì mà tôi và các bạn cần phải học sử dụng IDA? Đó là vì, trong khi OllyDbg bó hẹp chúng ta bởi nó chỉ là một trình gỡ lỗi 32-bit, hoạt động ở chế độ user mode của Windows thì IDA Pro là một công cụ reversing hoàn chỉnh, sử dụng được trên cả hai nền tảng 32-bit và 64-bit, vừa là một trình disassembler đồng thời cũng hỗ trợ debug như một trình debugger.

Mặc dù, hiện nay các chuyên gia đang chuyển dần sang sử dụng x64dbg (https://github.com/x64dbg) để thay thế cho OllyDbg, nhưng dường như để vượt qua được cái bóng của OllyDbg thì sẽ vẫn còn cần một thời gian nữa. Minh chứng là các chuyên gia phân tích của China hiện nay vẫn dùng OllyDbg hàng ngày: https://ti.360.net/blog/articles/suspected-molerats-new-attack-in-the-middle-east-en/ hay chuyên gia Vitali Kremez vẫn dùng OllyDbg để phân tích song song với IDA: https://www.vkremez.com/2018/12/lets-learn-dissecting-apt28-zebrocy.html. Tác giả Kao tại Blog https://lifeinhex.com đã đưa ra những nhận xét cá nhân vì sao ông chưa sử dụng x64dbg trong công việc. Các bạn có thể đọc thêm tại đây: https://lifeinhex.com/why-im-not-using-x64dbg/https://lifeinhex.com/x64dbg-2-years-later/

Lý do nữa để cần phải biết sử dụng IDA là bởi công cụ này cung cấp khả năng phân tích tĩnh (static analysis) tốt hơn nhiều so với việc thực hiện bằng OllyDbg. Tuy nhiên, đối với những người ban đầu mới làm quen IDA sẽ thấy việc sử dụng nó khá phức tạp. Bù lại, khác với OllyDbg/x64dbg, IDA có thể hoạt động được trên các môi trường hệ điều hành khác nhau như Windows, Linux hoặc Mac OS X và thậm chí là remote (từ xa):

Danh sách các kiến trúc vi xử lý mà IDA hỗ trợ cũng rất rộng:

Qua đó, các bạn thấy việc học cách sử dụng IDA sẽ cho phép chúng ta mở rộng phạm vi công việc của mình, mặc dù trong loạt bài viết này tôi sẽ chỉ tập trung vào hai môi trường Windows 32 và 64 bit, ở chế độ user mode và đôi khi là kernel mode. Thực tế cho thấy, khi làm quen với công cụ này sẽ cho phép chúng ta dễ dàng thích ứng với bất kỳ môi trường nào.

Như đã nói ở trên, loạt bài viết này sẽ bắt đầu từ những kiến thức cơ bản nhất, do vậy sẽ có nhiều nội dung lặp lại mà có thể các bạn đã đọc trong loạt bài viết về OllyDbg. Các kiến thức đó sẽ được trình bày lại ở đây một lần nữa nhưng là với IDA và tôi sẽ cố gắng đi thêm nhiều vấn đề hơn. Nếu ai đó thấy những gì bạn đọc trong loạt bài viết này là rất phức tạp hoặc nhàm chán, có lẽ bạn nên dành thời gian thời gian để đọc các bài viết về OllyDbg trước để cảm thấy dễ dàng hơn hoặc làm công việc khác mà khiến bạn cảm thấy có hứng thú. Loạt bài viết này sẽ bao gồm tất cả các kĩ thuật như phân tích tĩnh (static) và động (dynamic), cracking, exploit và unpacking. Tôi sẽ cố gắng để đề cấp tới càng nhiều kĩ năng càng tốt. Hi vọng thế!!

Nhiệm vụ đầu tiên

Việc đầu tiên để có thể tìm hiểu về IDA là ta cần phải có công cụ này trước đã. Tuy nhiên, vấn đề ở chỗ, IDA là một chương trình mất phí. Để có được quyền sử dụng IDA Pro thì chúng ta phải bỏ tiền ra mua và cái giá của nó không hề rẻ. Hơn nữa, không phải cứ có tiền là mua được. Vì lý do trên, tôi không thể cung cấp IDA trực tiếp cho các bạn được. Tuy nhiên, với kĩ năng Google trong thời đại CMCN 4.0, tôi nghĩ các bạn hoàn toàn có thể tìm kiếm được các phiên bản đã leaked trên mạng là IDA Pro 6.8 hoặc 7.0 + Hexrays. Đây là ảnh minh họa phiên bản IDA 6.8 mà tôi có:

Trong hình trên, các bạn thấy có các tệp nén zip cùng với trình cài đặt của IDA là idapronw_hexarmw_hexx64w_hexx86w_150413_cb5d8b3937caf856aaae750455d2b4ae. Mật khẩu để cài đặt nằm trong file install_pass.txt. Ngoài ra, khi cài đặt IDA, nó cũng sẽ tự động cài đặt thêm phiên bản Python 2.7.6. Khuyến nghị các bạn nên sử dụng bản Python đi kèm với IDA. Do đó, trước khi cài đặt IDA, bạn nên gỡ bỏ bản Python khác đã được cài đặt trên máy để tránh xung đột.

Bên lề: trên thực tế ta vẫn có thể cài đặt riêng phiên bản Python khác mà không cần sử dụng bản đi kèm theo bộ cài đặt IDA. Ví dụ, tôi đang sử dụng bản 2.7.10:

Sau khi cài đặt xong ta sẽ mở IDA lên để làm quen, và tương tự như trong loạt bài viết về OllyDbg, chúng ta sẽ sử dụng crackme là Cruehead để thực hành (crackme này bạn nào đọc bộ OllyDbg chắc sẽ có hoặc có thể download tại đây: https://mega.nz/#!KbAWzK7D!LOnvJeYpXpxkMIH_jLGzx7VKVTQg1LifsFQMnOj_WeM). Vì crackme là file thực thi 32-bit nên ta sẽ mở nó bằng phiên bản IDA 32-bit.

Bên lề: Để kiểm tra xem một file exe là 32 hay 64 bit sẽ có nhiều cách, ở đây tôi sử dụng một chương trình Hex Editor, chẳng hạn như là HxD (https://mh-nexus.de/en/downloads.php?product=HxD). Dựa vào dấu hiệu sau để biết file là 32-bit hay 64-bit:

Như trên hình, khi tôi mở công cụ SnippingTool (có trên các phiên bản Windows mới) trong HxD, dựa vào vào dấu hiệu sau chữ PE: PE..d† thì ta khẳng định đây là một file 64-bit. Trong khi, đối với crackme Cruehead của chúng ta, sau PE có: PE..L, vậy đây là một file 32-bit:

Từ thông tin đó, chúng ta biết sẽ phải mở Crackme bằng phiên bản 32-bit của IDA. Khi giao diện của IDA xuất hiện, chọn New để mở một tệp tin mới, tìm đường dẫn tới Crackme để load nó vào IDA:


Khi bạn nạp một file vào IDA, IDA sẽ có gắng nhận diện định dạng của file đó (ví dụ: IDA đã nhận biết chính xác crackme là một PE file) cùng với kiểu kiến trúc vi xử lý. Quá trình này cho ta thấy IDA hành xử giống với Windows loader. Mặc định, IDA sẽ không load kèm PE header hay resource section trong quá trình phân rã code của nó. Nếu bạn tích chọn Manual load, IDA sẽ hỏi bạn khi bạn muốn nạp từng section, kèm theo PE header.

Giữ nguyên các thiết lập mặc định và nhấn OK để IDA tiến hành phân tích file. Nếu chúng ta đồng ý hiển thị chế độ Proximity View (có từ phiên bản IDA v6.2), một màn hình graph sẽ xuất hiện cung cấp thông tin về các hàm mà IDA nhận diện được trong Crackme theo mô hình cây như bên dưới. Tóm lại, ở chế độ proximity view này, địa chỉ hiện tại, hàm gọi (caller) và hàm được gọi (callee) được biểu diễn dưới dạng tập hợp các node có liên kết với nhau:

Để chuyển qua lại giữa chế độ đồ hoạ (Graph) hoặc các lệnh ở chế độ Text bình thường, ta sử dụng phím tắt là spacebar. Tại Options – General, tích chọn Line prefixes để hiện thị thêm thông tin về địa chỉ bộ nhớ ở bên cạnh các lệnh trong chế độ đồ hoạ của IDA và tại Number of opcode bytes, nếu chúng ta thay đổi giá trị tại đó (mặc định là bằng 0), chúng ta sẽ thấy các opcodes hoặc các bytes xuất hiện tại mỗi lệnh.

Khi phân tích một tệp tin thực thi, đầu tiên IDA mở ở chế độ Disassembler (hay có thể được gọi là Loader) và sẽ không thực thi chương trình. IDA lúc này chỉ phân tích tĩnh để phục vụ cho mục đích reversing và tạo tệp tin .idb (cơ sở dữ liệu) để lưu toàn bộ quá trình phân tích của chúng ta.

Để có thể debug được, chúng ta phải lựa chọn các tùy chọn debuggers khác nhau được hỗ trợ bởi IDA và thực hiện trong chế độ Debugger mà chúng ta sẽ tìm hiểu ở các bài viết sau này. IDA cung cấp rất nhiều cửa sổ được xem như là các tab và khi truy cập View-Open Subview, chúng ta có thể mở các tab này theo mục đích sử dụng riêng của từng người.

Mới đầu, khi dùng IDA các bạn sẽ thấy chưa quen và có cảm giác hơi ngợp vì có quá nhiều tính năng. Thậm chí tại màn hình graph của IDA bạn sẽ thấy xuất hiện nhiều chỗ có cùng một địa chỉ. Ví dụ như tại điểm bắt đầu của một hàm, các địa chỉ được lặp lại nhiều lần, điều này xảy ra là bình thường vì có rất nhiều thông tin từ địa chỉ đó và nó không đủ để biểu diễn / hiển thị trên cùng một dòng.

Mẹo nhỏ là tìm điểm kết thúc của cùng một địa chỉ được lặp lại, tại đó chúng ta sẽ thấy nơi bắt đầu của lệnh được disassembly. Trong trường hợp này, lệnh tương ứng với địa chỉ 401000 là push 0.

Trong IDA, ta có thể độc lập tùy chỉnh giao diện mặc định cho Loader và Debugger. Tại màn hình Loader, khi đã thiết lập các cửa sổ và các tab mà chúng ta sử dụng nhiều nhất theo ý thích của mình, ta có thể lưu lại bằng cách vào Windows-Save Desktop và tích chọn Default để lưu vào cấu hình mặc định. Tương tự, trong chế độ Debugger, ta cũng có thể tùy chỉnh theo ý muốn và lưu lại thành cấu hình mặc định.

Tại bất kỳ tab nào của IDA như Functions, Strings, Names, v.v …

Chúng ta có thể tìm kiếm tại các cửa sổ này bằng cách nhấn CTRL + F, khung tìm kiếm nhỏ sẽ xuất hiện bên dưới, cho phép lọc theo những gì chúng ta nhập vào. Trong View-Open Subview-Strings như ở ví dụ bên dưới, IDA sẽ hiển thị cho ta những chuỗi có chứa “try”:

Ngoài ra, nếu tôi chọn View-Open Subview-Disassembly, tôi có thể mở một cửa sổ disassembly thứ hai, qua đó có thể truy xuất tới các hàm khác với cửa sổ đầu tiên và như vậy ta có thể phân tích được nhiều hàm tại cùng một thời điểm. Đây là chính là một lợi thế của IDA so với OllyDbg/x64dbg:

IDA cũng cung cấp một cửa sổ hiển thị dạng hexadecimal tương tự như cửa sổ Dump của OllyDBG/x64dbg. Trong IDA, cửa sổ này có tên là Hex View. Nếu bạn không thấy cửa sổ này, có thể hiển thị nó thông qua View – Open Subview- Hex dump:

Cũng từ Open Subview, ta có thể truy xuất thông tin về các hàm được chương trình import thông qua Imports tab như hình dưới đây:

Cũng tại màn hình Loader của IDA, các bạn cũng sẽ thấy một màn hình nhỏ khác có tên là Graph overview (Nếu không thấy, vào View – Graph Overview để mở nó), nằm ở góc dưới trái. Cửa sổ này rất hữu ích, nó hiển thị các code block của hàm mà ta đang phân tích, qua đó ta có thể di chuyển qua lại giữa các code block một cách nhanh chóng và dễ dàng.

IDA cũng có các tab dành riêng khác như Structures, Exports, Names, Segments v..v… chúng sẽ được giải thích khi chúng ta tìm hiểu và sử dụng tới các Tab này. Thanh điều hướng (navigator) phía trên hiển thị các phần khác nhau của một file đang phân tích bằng các màu khác nhau.

Ngay bên dưới là các thông tin mô tả cho chúng ta biết mỗi màu có ý nghĩa gì, ví dụ màu xám là .data section và nếu bạn nhấp vào phần màu xám đó, IDA sẽ chuyển tới section có địa chỉ màu xám. Trong hình trên, chúng ta thấy có màu hồng tương ứng với external symbol hay .idata section và màu xanh tương ứng với các hàm được phát hiện tại code section.

Phần thứ nhất đến đây là hết, trong phần này tôi đã cung cấp cho các bạn một cái nhìn tổng quan về IDA. Trong các phần tiếp theo, chúng ta sẽ tìm hiểu chi tiết hơn.

Bên lề: trong loạt bài viết này có thể tôi sẽ sử dụng lẫn lộn giữa IDA 6.8 và IDA 7. Khác biệt giữa hai bản này tôi xin đưa ra một hình minh họa sau (phân tích cùng một binary) để các bạn thấy vì sao “giang hồ mạng” thường chờ đợi bản leak mới nhất của IDA 😀

IDA 6.8
IDA 7.0 (nhận diện được hàm main cũng các hàm thư viện)

Kết thúc phần 1. Hẹn các bạn trong phần 2.

m4n0w4r

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

Ủ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

IDA Tutorial….

Posted: February 15, 2019 in IDA Tutorial...
Tags: ,

  • – Nhiều bạn hỏi tôi: “Ngoài bộ OllyDbg đang viết dở (rất dở 🙂 ) cùng hầm bà lằng các bài viết khác thì anh có viết gì liên quan đến IDA không?
  • – Tôi trả lời: “Có. Tôi đang viết …“.
  • – “Vậy anh có định đưa lên Blog không anh?
  • – Tôi trả lời: “Có lẽ phải chờ một dịp thật đặc biệt hoặc một lúc nào đó tâm trạng tôi cảm thấy vui thì tôi sẽ chia sẻ các bài viết này…

Và ngày đặc biệt đó cũng đã tới!! Tôi đón thêm một cô công chúa nhỏ sau một quãng thời gian chờ đợi đủ dài … Cũng như các bạn hàng ngày vẫn vào Blog của tôi để chờ xem có gì mới… Tôi cảm ơn các bạn đi “ngang qua” Blog, để lại vài dòng comment để tôi biết rằng nó vẫn còn sống 😦

Trong không khí của bỉm sữa không gì tả nổi, tôi quyết định sẽ publish bộ IDA mà tôi đang viết dở (thực sự rất dở.. 🙂 ) . Bộ này tôi nghĩ dành cho những người mới bắt đầu tìm hiểu về IDA, bạn nào expert rồi thì có thể đọc cho vui và góp ý để tôi chỉnh lại những chỗ còn thiểu sót.


Tôi sẽ sớm đưa các bài viết lên Blog …. Các bạn chờ nhé!!!

PS1: Bộ tuts này không phải tôi nghĩ ra, tôi đã xin phép viết lại từ bộ tuts của thầy Ricardo Narvaja – ông là linh hồn và là người sáng lập ra nhóm Crackslatinos, hiện ông đang làm việc tại Core Security dưới vai trò là Exploit Writer. Bộ OllyDbg tôi viết cũng là dựa vào các bài viết mà ông chia sẻ. Tôi ngưỡng mộ ông bởi trong thời gian sinh hoạt ở CLS, Ricardo đã viết và chia sẻ vô số các bài viết về cracking và reverse engineering.

PS2: Nhiều bạn đọc các bài viết của tôi có ý muốn ủng hộ vật chất, thậm chí có bạn hỏi tôi có viết sách không để mua 🙂 . Thế nên, tôi sẽ đính kèm thêm thông tin donate 😀 để những bạn nào có lòng có thể ủng hộ bằng “bỉm sữa” hoặc “quân huy”.

Regards

m4n0w4r


Mới đây, một người bạn nước ngoài thông báo cho tôi về sample có nội dung từ “Bộ Công Thương” gửi tới “Tập đoàn Dầu khí Việt Nam” như sau:

Bạn còn nhắn tôi : “APT có vẻ thích Việt Nam nhỉ!!” …Tôi không biết là mình nên vui hay nên buồn 😀

1. Stage 1 — Phân tích sơ bộ

Kiểm tra sơ bộ thấy sample này có chứa mã VBA, nhắm vào người sử dụng Office 2007+:

Nội dung của VBA code như sau:

VBA code thực hiện nhiệm vụ giải mã chuỗi Base64, sau đó tạo file Chrome.jstại thư mục C:\Windows\Tasks và lưu nội dung đã giải mã vào file này. Kiểm tra tiếp thông tin metadata liên quan:

Như vậy, VBA code sẽ tạo schedule task để thực thi file Chrome.js.

2. Stage 2 — Giải mã chuỗi Base64

Có thể dùng VBA Editor của Word để debug và có được nội dung của chuỗi Base64. Tuy nhiên, khi đọc code thấy chuỗi Base64 này sẽ được giải mã 3 lần, thông qua CyberChef tôi có thể giải mã được mà không cần phải debug:

Toàn bộ nội dung sẽ lưu vào Chrome.js sau khi giải mã:

3. Stage 3 — Giải mã JS

Chrome.js sẽ giải mã chuỗi base64 được lưu tại biến “c”, sau đó sẽ thực thi kết quả sau giải mã. Ta có nội dung giải mã của chuỗi Base64 trên như hình:

Ta có được một powershell script với nội dung như sau:

4. Stage 4 — Phân tích Powershell

Tại thời điểm phân tích tôi vẫn kết nối được tới C2:

Powershell sẽ kết nối tới C2 tại hxxp://154[.]16[.]37[.]122/GoogleUpdate/Update.php để đọc nội dung command từ Update.php vào biến $Cmd.

Nội dung của Update.php như sau:

Sử dụng wmic để lấy uuid và encode uuid này thành một chuỗi Base64:

Cuối cùng cấu thành một Uri có chứa chuỗi uuid đã encode và kết nối tới uri này:

Tới đây, tôi không thấy có thêm thông tin gì khác, cảm giác như kẻ tấn công chỉ muốn thăm dò xem người dùng có mở tài liệu này hay không để chuẩn bị cho một đợt tấn công khác!

5. IoCs

– Doc file: b22d7b196ca03b43f9b140732a3d317f328e5d5f53379c2520a0f05a17d6e617

– C2: hxxp://154[.]16[.]37[.]122

– Scheduled tasks: schtasks /create /sc MINUTE /tn “Chrome” /tr “C:\Windows\Tasks\Chrome.js” /mo 2 /F & schtasks /create /sc MINUTE /tn “Chrome” /tr “C:\Windows\Tasks\Chrome.js” /mo 2 /RU SYSTEM


Nhờ người em hỗ trợ, tôi có được một sample mới c580d77722d85238ed76689a17b0205b4d980c010bef9616b8611ffba21b142e sử dụng CVE-2017–11882. Sample này có thay đổi chút về OLE object, init_key để decrypt binary, cũng như các dropped binary so với mẫu tôi đã viết tại đây https://kienmanowar.wordpress.com/2018/11/08/la-1937cn-hay-oceanlotus-hay-lazarus/

1. Stage 1 — Phân tích sơ bộ

Kiểm tra thấy đây là một file RTF:

Sử dụng rtfobj để xem có các embedded objects không, thấy có 4 objects:

Thông qua Profiler, có được thông tin sau:

CVE-2017–11882 Signature
Embedded file

Mở file bằng ứng dụng Word không thấy có nội dung gì (theo đánh giá cá nhân, tụi này làm mẫu không bằng các mẫu nhắm vào VN, không viết nổi một nội dung cho tử tế 😀). Nó sẽ drop file e.m vào thư mục Temp . File này sẽ có nội dung như trên hình:

2. Stage 2 — Lấy binary được giải mã

Với sample này, tôi không áp dụng được dụng tính năng Image File Execution Options (IFEO) nên tôi dùng HxD để patch Entry Point của EQNEDT32.exethành 0xEB 0xEF.

Sau đó mở file bằng ứng dụng Word, dùng OllyDBG tiến hành attach tiến trình EQNEDT32.exe. Sau khi attach xong khôi phục lại các bytes gốc đã patch bằng HxD. Đặt một bp tại CreatFileW:

Thông qua shellcode gọi hàm VirtualAlloc để cấp phát vùng nhớ phục vụ cho việc lưu nội dung của file e.m:

Tiếp theo gọi hàm ReadFile để đọc nội dung từ e.m và lưu vào vùng nhớ ở trên:

Dùng vòng lặp xor để giải mã dữ liệu tại vùng nhớ trên (thuật toán tương tự như bài viết https://kienmanowar.wordpress.com/2018/11/08/la-1937cn-hay-oceanlotus-hay-lazarus/ , chỉ khác init_key):

Sau vòng lặp trên có được một PE file mới như sau:

Dump vùng nhớ này ra đĩa để phân tích:

3. Stage 3 — Phân tích binary đã dump

Load binary ở trên vào IDA, nó thực hiện tạo thư mục có tên IISWebClient tại %appdata%:

Thực hiện giải mã một buffer:

Sau đó copy toàn bộ các bytes đã giải mã ở trên vào vùng nhớ đã được cấp phát:

Tạo một key là “Direct3D” tại HKEY_CURRENT_USER\Software & lưu toàn bộ decrypted bytes:

Tiếp theo, drop 3 files vào thư mục IISWebClient đã tạo ở trên:

· iassvcs.exe (signed by Symantec).

· sqlite3.dll (signed by Qihoo 360).

· RasTls.dll (signed by Avira — not valid cert).

Thông tin Digital Signatures của các files:

Tạo persistence key để tự động chạy tại “Software\Microsoft\windows NT\CurrentVersion\windows”:

Sau khi tạo key trong Registry xong, thực thi file iassvcs.exe, file này sẽ load các đã drop cùng thư mục:

Binary cuối cùng được lưu thành file 189AFE4.TMP:

Tiến trình iassvcs.exe sau khi thực thi sẽ kết nối tới C2 tại:

4. IOCs

· Malicious RTF:c580d77722d85238ed76689a17b0205b4d980c010bef9616b8611ffba21b142e

· Decrypted binary: 8D7425AE30FD2D5196EC4DCD2540B31A0D26772F

· Dropped binary:

o %appdata%\IISWebClient\iassvcs.exe: 62944E26B36B1DCACE429AE26BA66164

o %appdata%\IISWebClient\sqlite3.dll: FEE0B982AF421FF8C16C0187B376B086

o %appdata%\IISWebClient\RasTls.dll: C6A73E29C770065B4911EF46285D6557

· C2:

o Name: skylineqaz[.]crabdance[.]com

o Name: xn — ylineqaz-y25ja[.]crabdance[.]com

· Registry:

o “HKCU\Software\Microsoft\windows NT\CurrentVersion\windows”; Value name “Load”; Data: C:\Users\{username}\AppData\Roaming\IISWEB~1\iassvcs.exe

o “HKEY_CURRENT_USER\Software\Direct3D”


Hôm nay, tình cờ lướt twitter của @DissectMalware, tôi có đọc được một mẹo nhỏ hỗ trợ để lấy được toàn bộ malicious script.

Link sample: hxxps://www.hybrid-analysis.com/sample/4991e2bf6c1384c9077366f9bebf159001e5ba922e9b615cf8a331c69efab586?environmentId=120

Thông qua HxD biết được đây là OLE Compound File:

Dùng olevba (https://github.com/decalage2/oletools) có được thông tin về VBA code:

Thường thì các bạn hay gặp các samples sử dụng Document_Open() hay AutoOpen() để tự động thực thi VBA khi người dùng mở tài liệu (chính xác hơn là sau khi nhấn nút Enable Content). Còn ở sample này, nó sử dụng AutoClose() để khi người dùng đóng tài liệu thì VBA code sẽ được thực thi.

Khi parse sample bằng olevba, công cụ có highlight đoạn code liên quan đến thực thi Shell như trên hình. Tiến hành debug VBA code và đặt breakpoint tại chỗ Shell này ta sẽ thấy mal_doc gọi cmd.exe để thực thi một đoạn powershell script:

Như các bạn đã biết, khi debug thì hạn chế của tính năng Watch trong trình debugger sẽ không hỗ trợ ta xem và lấy được toàn bộ nội dung của một chuỗi dài như trên hình.

Còn nếu dùng Process Hacker thì phải thật “nhanh tay” mới lấy được toàn bộ nội dung của Powershell như sau:

Tuy nhiên, có một cách khá hay được @DissectMalware chia sẻ nhằm giúp ta lấy được nội dung của toàn bộ script, đó là sử dụng ActiveDocument.Content.InsertAfter để ghi toàn bộ đoạn text ra chính tài liệu. Bổ sung đoạn code này vào trước lệnh Shell như sau:

Sau khi trace qua ta sẽ có được toàn bộ nội dung của script được ghi ra tài liệu như sau:

Decode chuỗi Base64 trên ta có được nội dung của script thực hiện nhiệm vụ download một PE file từ C2 về máy nạn nhân để thực thi:

….

Cập nhật:

Ngoài cách trên, có thể áp dụng cách của anh TQN là sử dụng lệnh Debug.Print. Bổ sung đoạn code sau vào trước lệnh Shell:

Thực hiện Debug, trace qua lệnh và quan sát cửa sổ Immediate, ta sẽ có được nội dụng của toàn bộ script:

End.

m4n0w4r


1. Tìm VBA code

Hôm rồi, một người bạn bên ARTeam có gửi cho tôi một sample, có vẻ lại nhắm vào ai đó tại VN:

Sample này có dạng MIME :

Mở bằng Notepad++ và tìm dòng “Content-Location”, tôi có được thông tin sau:

Thực hiện decode toàn bộ nội dung base64 ở trên có được thông tin về OLE file:

OLE file

Parse ole file có được thông tin về VBA code:

VBA code

2. Nhiệm vụ của VBA code

Khi tài liệu được mở, lấy đường dẫn tới thư mục temp (%temp%):

Tạo file msohtml.log trong thư mục temp, tiến hành decode base64 data:

Nội dung sau khi decode được lưu vào file msohtml.log:

Như trên hình, đây là một mảng các giá trị decimal, qua một vòng lặp thực hiện xor với key là 372 để decode và thực thi lệnh. Tôi sẽ decode sau!

Quay trở lại với VBA code, nó thực hiện copy wscript.exe thành msohtml.exe vào thư mục temp (cùng chỗ với msohtml.log):

Tạo một Schedule Task với tên là WindowsMediaUpdates và Action là explorer.exe shell:::{giá trị CLSID đã tạo ở trên}.

Tạo Registry key với CLSID đã tạo ở trên, ví dụ: “HKCU\Software\Classes\CLSID\{93A61ECC-2527–498C-B94A-5CAA00284A5A}\Shell\Manage\Command\”.

Tóm lại, VBA code sẽ thực hiện drop một script lưu tại file msohtml.log, nhân bản wscript.exe thành msohtml.exe. Xậy dựng command trong Registry với CLSID ngẫu nhiên và tạo một schedule task với tên là WindowsMediaUpdates để thực hiện command.

3. Nhiệm vụ của msohtml.log

Thực hiện decode nội dung của msohtml.log bằng Reneo (www.kahusecurity.com/tools.html) sẽ có được nội dụng của script:

Trong script decode được ở trên lại có một đoạn code như sau:

Tiếp tục decode sẽ có được kết quả:

Như vậy, script khi thực hiện sẽ móc lên C2 để download một file có đuôi .gif: hxxps://beta[.]officopedia[.]com/dr/msg[.]gif. Tính tới thời điểm phân tích thì file này không còn tồn tại.

4. IOCs

File (ITW: Scan_Mau_Ao_Thun.doc): 5c41652ee19351f344243d2f10bc79b024db1183598df8e8474e0f4629f0a49a

Other script: msohtml.log

940FBAC34F7F2DB40617F3E0F68DC395CB9D8E71DE20788E1524FD35838AB5CF

Task Scheduler:

Name: WindowsMediaUpdates

Action: explorer.exe shell:::{random_CLSID}

Registry key:

“HKCU\Software\Classes\CLSID\{random_CLSID}\Shell\Manage\Command\”

C2:

hxxps://beta[.]officopedia[.]com


Vô tình bắt gặp trên twitter của @blu3_team (Tôi không rõ sao acc này lại rất hay có được những mẫu target vào VN), tôi tò mò muốn biết kĩ thuật đằng sau nó là gì bởi tôi thấy nó tương tự như một bài mà tôi đã đọc https://medium.com/@Sebdraven/malicious-document-targets-vietnamese-officials-acb3b9d8b80a, và vì xem comment, người nghi ngờ là OceanLotus, người khẳng định là 1937CN Team

Xin lỗi vì bài viết khá dài, tôi cũng không biết làm thế nào để cho nó ngắn hơn :D, nếu bạn không có thời gian để đọc hết thì bấm một like rồi chuyển trang khác. Phần tôi, một là do tôi thích viết, mặt khác cũng là cách tôi tự rèn kĩ năng … phần nữa là vì tôi biết rằng chỉ khi mình thực sự bắt tay vào phân tích mới thấy nó khác xa với những gì mình đọc bằng mắt và tưởng tượng….



1. Môi trường thực hiện

1. Máy ảo REMnux (https://remnux.org/): sử dụng để phân tích files, giả lập Internet services và capture network traffic.

2. Máy ảo Win10x64 (tự build): sử dụng cho Static & Dynamic Analysis

a. Cài đặt sẵn các cộng cụ debugger & disassembler: OllyDbg, x64dbg, IDA …

b. Cài đặt sẵn Office 2013.

c. Enable tải khoản Administrator (mặc định tài khoản này bị disable) và đăng nhập bằng tải khoản này để thực hiện phân tích.

2. Phân tích theo hành vi

Khi mở tài liệu trên máy ảo Win10, sẽ thấy ứng dụng EQNEDT32.exe được gọi, sau đó xuất hiện thêm hai tiến trình khác là QcConsol.exedllhst3g.exe:


Trên máy ảo REMnux chạy Wireshark để capture traffic từ máy ảo Win10, thu được kết quả kết nối tới C2 server là login[dot]dangquanwatch[dot]com:

Log của Noriben (https://github.com/Rurik/Noriben) cung cấp:

3. Phân tích sample trên REMnux

Sample nhận được là một file có định dạng RTF:

Sử dụng rtfobj (https://github.com/decalage2/oletools), biết được sample này có 3 objects:


Object tại id 0 có FileName là 8.t, khi mở tài liệu thì file này sẽ được drop vào thư mục Temp trên máy. Hai object còn lại được nhận diện là “Not a well formed ole object”.

Dùng luôn rtfobj để dump toàn bộ các objects này:

Kiểm tra thông tin từng file. Đầu tiên là b45087ad4f7d84758046e9d6eb174530fee98b069105a78f124cbde1ecfb0415_8.t:

Theo data như trên hình thì khả năng file này đã bị mã hóa và sẽ được giải mã sau khi drop vào thự mục Temp.

Với file b45087ad4f7d84758046e9d6eb174530fee98b069105a78f124cbde1ecfb0415_object_000C11FB.raw:

Căn cứ vào dấu hiệu như trên hình thì khả năng file RTF có thể sẽ sử dụng exploit CVE-2017–11882 (https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2017-11882).

Kiểm tra file còn lại là b45087ad4f7d84758046e9d6eb174530fee98b069105a78f124cbde1ecfb0415_object_000C11E9.raw:

File này khả năng sẽ chứa đoạn shellcode để thực hiện sau khi máy nạn nhân bị exploit. Thông tin sợ bộ là như vậy, tiếp theo ta sẽ thực hiện debug sample này để xem file 8.t được sử dụng như thế nào.

4. Debug maldoc trên Windows10

Liên quan tới exploit CVE-2017–11882, khi chạy sample, Winword.exe sẽ gọi tiến trình EQNEDT32.exe để handle OLE object. Tuy nhiên, Winword.exe không phải là process cha của EQNEDT32.exe, tiến trình EQNEDT32.exe được gọi bởi Winword.exe thông qua việc sử dụng COM Object như hình dưới đây:

Như vậy, bằng cách nào đó ta phải attach được EQNEDT32.exe vào debugger để debug. Ở đây, tôi sử dụng một kĩ thuật của M$ là Image File Execution Options (IFEO: https://blogs.msdn.microsoft.com/mithuns/2010/03/24/image-file-execution-options-ifeo/).

Vào Registry, tạo một key như sau hoặc nếu cài Word2013 trở lên thì khả năng có sẵn key này (vì tôi thấy trên máy tôi có sẵn):

Tiếp theo, tạo một string value để khởi chạy debugger khi EQNEDT32.exe được thực thi, qua đó sẽ attach đươc debugger vào tiến trình của EQNEDT32.exe.

Với thiết lập như trên, kiểm tra lại bằng Autoruns sẽ như sau:

Lưu ý: khi thiết lập IFEO, các thiết lập sẽ tự động bộ giữa hai key: HKLM\Software\Wow6432Node\Microsoft\Windows NT\CurrentVersion\Image File Execution OptionsHKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options

Tiếp theo, mở WINWORD.EXE, sau đó từ Winword mở tài liệu malicious rtf. Lúc này, tiến trình EQNEDT32.exe cũng sẽ được khởi chạy và được attach vào debugger:

Tại debugger, ta đang dừng lại tại EP(Entry Point) của EQNEDT32.exe:

Kiểm tra ta thấy file 8.t đã được drop vào thư mục Temp:

Đặt BP tại API CreatFileW, sau đó nhấn F9 để thực thi, ta thấy chương trình sẽ thực hiện mở file 8.t để đọc nội dung:

Trace qua hàm này và return, sẽ tới shellcode của exploit:

Gọi hàm GetFileSize để lấy kích thước của 8.t:

Sau đó, gọi hàm VirtualAlloc để thực hiện cấp phát một vùng nhớ:

Vùng nhớ được cấp phát trỏ bởi thanh ghi EAX, follow theo vùng nhớ này để xem code sẽ tác động gì lên nó:

Hàm ReadFile được gọi để đọc ra nội dung của 8.t:

Toàn bộ nội dung của 8.t được đọc vào vùng nhớ đã được cấp phát ở trên:

Tiếp tục trace sẽ tới đoạn shellcode thực hiện giải mã toàn bộ nội dung của file 8.t trong memory tại 0x4F70000:

Sau vài lần trace code sẽ thấy được dấu hiệu MZ, nhưng vậy file 8.t sau khi giải mà sẽ là một PE file:

Cho thực hiện xong toàn bộ vòng lặp giải mã trên sẽ có được một PE hoàn chỉnh trong bộ nhớ:

Dump PE mới này và lưu lại để thực hiện phân tích sau:

File dump được là một exe file:

Tiếp tục debug, shellcode gọi tiếp hàm VirtualAlloc để cấp phát một vùng nhớ khác tại địa chỉ 0x5170000:

PE được giải mã tại vùng nhớ 0x4F70000 sẽ được copy vào vùng nhớ mới được cấp phát ở trên:

Dump vùng mem trên ra bộ nhớ, và vì file này đã được mapping trên memory và có thay đổi, nên sử dụng cộng cụ pe_unmapper của hasherezade (https://github.com/hasherezade/pe_recovery_tools/tree/master/pe_unmapper) để chuyển đổi từ virtual format về định dạng raw:

Debug tiếp, shellcode gọi hàm GetModuleFileNameA được gọi để lấy đường dẫn của EQNEDT32.exe:

Sử dụng CreateProcessA (CreateProcessInternalA) để tạo một process EQNEDT32.exe khác ở trạng thái Suspended. Do đang thiết lập tính năng IFEO nên ta sẽ thấy process của debugger thay vì là process EQNEDT32.exe:

Note: Nếu thực hiện lại, tới bước này thì sử dụng Autoruns để bỏ việc sử dụng IFEO và cho thực hiện hàm CreateProcessA, ta sẽ có được kết quả đúng như hình:

Đoạn code tiếp theo sẽ lấy thread context bằng GetThreadContext, đọc dữ liệu từ vùng nhớ với hàm ReadProcessMemory, gọi VirtualProtectEx (PAGE_EXECUTE_READWRITE 0x40) để thay đổi trang thái của vùng nhớ trên Suspend process, và cuối cùng shellcode ghi đè lên bằng PE tại địa chỉ 0x5170000:

Thực hiện đặt lại thread context bằng SetThreadContext, cuối cùng shellcode thực hiện hàm ResumeThread để launch PE mới:

Tổng kết lại, toàn bộ quá trình thực hiện của shellcode là giải mã file 8.t thành một PE mới, sau đó thực hiện nhân bản sang một vùng nhớ khác, thực hiện tạo một fork process mới là EQNEDT32.exe, cuối cùng áp dụng kĩ thuật runPE để launch EQNEDT32.exe mới đã bị ghi đè code bởi nội dung của file 8.t.

5. Phân tích binary đã dump

Như đã biết khi phân tích dynamic, sau khi resume thread thì malware sẽ drop ra disk các file sau: QcConsol.exe; QcLite.dll; stdole.tlb vào thư mục %AppData%\Microsoft\Windows\Printer Shortcuts.

Ở trên tôi có 2 file đã dump là _04F70000.memdrop_bin.exe (được unmap bằng công cụ pe_unmapper). Tuy nhiên, chỉ có file _04F70000.mem là thực thi được bình thường, còn file drop_bin.exe thì bị crash (mặc dù lúc fix, kiểm tra bằng PE bear thấy mọi thứ đều ok. Tôi có chat hỏi về vấn đề này thì nhận được trả lời của hasherezade như sau: “dumped samples may not always work, so it is normal”).

Mở IDA và load file _04F70000.exe (đổi tên lại từ file .mem), dừng lại tại WinMain():

Binary lấy đường dẫn tới thư mục %AppData%\Microsoft\Windows\Printer Shortcuts:

Cấu thành đường dẫn của các file:

Tới đoạn code thực hiện call sub_331860 3 lần để thực hiện drop các file trên vào thư mục chỉ định. Tôi đổi tên sub này thành thành drop_file như hình:

Đi sâu vào hàm này sẽ gặp vòng lặp xor thực hiện decode bytes, sau đó là đoạn code thực hiện WriteFile vào thư mục:

Kết quả sau khi thực hiện hàm drop_file đầu tiên, có được file QcConsol.exe:

Đây là một file hợp lệ, có chữ kí và được phát triển bởi hãng McAfee, Inc.:

Lời gọi hàm drop_file thứ 2 sẽ drop ra file QcLite.dll, file này không có thông tin gì về Signature cũng như info, như vậy malicious code sẽ nằm ở file này:

Lời gọi hàm drop_file thứ 3 sẽ drop ra file stdole.tlb. Thông tin về .tlb có thể xem tại đây (https://docs.microsoft.com/en-us/windows/desktop/midl/com-dcom-and-type-libraries):

Tiếp tục, cấu thành một command như sau:

Cuối cùng, gọi hàm WinExec để thực thi QcConsol.exe với tham số là “-LowIntegrityServer”:

Như vậy, với việc thực thi thành công, QcConsol.exe chắc chắn sẽ phải load QcLite.dll vào để thưc thi malicious code.

6. DLL hijacking — Phân tích file QcConsol.exe

Load file vào IDA nhận được thông báo:

Để nạp được QcLite.dll, QcConsol.exe sử dụng API LoadLibraryW và sau đó gọi GetProcAddress để lấy địa chỉ của hàm. Về bản chất khi thực hiện nạp module thì đồng thời code của dll cũng sẽ được thực hiện bắt đầu từ DllMain:

7. Phân tích sơ bộ file QcLite.dll

Gọi hàm VirtualAlloc để cấp phát một vùng nhớ:

Lấy đường dẫn đầy đủ tới QcLite.dll:

Dll này sẽ load file .tlb:

Gọi hàm CreatFileW để mở file này (lpFileName trỏ tới stdole.tlb):

Lấy kích thước của stdole.tlb:

Đọc dữ liệu từ stdole.tlb và lưu vào vùng nhớ đã cấp phát ở trên:

Thực hiện vòng lặp sử dụng xor để decode toàn bộ dữ liệu của stdole.tlb đã được copy lên memory:

Kết quả có được sau khi decode, nghi ngờ khả năng đây có thể sẽ là một PE file khác:

Qua rất nhiều rop_chain (tôi đoán thế :D) thì sẽ nhảy tới vùng nhớ trên để thực thi code (Cách nhanh nhất thì các bạn có thể đặt một HWBP on Execute tại 4 bytes đầu 0x50 0x68 0xA7 0x45; sau đó nhấn F9 là tới):

Shellcode tại 0x01A10000 sẽ truy cập PEB (Process Environment Block) để lấy ra địa chỉ base address của kernel32.dll:

Sau khi có được base address của kernel32.dll, shellcode sẽ tìm địa chỉ của hàm API GetProcAddress:

Với hàm API GetProcAddress, shellcode sẽ lấy địa chỉ của các hàm API khác là LoadLibraryA, VirtualAlloc, FreeLibraryA, Sleep:

Sử dụng hàm VirtualAlloc để cấp phát một vùng nhớ và gọi hàm decode_data() để decode bytes trong shellcode vào vùng nhớ cấp phát:

Tiếp tục sử dụng VirtualAlloc để cấp phát thêm một vùng nhớ khác với kích thước lấy từ vùng nhớ trên (dwSize = SizeOfImage = PE_header + 0x50) và thiết lập vùng nhớ mới này là PAGE_EXECUTE_READWRITE:

Sau khi lấy được section header tại vùng nhớ 0x01880000 ở trên, thực hiện vòng lặp để copy toàn bộ các section data sang vùng nhớ mới được cấp phát:

Tiến hành resolve toàn bộ địa chỉ API ghi lại vào IAT của vùng nhớ mới:

Sau khi lấy địa chỉ của toàn bộ các API cần thiết, sử dụng lệnh call để nhảy tới vùng nhớ để thực hiện lệnh:

Tiếp tục debug xuyên qua nhiều lớp call sẽ tới đoạn gọi hàm CreateThread để tạo một thread mới:

Đi tới ThreadFunction tại địa chỉ 0x01EE35D0. Code tại đây thực hiện lấy thông tin binary có sẵn của Windows là dllhst3g.exe:

Xem tổng quan code thì thấy có đoạn code liên quan đến C2 (login[dot]dangquanwatch[dot]com):

Tạo một thread khác làm nhiệm vụ tạo Persistent trong Registry HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run:

Gọi hàm WritePrivateProfileStringW để ghi string vào file tại “C:\ProgramData\desktop.ini”:

Thiết lập thuộc tính cho file với hàm SetFileAttributesW:

Tạo một Mutex {986AFDE7-F299–4A7D-BBF4-CA756FC01F1B65027208}, tuy nhiên handle tới mutext này sẽ bị đóng ngay sau đó:

Tiếp tục sử dụng bộ API CreateFileW, GetFileSize, VirtualAlloc, ReadFile để một lần nữa đọc ra nội dung được lưu trong file %AppData%\Microsoft\Windows\Printer Shortcuts\stdole.tlb và thực hiện decode dữ liệu giống như đã nói ở bước trước:

Thực hiện kĩ thuật inject code bằng cách gọi hàm CreateProcessW để khởi động tiến trình dllhst3g.exe ở trạng thái Suspended:

Cấp phát vùng nhớ trong tiến trình này thông qua hàm VirtualAllocEx:

Gọi hàm WriteProcessMemory để ghi dữ liệu từ 0x00F90000 (buffer chứa data đã decode của stdole.tlb) vào vùng nhớ đã cấp phát tại tiến trình dllhst3g.exe, đặt lại thread context và resume thread. Lúc này dllhst3g.exe sẽ thực thi bình thường và thực thi luôn malicious code:

8. Debug dllhst3g.exe

Hoàn thành xong việc inject code vào dllhst3g.exe sẽ gọi ExitProcess để kết thúc tiến trình QcConsol.exe và tiếp tục thực thi tiến trình dllhst3g.exe. Do dllhst3g.exe bị inject code của file stdole.tlb sau khi decode trên bộ nhớ, nên cách thức hoạt động cũng tương tự. Để có thể debug xem dllhst3g.exe sẽ làm gì thì trước khi thực hiện bước WriteProcessMemory ở trên, sửa 2 bytes đầu là 0x50 0x68 thành 0xEB 0xFE. Sau khi resume thread, mở một debugger khác để attach và khôi phục lại 2 bytes đã bị sửa.

Lúc này, debug sẽ thấy code tạo một mutext và đọc lại nội dung từ file “C:\ProgramData\desktop.ini” và decode string trong file này thành:

Gắn thêm tham số: 0206F4E4 00D80B30 UNICODE “”C:\Users\REM\AppData\Roaming\Microsoft\Windows\Printer Shortcuts\QcConsol.exe” –LowIntegrityServer và gọi hàm WinExec để thực thi

Tiến trình mới này sẽ kết nối tới C2 (Ở đây tôi đang lái traffic về REMnux):

Tại máy REMnux, sử dụng wireshark sẽ capture được thông tin như hình:

9. IOCs

Domain: login[dot]dangquanwatch[dot]com / IP: 185.77.129.142

RTF: b45087ad4f7d84758046e9d6eb174530fee98b069105a78f124cbde1ecfb0415

8.t: 6328dd14eda2ef983810c0c7b3af47298b5998e4fa52d97b204be2818f08bb69

Binary:

QcConsol.exe: 9f3114e48dd0245467fd184bb9655a5208fa7d13e2fe06514d1f3d61ce8b8770

QcLite.dll: 5b652205b1c248e5d5fc0eb5f53c5754df829ed2479687d4f14c2e08fbf87e76

Others:

stdole.tlb: ba620bad026f25ba6decc4bdcefc6415b563503cf9eaddc4e1137a5871d5cee2

desktop.ini: 31c2be9ca29fb2bd8096720c221ee9682f013eee119b02d390d6efc12684392d

Registry:

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run & HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run

ValueName: Windows HD Audio Manager

Data: %AppData%\MICROS~1\Windows\PRINTE~1\QcConsol.exe -LowIntegrityServer

Future ….

Posted: October 13, 2018 in Uncategorized

Vẫn còn đang dang dở…. khi nào vui vui tôi sẽ public 😀

future_tuts

Nhật ký trà đá 0 chấm 4

Posted: September 18, 2018 in Uncategorized
Tags:

Trà đá hacking #7 lần này để lại cho tôi rất nhiều kỉ niệm:
  • Lần đầu tiên tôi lưu trú ở SG gần 1 tuần (chợt nhớ ra là không phải nhưng chót rồi thôi cứ để nguyên 😀 )
  • Lần đầu tiên tôi làm training ở SG, như một sự kiện bên lề của hội thảo.
  • Lần đầu tiên tôi được trải nghiệm trên con exciter huyền thoại của g4mm4
  • Lần đầu tiên tôi ngồi Q5, ăn Q7, nhảy Q1, ngủ Q2… –
  • Lần đầu tiên tôi ăn lẩu cá kèo mà bị bắn pháo hoa tại một khách sạn ở Q3.
  • Lần đầu tiên trà đá tổ chức nguyên ngày.
  • Lần đầu tiên trà đá cũng phải có câu mở đầu Cờ Mờ 4.0
  • Lần đầu tiên tôi tôi kiêm cả zommer lẫn presenter tại trà đá.
  • – ….

Thông qua g4mm4, tôi được biết trà đá hacking #7 có làm Workshop đào tạo trước hội thảo trong thời gian 2 ngày . Lúc g4mm4 ngỏ ý, tôi có trao đổi xem đã ai đăng kí làm trước chưa, nếu có rồi thì ưu tiên người tới trước. “Loằng ngoằng” thế nào, g4mm4 nhắn tôi : “Chốt lại chỉ có mỗi anh đăng kí training”. Tôi đồng ý luôn! Toàn bộ các việc từ PR, giật tít, lập form đăng kí, liên hệ thuê phòng… tôi nhờ g4mm4 làm hết, đơn giản vì tôi biết riêng khoản này không ai qua nổi g4mm4 và thực tế cũng chẳng còn ai khác 🙂 … tóm lại g4mm4 chạy cả hai show: Workshop + Trà đá. Cảm nghĩ cá nhân, nếu trà đá không có người còn nhiết huyết như g4mm4 thì chắc tới số 10 là dẹp …

Những tuần trước ngày dự kiến training là khoảng thời gian mà có lẽ tôi với g4mm4 chat với nhau nhiều nhất :P, cùng lúc đó tôi cũng báo g4mm4 là tôi submit một bài talk ở trà đá, mục đích là để nếu lỡ có “thọt” vụ training tôi còn có tí vớt vát cho quãng thời gian ở SG 😛 . Tôi cũng để nghị g4mm4 cho tôi talk sau anh Quỳnh 😀 … điều này vô tình lại “trở thành hợp lý” khi tôi được vinh dự support người anh trong quá trình present cũng như nhiều bạn diễn giả khác

Những ngày sau đó là thông tin về số lượng người đăng kí học, có thời điểm tôi và g4mm4 đứng giữa hai từ “Chốt” và “Nghỉ“. Đỉnh điểm, tôi nhắn đúng dòng này cho g4mm4: “Okie, có sao dạy thế vậy, chả mấy khi” và nhận được hình này từ g4mm4 (Cảm động !!! )

Địa điểm training được g4mm4 gửi trước để tôi biết, bàn ghế hệt như chỗ mà thằng ku con tôi đang đi học tiếng Anh 😀

Quãng thời gian lưu trú ở SG, tôi thực sự phải cảm ơn người em 0day đã nhiệt tình đưa đón, tạo điều kiện về chỗ ăn ngủ… Từ trung tâm SG phi về Q2- nơi ông em tôi ở hơi xa, nhưng được cái không khí thoáng mát. Ông em tôi công tác SG cũng nhiều năm rồi nên tay lái lụa khỏi bàn :). Từ khu chỗ nhà ông em, tôi lần đầu tiên dòm được tòa nhà Landmark về đêm trông rất đẹp:

Đối diện với khu chung cư là tòa nhà của TCT, cái tòa nhà to vãi cả đái, tôi không rõ trong đó có tổng cộng bao nhiêu nhân viên, nhưng để ngồi được hết ngần này tầng thì chắc là đông lắm:

Logo của tòa nhà này lại gợi cho tôi nhiều kỉ niệm vui buồn khi tôi còn làm ở công ty cũ. Cái thời “thầu” + “bè” nó giống như kiểu “bạn” + “bè”. Bạn thì ít mà bè thì nhiều Thờ