This blog post provides details about nine vulnerabilities affecting the IPv6 network protocol stack of EDK II, TianoCore's open source reference implementation of UEFI.

Introduction

In this blog post we describe PixieFAIL, nine vulnerabilities that affect EDK II, the de-facto open source reference implementation of the UEFI specification and possibly all implementations derived from it. The vulnerabilities are present in the network stack of EDK II and can be exploited during the network boot process.

Network boot is a standard feature on enterprise computers and servers. Using network boot to load an OS image from the network at boot time is popular in data centers and high-performance computing (HPC) environments, since server farms and HPC clusters may have hundreds or thousands of compute nodes that need to be provisioned with the same operating system and software configuration, and downloading and running the OS from a central set of servers (Boot servers) could greatly simplify management. In order to provide this network booting feature, UEFI implements a full IP stack at the DXE phase, opening the door to attacks from the local network during this early stage of the boot process.

The Preboot Execution Environment (PXE) is the specification for a standardized client-server solution to allow computers to boot over the network, colloquially referred to as netboot or Pixie boot. It was originally introduced by Intel in 1998 and later incorporated into the UEFI specification. Besides IPv4, PXE originally relied on a handful of simple protocols to achieve network booting: DHCP, UDP, and TFTP. These were considered relatively simple protocols that could be implemented with a sufficiently small footprint to fit in Option ROM or Network Interface Card (NIC) firmware. Since the release of version 2.2 of the UEFI specification in 2010, IPv6-based PXE has been part of the UEFI specification as well. In the case of IPv6, besides the core protocol, other protocols such as ICMPv6, Neighbor Discovery (ND), Multicast Listener Discovery (MLD), UDP, TCP, DHCPv6, DNS, and TFTP are implemented, which increased the overall attack surface.

EDK II is an open source implementation of the UEFI specification developed and maintained by Tianocore, a community of developers from software vendors that also leverage the project for their own UEFI implementations. In a prior blog post Gwaby explored the attack surface and exploitability of EDK II and showed that even a bug that provides just minimal capability could be used to achieve arbitrary code execution if the context is right. UEFI vulnerabilities that could be triggered remotely seemed like an interesting context for exploitation, and eventually persistence, but where would we look for those?

The EDK II UEFI reference implementation provides both IPv4- and IPv6-based PXE. In the latest available specification (UEFI 2.10) as of this writing, IPv6-based PXE is described in section "24.3.18 - Netboot6".

We performed a cursory inspection of NetworkPkg, Tianocore's EDK II PXE implementation, and identified nine vulnerabilities that can be exploited by unauthenticated remote attackers on the same local network, and in some cases, by attackers on remote networks. The impact of these vulnerabilities includes denial of service, information leakage, remote code execution, DNS cache poisoning, and network session hijacking.

Affected vendors

Other vendors that use EDK II's NetworkPkg module (non-exhaustive list):

CERT/CC published Vulnerability Note VU#132380 with a more comprehensive list of affected vendors, and guidance to deploy fixes and mitigations.

Proof of concept scripts to detect the presence of vulnerabilities 1 to 7 are available here.

Technical details

Preboot Execution Environment (PXE)

In order to boot from the network, a client system must be able to locate, download, and execute code that sets up, configures, and runs the operating system. This is usually done in several stages, starting with a minimal program that is downloaded from a network server using a simple protocol, such as TFTP, which then downloads and runs a second booting stage or the full operating system image.

To locate this minimal program, called Network Bootstrap Program (NBP), the PXE client relies on a DHCP server to both obtain the configuration parameters to configure its network interface with a valid IP address and to receive a list of Boot Servers to query for the NBP file. Since the DHCP server must provide such a list and other special parameters, the PXE client has to send some mandatory PXE-releated DHCP Options, consequently, the DHCP server must be "PXE enabled", i.e. configured appropriately to recognize PXE client options and to reply with the proper DHCP server options. To facilitate network booting without having to modify the configuration of operational DHCP servers servicing all types of clients, not just PXE clients, the specification splits the regular DHCP server functionality from the PXE-related functionality into two separate services, the DHCP service, and the proxy DHCP service, running on different ports on the same or separate machines.

The PXE client selects a Boot Server and communicates with it using the DHCP protocol to obtain the filename and file service (TFTP) parameters necessary to download the NBP. Finally, the NBP is downloaded, verified (in the case of Secure Boot), and executed. Once the next booting stage or the fully-fledged operating system image is downloaded and setup, execution control is transferred to it. A more detailed description of the PXE boot process is provided here.

PXE over IPv6 is based on a combination of the DHCPv6 and TFTP protocols, which in turn implies the usage of IPv6 and UDP at layers 3 and 4, respectively. Because a DHCP server may provide a list of Boot Servers by hostname, it is also necessary to have a working implementation of the DNS protocol.

The following picture from the UEFI 2.10 specification summarizes the PXE boot process over IPv6:

CVE-2023-45229: Integer underflow when processing IA_NA/IA_TA options in a DHCPv6 Advertise message

When trying to boot using PXE over IPv6, the EDK II firmware starts by sending DHCPv6 Solicit messages. DHCPv6 servers answer with an Advertise message. Advertise messages are handled in EDK II by the Dhcp6HandleAdvertiseMsg function in NetworkPkg/Dhcp6Dxe/Dhcp6Io.c, which calls Dhcp6SeekStsOption in order to search for Status codes in the received message. A Status code may appear as an option in the DHCPv6 message, or as an option inside another option, such as the IA_NA or IA_TA options.

When seeking for Status codes encapsulated in IA_NA or IA_TA options, the vulnerable function Dhcp6SeekStsOption in NetworkPkg/Dhcp6Dxe/Dhcp6Io.c does the following:

EFI_STATUS
Dhcp6SeekStsOption (
  IN     DHCP6_INSTANCE    *Instance,
  IN     EFI_DHCP6_PACKET  *Packet,
  OUT    UINT8             **Option
  )
{
[...]
    //
    // Seek in encapsulated options, IA_NA and IA_TA.
    //
    *Option = Dhcp6SeekIaOption (
                  Packet->Dhcp6.Option,
                  Packet->Length - sizeof (EFI_DHCP6_HEADER),
                  &Instance->Config->IaDescriptor
                  );
    if (*Option == NULL) {
        return EFI_SUCCESS;
    }

[...]
    if (Instance->Config->IaDescriptor.Type == Dhcp6OptIana) {
[1]     IaInnerOpt = *Option + 16;
[2]     IaInnerLen = (UINT16)(NTOHS (ReadUnaligned16 ((UINT16 *)(*Option + 2))) - 12);
    } else {
[3]     IaInnerOpt = *Option + 8;
[4]     IaInnerLen = (UINT16)(NTOHS (ReadUnaligned16 ((UINT16 *)(*Option + 2))) - 4);
    }

[5] *Option = Dhcp6SeekOption (IaInnerOpt, IaInnerLen, Dhcp6OptStatusCode);

[...]

Basically, the IA_NA and IA_TA options in an Advertise message are trusted without doing basic sanity checks, e.g.:

  • in the case of IA_NA options:

    • that the option is at least 16 bytes in length before setting IaInnerOpt to *Option + 16, at [1].
    • that the value of the optlen field of the option is at least 12, before subtracting 12 from it, at [2].
  • in the case of IA_TA options:

    • that the option is at least 8 bytes in length before setting IaInnerOpt to *Option + 8, at [3].
    • that the value of the optlen field of the option is at least 4, before subtracting 4 from it, at [4].

The lack of checking for sane values in the optlen field of these options allows to trigger an integer underflow, by setting the optlen field to a value < 12 (in the case of IA_NA) or < 4 (in the case of IA_TA).

As an example, if we send a DHCPv6 Advertise message containing an IA_NA option with its optlen field set to 11, when the code above calls Dhcp6SeekOption at [5] to scan forward for status options, the IaInnerLen parameter will be set to 0xFFFF (11 - 12), and as a result function Dhcp6SeekOption will attempt to read memory well past the end of the received packet.

CVE-2023-45230: Buffer overflow in the DHCPv6 client via a long Server ID option

After doing the initial Solicit/Advertise exchange, a DHCPv6 client needs to send a Request message. This is done via the Dhcp6SendRequestMsg function in NetworkPkg/Dhcp6Dxe/Dhcp6Io.c.

This Request message needs to keep a few DHCPv6 options sent during the Solicit message, such as OPTION_ORO (0x6), OPTION_CLIENT_ARCH_TYPE (0x3D), OPTION_NII (0x3E), and OPTION_VENDOR_CLASS (0x10). These options are kept in Instance->Config->OptionList. Their lengths are added up ([1]), in order to calculate the size of the buffer needed to store the Request packet. Notice that the total size of the allocation is DHCP6_BASE_PACKET_SIZE (1024, defined in NetworkPkg/Dhcp6Dxe/Dhcp6Impl.h) plus the sum of the lengths of the options mentioned before ([2]). In our tests UserLen typically ends up having the value 67, so the total size of the allocation is 1091 bytes.

EFI_STATUS
Dhcp6SendRequestMsg (
    IN DHCP6_INSTANCE  *Instance
    )
    [...]
    //
    // Calculate the added length of customized option list.
    //
    UserLen = 0;
    for (Index = 0; Index < Instance->Config->OptionCount; Index++) {
[1]     UserLen += (NTOHS (Instance->Config->OptionList[Index]->OpLen) + 4);
    }

    //
    // Create the Dhcp6 packet and initialize common fields.
    //
[2] Packet = AllocateZeroPool (DHCP6_BASE_PACKET_SIZE + UserLen);
    if (Packet == NULL) {
        return EFI_OUT_OF_RESOURCES;
    }

    Packet->Size                       = DHCP6_BASE_PACKET_SIZE + UserLen;
    [...]

The Request message also needs to retain the Server ID (0x2) option that was previously sent by the DHCPv6 server in the Advertise message. It is retrieved this way:

    [...]
    //
    // Get the server Id from the selected advertisement message.
    //
    Option = Dhcp6SeekOption (
             Instance->AdSelect->Dhcp6.Option,
             Instance->AdSelect->Length - 4,
             Dhcp6OptServerId
             );
    if (Option == NULL) {
        return EFI_DEVICE_ERROR;
    }

    ServerId = (EFI_DHCP6_DUID *)(Option + 2);
    [...]

Then, the Request packet is built by assembling all the needed options, one after the other: first the Client ID, then the Elapsed Time, then the Server ID, and so on. Notice that when appending the Server ID option (which is fully controlled by the DHCPv6 server), its Length field is fully trusted, without any sanity checks. Therefore, a Server ID option with an overly large Length field can overflow the Packet->Dhcp6.Option buffer with fully controlled data (overflow data comes from the DUID field of the Server ID option).

  [...]
  //
  // Assembly Dhcp6 options for request message.
  //
  Cursor = Packet->Dhcp6.Option;

  Length = HTONS (ClientId->Length);
  Cursor = Dhcp6AppendOption (
             Cursor,
             HTONS (Dhcp6OptClientId),
             Length,
             ClientId->Duid
             );

  Cursor = Dhcp6AppendETOption (
             Cursor,
             Instance,
             &Elapsed
             );

  Cursor = Dhcp6AppendOption (
             Cursor,
             HTONS (Dhcp6OptServerId),
             ServerId->Length,
             ServerId->Duid
             );

The very same bug seems to be present in functions Dhcp6SendDeclineMsg, Dhcp6SendReleaseMsg, and Dhcp6SendRenewRebindMsg (NetworkPkg/Dhcp6Dxe/Dhcp6Io.c).

CVE-2023-45231: Out of Bounds read when handling a ND Redirect message with truncated options

Function Ip6ProcessRedirect in NetworkPkg/Ip6Dxe/Ip6Nd.c handles Neighbor Discovery (ND) protocol's Redirect messages. This function calls Ip6IsNDOptionValid (NetworkPkg/Ip6Dxe/Ip6Option.c) to verify that all the options included in the Redirect message are valid.

EFI_STATUS
Ip6ProcessRedirect (
  IN IP6_SERVICE     *IpSb,
  IN EFI_IP6_HEADER  *Head,
  IN NET_BUF         *Packet
  )
{
    [...]
    // All included options have a length that is greater than zero.
    //
    OptionLen = (UINT16)(Head->PayloadLength - IP6_REDITECT_LENGTH);
    if (OptionLen != 0) {
        Option = NetbufGetByte (Packet, IP6_REDITECT_LENGTH, NULL);
        ASSERT (Option != NULL);

        if (!Ip6IsNDOptionValid (Option, OptionLen)) {
            goto Exit;
        }
    }
  [...]

If the options section of an incoming ND Redirect message is made of one single option, which is truncated and composed of only the Option Code (i.e. 1 single byte), Ip6IsNDOptionValid returns TRUE. This happens because Ip6IsNDOptionValid is called with parameter OptionLen == 1, and sizeof(IP6_OPTION_HEADER) is 2, therefore the while loop in charge of traversing the list of options ([1]) is never entered, and we just reach the return TRUE at the end ([2]), meaning that the truncated option is considered as valid, even though it is not.

BOOLEAN
Ip6IsNDOptionValid (
  IN UINT8   *Option,
  IN UINT16  OptionLen
  )
{
    Offset = 0;

    //
    // RFC 4861 states that Neighbor Discovery packet can contain zero or more
    // options. Start processing the options if at least Type + Length fields
    // fit within the input buffer.
    //
[1] while (Offset + sizeof (IP6_OPTION_HEADER) - 1 < OptionLen) {
        [...]
    }

[2] return TRUE;
}

After that, back in Ip6ProcessRedirect, options are actually processed. If our options section of the packet is composed of a single byte (i.e. a truncated option composed of just the option code), the Length variable takes value 1 at [3], so it enters the while block at [4], and at [5] or [6] (depending on the option code we specified in the truncated option), one byte is read past the end of the packet, which is the missing length field of our truncated option.

  // Check the options. The only interested option here is the target-link layer
  // address option.
  //
[3] Length          = Packet->TotalSize - 40;
    Option          = (UINT8 *)(IcmpDest + 1);
    LinkLayerOption = NULL;
[4] while (Length > 0) {
        switch (*Option) {
        case Ip6OptionEtherTarget:

            LinkLayerOption = (IP6_ETHER_ADDR_OPTION *)Option;
[5]         OptLen          = LinkLayerOption->Length;
            if (OptLen != 1) {
                //
                // For Ethernet, the length must be 1.
                //
                goto Exit;
            }

            break;

        default:

[6]         OptLen = *(Option + 1);
            if (OptLen == 0) {
                //
                // A length of 0 is invalid.
                //
                goto Exit;
            }

            break;
    }

    Length -= 8 * OptLen;
    Option += 8 * OptLen;
  }

CVE-2023-45232: Infinite loop when parsing unknown options in the Destination Options header

The function Ip6IsExtsValid in NetworkPkg/Ip6Dxe/Ip6Option.c validates the extension headers that can be found in an incoming IPv6 packet. When dealing with a Destination Options extension header, function Ip6IsOptionValid is called at [1] to validate whatever options are embedded in said Destination Options header:

BOOLEAN
Ip6IsExtsValid (
  IN IP6_SERVICE  *IpSb           OPTIONAL,
  IN NET_BUF      *Packet         OPTIONAL,
  IN UINT8        *NextHeader,
  IN UINT8        *ExtHdrs,
  IN UINT32       ExtHdrsLen,
  IN BOOLEAN      Rcvd,
  OUT UINT32      *FormerHeader   OPTIONAL,
  OUT UINT8       **LastHeader,
  OUT UINT32      *RealExtsLen    OPTIONAL,
  OUT UINT32      *UnFragmentLen  OPTIONAL,
  OUT BOOLEAN     *Fragmented     OPTIONAL
  )
{
  [...]
  while (Offset <= ExtHdrsLen) {
    switch (*NextHeader) {
    [...]
    case IP6_DESTINATION:
        [...]
        Offset++;
        Option    = ExtHdrs + Offset;
        OptionLen = (UINT8)((*Option + 1) * 8 - 2);
        Option++;
        Offset++;

[1]     if ((IpSb != NULL) && (Packet != NULL) && !Ip6IsOptionValid (IpSb, Packet, Option, OptionLen, Offset)) {
          return FALSE;
        }
        [...]

The function Ip6IsOptionValid handles different option types. If it's not one of Pad1, PadN, or RouterAlert, the switch case falls into the default clause at [3]. In that case, the option code is masked with Ip6OptionMask (0xC0) at [4], and if the result of the AND operation is Ip6OptionSkip (0x00), then the Offset variable is updated by adding the value of the Length field of the option at [5]. Notice that it doesn't check if the Length field of the option is 0, in which case the Offset value is never modified, and thus an infinite loop ensues, since execution can never break out of the while loop at [2].

Ip6IsOptionValid (
  IN IP6_SERVICE  *IpSb,
  IN NET_BUF      *Packet,
  IN UINT8        *Option,
  IN UINT8        OptionLen,
  IN UINT32       Pointer
  )
{
    [...]
[2]   while (Offset < OptionLen) {
        OptionType = *(Option + Offset);

        switch (OptionType) {
        case Ip6OptionPad1:
            [...]
            break;
        case Ip6OptionPadN:
            [...]
            break;
        case Ip6OptionRouterAlert:
            [...]
            break;
        [...]
[3]     default:
            //
            // The highest-order two bits specify the action must be taken if
            // the processing IPv6 node does not recognize the option type.
            //
[4]         switch (OptionType & Ip6OptionMask) {
            case Ip6OptionSkip:
[5]             Offset = (UINT8)(Offset + *(Option + Offset + 1));
                break;
            [...]

As a result of this infinite loop, the affected computer never finishes booting up.

CVE-2023-45233: Infinite loop when parsing a PadN option in the Destination Options header

As mentioned in the previous bug, function Ip6IsOptionValid in NetworkPkg/Ip6Dxe/Ip6Option.c is called to validate whatever options are embedded in a Destination Options header within an IPv6 packet. The function Ip6IsOptionValid handles different option types. One of those supported types is PadN ([2]). In that case, the Offset variable is updated by adding the value of the Length field of the PadN option, plus 2 ([3]). If the Length field of the option is 0xFE, then 0x100 (0xFE + 2) will be added to Offset. But the result of that addition is truncated to a UINT8 (the type of the Offset variable), which means that when adding 0x100 to Offset it will remain unmodified, and thus an infinite loop ensues, since execution can never break out of the while loop at [1].

Ip6IsOptionValid (
  IN IP6_SERVICE  *IpSb,
  IN NET_BUF      *Packet,
  IN UINT8        *Option,
  IN UINT8        OptionLen,
  IN UINT32       Pointer
  )
{
    UINT8  Offset;
    [...]
[1]  while (Offset < OptionLen) {
        OptionType = *(Option + Offset);

        switch (OptionType) {
        [...]
[2]     case Ip6OptionPadN:
            //
            // It is a PadN option
            //
[3]         Offset = (UINT8)(Offset + *(Option + Offset + 1) + 2);
            break;

As a consequence of this infinite loop, the affected computer never finishes booting up.

CVE-2023-45234: Buffer overflow when processing DNS Servers option in a DHCPv6 Advertise message

The function PxeBcHandleDhcp6Offer in NetworkPkg/UefiPxeBcDxe/PxeBcDhcp6.c handles DHCPv6 offers made by DHCPv6 servers in response to EDK II Solicit messages. The function allocates a pool buffer with the size given by the OPTION_DNS_SERVERS (0x17) option length at [1], which is controlled by the DHCPv6 server, then at [2] it calls CopyMem with a fixed size: sizeof (EFI_IPv6_ADDRESS), which is 0x10. This means that if the length of the OPTION_DNS_SERVERS option included in the server response is shorter than 0x10, then that leads to a buffer overflow.

PxeBcHandleDhcp6Offer (
  IN PXEBC_PRIVATE_DATA  *Private
  )
{
    [...]
    // First try to cache DNS server address if DHCP6 offer provides.
    //
    if (Cache6->OptList[PXEBC_DHCP6_IDX_DNS_SERVER] != NULL) {
[1]     Private->DnsServer = AllocateZeroPool (NTOHS (Cache6->OptList[PXEBC_DHCP6_IDX_DNS_SERVER]->OpLen));
        if (Private->DnsServer == NULL) {
            return EFI_OUT_OF_RESOURCES;
        }

[2]     CopyMem (Private->DnsServer, Cache6->OptList[PXEBC_DHCP6_IDX_DNS_SERVER]->Data, sizeof (EFI_IPv6_ADDRESS));
  }

CVE-2023-45235: Buffer overflow when handling Server ID option from a DHCPv6 proxy Advertise message

The function PxeBcRequestBootService in NetworkPkg/UefiPxeBcDxe/PxeBcDhcp6.c builds and sends a request to retrieve the boot file, and parses the reply.

EFI_STATUS
PxeBcRequestBootService (
  IN  PXEBC_PRIVATE_DATA  *Private,
  IN  UINT32              Index
  )
{
[...]
    EFI_PXE_BASE_CODE_DHCPV6_PACKET  *Discover;
[...]

[1] Discover = AllocateZeroPool (sizeof (EFI_PXE_BASE_CODE_DHCPV6_PACKET));
    if (Discover == NULL) {
        return EFI_OUT_OF_RESOURCES;
    }

    //
    // Build the request packet by the cached request packet before.
    //
    Discover->TransactionId = IndexOffer->Dhcp6.Header.TransactionId;
    Discover->MessageType   = Request->Dhcp6.Header.MessageType;
    RequestOpt              = Request->Dhcp6.Option;
    DiscoverOpt             = Discover->DhcpOptions;
    DiscoverLen             = sizeof (EFI_DHCP6_HEADER);
    RequestLen              = DiscoverLen;

    //
    // Find Server ID Option from ProxyOffer.
    //
[2] if (Private->OfferBuffer[Index].Dhcp6.OfferType == PxeOfferTypeProxyBinl) {
[3]     Option = PxeBcDhcp6SeekOption (
                   IndexOffer->Dhcp6.Option,
                   IndexOffer->Length - 4,
                   DHCP6_OPT_SERVER_ID
                   );
        if (Option == NULL) {
          return EFI_NOT_FOUND;
        }

      //
      // Add Server ID Option.
      //
      OpLen = NTOHS (((EFI_DHCP6_PACKET_OPTION *)Option)->OpLen);
[4]   CopyMem (DiscoverOpt, Option, OpLen + 4);
      DiscoverOpt += (OpLen + 4);
      DiscoverLen += (OpLen + 4);
    }

At [1] the function allocates a buffer in the pool to hold an EFI_PXE_BASE_CODE_DHCPV6_PACKET structure, which is defined this way in MdePkg/Include/Protocol/PxeBaseCode.h:

///
/// DHCPV6 Packet structure.
///
typedef struct {
  UINT32    MessageType   : 8;
  UINT32    TransactionId : 24;
  UINT8     DhcpOptions[1024];
} EFI_PXE_BASE_CODE_DHCPV6_PACKET;

If the DHCPv6 offer it has received is a proxy one ([2]), then the function searches for the Server ID option contained within said DHCPv6 offer ([3]), and proceeds to copy it to Discover->DhcpOptions (the buffer that was previously allocated in the pool at [1]), blindly trusting the option length as the size of the copy, which is fully controlled by the DHCPv6 server ([4]).

As a result, if the Server ID option of the DHCPv6 proxy offer is longer than 1024 bytes (the size of the DhcpOptions member of the EFI_PXE_BASE_CODE_DHCPV6_PACKET struct), it results in a pool buffer overflow.

CVE-2023-45236: Predictable TCP Initial Sequence Numbers

Predictable TCP Initial Sequence Numbers (ISNs) are known to be a security issue since 1985 (see Morris1985 and Belovin1989) and a practical attack that exploits it was described in 1995 by Laurent Joncheray (Joncheray1995). A real world instance of an attack that exploited weak TCP ISNs to inject data in a TCP session to compromise a system was disclosed in 1995 (Shimomura1995).

Given these and other findings, RFC1948 proposed an algorithm to generate TCP ISNs in a way that prevents the described attack. In 2012 RFC6528 officially updated the TCP protocol specification and the new TCP ISN algorithm achieved the status of Proposed Standard. In August 2022, RFC 9293 a revision of the core TCP specification was published as the new Internet Standard, and the aforementioned algorithm was recommended for the generation of TCP ISNs.

The TCP implementation in Tianocore's EDK II IP stack generates trivially predictable TCP Initial Sequence Numbers and it is therefore vulnerable to TCP session hijack attacks.

The ISN for a new TCP instance is populated in TcpInitTcbLocal by calling TcpGetIss() as shown below in file TcpMisc.c

TcpInitTcbLocal (
  IN OUT TCP_CB  *Tcb
  )
{
  //
  // Compute the checksum of the fixed parts of pseudo header
  //
  if (Tcb->Sk->IpVersion == IP_VERSION_4) {
    Tcb->HeadSum = NetPseudoHeadChecksum (
                     Tcb->LocalEnd.Ip.Addr[0],
                     Tcb->RemoteEnd.Ip.Addr[0],
                     0x06,
                     0
                     );
  } else {
    Tcb->HeadSum = NetIp6PseudoHeadChecksum (
                     &Tcb->LocalEnd.Ip.v6,
                     &Tcb->RemoteEnd.Ip.v6,
                     0x06,
                     0
                     );
  }

> Tcb->Iss    = TcpGetIss (); /** new ISN is populated **/
  Tcb->SndUna = Tcb->Iss;
  Tcb->SndNxt = Tcb->Iss;

  Tcb->SndWl2 = Tcb->Iss;
  Tcb->SndWnd = 536;

The function TcpGetIss simply increments a global counter using a fixed increment as seen below:

TCP_SEQNO
TcpGetIss (
  VOID
  )
{
  mTcpGlobalIss += TCP_ISS_INCREMENT_1;
  return mTcpGlobalIss;
}

The global variable mTcpGlobalIss is initialized to a fixed value in line 23 of TcpMisc.c

TCP_SEQNO  mTcpGlobalIss = TCP_BASE_ISS;

The global variable is also updated with a fixed increment by the timer in TcpTimer.c

VOID
EFIAPI
TcpTickingDpc (
  IN VOID  *Context
  )
{
  LIST_ENTRY  *Entry;
  LIST_ENTRY  *Next;
  TCP_CB      *Tcb;
  INT16       Index;

  mTcpTick++;
  mTcpGlobalIss += TCP_ISS_INCREMENT_2;

The values TCP_BASE_ISS, TCP_ISS_INCREMENT_1 and TCP_ISS_INCREMENT_2 are defined in TcpMain.h:

///
/// The implementation selects the initial send sequence number and the unit to
/// be added when it is increased.
///
#define TCP_BASE_ISS         0x4d7e980b
#define TCP_ISS_INCREMENT_1  2048
#define TCP_ISS_INCREMENT_2  100

Therefore Tianocore's EDK II TCP implementation generates ISNs using fixed increments from a fixed base value and thus is susceptible to TCP session injection and session hijack attacks.

CVE-2023-45237: Use of a Weak PseudoRandom Number Generator

The EDK II IP stack uses a pseudorandom number generator which is defined in Network/Include/Library/NetLIb.h as:

#define NET_RANDOM(Seed)  ((UINT32) ((UINT32) (Seed) * 1103515245UL + 12345) % 4294967295UL)

Throughout the NetworkPkg stack the above macro is used with Seed usually taking the value from the NetRandomInitSeed() function defined in NetworkPkg/Library/DxeNetLib/DxeNetLib.c as follows:

UINT32
EFIAPI
NetRandomInitSeed (
  VOID
  )
{
  EFI_TIME  Time;
  UINT32    Seed;
  UINT64    MonotonicCount;

  gRT->GetTime (&Time, NULL);
  Seed  = (Time.Hour << 24 | Time.Day << 16 | Time.Minute << 8 | Time.Second);
  Seed ^= Time.Nanosecond;
  Seed ^= Time.Year << 7;

  gBS->GetNextMonotonicCount (&MonotonicCount);
  Seed += (UINT32)MonotonicCount;

  return Seed;
}

The above function outputs an integer value based on the platform's clock time at which the function is called and the platform's monotonic counter. The C language idiom NET_RANDOM (NetRandomInitSeed() is used in several NetworkPkg functions to generate supposedly random unique numeric identifiers for various network protocol fields. Generating numbers in such a way does not produce random numbers with a uniform distribution, instead, it produces easily predictable numbers and a biased distribution. Furthermore, the use of the same idiom in different network layers to assign allegedly random values to protocols' fields or objects makes it possible for an external observer to recover the internal state of the generator by obtaining samples of the numbers used in any such network layer.

The idiom is used to generate DNS query IDs in ConstructDNSQuery:

ConstructDNSQuery (
  IN  DNS_INSTANCE  *Instance,
  IN  CHAR8         *QueryName,
  IN  UINT16        Type,
  IN  UINT16        Class,
  OUT NET_BUF       **Packet
  )
{
 ...
  //
  // Fill header
  //
  DnsHeader                    = (DNS_HEADER *)Frag.Bulk;
  DnsHeader->Identification    = (UINT16)NET_RANDOM (NetRandomInitSeed ());
  DnsHeader->Flags.Uint16      = 0x0000;
  DnsHeader->Flags.Bits.RD     = 1;
  DnsHeader->Flags.Bits.OpCode = DNS_FLAGS_OPCODE_STANDARD;
  DnsHeader->Flags.Bits.QR     = DNS_FLAGS_QR_QUERY;
  ...

It is also used to initialize the value of the IPv4 IP ID field in Ip4DriverBindingStart():

//
  // Initialize the IP4 ID
  //
  mIp4Id = (UINT16)NET_RANDOM (NetRandomInitSeed ());

which is simply incremented for each outgoing IPv4 datagram in Ip4Output()

  // Before IPsec process, prepared the IP head.
  // If Ip4Output is transmitting RawData, don't update IPv4 header.
  //
  HeadLen = sizeof (IP4_HEAD) + ((OptLen + 3) & (~0x03));

  if ((IpInstance != NULL) && IpInstance->ConfigData.RawData) {
    RawData = TRUE;
  } else {
    Head->HeadLen = (UINT8)(HeadLen >> 2);
    Head->Id      = mIp4Id++;
    Head->Ver     = 4;
    RawData       = FALSE;
  }

The same idiom is used to initialize the fragment ID for IPv6 fragmentation Extension Headers, to generate DHCP transaction IDs on both Dhcp4 and Dhcp6 code, to obtain ephemeral port numbers for UDP and TCP, and in other parts of the stack.

The security and privacy vulnerabilities arising from the use of a weak pseudorandom number generator in various Internet Protocols are described in RFC 9414 and a number of algorithms to address those issues are suggested in RFC 9415. In this particular case, the use of a weak PRNG could facilitate DNS and DHCP poisoning attacks, information leakage, denial of service, and data insertion attacks at the IPv4 and IPv6 layer (due also to the use of a per datagram unit increment of the corresponding ID fields).

At the root of this issue is the use of a Linear Congruential Generator to generate a sequence of pseudorandom security-sensitive numbers. The unsuitability of LCGs for security-sensitive purposes (where the numbers generated should not be guessable) has been known for decades, for example in Stern 1987.

Updates

As a response to this blogpost, Binarly REsearch added nine corresponding rules to FwHunt, their UEFI analyzer.

Acknowledgements

We would like to thank our colleagues at Quarkslab for providing feedback and reviewing and editing this blog post. We would also like to thank Vijay Sarvepalli of CERT/CC, the team members of CERT-FR, and all the PSIRT and product security coordinators from the multiple vendors that participated in the disclosure process.

Disclosure timeline

Below we include a timeline of all the relevant events during the coordinated vulnerability disclosure process with the intent of providing transparency to the whole process and our actions. The timeline also serves as a detailed example of the complexity of reporting vulnerabilities and coordinating the development and release of security fixes in a complex multi-vendor firmware supply chain.

  • 2023-08-03 Quarkslab sent to CERT/CC a report describing the vulnerabilities and providing proof-of-concept programs to reproduce the first 7 of them. A case was opened in CERT's vulnerability coordination portal. Disclosure deadline is set to November 2nd, 2023.
  • 2023-08-03 CERT/CC made the report in the vulnerability reporting portal available to vendors and set the target disclosure date to November 2nd, 2023.
  • 2023-08-04 Quarkslab opened 9 issues in Google's ChromeOS issue tracker because EDK2 is included as a package in the ChromeOS source code tree.
  • 2023-08-05 Google indicated that EDK2 is not used in production Chromebooks and therefore they are not affected by the vulnerabilities.
  • 2023-08-08 Tianocore opened an issue in their issue tracker.
  • 2023-08-08 Insyde Software requested clarifications about the disclosure date. Quarkslab indicated disclosure is set to November 2nd, 2023, 90 days since the initial report.
  • 2023-08-17 Quarkslab posted on the vulnerability coordination portal a request for a status update and asked if the vulnerabilities have been triaged, if any vendors have confirmed being vulnerable, if there were any estimated dates for fixes, if CVEs have been assigned, and indicated that it did not have access to Tianocore's bug tracking system to see progress on the treatment of the issues.
  • 2023- 08-18 AMI informed that Tianocore's PSIRT lead had retired and explained that once a new one was assigned, a volunteer would pick up the issue. Indicated that November 2nd, 2023 was too optimistic for a deadline and asked if Quarkslab had plans for public disclosure.
  • 2023-08-18 Quarkslab replied that disclosure was potentially planned in talk at an upcoming security conference and that in any case, results of the associated research project had to be published in 2023.
  • 2023-08-18 AMI informed that Tianocore was considering acknowledging vulnerability finders in their public advisories and that to improve responsiveness anyone could contribute fixes.
  • 2023-08-20 Quarkslab encouraged Tianocore's adoption of the policy of acknowledging bug finders as other vendors do and asked if it was suggested that to improve responsiveness Quarkslab should contribute the fixes. Indicated that the suggestion would be considered but since vendors in the Tianocore consortium had 100x to 1000x more engineering capacity and based their commercial UEFI implementations on EDK2, it would be more reasonable for them to develop the fixes and not rely on external contributors.
  • 2023-08-28 AMI wrote that often researchers are well positioned to implement fixes, informed that it opened 9 issues in Tianocore bug tracker to follow each vulnerability individually, and asked if Quarkslab felt it could remediate any.
  • 2023-08-28 Quarkslab replied that the maintainers and developers of EDK2 would be in the best position to develop and test fixes, as they are already familiar with the code and do not have to incur in any setup cost to do it. Quarkslab would rather like that the organizations supporting Tianocore dedicated their vast resources to implement fixes in a timely manner. In Quarkslab's opinion, fixes for vulnerabilities 1 to 7 should be easy to produce quickly, while vulnerabilities 8 and 9 would take more time as they require implementation and use of a new PRNG (possibly using building blocks that already exist in EDK2) in the PXE stack. However, given that these issues have been known and documented for over a quarter of a century, they should be considered public knowledge in any open source network stack.
  • 2023-09-06 Insyde Software asked Quarkslab if the publication date could be postponed by 30 days, as the issues had to be addressed by 4 sets of PSIRT teams (Tianocore, IFVs, ODMs, OEMs).
  • 2023-09-06 Quarkslab agreed to re-schedule disclosure to December 1st, 2023 and said that so far only Insyde Software indicated it was affected and addressing the vulnerabilities.
  • 2023-09-09 Phoenix Technologies indicated that several members of Tianocore's open-source community were working on patches, and the first 7 vulnerabilities would have patches shortly and the other two would take a bit longer. CVEs would be assigned soon.
  • 2023-09-10 Phoenix Technologies shared the CVEs assigned by Tianocore.
  • 2023-09-11 A Tianocore core developer indicated that he was working on making fixes unit-testable.
  • 2023-09-14 CERT/CC updated the target disclosure date to December 1st, 2023.
  • 2023-10-06 AMI indicated that non-validated patches were available on Tianocore's bug tracker.
  • 2023-10-12 Google indicated that it removed the unused EDK2 package from their source code repos. The 9 issues were closed.
  • 2023-10-23 Microsoft asked if it was possible to postpose the Dec. 1, 2023 disclosure date as they would need more time to deploy a complete fix on their cloud infrastructure and depended on some partners for it. Also asked if Quarkslab was to publish the details in a blog post.
  • 2023-10-23 Quarkslab agreed to reschedule disclosure to December 7th, 2023 but reminded that, as stated in August, needed to publish the research work within 2023. It also indicated that since vendors were already progressing towards releasing fixes, it did not think reasonable to delay disclosure much longer, and confirmed that details about the vulnerabilities would be published in a blog post.
  • 2023-10-25 Microsoft said that postponing disclosure one week was immaterial as it would not allow sufficient time to roll out comprehensive fixes to customers. Asked for the specific technical content of the blog post to be published, expressing concerns that exploit code would be included and asked if it was possible not to do so. Indicated that they had confirmed with Tianocore that patches were not finalized yet and were unlikely to be available before the December disclosure date and that they disagreed with Quarkslab's assumption that "vendors were progressing towards releasing fixes". Microsoft asked to hold off disclosure until May 2024.
  • 2023-10-25 A Tianocore core developer requested more time to get products patched and clarified that there were no validated patches on the open source side yet, a first draft had been submitted for review and the expectation was they'd become available in November to Tianocore's infosec community. Patches would then have to be integrated, tested and deployed to customer devices which usually takes months. The core developer referenced a UEFI Forum paper which describes the complex supply chain challenges of the UEFI ecosystem, and indicated that "any public announcement before middle of 2024 would cause significant negative impact."
  • 2023-10-25 Phoenix Technologies expressed strong support for Tianocore's and Microsoft's position and indicated that the ongoing case was mentioned in a podcast (without providing any technical details) to exemplify the difficulties of producing patches and why embargo lengths must be flexible.
  • 2023-10-27 CERT/CC asked Tianocore and vendors to continue working with supply-chain channels and partners towards the disclosure date as it may be difficult for Quarkslab to postpone it since it was also working with other stakeholders.
  • 2023-11-01 A Tianocore core developer indicated that some patches were being unit tested and submitted for validation.
  • 2023-11-07 AMI told Quarkslab that Tianocore considered CVE-2023-45236 and CVE-2023-45237 weaknesses rather than vulnerabilities primarily due to lack of "proven, computationally feasible exploitability" and asked to provide a PoC if it existed.
  • 2023-11-08 Dell asked for confirmation that Quarkslab planned to disclose the vulnerabilities in a blog post.
  • 2023-11-11 Microsoft asked Quarkslab to share a technical draft of the contents that would be published in the blog, reiterated its concerns that it would contain explicit details regarding exploit code and asked for the exploit code not to be added.
  • 2023-11-13 Dell agreed with Microsoft's position that delaying disclosure one week did not make a difference, and said that a later disclosure (May 2024) would be more suitable.
  • 2023-11-13 A Tianocore core developer indicated that patches for the vulnerabilities 1 to 7 were available in a private Pull Request on Tianocore's GitHub repository.
  • 2023-11-13 Microsoft stated that 4 vendors had expressed concerns about the December 12th, 2023 disclosure date and asked CERT/CC to relay their message to Quarkslab.
  • 2023-11-14 Quarkslab responded to AMI's message communicating Tianocore's assessment of CVE-2023-45236 and CVE-2023-45237 as weaknesses rather than vulnerabilities. Quarkslab expressed disagreement with such an assessment stating that CVE-2023-45236 (TCP ISNs with the same algorithm as EDK2) had been exploited in the wild in 1995 and merited a security advisory published by CERT, mitigations were proposed in RFC 1948 in 1996, Microsoft considered it a Critical vulnerability in 1999, motivated another CERT Advisory in 2001 and was fixed again in the Linux kernel in 2011. Over almost 3 decades every major operating system vendor considered it a vulnerability and fixed it as such and therefore Quarkslab did not believe producing an actual exploit was necessary to prove "computationally feasible exploitability". With regards to CVE-2023-45237 (Use of weak PRNG to generate numeric IDs in the network stack) the same long history of vulnerability disclosures and fixes could be pointed out in several components that suffered from the problem, the most known being predictable DNS query IDs, first reported and fixed in 1997 and multiple times afterwards, including the fixes issued in 2008 for almost every DNS resolver and DNS server on the planet (for example in MS08-20). Quarkslab's vulnerability report pointed at RFC 9414 which details the long history of problems associated to generation of numeric IDs in network protocols. At the core of these issues is the use of a Linear Congruential Generator to generate a sequence of pseudorandom numbers. EDK2 uses the same LCG as glibc 2.26 (the exact same parameters). The unsuitability of LCGs for security-sensitive purposes (where the numbers generated should not be guessable) have been known for decades, for example in Stern 1987, therefore Quarkslab did not consider necessary to produce an exploit to prove exploitability.
  • 2023-11-14 Quarkslab replied to the prior requests and commentary from various vendors as follows:
    • Stated that the blog post about the issues would contain the technical report submitted to the disclosure coordination forum and a detailed timeline of the relevant events in the disclosure process. It would include proof-of-concept code to trigger vulnerabilities 1 to 7 but NOT exploit code. Reiterated that the purpose of reporting the vulnerabilities was to help vendors identify and fix them, not to debate about the editorial policies for Quarkslab research work. Nonetheless it was willing to discuss that, as well as the quality and lack of technical information in the security advisories and bulletins published by vendors, at an appropriate venue or in a different context.
    • Indicated that the Quarkslab's statement of October 23rd ("vendors were progressing towards releasing fixes") was based not on opinion but the factual evidence of vendor posts on the vulnerability coordination portal.
    • Strongly disagreed with the opinion expressed by a vendor that "any public announcement before middle of 2024 would cause significant negative impact" and argued that what had significant negative impact was the risk those vulnerabilities were posing to the uninformed users and organizations that run EDK2-derived firmware implementations with the vulnerable NetworkPkg component, and that informing them of their exposure and giving them the necessary information to either fix the vulnerabilities (ideally with official patches), to mitigate them, or to be able to detect active attacks was actually the opposite of having "significant negative impact".
    • Indicated that 3 months after the initial report date not a single vendor had committed to an estimated release date for fixes. The original 90-day embargo period was extended to 120 days upon request from a vendor (Insyde Software) and at the time (2 months prior) no vendor objected the new deadline. Yet only 2 weeks ago one vendor proposed to postpone the disclosure deadline for 6 months with no actual concrete release date or timeline for fixes.
    • Commented that the referenced UEFI Forum paper provides a good description of the complexity of the UEFI firmware supply chain but it argued for a 300-day embargo period by presenting an exceptional case as if it was the general one, and assuming that the existing series of interlocked, extremely waterfall-oriented, software development lifecycles of all stakeholders should explain the need of such period. Quarkslab noted that in August 2023 the US Cybersecurity & Infrastructure Security Agency (CISA) published A Call to Action: Bolster UEFI Cybersecurity Now calling for UEFI vulnerability response, including timely and effective update mechanisms.
    • Summarized its position with the following: Quarkslab was being asked to extend the embargo period from 120 days (which had already been extended from the original 90) to about 10 months counting from initial report date, without any firm commitment from any vendor to an actual release date and an opaque timeline. Delaying disclosure implies keeping Quarkslab's partners and customers at risk and in the dark about the vulnerabilities, which in turn put the company in a very uncomfortable position where malicious activities or cyberattacks could happen during that extended time frame. Because of the above, a priori was not inclined to extend our embargo period but since Quarkslab is a company incorporated in France, it decided to consult with CERT-FR (the national CERT of France operated by ANSSI, the French cybersecurity agency) to determine how to proceed. Precise status updates and firm commitment to a release date from affected vendors would be useful to inform any decision.
  • 2023-11-23 Quarkslab contacted CERT-FR and provided the technical report of the vulnerabilities and a summary of the coordinated vulnerability disclosure timeline so far.
  • 2023-11-27 CERT-FR acknowledged the report and summary timeline and requested further discussion in a conference call.
  • 2023-11-27 Quarkslab informed all stakeholder that it was discussing with CERT-FR and in the meantime postponed disclosure to December 12th, 2023. Reiterated that status updates and firm commitment to a release date from affected vendors would be useful to inform any decision.
  • 2023-11-27 CERT/CC updated the target disclosure date to December 12th, 2023.
  • 2023-12-06 Quarkslab indicated that at a week before the disclosure date it still did not have any status update from any vendor nor commitments to any release date for fixes.
  • 2023-12-06 Insyde Software submitted a status update and flagged it as public.
  • 2023-12-06 AMI updated their status and indicated the PSIRT activities including advertisement and remediation have been on-going with customers. An advisory would be issued on December 12th, 2023.
  • 2023-12-06 Microsoft updated the status and indicated internal discussions regarding fix commitment timelines were still ongoing.
  • 2023-12-09 Quarkslab indicated that it was still coordinating with CERT-FR and CERT/CC and decided to postpone disclosure one week, to December 19th, 2023 to have a bit more time to prepare and converge on dissemination plans.
  • 2023-12-09 CERT/CC updated the target disclosure date to December 19th, 2023.
  • 2023-12-12 Microsoft asked Quarkslab to confirm that it would publish in the blog post the same report as provided in the forum. Asked to for a preview of Quarkslab post.
  • 2023-12-12 Quarkslab replied that it already confirmed that the publication will be the same as the report provided to vendors in the vulnerability coordination portal and that it would include a detailed timeline of the relevant communications towards coordinated disclosure. Reiterated that Quarkslab participation in the portal was to coordinate with vendors the release of patches not to discuss the contents of Quarkslab's publications. Asked for Microsoft's estimated date to release fixes, affected products or services, and whether Azure would be exposed.
  • 2023-12-14 CERT-FR asked Quarkslab to clarify circumstances in which the vulnerabilities could be exploited from remote networks.
  • 2023-12-15 Quarkslab described scenarios in which some vulnerabilities could be exploited from remote networks.
  • 2023-12-15 Quarkslab wrote that it discussed the disclosure date with CERT-FR and CERT/CC and CERT-FR indicated they'd like to have additional time to coordinate dissemination of the information to their stakeholders and to other national CERTs. All parties agreed that it would be useful to postpone publication past the end of the year, as many organizations impose a "freeze" on any changes to their data centers and compute infrastructure during the end-of-year period. In view of that it was decided to postpone disclosure to January 15th, 2024. However, if an attack leveraging any of the reported vulnerabilities was discovered in the wild or public disclosure of any of them was observed before the embargo date, Quarkslab would immediately publish the technical report describing the vulnerabilities.
  • 2023-12-15 CERT/CC updated the target disclosure date to January 15th, 2024.
  • 2023-12-27 CERT-FR asked CERT/CC and Quarkslab if they agreed that CERT-FR would send an early warning to national CERTs before January 15th, 2024.
  • 2023-12-27 Quarkslab agreed to sending an early warning on January 11th, 2024 or any earlier date of CERT-FR's choosing.
  • 2023-12-27 CERT/CC agreed to an early warning on January 11th, 2024 as well.
  • 2024-01-08 CERT-FR requested an estimated release date for fixes from several vendors.
  • 2024-01-08 Insyde Software informed that it had already deployed fixes to their downstream supply chain.
  • 2024-01-08 CERT/CC updated the target disclosure date to January 16th, 2024 as January 15th is a non-working day in the USA.
  • 2024-01-09 AMI informed that it already communicated remediation information to its customers.
  • 2024-01-09 Phoenix Technologies informed that it updated its status to vulnerable and was delivering fixes to partner OEMs.
  • 2024-01-16 Quarkslab blog post published.

If you would like to learn more about our security audits and explore how we can help you, get in touch with us!