对高通启动链的一点研究下--解锁您的骁龙8gen5

上一期我们总结了一下我之前所做的一点微小的工作(当然都是启动链上的,对于内核的研究目前还是不会,当然对于华为的5%模式进行过一点研究但是比较简单因为CRC碰撞,但是当时比较得瑟(),然后呢,我还一直没有触碰过高通平台。
这个事情说起来也是很抽象,大概是有一天有人在群里说高通有啥漏洞然后发了一个47372,这个47372是个CVE,大概是说xbl_loader加载elf时候不检查program header的个数或者检查不到位然后可以弄一堆program header也就是segment的个数*program header的尺寸可以溢出什么的。
然后我想到之前我是研究过msm8916的PBL以及早期的SBL的elf的加载逻辑,实际上很严谨的,而这里却突然出现这样一个漏洞也是很感兴趣,但是呢,这个漏洞实际上出了CVE号而且影响的大多不是手机平台,所以我保持怀疑感觉这个大概被修复了。
然后过一段时间闲鱼上就已经出现了一些8e5解锁bl的信息,通过,抓包,有些人发现了gbl的逻辑。
然后群里有人发了高通的什么GBL相关的东西,这里就非常的神秘,说是高通平台在比较新的soc上会加载一个GBL,然后呢tg群里有人说了一句GBL is only signed by google,这里很容易想到问题了,这个GBL会经过高通的签名验证吗?然后我就想到紫光平台的bsp漏洞一样,可能新加的东西签名验证是独立的逻辑不完善之类的。但是当时窝比较忙再加上对高通平台不感兴趣,尤其是对UEFI平台,所以看了一眼就不管了,压根就没打算复现,而且我也不知道GBL是怎么被加载的加载的路径是什么。

后来呢,有一天,有人拉我来研究高通漏洞,窝其实最开始不大愿意因为我已经不大感兴趣了,然后当时他们是想着复现47372,而且也不知道到底在逆向什么东西,我也插不上。当时我说47372早被patch了肯定,看一下GBL相关的也没听。后来呢,结果拉我研究的人自己先到别的平台发自己复现了,然后放了一张外版还是工程机的截图,当然实际上没有复现。我倒是没觉得有啥,但是呢后面他们有跑偏了因为我说我们的重点是逆向GBL的加载路径,而他呢直接下一个暴论说什么对这个分区的任何修改都不会被执行然后我多问几句就很不耐烦让我去看他们之前的结果。我当然看了啊我说这个AI生成的是有点问题的整个流程和gsort无关但是人家也不信而且态度也不好,后来还把复现的群解散了。我寻思这样多少有点不大好。
本来我都不打算研究高通的漏洞,你这样耍我倒是勾起了我的好奇心,我倒是想要复现这个漏洞了。后来我说把这个ABL塞到一个工程机里看日志人家也不看,然后后来还因为一点小事把我给踢了。
那我觉得很抽象了,拉我过来研究结果自己没研究清楚先到处发然后真要研究时候人家又不研究了而且讨论的时候思路都统一不到一起,然后不研究了之后就开始不耐烦最后把人踢了。
另外提一下之前我手里几乎没有任何高通设备所以说我自己没办法进行研究。也买不起。
哪有这样玩的感觉非常难过呜呜呜。
于是呢,我就决定要把这个漏洞复现出来。
这个漏洞已经在2月补丁修复

现在正片开始。

之前呢,我们研究过高通835之前的平台
高通835之前的平台,启动链比较简单
PBL->SBL1->TZ/LK->KERNEL
PBL是高通的BOOTROM
而SBL1 TZ 包括9008的firehose是在EL3下运行的
所以说如果我们有没熔断的机器,那么我们可以控制整个启动链并且转储PBL
这里面我们之前研究过华为的早期高通设备,没有进行熔断,但是SBL1进行了定制,我们通过patch SBL1去除验证可以接管启动链替换成自己编译的lk1st甚至开启KVM
但是这里面需要对sbl1签测试签名,也就是要把哈希对上但是公钥可以不对。
有https://github.com/msm8916-mainline/qtestsign

对于熔断的设备,我尝试寻找过一些PBL漏洞,PBL使用高通的签名验证验证SBL1/firehorse(ELF),实际上在PBL里面这两个启动模式只是启动的介质不一样没有本质差别,会发现PBL会在加载hash segment之后先验证hash segment的签名,然后后面每加载一个segment都要先验证hash是不是hash segment对应的,最重要的是在验证完hash segment的签名之后立马就要验证elf header+prog header的hash,这导致没办法更改加载地址什么的,而hash segment的加载地址是硬编码的(当然尺寸忘了能不能溢出,能的话也没啥办法利用)

所以说这块在PBL以及下一级SBL1里面是安全的。
当然据说有神秘PBL漏洞估计是这个sahara的吧。

然后就是这个aleph大名鼎鼎的firehorse漏洞,其实这个漏洞没啥含金量但是在安全研究的早期发这个非常厉害了。
这个是说firehorse里面有一个peek poke命令可以读写任意地址
firehose运行在EL3,而且高通在PBL阶段就使用主CPU有MMU
所以说可以通过重新映射内存的方式把一段SRAM的区域映射到PBL,这样可以实现对PBL的热修补
然后重新执行这个PBL跳过后续的签名验证。
当然这里面处理的主要是一些EMMC初始化问题
https://alephsecurity.com/2018/01/22/qualcomm-edl-1/
然后有一些firehose是64位的但是PBL是32位的,从64位切换回32位比较困难。
然后fxsheep实现了https://github.com/fxsheep/firehorse_land/(fxsheep是超级巨佬tql)
后来,quarkslab又试验了一次
然后呢,后来firehose就禁用了这个peek poke命令,而且,最恶心的是,从835开始高通开始引入UEFI,然后firehorse运行在EL1了。
当然了,LG之前使用自己的SBL1还有读取图像的任意写入漏洞能够导致EL3代码执行。
还有早期的高通好像还分为sbl1 sbl2 sbl3时候还能够覆盖中断向量表。
至于在早期高通上解锁BL有这个devinfo漏洞,也就是说BL的解锁状态是devinfo的一个标志位,devinfo存储在分区中,所以修改Devinfo分区可以实现解锁。
后来呢,高通修复了,改成了还是存在devinfo,但是一旦设备熔断,那么devinfo储存在RPMB中,而RPMB需要key才能写入,key通过tz的相关组件生成。但是工程机还是看devinfo分区。(ps:这里很关键)
当然OEM修复这个漏洞方法比较极端,小米就是采取了RSA签名验证的机制,而且每次开机都会验证签名,原始的token是和cpuid有关,所以别的机子的解锁的分区刷进来是没有用的。那么这时候即使有临时安全启动绕过也不能解锁BL。

老高通其实非常有意思因为安全启动链比较简洁并且可玩性高,但是由于逐渐过时了研究的人少了。
当然有一个比较全面的也是checkm30发现的大佬写的,https://hhj4ck.github.io/qualcomm/secboot-off-qcm2150.html
接下来就来到了比较新一点的高通高通835-sm8350
从这时候开始,高通引入了一个xbl abl分区,也就是说PBL负责验证并且加载xbl和xbl_sec,然后xbl_sec在el3,其余所有组件要么在el2要么在el1,然后高通采取了双签名,也就是对于xbl_sec以及tz相关的分区高通自己还签了一层名,所以OEM也不能随意更改,包括工程机也不能。而xbl abl sm8350之前还有hyp什么的只有OEM的签名,所以工程机可以随意修改。但是这些组件的异常级别比较低。
而firehose的漏洞被修复了,所以转储PBL是困难的。
但是小米还是有改制漏洞,在高通845以及之前是离线解锁的,所以这个签名应该是固定的,有一个特制的fastboot命令写入这个签名就能实现解锁。
而一些低一点的安卓版本没有采用AVB那么对boot分区 recovery分区的校验里面有任意写入/逻辑漏洞 的问题,参见xda上关于小米的解锁漏洞。
另外联想好像新soc都有这种漏洞,好像不解锁可以自由加载recovery,非常震撼。
这个漏洞后续可以再复现(x)
那么这个xbl和abl共同组成了一个UEFI模式的引导,而ABL和XBL都变成一个分区一样的镜像里面塞很多东西。
这里面ABL还是ELF文件,但是使用UEFITOOL解压能够发现里面的子内容,说明ABL是自解压的。
而ABL的代码藏在linuxloader里面。
接下来就是pentestpartner的大名鼎鼎的CVE-2021-1931,也就是ABL的USB协议的缓冲区溢出,可以覆盖ABL代码本身,这样直接强行执行解锁函数甚至是临时绕过安全启动。
这个好像影响sdm660 sdm845 sdm835
这个漏洞暂时还没有复现但是以及有成熟的利用了后面准备复现一下(x

那么接下来就是PBL了,我一直以为PBL没有漏洞,直到某一天我搜索到一个2021年的CVE说sahara协议里面有漏洞。
那么接下来我们介绍一下高通的9008
高通通过工程线/组合键/写一些magic值/短接 的方式可以让PBL处于9008模式
这个模式PBL会初始化一个sahara的USB协议和主机通信
sahara协议可以用来发送后续的firehose引导,也可以读OEM ID PK HASH等信息,(新一点好像读不了了),然后可以读取一些内存,还有一个握手的hello packet(这里好像还有漏洞)

而发送firehose引导之后firehose相当于一个xbl,能够和主机进行USB通信,通信的格式是XML。
而这里面OEM可以定制,比如说小米加了9008授权,也就是设备发送一个token,然后主机进行签名发给设备,设备验证通过之后才能使用其他命令。
那么之前的免授权漏洞是怎么样的呢,是说如果主机不请求token那么设备里面token位置的内存是空的,是个默认值,那么这个签名是固定的,由于我们正常是把token让售后去签名,那么我们拿这个值是固定的token去签名得到的签名是固定的,那么只要这一个签名就可以给所有的同soc的设备反复使用。
这个漏洞一直持续到高通888
其他机型也有差不多的逻辑漏洞。
后来呢,是怎么修复的呢?就是加了一个验证如果之前没有请求过token那么就不接受发送的签名。

所以说,如果我们有一个sahara漏洞那么我们就能以EL3的权限执行任意代码接管整个启动链!

那么我们继续搜索发现某视频网站上有一个hexacon2023的录像,里面讲的是 there is a hole in sahara
还有一个高通的漏洞报告上面提了一个sahara里面有一个0x13的命令,是reset state machine的,利用这个可以实现代码执行绕过安全启动!那么这就看起来很复杂了,具体是怎么回事呢?
image
image
image
image
image
image
image
image
image

就是说从高通835开始PBL是64位的了,sahara协议多了一个0x13命令可以清空当前sahara的状态机然后重新执行sahara协议处理的函数,但是这里采用了一个做法是直接调用这个sahara初始化的函数而这个函数又调用sahara处理的主函数,而主函数就是处理这些命令的,所以这是一个递归,如果我们一直发送0x13这个命令那么栈顶地址就会一直减少,每次减少一个固定值,如果一直执行这个命令直到栈的空间覆盖到数据区域甚至是有函数指针的区域就可能实现任意代码执行。
但是呢,高通835的栈区域地址往上是空的一段,而MMU开启之后对这些无效地址访问会crash,并不会像mtk那样,所以说对于高通835这个漏洞无法利用
但是高通665 高通730 高通710 高通845 高通855(最多到高通855P),这些的栈区域地址往上是数据区域,而我们可以反复发送0x13命令减小栈顶地址直到到达数据区域,数据区域有一些函数指针,然后我们发现发送镜像这个命令会读取镜像的证书的签名和公钥部分,这部分储存在栈空间,所以我们发送0x13直到到达或者跨越我们想要覆盖的函数指针的上一次,然后通过发送镜像在里面魔改签名和公钥部分实现精准的函数指针的控制,这样我们就能拿到任意代码执行...
但是问题远没有这么简单,首先是MMU问题,MMU可以控制内存区域是否可执行,我们需要先通过ROP给指定内存区域的XN修改成可执行,然后在里面布置shellcode.
通过对PBL的逆向我们会发现高通双签名的逻辑,他会根据一些efuse决定是否要验证高通自己的签名和OEM的签名,但是第一个对应的efuse是默认熔断的,所以都会验证。
接下来是xbl_sec的问题,我们要知道xbl_sec的加载地址,然后可以patch xbl_sec去掉镜像验证相关的逻辑
但是新问题来了,关于ufs初始化的问题,在sahara协议下ufs不一定能够正确初始化,还有DRAM可能不一定被初始化。
然后还有MMU问题,不正确的配置MMU会导致运行到trustzone时候崩溃,因为这时候重新配置了一下MMU导致之前访问的无效地址导致的crash这里都爆发了。
image

然后我们逐级patch 启动链,甚至说可以把uefi放到HYP里面实现KVM.
但是具体实现起来还有很多问题,所以买了红米k20pro但是一直没有完整的复现,之前就是看了下PBL。
这个地方我看到已经有成熟的利用了包括oxygen和某贼工具箱。
以后还是要复现一下(x)

从sm8250开始,高通通过efuse修补了PBL使得0x13时候会处理堆栈修复了这个漏洞。
sm8350开始彻底从PBL代码修复了这个漏洞。
但是据说,sm8350仍然存在神秘PBL漏洞能够进行任意代码执行。

firehose之前还有一个XML解析的漏洞(CVE-2024)

再后来,高通
后面开始PBL和TME分开了TME负责镜像验证,但是我们不排除这里没有安全漏洞
从这里开始算是进入高通soc的新阶段

也就是高通在xbl和abl之间又加了一个UEFI
这个启动流程说实话我是不懂的。

但是有一件事很清楚,我们基本上只能在EL1的权限级别找漏洞。

为什么不能是TZ漏洞呢?因为即使在高通835之前一些TZ的漏洞利用都非常复杂
我们是通过SCM call去调用tee里面的一些命令,那么这里面可能对内存边界的检查不完善导致任意代码写,或者是缓冲区溢出,但是基本都是写入0或者很少的字节,然后进行一系列ROP链关掉各种检查才能拿到权限。而且要和tee通信需要有EL1也就是内核态的权限,但是即使我们有root都不好控制这个权限,我们需要一个内核漏洞之类的。所以这样利用难度太高,而且需要用户在正常开机状态下执行,至少我是不会。也有一种可能是我们先利用ABL漏洞,拿到完整的EL1权限,然后在这里和tee通信,这样最多能够提权到EL
2(好像有实现的)当然也许有并且有大佬研究出来了,但是我菜呜呜。

我们也不清楚高通到底做了什么改动,但是能看出来xbl做的事情减少了,很多东西交给UEFI,然后abl里面变化比较大。也就是高通希望控制OEM厂商的权限,不希望定制自己的BL锁。所以说甚至小米都换成了读标志位判断解锁状态!!而不是每次开机验证签名!!
但是呢,别高兴太早,这个解锁状态存储在RPMB里面,没有key是无法写入的,而key由tee控制,所以说没有TZ漏洞的话我们只能想办法模拟去执行这个解锁函数。也就是写入解锁状态的函数!
那么也就是只要有一个临时的代码执行权限我们就可以永久解锁BL!!

这是高通最大的变化!!那么现在我们只需要一个abl临时/永久的漏洞

然后就回想到开头我们所提到的GBL.
3048788235881d4e1d6a5587001499a8
920e6fec3621b2fd8a2e1b40c0e193c2

现在,我们要弄清楚几件事情:
GBL是一个uefi app,那么是从哪个分区加载的?
如何编译uefi app?
这个分区有没有什么格式要求?
加载这个分区之后有没有签名验证?
如果有的话是什么形式的签名验证,是否完善严谨?
解锁状态是利用哪个函数写入的?
为什么解锁状态必须在ABL时候调用函数写入?同样是EL1,为什么不能在内核里调用?
如果我们能够加载任意的UEFI App,那么能不能直接调用ABL的解锁函数?也就是说,uefi内部的函数调用的逻辑是怎样的?
ABL的加载地址是什么?

那么我们开始一点一点拆解这些问题。

首先我们要先逆向ABL看到GBL相关的加载逻辑
我们使用UEFITOOL
image

解压出linuxloader.efi之后
我们可以看到GBL的加载逻辑
image
image
image
image

就是无论如何GBL都会加载,并不会在别的分区读取什么标志位
而如果加载失败就继续后续的正常启动
而且可以看到字符串efisp
那么可能在efisp efisp_a efisp_b等分区加载GBL
sub_1b568是GetBlkIOHandles
原型是(通过网上公开的比较古老的固件得到):

GetBlkIOHandles (
                  IN      UINT32                   SelectionAttrib,
                  IN      PartiSelectFilter        *FilterData,
                  OUT     HandleInfo               *HandleInfoPtr,
                  IN OUT  UINT32*                  MaxBlkIopCnt
                 );

第二个参数的含义

typedef struct {
  EFI_GUID      *RootDeviceType;  /* GUID Selecting the root device type */
  EFI_GUID      *PartitionType;   /* Partition Type to match */
  CHAR8         *VolumeName;        /* Mounted filesystem volume name to match */
  CHAR16        *PartitionLabel;    /* Partition label to match */
} PartiSelectFilter;

PartitionLabel是分区名,也就是v658[3]
v678是efisp
第一个参数526是0x20E

#define   BLK_IO_SEL_MEDIA_TYPE_REMOVABLE          0x0001UL
#define   BLK_IO_SEL_MEDIA_TYPE_NON_REMOVABLE      0x0002UL
#define   BLK_IO_SEL_PARTITIONED_GPT               0x0004UL
#define   BLK_IO_SEL_PARTITIONED_MBR               0x0008UL
#define   BLK_IO_SEL_MATCH_PARTITION_TYPE_GUID     0x0010UL
#define   BLK_IO_SEL_SELECT_MOUNTED_FILESYSTEM     0x0020UL
#define   BLK_IO_SEL_SELECT_BY_VOLUME_NAME         0x0040UL

/* Select only the root device handle indicated. Doesn't return
 * any partitions within.
 * Currently this filter applies only for eMMC device, not the external
 * device connected via USB */
#define   BLK_IO_SEL_SELECT_ROOT_DEVICE_ONLY       0x0080UL
/* Select the handle that's on the indicated root device.
 * Currently this filter applies only for eMMC device, not the external
 * device connected via USB */
#define   BLK_IO_SEL_MATCH_ROOT_DEVICE             0x0100UL

/* Select through partition name*/
#define   BLK_IO_SEL_MATCH_PARTITION_LABEL         0x0200UL

/* Do case insensetive string comparisons */
#define   BLK_IO_SEL_STRING_CASE_INSENSITIVE       0x0400UL

就是按照分区名查找,查找gpt或者mbr的,这个分区是不可移动介质。
但是后面是怎么从分区加载uefi app不是很清晰
然后加载镜像
那么加载调用的是gBS->LoadImage,这个会进行uefi的签名验证
接下来我们来分析一些LoadImage函数
它的实现一开始我还没找到
实际上在

EFI_STATUS
EFIAPI
CoreLoadImage (
  IN BOOLEAN                    BootPolicy,
  IN EFI_HANDLE                 ParentImageHandle,
  IN EFI_DEVICE_PATH_PROTOCOL   *FilePath,
  IN VOID                       *SourceBuffer   OPTIONAL,
  IN UINTN                      SourceSize,
  OUT EFI_HANDLE                *ImageHandle
  )

签名验证是这个函数

  if (gSecurity2 != NULL) {
    //
    // Verify File Authentication through the Security2 Architectural Protocol
    //
    SecurityStatus = gSecurity2->FileAuthentication (
                                  gSecurity2,
                                  OriginalFilePath,
                                  FHand.Source,
                                  FHand.SourceSize,
                                  BootPolicy
                                  );
    if (!EFI_ERROR (SecurityStatus) && ImageIsFromFv) {
      //
      // When Security2 is installed, Security Architectural Protocol must be published.
      //
      ASSERT (gSecurity != NULL);
 
      //
      // Verify the Authentication Status through the Security Architectural Protocol
      // Only on images that have been read using Firmware Volume protocol.
      //
      SecurityStatus = gSecurity->FileAuthenticationState (
                                    gSecurity,
                                    AuthenticationStatus,
                                    OriginalFilePath
                                    );
    }
  } else if ((gSecurity != NULL) && (OriginalFilePath != NULL)) {
    //
    // Verify the Authentication Status through the Security Architectural Protocol
    //
    SecurityStatus = gSecurity->FileAuthenticationState (
                                  gSecurity,
                                  AuthenticationStatus,
                                  OriginalFilePath
                                  );
  }

这个函数要求配置gSecurity和gSecurity2

//
// DXE Core Global Variables for all of the Architectural Protocols.
// If a protocol is installed mArchProtocols[].Present will be TRUE.
//
// CoreNotifyOnArchProtocolInstallation () fills in mArchProtocols[].Event
// and mArchProtocols[].Registration as it creates events for every array
// entry.
//
EFI_CORE_PROTOCOL_NOTIFY_ENTRY  mArchProtocols[] = {
  { &gEfiSecurityArchProtocolGuid,         (VOID **)&gSecurity,      NULL, NULL, FALSE },
 
//
// Optional protocols that the DXE Core will use if they are present
//
EFI_CORE_PROTOCOL_NOTIFY_ENTRY  mOptionalProtocols[] = {
  { &gEfiSecurity2ArchProtocolGuid,        (VOID **)&gSecurity2,     NULL, NULL, FALSE },

而这些依赖于若干组件,但是核心是UEFI secure boot

PlatformSecureLib|OvmfPkg/Library/PlatformSecureLib/PlatformSecureLib.inf
TpmMeasurementLib|SecurityPkg/Library/DxeTpmMeasurementLib/DxeTpmMeasurementLib.inf
AuthVariableLib|SecurityPkg/Library/AuthVariableLib/AuthVariableLib.inf
 
gUefiOvmfPkgTokenSpaceGuid.PcdSecureBootEnable|TRUE
gEfiSecurityPkgTokenSpaceGuid.PcdOptionRomImageVerificationPolicy|0x00
 
MdeModulePkg/Universal/SecurityStubDxe/SecurityStubDxe.inf {
 <LibraryClasses>
  NULL|SecurityPkg/Library/DxeImageVerificationLib/DxeImageVerificationLib.inf
 }
SecurityPkg/VariableAuthenticated/SecureBootConfigDxe/SecureBootConfigDxe.inf

如果UEFI secure boot没有开启就不会进行签名验证,这里是从哪里看出来的呢,是验证的主函数DxeImageVerificationLib

/**
  Provide verification service for signed images, which include both signature validation
  and platform policy control. For signature types, both UEFI WIN_CERTIFICATE_UEFI_GUID and
  MSFT Authenticode type signatures are supported.

  In this implementation, only verify external executables when in USER MODE.
  Executables from FV is bypass, so pass in AuthenticationStatus is ignored.

  The image verification policy is:
    If the image is signed,
      At least one valid signature or at least one hash value of the image must match a record
      in the security database "db", and no valid signature nor any hash value of the image may
      be reflected in the security database "dbx".
    Otherwise, the image is not signed,
      The SHA256 hash value of the image must match a record in the security database "db", and
      not be reflected in the security data base "dbx".

  Caution: This function may receive untrusted input.
  PE/COFF image is external input, so this function will validate its data structure
  within this image buffer before use.

  @param[in]    AuthenticationStatus
                           This is the authentication status returned from the security
                           measurement services for the input file.
  @param[in]    File       This is a pointer to the device path of the file that is
                           being dispatched. This will optionally be used for logging.
  @param[in]    FileBuffer File buffer matches the input file device path.
  @param[in]    FileSize   Size of File buffer matches the input file device path.
  @param[in]    BootPolicy A boot policy that was used to call LoadImage() UEFI service.

  @retval EFI_SUCCESS            The file specified by DevicePath and non-NULL
                                 FileBuffer did authenticate, and the platform policy dictates
                                 that the DXE Foundation may use the file.
  @retval EFI_SUCCESS            The device path specified by NULL device path DevicePath
                                 and non-NULL FileBuffer did authenticate, and the platform
                                 policy dictates that the DXE Foundation may execute the image in
                                 FileBuffer.
  @retval EFI_SECURITY_VIOLATION The file specified by File did not authenticate, and
                                 the platform policy dictates that File should be placed
                                 in the untrusted state. The image has been added to the file
                                 execution table.
  @retval EFI_ACCESS_DENIED      The file specified by File and FileBuffer did not
                                 authenticate, and the platform policy dictates that the DXE
                                 Foundation may not use File. The image has
                                 been added to the file execution table.

**/
EFI_STATUS
EFIAPI
DxeImageVerificationHandler (
  IN  UINT32                           AuthenticationStatus,
  IN  CONST EFI_DEVICE_PATH_PROTOCOL   *File,
  IN  VOID                             *FileBuffer,
  IN  UINTN                            FileSize,
  IN  BOOLEAN                          BootPolicy
  )
{
  EFI_IMAGE_DOS_HEADER                 *DosHdr;
  BOOLEAN                              IsVerified;
  EFI_SIGNATURE_LIST                   *SignatureList;
  UINTN                                SignatureListSize;
  EFI_SIGNATURE_DATA                   *Signature;
  EFI_IMAGE_EXECUTION_ACTION           Action;
  WIN_CERTIFICATE                      *WinCertificate;
  UINT32                               Policy;
  UINT8                                *SecureBoot;
  PE_COFF_LOADER_IMAGE_CONTEXT         ImageContext;
  UINT32                               NumberOfRvaAndSizes;
  WIN_CERTIFICATE_EFI_PKCS             *PkcsCertData;
  WIN_CERTIFICATE_UEFI_GUID            *WinCertUefiGuid;
  UINT8                                *AuthData;
  UINTN                                AuthDataSize;
  EFI_IMAGE_DATA_DIRECTORY             *SecDataDir;
  UINT32                               OffSet;
  CHAR16                               *NameStr;
  RETURN_STATUS                        PeCoffStatus;
  EFI_STATUS                           HashStatus;
  EFI_STATUS                           DbStatus;
  BOOLEAN                              IsFound;

  SignatureList     = NULL;
  SignatureListSize = 0;
  WinCertificate    = NULL;
  SecDataDir        = NULL;
  PkcsCertData      = NULL;
  Action            = EFI_IMAGE_EXECUTION_AUTH_UNTESTED;
  IsVerified        = FALSE;
  IsFound           = FALSE;

  //
  // 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;
  }
  //
  // If policy is always/never execute, return directly.
  //
  if (Policy == ALWAYS_EXECUTE) {
    return EFI_SUCCESS;
  }
  if (Policy == NEVER_EXECUTE) {
    return EFI_ACCESS_DENIED;
  }

  //
  // The policy QUERY_USER_ON_SECURITY_VIOLATION and ALLOW_EXECUTE_ON_SECURITY_VIOLATION
  // violates the UEFI spec and has been removed.
  //
  ASSERT (Policy != QUERY_USER_ON_SECURITY_VIOLATION && Policy != ALLOW_EXECUTE_ON_SECURITY_VIOLATION);
  if (Policy == QUERY_USER_ON_SECURITY_VIOLATION || Policy == ALLOW_EXECUTE_ON_SECURITY_VIOLATION) {
    CpuDeadLoop ();
  }

  GetEfiGlobalVariable2 (EFI_SECURE_BOOT_MODE_NAME, (VOID**)&SecureBoot, NULL);
  //
  // Skip verification if SecureBoot variable doesn't exist.
  //
  if (SecureBoot == NULL) {
    return EFI_SUCCESS;
  }

  //
  // Skip verification if SecureBoot is disabled but not AuditMode
  //
  if (*SecureBoot == SECURE_BOOT_MODE_DISABLE) {
    FreePool (SecureBoot);
    return EFI_SUCCESS;
  }

而uefi的签名验证是uefi自己的标准,对efi文件进行签名验证
而这里需要db/dbx提供签名的白名单/黑名单列表
而efi文件是个PE32可执行文件
但是我们发现UEFI加载linuxloader时候也是调用的gBS->LoadImage,那么对linuxloader.efi进行分析发现没有带额外的签名.

Get-AuthenticodeSignature "D:\adb tools\qualcomm-reverse\Section_PE32_image_F536D559-459F-48FA-8BBC-43B554ECAE8D_LinuxLoader_body.efi"

image

以说我们基本上断定loadimage不会进行签名验证。但是edk2代码的逻辑里面是有签名验证的函数的,但是这需要UEFI secureboot开启,也就是说,我们几乎可以断定,在高通平台上,UEFI secure boot没有开启。
为什么这么说呢?
我们找一份很古老的高通的uefi的代码,显然这份代码里面没有GBL相关。

我们来分析AuthVariableLib
image
如果platform key不存在那么是setup mode
image
只有是user mode才会配置secure boot状态
但是我们没有看到配置platform key的代码
另外,SecurityPkg/Library/DxeImageVerificationLib/DxeImageVerificationLib.inf这个文件压根没有被其他的描述文件包含
SecurityPkg/Library/AuthVariableLib/AuthVariableLib.inf这个也没有
所以,可以断定高通没有开启UEFI secure boot

确认了UEFI secure boot没有开启之后,也就意味着我们的GBL是不经过签名验证的,这样我们就实现了EL1的代码执行。
gBS->StartImage是加载已经load完的efi文件,但是执行完还会退回linuxloader的执行。

下一个问题是,我不会编译uefi app来着,而且求助群友还被骂了...
然后我也不知道高通的解锁状态是怎么写入的。
那么我们接着分析
image
这个函数被很多和解锁状态相关的函数调用
那么这里是调用了一个UEFI protocol
我也不知道这是啥protocol
但是如果我们能够直接调用ABL这个函数不就可以写入解锁状态了吗?
而且根据分析1是解锁0是上锁,而且应该一个负责lock state一个负责critical lock state
所以下一步是知道abl的linuxloader的加载地址,那么根据偏移量就可以算出对应函数的地址
但是这块,我还是太菜了呜呜...想简单了...
实际上linuxloader作为efi文件是通过gBS->LoadImage加载的然后通过gBS->StartImage执行的,那么要看这里
image
如果没有指定加载地址,那么默认是通过一个分配器来分配内存,也就是说加载地址不是固定的。
而源码里面也的确没有明确linuxloader的加载地址(也可能有但是我们得到的内容太少了毕竟过时了)
所以这条路死了...
那么还有一条路,就是我们模仿abl调用的protocol来写入解锁状态
那这样就,我们就需要对这块进行了解。

由于我们不能简单是调用解锁函数了,要清楚解锁状态具体是怎么写入的
我之前想起来我看过一篇文章讲了高通的解锁状态存储在devinfo,一旦熔断devinfo存储在rpmb的才是有价值的
而我们这个解锁函数调用了VBRwDevice,
image
image
image
根据代码我们能够看到这个调用了QCOM_VERIFIEDBOOT_PROTOCOL,其中有一个函数是VBRwDeviceState
要调用一个protocol需要使用它的GUID,通过gBS->LocateProtocol来定位协议,第一个参数是GUID,第二个参数是NULL,第三个参数是定位到的协议存储的地址
定位到协议之后就可以调用这个协议的成员和成员函数
实际上是根据偏移量来区分函数的,也就是一个大结构体
image
我们直接调用这个写devinfo的函数
第一个参数是协议的地址,第二个参数是读还是写,这里我们是写,是WRITE_CONFIG,第三个参数是devinfo所在的内存地址,第4个参数是devinfo的长度
那么问题来了,devinfo的格式是怎么样的呢?
我最开始想的是,直接在第0字节和第1字节分别写入1或者是第0字节和第8字节写入1(因为是小端序)

/** @file VbRwStateApp.c

  UEFI Application that calls QCOM_VERIFIEDBOOT_PROTOCOL.VBRwDeviceState
  with op=WRITE_CONFIG (1), 16-byte buffer: byte0=1, byte8=1.

  Copyright (c) 2025. All rights reserved.

**/

#include <Uefi.h>
#include <Library/UefiLib.h>
#include <Library/UefiApplicationEntryPoint.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/BaseMemoryLib.h>
#include <Protocol/EFIVerifiedBoot.h>

#define VB_RW_BUF_SIZE  16

/**
  

  @param[in] ImageHandle    Image handle for this application.
  @param[in] SystemTable    Pointer to the EFI System Table.

  @retval EFI_SUCCESS      Call succeeded.
  @retval other            LocateProtocol or VBRwDeviceState failed.
**/
EFI_STATUS
EFIAPI
UefiMain (
  IN EFI_HANDLE        ImageHandle,
  IN EFI_SYSTEM_TABLE  *SystemTable
  )
{
  EFI_STATUS                    Status;
  QCOM_VERIFIEDBOOT_PROTOCOL   *VbProtocol;
  UINT8                         Buf[VB_RW_BUF_SIZE];
  UINT32                        BufLen = VB_RW_BUF_SIZE;

  // Locate QCOM Verified Boot Protocol
  Status = gBS->LocateProtocol (
                  &gEfiQcomVerifiedBootProtocolGuid,
                  NULL,
                  (VOID **)&VbProtocol    
                  );
  if (EFI_ERROR (Status)) {
    Print (L"VbRwStateApp: LocateProtocol(VerifiedBoot) failed: %r\n", Status);
    return Status;
  }

  // Prepare 16-byte buffer: byte 0 = 1, byte 8 = 1, rest zero
  SetMem (Buf, sizeof (Buf), 0);
  Buf[0] = 1;
  Buf[8] = 1;

  // Call VBRwDeviceState: op = WRITE_CONFIG (1)
  Status = VbProtocol->VBRwDeviceState (
                         VbProtocol,
                         WRITE_CONFIG,
                         Buf,
                         BufLen
                         );
  if (EFI_ERROR (Status)) {
    Print (L"VbRwStateApp: VBRwDeviceState(WRITE_CONFIG) failed: %r\n", Status);
    return Status;
  }

  Print (L"VbRwStateApp: VBRwDeviceState(WRITE_CONFIG) success.\n");
  return EFI_SUCCESS;
}

结果不行。
后来我想起来之前看到的那篇文章
也就是devinfo是储存很多状态的,是一个结构体形式的
然后我们找到这篇文章
image
那么我们只修改指定位置的解锁位
再试一次,又失败了...
这时候我意识到直接这样修改会覆盖前面的magic number
而magic number又会被检查
image
那么我们干脆提取设备的devinfo吧
然后devinfo里面非空内容的长度刚好是3344
image
这就非常的合理。
所以说我们直接导出整个3344长度的devinfo以char数组的形式然后修改解锁位然后写入
这样就得到了最终版本的代码
我们为了稳定在执行完写入devinfo之后加一个死循环卡住

下一步就是如何编译了
我们由于新加了一个uefi app,我们是把他塞到QcomTestPkg里面
image
然后加一个inf文件
image
然后在 boot/QcomPkg/QcomPkg.dec 的 [Includes.common] 中增加了 QcomModulePkg/Include,这样 #include <Protocol/EFIVerifiedBoot.h> 能正确解析。
接下来是如何编译的问题
最开始我是打算在windows平台下编译结果总是出各种奇怪的问题
后来我们换到了linux平台去编译。
我们的编译命令

export WORKSPACE="/home/hicode002/qcomuefi/boot_images/boot_images/edk2" && export PYTHONPATH="/home/hicode002/qcomuefi/boot_images/boot_images/edk2/BaseTools/Source/Python" && export EDK_TOOLS_PATH="/home/hicode002/qcomuefi/boot_images/boot_images/edk2/BaseTools" && export PACKAGES_PATH="/home/hicode002/qcomuefi/boot_images/boot_images/edk2:/home/hicode002/qcomuefi/boot_images/boot_images/boot:/home/hicode002/qcomuefi/boot_images/boot_images/sdk:/home/hicode002/qcomuefi/boot_images/boot_images/ssg" && export PATH="$PATH:/home/hicode002/qcomuefi/boot_images/boot_images/edk2/BaseTools/BinWrappers/PosixLike" && export GCC49_AARCH64_PREFIX="aarch64-linux-gnu-" && python3 edk2/BaseTools/Source/Python/build/build.py -p QcomPkg/QcomTestPkg/QcomTestPkg.dsc -a AARCH64 -b DEBUG -t GCC49 -m QcomPkg/QcomTestPkg/VbRwStateApp/VbRwStateApp.inf

如果我们不添加GCC49_AARCH64_PREFIX那么就会报找不到编译器的错误,这里卡住好久
如何如果不添加PATH那么posixlike这里会报错
然后还会报很多文件找不到的错误,这里我们借助AI一点点新建空白文件和对代码进行修补,最后成功的编译出来了。
使用-m参数是我们只想要这一个UEFI app
下一个问题是,efisp的分区格式是什么,怎么把efi文件塞进去。
这里是踩大坑的地方。由于我对UEFI完全不熟悉,根据互联网上关于GBL的文档,都是uboot加载gbl,都是需要把GBL塞到一个vfat格式的分区,我也这么做的最开始,但是没有任何反应系统正常启动了。那么这时候就有两种情况
第一种情况是我的uefi app编译的有问题,比如说代码写的有问题,那么这种情况我们直接写入一个死循环测试,还是正常启动没有卡住
说明gbl根本没有被加载!
那么就只剩下第二种情况,GBL这个efi文件在efisp放置的格式不正确。
比如说对路径 分区格式 文件名有要求
实话说这块一直没有想到,一直觉得execimgfromvolume是要有分区格式的。
后来某大佬跟我说不需要分区格式把efi文件直接塞进去就可以
现在我们想一想,linuxloader里面根本没有传入有意义的path所以说的确是不需要分区格式的。

所以说,我们只需要把我们编译好的efi文件塞进efisp分区就好了!
这里可以使用9008写入也可以拆机通过编程器写入或者飞线之类的。
然后,重启,我们的死循环生效了!
接着,改成我们写入解锁状态的efi文件,然后,重启手机
我们看到一行代码!
1c5bc8fab63febd6925120bb21e870e0_720
然后重启要求强制恢复出厂!
看到这里,基本上就稳了。
然后我们就有
7b2e38586faaff639dd38fddd33bdc72
e41879503ef77ed5c679d3ee458c0cee_720
于是您就有了解锁的红魔11
那么同样的道理,适用于小米17 小米17pro k90promax 努比亚z80等8gen5的设备
但是我们需要9008写入分区,或者说需要拆机写入分区。

当然,这个漏洞的局限性很大,要写入分区,而且只有8e5才能用,那么8e5之前还有其他的漏洞,大概率是通过一些溢出来进行提权
拿到完整的ABL权限,然后调用解锁函数。
那么大概率是通过fastboot下的一些命令的溢出导致的。
如果能够在内核态提权就可以免拆写入分区,这是后续的两个研究方向。(所以这是个入门级的漏洞)
另一个角度来说,如果ov等厂商采用了oem的解锁方式,但是允许加载未签名的gbl,那么可以实现一个custom abl从而直接解锁加载未经签名的kernel!

那么就完结撒花吧!

posted @ 2026-02-27 20:46  sakrain  阅读(9115)  评论(5)    收藏  举报