Ở phần trước, tôi đã thực hiện unpack file thành công và file sau khi unpack có thể thực thi bình thường. Trong phần này, chúng ta sẽ tiến hành reverse nó để tìm hiểu cách thức hoạt động cũng như xem xét có thể viết keygen đơn giản bằng Python hay không.

Bạn nên nhớ rằng nếu chỉ để phân tích tĩnh chương trình thì ta không cần phải thực hiện đầy đủ các bước unpack như phần trước. Chúng ta chỉ cần tới được OEP và tạo một bản snapshot (Take Memory Snapshot), sau đó chép file idb sang chỗ khác và mở nó. Bằng cách này ta có thể tiếp tục quá trình phân tích tĩnh. Tuy nhiên, việc unpack file hoàn chỉnh sẽ hỗ trợ chúng ta rất nhiều, cho phép ta có thể debug được chương trình dễ dàng hơn.

OK, mở IDA và load file đã unpacked vào. Sau khi IDA phân tích xong, chuyển tới cửa sổ Strings để tìm kiếm các chuỗi:

Chúng ta đã biết được đầu tiên chương trình thực hiện in ra chuỗi yêu cầu người dùng nhập vào một tên bất kỳ:

Vì vậy, tại màn hình Strings ở trên, ta nhấp đúp vào chuỗi đó sẽ chuyển qua màn hình IDA Disassembly:

Tiếp theo nhấn “x” để tìm kiếm các đoạn code sử dụng tới chuỗi này. Kết quả có được:

Ta đi tới địa chỉ trên:

Từ đây, ta sẽ bắt đầu quá trình phân tích tĩnh.

Đầu tiên, các bạn sẽ thấy đoạn code prologue khi bắt đầu của một hàm, thanh ghi EBPbase frame của hàm trước đó sẽ được lưu vào ngăn xếp bằng câu lệnh PUSH EBP, sau đó hàm sử dụng lệnh MOV EBP, ESP để thiết lập cho thanh ghi EBP trở thành base frame cho hàm hiện tại đang phân tích (thanh ghi EBP kể từ đây sẽ được sử dụng để tham chiếu tới các biến cục bộ và tham số truyền vào cho hàm).

Câu lệnh tiếp theo SUB ESP, 94h làm nhiệm vụ dành riêng ra một khoảng không gian là 0x94 bytes cho các biến cục bộ và các buffers, kể từ giá trị base của thanh ghi EBP.

Bằng cách nhấp đúp vào bất kỳ biến hoặc tham số nào của hàm, IDA sẽ đưa ta tới cửa sổ biểu diễn thông tin về Stack của hàm đó:

Theo quan sát tại Stack của hàm, ta thấy rằng đây là một hàm không nhận bất kì tham số nào, bởi vì tham số thường sẽ được truyền vào thông qua các lệnh PUSH trước khi thực hiện lời gọi hàm, và các tham số này sẽ phải nằm bên dưới địa chỉ trở về (r). Trong trường hợp hàm mà ta đang phân tích, bên dưới r không có thông tin gì và đó là lý do tại sao ta biết được đây là hàm không nhận tham số truyền vào.

Đây cũng chính là hàm main() của chương trình và thường hàm main() sẽ có các tham số là: env, argv argc, vv. Nhưng vì các tham số này không được sử dụng bên trong hàm nên IDA sẽ không quan tâm đến các tham số này. Nhấn x tại đầu hàm để tìm xrefs:

Nhấn OK, ta sẽ tới đây:

Với kết quả như trên hình, tôi sẽ đổi tên hàm thành main(). Sau khi đổi tên như vậy, IDA sẽ tự động thêm ba tham số vào cho hàm như hình dưới đây:

Nếu chúng ta nhấn x tại bất kỳ một trong ba tham số này, ta sẽ nhận được thông báo như bên dưới. Đó là bởi vì trong hàm main() không sử dụng tới chúng:

Vì các tham số này không được sử dụng nên chúng ta sẽ không cần quan tâm đến chúng nữa. Ta quay trở lại với cửa sổ Stack của hàm để phân tích tiếp:

Lúc này tại cửa sổ Stack, ta thấy các tham số đã được bổ sung thêm và nằm bên dưới của địa chỉ trở về. Phía trên địa chỉ trở về là “s”, hàm ý rằng đó chính là giá trị EBP của hàm trước khi gọi hàm main() được lưu vào Stack. Như đã đề cập ở trên, giá trị EBP này được lưu lại thông qua lệnh PUSH EBP, bên trên “s” sẽ là không gian dành cho các biến cục bộ được khai báo trong main, thông thường các biến này có dấu hiệu nhận biết là var_4, var_x v..v… Biến var_4 này được sử dụng để bảo vệ ngăn xếp khỏi lỗi tràn bộ đệm (buffer overflow). Ta chọn biến này và nhấn x sẽ có kết quả như sau:

Ta thấy có hai chỗ sử dụng tới biến var_4. Một là tại chỗ bắt đầu của hàm khi hàm lấy giá trị của security cookie vào thanh ghi eax, rồi gán lại giá trị đó cho biến var_4:

Cookie security là một giá trị ngẫu nhiên, được đem XOR với thanh ghi EBP, kết quả được bao nhiêu sẽ lưu lại vào biến var_4 khi hàm bắt đầu. Ta phân tích đoạn code thứ hai sử dụng tới biến var_4:

Tại đoạn code trên, ta thấy nó thực hiện lấy ra giá trị đã lưu tại biến var_4, sau đó XOR lại với EBP để phục hồi giá trị ban đầu trong ECX và bên trong lệnh CALL bên dưới sẽ thực hiện kiểm tra giá trị này:

Nếu như mọi thứ đều hợp lệ, hàm sẽ return bình thường, nhưng nếu thanh ghi ECX không giữ giá trị ban đầu của _security_cookie, nó sẽ rẽ nhánh sang lệnh JMP để exit và không cho phép ta thực hiện lệnh RET của hàm.

Chúng ta sẽ thấy rằng việc thực hiện exit nó chỉ có thể xảy ra khi có Overflow làm ghi đè lên giá trị của biến var_4 bên trong hàm. Do đó, chúng ta có thể đổi tên biến var_4 thành CANARY hoặc SECURITY COOKIE và đổi tên hàm thành Check_Canary():

Sau khi đổi tên như trên, ta thấy code trông cải thiện hơn một chút:

Tiếp theo, chúng ta thấy 3 biến mà chưa rõ mục đích sử dụng, hai biến được khởi gán bằng 0, và một biến IDA nhận diện được tên là Size, được khởi gán bằng 8. Quan sát các tham chiếu đến biến var_7d, chúng ta thấy nó được sử dụng ở đây:

Biến này sẽ nhận giá trị được lưu tại thanh ghi AL (giá trị của AL được gán thông qua lệnh CALL ở bên trên) và sau đó lại gán lại vào thanh ghi EDX. Tiếp theo chương trình kiểm tra xem có bằng 0 hay không để quyết định rẽ nhánh code theo hướng “Good” hoặc “Bad”. Do đó, đây là một biến kiểu byte. Ta sẽ đổi tên biến này thành SUCCESS_FLAG. Tại cửa sổ Stack của hàm, ta thấy rằng IDA đã nhận diện được đây là biến có kích thước một byte.

Nhấn N để đổi lại tên biến. Sau đó, tôi đổi màu lại các khối code trong IDA như trên hình để dễ dàng hơn trong việc nhận diện và phân tích bằng IDA:

Rõ ràng là nếu tôi chỉ cần patch tại lệnh nhảy JZ thì tôi sẽ đạt được mục đích của mình, nhưng chúng ta sẽ không làm như thế. Chúng ta cần phân tích sâu hơn để đạt được mục tiêu đã đề ra. Tiếp tục phân tích biến tiếp theo là var_90:

Ta thấy rằng, ban đầu biến này được khởi gán bằng 0 ở đầu hàm, sau đó nó được sử dụng tại đoạn code như trên hình. Đoạn code này nằm trong một vòng lặp. Phân tích sâu hơn ta thấy nó sẽ đọc lần lượt từng byte một từ biến Buf tại 0x231109 vào EDX, sau đó cộng với biến var_90 (ban đầu là 0) và lưu lại vào EDX, cuối cùng lại lưu lại vào biến var_90. Như vậy, ta nhận thấy thanh ghi EDX luôn là tổng của tất cả các bytes, do vậy ta có thể đổi tên biến này thành SUMMARY:

Cùng với quá trình phân tích trên, ta cũng có thể nhận ra biến var_84 chính là bộ đếm của vòng lặp, sau mỗi lần lặp biến này sẽ được tăng thêm 1, và thoát khỏi vòng lặp nếu như giá trị của biến này lớn hơn hoặc bằng 4. Đoạn code thực hiện tăng biến đếm này như hình dưới và tôi sẽ đổi tên nó thành COUNTER:

Biến COUNTER này cũng được sử dụng tại 0x231109 như là một index để đọc các bytes từ biến Buf:

Tiếp theo chúng ta sẽ nghiên cứu biến Buf để xem nó sẽ chứa nội dung gì:

Tại đoạn code trên ta có thể thấy chương trình sử dụng hàm gets_s() để nhận thông tin mà người dùng nhập vào từ bàn phím. Qua đó, ta biết được biến Buf sẽ là nơi chứa chuỗi tên của người dùng nhập vào và chuỗi này có kích thước tối đa là 8 byte (Size được gán bằng 8 ở đầu hàm).

Trước đó, ta thấy một hàm tại địa chỉ 0x2310A0 bên dưới chuỗi yêu cầu nhập tên người dùng, ta có thể khẳng định luôn đây là hàm printf. Do đó, ta đổi tên cho hàm này:

Chuyển qua cửa số Stack, có thể thấy độ lớn của biến Buf này bằng cách nhấn chuột phải và chọn Array. Kết quả, biến Buf sẽ có kích thước là 120 bytes:

Hoàn toàn khớp với khai báo của nó trong mã nguồn của thầy Ricardo:

Sau khi chuyển đổi như trên, ta thấy thông tin về tham số và các biến tại cửa sổ Stack đã trở nên rõ ràng hơn rất nhiều. Tiếp theo, sau khi nhận thông tin do người dùng nhập vào và lưu vào trong biến Buf, chương trình sẽ sử dụng hàm strlen() để lấy ra chiều dài của chuỗi đã nhập vào:

Chiều dài chuỗi thu được qua hàm strlen() sẽ được lưu vào biến var_88, do vậy ta đổi tên biến này thành string_length:

Nếu biến này nhỏ hơn 4, tức là chiều dài của chuỗi nhập vào nhỏ hơn 4 thì chương trình sẽ gọi hàm exit() để thoát luôn. Tiếp theo là vòng lặp (mà ta đã phân tích ở trên) thực hiện cộng 4 byte đầu tiên của chuỗi mà chúng ta nhập vào, vì vậy ta sẽ nhóm các khối lệnh này lại để dễ nhìn hơn. Ta nhóm bằng cách chọn từng khối và nhấn Ctrl. Đặt tên cho khối đã nhóm. Để quay trở về trạng thái cũ chỉ việc nhấn chuột phải tại đó và chọn Ungroup.

Tiếp theo, ta thấy rằng chương trình lại sử dụng lại biến Buf để nhận mật khẩu mà người dùng nhập vào. Có thể sử dụng lại biến Buf này là vì chương trình đã tính toán xong.

Cũng tương tự như trên, chương trình sử dụng hàm strlen() để tính toán độ dài của mật khẩu và nếu nhỏ hơn 4 thì cũng sẽ exit() luôn:

Tôi đổi màu các khối như trên hình để dễ nhìn hơn. Như vậy, nếu chiều dài mật khẩu là 4 hoặc lớn hơn, chúng ta sẽ rẽ nhánh theo khối màu xanh lam như trên hình. Tại khối lệnh màu xanh lam này sẽ lấy mật khẩu và chuyển đổi nó sang dạng thập lục phân bằng hàm atoi(). Tại thanh Python của IDA hành động trên sẽ tương đương với hàm hex(). Ví dụ, tôi thực hiện như sau:

Sau khi chuyển đổi bằng hàm atoi() thì chương trình lưu kết quả chuyển đổi vào biến var_8C, do đó tôi đổi tên biến này như hình:

Sau đó, ta thấy rằng mật khẩu ở dạng Hexa này sẽ được đem XOR với một giá trị mặc định của chương trình là 0x1234. Kết quả sau khi thực hiện lệnh XOR sẽ được lưu lại vào cùng một biến. Tiếp tục quá trình phân tích, ta thấy biến lưu tổng 4 byte đầu tiên của chuỗi tên người dùng và giá trị hex đã tính toán trước đó được truyền cho một hàm bên dưới tại 0x2311A4. Kết quả trả về của hàm này sẽ lưu vào thanh ghi AL để từ đó đưa ra quyết định nhảy tới “good” hay “bad”. Do đó, ta sẽ đổi tên hàm thành CHECK_EXIT():

Như vậy, có thể thấy hàm CHECK_EXIT() sẽ nhận hai tham số truyền vào thông qua hai lệnh PUSH. Vì vậy, chúng ta sẽ đổi tên cả hai tham số trong hàm này tương ứng với các biến ta đã phân tích như sau:

Sau khi thay tên cho các tham số để dễ hiểu hơn, ta nhấn chuột phải tại hàm để đặt lại kiểu cho hàm như sau:

Làm như vậy hàm sẽ được khai báo lại như hình:

Quay trở lại nơi gọi hàm sẽ thấy IDA tự động thêm các chú thích cho các tham số tương ứng:

Có thể thấy IDA đã thực hiện công việc rất tuyệt vời, giờ đi vào phân tích kĩ hơn nhiệm vụ của hàm CHECK_EXIT():

Quan sát code của hàm, trước khi làm phép so sánh thì hàm này thực hiện lệnh SHL EAX, 1, tương đương với việc đem giá trị tại EAX nhân 2. Vì vậy, nếu hai giá trị tại lệnh so sánh là bằng nhau, ta sẽ đi đến khối màu xanh lam, nơi code của hàm thiết lập cho thanh ghi AL bằng 1 (mà thanh ghi AL này sẽ được gán cho biến SUCCESS_FLAG để quyết định ta sẽ nhận thông báo “good” hay là “bad”).

Như vậy, tóm tắt lại toàn bộ quá trình đã phân tích ở trên:

  • Chương trình sẽ lấy 4 byte đầu tiên từ tên của người dùng nhập vào và thực hiện phép cộng dồn.
  • Tiếp theo sẽ nhận Password mà người dùng nhập vào, chuyển đổi sang dạng Hexa và đem thực hiện XOR với giá trị mặc định 0x1234. Cuối cùng sẽ đem kết quả tính toán này nhân với 2.

Chúng ta sẽ xây dựng một công thức tính toán với giả định rằng ta có tên của người dùng nhập vào bởi vì keygen sẽ dựa vào thông tin này để tính toán. Với tên của người dùng bất kỳ thì keygen sẽ sinh ra một mật mật khẩu tương ứng. Ta có như sau:

  • X = PASSWORD đã được chuyển đổi sang HEXA
  • (X ^ 0x1234) * 2 = SUMMARY
  • X ^ 0x1234 = (SUMMARY / 2)
  • X = (SUMMARY/2) ^ 0x1234

Ta đã biết cách hoạt động của lệnh XOR ở các phần trước rồi, tôi xin nhắc lại như sau: A ^ B = C –> A = B ^ C;

Do đó để tìm X thì công thức sẽ là: X = (SUMMARY/2) ^ 0x1234

Tôi viết thử một script bằng python như bên dưới, trong script này tôi cố định tên của người dùng là “manowar”, có chiều dài nhỏ hơn 8 bytes, vậy tổng của chuỗi này sẽ như sau:

Nhớ lại code của chương trình, ta không cộng toàn bộ chuỗi mà chỉ tính tổng có bốn kí tự đầu mà thôi. Do đó, tôi giới hạn lại như sau:

Với đoạn code trên, ta có thể tạo một keygen cho bất kỳ tên người dùng nào, việc lấy thông tin người dùng nhập vào sẽ thông qua hàm raw_input():

Kết quả cho chuỗi “manowar” vẫn giống như trên nhưng thay vào đó ta có thể tính toán cho bất kỳ tên người dùng nào. Ví dụ:

Chúng ta có biểu thức tính toán mật khẩu như sau:

X = (SUMATORIA/2) ^ 0x1234

Do vậy, ta viết lại biểu thức này bằng python như bên dưới đây:

Ta thử nhập tên và password tìm được ở trên, kết quả như hình minh họa dưới đây:

Chúng ta đã có keygen hoàn chỉnh, ở đây ta không cần phải thực hiện chuyển đổi mật khẩu từ dạng hexa sang thập phân bởi vì Python mặc định khi xuất ra màn hình là ở dạng thập phân rồi.

Kiểm tra với một chuỗi name dài hơn như trên hình, ta thấy rằng nó chỉ cộng 4 ký tự đầu tiên của tên người dùng, do đó kết quả tính toán ra password sẽ là như nhau. Bài tập này sẽ bị crash khi ta nhập vào chuỗi name có 8 kí tự bởi vì 8 kí này sẽ bao gồm cả kí tự kết thúc chuỗi. Chuỗi mà có 7 kí tự thì sẽ chạy bình thường. Chỉ có một vấn đề khi nếu tổng 4 kí tự cho ra kết quả là một số lẻ:

Sẽ không có giải pháp vì mật khẩu cuối cùng sẽ được nhân với 2 và được kết quả sẽ không bao giờ là số lẻ. Do vậy, ta cần phải bổ sung thêm đoạn kiểm tra sau:

Trong đoạn code trên, chúng ta sẽ kiểm tra phần dư khi thực hiện phép chia cho 2. Nếu kết quả bằng 0 thì là hợp lệ, còn khác 0 thì sẽ hiển thị thông báo để thử một chuỗi tên khác:

Tôi nghĩ rằng keygen hoạt động như vậy là ổn rồi, do đó tôi xin được kết thúc bài viết này tại đây.

Hẹn gặp lại các bạn ở phần 19!!!

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


Chúng ta lại gặp nhau ở phần 17 và trong phần này tôi và các bạn sẽ giải quyết bài tập đã đưa ra ở phần 16 (PACKED_PRACTICA_1.exe).

Bài tập này rất đơn giản và bạn phải thực hiện unpack như các bước tôi đã trình bày ở các phần trước. Sau đó reverse nó để tìm hiểu quá trình hoạt động cũng như viết một keygen bằng ngôn ngữ nào cũng được. Ở phần 17 này, ta thay đổi một chút, tôi sẽ thực hiện unpack từ xa thông qua một máy ảo Windows 10 chạy trên nền VMware Workstation. Máy chính dùng để debug từ xa trong trường hợp này cũng là Windows 10 (máy mà tôi đã cài đặt và sử dụng IDA), tuy nhiên bạn có thể tùy chọn sử dụng các hệ điều hành khác.

Tóm lại, để tránh bị nhầm lẫn, tôi sẽ gọi máy mà tôi đã cài đặt IDA là máy chính hay “Main” và máy Windows 10 mà tôi chạy trên VMware, nơi sẽ dùng để chạy packed file là “Target“.

Bên cạnh packed file đã được copy vào máy Target, tôi cũng phải chép file win32_remote.exe từ thư mục mà tôi đã cài đặt IDA trên máy Main ({IDA_path}\dbgsrv) vào máy Target. Do máy ảo Windows 10 của tôi là 64 bits, mà packed file lại là 32 bits, do vậy tôi phải chạy máy chủ 32-bit là vì thế.

Chúng ta cũng vẫn phải có file PACKED_PRACTICA_1.exe trên máy Main để thực hiện phân tích local. Load file này vào IDA để phân tích và nhớ chọn Manual Load để nạp tất cả các sections liên quan. Sau khi IDA loader phân tích xong, tại màn hình Disassembly của IDA sẽ hiển thị Entry Point của chương trình. Lúc này, chúng ta sẽ không chạy Debugger.

Theo lưu ý của Ricardo thì một điều khá quan trọng là không đổi tên của file idb, đó là vì nếu tệp thực thi là PACKED_PRACTICA_1.exe trong Main, khi load vào IDA để phân tích nó sẽ lưu một database với phần mở rộng là idb trong cùng thư mục, file này trùng tên và chỉ khác phần mở rộng là PACKED_PRACTICA_1.idb và không có thêm tên nào khác, vì nếu thay đổi sẽ có vấn đề trong việc nhận biết tiến trình từ xa thuộc về cùng một file thực thi đã được phân tích locally.

Cần đảm bảo máy MainTarget kết nối được với nhau. Tiến hành chạy file win32_remote.exe trên máy Target. Kết quả như hình dưới đây:

Như kết quả trên hình, IDA remote debug server đã chạy và đang lắng nghe tại một địa chỉ IP và cổng tương ứng. Trong trường hợp của tôi, địa chỉ IP là 192.168.100.5 và cổng là 23946. Bạn lấy kết quả có được trên máy bạn, sau đó cấu hình thay đổi debugger như sau:

Trong phần thiết lập cấu hình, ở Hostname nhập vào địa chi IP của IDA remote debug server và port tương ứng. Vì chúng ta cần phải thực thi packed file để thực hiện unpack nó, do ta không thể attach file nên sẽ phải sửa lại đường dẫn thư mục trỏ tới đúng vị trí của file thực thi trên máy Target.

Trong trường hợp của tôi, packed file trên máy Target được để tại Desktop, vậy nên đường dẫn của tôi sẽ là C:\Users\admin\Desktop\PACKED_PRACTICA_1.exe

Với thông tin trên, tôi sửa lại các đường dẫn như sau:

Cấu hình xong nhấn OK để xác nhận các thiết lập, sau đó nhấn Start Process, chương trình sẽ dừng lại tại Entry Point của packed file trong chế độ Debugger.

Chúng ta có thể nhận thấy có một điểm khác lạ ở đây, đó là file thực thi có các địa chỉ ngẫu nhiên sau mỗi lần thực thi. Đó là một điểm rất quan trọng, vì ở các phần trước chúng ta cho thực thi file, dump file và sửa IAT của cùng một tiến trình mà không hề tắt nó, địa chỉ của file cũng hề thay đổi sau mỗi lần thực hiện debug.

Mở cửa sổ Segments và quan sát section đầu tiên (UPX0) bên dưới HEADER. Tại máy của tôi thì section này bắt đầu từ 0x961000 và kết thúc tại 0x968000:

Sử dụng tính năng Search text của IDA để tìm lệnh POPAD hoặc POPA, chúng ta sẽ tìm thấy nơi lệnh này được thực hiện trước khi nhảy tới OEP.

Theo kết quả trên hình, ta biết được OEP của chương trình là 0x96146e. Tuy nhiên, tôi cũng có thể tìm thấy địa chỉ này bằng cách đặt một Breakpoint on execution lên toàn bộ section đầu tiên.

Chuyển tới địa chỉ 0x961000, nơi bắt đầu của section đầu tiên. Tại đây, ta quan sát thấy các thông tin về section, rõ ràng là các địa chỉ không khớp bởi vì nó được sinh ngẫu nhiên. Như ta thấy, địa chỉ Imagebase không phải là 0x400000, nhưng giá trị Virtual Size vẫn là 0x7000 (vì section này bắt đầu ở 0x961000 và kết thúc ở 0x968000).

Nhấn F2 tại địa chỉ bắt đầu của section này (trên máy tôi là 0x961000) và cấu hình thiết lập cho breakpoint cần đặt như sau:

Kiểm tra và xóa tất cả các breakpoint khác (nếu có) và chỉ để lại breakpoint vừa đặt ở trên, sau đó nhấn F9 để chạy. Chương trình break và thông tin tại màn hình Disassembly khớp với địa chỉ chúng ta đã thấy đó là OEP. Xóa Breakpoint đã đặt đi để nó không con bôi đỏ toàn bộ section nữa:

Sau đó, cho IDA thực hiện Reanalyze Program. Kết quả có được như sau:

Quay lại cửa sổ Segments, thu thập thông tin liên quan (địa chỉ bắt đầu & kết thúc của file) và chỉnh sửa lại Python script để phục vụ cho việc dump file

Trên máy tôi, giá trị Imagebase của file lúc này là 0x960000 và địa chỉ kết thúc của file là 0x96b000. Tôi chỉnh lại Python script như sau:

Thay đổi xong, quay lại IDA và cho thực thi script này thông qua File > Script file:

Script thực hiện sẽ dump ra cho chúng ta file có tên là dumped.bin (file này nằm trên máy Main do ta chạy script tại đó). Load file này vào PEditor và thực hiện Dump Fixer như đã làm ở các phần trước.

Fix xong, đổi lại tên của file thành dumped.exe:

Chép file dumped.exe đã fix ở bước trên vào máy Target. Tiếp theo sẽ là rebuild IAT. Tại máy Target ta mở Scylla và lựa chọn process của packed file.

Sửa lại OEP thành 0x96146e vì đó là địa chỉ mà chúng ta đã tìm thấy. Sau đó nhấn IAT Autosearch & Get Imports:

Sau khi thực hiện xong, ta thấy rằng có hai Invalid API. Lấy địa chỉ Invalid này cộng với thông tin ImageBase của file:

Python>hex(0x960000+0x20d4)
0x9620d4

Tại cửa sổ Hex View ta tới địa chỉ này như trên hình, thông tin có được trông có vẻ nó không phải là một phần của IAT. Để xác minh ta chuyển qua cửa sổ IDA Disassembly và tìm tại địa chỉ này có xrefs nào tới không. Kết quả có được như sau:

Hãy xem nó đi đâu:

Chúng tôi thấy rằng nó kết thúc bằng một câu lệnh RET. Nhự vậy, ta thấy đó không phải là một hàm API, vì vậy ta có thể nhấn chuột phải và chọn Cut Thunk để loại bỏ giá trị Invalid này ra khỏi IAT. Tương tự, ta kiểm tra với địa chỉ:

Python>hex(0x960000+0x20dc)
0x9620dc

Địa chỉ này sẽ gọi tới hàm API:

Kết quả này trùng với thông tin hàm mà Scylla đã tìm ra được:

Do đó, ta có thể Cut Thunk này và chuẩn bị bước Fix Dump:

Mọi thứ trông có vẻ OK rồi, giờ ta tiến hành bước Fix Dump cho file dumped.exe. Scylla sẽ lưu thành một file mới có tên là dumped_SCY.exe. Tuy nhiên, ta chạy thử thì file sau khi đã rebuild IAT này sẽ không chạy được:

Điều này xảy ra là bởi vì trong dumped file, các địa chỉ được tạo ra trong lúc runtime và không thuộc về IAT, không được phân bổ lại do chúng luôn luôn thay đổi mỗi khi bắt đầu. Do vậy, để file có thể run được, ta phải loại bỏ cơ chế bảo vệ địa chỉ ngẫu nhiên này.

Load dumped_SCY.exe vào một IDA mới cùng với tùy chọn Manual Load, sau đó ta đi tới HEADER của file:

Tại đây bạn sẽ nhìn thấy một trường có tên là Dll Characteristics. Trên máy bạn, trường này sẽ có một giá trị khác 0:

Để sửa nó thành giá trị 0, chọn Edit > Patch Program > Change Word và sửa thành 0 như hình dưới đây:

Nhấn OK để chấp nhận chỉnh sửa:

Và sau đó chọn Apply patches to input file để save lại.

Hoặc có một số cách khác ít thô bạo hơn cách trên:

  • Sử dụng các trình PE Editor: Các bạn có thể sử dụng DIE/CFF Explorer/ PE-bear để bỏ thuộc tính Dynamic Base:

Thử chạy lại file đã fix xem thế nào, kết quả là chạy được bình thường rồi 🙂 :

Như vậy là chúng ta đã thực hiện unpack thành công, trong phần tiếp theo tôi và các bạn sẽ reverse file đã unpack này để hiểu cách thức hoạt động của nó cũng như viết keygen. Hẹn gặp lại các bạn trong phần 18!!

Image result for it's over the ring gif

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ước khi tiếp tục tìm hiểu thêm về các chủ đề khác, chúng ta sẽ thực hành thêm một vài bài tập unpack với các packer khác. Trong phần này target sẽ là UnPackMe_ASPack 2.2 (https://mega.nz/#!7XJ33I6D!DvNo6dNeCeyTDoXpSM9zeZWIi1kpALs26oCNd2tCUbY)

Load file bị packed vào IDA:

Ta dừng lại tại Entry Point của file, tại đó bắt đầu bằng một lệnh PUSHAD. Lệnh PUSHAD này thực hiện lưu toàn bộ giá trị hiện thời của các thanh ghi vào Stack theo thứ tự như sau:

Trực quan hơn các bạn có thể xem ví dụ minh họa dưới đây:

Ngược lại với PUSHAD, ta có lệnh POPAD, là lệnh lấy các giá trị từ stack và lưu lại vào các thanh ghi theo thứ tự như mô tả bên dưới (giá trị của thanh ghi ESP trên Stack sẽ được bỏ qua. Thay vào đó nó sẽ được tăng lên sau khi mỗi thanh được pop ra).

Đối với các trình packer đơn giản, hầu hết chúng đều bắt đầu với lệnh PUSHAD để lưu trạng thái ban đầu của các thanh ghi khi bắt đầu và sử dụng POPAD để khôi phục lại các giá trị đã lưu, trước khi nhảy tới OEP để thực hiện code của chương trình đã “rã code” hoàn toàn trong bộ nhớ.

Nhờ vào dấu hiệu này, chúng ta có thể dễ dàng tìm thấy OEP bằng cách sử dụng phương pháp PUSHAD-POPAD. Tuy nhiên, với các trình packer tiên tiến hơn về sau, các nhà phát triển đã nhận ra được điểm yếu này và tránh sử dụng những câu lệnh trên.

Vậy phương pháp này là như thế nào? Chúng ta hãy cùng xem xét với file đã load.

Trước tiên, chúng ta cần lựa chọn trình debugger và chạy nó. Cách nhanh nhất là tại Debugger > Select Debugger, chọn Local Win32 Debugger (hoặc bạn nào dùng IDA 7+ thì là Local Windows Debugger). Nhưng bây giờ, để thực hành chúng ta sẽ thực hiện thông qua Python. Ta có thể gõ từng câu lệnh một tại thanh Python của IDA hoặc sử dụng plugin mà chúng ta đã cài đặt là IpyIDA sẽ tiện lợi hơn. Tôi sẽ sử dụng plugin này để minh họa:

Trước tiên, tôi import idc, sau đó khi gõ idc.Load và nhấn TAB, plugin sẽ cung cấp cho tôi các hàm có liên quan. Ở đây tôi chọn idc.LoadDebugger. Trong trường hợp của chúng ta, ta phải chọn win320 cho local debugger (1 là dành cho remote debugger). Sau khi gõ lệnh, ta có kết quả trả về là True như sau:

Như trên hình, ta thấy rằng nó đã được chọn, nếu lặp lại lệnh này một lần nữa ta sẽ nhận được FALSE vì đã hoạt động rồi.

Phương pháp PUSHAD dựa trên việc thực hiện lệnh PUSHAD và trong lệnh tiếp theo, ta tìm các thanh ghi đã được lưu vào Stack và sau đó đặt một breakpoint để dừng trình gỡ lỗi khi nó cố gắng phục hồi giá trị các thanh ghi bằng lệnh POPAD, ngay trước khi nhảy tới OEP sau khi giải nén xong mã gốc.

Vì vậy, nhấn F2 để đặt một breakpoint tại lệnh bên dưới lệnh PUSHAD, ta sẽ dừng lại tại lệnh này sau khi cho thực thi chương trình. (Ở đây lệnh PUSHA tương tự như PUSHAD).

Nếu bạn muốn thực hiện thao tác trên bằng Python thì có thể gõ lệnh sau:

Bằng lệnh trên, ta đã đặt một breakpoint từ Python. Tham số đầu tiên là địa chỉ ta muốn đặt bp, tham số thứ hai là kích thước của breakpoint và tham số thứ ba là kiểu bp. Trong trường hợp này, ta muốn dừng thực thi của chương trình thông qua software bp, do đó truyền vào là BPT_SOFT hoặc 0.

Chúng ta đã lựa chọn được Debugger ở bước trước, và cũng đã đặt breakpoint. Bây giờ, ta khởi động trình debugger để buộc nó dừng lại tại breakpoint đã đặt. Rất đơn giản bằng cách nhấn F9 hoặc từ Python, ta gõ lệnh sau:

Với lệnh này, trình debugger mà ta lựa chọn sẽ khởi chạy, và nếu tất cả mọi thứ đều ngon lành, ví dụ trong trường hợp này, nó sẽ dừng lại tại Breakpoint mà chúng ta đã đặt tại địa chỉ 0x46B002:

Bây giờ, quan sát giá trị các thanh ghi đã được lưu vào cửa sổ Stack, ta sẽ đặt một bp ở dòng đầu tiên để dừng lại ở đó, vì đó là nơi các giá trị của các thanh ghi được lưu bởi PUSHAD sẽ được khôi phục lại bằng POPAD.

Như vậy, chúng ta phải đặt BP tại 0x19FF64 (trong trường hợp trên máy của tôi). Chuyển con trỏ để lựa chọn cửa sổ Disassembly và sau đó nhấn vào mũi tên nhỏ bên cạnh thanh ghi ESP, ta sẽ tới đây:

Bằng cách nhấn mũi tên bên cạnh thanh ghi như thế IDA sẽ đi tới địa chỉ này tại màn hình Disassembly. Từ đó, chúng ta có thể đặt Breakpoint bằng cách nhấn F2, nhưng chúng ta sẽ phải cấu hình lại bp là vì trong trường hợp này cần phải sử dụng On Read and Write chứ không phải On execution, bởi ta muốn dừng lại khi nó phục hồi hoặc đọc giá trị chứ không phải là thực thi mã tại đó.

Khi nhấn F2, cửa sổ Breakpoint settings sẽ hiện ra cho phép ta cấu hình như sau:

Để kiểm tra breakpoint đã đặt có chính xác không, ta xem tại Debugger > Breakpoints > Breakpoint List:

Tại cửa sổ Breakponts ở trên, ta có thể nhấn chuột phải và chọn Edit để thay đổi cấu hình breakpoint mà chúng ta muốn.

Vậy ta có thể đặt bp tương tự như đã làm thông qua Python được không? Hoàn toàn được nhé:

Với câu lệnh trên thì tham số đầu tiên là địa chỉ cần đặt bp, tham số thứ hai là kích thước của bp và tham số 3 kiểu breakpoint cần đặt. Trong trường hợp này Read/Write Access như mô tả trong bảng trên. Do đó, nếu tôi gõ lệnh, một breakpoint tương tự sẽ được thiết lập như khi ta thực hiện bằng tay.

Bây giờ vô hiệu hóa các BP đã đặt trước đó trong danh sách các Breakpoints bằng cách nhấp chuột phải và chọn Disable hoặc từ Python gõ lệnh:

Với tham số thứ hai bằng 1, tức là ta kích hoạt nó, còn bằng 0 thì tức là ta tắt nó. Kết quả tại màn hình danh sách các bp như sau (màu xanh tức là bp đã bị disable):

Tiếp theo nhấn F9 để tiếp tục hoặc gõ lệnh sau:

Nhấn OK ta sẽ dừng lại tại đây:

Ta thấy chương trình dừng lại ngay sau lệnh POPAD khi nó khôi phục các thanh ghi và cũng thấy rằng từ Stub này nó sẽ nhảy tới địa chỉ OEP tại 0x4271b0 thông qua cặp lệnh PUSH & RET (tương tự như lệnh JMP). Do vậy, ta trace code để thực hiện các lệnh này cho đến khi tới được OEP như hình dưới đây:

Tại OEP, cho IDA phân tích lại chương trình giống như đã làm trong bài viết trước. Kết quả có được như sau:

Lúc này thì toàn bộ code của chương trình đã được unpack hoàn toàn trên bộ nhớ, công việc tiếp theo là dump chương trình. Ta phải tìm địa chỉ ImageBase và địa chỉ cuối cùng trong section cuối cùng của file thực thi. Trong cửa sổ Segments, tôi thấy ImageBase0x400000 và địa chỉ kết thúc là 0x46e000:

Thay vì sử dụng idc script đã đề cập ở phần 15, phần này tôi sẽ sử dụng một script tương tự như vậy nhưng viết bằng Python. Nội dung của script đơn giản như sau:

Đoạn code python sử dụng để dump file như trên hình, tôi lưu nó với tên là ipython_dump.py. Sau đó, thực thi script này thông qua File-> Script File. Sau khi thực hiện xong ta sẽ thấy có tập tin dumped.bin được tạo ra. Tiếp theo ta sẽ sử dụng trình PE Editor để fix lại file dump này (tại cửa sổ Section Table Viewer, nhấn chuột phải và chọn Dump Fixer):

Kết quả file sẽ lấy lại được icon như hình dưới đây:

Phần dump file là coi như xong. Tiếp là công việc rebuild lại IAT. Mở Scylla 0.98 và chọn process của file mà hiện ta đang dừng lại tại OEP:

Thay thế bằng OEP là 0x4271B0, nhấn IAT Autosearch (nhấn Yes để chấp nhận sử dụng ) và tiếp theo là Get Imports. Nếu nhấn Show Invalid, ta sẽ thấy có một loạt các APIs lỗi. Thử xem liệu Scylla có thể khắc phục các hàm invalid này một cách tự động hay không?

Chúng ta thấy rằng Scylla không thể sửa được, vì vậy ta sẽ thực hiện bằng tay.

Như trên hình, chúng ta thấy hàm API đầu tiên tại 0x460818, đây là hàm hợp lệ, còn trên đó là các giá trị không hợp lệ. Bắt đầu với địa chỉ không hợp lệ đầu tiên tại 0x4600ec.

Bạn thấy rằng nội dung không trỏ đến bất kỳ địa chỉ hợp lệ nào và nếu bạn nhấn CTRL + X sẽ không tìm thấy tham chiếu nào:

Trong khi ở các hàm API chuẩn sẽ có các tham chiếu sử dụng tới hàm API đó. Ví dụ:

Do vậy, các địa chỉ này không phải thuộc IAT, ta sẽ xóa chúng.

Nếu như tôi nhấn clear và nhấn lại IAT Autosearch một lần nữa, nhưng tuy nhiên lúc này tôi không đồng ý chọn Advanced mode, ta thấy Scylla sẽ tìm được đầy đủ các thông tin về IAT. Chỉ có duy nhất một vị trí invalid cần phải xác minh lại:

Căn cứ vào vị trí invalid ta tìm được hàm API bị thiếu là:

Tiến hành fix lại hàm này trong Scylla:

Sau khi fix xong, bước cuối cùng lựa chọn Fix Dump để repair lại dump file:

Sau khi sửa xong, ta có thể thực thi file một cách bình thường:

Phần 16 này xin được kết thúc tại đây. Tôi gửi kèm một bài tập nhỏ để các bạn thực hành, nhiệm vụ của các bạn là unpack file PACKED_PRACTICA_1.exe (https://mega.nz/#!jGQHDQpA!tRHdeG_z96GwEqY3ZDh0j0XEE0Ja01lIYbEEhufIVT8). Hẹn gặp các bạn ở phần 17!

Image result for unpackme 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:

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.