Lenovo Protect Kernel Heap OOB Write in GetRegistryRepairList ---[ 1. Overview An integer overflow leading to a heap out of bounds write bug (OOBW) exists in the handling of the "GetRegistryRepairList" 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 "GetRegistryRepairList", the command code is 36 and the data comprises only the 4 byte command code. The attack requires the provision of an output buffer of suffificient size. ---[ 3.1 Ingeger Overflow in GetRegistryRepairList The Lenovo Protect driver contains a working list of registry kets to be repaired. This is a singly linked list stored in a global variable at offset 1DF30 in the driver's .data section (the list head is stored at 1DF28). Opcode 36 allows the user mode management software to request the total list of strings into an output buffer. The function first calculates the required size thus (variable names will differ): 1. unsigned int totalLen = 0; 2. ... 3. for ( i = g_RegistryRepairListHead; 4. i != (LinkedListUnicodeString *)&g_RegistryRepairListHead; 5. i = (LinkedListUnicodeString *)i->ListEntry.Flink ) 6. { 7. totalLen += i->String.ObjectSize; 8. } It iterates over the linked list, pulling the objects size (the first member of the string object following the LIST_HEAD) and adding it to an unsigned integer. ---[ 3.2 Heap Overflow Once the total length is calculated, the routine then uses that size to allocate a non-executable kernel heap buffer thus: 9. SIZE_T allocSize = 24LL; 10. if ( totalLen ) 11. allocSize = totalLen + 8LL; 12. if ( Outbuffer != NULL ) 13. { 14. if ( OutLength < allocSize ) 15. { 16. result = 0xC0000023; 17. } 18. else 19. { 20. temporaryBuffer = ExAllocatePoolWithTag( 21. POOL_NX_ALLOCATION, allocSize, 'ssss' 22. ); The total size to allocate is then incremented by 8 and stored in a 64-bit size_t value to match the required argument type for ExAllocatePoolWithTag. However, if the total size of the allocation exceeds 2 ^ 32-1 (the maximum value that can be stored in an unsigned integer) then the value of totalLen on line 7 will wrap back around to zero. This means that Registry Repair lists with a large number of long strings will cause a small allocation. The code then iterates over the list again and memcpys the values into the allocated buffer. 23. for ( j = (ListOfStrings *)g_RegistryRepairListHead; 24. j != (ListOfStrings *)&g_RegistryRepairListHead && written < totalLen; 25. j = (ListOfStrings *)j->ListEntry.Flink ) 26. { 27. memmove(p + written + 8, &j->String, j->String.ObjectSize); 28. written += j->String.ObjectSize; 29. } Very usefully, the condition on line 24 above "written < totalLen" allows us to perform a single controlled overflow of the destination heap buffer as the attacker can specify the length of the final buffer to be overwritten, and all previous lengths, thus choosing how much to overflow the ineger. ---[ 3.3 Reliable Exploitation In order to reliably exploit the bug, it is necessary to first clear the repair list by sending a single command with opcode 35. Then repeatedly add strings to the list using the opcode 33. The details regarding the protocol for these two commands is not covered in this report. Then the final call to opcode 36 is called once to trigger the overflow. ---[ 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 (0xFFFFA689A2800000,0x0000000000000002,0xFFFFF8024329C2A0,0x0000000000000002) Driver at fault: *** lrtp.sys - Address FFFFF8024329C2A0 base at FFFFF80243290000, DateStamp 64ec68dc . Break instruction exception - code 80000003 (first chance) The stack trace: 05 ffffec03`d3b6ac70 fffff802`3ee0e46d nt!MmAccessFault+0x400 06 ffffec03`d3b6ae10 fffff802`4329c2a0 nt!KiPageFault+0x36d 07 ffffec03`d3b6afa8 fffff802`43297cd2 lrtp+0xc2a0 08 ffffec03`d3b6afb0 fffff802`4329b8a1 lrtp+0x7cd2 09 ffffec03`d3b6b000 fffff802`3bc5bc8a lrtp+0xb8a1 0a ffffec03`d3b6b040 fffff802`3bc927a9 FLTMGR!FltpFilterMessage+0xda 0b (Inline Function) --------`-------- FLTMGR!FltpMsgDeviceControl+0xfc 0c ffffec03`d3b6b0a0 fffff802`3bc54a80 FLTMGR!FltpMsgDispatch+0x179 0d ffffec03`d3b6b110 fffff802`3ec4ad55 FLTMGR!FltpDispatch+0xe0 Where frame 09 is the handler for filter messages, frame 08 is the routine for outputting all registry repair list entries, and frame 07 is memmove. The faulting instruction from the memmove routine. 2: kd> u 0xFFFFF8024329C2A0 lrtp+0xc2a0: fffff802`4329c2a0 f30f7f40f0 movdqu xmmword ptr [rax-10h],xmm0 fffff802`4329c2a5 4803c8 add rcx,rax fffff802`4329c2a8 4d8bc8 mov r9,r8 fffff802`4329c2ab 49c1e905 shr r9,5 fffff802`4329c2af 4981f900200000 cmp r9,2000h fffff802`4329c2b6 0f8776000000 ja lrtp+0xc332 (fffff802`4329c332) fffff802`4329c2bc 4983e01f and r8,1Fh fffff802`4329c2c0 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 This bug can easily be mitigated by changing the type of the variable used for counting the required buffer space to one with a 64-bit width (a SIZE_T for example). This will remove the possibility to overflow the integer. ---[ 9. CVE Credit Please provide CVE credit to Gareth Evans (Kryc)