Uninformed: Informative Information for the Uninformed

Vol 4» 2006.Jun


Improper Validation of Kernel Object Types

Windows exposes many kernel features through a series of ``kernel objects''. These objects may be acted upon by user mode through the user of handles. Handles are integral values that are translated by the kernel into pointers to a particular object upon which something (typically a system service) interacts with on behalf of a caller. All objects share the same handle namespace.

Because of this handle namespace sharing between objects of different types, one of the jobs of a system service inspecting a handle is to verify that the object that it refers to is of the expected type. This is accomplished by an object manager routine ObReferenceObjectByHandle, which performs the translation of handles to object pointers and does an optional built-in type check by comparing a type field in the standard object header to a passed in type.

Since KAV hooks system services, it inevitably must deal with kernel handles. Unfortunately, it does not do so correctly. In some cases, it does not ensure that a handle refers to an object of a particular type before using the object pointer. This will result in corruption or a system crash if a handle of the wrong type is passed to a system service.

One such case is the KAV NtResumeThread hook, which attempts to track the state of running threads in the system. In this particular case, it does not seem possible for user mode to crash the system by passing an object of the wrong type as the returned object pointer because it is simply used as a key in a lookup table that is prepopulated with thread object pointers. KAV also hooks NtSuspendThread for similar purposes, and this hook has the same problem with the validation of object handle types.

.text:F82245E0 ; NTSTATUS __stdcall KavNtResumeThread(
		HANDLE ThreadHandle,
		PULONG PreviousSuspendCount)
.text:F82245E0 KavNtResumeThread proc near             ; DATA XREF: sub_F82249D0+FBo
.text:F82245E0
.text:F82245E0 ThreadHandle    = dword ptr  8
.text:F82245E0 PreviousSuspendCount= dword ptr  0Ch
.text:F82245E0
.text:F82245E0    push    esi
.text:F82245E1    mov     esi, [esp+ThreadHandle]
.text:F82245E5    test    esi, esi
.text:F82245E7    jz      short loc_F8224620
.text:F82245E9    lea     eax, [esp+ThreadHandle] ;
.text:F82245E9               ; This should pass an object type here!
.text:F82245ED    push    0               ; HandleInformation
.text:F82245EF    push    eax             ; Object
.text:F82245F0    push    0               ; AccessMode
.text:F82245F2    push    0               ; ObjectType
.text:F82245F4    push    0F0000h         ; DesiredAccess
.text:F82245F9    push    esi             ; Handle
.text:F82245FA    mov     [esp+18h+ThreadHandle], 0
.text:F8224602    call    ds:ObReferenceObjectByHandle
.text:F8224608    test    eax, eax
.text:F822460A    jl      short loc_F8224620
.text:F822460C    mov     ecx, [esp+ThreadHandle]
.text:F8224610    push    ecx
.text:F8224611    call    KavUpdateThreadRunningState
.text:F8224616    mov     ecx, [esp+ThreadHandle] ; Object
.text:F822461A    call    ds:ObfDereferenceObject
.text:F8224620
.text:F8224620 loc_F8224620:              ; CODE XREF: KavNtResumeThread+7j
.text:F8224620               ; KavNtResumeThread+2Aj
.text:F8224620    mov     edx, [esp+PreviousSuspendCount]
.text:F8224624    push    edx
.text:F8224625    push    esi
.text:F8224626    call    OrigNtResumeThread
.text:F822462C    pop     esi
.text:F822462D    retn    8
.text:F822462D KavNtResumeThread endp
.text:F822462D


.text:F8224590 ; NTSTATUS __stdcall KavNtSuspendThread(
		HANDLE ThreadHandle,
		PULONG PreviousSuspendCount)
.text:F8224590 sub_F8224590    proc near               ; DATA XREF: sub_F82249D0+113o
.text:F8224590
.text:F8224590 ThreadHandle    = dword ptr  8
.text:F8224590 PreviousSuspendCount= dword ptr  0Ch
.text:F8224590
.text:F8224590    push    esi
.text:F8224591    mov     esi, [esp+ThreadHandle]
.text:F8224595    test    esi, esi
.text:F8224597    jz      short loc_F82245D0
.text:F8224599    lea     eax, [esp+ThreadHandle] ;
.text:F8224599               ; This should pass an object type here!
.text:F822459D    push    0               ; HandleInformation
.text:F822459F    push    eax             ; Object
.text:F82245A0    push    0               ; AccessMode
.text:F82245A2    push    0               ; ObjectType
.text:F82245A4    push    0F0000h         ; DesiredAccess
.text:F82245A9    push    esi             ; Handle
.text:F82245AA    mov     [esp+18h+ThreadHandle], 0
.text:F82245B2    call    ds:ObReferenceObjectByHandle
.text:F82245B8    test    eax, eax
.text:F82245BA    jl      short loc_F82245D0
.text:F82245BC    mov     ecx, [esp+ThreadHandle]
.text:F82245C0    push    ecx
.text:F82245C1    call    KavUpdateThreadSuspendedState
.text:F82245C6    mov     ecx, [esp+ThreadHandle] ; Object
.text:F82245CA    call    ds:ObfDereferenceObject
.text:F82245D0
.text:F82245D0 loc_F82245D0:              ; CODE XREF: sub_F8224590+7j
.text:F82245D0               ; sub_F8224590+2Aj
.text:F82245D0    mov     edx, [esp+PreviousSuspendCount]
.text:F82245D4    push    edx
.text:F82245D5    push    esi
.text:F82245D6    call    OrigNtSuspendThread
.text:F82245DC    pop     esi
.text:F82245DD    retn    8
.text:F82245DD sub_F8224590    endp
.text:F82245DD

Not all of KAV's hooks are so fortunate, however. The NtTerminateProcess hook that KAV installs looks into the body of the object referred to by the process handle parameter of the function in order to determine the name of the process being terminated. However, KAV fails to validate that the object handle given by user mode really refers to a process object.

This is unsafe for several reasons, which may be well known to the reader if one is experienced with Windows kernel programming.

  1. The kernel process structure definition (EPROCESS) changes frequently from OS release to OS release, and even between service packs. As a result, it is not generally safe to access this structure directly.

  2. Because KAV does not perform proper type checking, it is possible to pass an object handle to a different kernel object - say, a mutex - which may cause KAV to bring down the system because the internal object structures of a mutex (or any other kernel object) are not compatible with that of a process object.

KAV attempts to work around the first problem by attempting to discover the offset of the member in the EPROCESS structure that contains the process name at runtime. The algorithm used is to scan forward one byte at a time from the start of the process object pointer until a sequence of bytes identifying the name of the initial system process is discovered. (This routine is called in the context of the initial system process). This routine appears to be very common amongst anti-virus and other low-level products that attempt to make use of the image file name associated with a process.

.text:F82209E0 KavFindEprocessNameOffset proc near     ; CODE XREF: sub_F8217A60+FCp
.text:F82209E0    push    ebx
.text:F82209E1    push    esi
.text:F82209E2    push    edi
.text:F82209E3    call    ds:IoGetCurrentProcess
.text:F82209E9    mov     edi, ds:strncmp
.text:F82209EF    mov     ebx, eax
.text:F82209F1    xor     esi, esi
.text:F82209F3
.text:F82209F3 loc_F82209F3:              ; CODE XREF: KavFindEprocessNameOffset+2Ej
.text:F82209F3    lea     eax, [esi+ebx]
.text:F82209F6    push    6               ; size_t
.text:F82209F8    push    eax             ; char *
.text:F82209F9    push    offset aSystem  ; "System"
.text:F82209FE    call    edi ; strncmp
.text:F8220A00    add     esp, 0Ch
.text:F8220A03    test    eax, eax
.text:F8220A05    jz      short loc_F8220A16
.text:F8220A07    inc     esi
.text:F8220A08    cmp     esi, 3000h
.text:F8220A0E    jl      short loc_F82209F3
.text:F8220A10    pop     edi
.text:F8220A11    pop     esi
.text:F8220A12    xor     eax, eax
.text:F8220A14    pop     ebx
.text:F8220A15    retn
.text:F8220A16 ; ---------------------------------------------------------------------------
.text:F8220A16
.text:F8220A16 loc_F8220A16:              ; CODE XREF: KavFindEprocessNameOffset+25j
.text:F8220A16    mov     eax, esi
.text:F8220A18    pop     edi
.text:F8220A19    pop     esi
.text:F8220A1A    pop     ebx
.text:F8220A1B    retn
.text:F8220A1B KavFindEprocessNameOffset endp

.text:F8217B5C    call    KavFindEprocessNameOffset
.text:F8217B61    mov     g_EprocessNameOffset, eax

Given a handle to an object of the wrong type, KAV will read from the returned object body pointer in an attempt to determine the name of the process being destroyed. This will typically run off the end of the structure for an object that is not a process object (the Process object is very large compared to some objects, such as a Mutex object, and the offset of the process name within this structure is typically several hundred bytes or more). It is expected that this will cause the system to crash if a bad handle is passed to NtTerminateProcess.

.text:F82241C0 ; NTSTATUS __stdcall KavNtTerminateProcess(HANDLE ThreadHandle,NTSTATUS ExitStatus)
.text:F82241C0 KavNtTerminateProcess proc near         ; DATA XREF: sub_F82249D0+ABo
.text:F82241C0
.text:F82241C0 var_58          = dword ptr -58h
.text:F82241C0 ProcessObject   = dword ptr -54h
.text:F82241C0 ProcessData     = KAV_TERMINATE_PROCESS_DATA ptr -50h
.text:F82241C0 var_4           = dword ptr -4
.text:F82241C0 ProcessHandle   = dword ptr  4
.text:F82241C0 ExitStatus      = dword ptr  8
.text:F82241C0
.text:F82241C0    sub     esp, 54h
.text:F82241C3    push    ebx
.text:F82241C4    xor     ebx, ebx
.text:F82241C6    push    esi
.text:F82241C7    mov     [esp+5Ch+ProcessObject], ebx
.text:F82241CB    call    KeGetCurrentIrql
.text:F82241D0    mov     esi, [esp+5Ch+ProcessHandle]
.text:F82241D4    cmp     al, 2           ;
.text:F82241D4               ; IRQL >= DISPATCH_LEVEL? Abort
.text:F82241D4               ; ( This is impossible for a system service )
.text:F82241D6    jnb     Ret_KavNtTerminateProcess
.text:F82241DC    cmp     esi, ebx        ;
.text:F82241DC               ; Null process handle? Abort
.text:F82241DE    jz      Ret_KavNtTerminateProcess
.text:F82241E4    call    PsGetCurrentProcessId
.text:F82241E9    mov     [esp+5Ch+ProcessData.CurrentProcessId], eax
.text:F82241ED    xor     eax, eax
.text:F82241EF    cmp     esi, 0FFFFFFFFh
.text:F82241F2    push    esi             ; ProcessHandle
.text:F82241F3    setnz   al
.text:F82241F6    dec     eax
.text:F82241F7    mov     [esp+60h+ProcessData.TargetIsCurrentProcess], eax
.text:F82241FB    call    KavGetProcessIdFromProcessHandle
.text:F8224200    lea     ecx, [esp+5Ch+ProcessObject] ; Object
.text:F8224204    push    ebx             ; HandleInformation
.text:F8224205    push    ecx             ; Object
.text:F8224206    push    ebx             ; AccessMode
.text:F8224207    push    ebx             ; ObjectType
.text:F8224208    push    0F0000h         ; DesiredAccess
.text:F822420D    push    esi             ; Handle
.text:F822420E    mov     [esp+74h+ProcessData.TargetProcessId], eax
.text:F8224212    mov     [esp+74h+var_4], ebx
.text:F8224216    call    ds:ObReferenceObjectByHandle
.text:F822421C    test    eax, eax
.text:F822421E    jl      short loc_F8224246
.text:F8224220    mov     edx, [esp+5Ch+ProcessObject]
.text:F8224224    mov     eax, g_EprocessNameOffset
.text:F8224229    add     eax, edx
.text:F822422B    push    40h             ; size_t
.text:F822422D    lea     ecx, [esp+60h+ProcessData.ProcessName]
.text:F8224231    push    eax             ; char *
.text:F8224232    push    ecx             ; char *
.text:F8224233    call    ds:strncpy
.text:F8224239    mov     ecx, [esp+68h+ProcessObject]
.text:F822423D    add     esp, 0Ch
.text:F8224240    call    ds:ObfDereferenceObject
.text:F8224246
.text:F8224246 loc_F8224246:              ; CODE XREF: KavNtTerminateProcess+5Ej
.text:F8224246    cmp     esi, 0FFFFFFFFh
.text:F8224249    jnz     short loc_F8224255
.text:F822424B    mov     edx, [esp+5Ch+ProcessData.TargetProcessId]
.text:F822424F    push    edx
.text:F8224250    call    sub_F8226710
.text:F8224255
.text:F8224255 loc_F8224255:              ; CODE XREF: KavNtTerminateProcess+89j
.text:F8224255    lea     eax, [esp+5Ch+ProcessData]
.text:F8224259    push    ebx             ; int
.text:F822425A    push    eax             ; ProcessData
.text:F822425B    call    KavCheckTerminateProcess
.text:F8224260    cmp     eax, 7
.text:F8224263    jz      short loc_F822427D
.text:F8224265    cmp     eax, 1
.text:F8224268    jz      short loc_F822427D
.text:F822426A    cmp     eax, ebx
.text:F822426C    jz      short loc_F822427D
.text:F822426E    mov     esi, STATUS_ACCESS_DENIED
.text:F8224273    mov     eax, esi
.text:F8224275    pop     esi
.text:F8224276    pop     ebx
.text:F8224277    add     esp, 54h
.text:F822427A    retn    8
.text:F822427D ; ---------------------------------------------------------------------------
.text:F822427D
.text:F822427D loc_F822427D:              ; CODE XREF: KavNtTerminateProcess+A3j
.text:F822427D               ; KavNtTerminateProcess+A8j ...
.text:F822427D    mov     eax, [esp+5Ch+ProcessData.TargetProcessId]
.text:F8224281    cmp     eax, 1000h
.text:F8224286    jnb     short loc_F8224296
.text:F8224288    mov     dword_F8228460[eax*8], ebx
.text:F822428F    mov     byte_F8228464[eax*8], bl
.text:F8224296
.text:F8224296 loc_F8224296:              ; CODE XREF: KavNtTerminateProcess+C6j
.text:F8224296    push    eax
.text:F8224297    call    sub_F82134D0
.text:F822429C    mov     ecx, [esp+5Ch+ProcessData.TargetProcessId]
.text:F82242A0    push    ecx
.text:F82242A1    call    sub_F8221F70
.text:F82242A6    mov     edx, [esp+5Ch+ExitStatus]
.text:F82242AA    push    edx
.text:F82242AB    push    esi
.text:F82242AC    call    OrigNtTerminateProcess
.text:F82242B2    mov     esi, eax
.text:F82242B4    lea     eax, [esp+5Ch+ProcessData]
.text:F82242B8    push    1               ; int
.text:F82242BA    push    eax             ; ProcessData
.text:F82242BB    mov     [esp+64h+var_4], esi
.text:F82242BF    call    KavCheckTerminateProcess
.text:F82242C4    mov     eax, esi
.text:F82242C6    pop     esi
.text:F82242C7    pop     ebx
.text:F82242C8    add     esp, 54h
.text:F82242CB    retn    8
.text:F82242CE ; ---------------------------------------------------------------------------
.text:F82242CE
.text:F82242CE Ret_KavNtTerminateProcess:              ; CODE XREF: KavNtTerminateProcess+16j
.text:F82242CE               ; KavNtTerminateProcess+1Ej
.text:F82242CE    mov     ecx, [esp+5Ch+ExitStatus]
.text:F82242D2    push    ecx
.text:F82242D3    push    esi
.text:F82242D4    call    OrigNtTerminateProcess
.text:F82242DA    pop     esi
.text:F82242DB    pop     ebx
.text:F82242DC    add     esp, 54h
.text:F82242DF    retn    8
.text:F82242DF KavNtTerminateProcess endp

The whole purpose of this particular system service hook is ``shady'' as well. The hook prevents certain KAV processes from being terminated, even by a legitimate computer administrator - something that is once again typically associated with malicious software, such as rootkits, rather than commercial software applications. One possible explanation for this is that it is an attempt to prevent viruses from terminating the virus scanner processes itself, although one wonders how much of a concern this would be if KAV's real-time scanning mechanisms really do work as advertised.

Additionally, KAV appears to do some state tracking just before the process is terminated with this system service hook. The proper way to do this would have been through PsSetCreateProcessNotifyRoutine which is a documented kernel function that allows drivers to register a callback that is called on process creation and process exit.