Trong bài viết này, tôi sẽ không đi vào tìm hiểu ngữ nghĩa của data, bởi vì các cấu trúc dữ liệu là quá nhiều như các bạn đã thấy trong ví dụ ở trên.. Nếu như bạn có hứng thú trong việc
Trang 1có thể phán đoán hết được ý nghĩa của chúng, và làm sao chúng ta chọn được một phương án đúng nhất trong đó? Máy tính sẽ dùng cách thức nào để có thể hiểu được? Làm thế nào chúng ta biết được điều gì đang thực sự xảy ra ? Trong bài viết này, tôi sẽ không đi vào tìm hiểu ngữ nghĩa của data, bởi vì các cấu trúc dữ liệu là quá nhiều (như các bạn đã thấy trong ví dụ ở trên) Mỗi một định dạng file sẽ có một cấu trúc dữ liệu Chương trình sử dụng các phần mở dụng của file (.exe, dll v v ) như là một ám hiệu để biết cách cư xử với từng cấu trúc
Thay vào đó, tôi và các bạn sẽ tập trung vào các đoạn code thực thi, đặc biệt là các đoạn code cho x86 processor Chúng ta sẽ bắt đầu từ binary và kết thúc với ngôn ngữ C
II Binary to Hexadecimal
Như tôi đã nói ở trên, mức biểu diễn thấp nhất của thông tin (trong một môi trường máy tính) là binary Các đoạn mã mà máy tính có thể hiểu được được biểu diễn bằng những bit 0 và 1 dài vô tận.Điều này dẫn đến con người khó có thể hiểu được những chuỗi 0 và 1 mà họ nhìn thấy thể hiện cái gì, và điều gì sẽ xảy ra Nếu như bạn có hứng thú trong việc tìm hiểu nguyên lý hoạt động của các mạch trong CPU, tôi gợi ý bạn nên tìm đọc các quyển sách điện tử.Còn đối với tôi, tôi không biết nhiều về chúng để có thể giải thích cho bạn một cách chi tiết về nguyên lý hoạt động (Mặc dù có một thời gian tôi đã từng làm việc với những bộ vi xử lý đơn giản) Nhằm mục đích giảng giải, binary là một định dạng khó hiểu, vì nếu số lượng các số binary là quá lớn sẽ khiến cho chúng ta khó khăn trong việc quan sát
Đó chính là lý do tại sao các bạn thấy rằng thông thường chúng ta không bao giờ chỉnh sửa bất cứ gì theo định dạng binary, mà thay vào đó chúng được chuyển sang một định dạng dễ hiểu hơn mà tôi và các bạn đều biết, đó chính là
Hexadecimal.Nếu như các bạn thấy trong hệ binary chỉ có 2 số 0 và 1, hệ decimal
thì có 10 số (0,1,2,3,4,5,6,7,8,9) còn hệ Hexa sẽ có 16
(0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F) Bạn có thể sẽ rất ngạc nhiên là tại sao định dạng Hexa lại được chọn để biểu diễn mà không phải là hệ Decimal đã quá thân thuộc ngay từ khi lọt lòng Câu trả lời hết sức đơn giản.Đó là bởi vì tất cả các số khi được chuyển đổi vẫn nằm dưới định dang binary.Sử dụng 4bits thì tại một thời điểm bạn
có thể tạo ra chính xác 16 giá trị khác nhau từ 4 bits này, bắt đầu từ 0 cho tới 15, theo hệ Hexa thì là từ 0 cho tới F Điều này khiến cho hệ thống xử lý một cách dễ dàng hơn đơn giản chỉ bằng thay thế 4 bits bằng một số trong hệ Hexa Dưới đây là bảng minh họa quá trình chuyển đổi giữa các hệ, giúp các bạn phần nào hiểu thêm
về những gì mà tôi đã nói :
Trang 2Binary Decimal Hexadecimal
Như các bạn đã thấy trên bảng trên, tại giá trị Binary là 1000 thì giá trị ở hệ Hexa
là (10), đó là vì con số 1 đầu tiên có thể được biểu diễn thành 0001, trong khi số 0 thứ 2 được biêu diễn bằng (0000) Do đó chúng ta có kết quả là 00010000, giá trị này rất phù hợp với hệ số Binary
III Hexadecimal to Assembly Code
Việc sử dụng kí pháp hexadecimal giúp chúng ta viết các đoạn Binary code một cách nhanh hơn, và cho phép chúng ta nhiều cái nhìn tổng quát hơn Tuy nhiên nó vẫn không có ý nghĩa nhiều lắm cho con người bởi vì thực chất nó vẫn chỉ là những con số Chúng ta hãy quan sát một ví dụ về đoạn code Hexa dưới đây :
83EC20535657FF158C40400033DBA39881400053
Như tôi đã nói, đây chỉ là một phương pháp biểu diễn nhanh, ngắn gọn cho các số binary.Điều đó có nghĩa là nó không cho chúng ta thấy bất kì một ý nghĩa gì cả, cũng như nhìn vào đó chúng ta chẳng hiểu nó định làm gì, nhưng cái lợi của nó là ngắn gọn hơn so với việc biểu diễn dưới dạng binary rất dài với toàn số 0 và 1
Trang 3Đoạn Hexa ở trên bao gồm 40 kí tự, trong khi đó nếu chúng ta biểu diễn ở dạng binary chúng ta sẽ có được 160 (40 * 4 bits)
Đoạn mã ở trên không phải là một instruction lớn (thuật ngữ “instruction” ở đây để cập tới đoạn bytes code thực sự) Trên một vài bộ vi xử lý thì mỗi một instruction
sẽ có một kích thước nhất định(ví dụ : 2 bytes) vì vậy chúng ta có thể chia code thành các phần một cách dễ dàng theo kích thước để có được các câu lệnh khác nhau (Giả sử rằng bạn sẽ có được một điểm bắt đầu hợp lệ trong đoạn code) Bộ xử
lý x86 ít phức tạp hơn nhiều và có các kích thước instruction khác nhau Bây giờ bạn có thể ngạc nhiên làm thế nào chúng ta có thể luôn luôn tách được các
instructions theo cách này.Ý tưởng là như sau, chúng ta lấy byte đầu tiên, nhìn vào giá trị của nó, và byte này sẽ cho bạn biết cách tiến hành như thế nào Một vài điều
có thể xảy ra như sau :
_Nó có thể là một single byte instruction: ví dụ 90h là câu lệnh NOP (No
Operation) và kích thước của nó chỉ là 1 byte
_Có thể câu lệnh đó chưa được hoàn chỉnh: ví dụ Các lệnh (Instructions) mà được
bắt đầu bằng 0Fh , chúng ta phải cần thêm các bytes vào sau để nó tạo thành một
câu lệnh có nghĩa
_Câu lệnh được định nghĩa bởi một byte độc lập, nhưng vẫn cần có tham số, ví dụ :
8Bh chuyển một thanh ghi vào trong một thanh ghi khác.Những byte mà theo sau 8Bh sẽ miêu tả nó được chuyển đến từ đâu và nó được chuyển đên đâu
_Câu lệnh chưa hoàn chỉnh và cần thêm các tham số
Bởi vì chúng ta sẽ cần phải biết đó là câu lệnh nào để mà tách ra, chúng ta sẽ kết hợp quá trình tách các câu lệnh khác nhau với việc chuyển chúng sang một định dạng mà con người có thể đọc hiểu được một cách tương đương Ngôn ngữ mà con
người có thể đọc hiểu được đó chính là “Assembly Language”, thường được viết
tắt là ASM Quá trình chúng ta chuyển dịch một chương trình từ Code thô (Raw code) sang ASM, được gọi là quá trình “Disassembling” Việc làm này sẽ cho chúng ta khả năng để đọc hiểu ASM.Để có thể thực hiện được chúng ta cần phải có một số kinh nghiệm
Vì rằng rõ ràng không có hệ thống nào để hiều một đoạn hexadecimal code thực hiện công việc gì , đó là một công việc cực kì chán ngắt Tuy nhiên, việc hiểu được
nó làm việc thế nào là rất quan trọng.Tôi sẽ chứng minh điều này thông qua ví dụ
mà các bạn đã thấy ở trên
Trang 4Hãy quan sát lại đoạn Hexadecimal code :
83EC20535657FF158C40400033DBA39881400053
Chúng ta sẽ giả sử rằng byte đầu tiên chính là điểm bắt đầu hợp lệ và chúng ta sẽ
bắt đầu phân tích từ đó.Đầu tiên tôi sẽ lấy byte này ra, nó là 83h , sau đó chúng ta thực hiện công việc tra cứu dựa trên một bảng và bảng này tôi để trong phần Phụ lục A1 Khi xem trong bảng này chúng ta thấy nó cần phải có thêm byte khác để
mô tả nhiệm vụ của nó một cách đầy đủ nhất, và byte cần sẽ được hình thành từ
một “mod R/M” byte Để có được những gì đầy đủ về nhiệm vụ của câu lệnh chúng ta sử dụng thông tin từ byte này và tra cứu thêm bảng phụ lục thứ 2 (Phụ lục A2) để tìm kiếm thông tin thông qua “group #1”.Trong trường hợp này, byte
đó chính là ECh Một mod R/M byte bao gồm trường 3 bits sau:
Để phân tách các trường này, chúng ta quay trở lại với binary bằng cách biểu diễn
lại ECh :
EC = 1110 1100 = 11 101 100
Sử dụng bảng phụ lục A2, chúng ta sẽ thấy rằng những gì chúng ta biểu diễn ở trên
phù hợp với giá trị xx101xxx, và đây chính là cậu lệnh SUB Hai bit khác sẽ dùng
để miêu tả toán hạng đầu tiên của câu lệnh SUB Chúng ta lại xem tiếp trong một bảng phụ lục thứ 3 (Phụ lục A3), chúng ta tìm thấy 11 có nghĩa là chúng ta sẽ sử dụng trực tiếp một thanh ghi, và giá trị 100 ở trên chính là biểu diễn cho thanh ghi ESP Quay trở lại bảng phụ lục A1 chúng ta thấy rằng cần phải có một toán hạng nữa để điền vào, đó chính là ‘Ib’ (Input byte) Rất dễ dàng để chúng ta thấy rằng byte tiếp theo đó chính là 20h
Ghép tất cả những gì chúng ta vừa phân tích ở trên lại với nhau ,chúng ta sẽ có được một câu lệnh ASM đầu tiên :
83EC20 SUB ESP, 20
Okie như các bạn đã thấy, khá phức tạp phải không nào Chúng ta phải tra đi tra lại mới ra được còn máy tính thì thực hiện quá nhanh Tiếp theo chúng ta sẽ tiếp tục
Trang 5quá trình phân tích với câu lệnh kế tiếp, bắt đầu tại giá trị 53h Tra cứu trên bảng
A1 nó cho chúng ta biết đây là một byte độc lập mà không có tham số :
PUSH rBX (= PUSH EBX)
Vậy cuối cùng chúng ta có được kết quả với 4 bytes đầu tiên được biểu diễn bằng ASM :
83EC20 SUB ESP, 20
53 PUSH EBX
Như các bạn đã thấy việc làm này đã tiêu tốn của chúng ta rất nhiều thời gian phải không Tuy nhiên chúng ta thật may mắn khi có những công cụ đã thực hiện điều này cho chúng ta (ví dụ : HIEW) :
83EC20 sub esp,020
53 push ebx
56 push esi
57 push edi FF158C404000 call d,[0040408C]
33DB xor ebx,ebx A398814000 mov [00408198],eax
53 push ebx
Hoặc một cách khác cũng có thể giúp cho công việc của chúng ta đơn giản hơn đó
là nhờ đến Ollydbg, chúng ta cũng có được đoạn code tương tự :
Tuy nhiên nhiều khi nhìn vào chúng ta không thể hiểu ngay được ý nghĩa của chúng, vi dụ như địa chỉ ở trên tham chiếu đến hàm nào, có trỏ tới một String nào không v v Để giúp cho chúng ta các chương trình Disassemblers như IDA và W32DASM đã hỗ trợ rất nhiều Sử dụng IDA, chúng ta sẽ có được nhiều thông tin hơn :
Trang 6sub esp, 20h push ebx push esi push edi call ds:GetProcessHeap xor ebx, ebx
mov hHeap, eax push ebx ; lpModuleName
Như bạn đã thấy, IDA đã thực hiện thật tuyệt.Nó đã nhận ra được hàm call sẽ gọi tới API nào và nó cũng hiểu được giá trị trả về từ hàm đó, đó là hàm
(GetProcessHeap) và do đó nó sẽ đổi tên biến thành hHeap Đây chỉ là minh họa
nhỏ cho thấy những gì IDA có thể làm được, nhưng cũng đủ để thấy rằng nó cung cấp cho chúng ta rất nhiều thông tin hơn là những gì chúng ta quan sát trong HIEW Điều này thật là tuyệt vời và nó giúp cho chúng ta tiết kiệm được rất nhiều thời gian hơn vào việc làm bằng tay, và bên cạnh đó nó cũng giúp cho chúng ta có một điểm bắt đầu tốt cho quá trình phân tích code về sau