Skip to main content
Patch Diffing Microsoft Windows Wi-Fi Driver Vulnerability (CVE-2024-30078) - Part 2

Patch Diffing Microsoft Windows Wi-Fi Driver Vulnerability (CVE-2024-30078) - Part 2

Kapil Khot
Patch Diffing CVE-2024-30078 - This article is part of a series.
Part 2: This Article

Background
#

Recently, I had some free time, so I decided to continue my analysis of CVE-2024-30078, a remote code execution vulnerability in Windows Wi-Fi driver. In the previous blog post, we identified the patched file and started looking into the patched function Dot11Translate80211ToEthernetNdisPacket(). Our next step was to reverse engineer this function to better understand the variable data types and structures, and then rename them and change their data types in Ghidra to improve the code readability.

In this blog post, I’ll walk through the approach I took and the assumptions I made. The final part of this series will focus on validating those assumptions, making corrections (if needed), and creating a proof-of-concept exploit.

Reverse Engineering Dot11Translate80211ToEthernetNdisPack()
#

Since we had loaded the correct symbol files (.PDB files) in Ghidra, we at least know what Windows API calls this function is making. As we discussed in the previous blog post, we can analyse arguments passed to various Windows API calls this function makes, such as MmMapLockedPagesSpecifyCache() and NdisAdvanceNetBufferListDataStart(). I think we should look at MmMapLockedPagesSpecifyCache() first, as lvar5 is passed to it as an argument, which appears to be a dereferencing a pointer to param_2 + offset 0x28.

Looking at Dot11Translate80211ToEthernetNdisPack()’s signature, we see that param_2 is the second argument passed to it. It appears to be a Structure, as variables lvar5 and lvar2 hold values located at memory addresses param_2 + 0x28 and param_2 + 0x3c, respectively.

undefined8 Dot11Translate80211ToEthernetNdisPacket(longlong param_1,longlong param_2,undefined (**param_3) [16])
{
  <omitted>

  lVar5 = *(longlong *)(param_2 + 0x28);
  uVar2 = *(undefined4 *)(param_2 + 0x3c);
  
  if ((*(byte *)(lVar5 + 10) & 5) == 0) {
    lVar8 = MmMapLockedPagesSpecifyCache(lVar5,0,1,0,0,0x40000020);
  }
  else{
  <omitted>

  // param_2 + 0x20 is passed to this function
  NdisAdvanceNetBufferListDataStart(*(undefined8 *)(param_2 + 0x20),uVar19,1,NetMonShutdown);
  <omitted>

Fortunately, this API is documented on MSDN. The first parameter lVar5, passed to it is a pointer to Memory Descriptor List (MDL). It describes the physical memory pages backing a virtual memory address, allowing kernel-mode drivers to access user-mode memory.

The operating system uses a memory descriptor list (MDL) to describe the physical page layout for a virtual memory buffer. An MDL consists of an MDL structure that is followed by an array of data that describes the physical memory in which the I/O buffer resides.

The following screenshot from Ghidra shows members of this structure and their data types:

mdl structure

Based on this information, lets rename lVar5 to pMDL and also change its data type to _MDL. After retyping this variable in Ghidra, it looks like this:

undefined8 Dot11Translate80211ToEthernetNdisPacket(longlong param_1,NWIFI_MSDU *pMSDUStruct,undefined (**param_3) [16])
{
  <omitted>
  pMDL = *(_MDL **)(param_2 + 0x28);
  uVar2 = *(undefined4 *)(param_2 + 0x3c);    // another hint, there is something at offset 0x3c from param2. 
  
  if ((*(byte *)&pMDL->MdlFlags & 5) == 0) {
    pvMappedPageAddr = (PVOID)MmMapLockedPagesSpecifyCache(pMDL,0,1,0,0,0x40000020);
  }
  else {
    pvMappedPageAddr = pMDL->MappedSystemVa;
  }
  
  <omitted>

Also, there’s call to another Windows API, NdisAdvanceNetBufferListDataStart() where param_2 + 0x20 is passed as first argument. According to this MSDN article, the first argument to this function is a pointer to a previously allocated NET_BUFFER_LIST structure. The structure definition can be found in Ghidra and also in the nbl.h file from Windows Driver Kits installation directory.

At this point, I thought I had enough information to figure out what param2 really is. It could be a structure or a pointer. One way to find out is by setting up the target virtual machine for kernel debugging, connecting to it from the host machine, placing a breakpoint on the patched function, and then trying to connect to a Mobile Hotspot from the target (debuggee) VM. Once the breakpoint hits, I can check the CPU register values and try to determine their contents—whether they hold a specific value, a pointer, or a structure.

Another option is to check the Data Types Manager in Ghidra and look for a data type that has something at offsets 0x28, 0x3C, 0x20, etc. These offsets match the ones we see being referenced in the code from param_2. But with at least a hundred data types to read through, doing it manually wasn’t an option. So, I decided to write a short Ghidra script for it.

Since I hadn’t explored Ghidra scripting before, I asked ChatGPT to write it for me. But it didn’t work right out of the box. I had to figure out what it was trying to do and fix it. In the end, I basically rewrote the whole thing.

#@author SlidingWindow (Twitter: @Kapil_Khot)
#@category _NEW_
#@keybinding 
#@menupath 
#@toolbar 

from ghidra.program.model.data import Structure, DataTypeManager

# Get the current program's Data Type Manager
target_struct_name = "_MDL"	# Search for this structure in all available structures.
dtm = currentProgram.getDataTypeManager()
components = ""
field_name = ""
data_type_name = ""
struct_name = ""

for dt in dtm.getAllDataTypes():
    if isinstance(dt, Structure):  # Only process structures
        struct_name = dt.getName()
	components = dt.getComponents()      
	
	for comp in components:
		field_name = comp.getFieldName()
		data_type_name = comp.getDataType().getName()

		if target_struct_name in data_type_name:
			# print("[+] The following structure contains the structure {} you're looking for:\n{}".format(target_struct_name, struct_name))
			print("Structure '{}' contains '{}' ({}) at DECIMAL offset {}".format(struct_name, field_name, data_type_name, comp.getOffset()))
		else:
			continue

The following is the script output:

Find_Structs1.py> Running...
  <omitted first 40 lines>
  Structure '_IRP' contains 'MdlAddress' (_MDL *) at DECIMAL offset 8
  Structure '_MDL' contains 'Next' (_MDL *) at DECIMAL offset 0
  Structure '_NET_BUFFER_HEADER_s_0' contains 'CurrentMdl' (_MDL *) at DECIMAL offset 8
  Structure '_NET_BUFFER_HEADER_s_0' contains 'MdlChain' (_MDL *) at DECIMAL offset 32
  Structure '_NET_BUFFER_u_0_s_0' contains 'CurrentMdl' (_MDL *) at DECIMAL offset 8
  Structure '_NET_BUFFER_u_0_s_0' contains 'MdlChain' (_MDL *) at DECIMAL offset 32
  Structure 'DOT11_MDL_BOOKMARK' contains 'pNdisBuffer' (_MDL *) at DECIMAL offset 8
  Structure 'DOT11_MDL_SUBCHAIN' contains 'pHead' (_MDL *) at DECIMAL offset 0
  Structure 'DOT11_MDL_SUBCHAIN' contains 'pTail' (_MDL *) at DECIMAL offset 8
  Structure 'DOT11_MDL_SUBCHAIN' contains 'pMdlBeforeTail' (_MDL *) at DECIMAL offset 16
  Structure 'DOT11_MDL_SUBCHAIN' contains 'pOldTail' (_MDL *) at DECIMAL offset 24
  Structure 'DOT11_RMH_UNDO_LOG' contains 'pNdisBuffer' (_MDL *) at DECIMAL offset 0
  Structure 'DOT11_RMT_UNDO_LOG' contains 'pNewTail' (_MDL *) at DECIMAL offset 0
  Structure 'DOT11_RMT_UNDO_LOG' contains 'pMdlAfterNewTail' (_MDL *) at DECIMAL offset 8
  Structure 'NWIFI_MPDU' contains 'pHead' (_MDL *) at DECIMAL offset 0
  Structure 'NWIFI_MPDU' contains 'pTail' (_MDL *) at DECIMAL offset 8
  Structure 'NWIFI_MSDU' contains 'pHead' (_MDL *) at DECIMAL offset 40
  Structure 'NWIFI_MSDU' contains 'pTail' (_MDL *) at DECIMAL offset 48
Find_Structs1.py> Finished!

I had to scroll through about 53 lines, but that was still way better than manually searching through all the data types in Ghidra! The second-to-last line caught my attention as it shows that the NWIFI_MSDU structure contains a pointer to an MDL at offset 40. This offset is in decimal, which in hexadecimal is 0x28. If you remember, param2 + 0x28 is a pointer to MDL structure.

The following screenshot from Ghidra shows MSDU structure members:

msdu structure

So, it looks like the second argument passed to the patched function Dot11Translate80211ToEthernetNdisPacket() is an MSDU structure. Perhaps this is not the best way to figure out the data types of the arguments passed to a patched function and this approach might not work in all cases.

Fortunately, Paolo Stagno (@voidsec) and Aleksandr K have already analysed this patch (link here) and confirmed that the argument is indeed an MSDU structure. So, I think it’s safe to assume that we’re on the right track.

After identifying that param_2 is an MSDU structure, I spent some time analysing other structures, variables and Enums as well. Given that the patched function’s name suggests it handles the translation of 802.11 wireless frames into Ethernet NDIS packets, it’s reasonable to assume that its purpose is to convert wireless packets into Ethernet frames.

During this process, I also had to spend some time to read about 802.11 frame types, structure of 802.11 MAC frames, 802.1Q Logical Link Control (LLC) and Media Access Control (MAC), which are the sublayers of Data Link Layer.

I started by renaming variables and adjusting their data types in Ghidra for both the patched and unpatched versions of the file. Then, I compared them side by side to spot key differences, such as removed or newly added statements in the patched version. After analysing these changes, I added my comments based on my understanding of the code.

So, the code now looks like this:

undefined8 Dot11Translate80211ToEthernetNdisPacket(longlong param_1,NWIFI_MSDU *pMSDUStruct,undefined (**param_3) [16])
{
  undefined (*pauVar1) [16];
  byte bLLCEtherType;
  PVOID pvMappedPageAddr;
  bool bVar2;
  ulonglong uIsEnabledDeviceUsage;
  undefined7 extraout_var;
  PVOID pvLookAsideEntryPTR;
  undefined8 uInt32NtStatusCode;
  ushort uVar3;
  uint uReducedMacHdrSize2;
  undefined8 *puVar4;
  uint uReducedMacHdrSize;
  uint uVLanID;
  ushort *ulOffsetToLLC;
  UINT64 uInt64MacHdrSize;
  ULONG ulUsedDataSpace;
  undefined8 uDot1QNetBufferListInfo;
  undefined4 local_50;
  ushort uStack_4c;
  undefined2 uStack_4a;
  undefined2 uStack_48;
  _MDL *pMDL;
  ULONG pMDLByteCount;
  ushort uLLCEtherType;
  uint uMacHdrSize;
  ulong uPktLength;
  
  /* This is the first parameter passed to the MmMapLockedPagesSpecifyCache()function. So, it must be the pointer to _MDL structure. Searched for data type "_MDL" and Ghidra had it already. This structure definition is also found in "./Include/10.0.26100.0/km/wdm.h". 
                       
  Also, there's MSDN doc:
  https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-mmmaplockedpagesspecifycache
  */
  
  pMDL = pMSDUStruct->pHead;
  uPktLength = pMSDUStruct->uPktLength;

  if ((*(byte *)&pMDL->MdlFlags & 5) == 0) {
    pvMappedPageAddr = (PVOID)MmMapLockedPagesSpecifyCache(pMDL,0,1,0,0,0x40000020);
  }
  else {
    pvMappedPageAddr = pMDL->MappedSystemVa;
  }

  /* Does MacHdrsize indicate that this MSDU structure is for the MAC Data Service Unit (MSDU) in the MAC sub-layer of Data Link Layer frame (802.11 frame)? */

  uMacHdrSize = (pMSDUStruct->MSDUCore).uMacHdrSize;
  uInt64MacHdrSize = (UINT64)uMacHdrSize;
  pMDLByteCount = pMDL->ByteCount;

  /* If uMacHdrSize refers to the header in the MAC frame, then this must be an offset to the previous sub-layer, Logical Link Layer (LLC) in the data link layer (802.11) frame? */
  
  ulOffsetToLLC = (ushort *)((longlong)pvMappedPageAddr + (ulonglong)pMSDUStruct->uOffset);

  if ((uInt64MacHdrSize + 8 < 8) || ((ulonglong)pMDLByteCount < uInt64MacHdrSize + 8)) {
    LAB_1c0004725:
                    
    /* If the MAC Header + 8 bytes < 8, this means the packet is invalid. So, return error code NDIS_STATUS_INVALID_PACKET. This macro is defined in /c/Program Files (x86)/Windows Kits/10/Include/10.0.22621.0/shared/ndis/status.h*/
    
    uInt32NtStatusCode = 0xc001000f;

  }
  else {
        /* I hope I've got this right. This must be a VLAN ID? Because MSDU_Core structure has a pointer to _NET_BUFFER_LIST (pNdisPacket) in which the NetBufferListInfo seems to be an array of pointers. 
                       
        The 'NetBufferListInfo[4]' appears to be from NET_BUFFER_LIST structure and according to MSDN article: "NetBufferListInfo Specifies NET_BUFFER_LIST structure information that is common to all NET_BUFFER structures in the list. This information is often referred to as "out-of-band (OOB) data."
                       
        Looking at the '_NDIS_NET_BUFFER_LIST_INFO' enum, it seems the index 4 corresponds to Ieee8021QNetBufferListInfo, which specifies 802.1Q information about a packet. When you specify Ieee8021QNetBufferListInfo, NET_BUFFER_LIST_INFO returns the Value member of an NDIS_NET_BUFFER_LIST_8021Q_INFO structure. 
                       
        This structure can specify 802.1p priority and virtual LAN (VLAN) identifier information.
                       
        References:
        https://learn.microsoft.com/en-us/windows-hardware/drivers/network/net-buffer -list-structure
        https://learn.microsoft.com/en-us/windows-hardware/drivers/network/accessing- tcp-ip-offload-net-buffer-list-information
        */
    
        uDot1QNetBufferListInfo = ((pMSDUStruct->MSDUCore).pNdisPacket)->NetBufferListInfo[4];
                    
        /* Mask the upper 16 bits to extract the VLAN ID? */
        uVLanID = (uint)uDot1QNetBufferListInfo & 0xffff0000;
        uDot1QNetBufferListInfo = (void *)((ulonglong)uDot1QNetBufferListInfo & 0xffffffffffff0000);

    if (((char)*ulOffsetToLLC < '\0') && (uDot1QNetBufferListInfo == (void *)0x0)) {
      uReducedMacHdrSize = uMacHdrSize - 2 & 0xff;
      uReducedMacHdrSize2 = uReducedMacHdrSize - 4 & 0xff;

      if (-1 < (short)*ulOffsetToLLC) {
        uReducedMacHdrSize2 = uReducedMacHdrSize;
      }

      if ((*(byte *)((ulonglong)uReducedMacHdrSize2 + (longlong)ulOffsetToLLC) & 0xf) < 8) {
        uVLanID = (*(byte *)((ulonglong)uReducedMacHdrSize2 + (longlong)ulOffsetToLLC) & 0xf) <<
                  0x10;
        uDot1QNetBufferListInfo = (void *)(ulonglong)uVLanID;
      }
    }
    
    /* If my previous assumptions are valid, this resolves (dereferences) the EtherType field within the LLC sub-layer. Not all 802.11 MAC frames contain the LLC sublayer. Only the 802.11 Data frames do.
                       
    The Ether-type 0x8100 means a custom VLAN tag type (AKA C-Tag or Q-tag). The MSDU is inside the 802.11 Frame Body. The frame body looks like this: 
                       
      MAC Header | MSDU Payload | Trailer
                       
    The MSDU Payload looks like this (when it's a 802.11 Data frame):
                       
      Logical Link Layer (LLC) | Layer 3 to 7 payloads
                       
    And the LLC sub-layer with SNAP extension looks like this:
                       
      DSAP (SNAP) - 1 byte | SSAP (SNAP) - 1 byte | Control Field - 1 byte | Organization Unit Code - 3 bytes | Type - 2 bytes
                       
    The LLC sublayer takes a total of 8 bytes. Looking at the calculation below, the value at last two bytes of this sublayer (Windows structure in this case) is being stored in the variable and then compared to known Ether type.
                       
    References:
    https://howiwifi.com/2020/07/13/802-11-frame-types-and-formats/
    https://en.wikipedia.org/wiki/802.11_frame_types
    https://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml
    https://en.wikipedia.org/wiki/EtherType
    https://mrncciew.com/2014/11/01/cwap-802-11-data-frame-aggregation/
    https://stackoverflow.com/questions/42834940/how-to-find-out-encapsulated-protocol-inside-ieee802-11-frame
    https://www.youtube.com/playlist?list=PLBlnK6fEyqRgMCUAG0XRw78UA8qnv6jEx

    */
    
    uLLCEtherType = *(ushort *)(uInt64MacHdrSize + 6 + (longlong)ulOffsetToLLC);
    ulUsedDataSpace = uMacHdrSize - 0xe;
    
    if (uLLCEtherType == 0x81) {
      uIsEnabledDeviceUsage = Feature_1281542463__private_IsEnabledDeviceUsage();

      /* This seems to be the PATCH. Need to try sending a VLAN tagged data frame and it should result in 4 byte overwrite?? If the allocated/mapped memory buffer is less than MAC Header Size + 0xC,jump to label 1c0004725 where the function exits with error NDIS_STATUS_INVALID_PACKET. NOTE: This check is missing from the vulnerable version**
                        
      Why was this check added? After a bit of Googling, it seems pMDL->ByteCount means the mapped/allocated memory buffer. It's being checked if this buffer is less than ( MAC Header Size + 0xC). The structure is:
      
      MAC Header (several subfields in this header)  | LLC with SNAP ( since the ETHER Type is 0x8100 - this takes 8 bytes) | 802.1Q field of 4 bytes
                        
      So, in the vulnerable version, there;s no check if the mapped buffer is of 8 bytes or 12 bytes ( LLC with SNAP  + 802.1Q field )
                        
      https://en.wikipedia.org/wiki/IEEE_802.1Q */

      if (((int)uIsEnabledDeviceUsage != 0) && ((ulonglong)pMDLByteCount < (ulonglong)(pMSDUStruct->MSDUCore).uMacHdrSize + 0xc))
      goto LAB_1c0004725;

      uLLCEtherType = *(ushort *)((longlong)ulOffsetToLLC + uInt64MacHdrSize + 8);
      bLLCEtherType = (byte)uLLCEtherType;
      uVLanID = ((bLLCEtherType & 0xf) << 8 | (uint)(uLLCEtherType >> 8)) << 4 ^ uVLanID;

      if (((uVLanID & 0xfff0) != 0) && ((*(uint *)(param_1 + 0x80) & 0x200) == 0)) {
        if ((undefined **)WPP_GLOBAL_Control != &WPP_GLOBAL_Control) {
          WPP_SF_(*(undefined8 *)(WPP_GLOBAL_Control + 0x18),10, &WPP_632545ae6a7232f3ab98617ae9e39362_Traceguids);
        }
        return 0x10003;
      }

      if ((*(uint *)(param_1 + 0x80) & 0x40) != 0) {
        uVLanID = uVLanID | bLLCEtherType >> 5;
      }

      uDot1QNetBufferListInfo = (void *)CONCAT44(uDot1QNetBufferListInfo._4_4_,uVLanID);
      uLLCEtherType = *(ushort *)((longlong)ulOffsetToLLC + uInt64MacHdrSize + 10);
      ulUsedDataSpace = uMacHdrSize - 10;
    }

    ((pMSDUStruct->MSDUCore).pNdisPacket)->NetBufferListInfo[4] = uDot1QNetBufferListInfo;
    
    /* NOTE: This too seems to be part of the patch. Check what is this if sending a VLAN tagged frame doesn't trigger a crash. 
    
    This logical OR condition for Dot11CheckSelectiveTranslationTable() is NOT there in the vulnerable version. */

    /* This condition is different in the vulnerable version, WHY? */

    if (((ushort)(uLLCEtherType >> 8 | uLLCEtherType << 8) < 0x600) || (bVar2 = Dot11CheckSelectiveTranslationTable
    (param_1,*(short *)((longlong)ulOffsetToLLC + uInt64MacHdrSize + 6)), (int)CONCAT71(extraout_var,bVar2) != 0)) {
        
      uLLCEtherType = *ulOffsetToLLC & 0x100;
      
      if (uLLCEtherType == 0) {
        local_50 = *(undefined4 *)(ulOffsetToLLC + 2);
        uStack_4c = ulOffsetToLLC[4];
      }
      else {
        local_50 = *(undefined4 *)(ulOffsetToLLC + 8);
        uStack_4c = ulOffsetToLLC[10];
      }

      if ((*ulOffsetToLLC & 0x200) == 0) {
        uStack_4a = (undefined2)*(undefined4 *)(ulOffsetToLLC + 5);
        uStack_48 = (undefined2)((uint)*(undefined4 *)(ulOffsetToLLC + 5) >> 0x10);
        uVar3 = ulOffsetToLLC[7];
      }
      else if (uLLCEtherType == 0) {
        uStack_4a = (undefined2)*(undefined4 *)(ulOffsetToLLC + 8);
        uStack_48 = (undefined2)((uint)*(undefined4 *)(ulOffsetToLLC + 8) >> 0x10);
        uVar3 = ulOffsetToLLC[10];
      }
      else {
        uStack_4a = (undefined2)*(undefined4 *)(ulOffsetToLLC + 0xc);
        uStack_48 = (undefined2)((uint)*(undefined4 *)(ulOffsetToLLC + 0xc) >> 0x10);
        uVar3 = ulOffsetToLLC[0xe];
      }

      uLLCEtherType = ((short)uPktLength - (short)ulUsedDataSpace) - 0xe;
    }
    else {
      /* Most of the checks / statements in this parent 'else{}' block are different in the vulnerable version. WHY? */
      
      ulUsedDataSpace = ulUsedDataSpace + 8;
      uVar3 = *ulOffsetToLLC & 0x100;
      
      if (uVar3 == 0) {
        local_50 = *(undefined4 *)(ulOffsetToLLC + 2);
        uStack_4c = ulOffsetToLLC[4];
      }
      else {
        local_50 = *(undefined4 *)(ulOffsetToLLC + 8);
        uStack_4c = ulOffsetToLLC[10];
      }

      if ((*ulOffsetToLLC & 0x200) == 0) {
        uStack_4a = (undefined2)*(undefined4 *)(ulOffsetToLLC + 5);
        uStack_48 = (undefined2)((uint)*(undefined4 *)(ulOffsetToLLC + 5) >> 0x10);
        uVar3 = ulOffsetToLLC[7];
      }
      else if (uVar3 == 0) {
        uStack_4a = (undefined2)*(undefined4 *)(ulOffsetToLLC + 8);
        uStack_48 = (undefined2)((uint)*(undefined4 *)(ulOffsetToLLC + 8) >> 0x10);
        uVar3 = ulOffsetToLLC[10];
      }
      else {
        uStack_4a = (undefined2)*(undefined4 *)(ulOffsetToLLC + 0xc);
        uStack_48 = (undefined2)((uint)*(undefined4 *)(ulOffsetToLLC + 0xc) >> 0x10);
        uVar3 = ulOffsetToLLC[0xe];
      }
    }

    pvLookAsideEntryPTR = (PVOID)ExAllocateFromNPagedLookasideList(&Dot11Mem);

    if (pvLookAsideEntryPTR == (PVOID)0x0) {
    
      /* Return error STATUS_INSUFFICIENT_RESOURCES - Insufficient system resources exist to complete the API.
      This Macro is defined in ./Include/10.0.22621.0/shared/ntstatus.h
      #define STATUS_INSUFFICIENT_RESOURCES    ((NTSTATUS)0xC000009AL)  // ntsubauth
      */
      uInt32NtStatusCode = 0xc000009a;
    }
    else {
      *(undefined **)pvLookAsideEntryPTR = &Dot11Mem;
      *(undefined4 *)((longlong)pvLookAsideEntryPTR + 8) = 0x74766e43;
      pauVar1 = (undefined (*) [16])((longlong)pvLookAsideEntryPTR + 0x10);
      puVar4 = (undefined8 *)((ulonglong)ulUsedDataSpace + (longlong)ulOffsetToLLC);
      *pauVar1 = ZEXT816(0);
      *(undefined4 *)((longlong)pvLookAsideEntryPTR + 0x20) = 0;
      *(undefined8 *)*pauVar1 = *puVar4;
      *(undefined4 *)((longlong)pvLookAsideEntryPTR + 0x18) = *(undefined4 *)(puVar4 + 1);
      *(undefined2 *)((longlong)pvLookAsideEntryPTR + 0x1c) = *(undefined2 *)((longlong)puVar4 + 0xc);
      *puVar4 = CONCAT26(uStack_4a,CONCAT24(uStack_4c,local_50));
      *(uint *)(puVar4 + 1) = CONCAT22(uVar3,uStack_48);
      *(ushort *)((longlong)puVar4 + 0xc) = uLLCEtherType;
      
      /* The first argument to this NdisAdvanceNetBufferListDataStart() function is a pointer to a previously allocated NET_BUFFER_LIST structure. This structure is defined in ./Include/10.0.26100.0/km/ndis/nbl.h. 
      
      To Do: Based on this info, figure out what is param_2? What we know is param_2 + 0x20 is a pointer to previously allocated NET_BUFFER_LIST structure.
      
      References:
      https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/nblapi/nf-nbla pi-ndisadvancenetbufferlistdatastart
                       
      https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/nbl/ns-nbl-net _buffer_list
      */

      NdisAdvanceNetBufferListDataStart((pMSDUStruct->MSDUCore).pNdisPacket,ulUsedDataSpace,1,NetMonShutdown);
      *(ULONG *)((longlong)pvLookAsideEntryPTR + 0x20) = ulUsedDataSpace;
      *param_3 = pauVar1;
      uInt32NtStatusCode = 0;
    }
  }

  return uInt32NtStatusCode;
}

It looks like the vulnerability stems from how this function processes 802.11 data frames. When a Wi-Fi client receives a data frame from an Access Point, it checks the Ether Type value. If the Ether Type is 0x8100, it indicates a Customer VLAN tag (C-Tag), formerly known as a Q-Tag. This is documented on the IANA website here.

In the patched version, the following check has been added for the VLAN C-Tag (Q-Tag) frames:

if (((int)uIsEnabledDeviceUsage != 0) && ((ulonglong)pMDLByteCount < (ulonglong)(pMSDUStruct->MSDUCore).uMacHdrSize + 0xc))
      goto LAB_1c0004725;

In 802.11 data frames, when the EtherType is set to 0x8100, the Logical Link Control (LLC) layer and the Subnetwork Access Protocol (SNAP) together occupy 8 bytes, while the 802.1Q header adds another 4 bytes. It checks whether the allocated memory buffer is smaller than the required size for the MAC header plus 12 bytes. If yes, a jump to label 1c0004725 is taken, where the function exits with error NDIS_STATUS_INVALID_PACKET.

However, this check is missing in the vulnerable version, meaning it does not account for the extra 4 bytes from the 802.1Q header. As a result, these additional 4 bytes would be written beyond the allocated memory buffer (MDL) when the WiFi client processes an incoming packet where Ether Type is set to 0x8100.

Based on what I’ve read about 802.11 frame structure, data frames, LLC, and SNAP, the structure of the 802.11 data frame when Ether Type is 0x8100, in my understanding looks like the following.

802-11-data-frame

So, it looks like if we send a 802.11 data frame where Ether Type is set to 0x8100, the vulnerable version of ofDot11Translate80211ToEthernetNdisPack() function will end up overwriting 4 bytes outside an allocated memory buffer (MDL), which may result in a crash (denial-of-service).

What’s Next?
#

We can validate our assumptions by simulating an attack. We will need to set up a rogue Access Point on our attacking machine and connect to it from a vulnerable Windows VM. We will then send an 802.11 data frame with the Ether Type set to 0x8100 and see if it triggers a crash.

We can set up a rogue access point using Python; however, we will need a Wi-Fi adapter with packet injection capability, such as an Alfa adapter. Additionally, we will need to run a kernel debugging session on the vulnerable Windows VM and set a breakpoint on the Dot11Translate80211ToEthernetNdisPack() function before connecting to the rogue access point. This will allow us to perform dynamic analysis.

I will publish the final part of this blog series once I complete the dynamic analysis.

References
#



Patch Diffing CVE-2024-30078 - This article is part of a series.
Part 2: This Article