Getting Your Phone Unlocked
攻陷你的安卓设备:自底向上方法
对于一般用户而言,ivib一直以来是客制化难度最高的安卓手机品牌之一。
对于禁止BL锁解除,ivib官方说法是出于安全考虑,但是乖乖听话,你的设备真的就会安全吗?
本文以笔者手上的ivib F12为例(CPU为MT6891),讨论如何从底层代码漏洞出发攻陷你的(MTK)安卓设备。
MTK平台启动过程简述
设备上电后跳转至Boot ROM,进行USB握手,随后用根密钥检验Preloader签名并将其载入SRAM。
Preloader初始化DRAM、闪存和必要的设备,从闪存读取seccfg分区初始化Secure Boot配置,进行USB握手,初始化TrustZone,检验Little Kernel(Bootloader)签名,将其加载到DRAM并跳转。
LK根据Secure Boot配置决定是否校验内核并加载到DRAM,随后就是安卓系统的启动流程了。
想法
作为闭源代码,移动设备芯片的BROM通常有大量未公开的漏洞(掌握在黑灰产手里了),且一经出厂无法修复。当然针对MTK BROM也有很多公开的exp,比如mtkclient。
对出厂设备来说,唯一的补救方法就是推送修补过的Preloader,从而禁止BROM模式。ivib就对BROM进入作了限制,似乎只有完全放电后,再启动设备才可能进入BROM(我还没成功过)。
LK通常带有解锁Bootloader的功能(即关闭启动分区校验),从而允许用户加载自定义的内核(但是ivib的LK没有),而且解锁后会清空手机数据,攻击者无法通过这种手段提取用户数据。并且LK工作在EL1下,没有写seccfg分区的权限,即使利用漏洞成功启动也不能持久化。
那么要进行客制化就只能从Preloader入手了。Preloader(和BROM)工作在EL3下,有最高的权限并且能够直接操作物理内存,这里的任何漏洞都可能引发极为严重的安全问题。
实操
由于手机厂商通常公开发布固件包,获取Preloader分区的镜像异常容易,接下来我只需要找到Preloader的基址和Header长度就能愉快地反编译了。
经过一番搜寻,我找到了Header的定义(虽然没有完全对应上),得到加载地址0x00200F10和偏移0xF0。
Github上还能找到一堆过往芯片的Preloader源码,经过一些交叉引用的比对可以定位一些关键函数。
其中就包括usbdl_handler,它负责处理USB控制指令,通过它我们可以利用mtkclient实现好的USB协议和设备交互,BROM下也有相同的功能,只是它的漏洞更多。
其中最显眼的指令无非就是CMD_SEND_DA,CMD_JUMP_DA,CMD_READ16,CMD_WRITE16,这几个了,直觉告诉你这里边必出问题。
CMD_SEND_DA加载用户发送的DA(Download Agent)程序用于刷写闪存;CMD_JUMP_DA则让Preloader跳转执行DA。
CMD_SEND_DA
近年MTK设备的Preloader都有SLA(Serial Link Authentication)和DAA(Download Agent Authentication)机制,分别负责保证CMD_SEND_DA的调用者是授权用户(如售后)和检验DA文件的签名。我们可以看看它们是如何起作用的。
该设备没有SLA,只对DA签名进行验证。先检查DAA是否被开启,随后加载公钥验签,出现错误就进入assert里的死循环。
这里有一个问题在于,验签之前DA就已经被载入内存并且没有检验DA长度是否合法。
dlcomport->ops->recv根据使用的协议(USB/UART)调用usb_recv或者uart_recv接收传入数据并写入内存,而它们都没有检验写入地址和长度的合法性。da_addr原本可以由用户自由传入,这里直接写死成0x40200000,但是毛用没有。
其实我们已经可以利用它任意操纵32位地址的物理内存了,尽管BROM在低地址且不可写,但是这是因为ROM本来就不支持写操作,不会造成任何异常。只不过这样做要覆写低地址会非常慢,得通过小水管写几个G。
CMD_JUMP_DA
除了检验g_da_verified为1(SLA和DAA通过之后设置为1)以外没有任何检查,然后跳到0x40200000,单拎出来没啥问题。
CMD_READ16/CMD_WRITE16
下面是usbdl_write16的代码,usbdl_read16差不多,它竟然会检查len16的int值是否为负——那可太安全了;它竟然会调用sec_region_check检查读写区域是否合法——那可太安全了。
sec_region_check调用while_list_check检查读写区域是否落在一个硬编码的白名单中。
while_list_check调用is_in_region检查区域是否落在某个区间内,这里可以溢出,将base_addr设置为0xFFFFFFFE就能任意读写。
那就能dump出BROM(0x00000000-0x0001FFFF)并对Preloader进行修补了,我看互联网上好像还没有流传MT6891的BROM。
解锁
实际利用的时候发现从0x0020E000开始的一段内存写入就会使Preloader崩溃,原因未知。
需要优雅一些的方式在利用这些漏洞的同时保证Preloader完全正常运行,否则一旦后续刷写闪存时发生错误就难以挽救,因此我们不应随意污染栈的内容。
一开始看到usbdl_handler里还有被内联的usbdl_write32,心想只要发送CMD_READ16把内存dump出来再通过CMD_WRITE32在上一个栈帧把patch过的Preloader写回,就不会对栈和寄存器产生任何副作用。
但这样做忽略了指令Cache的存在,它导致usbdl_handler中执行的指令仍然是未被修补的。
那似乎只能去操纵返回地址了,usbdl_write32被内联在死循环里,那只能考虑usbdl_write16了。
调用链为main->usb_listen->usbdl_handler->usbdl_write16,可以让usbdl_write16跳转到修改白名单的payload,再跳到usbdl_handler结尾,跳回usb_listen结尾,再跳转到usb_listen的入口。这样就不会污染栈和寄存器(payload里应只操作R4-R7寄存器)并且允许我们再次连接设备。
经测试SLA和DAA的状态被写在ROM中,无法修改。但是可以直接操纵g_da_verified为1,再用CMD_WRITE16/32将DA写入,最后调用CMD_JUMP_DA。
之后就能直接刷写闪存了,mtkclient提供的DA包含了解锁功能,但我们还是看看seccfg分区的结构。
虽然这个分区有8MB,但是实际只会用到0x3C字节的Header。
它以0x4D4D4D4D开头,0x45454545结束,之间分别代表seccfg的版本、大小、Bootloader解锁状态(LKS),dm-verity状态(verity_state),Secure Boot状态(SBOOT_RUNTIME)。注意mtkclient在解锁的时候把verity_state设置成了DM_VERITY_GENERAL_ERROR (1),需要改成DM_VERITY_STATUS_OK (0)。
最后是校验序列,可以逆向sec_seccfg_init得到校验逻辑,这些函数Github上似乎没有源代码。它对最后0x20字节进行软/硬件解密并检查是否和前0x1C的完整性校验值是否相同,由常数可以判断使用SHA256。
本设备使用硬件加解密,在这种模式下会配置加密模块寄存器的值并与其交互。
交互的逻辑已经被mtkclient实现了,这个硬件模块叫Security Engine for JTAG protection,但是这种东西是怎么被逆向出来的?????
你注意到了吗,这样解锁就不会清空设备数据了,并且获得权限后攻击者就能用各种方式提取用户的隐私信息。
Chores
如果你想客制化的话,也许会以为到这就结束了?不存在的,ivib还有一斤大的要喂给你吃。
There’s a Hole In My Kernel
ivib对Linux进行了魔改,我们想要完成客制化还得在内核上动点手脚。
To Root or Not to Root
用户如果刷入Magisk修补过的boot分区将无法正常启动,或者无法获取root权限。很多用户在没有限制BROM进入的老设备上利用mtkclient成功解锁,但是之后还是死在这一步了。
经测试发现似乎只是在检查待执行的程序名是否为"su",如果是直接报错"Operation not permitted"。
可以用magiskboot解包boot.img得到vmlinux(注意还有个大小为0x60的Header),用kallsyms-finder解压出符号表再导入IDA就能愉快分析了。当然程序员还不至于这么笨,搜个"su"就能定位?不存在的。
之后搜索exec相关函数发现全都被strip掉了,搜几个别的系统调用发现只有exec相关的被strip掉了——真是此地无银三百两。这样就要用到一个查找Linux内核代码交叉引用的网站了。
随意翻翻,找到个do_execveat_common中用到的字符串"/dev/fd/%d/%s",发现史就是在这里拉的,调用了一个函数进行一些怪异的检查,其中就包括文件名是否为"su"。
Security-Blunted Linux (SBLinux)
尝试在设备上运行frida-server,得到报错"Unable to save SELinux policy to the kernel: Operation not permitted"。
要知道Linux报错一般都会有更加具体的原因,而不是和之前执行su一样笼统的EPERM,这只能说明这段代码根本就不是Linux维护者写的。
frida-server会通过写入/sys/fs/selinux/load的方式加载新的SELinux规则保证自身的权限。基于之前su的经验,我查看了open,write系统调用相关的代码,不过似乎没有改动。
那么土应该就动在SELinux头上了。/sys/fs/selinux是SELinux的sysfs接口挂载点,/sys/fs/selinux/load是加载安全规则的接口,它的write操作被绑定到了security_load_policy上。一看发现确实加了料,直接NOP掉,不过里面的判断逻辑确实没咋看懂。
The Big Bloatware Is Watching You
除了内核上的改动,ivib还在内置Bloatware里加了一些条条框框。你要不要看看这是什么?
在ivib服务里找字符串再定位几下应该就好了。比较搞笑的是它用了相当多的手段来判断设备是否解锁,但是最后全落到这个函数来更改系统设置变量并发送通知,把它Hook掉就解决了。
个人建议
- 尽可能少使用闭源代码以便公开审计
- 你能做的只有增删模块而不是改动内核的关键函数
- 要闭源要”安全“就直接对推送的更新镜像加密,增加攻击成本