• 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 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 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 ; 
.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 ; 
.data:0002D2AC                 dd offset _Smb2ValidateChangeNotify@4 ; 
.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 ; 
PAGE:0002EFED mov     [ebp+var_18], offset _SrvNegotiateHandler@16 ; SrvNegotiateHandler(x,x,x,x)
PAGE:0002EFF4 mov     [ebp+var_20], offset _SrvRegisterEndpoint@28 ; 
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 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 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


.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 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:0002FA7C jnz     short loc_2FA99


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 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.