Archive for April, 2019


Để 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