欢迎加入我们这场 Unblob(灵活的固件提取工具)的动手演示。 在本次研讨会环节中,我们将从一台电动汽车充电器中提取固件,深入 研究该固件,并最终对其进行仿真,以便实时与其中的 服务进行交互。Unblob 既适用于硬件版本,也适用于可下载版本 的固件,因此我们有丰富的目标环境。无需任何经验, 本环节适合各种技能水平,我们期待与大家现场 相见。
我们的目标是一台来自 Phoenix Contact 的电动汽车充电站控制器。你可以在此处找到更多详细信息。
CHARX control modular,交流充电控制器,搭载嵌入式 Linux 系统,IEC 61851-1,工作模式:Stand-Alone、Client、Server,
接口:
- 以太网(2x)
- 蜂窝通信(4G/2G)
- CHARX control modular 系统总线
- MICRO-USB type C
通信协议:
- OCPP 1.6J
- Modbus/TCP
- MQTT
可连接外围设备:
- 电能表
- RFID
- 直流剩余电流检测
- DIN 导轨安装
本次研讨会需要用到一些工具。你可以通过
运行 install-prerequisites 脚本来安装它们,方法如下:```sh
./install-prerequisites
## 获取固件
固件可以从供应商网站获取。此仓库中有一个名为
`download-firmware` 的脚本,你可以使用它来拉取固件,
无需打开浏览器。
我们今天的重点是供应商提供的固件,因为它包含我们所需的一切。
但类似的工作流程也可应用于从实机设备提取的内存转储。
有趣的是,我们甚至无需真实设备即可进行提取、探索和仿真。
## 使用 Unblob 进行提取
首先,确保所有依赖项都可用:```
unblob --show-external-dependencies
The following executables found installed, which are needed by unblob:
7z ✓
debugfs ✓
jefferson ✓
lz4 ✓
lziprecover ✓
lzop ✓
sasquatch ✓
sasquatch-v4be ✓
simg2img ✓
ubireader_extract_files ✓
ubireader_extract_images ✓
unar ✓
zstd ✓
现在我们可以使用 unblob 提取固件:``` unblob CHARXSEC3XXXSoftwareBundleV190.raucb
在一台配置不错的笔记本电脑上,提取大约需要 3 分钟。你应该会看到进度条向上移动:

提取完成后,应该会出现一个名为
`CHARXSEC3XXXSoftwareBundleV190.raucb_extract` 的目录。你可以进入
其中并列出内容。
### 数据块(Chunks)与未知数据块(Unknown Chunks)
Unblob 通过识别文件中的数据块来工作。如果某个块是
压缩流,它会被解压。如果是文件系统或归档文件,它
会被提取。如果提取或解压成功,这个块
被分割到磁盘后将被删除以释放空间。
这里,一个 SquashFS 版本 4 小端序(little-endian)块被分割到磁盘、提取,
然后删除。文件(以及因此产生的提取目录)使用
`{start_offset}-{end_offset}.{type}` 命名规则进行命名。
我们可以看到,在 squashfs 文件系统之后出现了 11KB 的 "unknown" 块。```
./0-132173824.squashfs_v4_le_extract
./132173824-132184833.unknown
你可以对它运行 binwalk 来查看其中包含的内容:```
binwalk 132173824-132184833.unknown
0 0x0 Object signature in DER format (PKCS header length: 4, sequence length: 10997 58 0x3A Certificate in DER format (x509 v3), header length: 4, sequence length: 4372 4434 0x1152 Certificate in DER format (x509 v3), header length: 4, sequence length: 4387
您可以使用 openssl 检查证书:```
dd if=132173824-132184833.unknown bs=1 skip=58 | openssl x509 -in /dev/stdin -inform der -noout -text
dd if=132173824-132184833.unknown bs=1 skip=4434 | openssl x509 -in /dev/stdin -inform der -noout -text
这意味着固件很可能使用厂商私钥签名,以便设备能够确保固件的真实性。
这是 unblob 的优势之一,它将未知的未知转化为可以被调查的已知的未知。
让我们来看看我们的 squashfs 文件系统的内容:``` ls -al 0-132173824.squashfs_v4_le_extract total 460532 drwxrwxr-x 4 kali kali 4096 dec 5 09:51 . drwxrwxr-x 3 kali kali 4096 dec 5 09:51 .. -rw-rw-r-- 1 kali kali 20971520 sep 8 09:59 bootimg.vfat drwxrwxr-x 3 kali kali 4096 dec 5 09:51 bootimg.vfat_extract -rwxrwxr-x 1 kali kali 2654 sep 19 2022 hook -rw-rw-r-- 1 kali kali 442 sep 8 09:59 manifest.raucm -rw-rw-r-- 1 kali kali 450584576 sep 8 09:59 root.ext4 drwxrwxr-x 22 kali kali 4096 sep 8 09:57 root.ext4_extract
我们可以看到一份纯文本清单、一个 shell 脚本、一个 MBR 和一个 EXT4 文件系统:```
find -maxdepth 1 -type f -exec file {} \;
./manifest.raucm: ASCII text
./hook: a /usr/bin/env sh script, ASCII text executable
./bootimg.vfat: DOS/MBR boot sector, code offset 0x3c+2, OEM-ID "mkfs.fat", sectors/cluster 4, reserved sectors 4, root entries 512, sectors 40960 (volumes <=32 MB), Media descriptor 0xf8, sectors/FAT 40, sectors/track 63, heads 255, hidden sectors 163158016, reserved 0x1, serial number 0xd09aad9c, label: "KERNEL ", FAT (16 bit)
./root.ext4: Linux rev 1.0 ext4 filesystem data, UUID=8ed19606-02c4-42e7-9cfb-1a2839f93ec4 (extents) (large files) (huge files)
Both bootimg.vfat and root.ext4 were handled and extracted by unblob. The
VFAT partition contains everything related to boot and OS (Linux kernel, DTB,
TEE):```
oftree: Device Tree Blob version 17, size=27469, boot CPU=0, string block size=1657, DT structure block size=25756
tee.bin: data
zImage: Linux kernel ARM boot executable zImage (little-endian)
zImage-imx6ul-ksp0632.dtb: Device Tree Blob version 17, size=27469, boot CPU=0, string block size=1657, DT structure block size=25756
你会看到 unblob 有点贪心,会从 Linux 内核(`zImage`)中提取出一个 ELF 文件和一个 CPIO 归档,这些对应于最小的内核和 ramdisk。
EXT4 文件系统包含由 Linux 内核在启动时挂载的根文件系统:```
ls -alh root.ext4_extract
total 88K
drwxrwxr-x 22 kali kali 4,0K sep 8 09:57 .
drwxrwxr-x 4 kali kali 4,0K dec 5 09:51 ..
drwxrwxr-x 2 kali kali 4,0K dec 5 09:51 bin
drwxrwxr-x 3 kali kali 4,0K dec 5 09:51 boot
drwxrwxr-x 16 kali kali 4,0K sep 8 09:56 data
drwxrwxr-x 2 kali kali 4,0K jul 18 00:09 dev
drwxrwxr-x 53 kali kali 4,0K dec 5 09:51 etc
drwxrwxr-x 18 kali kali 4,0K sep 8 09:56 home
drwxrwxr-x 2 kali kali 4,0K jul 18 03:13 identity
drwxrwxr-x 9 kali kali 4,0K dec 5 09:51 lib
drwxrwxr-x 2 kali kali 4,0K jul 18 03:14 log
drwxrwxr-x 2 kali kali 4,0K sep 8 09:57 lost+found
drwxrwxr-x 2 kali kali 4,0K jul 18 00:09 media
drwxrwxr-x 2 kali kali 4,0K jul 18 00:09 mnt
drwxrwxr-x 2 kali kali 4,0K jul 18 00:09 proc
drwxrwxr-x 2 kali kali 4,0K sep 8 09:57 run
drwxrwxr-x 2 kali kali 4,0K dec 5 09:51 sbin
drwxrwxr-x 2 kali kali 4,0K jul 18 03:14 sdcard
drwxrwxr-x 2 kali kali 4,0K jul 18 00:09 sys
drwxrwxrwx 2 kali kali 4,0K jul 18 00:09 tmp
drwxrwxr-x 11 kali kali 4,0K sep 8 09:56 usr
drwxrwxr-x 11 kali kali 4,0K dec 5 09:51 var
让我们使用一组不同的 unblob 选项来进行一些探索:```
unblob -e /tmp/out -f -k -d 3 --report /tmp/report.json
--log /tmp/unblob.log CHARXSEC3XXXSoftwareBundleV190.raucb
这里我们持续使用 (`-e`) 提取到 `/tmp/out`,同时使用 (`-f`) 强制覆盖,并通过 (`-k`) 保留雕刻出的块,但将递归深度 (`-d`) 限制为 3。我们将详细报告 (`--report`) 写入 `/tmp/report.json`,并将日志文件 (`--log`) 写入 `/tmp/unblob.log`。
查看一下日志文件,你会看到 unblob 的内部工作原理。
报告文件包含有关已分析文件(大小、文件类型、路径、魔数、MIME 类型、MD5/SHA1/SHA256 哈希)、块(大小、偏移量、熵分布)以及任务(提取、解压缩、雕刻)的详细信息。结合一点 Python,你可以从这些报告文件生成漂亮的可视化结果。
你可以使用本仓库中提供的 `diagram.py` Python 脚本自行创建它们:```
python3 diagram.py /tmp/report.json sunburst
python3 diagram.py /tmp/report.json treemap
这些命令将在浏览器中打开一个页面,其中包含基于 plotly 的可视化 图表,如下所示:


既然我们已经更清楚地了解了内部结构,是时候列出我们 需要对设备进行正确仿真所需的信息了。
理想情况下,我们需要收集以下几项信息:
要获取有关平台、CPU 和架构的一些详细信息,我们可以查看 固件中嵌入的设备树二进制文件。
在 VFAT 分区中,我们有以下 DTB:``` oftree: Device Tree Blob version 17, size=27469, boot CPU=0, string block size=1657, DT structure block size=25756 zImage-imx6ul-ksp0632.dtb: Device Tree Blob version 17, size=27469, boot CPU=0, string block size=1657, DT structure block size=25756
这些文件是设备树的二进制表示。我们可以通过使用 `device-tree-compiler` 来恢复它们的明文源代码:```
dtc -I dtb -o zImage-imx6ul-ksp0632.dts zImage-imx6ul-ksp0632.dtb
dtc -I dtb -o oftree.dts oftree
看一下 DTS,你很快就会发现一些有趣的细节,比如 model:``` /dts-v1/;
/ { #address-cells = <0x01>; #size-cells = <0x01>; model = "Phytec phyBOARD-Segin i.MX6 UltraLite"; <----- right here compatible = "phytec,imx6ul-pbacd-10\0phytec,imx6ul-pcl063\0fsl,imx6ul";
CPU:```
cpus {
#address-cells = <0x01>;
#size-cells = <0x00>;
cpu@0 {
compatible = "arm,cortex-a7"; <---- here
device_type = "cpu";
reg = <0x00>;
clock-latency = <0xee6c>;
operating-points = <0xa9ec0 0x137478 0x80e80 0x11edd8 0x60ae0 0xfa3e8 0x30570 0xe7ef0>;
fsl,soc-operating-points = <0xa9ec0 0x137478 0x80e80 0x11edd8 0x60ae0 0x11edd8 0x30570 0x11edd8>;
clocks = <0x01 0x5d 0x01 0x1a 0x01 0x26 0x01 0xdb 0x01 0x38 0x01 0x39 0x01 0x19>;
clock-names = "arm\0pll2_bus\0pll2_pfd2_396m\0secondary_sel\0step\0pll1_sw\0pll1_sys";
arm-supply = <0x02>;
soc-supply = <0x03>;
dc-supply = <0x04>;
};
};
或者甚至外设:``` --snip-- ethernet@2188000 { compatible = "fsl,imx6ul-fec\0fsl,imx6q-fec"; reg = <0x2188000 0x4000>; interrupts = <0x00 0x76 0x04 0x00 0x77 0x04>; clocks = <0x01 0x90 0x01 0x91 0x01 0x30 0x01 0x2c 0x01 0x2c>; clock-names = "ipg\0ahb\0ptp\0enet_clk_ref\0enet_out"; fsl,num-tx-queues = <0x01>; fsl,num-rx-queues = <0x01>; status = "okay"; pinctrl-names = "default"; pinctrl-0 = <0x23>; phy-mode = "rmii"; phy-handle = <0x24>;
mdio {
#address-cells = <0x01>;
#size-cells = <0x00>;
ethernet-phy@1 {
reg = <0x01>;
interrupt-parent = <0x0b>;
interrupts = <0x02 0x08>;
micrel,led-mode = <0x01>;
clocks = <0x01 0x2c>;
clock-names = "rmii-ref";
status = "okay";
linux,phandle = <0x24>;
phandle = <0x24>;
};
ethernet-phy@2 {
reg = <0x03>;
micrel,led-mode = <0x01>;
clocks = <0x01 0x2d>;
clock-names = "rmii-ref";
status = "okay";
linux,phandle = <0x13>;
phandle = <0x13>;
};
};
};
--snip-- flexcan@2090000 { compatible = "fsl,imx6ul-flexcan\0fsl,imx6q-flexcan"; reg = <0x2090000 0x4000>; interrupts = <0x00 0x6e 0x04>; clocks = <0x01 0x94 0x01 0x95>; clock-names = "ipg\0per"; status = "okay"; pinctrl-names = "default"; pinctrl-0 = <0x0f>; xceiver-supply = <0x10>; };
flexcan@2094000 { compatible = "fsl,imx6ul-flexcan\0fsl,imx6q-flexcan"; reg = <0x2094000 0x4000>; interrupts = <0x00 0x6f 0x04>; clocks = <0x01 0x96 0x01 0x97>; clock-names = "ipg\0per"; status = "disabled"; };
### Linux 内核版本信息
我们可以通过在被压缩内核所在的 VFAT 分区中进行 grep,
来识别 Linux 内核版本:```
grep 'Linux version' . -ra
./zImage_extract/7296-6371487.lzo_extract/7296-6371487:
Linux version 5.15.195 (oe-user@oe-host) (gcc version 8.3.0 (GCC)) #1 SMP PREEMPT
./zImage_extract/7296-6371487.lzo_extract/7296-6371487:
Linux version %s (%s)Bluetooth subsystem version %u.%uHCI socket registration failed
另一种方式是查看EXT4根文件系统中的内核模块:```
modinfo ./CHARXSEC3XXXSoftwareBundleV190.raucb_extract/0-138014720.squashfs_v4_le_extract/root.ext4_extract/lib/modules/5.15.195/kernel/net/bluetooth/bnep/bnep.ko.xz
filename: ./CHARXSEC3XXXSoftwareBundleV190.raucb_extract/0-138014720.squashfs_v4_le_extract/root.ext4_extract/lib/modules/5.15.195/kernel/net/bluetooth/bnep/bnep.ko.xz
alias: bt-proto-4
license: GPL
version: 1.3
description: Bluetooth BNEP ver 1.3
author: Marcel Holtmann [email protected]
srcversion: 86266D360814678CE0C94E9
depends:
intree: Y
name: bnep
vermagic: 5.15.195 preempt mod_unload modversions ARMv7 thumb2 p2v8
parm: compress_src:Compress sources headers (bool)
parm: compress_dst:Compress destination headers (bool)
## 模拟
我们可能已经在固件包中有了一个编译好的内核,但这个
内核是专门为某台特定机器(Phytec
phyBOARD-Segin i.MX6 UltraLite)编译的。
要正确使用这个内核,我们就需要能够将这个内核运行在
它所编译的那台精确机器的模拟环境上。如果该机器
尚未列入 QEMU 机器列表,这会非常耗时。注意:QEMU 中提供了
一个评估板机器(`mcimx6ul-evk - Freescale i.MX6UL Evaluation Kit
(Cortex-A7)`),但并不完全匹配。
相反,理论上,我们可以选择一台 QEMU 机器,并使用我们
在固件镜像中找到的各种元数据来为该机器配置和构建内核,
这样它也能够运行我们文件系统中的所有内容,并尽可能接近
原始状态。
从这里开始,我们有几种选择:
- *用户空间模拟*:我们可以引导进入自己的自定义 ramfs/rootfs,然后
加载目标固件 rootfs,并 chroot 到其中进行模拟。
- *全系统模拟*:我们可以尝试引导目标固件 rootfs
直接 (直接将其传递给 QEMU 机器)。
用户空间模拟通常有限,但却是调查、调试、
以及利用特定二进制文件的绝佳方式。我们强烈建议你看看
[EMUX](https://github.com/therealsaumil/emux)(来自 Saumil Shah),它可以满足你所有的
用户空间模拟需求。
无论如何,我们仍然需要一个能在 QEMU 机器中引导的内核,该内核
要尽可能匹配目标固件的预期。
### QEMU 机器
能够提供灵活性的通用型机器候选者很少。
经过大量实验,`virt`
(https://qemu.readthedocs.io/en/latest/system/arm/virt.html) 似乎具有
良好的灵活性/性能比,符合我们的目的。
在为给定的 QEMU 机器构建内核时,我们最好
确保我们可以使用 QEMU 捆绑的通用 virtio 设备。在
配置中,这些可能意味着以下一项或多项:```
CONFIG_VIRTIO=y
CONFIG_SCSI_VIRTIO=y
CONFIG_VIRTIO_PCI=y
CONFIG_VIRTIO_BLK=y
还有更多!这个列表并不完整。
对我们来说,使用一个具有相同(或至少非常 相似)vermagic 字符串的内核是有益的。系统内的各种组件都可能会 检查内核 vermagic,如果与预期不同就会 失败。这在内核模块中最为常见——内核通常会拒绝 加载 vermagic 与其自身不匹配的模块。虽然我们可以 在内核层面修补内核以忽略这一点,但我们仍然需要字符串开头的 “kernel release”部分与内核模块的 release 字符串匹配,因为它被用作 modules.dep 的搜索路径。
In the Phoenix Contact firmware, the vermagic is 5.15.195 preempt mod_unload modversions ARMv7 thumb2 p2v8.
The vermagic string is in a format like (from include/linux/vermagic.h):```c
#define VERMAGIC_STRING
UTS_RELEASE " "
MODULE_VERMAGIC_SMP MODULE_VERMAGIC_PREEMPT
MODULE_VERMAGIC_MODULE_UNLOAD MODULE_VERMAGIC_MODVERSIONS
MODULE_ARCH_VERMAGIC
- Kernel release = 内核版本 + `CONFIG_LOCALVERSION` 字符串。
- SMP = `CONFIG_SMP`
- PREEMPT = `CONFIG_PREEMPT`
- mod_unload = `CONFIG_MODULE_UNLOAD`
- modversions = `CONFIG_MODVERSIONS`
- ARMv7 thumb2 p2v8 = `MODULE_ARCH_VERMAGIC`(与内核编译所针对的架构相关,因此与传入的编译器/架构选项有关。)
一旦我们提取了内核源码,就可以对其进行修补,使其在比较 vermagic 字符串时不再那么严格。在 kernel/module.c 中,我们可以直接将 `return -ENOEXEC` 注释掉:```diff
} else if (!same_magic(modmagic, vermagic, info->index.vers)) {
pr_err("%s: version magic '%s' should be '%s'\n", info->name, modmagic, vermagic);
- return -ENOEXEC;
+ // return -ENOEXEC;
}
if (!get_modinfo(info, "intree")) {
从长远来看它可能未必有用,但很适合用于测试,只要我们的内核 vermagic 与模块 vermagic 之间存在轻微差异即可。这些模块(大多数情况下)仍然可以加载。
注意:这里我们提供了一个已经构建好的、符合目标要求的内核,因此你无需自行编译。它是由一个内部脚本构建的,该脚本:
multi_v7_defconfig 设置初始配置kernel/module.c 中的 vermagic 检查代码VIRTIO 选项以及 USB 或 CANBUS 等特定子系统让我们来看看 qemu-system-arm 命令的不同参数。我们通过以下方式提供内核:```sh
qemu-system-arm -M virt,highmem=off
-m ${AVAILABLE_MEM_GB}G -smp ${CPU_CORES} -cpu cortex-a15
-kernel zImage
EXT4 文件系统镜像作为 SCSI 磁盘附加:```sh
-device virtio-scsi-device \
-device scsi-hd,drive=SystemDisk \
-drive file=${FIRMWARE_IMAGE},format=raw,if=none,id=SystemDisk
我们通过这种方式设置合适的控制台输出:```sh
-append "${BOOT_PARAMS}"
-no-reboot
-nographic
-serial mon:stdio \
`BOOT_PARAMS` 在 `config.ini` 中设置,对应实际内核的 bootcmd,但做了两处小调整(控制台路径和 rootfs 路径)。
### 网络
现在我们已经有了一个可运行的模拟系统,是时候设置网络了。目标设备具有以下内容:
- 两个以太网接口(`eth0` 和 `eth1`)
- 一个 CANbus 接口(`can0`)
- 一个可用作 USB 以太网的 USB 接口(`usb0`)
- 一个 Qualcomm High Sierra 蜂窝调制解调器(`ppp0`)
使用 QEMU 模拟蜂窝调制解调器和 USB 接口非常麻烦,而且对于设备运行来说并非真正必需,因为这些接口是"可选的"。CAN 接口用于让控制器与其他控制器或充电站通信。`eth0` 接口默认通过请求 DHCP 租约提供上行连接,而 `eth1` 被设置为 192.168.4.1,用于在需要菊花链连接多个控制器时使用。
以太网接口被设置为 TAP 接口,并按如下方式连接:```
-netdev tap,id=tap0net,ifname=${ETH_1},script=no,downscript=no \
-device virtio-net-device,netdev=tap0net,mac=${ETH_1_MAC} \
-netdev tap,id=tap1net,ifname=${ETH_0},script=no,downscript=no \
-device virtio-net-device,netdev=tap1net,mac=${ETH_0_MAC} \
CANBUS 接口只是一个 PCI 接口,像这样连接:```
-object can-bus,id=canbus0
-device kvaser_pci,canbus=canbus0
-object can-host-socketcan,id=canhost0,if=can0,canbus=canbus0 \
下图展示了网络设置的布局:

如果你想按照 Phoenix Contact 的建议,通过菊花链方式尝试控制器间通信,可以使用类似这样的配置:

## 准备好了吗?各就各位。启动!
现在一切都已就绪,是时候启动它了!
首先,你需要像这样设置所有网络接口:```
sudo ./ifup.sh
确保你的上游接口在 config.ini 中正确设置:```
OUT_IF="eth0"
然后,您可以启动模拟器:```
sudo ./launch.sh
你会在日志中看到获取到的地址:``` Dec 5 10:07:46 ev3000 daemon.info avahi-daemon[1047]: Registering new address record for 192.168.4.1 on eth1.IPv4. Dec 5 10:07:46 ev3000 daemon.info avahi-daemon[1047]: Registering new address record for 192.168.88.181 on eth0.IPv4.
### 通过 SSH 访问
然后,您可以使用 `user-app` 用户通过 SSH 登录到模拟设备:```sh
ssh [email protected]
密码是“user”。首次登录时你将被强制更改密码:```sh ssh [email protected] The authenticity of host '192.168.88.181 (192.168.88.181)' can't be established. ED25519 key fingerprint is SHA256:qMupzEehNFpx6eaGZ4d3AY5Jlms3Pmxh3/yg9OGA28o. This key is not known by any other names Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added '192.168.88.181' (ED25519) to the list of known hosts. [email protected]'s password: WARNING: Your password has expired. You must change your password now and login again! Changing password for user-app Old password: Enter the new password (minimum of 5 characters) Please use a combination of upper and lower case letters and numbers. New password: Re-enter new password: passwd: password changed. Connection to 192.168.88.181 closed.
### Web 界面
您可以通过 http://192.168.4.1 访问 Web 界面。设备启动过程中可能会显示错误
消息。一切就绪并运行后,您应该会看到以下内容:

您可以使用以下账户登录:
| 用户名 | 密码 |
|----------|----------|
| manufacturer | manufacturer |
| operator | operator |
### CAN 流量
一切就绪并运行后,您可以捕获控制器发出的 CAN 流量:```
candump -i can0
can0 701 [1] 00000101
can0 701 [1] 00000101
can0 701 [1] 00000101
can0 701 [1] 00000101
can0 701 [1] 00000101
can0 701 [1] 00000101
注意:出于某种原因,QEMU 8.x 版本似乎在附加 CAN 控制器时存在一些困难。