Talos Vulnerability Report

TALOS-2024-2062

Microsoft Pragmatic General Multicast Server PgmCloseConnection stale memory dereference

September 25, 2024
CVE Number

CVE-2024-38140

SUMMARY

A memory corruption vulnerability exists in the Pragmatic General Multicast server in Microsoft Windows 10 Kernel. Specially crafted network packets can lead to access of stale memory structure resulting in memory corruption. An attacker can send a sequence of malicious packets to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

Microsoft Pragmatic General Multicast 10.0.19041.4474

PRODUCT URLS

Windows 10 - https://www.microsoft.com/en-us/windows Pragmatic General Multicast - https://www.microsoft.com/en-us/research/wp-content/uploads/2003/03/pgm_ieee_network.doc

CVSSv3 SCORE

9.8 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-459 - Incomplete Cleanup

DETAILS

The Pragmatic General Multicast protocol is an IP-based multicasting protocol that is standardized as RFC3208. This protocol is implemented by Microsoft as part of the Microsoft Message Queueing service that is available in different versions of the Windows Operating System. The PGM protocol intends to provide a mechanism in where members of a multicast group are able to detect lost or out-of-order messages and take corrective action to ensure reliable delivery of each packet composing a messaging stream. The PGM protocol also offers explicit support for forward error correction using Reed-Solomon encoding.

The entirety of the PGM protocol is implemented by Microsoft within the RMCAST.SYS driver, which is added as a protocol to the desired network adapter. Once the driver has been loaded by the “RMCAST” service, userspace processes may use the PGM protocol by instantiating a socket with the type “SOCK_RDM” (4) and protocol “IPPROTO_PGM” (113) for the “AF_INET” address family. Features such as error correction and adjustment of the window size may then be controlled by applying the corresponding socket options to the created socket. The Microsoft Message Queueing service may also be configured to bind to a specific port number of the PGM protocol.

In order to facilitate continuous delivery of multicast traffic between a client and server, the PGM protocol is based on the concept of a session. A session, as defined by the PGM protocol, is identified by a 16-bit port and a 48-bit globally-unique source identifier (GSI). During the lifetime of each PGM session, data packets being exchanged by the sender and receiver are ordered by a monotonically increasing sequence number. During this exchange, another packet type, known as an SPM (or Source Path Message), is also transmitted by the receiver in order to control the window size for the acceptable sequence numbers and to inform the receiver of the address associated with the original sender. Similar to TCP, the PGM protocol creates and destroys a session using the addition of a few options within the PGM header. These have the same naming scheme as TCP in that a session is created with the SYN option (to synchronize sequence numbers), and closed with the FIN option.

Once the driver has bound to a port number for a userspace process, it is then ready to receive messages from a client on the network. The incoming traffic for a packet on the network is initially processed by the TDI driver before being handed off to the RMCAST.SYS driver. The following function, TdiRcvDatagramHandler, is the initial entry point for handling incoming traffic that uses the PGM protocol. This function is primarily responsible for verifying the checksum of a complete PGM datagram at [1], and then determining whether the datagram is part of a pre-existing session (using the GSI field from the packet) or requires creating a new session. After verifying the checksum, the function will fetch the message type from the PGM header and assign it to a local variable at [2]. Upon receiving a message type of OD_TYPE (4), verifying its flags, and checking its size, the branch at [3] will be taken. This will execute the PgmNewInboundConnection function at [4] in order to create the new session.

NTSTATUS __stdcall TdiRcvDatagramHandler(
    tADDRESS_CONTEXT *ap_context_0,
    INT av_length_4,
    PTA_IP_ADDRESS ap_sourceAddress_8,
    int av_optionsLength_c,
    TDI_CMSG *ap_cmsg_10,
    ULONG av_dgramFlags_14,
    ULONG av_bytesIndicated_18,
    ULONG av_bytesAvailable_1c,
    ULONG *ap_resultBytesTaken_20,
    IPV4_HEADER *ap_packetHeader_24,
    PIRP *ap_resultIrp_28
)
20dc0: mov edi, edi
20dc2: push ebp
20dc3: mov ebp, esp
20dc5: and esp, 0FFFFFFF8h
20dc8: sub esp, 24h
20dcb: mov eax, [ebp+ap_resultBytesTaken_20]
20dce: xor ecx, ecx
20dd0: push ebx
20dd1: push esi
20dd2: push edi
...
20def: mov ebx, [ebp+av_bytesIndicated_18]
20df2: mov [esp+30h+NewIrql], al
20df6: cmp ebx, gv_pgmDynamicConfig_2a3c0.Data.v_maxMtu_78          ; Grab the number of bytes transmitted
20dfc: jbe short loc_20E04
20dfe: mov gv_pgmDynamicConfig_2a3c0.Data.v_maxMtu_78, ebx          ; Update a global variable with the current maximum MTU size
...
2100b: sub [ebp+av_bytesAvailable_1c], ecx
2100e: lea edi, [ecx+edx]
21011: sub ebx, ecx
21013: push ebx                                                     ; unsigned int
21014: push edi                                                     ; _BYTE *
21015: push 0                                                       ; __int16
21017: call tcpxsum(x,x,x)                                          ; [1] Perform checksum on pgm packet
2101c: cmp eax, 0FFFFh
21021: jz short loc_2105F
...
2105f: mov al, [edi+tBASIC_DATA_PACKET_HEADER.CommonHeader.Type]
21062: and al, 0Fh
21064: mov byte ptr [esp+30h+lv_messageType_18], al                 ; [2] Assign message type to local variable
...
212db: mov al, byte ptr [esp+30h+lv_messageType_18]                 ; Check message type
212df: cmp al, OD_TYPE
212e1: jnz loc_21377
212e7: cmp [edi+tBASIC_DATA_PACKET_HEADER.CommonHeader.Options], 0  ; Check options (PARITY)
212eb: jl return(STATUS_DATA_NOT_ACCEPTED)_115da
212f1: cmp ebx, 18h                                                 ; Check size
212f4: jnb short newInboundConnection_1132b                         ; [3] Branch to PgmNewInboundConnection
...
2132b: lea eax, [esp+30h+lp_commonSessionContext_1c]
2132f: mov ecx, esi                                                 ; tADDRESS_CONTEXT *
21331: push eax                                                     ; tRECEIVE_SESSION **
21332: push ebx                                                     ; ULONG
21333: push edi                                                     ; tBASIC_DATA_PACKET_HEADER *
21334: push [ebp+av_dgramFlags_14]                                  ; ULONG
21337: push edx                                                     ; PVOID
21338: mov edx, [ebp+av_length_4]                                   ; INT
2133b: call PgmNewInboundConnection(x,x,x,x,x,x,x)                  ; [4] Create new connection
21340: mov [esp+30h+var_14], eax
21344: test eax, eax
21346: jns short loc_2139E

The following is the PgmNewInboundConnection function which is responsible for allocating and initialize the session for the connection being created. At [5], the implementation will chain execution to the TdiDefaultConnectHandler function in order to inform the TDI layer of a new connection and return an IRP. At this point, the function only needs to populate the session with information related to the section. At [6], the function assigns the address of the ExInitializeNPagedLookasideList function to a register before copying the information from the packet that is needed to uniquely identify the connection. At [7], the function grabs the MTU length, adds 0x10 to it, and then stores it into the current context. Afterwards, the function uses the result to assign the maximum length for the session and sets a flag specifying that packet data is to be allocated from the lookaside list. Both the size and this flag are required to trigger the vulnerability described by this document. Afterwards at [8], a lookaside list will be constructed within the session for managing the connection using the maximum length that was calculated. When the function returns, the initialized session will be written to one of the function’s parameters. At this point, the connection will be associated with the attributes that were stored by the PgmNewInboundConnection function.

NTSTATUS __fastcall PgmNewInboundConnection(
    tADDRESS_CONTEXT *a1,
    INT a2,
    PVOID a3,
    ULONG a4,
    tBASIC_DATA_PACKET_HEADER *a5,
    ULONG a6,
    tRECEIVE_SESSION **a7
)
20546: mov edi, edi
20548: push ebp
20549: mov ebp, esp
2054b: sub esp, 68h
2054e: mov eax, ___security_cookie
20553: xor eax, ebp
...
2066f: lea eax, [ebp+lp_irp_48]
20672: push eax                                     ; AcceptIrp
20673: lea eax, [ebp+lp_connectionContext_60]
20676: push eax                                     ; ConnectionContext
20677: xor eax, eax
20679: push eax                                     ; Options
2067a: push eax                                     ; OptionsLength
2067b: push eax                                     ; UserData
2067c: push eax                                     ; UserDataLength
2067d: lea eax, [ebp+lv_remoteAddress_20]
20680: push eax                                     ; RemoteAddress
20681: push size TA_IP_ADDRESS                      ; RemoteAddressLength
20683: push [ebp+TdiEventContext]                   ; TdiEventContext
20686: call esi                                     ; [5] TdiDefaultConnectHandler(x,x,x,x,x,x,x,x,x)
20688: mov ecx, [ebp+lp_irp_48]
...
206c1: mov ecx, dword ptr [ecx+_IRP.Tail.Overlay.anonymous_3.anonymous_1.CurrentStackLocation]
206c4: mov ecx, [ecx+_IO_STACK_LOCATION.FileObject]
206c7: mov edi, [ecx+_FILE_OBJECT.FsContext]
206ca: test edi, edi
206cc: jz loc_20C26
...
2078b: movzx eax, word ptr gv_pgmDynamicConfig_2a3c0.Data.v_maxMtu_78       ; Fetch MTU from global dynamic configuration
20792: mov [edi+tCOMMON_SESSION_CONTEXT.v_data_8.vw_maxMTULength_54], ax    ; Store into session
20796: add eax, 0Eh
20799: mov [edi+tCOMMON_SESSION_CONTEXT.v_data_8.vw_maxFECPacketLength_56], ax
...
207db: mov edx, ds:ExInitializeNPagedLookasideList(x,x,x,x,x,x,x)
207e1: mov [ebp+lpf_initializeNonPagedLookasideList_50], edx                ; [6] Assign ExInitializeNPagedLookasideList
...
20807: cmp [ebx+tADDRESS_CONTEXT.vb_fecOptions_c1], 0
2080e: jz loc_209B6
...
209b6: mov esi, [ebp+lp_basicDataPackHeader_54]
209b9: push 2
209bb: movzx ecx, [esi+tBASIC_DATA_PACKET_HEADER.CommonHeader.SrcPort]              ; Read source port from packet
209be: mov eax, dword ptr [esi+tBASIC_DATA_PACKET_HEADER.CommonHeader.gSourceId]    ; Read global source id from packet
209c1: mov dword ptr [edi+tRECEIVE_SESSION.v_data_8.v_tsi_20.GSI], eax              ; Write global source id into session
209c4: mov ax, word ptr [esi+(tBASIC_DATA_PACKET_HEADER.CommonHeader.gSourceId+4)]  ; Read global source id from packet
209c8: mov word ptr [edi+(tRECEIVE_SESSION.v_data_8.v_tsi_20.GSI+4)], ax            ; Write global source id into session
209cc: mov ah, cl
209ce: mov al, ch
209d0: mov [edi+tRECEIVE_SESSION.v_data_8.v_tsi_20.hPort], ax                       ; Copy port into session
209d4: pop eax
...
209eb: movzx ecx, [edi+tCOMMON_SESSION_CONTEXT.v_data_8.vw_maxMTULength_54]                     ; Grab MTU length from session
209ef: add ecx, 10h                                                                             ; Add 0x10 to MTU length
209f2: mov [eax+tRECEIVE_CONTEXT.v_maxBufferLength_cc], ecx                                     ; [7] Assign to context as maximum buffer length
...
209fb: mov eax, [ecx+tRECEIVE_CONTEXT.v_maxBufferLength_cc]                                     ; Grab maximum buffer length from context
20a01: mov [ecx+tRECEIVE_CONTEXT.v_dataBufferLookasideLength_d0], eax                           ; [7] Use maximum buffer length for data buffer
20a07: mov ecx, [edi+tRECEIVE_SESSION.v_data_8.p_receiver_14]                                   ; Grab receiver context from session
20a0a: or [edi+tRECEIVE_SESSION.v_data_8.v_sessionFlags_c], PGM_SESSION_DATA_FROM_LOOKASIDE     ; [7] Initialize flags for LookasideList
...
20a11: movzx eax, [ecx+tRECEIVE_CONTEXT.vw_maxPacketsBufferedInLookaside_d8]
20a18: push eax                                                                 ; Depth
20a19: push 446D6750h                                                           ; Tag
20a1e: push [ecx+tRECEIVE_CONTEXT.v_dataBufferLookasideLength_d0]               ; Size
20a24: lea eax, [ecx+tRECEIVE_CONTEXT.v_dataBufferLookaside_e0]                 ; [8] Field containing lookaside list
20a2a: push 200h                                                                ; Flags
20a2f: push 0                                                                   ; Free
20a31: push 0                                                                   ; Allocate
20a33: push eax                                                                 ; Lookaside
20a34: call edx                                                                 ; [8] Create non-paged lookaside list
...

Now that a session has been created from processing the first packet, the TdiRcvDatagramHandler will be able to use any following packets to locate the connection associated with the session. Similarly to how the first packet was processed, the checksum for the PGM packet will checked at [9]. Afterwards at [10], a loop will be entered which will iterate through all of the available connections. Each iteration will attempt to match the GSI and port number at [11]. Once the session has been found, at [12] a branch will be taken in order to check the PGM message type. With the provided proof-of-concept, the second PGM message type is of type SPM_TYPE (0). As per RFC3208, an SPM message is used by a source to transmit the address of the source and to maintain the current transmit window for a connection. At [13], the PGM message type from the packet will be checked to distinguish whether the packet is a request, repair, or data type. Due to sending an SPM_TYPE (0), none of these conditions will match. As a result, the function will hand off responsibility to the PgmProcessIncomingPacket function at [14].

NTSTATUS __stdcall TdiRcvDatagramHandler(
    tADDRESS_CONTEXT *ap_context_0,
    INT av_length_4,
    PTA_IP_ADDRESS ap_sourceAddress_8,
    int av_optionsLength_c,
    TDI_CMSG *ap_cmsg_10,
    ULONG av_dgramFlags_14,
    ULONG av_bytesIndicated_18,
    ULONG av_bytesAvailable_1c,
    ULONG *ap_resultBytesTaken_20,
    IPV4_HEADER *ap_packetHeader_24,
    PIRP *ap_resultIrp_28
)
20dc0: mov edi, edi
20dc2: push ebp
20dc3: mov ebp, esp
20dc5: and esp, 0FFFFFFF8h
20dc8: sub esp, 24h
...
2100b: sub [ebp+av_bytesAvailable_1c], ecx
2100e: lea edi, [ecx+edx]
21011: sub ebx, ecx
21013: push ebx                                                     ; unsigned int
21014: push edi                                                     ; _BYTE *
21015: push 0                                                       ; __int16
21017: call tcpxsum(x,x,x)                                          ; [9] Perform checksum on pgm packet
2101c: cmp eax, 0FFFFh
21021: jz short loc_2105F
...
21113: lea ecx, [esi+tADDRESS_CONTEXT.v_associatedConnections_10]   ; [10] Check all associated connections
21116: mov [esp+30h+NewIrql], al
2111a: mov eax, [ecx]
2111c: jmp short continue_1118b
...
loop_1111e:
2111e: lea ecx, [eax+tRECEIVE_SESSION.v_data_8.v_spinLock_f0]
21124: mov [esp+30h+lp_commonSessionContext_1c], eax
21128: call ds:KfAcquireSpinLock(x)
...
2114d: mov eax, dword ptr [ecx+tRECEIVE_SESSION.v_data_8.v_tsi_20.GSI]      ; [11] Check global source id
21150: cmp eax, [esp+30h+var_10]
21154: jnz short loc_2116D
21156: mov eax, dword ptr [ecx+(tRECEIVE_SESSION.v_data_8.v_tsi_20.GSI+4)]  ; [11] Check global source id and port
21159: cmp eax, [esp+30h+var_4]
2115d: jnz short loc_2116D
...
2118b: mov [esp+30h+var_14], eax
2118f: cmp eax, ecx                                         ; [10] Loop through next connection
21191: jnz short loop_1111e
21193: jmp loc_2127B
...
2127b: mov dl, [esp+30h+NewIrql]
2127f: lea ecx, [esi+tADDRESS_CONTEXT.v_spinLock_12c]
21285: call ds:KfReleaseSpinLock(x,x)
2128b: cmp [esp+30h+lp_commonSessionContext_1c], 0          ; [12] Check for matching session
21290: jnz loc_2139E
...
2139e: mov eax, [esi+tADDRESS_CONTEXT.v_flags_20]
213a1: and eax, 0Ch
213a4: cmp al, SPMR_TYPE                                    ; [13] Check pgm packet type (SPM Request)
213a6: jnz loc_21472
...
21472: mov al, byte ptr [esp+30h+lv_messageType_18]
21476: cmp al, OD_TYPE                                      ; [13] Check pgm packet type (data)
21478: jz short handle_pgmtype(OD_TYPE,RD_TYPE)_1149a
2147a: cmp al, RD_TYPE                                      ; [13] Check pgm packet type (repair)
2147c: jz short handle_pgmtype(OD_TYPE,RD_TYPE)_1149a
...
2147e: push [esp+30h+lv_messageType_18]                     ; UCHAR
21482: mov edx, [esp+34h+lp_commonSessionContext_1c]        ; tRECEIVE_SESSION *
21486: push edi                                             ; tCOMMON_HEADER *
21487: push ebx                                             ; ULONG
21488: push [ebp+ap_sourceAddress_8]                        ; PTA_IP_ADDRESS
2148b: push ecx                                             ; INT
2148c: mov ecx, esi                                         ; tADDRESS_CONTEXT *
2148e: call PgmProcessIncomingPacket(x,x,x,x,x,x,x)         ; [14] Process incoming pgm packet

The following function, will be used by the driver to determine the correct handler for processing the packet by using the message type. As the message type for the second packet is SPM_TYPE (0), the handler for processing Source Path Messages will be used. At [15], the message type will be loaded from one of the parameters and checked for a non-zero value. If a message type matching SPM_TYPE (0) was determined, the minimum size of the packet will be checked at [16]. After the session has been validated, the ProcessSpmPacket function will be used to actually parse the Source Path Message packet.

NTSTATUS __fastcall PgmProcessIncomingPacket(
    tADDRESS_CONTEXT *a1,
    tRECEIVE_SESSION *a2,
    __unused INT a3,
    PTA_IP_ADDRESS a4,
    ULONG av_length_8,
    tCOMMON_HEADER *a6,
    UCHAR a7
)
20280: mov edi, edi
20282: push ebp
20283: mov ebp, esp
20285: sub esp, 10h
20288: push ebx
20289: mov ebx, dword ptr [ebp+av_messageType_10]       ; [15] Check message type
...
20295: test bl, bl                                      ; [15] Check non-zero message type
20297: jnz loc_2037B
2029d: mov ebx, [ebp+av_length_8]                       ; [16] Verify minimum message size
202a0: cmp ebx, 24h ; '$'
202a3: jnb short loc_202CC
...
202cc: test esi, esi
202ce: jz short loc_20347
202d0: cmp [esi+tRECEIVE_SESSION.v_data_8.v_verify_0], 'RSES'
202d7: jnz short loc_20347
...
202df: mov ecx, [esi+tRECEIVE_SESSION.v_data_8.p_receiver_14]
...
202ef: push [ebp+ap_header_c]                   ; tBASIC_SPM_PACKET_HEADER *
...
2030e: mov ecx, [ebp+lv_addressContext_4]       ; tADDRESS_CONTEXT *
20311: push ebx                                 ; ULONG
20312: call ProcessSpmPacket(x,x,x,x)           ; [17] Process Source Path Message packet
20317: mov ecx, eax
20319: mov [ebp+av_length_8], ecx

The following function, ProcessSpmPacket, is used to parse a Source Path Message from a PGM packet. This function starts out at [18] by using the ProcessOptions function to process the options within the header of the PGM message. The options are then stored to a local variable upon completion. After the options have been parsed, the ProcessSpmPacket will extract the source address from the SPM packet and store it into the session. Later at [19], the ProcessSpmPacket function will check each potential option that was parsed by the ProcessOptions function. The proof-of-concept explictly sets the PARITY_PRM (8) option, which will result in the branch at [20] being skipped. The PARITY_PRM (8) option results in the current function allocating space that is used to support performing error correction on the packet. At [21], the function allocates space for the error correction buffer and then creates a context to track the current error correction state. After the FEC buffer and context has been allocated and assigned into the session, at [22] the error correction options are read from the parsed options, and written directly into the current session at [23]. The processing of the second packet, containing an SPM message, has now put the session into the correct state to trigger the vulnerability.

NTSTATUS __fastcall ProcessSpmPacket(
    tADDRESS_CONTEXT *a1,
    tRECEIVE_SESSION *a2,
    ULONG av_length_0,
    tBASIC_SPM_PACKET_HEADER *a4
)
1fa3e: mov edi, edi
1fa40: push ebp
1fa41: mov ebp, esp
1fa43: sub esp, 4Ch
1fa46: mov eax, ___security_cookie
1fa4b: xor eax, ebp
1fa4d: mov [ebp+var_4], eax
...
1fa65: xor edi, edi
1fa67: mov [ebp+lp_packetHeader_34], edx            ; Copy header from parameter
...
1fa73: mov ecx, [ebp+lp_packetHeader_34]
1fa76: lea eax, [ebp+lv_packetOptions_24]           ; [18] Destination variable for options
1fa79: push edi                                     ; tNAKS_LIST *
1fa7a: push eax                                     ; tPACKET_OPTIONS *
1fa7b: movzx eax, [edx+tBASIC_SPM_PACKET_HEADER.CommonHeader.Type]
1fa7f: add ecx, size tBASIC_SPM_PACKET_HEADER       ; tPACKET_OPTION_LENGTH *
1fa82: mov edx, [ebp+av_length_0]
1fa85: and eax, 0Fh
1fa88: push eax                                     ; ULONG
1fa89: add edx, -24h                                ; ULONG
1fa8c: call ProcessOptions(x,x,x,x,x)               ; [18] Parse options from PGM header
1fa91: test eax, eax
1fa93: jns short loc_1FACA
...
1faca: mov edx, [ebp+lp_packetHeader_34]
1facd: mov eax, dword ptr [edx+tBASIC_SPM_PACKET_HEADER.PathNLA.NLA_AFI]
1fad0: mov esi, [edx+tBASIC_SPM_PACKET_HEADER.PathNLA.IpAddress]
1fad3: mov [ebp+var_40], eax
1fad6: mov ax, [edx+tBASIC_SPM_PACKET_HEADER.CommonHeader.TSDULength]
...
1fe7f: mov ecx, [ebp+lp_packetHeader_34]
1fe82: mov edx, ABORT
1fe87: test [ebp+lv_packetOptions_24.OptionsFlags], edx             ; [19] Check ABORT option
1fe8a: jz short loc_1FECB
...
1fecb: test byte ptr [ebp+lv_packetOptions_24.OptionsFlags], RST    ; [19] Check RST option
1fecf: jz short loc_1FF19
...
1ff19: test byte ptr [ebp+lv_packetOptions_24.OptionsFlags], FIN    ; [19] Check FIN option
1ff1d: jz short loc_1FF85
...
1ffa5: mov eax, [ebp+lv_packetOptions_24.OptionsFlags]
1ffa8: mov esi, PARITY_PRM                                          ; [20] Check PARITY_PRM option
1ffad: test esi, eax
1ffaf: jz loc_201CA
...
1ffd9: movzx ecx, [ebx+tRECEIVE_SESSION.v_data_8.vw_maxFECPacketLength_56]
1ffdd: movzx eax, dl
1ffe0: imul ecx, eax
1ffe3: push 336D6750h ; Tag
1ffe8: add ecx, ecx
1ffea: push ecx ; NumberOfBytes
1ffeb: push esi ; PoolType
1ffec: call ds:ExAllocatePoolWithTag(x,x,x)                         ; [21] Allocate space for FEC
1fff2: mov [ebx+tRECEIVE_SESSION.v_data_8.p_fecBuffer_50], eax      ; [21] Store allocated buffer in session
1fff5: test eax, eax
1fff7: jnz short loc_20039
...
20039: movzx edx, [ebp+lv_packetOptions_24.v_fecContext_1c.vb_fecGroupInfo_0]
2003d: lea ecx, [ebx+tRECEIVE_SESSION.v_data_8.v_fecContext_3c]
20040: xor eax, eax
20042: inc eax
20043: push eax                         ; size_t
20044: push 0FFh                        ; int
20049: call CreateFECContext(x,x,x,x)   ; [21] Create FEC context for error correction
2004e: mov esi, eax
20050: test esi, esi
20052: jns short loc_2008D
...
200cd: mov al, byte ptr [ebp+lv_packetOptions_24.v_fecContext_1c.vb_fecOptions_3]       ; [22] Read FEC Options
200d0: xor ecx, ecx
200d2: mov edx, dword ptr [ebp+lv_packetOptions_24.v_fecContext_1c.vb_fecGroupInfo_0]   ; [22] Read FEC Group Info
200d5: inc ecx
200d6: mov [ebx+tRECEIVE_SESSION.v_data_8.vb_fecOptions_5d], al                         ; [23] Store in session
200d9: mov dword ptr [ebp+var_2C.vb_fecGroupInfo_0], edx
200dc: mov [ebx+tRECEIVE_SESSION.v_data_8.vb_fecGroupSize_5c], dl                       ; [23] Store in session
200df: test cl, al
200e1: jz short loc_200F3

Once the session is in the correct state, the final packet sent by the proof-of-concept will be picked up by the TdiRcvDatagramHandler function. This packet should be a data packet, using type OD_DATA (4), and include the option PARITY_GRP (9) within its extension header. At [24], the TdiRcvDatagramHandler function will check the packet type before handing off the packet to the ProcessDataPacket function at [25].

NTSTATUS __stdcall TdiRcvDatagramHandler(
    tADDRESS_CONTEXT *ap_context_0,
    INT av_length_4,
    PTA_IP_ADDRESS ap_sourceAddress_8,
    int av_optionsLength_c,
    TDI_CMSG *ap_cmsg_10,
    ULONG av_dgramFlags_14,
    ULONG av_bytesIndicated_18,
    ULONG av_bytesAvailable_1c,
    ULONG *ap_resultBytesTaken_20,
    IPV4_HEADER *ap_packetHeader_24,
    PIRP *ap_resultIrp_28
)
20dc0: mov edi, edi
20dc2: push ebp
20dc3: mov ebp, esp
20dc5: and esp, 0FFFFFFF8h
20dc8: sub esp, 24h
...
21472: mov al, byte ptr [esp+30h+lv_messageType_18]
21476: cmp al, OD_TYPE                                              ; [24] Check pgm packet type (data)
21478: jz short handle_pgmtype(OD_TYPE,RD_TYPE)_1149a
2147a: cmp al, RD_TYPE                                              ; [24] Check pgm packet type (repair)
2147c: jz short handle_pgmtype(OD_TYPE,RD_TYPE)_1149a
...
handle_pgmtype(OD_TYPE,RD_TYPE)_1149a:
2149a: xor edx, edx
2149c: cmp [esi+tADDRESS_CONTEXT.v_receiverMcastAddress_cc], edx
214a2: jz short return(STATUS_DATA_NOT_ACCEPTED)_1151f
214a4: cmp al, OD_TYPE                                              ; [24] Check pgm packet type (data)
214a6: mov eax, [esp+30h+lp_commonSessionContext_1c]
214aa: mov eax, [eax+tRECEIVE_SESSION.v_data_8.p_receiver_14]
214ad: jnz short loc_214BE
...
214bc: jmp short processDataPacket_114cb
...
214cb: mov eax, [esp+30h+lp_commonSessionContext_1c]
214cf: push [esp+30h+lv_messageType_18]                 ; UCHAR
214d3: push edi                                         ; tBASIC_DATA_PACKET_HEADER *
...
214dc: push ebx                                         ; SEQ_TYPE
...
214ee: mov ecx, esi                                     ; tADDRESS_CONTEXT *
214f0: mov eax, [esp+3Ch+lp_commonSessionContext_1c]
...
21511: mov edx, [esp+3Ch+lp_commonSessionContext_1c]    ; tRECEIVE_SESSION *
21515: call ProcessDataPacket(x,x,x,x,x)                ; [25] Process the data packet
2151a: jmp return(@eax)_11493

The ProcessDataPacket function is relatively simple and primarily is used by the driver to check that the sequence number for the data packet is within the window size of the current session. At [26], the current session is preserved in a variable stored within the frame. At [27], the boundaries of the window for the current session is checked before the sequence number of the data packet is verified. Afterwards at [28], the ProcessDataPacket function checks whether the data packet is used for repairing a session with RD_TYPE (5) or simply contains data with OD_TYPE (4). Once the message type has been verified, the PGM packet and session is then handed off to the PgmHandleNewData function call at [29].

NTSTATUS __fastcall ProcessDataPacket(
    tADDRESS_CONTEXT *a1,
    tRECEIVE_SESSION *a2,
    ULONG av_length_0,
    tBASIC_DATA_PACKET_HEADER *ap_dataPacketHeader_4,
    UCHAR av_messageType_8
)
1f7b6: mov edi, edi
1f7b8: push ebp
1f7b9: mov ebp, esp
1f7bb: sub esp, 18h
1f7be: cmp [ebp+av_length_0], 18h
1f7c2: mov eax, ecx
...
1f7c7: mov edi, edx
1f7c9: mov [ebp+lp_addressContext_8], eax                                   ; [26] Store address context to local variable
1f7cc: jnb short loc_1F802
...
1f895: mov ecx, [edx+tRECEIVE_CONTEXT.v_firstNakSequenceNumber_28]          ; [27] Check sequence number window
1f898: mov eax, ecx
1f89a: sub eax, [edx+tRECEIVE_CONTEXT.v_lastTrailingEdgeSequenceNumber_20]  ; [27] Check sequence number window
1f89d: jns loc_1F972
...
1f972: sub ecx, esi                                                         ; [27] Check sequence number
1f974: test ecx, ecx                                                        ; [27] Check sequence number
1f976: jle short loc_1F9C3
...
1f9c3: cmp [ebp+av_messageType_8], OD_TYPE          ; [28] Check PGM message type
1f9c7: jnz short loc_1F9D3
...
1f9d3: mov esi, [ebp+lp_addressContext_8]
1f9d6: lea eax, [ebp+NewIrql]
1f9d9: push eax                                     ; KIRQL *
1f9da: lea eax, [ebp+var_2]
1f9dd: mov edx, esi                                 ; tADDRESS_CONTEXT *
1f9df: push eax                                     ; KIRQL *
1f9e0: push dword ptr [ebp+av_messageType_8]        ; UCHAR
1f9e3: lea ecx, [ebp+var_C]                         ; SEQ_TYPE *
1f9e6: push [ebp+ap_dataPacketHeader_4]             ; tBASIC_DATA_PACKET_HEADER *
1f9e9: push [ebp+av_length_0]                       ; USHORT
1f9ec: push edi                                     ; tRECEIVE_SESSION *
1f9ed: call PgmHandleNewData(x,x,x,x,x,x,x,x)       ; [29] Handle PGM data

Inside the PgmHandleNewData function is the logic used by the driver to collect all the data packets sent by a sender. This function is responsible for receive packets out of order and combining them in a way so that they are alerted to the userspace process contiguously. Upon entry at [30], the function will first read the option flags from the PGM packet header and the packet length from a parameter, and store them into a set of variables located on the stack. Afterwards, the ProcessOptions function will be used at [31] to parse the options out of the PGM extension header. After the options have been processed, they will be checked at [32]. At this point in the proof-of-concept, the PGM packet being processed contains only the PARITY_GRP (9) option. After determining the FIN option is not within the packet, the FEC options and group size fields that were assigned to the session during the transmission of the second packet will be checked. As the second packet has assigned non-zero values to both of these fields, none of the listed branches will be taken. The final branch at [34], will then check that the PARITY (0x80) flag is set within the packet in order to guard the check for the PARITY_GRP (9) option and FEC group at [35]. After checking the option flags again at [36], the PgmHandleNewData function will then call AllocateDataBuffer at [37] with the current packet size.

NTSTATUS __fastcall PgmHandleNewData(
    SEQ_TYPE *a1,
    tADDRESS_CONTEXT *a2,
    tRECEIVE_SESSION *ap_session_0,
    USHORT a4,
    tBASIC_DATA_PACKET_HEADER *a5,
    UCHAR a6,
    KIRQL *a7,
    KIRQL *a8
)
1ed52: mov edi, edi
1ed54: push ebp
1ed55: mov ebp, esp
1ed57: sub esp, 5Ch
1ed5a: mov eax, ___security_cookie
1ed5f: xor eax, ebp
1ed61: mov [ebp+var_4], eax
...
1ed7f: mov al, [edx+tBASIC_DATA_PACKET_HEADER.CommonHeader.Options]     ; [30] Read option flags from header
1ed82: mov esi, [ebp+ap_session_0]
1ed85: mov [ebp+lvb_pgmFlags_26], al                                    ; Store options to frame variable
...
1edc6: mov edx, dword ptr [ebp+av_length_4]                             ; [30] Read length from parameter
...
1edd6: movzx ecx, dx
1edd9: mov [ebp+lv_packetLength_44], ecx                                ; Store in frame variable
...
1ede2: push eax                             ; tNAKS_LIST *
1ede3: lea eax, [ebp+lv_packetOptions_24]
1ede6: push eax                             ; tPACKET_OPTIONS *
1ede7: movzx eax, byte ptr [edi+4]
1edeb: lea edx, [ecx-18h]                   ; ULONG
1edee: and eax, 0Fh
1edf1: lea ecx, [edi+18h]                   ; tPACKET_OPTION_LENGTH *
1edf4: push eax                             ; ULONG
1edf5: call ProcessOptions(x,x,x,x,x)       ; [31] Process options out of PGM header
1edfa: mov edi, eax
1edfc: mov [ebp+var_3C], eax
1edff: test edi, edi
1ee01: jns short loc_1EE45
...
1ef50: movzx eax, [ebp+lv_packetOptions_24.OptionsLength]
1ef54: and [ebp+lv_bytesTaken_40], 0
1ef58: add eax, 18h
1ef5b: test [ebp+lv_packetOptions_24.OptionsFlags], CURR_TGSIZE     ; [32] Check CURR_TGSIZE option
1ef62: mov edx, [ebp+lp_receiveSession_2c]
1ef65: mov [ebp+var_3C], eax
1ef68: jz short loc_1EFB8
...
1efb8: test byte ptr [ebp+lv_packetOptions_24.OptionsFlags], FIN    ; [32] Check FIN option
1efbc: jz short notPacketOption(FIN)_f024
...
1f024: mov ecx, [ebp+lp_receiveSession_2c]
1f027: mov [ebp+var_35], 0
1f02b: cmp [ecx+tRECEIVE_SESSION.v_data_8.vb_fecOptions_5d], 0      ; [33] Check FEC options in Session
1f02f: jz loc_1F154
1f035: mov al, [ecx+tRECEIVE_SESSION.v_data_8.vb_fecGroupSize_5c]
1f038: mov ah, al
1f03a: mov [ebp+lvb_nakIndex_27], al
1f03d: dec ah
1f03f: and ah, byte ptr [ebp+var_34]
1f042: cmp [ebp+lvb_pgmFlags_26], 0                                 ; [34] Read stored flags
1f046: mov [ebp+var_25], ah
1f049: jl short loc_1F095                                           ; [34] Check PARITY (0x80) flag
...
1f095: movzx eax, [esi+tNAK_FORWARD_DATA.PacketsInGroup]
1f099: movzx edx, [esi+tNAK_FORWARD_DATA.NumDataPackets]
1f09d: mov [ebp+var_30], eax
1f0a0: movzx eax, [esi+tNAK_FORWARD_DATA.NumParityPackets]
1f0a4: add eax, edx
1f0a6: cmp eax, [ebp+var_30]
1f0a9: jnb short return(STATUS_DATA_NOT_ACCEPTED)_f055
1f0ab: movzx eax, [esi+tNAK_FORWARD_DATA.NextIndexToIndicate]
1f0af: add eax, edx
1f0b1: cmp eax, [ebp+var_30]
1f0b4: jnb short return(STATUS_DATA_NOT_ACCEPTED)_f055
1f0b6: test [ebp+lv_packetOptions_24.OptionsFlags], PARITY_GRP              ; [35] Check PARITY_GRP option
1f0bd: jz short loc_1F05A
1f0bf: mov al, [ebp+lv_packetOptions_24.v_fecContext_1c.vb_fecGroupInfo_0]  ; [35] Check FEC Group
1f0c2: test al, al
1f0c4: jnz short loc_1F119
...
1f437: mov al, [esi+tNAK_FORWARD_DATA.NumParityPackets]
1f43a: add al, [esi+tNAK_FORWARD_DATA.NumDataPackets]
1f43d: mov ah, [ebp+lvb_pgmFlags_26]                        ; [36] Read stored flags
1f440: mov [ebp+lvb_nakIndex_27], al
1f443: test ah, ah
1f445: jns short loc_1F44F                                  ; [36] Branch if PARITY (0x80) flag is clear
1f447: mov ecx, [ebp+lv_packetLength_44]
1f44a: add ecx, 0Eh
1f44d: jmp short loc_1F45F
...
1f5c9: movzx eax, al
1f5cc: lea edx, [esi+tNAK_FORWARD_DATA.pPendingData]
1f5cf: mov [ebp+lp_addressContext_54], eax
1f5d2: shl eax, 5
1f5d5: push ecx                             ; ULONG
1f5d6: add edx, eax                         ; tPENDING_DATA *
1f5d8: mov [ebp+var_4C], eax
1f5db: mov ecx, edi                         ; tRECEIVE_SESSION *
1f5dd: call AllocateDataBuffer(x,x,x)       ; [37] Call AllocateDataBuffer with current session
1f5e2: test eax, eax
1f5e4: jnz short loc_1F61B

Finally, the following function will be used to allocate space for the data from the received packet. This function is directly responsible for clearing the flag that puts the current connection into the necessary state to trigger the described vulnerability. At [38] is a conditional that checks the current packet size against the size used for the lookaside list containing data buffers for the session. If the packet size is larger than the size of buffers allocated from the lookaside list, a branch will be taken in order to use the ExAllocatePoolWithTag function for the data packet. At [39], the current flags will be loaded from the current session and then cleared prior to calling ExAllocatePoolWithTag. Afterwards, the maximum buffer length for the session will be updated if the current packet size is larger than the length in the session.

PVOID __fastcall AllocateDataBuffer(
    tRECEIVE_SESSION *a1,
    tPENDING_DATA *a2,
    ULONG av_length_0
)
1a500: mov edi, edi
1a502: push ebp
1a503: mov ebp, esp
1a505: push ebx
1a506: mov ebx, [ebp+av_length_0]                                       ; Current packet length
1a509: push esi
1a50a: push edi                                                         ; Lookaside
1a50b: mov edi, ecx
1a50d: mov esi, edx
...
1a50f: mov eax, [edi+tRECEIVE_SESSION.v_data_8.v_sessionFlags_c]        ; [39] Load flags from session
1a512: test eax, PGM_SESSION_DATA_FROM_LOOKASIDE
1a517: jz short loc_1A544
...
1a519: mov ecx, [edi+tRECEIVE_SESSION.v_data_8.p_receiver_14]
1a51c: cmp ebx, [ecx+tRECEIVE_CONTEXT.v_dataBufferLookasideLength_d0]   ; [38] Compare packet size with lookaside buffer size
1a522: ja short loc_1A544
...
1a544: push 446D6750h                                                   ; Tag
1a549: push ebx                                                         ; NumberOfBytes
1a54a: and eax, not (PGM_SESSION_DATA_FROM_LOOKASIDE)                   ; [39] Clear the LOOKASIDE flag from the session.
1a54f: push 200h                                                        ; PoolType
1a554: mov [edi+tRECEIVE_SESSION.v_data_8.v_sessionFlags_c], eax        ; [39] Update the session with the new flags
1a557: call ds:ExAllocatePoolWithTag(x,x,x)                             ; Allocate space for data packet
1a55d: mov [esi+tPENDING_DATA.pDataPacket], eax                         ; Assign to pending data
...
1a55f: mov eax, [edi+tRECEIVE_SESSION.v_data_8.p_receiver_14]
1a562: cmp ebx, [eax+tRECEIVE_CONTEXT.v_maxBufferLength_cc]
1a568: jbe short leave_a570
1a56a: mov [eax+tRECEIVE_CONTEXT.v_maxBufferLength_cc], ebx             ; Update maximum buffer length for session

Once the connection for the current session has been put into the correct state, the connection can be closed to trigger the vulnerability. This can occur due to timeout or as a result of the server explicitly closing the connection. When closing a connection, the driver will use the PgmCloseConnection function. This function is shown in the following listing. To distinguish how to destroy the session for the connection, at [40] the function will check a field within the session to determine whether the connection is for a sender or a receiver. At [41], the flag that was cleared by AllocateDataBuffer when sending the 3rd packet will be checked. Due to this flag being cleared at the time of destroying the connection, the call to ExDeleteNPagedLookasideList will be skipped. Afterwards at [42], the entire session will be freed by ExFreePoolWithTag removing any reference to the lookaside list that was used to allocate each data buffer for the connection.

NTSTATUS __fastcall PgmCloseConnection(PIRP a1, PIO_STACK_LOCATION a2)
{
  FsContext = (tCOMMON_SESSION_CONTEXT *)a2->FileObject->FsContext;
...
  a2->FileObject->FsContext = 0;
  if ( FsContext->v_data_8.vb_fecOptions_5d )
    DestroyFECContext(&FsContext->v_data_8.v_fecContext_3c.k);
  v4 = FsContext->v_data_8.p_sender_10;                                                     // [40] Check if session is a sender or receiver
  if ( !v4 )
  {
    v8 = FsContext->v_data_8.p_receiver_14;
    if ( v8 )
    {
      if ( (FsContext->v_data_8.v_sessionFlags_c & PGM_SESSION_DATA_FROM_LOOKASIDE) != 0 )  // [41] Check if flag is set
        ExDeleteNPagedLookasideList(&v8->v_dataBufferLookaside_e0);                         // [41] Conditionally delete the lookaside list
      v9 = FsContext->v_data_8.p_fecBuffer_50;
      if ( v9 )
        ExFreePoolWithTag(v9, 0);
      if ( FsContext->v_data_8.vb_fecGroupSize_5c )
      {
        ExDeleteNPagedLookasideList(&FsContext->v_data_8.p_receiver_14->v_nonParityContextLookaside_130);
        if ( FsContext->v_data_8.vb_fecOptions_5d )
          ExDeleteNPagedLookasideList(&FsContext->v_data_8.p_receiver_14->v_parityContextLookaside_180);
      }
...
    }
...
  }
...
  ExFreePoolWithTag(FsContext, 0);          // [42] Free the entire session
  return 0;
}

After the session has been freed, when the next iteration of the ExpScanGeneralLookasideList function gets called by the nt!KeBalanceSetManager system thread, an address that is used for arithmetic will be dereferenced at [43]. This function adjusts the depth of the target lookaside list. If this address can be reliably controlled, this can potentially be used to corrupt memory.

nt!ExpScanGeneralLookasideList+0x28:
8251a648 bfffff0000      mov     edi,0FFFFh
8251a64d 8d4900          lea     ecx,[ecx]
...
nt!ExpScanGeneralLookasideList+0x30:
8251a650 8b46e0          mov     eax,dword ptr [esi-20h]    ; [43] Dereference address for number of allocations in lookaside cache
8251a653 8bd0            mov     edx,eax
8251a655 2b560c          sub     edx,dword ptr [esi+0Ch]    ; [43] Dereference address for number of allocations in lookaside cache
8251a658 89460c          mov     dword ptr [esi+0Ch],eax    ; [44] Write result to address
...
8251a66a 663bc7          cmp     ax,di
8251a66d 7422            je      nt!ExpScanGeneralLookasideList+0x71 (8251a691)

Crash Information

This WinDbg walkthrough uses the following conditional breakpoints.

bp rmcast.sys+4b8b ".printf\"tCOMMON_SESSION_CONTEXT(%p).v_data_8.v_sessionFlags_c is %#p\\n\",@edi,poi(@edi+8+0xc);g"
bp rmcast.sys+4b98 "r;g"
bp rmcast.sys+4bf2 ".printf\"Calling ExFreePoolWithTag(%p) with [%#p, %#p]\\n\",@ebx,poi(@esp+4*0),poi(@esp+4*1)"
bp rmcast.sys+a500 ".printf\"Entry -> AllocateDataBuffer(%#p, %#p, %#p)\\n\",@ecx,@edx,poi(@esp+4+4*0);g"
bp rmcast.sys+a522 ".printf\"Packet length is %#x\\ntRECEIVE_CONTEXT(%#p).v_dataBufferLookasideLength_d0 is %#x\\nComparing %#x (packet length) to %#x (lookaside)\\n\\n\",@ebx,@ecx,poi(@ecx+0xd0),@ebx,poi(@ecx+0xd0);r;g"
bp rmcast.sys+a554 ".printf\"Updating tCOMMON_SESSION_CONTEXT(%#p).v_data_8.v_sessionFlags_c from %#x to %#x\\n\\n\",@edi,poi(@edi+8+0xc),@eax;g"
bp rmcast.sys+fa3e ".printf\"Entry -> ProcessSpmPacket(%#p, %#p, %#p, %#p)\\n\",@ecx,@edx,poi(@esp+4+4*0),poi(@esp+4+4*1);g"
bp rmcast.sys+100d6 ".printf\"Updating tCOMMON_SESSION_CONTEXT(%#p).v_data_8.vb_fecOptions_5d from %#x to %#x (packet options)\\n\",@ebx,by(@ebx+8+0x5d),@al;g"
bp rmcast.sys+100dc ".printf\"Updating tCOMMON_SESSION_CONTEXT(%#p).v_data_8.vb_fecGroupSize_5c from %#x to %#x (packet options)\\n\\n\",@ebx,by(@ebx+8+0x5c),@dl;g"
bp rmcast.sys+10546 ".printf\"Entry -> PgmNewInboundConnection(%#p, %#p, %#p, %#p, %#p, %#p, %#p)\\n\",@ecx,@edx,poi(@esp+4+4*0),poi(@esp+4+4*1),poi(@esp+4+4*2),poi(@esp+4+4*3),poi(@esp+4+4*4);g"
bp rmcast.sys+10792 ".printf\"gv_pgmDynamicConfig_2a3c0.Data.v_maxMtu_78 is %#x\\nUpdating tCOMMON_SESSION_CONTEXT(%#p).vw_maxMTULength_54 from %#x to %#x\\n\",@eax,@edi,wo(@edi+8+0x54),@ax;g"
bp rmcast.sys+10799 ".printf\"Updating tCOMMON_SESSION_CONTEXT(%#p).vw_maxFECPacketLength_56 from %#x to %#x\\n\\n\",@edi,wo(@edi+8+0x56),@ax;g"
bp rmcast.sys+109f2 ".printf\"tCOMMON_SESSION_CONTEXT(%#p).v_data_8.vw_maxMTULength_54 is %#x\\nUpdating tRECEIVE_CONTEXT.v_maxBufferLength_cc from %#x to %#x\\n\",@edi,wo(@edi+8+0x54),poi(@eax+0xcc),@ecx;g"
bp rmcast.sys+10a01 ".printf\"tRECEIVE_CONTEXT(%#p).v_maxBufferLength_cc is %#x\\nUpdating tRECEIVE_CONTEXT(%#p).v_dataBufferLookasideLength_d0 from %#x to %#x\\n\\n\",@ecx,poi(@ecx+0xcc),@ecx,poi(@ecx+0xd0),@eax;g"

The base addresses for both the kernel and RMCAST.SYS driver during this walkthrough are as follows.

0: kd> lm m nt
Browse full module list
start    end        module name
81476000 81bef000   nt         (pdb symbols)          c:\mss\ntkrpamp.pdb\D731792263AA1951060D58BE484982761\ntkrpamp.pdb

0: kd> lm vm rmcast
Browse full module list
start    end        module name
a2700000 a2733000   RMCAST     (pdb symbols)          c:\mss\rmcast.pdb\C59E73338FF30F35ACC04D9492ABA57A1\rmcast.pdb
    Loaded symbol image file: RMCAST.sys
    Image path: RMCAST.sys
    Image name: RMCAST.sys
    Browse all global symbols  functions  data
    Image was built with /Brepro flag.
    Timestamp:        DC0BCB0A (This is a reproducible build file hash, not a timestamp)
    CheckSum:         0002A034
    ImageSize:        00033000
    Translations:     0000.04b0 0000.04e4 0409.04b0 0409.04e4
    Information from resource tables:

After applying the listed breakpoints and resuming the target machine, the proof of concept can be executed against the target PGM server. The first conditional breakpoint that should be reached is inside the PgmNewInboundConnection function. This will display the MTU being copied from the global dynamic configuration into the new session being created.

0: kd> g
Entry -> PgmNewInboundConnection(0xa10f5ea0, 0x16, 0x805db588, 0x1008, 0xaac0a036, 0x24, 0x805db4cc)
gv_pgmDynamicConfig_2a3c0.Data.v_maxMtu_78 is 0x5c4
Updating tCOMMON_SESSION_CONTEXT(0xa6319ed0).vw_maxMTULength_54 from 0 to 0x5c4
Updating tCOMMON_SESSION_CONTEXT(0xa6319ed0).vw_maxFECPacketLength_56 from 0 to 0x5d2

Once the MTU has been copied into the session, its value will then be copied from the session into the context that is used to specify the size of the data buffers allocated by the lookaside list. Immediately afterwards, the lookaside list for the data buffers will be initialized to complete constructing the session for maintaining the connection. It is worth noting that the condtional breakpoint displays the address of the receive context being used to maintain the current session (0x92841d60).

tCOMMON_SESSION_CONTEXT(0xa6319ed0).v_data_8.vw_maxMTULength_54 is 0x5c4
Updating tRECEIVE_CONTEXT.v_maxBufferLength_cc from 0 to 0x5d4
tRECEIVE_CONTEXT(0x92841d60).v_maxBufferLength_cc is 0x5d4
Updating tRECEIVE_CONTEXT(0x92841d60).v_dataBufferLookasideLength_d0 from 0 to 0x5d4

The next breakpoint will be triggered when the proof-of-concept transmits an SPM packet to the server. The options specified in the packet result in initializing the byte field at offset +0x65 of the session and copy the size specified as group count to offset +0x64 of the session. These fields needs to be initialized before the next data packet is sent to ensure the flag controlling which allocator to use will be cleared.

Entry -> ProcessSpmPacket(0xa10f5ea0, 0xa6319ed0, 0x30, 0x929b5642)
Updating tCOMMON_SESSION_CONTEXT(0xa6319ed0).v_data_8.vb_fecOptions_5d from 0 to 0x1 (packet options)
Updating tCOMMON_SESSION_CONTEXT(0xa6319ed0).v_data_8.vb_fecGroupSize_5c from 0x1 to 0x4 (packet options)

Once the SPM packet has initialied the FEC fields for the session, the next packet should have a message type of OD_DATA (4). After validating the checksum, the PgmHandleNewdata function will be called. Due to the state of the current session, the next breakpoint will interrupt execution when the AllocateDataBuffer function is called. The third parameter of this function is the size of the data packet being processed.

Entry -> AllocateDataBuffer(0xa6319ed0, 0x8e5a9f80, 0x69e)
Packet length is 0x69e
tRECEIVE_CONTEXT(0x92841d60).v_dataBufferLookasideLength_d0 is 0x5d4
Comparing 0x69e (packet length) to 0x5d4 (lookaside)

The size of the packet will be compared to the size of the allocation for the lookaside data buffers. If the packet size is larger than the size assigned to the lookaside buffer length, and the FEC fields are initialized, then the comparison will succeed. This is demonstrated in the following CPU state showing that the branch instruction ends up being taken.

eax=00008220 ebx=0000069e ecx=92841d60 edx=8e5a9f80 esi=8e5a9f80 edi=a6319ed0
eip=a270a522 esp=805db1a8 ebp=805db1b4 iopl=0         nv up ei pl nz na pe nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000206
RMCAST!AllocateDataBuffer+0x22:
a270a522 7720            ja      RMCAST!AllocateDataBuffer+0x44 (a270a544) [br=1]

After the branch has been taken, the PGM_SESSION_DATA_FROM_LOOKASIDE (0x8000) flag is cleared from the current session flags.

Updating tCOMMON_SESSION_CONTEXT(0xa6319ed0).v_data_8.v_sessionFlags_c from 0x8220 to 0x220

After the PGM_SESSION_DATA_FROM_LOOKASIDE (0x8000) has been cleared, the session is ready to be closed to trigger the denial of service. The next breakpoint that will interrupt execution will reside within the PgmCloseConnection function. As shown in the listing earlier in this advisory, a condition in the PgmCloseConnection function checks to see if the PGM_SESSION_DATA_FROM_LOOKASIDE (0x8000) is set.

Entry -> ProcessSpmPacket(0xa10f5ea0, 0xa6319ed0, 0x30, 0x91c2903a)
tCOMMON_SESSION_CONTEXT(a6319ed0).v_data_8.v_sessionFlags_c is 0x2160

If it is not, then the function will not delete the non-paged lookaside list stored at offset +0xE0 of the session. This is demonstrated by the next breakpoint showing that the execution of the PgmCloseConnection function takes the conditional branch instruction to skip over the call to ExDeleteNPagedLookasideList.

eax=92841d60 ebx=81ad8f52 ecx=00000000 edx=8e500a30 esi=81ae17ef edi=a6319ed0
eip=a2704b98 esp=88d3a438 ebp=88d3a460 iopl=0         nv up ei pl zr na pe nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000246
RMCAST!PgmCloseConnection+0xfc:
a2704b98 7408            je      RMCAST!PgmCloseConnection+0x106 (a2704ba2) [br=1]

Prior to completing execution of the PgmCloseConnection function, the debugger will interrupt execution before freeing the entire session. The address being freed (0x92841d60) matches the context that was displayed earlier when monitoring the current state of the session. Stepping over this will free the context and the only reference to the lookaside list from memory.

Calling ExFreePoolWithTag(81ad8f52) with [0x92841d60, 0]
RMCAST!PgmCloseConnection+0x156:
a2704bf2 ffd3            call    ebx

If driver verifier is enabled for the RMCAST.SYS driver, this will trigger an immediate bugcheck.

0: kd> p
KDTARGET: Refreshing KD connection

*** Fatal System Error: 0x000000c4
                       (0x000000CC,0x92841E40,0x92841D60,0x000002A0)

WARNING: This break is not a step/trace completion.
The last command has been cleared to prevent
accidental continuation of this unrelated event.
Check the event, location and thread before resuming.
Break instruction exception - code 80000003 (first chance)

A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

0: kd> !analyze -v
Connected to Windows 10 19041 x86 compatible target at (Tue Jul 30 21:22:15.878 2024 (UTC - 7:00)), ptr64 FALSE
Loading Kernel Symbols
...............................................................
................................................................
.............................................................
Loading User Symbols

Loading unloaded module list
.......Unable to enumerate user-mode unloaded modules, Win32 error 0n30
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

DRIVER_VERIFIER_DETECTED_VIOLATION (c4)
A device driver attempting to corrupt the system has been caught.  This is
because the driver was specified in the registry as being suspect (by the
administrator) and the kernel has enabled substantial checking of this driver.
If the driver attempts to corrupt the system, BugChecks 0xC4, 0xC1 and 0xA will
be among the most commonly seen crashes.
Arguments:
Arg1: 000000cc, Freeing pool allocation that contains active lookaside list.
Arg2: 92841e40, Lookaside list address.
Arg3: 92841d60, Pool allocation start address.
Arg4: 000002a0, Pool allocation size.

Debugging Details:
------------------


KEY_VALUES_STRING: 1

    Key  : Analysis.CPU.mSec
    Value: 5249

    Key  : Analysis.DebugAnalysisManager
    Value: Create

    Key  : Analysis.Elapsed.mSec
    Value: 6062

    Key  : Analysis.Init.CPU.mSec
    Value: 103343

    Key  : Analysis.Init.Elapsed.mSec
    Value: 36140186

    Key  : Analysis.Memory.CommitPeak.Mb
    Value: 114

    Key  : WER.OS.Branch
    Value: vb_release

    Key  : WER.OS.Timestamp
    Value: 2019-12-06T14:06:00Z

    Key  : WER.OS.Version
    Value: 10.0.19041.1


BUGCHECK_CODE:  c4

BUGCHECK_P1: cc

BUGCHECK_P2: ffffffff92841e40

BUGCHECK_P3: ffffffff92841d60

BUGCHECK_P4: 2a0

PROCESS_NAME:  System

STACK_TEXT:  
88d39e8c 81691f3b     00000003 48a9369f 00000065 nt!RtlpBreakWithStatusInstruction
88d39ee0 81691907     88632980 88d3a2cc 88d3a30c nt!KiBugCheckDebugBreak+0x1f
88d3a2a0 815f58ee     000000c4 000000cc 92841e40 nt!KeBugCheck2+0x79d
88d3a2c4 815f5825     000000c4 000000cc 92841e40 nt!KiBugCheck2+0xc6
88d3a2e4 81ad7277     000000c4 000000cc 92841e40 nt!KeBugCheckEx+0x19
88d3a30c 81ae1a48     92841e40 92841d60 000002a0 nt!VerifierBugCheckIfAppropriate+0x8e
88d3a330 816ffd2a     92841d60 000002a0 92841d60 nt!VfCheckForLookaside+0x6a
88d3a340 816521d5     92841d60 92841d60 92841d60 nt!ExpCheckForLookaside+0x1a
88d3a354 81702b5d     00000000 92841d60 86e0a000 nt!ExpFreePoolChecks+0xbb813
88d3a398 81648d14     88d3a414 92841d60 81ad8f52 nt!ExpFreeHeapSpecialPool+0x3f
88d3a3e8 817241b0     88d3a414 82115eea 92841d60 nt!ExFreeHeapPool+0xd3d14
88d3a3f0 82115eea     92841d60 00000000 a6319ed0 nt!ExFreePoolWithTag+0x10
WARNING: Stack unwind information not available. Following frames may be wrong.
88d3a414 81ad8f99     92841d60 00000000 81ae17ef VerifierExt!XdvHibernationNotification+0xfcc2
88d3a428 a2704bf4     92841d60 00000000 a6341f00 nt!VerifierExFreePoolWithTag+0x47
88d3a440 a2706031     a9cd4ae0 942113a0 94211302 RMCAST!PgmCloseConnection+0x158
88d3a460 815d3ef1     942113a0 a6341f00 a6341f00 RMCAST!PgmDispatchClose+0xf3
88d3a478 81ace98b     a6341f00 942113a0 00000000 nt!IopfCallDriver+0x3f
88d3a494 81649696     00000000 a74b2668 00000000 nt!IovCallDriver+0x1f0
88d3a4a8 81827541     86ffb9a0 a74b2650 a74b2668 nt!IofCallDriver+0xd0736
88d3a4e0 8186593c     a74b2668 a74b2650 00000000 nt!IopDeleteFile+0x121
88d3a500 8151f16d     a6235878 a6235878 00310030 nt!ObpRemoveObjectRoutine+0x5c
88d3a518 918a819f     a6235878 a62358f0 00310030 nt!ObfDereferenceObject+0x8d
88d3a530 91863750     91883851 a62358f0 81571360 afd!AfdFreeConnectionResources+0xf67f
88d3a54c 91883868     88d3a5a8 918631b7 a62358f0 afd!AfdFreeConnectionEx+0x24
88d3a554 918631b7     a62358f0 a119b680 91863160 afd!AfdFreeConnection+0x17
88d3a568 8156c78a     8ff4cc40 00000000 86f934c0 afd!AfdDoWork+0x57
88d3a5a8 814e5ec8     90ab1fc8 00000000 8a0399c0 nt!IopProcessWorkItem+0xf8
88d3a5f8 814b0878     86f934c0 48a90e4f 00000000 nt!ExpWorkerThread+0xf8
88d3a630 816178f1     814e5dd0 86f934c0 00000000 nt!PspSystemThreadStartup+0x4a
88d3a63c 00000000     00000000 00000000 00000000 nt!KiThreadStartup+0x15


SYMBOL_NAME:  RMCAST!PgmCloseConnection+158

MODULE_NAME: RMCAST

IMAGE_NAME:  RMCAST.sys

STACK_COMMAND:  .cxr; .ecxr ; kb

BUCKET_ID_FUNC_OFFSET:  158

FAILURE_BUCKET_ID:  0xc4_cc_VRF_RMCAST!PgmCloseConnection

OS_VERSION:  10.0.19041.1

BUILDLAB_STR:  vb_release

OSPLATFORM_TYPE:  x86

OSNAME:  Windows 10

FAILURE_ID_HASH:  {c9825418-c239-00e5-360c-6ebe94193a83}

Followup:     MachineOwner
---------

Exploit Proof of Concept

This vulnerability requires a server to be bound to a multicast address and port. The Microsoft Message Queueing service can be configured to do this via the Management snap-in. To configure the service, create a new private queue, and then right-click on it to view its properties. Select the “Multicast” tab, and enter in the desired multicast address and port. For example, to bind to address 234.5.6.7 port 57005 use the string “234.5.6.7:57005”.

Alternatively, the poc.pgm-mcast-server.src.zip contains a server that can be built with MinGW toolchain. Once the server has been built, it can be run with the address and port number to bind with as its parameters. To terminate the server (and close the connection), Ctrl+C should be sufficient.

C> server.exe
Usage: server.exe host port

C> server.exe 234.5.6.7 57005
Mode was set to recv
Preparing to listen on 0xea050607:57005
Socket listening (16) on 0xea050607:57005...
Accepting connections on socket 196

The proof-of-concept depends on Python3, and requires Linux or FreeBSD to run. After ensuring that the host is routing multicast addresses through the desired interface, and the target server is bound to a PGM address and port, the proof-of-concept can be used to send the required traffic to the PGM endpoint. If the correct interface cannot be detected automatically, the interface can be selected by name using the “-iface” parameter.

$ python poc.py3.zip
usage: poc.py3.zip [-h] [-s SEQNO] [-S SRCNO] [-g GSI] [--sport SPORT] [--source SADDR] [-j] [-w WAIT] [-iface INTERFACE] host port
poc.py3.zip: error: the following arguments are required: host, port

$ python poc.py3.zip -iface vmx0 234.5.6.7 57005
...

Triggering the vulnerability requires the global size to be correct and for the connection to be closed. If using driver verifier along with the distributed server, the driver should crash immediately after terminating the server.

VENDOR RESPONSE

Talos independently discovered this issue and reported it to Microsoft prior to their August 2024 patch release. However it turns out that a Microsoft researcher had already internally discovered this issue and Microsoft released the fix as part of their August 2024 patch release.

TIMELINE

2024-08-13 - Vendor Disclosure
2024-08-13 - Vendor Patch Release
2024-09-25 - Public Release

Credit

Discovered by a member of Cisco Talos.