Posts Tagged ‘IDA’


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


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

Load file bị packed vào IDA:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Image result for unpackme funny

Xin gửi lời cảm ơn chân thành tới thầy Ricardo Narvaja!

m4n0w4r

Ủng hộ tác giả

Nếu bạn cảm thấy những gì tôi chia sẻ trong bài viết là hữu ích, bạn có thể ủng hộ bằng “bỉm sữa” hoặc “quân huy” qua địa chỉ:

Tên tài khoản: TRAN TRUNG KIEN
Số tài khoản: 0021001560963
Ngân hàng: Vietcombank


Vài dòng lan man:

Mới đây Sẻ đệ (yeuchimse) đóng Blog, làm tôi cũng nghĩ hay thôi, dăm ba cái Blog, view cũng lèo tèo, viết lách vừa tốn thời gian chỉnh sửa, chụp choẹt…. chắc cũng đóng nốt cho nhanh...

Anh em trong Gờ-rúp “kín” thi thoảng lại: “Già rồi, viết lách cái gì. Định kiếm fame đến bao giờ!!“. Thôi thì …..

………..

…………†

Dành cho những bạn nào chưa nghe bản guitar solo này, hãy xem cách Steve Vai “nựng” đàn để tìm cho mình những cảm hứng riêng

Trong phần trước, tôi đã cho các bạn thấy một vài phương pháp để có thể phát hiện và tới được OEP trong một file bị packed. Phần này, chúng ta sẽ tiếp tục với hai bước còn thiếu mà tôi đã đề cập, đó là: dump filerebuild IAT.

Dump file

Các bạn có thể thực hành lại để tới OEP và Reanalyze chương trình, sau khi thực hiện xong ta có mọi thứ sẵn sàng cho việc Dump file. Hành động dump file có thể hiểu một cách “chân phương” là sau khi tới được OEP, toàn bộ code gốc của chương trình cùng các hàm APIs đã được “bung” đầy đủ trên memory, lúc này ta thực hiện thao tác dump để lưu thành một file.

Ở đây, tôi sẽ sử dụng một IDC Script để thực hiện công việc này (không phải là Python script). Nội dung của script như sau:

static main()
{
	auto fp, ea;
	fp = fopen("dumped.bin", "wb");
	for (ea = 0x400000; ea < 0x40b200; ea++)
		fputc(Byte(ea), fp);
}

Trong script trên, chúng ta thực hiện dump từ địa chỉ ImageBase0x400000 tới địa chỉ lớn nhất mà ta thấy trong tab IDA Segments. Đó chính là section cuối cùng của file thực thi, trong trường hợp này là section OVERLAY, tại địa chỉ 0x40b200.

Copy&Paste script trên vào Notepad và lưu lại với tên là dumper.idc. Quay lại IDA và cho thực thi script này thông qua File > Script file  (IDA hỗ trợ cả Python và IDC scripts). Kết quả tôi có được tập tin dumped.bin như hình dưới:

Tôi tạo một bản sao của file đã dump ra và đổi tên nó thành một tập tin có đuôi mở rộng .exe:

Các bạn để ý thì thấy rằng, file sau khi đổi tên không hề có icon mặt cười như ở file gốc. Để fix được thiếu sót này các bạn có thể sử dụng chương trình PE Editor v1.7 (download tại đây: https://mega.nz/#!uGpHkKTD!bkxqBmj2Ib0FC6QDln7Cf1DnXSqRazinTuXNbAdFCjE). Chạy chương trình, load file exe và sau đó chọn sections:

Tại màn hình chứa các thông tin về Sections:

Nhấp chuột phải vào một section bất kì và chọn Dumpfixer, tương tự như hình dưới đây:

Bước dumpfixer này thực chất là thiết lập cho Raw_size = Virtual_sizeRaw_offset = Virtual_offset. Kết quả có được như sau:

OK, như vậy là đã lấy lại được icon của file. Tạm thời ở thời điểm này, file đã dump ra được tốt rồi, ít nhất là icon đã hiển thị đúng, nhưng tuy nhiên file này vẫn chưa thể thực thi được bình thường bởi vì cần phải sửa cả IAT nữa.

Rebuild IAT

IAT là gì?

IAT viết đầy đủ là Import Address Table, là một bảng nằm trong file thực thi và được sử dụng khi chương trình hoạt động. Bảng này lưu tất cả địa chỉ của các hàm được import, từ đó chương trình có thể sử dụng để chạy trên bất kỳ máy nào (chính xác là trên bất kỳ môi trường OS nào).

Nếu IAT là đầy đủ và chính xác thì khi ta đưa tập tin thực thi cho một người khác, IAT sẽ được điền đầy đủ các giá trị tương ứng (địa chỉ các hàm APIs) trên máy đó, bất kể hệ điều hành hoạt động trên máy đó là gì, tính tương thích sẽ được duy trì và chương trình sẽ thực thi được một cách bình thường.

Nghĩa là, IAT sẽ luôn nằm ở một vị trí nhất định trong mỗi tệp thực thi, và sẽ có các vị trí cố định cho mỗi hàm để điền vào. Các bạn nhớ lại phần trước tôi có so sánh (chưa giải thích kĩ) sự khác biệt giữa hình ảnh từ tệp tin bị packed khi ta tới được OEP (trước khi thực hiện việc dump) với file gốc ban đầu:

Cả hai đều hiển thị địa chỉ 0x403238 và dường như chúng có cùng nội dung. Bây giờ chúng ta mở lại file gốc:

Quan sát ở bên dưới thấy có địa chỉ file offset (trên ổ đĩa) là 0x1038. Tôi dùng trình Hex Editor là HxD để load file gốc và tìm tới địa chỉ offset này:

Tại HxD, ta thấy nội dung tại offset 0x10380x355e. Nếu tôi cộng giá trị 0x355e vào địa chỉ ImageBase0x400000, tôi có kết quả là 0x40355e. Vậy có thông tin gì lưu tại địa chỉ mà ta vừa tính toán được? Để làm rõ hơn, tôi cho IDA load lại file gốc cùng với tùy chọn Manual Load, mục đích để IDA nạp tất cả các sections của tập tin thực thi:

Sau khi chấp nhận cho tải tất cả các sections, đợi khi IDA phân tích toàn bộ file xong, ta đi đến địa chỉ 0x40355e. Ở bên phải lúc này ta thấy được tên của hàm API là:GetModuleHandleA

Tương tự đối với các ví trí khác mà tôi highlight làm ví dụ như hình dưới:

Như vậy, từng giá trị trên sẽ được cộng với địa chỉ ImageBase để từ đó tìm ra tên của các hàm tương ứng. Và cũng từ các tên hàm đó sẽ lấy được các địa chỉ tương ứng của từng hàm trong máy lúc runtime. Ví dụ, như trong trường hợp này nó sẽ tìm địa chỉ của hàm GetModuleHandleA() trên máy của chúng ta và sửa lại giá trị 5e 35 bằng địa chỉ thật của hàm API (ví dụ: 757116D0  kernel32.GetModuleHandleA)

Đó chính là lý do tại sao một tệp tin thực thi có thể chạy trên bất kỳ máy nào, bởi vì nó sẽ luôn luôn tìm tên của API tương ứng trong mỗi mục của IAT và tìm ra địa chỉ hợp lệ của hàm tương ứng với từng máy khác nhau khi nó thực thi. Và đó cũng là lý do tại sao trên bất kỳ máy nào nếu tôi thực hiện một lời gọi hàm như sau:

CALL [0x403238]

Chương trình sẽ luôn luôn hoạt động bởi vì 0x403238 là mục IAT của hàm GetModuleHandleA(), nội dung thay đổi chính là địa chỉ hàm mà hệ điều hành sẽ lưu lại bằng cách chỉnh sửa lại giá trị ban đầu 5e 35 (trỏ đến chuỗi tên của hàm sau khi cộng thêm ImageBase).

Giữ nguyên hai màn hình IDA đang mở (một cho file bị packed và đang dừng lại ở OEP; còn 1 cho file gốc), ta mở thêm một IDA thứ ba và load file đã dumped mà tôi vừa đổi tên là:dumped.exe.

Chuyển tới địa chỉ của IAT là 0x403238:

Quan sát bên dưới, ta thấy địa chỉ file offset lúc này là 0x3238, địa chỉ này không khớp với file gốc bởi sau khi thực hiện dumpfixer đã thay đổi kích thước trên đĩa (Raw Size) bằng với kích thước ảo (Virtual size), dẫn tới địa chỉ offset cũng thay đổi, cho nên địa chỉ của hàm API GetModuleHandleA() cũng vì thế mà thay đổi theo.

Sử dụng HxD mở file dump để kiểm tra, tìm tới offset 0x3238. Kết quả ta thấy như sau:

Chúng ta thấy rằng giá trị tại đó là địa chỉ của một hàm API, chứ không phải là một offset để cộng với ImageBase nhằm tìm tên của hàm API nữa. Do vậy, ta có thể hiểu rằng khi chương trình chạy, hệ thống sẽ lấy được địa chỉ chính xác của hàm API và lưu địa chỉ này ở đó, vậy nên khi thực hiện dump chương trình thì địa chỉ của hàm GetModuleHandleA() cũng sẽ được lưu kèm theo file đã dump. Để kiểm tra ta có thể tới địa chỉ của hàm này trong IDA như hình dưới:

Vậy vấn đề ở đây là gì?

Khi ta thực thi file dump, hệ thống sẽ tìm kiếm IAT và lấy ra giá trị này, sau đó cộng với địa chỉ ImageBase để từ đó tìm ra tên của hàm, cuối cùng sẽ tìm địa chỉ của hàm … Nhưng vì khi ta thực hiện dump file đã vô tình đã phá vỡ nguyên tắc này, do giá trị được lưu cuối cùng chính là địa chỉ thật của hàm API, và bởi vậy chương trình sẽ bị crash khi khởi chạy vì nó không thể điền vào IAT chính xác.

Để giải quyết được vấn đề này ta cần phải tìm một cách để sửa lại IAT và khôi phục lại tất cả các offset – trỏ vào các chuỗi chứa tên của các hàm API. Công việc này nếu làm bằng tay sẽ rất oải, do số lượng hàm API rất nhiều, vậy nên chúng ta sẽ sử dụng một công cụ hỗ trợ có tên là Scylla:

https://forum.tuts4you.com/files/file/576-scylla-imports-reconstruction/

Chạy ứng dụng này. Tại chỗ Attach to an active process, tôi chọn process của file bị packed:

Nhớ rằng bạn vẫn phải giữ nguyên màn hình của IDA đang dừng lại tại OEP.

Tại Scylla, thay đổi ​OEP​ thành giá trị chính xác là 0x​401000:

Tiếp theo nhấn​ ​IAT​ ​Autosearch, một bảng thông báo xuất hiện:

Điều này có nghĩa là, IAT bắt đầu tại địa chỉ 0x403184 và có kích thước là 0x108. Nhấn OK để chấp nhận, sau đó nhấn Get Imports, ta có kết quả như sau:

Các bạn thấy rằng Scylla đã lấy được tất cả thông tin trừ 1 giá trị khi ta nhấn nhấn Show Invalid. Mặt khác, chúng ta thấy rằng offset 0x3238 trong packed file tương ứng với GetModuleHandleA(), như vậy là Scylla đã làm việc rất tốt:

Tại packed file, đi tới địa chỉ API mà Scylla thông báo là không hợp lệ tại 0x403208 (kết quả có thể khác ở trên máy của các bạn), để xem tại đó có thông tin gì:

IDA cung cấp thông tin là unk_6E33AC50, nghĩa là nó chưa nhận biết được code tại đây. Đi tới địa chỉ này:

Ta tới thư viện apphelp.dll, nhấn C để chuyển đổi các bytes tại đây thành code, tạo hàm và cho Reanalyze lại chương trình:

Kết quả ta có được code thuộc thư viện apphelp.dll sẽ gọi tới hàm API GetDC(). Nhấp đúp chuột tại offset szGetdcWndPDcP, ta tới đây:

Như vậy, hàm API GetDC() thuộc thư viện user32.dll. Do đó, quay trở lại màn hình của Scylla, nhấp đúp chuột invalid API này và chọn như hình bên dưới để sửa lại:

Nhấn OK để sửa và như vậy ta sẽ có được các hàm API hợp lệ. Nếu ta bấm Show Suspect, Scylla sẽ cung cấp các APIs mà nó còn nghi ngờ, để xác minh ta lại quay lại IDA và đi đến các địa chỉ 0x4031BC, 0x40322C0x403278 (kết quả này có thể khác trên máy của các bạn):

Thông tin mà IDA cung cấp như sau:

Kết quả thu được là chính xác hoàn toàn rồi, cuối cùng là nhấn nút Fix Dump:

Scylla sẽ lưu file đã fix với tên mới là dumped_SCY.exe. Nhấp đúp vào để chạy thử ta thấy crackme chạy ok như file gốc:

Chúng ta thấy rằng khi mở file đã fix ở trên trong IDA, ta thấy nó bắt đầu từ OEP 0x401000 và tên của APIs đã được fix hoàn toàn và trông không khác gì với bản gốc:

Như vậy qua bài viết này, tôi đã hướng dẫn các bạn thực hiện xong toàn bộ quá trình unpack một file được pack bằng một packer dễ. Phần tiếp theo ta sẽ xem xét một vài ví dụ khó hơn.

Phần này xin được kết thúc tại đây. Hẹn … à mà thôi! Khi nào có hứng tôi đưa lên tiếp…..

Image result for brain dump funny

Xin gửi lời cảm ơn chân thành tới thầy Ricardo Narvaja!

m4n0w4r

Ủng hộ tác giả

Nếu bạn cảm thấy những gì tôi chia sẻ trong bài viết là hữu ích, bạn có thể ủng hộ bằng “bỉm sữa” hoặc “quân huy” qua địa chỉ:

Tên tài khoản: TRAN TRUNG KIEN
Số tài khoản: 0021001560963
Ngân hàng: Vietcombank


Vài dòng lan man:

Tôi đoán có nhiều bạn trông chờ và đặt nhiều kì vọng vào bộ tuts này như kiểu một tài liệu sẽ làm thay đổi cuộc đời :P. Nó sẽ biến bạn trở thành một “chuyên gia hàng đầu”, được nhiều người săn đón 😦

Đứng kì vọng để rồi thất vọng!!!

Không hiểu sao tôi rất dị ứng với từ chuyên gia, mỗi lần bị giới thiệu như vậy trong người tôi nó thất bứt rứt lắm…..

Nhiều bạn cũng có thắc mắc sao lâu tôi chưa ra bài mới. Điều này rất khó nói 😀:
– Nó phụ thuộc vào khung thời gian trống còn lại trong ngày.
– Hơn nữa cũng phụ thuộc nhiều vào người đọc. Đọc giả có tâm, đọc, góp ý/thắc mắc những chỗ tôi viết chưa ổn và hơn hết họ đọc tới đọc đến tận chỗ “bỉm sữa” :). Đọc giả có tâm hơn nữa, họ không cần đọc mà kéo roẹt xuống chỗ có dòng “bỉm sữa” :D. Còn phần đa thì chắc là đọc đến chỗ “ấy” thì dừng lại….

Tuyển tập các bài viết này sẽ thường xuyên thay đổi nội dung và sẽ bao gồm các chủ đề khác nhau liên quan đến reversing như: static reversing, debugging, unpacking và exploiting.

Trong phần này, chúng ta sẽ unpack file PACKED_CRACKME.exe (tải tại đây), được packed bằng phiên bản UPX mới nhất (tính đến thời điểm viết bài). Tuy nhiên, điều này không có nghĩa là tôi sẽ viết nhiều bài khác chỉ nói về chủ đề unpacking, chúng ta sẽ thay đổi và trộn lẫn các chủ đề khác nhau để không bị nhàm chán. Do đó, bên cạnh việc tìm hiểu Unpacking bằng IDA thì đồng thời ta sẽ vẫn nghiên cứu thêm các chủ đề khác.

File bị Packed?

Định nghĩa đơn giản về tập tin packed đó là một tập tin được ẩn mã thực thi gốc của chương trình và lưu lại bằng cách áp dụng các kỹ thuật nén hoặc mã hóa để tránh không bị reverse một cách dễ dàng. Bên cạnh đó nó cũng được chèn thêm một đoạn Stub hay một section, để khi thực thi chương trình, đoạn stub này sẽ nhận code đã được mã hóa, giải mã nó trong bộ nhớ, lưu code giải mã vào trong bất kỳ section nào hoặc là chính nó và cuối cùng nhảy tới vùng code này để thực thi (đó chính là code gốc ban đầu của file).

Có rất nhiều biến thể của các trình packers, và đa phần trong số chúng là các trình protectors, sử dụng các kĩ thuật như hủy bảng IAT hay import table, hủy thông tin về Header của file. Bổ sung thêm các cơ chế anti-debugger để tránh việc unpack và khôi phục lại tập tin ban đầu.

Image result for packers landscape

Thực hành

Ví dụ đơn giản nhất mà các chuyên gia hay sử dụng để minh họa về packer đó chính là UPX. Trình packer này không áp dụng các kĩ thuật anti-debug, hay các tiểu xảo nào khác được đề cập ở trên, tuy nhiên nó lại giúp ta khởi đầu với những kĩ năng đơn giản nhất trong quá trình thực hiện unpacking. Để tiện cho các bạn trong việc thực hành, file PACKED_CRACKME.EXE đã được đính kèm link tải về ở trên.

Load file vào IDA và chọn Manual load vì tùy chọn này sẽ giúp chúng ta có thể nạp tất các sections của file, đồng thời bỏ lựa chọn Create imports segment (Thầy Ricardo khuyến nghị nên bỏ chọn khi làm việc với các packed files). Kết quả sau khi load vào IDA như hình dưới:

Ta đang dừng tại địa chỉ bắt đầu hay còn gọi là Entry Point (EP) của PACKED_CRACKME.exe. Với file bị packed thì địa chỉ EP là 0x409be0, trong khi ở file gốc thì ta sẽ dừng tại địa chỉ EP là 0x401000, như hình minh họa bên dưới:

Ngoài ra, so sánh các sections hay segments của cả hai file, ta thấy rằng dưới header của file sau khi bị packed có thêm một section mới với tên là UPX0, có kích thước trong bộ nhớ lớn hơn so với file gốc ban đầu.

File gốc

File bị PACKED

Theo quan sát trên hình, section UPX0 của file bị packed kết thúc tại địa chỉ 0x409000, trong khi toàn bộ các sections ở file gốc đều nằm trong vùng nhớ bắt đầu từ 0x401000 đến 0x408200. Ở đây, ta đang đề cập đến bộ nhớ ảo, đó là khi một chương trình thực thi, nó có thể chiếm dung lượng 1k trên ổ đĩa (HDD) nhưng sẽ chiếm 20k hoặc hơn thế nữa trong bộ nhớ.

Điều này có thể thấy được khi phân tích trong IDA, ví dụ, tại địa chỉ bắt đầu 0x401000 của file gốc ta thấy các thông tin như sau:

Trong hình trên (Section size in file) chiếm 0x600 byte, trong khi trong bộ nhớ (Virtual size) chiếm 0x1000. Trở về file bị packed, nếu chúng ta chuyển tới địa chỉ 0x401000 – đó là nơi bắt đầu của section UPX0.

Chúng ta thấy rằng Section size in file chiếm 0x0 byte trên đĩa, nhưng trong bộ nhớ nó chiếm 0x8000. Điều này có nghĩa là, nó dành ra một không gian trống phục vụ cho việc khôi phục lại mã chương trình gốc ban đầu tại đây và sau đó nhảy tới đây để thực hiện lệnh. Như vậy, nó dành đủ không gian để thực hiện việc khôi phục code này.

Ta cũng thấy rằng địa chỉ 0x401000 có kèm theo tiền tố dword_ ở phía trước nghĩa là nội dung của nó là một DWORD. Dấu (?) hàm ý nó cần được dành riêng và không chứa bất kỳ giá trị nào, từ khóa dup có nghĩa là dword đó được nhân với 0xc00, như vậy kết quả là sẽ dành ra 0x3000 bytes:

Tiếp theo tại 0x404000, ta thấy có 1400h dup(?). Có nghĩa là cần dành ra vùng nhớ:

Do đó, tổng số cần có 0x8000 bytes sẽ được dành riêng trong bộ nhớ để đặt chương trình vào đó. Tại dword_401000 ta thấy có một tham chiếu trong mã thực thi, ta sẽ xem xét câu lệnh này làm gì sau.

Tiếp theo bên dưới, ta thấy file packed có thêm một section thứ hai là UPX1, section này có kích thước trên đĩa là 0xe00 và trong bộ nhớ 0x1000. Đây có khả năng là nơi chương trình lưu một số kĩ thuật mã hóa đơn giản nhằm để che dấu mã gốc.

Nếu kiểm tra thông tin tham chiếu tại địa chỉ bắt đầu của section là 0x409000, ta có kết quả:

Chúng ta thấy có một tham chiếu bên dưới (Down) trong code thực thi của chương trình. Chuyển tới vùng code đó:

Tại Stub trên, sau địa chỉ Entry Point, chương trình nạp địa chỉ 0x409000 vào thanh ghi ESI (Ta biết lấy địa chỉ là bởi có tiền tố offset ở phía trước). Chuyển qua chế độ Text mode bằng cách nhấn phím space bar, tại đó chúng ta thấy như sau:

Đoạn stub này nằm cùng section UPX1, bên dưới code của chương trình gốc đã packed. Nghĩa là tại section UPX1, trình packer đã thực hiện lưu các bytes đã encrypted của chương trình gốc tại đây và stub code bắt đầu từ 0x409be0.

Ta có thể dễ dàng nhận ra vùng Stub sẽ đọc các bytes từ 0x409000, sau đó áp dụng một số thao tác tính toán và lưu lại kết quả sau tính toán vào 0x401000. Ta thấy thanh ghi EDI = ESI-0x8000:

Nói cách khác, chương trình sẽ sử dụng vùng nhớ trỏ bởi ESI (như là source), từ đó đọc dữ liệu ra và áp dụng các tính toán nhất định, sau đó lưu vào vùng nhớ trỏ bởi EDI (như là dest) để khôi phục lại code ban đầu của chương trình.

Chúng ta đã biết tại 0x401000 có một tham chiếu trong mã thực thi, nếu chúng ta nhấp đúp vào tham chiếu đó:

Ta tới vùng code chứa một lệnh nhảy (jmp) tới 0x401000:

Jmp near là một lệnh nhảy trực tiếp đến địa chỉ, do vậy nó sẽ nhảy thẳng đến 0x401000. Rõ ràng, ở đây sau khi thực thi toàn bộ mã lệnh tại Stub và tái tạo lại mã gốc ban đầu của chương trình, chương trình sẽ nhảy tới OEP tại địa chỉ 0x401000 (Entry Point gốc), không giống như Entry Point của Stub0x00409BE0.

Ta sẽ gọi tắt là OEP hay Original Entry Point để hàm ý rằng đó chính Entry Point (EP) của chương trình gốc ban đầu. Điều này là hiển nhiên vì khi một chương trình bị packed, ta hoàn toàn không biết địa chỉ này ở đâu và chỉ khi ta có file gốc thì ta mới có biết được EP ban đầu là 0x401000 như hình minh họa dưới đây:

Tóm lại, khi chúng ta làm việc với một chương trình bị packed, ta sẽ không biết OEP của nó ở đâu bởi ta không có file gốc ban đầu, do đó chúng ta sẽ phải áp dụng các kĩ thuật để tìm ra OEP. Quay lại với phân tích ở trên, khi Stub hoàn thành tất cả các thủ thuật của nó và khôi phục lại mã gốc, nó sẽ nhảy tới OEP để từ đó thực thi chương trình.

Chính vì thế, ta có thể đặt một Breakpoint tại lệnh Jmp tới OEP để xem code của chương trình gốc có được khôi phục như ta đã suy đoán như trên không. Thử đặt một BP như hình dưới:

Sau đó chọn debugger là Local Win32 Debugger và nhấn Start debugger. Ngay lập tức, ta sẽ dừng tại BP vừa đặt ở trên:

Nhấn F8 để trace qua lệnh này:

IDA sẽ hiển thị một thông báo như trên, cứ nhấn Yes để thông báo cho IDA biết và nhận diện lại section UPX0 ban đầu như là CODE (ban đầu nó được định nghĩa là DATA).

Ta thấy rằng stub đã giải nén code và nhảy tới 401000 để thực thi. So sánh thì thấy code này rất giống với code tại địa chỉ 0x401000 ở file gốc, nhưng tuy nhiên ta lại không thể chuyển sang chế độ đồ họa bởi vì lúc này nó không được định nghĩa như là một function (chỉ là loc_401000).

Để chuyển được sang chế độ đồ họa, có một tùy chọn ẩn ở góc dưới bên trái của màn hình IDA, bằng cách nhấp chuột phải tại đó và chọn Reanalyze program:

Bằng cách này, địa chỉ loc_401000 đã thay đổi thành sub_401000 cho biết bây giờ nó đã được hiểu như là một hàm. Vì vậy, ta có thể chuyển sang chế độ đồ hoạ bằng cách nhấn phím tắt space bar:

Ta thấy, code giờ đây đã đẹp hơn 🙂

Tuy nhiên, quan sát kĩ một chút ta sẽ thấy có sự khác biệt, ở file gốc tại địa chỉ 0x401002 sẽ hiển thị lời gọi tới hàm API CALL GetModuleHandleA, trong khi tại file ta đang phân tích chỉ hiển thị lệnh CALL sub_401056. Đi vào lệnh Call này để xem code của nó là gì:

Ta lại thấy sự khác biệt với bản gốc. Nếu tại file gốc khi ta truy cập vào CALL GetModuleHandleA:

Ta thấy có một lệnh nhảy gián tiếp tới hàm API. Vậy còn tại file đang đang phân tích thì sao? Hàm API đã đi đâu? Thử follow theo lệnh nhảy tại file đang phân tích, ta có được thông tin như sau:

Như trên hình, nội dung tại 0x403028 là một offset (off_), đó là địa chỉ của API GetModuleHandleA và tại file gốc, cũng quan sát địa chỉ này tại section .idata thì ta thấy cũng chứa địa chỉ của cùng một hàm API.

Mặc dù, ta thấy chúng đều nhảy tới cùng một địa chỉ, tuy nhiên có một sự khác biệt rất quan trọng mà chúng ta sẽ tìm hiểu sau. Như vậy, có thể nói tới thời điểm này code gốc của chương trình đã được unpack hoàn toàn.

Có một cách khác nữa để tìm OEP, đó là tìm lệnh được thực thi đầu tiên ở section đầu tiên. Tuy nhiên, phương pháp này không phải lúc nào cũng có hiệu quả :). Để làm theo cách này, ta khởi động lại target trong IDA, cấu hình debugger để dừng lại tại Entrypoint khi thực thi. Sau đó, cho thực thi file packed, ta sẽ dừng lại ở entry point:

Tiếp theo, ta chuyển tới section đầu tiên bắt đầu tại địa chỉ 0x401000.

Tại đó, tôi nhấn F2 để đặt một breakpoint và cấu hình để dừng khi thực thi (Execute) – có nghĩa là chương trình sẽ break chỉ khi thực thi lệnh mà không dừng khi nó đọc hoặc ghi dữ liệu, đó là điều tôi kì vọng. Và vì tôi không biết chính xác lệnh nào sẽ được thực thi đầu tiên nên Breakpoint on execute mà tôi đặt sẽ bao gồm toàn bộ section (0x8000 bytes). Cụ thể như hình minh họa dưới đây:

Sau khi thiết lập breakpoint như trên xong, nhấn OK, lúc này toàn bộ section sẽ được đánh dấu bằng màu đỏ như hình:

Sau đó, nếu có breakpoint nào mà các bạn đã đặt trước đó thì hay tiến hành vô hiệu hóa bằng cách truy cập Debugger->Breakpoint->Breakpoint List.

Nhấn chuột phải tại breakpoint và chọn Disable breakpoint, tương tự như hình:

Sau đó cho thực thi chương trình. Khi chương trình break, ta thấy sẽ dừng lại tại lệnh đầu tiên ở section vừa được tạo, trong trường hợp này, 0x401000 chính là OEP mà tôi tìm được:

Sau khi đã đạt được mục đích, ta vô hiệu hóa breakpoint đã đặt. Sau đó tiến hành phân tích lại toàn bộ chương trình, kết quả có được tương tự như những gì đã làm ở trước:

OK, qua toàn bộ phân tích trên chúng ta đã có được OEP gốc của chương trình thông qua hai cách thực hiện khác nhau. Công việc tiếp theo mà ta cần làm là dump file và rebuild lại toàn bộ IAT để đảm bảo file sau khi unpacked thực thi được một cách bình thường như file gốc ban đầu. Tuy nhiên, công việc đó sẽ làm ở các phần tiếp theo, phần hôm nay là đủ rồi, các bạn có thể tạo một bản Snapshot để có thể quay lại vào phần tới.

Phần này xin được kết thúc tại đây. Hẹn gặp các bạn ở phần 15!

Xin gửi lời cảm ơn chân thành tới thầy Ricardo Narvaja!

m4n0w4r

Ủng hộ tác giả

Nếu bạn cảm thấy những gì tôi chia sẻ trong bài viết là hữu ích, bạn có thể ủng hộ bằng “bỉm sữa” hoặc “quân huy” qua địa chỉ:

Tên tài khoản: TRAN TRUNG KIEN
Số tài khoản: 0021001560963
Ngân hàng: Vietcombank


Ở phần 13 này, chúng ta sẽ thư giãn một chút trước khi tiếp tục với các bài thực hành khác để tìm hiểu sâu hơn về cách sử dụng IDA. Phần này tôi sẽ giới thiệu tới các bạn một plugin khá tiện lợi và thú vị, cho phép chúng ta có thể xử lý tốt hơn với Python.

Plugin có tên là IpyIDA, được phát triển bởi Marc-Etienne M.Léveillé (https://twitter.com/marc_etienne_), một chuyên gia hiện làm việc tại hãng ESET. Việc cài đặt plugin này khá dễ dàng, chỉ bằng cách sao chép và dán dòng lệnh sau vào thanh Python của IDA:

import urllib2; exec urllib2.urlopen('https://github.com/eset/ipyida/raw/stable/install_from_ida.py').read()

Trong trường hợp có vấn đề hoặc gặp lỗi, bạn có thể truy cập trang chủ của plugin tại đây: https://github.com/eset/ipyida

Câu lệnh trên sẽ tiến hành cài đặt một cách hoàn toàn tự động trong vòng vài phút. Sau khi cài đặt xong, ta có thể sử dụng nó thông qua Edit > Plugins > IpyIDA, cửa sổ Ipython Console xuất hiện tương tự như hình minh họa bên dưới:

Ta thấy rõ ràng nó có nhiều tính năng mạnh hơn thanh Python của IDA. Để tra cứu các tính năng của plugin này, ta nhấn phím ?:

IPython -- An enhanced Interactive Python

=========================================

IPython offers a combination of convenient shell features, special commands and a history mechanism for both input (command history) and output (results caching, similar to Mathematica). It is intended to be a fully compatible replacement for the standard Python interpreter, while offering vastly improved functionality and flexibility.

At your system command line, type 'ipython -h' to see the command line options available. This document only describes interactive features.

MAIN FEATURES

-------------

* Access to the standard Python help. As of Python 2.1, a help system is available with access to object docstrings and the Python manuals. Simply type 'help' (no quotes) to access it.

* Magic commands: type %magic for information on the magic subsystem.

* System command aliases, via the %alias command or the configuration file(s).

* Dynamic object information:

Typing ?word or word? prints detailed information about an object. If certain strings in the object are too long (docstrings, code, etc.) they get snipped in the center for brevity.

Typing ??word or word?? gives access to the full information without snipping long strings. Long strings are sent to the screen through the less pager if longer than the screen, printed otherwise.

The ?/?? system gives access to the full source code for any object (if available), shows function prototypes and other useful information.

If you just want to see an object's docstring, type '%pdoc object' (without quotes, and without % if you have auto magic on).

* Completion in the local namespace, by typing TAB at the prompt.

At any time, hitting tab will complete any available python commands or variable names, and show you a list of the possible completions if there's no unambiguous one. It will also complete filenames in the current directory.

This feature requires the readline and rlcomplete modules, so it won't work if your Python lacks readline support (such as under Windows).

* Search previous command history in two ways (also requires readline):

- Start typing, and then use Ctrl-p (previous,up) and Ctrl-n (next,down) to search through only the history items that match what you've typed so far. If you use Ctrl-p/Ctrl-n at a blank prompt, they just behave like normal arrow keys.

- Hit Ctrl-r: opens a search prompt. Begin typing and the system searches your history for lines that match what you've typed so far, completing as much as it can.

- %hist: search history by index (this does *not* require readline).

* Persistent command history across sessions.

* Logging of input with the ability to save and restore a working session.

* System escape with !. Typing !ls will run 'ls' in the current directory.

* The reload command does a 'deep' reload of a module: changes made to the module since you imported will actually be available without having to exit.

* Verbose and colored exception traceback printouts. See the magic xmode and xcolor functions for details (just type %magic).

* Input caching system:

IPython offers numbered prompts (In/Out) with input and output caching. All input is saved and can be retrieved as variables (besides the usual arrow key recall).

The following GLOBAL variables always exist (so don't overwrite them!):

_i: stores previous input.

_ii: next previous.

_iii: next-next previous.

_ih : a list of all input _ih[n] is the input from line n.

Additionally, global variables named _i are dynamically created ( being the prompt counter), such that _i == _ih[]

For example, what you typed at prompt 14 is available as _i14 and _ih[14].

You can create macros which contain multiple input lines from this history, for later re-execution, with the %macro function.

The history function %hist allows you to see any part of your input history by printing a range of the _i variables. Note that inputs which contain magic functions (%) appear in the history with a prepended comment. This is because they aren't really valid Python code, so you can't exec them.

* Output caching system:

For output that is returned from actions, a system similar to the input cache exists but using _ instead of _i. Only actions that produce a result (NOT assignments, for example) are cached. If you are familiar with Mathematica, IPython's _ variables behave exactly like Mathematica's % variables.

The following GLOBAL variables always exist (so don't overwrite them!):

_ (one underscore): previous output.

__ (two underscores): next previous.

___ (three underscores): next-next previous.

Global variables named _ are dynamically created ( being the prompt counter), such that the result of output is always available as _.

Finally, a global dictionary named _oh exists with entries for all lines which generated output.

* Directory history:

Your history of visited directories is kept in the global list _dh, and the magic %cd command can be used to go to any entry in that list.

* Auto-parentheses and auto-quotes (adapted from Nathan Gray's LazyPython)

1. Auto-parentheses Callable objects (i.e. functions, methods, etc) can be invoked like this (notice the commas between the arguments)::

In [1]: callable_ob arg1, arg2, arg3

and the input will be translated to this::callable_ob(arg1, arg2, arg3)

This feature is off by default (in rare cases it can produce undesirable side-effects), but you can activate it at the command-line by starting IPython with `--autocall 1`, set it permanently in your configuration file, or turn on at runtime with `%autocall 1`.

You can force auto-parentheses by using '/' as the first character of a line. For example::

In [1]: /globals # becomes 'globals()'

Note that the '/' MUST be the first character on the line! This won't work::

In [2]: print /globals # syntax error

In most cases the automatic algorithm should work, so you should rarely need to explicitly invoke /. One notable exception is if you are trying to call a function with a list of tuples as arguments (the parenthesis will confuse IPython)::

In [1]: zip (1,2,3),(4,5,6) # won't work

but this will work::

In [2]: /zip (1,2,3),(4,5,6)

------> zip ((1,2,3),(4,5,6))

Out[2]= [(1, 4), (2, 5), (3, 6)]

IPython tells you that it has altered your command line by displaying the new command line preceded by -->. e.g.::

In [18]: callable list

-------> callable (list)

2. Auto-Quoting You can force auto-quoting of a function's arguments by using ',' as the first character of a line. For example::

In [1]: ,my_function /home/me # becomes my_function("/home/me")

If you use ';' instead, the whole argument is quoted as a single string (while ',' splits on whitespace)::

In [2]: ,my_function a b c # becomes my_function("a","b","c")

In [3]: ;my_function a b c # becomes my_function("a b c")

Note that the ',' MUST be the first character on the line! This won't work::

In [4]: x = ,my_function /home/me # syntax error

____________________________________________________________________

OK, như các bạn thấy IPyIDA có khá nhiều tính năng và cần phải có thời gian để tìm hiểu dần. Để xóa các thông tin đã hiển thị ở trên, nhấn phím ESC.

Một tính năng hay của plugin này là nó cũng cấp khả năng tự động hoàn tất lệnh bằng phím TAB giống như bạn gõ lệnh ở terminal trên Linux (tính năng này không có ở thanh Python mặc định của IDA). Ví dụ: nếu tôi gõ imp và nhấn TAB, nó sẽ tự động autocomplete thành import. Sau đó tôi nhấn phím cách và nhấn TAB một lần nữa:

Như trên hình, ta thấy được toàn bộ các modules có thể import, sau đó sử dụng các mũi tên điều hướng  để lựa chọn modules cần import và thoát ra bằng cách nhấn ESC.

Khi tôi nhập dấu “?” một lần, nó cung cấp cho ta thông tin nhanh về module đó:

Và nếu tôi nhập dấu “?” hai lần nó sẽ hiển thị code như hình dưới đây:

Để thoát ta chỉ việc nhấn phím ESC. Bằng việc sử dụng các mũi tên lên và xuống, ta có thể đi đến các lệnh trước đó đã sử dụng. Hoặc sử dụng %hist để hiện thị thông tin lịch sử về các lệnh:

%edit sẽ mở ứng dụng notepad của Windows. Còn %edit x-y sẽ mở một notepad chứa các lệnh đã gõ nằm trong khoảng đó, tương tự như hình sau:

%history -n thêm số dòng bên cạnh các câu lệnh đã sử dụng:

Rõ ràng IPython khá mạnh và nó có rất nhiều câu lệnh mà bạn có thể tìm thấy ở đây: http://ipython.org/ipython-doc/3/index.html

Chúng ta sẽ làm một vài ví dụ đơn giản với plugin mới này.

Câu lệnh trên thực hiện lấy địa chỉ hiện tại của con trỏ. Nếu tôi sử dụng lệnh %edit như dưới đây, trình notepad của Windows sẽ được gọi lên để lưu các lệnh thành python script:

Sau đó, ta cho thực thi thông qua menu File-Script file, script sẽ cho kết quả tương tự:

Ngoài ra, lệnh idc.GetDisasm (ea) sẽ cung cấp cho chúng ta lệnh ASM tại nơi con trỏ đang đứng:

Nếu thay đổi con trỏ sang câu lệnh khác, ta sẽ phải tìm lại giá trị ea một lần nữa. Với câu lệnh idc.GetOpnd, ta có thể lấy được thông tin về toán hạng đầu tiên hoặc thứ hai của câu lệnh. Ví dụ như sau:

Đoạn code dưới đây thực hiện in ra tên của hàm hiện tại:

Tên của tất cả các hàm được liệt kê thông qua đoạn code sau:

Các câu lệnh bên trong hàm:

Tìm tham chiếu đến hàm. Nếu ta đặt con trỏ tại đầu của một hàm, sau đó nhấn X để tìm các xrefs đến hàm này thì có được kết quả như sau:

Ta hoàn toàn có thể sử dụng câu lệnh để làm được việc tương tự như trên, ví dụ:

Có thể thấy plugin này mang lại cho chúng ta rất nhiều tiện lợi và IDApython có hàng ngàn câu lệnh phục vụ để thiết lập các breakpoints, log, thực thi debugger, v..v…

Phần 13 đến đây là kết thúc. Hẹn gặp lại các bạn ở phần 14!

Xin gửi lời cảm ơn chân thành tới thầy Ricardo Narvaja!

m4n0w4r

Ủng hộ tác giả

Nếu bạn cảm thấy những gì tôi chia sẻ trong bài viết là hữu ích, bạn có thể ủng hộ bằng “bỉm sữa” hoặc “quân huy” qua địa chỉ:

Tên tài khoản: TRAN TRUNG KIEN
Số tài khoản: 0021001560963
Ngân hàng: Vietcombank