本文将详细介绍Secure Boot 相关的概念,包括 密钥的生成 以及 如何签名EFI文件,如何在UEFI Shelle下执行签名文件,BIOS中如何实现Secure Boot等
目录
安全启动(Secure Boot)是一种计算机系统的安全功能,旨在确保系统启动过程中只能加载经过数字签名的受信任的操作系统和启动加载程序。通过使用安全启动,系统可以防止恶意软件在启动过程中植入并运行,提高系统的安全性。
安全启动的工作原理通常包括以下几个步骤:
1. **启动固件验证**:在计算机启动时,启动固件(如UEFI固件)会验证操作系统引导加载程序的数字签名,确保其来自受信任的发布者。
2. **操作系统验证**:启动加载程序会验证操作系统内核和驱动程序的数字签名,以确保它们没有被篡改。
3. **启动链验证**:整个启动链中的每个组件都会被验证,包括引导加载程序、内核、驱动程序等。
4. **安全启动状态**:如果所有组件的数字签名验证通过,系统将进入安全启动状态,允许操作系统正常加载和运行。
通过安全启动,系统可以有效防止恶意软件在启动过程中进行植入和篡改,提高系统的整体安全性。安全启动通常与UEFI固件结合使用,是现代计算机系统中的重要安全功能之一。
BIOS启动过程中会执行BootLoader来加载系统,最常用的就是GRUB,它可能是一个名为bootx64.efi的UEFI应用。BIOS启动的最后会将控制权交给这个bootx64.efi,并由后者来启动系统。但是这里存在一个问题,如何保证这个应用真的是我们需要的呢?如果该应用被修改甚至替换了,导致执行一些我们不希望其执行的代码,则是一个非常严重的安全漏洞。
EDK中的Security Boot模块:
Platform Key (PK) - PK 是用于在硬件平台层和硬件平台拥有者建立起的信任关系 ,规定一个硬件平台只能被一个拥有者所拥有,即 PK 只能存在一个 ,与拥有者相关的公钥被存储在 FLASH 里面的 PK 变量里面,同时,拥有者的私钥可以来对 PK, KEK, db, dbx 进行签名和管理。
Key Exchange Key (KEK) - KEK 是用于在硬件平台和操作系统之间建立信任关系 ,KEK 的公钥可以在主板的 FLASH 存在多个不同项,即 KEK 可以存在多个 ,每一项对应一种可以被启动的操作系统,同样,KEK 的私钥可以来对 db, dbx 进行签名和管理。
Database (db) - db 是用于对 被许可的 EFI 文件予以加载的数据签名库 ,和 KEK 一样, db 的公钥可以存在很多项 。(在 UEFI 平台里面,操作系统加载文件就是一个 EFI 文件)
Database Excluded (dbx) - dbx 是一个 黑名单数据签名库 ,只要谁的 EFI 签名在这,谁就被屏蔽掉, dbx 的公钥也可以存在很多项 。后面不会使用这个。
测试方法:
建立 PK,KEK,db 密钥对,同时还需要创建EFI list。
1. 加载PK。 ROOTCA.cer
2. 加载 KEKCA.cer。
3. 加载DB。 UEFICA。
当不导入PK DB KEK 文件时
运行不加签名的文件和加签名的文件,都会 显示Access Denied. 原因就是Security Boot 校验Load的文件没有权限
导入加载PK。 ROOTCA.cer 导入 KEKCA.cer。 导入DB
保存后进Shell, 运行加签名的文件, 能运行。 代表进行签名后的efi才允许运行。对于BIOS 启动到OS 会去加载bootx64.efi 或者grubx64.efi
当打开Security boot 时就会去验证efi。
我们怎么签名Efi文件和启动项,怎么导入对应的证书呢?
1、生成证书
需要三个证书: PK、KEK、DB
运行以下脚本后会生成一组PK KEK DB 用以下证书就可以去进行证书导入以及签名efi 文件
#!/bin/bash
echo -n "请输入一个通用名,比如公司名称或个人名字: "
read NAME
openssl req -new -x509 -newkey rsa:2048 -subj "/CN=$NAME PK/" -keyout PK.key \
-out PK.crt -days 3650 -nodes -sha256
openssl req -new -x509 -newkey rsa:2048 -subj "/CN=$NAME KEK/" -keyout KEK.key \
-out KEK.crt -days 3650 -nodes -sha256
openssl req -new -x509 -newkey rsa:2048 -subj "/CN=$NAME DB/" -keyout DB.key \
-out DB.crt -days 3650 -nodes -sha256
openssl x509 -in PK.crt -out PK.cer -outform DER
openssl x509 -in KEK.crt -out KEK.cer -outform DER
openssl x509 -in DB.crt -out DB.cer -outform DER
echo $(uuidgen) > myGUID.txt
cert-to-efi-sig-list -g $GUID PK.crt PK.esl
cert-to-efi-sig-list -g $GUID KEK.crt KEK.esl
cert-to-efi-sig-list -g $GUID DB.crt DB.esl
rm -f noPK.esl
touch noPK.esl
sign-efi-sig-list -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
-k PK.key -c PK.crt PK PK.esl PK.auth
sign-efi-sig-list -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
-k PK.key -c PK.crt PK noPK.esl noPK.auth
sign-efi-sig-list -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
-k PK.key -c PK.crt KEK KEK.esl KEK.auth
sign-efi-sig-list -t "$(date --date='1 second' +'%Y-%m-%d %H:%M:%S')" \
-k KEK.key -c KEK.crt db DB.esl DB.auth
echo ""
echo "For use with KeyTool, copy the *.auth and *.esl files to a FAT USB"
echo "flash drive or to your EFI System Partition (ESP)."
echo "For use with most UEFIs' built-in key managers, copy the *.cer files."
echo ""
运行该脚本后生成证书
sbsign --key db.key --cert db.crt --output grubx64sign.efi.signed grub64.efi
apt-get install sbsigntool
导入证书后, Sign过的efi 才能运行 否则会返回无权限。
当不添加DB 时去加载签名过的EFI 会显示 Image被签名但是不在DB中 。 PK是可以去校验签名 但是DB时我们的签名白名单支持库。
3.基本原理:看看BIOS中如何实现Security Boot
在BIOS中加载EFI File会使用gbs->LoadImage去加载镜像,调用的的函数原型是 CoreLoadImageCommon
该函数就包含了安全校验
Security2StubAuthenticate -->Defer3rdPartyImageLoad
Security2StubAuthenticate -->Defer3rdPartyImageLoad
使用证书签名文件后,可以看到会在EFI文件后面加一段密钥相关的二进制内容 里面就包含了使用的证书公钥等一些信息
BIOS中校验File中是否由DB/DBX的Signature,主要使用IsSignatureFoundInDatabase 去检查。
///
/// Varialbe name with guid EFI_IMAGE_SECURITY_DATABASE_GUID
/// for the authorized signature database.
///
#define EFI_IMAGE_SECURITY_DATABASE L"db"
///
/// Varialbe name with guid EFI_IMAGE_SECURITY_DATABASE_GUID
/// for the forbidden signature database.
///
#define EFI_IMAGE_SECURITY_DATABASE1 L"dbx"
EFI_STATUS
IsSignatureFoundInDatabase (
IN CHAR16 *VariableName,
IN UINT8 *Signature,
IN EFI_GUID *CertType,
IN UINTN SignatureSize,
OUT BOOLEAN *IsFound
)
{
EFI_STATUS Status;
EFI_SIGNATURE_LIST *CertList;
EFI_SIGNATURE_DATA *Cert;
UINTN DataSize;
UINT8 *Data;
UINTN Index;
UINTN CertCount;
//
// Read signature database variable.
//
*IsFound = FALSE;
Data = NULL;
DataSize = 0;
Status = gRT->GetVariable (VariableName, &gEfiImageSecurityDatabaseGuid, NULL, &DataSize, NULL);
if (Status != EFI_BUFFER_TOO_SMALL) {
if (Status == EFI_NOT_FOUND) {
//
// No database, no need to search.
//
Status = EFI_SUCCESS;
}
return Status;
}
Data = (UINT8 *)AllocateZeroPool (DataSize);
if (Data == NULL) {
return EFI_OUT_OF_RESOURCES;
}
Status = gRT->GetVariable (VariableName, &gEfiImageSecurityDatabaseGuid, NULL, &DataSize, Data);
if (EFI_ERROR (Status)) {
goto Done;
}
//
// Enumerate all signature data in SigDB to check if signature exists for executable.
//
CertList = (EFI_SIGNATURE_LIST *)Data;
while ((DataSize > 0) && (DataSize >= CertList->SignatureListSize)) {
CertCount = (CertList->SignatureListSize - sizeof (EFI_SIGNATURE_LIST) - CertList->SignatureHeaderSize) / CertList->SignatureSize;
Cert = (EFI_SIGNATURE_DATA *)((UINT8 *)CertList + sizeof (EFI_SIGNATURE_LIST) + CertList->SignatureHeaderSize);
if ((CertList->SignatureSize == sizeof (EFI_SIGNATURE_DATA) - 1 + SignatureSize) && (CompareGuid (&CertList->SignatureType, CertType))) {
for (Index = 0; Index < CertCount; Index++) {
if (CompareMem (Cert->SignatureData, Signature, SignatureSize) == 0) {
//
// Find the signature in database.
//
*IsFound = TRUE;
//
// Entries in UEFI_IMAGE_SECURITY_DATABASE that are used to validate image should be measured
//
if (StrCmp (VariableName, EFI_IMAGE_SECURITY_DATABASE) == 0) {
SecureBootHook (VariableName, &gEfiImageSecurityDatabaseGuid, CertList->SignatureSize, Cert);
}
break;
}
Cert = (EFI_SIGNATURE_DATA *)((UINT8 *)Cert + CertList->SignatureSize);
}
if (*IsFound) {
break;
}
}
DataSize -= CertList->SignatureListSize;
CertList = (EFI_SIGNATURE_LIST *)((UINT8 *)CertList + CertList->SignatureListSize);
}
Done:
if (Data != NULL) {
FreePool (Data);
}
return Status;
}
1. 首先,函数会尝试从变量中读取签名数据库的数据。
2. 如果成功读取到数据,函数会遍历签名列表,逐个比较签名数据,查找是否存在与给定签名匹配的条目。
3. 如果找到匹配的签名,函数会将 `IsFound` 设置为 `TRUE`,表示找到了该签名。
4. 如果在 `EFI_IMAGE_SECURITY_DATABASE` 中找到了签名,还会调用 `SecureBootHook` 函数来处理安全启动相关的操作。
主要是将变量度量信息存入到TPM PCR中
VOID
EFIAPI
SecureBootHook (
IN CHAR16 *VariableName,
IN EFI_GUID *VendorGuid,
IN UINTN DataSize,
IN VOID *Data
)
{
EFI_STATUS Status;
if (!IsSecureAuthorityVariable (VariableName, VendorGuid)) {
return;
}
if (IsDataMeasured (VariableName, VendorGuid, Data, DataSize)) {
DEBUG ((DEBUG_ERROR, "MeasureSecureAuthorityVariable - IsDataMeasured\n"));
return;
}
Status = MeasureVariable (
VariableName,
VendorGuid,
Data,
DataSize
);
DEBUG ((DEBUG_INFO, "MeasureBootPolicyVariable - %r\n", Status));
if (!EFI_ERROR (Status)) {
AddDataMeasured (VariableName, VendorGuid, Data, DataSize);
}
return;
}
然后就是BIOS Setup代码实现
SECUREBOOT_CONFIG_PRIVATE_DATA mSecureBootConfigPrivateDateTemplate = {
SECUREBOOT_CONFIG_PRIVATE_DATA_SIGNATURE,
{
SecureBootExtractConfig,
SecureBootRouteConfig,
SecureBootCallback
}
};
HiiHandle = HiiAddPackages (
&gSecureBootConfigFormSetGuid,
DriverHandle,
SecureBootConfigDxeStrings,
SecureBootConfigBin,
NULL
);
Vfr中都使用Label 在C中动态更新页面:
多个Lable 都在C中去动态更新
label FORMID_ENROLL_PK_FORM;
label LABEL_END;
label FORMID_ENROLL_KEK_FORM;
label LABEL_END;
label SECUREBOOT_ENROLL_SIGNATURE_TO_DB;
label LABEL_END;
label SECUREBOOT_ENROLL_SIGNATURE_TO_DBX;
label LABEL_END;
label FORMID_ENROLL_PK_FORM;
label LABEL_END;
详细信息都可以参考EDKII 的源码:
- 签名EFI文件:使用证书去sign efi文件 使用到sbsigntool 即可
- 签名过程