Lenovo Protect Kernel Heap OOB Write in AddRegistryRepairList ---[ 1. Overview A race condition leading to a heap out of bounds write (OOBW) exists in the handling of the "AddRegistryRepairList" command to the Lenovo Protect Driver. Exploitation of this bug allows an attacker to overwrite kernel heap memory within the non-paged nonexecutable pool with attacker-supplied arbitrary data. The lrtp.sys driver referenced in this bug report is version 4.0.40.12151 with SHA1 hash 310d8a24142439c9126520bfa02ab3d382a32168. PLEASE NOTE: Variable names and protocol details used in this report will not match those used in the original source for the driver and are only used to convey the usage as observed in the driver. ---[ 2. Lenovo Protect Driver Lenovo Protect is an endpoint protection solution bundled with the Lenovo Secure Browser (available from https://browser.lenovo.com.cn/). Amongst other things, it provides protection against the modification of several key operating system components, including registry keys and files. This is accomplished through the use of a kernel filter driver called lrtp.sys. LRTP is configured with Filter Driver Communication Ports with a custom messaging protocol. ---[ 3. lrtp Custom protocol The protocol used by the driver is relatively straightforward. The first DWORD represents the command ID and the remaining data is specific to the command used. Handlers for each command code are dispatched in the MessageNotifyCallback routine for the communication port "\\LRTPPort" registered using FltCreateCommunicationPort. In the case of "AddRegistryRepairList", the command code is 33 and the data comprises a header representing the total length of the buffer containing the strings, then several string entries. The resulting payload looks like this: |DWORD Command||DWORD Unknown||DWORD ListlenBytes|STRING String[n] Where the structure of a STRING is thus: |DWORD ObjectSize|DWORD Code|DWORD ByteLength|BYTE Buffer[ByteLength] ---[ 3.1 AddRegistryRepairList Handler The Handler for AddRegistryRepairList iterates over the string array provided by the caller, in turn calling a subroutine for each STRING object. The subroutine first checks that the ObjectSize and ByteLength are not zero, then also checks that the first byte of the provided string buffer is not zero. It then allocates a new kernel heap buffer of length ObjectSize + sizeof(LIST_ENTRY). If this allocation is successful, it proceeds to memcpy the STRING object into the new buffer. Once this is done, the routine checks to see whether this string is already present in the repair list, and if not, it adds it to the start of the linked list. ---[ 4. Heap OOB Write An error exists wherein the driver does not capture the user input buffer before using it for ongoing processing. Below is the reverse engineered flow of the add repair string routine. 1. if ( NewString->ObjectSize 2. && NewString->StringLength 3. && NewString->Buffer[0]) 4. ... 5. { 6. stringBuffer = ExAllocatePoolWithTag( 7. (POOL_TYPE)POOL_NX_ALLOCATION, 8. NewString->ObjectSize + sizeof(LIST_ITEM), 9. 'ssss'); 10. 11. if (stringBuffer) 12. { 13. memmove(&stringBuffer->String, NewString, NewString->ObjectSize); 14. } 16. ... 17. } The documentation for the PFLT_MESSAGE_NOTIFY callback function states that the buffer provided to the message handler is a pointer to a raw, inlocked user-mode buffer. This is important because it means that the contents of the buffer can be manipulated during the processing of the message. With this in mind there is a race condition between the amount of data allocated on lines 6-9, and the amount of memory copied into the buffer on line 13. A malicious application can spawn a second thread which repeatedly changes the value of the STRING's ObjectSize field, hoping to succesfully change it between the allocation and the memmove. As there is no downside to losing the race, the user-mode application can repeatedly try this until successful. In testing, this was found to work within only a couple of seconds. ---[ 5. Signature Verification The driver does validate the identity of the user-mode application by ensuring that it has the string value "MORIYA" and a valid signature. However, this can be bypassed by injecting a DLL into the LISTProt.exe file (requiring high integrity). ---[ 6. Kernel Panic The resulting Windows kernel panic is similar to this (offsets will change). The error code 50 signifies a page fault in non-paged memory, and the 2 signifying a write. *** Fatal System Error: 0x00000050 (0xFFFF938D5F87045C,0x0000000000000002,0xFFFFF8068530C2A0,0x0000000000000002) Driver at fault: *** lrtp.sys - Address FFFFF8068530C2A0 base at FFFFF80685300000, DateStamp 64ec68dc . Break instruction exception - code 80000003 (first chance) The faulting instruction from the memmove routine. 1: kd> u FFFFF8068530C2A0 lrtp+0xc2a0: fffff806`8530c2a0 f30f7f40f0 movdqu xmmword ptr [rax-10h],xmm0 fffff806`8530c2a5 4803c8 add rcx,rax fffff806`8530c2a8 4d8bc8 mov r9,r8 fffff806`8530c2ab 49c1e905 shr r9,5 fffff806`8530c2af 4981f900200000 cmp r9,2000h fffff806`8530c2b6 0f8776000000 ja lrtp+0xc332 (fffff806`8530c332) fffff806`8530c2bc 4983e01f and r8,1Fh fffff806`8530c2c0 f30f6f440af0 movdqu xmm0,xmmword ptr [rdx+rcx-10h] ---[ 7. Proof of Concept Exploit A rudimentary proof of concept exploit can be found attached to this report. To run the exploit, clone https://github.com/kryc/DllInjector. Replace the DLL code with that in the PoC and compile for x86. Then run the DLLInjector in an Administrator command prompt. .\DllInjector.exe -launch \ "C:\Program Files (x86)\Lenovo\LenovoInternetSoftwareFramework\LISFProt.exe" It may take one or two attempts, but you should see a kernel panic. ---[ 8. Suggested Fix As previously mentionned, the reason that this bug is exploitable is the fact that the user buffer is not captured between the allocation and copy. One possible fix for this issue is to capture the STRING object within Kernel memory before carrying out any further action. ---[ 9. CVE Credit Please provide CVE credit to Gareth Evans (Kryc)