作为会议的一部分 0x0A DC7831
在本文中,我们将描述如何在模拟器中运行设备固件,演示与调试器的交互,并对固件执行小型动态分析。
史前
很久以前,在一个遥远的星系
几年前,我们的实验室需要研究设备的固件。 固件通过引导加载程序进行压缩和解压。 他以一种非常复杂的方式做到了这一点,多次移动内存中的数据。 然后固件本身会主动与外设进行交互。 而这一切都在 MIPS 内核上。
由于客观原因,可用的模拟器不适合我们,但我们仍然想运行代码。 然后我们决定制作我们自己的模拟器,它会做最少的事情并允许我们解压主要固件。 我们尝试了一下并且成功了。 我们想,如果我们添加外设来执行主要固件会怎样。 这并没有造成太大的伤害——而且也很有效。 我们再三考虑,决定制作一个成熟的模拟器。
结果是一个计算机系统模拟器
为什么是科皮猫?
这是一个文字游戏。
- 复印机 (英语,名词 [ˈkɒpɪkæt]) - 模仿者,模仿者
- 猫 (英语,名词 [ˈkæt]) - 猫,猫 - 该项目创建者之一最喜欢的动物
- 字母“K”来自 Kotlin 编程语言
山寨
创建模拟器时,设定了非常具体的目标:
- 快速创建新外设、模块、处理器内核的能力;
- 从各种模块组装虚拟设备的能力;
- 将任何二进制数据(固件)加载到虚拟设备内存中的能力;
- 使用快照(系统状态的快照)的能力;
- 通过内置调试器与仿真器交互的能力;
- 很好的现代开发语言。
因此,选择 Kotlin 进行实现、总线架构(这是模块通过虚拟数据总线相互通信的情况)、JSON 作为设备描述格式,以及 GDB RSP 作为与调试器交互的协议。
开发已经进行了两年多一点,并且正在积极进行中。 在此期间,实现了 MIPS、x86、V850ES、ARM 和 PowerPC 处理器内核。
该项目正在不断发展,是时候向更广泛的公众展示它了。 稍后我们将对项目进行详细描述,但现在我们将重点关注使用 Kopycat。
对于最不耐烦的人,可以从以下位置下载模拟器的促销版本
模拟器中的犀牛
让我们回想一下,早些时候在 SMARTRHINO-2018 会议上,为教授逆向工程技能而创建了一个测试设备“Rhinoceros”。 静态固件分析的过程描述于
现在让我们尝试添加“扬声器”并在模拟器中运行固件。
我们需要:
1)Java 1.8
2)Python和模块
对于Windows:
1)
2)
对于Linux:
1)索卡特
您可以使用 Eclipse、IDA Pro 或 Radare2 作为 GDB 客户端。
它是如何工作的呢?
为了在模拟器中执行固件,需要“组装”一个虚拟设备,它是真实设备的模拟。
真实设备(“rhino”)可以在框图中显示:
该模拟器具有模块化结构,最终的虚拟设备可以在 JSON 文件中描述。
JSON 105 行
{
"top": true,
// Plugin name should be the same as file name (or full path from library start)
"plugin": "rhino",
// Directory where plugin places
"library": "user",
// Plugin parameters (constructor parameters if jar-plugin version)
"params": [
{ "name": "tty_dbg", "type": "String"},
{ "name": "tty_bt", "type": "String"},
{ "name": "firmware", "type": "String", "default": "NUL"}
],
// Plugin outer ports
"ports": [ ],
// Plugin internal buses
"buses": [
{ "name": "mem", "size": "BUS30" },
{ "name": "nand", "size": "4" },
{ "name": "gpio", "size": "BUS32" }
],
// Plugin internal components
"modules": [
{
"name": "u1_stm32",
"plugin": "STM32F042",
"library": "mcu",
"params": {
"firmware:String": "params.firmware"
}
},
{
"name": "usart_debug",
"plugin": "UartSerialTerminal",
"library": "terminals",
"params": {
"tty": "params.tty_dbg"
}
},
{
"name": "term_bt",
"plugin": "UartSerialTerminal",
"library": "terminals",
"params": {
"tty": "params.tty_bt"
}
},
{
"name": "bluetooth",
"plugin": "BT",
"library": "mcu"
},
{ "name": "led_0", "plugin": "LED", "library": "mcu" },
{ "name": "led_1", "plugin": "LED", "library": "mcu" },
{ "name": "led_2", "plugin": "LED", "library": "mcu" },
{ "name": "led_3", "plugin": "LED", "library": "mcu" },
{ "name": "led_4", "plugin": "LED", "library": "mcu" },
{ "name": "led_5", "plugin": "LED", "library": "mcu" },
{ "name": "led_6", "plugin": "LED", "library": "mcu" },
{ "name": "led_7", "plugin": "LED", "library": "mcu" },
{ "name": "led_8", "plugin": "LED", "library": "mcu" },
{ "name": "led_9", "plugin": "LED", "library": "mcu" },
{ "name": "led_10", "plugin": "LED", "library": "mcu" },
{ "name": "led_11", "plugin": "LED", "library": "mcu" },
{ "name": "led_12", "plugin": "LED", "library": "mcu" },
{ "name": "led_13", "plugin": "LED", "library": "mcu" },
{ "name": "led_14", "plugin": "LED", "library": "mcu" },
{ "name": "led_15", "plugin": "LED", "library": "mcu" }
],
// Plugin connection between components
"connections": [
[ "u1_stm32.ports.usart1_m", "usart_debug.ports.term_s"],
[ "u1_stm32.ports.usart1_s", "usart_debug.ports.term_m"],
[ "u1_stm32.ports.usart2_m", "bluetooth.ports.usart_m"],
[ "u1_stm32.ports.usart2_s", "bluetooth.ports.usart_s"],
[ "bluetooth.ports.bt_s", "term_bt.ports.term_m"],
[ "bluetooth.ports.bt_m", "term_bt.ports.term_s"],
[ "led_0.ports.pin", "u1_stm32.buses.pin_output_a", "0x00"],
[ "led_1.ports.pin", "u1_stm32.buses.pin_output_a", "0x01"],
[ "led_2.ports.pin", "u1_stm32.buses.pin_output_a", "0x02"],
[ "led_3.ports.pin", "u1_stm32.buses.pin_output_a", "0x03"],
[ "led_4.ports.pin", "u1_stm32.buses.pin_output_a", "0x04"],
[ "led_5.ports.pin", "u1_stm32.buses.pin_output_a", "0x05"],
[ "led_6.ports.pin", "u1_stm32.buses.pin_output_a", "0x06"],
[ "led_7.ports.pin", "u1_stm32.buses.pin_output_a", "0x07"],
[ "led_8.ports.pin", "u1_stm32.buses.pin_output_a", "0x08"],
[ "led_9.ports.pin", "u1_stm32.buses.pin_output_a", "0x09"],
[ "led_10.ports.pin", "u1_stm32.buses.pin_output_a", "0x0A"],
[ "led_11.ports.pin", "u1_stm32.buses.pin_output_a", "0x0B"],
[ "led_12.ports.pin", "u1_stm32.buses.pin_output_a", "0x0C"],
[ "led_13.ports.pin", "u1_stm32.buses.pin_output_a", "0x0D"],
[ "led_14.ports.pin", "u1_stm32.buses.pin_output_a", "0x0E"],
[ "led_15.ports.pin", "u1_stm32.buses.pin_output_a", "0x0F"]
]
}
注意参数 固件 部分 的params 是可以作为固件加载到虚拟设备中的文件的名称。
虚拟设备及其与主操作系统的交互可以用下图表示:
模拟器当前的测试实例涉及与主操作系统的COM端口的交互(调试UART和蓝牙模块的UART)。 这些可以是设备连接的真实端口或虚拟 COM 端口(为此,您只需要 com0com/socat).
目前从外部与模拟器交互的方式主要有两种:
- GDB RSP协议(相应地,支持该协议的工具有Eclipse/IDA/radare2);
- 内部模拟器命令行(Argparse 或 Python)。
虚拟COM端口
为了通过终端与本地计算机上的虚拟设备的 UART 进行交互,您需要创建一对关联的虚拟 COM 端口。 在我们的例子中,一个端口由模拟器使用,第二个端口由终端程序(PuTTY 或 screen)使用:
使用com0com
虚拟 COM 端口使用 com0com 套件(控制台版本 - C:Program Files (x86)com0comsetupс.exe, 或 GUI 版本 - C:Program Files (x86)com0comsetupg.exe):
勾选复选框 启用缓冲区溢出 对于所有创建的虚拟端口,否则模拟器将等待 COM 端口的响应。
使用socat
在 UNIX 系统上,虚拟 COM 端口由模拟器使用 socat 实用程序自动创建;为此,只需在启动模拟器时指定端口名称中的前缀即可 socat:
.
内部命令行界面(Argparse 或 Python)
由于 Kopycat 是一个控制台应用程序,因此模拟器提供了两个命令行界面选项用于与其对象和变量交互:Argparse 和 Python。
Argparse 是 Kopycat 中内置的 CLI,并且始终可供所有人使用。
另一种 CLI 是 Python 解释器。 要使用它,您需要安装 Jep Python 模块并配置模拟器以使用 Python(将使用安装在用户主系统上的 Python 解释器)。
安装 Python 模块 Jep
Linux下Jep可以通过pip安装:
pip install jep
要在 Windows 上安装 Jep,必须首先安装 Windows SDK 和相应的 Microsoft Visual Studio。 我们让您的生活变得更加轻松
pip install jep-3.8.2-cp27-cp27m-win_amd64.whl
要检查Jep的安装情况,需要在命令行中运行:
python -c "import jep"
应收到以下消息作为响应:
ImportError: Jep is not supported in standalone Python, it must be embedded in Java.
在您系统的模拟器批处理文件中(山寨.bat - 对于 Windows, 山寨 - 对于 Linux)到参数列表 DEFAULT_JVM_OPTS
添加一个附加参数 Djava.library.path
— 它必须包含已安装的 Jep 模块的路径。
Windows 的结果应该是这样的一行:
set DEFAULT_JVM_OPTS="-XX:MaxMetaspaceSize=256m" "-XX:+UseParallelGC" "-XX:SurvivorRatio=6" "-XX:-UseGCOverheadLimit" "-Djava.library.path=C:/Python27/Lib/site-packages/jep"
启动Kopycat
模拟器是一个控制台 JVM 应用程序。 启动是通过操作系统命令行脚本(sh/cmd)执行的。
Windows下运行命令:
binkopycat -g 23946 -n rhino -l user -y library -p firmware=firmwarerhino_pass.bin,tty_dbg=COM26,tty_bt=COM28
使用 socat 实用程序在 Linux 下运行的命令:
./bin/kopycat -g 23946 -n rhino -l user -y library -p firmware=./firmware/rhino_pass.bin, tty_dbg=socat:./COM26,tty_bt=socat:./COM28
-g 23646
— 将开放用于访问 GDB 服务器的 TCP 端口;-n rhino
——主系统模块名称(组装设备);-l user
— 用于搜索主模块的库的名称;-y library
— 搜索设备中包含的模块的路径;firmwarerhino_pass.bin
— 固件文件的路径;- COM26 和 COM28 是虚拟 COM 端口。
结果会出现提示 Python >
( Argparse >
):
18:07:59 INFO [eFactoryBuilder.create ]: Module top successfully created as top
18:07:59 INFO [ Module.initializeAndRes]: Setup core to top.u1_stm32.cortexm0.arm for top
18:07:59 INFO [ Module.initializeAndRes]: Setup debugger to top.u1_stm32.dbg for top
18:07:59 WARN [ Module.initializeAndRes]: Tracer wasn't found in top...
18:07:59 INFO [ Module.initializeAndRes]: Initializing ports and buses...
18:07:59 WARN [ Module.initializePortsA]: ATTENTION: Some ports has warning use printModulesPortsWarnings to see it...
18:07:59 FINE [ ARMv6CPU.reset ]: Set entry point address to 08006A75
18:07:59 INFO [ Module.initializeAndRes]: Module top is successfully initialized and reset as a top cell!
18:07:59 INFO [ Kopycat.open ]: Starting virtualization of board top[rhino] with arm[ARMv6Core]
18:07:59 INFO [ GDBServer.debuggerModule ]: Set new debugger module top.u1_stm32.dbg for GDB_SERVER(port=23946,alive=true)
Python >
与 IDA Pro 交互
为了简化测试,我们使用Rhino固件作为源文件在IDA中进行分析,格式为
您还可以使用没有元信息的主固件。
在 IDA Pro 中启动 Kopycat 后,在调试器菜单中转到“切换调试器...“然后选择”远程 GDB 调试器”。 接下来,设置连接:菜单 调试器 - 处理选项...
设置值:
- 应用程序 - 任何值
- 主机名:127.0.0.1(或运行 Kopycat 的远程计算机的 IP 地址)
- 端口:23946的
现在调试按钮可用(F9 键):
单击它可以连接到模拟器中的调试器模块。 IDA 进入调试模式,其他窗口变得可用:有关寄存器的信息、有关堆栈的信息。
现在我们可以使用调试器的所有标准功能:
- 逐步执行指令(踏入 и 跨过去 — 分别为 F7 和 F8 键);
- 开始和暂停执行;
- 为代码和数据创建断点(F2 键)。
连接到调试器并不意味着运行固件代码。 当前执行位置必须是地址 0x08006A74
— 功能开始 重置处理程序。 如果向下滚动列表,您可以看到函数调用 主。 您可以将光标放在这一行(地址 0x08006ABE
)并执行操作 运行直到光标 (F4 键)。
接下来可以按F7进入该功能 主.
如果您运行命令 继续处理 (F9 键),然后将出现“请稍候”窗口,并带有一个按钮 暂停:
按下时 暂停 固件代码的执行被挂起,并且可以从被中断的代码中的同一地址继续执行。
如果继续执行代码,您将在连接到虚拟 COM 端口的终端中看到以下行:
“状态旁路”线的存在表明虚拟蓝牙模块已切换到从用户COM端口接收数据的模式。
现在在蓝牙终端(图中的COM29)就可以按照Rhino协议输入命令了。 例如,“MEOW”命令将返回字符串“mur-mur”给蓝牙终端:
不完全模仿我
构建模拟器时,您可以选择特定设备的细节/模拟级别。 例如,可以通过不同的方式模拟蓝牙模块:
- 该设备通过全套命令进行完全模拟;
- 仿真AT命令,从主系统COM口接收数据流;
- 虚拟设备提供完整的数据重定向到真实设备;
- 作为一个总是返回“OK”的简单存根。
当前版本的仿真器采用的是第二种方式——虚拟蓝牙模块进行配置,配置完成后切换到将数据从主系统的COM口“代理”到仿真器的UART口的模式。
让我们考虑一下在外围设备的某些部分未实现的情况下对代码进行简单检测的可能性。 例如,如果负责控制数据传输到 DMA 的定时器尚未创建(检查在函数中执行) ws2812b_等待位于 0x08006840
),那么固件将一直等待标志位被重置 忙碌位于 0x200004C4
显示DMA数据线的占用情况:
我们可以通过手动重置标志来解决这种情况 忙碌 安装后立即。 在IDA Pro中,您可以创建一个Python函数并在断点中调用它,然后将值1写入标志后将断点本身放入代码中 忙碌.
断点处理程序
首先,我们在 IDA 中创建一个 Python 函数。 菜单 文件 - 脚本命令...
在左侧列表中添加一个新片段,为其命名(例如, BPT),
在右侧的文本字段中输入功能代码:
def skip_dma():
print "Skipping wait ws2812..."
value = Byte(0x200004C4)
if value == 1:
PatchDbgByte(0x200004C4, 0)
return False
之后我们按下 运行 并关闭脚本窗口。
现在让我们看看代码 0x0800688A
,设置断点(F2键),编辑它(上下文菜单 编辑断点...),不要忘记将脚本类型设置为Python:
如果当前标志值 忙碌 等于1,那么你应该执行该函数 跳过DMA 在脚本行中:
如果运行固件来执行,可以在IDA窗口中看到断点处理程序代码的触发 输出 按行 Skipping wait ws2812...
。 现在固件不会等待标志被重置 忙碌.
与模拟器交互
为了模仿而模仿不太可能带来喜悦和喜悦。 如果模拟器可以帮助研究人员查看内存中的数据或建立线程的交互,那就更有趣了。
我们将向您展示如何动态地建立 RTOS 任务之间的交互。 如果代码正在运行,您应该首先暂停它的执行。 如果你去函数 蓝牙任务条目 到“LED”命令的处理分支(地址 0x080057B8
),然后就可以看到先创建了什么然后发送到系统队列 led控制队列句柄 一些消息。
您应该设置断点来访问变量 led控制队列句柄位于 0x20000624
并继续执行代码:
结果,停止将首先发生在地址处 0x080057CA
在调用函数之前 osMailAlloc,然后在地址处 0x08005806
在调用函数之前 osMailPut,然后过了一会儿 - 到地址 0x08005BD4
(在调用函数之前 osMailGet),属于函数 leds_task_entry (LED 任务),即任务切换,现在 LED 任务收到控制权。
通过这种简单的方式,您可以确定 RTOS 任务如何相互交互。
当然,实际上,任务的交互可能更加复杂,但是使用模拟器,跟踪这种交互变得不那么费力。
使用 Radare2 启动
您不能忽视像 Radare2 这样的通用工具。
要使用 r2 连接到模拟器,命令如下所示:
radare2 -A -a arm -b 16 -d gdb://localhost:23946 rhino_fw42k6.elf
现已推出(dc
)并暂停执行(Ctrl+C)。
不幸的是,目前,r2 在使用硬件 gdb 服务器和内存布局时存在问题;因此,断点和步骤不起作用(命令 ds
)。 我们希望这个问题能尽快得到解决。
使用 Eclipse 运行
使用模拟器的选项之一是调试正在开发的设备的固件。 为了清楚起见,我们还将使用 Rhino 固件。 您可以下载固件源
我们将使用套件中的 Eclipse 作为 IDE
为了让模拟器加载在Eclipse中直接编译的固件,需要添加参数 firmware=null
到模拟器启动命令:
binkopycat -g 23946 -n rhino -l user -y modules -p firmware=null,tty_dbg=COM26,tty_bt=COM28
设置调试配置
在 Eclipse 中,选择菜单 运行-调试配置... 在打开的窗口中的部分中 GDB硬件调试 您需要添加新配置,然后在“Main”选项卡上指定当前项目和应用程序进行调试:
在“调试器”选项卡上,您需要指定 GDB 命令:
${openstm32_compiler_path}arm-none-eabi-gdb
并输入连接 GDB 服务器的参数(主机和端口):
在“启动”选项卡上,您必须指定以下参数:
- 启用复选框 加载图像 (以便将组装好的固件映像加载到模拟器中);
- 启用复选框 加载符号;
- 添加启动命令:
set $pc = *0x08000004
(将 PC 寄存器设置为内存地址处的值0x08000004
- 地址存储在那里 复位处理程序).
注意,如果你不想从Eclipse下载固件文件,那么选项 加载图像 и 运行命令 无需注明。
单击“调试”后,您可以在调试器模式下工作:
- 一步步执行代码
- 与断点交互
注意. Eclipse 有,嗯...一些怪癖...你必须忍受它们。 例如,如果启动调试器时出现消息“No source available for “0x0””,则执行 Step 命令(F5)
取而代之的是结论
模拟本机代码是一件非常有趣的事情。 设备开发人员可以在没有真实设备的情况下调试固件。 对于研究人员来说,这是进行动态代码分析的机会,即使使用设备,这也并不总是可行。
我们希望为专家提供一种方便、适度简单且不需要花费大量精力和时间来设置和运行的工具。
在评论中写下您使用硬件模拟器的体验。 我们邀请您进行讨论,并很乐意回答问题。
只有注册用户才能参与调查。
你用模拟器做什么?
-
我开发(调试)固件
-
我正在研究固件
-
我推出游戏(Dendi、Sega、PSP)
-
还有什么(写在评论里)
7 位用户投票。 2 名用户弃权。
您使用什么软件来模拟本机代码?
-
QEMU
-
独角兽引擎
-
变形虫
-
还有什么(写在评论里)
6 位用户投票。 2 名用户弃权。
您希望改进您正在使用的模拟器的哪些方面?
-
我想要速度
-
我想要轻松设置/启动
-
我想要更多与模拟器交互的选项(API、钩子)
-
我对一切都很满意
-
还有什么(写在评论里)
由 8 位用户投票。 1 位用户弃权。
来源: habr.com