Rachit Arora

Windows Process injection via KernelCallbackTable

May 19, 2024

Contents

Overview KernelCallbackTable

img

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.

https://i.postimg.cc/hjRshK4s/image.png

We can use the dps command to display specific content within an address.

https://i.postimg.cc/5NMtp0n1/image.png

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.

Step 2: Retrieve the PEB Structure Information

Step 3: Reading the PEB and KernelCallbackTable using ReadProcessMemory

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.

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

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.