[ayoung@blog posts]$ cat ./How-To-Tame-Your-Unicorn阅读.md

How-To-Tame-Your-Unicorn阅读

[Last modified: 2025-02-18]

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

漏洞位于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_seqxmodem->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

然而有两个问题:

所以 只需要先发一个有效的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中,所以代码不可写,但可以改栈 覆写返回地址从而直接跳转到下载的代码 同时幸运的是如果由于签名错误导致下载失败,已经加载的镜像会留在内存里,然后协议重试

  1. 先将修改的、没有签名的镜像加载到0x22000
  2. 然后实施攻击 绕过地址校验,覆盖调用栈返回地址,从而跳转到被加载的镜像中

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会被放进pclr距离栈顶12 dwords(POT: 0x49bfc, YAL: 0x4dbfc)所以是一个已知地址 在load函数中,download_xloader函数之后调用镜像验证函数,所以覆盖上面提到的lr寄存器可以跳过镜像验证,直接跳转到下载的任意代码,从而在bootrom阶段实现任意代码执行

reference