Posts Tagged ‘IDA’


Trong phần 23 này tôi sẽ giải quyết câu hỏi đặt ra đối với file IDA1.exe đã được gửi kèm trong phần 22. Đầu tiên, load file vào IDA để phân tích, ta sẽ dừng lại tại hàm main():

Tại đây, bạn cuộn chuột xuống dưới sẽ nhìn thấy chuỗi “you are a winnner man je\n“. Đó chính là nơi mà chúng ta cần phải tới được. Tôi đổi màu cho block này để tiện cho việc nhận diện một cách dễ dàng.

Quan sát bên trên một chút, ta thấy rằng để có thể tới được đoạn code hiển thị chuỗi mong muốn thì phải vượt qua được đoạn code thực hiện so sánh giữa biến var_C với một hằng số 0x51525354. Nhấn chuột phải tại giá trị 0x51525354, IDA sẽ cung cấp cho chúng ta một loạt các lựa chọn chuyển đổi, ở đây chúng ta sẽ lựa chọn chuyển thành các kí tự ‘QRST’:

Các bạn để ý một chút sẽ dễ dàng có thể nhận ra, file IDA1.exe được biên dịch không sử dụng cơ chế CANARY protection, vì nếu có áp dụng thì ở của đầu hàm nó sẽ phải đọc dữ liệu từ một địa chỉ thuộc section data vào thanh ghi eax và đem xor với thanh ghi ebp, kết quả được lưu vào biến nằm ngay trên giá trị của ebp đã được lưu vào Stack trước đó. Trước khi kết thúc hàm biến này sẽ được đọc ra để kiểm tra lại. Tóm lại file IDA1.exe không áp dụng Stack Canary, ta sẽ phân tích tĩnh bố trí các biến và tham số tại Stack của hàmmain(), nhấp đúp vào bất kỳ biến nào ta sẽ có kết quả như sau:

Tại đây, ta thấy rằng biến duy nhất xuất hiện trong Stack, ngay bên trên giá trị của EBP được lưu, đó là biến var_C. Chọn biến này và nhấn “X”, sẽ thấy nó được sử dụng để so sánh với chuỗi “QRST”. Căn cứ trên kết quả so sánh sẽ đi tới đoạn code hiển thị thông báo “winner” hay là không:

Tóm lại, file này không áp dụng CANARY, vì như ta thấy rõ ràng trong trường hợp này thì đây là một lệnh kiểm tra thuộc mã nguồn của chương trình. CANARY không thể “trộn” lẫn nằm xen kẽ với các quyết định rẽ nhánh trong mã của chương trình, nó phải được bổ sung bởi chính trình biên dịch, và nằm ở bên ngoài code ban đầu.

Cho nên để tránh nhầm lẫn biến var_C với CANARY, ta có thể đổi tên nó thành var_Decision.

Như trên hình, biến này được sử dụng hai lần trong hàm main(). Vậy làm thế nào chúng ta có thể thay đổi giá trị của biến var_Decision để khiến cho chương trình rẽ nhánh đi tới đoạn code hiển thị thông báo “winner”?

Khi các bạn quan sát kĩ vào các dòng lệnh asm thì điều sẽ khiến bạn chú ý là có các biến được truy cập thông qua thanh ghi EBP và có những biến khác được truy cập thông qua thanh ghi ESP:

Toàn bộ các lệnh khởi tạo ban đầu được thêm vào bởi trình biên dịch nhằm thiết lập thanh ghi ESP nằm trên các biến cục bộ và buffer. Lệnh SUB ESP, 0xB8 làm nhiệm vụ dành ra không gian phục vụ cho các biến cục bộ và buffer được khai báo bên trong thân hàm main(). Để biết cụ thể thì các bạn có thể debug để hiểu rõ hơn. Bản thân IDA có một trợ giúp rất hữu ích là “Stack Pointer”, hỗ trợ hiển thị sự thay đổi của thanh ghi ESP kể từ điểm bắt đầu của hàm (giá trị tại đầu hàm được thiết lập bằng 0).

Kết quả tại code của chương trình sẽ xuất hiện thêm thông tin như hình dưới đây:

Quan sát trên hình các bạn sẽ thấy sự tác động của mỗi lệnh lên ngăn xếp bắt đầu từ đầu hàm, sau khi thực hiện PUSH EBP, thanh ghi ESP sẽ bị giảm đi 4, do vậy dòng lệnh thứ hai có thêm số 004 ở bên phải, hàm ý là stack pointer thay đổi. Tuy nhiên, ở câu lệnh thứ hai là MOV EBP, ESP, vì đây chỉ là một lệnh MOV nên lệnh này không làm thay đổi Stack, do vậy giá trị của ESP vẫn giữ nguyên, thanh ghi thay đổi là EBP.

Đó là lý do tại sao dòng lệnh thứ ba có cùng giá trị là 004 vì lúc này chưa thay đổi ESP. Như vậy, ta thấy rằng ở thời điểm này giá trị ESPEBP là giống nhau và thanh EBP từ đây có thể được sử dụng để truy xuất các biến cục bộ cũng như các tham số của hàm. Dòng lệnh thứ 3 thực hiện trừ ESP đi 0xB8, do vậy kết quả sẽ là giá trị 0xBC (so với điểm bắt đầu) và vì EBP vẫn ở mức 004 nên chênh lệch giữa hai thanh ghi EBPESP sẽ là 0xB8.

Quan sát kĩ hơn thì thấy khoảng cách này không thay đổi nữa ngay cả khi thực hiện các lời gọi hàm call __allocacall ___main (được tự động thêm vào trong quá trình biên dịch chương trình). Tóm lại, khoảng cách giữa EBPESP0xB8, và đây chính là khoảng không gian dành cho các biến cục bộ và buffer được khai báo trong thân của hàm main().

Tiếp tục đọc code, các bạn thấy có biến được IDA đặt tên là var_9c. Kiểm tra toàn bộ hàm main() thì thấy biến này không được sử dụng cho việc gì khác, vì vậy nó có thể là một biến tạm thời được tạo bởi trình biên dịch, ta sẽ đặt tên nó là Temp để dễ nhận biết:

Bên dưới biến Temp chúng ta có biến Buffer:

Nhấn chuột phải tại biến Buffer này và chuyển đổi sang kiểu array, ta sẽ được kết quả là một biến Buffer có kích thước là 140 phần tử, mỗi phần tử có kích thước 1 byte:

Nếu chúng ta có thể làm tràn biến Buffer bằng cách đẩy vào dữ liệu nhiều hơn 140 bytes, như vậy sẽ gây ra Buffer Overflow, lúc đó ta có thể ghi đè giá trị lên biến var_Decision và nếu có thể được sẽ ghi đè dữ liệu lên cả giá trị EBP đã được lưu và đè lên cả địa chỉ trở về (r). Điều này tùy thuộc lượng dữ liệu mà ta có thể nhồi vào biến Buffer.

Tiếp tục quá trình reversing, điểm gây chú ý tiếp theo liên quan tới trình biên dịch, đó là là cách nó truyền các tham số cho hàm. Ở target này, thay vì sử dụng lệnh PUSH để lưu các tham số vào Stack (như vẫn thường gặp), thì ở đây ta thấy chương trình lưu trực tiếp thông qua lệnh MOV. Các bạn quan sát lệnh printf, rõ ràng trong trường hợp này printf có một đối số duy nhất là địa chỉ của chuỗi “you are a winner man je\n” và không có tham số nào khác:

Ta thấy chương trình truy xuất địa chỉ của chuỗi trên vì có tiền tố offset ở phía trước và lưu vào biến có tên Format trong ngăn xếp. Nhưng cụ thể là ở đâu trong Stack? Lúc này chương trình không sử dụng thanh ghi EBP để truy cập đến tham số của hàm mà sử dụng chính thanh ghi ESP, tuy nhiên ký hiệu (esp+0B8h+Format) hơi khó hiểu, nếu chúng ta nhấn chuột phải tại đây IDA sẽ cung cấp cho ta thông tin thay thế như sau:

Nếu ta chọn cách biểu diễn như IDA gợi ý:

Qua việc chuyển đổi trên, ta có thể hiểu bản chất của câu lệnh mov sẽ thực hiện việc lưu địa chỉ của chuỗi vào trong nội dung thanh ghi ESP (đó chính là đỉnh của Stack), để sử dụng nó như một tham số của hàm. Do vậy, chương trình thay vì sử dụng lệnh PUSH thì nó sử dụng lệnh MOV để truyền tham số cho hàm, và theo logic thì lệnh PUSH sẽ thay làm đổi giá trị của ESP, trong khi lệnh MOV thì không, các bạn có thể thấy được thông qua giá trị 0xBC ngay trước hàm printf():

Cách truyền tham số này cũng được áp dụng đối với tất cả các hàm APIs khác mà IDA1.exe sử dụng. Áp dụng cách thức chuyển đổi tương tự như đã làm ở trên, ta sẽ có được các đoạn mã lệnh asm nhìn dễ hiểu hơn:

Tiếp tục quá trình phân tích, hàm printf() đầu tiên nhận ba tham số truyền vào cho nó. Vì sao lại biết được là có 3 tham số? Là bởi vì hàm in ra màn hình chuỗi sử dụng các giá trị để thay thế cho format string: “buf:%08x cookie:%08x \ n”. Các giá trị “%08x” sẽ được thay thế bằng hai tham số được truyền trước đó tại [esp+8][esp+4]:

Cách truyền tham số cho hàm thường sẽ là từ phải qua trái. Do đó, tham số thứ 3 của hàm printf() là địa chỉ của biến var_Decision, có được thông qua lệnh LEA và lưu tại thanh ghi EAX. Sau đó lưu địa chỉ này vào vị trí [ESP+8] trên Stack. Tham số thứ hai là địa chỉ của biến Buffer, cũng thực hiện theo cách tương tự nhưng được lưu vào bị trí [ESP+4] trên Stack và tham số đầu tiên được lưu tại [ESP] là địa chỉ của chuỗi “buf: %08x cookie: %08x\n”.

Nếu chạy trực tiếp file này tại màn hình console, ta sẽ thấy chuỗi được in ra màn hình cùng với địa chỉ của cả hai biến:

Tiếp theo, chương trình sử dụng hàm gets() để đọc các kí tự từ stdin (standard input) và lưu vào biến Buffer. Ta để ý ở đây số lượng kí tự nhập vào bằng bàn phím là tùy ý mà không có bất kỳ giới hạn nào, tức là ta có thể nhập vào nhiều hơn 140 ký tự mà không có vấn đề gì:

Đoạn code trên sẽ tương ứng với mã giả C như sau: gets(Buffer);

Vì vậy, giả sử nếu ta ghi vào Buffer này 140 chữ ‘A’ và kèm theo sau đó là chuỗi “TSRQ” (theo kiểu little endian), chương trình sẽ nhảy tới đoạn code hiển thị thông báo “winner” vì khi đó biến var_Decision nằm ngay bên dưới Buffer, sẽ được ghi đè bởi giá trị “QRST”. Kiểm tra bằng tay trước khi chúng ta thực hiện tự động bằng script:

Thực hiện lệnh tại Python bar của IDA, sau đó copy chuỗi được sinh ra:

“AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATSRQ”

Chạy chương trình và dán chuỗi này vào, kết quả có được như sau:

Kết quả đúng như mong đợi, ta tiến hành tạo script để chạy tự động, script này cũng giống như script đã tạo ở phần trước trước, chỉ khác ở này chỉ truyền một input cho chương trình thôi:

from subprocess import *
p = Popen([r'IDA1.exe', 'f'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)

print "Attach to the debugger and press Enter\n"
raw_input()

user_input="A" *140 + "TSRQ\n"
p.stdin.write(user_input)

testresult = p.communicate()[0]

print user_input
print(testresult)

Kiểm tra kết quả sau khi thực hiện script:

Phần 23 đến đây là kết thúc. Tôi gửi các bạn bài tập IDA2.exe (https://mega.nz/#!jXpgGSKJ!jx8p41yH5Drqq78fKonMqVQhmQf2BqwbJh9aqHDwZ_o) để các bạn thực hành, nó tương tự như IDA1.exe.

Image result for that was easy funny gif office space

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

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 22 này chúng ta sẽ tìm hiểu một chút về các công cụ bổ trợ cho IDA trong việc việc so sánh sự khác nhau giữa hai binary.

Nếu các bạn là dân dev, hay dân văn phòng suốt ngày lọ mọ với một mớ code/ tài liệu thì chắc sẽ chẳng lạ gì với việc tự làm “bằng cơm” hoặc sử dụng công cụ chuyên dụng để so sánh sự khác nhau/ sai khác trong cùng một source code hoặc giữa cùng một tài liệu tại những thời điểm khác nhau.

Tôi lấy ví dụ về việc so sánh giữa hai file văn bản: bên trái là file gốc vs bên phải là file đã chỉnh sửa (nhưng chữ sửa trong file để font màu trắng). Nếu không dùng tool mà làm “bằng cơm” để kiểm tra sự thay đổi, tôi đố các bạn tìm được hell-yes-onion-head-emoticon

Với các file binary cũng vậy, công cụ differ cũng sẽ cho ta biết sự khác biệt/ thay đổi giữa hai phiên bản của cùng một chương trình, các công cụ này sẽ cố gắng thực hiện phân tích, so khớp các hàm trong chương trình và đưa ra kết qủa về những hàm có sự thay đổi và thay đổi ở đâu.

Ta thấy rõ ràng để thực hiện công việc này không phải là dễ dàng, đặc biệt là khi có những thay đổi lớn từ phiên bản này sang phiên bản khác, sự thay đổi này có thể đến từ việc cập nhật các bản vá bảo mật để giải quyết các lỗ hổng tồn tại trong chương trình, hoặc có thể là những cải tiến mới trong tính năng của chương trình, v.v… Với dân chuyên crack soft hoặc tìm hiểu cracking thì việc so sánh giữa file gốc và file đã patch có thể giúp cho họ tìm hiểu được cách patch của các cracker khác. Với dân chuyên nghiên cứu exploit thì việc làm này có thể giúp họ biết được một bản vá bảo mật có giải quyết được triệt để lỗi hay không? Hay là patch lỗi này lại sinh ra lỗi khác có thể khai thác được.

Tính tới thời điểm hiện tại, cá nhân tôi biết được có 3 công cụ differ sử dụng kết hợp với IDA. Trong bài viết này, tôi sẽ giới thiệu lần lượt các công cụ này và tùy cảm nhận của từng người mà lựa chọn cho mình công cụ phù hợp hoặc sử dụng kết hợp.

Note: Thời điểm tôi chỉnh sửa lại bài viết này thì Bindiff đã ra phiên bản 5 (https://www.zynamics.com/software.html); Diaphora cũng được Joxean Koret cập nhật liên tục để có thể làm việc với các phiên bản IDA 6.9 tới 7.3

1. BinDiff

Công cụ phải nói tới đầu tiên chính là BinDiff, mục tiêu của nó là một công cụ hỗ trợ so sánh các tập tin nhị phân nhằm giúp các chuyên gia nghiên cứu lỗ hổng có thể nhanh chóng tìm thấy sự khác biệt và tương đồng trong mã chương trình được phân rã ở dạng câu lệnh asm. Công cụ này được phát triển bởi Zynamics, công ty này sau đó được Google mua lại vào năm 2011. Phiên bản mà tôi sử dụng tại thời điểm của bài viết này là BinDiff v4.3. Thông tin chi tiết về BinDiff có thể tìm đọc tại đây: https://www.zynamics.com/software.html

Các bạn download về và tiến hành cài đặt. Việc cài đặt BinDiff dễ như các bạn cài Win dạo, khác mỗi là bạn không phải kiếm cr@ck thôi, chú ý là trên máy cần phải cài đặt Java Runtime Enviroment (JRE) trước nhé. BinDiff 4.3 hiện chỉ hỗ trợ cho IDA 6.x, chưa có plugin cho 7.x (Note: hiện bản BinDiff 5 đã hỗ trợ cho IDA 7.x).

Để minh họa cho việc sử dụng BinDiff, tôi sẽ sử dụng các file .idb có sẵn: một file có vuln và một file đã được fix. Mở IDA lên và load database của file có vuln (VULNERABLE_o_NO.idb) vào:

Sau khi IDA load xong database của file có vuln, truy cập menu Edit > Plugins và chọn BinDiff 4.3:

Cửa sổ BinDiff sẽ xuất hiện như hình dưới:

Tiếp theo chọn Diff Database… và tìm tới database của file đã fix vuln để so sánh:

BinDiff sẽ chạy và phân tích hai databse này:

Sau khi thực hiện xong, BinDiff sẽ hiển thị kết quả cho chúng ta. Có thể các cửa sổ của BinDiff tại IDA của các bạn sẽ không giống như tôi, các bạn có thể thực hiện drag & drop để lựa chọn một chế độ xem phù hợp nhất:

Tab mà chúng ta quan tâm là “Matched Functions”:

Tại đây, các ban sẽ thấy cột đầu tiên (similarity) cung cấp kết quả về sự giống nhau giữa các hàm, Theo kinh nghiệm của nhiều người đã dùng BinDiff thì nếu giá trị trả về bằng 1.00 thì có nghĩa là hai hàm đó hoàn toàn giống nhau, không có thay đổi gì, ngược lại nếu giá trị này < 1.00 thì có nghĩa là cùng một hàm nhưng đã có sự thay đổi, khác biệt. Để dễ dàng và thuận tiện thì chúng ta chỉ việc nhấp chuột vào đầu cột đó để sắp xếp chúng theo kết quả từ khác nhau đến giống nhau. Tương tự như hình:

Theo kết quả có được như trên hình, ta thấy rằng ở đây chỉ có một điểm tương đồng nhỏ hơn 1. Nhấn chuột phải tại đó và chọn View Flowgraphs hoặc nhấn phím tắt là Ctrl+E:

Lúc này, plugin sẽ sẽ gọi tới bindiff.jar trong thư mục cài đặt của BinDiff để hiển thị FlowGraphs của hai hàm có sự thay đổi ở trên:

Như trên hình, ta thấy các khối được highlight bằng màu xanh, đó là những khối có code giống nhau, còn khối được highlight bằng màu vàng là khối có sự thay đổi và những câu lệnh thay đổi cũng được BinDiff highlight bằng những màu khác nhau để ta có thể dễ dàng nhận ra:

Như trên hình, các bạn sẽ nhận thấy được sự thay đổi ở lệnh nhảy, và chúng ta cũng đã biết việc đổi từ lệnh nhảy JLE thành JBE là cách để chương trình tránh bị dính lỗi Buffer Overflow, do vậy nếu trong một chương trình mà ta có cả phiên bản bị lỗi và phiên bản đã được vá lỗi, sau khi so sánh và nhìn vào các hàm đã thay đổi thì chúng ta sẽ phải thực hiện reverse lại hàm đó để xem nó có thực sự là lỗ hổng của chương trình hay không.

Một trong những ưu điểm của BinDiff so với hai công cụ sắp được đề cập là ngoài việc hỗ trợ chế độ đồ họa tương tác rất tốt, khả năng parse trực tiếp các file .idb mà không cần phải tạo ra các file trung gian thì nó còn cung cấp khả năng tìm kiếm rất tiện lợi, qua đó ta có thể tìm kiếm địa chỉ và bất kỳ đoạn text nào. Ví dụ, tôi thực hiện tìm kiếm chuỗi “cmp”:

Bên cạnh đó, BinDiff hỗ trợ sao chép địa chỉ của khối có thay đổi, qua đó ta có thể đi tới được khối này trong IDA bằng việc nhấn G và dán địa chỉ đã copy vào:

BinDiff cũng cung cấp một cửa sổ nhỏ tương tự như cửa sổ Graph Overview của IDA để giúp dễ dàng di chuyển, quan sát các hàm cùng danh sách các khối đã được phân rã:

Như các bạn thấy BinDiff cung cấp cho chúng ta một giao diện cùng các tính năng hữu ích, với những file đơn giản như trong ví dụ này thì các bạn thấy có vẻ dễ dàng, tuy nhiên với các ứng dụng lớn và phức tạp hơn thì sẽ không ngon ăn như thế này đâu.

“Dễ thế này thì Đông Lào người ta đi săn bug hết!!”

Do BinDiff hiện tại chỉ hỗ trợ IDA 6.x, với phiên bản IDA 7.x thì phải sử dụng BinExport (https://github.com/google/binexport) để xuất ra file và import vào Bindiff để so sánh. Phiên bản BinExport mà tôi đang sử dụng được build bởi bạn Ngôn Nguyễn (aka @computerline). Cách thức thực hiện như sau, đầu tiên load db của file có lỗi (VULNERABLE_o_NO.idb) vào IDA 7.x. Sau khi load xong, vào Edit > Plugins chọn BinExport:

Giao diện của BinExport sẽ xuất hiện, chọn BinExport v2 Binary Export:

Sau đó lưu lại với tên bất kì với phần mở rộng là .BinExport (ví dụ: VULNERABLE_o_NO.BinExport)

Tiếp theo, dùng IDA load db của file đã fix lỗi (NO_VULNERABLE.idb) và cũng thực hiện tương tự như trên, lưu thành file có tên là NO_VULNERABLE.BinExport:

Sau khi có hai file được export bằng BinExport, chạy trực tiếp Bindiff và load hai file này vào để so sánh:

Sau khi nhấn Diff thì bên tab Workspace sẽ hiển thị tên hai file được so sánh và tab Overview sẽ cung cấp kết quả so sánh:

Nhấp đúp vào tên hai file được so sánh tại tab Workspace, BinDiff sẽ hiển thị kết quả chi tiết:

Tiếp theo, chọn Matched Functions, sắp xếp lại theo mức độ tương đồng (Similarity):

Cuối cùng, nhấn chuột phải tại hàm có sự khác biệt, chọn Open Flow Graph và phân tích kết quả trả về:

2. Turbodiff

Công cụ cho phép so sánh binary tiếp theo là TurboDiff. Đây là một plugin được code bởi tác giả Nicolas Economou (@NicoEconomou), là đồng nghiệp trước đây với thầy Ricardo Narvaja tại Core Security. Các bạn có thể download plugin này tại: https://www.coresecurity.com/corelabs-research/open-source-tools/turbodiff, tuy nhiên đây là phiên bản cũ. Phiên bản mà thầy Ricardo Narvaja sử dụng là phiên bản mới hơn. Việc cài đặt rất dễ, chỉ việc chép file turbodiff.plw vào thư mục plugins của IDA là xong.

Tương tự như đã làm với BinDiff, nhưng khác chút là ta load database NO_VULNERABLE.idb của file đã fix lỗi trước. Sau khi IDA load xong, vào menu Edit > Plugins và chọn Turbodiff:

Để so sánh được thì TurboDiff cần phải lấy thông tin từ file .idb:

Lựa chọn “take info from this idb” và nhấn OK, turbodiff sẽ phân tích và tạo ra 2 file có đuôi mở rộng là .dis và .ana:

Sau đó, mở database của file có lỗi và cũng làm tương tự như trên:

Khi phân tích xong, ta tiếp tục chọn turbodiff một lần nữa, nhưng lần sẽ thực hiện so sánh bằng cách chọn “compare with…” và nhấn OK:

Chọn file cần so sánh là db của file đã được fix lỗi:

Giữ nguyên cấu hình mặc định của turbodiff và nhấn OK:

Ta sẽ có được kết quả như sau:

Tại tab “Turbodiff results” ta nhấn CTRL + F và tìm kiếm từ khóa là “changed” hoặc “suspicious” để hiển thị những chỗ thay đổi:

Sau khi có được kết quả như hình, nhấp đúp vào đó, turbo diff sẽ sử dụng wingrap32 để hiển thị flow graph:

Như các bạn thấy turbodiff đã cung cấp thông tin về khối lệnh có sự thay đổi. Tương tự như Bindiff, turbodiff cũng sử dụng một mã màu để biểu diễn cho tỉ lệ thay đổi, màu xanh lá cây được sử dụng cho các khối có những thay đổi ít, màu vàng sẽ dùng cho các khối có thay đổi nhiều và màu đỏ sử dụng cho các khối được thêm vào. Rõ ràng, về mặt đồ họa và khả năng tương tác thì không thể so sánh được với Bindiff, nhưng bù lại là tốc độ của Turbodiff thực sự rất nhanh. Hiệu năng là điều rất đáng quan tâm nếu áp dụng với các file có kích thước lớn và nó không hiển thị quá màu mè như Bindiff. Một điểm khác nữa với Bindiff là Turbodiff phải parse database của IDA thành các file trung gian rồi mới so sánh.

3. Diaphora

Công cụ so sánh binary cuối cùng mà tôi giới thiệu với các bạn là diaphora, đây là một plugin do Joxean Koret (@matalaz) viết bằng Python. Joxean Koret là tác giả của cuốn sách “The Antivirus Hacker’s Handbook” và hình như có quen biết với anh Quỳnh khi làm việc ở Coseinc. Để sử dụng được diaphora các bạn có thể download tại đây: https://github.com/joxeankoret/diaphora. Tính đến thời điểm hiện tại diaphora không còn hỗ trợ IDA 6.8 nữa, mà chỉ làm việc với IDA Pro 6.9, 6.95 và 7.0.

Tương tự như đã làm với các công cụ trước, đầu tiên ta load db của file đã fix lỗi vào IDA trước. Do là dạng python script, nên để chạy được diaphora ta vào File > Script File…

Tìm tới thư mục chứa file diaphora.py và lựa chọn file này để chạy:

Màn hình Diaphora sẽ xuất hiện, ta có thể lựa chọn đường dẫn để lưu file SQLite db cho file hiện tại đang phân tích bởi IDA hoặc giữ nguyên đường dẫn mà Diaphora thiết lập:

Sau đó nhấn OK để Diaphora tiến hành công việc phân tích và lưu thành file sqlite:

Thực hiện tương tự với file có lỗi:

Sau khi thực hiện xong hai bước trên ta sẽ có được 2 file sqlite để phục vụ cho việc so sánh. Tiếp theo, ta mở lại diaphora.py (lúc này IDA đang mở db của file có lỗi):

Ở phần “SQLite database to diff against”, ta tìm đến file sqllite được tạo ra trước đó của file đã fix lỗi. Chọn file này và nhấn Open:

Cuối cùng nhấn OK để Diaphora tiến hành xử lý vá so sánh. Diaphora sẽ hỏi như hình dưới, chỉ việc nhấn Yes là xong:

Sau khi compare xong, diaphora sẽ hiển thị 2 tab mới tại IDA với tên là Best matchesPartial matches. Tại tab Best matches là các hàm giống nhau hoàn toàn và không thay đổi, nên ta không cần quan tâm tới tab này:

Tab Partial matches sẽ cung cấp cho chúng ta thông tin về các hàm có khả năng khác nhau:

Ta thấy diaphora đã tìm được hai hàm bị thay đổi như trên. Về cơ bản, chủ quan mà nói tôi thấy diaphora cho kết quả hên xui so với Bindiff và Turbodiff, nếu chạy với IDA7 thì không ra được Partial maches, chạy với IDA 6.8 thì mới ra được kết quả mong muốn. Ngoài ra, do được viết bằng python nên diaphora chạy khá lâu, theo tôi thì nó chạy lâu nhất, gặp phải db lớn là xác định ngồi đợi. Để xem được sự thay đổi ở một hàm nào đó, nhấn chuột phải tại hàm đó và chọn chế độ Diff assembly in a graph:

Kết quả có được như hình dưới đây:

Chế độ graph này của diaphora nhìn có vẻ tốt hơn Turbodiff một chút, nhưng cũng không cho ta khả năng tương tác như ở Bindiff, khối quan trọng đã thay đổi được highlight bằng màu đỏ, còn màu vàng là khối có những thay đổi nhỏ, ví dụ như ta đổi tên của một biến. Ngoài ra, diaphora còn cung cấp tùy chọn khác là Diff pseudo code, sử dụng Hexrays decompiler đi kèm với IDA để xây dựng lại mã nguồn của file thực thi. Kết quả có được khi lựa chọn tùy chọn này như hình dưới đây:

Phần 22 đến đây là kết thúc nhé các bạn!

Image result for diff funny

Trong phần này tôi cung cấp một file IDA1.exe (https://mega.nz/#!mHAU1AaS!Zm2g_pXoiCDWiKwSdRJsJ0tYNTRjc6JSkBJyr6h7j0g). Tôi muốn các bạn phân tích nó và xem liệu nó có lỗi hay không và nếu có thì bạn thực hiện khai thác buffer overflow làm sao để thay đổi luồng thực thi của chương trình để từ đó hiển thị thông báo You are a winner man.

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

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 20, tôi đã đưa ra một bài tập nhỏ để các bạn phân tích xem chương trình có khả năng bị vuln hay là không. Trong phần 21 này, tôi sẽ cùng với các bạn tìm hiểu, phân tích và đưa ra câu trả lời. Mã nguồn gốc của chương trình được thầy Ricardo Narvaja cung cấp lại dưới đây để các bạn có hình dung rõ ràng hơn về chương trình. Các file .idb và file thực thi để bạn có thể reverse bằng IDA và thực hiện debug nếu cần có thể download tại: https://mega.nz/#!uXQExSYD!6GT0LsA803BVGoXp82muCnk-GsCEGBEuT7BZwrhSBBA

Đây là toàn bộ mã nguồn của chương trình:

Giống như ở các phần trước, trước tiên ta sẽ tiến hành việc phân tích tĩnh chương trình bằng IDA. Sau khi load vào IDA, ta dừng lại tại hàm main(). Tại đây, giống như phân tích ở phần trước, ta đã biết được nhiệm vụ của biến var_4, do đó đổi tên nó thành CANARY như đã làm.

Nhấp đúp vào biến Buf, IDA sẽ chuyển tới cửa sổ biểu diễn Stack của hàm main(). Tiếp theo, chuột phải tại biến Buf và chọn Array:

IDA tự động nhận diện được kích thước của buffer là 16 bytes. Nhấn OK để chấp nhận, ta có được Stack layout của hàm main() như sau:

Theo lý thuyết đã tìm hiểu, nếu như chúng ta có thể ghi dữ liệu vào buffer này nhiều hơn 16 bytes thì chương trình sẽ có khả năng bị lỗi. Các bạn đừng bị nhầm lẫn giữa các lỗi phổ biến hoặc lỗi crash chương trình với các lỗ hổng, không phải cứ gây crash cho một chương trình thì khẳng định ngay đó là lỗ hổng. Ví dụ, tôi có các câu lệnh sau:

XOR ECX, ECX
DIV ECX 

Đây là một lỗi vì ta biết câu lệnh XOR sẽ xóa thanh ghi ECX hay cụ thể hơn sau lệnh XOR thì ECX sẽ bằng 0x0. Khi thực hiện câu lệnh DIV ECX, tức là thực hiện phép chia cho số 0, điều này sẽ gây ra một ngoại lệ (exception), nếu exception này không được xử lý thì sẽ gây crash chương trình. Như đã nói, có nhiều loại lỗ hổng khác nhau và ở bài viết này tôi chỉ tập trung vào một lỗi đơn giản là Buffer Overflow.

Khi xảy ra tràn tại một Buffer của chương trình tức là ta có thể ghi dữ liệu vượt ra ngoài không gian dành riêng cho Buffer đó, chương trình lúc đó sẽ được xem là Vulnerable bởi vì lỗi Buffer Overflow có thể xảy ra. Tiếp theo, ta sẽ phải kiểm tra xem bên cạnh việc chương trình có Vulnerable thì có thể Exploitable hay không? Vì nhiều lúc chương trình có thể có Vulnerable nhưng lại không thể Exploitable bởi chương trình đã có biện pháp phòng tránh hoặc các cơ chế mitigation của hệ thống như Stack Canary giúp ngăn chặn việc khai thác lổ hổng của chương trình. Tuy nhiên, đó sẽ một chủ đề khác ở các bài viết tiếp theo.

Vì vậy, ý tưởng ở đây là ta sẽ phân tích nếu buffer (cụ thể ở ví dụ này) với kích thước 16-bytes mà bị tràn, bên dưới nó sẽ là biến CANARY, nếu ta ghi đè được dữ liệu lên biến này khi đẩy dữ liệu vào Buf thì rõ ràng là sẽ có lỗi Buffer Overflow.

Tại IDA, bên dưới lệnh in ra màn hình chuỗi “Please Enter Your Number Of Choice” ta bắt gặp khối lệnh sau:

Toàn bộ đoạn code asm trên hình tương ứng với câu lệnh sau trong mã nguồn gốc:

while ((c = getchar()) != '\n' && c != EOF);

Lệnh này thường hay được sử dụng sau hàm scanf, sử dụng hàm getchar() đọc để đọc giá trị 0xA (là mã ASCII tương ứng với New line – line break (ngắt dòng)) từ standard input (do người dùng nhập vào). Tóm lại, ở đây hàm getchar() sẽ trả về mã phím do người dùng ấn, ‘\n’ sẽ tương ứng với việc người dùng nhấn phím Enter. Vòng lặp while sẽ kiểm tra phím người dùng nhập vào, nếu nhập vào Enter thì thoát vòng lặp, còn nhập vào phím khác là nó quay lại vòng lặp. Về bản chất khi lập trình thì người ta có thể ngầm hiểu việc nhấn phím “Enter” là sự kết hợp của hai mã ASCII là 0x13 (carriage return)0xA (line feed).

Bảng mã các kí tự ASCII (không thể in ra màn hình):
Ascii code 00 = NULL (null character)
Ascii code 01 = SOH (Header start)
Ascii code 02 = STX (Start of text)
Code ascii 03 = ETX (End of text, heart stick English poker cards)
Code ascii 04 = EOT (End of transmission, stick diamonds decks of poker)
Code ascii 05 = ENQ (Consult, stick clubs English poker cards)
Ascii code 06 = ACK (Recognition, stick cards poker cards)
Ascii code 07 = BEL (Ring)
Ascii code 08 = BS (Backspace)
Ascii code 09 = HT (horizontal Tab)
Ascii code 10 = LF (New line - line break)
Ascii code 11 = VT (Vertical Tab)
Ascii code 12 = FF (New page - page break)
Ascii code 13 = CR (ENTER - carriage return)

Để tiện cho việc kiểm tra chương trình, ở đây chúng ta sẽ sử dụng một đoạn python script như sau:

from subprocess import *
import time
p = Popen([r'VULNERABLE_o_NO.exe','f'],stdout=PIPE,stdin=PIPE, stderr=STDOUT)

print "Attach to the debugger and press Enter\n"
raw_input()

size="10\n"
p.stdin.write(size)

time.sleep(0.5)

user_input="AAAA\n"
p.stdin.write(user_input)

testresult = p.communicate()[0]
time.sleep(0.5)
print(testresult)
print size
print user_input

Script trên sử dụng module của python là subprocess để tương tác, quản lý process. Chi tiết về module này có thể xem tại đây: https://docs.python.org/2/library/subprocess.html. Đầu tiên script sử dụng Popen để khởi động process:

p = Popen([r'VULNERABLE_o_NO.exe','f'],stdout=PIPE,stdin=PIPE, stderr=STDOUT)

Kèm theo đó nó thực hiện chuyển hướng stdinstdout để từ đó ta có thể gửi các kí tự cho process này như thể là ta thực hiện nhập dữ liệu trên màn hình. Sau đó, ta sử dụng hàm raw_input() để sau khi process khởi động xong thì script sẽ dừng lại cho đến khi nhấn phím Enter. Việc này cho phép ta có thể dùng IDA để attach process, qua đó sẽ chờ thông tin nhập vào qua stdin.

Tiếp theo, script sẽ tự động thực hiện việc truyền dữ liệu cho process:

size="10\n"
p.stdin.write(size)

time.sleep(0.5)

user_input="AAAA\n"
p.stdin.write(user_input)

Trong mã nguồn gốc của chương trình ta thấy sẽ có hai lần nó yêu cầu người dùng nhập dữ liệu, vì vậy tại python script ta cũng thực hiện công việc tương tự. Đầu tiên, chương trình yêu cầu nhập vào một số tùy chọn và gán cho biến size, do đó trong script tôi truyền vào số 10. Tiếp theo, chương trình sử dụng gets_s() để nhận dữ liệu người dùng nhập vào với kích thước mà ta đã nhập trước đó, ở đây tôi truyền vào 4 chữ A, nhỏ hơn so với kích thước đã nhập.

Kiểm tra thử xem script có hoạt động đúng như chúng ta nghĩ hay không? Chạy thử script, nó sẽ dừng lại tại thông báo “Attach to the debugger and press Enter” để chờ ta nhấn Enter, như vậy ta sẽ có thể attach nó vào IDA để debug:

Hiện tại, ta vẫn đang mở file VULNERABLE_o_NO.exe để phân tích nó bằng Loader của IDA, do vậy để có thể attach được process thì chọn

sau đó vào menu Debugger và chọn Attach to process. Cửa sổ Choose process to attach to sẽ xuất hiện tương tự như sau:

Cuộn chuột xuống dưới để tìm tới process của chúng ta:

Chọn process cần attach và nhấn OK. Sau khi IDA attach xong sẽ dừng lại, lúc đó ta nhấn F9 để cho process tiếp tục thực thi. Sau đó, trước khi nhấn Enter tại màn hình thực thi python script, ta sẽ đặt một Breakpoint sau lệnh scanf_s, vì khi ta nhấn Enter thì script sẽ tự động truyền các input mà process yêu cầu. Đặt breakpoint tương tự như hình dưới:

Sau khi đặt bp xong, quay lại màn hình thực thi script và nhấn Enter. Lúc đó tại IDA sẽ break tại bp ta vừa đặt:

Với việc break tại breakpoint như trên, ta hi vọng biến Size lúc này sẽ chứa giá trị là 10 như ta đã thực hiện trong script. Để kiểm tra ta di chuyển chuột và trỏ vào biến Size, IDA sẽ cung cấp thông tin như sau:

Như vậy, ta thấy rằng biến Size đã nhận giá trị là 0xA (tương đương với 10 ở hệ thập phân) đúng như giá trị đã truyền vào thông qua script. Nên nhớ rằng khi làm việc với disassembler/debugger thì các số thập phân đều được chuyển đổi sang hệ hexa.

Nhấn F8 để trace tới hàm getchar(), các bạn sẽ thấy bên dưới có một lệnh so sánh thanh ghi eax (lưu kết quả trả về của hàm getchar()) với 0xA, mà thực tế là ta không hề truyền bất kỳ kí tự 0xA nào thông qua script cả.

Quay lại một chút các lệnh trong python script:

size="10\n"
p.stdin.write(size)

time.sleep(0.5)

user_input="AAAA\n"
p.stdin.write(user_input)

Nếu các bạn để ý thì với escape characternewline (\n) trong script sẽ tương ứng với ascii code là 10 (0xA) – Line Feed và CarriageReturn (\r)13 (0xD). Do đó, khi nhấn F8 để trace qua hàm getchar() và quan sát giá trị của thanh ghi eax, ta sẽ thấy kết quả như sau:

Như vậy ta thấy rằng thanh ghi eax sẽ lưu ascii code trả về từ hàm getchar()0xA. Do đó, chương trình ngầm hiểu rằng người dùng đã nhấn phím Enter nên sau khi so sánh sẽ thoát khỏi vòng lặp. Mục đích của đoạn code trên là khi đọc ra giá trị 0xA sẽ thực hiện loại bỏ nó ra khỏi stdin và xóa nó để phục vụ cho việc nhập dữ liệu tiếp theo từ bàn phím.

Sau khi thoát khỏi vòng lặp ta sẽ tới đoạn code so sánh kích thước ta truyền cho biến Size0xA với kích thước tối đa mà buffer có thể chấp nhận là 0x10. Và do lúc này giá trị của biến Size nhỏ hơn nên chương trình sẽ tiếp tục đi tới nhánh gọi hàm gets_s():

Tiếp tục trace bằng F8 qua lời gọi hàm gets_s():

Di chuyển chuột và lựa chọn biến Buf để kiểm tra xem sau khi thực hiện hàm gets_s() thì biến này đang lưu gì. Ta có kết quả như sau:

Ta thấy rằng biến Buf lúc này đang lưu các chữ cái ‘A’ mà đã truyền vào thông qua script. Như vậy, script đã hoạt động rất tốt, qua đó cho phép ta có thể kiểm tra và debug những gì sẽ xảy ra trong chương trình. Tạm thời dừng tại đây chút. Bây giờ nếu tôi cho thực thi lại script và làm lại công việc trên từ đầu, tuy nhiên khi tới lời gọi tới hàm getchar() thì tôi bỏ qua nó bằng cách thay đổi giá trị của thanh ghi EIP để trỏ tới lệnh khác và bỏ qua đoạn đọc ra 0xA để so sánh:

Khi ta thay đổi EIP như trên thì lúc đó chương trình sẽ không thực hiện lọc ascii code 0xA nữa. Vậy ta sẽ xem điều gì xảy ra nếu bỏ qua đoạn code đó?

Trace code bằng F8 qua lời gọi hàm gets_s(), khi quan sát nội dung của biến Buf thì ta thấy rằng nó không có thông tin gì cả, như kiểu là ta chẳng nhập gì cho chương trình (mặc dù rõ ràng script không hề thay đổi):

Điều này dẫn tới kết luận là kí tự 0xA vẫn còn trong stdin phải được lọc sau khi thực hiện nhận user input để nó không làm ảnh hưởng tới các đoạn code xử lý tiếp theo trong chương trình nếu muốn tiếp tục nhận user input, đó là lý do tại sao phải có dòng lệnh này:

while ((c = getchar()) != '\n' && c != EOF);

OK, quay trở lại nội dung chính, hiện ta đã có được script phục vụ việc test chương trình. Bây giờ ta sẽ chỉnh sửa script này một chút để có thể phân tích được crash xảy ra tại gets_s() khi chúng ta nhập vào kích thước tối đa, qua đó sẽ phân tích xem có điều gì khác lạ nữa không hay chỉ đơn giản là một crash bình thường. Tôi sửa lại script như sau:

size="16\n"
p.stdin.write(size)

time.sleep(0.5)

user_input="A"*16 + "\n"
p.stdin.write(user_input)

Ta sẽ xem điều gì sẽ xảy ra khi thay đổi lại script như trên. Thực hiện lại tương tự như trên, ta trace tới lệnh lệnh lea lấy địa chỉ của biến Buf:

Khi trace qua lệnh LEA, tôi ghi lại địa chỉ của biến Buf, trên máy của tôi lúc này là: 0x0115F9D4.

Tiếp theo, nhấp đúp chuột vào biến CANARY và nhấn phím D nhiều lần cho đến khi chuyển nó thành một dword (dd). Ta ghi lại địa chỉ của biến này, trên máy của tôi là 0x0115F9E4 và giá trị của CANARY lúc này trên máy tôi là 0x816077B5h:

Sau khi lưu lại các thông tin liên quan, nhấn F9 để thực thi:

Đây là một exception do API sinh ra, nhấn OK để chấp nhận.

Tại đây, ta nhấn G và nhập vào địa chỉ của biến BufferCANARY để kiểm tra xem có thay đổi gì với những biến này không. Kết quả ta thấy ta thấy rằng CANARY vẫn giữ nguyên giá trị ban đầu còn Buf được điền đấy các chữ cái ‘A’:

Như vậy, với việc nhập giá trị vào bằng với kích thước tối đa sẽ không gây ra tràn, một điều chắc chắn là vùng buffer lúc này đã được điền đầy đủ và không có kí tự null (zero) cuối cùng của chuỗi, vì thế có thể gây ra vấn đề nếu chương trình tiếp tục chạy. Tuy nhiên, trong trường hợp này khi có exception (ngoại lệ) thì exception đó đã không được kiểm soát, vậy nên chương trình bị close. Do đó, ở đây chỉ đơn giản là một crash (tôi cũng nghĩ rằng nó đặt một số không ở đầu của bộ đệm nhằm để hủy bỏ chuỗi).

Nếu chương trình có xử lý ngoại lệ và tiếp tục, nó sẽ loại bỏ dữ liệu khỏi bộ đệm vì nếu nó lấy dữ liệu đó và sử dụng như là một chuỗi, nhưng lại không có null byte để báo hiệu kết thúc chuỗi, thì có thể sẽ nối thêm vào dữ liệu tiếp theo trong ngăn xếp và gây ra vấn đề. Nhưng bằng cách thiết lập giá trị 0 ở đầu của bộ đệm thì sẽ làm mất hiệu lực của dữ liệu.

Như vậy, ta biết rằng sẽ không xảy ra tràn ở đây, ta quay trở lại với việc phân tích tĩnh. Hãy tập trung vào đoạn code này:

Như đã đọc ở các phần trước, ta biết rằng lệnh nhảy JL hoặc JLE sẽ xem xét vào số có dấu hoặc EAX có thể là số âm. Ví dụ, nếu EAX có giá trị là 0xFFFFFFFF (tức là -1) thì nó sẽ nhỏ hơn 0x10. Như vậy là nếu ta sửa lại script để truyền vào biến Size giá trị -1 thì có thể sẽ vượt qua được đoạn so sánh ở trên. Script được sửa lại như sau:

size="-1\n"
p.stdin.write(size)
time.sleep(0.5)

user_input="A" *0x2000 + "\n"
p.stdin.write(user_input)

Chạy lại script với các giá trị đã thay đổi. Ta dừng lại tại breakpoint đã đặt trong IDA:

Lúc này ta kiểm tra giá trị của biến Size xem thay đổi có đúng mong muốn không:

Ta thấy rằng nó chứa giá trị là 0xFFFFFFFF, nhấp đúp chuột vào biến này và nhấn D để nhóm lại cho đến khi trở thành một dword:

Tiếp theo, ta trace code cho tới đoạn thực hiện so sánh với giá trị maximum:

Rõ ràng nếu xem 0xFFFFFFFF-1, thì khi thực hiện câu lệnh so sánh và xem xét dấu, cờ SF lúc này sẽ được bật và như vậy ta sẽ vẫn đi tới đoạn code thực hiện hàm gets_s(). Thực tế thì hàm gets_s cũng giống như là memcpy và bất kỳ hàm API nào thực hiện việc sao chép hoặc cấp phát, kích thước truyền vào cho hàm sẽ luôn được hiểu như là số không dấu (unsigned), bởi vì đơn giản là không có kích thước âm, điều đó là không thể xảy ra, do vậy chương trình lúc này sẽ hiểu 0xFFFFFFFF là một số dương cực lớn được truyền cho hàm gets_s(), do vậy sẽ có tràn xảy ra.

Ta ghi lại địa chỉ của buffer, lần này trên máy tôi nó được cấp phát tại 0x004CFAF8.

Sau đó ta nhấn F9 để thực thi, chương trình sẽ dừng tại đây trong IDA:

Đi tới địa chỉ của buffer, kết quả có được như sau:

Tại đầu của buffer, ta nhấn phím ‘A’ nhằm chuyển toàn bộ kí tự đơn lẻ này thành chuỗi để dễ nhìn hơn:

Nhìn trông ngon lành hơn nhiều lolz. Tiếp theo ta chuyển nó thành dạng Array:

Ta thấy rằng với số lượng kí tự ghi vào buffer như trên đã chiếm hết toàn bộ Stack, đồng thời ghi đè lên cả biến CANARY cũng như các giá trị khác. Như trong hình, bên dưới Stack là một section khác có tên là debug024 (trên máy của tôi):

Bằng cách trên, ta đã xác minh được rằng chương trình này có vulnerable. Giờ làm cách nào để sửa được lỗi này? Rõ ràng là, thay vì sử dụng các lệnh nhảy như JL hoặc JLE (căn cứ vào dấu) thì ta cần sử dụng lệnh nhảy JB hoặc JBE, vì các lệnh này không quan tâm nếu ta truyền vào là -1, nó sẽ là 0xFFFFFFFF, nhưng khi tới lệnh so sánh nó sẽ được xem là số dương và như vậy sẽ lớn hơn 0x10. Khi lớn hơn giá trị maximum của buffer thì sẽ thoát chương trình.

Do vậy, ta sửa lại mã nguồn của chương trình như sau:

Ta thay đổi lại khai báo kiểu của biến size thành “unsigned”, qua đó chương trình từ có Vulnerable sẽ thành Not Vulnerable. Sửa xong code, biên dịch lại chương trình và lưu với tên mới là NO_VULNERABLE.exe. Sau đó load file đã sửa vào IDA, ta thấy lệnh nhảy bây giờ đã thay đổi như hình dưới:

Như các bạn thấy, chỉ cần thay đổi kiểu biến thành “unsigned”, sau khi biên dịch, lệnh nhảy sẽ được đổi sang kiểu lệnh nhảy không còn quan tâm đến dấu của số như ban đầu nữa. Tiếp theo, ta sửa lại script chỉ đơn giản bằng cách đổi tên của file thực thi, lúc này là NO_VULNERABLE.exe, các lệnh khác trong script giữ nguyên không thay đổi.

Thực hiện toàn bộ quá trình đã làm như trên bằng cách cho chạy script và attach process vào IDA, sau đó ta trace code tới câu lệnh so sánh để xem điều gì xảy ra. Ta thấy biến Size lúc này vẫn giữ giá trị là 0xFFFFFFFF do script truyền vào:

Nhấn F8 để trace qua lệnh so sánh và dừng tại lệnh nhảy:

Ta thấy lúc này mũi tên màu đỏ sẽ nhấp nháy báo hiệu chương trình sẽ rẽ nhánh sang khối lệnh gọi hàm exit() để thoát chương trình và như vậy sẽ tránh được tràn, đó là vì lệnh nhảy JBE xem xét rằng giá trị 0xFFFFFFFF (vì không quan tâm đến dấu) là một số dương rất lớn, do vậy sẽ lớn hơn giá trị 0x10.

Kết luận, câu trả lời cho bài tập này là đây là một chương trình có Vulnerable và nó đã được sửa lại bằng cách thay đổi kiểu của biến size thành “unsigned int size”, từ đó sau khi biên dịch lại lệnh nhảy JLE được thay bởi lệnh nhảy JBE.

Phần 21 kết thúc tại đây, hẹn gặp lại các bạn ở phần 22!

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


Vulnerabilities

Trong phần này, chúng ta sẽ tìm hiểu về các lỗ hổng bảo mật và cách phân tích một số lỗi dễ khai thác nhất.

Các ví dụ minh họa trong bài viết download tại đây: https://mega.nz/#!SOAzVCCK!rPFdjLSpDGFlNCho-xkkcWOj0Sy3GbEzShHwGtiqwCE

Lỗ hổng là gì?

Trong lĩnh vực bảo mật máy tính, từ “vulnerability” ám chỉ đến điểm yếu trong một hệ thống cho phép kẻ tấn công có thể xâm phạm tính bảo mật, tính toàn vẹn, tính sẵn sàng của hệ thống đó, từ đó có thể kiểm soát truy cập và tính nhất quán của hệ thống hoặc dữ liệu và các ứng dụng.

Vulnerabilities” là kết quả của lỗi hoặc tính toán sai trong thiết kế của một hệ thống. Mặc dù, xét theo nghĩa rộng hơn, chúng cũng có thể là kết quả của những hạn chế về mặt công nghệ, bởi vì, về nguyên tắc, không có hệ thống nào là an toàn tuyệt đối 100%. Do đó, sẽ có những lỗ hổng trên lý thuyết và các lỗ hổng thực tế có thể bị khai thác.

Quy tắc tương tự áp dụng cho các chương trình. Một chương trình có khả năng bị khai thác lổ hổng là chương trình có lỗi và tùy thuộc vào loại lỗi, chúng có thể bị khai thác để thực thi mã khai thác trong chương trình đó. Bên cạnh đó, các cơ chế kiểm soát của chương trình cũng có thể bị lỗi và cho phép người sử dụng thực hiện các hành động bất hợp pháp, gây crash cho chương trình hoặc thông qua đó có thể nâng quyền, v…v…

Tất nhiên, ở mức độ lỗi liên quan đến bộ nhớ thì đơn giản nhất là tràn bộ đệm. Lỗi này xảy ra khi một chương trình dành ra một vùng nhớ hay vùng đệm để phục vụ lưu trữ dữ liệu và vì lý do nào đó kích thước của dữ liệu được sao chép vào vùng nhớ này không được kiểm tra đúng đắn, dẫn đến lỗi tràn bộ đệm xảy ra, sao chép dữ liệu vượt quá kích thước dành riêng, dẫn tới có thể ghi đè lên các biến, các tham số và các con trỏ trong bộ nhớ. Một trong những lỗi tràn bộ đệm kinh điển nhất là stack overflow (tràn ngăn xếp), xảy ra khi có tràn bộ đệm trong ngăn xếp. Trong mã nguồn của một chương trình C, một buffer có thể được khai báo đơn giản như sau:

char buf[xxx];

Trong đó xxx là kích thước của bộ đệm. Ở ví dụ dưới đây, buffer được khai báo có kích thước là 0x300 bytes (vì là kiểu char), như vậy trong ngăn xếp sẽ dành ra cho buffer này 0x300 bytes để lưu dữ liệu.

Ngoài việc khai báo một buffer thì đoạn code trên không thực hiện thêm công việc nào khác, nhưng tôi vẫn thực hiện biên dịch và phân tích nó trong IDA. File này có tên là Compiled_1.exe (Compilado_1.exe). Sau khi load vào IDA, tại cửa sổ Names, thực hiện tìm kiếm các tham số argc hoặc argv mà hàm main() sử dụng. Bằng cách này ta sẽ tìm được hàm main() của chương trình, kết quả có được như sau:

Nhấp đúp vào một trong hai kết quả được đánh dấu trên hình, IDA sẽ đưa ta tới đây:

Tiếp tục nhấp đúp chuột vào thông tin tại DATA XREF được highlight như trên hình:

Sử dụng tính năng xrefs của IDA, chúng ta sẽ đến với khối lệnh gọi tới hàm main (call sub_401000) của chương trình:

Đi vào hàm sub_401000(), ta thấy trong code của hàm không dành ra bất kì không gian nào cho biến buf bởi vì rõ ràng ở code của chương trình không hề sử dụng tới buffer này, nó chỉ thực hiện thiết lập thanh ghi EAX = 0 tương đương với câu lệnh return 0 trong hàm main().

Để chương trình thực hiện việc dành ra không gian cho bộ đệm thì trong hàm main() chúng ta buộc phải sử dụng tới buffer đã khai báo này. Do đó, thực hiện sửa lại code của chương trình như sau:

Ở đây tôi sử dụng hàm gets_s() để nhận thông tin mà người dùng gõ tại màn hình console và lưu thông tin đó vào trong bộ đệm buf mà chúng ta đã khai báo trong chương trình. Dữ liệu lưu tại buf có thể được sử dụng sau này.

Như trên hình, ta thấy hàm gets_s() nhận hai tham số truyền vào: một buffer để lưu input từ người dùng và số kí tự tối đa mà người dùng có thể nhập nhằm tránh không bị tràn (overflow). Như vậy, trong ví dụ này rõ ràng là không có overflow xảy ra, vì kích thước được sao chép không được lớn hơn 0x300 bytes của buffer đã tạo.

Căn cứ vào đoạn code trên thì bạn có thể thấy rằng sẽ không có khả năng để xảy ra overflow, vì dữ liệu được phép copy vào biến buf chỉ nhỏ hơn hoặc bằng 0x300 bytes mà thôi. Hãy phân tích thử trong IDA xem thế nào, file này có tên là Compiled_2.exe (Compilado_2.exe). Nếu được load kèm theo pdb file thì kết quả tại IDA sẽ tương tự như sau:

Với việc load kèm file pdb như vậy thì IDA sẽ nhận diện được các tham số và các biến tốt hơn nhiều. Chúng ta hãy phân tích một chút trong IDA, nhấp đúp vào một biến hoặc một tham số bất kỳ ta sẽ chuyển tới cửa sổ cung cấp thông tin về Stack của hàm main():

Tại đây, ta thấy ba tham số chính của hàm main()envp, argvargc, tuy nhiên trong thân của hàm main ta không sử dụng tới bất kì tham số nào. Các tham số này được truyền vào hàm main thông qua các câu lệnh push, trước khi chương trình có lời gọi thực sự tới hàm main().

Như vậy, với các lệnh push này sẽ truyền ba tham số vào Stack trước khi thực hiện lệnh CALL. Ta biết rằng, trước khi thực hiện hàm thì chương trình cũng sẽ lưu địa chỉ trở về (return address) vào Stack, mục đích của việc làm như vậy là để chương trình biết nơi mà nó sẽ trở về sau khi thoát khỏi hàm. Trong ví dụ này, địa chỉ trở về sẽ luôn có giá trị cố định là 0x401200 (nếu như ta không sử dụng cơ chế bảo vệ ASLR).

Như vậy, chương trình sẽ quay trở về địa chỉ này sau khi thực hiện xong hàm main(). Vì vậy, phải lưu lại giá trị 0x401200 (r) vào stack và nó nằm ở trên 3 tham số của hàm main():

Sau khi thực hiện truyền tham số cho main() và lưu lại địa chỉ trở về, chương trình bắt đầu thực hiện hàm main(). Việc đầu tiên mà tại mỗi hàm luôn thực hiện chính là lệnh PUSH EBP.

Lệnh này sẽ lưu vào stack giá trị của thanh ghi EBP, giá trị này đang được sử dụng bởi hàm đã gọi hàm main(). Do đó, giá trị này sẽ nằm ngay phía trên địa chỉ trở về là 0x401200. Chúng ta không biết được giá trị này bởi vì nó sẽ thay đổi khi thực thi chương trình. Ta chỉ cần nhớ rằng câu lệnh này sẽ lưu lại giá trị EBP của hàm gọi (caller) lên stack và nó sẽ nằm ngay trên địa chỉ trở về (r), hay ngắn ngọn hơn là lưu lại old frame vào Stack.

Câu lệnh tiếp theo được thực hiện là:

Lệnh này làm nhiệm vụ thiết lập lại thanh ghi EBP trở thành Base (cơ sở) của hàm main(), bằng cách gán nó bằng với ESP. Lệnh mov ở đây chỉ thay đổi giá trị của EBP, không tác động gì tới ngăn xếp. Sau khi thiết lập xong EBP, hàm main thực hiện lệnh sub esp, 0x304, lệnh này có nhiệm vụ di chuyển thanh ghi ESP để dành ra không gian cho các biến cục bộ và bộ đệm trong ngăn xếp. Các biến cục bộ và các buffer này sẽ nằm phía trên giá trị EBP vừa được lưu.

Như trên hình ta thấy không gian dành cho các biến cục bộ và bộ đệm sẽ nằm ngay phía trên s (Stored EBP). Tại hàm main(), ta sẽ thấy biến đầu tiên hầu như luôn luôn xuất hiện nếu chương trình áp dụng cơ chế bảo vệ Stack (stack canary), trong trường hợp này nó được đặt tên là var_4:

Như quan sát trong đoạn code trên, ta thấy nó đọc giá trị của __security cookie và lưu vào thanh ghieax, đây là một giá trị ngẫu nhiên, được sinh ra khác nhau mỗi khi chương trình được thực thi. Sau đó, nó được đem xor với EBP và lưu lại vào biến var_4 như chúng ta đã thấy. Do đó, ta đổi tên nó thành CANARY.

Tiếp theo, ta quan sát và thấy ở trên biến CANARY này là biến Buf. Kiểm tra thông tin của Buf tại cửa số Stack của hàm main().

Một kinh nghiệm nhỏ khi phân tích các biến của một hàm đó là, nếu ta thấy bên dưới của một biến là không gian trống liên tục (gồm toàn các db ? tương tự như trên hình) tại cửa sổ Stack của hàm, thì rất có thể đó là một buffer. Do đó, tôi nhấn phải chuột tại Buf và chọn Array:

Ta thấy IDA tự động tính toán được kích thước của mảng này là 768 (ở hệ thập phân), và mỗi phần tử của mảng có kích thước là 1 byte. Nếu ta chuyển đổi giá trị 768 sang hệ hexadecimal thì sẽ chính là 0x300. Ta nhấn OK để chấp nhận thông tin mà IDA đã cung cấp. Như vậy, ta đã có được biến Buf như trong mã nguồn gốc của chương trình, được định nghĩa là một bộ đệm có kích thước 0x300 bytes ở hệ hexa hay 768 ở hệ thập phân.

Tiếp theo, bên dưới ta sẽ thấy lời gọi tới hàm gets_s(), hàm này nhận hai tham số truyền vào như đã phân tích ở trên. Trong đó, tham số thứ hai là kích thước tối đa, ở đây là 0x300 và tham số thứ nhất là địa chỉ của buffer, địa chỉ này có được thông qua lệnh lea:

Với thông tin như vậy ta đã xác minh được rằng kích thước của Buf0x300 bytes và thông qua hàmgets_s() thì dữ liệu sao chép vào buffer này tối đa là 0x300. Như vậy, rõ ràng là nếu chúng ta có thể làm tràn bộ đệm này bằng cách ghi vào đó nhiều hơn 0x300 bytes, thì dữ liệu ghi vào sẽ ghi đè lên biến CANARY, giá trị của EBP (đã lưu bằng lệnh push ebp) và địa chỉ trở về r (return address) nằm ngay bên dưới buffer:

Tuy nhiên, ở ví dụ này thì ta sẽ không thể thực hiện overflow được vì code của chương trình đã kiểm soát tốt vùng buffer. Ta xem xét một ví dụ khác như sau:

Trong ví dụ tiếp theo này, chương trình yêu cầu người dùng nhập vào một số tùy ý và số đó sẽ tương ứng với kích thước tối đa của dữ liệu được phép sao chép vào buffer. Nếu như kích thước này được kiểm tra kĩ lưỡng thì chương trình sẽ tránh được lỗi tràn bộ đệm. Nếu ta thay lại kích thước của biến buf trên thành 0x10 byte hay tương ứng với 16 ở hệ thập phân và yêu cầu người dùng nhập vào kích thước của buffer, kích thước này sau đó sẽ được sử dụng trong hàm gets_s.

Tuy nhiên, trong code của chương trình lại không hề kiểm tra giá trị kích thước tối đa mà buf có thể chấp nhận, tức là kiểm tra giá trị nhập vào có lớn hơn kích thước khai báo của buf hay không. Nếu ta biên dịch chương trình và thực thi để kiểm tra (Compiled_3.exe (Compilado_3.exe)):

Có lỗi đã xảy ra và như vậy khả năng chương trình có thể có vuln. Thử load chương trình vào trong IDA (ngay cả khi chúng ta không có mã nguồn của nó), ta sẽ thấy code như sau:

Giống như ví dụ trước, ta đã biết được đâu là biến CANARY, ở ngay phía trên của CANARYBuf. Chiều dài hay kích thước của buffer là 16 byte tương ứng với 0x10 ở hệ hexa:

Vì vậy, nếu ta có thể sao chép nhiều hơn 16 bytes vào bộ đệm thì sẽ xảy ra overflow và dữ liệu sẽ được ghi đè lên các giá trị CANARY, Stored ebpReturn Address:

Ta xem xét tiếp biến Size:

Sau khi in ra màn hình thông báo “Please Enter Your Number” bằng hàm printf(),chương trình sử dụng hàm scanf_s() để đọc dữ liệu mà người dùng nhập vào từ bàn phím và lưu giá trị nhập vào đó vào biến Size. Đây là một biến có kích thước dword và địa chỉ của biến được lấy ra bằng lệnh LEA vào thành ghi eax như các bạn đã thấy trên hình.

Thông tin về hàm scanf_s() trên MSDN như sau:

Như vậy, hàm scanf_s() chịu trách nhiệm đọc dữ liệu từ stdin (standard input stream) và ghi dữ liệu vào vị trí được cung cấp bởi tham số truyền vào. Mỗi tham số của hàm này phải là một con trỏ tới một biến có kiểu tương ứng với kiểu dữ liệu được chỉ ra trong format. Do đó, hàm scanf_s() sẽ trái ngược với hàm printf(), thay vì in ra màn hình một chuỗi theo định dạng thì nó nhận dữ liệu do người dùng nhập vào, tham số lưu dữ liệu có kiểu trùng với kiểu định nghĩa bởi format. Ví dụ, trong trường hợp này là “%d” thì có nghĩa là nhận một số thập phân mà người dùng nhập vào, cho nên biến Size phải có kiểu là int.

Bằng cách này, khi bạn gọi hàm gets_s() mà sử dụng kích thước do người dùng nhập vào trước đó, thì gets_s() sẽ sao chép số byte tương ứng và nếu như lớn hơn 0x10 thì sẽ xảy ra tràn bộ đệm.

Một giải pháp khả thi để bảo vệ là sẽ tiến hành kiểm tra độ dài của kích thước mà người dùng nhập vào trước khi thực hiện hàm gets_s(), như trong ví dụ minh họa dưới đây:

Phần 20 xin phép được dừng lại ở đây. Trong phần tiếp theo chúng ta sẽ cùng phân tích xem với giải pháp trên thì có thể giúp chương trình hết vuln không hay là vẫn bị dính. Target đã được biên dịch sẵn với tên là VULNERABLE_o_NO.exe để các bạn phân tích thử. Chúng ta sẽ thảo luận về nó ở phần sau.

Hẹn gặp các bạn ở phần 21.

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ải qua 18 phần tôi ghĩ các bạn đã có được những kiến thức nhất định, trong phần 19 này chúng ta sẽ thực hiện reverse Cruehead Crackme một cách đầy đủ nhất để hiểu cách hoạt động cũng như code một keygen nhỏ.

Load crackme vào IDA và bỏ chọn Manual Load bởi đây là file gốc không bị packed nên ta không cần phải sử dụng đến tùy chọn này.

Sau khi phân tích xong, ta thấy IDA dừng lại EP của crackme như trên hình. Do crackme này không phải là một ứng dụng dạng console như ở các phần trước, nên nhiệm vụ của ta không nhất thiết là phải đi tìm được hàm main. Ta biết rằng với các ứng dụng kiểu Window Applications thường có một message loop nhằm xử lý những gì người dùng tương tác với cửa sổ, nút bấm và thực hiện theo từng hành động của người dùng. Nó được lập trình để thực hiện các chức năng khác nhau.

Cách phổ biến nhất khi làm việc trong IDA vẫn là tìm kiếm các strings quan trọng tại cửa sổ Strings, nếu không có kết quả như mong muốn, ta sẽ tìm kiếm thông tin về các hàm APIs hoặc các hàm được viết bởi chính người lập trình. Trong trường hợp này, các strings của crackme có thể quan sát được rất dễ dàng, vì vậy chúng ta sẽ đi theo hướng tiếp cận này.

Tại màn hình Strings, có rất nhiều chuỗi nhưng ta sẽ dò theo chuỗi “No luck!” vì ta đoán chuỗi này sẽ đưa chúng ta đến vùng code đưa ra quyết định rẽ nhánh nào đó của Crackme. Trước tiên, nhấp đúp vào chuỗi này, ta sẽ chuyển tới màn hình Disassembly như hình dưới:

Tại đây, ta tìm kiếm các tham chiếu bằng cách nhấn phím tắt X hoặc Ctrl + X. Kết quả có hai tham chiếu đến chuỗi “No luck!”:

Hãy xem xét lệnh đầu tiên. Chọn lệnh này và nhấn OK, ta sẽ tới đây:

Tôi đổi màu cho khối lệnh này thành màu đỏ (hoặc màu nào khác tùy bạn) đồng thời cũng đổi tên luôn cho sub này vì nhìn vào các lệnh tại đây các bạn cũng đã hiểu được mục đích của nó rồi.

Tiếp theo ta xem sub này được gọi từ đâu khác trong code của Crackme, ta có được kết quả như sau:

Tại vùng code này, tôi cũng đổi màu cho block để dễ dàng nhận biết. Đồng thời với kết quả có được thì chắc chắn nhánh bên cạnh sẽ là nơi hiển thị thông báo “Good work!”. Ta đi tới các lệnh tại sub_0x40134d:

Rõ ràng là hàm này làm nhiệm vụ hiển thị thông báo “Good work!”, do vậy tôi thực hiện đổi màu của khối lệnh này cũng như tên của hàm như trên hình. Nhấn phím tắt ESC để quay ngược trở lại nơi gọi tới hàm này, ta có kết quả như sau:

Tạm thời phân tích sơ bộ về các đoạn code liên quan đến việc hiện thị chuỗi “No luck!” đầu tiên đã xong. Tiếp theo, ta chuyển tới đoạn code thứ hai có tham chiếu tới chuỗi “No luck!”. Tại đây tôi cũng chuyển màu cho khối lệnh này và đổi tên cho loc_ tương tự như hình:

Như vậy, ngoài thông báo lỗi ở trên, chúng ta còn có một thông báo lỗi tương tự khác ở đoạn code sau:

Tại đây, chúng ta thấy rằng sub_40137E() nhận một tham số truyền vào cho hàm này, đó là địa chỉ (offset) của một biến được IDA nhận diện là String. Nhấp đúp chuột vào biến này ta sẽ đi tới đây:

Với thông tin IDA cung cấp, thì đây là một buffer có kích thước 3698 bytes tại địa chỉ 0x40218E, thuộc section DATA của Crackme. Nhấn X tại biến này ta sẽ thấy có hai tham chiếu tới nó:

Trước tiên, đi tới lệnh push như tôi đã đánh dấu mũi tên trên hình. Tại đó có các lệnh như sau:

Ta thấy chuỗi này được truyền vào như là tham số cho hàm API là GetDlgItemTextA(), API này có nhiệm vụ nhận thông tin người dùng nhập vào từ hộp thoại. Tra cứu thêm thông tin về hàm này trên MSDN:

Như vậy là biến String sẽ được sử dụng làm buffer để lưu trữ dữ liệu mà người dùng nhập vào thông qua hàm GetDlgItemTextA(). Độ dài tối đa của chuỗi được copy vào buffer là 0xB (11).

Quan sát trên hình, bên dưới ta thấy có khối code tương tự như khối ta vừa phân tích và khối này cũng xử lý cùng một hWnd. Do đó, tôi suy đoán toàn bộ khối lệnh này được sử dụng để lấy thông tin về tên người dùng và mật khẩu khi nhập vào các textbox của Crackme.

Qua đoạn code trên, ta cũng thấy rằng cả hai textbox này đều có một giá trị định danh là nIDDlgItem tương ứng cho chúng là 0x3E80x3E9. Bằng việc sử dụng ứng dụng Greatis WinDowse (http://greatisprogramming.com/freedownload/wdsetup.exe), chương trình này sẽ cung cấp cho ta thông tin về các cửa sổ:

Với sự hỗ trợ của Greatis WinDowse, tôi đã xác định được ID của textbox đầu tiên (Name) là 0x3E8, do đó textbox thứ hai (Serial) sẽ là 0x3E9. Do đó, quay trở lại IDA để thực hiện đổi tên lại cho các buffer này, buffer đầu tiên tương ứng với chuỗi name người dùng nên tôi đặt là String_User và buffer thứ hai tương ứng với mật khẩu nên tôi đặt là String_Password. Cả hai buffer này chỉ chấp nhận chuỗi nhập vào có chiều dài tối đa là 0x0B như đã phân tích ở trên:

Tới lúc này ta đã biết được nơi sẽ lưu trữ thông tin mà người dùng nhập vào, chúng ta tiếp tục phân tích tiếp xem code của chương trình sẽ làm gì với các buffer này. Đầu tiên là String_User, code xử lý nó tại đây:

Trước khi đi vào phân tích sâu hơn ta sẽ đổi tên lại các hàm để code nhìn rõ ràng hơn. Hàm đầu tiên nhận tham số truyền vào là biến String_User, do đó hàm này có nhiệm vụ xử lý chuỗi Name, còn hàm thứ hai nhận tham số truyền vào là biến String_Password, do đó nhiệm vụ của hàm này chắc chắn sẽ liên quan tới mật khẩu mà người dùng nhập vào. Sau khi đổi tên hàm tôi có kết quả như hình:

Bây giờ, ta sẽ đi vào phân tích hàm đầu tiên là Process_User():

Tại hàm này, ta thấy nó có một tham số là arg_0, do tham số của hàm được truyền vào trước khi gọi hàm nên tham số này chắc chắn là chuỗi tên của người dùng. Tôi đặt lại tên cho tham số này và sử dụng lại cùng một tên để cho dễ nhớ. Tôi ghép thêm tiền tố offset để nhớ rằng đây là một biến con trỏ, trỏ tới chuỗi được lưu. Tiếp theo tôi đặt lại kiểu cho hàm này bằng cách nhấn phím tắt Y.

Khi quay trở lại vị trí thực hiện lời gọi hàm, chúng ta thấy IDA đã tự động bổ sung thêm comment trùng với tên của tham số mà ta đã đặt trong hàm.

Quay trở lại phân tích hàm Process_User(), hàm này sử dụng một vòng lặp thực hiện đọc từng byte của String_User. Vòng lặp này sẽ đi theo hướng mũi tên màu đỏ và lặp lại chừng nào byte đọc ra không phải là 0x0, nghĩa là khi chưa kết thúc chuỗi:

Sau mỗi lần xử lý của vòng lặp ta sẽ tấy nó thực hiện lệnh inc nhằm tăng giá trị của thanh ghi ESI để đọc kí tự tiếp theo trong String_User, sau đó thực đi theo hướng mũi tên màu xanh lam để thực hiện tiếp:

Kí tự được đọc ra đầu tiên sẽ được đem so sánh với giá trị 0x41. Để biết nó là gì, nhấn chuột phải tại giá trị 0x41 này và đổi thành ‘A’ là kí tự ASCII của giá trị đó:

Kết quả có được như sau:

Ta thấy, nếu kí tự đọc ra thấp hơn chữ cái ‘A’, crackme sẽ rẽ nhánh sang đoạn code hiển thị thông báo “No luck!”. Vì vậy, nếu quan sát nội dung của bảng mã ASCII thì ta hiểu rằng Crackme không chấp nhận có số trong chuỗi tên của người dùng (vì chúng phải lớn hơn hoặc bằng với A).

Vậy tóm lại, đầu tiên crackme sẽ kiểm tra tất cả các ký tự trong String_User phải lớn hơn hoặc bằng 0x41 (A).

Tiếp theo, crackme một lần nữa kiểm tra kí tự đọc ra nếu không thấp hơn (JNB) chữ cái ‘Z’ thì sẽ rẽ đến nhánh tới địa chỉ 0x401394, còn nếu kí tự đọc ra là thấp hơn thì sẽ tiếp tục với các kí tự tiếp thông qua khối màu xanh lam như tôi đã đổi trên hình.

Như vậy, crackme này chấp nhận các chữ cái hoa do người dùng nhập vào. Các chữ cái lớn hơn hoặc bằng ‘Z’ sẽ đi đến khối lệnh tại địa chỉ 0x401394. Ta phân tích hàm sub_4013D2() xem nhiệm vụ của nó là gì.

Tôi đổi tên hàm này thành Convert_to_Uppercase() dựa vào các câu lệnh mà hàm này thực hiện. Nếu kí tự nhập vào lớn hơn ‘Z’ thì sẽ được trừ đi 0x20 và được lưu lại vào cùng buffer.

Nghĩa là, nếu bạn nhập vào là chữ “a” thường, tương đương mã là 0x61, sau khi trừ 0x20 thì kết quả sẽ là 0x41, tương đương với chữ “A“. Hàm này sẽ thực hiện công việc tương tự cho tất cả các ký tự lớn hơn hoặc bằng “Z”. Do đó, nếu ta nhập vào là “Z”, sau khi trừ đi 0x20 ta có kết quả là 0x3a, đó chính là dấu “:” trong bảng mã ASCII:

Note: Thực ra cũng không hẳn là Convert_to_Uppercase(), nó phụ thuộc vào người dùng nhập vào. Ta cứ giả sử lý tưởng hóa người dùng nhập vào một chuỗi không vi phạm 🙂 .

OK, vậy là ta có thể xây dựng một Python script để tạo ra keygen như sau:

user = raw_input()
len_user = len(user)
if (len_user &gt; 0xb):
    exit()

userName=""

for i in range(len_user):
    if (ord(user[i]) < 0x41):
        print "INVALID CHARACTER!"
        exit()
    if (ord(user[i]) &gt;= 0x5A):
        userName+= chr(ord(user[i]) - 0x20)
    else:
        userName+= chr(ord(user[i]))

print "User Name:", userName


Chúng ta thấy rằng script trên thực hiện lại công việc tương tự như crackme đã làm, nó sẽ lấy các ký tự trong chuỗi người dùng nhập vào và so sánh nó với 0x41. Nếu nhỏ hơn sẽ thông báo đó là một kí tự không hợp lệ và thoát luôn. Còn nếu không nhỏ hơn 0x41, kí tự được so sánh tiếp với 0x5A. Nếu lớn hơn hoặc bằng 0x5A, kí tự đó được trừ đi 0x20 và được gán lại vào chuỗi userName.

Kiểm tra thử script này như hình dưới đây:

Và nếu tôi nhập chuỗi có chứa chữ “Z”, nó sẽ được thay thế bằng dấu “:” như chúng ta đã phân tích ở trên:

OK, như vậy là đoạn script trên đã thực hiện đúng công việc mà vòng lặp trong code của crackme đã làm. Quay trở lại IDA, ta tiếp tục phân tích tiếp xem sau khi thoát khỏi vòng lặp thì code của Crackme sẽ làm gì tiếp. Khi vòng lặp kiểm tra và biết kí tự của chuỗi nhập vào bằng 0 (báo hiệu kết thúc chuỗi), thì nó sẽ thoát khỏi Loop và rẽ nhánh theo hướng mũi tên màu xanh lá cây như hình:

Tại khối lệnh bắt đầu từ địa chỉ 0x40139C, ta thấy chương trình gọi lệnh pop esi. Nhấn vào thanh ghi esi này thì IDA sẽ highlight toàn bộ các vị trí trong code có sử dụng tới thanh ghi này:

Chúng ta thấy rằng trước khi khôi phục lại thanh ghi esi, đoạn đầu code của crackme đã sử dụng lệnh push để lưu giá trị ban đầu của esi vào ngăn xếp, mà ta thấy rằng giá trị ban đầu này của esi là trỏ tới đầu chuỗi mà người dùng nhập vào. Như vậy, với lệnh pop esi, giá trị của esi sẽ được khôi phục lại trước khi gọi hàm sub_0x4013c2(). Tóm lại, trước khi đi vào sub_0x4013c2() thì thanh ghi esi lúc này sẽ trỏ lại về chuỗi mà người dùng đã nhập vào, tuy nhiên lúc này chuỗi đó đã được chuyển đổi rồi.

Phân tích lệnh tại hàm sub_0x4013c2() như trên hình, ta thấy rằng code tại đây là một vòng lặp thực hiện lấy ra từng kí tự trong chuỗi và cộng dồn lại. Đó là lý do tại sao tôi đặt trên cho hàm này là Sum_Chars(). Bổ sung code này vào trong Python script ở trên:

Như vậy, script của ta đã hoàn thành công việc cộng tất cả các byte và in ra tổng. Để kiểm tra script có thực hiện đúng không, ta có thể đặt một breakpoint tại lệnh XOR bên dưới hàm cộng, cho chạy chương trình và nhập vào tên người dùng là “manowar” giống như lúc chạy script và mật khẩu tùy ý, ví dụ 989898.

Như trên hình, ta thấy thanh ghi EDI sẽ lưu kết quả của hàm cộng, giá trị của thanh ghi EDI lúc này đang là 0x215, hoàn toàn khớp với script ta đã viết ở trên. Sau đó, giá trị của EDI được đem đi XOR với giá trị mặc định 0x5678 (thường gọi là XOR_KEY). Do đó, tôi bổ sung thêm lệnh này vào script như sau:

Nếu vẫn đang dừng lại tại BP ở trên, ta nhấn F8 để trace qua lệnh XOR này, quan sát giá trị của thanh ghi EDI và so sánh với kết quả thực hiện bằng script hoàn toàn khớp nhau:

Sau đó, tại code của chương trình, kết quả sau lệnh XOR được gán cho thanh ghi EAX để làm kết quả trả về của hàm và nhảy tới địa chỉ 0x4013C1 để thoát khỏi hàm.

Sau khi ra khỏi hàm, giá trị của thanh ghi EAX sẽ được lưu lại thông qua lệnh push eax và sau đó được khôi phục lại bằng lệnh pop eax trước khi thực hiện so sánh. Điều này có nghĩa là trong câu lệnh cmp eax, ebx, toán hạng đầu tiên là thanh ghi EAX sẽ là kết quả trả về từ hàm Process_User().

Với chuỗi tên người dùng nhập vào đã xong, tiếp tục phân tích để xem điều gì sẽ xảy ra với mật khẩu mà người dùng nhập vào tại hàm Process_Password(). Đi vào phân tích code tại hàm:

Sau khi phân tích code, nhận thấy hàm này sẽ thực hiện đọc từng byte của chuỗi password, lưu vào thanh ghi BL và đem trừ đi giá trị là 0x30, kết quả vẫn lưu lại trong thanh ghi EBX. Tiếp theo, lấy giá trị của EDI (ban đầu được khởi tạo bằng 0) đem nhân với 0xA (được gán cho EAX) rồi cộng với EBX. Viết lại toàn bộ đoạn code này trong script như bên dưới. Trong script tôi để sẵn mật khẩu cố định là “989898” để xem kết quả sau khi tính toán là thế nào.

Chạy lại script, kết quả có được như sau:

Như vậy sau khi thực hiện script, với mật khẩu là 989898 thì kết quả tính toán là một giá trị ở dạng hex là 0xf1aca. Nếu thử chuyển đổi ngược lại thì tôi thấy giá trị hex này chính là của chuỗi 989898.

Do vậy, có thể tóm tắt lại đoạn code của hàm xử lý mật khẩu bước đầu sẽ thực hiện việc chuyển đổi chuỗi số mà ta nhập vào sang dạng hex, tương tự như việc ta gõ lệnh hex() ở thanh Python bar của IDA như trên hình. Do đó, trong script tôi có thể thay bằng câu lệnh đơn giản là hex(password). Khi chạy sẽ vẫn cho ra kết quả tương tự.

Tiếp theo, sau khi chuyển sang hạng hexa và lưu kết quả chuyển đổi tại thanh ghi EDI, trong code của crackme sẽ gọi lệnh xor edi với giá trị mặc định là 0x1234. Kết quả được bao nhiêu sẽ gán lại cho thanh ghi EBX thay vì sử dụng thanh ghi EAX (vì thanh ghi EAX đang được sử dụng để lưu kết quả khác). Sau khi thoát ra khỏi hàm, trước khi thực hiện so sánh chương trình sẽ khôi phục lại giá trị của EAX mà đã được tính toán bởi hàm Process_User().

Đến đây, ta có thể tổng kết lại toàn bộ quá trình xử lý của crackme như sau:

hex(password)^0x1234 = XOR_Result (Là kết quả tính toán mà hàm Process_User() đã trả về).

Do đó:

hex(password) = XOR_Result ^ 0x1234

Như vậy, bằng cách xor ngược lại như trên ta sẽ có được password tương ứng với chuỗi name nhập vào. Chỉnh sửa lại script trên như sau:

Chạy thử script sau khi đã sửa lại với chuỗi “manowar”:

Với kết quả có được, ta nhập vào Crackme để kiểm tra thử:

Tới đây ta đã có được một keygen hoàn chỉnh. Trong script này ta không cần thiết phải thực hiện chuyển đổi lại kết quả sang dạng thập phân bởi vì Python đã làm việc chuyển đổi này một cách tự động.

Dưới đây là toàn bộ source code của keygen:

sum = 0
user = raw_input()
len_user = len(user)
if (len_user &gt; 0xb):
	exit()
userName=""
for i in range(len_user):
	if (ord(user[i]) < 0x41):
		print "INVALID CHARACTER!"
		exit()
	if (ord(user[i]) &gt;= 0x5A):
		userName+= chr(ord(user[i]) - 0x20)
	else:
		userName+= chr(ord(user[i]))
print "User Name:", userName
for i in range(len(userName)):
	sum+= ord(userName[i])
print "Total:", hex(sum)
xor_result = sum ^ 0x5678
print "XOR_Result:", hex(xor_result)
#-----------------------------------
password = xor_result ^ 0x1234
print "Password:", password

Ta kiểm tra thử keygen với tên nhập vào có chứa “Z”:

Toàn bộ phần 19 đến đây là kết thúc, như vậy là chúng ta đã reversed toàn bộ quá trình hoạt động của Cruehead crackme và viết được một keygen hoàn chỉnh bằng Python. Hẹn gặp lại các bạn trong phần 20!

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