You are on page 1of 15

Đị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 tập tin).

1. Load tập tin 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 tập tin, đồng thời bỏ lựa chọn Create imports
segment (nên bỏ chọn khi làm việc với các tập tin packed). Kết quả sau khi load
vào IDA như hình dưới:
2. 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 tập tin bị packed thì địa chỉ EP
là 0x409be0, trong khi ở tập tin 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 tập tin, ta thấy rằng dưới header của
tập tin 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 tập tin gốc ban đầu.
Tập tin gốc

Tập tin bị PACKED


Theo quan sát trên hình, section UPX0 của tập tin bị packed kết thúc tại địa chỉ 0x409000,
trong khi toàn bộ các sections ở tập tin 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
tập tin gốc ta thấy các thông tin như sau:

Trong hình trên (Section size in tập tin) chiếm 0x600 byte, trong khi trong bộ nhớ
(Virtual size) chiếm 0x1000. Trở về tập tin 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 tập tin 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:

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

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

5. Tiếp theo bên dưới, ta thấy tập tin 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 đó:
6. 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.

7. 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 đó:

8. 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 Stub ở 0x00409BE0.
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ó tập tin 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ó tập tin 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.

9. Đặ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:

10. 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:

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


12. 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 ở tập tin 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).

13. Để 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:

14. 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:
Đến đây, chúng ta đã có thể thấy code tạm ổn rồi

Tuy nhiên, quan sát kĩ một chút ta sẽ thấy có sự khác biệt, ở tập tin 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
tập tin 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 tập tin gốc khi ta truy cập vào CALL
GetModuleHandleA:

15. 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 tập tin đang đang phân
tích thì sao? Hàm API đã đi đâu? Thử lần theo lệnh nhảy tại tập tin đ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 tập tin 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.
16. Tiếp theo, ta chuyển tới section đầu tiên bắt đầu tại địa chỉ 0x401000.

17. 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. Và vì 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 đã được đặt sẽ bao gồm toàn bộ section (0x8000 bytes).
Cụ thể như hình minh họa dưới đây:
18. 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:

19. Sau đó, nếu có breakpoint nào đã đặ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.

20. Nhấn chuột phải tại breakpoint và chọn Disable breakpoint, tương tự như hình:
21. Sau đó cho thực thi chương trình. Khi chương trình break, 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à
chúng ta đã tìm được:

22. 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:

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 tập tin.
 Và rebuild lại toàn bộ IAT để đảm bảo tập tin sau khi unpacked thực thi được một
cách bình thường như tập tin gốc ban đầu.

You might also like