一个项目设备,记录下获取到固件的过程。

尝试1:串口提取(失败)

首先通过USB2TTL拿到了U-Boot shell。命令里没有md等命令,尝试通过设置bootargs环境变量方式来。

尝试2:mtd 挂载(失败)

还考虑使用 mtd 挂载的方式来读取,不过经过尝试,该方法仅适用于 ubi.img 没有错误的情况,否则会在 ubi attach 的时候报错。
对应的 flash 芯片为 GD5F2GQ5UExxG,对应文档链接:https://download.gigadevice.com/Datasheet/DS-00890-GD5F2GQ5UExxG-Rev1.6.pdf
可以看到比较重要的数据:

1
2
3
4
5
# 这四位是 read ID 返回的四个字节
Manufacturer ID: 0xC8
Device ID: 0x52
Byte2 Byte value: none
Byte3 Byte value: none

提取到的 bin 文件差不多为 256MB ,共有 2k 个块
进行挂载不太行,首先是没有对应的 mtd simulated device,使用其他的类似的挂载上去也会发现有错误。

尝试3:ECC 修复(失败)

这里感觉是解决 bit flip 的最正规的方法,但奈何 提到的 flash 格式与 官方 datasheet 格式不一致,怀疑是厂商自定义了一套软件层的 ECC。以下仅作参考
感觉可以尝试利用起来 flash 的 ECC 了,如果真的是有 bit flip 存在的话。
直接在 linux 源码中找到了该设备对应的 布局源码:

1
2
3
4
5
6
7
8
9
10
SPINAND_INFO("GD5F2GQ5UExxG",
SPINAND_ID(SPINAND_READID_METHOD_OPCODE_DUMMY, 0x52),
NAND_MEMORG(1, 2048, 128, 64, 2048, 40, 1, 1, 1),
NAND_ECCREQ(4, 512),
SPINAND_INFO_OP_VARIANTS(&read_cache_variants_2gq5,
&write_cache_variants,
&update_cache_variants),
SPINAND_HAS_QE_BIT,
SPINAND_ECCINFO(&gd5fxgqx_variant2_ooblayout,
gd5fxgq5xexxg_ecc_get_status)),

问了 gpt 可以知道基本信息:

  • page_size: 2048,每页2kb
  • oob_size: 128,每个 oob 区 128 字节
  • pages_per_block:每块 64 页
  • ECC 要求:每 512 字节 需要 4-bit ECC

按之前的理论,一个 page 一个 oob 区,其内存布局如下:

1
2
3
4
5
6
data1: 0x000 - 0x410    (size: 0x410)
oob1/free: 0x410 - 0x43a (size: 0x2a)
data2: 0x43a - 0x800 (size: 0x3c6)
oob2/free: 0x800 - 0x802 (size: 0x2)
data3: 0x802 - 0x82c (size: 0x2a)
oob3/free: 0x82c - 0x880 (size: 0x54)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/mnt/SJTU/研究生/任务/Projects/码头/camera/haikang/firmware/most_common
base ❯ hexdump -v -C -n $((2176*103)) ./most_common.BIN| tail -n 9
00036b00 ff ff 73 48 ef 6f 7c 68 e9 a1 6e 4a bd 8e 31 b6 |..sH.o|h..nJ..1.|
00036b10 75 d7 67 e3 f3 55 df ec fc cc 7a 17 41 41 ce 5c |u.g..U....z.AA.\|
00036b20 7c eb f6 4b 95 e5 da b0 72 86 55 c5 a3 6d 8d 81 ||..K....r.U..m..|
00036b30 7c 4a 8c 48 a6 b4 df 9f 31 f8 d3 76 2c 4c ec 6b ||J.H....1..v,L.k|
00036b40 c7 84 41 c9 88 e8 07 92 29 41 ab d3 3e 87 18 6a |..A.....)A..>..j|
00036b50 f6 ca 4e 9b 77 7c ff ff ff ff ff ff ff ff ff ff |..N.w|..........|
00036b60 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
00036b70 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
00036b80

/mnt/SJTU/研究生/任务/Projects/码头/camera/haikang/firmware/most_common
base ❯ hexdump -v -C -n $((2176*13)) ./most_common.BIN| tail -n 9
00006e00 ff ff 5a e3 11 00 00 0a 40 20 c2 e3 01 50 a0 e3 |..Z.....@ ...P..|
00006e10 20 20 82 e3 00 20 83 e5 48 20 1b e5 01 3a a0 e3 | ... ..H ...:..|
00006e20 70 30 41 e3 00 10 a0 e3 08 20 83 e5 fb 77 2d 55 |p0A...... ...w-U|
00006e30 8b 33 62 35 c0 aa 38 84 74 70 11 80 5b 9e 7c d5 |.3b5..8.tp..[.|.|
00006e40 db b2 41 91 90 ef 8f 0f dd 5d f3 f0 cd 2a 0c 47 |..A......]...*.G|
00006e50 10 4f b7 0f 8f 68 ff ff ff ff ff ff ff ff ff ff |.O...h..........|
00006e60 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
00006e70 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
00006e80

/mnt/SJTU/研究生/任务/Projects/码头/camera/haikang/firmware/most_common
base ❯ hexdump -v -C -n $((2176*3)) ./most_common.BIN| tail -n 9
00001900 ff ff db e5 00 40 8e e5 5c 00 00 0a 0b 00 00 3a |.....@..\......:|
00001910 04 00 50 e3 03 00 00 1a 2e 00 5b e5 70 00 ef e6 |..P.......[.p...|
00001920 1c d0 4b e2 f0 8b bd e8 00 30 e0 e3 53 23 5a 08 |..K......0..S#Z.|
00001930 6c 64 97 b8 68 c4 f8 6e d2 f8 a8 10 46 ff e4 14 |ld..h..n....F...|
00001940 ce d7 39 a9 7b 68 8b 56 cf 52 21 6c 92 17 e1 71 |..9.{h.V.R!l...q|
00001950 08 18 40 c6 2d bd ff ff ff ff ff ff ff ff ff ff |..@.-...........|
00001960 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
00001970 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
00001980

如上图所示,可见 oob2 区域固定为 0xffff 两个字节,最后 oob3 区域的最后 0x2a 字节固定为 0xff
编写脚本验证可知该条件成立:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import os

BLOCK_SIZE = 0x880
violation_num = 0

blank_pattern1 = b"\xff"*0x2
blank_pattern2 = b"\xff"*0x2a
const_pattern1 = b"\xff"*0x2
const_pattern2 = b"\xff"*0x2a

with open("./most_common.BIN", "rb") as f:
size = f.seek(0, os.SEEK_END)
print(f"target firmware size: {hex(size)}")
block_num = size // BLOCK_SIZE

for i in range(block_num):
f.seek(BLOCK_SIZE*i+0x800)
if i % 0x100 == 0:
print(f"read block ({i+1}/{block_num})")
if f.read(2) != const_pattern1:
print(f"block {i} violation pattern1!")
violation_num += 1

f.seek(BLOCK_SIZE*i+0x856)
if f.read(0x2a) != const_pattern2:
print(f"block {i} violates pattern2!")
violation_num += 1

print(f"result: violations block num is {violation_num}, total block num is {block_num}.")

也就是说有0x2c字节是固定值,剩下 0x80 -0x2c=84 字节即可能为进行ecc的部分,不过按照 linux 源码机制 应该是 有64字节为ecc,其他字节均为 free 字段,每 512 字节对应 4-bit ecc
这里放一下官方的 对应芯片 的 datasheet 吧:

据官方描述,应该是有64字节来用于 ECC 纠错
这里假设是要把原布局改为:data1 - data2 - data3 - oob2 - oob1 - oob3,然后可以得到正常的 2048 + 128 内存布局
有一个比较明显的例子:
在 0xa766990 地址,应该是直接将 数据data3 拼接到 data2 后面,
更新,这一步放弃了,看不懂 ecc 的格式,后面还是建议多次读,取众数吧

尝试2:flash dump固件(部分成功)

该方法也是我们最初采取的方法,但是当时提出来拿到 ubi 后发现各种方式都无法直接解析就暂时搁置了… 而且有一个很抽象的问题就是每次读取的 flash 内容都存在随机位置的 bit flip 。这个可能是使用热风枪吹 flash 时过热把 flash 烧坏了,不过后续还是部分绕过了该问题。

固件去除 ECC 段

查看从 flash 中 dump 到了固件,可以发现每隔0x800存在0x80的 spare padding 内容,需要进行patch,这一部分在后续继续查阅资料可以发现是 OOB(ECC)区域,并且该区域位置很奇怪(在0x410 有 0x2a 字节不知道是什么用处,在 0x802 后也有 0x2a 字节则是有效数据,需要保留)。但是发现每次dump到的flash不太一样,大概率是flash被吹坏了。

也有可能是 NAND 有一个 bit flip 的机制(不太懂,这个我觉得大概率就是flash质量不行了 XD

ubi 格式修复

使用 github 上的 ubidump repo 工具来解析。使用这个的目的是源码便于理解以及修改。
这里出现了另外一个问题是针对上面去除 ECC 后的 ubi.img 进行提取,只识别到app_pri, cfg_pri两个类型,以及只有一个文件。
通过调试发现可以通过修改 ubidump.py 的逻辑来改进,(应该是 vid_data,ubidump 没有集成多个 vtable 的能力,因此会默认选取最后一个 vtable[0])
但是还是只能获取到部分,此时能提取到的文件从1个变为了1MB的文件系统,感觉还是损坏的问题(没错,就是 bit flip,导致有很多错误致使提取提前结束)。

brute force crc

这一操作非常的不优雅,但是却极其有效,因为基于了一个事实:发生 bit flip 的概率是比较小的,并且 ubi 对应的一个 inode data 的大小也不够大,因此在一个 node 中的 data 的 crc 校验失败大概率仅有一个 bit flip 导致,因此只要稍微 爆破 一下即可(实际上在爆破中确实有大 node 会导致卡比较久的时间)
首先在 listfiles 功能上进行实验,因为在 ubidump 提取文件时比如在 listfiles 功能中,解析的大多数 node 的 data 大小都比较小,可以尝试爆破的手段。
经过尝试非常可取,可以将所有的文件都列举出来(中间有一个 invalid node size 的报错,当然也是由于 bit filp 导致的 : ),这个会在最后再简单提及)
简单实现了一个假设只需要爆破1bit的情况的脚本。提取目标固件,可以成功提取到大小 38 MB 的文件系统。经过统计,共出现了422处需要爆破的位置,共有406处爆破成功crc。失败文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
test/app_pri/hik_ar9331_2.bin
test/app_pri/heop/package/smart/dsp/model/ped_ied_detect_888/coef.bin
test/app_pri/heop/package/smart/dsp/model/ped_ied_cls3d_888/coef.bin
test/app_pri/heop/package/smart/dsp/libHMS_lib_V4.5_banding.a
test/app_pri/heop/package/smart/fsa/lib/libobject_leave_event.so.1.1.0
test/app_pri/heop/package/smart/fsa/smart
test/app_pri/app.tar.gz
test/app_pri/h8_modules.tgz
test/app_pri/webLib/doc/ui/images/pwd-reset.png
test/app_pri/webLib/doc/page/panorama/image/refresh_click.png
test/app_pri/webLib/doc/script/lib/jsPlugin/playctrl/Decoder.wasm
test/app_pri/webLib/doc/script/lib/excanvas.js
test/app_pri/webLib/doc/script/lib/angularjs/angularjs/angular.min.js
test/app_pri/webLib/doc/script/playback/search.js
test/app_pri/webLib/doc/script/config/faceAttendance/faceAttendanceAlgorithm.js
test/app_pri/webLib/doc/script/config/system/newSettingVCAResource.js

然后发现还存在其他的报错类型,其实大概率也是因为仅靠 crc 来爆破并不是唯一解的原因,这种情况大概有68-16=52个,报错信息如下:

1
ERROR writing b'test/app_pri/curl', Compressed data violation -6

不过其实大差不差了,因为我们不需要模拟 : ),可以进行基本的二进制程序分析了。

接下来是另外一个问题:invalid inode size,也是 bit flip 的问题,直接修改掉即可
这里我们通过 listfiles 功能可以得到所有的文件数量为: 2135个(多了114个文件)

更新之前的结果,共有 72 个文件失败,19 个直接由于crc爆破失败导致。
似乎还有一些文件没有提出来,这个似乎是因为 vid_table 的选取问题,因为这一部分的修复为直接硬编码指定了对应 rootfs 的,其他的直接忽略掉了。