Monday, February 22, 2010

The Fix That Never Was

Approximately two years ago I found a memory corruption bug in one of the kernel drivers shipped with avast! 4.7. After I reported the bug to ALWIL Software they only took about two weeks to provide a fixed version. That's quite fast for a commercial software vendor. To be honest, I never checked if and how the bug was fixed with that new version. Anyway, a few weeks ago I decided to have a look at the avast! drivers again. While reversing the drivers I quickly realized that the measures taken by ALWIL Software to fix my 2 year old bug weren't sufficient.

►  A Short Recap of the "old" Bug

Disassembly of the vulnerable aavmker4.sys driver (avast! 4.7, file version 4.7.1098.0):
[..]
.text:00010E06    loc_10E06: 
.text:00010E06      xor     edx, edx
.text:00010E08      mov     eax, [ebp+v38_uc]
.text:00010E0B      mov     [eax], edx
.text:00010E0D      mov     [eax+4], edx
.text:00010E10      add     esi, 4         ; src
.text:00010E13      mov     ecx, 21Ah      ; len
.text:00010E18      mov     edi, [eax+18h] ; dst
.text:00010E1B      rep movsd              ; memcpy
[..]
The memcpy() function at .text:00010E1B gets called with the following parameters:
memcpy (EDI, ESI, ECX); 
 
EDI (dst): the value is extracted from user controlled IOCTL input data
ESI (src): points to user controlled IOCTL input data
ECX (len): 0x21A
As both the destination address as well as the source data of that memcpy() call could be controlled by the requesting user it was possible to overwrite arbitrary memory addresses with arbitrary values. This could be exploited to control the kernel execution flow and to execute arbitrary code at the kernel level. For more details see TKADV2008-002.

► The Fix

The "new" version of the kernel driver (avast! 4.8 <= 4.8.1368.0) implements the following check to remediate the described bug.

Disassembly of the vulnerable aavmker4.sys driver (avast! 4.8, file version 4.8.1356.0):
[..] 
.text:00010F21    loc_10F21:  
.text:00010F21      and    [ebp+var_4], 0 
.text:00010F25      cmp    dword ptr [edi], 0 
.text:00010F28      jz     loc_10FC5 
.text:00010F2E [1]  mov    esi, [edi+870h] 
.text:00010F34      mov    [ebp+v34_uc], esi 
.text:00010F37      mov    eax, ds:MmUserProbeAddress 
.text:00010F3C [2]  cmp    esi, [eax]      ; user space or kernel space?
.text:00010F3E      jnb    short loc_10F46 
[..]
Before the described memcpy() function is called a pointer value gets extracted from the user supplied IOCTL input data. That value is then stored in ESI (see [1]). Then it is checked if ESI points into user space or kernel space (see [2]). This is done by comparing the pointer with MmUserProbeAddress. If ESI points into kernel space, the memcpy() function gets called as before. If it points into user space memcpy() won't be called.

If ESI points into kernel space the following code gets executed:
[..] 
.text:00010F99    loc_10F99: 
.text:00010F99      xor    edx, edx 
.text:00010F9B      mov    eax, [ebp+v34_uc] 
.text:00010F9E      mov    [eax], edx 
.text:00010FA0      mov    [eax+4], edx 
.text:00010FA3      lea    esi, [edi+4]    ; src 
.text:00010FA6      mov    ecx, 21Ah       ; len 
.text:00010FAB      mov    edi, [eax+18h]  ; dst 
.text:00010FAE      rep movsd              ; memcpy 
[..]
The memcpy() function at .text:00010FAE gets called with the following parameters:
memcpy (EDI, ESI, ECX); 
 
EDI (dst): the value is extracted from a user defined kernel space address
ESI (src): points to user controlled IOCTL input data
ECX (len): 0x21A
Assumption of the "new" check: As the destination address of memcpy() is now extracted from kernel space memory it isn't possible to directly control that value from user space anymore.

This only holds true if we weren't able to (temporarily) store user controlled data at a user defined address in kernel space. Unfortunately, the aavmker4.sys driver supports at least one IOCTL that allows an unprivileged user to temporarily store arbitrary data at a known kernel space address.

Well, the fix that never was ;)

► Exploitation

Exploiting this bug is quite easy:

STEP 1: Use one of the IOCTLs supported by aavmker4.sys to temporarily store arbitrary data at a known kernel space address (e.g. the IOCTL 0xb2d6001c).

STEP 2: Send a request to the vulnerable IOCTL. Store a pointer at offset 0x870 of the IOCTL data that points to the kernel space address of STEP 1.

STEP 3: Manipulate a function pointer.

STEP 4: Fun and profit.

Working out the rest of the attack is left as an exercise for the reader ;)

Affected are avast! 4.8 <= 4.8.1368.0 and avast! 5.0 < 5.0.418.0. See also my security advisory (TKADV2010-003) for further information.

► EIP Control

I wrote a proof of concept (poc) that gains control of the kernel execution flow (EIP control). Output of the kernel debugger when executing the poc (avast! 5.0 under Windows XP SP3):
#################### AAVMKER: WRONG RQ ######################!
Access violation - code c0000005 (!!! second chance !!!)
41414141 ??              ???

kd> kb
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for Aavmker4.SYS - 
ChildEBP RetAddr  Args to Child              
WARNING: Frame IP not in any known module. Following frames may be wrong.
edc99bdc f7955cae 8621ada8 e21f0030 00000001 0x41414141
edc99c34 804ee129 861f8030 866c0d98 806d22d0 Aavmker4+0xcae
edc99c44 80574dde 866c0e08 865833c0 866c0d98 nt!IopfCallDriver+0x31
edc99c58 80575c7f 861f8030 866c0d98 865833c0 nt!IopSynchronousServiceTail+0x70
edc99d00 8056e4ec 000002f8 00000000 00000000 nt!IopXxxControlFile+0x5e7
edc99d34 8053d648 000002f8 00000000 00000000 nt!NtDeviceIoControlFile+0x2a
edc99d34 7c91e514 000002f8 00000000 00000000 nt!KiFastCallEntry+0xf8
039cb1b0 65006e90 000002f8 b2d60034 039cb1fc 0x7c91e514
039cb24c 00000000 00000000 00000000 00000000 0x65006e90

kd> r
eax=8621ada8 ebx=866c0d98 ecx=00000000 edx=00000000 esi=867d2bc0 edi=80527660
eip=41414141 esp=edc99be0 ebp=edc99c34 iopl=0         nv up ei pl nz na po nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010202
41414141 ??              ???
If no kernel debugger is attached the poc gives the following BSoD: