REVERSING WITH IDA FROM SCRATCH (P8)

m4n0w4r
tradahacking
Published in
15 min readMar 15, 2019

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

--

--