- Date: October 6, 2009
- Author: Bow Sineath, Security Researcher,
SecureWorks Counter Threat Unit SM (CTU)
The recent SMBv2 vulnerability (CVE-2009-3103) in Microsoft Windows has gotten a lot of attention in the past few weeks. We decided that given the publicity and nature of the vulnerability, it would be interesting to post a threat analysis. With the release of Stephen Fewer's Metasploit module to exploit this vulnerability, technical details of the vulnerability are now publicly available.
Our analysis was limited to static binary analysis of srv2.sys and srvnet.sys.
The crash occurs within Smb2ValidateProviderCallback(PVOID DestinationBuffer):
.text:00017745
.text:00017745 loc_17745:
.text:00017745 movzx eax, word ptr [esi+0Ch]
.text:00017749 mov eax, _ValidateRoutines[eax*4]
.text:00017750 test eax, eax
.text:00017752 jnz short loc_1775B
This code is accessing an array of function pointers using a user-supplied index. This function pointer is then called here:
.text:0001775B
.text:0001775B loc_1775B:
.text:0001775B push ebx
.text:0001775C call eax ; Smb2ValidateNegotiate(x) ; Smb2ValidateNegotiate
The table consists of 19 function pointers, which seem to validate requests prior to actually executing them.
.data:0002D270 _ValidateRoutines dd offset _Smb2ValidateNegotiate@4
.data:0002D270 ; DATA XREF: Smb2ValidateProviderCallback(x)+4EA r
.data:0002D270 ; Smb2ValidateNegotiate(x)
.data:0002D274 dd offset _Smb2ValidateSessionSetup@4 ; Smb2ValidateSessionSetup(x)
.data:0002D278 dd offset _Smb2ValidateLogoff@4 ; Smb2ValidateLogoff(x)
.data:0002D27C dd offset _Smb2ValidateTreeConnect@4 ; Smb2ValidateTreeConnect(x)
.data:0002D280 dd offset _Smb2ValidateTreeDisconnect@4 ;
Smb2ValidateTreeDisconnect(x)
.data:0002D284 dd offset _Smb2ValidateCreate@4 ; Smb2ValidateCreate(x)
.data:0002D288 dd offset _Smb2ValidateClose@4 ; Smb2ValidateClose(x)
.data:0002D28C dd offset _Smb2ValidateFlush@4 ; Smb2ValidateFlush(x)
.data:0002D290 dd offset _Smb2ValidateRead@4 ; Smb2ValidateRead(x)
.data:0002D294 dd offset _Smb2ValidateWrite@4 ; Smb2ValidateWrite(x)
.data:0002D298 dd offset _Smb2ValidateLock@4 ; Smb2ValidateLock(x)
.data:0002D29C dd offset _Smb2ValidateIoctl@4 ; Smb2ValidateIoctl(x)
.data:0002D2A0 dd offset _Smb2ValidateCancel@4 ; Smb2ValidateCancel(x)
.data:0002D2A4 dd offset _Smb2ValidateEcho@4 ; Smb2ValidateEcho(x)
.data:0002D2A8 dd offset _Smb2ValidateQueryDirectory@4 ;
Smb2ValidateQueryDirectory(x)
.data:0002D2AC dd offset _Smb2ValidateChangeNotify@4 ;
Smb2ValidateChangeNotify(x)
.data:0002D2B0 dd offset _Smb2ValidateQueryInfo@4 ; Smb2ValidateQueryInfo(x)
.data:0002D2B4 dd offset _Smb2ValidateSetInfo@4 ; Smb2ValidateSetInfo(x)
.data:0002D2B8 dd offset _Smb2ValidateOplockBreak@4 ; Smb2ValidateOplockBreak(x)
When the driver is first loaded, it initializes a series of structures that are responsible for registering the driver. One of the first steps that occurs is registering a series of callbacks:
PAGE:0002EFCF push offset _SrvNetProvider
PAGE:0002EFD4 lea eax, [ebp+DestinationString]
PAGE:0002EFD7 push eax
PAGE:0002EFD8 mov [ebp+var_14], offset _SrvConnectHandler@16 ; SrvConnectHandler(x,x,x,x)
PAGE:0002EFDF mov [ebp+var_C], offset _SrvDisconnectHandler@12 ; SrvDisconnectHandler(x,x,x)
PAGE:0002EFE6 mov [ebp+var_10], offset _SrvReceiveHandler@36 ;
SrvReceiveHandler(x,x,x,x,x,x,x,x,x)
PAGE:0002EFED mov [ebp+var_18], offset _SrvNegotiateHandler@16 ; SrvNegotiateHandler(x,x,x,x)
PAGE:0002EFF4 mov [ebp+var_20], offset _SrvRegisterEndpoint@28 ;
SrvRegisterEndpoint(x,x,x,x,x,x,x)
PAGE:0002EFFB mov [ebp+var_1C], offset _SrvDeregisterEndpoint@12 ; SrvDeregisterEndpoint(x,x,x)
PAGE:0002F002 mov [ebp+var_8], offset _SrvCredentialHandler@16 ; SrvCredentialHandler(x,x,x,x)
PAGE:0002F009 call _SrvNetRegisterClient@8 ; SrvNetRegisterClient(x,x)
srvnet.sys is another driver that exports the SrvNetRegisterClient() routine. The srvnet.sys routine modifies a device extension (http://msdn.microsoft.com/en-us/library/ms794734.aspx), which maintains some internal state on each driver that registers via SrvNetRegisterClient(). This object is allocated with a size of 0x160 bytes when srvnet.sys is loaded (From DriverLoad()):
INIT:00028180
INIT:00028180 loc_28180:
INIT:00028180 lea eax, [ebp+DeviceObject]
INIT:00028183 push eax ; DeviceObject
INIT:00028184 push 0 ; Exclusive
INIT:00028186 push 100h ; DeviceCharacteristics
INIT:0002818B push 14h ; DeviceType
INIT:0002818D lea eax, [ebp+DestinationString]
INIT:00028190 push eax ; DeviceName
INIT:00028191 push 160h ; DeviceExtensionSize
INIT:00028196 push [ebp+DriverObject] ; DriverObject
INIT:00028199 call ds:__imp__IoCreateDevice@28 ; IoCreateDevice(x,x,x,x,x,x,x)
INIT:0002819F mov esi, eax
INIT:000281A1 test esi, esi
INIT:000281A3 jge short loc_281DF
INIT:000281FD mov eax, [ebp+DeviceObject]
INIT:00028200 mov eax, [eax+DEVICE_OBJECT.DeviceExtension]
INIT:00028203 push eax ; Resource
INIT:00028204 mov _SrvNetDeviceExtension, eax ;
Store ptr to DeviceExtension in a global variable
Within the undocumented device extension, an array of no more than 4 pointers to objects created by SrvNetRegisterClient() is maintained. These objects are allocated at the start of SrvNetRegisterClient():
.text:00014BF9 push 6662534Ch ; Tag
.text:00014BFE add eax, 78h
.text:00014C01 push eax ; int
.text:00014C02 push edi ; PoolType
.text:00014C03 call _SrvNetAllocatePoolWithTag@12 ; SrvNetAllocatePoolWithTag(x,x,x)
.text:00014C08 mov ebx, eax
.text:00014C0A cmp ebx,
The pointer to the object is then added at the end of the array in the device extension:
.text:00014D5B mov ecx, _SrvNetDeviceExtension
.text:00014D61 mov [ecx+esi*4+0DCh], ebx
Each of these objects contains the function pointers shown when srv2.sys calls SrvNetRegisterClient():
.text:00014C77 pop ecx ; ECX = 9
.text:00014C78 lea edi, [ebx+4Ch] ; EBX = DeviceExtension,
ESI = arg_0 (pointer to base of function pointer list)
.text:00014C7B rep movsd ; move 9 DWORD objects from *ESI into *EDI
The array roughly looks like this:
0x4C : 8 byte LSA_UNICODE_STRING structure
0x54 : *SrvRegisterEndpoint()
0x58 : *SrvDeRegisterEndpoint()
0x5C : *SrvNegotiateHandler()
0x60 : *SrvConnectHandler()
0x64 : *SrvReceiveHandler()
.....
Later in srvnet.sys, these routines will be called, for example within SrvNetCommonReceiveHandler():
.text:00016477 loc_16477:
.text:00016477 movzx eax, word ptr [ebp+var_8]
.text:0001647B mov ecx, _SrvNetDeviceExtension
.text:00016481 lea eax, [ecx+eax*4+0DCh]
.text:00016488 cmp dword ptr [eax], 0
.text:0001648B jz short loc_164B2
.text:00016495 push [ebp+arg_14]
.text:00016498 mov eax, [edi+70h]
.text:0001649B push [ebp+arg_8]
.text:0001649E push [ebp+arg_4]
.text:000164A1 push dword ptr [ebx+eax*4+0CCh]
.text:000164A8 call dword ptr [edi+5Ch] ; Call SrvNegotiateHandler()
from DeviceExtension->CallbackArray
.text:000164AB test eax, eax
.text:000164AD mov [ebp+var_4], eax
.text:000164B0 jge short loc_1
The negotiate handler performs some validation, the most important of which is this check:
.text:0001602B cmp byte ptr [eax+4], 72h ; EAX = SMB packet data
.text:0001602F jnz loc_160EC
This checks the second DWORD in the packet for the negotiate SMB command, which is 0x72. If this check fails, then the routine returns an error.
Continuing to follow the code down in SrvNetCommonReceiveHandler() inside of srvnet.sys, we see that shortly after the call to the SrvNegotiateHandler() callback, the pointer to SrvConnectHandler() is stored in a structure:
.text:000164BE
.text:000164BE loc_164BE: ;
.text:000164BE lea eax, [edi+60h] ;
.text:000164C1 mov [esi+16Ch], eax ; SrvConnectHandler()
.text:000164C7 mov eax, [edi+70h]
.text:000164CA mov eax, [ebx+eax*4+0CCh]
.text:000164D1 mov [esi+0A8h], eax
.text:000164D7 mov eax, _pSrv2TraceInfo
.text:000164DC test byte ptr [eax+0Ch], 1
.text:000164E0 jz short loc_165
This pointer is accessed again later within SrvNetCommandReceiveHandler():
.text:000165D0 mov [ebp+var_14], ax
.text:000165D4 mov eax, [esi+16Ch]
.text:000165DA push ebx
.text:000165DB call dword ptr [eax] ; SrvConnectHandler()
We then see it being used to call SrvReceiveHandler() shortly after:
.text:00016687 loc_16687:
.text:00016687 push [ebp+arg_20]
.text:0001668A mov eax, [esi+16Ch]
.text:00016690 push [ebp+arg_1C]
.text:00016693 mov dword ptr [esi+8], 3
.text:0001669A push [ebp+arg_14]
.text:0001669D push [ebp+arg_C]
.text:000166A0 push [ebp+arg_8]
.text:000166A3 push [ebp+arg_4]
.text:000166A6 push [ebp+arg_10]
.text:000166A9 push dword ptr [edi]
.text:000166AB push dword ptr [esi+0A8h]
.text:000166B1 call dword ptr [eax+4] ; SrvReceiveHandler()
This chain of function calls will be important when understanding how the data passes between the different routines in srv2.sys.
The srsv2.sys driver maintains an internal list of "service providers" that provide different services, including validation and execution. This list is initialized in DriverStart() by calling Smb2ProviderRegister(), which calls another routine, SrvRegisterProvider(), which maintains a global list of providers within the driver. The SrvRegisterProvider() routine takes the following structure in addition to a callback as arguments:
.text:0001235B ; int __stdcall Smb2ProviderRegister()
.text:0001235B _Smb2ProviderRegister@0 proc
.text:0001235B push 2
.text:0001235D push 3050h
.text:00012362 push offset _Smb2ValidateProviderCallback@4 ; Smb2ValidateProviderCallback(x)
.text:00012367 push offset _Smb2ValidateProviderName ; "Smb2Validate"
.text:0001236C call _SrvRegisterProvider@16 ; SrvRegisterProvider(x,x,x,x
_Smb2ValidateProviderName:
.data:0002D164 _Smb2ValidateProviderName dw 18h
; DATA XREF: Smb2ProviderRegister()+C o
.data:0002D166 dw 18h
.data:0002D168 dd offset aSmb2validate ; "Smb2Validate"
The SrvRegisterProvider() routine is also responsible for the initialization of a 36-byte structure. I didn't reverse engineer the entire structure, but there are a few important offsets noted in the comments:
.data:0002D138 _NullProvider dw 0Ah ; DATA XREF: SrvInitializeProviderList() o
.data:0002D138 ; SrvCleanupProviderList()+D o
.data:0002D13A dw 0
.data:0002D13C dd 0DCh
.data:0002D140 dw 24h
.data:0002D142 dw 0
.data:0002D144 dd 1
.data:0002D148 dd offset _NullProviderName ;
(struct ProviderName *)p_providerName
.data:0002D14C dd 0 ; (struct Provider *)p_next
.data:0002D150 db 0
.data:0002D151 db 0
.data:0002D152 db 0
.data:0002D153 db 0
.data:0002D154 dd 0FFFFFFFFh
.data:0002D158 dd offset _NullProviderCallback@4 ; Provider callback routine
The _NullProviderName is a pointer to a provider name structure similar to the one passed as an argument to _SrvRegisterProvider(). The NULL provider above (_NullProvider) is the first provider initialized by SrvInitializeProviderList() (and used in cleanup code); it is also the first entry in a linked list of provider structures. The service provider list (_SrvProviderList) is first initialized with a pointer to this NULL entry. Each call to _SrvRegisterProvider() will subsequently add a new entry to the end of the linked list.
At this point we understand that the provider which leads to the vulnerable code is going to be added to a linked list with the other 3 providers. We can then move on to SrvProcessPacket() where we see this structure is accessed:
PAGE:0002FA40 mov eax, _SrvProviderList
PAGE:0002FA45 mov [esi+15Ch], eax
PAGE:0002FA62
PAGE:0002FA62 loc_2FA62: ;
PAGE:0002FA62 mov eax, [esi+15Ch] ; EAX = &cur_provider;
PAGE:0002FA68 mov ecx, [eax+1Ch]
PAGE:0002FA6B test [esi+158h], ecx
PAGE:0002FA71 jz short loc_2FA7E
PAGE:0002FA73 push esi
PAGE:0002FA74 call dword ptr [eax+20h] ; cur_provider->CallBack()
PAGE:0002FA77 cmp eax, STATUS_MORE_PROCESSING_REQUIRED
PAGE:0002FA7C jnz short loc_2FA99
PAGE:0002FA7E
PAGE:0002FA7E loc_2FA7E: ;
PAGE:0002FA7E mov eax, [esi+15Ch] ; EAX = &cur_provider
PAGE:0002FA84 mov eax, [eax+14h]
PAGE:0002FA87 cmp eax, edi ; EDI = 0
PAGE:0002FA89 mov [esi+15Ch], eax ; cur_provider = cur_provider->next
PAGE:0002FA8F jnz short loc_2FA62
The code above is initializing a variable with a pointer to the head of the linked list of providers (_NullProvider), then iterating through the list to discover if it needs to take action. This is the point where the vulnerable routine is called. The validation routine will first be called via Smb2ValidateProviderCallback(), and if more processing is required and no error occurs (which will be the case with most, if not all of the callbacks in the validation provider), STATUS_MORE_PROCESSING_REQUIRED will be returned and the next call will be to the _Smb2ExecuteProviderCallback() routine, which is the Smb2Execute provider that is registered after the validation provider.
The structure pointed to by ESI in the code above is heavily used throughout the code and wasn't fully reversed. It is a 0x410 byte structure that is initialized by SrvAllocateWorkItemForConnection() and contains some data used to maintain the work queue. At the base of the structure is a pointer to the actual data from the packet.
The SrvProcessPacket() routine will eventually be called by SrvReceiveHandler(), which was registered in the device extension array inside of srvnet.sys. Once SrvProcessPacket() is called, the faulting routine will be reached after some more processing. It is important to remember that this will only occur if SrvNegotiateHandler() is successful, meaning the SMB command must be 0x72.
The vulnerable routine, Smb2ValidateProviderCallback(), begins by checking the first 4 bytes of the buffer for two different versions of the SMB header:
.text:000172F8
.text:000172F8 loc_172F8:
.text:000172F8 mov edx, [esi]
.text:000172FA cmp edx, 'BMS¦'
.text:00017300 jz short loc_17343
.text:00017302 cmp edx, 424D53FFh ; BMS\xFF
.text:00017308 jnz short loc_1731A
The routine then proceeds down to perform various processing depending on what the version in the SMB header was, eventually pulling the WORD from the SMB packet and using it in the index as demonstrated earlier.