首页
社区
课程
招聘
[原创]UEFI下的HOOK与回调
发表于: 2025-4-28 09:19 1984

[原创]UEFI下的HOOK与回调

2025-4-28 09:19
1984

0.介绍

UEFI环境作为先于操作系统启动创建的环境,在安全方面有重要作用。本文侧重于UEFI环境下HOOK与回调,而非操作系统的引导过程。
由于UEFI固件会随硬件而异,所以本文给出的代码无法保证在任意机器上正常运行,特别是直接修改SystemTable的代码。
测试环境
测试机:Lenovo X1 Carbon,固件版本1.3.11
测试OS:Windows10.0.19045
编译工具链:MSVC(VS2022)+NASM+IASL

1.使用CreateEventEx函数

什么是CeateEventEx:

CreateEvent 函数创建事件对象并将其加入事件组,用于异步操作、定时器或协议通知等场景。
函数的原型:

1
2
3
4
5
6
7
8
9
10
EFI_STATUS
EFIAPI
CreateEventEx (
  IN UINT32                  Type,          // 事件类型
  IN EFI_TPL                 NotifyTpl,     // 回调的任务优先级(TPL)
  IN EFI_EVENT_NOTIFY        NotifyFunction,// 回调函数指针
  IN CONST VOID              *NotifyContext,// 传递给回调的上下文
  IN CONST EFI_GUID          *EventGroup,   // 事件组 GUID(可选)
  OUT EFI_EVENT              *Event         // 返回的事件句柄
);

这里着重说一下EventGroup的作用:
由CreateEventEx生成的事件会加入到EventGroup中。当EventGroup中的任一事件被触发后,组中的所有其他事件都会被触发,进而同组内所有的Notification函数都将被加入到待执行队列。同组内NotifyTpl(优先级)高的Notification函数会先被执行。

如果输入参数EventGroup为NULL,则CreateEventEx退化为CreateEvent。
注意:
Type不能是EVT_SIGNAL_EXIT_BOOT_SERVICES 或 EVT_SIGNAL_VIRTUAL_ADDRESS_ CHANGE,因为这两种类型有各自对应的Group,也就是说,

1
2
3
4
5
6
7
gBS -> CreateEvent(
    EVT_SIGNAL_EXIT_BOOT_SERVICES ,
    TPL_NOTIFY,
    myNotifyFunction,
    NULL,
    &myEvent
);

1
2
3
4
5
6
7
8
gBS -> CreateEventEx(
    EVT_NOTIFY_SIGNAL,
    TPL_NOTIFY,
    myNotifyFunction,
    NULL,
    &gEfiEventExitBootServicesGuid,
    &myEvent
);

作用完全相同。
下面列出了 UEFI预定义的4个Event组。

EFI_EVENT_GROUP_EXIT_BOOT_SERVICES
GUID: gEfiEventExitBootServicesGuid。
ExitBootServices执行时触发该组内所有Event。组内Event属性同于EVT_SIGNAL_EXIT_BOOT_SERVICES类型的 Event。

EFI_EVENT_GROUP_VIRTUAL_ADDRESS_CHANGE
GUID: gEfiEventVirtualAddressChangeGuid。
SetVirtualAddressMap执行时触发该组内所有Event。组内Event属性同于EVT_SIGNAL_VIRTUAL_ADDRESS_CHANGE类型的Event。

EFI_EVENT_GROUP_MEMORY_MAP_CHANGE
GUID: gEfiEventMemoryMapChangeGuid。
Memory Map改变时触发该组内所有Event。在Notification函数中不能使用启动服务的内存分配函数。
EFI_EVENT_GROUP_READY_TO_BOOT
GUID:gEfiEventReadyToBootGuid。
Boot Manager加载并且执行一个启动项时触发该组内所有Event。

演示:拦截Windows的启动过程

这个演示实现了对Windows启动的拦截
Event组填什么?
根据IDA对winload.efi的分析,winload在建立基本的执行环境后便立即将EFI_SYSTEM_TABLE封装到HAL中,然后通过HalEfiRuntimeSevrice访问UEFI服务,这可能会对驱动造成损伤,所以在Windows还未配置内存前接管执行最好,即EFI_EVENT_GROUP_READY_TO_BOOT后执行CallbackFunction函数。
此事件发生在BIOS DXE阶段结束时,操作系统引导加载程序收到控制信号之前,允许回调在操作系统之前接管执行。
因此,Event组填EFI_EVENT_ GROUP_READY_TO_BOOT即可。

EfiGetMemoryMap用来调用BootSrevices

EfiGetMemoryMap在转换地址后会陷入HAL层,实现对UEFI服务的调用

另一个可能
winload在建立基本的执行环境后便立即将EFI_SYSTEM_TABLE封装到HAL中,然后通过HalEfiRuntimeSevrice访问UEFI服务,,这也包括对ExitBootSevices的调用。所以我们可以对ExitBootSevices创建事件,从而实现HOOK。
那么回调函数在何处接管执行呢?是否需要上下文的处理呢?
OslFwpSetupPhase1

OslFwpKernelSetupPhase1函数调用EfiGetMemoryMap函数来获取之前配置的EFI_BOOT_SERVICE结构的指针,然后将其存储在一个全局变量中,以便通过HAL进行之后的操作。
之后,OslFwpKernelSetupPhase1调用EFI函数ExitBootServices。因此,这是回调函数的一个触发点。如果在这之前触发,可能会打断内核加载的执行流,从而崩溃。

其他提示
由于UEFI_APPLICATION无法常驻内存,所以我们需要开发DXE_DRIVER
所以这是实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Status = gBS->CreateEventEx(
        EVT_NOTIFY_SIGNAL,
        TPL_CALLBACK,
        CallbackFunction,
        NULL,
        &gEfiEventReadyToBootGuid,
        &Event
    );
 
    if (EFI_ERROR(Status)) {
        DEBUG((EFI_D_ERROR, "[MyDriver] Event creation failed! %r\n", Status));
        gBS->FreePool(Protocol);
        return Status;
    }
}

该函数采用CreateEventEx函数创建回调,在收到EFI_EVENT_ GROUP_READY_TO_BOOT后执行CallbackFunction函数。
此事件发生在BIOS DXE阶段结束时,操作系统引导加载程序收到控制信号之前,允许回调在操作系统之前接管执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
EFI_STATUS EFIAPI
CallbackFunction(
  IN EFI_EVENT    Event,
  IN VOID         *Context
)
{
  EFI_STATUS  Status;
  EFI_EVENT   TimerEvent;
 
  Print("Callback function hit!\n");
  Print("2s to wait");
 
  // Create a function
  Status = gBS->CreateEvent(EVT_TIMER, 0, NULL, NULL, &TimerEvent);
  if (EFI_ERROR(Status)) return Status;
 
  // Set a timer
  Status = gBS->SetTimer(TimerEvent, TimerRelative, 20000000);
  if (EFI_ERROR(Status)) {
    gBS->CloseEvent(TimerEvent);
    return Status;
  }
 
  gBS->WaitForEvent(1, &TimerEvent, NULL);
  Print("Resume after pause!\n");
 
  gBS->CloseEvent(TimerEvent);
  return EFI_SUCCESS;
}

这个回调只是打印信息,暂停2s后便退出。
完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include <Uefi.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/DebugLib.h>
#include <Guid/EventGroup.h>
 
EFI_GUID gMyCustomProtocolGuid = {0x}; // 替换GUID
//A4F610BB-CBA1-01E0-1135-FB00E055C6AF
 
typedef struct _MY_CUSTOM_PROTOCOL {
  UINT32 Version;
  EFI_STATUS (EFIAPI *Function)(IN UINTN Number);
} MY_CUSTOM_PROTOCOL;
 
VOID
EFIAPI
CallbackFunction(
    IN EFI_EVENT    Event,
    IN VOID         *Context
);
 
EFI_STATUS
EFIAPI
InitializeMyDriver(
    IN EFI_HANDLE        ImageHandle,
    IN EFI_SYSTEM_TABLE  *SystemTable
)
{
    EFI_STATUS          Status = EFI_SUCCESS;
    MY_CUSTOM_PROTOCOL  *Protocol = NULL;
    EFI_EVENT           Event;
    EFI_HANDLE          Handle = NULL;
 
    DEBUG((EFI_D_INFO, "[MyDriver] Initializing...\n"));
 
    Status = gBS->AllocatePool(
        EfiBootServicesData,
        sizeof(MY_CUSTOM_PROTOCOL),
        (VOID**)&Protocol
    );
     
    if (EFI_ERROR(Status)) {
        DEBUG((EFI_D_ERROR, "[MyDriver] Memory allocation failed! %r\n", Status));
        return Status;
    }
 
 
    Protocol->Revision = 1;
    Protocol->Function = CallbackFunction;
 
    Status = gBS->CreateEventEx(
        EVT_NOTIFY_SIGNAL,
        TPL_CALLBACK,
        CallbackFunction,
        NULL,
        &gEfiEventReadyToBootGuid,
        &Event
    );
 
    if (EFI_ERROR(Status)) {
        DEBUG((EFI_D_ERROR, "[MyDriver] Event creation failed! %r\n", Status));
        gBS->FreePool(Protocol);
        return Status;
    }
 
    Status = gBS->InstallProtocolInterface(
        &Handle,
        &gMyCustomProtocolGuid,
        EFI_NATIVE_INTERFACE,
        Protocol
    );
 
    if (EFI_ERROR(Status)) {
        DEBUG((EFI_D_ERROR, "[MyDriver] Protocol installation failed! %r\n", Status));
        gBS->CloseEvent(Event);
        gBS->FreePool(Protocol);
        return Status;
    }
 
    DEBUG((EFI_D_INFO, "[MyDriver] Initialization completed successfully\n"));
    return Status;
}
 
VOID
EFIAPI
CallbackFunction(
    IN EFI_EVENT    Event,
    IN VOID         *Context
)
{
    EFI_STATUS  Status;
  EFI_EVENT   TimerEvent;
 
  Print("Callback function hit!\n");
  Print("2s to wait")
 
  // Create a function
  Status = gBS->CreateEvent(EVT_TIMER, 0, NULL, NULL, &TimerEvent);
  if (EFI_ERROR(Status)) return Status;
 
  // Set a timer
  Status = gBS->SetTimer(TimerEvent, TimerRelative, 20000000);
  if (EFI_ERROR(Status)) {
    gBS->CloseEvent(TimerEvent);
    return Status;
  }
 
  gBS->WaitForEvent(1, &TimerEvent, NULL);
  Print("Resume after pause!\n");
 
  gBS->CloseEvent(TimerEvent);
  return EFI_SUCCESS;
 
}

2.直接修改SystemTable

注意:这是一个非标准实现方法,如果直接介入引导过程会导致系统不稳定,所以这里只实现拦截Print的输出。
在UEFI环境中,所有程序均运行在同一层级,所以直接修改UEFI的SystemTable实现HOOK,类似于Windows的SSDT HOOK。在EFI程序的入口点,传递了指向SystemTable的指针,所以通过LocateProtocol可以直接定位目标函数进而实现HOOK。

目标Protocol的获取与修改

1
2
3
4
5
6
7
8
//get the pointer
gBootSerivces = SystemTable->BootServices;
 
efiStatus= gBootServices->LocateProtocol(
        &gEfiSimpleTextOutProtocolGuid,
        NULL,
        (VOID**)&OriginalConOut
    );

这样,我们便获取了指向目标Protocol的指针。

1
2
3
4
//save the original one
OriginalOutputString = OriginalConOut->OutputString;
//Hook it
OriginalConOut->OutputString = CustomOutputString;

实现HOOK并保留原始函数指针以便恢复或调用。在这里,为了演示方便,CustomOutputString函数会打印HOOKED:Strings After Hooked。

1
2
3
4
5
6
7
8
9
10
11
EFI_STATUS
EFIAPI
CustomOutputString(
  IN EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This,
  IN CHAR16 *String
)
{
 
  Print(L"HOOKED:Strings After Hooked\n");
  return OriginalOutputString(This, String);
}

存在的问题

1.Access Violation
UEFI对SystemTable实现了WriteProtect,CR0的WP位(第16位)用于防止代码修改只读页面,如果直接修改,会触发Access Violation,就像这样:

但由于在UEFI环境中,所有程序均运行在同一层级,所以可以直接写入CR0寄存器绕过保护。

使用汇编修改CR0,注意不用刷新TLB(invlpg),因为分页还未开启。

1
2
3
4
5
6
7
8
9
10
11
12
;nasm asm code
 
global AsmReadCr0
AsmReadCr0:
    mov rax cr0
    ret
 
global AsmWriteCR0
AsmWriteCR0:
    mov cr0 rcx
    mov rax rcx
    ret

C的调用

1
2
3
4
UINTN Cr0 = AsmReadCr0();
AsmWriteCR0(Cr0 & ~0x10000);
OriginalConOut->OutputString = CustomOutputString;
AsmWriteCR0(CR0);

完整的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <Uefi.h>
#include <intrin.h> //breakpoint lib
#include <Library/PcdLib.h>
#include <Library/UefiLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiApplicationEntryPoint.h>
 
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *OriginalConOut = NULL;
EFI_TEXT_STRING OriginalOutputString = NULL;
 
EFI_STATUS
EFIAPI
CustomOutputString(
  IN EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *This,
  IN CHAR16 *String
)
{
 
  Print(L"HOOKED:Strings After Hooked\n");
  return OriginalOutputString(This, String);
}
 
EFI_STATUS
EFIAPI
HookPrint(
  IN BOOLEAN isForceDebug
)
{
  EFI_STATUS efiStatus;
  CpuBreakPoint();
  if(!isForceDebug){
    return EFI_SUCCESS;
  }
 
 
  efiStatus= gBS->LocateProtocol(
    &gEfiSimpleTextOutProtocolGuid,
    NULL,
    (VOID**)&OriginalConOut
  );
  CpuBreakPoint();
  if(EFI_ERROR(efiStatus)){
    return efiStatus;
  }
 
  OriginalOutputString = OriginalConOut->OutputString;
  CpuBreakPoint();
  UINTN Cr0 = GetCR0();
  WriteCR0(Cr0 & ~0x10000);
  OriginalConOut->OutputString = CustomOutputString;
  WriteCR0(CR0);
 
  return EFI_SUCCESS;
}
 
EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  UINT32  Index;
  EFI_STATUS efiStatus;
 
  Print(L"Strings before hook");
  efiStatus = HookPrint(TRUE);
  Print(L"String After Hooked");
  return efiStatus;
}


但是,这对于启用SecureBoot的机器无用,

那么是否可以这样复制协议实例到可写内存,再重新安装协议

1
2
3
4
5
6
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *NewConOut;
gBS->AllocatePool(EfiBootServicesData, sizeof(*NewConOut), (VOID**)&NewConOut);
CopyMem(NewConOut, ConOut, sizeof(*NewConOut));
NewConOut->OutputString = CustomOutputString;
// Install a new one
// ...

但是这个就没有深入实现了,因为Emulator的环境不支持,而在实机环境就炸了....

总结

对Windows启动过程的介绍,可以看这个WindowsUEFI启动介绍
欢迎批评指正


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2025-5-6 23:13 被TurkeybraNC编辑 ,原因:
收藏
免费 3
支持
分享
最新回复 (1)
雪    币: 4064
活跃值: (4397)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
2
感谢分享。
2025-5-4 19:02
0
游客
登录 | 注册 方可回帖
返回