overview
bootrom xloader( xloader and xloader2/UCE ) fastboot
大多数安卓设备fastboot功能包含在应用bootloader中,只加载安卓内核且通常运行在normal world EL1中
然而华为fastboot直接运行在EL3,不仅负责加载安卓内核,还负责加载所有其他镜像
物理内存布局
1 0x00000000-0x00010000 bootrom 2 0x00022000-0x00050000 xloader 3 0x60000000-0x60010000 uce (depending on the model) 4 0x10000000-0x20000000 DDR-slice view
Vulnerabilities
- Unchecked Data Length in Head Chunk
- Unchecked Data Chunk Count
- Tail Chunk Insufficient Boundary Condition Check
- Head Re-Send State Machine Confusion
- Ineffective Downgrade Protection
- Address Verification Bypass in Xloader
Unchecked Data Length in Head Chunk
漏洞位于bootrom和xloader中 head chunk中发送的size未校验,可以写入过多数据
xmodem->file_download_length直接从数据包中length计算得到
xmodem->total_frame_count直接根据xmodem->file_download_length计算
void usb_xmodem(xmodem_t *xmodem) { /* first check message length, sequence number, and crc checksum */ (...) /* command parsing begins */ byte cmd = (xmodem->msg).cmd; if (cmd == 0xfe) { /* head command */ int file_type = (xmodem->msg).file_type; if ( (seq==0) && (msg_len==14) && (file_type-1 & 0xff) < 2 ) { uint length = xmodem->msg[ 4] << 0x18 | xmodem->msg[ 5] << 0x10 | xmodem->msg[ 6] << 0x08 | xmodem->msg[ 7]; (...) xmodem->file_download_length = length /* Address check */ (...) if ((length % 1024) == 0) size = 1; else size = 2; xmodem->total_frame_count = size + (length / 1024); (...) } send_usb_response(xmodem, 0x55); return; } /* after this, data and tail chunk are processed without any checking on xmodem->total_frame_count */ (...) }
if (cmd == 0xda) { /* data command */ if (seq == (xmodem->next_seq & 0xff)) { if (xmodem->next_seq == xmodem->total_frame_count - 1) size = xmodem->file_download_length - xmodem->latest_seen_seq * 1024; else size = 1024; if (msg_len == size + 5) { memcpy( xmodem->file_download_addr_1 + xmodem->latest_seen_seq*1024, xmodem->msg, size); xmodem->total_received = xmodem->total_received - 5; xmodem->latest_seen_seq = xmodem->latest_seen_seq + 1; xmodem->next_seq = xmodem->next_seq + 1; send_usb_response(xmodem, 0xaa); return; } xmodem->total_received -= msg_len; send_usb_response(xmodem, 0x55); return; } /* Repeated chunk handling code */ (...) }
if (cmd == 0xed) { /* tail command */ if ((xmodem->next_seq == seq) || (msg_len == 5)) { xmodem->next_seq = xmodem->next_seq + 1; xmodem->latest_seen_seq = xmodem->latest_seen_seq + 1; if (xmodem->latest_seen_seq != xmodem->total_frame_count ) { send_usb_response(xmodem, 0x55); return; } send_usb_response(xmodem, 0xaa); /* reset the inner struct on receiving a valid tail */ (...) return; } send_usb_response(xmodem, 0x55); return; }
不知道为啥发送tail chunk收不到ACK
Unchecked Data Chunk Count
漏洞位于xloader中 xmodem实现未计算成功接收的data chunk数量,仅以接受到tail chunk为边界
根据上面代码,data chunk处理中只过滤虚假消息(如data size错误或seq不同步)
预期大小总是1024字节,除了最后一个data chunk大小是剩余的字节数
一旦xmodem->latest_seen_seq >= xmodem->total_frame_count,就没有检查来避免处理更多data chunk。当前下载地址仅取决于xmodem->latest_seen_seq计数器,该计数器按每个数据块递增,与块总数无关
所以可以发送超出head chunk里长度的data chunk,可能溢出指定的下载缓冲区
Tail Chunk Insufficient Boundary Condition Check
漏洞位于bootrom和xloader
tail chunk处理中进入第一个if后,xmodem->next_seq和xmodem->latest_seen_seq自增一,然后判断xmodem->latest_seen_seq是否已经等于xmodem->total_frame_count,等于代表接收完毕,不等于说明未接收完,则会return,后续可以继续接收chunk。而对data chunk处理时会根据xmodem->latest_seen_seq计算偏移进行拷贝
问题在于发送错误tail chunk后,xmodem->latest_seen_seq的自增没有被清除
可以通过注入不在位置的tail chunk增加该计数器,进而索引到更大内存范围
memcpy( xmodem->file_download_addr_1 + xmodem->latest_seen_seq*1024, xmodem->msg, size);
Head Re-Send State Machine Confusion
漏洞位于bootrom
void usb_xmodem(xmodem_t *xmodem) { /* first check message length, sequence number, and crc checksum */ (...) /* command parsing begins */ byte cmd = (xmodem->msg).cmd; if (cmd == 0xfe) { /* head command */ int file_type = (xmodem->msg).file_type; if ( (seq==0) && (msg_len==14) && (file_type-1 & 0xff) < 2 ) { uint length = xmodem->msg[ 4] << 0x18 | xmodem->msg[ 5] << 0x10 | xmodem->msg[ 6] << 0x08 | xmodem->msg[ 7]; uint address = xmodem->msg[ 8] << 0x18 | xmodem->msg[ 9] << 0x10 | xmodem->msg[10] << 0x08 | xmodem->msg[11]; /* ISSUE: address is always set in the internal structure before verified */ xmodem->file_type = file_type; xmodem->file_download_length = length; xmodem->file_download_addr_1 = address; xmodem->file_download_addr_2 = address; if (address == 0x22000) { /* limit download address */ if ((length % 1024) == 0) size = 1; else size = 2; /* initialize inner struct to the download details */ xmodem->total_received = 0; xmodem->latest_seen_seq = 0; xmodem->total_frame_count = size + (length / 1024); xmodem->next_seq = 1; send_usb_response(xmodem, 0xaa); return; } /* ISSUE: xmodem->next_seq is NOT reset if the address was invalid */ send_usb_response(xmodem, 0x07); /* address error */ return; } send_usb_response(xmodem, 0x55); return; } if (xmodem->next_seq == 0) { /* there hasn't been any head command so far but download must start with a head chunk! */ usb_bulk_in__listen(xmodem); return; } /* after this, data and tail chunk are both processed and accepted */ (...) }
乍一看只有当xmodem->next_seq==1且head chunk提供合法地址 状态机才允许处理data chunk或tail chunk
然而有两个问题:
- 下载地址和大小在实际地址校验前被保存,验证失败也不会重置,
xmodem->file_*元素可以用任意值填充 - 即使发现地址无效,状态机值
xmodem->next_seq也没有被重置
所以 只需要先发一个有效的head chunk,让状态机值xmodem->next_seq等于1,然后发送一个包含错误地址的head chunk,此时状态机值不变,而xmodem->file_*被填充了非法地址,之后可以继续发送其他chunk,绕过地址校验
usb_xmodem函数在POT模型(kirin710)的地址是0x3224,在YAL模型(kirin980)的地址是0x4348
以下代码片段概述了该代码在引导 ROM 中如何被调用。由于 usb_xmodem 只是通过在 USB 描述结构中注册的回调间接调用,代码片段展示了回调的设置以及实际的跳转过程
reset_vector /* YAL: 0x0048, POT: 0x0048 */ └ load /* YAL: 0x0650, POT: 0x061c */ └ download_xloader /* YAL: 0x0470, POT: 0x04ac */ └ actual_usb_things /* YAL: 0x30b4, POT: 0x204c */ └ maybe_init_usb /* YAL: 0x2f9c, POT: 0x1f40 */ └ some_usb_loop /* YAL: 0x3238, POT: 0x21b8 */ ├ calls_usb_init /* YAL: 0x336c, POT: 0x22c8 */ │ ├ 0x42d0: a847 blx r5 /* callback to xmodem YAL */ │ └ 0x31cc: a847 blx r5 /* callback to xmodem POT */ ├ usb_init /* YAL: 0x4258 */ │ │ /* sets the callback function to 'usb_xmodem' */ │ └ 0x3b16: c4f8c030 str.w r3=>usb_xmodem+1,[r4,#0xc0] └ inner_things_to_huge_usb_init /* POT: 0x21a0 */ └ huge_usb_init /* POT: 0x3154 */ └ usb_init_struct_fill /* POT: 0x271c */ │ /* sets the callback function to 'usb_xmodem' */ └ 0x2a2c: c4f8c030 str.w r3=>usb_xmodem+1,[r4,#0xc0]
Ineffective Downgrade Protection
单调版本计数器 防止降级 版本计数器可在4096字节长的VRL报头中找到,位于0x1a4,0x470,0x73c偏移 版本计数器由两个4字节部分组成:类型和值
下面是POT模型的OTA中获取的xloader VRL头(version LGRP2-OVS_9.1.0.327)

bootrom/xloader/fastboot中处理版本值的代码很相似
处理函数名为DX_SB_VerifyNvCounter (名字从一个很老的fastboot 镜像中获取)
感觉白皮书里的伪代码好像写反了
kirin710和kirin980所有固件中版本值总是1,即没有被使用。就usb下载模式而言,加载最新xloader和较旧的xloader几乎没区别。从而可以加载旧版但已签名的xloader引入漏洞
Address Verification Bypass in Xloader
旧版xloader中xmodem未校验地址,新版有。利用前一个漏洞加载旧版本xloader从而绕过地址校验
Android-9中的xloader
/* xloader-9 */ void usb_xmodem(xmodem_t *xmodem) { (...) if (cmd == 0xfe) { /* head command */ int file_type = (xmodem->msg).file_type; if ( (seq==0) && (msg_len==14) && (file_type-1 & 0xff) < 2 ) { uint length = xmodem->msg[ 4] << 0x18 | xmodem->msg[ 5] << 0x10 | xmodem->msg[ 6] << 0x08 | xmodem->msg[ 7]; uint address = xmodem->msg[ 8] << 0x18 | xmodem->msg[ 9] << 0x10 | xmodem->msg[10] << 0x08 | xmodem->msg[11]; xmodem->file_type = file_type; xmodem->file_download_length = length; /* VULNERABILITY: There is no verification on address! */ xmodem->file_download_addr_1 = address; xmodem->file_download_addr_2 = address; int size = ((length % 1024) == 0) ? 1 : 2; xmodem->total_received = 0; xmodem->latest_seen_seq = 0; xmodem->total_frame_count = size + (length / 1024); xmodem->next_seq = 1; send_usb_response(xmodem, 0xaa); return; } } /* other commands and error handling */ (...) }
Android-10上代码如下:
/* xloader-10 */ void usb_xmodem(xmodem_t *xmodem) { /* sequence number and checksum check */ (...) if (cmd == 0xfe) { /* head command */ if ( (seq==0) && (msg_len==14) ) { int file_type = (xmodem->msg).file_type; if (file_type-1 & 0xff) < 2 ) { uint length = xmodem->msg[ 4] << 0x18 | xmodem->msg[ 5] << 0x10 | xmodem->msg[ 6] << 0x08 | xmodem->msg[ 7]; uint address = xmodem->msg[ 8] << 0x18 | xmodem->msg[ 9] << 0x10 | xmodem->msg[10] << 0x08 | xmodem->msg[11]; xmodem->file_type = file_type; xmodem->file_download_length = length; xmodem->file_download_addr_1 = address; /* PATCH: address validation */ if (check_address_valid(address, length) == 0) { /* address is in range */ xmodem->file_download_addr_2 = address; size = ((length % 1024) == 0) ? 1 : 2; xmodem->total_received = 0; xmodem->latest_seen_seq = 0; xmodem->total_frame_count = size + (length / 1024); xmodem->next_seq = 1; send_usb_response(xmodem, 0xaa); return; } else { /* clear all of the members on an invalid address */ xmodem->file_type = 0; xmodem->file_download_length = 0; xmodem->file_download_addr_1 = 0; xmodem->file_download_addr_2 = 0; xmodem->total_received = 0; xmodem->latest_seen_seq = 0; xmodem->total_frame_count = 0; xmodem->next_seq = 0; } } } } /* other commands and error handling */ (...) }
Kirin980 漏洞利用
任意写
head resend 先发送一个对的,再发一个错的,之后再继续发送data downgrade 传旧版xloader (不是很懂) tail increment 漏洞 不是很懂 暂略
BootRom
对于bootrom,在ROM中,所以代码不可写,但可以改栈 覆写返回地址从而直接跳转到下载的代码 同时幸运的是如果由于签名错误导致下载失败,已经加载的镜像会留在内存里,然后协议重试
- 先将修改的、没有签名的镜像加载到0x22000
- 然后实施攻击 绕过地址校验,覆盖调用栈返回地址,从而跳转到被加载的镜像中
bootrom的执行是单线程的且非常确定,调用栈几乎可以精准地重构
不建议覆写usb_xmodem的直接返回地址,因为在处理完xmodem协议后一些USB维护函数需要按顺序运行才能保持USB接口激活
在上述维护函数结束、但镜像验证还未开始的地方修改返回地址
最小调用栈如下
reset_vector /* YAL: 0x0048, POT: 0x0048 */ └ load /* YAL: 0x0650, POT: 0x061c */ │ push { r4, r5, r6, r7, r8, r9, r10, lr } │ sub sp, 16 │ => in total stack moved by 12 dwords └ download_xloader /* YAL: 0x0470, POT: 0x04ac */ push { r3, r4, r5, lr }
当download_xloader返回(即使下载失败),压入的lr会被放进pc。lr距离栈顶12 dwords(POT: 0x49bfc, YAL: 0x4dbfc)所以是一个已知地址
在load函数中,download_xloader函数之后调用镜像验证函数,所以覆盖上面提到的lr寄存器可以跳过镜像验证,直接跳转到下载的任意代码,从而在bootrom阶段实现任意代码执行