什么是Secure boot
Secure boot,顾名思义,就是安全启动,安全启动是由 PC 行业成员开发的安全标准,旨在帮助确保设备仅使用原始设备制造商 (OEM) 信任的软件启动。当 PC 启动时,固件会检查每个启动软件的签名,包括 UEFI 固件驱动程序(也称为 Option ROM)、EFI 应用程序和操作系统。如果签名有效,则 PC 将启动,固件将控制权交给操作系统。
OEM 可以使用固件制造商的说明创建安全启动密钥并将其存储在电脑固件中。添加 UEFI 驱动程序时,还需要确保这些驱动程序已签名并包含在安全启动数据库中。
首先我们看下证书有哪些:
以及他们放置的地方,这些放置在fdf文件中,通过guid进行索引:
Key都是成对的,即公钥与私钥,公钥放置在了BIOS中,私钥用来给需要在主板上运行的bootloader、驱动以及程序进行签名使用。
怎么验证安全启动是有效的
使用U盘,制作一个shell盘,在安全启动开启安装key的前提下,是不可以进入shell的,而经过签名的shell程序,是能正常进入的。
安全引导的密钥类型
数据库密钥(db)——它用于对运行的二进制文件(引导加载程序、引导管理器、shell、驱动程序等)进行签名或验证。db可以保存多个KEY值——对于某些目的来说,这是一个重要的事实。注意,db可以包含公钥(与可用于对多个二进制文件签名的私钥相匹配)和hash值(用于描述单个二进制文件)。
数据库黑名单(dbx)——dbx是一种反数据库;它包含与已知恶意软件或其他不受欢迎的软件相对应的键和hash值。可以像安装db一样安装键或hash值。如果二进制文件匹配同时存在于db和dbx中的键或hash值,则dbx应该优先。
密钥交换密钥(KEK)——KEK用于对密钥(公钥)进行签名,以便固件在将它们输入数据库(db或dbx)时将它们视为有效的。如果没有KEK,固件将无法知道新密钥是有效的还是由恶意软件提供的。因此,在没有KEK的情况下,安全引导将是一个笑话,或者要求数据库保持静态(更新db数据库时验证)。由于安全启动的临界点是dbx,因此静态数据库将不可用。电脑通常有两个kek,一个来自系统厂商,一个来自主板制造商。这使得任何一方都可以发布更新。
平台密钥(PK)——PK是安全启动中的顶级密钥,它与KEK相关的功能类似于KEK与db和dbx的功能。UEFI安全启动支持单个PK,通常由主板制造商提供。因此,只有主板制造商可以完全控制计算机。自己控制安全启动过程的一个重要部分是用自己生成的PK放在主板flash中,递级验证来保证安全启动的可靠性。
代码梳理
安全启动前的准备:
主要准备条件在dsc、fdf文件中,也就是我们需要哪些文件去进行支持
1、需要支持的库
IntrinsicLib|CryptoPkg/Library/IntrinsicLib/IntrinsicLib.inf
AuthVariableLib|SecurityPkg/Security/Library/AuthVariableLib26/AuthVariableLib.inf
BaseCryptLib|CryptoPkg/Library/BaseCryptLib/BaseCryptLib.inf
PlatformSecureLib|SecurityPkg/Library/PlatformSecureLib/PlatformSecureLib.inf
2、Policy的定义选择,也就是哪些文件需要什么样的验证方式
gEfiSecurityPkgTokenSpaceGuid.PcdOptionRomImageVerificationPolicy|0x04
gEfiSecurityPkgTokenSpaceGuid.PcdFixedMediaImageVerificationPolicy|0x04
gEfiSecurityPkgTokenSpaceGuid.PcdRemovableMediaImageVerificationPolicy|0x04
2、安全启动的配置文件,这里面包含了界面的显示,安全启动下Setup mode(工厂模式)User mode(用户模式)的文件加载、数据库的建立以及variable的生成,这里面涉及到怎么把前面提到的证书通过索引导入固件建立数据库,并存放在nvrom区域。
Security/SecureBootConfigDxe/SecureBootConfigDxe.inf
3、验签文件,也就是验证执行的文件是否是经过验签的:
MdeModulePkg/Universal/SecurityStubDxe/SecurityStubDxe.inf {
<LibraryClasses>
NULL|SecurityPkg/Library/DxeImageVerificationLib/DxeImageVerificationLib.inf
!if $(TPM_SUPPORT) == TRUE
NULL|SecurityPkg/Library/DxeTpm2MeasureBootLib/DxeTpm2MeasureBootLib.inf
!endif
}
4、除了正常的botloader和驱动程序外,机器还可以通过网络进行启动,这一部分同样需要进行验签才能运行,这就需要用到下列的文件。
NetworkPkg/TlsDxe/TlsDxe.inf
NetworkPkg/TlsAuthConfigDxe/TlsAuthConfigDxe.inf
5、nvrom开辟区域供安全启动使用
gEfiMdeModulePkgTokenSpaceGuid.PcdMaxVariableSize|0x10000
NorFlashDxe/NorFlashAuthenticatedDxe.inf
如何进行验签:
在加载生成DB数据库前,需要先了解文件是如何进行验签,先看一张图,后续根据代码了解图片中的内容:
咱们以option ROM文件验签为例子,上文中我们看到PcdOptionRomImageVerificationPolicy,在代码中查询,找到位置:DxeImageVerificationLib.C下的DxeImageVerificationHandler函数:
//
// Check the image type and get policy setting.
//
switch (GetImageType (File)) {
case IMAGE_FROM_FV:
Policy = ALWAYS_EXECUTE;
break;
case IMAGE_FROM_OPTION_ROM:
Policy = PcdGet32 (PcdOptionRomImageVerificationPolicy);
break;
case IMAGE_FROM_REMOVABLE_MEDIA:
Policy = PcdGet32 (PcdRemovableMediaImageVerificationPolicy);
break;
case IMAGE_FROM_FIXED_MEDIA:
Policy = PcdGet32 (PcdFixedMediaImageVerificationPolicy);
break;
default:
Policy = DENY_EXECUTE_ON_SECURITY_VIOLATION;
break;
}
看下Policy值的定义:
//
// Image type definitions.
//
#define IMAGE_UNKNOWN 0x00000001
#define IMAGE_FROM_FV 0x00000002
#define IMAGE_FROM_OPTION_ROM 0x00000004
#define IMAGE_FROM_REMOVABLE_MEDIA 0x00000008
#define IMAGE_FROM_FIXED_MEDIA 0x00000010
//
// Authorization policy bit definition
//
#define ALWAYS_EXECUTE 0x00000000
#define NEVER_EXECUTE 0x00000001
#define ALLOW_EXECUTE_ON_SECURITY_VIOLATION 0x00000002
#define DEFER_EXECUTE_ON_SECURITY_VIOLATION 0x00000003
#define DENY_EXECUTE_ON_SECURITY_VIOLATION 0x00000004
#define QUERY_USER_ON_SECURITY_VIOLATION 0x00000005
通过判断,如果总是执行或者从不执行,则直接返回,然后我们再去找secureboot的variable,判断它是开还是关的,是关的那么直接不验证,也就是正常启动,如果是开的,咱们再继续进行下一步,获取DOS的header
//
// Read the Dos header.
//
if (FileBuffer == NULL) {
return EFI_INVALID_PARAMETER;
}
mImageBase = (UINT8 *) FileBuffer;
mImageSize = FileSize;
ZeroMem (&ImageContext, sizeof (ImageContext));
ImageContext.Handle = (VOID *) FileBuffer;
ImageContext.ImageRead = (PE_COFF_LOADER_READ_FILE) DxeImageVerificationLibImageRead;
判断是否为有效的PE image:
//
// Get information about the image being loaded
//
Status = PeCoffLoaderGetImageInfo (&ImageContext);
if (EFI_ERROR (Status)) {
//
// The information can't be got from the invalid PeImage
//
DEBUG ((DEBUG_INFO, "DxeImageVerificationLib: PeImage invalid. Cannot retrieve image information.\n"));
goto Done;
}
如果是有效的,再继续获取PE header,然后检查PE/COFF image---> Get the magic value from the PE/COFF Optional Header-->Use PE32 offset.-->Use PE32+ offset-->Start Image Validation.
前面的直接略过了,最重要的image验证就此开始,首先将image的hash值进行匹配,查看其是否是验签的,如果没有验签,则查看hash值数据是否在DB的数据库中存在,且不存在DBX的数据库中,如果符合的话,验证通过,如果是验签的,里面包含验签的信息,则遍历DB数据库中的证书,查看是否有符合的,如果有符合的且不再DBX数据库中,验证通过,如果都不满足,则说明这个验签的image不符合。具体代码就不贴了,代码有点多,逐句分析也有些晦涩难懂,不过意思大概就是这么个意思。下面来说下SecureBootConfigDxe配置文件
SecureBootConfigDxe
这里面会有UNI、VFR文件等,也就是会在BIOS设置界面生成一个选项,供用户选择,当启动安全模式的时候,会有两个模式,工厂模式和用户模式,entrypoint什么的就不介绍了,还有些variable相关的设置,主要说下这里面是怎么将证书导入生成DB数据库以及DBX数据库的。
供后续理解:
typedef struct{
EFI_GUID *VarGuid;
CHAR16 *VarName;
} AUTHVAR_KEY_NAME;
STATIC AUTHVAR_KEY_NAME gSecureKeyVariableList[] = {
{&gEfiGlobalVariableGuid, EFI_PLATFORM_KEY_NAME}, // PK 0
{&gEfiGlobalVariableGuid, EFI_KEY_EXCHANGE_KEY_NAME}, // KEK 1
{&gEfiImageSecurityDatabaseGuid, EFI_IMAGE_SECURITY_DATABASE}, // db 2
{&gEfiImageSecurityDatabaseGuid, EFI_IMAGE_SECURITY_DATABASE1}, // dbx 3
};
STATIC
struct{
EFI_GUID *File;
CHAR16 *UiName;
AUTHVAR_KEY_NAME *KeyName;
UINT8 *Data;
UINTN DataSize;
EFI_GUID *SignatureOwner;
}gSecureKeyTable[] = {
{(EFI_GUID*)PcdGetPtr(PcdSecureKeyDBFile), L"DB", &gSecureKeyVariableList[2], NULL, 0, &gMySignatureOwnerGuid},
{(EFI_GUID*)PcdGetPtr(PcdSecureKeyMSKEKFile), L"MSKEK", &gSecureKeyVariableList[1], NULL, 0, &gMicrosoftSignatureOwnerGuid},
{(EFI_GUID*)PcdGetPtr(PcdSecureKeyMSProFile), L"MSPro", &gSecureKeyVariableList[2], NULL, 0, &gMicrosoftSignatureOwnerGuid},
{(EFI_GUID*)PcdGetPtr(PcdSecureKeyMSUEFFile), L"MSUEF", &gSecureKeyVariableList[2], NULL, 0, &gMicrosoftSignatureOwnerGuid},
{(EFI_GUID*)PcdGetPtr(PcdSecureKeyPKFile), L"PK", &gSecureKeyVariableList[0], NULL, 0, &gMySignatureOwnerGuid},
};
以增加DBXkey为例子:
STATIC
EFI_STATUS
AddDbxInitKey (
VOID
)
{
EFI_STATUS Status;
Status = AppendX509FromFV(
(EFI_GUID*)PcdGetPtr(PcdSecureKeyMSDBXFile),
gSecureKeyVariableList[3].VarName,
gSecureKeyVariableList[3].VarGuid,
&gMicrosoftSignatureOwnerGuid
);
DEBUG((EFI_D_INFO, "%a() %r\n", __FUNCTION__, Status));
return Status;
}
STATIC
EFI_STATUS
AppendX509FromFV(
IN EFI_GUID *CertificateGuid,
IN CHAR16 *VariableName,
IN EFI_GUID *VendorGuid,
IN EFI_GUID *SignatureOwner
)
{
EFI_STATUS Status;
VOID *Data;
UINTN DataSize;
UINTN SigDBSize;
UINT32 Attr;
UINTN X509DataSize;
VOID *X509Data;
X509DataSize = 0;
X509Data = NULL;
SigDBSize = 0;
DataSize = 0;
Data = NULL;
Status = GetX509Cert( CertificateGuid, &X509Data,&X509DataSize);
if (EFI_ERROR (Status)) {
goto ON_EXIT;
}
SigDBSize = X509DataSize;
Data = AllocateZeroPool (SigDBSize);
if (Data == NULL) {
Status = EFI_OUT_OF_RESOURCES;
goto ON_EXIT;
}
CopyMem ((UINT8* )Data, X509Data, X509DataSize);
Attr = EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_RUNTIME_ACCESS
| EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS;
//
// Check if signature database entry has been already existed.
// If true, use EFI_VARIABLE_APPEND_WRITE attribute to append the
// new signature data to original variable
//
Status = gRT->GetVariable(
VariableName,
VendorGuid,
NULL,
&DataSize,
NULL
);
if (Status == EFI_BUFFER_TOO_SMALL) {
Attr |= EFI_VARIABLE_APPEND_WRITE;
} else if (Status != EFI_NOT_FOUND) {
goto ON_EXIT;
}
Status = gRT->SetVariable(
VariableName,
VendorGuid,
Attr,
SigDBSize,
Data
);
STATIC
EFI_STATUS
GetX509Cert (
IN EFI_GUID *ImageGuid,
OUT VOID **DefaultsBuffer,
OUT UINTN *DefaultsBufferSize
)
{
EFI_STATUS Status;
EFI_FIRMWARE_VOLUME2_PROTOCOL *Fv;
UINTN FvProtocolCount;
EFI_HANDLE *FvHandles;
UINTN Index1;
UINT32 AuthenticationStatus;
*DefaultsBuffer = NULL;
*DefaultsBufferSize = 0;
FvHandles = NULL;
Status = gBS->LocateHandleBuffer (
ByProtocol,
&gEfiFirmwareVolume2ProtocolGuid,
NULL,
&FvProtocolCount,
&FvHandles
);
if (!EFI_ERROR (Status)) {
for (Index1 = 0; Index1 < FvProtocolCount; Index1++) {
Status = gBS->HandleProtocol (
FvHandles[Index1],
&gEfiFirmwareVolume2ProtocolGuid,
(VOID **) &Fv
);
*DefaultsBufferSize= 0;
Status = Fv->ReadSection (
Fv,
ImageGuid,
EFI_SECTION_RAW,
0,
DefaultsBuffer,
DefaultsBufferSize,
&AuthenticationStatus
);
最终将这些获取到的数据进行导入处理;
STATIC
EFI_STATUS
AddSecureBootVarNoAuth(
CHAR16 *VarName,
EFI_GUID *VarGuid,
CONST UINT8 *CertData,
UINTN CertDataSize,
EFI_GUID *SignatureOwner
)
{
EFI_STATUS Status;
EFI_SIGNATURE_LIST *SignList;
UINTN SignListSize;
UINT8 *VarData;
UINTN VarDataSize;
UINT32 Attribute;
UINTN DataSize;
EFI_SIGNATURE_DATA *SignData;
SignList = NULL;
VarData = NULL;
SignListSize = sizeof(EFI_SIGNATURE_LIST) + OFFSET_OF(EFI_SIGNATURE_DATA, SignatureData) + CertDataSize;
SignList = (EFI_SIGNATURE_LIST*)AllocatePool(SignListSize);
if(SignList == NULL){
Status = EFI_OUT_OF_RESOURCES;
goto ProcExit;
}
CopyMem(&SignList->SignatureType, &gEfiCertX509Guid, sizeof(EFI_GUID));
SignList->SignatureListSize = (UINT32)SignListSize;
SignList->SignatureHeaderSize = 0;
SignList->SignatureSize = (UINT32)(OFFSET_OF(EFI_SIGNATURE_DATA,SignatureData) + CertDataSize);
SignData = (EFI_SIGNATURE_DATA*)((UINT8*)SignList + sizeof(EFI_SIGNATURE_LIST));
CopyMem(&SignData->SignatureOwner, SignatureOwner, sizeof(EFI_GUID));
CopyMem(&SignData->SignatureData[0], CertData, CertDataSize);
VarData = (UINT8*)SignList;
VarDataSize = SignListSize;
Status = CreateDummyTimeBasedPayload(&VarDataSize, &VarData);
if(EFI_ERROR(Status)){
if((UINTN)VarData == (UINTN)SignList){
VarData = NULL;
}
goto ProcExit;
}
Attribute = EFI_VARIABLE_NON_VOLATILE |
EFI_VARIABLE_BOOTSERVICE_ACCESS |
EFI_VARIABLE_RUNTIME_ACCESS |
EFI_VARIABLE_TIME_BASED_AUTHENTICATED_WRITE_ACCESS;
if(StrCmp(VarName, EFI_PLATFORM_KEY_NAME)!=0){
DataSize = 0;
Status = gRT->GetVariable(
VarName,
VarGuid,
NULL,
&DataSize,
NULL
);
if(Status == EFI_BUFFER_TOO_SMALL){
Attribute |= EFI_VARIABLE_APPEND_WRITE;
}
}
Status = gRT->SetVariable (
VarName,
VarGuid,
Attribute,
VarDataSize,
VarData
);
配置中的选项和正常选项一样,选择什么样的功能就进行什么样的操作,还有一些内容需要仔细斟酌,不过大致的原理就是如上所说的,当然,述说的还不是那么全面,只是对安全启动有个大概的了解。