Unpack Themida Malware
Gần đây, Nhóm Viettel Threat Intelligence có nhận được một mẫu malware có nguồn gốc Trung Quốc. Theo quan sát của chúng tôi thì nhóm tin tặc này đã tấn công vào các cơ quan tổ chức ở Việt Nam ít nhất là từ năm 2013.
Mẫu thu được là một file được pack bằng Themida. Các file thực thi thường bị pack nhằm lẩn tránh các trình quét virus, hoặc nhằm cản trở việc phân tích của con người, việc unpack nhằm chống lại quá trình này. Themida là một packer phức tạp với nhiều tính năng. Chúng ta có thể thấy môt số tính năng này thông qua trang chủ của nhà cung cấp:
-
Anti-debugger techniques that detect/fool any kind of debugger
-
Different encryption algorithms and keys in each protected application
-
Anti-API scanners techniques that avoids reconstruction of original import table
-
Anti-disassember techniques for any static and interactive disassemblers
-
Multiple polymorphic layers with more than 50.000 permutations
-
Advanced API-Wrapping techniques
-
Anti-Memory patching and CRC techniques in target application
-
Many many more…
Những điều trên là những trở ngại gặp phải trong quá trình phân tích, ngoài ra còn có rất nhiều tính năng khác mà chúng tôi “may mắn” không gặp phải. Cũng bởi vì có rất ít tài liệu tham chiếu đề cập đến việc unpack themida, chúng tôi viết ra đây với mục đích giới thiệu một vài kỹ thuật nhỏ cho bài toán này.
Trong bài sẽ tập trung vào hai vấn đề chính mà chúng tôi cho là đáng quan tâm nhất đối với việc unpack một file PE, đó là tìm được Entry Point (nơi mà một chương trình bắt đầu việc thực thi) và khôi phục được bảng IAT ban đầu (Import Address Table, các hàm thư viện nhập mà một chương trình sử dụng đều được đặt trong bảng này). Ngoài ra bài viết cũng nêu ra cách mà Themida đánh bại trình Import Recontructor như Scylla
Nội dung của bài này có thể tóm tắt như sau.
1. Tìm Entry Point
Dump process và sử dụng IDA
2. Khôi phục bảng IAT
Các loại lệnh Call
Cách hoạt động của Scylla
Cách Themida bypass Scylla
Viết Script
1. Tìm Entry Point
Do packer luôn decrypt payload (payload là đoạn code và dữ liệu mà packer muốn che giấu) trước khi thực thi, ta cần chạy chương trình và dump trạng thái process mà chứa payload để phân tích sâu hơn. Để mà themida thực thi phần payload này ta cần vượt qua một vài kỹ thuật anti-debug. Các kỹ thuật anti-debug nhằm ngăn cản chúng ta debug một chương trình. Ở đây chúng tôi sử dụng ScyllaHide Plugin trong x64dbg và load profile Themida, điều này đảm bảo một vài kỹ thuật anti-debug bị vô hiệu hóa. Trong quá trình chạy có một vài Exception Privileged instruction do lệnh sti gây ra. Các exception này được tạo ra nhằm thay đổi luồng thực thi của tiến trình đang chạy như là một cơ chế anti-debug, nếu ở trong trình debugger.
Ta có thể mô tả cơ chế này:
push offset continue ; exception handler
push dword ptr fs:[0]
mov fs:[0], esp
sti ; exception
Khi một exception xảy ra, trình debugger của chúng ta sẽ có quyền quyết định xem là nó sẽ xử lý ngoại lệ này hay bỏ qua ngoại lệ này cho chương trình xử lý [1].
Chuỗi SEH (Structured Exception Handlers) lưu những handle để xử lý những exception này, nó được tham chiếu bởi fs:[0] hoặc ta cũng có thể xem trong mục SEH của x64dbg:
Ta chỉ cần ấn Shift+F9 để chuyển hết việc xử lý ngoại lệ cho Themida ( tức chuyển cho Exception Handler)
Để kiểm tra tiến trình đã chạy đến trạng thái payload hay chưa, ta kiểm tra phần code ở memory và phần code nằm trên đĩa:
Như hình trình ta có thể thấy phần code dump đã thay đổi so với file nằm trên đĩa , ta sẽ dump xuống tiến trình này để phân tích sâu hơn. Ta cần lưu ý ở đây là ta chỉ cần dump các section mà chứa code và data của payload nhằm tiết kiệm kích thước file (section chứa payload thường là phần thay đổi so với file ban đầu, file sau khi unpack cần được map lên đúng với trường Virtual Address trong Section Header)
Như trong hình ta chỉ cần dump xuống 2 section là “ ” (section này không có trường name) và “.rsrc. Ở đây ta không lấy phần header, bởi vì header giống với file bị packed nằm trên đĩa (việc không khôi phục hoặc xóa hoàn toàn header ngày càng được sử dụng phổ biến ở các biến thể mã độc, điều này nhằm ngăn cản việc dump xuống và phân tích file unpack, chúng ta cần sẵn sàng tâm thế để đối phó với trường hợp này).
Ta có thể xem file dump xuống là một file ở dạng binary không có header (tương tự shellcode), load file này vào IDA và đọc nó như là một file nhị phân 32 bit.
IDA có một tính năng rất hay là khả năng nhận dạng các hàm thư viện: FLIRT- Fast Library Identification and Recognition Technology [2] (IDA xây dựng signature cho mỗi thư viện và so sánh các hàm nhận ra bởi IDA với signature này để xác định hàm thư viện, thông thường IDA tự động apply các signature khi ta load PE vào IDA, nhưng đôi khi ta phải quyết định xem signature cần áp dụng là gì, ta cũng nên thường xuyên cập nhật các file signature hoặc tạo ra các signature mới để áp dụng cho trường hợp của chúng ta)
Ở đây, do biết được trình biên dịch sử dụng là MSVC (ta kiểm tra các string của binary dump xuống để trích xuất các thông tin), ta áp dụng signature cho thư viện này
Như hình sau đây ta thấy sự khác biệt trước và sau khi áp dụng FLIRT
Ta thấy FLIRT đã nhận diện được 329 hàm thư viện, ta có thể xác định vùng code người dùng là khoảng vùng nhớ bắt đầu từ offset 0. Ta chỉ cần tập trung phân tích vào vùng code này.
IDA cũng cấp cấp một tính năng khác là Xref, ta kiểm tra Xrefs graph to… cho các hàm mà chúng ta đã xác định ở trên, kết quả tương tự như thế này (với hàm sub_150):
Như chúng ta thấy các hàm đều bắt đầu thực thi từ sub_15A0, và từ sub_15A0 này không có ref nào đến cả.
Ta có thể giả thiết rằng đây là hàm main của chương trình.
Để chắc chắn hơn, ta kiểm tra hàm này trong debugger (VA = imagebase + offset+ 0x1000 do chúng ta không dump phần header):
CreateMutexA, WSAStartup là những hàm thường thấy để kiểm tra một phiên bản khác của malware có đang chạy không và để khởi tạo socket.
Nhưng cái chúng ta cần tìm là Entry Point. Vì sao IDA không nhận diện được Entry Point?
Việc tìm được hàm main giúp chúng ta nhanh chóng xác định được luồng logic của chương trình khi thao tác trong debugger, nhất là khi mà không khôi phục được file ban đầu. Ngoài ra hàm main cũng giúp chúng ta thu hẹp được khoảng cách tìm entry point: nó phải nằm đâu đó sau khi Themida điền địa chỉ thực vào bảng IAT ban đầu và trước khi gọi đến hàm main này.
Trở lại với câu hỏi, vì sao IDA không nhận ra Entry point. Trong các thuật toán disassembly (Liner Disassembly và Flow-Oriented Disassembly, IDA sử dụng Flow-Oriented Algorithm), chúng ta cần cung cấp một vị trí mà thuật toán bắt đầu quá trình quét các lệnh assembly từ vị trí đó. Ở file PE, vị trí bắt đầu là Entry Point, ở file nhị phân của chúng ta, có lẽ IDA bắt đầu từ vị trí offset 0.
Chúng ta cần phải tìm Entry Point bằng x64dbg.
Sau khi theo vết trong debugger (trong khoảng xác định ở trên), ta xác định được RVA Entry Point là:
0x4C80 (tương ứng với offset 0x3C80 trong biary file), ta tìm đến offset này, nhấn C để chuyển vùng nhớ này sang code, nhấn P để tạo function:
Ta thấy IDA đã nhận ra các hàm thư viện và gọi đến hàm sub_15A0 (hàm main ta đã xác định trước đó)
Đến đây ta đã xác định được Entry Point. Phần tiếp theo là cần xây dựng lại bảng IAT
2. Reconstruct IAT
Như đã nói ở trên, phần header của payload không được Themida khôi phục, chúng ta không có dữ liệu để khôi phục lại toàn bộ phần header, thay vào đó ta chỉ cần khôi phục một số trường quan trọng, các trường khác ta có thể có các trick để xử lý khác nhau. Các trường quan trọng: Entry Point, IAT, Export Table, TLS callback, Resource (nếu có)
Ở đây ta tìm cách khôi phục bảng IAT
Nếu ta dùng Scylla để xây dựng lại bảng IAT, cung cấp OEP cho Scylla, Scylla vẫn nhận ra được một số thư viện Import, tuy nhiên nếu ta dump xuống vẫn có vùng nhớ như thế này:
Nhìn vào hàm tương ứng trong x64dbg:
Các hàm API không được nhận ra bởi Scylla. Ở đây có 2 vấn đề liên quan là: Các loại của lệnh Call
và cách Scylla xây dựng lại bảng IAT
a, Các loại lệnh call [3]
Trong trường hợp này ta có thể chia lệnh call ra 2 loại: Call tương đối và call tuyệt đối.
Call tương đối là khi mà địa chỉ đích được tính toán qua độ lệch so với vị trí gọi, call này có opcode bắt đầu bằng E8
Ví dụ:
call 0x400000 (e8 fc ff 3f 00) sẽ nhảy đến vị trí mới có độ lệch 0x400000 so với vị trí gọi (EIP)
Call tuyệt đối là khi địa chỉ đích là một địa chỉ xác định, call này thường có opcode bắt đầu bằng FF 15
Ví dụ: call dword ptr 0x400000(ff 15 00 00 40 00) sẽ thực thi tại vị trí lưu tại 0x40000
Do mỗi hàm API import được lưu bằng 1 dword trong bảng IAT, dword này sẽ được điền bằng địa chỉ thực của import function mỗi khi file được load vào bộ nhớ. Lệnh call để gọi đến các import function bao giờ cũng là một lệnh gọi tuyệt đối và luôn được biểu diễn bằng 6 byte
Điều này cũng đúng khi áp dụng cho lệnh jmp-FF 25 ?? ?? ?? ?? (call= push+jump)
Ở đây ta nhìn vào cách các hàm API được gọi, ví dụ call Sleep (E8 99EEEF04) đây là cách gọi tương đối, chỉ được tính toán trong thời gian chạy, bởi không thể cố định được vị trí hàm Sleep ở mỗi lần nạp vào bộ nhớ.
Byte thừa ra được patch thành 0x90 (NOP)
Ở đây ta có thể thấy, Themida đã patch các lời gọi đến import function bằng cách tính toán lại độ lệch trong thời gian chạy
Ta cũng thấy một trường hợp khác:
Lời gọi tương đối đến 5030000 cũng bị patch bằng 0x90, kiểm tra ta thấy vùng nhớ 5030000 được cấp phát bằng VirtualAlloc, trong hàm này ta thấy bị obfuscate rất rõ, nhưng kết quả đều nhảy về một hàm API
Ta tạm gọi những hàm tương tự như 0x5030000 là trampoline.
b, How Scylla works?
Chúng ta cần xem lại cách Scylla xử lý dẫn đến sai lệch khi phục dựng lại IAT. Scylla là trình reconstruct IAT tương tự như ImpRec nhưng mã nguồn mở [4]
Chúng ta cung cấp cho Scylla OEP, nhấn nút IAT Autosearch, thì IAT sẽ trả lại giá trị ở 2 trường VA và Size. Khi chúng ta nhấn nút IAT Autosearch, IAT sẽ quét từ vị trí OEP để tìm ra vị trí mà có lệnh call tuyệt đối, vị trí này sẽ được check xem có là một API Pointer không bằng cách so sánh với một List APi được xây dựng từ các module được load vào
Vị trí này nếu thỏa mãn là cơ sở để tim ra vị trí bắt đầu và size của bảng IAT ban đầu
Scylla cũng cấp một tùy chọn Advance Search khác, dựa trên việc tìm tất cả các call tuyệt đối ở toàn bộ code memory section, sau đó loại bỏ các Pointer không hợp lệ, mục đích cũng chỉ là tìm ra được bảng IAT ban đầu
Khi ấn nút Get Imports, các địa chỉ thực của import function được tìm trong bảng IAT (bảng IAT là kết quả của bước trên), sẽ được chuyển thành tên hàm tương ứng
Sau cùng, những giá trị name import được tìm được này được sử dụng để tạo ra một section mới “.SYC” chứa các tên hàm vừa tìm được để IAT, hoặc OFT trỏ đến
Dù cho section .SYC được tạo, nó vẫn sử dụng bảng IAT cũ, nếu ta tick vào New IAT option, nó sẽ tạo ra bảng IAT mới, nhưng đồng thời cũng phải tính toán lại giá trị của hàm call. VD: call Sleep -> call [0x40E000] – FF15 000E4000 phải trở thành call Sleep-> call [0x40F000] – FF15 000F4000 nếu như IAT chuyển từ 40E000 -> 40F000
Ta có thể tóm tắt lại như sau:
Scylla sử dụng call tuyệt đối để tìm kiếm bảng IAT ban đầu
Bảng IAT ban đầu được chuyển từ địa chỉ hàm thành tên hàm được nhập ( trái ngược với hàm GetPocAddress)
c, How Themida Defeats Scylla?
Ở trên ta đã thấy Themida đã chuyển lời gọi call tuyệt đối thành call tương đối để ngăn cản Scylla tìm ra bảng IAT ban đầu, nhưng từng đó chưa đủ.
Ta có vài câu hỏi nữa: Làm sao tìm ra được các hàm nhập khi không có bảng IAT ban đầu? Có thể tìm được bảng IAT ban đầu mà không phụ thuộc vào Scylla không?
-
Ở câu hỏi thứ nhất, ta thấy rằng có thể quét vùng code tìm các lời gọi tương đối, kiểm tra đích đến có là địa chỉ của API import không, cách này đòi hỏi phải custom Scylla một chút
-
Ở câu hỏi thứ hai, chúng tôi đã thử tìm kiếm refer đến những hàm trampoline, để xem có vùng nhớ nào lưu con trỏ đến những hàm này không: Kết quả là những hàm này đều quy về một vùng nhớ:
Các địa chỉ trampoline được ngăn cách bởi dword 0. Ta có thể giả thiết rằng đây là vùng nhớ IAT ban đầu.
Ta có thể thấy cách làm của Themida:
-
Mỗi địa chỉ hàm import tạo ra một Trampoline tương ứng, trong Trampoline không thay đổi luồng thực thi của chương trình.
-
Tại mỗi lệnh call đến IAT sẽ được tính toán lại để trở thành lời gọi call tương đối tới Trampoline.
-
Bảng IAT dùng xong không xóa đi.
Đến đây do đã xác định được bảng IAT ban đầu, ta chỉ cần chuyển các con trỏ đến trampoline thành địa chỉ hàm API import, là định dạng mà Scylla có thể hiểu được.
Ta cũng cần chuyển những nơi mà gọi tương đối đến trampoline thành gọi tuyệt đối đến các DWORD trong IAT.
d, Viết Script
Ta có thể đếm số lần phải chuyển đổi bằng cách đếm số thực thể trong IAT, ta cũng có thể tìm bằng command trong x64dbg [5]:
Findall startAddress, “E8 ?? ?? ?? ?? 90” hoặc “90 ?? ?? ?? ?? E8” để đếm số lần xảy ra (hơn 200 trường hợp) để tự động hóa quá trình này ta sẽ dùng script của x64dbg để tận dụng các command của debugger này. X64dbg cũng mô tả đầy đủ các command trong document của nó. Ở đây ta thấy một số lệnh khả dụng:
dis.imm(Address) - trả về kết quả các lệnh call tương đối về địa chỉ đích (tức trampoline)
find address, “E8 ?? ?? ?? ?? 90” để tìm địa chỉ của hàm call tương đối đến trampoline
asm address, “ call [0x{x@eax}]” để chuyển tại vị trí address thành opcode call tuyệt đối đến địa chỉ được lưu trong eax. VD: asm 0400000, “call [0x{x@401000}]” sẽ chuyển opcode ở 0x0400000 thành FF15 00104000 (call dword ptr [401000])
Một vấn đề khác là ta cần xác định các trampoline trỏ đến API nào, ta thấy rằng một số hàm import trong Winhttp.dll hay ws2_32.dll không bị trampoline, chỉ những hàm trong kernel32 mới bị.
Điều này dựa trên thực tế là các hàm trong kernel32 chỉ xử lý các đối số truyền vào và chuyển tiếp đến các hàm mức thấp hơn như kernelbase.dll hoặc ntdll.dll.
Để tìm những API được gọi từ trampoline, ta sử dụng lệnh find để tìm những jmp hoặc call tuyệt đối đầu tiên được tìm thấy.
Kết quả sau khi fix lại IAT, ta thấy các địa chỉ trampoline đã chuyển thành các địa chỉ của các hàm import tương ứng và Scylla có thể đọc được những hàm này:
Bây giờ dùng Scylla để lấy tên các hàm, do đã biết vị trí chính xác của IAT ta điền trực tiếp địa chỉ này và size(IAT) cho Scylla xử lý. Ta thấy rằng có cả tên KernelBase.dll và Ntdll.dll trong tên thư viện import.
Điều này là hệ quả của việc chuyên tiếp từ Kernel32 sang kernelbase.dll, tuy nhiên ta chỉ cần lấy được tên hàm được nhập, còn tên thư viện có thể sửa thành kernel32 bằng các công cụ Manual PE File như CFF ExPlorer.
Bây giờ ta có thể load vào IDA:
Ta có thể thấy sự khác nhau trước và sau khi tìm được EP và khôi phục IAT
Sample: 163ca3c9bd63f4145161ce9364a31efb0207e400938e390251d373ed228283ec
References
[1] A. H. Michael Sikorski, “First and Second-Chance Exceptions,” in Practical Malware Analysis: The Hands-On Guide to Dissecting Malicious Software, p. 176.
[2] C. EAGLE, “Library recognition using FLIRT signatures,” in The IDA PRO Book, 2nd Edition.
[3] “Call Procedure,” [Online]. Available: https://c9x.me/x86/html/file_module_x86_id_26.html.
[4] “https://github.com/NtQuery/Scylla," [Online].
[5]“x64dbg Commands,” [Online]. Available: https://x64dbg.readthedocs.io/en/latest/commands/.