Vài dòng lan man:

Mới đây Sẻ đệ (yeuchimse) đóng Blog, làm tôi cũng nghĩ hay thôi, dăm ba cái Blog, view cũng lèo tèo, viết lách vừa tốn thời gian chỉnh sửa, chụp choẹt…. chắc cũng đóng nốt cho nhanh...

Anh em trong Gờ-rúp “kín” thi thoảng lại: “Già rồi, viết lách cái gì. Định kiếm fame đến bao giờ!!“. Thôi thì …..

………..

…………†

Dành cho những bạn nào chưa nghe bản guitar solo này, hãy xem cách Steve Vai “nựng” đàn để tìm cho mình những cảm hứng riêng

Trong phần trước, tôi đã cho các bạn thấy một vài phương pháp để có thể phát hiện và tới được OEP trong một file bị packed. Phần này, chúng ta sẽ tiếp tục với hai bước còn thiếu mà tôi đã đề cập, đó là: dump filerebuild IAT.

Dump file

Các bạn có thể thực hành lại để tới OEP và Reanalyze chương trình, sau khi thực hiện xong ta có mọi thứ sẵn sàng cho việc Dump file. Hành động dump file có thể hiểu một cách “chân phương” là sau khi tới được OEP, toàn bộ code gốc của chương trình cùng các hàm APIs đã được “bung” đầy đủ trên memory, lúc này ta thực hiện thao tác dump để lưu thành một file.

Ở đây, tôi sẽ sử dụng một IDC Script để thực hiện công việc này (không phải là Python script). Nội dung của script như sau:

static main()
{
	auto fp, ea;
	fp = fopen("dumped.bin", "wb");
	for (ea = 0x400000; ea < 0x40b200; ea++)
		fputc(Byte(ea), fp);
}

Trong script trên, chúng ta thực hiện dump từ địa chỉ ImageBase0x400000 tới địa chỉ lớn nhất mà ta thấy trong tab IDA Segments. Đó chính là section cuối cùng của file thực thi, trong trường hợp này là section OVERLAY, tại địa chỉ 0x40b200.

Copy&Paste script trên vào Notepad và lưu lại với tên là dumper.idc. Quay lại IDA và cho thực thi script này thông qua File > Script file  (IDA hỗ trợ cả Python và IDC scripts). Kết quả tôi có được tập tin dumped.bin như hình dưới:

Tôi tạo một bản sao của file đã dump ra và đổi tên nó thành một tập tin có đuôi mở rộng .exe:

Các bạn để ý thì thấy rằng, file sau khi đổi tên không hề có icon mặt cười như ở file gốc. Để fix được thiếu sót này các bạn có thể sử dụng chương trình PE Editor v1.7 (download tại đây: https://mega.nz/#!uGpHkKTD!bkxqBmj2Ib0FC6QDln7Cf1DnXSqRazinTuXNbAdFCjE). Chạy chương trình, load file exe và sau đó chọn sections:

Tại màn hình chứa các thông tin về Sections:

Nhấp chuột phải vào một section bất kì và chọn Dumpfixer, tương tự như hình dưới đây:

Bước dumpfixer này thực chất là thiết lập cho Raw_size = Virtual_sizeRaw_offset = Virtual_offset. Kết quả có được như sau:

OK, như vậy là đã lấy lại được icon của file. Tạm thời ở thời điểm này, file đã dump ra được tốt rồi, ít nhất là icon đã hiển thị đúng, nhưng tuy nhiên file này vẫn chưa thể thực thi được bình thường bởi vì cần phải sửa cả IAT nữa.

Rebuild IAT

IAT là gì?

IAT viết đầy đủ là Import Address Table, là một bảng nằm trong file thực thi và được sử dụng khi chương trình hoạt động. Bảng này lưu tất cả địa chỉ của các hàm được import, từ đó chương trình có thể sử dụng để chạy trên bất kỳ máy nào (chính xác là trên bất kỳ môi trường OS nào).

Nếu IAT là đầy đủ và chính xác thì khi ta đưa tập tin thực thi cho một người khác, IAT sẽ được điền đầy đủ các giá trị tương ứng (địa chỉ các hàm APIs) trên máy đó, bất kể hệ điều hành hoạt động trên máy đó là gì, tính tương thích sẽ được duy trì và chương trình sẽ thực thi được một cách bình thường.

Nghĩa là, IAT sẽ luôn nằm ở một vị trí nhất định trong mỗi tệp thực thi, và sẽ có các vị trí cố định cho mỗi hàm để điền vào. Các bạn nhớ lại phần trước tôi có so sánh (chưa giải thích kĩ) sự khác biệt giữa hình ảnh từ tệp tin bị packed khi ta tới được OEP (trước khi thực hiện việc dump) với file gốc ban đầu:

Cả hai đều hiển thị địa chỉ 0x403238 và dường như chúng có cùng nội dung. Bây giờ chúng ta mở lại file gốc:

Quan sát ở bên dưới thấy có địa chỉ file offset (trên ổ đĩa) là 0x1038. Tôi dùng trình Hex Editor là HxD để load file gốc và tìm tới địa chỉ offset này:

Tại HxD, ta thấy nội dung tại offset 0x10380x355e. Nếu tôi cộng giá trị 0x355e vào địa chỉ ImageBase0x400000, tôi có kết quả là 0x40355e. Vậy có thông tin gì lưu tại địa chỉ mà ta vừa tính toán được? Để làm rõ hơn, tôi cho IDA load lại file gốc cùng với tùy chọn Manual Load, mục đích để IDA nạp tất cả các sections của tập tin thực thi:

Sau khi chấp nhận cho tải tất cả các sections, đợi khi IDA phân tích toàn bộ file xong, ta đi đến địa chỉ 0x40355e. Ở bên phải lúc này ta thấy được tên của hàm API là:GetModuleHandleA

Tương tự đối với các ví trí khác mà tôi highlight làm ví dụ như hình dưới:

Như vậy, từng giá trị trên sẽ được cộng với địa chỉ ImageBase để từ đó tìm ra tên của các hàm tương ứng. Và cũng từ các tên hàm đó sẽ lấy được các địa chỉ tương ứng của từng hàm trong máy lúc runtime. Ví dụ, như trong trường hợp này nó sẽ tìm địa chỉ của hàm GetModuleHandleA() trên máy của chúng ta và sửa lại giá trị 5e 35 bằng địa chỉ thật của hàm API (ví dụ: 757116D0  kernel32.GetModuleHandleA)

Đó chính là lý do tại sao một tệp tin thực thi có thể chạy trên bất kỳ máy nào, bởi vì nó sẽ luôn luôn tìm tên của API tương ứng trong mỗi mục của IAT và tìm ra địa chỉ hợp lệ của hàm tương ứng với từng máy khác nhau khi nó thực thi. Và đó cũng là lý do tại sao trên bất kỳ máy nào nếu tôi thực hiện một lời gọi hàm như sau:

CALL [0x403238]

Chương trình sẽ luôn luôn hoạt động bởi vì 0x403238 là mục IAT của hàm GetModuleHandleA(), nội dung thay đổi chính là địa chỉ hàm mà hệ điều hành sẽ lưu lại bằng cách chỉnh sửa lại giá trị ban đầu 5e 35 (trỏ đến chuỗi tên của hàm sau khi cộng thêm ImageBase).

Giữ nguyên hai màn hình IDA đang mở (một cho file bị packed và đang dừng lại ở OEP; còn 1 cho file gốc), ta mở thêm một IDA thứ ba và load file đã dumped mà tôi vừa đổi tên là:dumped.exe.

Chuyển tới địa chỉ của IAT là 0x403238:

Quan sát bên dưới, ta thấy địa chỉ file offset lúc này là 0x3238, địa chỉ này không khớp với file gốc bởi sau khi thực hiện dumpfixer đã thay đổi kích thước trên đĩa (Raw Size) bằng với kích thước ảo (Virtual size), dẫn tới địa chỉ offset cũng thay đổi, cho nên địa chỉ của hàm API GetModuleHandleA() cũng vì thế mà thay đổi theo.

Sử dụng HxD mở file dump để kiểm tra, tìm tới offset 0x3238. Kết quả ta thấy như sau:

Chúng ta thấy rằng giá trị tại đó là địa chỉ của một hàm API, chứ không phải là một offset để cộng với ImageBase nhằm tìm tên của hàm API nữa. Do vậy, ta có thể hiểu rằng khi chương trình chạy, hệ thống sẽ lấy được địa chỉ chính xác của hàm API và lưu địa chỉ này ở đó, vậy nên khi thực hiện dump chương trình thì địa chỉ của hàm GetModuleHandleA() cũng sẽ được lưu kèm theo file đã dump. Để kiểm tra ta có thể tới địa chỉ của hàm này trong IDA như hình dưới:

Vậy vấn đề ở đây là gì?

Khi ta thực thi file dump, hệ thống sẽ tìm kiếm IAT và lấy ra giá trị này, sau đó cộng với địa chỉ ImageBase để từ đó tìm ra tên của hàm, cuối cùng sẽ tìm địa chỉ của hàm … Nhưng vì khi ta thực hiện dump file đã vô tình đã phá vỡ nguyên tắc này, do giá trị được lưu cuối cùng chính là địa chỉ thật của hàm API, và bởi vậy chương trình sẽ bị crash khi khởi chạy vì nó không thể điền vào IAT chính xác.

Để giải quyết được vấn đề này ta cần phải tìm một cách để sửa lại IAT và khôi phục lại tất cả các offset – trỏ vào các chuỗi chứa tên của các hàm API. Công việc này nếu làm bằng tay sẽ rất oải, do số lượng hàm API rất nhiều, vậy nên chúng ta sẽ sử dụng một công cụ hỗ trợ có tên là Scylla:

https://forum.tuts4you.com/files/file/576-scylla-imports-reconstruction/

Chạy ứng dụng này. Tại chỗ Attach to an active process, tôi chọn process của file bị packed:

Nhớ rằng bạn vẫn phải giữ nguyên màn hình của IDA đang dừng lại tại OEP.

Tại Scylla, thay đổi ​OEP​ thành giá trị chính xác là 0x​401000:

Tiếp theo nhấn​ ​IAT​ ​Autosearch, một bảng thông báo xuất hiện:

Điều này có nghĩa là, IAT bắt đầu tại địa chỉ 0x403184 và có kích thước là 0x108. Nhấn OK để chấp nhận, sau đó nhấn Get Imports, ta có kết quả như sau:

Các bạn thấy rằng Scylla đã lấy được tất cả thông tin trừ 1 giá trị khi ta nhấn nhấn Show Invalid. Mặt khác, chúng ta thấy rằng offset 0x3238 trong packed file tương ứng với GetModuleHandleA(), như vậy là Scylla đã làm việc rất tốt:

Tại packed file, đi tới địa chỉ API mà Scylla thông báo là không hợp lệ tại 0x403208 (kết quả có thể khác ở trên máy của các bạn), để xem tại đó có thông tin gì:

IDA cung cấp thông tin là unk_6E33AC50, nghĩa là nó chưa nhận biết được code tại đây. Đi tới địa chỉ này:

Ta tới thư viện apphelp.dll, nhấn C để chuyển đổi các bytes tại đây thành code, tạo hàm và cho Reanalyze lại chương trình:

Kết quả ta có được code thuộc thư viện apphelp.dll sẽ gọi tới hàm API GetDC(). Nhấp đúp chuột tại offset szGetdcWndPDcP, ta tới đây:

Như vậy, hàm API GetDC() thuộc thư viện user32.dll. Do đó, quay trở lại màn hình của Scylla, nhấp đúp chuột invalid API này và chọn như hình bên dưới để sửa lại:

Nhấn OK để sửa và như vậy ta sẽ có được các hàm API hợp lệ. Nếu ta bấm Show Suspect, Scylla sẽ cung cấp các APIs mà nó còn nghi ngờ, để xác minh ta lại quay lại IDA và đi đến các địa chỉ 0x4031BC, 0x40322C0x403278 (kết quả này có thể khác trên máy của các bạn):

Thông tin mà IDA cung cấp như sau:

Kết quả thu được là chính xác hoàn toàn rồi, cuối cùng là nhấn nút Fix Dump:

Scylla sẽ lưu file đã fix với tên mới là dumped_SCY.exe. Nhấp đúp vào để chạy thử ta thấy crackme chạy ok như file gốc:

Chúng ta thấy rằng khi mở file đã fix ở trên trong IDA, ta thấy nó bắt đầu từ OEP 0x401000 và tên của APIs đã được fix hoàn toàn và trông không khác gì với bản gốc:

Như vậy qua bài viết này, tôi đã hướng dẫn các bạn thực hiện xong toàn bộ quá trình unpack một file được pack bằng một packer dễ. Phần tiếp theo ta sẽ xem xét một vài ví dụ khó hơn.

Phần này xin được kết thúc tại đây. Hẹn … à mà thôi! Khi nào có hứng tôi đưa lên tiếp…..

Image result for brain dump 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


Vài dòng lan man:

Tôi đoán có nhiều bạn trông chờ và đặt nhiều kì vọng vào bộ tuts này như kiểu một tài liệu sẽ làm thay đổi cuộc đời :P. Nó sẽ biến bạn trở thành một “chuyên gia hàng đầu”, được nhiều người săn đón 😦

Đứng kì vọng để rồi thất vọng!!!

Không hiểu sao tôi rất dị ứng với từ chuyên gia, mỗi lần bị giới thiệu như vậy trong người tôi nó thất bứt rứt lắm…..

Nhiều bạn cũng có thắc mắc sao lâu tôi chưa ra bài mới. Điều này rất khó nói 😀:
– Nó phụ thuộc vào khung thời gian trống còn lại trong ngày.
– Hơn nữa cũng phụ thuộc nhiều vào người đọc. Đọc giả có tâm, đọc, góp ý/thắc mắc những chỗ tôi viết chưa ổn và hơn hết họ đọc tới đọc đến tận chỗ “bỉm sữa” :). Đọc giả có tâm hơn nữa, họ không cần đọc mà kéo roẹt xuống chỗ có dòng “bỉm sữa” :D. Còn phần đa thì chắc là đọc đến chỗ “ấy” thì dừng lại….

Tuyển tập các bài viết này sẽ thường xuyên thay đổi nội dung và sẽ bao gồm các chủ đề khác nhau liên quan đến reversing như: static reversing, debugging, unpacking và exploiting.

Trong phần này, chúng ta sẽ unpack file PACKED_CRACKME.exe (tải tại đây), được packed bằng phiên bản UPX mới nhất (tính đến thời điểm viết bài). Tuy nhiên, điều này không có nghĩa là tôi sẽ viết nhiều bài khác chỉ nói về chủ đề unpacking, chúng ta sẽ thay đổi và trộn lẫn các chủ đề khác nhau để không bị nhàm chán. Do đó, bên cạnh việc tìm hiểu Unpacking bằng IDA thì đồng thời ta sẽ vẫn nghiên cứu thêm các chủ đề khác.

File bị Packed?

Định nghĩa đơn giản về tập tin packed đó là một tập tin được ẩn mã thực thi gốc của chương trình và lưu lại bằng cách áp dụng các kỹ thuật nén hoặc mã hóa để tránh không bị reverse một cách dễ dàng. Bên cạnh đó nó cũng được chèn thêm một đoạn Stub hay một section, để khi thực thi chương trình, đoạn stub này sẽ nhận code đã được mã hóa, giải mã nó trong bộ nhớ, lưu code giải mã vào trong bất kỳ section nào hoặc là chính nó và cuối cùng nhảy tới vùng code này để thực thi (đó chính là code gốc ban đầu của file).

Có rất nhiều biến thể của các trình packers, và đa phần trong số chúng là các trình protectors, sử dụng các kĩ thuật như hủy bảng IAT hay import table, hủy thông tin về Header của file. Bổ sung thêm các cơ chế anti-debugger để tránh việc unpack và khôi phục lại tập tin ban đầu.

Image result for packers landscape

Thực hành

Ví dụ đơn giản nhất mà các chuyên gia hay sử dụng để minh họa về packer đó chính là UPX. Trình packer này không áp dụng các kĩ thuật anti-debug, hay các tiểu xảo nào khác được đề cập ở trên, tuy nhiên nó lại giúp ta khởi đầu với những kĩ năng đơn giản nhất trong quá trình thực hiện unpacking. Để tiện cho các bạn trong việc thực hành, file PACKED_CRACKME.EXE đã được đính kèm link tải về ở trên.

Load file vào IDA và chọn Manual load vì tùy chọn này sẽ giúp chúng ta có thể nạp tất các sections của file, đồng thời bỏ lựa chọn Create imports segment (Thầy Ricardo khuyến nghị nên bỏ chọn khi làm việc với các packed files). Kết quả sau khi load vào IDA như hình dưới:

Ta đang dừng tại địa chỉ bắt đầu hay còn gọi là Entry Point (EP) của PACKED_CRACKME.exe. Với file bị packed thì địa chỉ EP là 0x409be0, trong khi ở file gốc thì ta sẽ dừng tại địa chỉ EP là 0x401000, như hình minh họa bên dưới:

Ngoài ra, so sánh các sections hay segments của cả hai file, ta thấy rằng dưới header của file sau khi bị packed có thêm một section mới với tên là UPX0, có kích thước trong bộ nhớ lớn hơn so với file gốc ban đầu.

File gốc

File bị PACKED

Theo quan sát trên hình, section UPX0 của file bị packed kết thúc tại địa chỉ 0x409000, trong khi toàn bộ các sections ở file gốc đều nằm trong vùng nhớ bắt đầu từ 0x401000 đến 0x408200. Ở đây, ta đang đề cập đến bộ nhớ ảo, đó là khi một chương trình thực thi, nó có thể chiếm dung lượng 1k trên ổ đĩa (HDD) nhưng sẽ chiếm 20k hoặc hơn thế nữa trong bộ nhớ.

Điều này có thể thấy được khi phân tích trong IDA, ví dụ, tại địa chỉ bắt đầu 0x401000 của file gốc ta thấy các thông tin như sau:

Trong hình trên (Section size in file) chiếm 0x600 byte, trong khi trong bộ nhớ (Virtual size) chiếm 0x1000. Trở về file bị packed, nếu chúng ta chuyển tới địa chỉ 0x401000 – đó là nơi bắt đầu của section UPX0.

Chúng ta thấy rằng Section size in file chiếm 0x0 byte trên đĩa, nhưng trong bộ nhớ nó chiếm 0x8000. Điều này có nghĩa là, nó dành ra một không gian trống phục vụ cho việc khôi phục lại mã chương trình gốc ban đầu tại đây và sau đó nhảy tới đây để thực hiện lệnh. Như vậy, nó dành đủ không gian để thực hiện việc khôi phục code này.

Ta cũng thấy rằng địa chỉ 0x401000 có kèm theo tiền tố dword_ ở phía trước nghĩa là nội dung của nó là một DWORD. Dấu (?) hàm ý nó cần được dành riêng và không chứa bất kỳ giá trị nào, từ khóa dup có nghĩa là dword đó được nhân với 0xc00, như vậy kết quả là sẽ dành ra 0x3000 bytes:

Tiếp theo tại 0x404000, ta thấy có 1400h dup(?). Có nghĩa là cần dành ra vùng nhớ:

Do đó, tổng số cần có 0x8000 bytes sẽ được dành riêng trong bộ nhớ để đặt chương trình vào đó. Tại dword_401000 ta thấy có một tham chiếu trong mã thực thi, ta sẽ xem xét câu lệnh này làm gì sau.

Tiếp theo bên dưới, ta thấy file packed có thêm một section thứ hai là UPX1, section này có kích thước trên đĩa là 0xe00 và trong bộ nhớ 0x1000. Đây có khả năng là nơi chương trình lưu một số kĩ thuật mã hóa đơn giản nhằm để che dấu mã gốc.

Nếu kiểm tra thông tin tham chiếu tại địa chỉ bắt đầu của section là 0x409000, ta có kết quả:

Chúng ta thấy có một tham chiếu bên dưới (Down) trong code thực thi của chương trình. Chuyển tới vùng code đó:

Tại Stub trên, sau địa chỉ Entry Point, chương trình nạp địa chỉ 0x409000 vào thanh ghi ESI (Ta biết lấy địa chỉ là bởi có tiền tố offset ở phía trước). Chuyển qua chế độ Text mode bằng cách nhấn phím space bar, tại đó chúng ta thấy như sau:

Đoạn stub này nằm cùng section UPX1, bên dưới code của chương trình gốc đã packed. Nghĩa là tại section UPX1, trình packer đã thực hiện lưu các bytes đã encrypted của chương trình gốc tại đây và stub code bắt đầu từ 0x409be0.

Ta có thể dễ dàng nhận ra vùng Stub sẽ đọc các bytes từ 0x409000, sau đó áp dụng một số thao tác tính toán và lưu lại kết quả sau tính toán vào 0x401000. Ta thấy thanh ghi EDI = ESI-0x8000:

Nói cách khác, chương trình sẽ sử dụng vùng nhớ trỏ bởi ESI (như là source), từ đó đọc dữ liệu ra và áp dụng các tính toán nhất định, sau đó lưu vào vùng nhớ trỏ bởi EDI (như là dest) để khôi phục lại code ban đầu của chương trình.

Chúng ta đã biết tại 0x401000 có một tham chiếu trong mã thực thi, nếu chúng ta nhấp đúp vào tham chiếu đó:

Ta tới vùng code chứa một lệnh nhảy (jmp) tới 0x401000:

Jmp near là một lệnh nhảy trực tiếp đến địa chỉ, do vậy nó sẽ nhảy thẳng đến 0x401000. Rõ ràng, ở đây sau khi thực thi toàn bộ mã lệnh tại Stub và tái tạo lại mã gốc ban đầu của chương trình, chương trình sẽ nhảy tới OEP tại địa chỉ 0x401000 (Entry Point gốc), không giống như Entry Point của Stub0x00409BE0.

Ta sẽ gọi tắt là OEP hay Original Entry Point để hàm ý rằng đó chính Entry Point (EP) của chương trình gốc ban đầu. Điều này là hiển nhiên vì khi một chương trình bị packed, ta hoàn toàn không biết địa chỉ này ở đâu và chỉ khi ta có file gốc thì ta mới có biết được EP ban đầu là 0x401000 như hình minh họa dưới đây:

Tóm lại, khi chúng ta làm việc với một chương trình bị packed, ta sẽ không biết OEP của nó ở đâu bởi ta không có file gốc ban đầu, do đó chúng ta sẽ phải áp dụng các kĩ thuật để tìm ra OEP. Quay lại với phân tích ở trên, khi Stub hoàn thành tất cả các thủ thuật của nó và khôi phục lại mã gốc, nó sẽ nhảy tới OEP để từ đó thực thi chương trình.

Chính vì thế, ta có thể đặt một Breakpoint tại lệnh Jmp tới OEP để xem code của chương trình gốc có được khôi phục như ta đã suy đoán như trên không. Thử đặt một BP như hình dưới:

Sau đó chọn debugger là Local Win32 Debugger và nhấn Start debugger. Ngay lập tức, ta sẽ dừng tại BP vừa đặt ở trên:

Nhấn F8 để trace qua lệnh này:

IDA sẽ hiển thị một thông báo như trên, cứ nhấn Yes để thông báo cho IDA biết và nhận diện lại section UPX0 ban đầu như là CODE (ban đầu nó được định nghĩa là DATA).

Ta thấy rằng stub đã giải nén code và nhảy tới 401000 để thực thi. So sánh thì thấy code này rất giống với code tại địa chỉ 0x401000 ở file gốc, nhưng tuy nhiên ta lại không thể chuyển sang chế độ đồ họa bởi vì lúc này nó không được định nghĩa như là một function (chỉ là loc_401000).

Để chuyển được sang chế độ đồ họa, có một tùy chọn ẩn ở góc dưới bên trái của màn hình IDA, bằng cách nhấp chuột phải tại đó và chọn Reanalyze program:

Bằng cách này, địa chỉ loc_401000 đã thay đổi thành sub_401000 cho biết bây giờ nó đã được hiểu như là một hàm. Vì vậy, ta có thể chuyển sang chế độ đồ hoạ bằng cách nhấn phím tắt space bar:

Ta thấy, code giờ đây đã đẹp hơn 🙂

Tuy nhiên, quan sát kĩ một chút ta sẽ thấy có sự khác biệt, ở file gốc tại địa chỉ 0x401002 sẽ hiển thị lời gọi tới hàm API CALL GetModuleHandleA, trong khi tại file ta đang phân tích chỉ hiển thị lệnh CALL sub_401056. Đi vào lệnh Call này để xem code của nó là gì:

Ta lại thấy sự khác biệt với bản gốc. Nếu tại file gốc khi ta truy cập vào CALL GetModuleHandleA:

Ta thấy có một lệnh nhảy gián tiếp tới hàm API. Vậy còn tại file đang đang phân tích thì sao? Hàm API đã đi đâu? Thử follow theo lệnh nhảy tại file đang phân tích, ta có được thông tin như sau:

Như trên hình, nội dung tại 0x403028 là một offset (off_), đó là địa chỉ của API GetModuleHandleA và tại file gốc, cũng quan sát địa chỉ này tại section .idata thì ta thấy cũng chứa địa chỉ của cùng một hàm API.

Mặc dù, ta thấy chúng đều nhảy tới cùng một địa chỉ, tuy nhiên có một sự khác biệt rất quan trọng mà chúng ta sẽ tìm hiểu sau. Như vậy, có thể nói tới thời điểm này code gốc của chương trình đã được unpack hoàn toàn.

Có một cách khác nữa để tìm OEP, đó là tìm lệnh được thực thi đầu tiên ở section đầu tiên. Tuy nhiên, phương pháp này không phải lúc nào cũng có hiệu quả :). Để làm theo cách này, ta khởi động lại target trong IDA, cấu hình debugger để dừng lại tại Entrypoint khi thực thi. Sau đó, cho thực thi file packed, ta sẽ dừng lại ở entry point:

Tiếp theo, ta chuyển tới section đầu tiên bắt đầu tại địa chỉ 0x401000.

Tại đó, tôi nhấn F2 để đặt một breakpoint và cấu hình để dừng khi thực thi (Execute) – có nghĩa là chương trình sẽ break chỉ khi thực thi lệnh mà không dừng khi nó đọc hoặc ghi dữ liệu, đó là điều tôi kì vọng. Và vì tôi không biết chính xác lệnh nào sẽ được thực thi đầu tiên nên Breakpoint on execute mà tôi đặt sẽ bao gồm toàn bộ section (0x8000 bytes). Cụ thể như hình minh họa dưới đây:

Sau khi thiết lập breakpoint như trên xong, nhấn OK, lúc này toàn bộ section sẽ được đánh dấu bằng màu đỏ như hình:

Sau đó, nếu có breakpoint nào mà các bạn đã đặt trước đó thì hay tiến hành vô hiệu hóa bằng cách truy cập Debugger->Breakpoint->Breakpoint List.

Nhấn chuột phải tại breakpoint và chọn Disable breakpoint, tương tự như hình:

Sau đó cho thực thi chương trình. Khi chương trình break, ta thấy sẽ dừng lại tại lệnh đầu tiên ở section vừa được tạo, trong trường hợp này, 0x401000 chính là OEP mà tôi tìm được:

Sau khi đã đạt được mục đích, ta vô hiệu hóa breakpoint đã đặt. Sau đó tiến hành phân tích lại toàn bộ chương trình, kết quả có được tương tự như những gì đã làm ở trước:

OK, qua toàn bộ phân tích trên chúng ta đã có được OEP gốc của chương trình thông qua hai cách thực hiện khác nhau. Công việc tiếp theo mà ta cần làm là dump file và rebuild lại toàn bộ IAT để đảm bảo file sau khi unpacked thực thi được một cách bình thường như file gốc ban đầu. Tuy nhiên, công việc đó sẽ làm ở các phần tiếp theo, phần hôm nay là đủ rồi, các bạn có thể tạo một bản Snapshot để có thể quay lại vào phần tới.

Phần này xin được kết thúc tại đây. Hẹn gặp các bạn ở phần 15!

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 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: