Security of the Intel Graphics Stack - Part 2 - FW <-> GuC

33 minute read

Today we'll continue our voyage into the graphics subsystem components.

The question we'll try to answer is what kind of communications occur between the GuC and the rest of the system. In this post we'll look at firmware components and next post at Windows components.

For a reminder what the GuC is, look at part1 post .

Part 1: The IntelGOP DXE driver

The Intel Graphics Output Protocol (GOP) EFI DXE driver can be extracted in various versions from many UEFI capsules available through many vendors. For this post I redid my original analysis on a recent version from a CanonLake system.

The purpose of this exercise is to try and see whether the GOP driver communicates with the GuC over the PCIe bus (TL;dr: it doesn't)

The binary isn't to large - 84KB, so we can try to completely reverse engineer it. I used both IDA+HexRays and a dynamic analysis UEFI emulator I developed for just these cases. The emulator lets you run EFI DXE drivers in Windows simulating many UEFI services and allowing me to modify/inspect EFI interfaces, hook UEFI protocol structs, and even has some fuzzing capabilities.

Looking at the driver's entrypoint we see it stores the different service tables in globals and then jumps to the main() functions I called GopEntryPoint().

.text:0000000000001580 ; EFI_STATUS __fastcall ModuleEntryPoint(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
.text:0000000000001580                 public _ModuleEntryPoint
.text:0000000000001580 _ModuleEntryPoint proc near             ; DATA XREF: HEADER:00000000000000E8↑o
.text:0000000000001580                 sub     rsp, 28h
.text:0000000000001584                 mov     r8, [rdx+60h]
.text:0000000000001588                 mov     rax, [rdx+58h]
.text:000000000000158C                 mov     cs:gIMAGE_HANDLE, rcx
.text:0000000000001593                 mov     cs:gBOOT_SERVICES, r8
.text:000000000000159A                 mov     cs:gRUNTIME_SERVICES, rax
.text:00000000000015A1                 mov     cs:gBOOT_SERVICES2, r8
.text:00000000000015A8                 mov     cs:gSYSTEM_TABLE2, rdx
.text:00000000000015AF                 call    GopEntryPoint
.text:00000000000015B4                 add     rsp, 28h
.text:00000000000015B8                 retn
.text:00000000000015B8 _ModuleEntryPoint endp

GopEntryPoint() first part is really boring, just setting up version information in global strings.

_int64 __fastcall GopEntryPoint(EFI_HANDLE img_handle_arg)
  EFI_HANDLE image_handle; // rbx
  CHAR16 *driver_desc_ptr; // rax
  __int64 img_handle; // r11
  __int64 result; // rax
  EFI_HANDLE Handle; // [rsp+50h] [rbp+18h]
  EFI_LOADED_IMAGE_PROTOCOL *Interface; // [rsp+58h] [rbp+20h]

  image_handle = img_handle_arg;
  v2 = atoi(L"0") == 1;
  driver_desc_ptr = gDriverDescription;
  v4 = 'I';
  byte_142A0 = v2;
    *driver_desc_ptr = v4;
    v4 = *(CHAR16 *)((char *)driver_desc_ptr + (char *)L"Intel(R) GOP Driver" - (char *)gDriverDescription);
  while ( v4 );
  *driver_desc_ptr = 0;
  strcat(gDriverDescription, L" [");
  strcat(gDriverDescription, L"11");
  strcat(gDriverDescription, L".");
  strcat(gDriverDescription, L"0");
  strcat(gDriverDescription, L".");
  strcat(gDriverDescription, L"1014");
  strcat(gDriverDescription, L"]");
  gDriverState.ImgHandle = img_handle;
  v12 = &gDriverVersion;
  v13 = '1';
    *v12 = v13;
    v13 = *(CHAR16 *)((char *)v12 + (char *)L"11" - (char *)&gDriverVersion);
  while ( v13 );
  *v12 = 0;
  strcat(&gDriverVersion, L".");
  strcat(&gDriverVersion, L"0");
  strcat(&gDriverVersion, L".");
  strcat(&gDriverVersion, L"1014");
  gDriverState.ControllerName = (__int64)L"Intel(R) Graphics Controller";
  gDriverState.DriverVersion = v17;
  v18 = atoi(L"1014");

The second part does the actual work. First it looks for the EFI_LOADED_IMAGE_PROTOCOL to setup a the unload routine:

  gDRIVER_BINDING_PROTOCOL.Version = v18 + v19;
  result = gBOOT_SERVICES->OpenProtocol(
             (void **)&Interface,
  if ( result >= 0 )
    Interface->Unload = (EFI_IMAGE_UNLOAD)UnloadImage;

And then install four protocol handlers, three of which I identified: one for driver binding and two for component name handling. The InstallMultipleProtocolInterfaces(..) can accept multiple protocols, each protocol has a GUID and the “virtual table” like structure used by UEFI. The final entry is NULL. Most UEFI protocol GUIDs are public (and appear in the EDK) so we can identify them easily and this identify the virtual table structures associated with them, for example for the UEFI binding protocol we have in DriverBinding.h:




	IN EFI_HANDLE ControllerHandle,

	IN EFI_HANDLE ControllerHandle,

	IN EFI_HANDLE ControllerHandle,
	IN UINTN NumberOfChildren,

	UINT32 Version;
	EFI_HANDLE ImageHandle;
	EFI_HANDLE DriverBindingHandle;

This enables us to reverse the rest of GopEntryPoint:

    Handle = image_handle;
    gDRIVER_BINDING_PROTOCOL.DriverBindingHandle = Handle;
    gDRIVER_BINDING_PROTOCOL.ImageHandle = image_handle;
    result = gBOOT_SERVICES->InstallMultipleProtocolInterfaces(
    if ( result >= 0 )
      qword_142B0 = (__int64)image_handle;
  return result;

All the GUID values appear close to each other at the beginning of the binary, so we can take a shortcut and find all the GUIDs the driver uses:

.text:0000000000000240 EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID dd 9042A9DEh            ; Data1
.text:0000000000000240                                         ; DATA XREF: HEADER:00000000000000EC↑o
.text:0000000000000240                                         ; HEADER:00000000000001D4↑o ...
.text:0000000000000240                 dw 23DCh                ; Data2
.text:0000000000000240                 dw 4A38h                ; Data3
.text:0000000000000240                 db 96h, 0FBh, 7Ah, 0DEh, 0D0h, 80h, 51h, 6Ah; Data4
.text:0000000000000250 EFI_EDID_ACTIVE_PROTOCOL_GUID dd 0BD8C1056h           ; Data1
.text:0000000000000250                                         ; DATA XREF: InstallGraphicsProto+124↓o
.text:0000000000000250                                         ; uninstall2?+9B↓o ...
.text:0000000000000250                 dw 9F36h                ; Data2
.text:0000000000000250                 dw 44ECh                ; Data3
.text:0000000000000250                 db 92h, 0A8h, 0A6h, 33h, 7Fh, 81h, 79h, 86h; Data4
.text:0000000000000260 EFI_EDID_DISCOVERED_PROTOCOL_GUID dd 1C0C34F6h            ; Data1
.text:0000000000000260                                         ; DATA XREF: sub_1CA4+2A5↓o
.text:0000000000000260                                         ; InstallGraphicsProto+DF↓o ...
.text:0000000000000260                 dw 0D380h               ; Data2
.text:0000000000000260                 dw 41FAh                ; Data3
.text:0000000000000260                 db 0A0h, 49h, 8Ah, 0D0h, 6Ch, 1Ah, 66h, 0AAh; Data4
.text:0000000000000270 GOP_DISPLAY_BRIGHTNESS_PROTOCOL_GUID dd 6FF23F1Dh            ; Data1
.text:0000000000000270                                         ; DATA XREF: sub_1F78+B1↓o
.text:0000000000000270                                         ; uninstall2?+14B↓o ...
.text:0000000000000270                 dw 877Ch                ; Data2
.text:0000000000000270                 dw 4B1Bh                ; Data3
.text:0000000000000270                 db 93h, 0FCh, 0F1h, 42h, 0B2h, 0EEh, 0A6h, 0A7h; Data4
.text:0000000000000280 GOP_DISPLAY_BIST_PROTOCOL_GUID dd 0F51DD33Ah           ; Data1
.text:0000000000000280                                         ; DATA XREF: sub_1F78+75↓o
.text:0000000000000280                                         ; uninstall2?+F5↓o ...
.text:0000000000000280                 dw 0E57Fh               ; Data2
.text:0000000000000280                 dw 4020h                ; Data3
.text:0000000000000280                 db 0B4h, 66h, 0F4h, 0C1h, 71h, 0C6h, 0E4h, 0F7h; Data4
.text:0000000000000290 EFI_PCI_IO_PROTOCOL_GUID dd 4CF5B200h            ; Data1
.text:0000000000000290                                         ; DATA XREF: DriverBindingProtoSupported+CB↓o
.text:0000000000000290                                         ; DriverBindingProtoSupported+173↓o ...
.text:0000000000000290                 dw 68B8h                ; Data2
.text:0000000000000290                 dw 4CA5h                ; Data3
.text:0000000000000290                 db 9Eh, 0ECh, 0B2h, 3Eh, 3Fh, 50h, 2, 9Ah; Data4
.text:00000000000002A0 GOP_COMPONENT_NAME2_PROTOCOL_GUID dd 651B7EBDh            ; Data1
.text:00000000000002A0                                         ; DATA XREF: GopEntryPoint+22F↓o
.text:00000000000002A0                 dw 0CE13h               ; Data2
.text:00000000000002A0                 dw 41D0h                ; Data3
.text:00000000000002A0                 db 82h, 0E5h, 0A0h, 63h, 0ABh, 0BEh, 9Bh, 0B6h; Data4
.text:00000000000002B0 UNKNOWN_PROTOCOL_GUID dd 0DBCB2FCDh           ; Data1
.text:00000000000002B0                                         ; DATA XREF: UnloadImage+9A↓o
.text:00000000000002B0                                         ; GopEntryPoint+203↓o
.text:00000000000002B0                 dw 0E29Ah               ; Data2
.text:00000000000002B0                 dw 410Eh                ; Data3
.text:00000000000002B0                 db 9Dh, 0D9h, 0FAh, 9Dh, 5Fh, 0F4h, 0CDh, 0A7h; Data4
.text:00000000000002C0 MAYBE_AUX_PROTOCOL_GUID? dd 0C7D4703Bh           ; Data1
.text:00000000000002C0                                         ; DATA XREF: DriverBindingProtoStartImp+2A8↓o
.text:00000000000002C0                                         ; DriverBindingProtoStop+70↓o
.text:00000000000002C0                 dw 0F36h                ; Data2
.text:00000000000002C0                 dw 4E51h                ; Data3
.text:00000000000002C0                 db 0A9h, 83h, 5Eh, 61h, 0ACh, 0B8h, 68h, 3Ch; Data4
.text:00000000000002D0 EFI_DEVICE_PATH_PROTOCOL_GUID dd 9576E91h             ; Data1
.text:00000000000002D0                                         ; DATA XREF: DriverBindingProtoSupported+5F↓o
.text:00000000000002D0                                         ; DriverBindingProtoSupported+A2↓o ...
.text:00000000000002D0                 dw 6D3Fh                ; Data2
.text:00000000000002D0                 dw 11D2h                ; Data3
.text:00000000000002D0                 db 8Eh, 39h, 0, 0A0h, 0C9h, 69h, 72h, 3Bh; Data4
.text:00000000000002E0 EFI_LOADED_IMAGE_PROTOCOL_GUID dd 5B1B31A1h            ; Data1
.text:00000000000002E0                                         ; DATA XREF: GopEntryPoint+169↓o
.text:00000000000002E0                 dw 9562h                ; Data2
.text:00000000000002E0                 dw 11D2h                ; Data3
.text:00000000000002E0                 db 8Eh, 3Fh, 0, 0A0h, 0C9h, 69h, 72h, 3Bh; Data4
.text:00000000000002F0 EFI_DRIVER_BINDING_PROTOCOL_GUID dd 18A031ABh            ; Data1
.text:00000000000002F0                                         ; DATA XREF: UnloadImage+BB↓o
.text:00000000000002F0                                         ; GopEntryPoint+1D2↓o
.text:00000000000002F0                 dw 0B443h               ; Data2
.text:00000000000002F0                 dw 4D1Ah                ; Data3
.text:00000000000002F0                 db 0A5h, 0C0h, 0Ch, 9, 26h, 1Eh, 9Fh, 71h; Data4
.text:0000000000000300 EFI_COMPONENT_NAME2_PROTOCOL_GUID dd 6A7A5CFFh            ; Data1
.text:0000000000000300                                         ; DATA XREF: UnloadImage+A1↓o
.text:0000000000000300                                         ; GopEntryPoint+1B8↓o
.text:0000000000000300                 dw 0E8D9h               ; Data2
.text:0000000000000300                 dw 4F70h                ; Data3
.text:0000000000000300                 db 0BAh, 0DAh, 75h, 0ABh, 30h, 25h, 0CEh, 14h; Data4

A few couldn't be identified. Another "fast forward" trick I can use is to find all locations protocols are installed or requested. If we look at how protocols are installed using gBOOT_SERVICES::InstallMultipleProtocolInterfaces:

.text:0000000000002938 FF 90 48 01 00 00                 call    qword ptr dword_148[rax]

We see the offset is pretty large, 0x148. We can just search for the wildcard "call qword ptr dword_148[reg]" and see if reg contains the global gBOOT_SERVICES. This way we can jump directly to the functions and identify what they do and name them:

Address	Function	Instruction
.text:000000000000188B	GopEntryPoint	                    FF 90 48 01 00 00                 call    [rax+EFI_BOOT_SERVICES.InstallMultipleProtocolInterfaces]
.text:00000000000018C3	GopEntryPoint	                    FF 90 48 01 00 00                 call    [rax+EFI_BOOT_SERVICES.InstallMultipleProtocolInterfaces]
.text:00000000000018E8	GopEntryPoint	                    FF 90 48 01 00 00                 call    [rax+EFI_BOOT_SERVICES.InstallMultipleProtocolInterfaces]
.text:0000000000001ECC	EnumConnectionsAndInstallEdidProto	FF 90 48 01 00 00                 call    [rax+EFI_BOOT_SERVICES.InstallMultipleProtocolInterfaces]
.text:0000000000001F50	EnumConnectionsAndInstallEdidProto	FF 90 48 01 00 00                 call    [rax+EFI_BOOT_SERVICES.InstallMultipleProtocolInterfaces]
.text:0000000000001FFA	InstallBrightnessProto	            FF 90 48 01 00 00                 call    [rax+EFI_BOOT_SERVICES.InstallMultipleProtocolInterfaces]
.text:0000000000002036	InstallBrightnessProto              FF 90 48 01 00 00                 call    [rax+EFI_BOOT_SERVICES.InstallMultipleProtocolInterfaces]
.text:000000000000221F	InstallGraphicsProto	            FF 90 48 01 00 00                 call    [rax+EFI_BOOT_SERVICES.InstallMultipleProtocolInterfaces]
.text:00000000000022A0	InstallGraphicsProto             	FF 90 48 01 00 00                 call    [rax+EFI_BOOT_SERVICES.InstallMultipleProtocolInterfaces]
.text:0000000000002938	DriverBindingProtoStartImp        	FF 90 48 01 00 00                 call    [rax+EFI_BOOT_SERVICES.InstallMultipleProtocolInterfaces]

This also gets as all the function tables for these protocols, and helps us understand the global state struct for the driver. Unlike C++, the UEFI function receive a This pointer that contains both data members and function pointers, for example for the GOP protocol:

    IN UINTN SourceX, IN UINTN SourceY,
    IN UINTN DestinationX, IN UINTN DestinationY,
    IN UINTN Width, IN UINTN Height,

typedef struct {
    UINT32 MaxMode;
    UINT32 Mode;
    UINTN SizeOfInfo;
    UINTN FrameBufferSize;


So the protocol structure has to be stored in some state structure. If the state structure is a singleton it can be stored as a global, but if we want multiple copies the driver allocates a state structure, places the protocol structure in a known offset within, and then can calculate the start of the structure from the This pointer provided to the protocol functions. We can use this information to try to piece together this global structre:

00000000 DriverState     struc ; (sizeof=0xE8, mappedto_92)
00000000                                         ; XREF: .text:gDriverState/r
00000000 language        dq ?                    ; offset
00000008 ImgHandle       dq ?                    ; XREF: GopEntryPoint+A9/w
00000010 field_10        dd ?
00000014 field_14        dd ?
00000018 graphics_proto  dq ?
00000020 field_20        dq ?                    ; XREF: GetDriverVersion+16/o
00000028 DriverVersion   dq ?                    ; XREF: GopEntryPoint+125/w
00000030 field_30        dq ?
00000038 active_proto_copy dq ?
00000040 field_40        dq ?                    ; XREF: GetControllerName+99/o
00000048 ControllerName  dq ?                    ; XREF: GopEntryPoint+11E/w
00000050 field_50        dq ?
00000058 field_58        dq ?
00000060 brightness_proto dq ?                   ; XREF: UnloadImage+8E/o
00000060                                         ; GopEntryPoint+1EE/o
00000068 name_proto      dq ?
00000070 bist_proto_orig GOP_DISPLAY_BIST_PROTOCOL_FUNC_TABLE ?
00000070                                         ; XREF: InstallBrightnessProto+50/o
00000080 bist_proto      GOP_DISPLAY_BIST_PROTOCOL ?
00000080                                         ; XREF: sub_44D8+21/o
00000080                                         ; sub_44D8+28/w ...
00000094 field_94        dd ?
00000098 field_98        dq ?                    ; XREF: sub_4900+24/o
00000098                                         ; sub_4900+2F/w ...
000000A0 field_A0        dq ?                    ; XREF: sub_4900+36/w
000000A0                                         ; sub_4900+319/r ...
000000A8 field_A8        dq ?                    ; XREF: sub_245C+14/r
000000A8                                         ; sub_245C+1B/o ...
000000B0 field_B0        dq ?                    ; XREF: sub_245C+86/r
000000B0                                         ; sub_259C+6C/r ...
000000B8 field_B8        dq ?                    ; XREF: sub_35A4+37A/o
000000B8                                         ; sub_35A4+384/w ...
000000C0 field_C0        dq ?                    ; XREF: sub_35A4+38B/w
000000C0                                         ; sub_35A4+3EF/r ...
000000C8 field_C8        dq ?
000000D0 field_D0        dq ?                    ; XREF: sub_35A4+420/o
000000D8 field_D8        dq ?
000000E0 field_E0        dq ?
000000E8 DriverState     ends

and so on.

It won't be too interesting to just dump more and more dissassembled functions here, as our goal is to find possible access to GuC. None of the functions I identified had any connection to the GuC, so next I looked at all accesses to PCI devices, as GuC accesses should be made using PCI. The devices are identified using EFI_DEVICE_PATH_PROTOCOL and accessed through EFI_PCI_IO_PROTOCOL_GUID.

DriverBindingProtoSupported+CB lea rdx, EFI_PCI_IO_PROTOCOL_GUID
DriverBindingProtoSupported+173 lea rdx, EFI_PCI_IO_PROTOCOL_GUID
EnumConnectionsAndInstallEdidProto+259 lea rdx, EFI_PCI_IO_PROTOCOL_GUID
sub_245C+9C lea r8, EFI_PCI_IO_PROTOCOL_GUID
sub_259C+33 lea r8, EFI_PCI_IO_PROTOCOL_GUID
sub_259C+81 lea r8, EFI_PCI_IO_PROTOCOL_GUID
DriverBindingProtoStartImp+44 lea rdx, EFI_PCI_IO_PROTOCOL_GUID
DriverBindingProtoStartImp+20C lea rdx, EFI_PCI_IO_PROTOCOL_GUID
uninstall?+76 lea rdx, EFI_PCI_IO_PROTOCOL_GUID
uninstall?+220 lea rdx, EFI_PCI_IO_PROTOCOL_GUID
DriverBindingProtoStop+DD lea rdx, EFI_PCI_IO_PROTOCOL_GUID
DriverBindingProtoStop+120 lea rdx, EFI_PCI_IO_PROTOCOL_GUID
sub_2EC0+158 lea r8, EFI_PCI_IO_PROTOCOL_GUID
GetControllerName+3A lea rdx, EFI_PCI_IO_PROTOCOL_GUID
GetControllerName+59 lea rdx, EFI_PCI_IO_PROTOCOL_GUID
GetControllerName:loc_55C6 lea r8, EFI_PCI_IO_PROTOCOL_GUID

Some places are spurios, like:

.text:00000000000024F8                 lea     r8, EFI_PCI_IO_PROTOCOL_GUID
.text:00000000000024FF                 mov     rcx, rsi
.text:0000000000002502                 call    sub_5F04

Since sub_5F04 overrides r8 immediatly:

.text:0000000000005F04 sub_5F04        proc near               ; CODE XREF: sub_245C+A6↑p
.text:0000000000005F04                                         ; sub_259C+41↑p ...
.text:0000000000005F04 count           = qword ptr -18h
.text:0000000000005F04 arg_0           = qword ptr  8
.text:0000000000005F04 proto_info      = qword ptr  20h
.text:0000000000005F04                 mov     [rsp+arg_0], rbx
.text:0000000000005F09                 push    rdi
.text:0000000000005F0A                 sub     rsp, 30h
.text:0000000000005F0E                 mov     rax, cs:gBOOT_SERVICES
.text:0000000000005F15                 mov     rdi, rdx
.text:0000000000005F18                 lea     r9, [rsp+38h+count]
.text:0000000000005F1D                 lea     r8, [rsp+38h+proto_info]      ;; HERE!!

Long story short: no code in the GOP DXE driver communicates with the GuC.

Before moving on to CSME vs GuC, I was curious who exactly uses all these protocols, in the rest of the UEFI BIOS and Windows. I extracted the UEFI capsule and also mounted the Windows ISO and WIM files (dism /mount-image /imagefile:e:\sources\install.wim /index:1 /mountdir:c:\mnt\install /readonly), and then ran the following python script:

from struct import unpack
from os import walk
from mmap import mmap, ACCESS_READ
import os.path as path

((0xDE, 0xA9, 0x42, 0x90, 0xDC, 0x23, 0x38, 0x4A, 0x96, 0xFB, 0x7A, 0xDE, 0xD0, 0x80, 0x51, 0x6A), 'EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID'),
((0x56, 0x10, 0x8C, 0xBD, 0x36, 0x9F, 0xEC, 0x44, 0x92, 0xA8, 0xA6, 0x33, 0x7F, 0x81, 0x79, 0x86), 'EFI_EDID_ACTIVE_PROTOCOL_GUID'),
((0xF6, 0x34, 0x0C, 0x1C, 0x80, 0xD3, 0xFA, 0x41, 0xA0, 0x49, 0x8A, 0xD0, 0x6C, 0x1A, 0x66, 0xAA), 'EFI_EDID_DISCOVERED_PROTOCOL_GUID'),
((0x1D, 0x3F, 0xF2, 0x6F, 0x7C, 0x87, 0x1B, 0x4B, 0x93, 0xFC, 0xF1, 0x42, 0xB2, 0xEE, 0xA6, 0xA7), 'GOP_DISPLAY_BRIGHTNESS_PROTOCOL_GUID'),
((0x3A, 0xD3, 0x1D, 0xF5, 0x7F, 0xE5, 0x20, 0x40, 0xB4, 0x66, 0xF4, 0xC1, 0x71, 0xC6, 0xE4, 0xF7), 'GOP_DISPLAY_BIST_PROTOCOL_GUID'),
#((0x00, 0xB2, 0xF5, 0x4C, 0xB8, 0x68, 0xA5, 0x4C, 0x9E, 0xEC, 0xB2, 0x3E, 0x3F, 0x50, 0x02, 0x9A), 'EFI_PCI_IO_PROTOCOL_GUID'),
#((0xBD, 0x7E, 0x1B, 0x65, 0x13, 0xCE, 0xD0, 0x41, 0x82, 0xE5, 0xA0, 0x63, 0xAB, 0xBE, 0x9B, 0xB6), 'GOP_COMPONENT_NAME2_PROTOCOL_GUID'),
((0xCD, 0x2F, 0xCB, 0xDB, 0x9A, 0xE2, 0x0E, 0x41, 0x9D, 0xD9, 0xFA, 0x9D, 0x5F, 0xF4, 0xCD, 0xA7), 'UNKNOWN_PROTOCOL_GUID'),
((0x3B, 0x70, 0xD4, 0xC7, 0x36, 0x0F, 0x51, 0x4E, 0xA9, 0x83, 0x5E, 0x61, 0xAC, 0xB8, 0x68, 0x3C), 'MAYBE_AUX_PROTOCOL_GUID?'),
#((0x91, 0x6E, 0x57, 0x09, 0x3F, 0x6D, 0xD2, 0x11, 0x8E, 0x39, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B), 'EFI_DEVICE_PATH_PROTOCOL_GUID'),
#((0xA1, 0x31, 0x1B, 0x5B, 0x62, 0x95, 0xD2, 0x11, 0x8E, 0x3F, 0x00, 0xA0, 0xC9, 0x69, 0x72, 0x3B), 'EFI_LOADED_IMAGE_PROTOCOL_GUID'),
#((0xAB, 0x31, 0xA0, 0x18, 0x43, 0xB4, 0x1A, 0x4D, 0xA5, 0xC0, 0x0C, 0x09, 0x26, 0x1E, 0x9F, 0x71), 'EFI_DRIVER_BINDING_PROTOCOL_GUID'),
#((0xFF, 0x5C, 0x7A, 0x6A, 0xD9, 0xE8, 0x70, 0x4F, 0xBA, 0xDA, 0x75, 0xAB, 0x30, 0x25, 0xCE, 0x14), 'EFI_COMPONENT_NAME2_PROTOCOL_GUID')

guids = { bytes(k) : v for k, v in GUIDS }
first_dwords = set([unpack("<I", guid[0:4]) for guid in guids.keys()])

for root in ('c:\\mnt\\iso', 'c:\\mnt\\boot', 'c:\\mnt\\install', 'c:\\mnt\\uefi'):
    for dir, _, files in walk(root):
        for file in files:
            filename = dir + '\\' + file
                filelen = path.getsize(filename) & ~15
                if filelen == 0:
                with open(filename, 'rb') as file:
                    with mmap(file.fileno(), filelen, access=ACCESS_READ) as mem:
                        for ofs in range(0, filelen, 16):
                            if unpack("<I", mem[ofs:ofs+4]) in first_dwords:
                                guid = mem[ofs:ofs+16]
                                    name = guids[guid]
                                except KeyError:
            except PermissionError:

The UEFI setup and legacy components use the GOP and the EDID components:

c:\mnt\uefi\\ConSplitter.efi	310	EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID
c:\mnt\uefi\\CsmVideo.efi	2c0	EFI_EDID_DISCOVERED_PROTOCOL_GUID
c:\mnt\uefi\\CsmVideo.efi	2d0	EFI_EDID_ACTIVE_PROTOCOL_GUID
c:\mnt\uefi\\CsmVideo.efi	320	EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID
c:\mnt\uefi\\GraphicsConsole.efi	2b0	EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID
c:\mnt\uefi\\Setup.efi	2e0	EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID
c:\mnt\uefi\\UefiPxeBcDxe.efi	490	EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID

In Windows we have only:

c:\mnt\boot\Windows\Boot\EFI\bootmgfw.efi       a1a0    EFI_EDID_ACTIVE_PROTOCOL_GUID
c:\mnt\boot\Windows\Boot\EFI\bootmgfw.efi       a220    EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID
c:\mnt\boot\Windows\System32\winload.efi        17e210  EFI_EDID_ACTIVE_PROTOCOL_GUID
c:\mnt\boot\Windows\System32\winload.efi        17e2a0  EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID
c:\mnt\boot\Windows\System32\winresume.efi      122c00  EFI_EDID_ACTIVE_PROTOCOL_GUID
c:\mnt\boot\Windows\System32\winresume.efi      122c80  EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID
c:\mnt\boot\Windows\System32\Boot\winload.efi   17e210  EFI_EDID_ACTIVE_PROTOCOL_GUID
c:\mnt\boot\Windows\System32\Boot\winload.efi   17e2a0  EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID
c:\mnt\boot\Windows\System32\Boot\winresume.efi 122bf0  EFI_EDID_ACTIVE_PROTOCOL_GUID
c:\mnt\boot\Windows\System32\Boot\winresume.efi 122c70  EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID
c:\mnt\install\Windows\Boot\EFI\bootmgfw.efi    a1a0    EFI_EDID_ACTIVE_PROTOCOL_GUID
c:\mnt\install\Windows\Boot\EFI\bootmgfw.efi    a220    EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID
c:\mnt\install\Windows\System32\SecConfig.efi   110b80  EFI_EDID_ACTIVE_PROTOCOL_GUID
c:\mnt\install\Windows\System32\SecConfig.efi   110c00  EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID
c:\mnt\install\Windows\System32\winload.efi     17e210  EFI_EDID_ACTIVE_PROTOCOL_GUID
c:\mnt\install\Windows\System32\winload.efi     17e2a0  EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID
c:\mnt\install\Windows\System32\winresume.efi   122c00  EFI_EDID_ACTIVE_PROTOCOL_GUID
c:\mnt\install\Windows\System32\winresume.efi   122c80  EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID
c:\mnt\install\Windows\System32\Boot\winload.efi        17e210  EFI_EDID_ACTIVE_PROTOCOL_GUID
c:\mnt\install\Windows\System32\Boot\winload.efi        17e2a0  EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID
c:\mnt\install\Windows\System32\Boot\winresume.efi      122bf0  EFI_EDID_ACTIVE_PROTOCOL_GUID
c:\mnt\install\Windows\System32\Boot\winresume.efi      122c70  EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID
c:\mnt\iso\bootx64.efi a1a0    EFI_EDID_ACTIVE_PROTOCOL_GUID
c:\mnt\iso\bootx64.efi a220    EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID

So basically most of the GOP DXE driver functions go unused and can be considered bloat …

Are EFI_GRAPHICS_OUTPUT_PROTOCOL and EFI_EDID_ACTIVE_PROTOCOL_GUID possible vectors for exploitation from UEFI -> Windows? Assume for example a DXE driver has a bug that can be exploited using specialized hardware, and you gain execution in the UEFI firmware during boot. Can these protocols be used as an attack surface to attack SecureBoot Windows?

As seen before, EFI_GRAPHICS_OUTPUT_PROTOCOL has a driver controlled Mode member



typedef struct {
    UINT32 MaxMode;
    UINT32 Mode;
    UINTN SizeOfInfo;
    UINTN FrameBufferSize;

These structure are used in several functions inside the console library shared by all the relevant Windows components. The two main functions are ConsoleEfiGopOpen and ConsoleEfiGopEnable:

__int64 __fastcall ConsoleEfiGopOpen(CONSOLE_DATA *this)
  if ( EfiOpenProtocol(this->efi_handle, (__int64)&EfiGraphicsOutputProtocol, &gop_protocol) >= 0 )
    status = EfiGopGetCurrentMode(gop_protocol, &mode, &mode_info);
    if ( status >= 0 )
      orig_mode = mode;
      new_mode = mode;
      ... check if mode is allowed, if not get allowed mode ...
      // fill state with mode data
      is_rgb = mode_info.PixelFormat == PixelBlueGreenRedReserved8BitPerColor;
      this_1->gop_protocol = gop_protocol;
      this_1->new_mode = new_mode;
      this_1->orig_mode = orig_mode;
      if ( is_rgb )
        bits_per_pixel = 32;
      else if ( mode_info.PixelFormat == PixelBitMask )
        bits_per_pixel = 24;      
      else {
        status = STATUS_UNSUCCESSFUL;
        goto exit_handler;
      this_1->orig_horiz_res = mode_info.HorizontalResolution;
      this_1->orig_vert_res = mode_info.VerticalResolution;
      pixels_per_scan_line = mode_info.PixelsPerScanLine;
      this_1->orig_bits_per_pixel = bits_per_pixel;
      result = 0i64;
      this_1->orig_pixels_per_scan_line = pixels_per_scan_line;
      return result;
    EfiCloseProtocol(this_1->efi_handle, &EfiGraphicsOutputProtocol);
    return (unsigned int)status;
  return 0xC00000BB;

EfiGopGetCurrentMode() in turn uses MmArchTranslateVirtualAddress to get physical addresses for the output:

int __fastcall EfiGopGetCurrentMode(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop, unsigned int *mode, EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *info)
  info_phys_addr = info;
  mode_phys_addr = mode;
  gop_phys_addr = gop;
  context_mode = *gCurrentExecutionContext;
  if ( *gCurrentExecutionContext != ExecutionContextFirmware )
    if ( gop )
      status = MmArchTranslateVirtualAddress(gop, (unsigned __int64 *)&phys_addr, 0i64, 0i64);
      status = 0;
    if ( !status )
    gop_phys_addr = phys_addr;
    is_mapped = mode_phys_addr ? MmArchTranslateVirtualAddress(
                                   (unsigned __int64 *)&phys_addr,
                                   0i64) : 0;
    if ( !is_mapped )
    mode_phys_addr = (unsigned int *)phys_addr;
    is_mapped_2 = info_phys_addr ? MmArchTranslateVirtualAddress(
                                     (unsigned __int64 *)&phys_addr,
                                     0i64) : 0;
    if ( !is_mapped_2 )
    info_phys_addr = (EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *)phys_addr;
  *mode_phys_addr = gop_phys_addr->Mode->Mode;
  mode_info = gop_phys_addr->Mode->Info;
  *(_OWORD *)&info_phys_addr->Version = *(_OWORD *)&mode_info->Version;
  info_phys_addr->PixelInformation = mode_info->PixelInformation;
  info_phys_addr->PixelsPerScanLine = mode_info->PixelsPerScanLine;
  if ( context_mode != ExecutionContextFirmware )
  return v3;

The most we can get from this is an arbitary read from physical memory by Windows.

Lets look at ConsoleEfiGopEnable:

unsigned int __fastcall ConsoleEfiGopEnable(CONSOLE_DATA *this)
  status = EfiGopGetCurrentMode(this->gop_protocol, &old_mode, &mode_info);
  if ( status < 0 )
    return status;
  new_mode_1 = old_mode;
  if ( old_mode != new_mode )
    status = EfiGopSetMode(this_1->gop_protocol, new_mode);
    if ( status >= 0 )
      EfiGopGetCurrentMode(this_1->gop_protocol, &mode, &mode_info);
      new_mode_1 = old_mode;
    if ( mode_info.PixelFormat == PixelBlueGreenRedReserved8BitPerColor )
        bits_per_pixel = 32;
    else if ( mode_info.PixelFormat == PixelBitMask )
        bits_per_pixel = 24;
    else { ...; return STATUS_UNSUCCESSFUL; }
    EfiGopGetFrameBuffer(this_1->gop_protocol, &frame_buffer_base, &frame_buffer_size);
    if ( BlMmMapPhysicalAddressEx(&frame_buffer, frame_buffer_base, frame_buffer_size, 8u, 0) >= 0
      || (status = BlMmMapPhysicalAddressEx(&frame_buffer, frame_buffer_base, frame_buffer_size, 1u, 0), status >= 0) )
      this_1->frame_buffer = (void *)frame_buffer_1;
      this_1->frame_buffer_size = frame_buffer_size;
      this_1->bits_per_pixel = bits_per_pixel;
      this_1->horiz_res = mode_info.HorizontalResolution;
      ... contonue filling this_1 with mode_info ...
      return result;

Here Windows map the physical address supplied by GOP->FrameBuffer (retrieved in EfiGopGetFrameBuffer) into Windows. We can control FrameBuffer so we might be able to arbitarily map any physical memory as the frame buffer.

How does that help us? If for example the OEM logo (specified in the 'BGRT' ACPI table) is copied to the FrameBuffer, we can write data under our control to a physical address under our control - after the bootmgr has already been verified as part of the Secure Boot process.

But this is tangental to this post so we’ll examine this vector in a future post.

Part 2: From CSME

Now lets turn to the question wether CSME accesses the GuC and vice-versa.

The CSME is really big, so an exhastive disassembly like we did for the GOP is less relevant. So where might the CSME engine need to communicate with the GuC?

One place that comes into mind is the PAVP - Protected Audio Video Path. This is the component that protects protected HD content from being copied. The protection is implemented by creating a secure pipeline from the media components in the Windows kernel, through the GFX driver, and all the way to the display. The CSME is used to protect the pipeline including certs, keys and much more.

We can start with the CSME HECI (Host Embedded Controller Interface) driver on Windows and find the relevant HECI messages. One group of interesting messages I found was for the LSPCON component. LSPCON stands for Level Shifter and Protocol Converter, which is used for HDR signalling over HDMI.

No hard work means no fish, so we go on a fishing expedition and finally manage to extract the PAVP component from an old CSME15 build. Its about 300KB in size, so still quite big.

Reversing this I went down a deep rabbit hole. I finally discovered a function I named PAVP_init_heci, that is called from main and initializes the HECI communication module in PAVP and registers an interface with three functions:

  • handle async messages - PAVP_handle_async_message
  • HECI connection request - PAVP_connect
  • HECI disconnect request - PAVP_disconnect (all the names are mine)

PAVP_heci_handle_async_message() handles different types of messages like widevine, asmf, PlayReady and so on. We are interested in CPHS - Intel Content Protection HECI Service, a function I named PAVP_process_cphs_message(). Digging deeper we eventually reach the LSPCON command handler:

.text:0010775B ; int __cdecl LSPCON_command_handler(PavpCtx *ctx, void *heci_msg, int heci_msg_len, int max_out_len, int *out_len)
.text:0010775B LSPCON_command_handler proc near        ; CODE XREF: PAVP_heci_command_handler+8D↑p
.text:0010775B var_14          = dword ptr -14h
.text:0010775B msg_len         = dword ptr -10h
.text:0010775B ctx             = dword ptr  8
.text:0010775B heci_msg        = dword ptr  0Ch
.text:0010775B heci_msg_len    = dword ptr  10h
.text:0010775B max_out_len     = dword ptr  14h
.text:0010775B out_len         = dword ptr  18h
.text:0010775B cmd = ebx
.text:0010775B                 push    ebp
.text:0010775C                 mov     ebp, esp
.text:0010775E                 push    edi
.text:0010775F                 push    esi
.text:00107760                 push    cmd
.text:00107761                 sub     esp, 8
.text:00107764                 mov     eax, [ebp+heci_msg_len]
.text:00107767                 mov     ecx, [ebp+ctx]
.text:0010776A                 mov     [ebp+msg_len], eax
.text:0010776D                 mov     eax, [ebp+max_out_len]
.text:00107770                 mov     cmd, [ebp+heci_msg]
.text:00107773                 mov     [ebp+var_14], eax
.text:00107776                 mov     esi, [ebp+out_len]
.text:00107779                 test    ecx, ecx
.text:0010777B                 jz      short err_cmd_not_in_range
.text:0010777D                 cmp     [ecx+PavpCtx.Lspcon], 0
.text:00107781                 jz      short err_cmd_not_in_range
.text:00107783                 test    cmd, cmd
.text:00107785                 setz    dl
.text:00107788                 test    esi, esi
.text:0010778A                 setz    al
.text:0010778D                 or      dl, al
.text:0010778F                 jnz     short err_cmd_not_in_range
.text:00107791                 cmp     [ebp+msg_len], 0Fh ; cmd_len <= sizeof(LSPCON_heci_command_header_t)
.text:00107795                 ja      short is_cmd_id_in_heci_range

It begins by verifying the command buffer is big enough to fit the LSPCON HECI command header:

00000000 LSPCON_heci_command_header_t struc ; (sizeof=0x10, mappedto_125)
00000000                                         ; XREF: LSPCON_HECICMD_PLAYBACK_DONE_IN/r
00000000                                         ; LSPCON_HECICMD_PLAYBACK_DONE_OUT/r ...
00000000 version         dd ?
00000004 cmdid           dd ?                    ; XREF: LSPCON_command_handler:is_cmd_id_in_heci_range/r
00000008 status          dd ?
0000000C size            dd ?                    ; XREF: LSPCON_command_handler+6B/w
0000000C                                         ; LSPCON_command_handler+91/w ...
00000010 LSPCON_heci_command_header_t ends

Next it checks the command is one of the 7 LSPCON HECI commands and retreives appropriate handler from a global handler list:

.text:001077B8 is_cmd_id_in_heci_range:                ; CODE XREF: LSPCON_command_handler+3A↑j
.text:001077B8                 mov     edi, [cmd+LSPCON_heci_command_header_t.cmdid]
.text:001077BB                 lea     eax, [edi-0E000h] ; is 0xE000 < id < 0xE008
.text:001077C1                 cmp     eax, 7
.text:001077C4                 jbe     short get_handler
.text:001077D4 get_handler:                            ; CODE XREF: LSPCON_command_handler+69↑j
.text:001077D4                 mov     edx, dword ptr ds:gLSPCONCmdHandlerTable[eax*8] ; gLSPCONCmdHandlerTable.HandleFunc
.text:001077DB                 test    edx, edx        ; EDX contains handler
.text:001077DD                 jnz     short check_cmd_data

The global list looks something like:

gLSPCONCmdHandlerTable[] = {
      { 0 },
      { LSPCON_set_dev_cert,           sizeof(LSPCON_HECICMD_SET_LSPCON_CERT_IN),      sizeof(LSPCON_HECICMD_SET_LSPCON_CERT_OUT)},
      { LSPCON_init_session,           sizeof(LSPCON_HECICMD_INIT_SESSION_IN),         sizeof(LSPCON_HECICMD_INIT_SESSION_OUT)},
      { LSPCON_init_limits,            sizeof(LSPCON_HECICMD_INIT_LIMITS_IN),          sizeof(LSPCON_HECICMD_INIT_LIMITS_OUT)},
      { LSPCON_playback_done,          sizeof(LSPCON_HECICMD_PLAYBACK_DONE_IN),        sizeof(LSPCON_HECICMD_PLAYBACK_DONE_OUT)},
      { LSPCON_ack,                    sizeof(LSPCON_HECICMD_MSG_ACK_IN),              sizeof(LSPCON_HECICMD_MSG_ACK_OUT)},
      { LSPCON_get_topology,           sizeof(LSPCON_HECICMD_GET_TOPOLOGY_IN),         sizeof(LSPCON_HECICMD_GET_TOPOLOGY_OUT)},

After verifying the size of the input and output structs the actual command handle is called.

.text:001077FD check_cmd_data:                         ; CODE XREF: LSPCON_command_handler+82↑j
.text:001077FD                 movzx   edi, word ptr ds:unk_82364[eax*8] ; gLSPCONCmdHandlerTable.InputSize
.text:00107805                 cmp     edi, [ebp+msg_len]
.text:00107808                 ja      short sizes_error
.text:0010780A                 movzx   eax, word ptr ds:unk_82366[eax*8] ; gLSPCONCmdHandlerTable.OutputSize
.text:00107812                 cmp     eax, [ebp+var_14]
.text:00107815                 ja      short sizes_error
.text:00107830 loc_107830:                             ; CODE XREF: LSPCON_command_handler+C5↑j
.text:00107830                 push    cmd
.text:00107831                 push    ecx
.text:00107846                 call    edx             ; Call Command Handler!

Reveresing all the command handlers we find something interesting in the most unexpected one (thus the last I REd): LSPCON_playback_done(). It took me a while to even understand its releated to the GuC, and I’ll explain later how it does so.

What does LSPCON_playback_done do? It checks whether HDCP restrictions should remain in place after a playback is complete.

The function begins by verifying the input parameter (LSPCON_HECICMD_PLAYBACK_DONE_IN) is valid:

.text:00107C6B ; int __cdecl LSPCON_playback_done(PavpCtx *ctx, void *msg)
.text:00107C6B LSPCON_playback_done proc near
.text:00107C6B cur_hdcp_requirements= dword ptr -18h
.text:00107C6B count_active_sessions= dword ptr -14h
.text:00107C6B var_10          = dword ptr -10h
.text:00107C6B ctx             = dword ptr  8
.text:00107C6B msg             = dword ptr  0Ch
.text:00107C6B ctx_ptr = edi
.text:00107C6B                 push    ebp
.text:00107C6C                 mov     ebp, esp
.text:00107C6E                 push    ctx_ptr
.text:00107C6F                 push    esi
.text:00107C70                 push    ebx
.text:00107C71                 sub     esp, 0Ch
.text:00107C74                 mov     [ebp+count_active_sessions], 0
.text:00107C7B                 mov     esi, [ebp+msg]
.text:00107C7E                 mov     eax, ds:stack_cookie_ptr
.text:00107C83                 mov     [ebp+var_10], eax
.text:00107C86                 xor     eax, eax
.text:00107C88                 mov     ctx_ptr, [ebp+ctx]
.text:00107C8B                 test    esi, esi
.text:00107C8D                 jnz     short check_valid_header
.text:00107C99 check_valid_header:                     ; CODE XREF: LSPCON_playback_done+22↑j
.text:00107C99                 mov     [esi+LSPCON_HECICMD_PLAYBACK_DONE_IN.header.size], 0
.text:00107CA0                 test    ctx_ptr, ctx_ptr
.text:00107CA2                 jz      short invalid_parameter
.text:00107CA4                 cmp     [ctx_ptr+PavpCtx.Lspcon], 0
.text:00107CA8                 jz      short invalid_parameter

And now comes the interesting part:

.text:00107CAA                 lea     eax, [ebp+count_active_sessions]
.text:00107CAD                 push    eax             ; num_active_sessions
.text:00107CAE                 push    0               ; type
.text:00107CB0                 push    ctx_ptr         ; ctx
.text:00107CB1                 call    GUC_get_active_sessions ; 
.text:00107CB6                 add     esp, 0Ch
.text:00107CB9                 mov     ebx, eax
.text:00107CBB                 test    eax, eax
.text:00107CBD                 jz      short got_active_sessions

If there are any remaining active sessions the code continues to check what level of HDCP protection they require and set protection to that level if it is lower then the current level, I won’t go into that disassembly as its not really interesting.

Why do I think GUC_get_active_sessions is actually related to GuC and why did I name it that? Lets continue by examining this function. Its just a wrapper around a function I called GUC_send_message that sends message no. 6,

.text:0010452C ; int __cdecl GUC_get_active_sessions(PavpCtx *ctx, int type, unsigned int *num_active_sessions)
.text:0010452C GUC_get_active_sessions proc near       ; CODE XREF: LSPCON_playback_done+46↓p
.text:0010452C guc2csme        = GUC2CSME_MSG ptr -18h
.text:0010452C csme2guc        = CSME2GUC_MSG ptr -10h
.text:0010452C ctx             = dword ptr  8
.text:0010452C type            = dword ptr  0Ch
.text:0010452C num_active_sessions= dword ptr  10h
.text:0010452C ctx_ptr = esi
.text:0010455B type_ok:
.text:0010455B                 mov     dword ptr [ebp+csme2guc.command], GUC_MSG_GET_ACTIVE_SESSIONS ; =6
.text:00104562                 mov     [ebp+csme2guc.data1], al
.text:00104565                 lea     eax, [ebp+guc2csme.value]
.text:00104568                 mov     [ebp+guc2csme.value], 0
.text:0010456F                 push    eax             ; guc2csme
.text:00104570                 lea     eax, [ebp+csme2guc]
.text:00104573                 push    eax             ; csme2guc
.text:00104574                 push    ctx_ptr         ; ctx
.text:00104575                 call    GUC_send_message

GUC_send_message() gets two parameters in addition to the PAVP context: a CSME2GUC structure and a GUC2CSME structure. How does it work? It tries to send the message several times in a loop, each time waiting for a short timeout. The first iteration of the loop also wakes the GuC by enabling it through managment functions (if it isn’t already enabled), and sending a special wake message using a function I named GUC_send_VDM().

.text:001041FF ; int __cdecl GUC_send_message(PavpCtx *ctx, CSME2GUC_MSG *csme2guc, GUC2CSME_MSG *guc2csme)
.text:001041FF GUC_send_message proc near              ; CODE XREF: GUC_get_active_sessions+49↓p
.text:001041FF                                         ; sub_1045C5+3F↓p
.text:001041FF ctx             = dword ptr  8
.text:001041FF csme2guc        = dword ptr  0Ch
.text:001041FF guc2csme        = dword ptr  10h
.text:001041FF attempt = esi
.text:001041FF ctx_ptr = ebx
.text:001041FF                 push    ebp
.text:00104200                 mov     ebp, esp
.text:00104202                 push    edi
.text:00104203                 push    attempt
.text:00104204                 xor     attempt, attempt
.text:00104206                 push    ctx_ptr
.text:00104207                 mov     ctx_ptr, [ebp+ctx]
.text:0010420A send_loop:                              ; CODE XREF: GUC_send_message+A3↓j
.text:0010420A                 inc     attempt
.text:0010420B                 cmp     attempt, 1
.text:0010420E                 jnz     short send_wake_msg_loop
.text:00104210 first_attempt:
.text:00104210                 push    ctx_ptr
.text:00104211                 call    GUC_disable_power_gate?
.text:00104216                 mov     edi, eax
.text:00104218                 pop     eax
.text:00104219                 test    edi, edi
.text:0010421B                 jnz     loc_1042A8
.text:00104221 send_wake_msg_loop:                     ; CODE XREF: GUC_send_message+F↑j
.text:00104221                                         ; GUC_send_message+4E↓j
.text:00104221                 push    VDM_CSME_TO_GUC_WAKE_REQ
.text:00104223                 push    0               ; msg
.text:00104225                 push    ctx_ptr
.text:00104226                 call    GUC_send_VDM    ; VDM == Vendor Defined Message?
.text:0010422B                 add     esp, 0Ch
.text:0010422E                 mov     edi, eax
.text:00104230                 test    eax, eax
.text:00104232                 jnz     msg_error
.text:00104238                 push    [ebp+guc2csme]
.text:0010423B                 push    GUC_IS_AWAKE
.text:0010423D                 push    ctx_ptr
.text:0010423E                 call    GUC_wait_for_message ; wait for GUC is awake message
.text:00104243                 add     esp, 0Ch
.text:00104246                 mov     edi, eax
.text:00104248                 cmp     eax, PAVP_STATUS_TRY_AGAIN
.text:0010424D                 jz      short send_wake_msg_loop
.text:0010424F                 cmp     eax, PAVP_STATUS_TIMEOUT
.text:00104254                 jnz     short got_awake_msg
.text:00104256 timeout:                                ; CODE XREF: GUC_send_message+92↓j
.text:00104256                                         ; GUC_send_message+C9↓j
.text:00104256                 mov     edi, PAVP_STATUS_TIMEOUT
.text:0010425B                 jmp     short loc_104297
.text:0010425D ; ---------------------------------------------------------------------------

Once the GuC awake message was received the actually GuC message is send, again with GUC_send_VDM().

.text:0010425D got_awake_msg:                          ; CODE XREF: GUC_send_message+55↑j
.text:0010425D                 test    eax, eax
.text:0010425F                 jnz     short loc_1042A8
.text:00104261                 mov     eax, [ebp+csme2guc]
.text:00104264                 push    VDM_FROM_CSME
.text:00104266                 push    dword ptr [eax+CSME2GUC_MSG.command]
.text:00104268                 push    ctx_ptr
.text:00104269                 call    GUC_send_VDM
.text:0010426E                 add     esp, 0Ch
.text:00104271                 mov     edi, eax
.text:00104273                 test    eax, eax
.text:00104275                 jnz     short loc_1042A8
.text:00104277                 push    [ebp+guc2csme]
.text:0010427A                 mov     eax, [ebp+csme2guc]
.text:0010427D                 movzx   eax, [eax+CSME2GUC_MSG.command]
.text:00104280                 push    eax
.text:00104281                 push    ctx_ptr
.text:00104282                 call    GUC_wait_for_message

Its then waits for the return message GUC_wait_for_message(). Now you have to say - Wise guy, how do you know this is actually releated to GuC? What is this VDM stuff? Did Ded Moroz drop them in your cabin?

VDMs are Vendor Defined Messages, a way to send custom messages to devices over a PCI bus. They are sent through IOCTLs to the VDM driver in CSME. The IOCTL gets data through a message:

00000000 IOCTL_VDM_WRITE struc ; (sizeof=0x12, mappedto_145)
00000000 addr_offset     dd ?
00000004 data            dd ?          ; This is a bitfield per the spec
00000008 info            VDM_TX ?
00000012 IOCTL_VDM_WRITE ends
00000000 VDM_TX          struc ; (sizeof=0xA, mappedto_142)
00000000                                         ; XREF: GucCtx/r
00000000                                         ; IOCTL_VDM_WRITE/r
00000000 msg             dd ?                    ; XREF: setup_guc_vdm+F/r
00000004 pci_req_id      dw ?                    ; XREF: setup_guc_vdm+12/w
00000006 tag             dw ?
00000008 pci_tgt_id      dw ?                    ; XREF: setup_guc_vdm+1C/w
0000000A VDM_TX          ends

Here you have the first hint of how I connected all this to the GuC. Lets just get the VDM function out of the way:

.text:0014889D VDM_write       proc near               ; CODE XREF: sub_1028DB+CE↑p
.text:0014889D                                         ; GUC_send_VDM+4F↑p ...
.text:0014889D var_40          = byte ptr -40h
.text:0014889D vdm_ioctl       = IOCTL_VDM_WRITE ptr -3Ch
.text:0014889D var_10          = dword ptr -10h
.text:0014889D fd              = dword ptr  8
.text:0014889D addr_info       = dword ptr  0Ch
.text:0014889D addr_offset     = dword ptr  10h
.text:0014889D data            = dword ptr  14h
.text:0014889D                 push    ebp
.text:0014889E                 mov     ebp, esp
.text:001488A0                 push    edi
.text:001488A1                 push    esi
.text:001488A2                 push    ebx
.text:001488A3                 sub     esp, 34h
.text:001488A6                 mov     ebx, [ebp+fd]
.text:001488A9                 mov     eax, ds:stack_cookie_ptr
.text:001488AE                 mov     [ebp+var_10], eax
.text:001488B1                 xor     eax, eax
.text:001488B3                 mov     edi, [ebp+addr_info]
.text:001488B6                 test    ebx, ebx
.text:001488B8                 js      short invalid_parameter
.text:001488BA                 test    edi, edi
.text:001488BC                 jz      short invalid_parameter
.text:001488BE                 lea     esi, [ebp+vdm_ioctl]
.text:001488C1 build_ioctl_data:
.text:001488C1                 push    44 ; sizeof(vdm_ioctl)
.text:001488C3                 push    0
.text:001488C5                 push    esi
.text:001488C6                 call    near ptr memset
.text:001488CB                 mov     eax, [ebp+addr_offset]
.text:001488CE                 mov     [ebp+vdm_ioctl.addr_offset], eax
.text:001488D1                 mov     eax, [ebp+data]
.text:001488D4                 mov     [], eax
.text:001488D7                 lea     eax, []
.text:001488DA                 push    0Ah             ; sizeof(TX info)
.text:001488DC                 push    edi
.text:001488DD                 push    0Ah
.text:001488DF                 push    eax
.text:001488E0                 call    near ptr memcpy_s
.text:001488E5                 lea     eax, [ebp+var_40]
.text:001488E8                 push    eax
.text:001488E9                 push    44
.text:001488EB                 push    esi
.text:001488EC                 push    44
.text:001488EE                 push    esi
.text:001488EF                 push    2        ; IOCTL write
.text:001488F1                 push    ebx
.text:001488F2                 call    near ptr ioctl_s

The IOCTL is sent to a file handle. Where is it set? We now go back to the PAVP init code and look for all places where file handles are init. There we find to functions I am pretty sure initialize the GuC and the Graphics Key Manager (GKM), thus I appropriatly named them GUC_init() and GKM_init() (I keep reminding you I named these functions as I have no clue what is their realy name, these are my guesses).

As usual, the function begins by checking it's input argument:

.text:001043C3 GUC_init        proc near               ; CODE XREF: pavp_init+259↑p
.text:001043C3 ctx             = dword ptr  8
.text:001043C3 ctx_ptr = ebx
.text:001043C3                 push    ebp
.text:001043C4                 mov     ebp, esp
.text:001043C6                 push    esi
.text:001043C7                 push    ctx_ptr
.text:001043C8                 mov     esi, 1005h
.text:001043CD                 mov     ctx_ptr, [ebp+ctx]
.text:001043D0                 test    ctx_ptr, ctx_ptr
.text:001043D2                 jz      invalid_paramter
.text:001043D8                 cmp     [ctx_ptr+PavpCtx.guc_ctx], 0
.text:001043DC                 jnz     invalid_paramter

Next it allocates a context for GuC operations:

.text:001043E2                 push    90 ; sizeof(GucContext
.text:001043E4                 push    1
.text:001043E6                 call    near ptr calloc ; allocate GucContext (0x5A bytes)
.text:001043EB                 mov     [ctx_ptr+PavpCtx.guc_ctx], eax
.text:001043EE                 test    eax, eax
.text:001043F0                 pop     esi
.text:001043F1                 pop     edx
.text:001043F2                 jnz     short alloc_ok  ; start with no FD

The struct itself:

00000000 GucCtx          struc ; (sizeof=0x5A, mappedto_140)
00000000 vdm_file_descriptor dd ?                ; XREF: GUC_init:alloc_ok/w
00000000                                         ; GUC_init+5C/w ...
00000004 pg_timer        Timer ?
00000028 watchdog        Timer ?                 ; XREF: GUC_command_handler+8C/o
0000004C vdm             VDM_TX ?                ; XREF: GUC_init:loc_104441/o
00000056 state           dd ?                    ; XREF: GUC_pg_timer_routine+39/w
00000056                                         ; GUC_init+127/w
0000005A GucCtx          ends

It first checks if a file descriptor has already been setup by the Graphics Key Manager, and if so uses the same file descriptor - apparently they share the same VDM channel. Otherwise a new FD is setup in setup_guc_vdm(). The rest of the code initializes two timers - one related to some kind of watchdog and the other to power managment.

.text:0010440C alloc_ok:                               ; CODE XREF: GUC_init+2F↑j
.text:0010440C                 mov     [eax+GucCtx.vdm_file_descriptor], 0FFFFFFFFh ; start with no FD
.text:00104412                 mov     eax, [ctx_ptr+PavpCtx.graphic_key_mgr]
.text:00104415                 test    eax, eax
.text:00104417                 jz      short no_gkm
.text:00104419                 mov     edx, [ctx_ptr+PavpCtx.guc_ctx]
.text:0010441C                 mov     eax, [eax+GkmCtx.vdm_file_descriptor]
.text:0010441F                 mov     [edx+GucCtx.vdm_file_descriptor], eax
.text:00104421 no_gkm:                                 ; CODE XREF: GUC_init+54↑j
.text:00104421                 mov     eax, [ctx_ptr+PavpCtx.guc_ctx]
.text:00104424                 cmp     [eax+GucCtx.vdm_file_descriptor], 0
.text:00104427                 jns     short loc_104441
.text:00104429                 push    4B00FDh
.text:0010442E                 mov     esi, 100Eh
.text:00104433                 push    2
.text:00104435                 call    near ptr log_printf_0
.text:0010443A                 pop     eax
.text:0010443B                 pop     edx
.text:0010443C                 jmp     invalid_paramter
.text:00104441 ; ---------------------------------------------------------------------------
.text:00104441 loc_104441:                             ; CODE XREF: GUC_init+64↑j
.text:00104441                 add     eax, GucCtx.vdm
.text:00104444                 push    eax
.text:00104445                 call    setup_guc_vdm
.text:0010444A                 mov     esi, eax

And this is the part we have been waiting for:

.text:00102810 setup_guc_vdm   proc near               ; CODE XREF: GKM_init+2D↓p
.text:00102810                                         ; GUC_init+82↓p
.text:00102810 vdm             = dword ptr  8
.text:00102810 vdm_ptr = edx
.text:00102810                 push    ebp
.text:00102811                 mov     eax, 1005h
.text:00102816                 mov     ebp, esp
.text:00102818                 mov     vdm_ptr, [ebp+vdm]
.text:0010281B                 test    vdm_ptr, vdm_ptr
.text:0010281D                 jz      short loc_10284A
.text:0010281F                 mov     al, byte ptr [vdm_ptr+(VDM_TX.msg+3)]
.text:00102822                 mov     dword ptr [vdm_ptr+VDM_TX.pci_req_id], 0B0h ; CSME: bus: 0, device: 22, function 0
.text:00102829                 or      eax, 7
.text:0010282C                 mov     [vdm_ptr+VDM_TX.pci_tgt_id], 10h ; GUC: buf: 0, device: 2, function 0
.text:00102832                 and     eax, 0FFFFFF8Fh
.text:00102835                 mov     byte ptr [vdm_ptr], 0D3h
.text:00102838                 mov     [vdm_ptr+3], al
.text:0010283B                 mov     al, [vdm_ptr+2]
.text:0010283E                 or      byte ptr [vdm_ptr+1], 0Fh
.text:00102842                 and     eax, 0FFFFFF80h
.text:00102845                 mov     [vdm_ptr+2], al
.text:00102848                 xor     eax, eax
.text:0010284A loc_10284A:                             ; CODE XREF: setup_guc_vdm+D↑j
.text:0010284A                 pop     ebp
.text:0010284B                 retn
.text:0010284B setup_guc_vdm   endp

Here we have the internal bus IDs for the GuC and CSME.

Results are retrieved using GUC_wait_for_message() - it uses select() to wait on the VDM file handle and parses the message. Something interesting I found out it that messages are not initiated only by the CSME - the GuC can initiate messages to the CSME and the CSME responds. GUC_wait_for_message() uses a handler table with 11 entries, but 4 are NULL.

For example, one message I decoded gets some production information for the chip:

.text:00103EDA GUC_api_get_production_info proc near
.text:00103EDA var_14          = byte ptr -14h
.text:00103EDA var_13          = byte ptr -13h
.text:00103EDA var_E           = byte ptr -0Eh
.text:00103EDA var_D           = byte ptr -0Dh
.text:00103EDA var_C           = dword ptr -0Ch
.text:00103EDA ctx             = dword ptr  8
.text:00103EDA ctx_ptr = esi
.text:00103EDA                 push    ebp
.text:00103EDB                 mov     ebp, esp
.text:00103EDD                 push    ctx_ptr
.text:00103EDE                 push    ebx
.text:00103EDF                 sub     esp, 0Ch
.text:00103EE2                 mov     [ebp+var_14], 0
.text:00103EE6                 mov     ctx_ptr, [ebp+ctx]
.text:00103EE9                 mov     eax, ds:stack_cookie_ptr
.text:00103EEE                 mov     [ebp+var_C], eax
.text:00103EF1                 xor     eax, eax
.text:00103EF3                 push    ctx_ptr
.text:00103EF4                 call    GUC_enable_power_gate
.text:00103EF9                 lea     eax, [ebp+var_14]
.text:00103EFC                 push    eax
.text:00103EFD                 call    test_byte_12h_from_snowball_rbe_sku
.text:00103F02                 pop     ecx
.text:00103F03                 test    eax, eax
.text:00103F05                 pop     ebx
.text:00103F06                 mov     ebx, 109h
.text:00103F0B                 jnz     short loc_103F48
.text:00103F0D                 mov     ebx, 9
.text:00103F12                 cmp     [ebp+var_14], 0
.text:00103F16                 jnz     short loc_103F48
.text:00103F18                 lea     eax, [ebp+var_13]
.text:00103F1B                 mov     ebx, 109h
.text:00103F20                 push    eax
.text:00103F21                 call    get_7_bytes_from_snowball_rbe_sku
.text:00103F26                 pop     edx
.text:00103F27                 test    eax, eax
.text:00103F29                 jnz     short loc_103F48
.text:00103F2B                 mov     bl, [ebp+var_E]
.text:00103F2E                 mov     al, [ebp+var_D]
.text:00103F31                 shr     bl, 2           ; actuall data from CPUs looks like production year & week
.text:00103F34                 and     eax, 0Fh
.text:00103F37                 shl     eax, 9
.text:00103F3A                 and     ebx, 3Fh
.text:00103F3D                 shl     ebx, 0Dh
.text:00103F40                 or      ebx, 109h
.text:00103F46                 or      ebx, eax
.text:00103F48 loc_103F48:                             ; CODE XREF: GUC_api_get_production_info+31↑j
.text:00103F48                                         ; GUC_api_get_production_info+3C↑j ...
.text:00103F48                 push    ctx_ptr
.text:00103F49                 call    GUC_enable_power_gate
.text:00103F4E                 push    2
.text:00103F50                 push    ebx
.text:00103F51                 push    ctx_ptr
.text:00103F52                 call    GUC_send_VDM
.text:00103F57                 mov     edx, [ebp+var_C]
.text:00103F5A                 xor     edx, ds:stack_cookie_ptr
.text:00103F60                 jz      short loc_103F67
.text:00103F62                 call    near ptr __stkchk
.text:00103F67 loc_103F67:                             ; CODE XREF: GUC_api_get_production_info+86↑j
.text:00103F67                 lea     esp, [ebp-8]
.text:00103F6A                 pop     ebx
.text:00103F6B                 pop     ctx_ptr
.text:00103F6C                 pop     ebp
.text:00103F6D                 retn
.text:00103F6D GUC_api_get_production_info endp

Why do I think this is related to production information? Because it reads data from a file called "/snowball/rbe_sku" (Intel’s name!). I don’t have any idea what Snowball means, RBE usualy means ROM Boot Extenion, so it reads data from the ROM? The actuall data from a few processors appears to be correlated to production year and work week for the CPU.

.text:00148AF7 test_byte_12h_from_snowball_rbe_sku proc near
.text:00148AF7                                         ; CODE XREF: pavp_init+10A↑p
.text:00148AF7                                         ; GUC_api_get_production_info+23↑p ...
.text:00148AF7 buffer          = byte ptr -24h
.text:00148AF7 stack_cookie    = dword ptr -8
.text:00148AF7 var_4           = dword ptr -4
.text:00148AF7 out_byte_12h    = dword ptr  8
.text:00148AF7                 push    ebp
.text:00148AF8                 mov     ebp, esp
.text:00148AFA                 push    ebx
.text:00148AFB                 sub     esp, 20h
.text:00148AFE                 mov     eax, ds:stack_cookie_ptr
.text:00148B03                 mov     [ebp+stack_cookie], eax
.text:00148B06                 xor     eax, eax
.text:00148B08                 lea     eax, [ebp+buffer]
.text:00148B0B                 push    1Ch
.text:00148B0D                 mov     ebx, [ebp+out_byte_12h]
.text:00148B10                 push    eax
.text:00148B11                 push    offset aSnowballRbeSku_0 ; "/snowball/rbe_sku"
.text:00148B16                 call    read_file_completely
.text:00148B1B                 add     esp, 0Ch
.text:00148B1E                 test    eax, eax
.text:00148B20                 jnz     short loc_148B2A
.text:00148B22                 mov     dl, [ebp+buffer+12h]
.text:00148B25                 and     edx, 1
.text:00148B28                 mov     [ebx], dl
.text:00148B2A loc_148B2A:                             ; CODE XREF: test_byte_12h_from_snowball_rbe_sku+29↑j
.text:00148B2A                 mov     ecx, [ebp+stack_cookie]
.text:00148B2D                 xor     ecx, ds:stack_cookie_ptr
.text:00148B33                 jz      short loc_148B3A
.text:00148B35                 call    near ptr __stkchk
.text:00148B3A loc_148B3A:                             ; CODE XREF: test_byte_12h_from_snowball_rbe_sku+3C↑j
.text:00148B3A                 mov     ebx, [ebp+var_4]
.text:00148B3D                 leave
.text:00148B3E                 retn
.text:00148B3E test_byte_12h_from_snowball_rbe_sku endp

.text:00148A54 read_file_completely proc near          ; CODE XREF: get_7_bytes_from_snowball_rbe_sku+21↓p
.text:00148A54                                         ; test_byte_12h_from_snowball_rbe_sku+1F↓p ...
.text:00148A54 filename        = dword ptr  8
.text:00148A54 buffer          = dword ptr  0Ch
.text:00148A54 byte_count      = dword ptr  10h
.text:00148A54                 push    ebp
.text:00148A55                 mov     ebp, esp
.text:00148A57                 push    edi
.text:00148A58                 push    esi
.text:00148A59                 push    ebx
.text:00148A5A                 push    0
.text:00148A5C count = esi
.text:00148A5C                 mov     count, [ebp+byte_count]
.text:00148A5F open_file:
.text:00148A5F                 push    [ebp+filename]
.text:00148A62                 call    near ptr open
.text:00148A67 file_handle = ebx
.text:00148A67                 mov     file_handle, eax
.text:00148A69                 pop     eax
.text:00148A6A                 test    file_handle, file_handle
.text:00148A6C                 pop     edx
.text:00148A6D                 mov     eax, 222
.text:00148A72                 js      short loc_148A98
.text:00148A74 read_file:
.text:00148A74                 push    count
.text:00148A75                 push    [ebp+buffer]
.text:00148A78                 push    file_handle
.text:00148A79                 call    near ptr read
.text:00148A7E close_file:
.text:00148A7E                 push    file_handle
.text:00148A7F                 mov     edi, eax
.text:00148A81                 call    near ptr close
.text:00148A86                 add     esp, 10h
.text:00148A89                 test    edi, edi
.text:00148A8B                 js      short loc_148A93
.text:00148A8D                 xor     eax, eax
.text:00148A8F                 cmp     edi, count
.text:00148A91                 jz      short loc_148A98
.text:00148A93 loc_148A93:                             ; CODE XREF: read_file_completely+37↑j
.text:00148A93                 mov     eax, 99
.text:00148A98 loc_148A98:                             ; CODE XREF: read_file_completely+1E↑j
.text:00148A98                                         ; read_file_completely+3D↑j
.text:00148A98                 lea     esp, [ebp-0Ch]
.text:00148A9B                 pop     ebx
.text:00148A9C                 pop     esi
.text:00148A9D                 pop     edi
.text:00148A9E                 pop     ebp
.text:00148A9F                 retn
.text:00148A9F read_file_completely endp


I am still actively working on this to see what attack surfaces there are from GuC->CSME and CSME->GuC, but it looks like Intel did a really good job checking bounds and arguments. The Graphics Key Manager is next in the queue, it look like the surface there is more promising.

There is also a lot more to decode in PAVP, I only decoded a small part of the context structure:

PavpCtx         struc ; (sizeof=0x80, mappedto_123)
00000000 field_0         dd ?
00000004 field_4         dd ?
00000008 heci_client     dd ?
0000000C server_ctx      dd ?
00000010 graphic_key_mgr dd ?                    ; XREF: GUC_init+4F/r
00000014 vkm             dd ?
00000018 guc_ctx         dd ?                    ; XREF: GUC_pg_timer_routine+32/r
00000018                                         ; GUC_disable_power_gate?+1E/r ...
0000001C Lspcon          dd ?                    ; XREF: LSPCON_command_handler+22/r
0000001C                                         ; LSPCON_playback_done+39/r ...
00000020 field_20        dd ?
00000024 timer_ctx       dd ?                    ; XREF: GUC_disable_power_gate?+56/r
00000024                                         ; GUC_command_handler+90/r ... ; struct offset (PavpPortConfig)
00000028 field_28        dd ?
0000002C port_cfg        PavpPortConfig ?
00000044 field_44        dd ?
00000048 field_48        dd ?
0000004C field_4C        dd ?
00000050 field_50        dd ?
00000054 handlers        dd ?                    ; XREF: GUC_command_handler+29/r
00000058 field_58        dd ?
0000005C field_5C        dd ?
00000060 field_60        dd ?
00000064 field_64        dd ?
00000068 field_68        dd ?
0000006C field_6C        dd ?
00000070 field_70        dd ?
00000074 field_74        dd ?
00000078 field_78        dd ?
0000007C field_7C        dd ?
00000080 PavpCtx         ends

Enough for today, especially as my day job has warmed up a bit in the last three weeks - more on that later! I promise it will be very interesting (but not hardware related).