相当一部分基于 Exynos 处理器的智能手机都用了一个名为 SBOOT 的专有引导加载程序。 比如三星 Galaxy S7,Galaxy S6 和 Galaxy A3,可能还有更多已上市的三星 Exynos 的智能手机[1]。
我有幸在评估各种 TEE 实现时逆向了这个 bootloader 程序的一部分。 本文是一系列关于 SBOOT 文章的第一篇。
文中回顾一些 ARMv8 概念,讨论了我所遵循的方法以及我做出的或正确或错误的假设,同时,分析了这个没有文档的专门使用在三星 Galaxy
S6 上的 blob。
▚ 上下文
最近,我有幸参与到可信执行环境(TEE)的几个实现中全职来排查 bug。
作为一个边缘项目,我开始挖掘更多的TEE实现,特别是在我的个人或者工作使用的智能手机上,巧合的是,他们来自同一个软件编辑器,即由 Trust 共同创办的Trustonic
[2] G&D和金雅拓。 我手头上智能手机唯一的共同点就是是它们都是基于Exynos处理器。
Trustonic 的 TEE,又名
虽然在文件系统中识别受信任应用程序是这个挑战最简单的一部分,
但在我分析的 Exynos 智能手机上寻找这个TEE操作系统无异于海底捞针。事实上,你能在其他一些智能手机(比如基于高通 SoC 的手机)上找到的存储 TEE 操作系统镜像的专有分区,在这部手机上却找不到。它极有可能存储在其他地方,可能在 bootloader 上,这就是为什么我开始逆向 SBOOT。本文是叙述我的TEE操作系统之旅系列文章的第一篇。我会把重点放在如何确定三星 S6
SBOOT 基地址还有把它加载到 IDA 上。
▚ ARMv8 的概念
在启动 IDA 之前,让我们回顾下 ARMv8 的一些基础。这些可能对熟悉 ARMv7,刚接触 ARMv8 的同学们有些用处。需要明确完整的文档的,请参阅 ARMv8 开发者指南[3]。我不是 ARMv8 专家,如果你看到任何错误或需要搞清楚的地方,随君评论。
▚ 异常级别
ARMv8 通过定义异常级别的概念来引入一个新的异常模型。 异常级别决定运行软件木块的权限级别(PL0 到 PL3)和运行它的处理器模式(非安全和安全)。 ELn 的运行对应 PLn 的权限,n 越大,运行的权限就越大。
▚ 异常向量表
出现异常时,处理器切换到一个异常向量表,运行相应的程序。 在 ARMv8 中,每个异常级别都有自己的异常向量表。 对于那些习惯于反向工程 ARMv7 bootloaders 的人,你会注意到它的格式与 ARMv7 完全不同:
聪明的读者可能已经注意到,在 ARMv8 上异常向量表占用 128(0x80)个字节长,而在 ARMv7 上每个条目只有 4 字节宽,并且每个条目保存一系列异常处理指令。
虽然异常向量表的位置由 ARMv7 上的 VTOR(向量表偏移寄存器)确定,但 ARMv8 使用三个 VBAR(基于向量的地址寄存器)VBAR_EL3,VBAR_EL2 和 VBAR_EL1。
请注意,一定程度上,这些句柄(或表入口)的运行将依赖于:
1. 异常类型(SError, FIQ, IRQ 或者 Synchronous)。
2. 如果异常异常级别相同,则使用堆栈指针(SP0 或 SPx)。
3. 如果异常的级别较低,则执行下一个较低级别(AArch64 或 AArch32)的执行状态。
一个运行在特定权限的软件模块可以使用专用指令和在底层异常级别上运行的软件交互。
例如,用户模式下的进程(EL0)通过发出超级用户调用(SVC)来执行内核(EL1)的系统调用,内核可以与管理程序调用(HVC)与管理程序(EL2)交互,或者直接与
执行安全监视器调用(SMC)的安全监视器(EL3)等。这些 service 的调用生成由一个异常向量表同步句柄来处理的同步异常。
我会在接下来的文章中更多地介绍我对这篇文章的一些见解。 让我们试着把 SBOOT加载到 IDA Pro,然后逆向它吧。
▚ 反编译 SBOOT
据我所知,SBOOT 用的是未的特有格式。
▚ 在 IDA Pro 中加载 SBOOT
三星
Galaxy S6 是由1.5GHz 64 位八核三星 Exynos 7420 的 CPU 驱动。请记住,ARMv8 处理器可以运行
AArch32 和 AArch64 架构的应用程序。 因此,可以将 SBOOT 加载为 32 位或 64 位 ARM 二进制。
我先预设,BootROM 不切换到 AArch32 架构,先将它作为 64 位二进制文件加载到IDA Pro 中,保留默认选项:
处理器类型:小端ARM架构[ARM]
作为64位代码反编译: 是的
许多 AArch64 位指令被自动识别。 解开反汇编的指令,基本模块是有意义的,让我认为我真的处理 AArch64 代码:
▚ 确定基地址
我花了几天时间来确定正确的基地址。 鉴于给你直接的解决方案是毫无意义的,我首先详细描述我所有的尝试,直到做出能给我真正的基地址的正确假设。 正如谚语所说,任何人写道:“授人以鱼不如授人以渔“。
▚ 谷歌一下,你就知道
我刚开始在网上搜索和三星 bootloader SBOOT 相关的东西。 不幸的是,这个问题的结果很少,只有 reverseengineering.stackexchange.com 上一个可追溯到 2015 年 3 月的线索[5]和这个相关。
这个线程主要给我了两个提示。 J-Cho 直觉上认为,这个 bootloader 从偏移量为 0x3F000 处开始,还断定它实际上从 0x10 开始。
因为我想排除我的假设,bootloader 的基地址是 0x00000000,并且其代码总是从 0x10 开始,我开始寻找在其他 Exynos 智能手机中使用的引导加载程序。 魅族智能手机的 SBOOT 在 0x10 没有给出有效的指令,证实了我的怀疑:
我还分析了在其他引导程序是否有遗留的任何调试字符串,能给我提示 SBOOT 通常在内存中哪里加载。
并没有彩头:(但是我有另一个:在魅族的 SBOOT 的一些字符串建议使用 U-Boot,即使 U-Boot 不使用三星 Galaxy
S6,这是一个值得探索的领先,我开始进一步挖掘。
▚ U-Boot仓库
U-Boot 是开源的,支持一系列 Exynos 芯片。 例如,Exynos 4和 Exynos 5 已经支持了5年多了。 Exynos 7 的支持尚未完全登陆,但根据其邮件列表[6],Exynos 7 ESPRESSO 的开发板存在一些补丁。
我可能错过了,但是遍历 ESPRESSO 开发板的补丁一无收获:(我尝试了从 Exynos 4 到 Exynos 7 板多个已知的基地址没有成功,是时候从另一个角度尝试了。
▚ ARM 字节池
如果您熟悉逆向 ARM 汇编,您一定注意到了大量使用字节池来保存要加载到寄存器中的某些常量值。 这个特性可以帮助我们查找大约在何处加载 SBOOT,特别是当从文字池加载分支目标地址时。
我搜索了 IDA
Pro 在操作数中标记有错误的所有分支指令(红色高亮显示)。
由于 bootloader 程序的代码是自包含的,我可以明确假定大多数分支目标地址必然命中在 bootLoader 程序本身的代码。
有了这个假设,我可以估算出 bootloader 的基地址。
从第一条指令,我注意到以下分支错误:
这些代码片段上的有趣的事实是:
跳转指令BR(跳转到寄存器)是没有条件的,而且它不会 return。
两个分支的操作数值相同(0x2104010),并且位于引导加载程序的早期。
最后一个字节是 0x10,这似乎也正是 bootloader 的代码开始的偏移量。
我任意假定地址 0x2104010 是一个复位地址,我试图在 0x2104000 处加载 SBOOT 二进制,具有以下选项:
处理器类型: ARM小端[ARM]
ROM起始地址: 0x2104000
加载地址: 0x2104000
作为64位代码反编译: 是的
至少,IDA Pro 找出的错误少了一些,表明我的假设可能是正确的。 当然,我还不能确定这个基地址是正确的,我需要逆向来进一步确定。 画外音:我几乎认定它正确:)
▚ ARM系统寄存器
鉴于我可能要得到潜在的基地址了,我继续逆向 SBOOT,苍天保佑代码流中没有异常。
当我想找到 TEE
OS 时,我开始搜索在安全监视器中执行的代码片段。 找到安全监视器的一个相当简单的技术在于寻找设置或读取只能从安全监视器访问的寄存器的指令。
如前所述,安全监视器在EL3中运行。 VBAR_EL3 是找到 EL3 代码的一个很好的选择,因为它保存EL3异常向量表的基地址,而且指向 SMC 句柄。
查找设置 VBAR_EL3 的指令
你还记得本文开头介绍的异常向量表格式吗? 它是由 16 个 0x80 字节的条目组成,保存异常处理程序的代码。 在搜索结果中,0x2111000 的代码似乎指向一个有效的异常向量表:
在 0x2111000 的异常向量表
即使如此,所选择的基地址仍然不是正确的:(当验证设置 VBAR_EL3 的其他指令时,可以注意到 0x210F000 在函数的中间:
异常向量表(不在)在 0x210F000
这些异常表明 0x2104000 不是正确的基地址。 让我们试试别的。
▚ Service 描述符
三星 Galaxy
S6 SBOOT 部分基于 ARM 可信固件[7]。
ARM 受信任的固件是开源的,并为 ARMv8-A 提供了安全领域软件的参考实现,包括一个在异常级别3(EL3)执行的安全监视器。
与安全监视器对应的汇编代码与 ARM 受信任的固件中的汇编代码完全相同。 这是一个好消息,因为它会给我的逆向工程节省时间和精力。
我试图在反汇编代码中找到另一个锚点以确定 SBOOT 的基地址。
在结构体中的char *类型的成员特别有趣,因为它们指向在编译时定义其地址的字符串。
在比较 SBOOT 反汇编代码和 ARM 可信固件源代码时,我确定了一个结构体,rt_svc_desc_t,它具有我正在寻找的属性:
typedef struct rt_svc_desc {
uint8_t start_oen;
uint8_t end_oen;
uint8_t call_type;
const char *name;
rt_svc_init_t init;
rt_svc_handle_t handle;
} rt_svc_desc_t;
根据 ARM
Trusted
Firmware 的源代码,rt_svc_descs 是一个 rt_svc_desc_t 数组,用于保存 service 导出的运行时 service 描述符。
它在函数 runtime_svc_init 中使用,通过调用函数 bl31_main 中的调试字符串可以轻松地将其在 SBOOT 中定位:
我试图在不同的地址上映射这个二进制,并检查是否可以找到 rt_svc_desc.name 条目的有效字符串。 这里是一个短小精悍的脚本:
import sys
import string
import struct
RT_SVC_DESC_FORMAT = "BBB5xQQQ"
RT_SVC_DESC_SIZE = struct.calcsize(RT_SVC_DESC_FORMAT)
RT_SVC_DESC_OFFSET = 0xcb50
RT_SVC_DESC_ENTRIES = (0xcc10 - 0xcb50) / RT_SVC_DESC_SIZE
if len(sys.argv) != 2:
print("usage: %s " % sys.argv[0])
sys.exit(1)
sboot_file = open(sys.argv[1], "rb")
sboot_data = sboot_file.read()
rt_svc_desc = []
for idx in range(RT_SVC_DESC_ENTRIES):
start = RT_SVC_DESC_OFFSET + (idx <
desc = struct.unpack(RT_SVC_DESC_FORMAT,
sboot_data[start:start+RT_SVC_DESC_SIZE])
rt_svc_desc.append(desc)
strlen = lambda x: 1 + strlen(x[1:]) if x and x[0] in string.printable else 0
for base_addr in range(0x2100000, 0x21fffff, 0x1000):
names = []
print("[+] testing base address %08x" % base_addr)
for desc in rt_svc_desc:
offset = desc[3] - base_addr
if offset
sys.exit(0)
name_len = strlen(sboot_data[offset:])
if not name_len:
break
names.append(sboot_data[offset:offset+name_len])
if len(names) == RT_SVC_DESC_ENTRIES:
print("[!] w00t!!! base address is %08x" % base_addr)
print(" found names: %s" % ", ".join(names))
在分析的 SBOOT 上运行此脚本给出以下输出:
$ python bf_sboot.py sboot.bin
[+] testing base address 02100000
[+] testing base address 02101000
[+] testing base address 02102000
[!] w00t!!! base address is 02102000
found names: mon_smc, std_svc, tbase_dummy_sip_fastcall,
tbase_oem_fastcall, tbase_smc, tbase_fastcall
[...]
成功了! 三星 Galaxy S6 SBOOT 的基地址为 0x02102000。 用这个基地址重新加载二进制文件到 IDA Pro 似乎纠正了所有迄今为止我已经看到的怪异的反汇编代码。 现在我们确定找到了!
▚ 反编译强化
逆向工程过程就像解决一个谜题。 一个人试图通过将一些信息放回一起来理解一个软件如何工作。 因此,你拥有的信息越多,解谜就越容易。 这里有一些提示,帮助我迟早找到正确的基地址。
▚ 丢失的函数
虽然 IDA Pro 在反编译常用文件格式方面做得很出色,但是在逆向未知二进制文件时可能会丢失很多函数。 在这种情况下,一个常见的习惯是写一个脚本寻找开始指令,并声明函数存在于这些点。 一个简单的 AArch64 函数序列看起来像这样:
// AArch64 PCS assigns the frame pointer to x29
sub sp, sp, #0x10
stp x29, x30, [sp]
mov x29, sp
mov x29,sp 指令是AArch64开端的相当可靠的标记。 找到函数的开始的思路是搜索这个标记并且在发现常见的开始指令(例如mov,stp,sub)时向后反向。 在 IDA Python 中搜索 AArch64 开始函数看起来像这样:
import idaapi
def find_sig(segment, sig, callback):
seg = idaapi.get_segm_by_name(segment)
if not seg:
return
ea, maxea = seg.startEA, seg.endEA
while ea != idaapi.BADADDR:
ea = idaapi.find_binary(ea, maxea, sig, 16, idaapi.SEARCH_DOWN)
if ea != idaapi.BADADDR:
callback(ea)
ea += 4
def is_prologue_insn(ea):
idaapi.decode_insn(ea)
return idaapi.cmd.itype in [idaapi.ARM_stp, idaapi.ARM_mov, idaapi.ARM_sub]
def callback(ea):
flags = idaapi.getFlags(ea)
if idaapi.isUnknown(flags):
while ea != idaapi.BADADDR:
if is_prologue_insn(ea - 4):
ea -= 4
else:
print("[*] New function discovered at %#lx" % (ea))
idaapi.add_func(ea, idaapi.BADADDR)
break
if idaapi.isData(flags):
print("[!] %#lx needs manual review" % (ea))
mov_x29_sp = "fd 03 00 91"
find_sig("ROM", mov_x29_sp, callback)
ARM64 IDA Plugins
▚ AArch64 mov 简化
编译器有时会优化代码,使得读取更难。 使用 IDA Pro 的 API,可以编写架构特定的代码简化器。 我发现由 @xerub 共享的 AArch64 代码简化器非常有用。 下面是 AArch64 反汇编的示例:
ROM:0000000002104200 BL sub_2104468
ROM:0000000002104204 MOV X19, #0x814
ROM:0000000002104208 MOVK X19, #0x105C,LSL#16
ROM:000000000210420C MOV X0, X19
@xerub's "AArch64 mov simplifier" [8] changes the disassembly as follows:
ROM:0000000002104200 BL sub_2104468
ROM:0000000002104204 MOVE X19, #0x105C0814
ROM:000000000210420C MOV X0, X19
聪明的读者可能会注意到,MOVE 不是一个有效的ARM64指令。 MOVE只是一个标记,告诉逆向工程师当前的指令已经被简化并被这条指令所取代。
▚ FRIEND插件
逆向工程 IDA Pro 中的 ARM 低级代码一直很乏味。 确定与系统控制协处理器相关的指令是一个可怕的经历,因为 IDA Pro 在没有寄存器别名的情况下反汇编指令。 如果让你选择,你最喜欢读哪一个:
msr vbar_el3, x0
or
msr #6, c12, c0, #0, x0
ARM 帮助插件有助于改进 IDA
Pro 的反汇编。 IDA AArch64 Helper 插件[9]由 Stefan Esser(@ i0n1c)就是一个这样的插件。
不幸的是,它不是公开的。 Alex Hude(@getorix)为 MacOS 写了一个类似的插件 FRIEND [10]。
如果你密切关注这个项目,我最近 push 了一些修改[11],上周已经合并,来让它跨平台。
现在,您有Windows,Linux 和 MacOS 版本的 FRIENDs了 :)
▚ 签名
如前所述,SBOOT 部分基于 ARM 可信固件[12]。 由于源代码可用,可以通过浏览源代码,重新编译它并做二进制比较(或签名匹配)来节省大量的逆向工程工作,以便尽可能多地恢复 symbol。
我通常结合多个二进制 diffing 工具来生成二进制之间的符号:
Rizzo [13] from Craig Heffner (devttys0)
Bindiff [14] from Zymanics
Diaphora [15] from Joxean Koret (@matalaz)
它们有时会产生互为补充的结果。
▚ 结论
在本文中,我描述了如何为三星 Galaxy S6 确定 SBOOT 的基地址,如何加载到 IDA Pro。 这里描述的方法应该适用于其他三星的智能手机,可能适用于其他制造商的使用了 Exynos SoC 的产品。
TEE 操作系统之旅将在下一篇文章中继续。 敬请关注!
▚ 链接
[1] http://www.samsung.com/semiconductor/minisite/Exynos/w/showcase/smartphones/
[2] https://www.trustonic.com/about-trustonic
[3] http://infocenter.arm.com/help/topic/com.arm.doc.den0024a/DEN0024A_v8_architecture_PG.pdf
[4] http://quoteinvestigator.com/2015/08/28/fish/
[5] http://reverseengineering.stackexchange.com/questions/10995/problem-with-ida-pro-6-8-disassemble-galaxy-s6-sboot
[6] http://lists.denx.de/mailman/listinfo/u-boot
[7] https://github.com/ARM-software/arm-trusted-firmware
[8] https://gist.github.com/xerub/c6936d219db8e6635d25
[9] https://youtu.be/dg6byIiAwtc
[10] https://github.com/alexhude/FRIEND
[11] https://github.com/alexhude/FRIEND/pull/6
[12] https://github.com/ARM-software/arm-trusted-firmware
[13] https://github.com/devttys0/ida/blob/master/plugins/rizzo/rizzo.py
[14] https://www.zynamics.com/software.html
[15] https://github.com/joxeankoret/diaphora
▚ 致谢
jb 为我们所有的讨论的帮助。
André“sh4ka”Moulu 鼓励我写这一系列的文章,描述我的到TEE操作系统之旅。
Quarkslab 的同事们对本文的反馈。
作者:Fernand Lone Sang
原文链接:http://blog.quarkslab.com/reverse-engineering-samsung-s6-sboot-part-i.html#id17
本文由 看雪翻译小组 daemond 编译
声明:转载请保留文章的完整性,注明作者、译者及出处, 并附上本文链接。
热 门 阅 读:
《走进企业看安全.易宝支付》第9站 开始报名!
C++编程的 42 条建议(六)完
MySQL Out-of-Band 注入攻击
安卓勒索软件的发展趋势(一)
在 Linux 上使用 AFL 对 Stagefright 进行模糊测试
......
更多优秀文章点击左下角“关注原文”查看!
看雪论坛:http://bbs.pediy.com/
微信公众号 ID:ikanxue
微博:看雪安全
投稿、合作:www.kanxue.com