【TrustZone相关漏洞导读】CVE-2021-39994:HUAWEI SMC SE Factory Check OOB Access

欢迎报名 OSR TrustZone Pwn
image

简述

  • 漏洞编号:CVE-2021-39994
  • 漏洞作者:Maxime Peterlin(马克西姆·彼得林)、Alexandre Adamski(亚历山大·亚当斯基)
  • 漏洞评级:CRITICAL(严重)
  • 漏洞位置:Kirin 810 : HUAWEI P40 Lite (JNY) : BL31 SMC handler for HISEE : S-EL3 Runtime
  • 影响版本:Kirin 810 : HUAWEI P40 Lite (JNY) : Before 2022 February
  • 漏洞固件:全量刷机包中的TRUSTFIRMWARE.img,但镜像已加密
  • 漏洞类型:作者定义为:OOB Access(越界访问),具体理解为:任意地址写固定值
  • 漏洞成因:BL31中处理向SE收发APDU的个别SMC handler,未对EL1传递过来的共享内存地址进行检查
  • 利用效果:从EL1的Linux Kernel出发,提权到S-EL3任意代码执行
  • 公开披露:Hexacon 2022: Hara-Kirin: Dissecting the Privileged Components of Huawei Mobile Devices (2022.10)
  • 漏洞细节:Impalabs Blog: Huawei Secure Monitor Vulnerabilities (2022.12)
  • 关联漏洞:CVE-2021-22437、CVE-2021-39993

作者

据Linkedin公开资料显示,Maxime Peterlin(马克西姆·彼得林 @lyte__ )和 Alexandre Adamski(亚历山大·亚当斯基 @NeatMonster_)均为法国人。另根据公开资料中的大学毕业时间,推测二人均为95年左右生人,因此目前年纪应该不到三十,年少有为。

image

职业上,他们二人可以说是形影不离。2018-2020年,二人同时就职于法国安全公司:Quarkslab(夸克实验室),可在Quarkslab的博客中找到二人曾经发表的技术文章,其内容以ARM Trustzone与Intel SGX相关的TEE技术为主:

2019年二人合作登上BlackHat USA 2019,议题为:Breaking Samsung’s ARM TrustZone。2020年二人离开Quarkslab。2021年二人在法国创办研究型安全公司Impalabs。本次介绍的漏洞:CVE-2021-39994,正是二人以Impalabs公司的名义,于2022年10月,在法国安全会议Hexacon 2022上进行公开披露的,其议题为:Hara-Kirin: Dissecting the Privileged Components of Huawei Mobile Devices。另外值得一提的是,Maxime Peterlin曾是法国CTF战队Quokka Light的队员,而Alexandre Adamski是著名IDA逆向协作工具IDArling的主要作者,可见二人都是实践派出身,相关技术博客:

漏洞

细节参考:Huawei Secure Monitor Vulnerabilities (2022.12)

这个漏洞简单来说就是,个别的SMC handler未对EL1传递过来的共享内存地址进行检查,直接就进行了使用,进而导致的任意地址写。分析漏洞需要了解的代码主要有两部分:

  1. EL1:Linux内核中向BL31中发送SE APDU的驱动代码 drivers/hisi/hisee/hisee.c(开源)
  2. S-EL3:BL31中接收EL1传递过来的APDU并转发给SE的smc handler中转代码(逆向)

在漏洞细节blog中,可见Impalabs的逆向结果非常残暴,几乎逆出了源码。以他们的逆向结果为准,这个漏洞本身主要关联于BL31的5个函数:

(1) hisee_smc_handler

首先SE相关的SMC调用分发主函数hisee_smc_handler,会统一调用se_smc_addr_check对需要检查共享内存地址的情况进行检查,然后再进行子功能函数的调用:

image

但根据业务情况,并不是所有子功能函数的参数均需要检查,在函数参数本身设计的就不是共享内存地址时,检查也不应该发生。因此是否进行内存地址检查,hisee_smc_handler需要根据调用的子功能函数进行区别对待,具体判断条件如下,x1为子功能码:

if (x1 - 0xb > 0x35 || ((1 << (x1 - 0xb)) & 0x2003e000002041) == 0)

注意x1是无符号数,因此0x0到0xa,减0xb均为无符号大正数,大于0x35。因此小于0xb的功能码,全部检查。大于等于0xb的,按照0x2003e000002041为掩码放过检查:

推测功能码最大为 0x40 (0xb + 0x35 = 0x40)

for i in range(0xb,0x41):
    if( ( (1 << (i - 0xb) ) & 0x2003e000002041 ) == 0):
        continue
    print(hex(i))

因此如下功能码对应的函数不在hisee_smc_handler函数中进行内存地址检查:

0xb 0x11 0x18 0x30 0x31 0x32 0x33 0x34 0x40

(2) se_factory_check

然而就在这几个hisee_smc_handler放过内存地址检查的函数中,功能码为0x11的se_factory_check函数,却将参数当成了共享内存地址使用,并且在其函数内部也没有进行地址检查,直接就传递给了set_message_header:

image

(3) set_message_header

此函数会将未检查的内存地址赋值给全局变量g_msg_hdr_addr供其他函数使用,直接的函数调用跟踪中断:

image

(4) send_ack

交叉引用g_msg_hdr_addr找到,在send_ack中会对g_msg_hdr_addr解引用并发生内存写操作,而send_ack也会在SE返回响应后被调用,至此漏洞发生:

image

(5) se_chip_test_ack

内存写的值在调用send_ack的se_chip_test_ack函数中可以控制为0xAABBCC55和0xc,故有限制的任意地址写产生,具体来说就是任意地址写固定值:

image

漏洞总结

image

这个漏洞的存在的基础,就是之前在ATF练习中提到的,在安全边界上使用共享内存传递数据的场景:

image

利用

所有漏洞的利用思路都是一步步扩大攻击者的能力,在二进制层面来说,就是一步步扩大可以控制的内存和寄存器。那么一个有限制的任意地址写,如何继续往下扩大内存和寄存器的控制能力呢?一般来说首先的目标就是限制代码,例如突破地址、大小、来源等检查,然后就可以利用更多的接口来完成更大的破坏。最终寻找到内存中与控制流相关的数据并劫持,完成目标点的任意代码执行。本漏洞的利用主要也是这个思路:

细节参考:Huawei Secure Monitor Vulnerabilities (2022.12)

  1. 首先利用有限制的任意地址写突破地址检查函数se_smc_addr_check
  2. 突破后,利用send_ack中的memcpy_s可以扩大出向任意地址写0x7c单字节的能力,以此修改某个smc handler为地址有限制(0x7c)的gadget,找到可以通过x2寄存器继续控制流劫持的gadget
  3. 由于x2寄存器任意可控,因此继续寻找任意地址的gadget,找到str w1, [x0]完成任意地址写,并平衡栈的gadget,至此有任意地址写任意4字节的能力
  4. 利用这个任意地址写任意4字节,继续修改某个smc handler为str x0, [x1];ret以及ldr w0, [x0,x1];ret等gadget,完成任意地址写8字节,以及任意地址读4字节。
  5. 利用彻底的任意地址写,修改页表,重新映射S-EL3的代码段为可写,修改S-EL3代码,完成S-EL3的任意代码执行!

(1) Disabling the CMA Whitelist

在地址检查函数se_smc_addr_check中,关键的两个变量g_cma_addr和g_cma_size控制着合法地址范围,并且这俩变量所在的内存页是可写的:

image

因此利用任意地址修改起始地址g_cma_addr为0xc,g_cma_size为0xAABBCC55,因此合法地址范围突破成0xc-0xAABBCC61,BL31主要使用的内存地址正在其中:

image

(2) Hijacking an SMC Handler Pointer

在突破se_smc_addr_check后,也许因为某些原因,他们没有利用其他的smc handler,而是利用了完成了地址写的send_ack中的memcpy_s,进而有更大能力的任意地址写。不过这里的写的内容为SE返回的data,仍然不能控制,首字节为0x7c,可以控制共享内存的size为1字节,因此有了任意地址写单字节0x7c的能力:

image

利用这个单字节写,修改bl31_secap_smc_handlers到一个可以继续通过x2控制流劫持的gadget(地址低位为0x7c)。并说明当触发此handler时x0-x3寄存器可控,此时调用父级函数为bl31_secap_handler,调用指令为blr x6:

image

(3) Temporary Write Primitive

找到str w1, [x0]完成任意地址写,并平衡栈的gadget,使得在bl31_secap_handler调用被篡改的bl31_secap_smc_handlers后,仍然能正常返回,不会引发BL31的崩溃,至此完成了任意地址写任意4字节的利用:

可能因为目前的过程比较麻烦,跳了2次gadget,并且只能写4字节,他们将此利用称之为暂时的写能力

image

(4) Stable Read/Write Primitives

利用这个4字节写,他们继续修改两个smc handler为如下gadget,至此有了完整彻底的任意地址读写:

image

(5) Double Mapping the Secure Monitor

利用利用彻底的任意地址写,修改页表,重新映射S-EL3的代码段为可写,修改S-EL3代码:

image

(6) Getting Code Execution in EL3

触发smc调用,完成S-EL3的任意代码执行!

image

利用总结

对于有限制的地址写,利用的过程就是一步步扩大,目标就是首先突破一些关键的限制函数:

image

对于修改页表的利用,在superhexagon以及之前的ATF赛题中都有涉及。可见,在有任意地址写能力的情况下,CFI(控制流完整性)无法保护操作系统这种数据和控制流相互交织的目标。

image

image

实践

总体来看,这个漏洞主要归咎于BL31中的se_factory_check,其未对参数进行检查。利用过程需要击基于EL1代码执行权限,因此至少要root手机,而华为新机型的root本就非常困难,因此复现整个漏洞的代价很高。所以我们本次实践的目标就定为逆向到目标漏洞,即在真实的固件中逆向找到这段代码,真正的在IDA中看到他:

image

那这段代码在哪呢?他是BL31中的一个smc handler,因此这段代码应该在手机固件的BL31部分中,因此也就应该存在于手机的全量固件中,即手机的全量刷机包中。因此这里我们实践的具体做法就是,寻找并解开目标手机的刷机包,然后在其中找到目标漏洞代码。

(1) HISEE 的前身 inSE

不过在对刷机包动手之前,我想重新审视一下目标(其实是因为接下来遇到的阻碍,让我们必须这样回顾)

在漏洞细节中Impalabs提到,这个漏洞至少影响2022年2月以前的P40 Lite。虽然这个漏洞不在BootROM中,但由于这个漏洞位置在BL31,因此应属于SoC平台相关代码,所以漏洞影响范围应该可以扩大到2022年2月以前所有使用Kirin 810平台的手机。另外经过其他渠道了解,这个漏洞并非只存在于Kirin 810平台中,因为HISEE的前身其实就是2016年在Kirin 960中首发的inSE功能,截止目前基本所有的海思旗舰和中高端SoC均带有此功能,而海思实现在BL31中的HISEE驱动也是一致的,所以,你想想吧…

ELS-AN00_02_HM_opensource/drivers/hisi/hisee/hisee_fs.h

image

在海思官网中,较老的SoC介绍中都会提一句带inSE功能,新的SoC如810、990、9000介绍中已经懒得提了,但其中必然实现inSE功能:

(2) 刷机包的寻找与拆解

这里我们就以Kirin 810为例,通过wikichip我们可以看到搭载了此SoC的手机,和老外同步,我们选择P40 Lite作为分析目标,因此要找到2022年2月之前的刷机包。华为目前已经关闭了官方下载手机刷机包的渠道,但国内外还是有非常多的站点和网友对刷机包进行搜集整理,P40 lite是海外上市的手机,所以直接搜索会有非常多的国外站点提供刷机包下载:

但是第一个免费的下载方式是google网盘,google经常会对大文件本身的下载做限制,而第二个需要visa付费,较麻烦。有绕过google限制的办法:

最后还是尝试搜索国内的刷机包网站,找到提供国内提供海外P40 lite刷机包站点:

根据第一个站点提供的信息可以确定,刷机包10.0.1.136的发布日期至少在2021年10月21之前,因此应当存在此漏洞。在第二个站点微信支付一块钱即可下载本版固件,直接给出网盘地址:

华为已经不提供传统的卡刷和线刷刷机包,因此现在能下载到的基本只有华为官方OTA包这一种刷机包,格式为zip压缩,解开之后主要的东西都封在UPDATE.APP这个文件中:

在我们下载的固件中需要解压两层,即解压update_sd_base.zip后即可看到UPDATE.APP这个大文件:

image

解压UPDATE.APP最好用的是windows下的Huawei Update Extractor,相关说明以及其他方法:

通过HuaweiUpdateExtractor打开UPDATE.APP,在Settings标签中取消勾选crc等校验,即可成功解析,很容易猜出TRUSTFIRMWARE.img就是BL31:

image

也可通过Android Image Tools中的emui_extractor提取:

  ./emui_extractor ./UPDATE.APP dump TRUSTFIRMWARE.img ./TRUSTFIRMWARE.img

接下来我们对提取出来的TRUSTFIRMWARE.img进行逆向,就应该可以看到漏洞代码啦!

(3) 固件加密!企图破产!

但当我们仔细研究TRUSTFIRMWARE.img时,却发现对其搜不出什么有用的字符串:

  strings ./TRUSTFIRMWARE.img | grep handler
 

用binwalk也没有识别出其中的ARM指令:

  binwalk -A ./TRUSTFIRMWARE.img

DECIMAL       HEXADECIMAL     DESCRIPTION
-------------------------------------------------------------

继续使用binwalk可以确定其熵值基本为1:

  binwalk -E ./TRUSTFIRMWARE.img 

image

这一切均说明了一个糟糕的结果:TRUSTFIRMWARE.img加密了!!!另外从 BlackHat USA 2021: How To Tame Your Unicorn 也可以发现这个事,麒麟旗舰以及中高端SoC上,TRUSTFIRMWARE均加密:

image

麒麟海思SoC的启动流程源于ATF,但也进行了些许修改:

  1. BL1: BootROM,实现了USB-XMODEM协议完成救砖刷机
  2. BL2: 拆解为xloader + fastboot
  3. BL31: 即TRUSTFIRMWARE.imge,由fastboot加载

所以是fastboot可以解密TRUSTFIRMWARE.img,但解密需要的元数据必然与SoC平台相关,例如efuse中的密钥,所以单纯依赖刷机包本身是不可能解开TRUSTFIRMWARE.img的。那Impalabs是怎么解开并逆向的呢?在他们的另一篇对华为Hypervisor研究的文章中提到,需要用BootROM漏洞(CheckM30)来提取手机efuse中的解密密钥,才能完成对加密镜像的解密:

Shedding Light on Huawei’s Security Hypervisor

image

然而这个密钥并未公开,用CheckM30从BootROM提取解密密钥又属于另一个漏洞的讨论范畴,那我们本次的实践就步于此了么?

(4) Mate 9,百密一疏!

之前提到,所有带inSE的华为手机,在TRUSTFIRMWARE.img(即BL31)中均有对应的处理代码(HISEE相关的smc handler)。因此所有搭载了960、970、980、990、710、810等麒麟SoC的手机刷机包中都应该有这部分代码,那这么多机型的这么多刷机包中,会不会就漏了一两个没加密的呢?还真有!经过了一宿的“搜山检海”,我终于找到了一个没加密的TRUSTFIRMWARE.img!他就在第一款搭载了Kirin 960的华为Mate 9的早期刷机包 MHA-AL00B_C765B188_Android7.0_EMUI5.0(2017年6月)中,也直接给出网盘地址:

老刷机包的打包方式是rar -> tar.gz -> UPDATE.APP,且压缩包名还叫SDupdate_Package.tar.gz,可能还是华为支持卡刷时代的产物

image

使用binwalk查看固件熵值,很明显没加密:

binwalk -E ./TRUSTFIRMWARE.img

image

搜字符串可以发现hisee_smc_handler!扔进IDA手动识别也可以发现一些函数,因此可以确认,Mate 9早期刷机包中的TRUSTFIRMWARE.img确实没有加密:

  strings ./TRUSTFIRMWARE.img | grep smc
hisee_smc_handler: g_download_req is 0
std_smc_hisi_service
efusec_smc_handler
cpu_volt_smc_handler
get_val_svc_smc_handler
isp_smc_handler
pcie_smc_handler
ivp_smc_handler
get_std_smc_active_flag(tsp_ctx->state)
tspd_smc_handler

  strings ./TRUSTFIRMWARE.img | grep -i inSE   
inse req callback para  err... cmd:%ud
inse callback exist...cmd:%ud
put inse callback para  err... cmdtype:%ud

如果仔细观察以上smc_handler的名字,可以看出这与ATF的smc_handler非常类似,也可以通过版本字符串看出,这就是ATF,版本为v1.1:

https://elixir.bootlin.com/arm-trusted-firmware/v1.1/source

  strings ./TRUSTFIRMWARE.img | grep -i debug
v1.1(debug):f2d7567

因此通过 Mate 9 的明文固件可以确认,麒麟海思旗舰SoC的启动链实现,在当时就是基于ATF。总之,感谢这个Mate 9,让我们的实践可以继续往下走。回顾 Mate 9,这是第一款搭载麒麟960的手机,也即华为海思的inSE技术的首发产品,所以也可以说与inSE相关的BL31代码,在当时其实也一起首发了。另外在此2017年6月份之后的所有固件,应当都对TRUSTFIRMWARE.img进行了加密,我也尝试寻找第二个搭载麒麟960的华为手机,P10的刷机包,但即使是EMUI 5版本的也进行了加密。所以Mate 9这个早期刷机包真的是百密一疏,猜测原因应当是这个时间点,正好是位于海思底层固件加密方案的技术过渡期,这才有了这个漏网之鱼:

2016.11.14: HUAWEI Mate 9发布会现场图集

image

回到正题,那这么老版本的TRUSTFIRMWARE.img里,存在我们的目标漏洞么?接下来我们就来逆向他!

(5) 时过境迁,漏洞未生

TRUSTFIRMWARE.img没有ELF格式,所以为了便于IDA识别,可以使用pwntools给他封一层ELF:

因为不知道TRUSTFIRMWARE.img的加载地址,所以直接设置为0,后续发现其中使用的字符串地址均为相对偏移,所以加载地址正确与否对分析没有太大影响

from pwn import *
context(arch='aarch64')
sc = open('TRUSTFIRMWARE.img','rb').read()
open('TRUSTFIRMWARE.elf','wb').write(make_elf(sc,vma=0))

扔到IDA里可以识别出一堆函数,通过对hisee_smc_handler字符串的交叉引用可以找到sub_6774函数,这就是hisee_smc_handler的主体路由函数,不过可见其中并没有单独处理0x11的子功能码,也没有判断是否检查的掩码0x2003e000002041,心顿时凉了半截,貌似版本差异有些大,功能逻辑可能都过于陈旧,漏洞代码可能还没写出来:

通过添加segment,将0x1A820 - 0x25600设置为只读段,即可让IDA的F5结果正常显示字符串

image

不过还是冷静下来仔细看看,还是能找到地址检查的函数,即sub_5ED8,并且这个逻辑和Impalabs的逆向结果基本完全一致!

image

image

但是判断是否需要进行地址检查的逻辑要比现在简单一些,但要非常注意的是,这里还是无符号判断,例如:7减8为全f,还是大于1,因此只有8、9、19不进行地址检查,其余的功能码均需要被检查:

image

以10号功能码为例,其中将a3当成地址并直接解引用访问了,这似乎存在非法地址解引用,但10号功能码不为8、9、19,属于地址检查的判断中,因此10号功能码处应该没有什么问题:

image

在0-7功能码的处理中,会看到很多将a3存到0x202342C8处的代码逻辑,但是这些功能码均在地址检查的范围中,所以应该也没什么问题:

image

在8、9、19功能码中,只有8号功能码的处理过程中使用了a3,虽然其将没有检查的a3存到了0x2022CBE8中,但经过分析,这里存储的a3也从没有被当成地址进行使用,因此应该也没有什么问题。所以Impalabs逆向出的漏洞函数se_factory_check,在这2017年6月的 Mate 9 固件中应该还没出生:

image

虽然我们没有办法看到P40 lite中TRUSTFIRMWARE.img的漏洞代码,但通过Mate 9,还是看到了这部分代码的早期面貌,并且确认在当时漏洞不存在,也算尽力而为了。

(6) 误判符号,罚银三两

在分析前期,我其实没看出来检查那是无符号,所以我当时认为0到9的功能码都不会被检查,然后继续往下分析了很久,并且判断漏洞存在,属实错怪人家了:

这个错误需要牢记,在判断大小时一定要注意符号!!!

image

按照错误判断分析,在0到9的功能码处理中,会看到很多将a3存到0x202342C8处的代码逻辑:

image

这似乎很像set_message_header的逻辑,所以如果能找到其他地方使用了0x202342C8中的存储地址,那么漏洞就可能发生:

image

然后我就去找0x202342C8这个地址的交叉引用,但IDA并不能直接寻找未映射空间的交叉引用,所以可以在IDA中添加一个新的内存段,让IDA重新识别以完成寻找。另外本代码中的0x202342C8地址写的实现代码,是通过存储了0x202342C8地址本身,进行的间接访问,例如0x683C这句其实是从0x25410中取出的0x202342C8这个数,所以直接对0x25410进行交叉引用,也可以找到使用0x202342C8地址的其他位置:

image

结果我还真找到了sub_5F94,写的值还真就是0xAABBCC55,和Impalabs逆向结果中的send_ack和se_chip_test_ack基本一致。所以当时我误以为漏洞存在,整体情况如下:

  • sub_6774即hisee_smc_handler,为HISEE功能入口,从这里出发
  • 首先可以使用不进行地址检查的6号功能码(其实检查了),将任意地址从a3写入0x202342C8
  • 然后使用19号功能码,调用经过sub_6554、sub_5F94即可完成向a3 + 4的地址处写入0xAABBCC55的任意地址写操作

image

这个向目标地址写0xAABBCC55的逻辑与Impalabs的逆向结果基本一致:

image

这份老代码总体看下来,虽然一打眼看起来与现在的截然不同,但仔细分析其内部的具体逻辑,就会发现即使这么多年过去了,部分代码逻辑也没有很大的变化,恍如昨日。不过因为0-7的功能码都会被检查,所以将未检查的a3写入0x202342C8中并不成立,因此根据我们的逆向实践,可以确认这个漏洞在当年,并不存在。