导语:物联网 (IoT) 设备已经成为我们日常生活、工作环境、医院、政府设施和车队的重要组成部分。
4.研究Xtensa架构特性
现在我们已经将所有段加载到适当的地址,我们可以开始逆向工程了。
但为了高效地做到这一点,我们需要更多地了解 Xtensa 架构,包括:
1.指令中的参数顺序
2.条件跳转的执行细节
3.编译器调用约定
4.堆栈组织
首先要探索的是指令中的参数顺序。例如:MOV R1, R2. 您可以在所有架构中找到此类指令,但这可能意味着将 R1 复制到 R2 或将 R2 复制到 R1。因此,了解指令中源代码的位置以及目标寄存器的位置至关重要。您可以在 GitHub 上找到Xtensa 指令集描述。
至于该MOV指令,在Xtensa中,表示将R2复制到R1。因此,第一个参数将是大多数简单指令(例如数学相关指令)中的目的地。例如,指令addi a14, a1, 0x38意味着 a14 = a1 + 0x38。
但对于存储数据的指令,情况则相反。例如,该指令s32i.n a5, a1, 0x10意味着 的值a5必须存储在地址 处(a1 + 0x10)。
要学习的第二件事是条件跳转的完成方式。有两种方法可以做到这一点:
1.使用专用指令进行比较操作,设置标志寄存器,然后进行条件跳转。
2.使用一条指令一次性执行所有这些操作。
Xtensa 执行后者:beqz a10, loc_400E1C54
使用单个指令来检查是否a10等于零,然后它要么跳转到loc_400E1C54,要么不跳转。
第三步是检查编译器使用的调用约定:将参数传递给函数的方式以及如何返回值。
Xtensa 以一种非常不寻常的方式传递参数。参数在调用指令之前放入寄存器中。但它们在函数中出现的寄存器与调用之前所在的寄存器不同:
以下是如何在汇编程序级别将参数传递给函数的示例:
这里我们有三个论据:
a10 是目的地址
a11 是源地址
a12 是要复印的尺寸
然而,一旦代码进入memcpy函数,这些值就会自动分别传输到a2、a3和a4寄存器中。
同样的技巧也用于返回值。在memcpy函数内部,该值存储在寄存器中a2,但从函数返回后,该值出现在a10.
返回的样子如下0:
这就是检查返回值的样子:
benz.na10在从调用返回时检查寄存器的值。
最后,有必要了解堆栈是如何组织的。
Xtensa 使用 a1 寄存器来创建堆栈帧。每个函数都以入口指令开始:entry a1,0xC0,其中0xC0是堆栈帧的大小,即函数需要用于堆栈变量的堆栈量。
通常,这些函数从初始化堆栈变量开始:
寄存器中的零值a5被写入基于a1寄存器的堆栈变量中。
在获得有关 Xtensa 架构的所有必要知识后,我们终于可以开始逆向其代码了。
5. 在 IDA 中对 Xtensa 代码进行逆向工程
与 ARM、MIPS 和 PowerPC 相比,Xtensa 不是最流行的架构,并且没有完整的功能列表。因此,IDA处理器模块会存在一些我们需要克服的限制。
IDA 中 Xtensa 处理器模块的主要限制是:
函数参数没有自动注释
堆栈帧不会自动创建
一些 ESP32 函数属于 IROM,因此存在对硬编码地址的调用
部分Xtensa指令未反汇编
让我们讨论一些克服这些挑战的技巧。
5.1. 函数参数的类型系统和注释
从 IDA 7.7 开始提供 Xtensa类型系统。在 IDA 中拥有可用的类型系统非常重要,因为它使逆向变得方便。特别是,它允许您导入 C 结构的定义并指定 IDA 使用的函数原型,以便在传输函数参数的指令附近放置自动注释。
但是,如果您没有类型系统,还有一个解决方法。
首先,让我们看看有类型系统时函数是什么样子的:
屏幕截图 13. 当有可用的类型系统时函数的外观
函数原型设置有参数的名称和类型,以便 IDA 可以使用此信息在调用站点注释参数:
屏幕截图 14. 函数原型
但 Xtensa 不会有这样的事情。另一种方法是使用 IDA 中的可重复注释功能。如果您在函数的开头设置可重复的注释,它将显示在所有调用站点上。
屏幕截图 15. 设置可重复注释
屏幕截图 16. 可重复的注释显示在所有调用站点上
因此,我们可以使用此功能来定义函数参数:
屏幕截图 17. 使用可重复注释功能定义函数参数
调用站点将如下所示:
屏幕截图 18. 调用站点
您可以在注释中选择寄存器名称,IDA 会在代码中突出显示它。因此,您可以轻松找到参数值。
5.2. 恢复堆栈帧
要恢复堆栈帧,您需要手动指定堆栈大小,然后通过在每个与堆栈一起使用的指令处按K 键来显示 IDA 的使用位置。
让我们探索一下config_router_safe函数,例如:
屏幕截图 19. config_router_safe 函数
很明显这里的栈帧大小是 0xC0。我们在函数的堆栈设置中使用该值(Alt+P):
屏幕截图 20. 使用 0xC0 值(堆栈帧大小)
从视觉上看,什么也不会发生,但是如果您通过按 Ctrl+K 转到该函数的堆栈帧,您会注意到堆栈空间现在已分配:
屏幕截图 21. 分配堆栈空间
接下来要做的是使用entry指令指定堆栈移位。在此之前,我们建议启用堆栈指针可视化,如下面的屏幕截图所示:
屏幕截图 22. 启用堆栈指针可视化
现在,代码应该如下所示:
屏幕截图 23.启用堆栈指针可视化后的代码
000是当前堆栈指针移位值,我们需要将其移位0xC0。为此,请将光标置于入口指令处,然后按Alt+K以查看以下窗口,您可以在其中指定新旧堆栈指针之间所需的差异:
屏幕截图 24. 将当前堆栈指针值移动 0xC0
作为此操作的结果,代码将如下所示:
屏幕截图 25. 移动当前堆栈指针移位值后的代码
现在,如果您开始在与寄存器一起使用的每条指令处按Ka1,IDA 将创建堆栈变量:
屏幕截图 26.IDA 创建新的堆栈变量
还可以编写 IDA 脚本来自动执行这些操作。
5.3. 调用 IROM
调用位于 CPU 的 IROM 部分而不是固件中的某些低级 API 的情况并不少见。在这种情况下,固件仅与包含定义的 IROM 函数地址的特殊链接器定义文件链接。
在逆向期间,IROM 函数调用如下所示:
屏幕截图 27. IROM 函数调用
40058E4C是 IROM 内的地址。但不可能知道固件调用了哪个函数。因此有必要检查 ESP32 工具链以查找链接器定义。
ESP32 芯片的 IDE 是Espressif IDE。在 IDE 文件中搜索 IROM 地址,我们会找到:C:\Espressif\frameworks\esp-idf-v4.4.2\components\esptool_py\esptool\flasher_stub\ld\rom_32.ld
屏幕截图 28. ESP32 ROM 地址表
这些值可以轻松转换为枚举数据类型:
屏幕截图 29. 将值转换为枚举数据类型
然后,我们需要导入 IDA,以便将 enum 应用于 IROM 地址值:
屏幕截图 30. 将枚举应用于 IROM 地址值
如果我们在 IROM 地址附近添加可重复的注释,它将使所有内容更容易阅读:
屏幕截图 31. 在 IROM 地址附近添加可重复注释后的代码
5.4. 无法识别的指令
经常发生的情况是,处理器模块已针对指令集的某些特定变体实现。然后制造商制造出具有 99% 兼容指令集的新 CPU,其中包含 10 多个新指令,这是最初没人想到的。因此IDA、Ghidra和Radare等工具可能无法反汇编一些新指令。
克服这一挑战的正确方法是扩展处理器模块并添加对新指令的支持。这需要对反汇编器 API 有深入的了解,而这些 API 并不那么容易理解。
让我们讨论一种可能的解决方法,用于解决以下情况:尽管存在一些无法识别的指令,但您只想让 IDA 创建函数。假设 IDA 不知道 RER 指令,并且在包含 RER 操作码的情况下无法创建该函数:
屏幕截图 32. 如果函数包含 RER 操作码,IDA 无法创建该函数
您可以按P多次。除了控制台窗口中出现错误外,不会发生任何事情:
屏幕截图 33. 控制台窗口中的错误
但是,这并不意味着 IDA 无法创建遵循 RER 指令的指令。您可以跳过 RER 指令的三个字节,然后创建代码:
屏幕截图 34. 跳过 RER 指令的三个字节后创建代码
接下来,您可以选择从输入到最后的整段代码retw.n,然后按P:
屏幕截图 35. 选择从 Entry 到 retw.n 的整段代码
之后,IDA 将创建该函数:
屏幕截图 36.IDA 创建一个函数
通常,反汇编程序无法识别的扩展指令在逆向过程中不会产生太大差异。可能导致问题的是执行调用、跳转或加载/存储等操作的新指令,因为代码流丢失并且对数据的引用丢失。
结论
对于涉及逆向工程物联网固件的项目来说,在转向业务逻辑之前研究未知的硬件架构至关重要。尽管逆向工程师可能需要几周的时间来学习该架构,但从长远来看,这种深入的研究有助于提高进一步工作的速度。
本文翻译自:https://www.apriorit.com/dev-blog/reverse-reverse-engineer-iot-firmware如若转载,请注明原文地址