TL;DR: HP had a keylogger in the keyboard driver. The keylogger saved scan codes to a WPP trace. The logging was disabled by default but could be enabled by setting a registry value (UAC required). Get the list of affected hardware and patch here: https://support.hp.com/us-en/document/c05827409

UPD: Turns out it’s not HP’s failure. Synaptics released a statement on the issue: https://www.synaptics.com/company/blog/touchpad-security-brief

From the statement I can conclude that other vendors (Lenovo, Dell, …) might be affected too. To check if your driver contains ‘the debug tool’ run “findstr.exe /I ulScanCode SynTP.sys” from the command line: if the driver is clean findstr won’t print anything. Also it worth to note that the “proprietary binary format” mentioned in the statement is easy to decode and is not necessarily “a rolling memory buffer” but might be a persistent file.

Sometime ago someone asked me if I can figure out how to control HP’s laptop keyboard backlit. I asked for the keyboard driver SynTP.sys, opened it in IDA and after some browsing noticed a few interesting strings:

Hmm, looks like a format string for a keylogger, right? The string is referenced in sub_140022C10 function:

Yet another string gave me a hint about the original name of sub_140022C10: CPalmDetect::KeyboardHookCallback. This is how KeyboardHookCallback looks after after applying names and decompilation:

char __fastcall KeyboardHookCallback ( unsigned int a1 , int a2 , unsigned int a3 ) { if ( DebugMask & 4 ) { LODWORD ( v19 ) = v3 ; TraceMessage (( __int64 ) "CPalmDetect::KeyboardHookCallback" , 3u , "ulScanCode=0x%02X, bKeyFlags=%X" , a1 , v19 ); v4 = DebugMask ; } ...

TraceMessage function ends up calling WPP tracing function:

void TraceMessage ( char * pFuncName , unsigned char Level , const char * pFmt , ...) { char LogString [ 0x100 ]; va_list va ; va_start ( va , pFmt ); if ( pFmt ) vsnprintf ( LogString , 0xFFu i64 , pFmt , va ); if ( WppDevObj != ( WPP_TRACE_CONTROL_BLOCK * ) & WppDevObj ) { if ( WppDevObj -> Unk & 2 ) { if ( WppDevObj -> Level >= Level ) WPP_SF_ss ( WppDevObj -> Logger , /*MessageNumber*/ 0xC , MessageGuid , pFuncName , LogString ); } } }

After all WPP_SF_ss logs formatted string using WmiTraceMessage function:

WPP_SF_ss ( unsigned short LoggerHandle , unsigned short MessageNumber , LPGUID pMessageGuid , char * pFuncName , char * pLogString ) { return WmiTraceMessage ( LoggerHandle , /*MessageFlags*/ WPP_TRACE_OPTIONS , MessageGuid , MessageNumber , ( pFuncName ) ? strlen ( pFuncName ) + 1 : 5 , ( pFuncName ) ? ( pFuncName ) : "NULL" , ( pLogString ) ? strlen ( pLogString ) + 1 : 5 , ( pLogString ) ? ( pLogString ) : "NULL" , 0 ); }

So, if DebugMask’s bit 2 is set, KeyboardHookCallback calls TraceMessage passing “ulScanCode=0x%02X, bKeyFlags=%X” and the scan code to the trace. The layout of TraceMessage fits DoTraceMessage macro. Dependently on Windows version, WPP tracing leverages either ETW or WMI mechanism. WPP_SF_ss is an autogenerated function defined in .tmh file. Its 3rd parameter is MessageGuid which in turn gets passed to WmiTraceMessage.

One more important detail is missing here: the providerId. To get the providerId I had to locate the use of WPP_INIT_TRACE macro. The macro stores the pointer to the providerId in ControlGuid field of WPP control block structure (_WPP_TRACE_CONTROL_BLOCK). After some XRefing I’ve got to the use of the macro and the providerId:

__int64 __fastcall WPP_INIT_TRACING ( PDRIVER_OBJECT pDrvObj , PUNICODE_STRING pRegPath ) { ... RegisterAtExitFunctions (); WppMainCb . Callback = 0 i64 ; WppMainCb . ControlGuid = ( __int64 ) & ControlGuid ; //ProviderId WppMainCb . Next = 0 i64 ; WppMainCb . RegistryPath = 0 i64 ; WppMainCb . FlagsLen = 1 ; WppMainCb . Level = 0 ; WppLoadTracingSupport (); WppMainCb . RegistryPath = 0 i64 ; WppInitKm ( pDrvObj , pRegPath ); ... }

So, driver’s providerId is {607781A3-0392-4422-87BC-C14CDEC63F9F} and MessageId is {0F18D222C-D861-F023-2733-D780688CFBDF}.

However, the value of DebugMask is 3, bit 2 is not set, debugging (and keylogging) is off by default:

XRefing to DebugMask revealed the function that sets the value of the variable. I named it GetDebugMask:

//----- (000000014008A23C) ---------------------------------------------------- __int64 __fastcall GetDebugMask ( _BYTE * a1 ) { result = GetDriverParameter ( a1 , ( __int64 ) "Debug" , ( __int64 ) "Mask" , ( __int64 ) & v3 ); DebugMask = result ; return result ; } // 1400D0FA0: using guessed type int DebugMask;

GetDebugMask calls CRegistryBase::GetDriverParameter (sub_14002C588) to read the value of DebugMask from Windows registry. GetDriverParameter’s code is long and boring, it’s enough to say that it searches the following subkeys looking for the value name passed in the 3rd parameter (Mask in our case):

HKLM\Software\Synaptics\%ProductName% HKLM\Software\Synaptics\%ProductName%\Default

%ProductName% might be “SynTP” or “PointerPort”. The value type is DWORD.

At this point I had to run some ETW capture software like MessageAnalyzer to read the trace but I couldn’t do that since I didn’t have HP laptop. The research were done by reading the code of SynTP.sys, I couldn’t verify if it’s correct or not. I tried to find HP laptop for rent and asked a few communities about that but got almost no replies. One guy even thought that I am a thief trying to rob someone. So, I messaged HP about the finding. They replied terrificly fast, confirmed the presence of the keylogger (which actually was a debug trace) and released an update that removes the trace. Get the list of affected models and fixed driver at HP website. The update also available via Windows update.