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.
Hã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:
Bit :
7 6 5 4 3 2 1 0
Meaning
:
mod reg R/M
Để 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 J. Tiếp theo chúng ta sẽ tiếp tục quá 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 :
sub 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.
Nhưng nhiệm vụ tiếp theo và rất quan trọng của chúng ta là làm cách nào để biểu đạt
đoạn code dưới dạng ASM đó thành đoạn code dưới dạng cú pháp của một ngôn ngữ bậc
cao (ví dụ như C).
IV. Assembly code to C
Bây giờ giả sử rằng tôi và các bạn có một đoạn code ASM, và chúng ta có thể đọc hiểu
được nó để biết được chương trình đang làm gì.Tuy nhiên, vì hầu hết các câu lệnh ASM
chỉ thực hiện một nhiệm vụ thông thường, do đó rất khó cho chúng ta biết được tổng quát
nhiệm vụ của chương trình đang thực hiện cái gì. Hãy xem một đoạn mã ASM dưới đây :
.004122F0: 55 push ebp
.004122F1: 8BEC mov ebp,esp
.004122F3: 83EC48 sub esp,048 ;"H"
.004122F6: 53 push ebx
.004122F7: 56 push esi
.004122F8: 57 push edi
.004122F9: C745F800000000 mov d,[ebp][-08],000000000 ;"
.00412300: EB09 jmps .00041230B ¯ (1)
.00412302: 8B45F8 mov eax,[ebp][-08]
.00412305: 83C001 add eax,001 ;"J"
.00412308: 8945F8 mov [ebp][-08],eax
.0041230B: 8B4508 mov eax,[ebp][08]
.0041230E: 50 push eax
.0041230F: FF1584A34300 call lstrlenA ;KERNEL32.dll
.00412315: 3945F8 cmp [ebp][-08],eax
.00412318: 7D2E jge .000412348 ¯ (2)
.0041231A: 8B4508 mov eax,[ebp][08]
.0041231D: 0345F8 add eax,[ebp][-08]
.00412320: 8A08 mov cl,[eax]
.00412322: 884DFF mov [ebp][-01],cl
.00412325: 0FB645FF movzx eax,b,[ebp][-01]
.00412329: 83F861 cmp eax,061 ;"a"
.0041232C: 7C18 jl .000412346 ¯ (1)
.0041232E: 0FB645FF movzx eax,b,[ebp][-01]
.00412332: 83F87A cmp eax,07A ;"z"
.00412335: 7F0F jg .000412346 ¯ (2)
.00412337: 0FB645FF movzx eax,b,[ebp][-01]
.0041233B: 83E820 sub eax,020 ;" "
.0041233E: 8B4D08 mov ecx,[ebp][08]
.00412341: 034DF8 add ecx,[ebp][-08]
.00412344: 8801 mov [ecx],al
.00412346: EBBA jmps .000412302 (3)
.00412348: 5F pop edi
.00412349: 5E pop esi
.0041234A: 5B pop ebx
.0041234B: 8BE5 mov esp,ebp
.0041234D: 5D pop ebp
.0041234E: C3 retn
Bạn thấy đấy, trên đây là một đoạn mã ASM sử dụng rất nhiều các câu lệnh đơn giản kết
hợp với nhau và cuối cùng là để thực hiện một nhiệm vụ nào đó mà chính chúng ta cần
phải tìm hiểu. Chúng ta sẽ bắt đầu làm việc từ câu lệnh đầu tiên và cứ như thế cho đến
h
ết, cố gắng để có một cái nhìn tổng quan nhất về những gì sẽ xảy ra bằng việc sử dụng
một “Pseudo-C” notation (kí pháp Giả ngôn ngữ C), và cuối cùng là để chuyển nó về
chính xác ở C code.
Okie có vẻ vẫn hơi mơ hồ, tôi sẽ cùng các bạn giải quyết. Đầu tiên chúng ta sẽ bắt đầu
với những dòng lệnh sau :
.004122F0: 55 push ebp
.004122F1: 8BEC mov ebp,esp
.004122F3: 83EC48 sub esp,048 ;"H"
.004122F6: 53 push ebx
.004122F7: 56 push esi
.004122F8: 57 push edi
Hai dòng lệnh đầu tiên còn được biết đến với một cái tên là “stack frame”.Về bản chất
đây là một ‘local’ stack bên trong của hàm, nơi mà chúng ta tưởng tượng như là một căn
phòng đặc biệt dùng để chứa các biến cục bộ (local variables). Việc tạo ra căn phòng này
có thể được thực hiện rất dễ dàng bằng cách đơn giản là giảm con trỏ stack đi một số bit
nào đó, cụ thể là bao nhiêu bytes c
ần thiết cho việc lưu trữ các biến cục bộ.
Một trong những lợi thế chính của Stack frame chính là ở thanh ghi EBP, nó có thể được
sử dụng như là một con trỏ cố định tới các biến tham chiếu (reference varibales) (Nằm ở
trên thanh ghi EBP là các tham số, ở dưới nó thì là các biến cục bộ) (Đọc thêm các bài
viết của anh Be)
Chú ý rằng các con trỏ Stack như (ESP và EBP) cần phải được phụ
c hồi lại trước rời
khỏi một hàm nào đó, để tránh cho việc lỗi Stack corruption.
.004122F0: 55 push ebp
.004122F1: 8BEC mov ebp,esp
.004122F3: 83EC48 sub esp,048 ;"H"