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" . 8B 4508 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: 8B 4508