This post was written by Marcin Noga with contributions by Earl Carter and Martin Lee.
New vulnerabilities for old operating systems may not seem particularly interesting, until you consider the large number of legacy machines running outdated versions of Windows. Windows XP has reached its end of life, meaning that new vulnerabilities will not be patched. In this post we will show that a recent vulnerability can be used as a platform for exploiting Windows XP.
In October, Microsoft released a bulletin for a privilege escalation vulnerability in the FASTFAT driver that was released as:
MS14-063 -- Vulnerability in FAT32 Disk Partition Driver Could Allow Elevation of Privilege (2998579), CVE-2014-4115.
Let me present some of the most interesting parts of the advisory and add some details from my own research.
When the bug kicks in…
In the advisory, Microsoft indicates that the following OS’s are vulnerable:
- Microsoft Windows Server 2003 SP2
- Vista SP2
- Server 2008 SP2
The Microsoft bulletin does not mention Windows XP, since Windows XP is no longer supported. According to my research, however, this vulnerability is also present in the Windows XP FASTFAT driver.
See the following video.
This vulnerability can be exploited on Windows XP SP3 using a malicious usb stick with a malformed FAT32 partition. Let’s examine the reaction when the USB is inserted into the system.
General Description
Vulnerable code existing in the FastFAT.sys driver can be exploited via an intentionally malformed FAT32 Boot sector delivered through a USB flash drive. If the field “Number of FATs” contains a value greater than 2, the driver will allocate insufficient memory. Further actions made by FastFAT.sys driver will then cause a pool overflow.
How it happens -- code analysis
The following is an example of a malformed FAT32 Boot Sector. The “Number of FATs” field is set to 119 (0x77 in hex), instead of the normal value of 2, making the boot sector malformed.
Member "Value (dec)" "Value (hex)" Size
" 00000000 struct BOOTSECTOR_FAT32" {...} 00000200
" 00000000 int8 jmp[00000003]" 00000003
" 00000003 char OemName[00000008]" MSDOS5.0 00000008
" 0000000B struct BPB_FAT32" {...} 00000035
" 0000000B uint16 BytesPerSector" 512 0200 00000002
" 0000000D int8 SectorsPerCluster" 1 01 00000001
" 0000000E uint16 ReservedSectors" 36 0024 00000002
" 00000010 int8 NumberOfFATs" 119 77 00000001
" 00000011 uint16 RootEntries" 0 0000 00000002
" 00000013 uint16 TotalSectors" 0 0000 00000002
" 00000015 int8 Media" -8 F8 00000001
" 00000016 uint16 SectorsPerFAT" 0 0000 00000002
" 00000018 uint16 SectorsPerTrack" 63 003F 00000002
" 0000001A uint16 HeadsPerCylinder" 255 00FF 00000002
" 0000001C uint32 HiddenSectors" 63 0000003F 00000004
" 00000020 uint32 TotalSectorsBig" 80262 00013986 00000004
"++ FAT32 Section
" 00000024 uint32 SectorsPerFAT" 618 0000026A 00000004
" 00000028 uint16 Flags" 0 0000 00000002
" 0000002A uint16 Version" 0 0000 00000002
" 0000002C uint32 RootCluster" 2 00000002 00000004
" 00000030 uint16 InfoSector" 1 0001 00000002
" 00000032 uint16 BootBackupStart" 6 0006 00000002
" 00000034 int8 Reserved[0000000C]" 0000000C
" 00000040 int8 DriveNumber" -128 80 00000001
" 00000041 int8 Unused" 1 01 00000001
" 00000042 int8 ExtBootSignature" 41 29 00000001
" 00000043 uint32 SerialNumber" 3896654535 E8423AC7 00000004
" 00000047 char VolumeLabel[0000000B]" "NO NAME " 0000000B
" 00000052 char FileSystem[00000008]" "FAT32 " 00000008
" 0000005A blob BootCode[000001A6]" 000001A6
Memory corruption is a common driver problem. Activation of a special pool will help us to locate the moment when a pool overflow starts to appear
verifier /volatile /flags 0x1 /adddriver fastfat.sys
After insertion of USB flash drive with a malformed FAT32 we can examine the crash dump that occurs due to insufficient memory allocation.
Crash dump info
kd> !analyze -v
DRIVER_PAGE_FAULT_BEYOND_END_OF_ALLOCATION (d6)
N bytes of memory was allocated and more than N bytes are being referenced.
This cannot be protected by try-except.
When possible, the guilty driver's name (Unicode string) is printed on
the bugcheck screen and saved in KiBugCheckDriver.
Arguments:
Arg1: 8a011008, memory referenced
Arg2: 00000001, value 0 = read operation, 1 = write operation
Arg3: b7ae63da, if non-zero, the address which referenced memory.
Arg4: 00000000, (reserved)
Debugging Details:
------------------
WRITE_ADDRESS: 8a011008 Special pool
FAULTING_IP:
Fastfat!FatCommonWrite+444
b7ae63da 8978fc mov dword ptr [eax-4],edi
MM_INTERNAL_CODE: 0
IMAGE_NAME: Fastfat.SYS
DEBUG_FLR_IMAGE_TIMESTAMP: 48025b94
MODULE_NAME: Fastfat
FAULTING_MODULE: b7ada000 Fastfat
DEFAULT_BUCKET_ID: DRIVER_FAULT
BUGCHECK_STR: 0xD6
PROCESS_NAME: System
TRAP_FRAME: bacdf8c0 -- (.trap 0xffffffffbacdf8c0)
ErrCode = 00000002
eax=8a01100c ebx=8a7ece90 ecx=00186c00 edx=00000800 esi=89a14b18 edi=00004800
eip=b7ae63da esp=bacdf934 ebp=bacdfab4 iopl=0 nv up ei ng nz ac pe cy
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010297
Fastfat!FatCommonWrite+0x444:
b7ae63da 8978fc mov dword ptr [eax-4],edi ds:0023:8a011008=????????
Resetting default scope
LAST_CONTROL_TRANSFER: from 8051cc4f to 804f8cb5
STACK_TEXT:
bacdf848 8051cc4f 00000050 8a011008 00000001 nt!KeBugCheckEx+0x1b
bacdf8a8 8054051c 00000001 8a011008 00000000 nt!MmAccessFault+0x8e7
bacdf8a8 b7ae63da 00000001 8a011008 00000000 nt!KiTrap0E+0xcc
bacdfab4 b7adab9a 89a14b18 8a7ece90 89c59020 Fastfat!FatCommonWrite+0x444
bacdfaf8 804ee119 89c59020 8a7ece90 806d12a4 Fastfat!FatFsdWrite+0xad
bacdfb08 8064d628 89ada170 00004000 89c59020 nt!IopfCallDriver+0x31
bacdfb2c 804ef411 bacdfb68 bacdfd40 00000000 nt!IovCallDriver+0xa0
bacdfb40 8050c497 89ada107 bacdfb68 bacdfbfc nt!IoSynchronousPageWrite+0xaf
bacdfc24 8050ce3d e10ec820 e10ec828 e10ec828 nt!MiFlushSectionInternal+0x3bf
bacdfc60 804e38a2 89c33d70 e10ec820 00000004 nt!MmFlushSection+0x1b5
bacdfce8 804e3bc4 00001000 00000000 00000001 nt!CcFlushCache+0x386
bacdfd2c 804e61ee 89da0290 8055b0c0 89da1da8 nt!CcWriteBehind+0xdc
bacdfd74 80534c02 89da0290 00000000 89da1da8 nt!CcWorkerThread+0x126
bacdfdac 805c6160 89da0290 00000000 00000000 nt!ExpWorkerThread+0x100
bacdfddc 80541dd2 80534b02 00000000 00000000 nt!PspSystemThreadStartup+0x34
00000000 00000000 00000000 00000000 00000000 nt!KiThreadStartup+0x16
STACK_COMMAND: kb
FOLLOWUP_IP:
Fastfat!FatCommonWrite+444
b7ae63da 8978fc mov dword ptr [eax-4],edi
SYMBOL_STACK_INDEX: 3
SYMBOL_NAME: Fastfat!FatCommonWrite+444
FOLLOWUP_NAME: MachineOwner
FAILURE_BUCKET_ID: 0xD6_VRF_Fastfat!FatCommonWrite+444
BUCKET_ID: 0xD6_VRF_Fastfat!FatCommonWrite+444
Followup: MachineOwner
Detailed analysis
To help debug driver memory issues, you can enable the special pool feature. This feature causes each memory allocation to be placed on a separate page. With the special pool active for fastfat.sys, you should begin executing the following when the overflow occurs,:
PAGE:0001C37A movzx edx, word ptr [eax+90h]
PAGE:0001C381 movzx ecx, cx
PAGE:0001C384 imul edx, ecx
PAGE:0001C387 mov [ebp+BytesPerFat], edx
PAGE:0001C38D
PAGE:0001C38D loc_1C38D:
PAGE:0001C38D mov cl, [eax+96h]
PAGE:0001C393 cmp cl, 2 ; cl = Number of FATs
PAGE:0001C396 jbe short RegularFATsAmount
PAGE:0001C398 push 'itaF' ; Tag
PAGE:0001C39D movzx eax, cl
PAGE:0001C3A0 push eax ; Number of FATs used as pool size
PAGE:0001C3A1 push 11h ; PoolType
PAGE:0001C3A3 call ds:__imp__ExAllocatePoolWithTag
PAGE:0001C3A9 mov edi, eax
PAGE:0001C3AB mov eax, [ebp+VCB]
PAGE:0001C3AE jmp short loc_1C3B6
PAGE:0001C3B0 ; ------------------------------------------------------------------
PAGE:0001C3B0
PAGE:0001C3B0 RegularFATsAmount:
PAGE:0001C3B0 lea edi, [ebp+var_174]
PAGE:0001C3B6
PAGE:0001C3B6 loc_1C3B6:
PAGE:0001C3B6 mov [ebp+unknow], edi
PAGE:0001C3BC and [ebp+counter], 0
PAGE:0001C3C3 cmp byte ptr [eax+96h], 0
PAGE:0001C3CA jbe short loc_1C410
PAGE:0001C3CC mov ecx, [ebp+offsetToFirstFAT]
PAGE:0001C3CF mov edx, ecx
PAGE:0001C3D1 sub edx, [ebp+StartingVbo]
PAGE:0001C3D4 lea eax, [edi+0Ch]
PAGE:0001C3D7
PAGE:0001C3D7 fillArrayLoop:
PAGE:0001C3D7 mov edi, [ebp+offsetToFirstFAT]
PAGE:0001C3DA mov [eax-4], edi
PAGE:0001C3DD mov [eax-0Ch], ecx
PAGE:0001C3E0 and dword ptr [eax-8], 0
PAGE:0001C3E4 mov [eax], edx
PAGE:0001C3E6 mov edi, [ebp+unknow3]
PAGE:0001C3EC mov [eax+4], edi
PAGE:0001C3EF inc [ebp+counter]
PAGE:0001C3F5 add ecx, [ebp+BytesPerFat]
PAGE:0001C3FB add eax, 18h
PAGE:0001C3FE mov edi, [ebp+VCB]
PAGE:0001C401 movzx edi, byte ptr [edi+96h] ; to EDI goes Number of FATs
PAGE:0001C408 cmp [ebp+counter], edi
PAGE:0001C40E jb short fillArrayLoop
Note: When the number of FATs is bigger than 2, the pool allocated is simply the number of FATs (instead of the number of FATs times size of a FAT entry)
PAGE:0001C39D movzx eax, cl
PAGE:0001C3A0 push eax ; Number of FATs used as pool size
PAGE:0001C3A1 push 11h ; PoolType
PAGE:0001C3A3 call ds:__imp__ExAllocatePoolWithTag
The Next loop appears and it iterates the number of times indicated by the number of FATs. In this loop, labeled as fillArrayLoop, the pool allocated by the code describe above is filled by an array of structures where the size of the structure equals 24 bytes (each structure consists of 6 DWORD’s). It Is easy to deduce that the iteration will lead to a pool overflow because during the pool allocation, the number of FATs was taken into account without including the size of structure element. It’s even easier to understand on pseudo code:
NumberOfFATs = *(_BYTE *)(VCB + 150);
if ( NumberOfFATs <= 2u )
{
ptrPool = &v100;
}
else
{
ptrPool = ExAllocatePoolWithTag((POOL_TYPE)17, NumberOfFATs, 'itaF');
[BUG]//NumberOfFATs !!! instead of NumberOfFATs * sizeof(SomeStructure)=24
v14 = VCB;
}
counter = 0;
if ( *(_BYTE *)(v14 + 0x96) )
{
v18 = offsetToFirstFAT;
unknow1 = offsetToFirstFAT - unknow2;
v20 = (int)((char *)ptrPool + 12);
do
{
*(_DWORD *)(v20 - 4) = offsetToFirstFAT;
*(_DWORD *)(v20 - 12) = v18;
*(_DWORD *)(v20 - 8) = 0;
*(_DWORD *)v20 = unknow1;
*(_DWORD *)(v20 + 4) = unknow3;
++counter;
v18 += BytesPerFat;
v20 += 24;
}
while ( counter < NumberOfFATs );
}
Exploitability
After three iterations of the loop cycle, here is how this array looks in memory:
#1
00 48 00 00
00 00 00 00
00 48 00 00
00 08 00 00
00 02 00 00
ef ef ef ef <- not initialized
#2
00 1c 05 00
00 00 00 00
00 48 00 00
00 08 00 00
00 02 00 00
ef ef ef ef <- not initialized
#3
00 f0 09 00
00 00 00 00
00 48 00 00
00 08 00 00
00 02 00 00
ef ef ef ef <- not initialized
Which values at a particular offset are we able to control ?
Offset +0: (v20 - 12)
For the first iteration it’s value equals offsetToFirstFAT and is the result of adding:
(v20 - 12) = (v20 - 12) + BytesPerFat.
Note: BytesPerFAT equals : Bytes per sector(= 512) * Sectors per fat(= 618) == 0x4D400
(316416)
Offset +4: (v20 - 8)
Constant 0
Offset + 8: (v20 - 4)
The value of this field always equals offsetToFirstFAT.
Note: offsetToFirstFAT : Bytes per sector * Reserved sectors == (0x4800)
Taking into account the information about how this bug is triggered and what portion of data we are able to control during page overflow this vulnerability appears to be hard to exploit. The most lucrative scenario for attacker would be to create a malformed FAT32 partition (that does not cause crashes after usb stick insertion) which contains an executable file with the possibility for the victim to run it.
When run, this executable, for example, would be responsible for kernel pool spraying. After that it would write any random data to the malformed partition, triggering at the same time the bug in FatCommonWrite. My observations show that when you create a nearly empty malformed partition containing a couple files, the bug only triggers when you open one of these files and try to save (write) modifications.
Even though this vulnerability seems to be hard to exploit, we should not disregard this vulnerability because history and recently Chris Evans’ write-up about off-by-one shows that any kind of scenario can become real in some circumstances.