## 1. Phishing * Mô hình tổng thể trải dài từ giai đoạn Phishing máy tính của nạn nhân cho đến giai đoạn máy tính nạn nhân thực thi mã độc rồi mã độc sẽ tự duy trì việc kiểm soát máy tính nạn nhân, sau đó thiết lập kết nối đến C2 Server của Attacker và Attacker sẽ thao tác với máy tính nạn nhân nhằm đánh cắp dữ liệu và các hành vi độc hại khác thông qua C2 Server được kết nối tới máy tính nạn nhân. ![image](https://hackmd.io/_uploads/BJuTBa-ap.png) * Chi tiết về từng bước hoạt động trong mô hình tổng thể của mình được đề cập ở trên như sau: * Đầu tiên, Attacker sẽ gửi một email phishing chứa đường link dẫn tới file Malware cho victim từ bên ngoài internet. * Sau khi Victim nhận được mail, tải file Malware về và thực thi file Malware trên máy tính, Malware sẽ yêu cầu kết nối đến C2 Server của Attacker và C2 Server sẽ thiết lập một session kết nối đến máy victim thông qua thông tin về máy Victim mà Malware lấy được bằng Win32API (Địa chỉ IP, Host name, …). * Sau khi thiết lập kết nối thành công, Attacker sẽ điều khiển máy victim gián tiếp bằng việc gửi lệnh đến cho C2 Server. ## 2. EXE :diamond_shape_with_a_dot_inside: **Check Mutant** * Khi mới bắt đầu thực thi, malware kiểm tra mutex **W34r3L3g10n** đã có tồn tại hay chưa ? Nếu mutex đã tồn tại thì nó ngay lập tức dừng việc thực thi còn ngược lại thì tạo mới mutex **W34r3L3g10n**. Bằng cách tạo và sử dụng mutex, malware có thể ngăn chặn việc nhiều bản sao của nó chạy đồng thời, giảm thiểu xung đột tài nguyên và tăng khả năng vượt qua các biện pháp bảo vệ. Hình bên dưới sẽ mô tả quá trình này một cách trực quan. ![image](https://hackmd.io/_uploads/HJbXTK2rT.png) * Cụ thể, malware sử dụng API **OpenMutexW()** với cờ **MUTEX_ALL_ACCESS** để mở mutex nếu như nó tồn tại. Nếu mutex chưa tồn tại trên hệ thống thì malware tạo mới mutex bằng API **CreateMutexW()**. Đoạn code bên dưới mô tả chi tiết chức năng này. ![image](https://hackmd.io/_uploads/HJ0_aLhSa.png) :diamond_shape_with_a_dot_inside: **Check Mouse Clicks** * Tiếp theo, malware kiểm tra số lần nhấp chuột phải và tiếp tục thực thi nếu số lần nhấp đã đủ **dwMinClicks**. Đây là một kỹ thuật trốn tránh khá tinh vi, nó dựa vào quan sát về hành vi của những người phân tích mã độc trong sandbox thường nhấp chuột ít hơn những người dùng máy tính bình thường. Đoạn code bên dưới mô tả chi tiết chức năng này. ![image](https://hackmd.io/_uploads/S14caL2BT.png) :diamond_shape_with_a_dot_inside: **Check SandBox** * Mỗi sandbox bất kỳ luôn có những tiến trình đặc trưng riêng, lí do của việc này có thể là để quản lí môi trường riêng cho nó. Hơn nữa, các tiến trình như **tcpview.exe**, **x64dbg.exe**, **wireshark.exe** thường xuất hiện trong sandbox với vai trò như là các công cụ phân tích mã độc. Dựa vào đặc điểm này, malware đã sử dụng một kỹ thuật anti-sandbox khác để né tránh việc bị phân tích. Hình bên dưới mô tả quá trình này một cách trực quan. ![image](https://hackmd.io/_uploads/HJUQ9K2Sp.png) * Cụ thể, malware sử dụng API **EnumProcesses()** để liệt kê các tiến trình đang có trong hệ thống. Sau đó, malware lấy tên của các tiến trình bằng hai API **EnumProcessModules()** và **GetModuleBaseName()**. Cuối cùng, malware kiểm tra các tiến trình hiện đang chạy trên hệ thống với danh sách **SandboxProcesses**, nếu như các tiến trình này không thuộc vào danh sách SandboxProcesses thì malware tiếp tục thực thi các chức năng tiếp theo. Đoạn code bên dưới mô tả chi tiết chức năng này. ![image](https://hackmd.io/_uploads/HJ66T82H6.png) ![image](https://hackmd.io/_uploads/rkyl083Sp.png) ![image](https://hackmd.io/_uploads/SJD-A83BT.png) * Đoạn code bên dưới mô tả chức năng của hàm **IsSandboxProcess()**. Chức năng của hàm này là duyệt qua danh sách SandboxProcesses chứa tên của các tiến trình đặc trưng trong sandbox và thực hiện kiểm tra tiến trình đang được duyệt có đang thuộc vào danh sách này hay không ? ![image](https://hackmd.io/_uploads/r1sb95hHT.png) :diamond_shape_with_a_dot_inside: **Check Parent Process** * Tiếp theo, malware kiểm tra tiến trình cha của nó có phải là services.exe hay không? Nếu đúng như vậy thì malware đang được chạy như là một service trong hệ thống. Hình bên dưới mô tả sơ đồ phân cấp tiến trình trong hệ thống windows, có thể thấy rằng mọi dịch vụ đang có trong hệ thống đều được tạo ra bởi tiến trình **services.exe**. ![image](https://hackmd.io/_uploads/rJSeNSjHa.png) * Nếu malware đang được chạy dưới dạng là một service thì nó đóng vai trò là injector để thực hiện chức năng chèn mã. Còn nếu như malware đang được chạy dưới dạng là một chương trình thực thi bình thường thì điều này có nghĩa rằng nó đang được chạy lần đầu, vì lí do này nên nó sẽ thực thi chức năng như là một dropper. * Cụ thể, malware sử dụng bộ ba API **CreateToolhelpSnapshot()**, **Process32First()**, và **Process32Next()** để liệt kê các tiến trình đang có trên hệ thống. Ngoài ra, malware còn sử dụng API **QueryFullProcessImageName()** để lấy tên của tiến trình nhằm mục đích kiểm tra tiến trình hiện tại có phải là services.exe hay không ? Cuối cùng malware sẽ thực thi API **StartServiceCtrlDispatcher()** nếu như nó là injector và gọi hàm **RunDropper()** nếu như nó đóng vai trò là dropper. Đoạn code bên dưới mô tả chi tiết chức năng này. ![image](https://hackmd.io/_uploads/B1PICI3Sa.png) ![image](https://hackmd.io/_uploads/BJf_AInSa.png) ![image](https://hackmd.io/_uploads/rygsRLhSa.png) :diamond_shape_with_a_dot_inside: **Dropper Functionality** * Chức năng của dropper về cơ bản là thực hiện copy chính nó vào trong thư mục **C:\Windows\System32\\** với tên là **DnsCon.exe**. Sau đó, dropper đăng ký một service mới có tên hiển thị là **Remote DNS Configuration** và có đường dẫn là **C:\Windows\System32\DnsCon.exe** để nhằm mục đích để thực hiện cơ chế persistence. Cuối cùng, dropper tự xóa đi chính nó để tránh bị phát hiện. Hình bên dưới sẽ mô tả quá trình này một cách trực quan. ![image](https://hackmd.io/_uploads/SkvLMzirp.png) * Cụ thể, malware sử dụng hai API **GetModuleFileName()** và **GetSystemDirectory()** để lấy đường dẫn thư mục system và tên file thực thi của nó. Sau đó, malware tiến hành sao chép và xóa chính nó bằng API **CopyFile()** và **DeleteFile()**. Cuối cùng, malware gọi hai API **OpenSCManager()** và **CreateService()** để tạo mới một malicious service thực hiện chức năng của injector. Đoạn code bên dưới mô tả chi tiết chức năng này. ![image](https://hackmd.io/_uploads/SyIJkPnH6.png) ![image](https://hackmd.io/_uploads/ByPeJv2Bp.png) :diamond_shape_with_a_dot_inside: **Injector Functionality** * Sau khi dropper thực hiện xong chức năng của nó thì ở những lần khởi động máy tính tiếp theo của nạn nhân sẽ luôn có một service được tự động chạy để thực hiện chức năng của một injector. * Đầu tiên, injector trích xuất encrypted .DLL có trong .rsrc section. Sau đó, injector sử dụng một bộ decrytor có sẵn để giải mã .DLL. Cuối cùng, injector sử dụng .DLL đã được giải mã để thực hiện kỹ thuật reflective DLL injection lên tiến trình explorer.exe đang có trong hệ thống. Hình bên dưới sẽ mô tả quá trình này một cách trực quan. ![image](https://hackmd.io/_uploads/ryisMfiBa.png) * Đoạn code bên dưới mô tả chức năng của hàm **ServiceMain()**, đây được xem như là entry point của một service bất kỳ. Về cơ bản, ServiceMain() gọi API **RegisterServiceCtrlHandler()** để đăng ký một service control handler, một service control handler có chức năng xử lý các control command như Stop, Pause, Continue, .... và những control command được đăng ký sẽ biểu diễn như là một bitmask thông qua trường dwControlsAccepted của cấu trúc SERVICE_STATUS. Tiếp theo service gọi hàm **RunInjector()** để thực hiện chức năng của injector và cuối cùng sẽ đặt trạng thái của service hiện tại thành **SERVICE_RUNNING**. ![image](https://hackmd.io/_uploads/ryNHJDhBp.png) ![image](https://hackmd.io/_uploads/HyUU1v3ra.png) * Đối với chức năng của control handler thì nó chỉ triển khai và hỗ trợ request **SERVICE_CONTROL_STP** và **SERVICE_CONTROL_SHUTDOWN**. Những service ngoài thực tế thường được triển khai để xử lý nhiều request như là **SERVICE_CONTROL_CONTINUE**, **SERVICE_CONTROL_INTERROGATE**, **SERVICE_CONTROL_PAUSE**, và **SERVICE_CONTROL_SHUTDOWN**. Trong thực tế, malware chỉ chủ yếu sử dụng service như là công cụ để đạt được persistence nên nó không nhất thiết phải định nghĩa các hàm xử lý request một cách đầy đủ, nó chỉ cần định nghĩa các thành phần chính để triển khai service là được. ![image](https://hackmd.io/_uploads/B1SY1vnra.png) * Sau khi service đã chuẩn bị xong môi trường để thực thi thì nó sẽ gọi hàm **RunInjector()** để thực hiện chức năng của injector. Đầu tiên injector sẽ trích xuất encrypted .DLL trong .rsrc section bằng các API **FindResource()**, **LoadResource()**, **SizeofResource()**, và **LockResource()**. Sau đó, injector gọi hàm **DecryptDLL()** để giải mã .DLL và lưu .DLL này vào bộ nhớ được cấp phát bằng API **HeapAlloc()**. Quá trình vừa nêu trên được thể hiện qua đoạn code dưới đây. ![image](https://hackmd.io/_uploads/SJwa1PnBa.png) ![image](https://hackmd.io/_uploads/SkrZlP3B6.png) * **DecryptDLL()** có chức năng như một bộ decryptor được dùng để giải mã encrypted .DLL bằng thuật toán AES-128. Đầu tiên, injector mở một algorithm handle để sử dụng nó trong các thao tác giải mã. Tiếp theo, injector gọi các API **BCryptGetProperty()**, **BCryptSetProperty()** để lấy thông tin về độ lớn của key, IV và để xác định thuật toán sẽ sử dụng là **AES-128 ở mode CBC**. Cuối cùng, injector gọi API **BCryptDecrypt()** để giải mã .DLL và lưu nó vào bộ nhớ được cấp phát trước đó. Đoạn code bên dưới mô tả chi tiết chức năng của **DecryptDLL()**. ![image](https://hackmd.io/_uploads/Bkz2xvhSa.png) ![image](https://hackmd.io/_uploads/HyC1Zvnra.png) ![image](https://hackmd.io/_uploads/rk0bbD2rp.png) ![image](https://hackmd.io/_uploads/BJ5mZPhB6.png) * Sau khi có được .DLL hoàn chỉnh, injector cần có quyền debug để có thể tạo thread hoặc thao tác trên bộ nhớ của các tiến trình khác. Vì injector cần phải sửa đổi bộ nhớ của explorer.exe trong quá trình thực hiện kỹ thuật reflective DLL injection nên nó bắt buộc phải có được quyền debug trước khi thực hiện kỹ thuật này. Injector đã sử dụng bộ ba API là **OpenProcessToken()**, **LookupPrivilegeValue()**, và **AdjustTokenPrivileges()** để thêm quyền debug vào token của nó. ![image](https://hackmd.io/_uploads/Hkl_Ww2B6.png) * Sau khi injector đã đạt được các điều kiện cần thiết, nó tiến hành lấy handle của explorer.exe bằng cách duyệt qua các tiến trình đang có trên hệ thống và sau đó gọi hàm **LoadRemoteLibraryR()** để bắt đầu quá trình tiêm mã. ![image](https://hackmd.io/_uploads/HJs9ZwnHT.png) ![image](https://hackmd.io/_uploads/B1UnZwhHa.png) * Trong hàm **LoadRemoteLibrary()**, injector đi tính toán offset của reflective loader tính từ vị trí bắt đầu của raw .DLL bằng hàm **GetReflectiveLoaderOffset()**. Sau đó, injector cấp phát bộ nhớ trên explorer.exe bằng API **VirtualAllocEx()** và chèn raw .DLL vào bộ nhớ vừa cấp phát bằng API **WriteProcessMemory()**. Cuối cùng, injector tạo một thread mới trong explorer.exe bằng API **CreateRemoteThread()** để thực thi reflective loader có trong raw .DLL vừa chèn. * Nhưng injector đã tính toán offset của reflective loader trong raw .DLL như thế nào ? Bản chất của reflective loader là một hàm trong .DLL được export ra ngoài, để có thể trích xuất các thông tin của export function thì ta cần phải dựa vào export directory có trong .DLL. Để giải thích được cách mà hàm **GetReflectiveLoaderOffset()** tính toán offset của reflective loader trong raw .DLL như thế nào thì trước tiên cần phải làm rõ khái niệm export directory là gì ?. ![image](https://hackmd.io/_uploads/HyhYylor6.png) ![image](https://hackmd.io/_uploads/H1dikeiHT.png) * Export directory được biểu diễn bằng cấu trúc **IMAGE_EXPORT_DIRECTORY**, cấu trúc này lưu trữ các trường thông tin quan trọng của các hàm và biến trong PE file được export ra bên ngoài. Các hàm và biến được export từ PE file này có thể được truy xuất và sử dụng bởi các PE file khác. Ví dụ: trong kernel32.dll định nghĩa rất nhiều export function được sử dụng chung bởi nhiều file thực thi khác trong hệ thống. Dưới đây là ý nghĩa của các trường trong cấu trúc **IMAGE_EXPORT_DIRECTORY**: * TimeDateStamp: trường này mô tả thời gian gần nhất mà module được cập nhật hoặc sửa đổi. * Name: chứa địa chỉ trỏ đến tên của module. * Base: chứa giá trị là một số nguyên và giá trị này được sử dụng trong quá trình tính toán RVA của export function bằng ordinal. * NumberOfFunctions: chứa số lượng export function của module. * NumberOfNames: chứa số lượng các export function có tên cụ thể. Giá trị này luôn bé hơn hoặc bằng NumberOfFunctions. * AddressOfFunctions: chứa địa chỉ của export address table (EAT), EAT là bảng chứa các RVA của export function. Số lượng phần tử trong bảng này luôn bằng giá trị của NumberOfFunctions. * AddressOfNames: chứa địa chỉ của mảng lưu trữ RVA của các tên hàm . Phải nhớ là số lượng phần tử trong mảng này luôn bằng giá trị của NumberOfNames. * AddressOfNamesOrdinals: là mảng chứa index của EAT. * Hình dưới mô tả chi tiết các trường trong cấu trúc **IMAGE_EXPORT_DIRECTORY**. ![image](https://hackmd.io/_uploads/HyB7T8hB6.png) * Sau khi nêu xong ý nghĩa của các trường có trong export directory thì tiếp theo cần đi làm rõ cơ chế để trích xuất RVA của một export function bất kỳ có trong module. Dưới đây là các bước cụ thể để tìm được RVA của export function: * Bước 1: ta duyệt qua từng phần tử của export name pointer table để trích xuất các RVA. Giá trị RVA này trỏ đến tên của một export function chứa trong function name table. * Bước 2: ta duyệt qua từng tên của export function để tìm kiếm export function mà ta mong muốn và đồng thời trích xuất index của function name table ở nơi mà hàm này được định vị. * Bước 3: tìm kiếm địa chỉ của ordinal array bằng trường AddressOfNamesOrdinals. * Bước 4: trích xuất giá trị của một phần tử trong ordinal array bằng giá trị index được tìm thấy ở bước 2. * Bước 5: tìm kiếm địa chỉ của EAT bằng trường AddressOfFunctions. * Bước 6: sử dụng giá trị ordinal ở bước 4 để làm index trong EAT, giá trị của phần tử tại vị trí index này chính là RVA của export function ta đang tìm kiếm. * Hình dưới mô tả một cách trực quan về các bước thực hiện vừa nêu trên. ![image](https://hackmd.io/_uploads/ryvuJdsS6.png) * Nhưng đến đây vẫn chưa xong bởi vì toàn bộ quá trình vừa nêu trên chỉ cung cấp RVA của một export function chứ không phải là file offset của nó. * Trước khi làm rõ vấn đề trên, cần hiểu rõ cấu trúc của section table. Section table là bảng gồm nhiều entry, mỗi entry của nó chứa một section header. Section header được biểu diễn bằng cấu trúc **IMAGE_SECTION_HEADER**, nó chứa các trường thông tin quan trọng của một section. Một số trường thông tin quan trọng của section header được liệt kê dưới đây: * VirtualAddress: chứa RVA của section. * VirtualSize: chứa độ lớn của section khi được nạp lên virtual memory. * SizeOfRawData: chứa độ lớn của section trong image file. * PointerToRawData: chứa file offset của section. * Name: là mảng 8 byte chứa tên của section. * Hình dưới mô tả chi tiết các trường trong cấu trúc **IMAGE_SECTION_HEADER**. ![image](https://hackmd.io/_uploads/HJWSl4hHT.png) * Để tính toán file offset của reflective loader thì cần tính tổng file offset của section và offset của reflective loader tính từ đầu section là được. Giá trị file offset của section chứa reflective loader chính là trường PointerToRawData trong section header đã đề cập ở trên. Còn offset của reflective loader tính từ đầu section được tính bằng cách lấy RVA của reflective loader trừ cho RVA của section chứa nó là được. Hình dưới mô tả biểu thức để chuyển đổi RVA sang file offset (RAW). ![image](https://hackmd.io/_uploads/S1qJFPsB6.png) * Hình bên dưới mô phỏng quá trình chuyển đổi trên một cách trực quan. ![image](https://hackmd.io/_uploads/rybdgMsra.png) * Dưới đây là đoạn code được dùng để tính toán file offset của reflective loader dựa vào ý tưởng đã nêu. ![image](https://hackmd.io/_uploads/BJUOrE3H6.png) ![image](https://hackmd.io/_uploads/rJDqSEnBa.png) ![image](https://hackmd.io/_uploads/BkK3B4nS6.png) ![image](https://hackmd.io/_uploads/HymJIN2S6.png) ![image](https://hackmd.io/_uploads/S1ue8V2Sp.png) * Đoạn code dưới đây có chức năng chuyển đổi RVA sang RAW. ![image](https://hackmd.io/_uploads/BJlavNnSa.png) ![image](https://hackmd.io/_uploads/rkbbdV3H6.png) ## 3. DLL. :diamond_shape_with_a_dot_inside: **Reflective Loader Functionality** :black_medium_square: **Tính toán base address cho raw .DLL** * Việc quan trọng đầu tiên mà reflective loader cần làm là tính toán giá trị base address của raw .DLL bởi vì giá trị này được sử dụng rất nhiều ở các bước tiếp theo. Trước tiên đi vào chi tiết cách thức để tìm base address của raw .DLL, cần làm rõ chức năng của hàm caller() được sử dụng bởi malware. * Dựa vào đoạn code bên dưới, có thể thấy caller() trả về giá trị trả về của hàm **_ReturnAddress()**. Mặc khác, hàm _ReturnAddress() lại trả về giá trị địa chỉ câu lệnh kế tiếp của câu lệnh gọi hàm caller(). ![image](https://hackmd.io/_uploads/HkANiShB6.png) * Hình bên dưới mô tả chức năng của hàm caller() một cách trực quan hơn. Dễ thấy rằng câu lệnh kế tiếp của lệnh gọi hàm caller() có địa chỉ là 0x2117 cho nên giá trị trả về của hàm caller() chính là giá trị 0x2117. ![image](https://hackmd.io/_uploads/By88Twora.png) * Dựa vào chức năng của hàm **caller()** vừa nêu trên, malware có thể lấy được địa chỉ của một lệnh assembly bất kỳ trong hàm ReflectiveLoader() và dựa vào địa chỉ này để tìm được base address của raw .DLL. Để tìm được base address của raw .DLL, chỉ cần đi ngược lại từ địa chỉ của lệnh assembly vừa tìm được cho đến khi nào phát hiện ra DOS signature là xong. Hình bên dưới mô tả quá trình này một cách chi tiết. ![image](https://hackmd.io/_uploads/rySEV-nr6.png) * Dựa vào hình dưới, có thể thấy DOS signature là trường đầu tiên trong DOS header mà DOS header lại là header đầu tiên của PE file cho nên vị trí của DOS signature cũng chính là base address đang cần tìm. DOS signature là trường 2 byte và có giá trị 0x4D5A. ![image](https://hackmd.io/_uploads/rkz2TmnH6.png) * Đoạn code bên dưới mô tả chi tiết quá trình trên. Cụ thể, malware giảm dần từng byte địa chỉ và thực hiện kiểm tra 2 byte tại địa chỉ này có bằng 0x4D5A hay không ? Nếu đúng là vậy thì malware tiếp tục kiểm tra giá trị trường **e_lfanew** của địa chỉ đang duyệt để xem giá trị này có lớn hơn hoặc bằng độ lớn của DOS header và bé hơn 1024 hay không ? Nếu thỏa mãn điều kiện thì địa chỉ đang duyệt chính là base address của raw .DLL. Điều kiện trên được sinh ra là vì trường e_lfanew lưu offset tính từ đầu DOS header đến vị trí bắt đầu của PE header và độ lớn tối đa của DOS header cộng với DOS stub luôn bé hơn 1024 byte cho nên giá trị trường e_lfanew cũng phải thuộc trong khoảng này. ![image](https://hackmd.io/_uploads/B1rQoB2S6.png) ![image](https://hackmd.io/_uploads/r1ZDirnH6.png) ![image](https://hackmd.io/_uploads/HkDKjS3Sp.png) :black_medium_square: **Tính Toán Địa Chỉ Của Các API Cần Dùng** * Reflecttive loader cần phải sử dụng một số API để thực hiện chức năng của nó cho nên trước khi thực hiện các chức năng này, nó cần tính toán địa chỉ của các API cần thiết. Cụ thể, reflective loader cần tính toán địa chỉ của 3 API có trong kernel32.dll là **LoadLibrary()**, **GetProcAddress()**, và **VirtualAlloc()**. Ngoài ra, reflective loader cũng cần tính toán địa chỉ của API **NtFlushInstructionCache()** có trong ntdll.dll, chức năng của API này sẽ được đề cập sau. * Để tính toán được địa chỉ của các API nêu trên, chỉ cần tính toán được base address của kernel32.dll và ntdll.dll. Sau đó, dùng offset và base address vừa có được để tính toán địa chỉ của các API này. * Vậy làm thế nào để tìm được base address của kernel32.dll và ntdll.dll ? Đầu tiên, phải đề cập đến cấu trúc Thread Environment Block (TEB), mỗi thread đều sở hữu riêng một TEB được lưu trên bộ nhớ để lưu trữ các thông tin quan trọng về nó. Trong chương trình 32 bit của Windows thì địa chỉ TEB được lưu trong thanh ghi fs. * Có một trường thuộc TEB tại offset 0x30 trong chương trình 32 bit chứa địa chỉ của cấu trúc Process Environment Block (PEB). Cấu trúc PEB chứa nhiều thông tin quan trọng của tiến trình bao gồm cả các .DLL mà tiến trình này sử dụng. Hình dưới hiển thị một vài trường quan trọng trong cấu trúc TEB. ![image](https://hackmd.io/_uploads/r1QKGDiBT.png) * Tại offset 0xC của cấu trúc PEB là trường Ldr, nó là con trỏ đến cấu trúc PEB_LDR_DATA và cấu trúc này chứa các thông tin về những DLL được nạp vào bộ nhớ. Cấu trúc PEB_LDR_DATA chứa trường InMemoryOrderModuleList tại offset 0x14, nó là con trỏ trỏ đến danh sách liên kết đôi mà mỗi phần tử trong danh sách này lưu trữ thông tin về một .DLL được nạp lên bộ nhớ của tiến trình ở thời điểm hiện tại và thứ tự của các phần tử có trong danh sách được sắp xếp theo thứ tự xuất hiện của các DLL trong bộ nhớ. Hình dưới hiển thị một vài trường quan trọng trong cấu trúc PEB. ![image](https://hackmd.io/_uploads/rJAJmDsS6.png) * Hình dưới hiển thị một vài trường quan trọng trong cấu trúc PEB_LDR_DATA. ![image](https://hackmd.io/_uploads/r1HMXvjH6.png) * Đến đây, reflective loader chỉ cần duyệt qua từng phần tử được biểu diễn bởi cấu trúc LDR_DATA_TABLE_ENTRY trong danh sách liên kết đôi để so sánh tên của .DLL cần tìm với trường BaseDllName tại offset 0x2c có tương ứng với nhau hay không ?. Nếu có thì malware trích xuất base address của .DLL đang duyệt thông qua trường DllBase tại offset 0x18. ![image](https://hackmd.io/_uploads/HJoj7vjSp.png) * Hình dưới đây mô tả sự liên kết giữa các cấu trúc được sử dụng trong quá trình trích xuất địa chỉ cho các .DLL. ![image](https://hackmd.io/_uploads/SyQRgwoST.png) * Sau khi có được base address của kernel32.dll và ntdll.dll bằng cách duyệt qua danh sách .DLL được tải bởi explorer.exe. Reflective loader tiếp tục tính toán RVA của các API cần tìm bằng cách sử dụng cấu trúc export directory như đã trình bày ở trên. Đoạn code bên dưới trình bày chi tiết việc tính toán địa chỉ cho các API cần dùng bởi malwarwe. ![image](https://hackmd.io/_uploads/HJT3iSnS6.png) ![image](https://hackmd.io/_uploads/H1OenH3Ba.png) ![image](https://hackmd.io/_uploads/rkffhrnB6.png) ![image](https://hackmd.io/_uploads/SkDd3H2H6.png) ![image](https://hackmd.io/_uploads/HkLihB2BT.png) ![image](https://hackmd.io/_uploads/SkMR3S2rp.png) ![image](https://hackmd.io/_uploads/S1TZTB3HT.png) ![image](https://hackmd.io/_uploads/H1Ewarhra.png) ![image](https://hackmd.io/_uploads/Bkrt6H2Sa.png) ![image](https://hackmd.io/_uploads/B1A2aBhS6.png) :black_medium_square: **Cấp Phát Bộ Nhớ Và Tải PE Header** * Sau khi có được địa chỉ của các API cần thiết, reflective loader cấp phát bộ nhớ trong explorer.exe bằng API **VirtualAlloc()** để nạp image từ raw .DLL. Bước đầu tiên của quá trình này là nạp PE header. ![image](https://hackmd.io/_uploads/SJ1sAH3Sp.png) ![image](https://hackmd.io/_uploads/Sk02AH2Bp.png) :black_medium_square: **Tải Các Section Vào Bộ Nhớ** * Sau đó, reflective loader tiến hành nạp các section của raw .DLL vào bộ nhớ được cấp phát. Đoạn code bên dưới thực hiện chi tiết quá trình này. ![image](https://hackmd.io/_uploads/SkesJU3Ba.png) ![image](https://hackmd.io/_uploads/Syb6y83rT.png) :black_medium_square: **Phân Giải Địa Chỉ Trong Import Address Table** * Sau khi reflective loader hoàn thành việc nạp header và các section, nó tiếp tục cần phân giải địa chỉ trong IAT (import address table) của .DLL. Nhưng trước tiên, cùng đề cập cách mà windows loader phân giải IAT và từ đó áp dụng những nguyên lý này để giải thích chức năng của reflective loader. * Khái niệm đầu tiên cần được nhắc đến là import directory, đây là một bảng chứa nhiều entry và entry cuối cùng trong bảng không được sử dụng. Mỗi entry trong import directory tương ứng với một .DLL được nạp lên tiến trình tại thời điểm chạy và thông tin của .DLL này được biểu diễn bằng cấu trúc IMAGE_IMPORT_DESCRIPTOR. * Trong cấu trúc IMAGE_IMPORT_DESCRIPTOR, cần quan tâm đến ba trường là OriginalFirstThunk, FirstThunk, và Name. OriginalFirstThunk, FirstThunk, và Name lần lượt lưu trữ RVA của import name table (INT), import address table (IAT), và tên của .DLL tương ứng. Cấu trúc chi tiết của IMAGE_IMPORT_DESCRIPTOR được hiển thị như ở hình dưới. ![image](https://hackmd.io/_uploads/SJpc2m3S6.png) * INT và IAT đều được biểu diễn chung bởi cấu trúc IMAGE_THUNK_DATA32, cấu trúc này được biểu diễn dưới dạng là một union gồm có bốn thành phần là ForwarderString, Function, Ordinal, AddressOfData. * Trong ngữ cảnh của INT, nếu như windows loader dùng giá trị ordinal để phân giải địa chỉ của API trương ứng thì trường Ordinal sẽ được sử dụng, còn nếu như windows loader phân giải địa chỉ IAT bằng cách khác thì trường AddressOfData sẽ được sử dụng để trỏ đến một đối tượng có cấu trúc là IMAGE_IMPORT_BY_NAME. * Trong ngữ cảnh của IAT, trường ForwarderString sẽ được sử dụng nếu như API đang được phân giải địa chỉ được định nghĩa ở một .DLL khác so với .DLL đang được duyệt và lúc này giá trị của ForwarderString trỏ đến tên một hàm được chuyển tiếp từ một .DLL khác, còn trường Function sẽ được sử dụng để lưu trữ địa chỉ của API sau khi đã phân giải xong. * Kết luận lại, có hai cách để phân giải địa chỉ trong IAT đó là sử dụng giá trị ordinal và tên của API tương ứng. ![image](https://hackmd.io/_uploads/S1eidbRara.png) * Cấu trúc IMAGE_IMPORT_BY_NAME được biểu diễn bởi hai trường chính là Hint và Name. Windows loader có thể sử dụng trường Hint hoặc trường Name để phân giải địa chỉ cho IAT. * Đối với trường Hint, windows loader sử dụng giá trị của trường này để làm index vào function name table trong export directory đã được đề cập trước đó và tiến hành phân giải địa chỉ. * Còn đối với trường Name, windows loader thực hiện phân giải địa chỉ bằng tên API tương ứng. Dễ thấy, cách phân giải IAT bằng trường Name tốn nhiều thời gian hơn cách phân giải IAT bằng trường Hint là vì cách sử dụng trường Hint không cần thiết phải thực hiện tìm kiếm nhị phân index trong function name table như cách sử dụng trường Name mà thay vào đó nó xem giá trị trường Hint như là index cần tìm. * Hình bên dưới mô tả chi tiết cấu trúc IMAGE_IMPORT_BY_NAME. ![image](https://hackmd.io/_uploads/SJyW0Q2ra.png) * Cơ chế phân giải IAT đang được đề cập được thực hiện ở runtime và nó được gọi là unbound import. Ngoài ra, có một cơ chế phân giải IAT tại thời điểm linking được gọi là bound import. * Trong cơ chế bound import thì khi windows loader tải chương trình lên bộ nhớ thì nó bắt đầu kiểm tra tính hợp lệ của các địa chỉ sẵn trong IAT. Lí do cho việc này là vì các .DLL chứa API mà chương trình sử dụng có thể bị sửa đổi hoặc cập nhật nên việc này dẫn đến các địa chỉ trong IAT lúc này không còn đúng nữa. Nếu các địa chỉ trong IAT không còn đúng nữa thì nó sẽ dùng các entry trong bảng INT để phân giải lại các địa chỉ không hợp lệ và điền vào lại IAT, có thể thấy INT đóng vai trò rất quan trọng trong việc triển khai cơ chế bound import. Hình dưới là chi tiết các trường trong cấu trúc IMAGE_BOUND_IMPORT_DESCRIPTOR, cấu trúc này được sử dụng trong quá trình phân giải IAT tại linking-time. ![image](https://hackmd.io/_uploads/HyeMamhS6.png) * Các bước cụ thể để phân giải IAT dựa vào các khái niệm đã nêu trên: * Bước 1: kiểm tra tên .DLL đang duyệt có phải là .DLL cần tìm kiếm hay không ? Nếu đúng thì chuyển sang bước 2. * Bước 2: thực hiện API LoadLibrary() để tải .DLL nếu như .DLL chưa xuất hiện trong bộ nhớ hiện tại của tiến trình. * Bước 3: truy xuất INT bằng RVA được lưu trong trường OriginalFirstThunk của cấu trúc IMAGE_IMPORT_DESCRIPTOR. * Bước 4: duyệt qua từng entry trong INT để thực hiện phân giải IAT. Ở bước này, có thể sử dụng giá trị ordinal hoặc hint/name table để phân giải IAT. * Bước 5: nếu sử dụng hint/name table để phân giải IAT thì tại bước này API GetProcAddress() sẽ được gọi để lấy địa chỉ của API tương ứng. * Bước 6: truy xuất IAT bằng RVA được lưu trong trường FirstThunk của IMAGE_IMPORT_DESCRIPTOR. * Bước 7: gán địa chỉ thu được ở bước 5 vào entry tương ứng trong IAT. ![image](https://hackmd.io/_uploads/B1GMnXnr6.png) * Đoạn code bên dưới thực hiện chi tiết quá trình phân giải IAT của reflective loader dựa vào những khái niệm và cách thức được nêu trước đó. ![image](https://hackmd.io/_uploads/SycDXLnBT.png) ![image](https://hackmd.io/_uploads/r1jYmI2H6.png) ![image](https://hackmd.io/_uploads/H1firU2H6.png) ![image](https://hackmd.io/_uploads/HJG788hrT.png) :black_medium_square: **Relocation** * Sau khi hoàn thành quá trình phân giải IAT, reflective loader tiếp tục thực hiện quá trình relocation. Quá trình relocation là cực kỳ quan trọng vì image base của explorer.exe khi nạp vào bộ nhớ có thể khác image base của raw .DLL. Nếu không thực hiện quá trình relocation một cách đúng đắn thì quá trình thực thi của explorer.exe có thể gây ra các lỗi nghiêm trọng. * Để biết những vị trí nào cần thực hiện relocation, chỉ cần truy xuất vào base relocation table. Base relocation table chứa các relocation block, mỗi relocation block chứa những thông tin về các vị trí cần được relocation trong một page (độ lớn 4KB). Một relocation block được biểu diễn bởi cấu trúc là IMAGE_BASE_RELOCATION và BASE_RELOCATION_ENTRY, trong đó đối tượng của cấu trúc IMAGE_BASE_RELOCATION nằm ở đầu relocation block và theo sau đó là gồm một hoặc nhiều đối tượng của cấu trúc BASE_RELOCATION_ENTRY (có thể hiểu là một page có thể có một hoặc nhiều vị trí cần phải relocation). * Dễ thấy, cấu trúc IMAGE_BASE_RELOCATION chứa RVA của page cần relocation và độ lớn của relocation block, còn cấu trúc BASE_RELOCATION_ENTRY chứa offset tính từ đầu page tới vị trí cần relocation và kiểu relocation (những kiểu relocation khác nhau sẽ thực hiện quá trình relocation một cách khác nhau). Hình bên dưới mô tả cấu trúc của IMAGE_BASE_RELOCATION. ![image](https://hackmd.io/_uploads/S1HG1NnH6.png) * Hình bên dưới mô tả cấu trúc của BASE_RELOCATION_ENTRY . ![image](https://hackmd.io/_uploads/H14ta9prp.png) * Đoạn code bên dưới thực hiện chi tiết chức năng relocation của reflective loader. ![image](https://hackmd.io/_uploads/HkJN9U2S6.png) ![image](https://hackmd.io/_uploads/Hk2ScIhSa.png) ![image](https://hackmd.io/_uploads/BJK_qUhST.png) :black_medium_square: **Execute DllMain()** * Ở bước cuối cùng, reflective loader chỉ cần gọi hàm DllMain() của .DLL là xong. Nhưng trước khi thực hiện DllMain(), cần phải gọi API **NtFlushInstructionCache()** trong ntdll.dll. Lí do của việc này là để đảm bảo các thay đổi trên cache của explorer.exe được cập nhật vào bộ nhớ vật lý. Điều này rất cần thiết đối với các hoạt động quan trọng, như việc thay đổi mã máy tại các địa chỉ thực thi hay đồng bộ hóa dữ liệu giữa các bộ nhớ cache và bộ nhớ vật lý. ![image](https://hackmd.io/_uploads/S1KIn83Sa.png) * Hình bên dưới mô quả tổng quan các chức năng của reflective loader đã được giải thích ở trên. ![image](https://hackmd.io/_uploads/SJ2KxZ2ra.png) :diamond_shape_with_a_dot_inside: **Obtain Mutant** * Sau khi reflective loader hoàn thành xong chức năng của nó thì đây cũng là lúc các chức năng chính của malware được thực thi. Việc đầu tiên malware làm trong hàm DllMain() là chiếm quyền truy cập mutex đã tạo trước đó, mục đích của việc này là để đảm bảo tại một thời điểm cụ thể thì chỉ có duy nhất một biến thể của malware được thực thi trên máy tính nạn nhân. Sau khi malware có được mutex thì nó bắt đầu gọi hàm RunPayload() để thực hiện các chức năng khác. ![image](https://hackmd.io/_uploads/Hyw17w3rp.png) :diamond_shape_with_a_dot_inside: **Domain Generation Algorithm** * Dynamic Domain Generation Algorithm (DGA) là một chiến lược được malware sử dụng để tạo ra các domain names động một cách tự động. Bằng cách này, malware có thể thay đổi liên tục các địa chỉ IP hoặc domain names của máy chủ C2, làm tăng khả năng sống sót và giảm khả năng bị phát hiện bởi các biện pháp bảo mật mạng. DGA cung cấp tính linh hoạt và sự thay đổi định kỳ, làm tăng độ phức tạp của phương thức tấn công và giúp malware tồn tại lâu dài trong môi trường mạng đa dạng. Hình dưới mô tả trực quan cách thức mà DGA hoạt động. ![image](https://hackmd.io/_uploads/By3Fr4iBa.png) * Cụ thể, malware thực hiện khởi tạo tối đa 30 domain name ngẫu nhiên từ một tập hợp có 10 kí tự cho trước cùng với giá trị seed bất kỳ. Tương ứng với mỗi domain name được tạo ra, malware cố gắng phân giải địa chỉ cho domain name đó bằng API gethostbyname() và nếu quá trình phân giải thành công thì malware sẽ dùng địa chỉ này để giao tiếp với C2. Quá trình thực hiện cơ chế DGA được trình bày chi tiết ở đoạn code bên dưới. ![image](https://hackmd.io/_uploads/HyCiODJLp.png) ![image](https://hackmd.io/_uploads/H14Mtv1U6.png) ![image](https://hackmd.io/_uploads/HJOA_PkIT.png) :diamond_shape_with_a_dot_inside: **Send System Information** * Sau khi đã tạo một socket connection để giao tiếp với C2 thành công, malware chủ động gửi một thông điệp đến phía C2. Sau đó, phía C2 trả về một thông điệp chứa RC4 key được dùng để mã hóa giao tiếp. Sau khi đã có key, malware tiến hành thu thập các thông tin có trên máy của nạn nhân như là user name, host name, ip address, current build, revision build, ... và sau đó xây dựng gói tin phản hồi dựa vào các thông tin này. Cuối cùng, malware mã hóa gói tin phản hồi bằng key vừa nhận được và gửi đến cho phía C2. Đoạn code bên dưới mô tả chi tiết chức năng thu thập thông tin hệ thống của malware. ![image](https://hackmd.io/_uploads/B1PBOPnra.png) ![image](https://hackmd.io/_uploads/HytwOv3BT.png) ![image](https://hackmd.io/_uploads/Bkridv3Ha.png) ![image](https://hackmd.io/_uploads/B1XauDnHa.png) :diamond_shape_with_a_dot_inside: **Enumerate Process** * Sau khi malware đã có được key mã hóa giao tiếp và gửi thông tin hệ thống đến phía C2 thì nó bắt đầu lắng nghe các câu lệnh điều khiển từ phía C2. Mỗi câu lệnh điều khiển tương ứng với một chức năng cụ thể mà malware cung cấp. Một trong các chức năng đó là liệt kê các tiến tiến trình đang có trên hệ thống. * Cụ thể, đầu tiên malware cấp phát động bộ nhớ bằng API **HeapAlloc()**. Sau đó, malware tiến hành liệt kê toàn bộ tiến trình có trên hệ thống bằng bộ ba API **CreateToolhelp32Snapshot()**, **Process32First()**, và **Process32Next()**. Với mỗi tiến trình được duyệt qua, malware thực hiện lưu trữ thông tin về PID và tên tiến trình vào bộ nhớ được cấp phát trước đó. Cuối cùng, malware mã hóa thông tin có trong bộ nhớ được cấp phát bằng thuật toán RC4 và gửi nó đến phía C2 bằng hàm **SendLargeChunk()**. ![image](https://hackmd.io/_uploads/r1nxcw2S6.png) ![image](https://hackmd.io/_uploads/HkIzqvnrT.png) ![image](https://hackmd.io/_uploads/r1wQcDhH6.png) * Chức năng của hàm SendLargeChunk() là một phần quan trọng trong việc triển khai giao thức để giao tiếp giữa malware và C2. Cụ thể, đầu tiên malware gửi 4 byte chứa độ lớn dữ liệu cần gửi đến C2. Sau đó malware gửi liên tiếp các chunk dữ liệu cho đến khi nào đã gửi đủ số lượng byte cần gửi thì dừng. ![image](https://hackmd.io/_uploads/rylggdnBT.png) :diamond_shape_with_a_dot_inside: **Terminate Process** * Một trong các chức năng khác của C2 là hủy một tiến trình bất kỳ đang có trên hệ thống. Cụ thể, malware truy xuất 4 byte tại byte thứ hai trong câu lệnh điều khiển để có được giá trị PID của tiến trình cần hủy. Sau đó, malware lấy handle của tiến trình cần hủy bằng API **OpenProcess()** với cờ **PROCESS_TERMINATE**. Cuối cùng, malware hủy tiến trình bằng API **TerminateProcess()** và chuẩn bị gói tin phản hồi chứa trạng thái hủy để gửi đến phía C2. Chi tiết chức năng này được hiển thị ở đoạn code bên dưới. ![image](https://hackmd.io/_uploads/SytDoP2ra.png) ![image](https://hackmd.io/_uploads/B1CKjD2rT.png) * Đây là hàm **SendSmallChunk()**, hàm này được chủ yếu sử dụng để gửi một gói tin phản hồi chứa mã trạng thái đến cho phía C2. ![image](https://hackmd.io/_uploads/HkcSldhBa.png) :diamond_shape_with_a_dot_inside: **Create Process** * Tạo tiến trình là một chức năng thường xuất hiện trong RAT, Backdoor, hoặc Trojan và không ngoại lệ, malware này cũng có chức năng tạo một tiến trình mới bằng đường dẫn được cung cấp bởi C2. * Cụ thể, đường dẫn mà malware cần tạo tiến trình bắt đầu từ byte thứ hai trong lệnh điều khiển. Sau khi có được đường dẫn của file thực thi, malware gọi API **CreateProcess()** để tạo mới tiến trình. Cuối cùng, malware chuẩn bị gói tin phản hồi chứa trạng thái khởi tạo tiến trình và gửi đến C2. Đoạn code bên dưới hiển thị đầy đủ cách thực hiện chức năng này của malware. ![image](https://hackmd.io/_uploads/SJVVnDnHa.png) ![image](https://hackmd.io/_uploads/ryXS2v3ra.png) :diamond_shape_with_a_dot_inside: **Shutdown Or Reboot** * Một chức năng khác của malware là shutdown hoặc reboot máy tính nạn nhân. Trước khi thực hiện việc này, malware cần lấy quyền shutdown cho token của nó thông qua bộ ba API **OpenProcessToken()**, **LookupPrivilegeValue()**, và **AdjustTokenPrivileges()**. Quyền shutdown trong hệ thống windows được biểu diễn bởi macro **SE_SHUTDOWN_NAME**. ![image](https://hackmd.io/_uploads/SJjVRvnHp.png) * Sau khi có được quyền shutdown, malware truy xuất thời gian mà máy tính sẽ tiếp tục hoạt động trước khi bị reboot hoặc shutdown tại byte thứ ba trong lệnh điều khiển. Ngoài ra, malware cũng truy xuất byte thứ hai trong lệnh điều khiển để xác định rằng máy tính nạn nhân nên shutdown hay reboot. Sau khi đã có được các thông tin cần thiết, malware gọi API **InitiateSystemShutdown()** để thực hiện chức năng của mình. Đoạn code bên dưới mô tả chi tiết chức năng này. ![image](https://hackmd.io/_uploads/ryDUAwnrp.png) ![image](https://hackmd.io/_uploads/rk7d0wnBa.png) :diamond_shape_with_a_dot_inside: **Encrypted Reverse Shell** * Reverse shell là một kỹ thuật phổ biến trong hacking, cho phép kẻ tấn công tạo ra một kết nối từ xa đến máy chủ của mình từ máy tính nạn nhân. Trong reverse shell truyền thống, dữ liệu truyền tải giữa máy nạn nhân và máy chủ không được mã hóa. Điều này có thể dẫn đến việc malware dễ bị theo dõi, phát hiện bởi các hệ thống an ninh mạng và firewall, và làm tăng nguy cơ bị chặn trước khi kết nối được thiết lập. Hình dưới minh họa quá trình malware thực hiện kỹ thuật reverse shell truyền thống. ![image](https://hackmd.io/_uploads/Hyupl3TSa.png) * Để vượt qua những hạn chế này, malware sử dụng kỹ thuật encrypted reverse shell. Trong kỹ thuật này, dữ liệu truyền tải giữa malware và C2 được mã hóa và giải mã bằng thuật toán RC4. Điều này làm cho dữ liệu trở nên khó hiểu và ngăn chặn được sự phát hiện của các công cụ an ninh mạng. Hình dưới minh họa quá trình malware thực hiện kỹ thuật encrypted reverse shell. ![image](https://hackmd.io/_uploads/SykBfn6Hp.png) * Cùng giải thích chi tiết cách thức thực hiện kỹ thuật encrypted reverse shell của malware. Đầu tiên, malware tạo ra hai unnamed pipe bằng API **CreatePipe()** với mục đích là để giao tiếp với tiến trình cmd.exe, malware sẽ sử dụng một unamed pipe được gọi là read pipe cho việc đọc dữ liệu từ cmd.exe và unnamed pipe còn lại được gọi là write pipe cho việc gửi dữ liệu đến cmd.exe. Sau đó, malware bắt đầu tạo ra tiến trình cmd.exe bằng API **CreateProcess()**, có thể thấy malware gán standard input cho cmd.exe bằng write pipe và gán standard output, standard error bằng read pipe. Tiếp theo, malware tạo ra hai thread để thực hiện chức năng của hai hàm **ReadDataFromCmd()** và **SendCommandToCmd()** bằng API **CreateThread()**. Cuối cùng, malware gọi API **WaitForMultipleObject()** để chờ đợi sự kiện được kích hoạt bởi hai thread vừa tạo và cmd.exe. Đoạn code bên dưới mô tả chi tiết chức năng vừa nêu. ![image](https://hackmd.io/_uploads/BJS7Id3Sp.png) ![image](https://hackmd.io/_uploads/ryNBUu2Hp.png) ![image](https://hackmd.io/_uploads/BymdUuhHa.png) ![image](https://hackmd.io/_uploads/SyZjUO3ST.png) ![image](https://hackmd.io/_uploads/BJFR8_hra.png) * Chức năng của hàm **SendCommandToCmd()** cũng khá đơn giản, nó liên tục nhận lệnh điều khiển từ C2 server, giải mã lệnh này bằng thuật toán RC4 và gửi lệnh đã giải mã đến cmd.exe thông qua write pipe. Chức năng của hàm SendCommandToCmd() được hiển thị chi tiết phía dưới. ![image](https://hackmd.io/_uploads/B1zkOu3rp.png) ![image](https://hackmd.io/_uploads/SyqlOuhr6.png) * Chức năng của **ReadDataFromCmd()** thì ngược lại so với SendCommandToCmd(), nó nhận thông tin thực thi lệnh từ cmd.exe thông qua read pipe, mã hóa thông tin này và gửi đến C2 server. Chức năng của hàm ReadDataFromCmd() được hiển thị chi tiết phía dưới. ![image](https://hackmd.io/_uploads/SyGUtdhra.png) ![image](https://hackmd.io/_uploads/BkJtYdhra.png) ![image](https://hackmd.io/_uploads/ry-cKd2ra.png) :diamond_shape_with_a_dot_inside: **Upload File** * Một trong các chức năng quan trọng của malware là upload file lên máy tính nạn nhân, chức năng này cho phép kẻ tấn công có thể upload các mẫu malware mới lên máy nạn nhân hoặc thay đổi thông tin cấu hình của máy tính nạn nhân. * Cụ thể, đầu tiên malware phân tích cú pháp của lệnh điều khiển để tìm ra độ lớn và đường dẫn của file cần upload. Sau đó, malware liên tục nhận các chunk dữ liệu, giải mã nó và ghi vào trong file, quá trình này được diễn ra cho đến khi nào số lượng byte malware đã nhận bằng với độ lớn của file cần upload. Đoạn code bên dưới giải thích một cách chi tiết chức năng này. ![image](https://hackmd.io/_uploads/SyqUTdhHa.png) ![image](https://hackmd.io/_uploads/ryPsau3BT.png) ![image](https://hackmd.io/_uploads/HJv6p_2Sa.png) :diamond_shape_with_a_dot_inside: **Download File** * Chức năng cuối cùng mà malware cung cấp là download file, chức năng này cho phép kẻ tấn công có thể download một file bất kỳ từ máy tính nạn nhân. * Cụ thể, đầu tiên malware phân tích cú pháp của lệnh điều khiển để tìm ra đường dẫn của file mà kẻ tấn công cần download. Tiếp theo, malware lấy độ lớn của file này bằng API **GetFileSize()** và đồng thời gửi 4 byte độ lớn này đến phía C2. Việc gửi 4 byte độ lớn đến C2 nhằm mục đích để thông báo cho C2 biết rằng nó nên cần nhận bao nhiêu byte dữ liệu. Sau đó, malware tiến hành đọc từng chunk dữ liệu từ file, mã hóa nó và gửi đến phía C2. ![image](https://hackmd.io/_uploads/S1_HWY3Hp.png) ![image](https://hackmd.io/_uploads/SyrqZthr6.png) ![image](https://hackmd.io/_uploads/S1S2bF2Bp.png) ## 3. C2 Server :diamond_shape_with_a_dot_inside: **Encryption** * Để đảm bảo tính bí mật về thông tin giao tiếp giữa C2 và các Agents (máy victim) thì RC4 cipher được sử dụng để mã hóa dữ liệu. RC4 là một stream cipher với ý tưởng khá đơn giản, ở đây nhóm sử dụng RC4 với key random cho từng kết nối với kích thước là 16 bytes. * Đầu tiên thì thuật toán này dựa vào kích thước của plaintext để tạo một mảng cùng kích thước bằng cách xáo trộn các phần tử có trong key. Từ đó đem output của quá trình trên thực hiện XOR với plaintext → ciphertext. * Liên quan đến vấn đề mã hóa đường truyền giữa c2 và các agents, có 4 hàm với từng chức năng cụ thể, chi tiết được trình bày bên dưới cho từng hàm. ![image](https://hackmd.io/_uploads/Hkjkd6-66.png) :diamond_shape_with_a_dot_inside: **Tạo key cho mã hóa RC4** * Đây là một hàm tạo key từ các ký tự có trong bảng alphabet và các số từ 0 đến 9, trong suốt quá trình giao tiếp giữa C2 và Agents, nhóm em chọn kích thước của tất cả các RC4 key là 16 bytes. * Quá trình tạo key sẽ random từng key cho đến khi đủ kích thước của key là 16 bytes là được thôi. Dưới đây là đoạn code mô tả về hàm tạo key. ![image](https://hackmd.io/_uploads/SyY7daZap.png) :diamond_shape_with_a_dot_inside: **Khởi tạo RC4 block để mã hóa** * Khởi tạo một box với kích thước là 256 bytes bằng cách xáo trộn các ký tự có trong key được đưa vào, từ đó ta có một box với 256 phần tử. Block này sẽ được sử dụng để XOR với plain text để cho ra kết quả đã được mã hóa và gửi đi trong mỗi lần trao đổi dữ liệu giữa Agents và C2. ![image](https://hackmd.io/_uploads/HkdLdTZ6p.png) :diamond_shape_with_a_dot_inside: **Mã hóa/Giải mã RC4 (RC4 Encryption/Decryption)** * Thực hiện mã hóa hoặc giải mã bằng cách XOR plaintext/ciphertext với box được tạo ra bởi hàm **RC4Init()**. Nếu kích thước của dữ liệu cần XOR vượt quá 256 bytes thì lặp lại box. ![image](https://hackmd.io/_uploads/ryPad6bap.png) * Trong trường hợp chỉ mã hóa/giải mã một đoạn data cụ thể nào đó thì có thêm hàm **RC4CryptSegment()** với chức năng tương tự như **RC4Crypt()**. Cụ thể đoạn code của hàm này như sau: ![image](https://hackmd.io/_uploads/rJlkKaZpT.png) :diamond_shape_with_a_dot_inside: **Luồng hoạt động của C2** * Conmand and Control Server (C2 Server) là một công cụ, một tên miền hay một chương trình lắng nghe các kết nối từ máy chủ bị nhiễm mã độc để truy xuất thông tin nhạy cảm từ máy chủ đó. Gồm có 3 luồng chính như sau: * **mainThread**: luồng này sẽ khởi tạo 2 luồng con để thực thi các nhiệm vụ khác, bản thân nó sẽ thực hiện nhiệm vụ thao tác với giao diện và cung cấp menu cho kẻ tấn công. * **hThreadListener**: luồng này thực hiện lắng nghe các kết nối mới đến c2 server. Đồng thời thực hiện xử lý thông tin các kết nối mới đó một cách bất đồng bộ nhưng không gây ra xung đột với các luồng khác. Phương pháp ở đây em sử dụng Critical Section Objects. * **hThreadCheckActive**: luồng cuối cùng này có nhiệm vụ liên tục gửi các gói tin đến cho các agent đã kết nối đến c2. Nếu một agent đã hủy kết nối nó sẽ xử lý agent đó một cách bất đồng bộ và cũng không gây ra các xung đột bằng Critical Section Objects. * Hình ảnh dưới đây sẽ minh họa cụ thể luồng hoạt động của C2. ![image](https://hackmd.io/_uploads/rJgrFTba6.png) * Đầu tiên chương trình sẽ thực thi một hàm **Init()** để khởi tạo các đối tượng **critical section** cần dùng cho việc đồng bộ các luồng và cấp phát vùng nhớ cho các tài nguyên cần thiết. Tiếp theo chương trình khởi tạo và thực thi các luồng như đã nói ở phía trên. Đoạn code chức năng của hàm Init() như sau: ![image](https://hackmd.io/_uploads/rkqvKTZTT.png) * Sau đó, hàm main sẽ gọi đến hàm **Init()** và chạy các thead chính ở trên. ![image](https://hackmd.io/_uploads/S1gtY6ZTp.png) :diamond_shape_with_a_dot_inside: **Giao diện chính (Menu) của C2** * Luồng chính là luồng thao tác với giao diện và cung cấp một menu tương tác cho kẻ tấn công với các Agents. C2 Server yêu cầu một vài tính năng cơ bản như upload, download, shell,… Và quản lý các Agentsđã được kết nối đến một cách hợp lý: * Về menu quản lý các Agents gồm có 2 phần: * **Main Menu**: Bao gồm các lệnh như help, quit, list, connect. Menu này có các chức năng liệt kê các agents đang kết nối đến C2 và yêu cầu tương tác với một agent cụ thể. * **Host Menu**: Menu này được sử dụng khi kẻ tấn công thực hiện yêu cầu kết nối với một Agents nhất định, nó bao gồm các lệnh custom để thực hiện tương tác với Agents. * Ngoài ra thì có thể sử dụng các lệnh như clear, quit, help để thao tác với các menu. Đầu tiên là đến với Main menu, nó sẽ có 2 nhiệm vụ cơ bản như sau: * **list**: Liệt kê thông tin cơ bản của các Agents đã kết nối đến C2. * **connect \<IPv4>**: với một tham số truyền vào là địa chỉ IPv4 của Agents cần tương tác, lệnh này yêu cầu một giao diện tương tác với socket được tạo ra để kết nối đến IP tương ứng. * Kết quả của các lệnh có trong main menu được thể hiện như hình dưới. Nếu một IP tồn tại trong danh sách các Agents kết nối đến C2, nó sẽ trả về kết quả thành công và đi vào Host Menu, ngược lại sẽ in ra kết quả thất bại. ![image](https://hackmd.io/_uploads/rknz9TZp6.png) * Tiếp theo, trong giai đoạn thao tác thì cần xử lý dữ liệu đầu vào và chuyển hướng nó đến hàm xử lý phù hợp. Với hàm **GetInput()** sẽ xử lý các tham số nhập vào từ kẻ tấn công và phân tách nó tách các từ riêng lẻ bằng cách tách chuỗi dựa trên ký tự khoảng trắng hoặc NULL → từ đó lưu vào trong một mảng 2 chiều: phần tử đầu tiên là lệnh và các phần tử tiếp theo là tham số tương ứng cho từng chức năng. Cuối cùng thì thực hiện giải phóng mảng này để tránh việc rò rỉ bộ nhớ. Đoạn code dưới đây sẽ mô tả về chức năng này. ![image](https://hackmd.io/_uploads/SJCrc6bap.png) * Tiếp theo là Host Menu gồm các lệnh là các chức năng có trong Malware. Malware sẽ tương tác với Agents thông qua TCP Stream rồi thực hiện yêu cầu của kẻ tấn công thông qua việc nhập các câu lệnh hàm. * Ví dụ với lệnh Download thì byte đầu tiên sẽ là lệnh biểu diễn download và các bytes tiếp theo sẽ là đường dẫn đến file cần download,… ![image](https://hackmd.io/_uploads/Hk2KcTZaT.png) * Với việc xử lý dữ liệu đầu vào, cũng như Main Menu thì nhóm cũng phân tách lệnh và tham số theo ký tự khoảng cách và NULL, đồng thời điều hướng nó thực thi với các hàm tương ứng. Đồng thời chú ý với các ký tự UNICODE, vì vậy nên cần chuyển từ CHAR sang WCHAR trước khi thực hiện truyền tham số cho các hàm yêu cầu kiểu tham số WCHAR như: download, upload,… Đoạn code dưới đây mô tả chức năng của Host Menu: ![image](https://hackmd.io/_uploads/r1zo5aZaa.png) ![image](https://hackmd.io/_uploads/r1un5T-aT.png) ![image](https://hackmd.io/_uploads/Hk6a5a-ap.png) :diamond_shape_with_a_dot_inside: **Luồng lắng nghe kết nối** * Ngoài luồng chính được kẻ tấn công dùng để tương tác với các Agents, có 2 luồng con cũng liên tục chạy song song bao gồm các luồng lắng nghe và kiểm tra kết nối. * Luồng **hThreadListener** là một chain xử lý các vấn đề liên quan đến việc lắng nghe các kết nối từ Agents và xử lý dữ liệu khi có bất kỳ một Agents nào đó kết nối đến C2. Luồng này gồm có 3 hàm chính như sau: * **Listener()**: Hàm này thực hiện khởi tạo socket và liên tục lắng nghe các Agents yêu cầu kết nối. * **RecvSysInfoAndSendKey()**: Hàm này được gọi khi một Agents yêu cầu kết nối đến c2, nó gửi RC4 key và trao đổi một số thông tin hệ thống. * **AgentConnected()**: xử lý và lưu trữ một số thông tin của Agents mới kết nối đến. * Luồng này là một vòng lặp vô hạn cho đến khi chương trình kết thúc, còn không nó sẽ cứ nhận yêu cầu kết nối từ các Agents. ![image](https://hackmd.io/_uploads/SJ0uop-aa.png) :diamond_shape_with_a_dot_inside: **Kênh lắng nghe C2** * Về Kênh lắng nghe C2, luồng lắng nghe trên port 5555 và hàm **accept()** là một hàm block cho đến khi có một kết nối đến. Sử dụng một vòng lặp để liên tục cho **hThreadListener** lắng nghe kết nối một cách liên tục. * Khi có một Agents kết nối đến, truyền socket đó cho hàm **RecvSysInfoAndSendKey()** để thực hiện trao đổi RC4 key dùng để mã hóa giao tiếp và nhận một số thông tin cơ bản về Agents. Dưới đây là đoạn code mô tả chức năng của hThreadListener, một trong 3 luồng chính dùng để lắng nghe các kết nối đến C2. ![image](https://hackmd.io/_uploads/ByD6oTZ6a.png) ![image](https://hackmd.io/_uploads/BkwRoaZ6p.png) :diamond_shape_with_a_dot_inside: **Trao đổi khóa và lấy thông tin máy victim** * Đây là một quá trình trong luồng **hThreadListener**, về cơ bản thì hàm này thực hiện trao đổi RC4 key được tạo random đến phía agent. Đồng thời các gói tin về sau sẽ được mã hóa bằng key này. * Khi có một Agents kết nối đến, một socket sẽ được tạo ra. Đồng thời c2 cũng thực hiện random key với kích thước là 16 bytes và gửi nó đến cho phía Agents vừa kết nối đến. Khi Agents thực hiện nhận key thành công, nó sẽ thu thập thông tin từ trong Registry và thông qua một số API như **GetDeviceName(),** **GetUserName()** rồi gửi về cho C2 (lưu ý rằng thông tin này được mã hóa bởi key vừa nhận được). * Tiếp tục luồng **hThreadListener**, C2 thực hiện giải mã thông tin hệ thống nhận được và tiếp tục gọi hàm **AgentConnected() **để lưu trữ và xử lý thông tin của kết nối mới. * Có thể thông tin nhận được từ Agents được mã hóa thành một dãy ký tự vô nghĩa với kích thước là 1 CHUNK (1024 bytes). ![image](https://hackmd.io/_uploads/H1bB26-TT.png) ![image](https://hackmd.io/_uploads/rJTUnpZ6p.png) ![image](https://hackmd.io/_uploads/HJqP3aWTT.png) :diamond_shape_with_a_dot_inside: **Kết nối Agents vào C2** * Quá trình cuối cùng trước khi quay lại lắng nghe kết nối của **hThreadListener**, quá trình này thực hiện lưu trữ một số thông tin của agent và in ra màn hình console các thông tin đó. * Với một buffer nhận vào như là tham số của hàm này, có thể trích xuất thông tin của agent từ buffer đó. Với cụ thể như sau: * 32 bytes đầu tiên là tên thiết bị của agent, kiểu wide char. * 544 bytes tiếp theo là username của agent, kiểu wide char. * 32 bytes tiếp theo là địa chỉ IPv4. * 4 bytes tiếp theo là build version. * 4 bytes tiếp theo là revision version. * **hThreadListener** bắt đầu từ **Listener()** → **RecvSysInfoAndSendKey()** → **AgentConnected()** và in ra các thông tin của agent đó như kết quả bên dưới: ![image](https://hackmd.io/_uploads/SJ6C26Waa.png) ![image](https://hackmd.io/_uploads/rykxp6Wpp.png) ![image](https://hackmd.io/_uploads/S1-Z6pbTp.png) ![image](https://hackmd.io/_uploads/HkmGTaZpa.png) :diamond_shape_with_a_dot_inside: **Kiểm tra trạng thái của Agents** * Kế đến là luồng cuối cùng được nhắc đến trong 3 luồng chính của chương trình thực thi: **hThreadCheckActive**. Luồng này bao gồm 2 hàm **CheckActiveAgent()** và **AgentDisconnected()** có nhiệm vụ kiểm tra kết nối đến các agent và thực hiện xóa thông tin của agent đó khi nó mất kết nối. ![image](https://hackmd.io/_uploads/BkR8TaW6p.png) * Ở đây C2 liên tục thực hiện gửi một gói tin với lệnh kiểm tra kết nối đến các Agents có trong danh sách các Agents đã và đang kết nối đến c2 theo khoảng thời gian là 1 giây 1 lần. Nếu một Agents nào đó trả về **SOCKET_ERROR (-1)**. Luồng **hThreadChechActive** sẽ thực hiện gọi **AgentDisconnected()** để xử lý socket kết nối với Agents đó và các thông tin liên quan. * Đồng thời ở đây có sử dụng đối tượng **criticalSection** để tránh xung động với luồng **mainThread** cũng có thể đang truyền tải dữ liệu lớn bằng các lệnh như shell, download, upload,… * Sau khi duyệt qua các phần tử trong mảng, thực hiện giải phóng đối tượng criticalSection và sleep 1 giây cho lần gửi kiểm tra kết nối tiếp theo. ![image](https://hackmd.io/_uploads/Hya9aaZ6p.png) * Tiếp tục, nếu Agents không còn kết nối, nó sẽ thực hiện đóng kết nối socket tương ứng của Agents đã bị ngắt giao tiếp với C2. Đồng thời xóa thông tin của nó khỏi danh sách các Agents đang kết nối với C2. * Ở đây cũng sử dụng một đối tượng csListAgent, đây cũng là một đối tượng **Critical Section** được sử dụng để kiểm soát các luồng truy cập và chỉnh sửa mảng **agent[]**. Sau khi thực hiện xóa Agents khỏi mảng này thì giải phóng đối tượng csListAgent. * Có thể thấy kết quả như sau khi có một kết nối bị đóng lại, kết quả này biểu diễn cho quy trình này của luồng **hThreadCheckActive**: ![image](https://hackmd.io/_uploads/SkQrC6Z6p.png) ![image](https://hackmd.io/_uploads/B1fL0pW6T.png)