在2月16日的会议0x0A DC7831 DEF CON Nizhny Novgorod的框架中,我们提交了一份有关二进制代码仿真的基本原理和我们自己的开发报告-硬件平台仿真器Kopycat的报告 。
在本文中,我们将描述在仿真器中启动设备固件,演示与调试器的交互以及对固件进行小的动态分析。
背景知识
很久以前在一个遥远的星系中
几年前,在我们的实验室中,需要研究设备的固件。 固件已压缩,由引导程序解压缩。 他以一种非常混乱的方式进行了此操作,几次将数据移入内存。 是的,然后固件本身与外围设备进行了主动交互。 而所有这些都在MIPS内核上。
出于客观原因,现有的模拟器不适合我们,但我仍然想运行代码。 然后,我们决定制作自己的仿真器,该仿真器将最小化并允许解压缩主固件。 我们尝试了-事实证明。 我们认为,如果我们添加外围设备以也执行主要固件该怎么办。 这不是很痛苦-而且也解决了。 我们再次考虑,决定制作一个完整的模拟器。
结果是计算机系统Kopycat的仿真器。

为什么选择Kopycat?有文字游戏。
- copycat (英语,n。[ˈkɒpɪkæt])-模仿者,模仿者
- 猫 (英语,n。[ˈkæt])-猫,猫-该项目一位创作者的最爱动物
- 字母“ K”-来自Kotlin编程语言
科比猫
创建仿真器时,设置了绝对特定的目标:
- 快速创建新的外围设备,模块,处理器核心的能力;
- 从各种模块组装虚拟设备的能力;
- 能够将任何二进制数据(固件)加载到虚拟设备的内存中;
- 处理快照(系统状态快照)的能力;
- 通过内置调试器与仿真器进行交互的能力;
- 很好的现代语言发展。
结果,选择了Kotlin来实现,总线体系结构(这是模块之间通过虚拟数据总线相互通信的时间),JSON作为设备描述格式以及GDB RSP作为与调试器进行交互的协议。
开发已经进行了两年多,并且正在积极进行中。 在此期间,实现了MIPS,x86,V850ES,ARM,PowerPC处理器内核。
该项目正在发展,是时候向大众介绍它了。 稍后我们将对该项目进行详细描述,但是现在我们将重点介绍使用Kopycat。
对于最急躁的人,可以在此处下载仿真器的促销版。
犀牛在模拟器中
回想一下,早在SMARTRHINO-2018大会上,就创建了一个测试设备“ Rhinoceros”,用于培训逆向工程技能。 本文介绍了静态固件分析的过程。
现在,让我们尝试添加“扬声器”并在仿真器中运行固件。
我们将需要:
1)Java 1.8
2)Python和用于在仿真器内部使用Python的Jep模块。 Windows的Jep模块的WHL组件可在此处下载 。
对于Windows:
1) com0com
2) 腻子
对于Linux:
1)socat
您可以将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"] ] }
请注意参数部分中的固件参数-这是可以作为固件下载到虚拟设备的文件的名称。
虚拟设备及其与主操作系统的交互可以表示如下:
仿真器的当前测试实例涉及与主OS的COM端口(调试UART和Bluetooth模块的UART)进行交互。 这些可以是连接设备的实际端口,也可以是虚拟COM端口( com0com / socat仅用于此目的) 。
当前有两种从外部与仿真器进行交互的主要方法:
- GDB RSP协议(分别支持该协议的工具-Eclipse / IDA / radare2);
- 模拟器内部命令行(Argparse或Python)。
为了通过终端与本地计算机上虚拟设备的UART进行交互,您需要创建几个连接的虚拟COM端口。 在我们的例子中,一个端口使用仿真器,第二个端口使用终端程序(PuTTY或屏幕):
使用com0com
虚拟com端口是使用com0com套件中的设置实用程序配置的(控制台版本为C:\ Program Files(x86)\ com0com \setup.exe,或者GUI版本为C:\ Program Files(x86)\ com0com \ setupg.exe ) :
选中所有创建的虚拟端口的启用缓冲区溢出复选框 ,否则仿真器将等待来自COM端口的响应。
使用socat
在UNIX系统上,仿真器使用socat实用程序自动创建虚拟COM端口,为此在启动仿真器时在端口名称中指定socat:
前缀就足够了。
内部命令行界面(Argparse或Python)
由于Kopycat是一个控制台应用程序,因此模拟器为命令行界面提供了两个选项以与其对象和变量进行交互:Argparse和Python。
Argparse是Kopycat内置的CLI,它始终对所有人可用。
另一个CLI是Python解释器。 要使用它,您需要安装Jep Python模块并配置模拟器以与Python一起使用(将使用安装在用户主系统上的Python解释器)。
安装Python Jep模块
在Linux下,可以通过pip安装Jep:
pip install jep
要在Windows下安装Jep,必须首先安装Windows SDK和相应的Microsoft Visual Studio。 我们稍微简化了您的任务,并为Windows的当前版本的Python创建了WHL JEP 程序集 ,因此可以从文件安装模块:
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.
在系统的仿真器批处理文件中(对于Windows为kopycat.bat,对于Linux为kopycat ),将附加参数Djava.library.path
添加到DEFAULT_JVM_OPTS
参数列表中-它应包含已安装的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下运行的命令:
bin\kopycat -g 23946 -n rhino -l user -y library -p firmware=firmware\rhino_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
-搜索设备中包含的模块的路径;firmware\rhino_pass.bin
固件文件的路径;- COM26和COM28是虚拟COM端口。
结果将是Python >
(或Argparse >
) 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固件作为ELF文件 (元信息保存在其中)作为IDA中进行分析的源文件。
您也可以使用不带元信息的主固件。
在IDA Pro中启动Kopycat之后,在“调试器”菜单中,转到“ 切换调试器... ”项,然后选择“ 远程GDB调试器 ”。 接下来,配置连接: 调试器菜单-进程选项...
设置值:
- 应用-任何价值
- 主机名:127.0.0.1(或运行Kopycat的远程计算机的IP地址)
- 港口:23946
现在可以使用调试开始按钮(F9键):
按下它-它连接到仿真器中的调试器模块。 IDA进入调试模式,其他窗口可用:有关寄存器的信息,有关堆栈的信息。
现在,我们可以使用调试器的所有标准功能:
- 分步执行指令(分别进入 F7和F8键)。
- 开始和暂停执行;
- 在代码和数据上创建断点(F2键)。
连接到调试器并不意味着启动固件代码。 当前执行位置必须为地址0x08006A74
- Reset_Handler函数的开始。 如果您向下滚动下面的列表,则可以看到对主功能的调用。 您可以将光标放在该行上(地址0x08006ABE
)并执行“ 运行直到光标”操作(F4键)。

接下来,您可以按F7进入主要功能。
如果执行继续过程命令(F9键),则将出现“请稍候”窗口,其中只有一个“ 挂起”按钮:
按下“ 挂起”后 ,将暂停执行固件代码,并且可以从被中断的代码中的同一地址继续执行固件代码。
如果继续执行代码,则在连接到虚拟COM端口的终端中,您可以看到以下几行:


字符串“状态旁路”的存在指示虚拟蓝牙模块已切换到从用户的COM端口接收数据的模式。
现在,在Bluetooth终端(图中为COM29)中,您可以根据Rhino协议输入命令。 例如,字符串“ mur-mur”返回到蓝牙终端中的“ MEOW”命令:

不完全模仿我
构建仿真器时,可以选择设备的详细程度/仿真。 因此,例如,可以以不同方式仿真蓝牙模块:
- 具有全套命令的完全仿真的设备;
- 模拟AT命令,并从主系统的COM端口接收数据流;
- 虚拟设备可将完整的数据重定向到真实设备;
- 作为一个简单的存根,始终返回“ OK”。
在当前版本的仿真器中,使用第二种方法-虚拟蓝牙模块执行配置,然后从主系统的COM端口切换到仿真器UART端口,以将数据切换为“代理”模式。
如果外围设备的某些部分未实现,则考虑简单地编写代码的可能性。 例如,如果未创建用于控制DMA中数据传输的计时器(在位于0x08006840
的ws2812b_wait函数中执行验证),则固件将始终等待位于0x200004C4
的busy标志重置DMA数据线:

我们可以通过在设置后立即手动重置忙标志来避免这种情况。 在IDA Pro中,您可以创建一个Python函数并在断点处调用它,并且必须在将值1写入busy标志后在代码中设置断点。
断点处理程序
首先,在IDA中创建一个Python函数。 文件菜单-脚本命令...
在左侧列表中添加一个新代码段,并为其命名(例如BPT ),
在右侧的文本框中,输入功能代码:
def skip_dma(): print "Skipping wait ws2812..." value = Byte(0x200004C4) if value == 1: PatchDbgByte(0x200004C4, 0) return False
之后,单击运行并关闭脚本窗口。
现在,让我们转到0x0800688A
的代码,设置断点(F2键),对其进行编辑( 编辑断点...上下文菜单),不要忘记设置脚本类型-Python:
如果busy标志的当前值为1,则应该在脚本行中执行skip_dma函数:

如果运行固件以执行,则可以在IDA中“ Skipping wait ws2812...
”行的“ 输出”窗口中看到断点处理程序代码。 现在,固件将不再等待重置忙标志。
仿真器交互
为了模仿而进行的模仿不太可能引起喜悦和喜悦。 如果仿真器帮助研究人员查看内存中的数据或建立流的交互作用,那就更有趣了。
我们展示了如何动态建立RTOS任务的交互。 首先,如果代码正在运行,则暂停其执行。 如果在“ LED”命令处理分支(地址0x080057B8
)中切换到bluetooth_task_entry函数,则可以看到首先创建的内容,然后将消息发送到ledControlQueueHandle系统队列。

您应该设置断点以访问位于0x20000624
的ledControlQueueHandle变量,然后继续执行代码:
结果,首先它将在调用osMailAlloc函数之前在地址0x080057CA
处停止,然后在调用osMailPut函数之前在0x08005806
处0x08005806
,然后在属于leds_task_entry函数(LED任务)(即LED任务)的地址0x08005BD4
上稍等片刻 。有一个任务开关,现在控件已接收到LED任务。

通过这种简单的方法,您可以确定RTOS任务之间如何交互。
当然,实际上,任务的交互可能会更加复杂,但是使用仿真器跟踪此交互的难度会降低。
在这里,您可以观看模拟器启动以及与IDA Pro交互的简短视频。
使用Radare2启动
您不能忽略Radare2这样的通用工具。
要使用r2连接到仿真器,命令将如下所示:
radare2 -A -a arm -b 16 -d gdb://localhost:23946 rhino_fw42k6.elf
现在可以启动( dc
)和暂停执行(Ctrl + C)。
不幸的是,在r2中,使用硬件gdb服务器和内存标记时存在问题,因此,断点和步骤( ds
命令)不起作用。 我们希望这一问题将在不久的将来得到解决。
使用Eclipse启动
使用仿真器的选项之一是调试正在开发的设备的固件。 为了清楚起见,我们还将使用Rhino固件。 您可以从此处下载固件源。
我们将使用System Workbench for STM32套件中的Eclipse作为IDE。
为了将在Eclipse中直接编译的固件加载到仿真器中,需要将firmware=null
参数添加到仿真器启动命令中:
bin\kopycat -g 23946 -n rhino -l user -y library -p firmware=null,tty_dbg=COM26,tty_bt=COM28
调试配置
在Eclipse中,选择Run-Debug Configurations ...菜单在打开的窗口的GDB Hardware Debugging部分中,您需要添加一个新配置,然后在“ Main”选项卡上指定当前项目和应用程序进行调试:

在“调试器”选项卡上,您必须指定GDB命令:
${openstm32_compiler_path}\arm-none-eabi-gdb
并输入用于连接到GDB服务器的参数(主机和端口):

必须在“启动”选项卡上指定以下参数:
- 启用复选框“ 加载映像” (以便将组装的固件映像加载到仿真器中);
- 启用选中标记加载符号 ;
- 添加一个启动命令:
set $pc = *0x08000004
(将内存中的值设置为PC寄存器中的地址0x08000004
的地址存储在此处)。
请注意 ,如果您不想从Eclipse下载固件文件,则无需指定“ 加载映像”和“运行命令”参数。

单击“调试”后,可以在调试模式下工作:
- 分步执行代码

- 与断点的交互

注意事项 Eclipse具有……一些功能……您必须忍受它们。 例如,如果在启动调试器时出现消息“没有可用的源“ 0x0”,则运行步骤命令(F5)
而不是结论
模拟本地代码是一件非常有趣的事情。 对于设备开发人员而言,无需实际设备即可调试固件。 对于研究人员-进行动态代码分析的能力,即使使用设备也不总是可能的。
我们希望为专家提供一种工具,该工具将方便,适度简单并且无需花费太多精力和时间来配置和启动。
在评论中写下有关您使用硬件仿真器的经验。 我们邀请您进行讨论,并乐于回答问题。