这篇研究论文将通过黑客的视角,详细阐述如何操作 NAND dump 以及如何获取 dump 文件中的所有文件。每一步骤以及所使用的方法均会细致解析,并配以实例说明。本文主要关注的是物理 NAND dump,这是从通用编程器中提取出的 dump 文件。相对应地,从引导加载程序(如 u-boot)中获得的 dump 文件被称为逻辑 NAND dump。
对于逻辑 NAND dump,数据的正确性由 Flash Translation Layer (FTL)负责维护。也就是说,FTL 会借助 Error Correcting Code (ECC)自动修复所有的位错误。然而,物理 NAND dump 中的数据通常会携带 ECC,这就需要我们自行推断如何利用 ECC 确保数据的准确性。如果数据中存在位错误,理应使用 ECC 进行适当的纠错。然而,推断 ECC 与数据如何相关联并非易事。若无法确定 ECC 和数据的关联性,也就无法利用 ECC 修复数据中的位错误。因此,必须对 NAND dump 进行系统的深度分析,揭示 ECC 和数据之间的密切关联。在此情境下,盲目尝试暴力破解并非明智之举,但是如果能借助深度分析的结果,盲目的暴力破解可以转化为有目的性的暴力破解。这样,获取 ECC 和数据之间密切关联的可能性将得以最大化。
修复数据中的位错误并移除 ECC 之后,NAND dump 就由物理形态转化为逻辑形态,此时便可以开始对实际的固件图像进行分析。作为论文的实际案例,我们将处理一个 UBI 镜像,并对 UBI 镜像的分析进行详细的讨论。根据从 UBI 镜像分析中得到的关键信息,我们提出了一个创新性的方法来恢复文件系统并提取文件系统内部承载的所有文件。需要特别注意的是,本论文讨论的整个过程无法通过像 binwalk 或 unblob 这样的自动化工具复制。此外,整个分析过程都会以手动、逐步的方式呈现,以确保所有步骤和概念都能够得到清晰的解释。现在,我们将不再纠结于理论讨论,而是进入实际的 NAND dump 分析环节,进行详细的介绍。
首先,让我们从一些基础的概念开始探讨。一个 NAND 闪存包含许多称为"页面"的单元,它们都是固定的大小,并以一定数量的"页面"组成一个"块"。鉴于我们的样本 NAND dump 是从一个实际的 NAND 芯片中获取的,型号为 MT29F2G08ABAEAWP,因此我们将以它为例,来介绍相关的硬件技术规范。
对于 MT29F2G08ABAEAWP 这款芯片,一个"页面"的大小是 2112 字节,其中包括 2048 字节的主数据和 64 字节的额外数据。64 个"页面"组成一个"块",而 2048 个这样的"块"构成了整个 NAND 闪存的存储空间,总计有 2048*64=131072 个"页面"。
对于每一个大小为 2112 字节的"页面",前 2048 字节用于数据存储,剩余的 64 字节则作为备用区域,用于承载错误纠正代码(Error Correcting Code, ECC)或某些供应商特定的元数据。在一些文献中,这部分备用区域有时也被称为 Out Of Band(OOB)。
对于我们的样本 NAND dump,如果我们在十六进制模式下查看,会发现第一个"页面"中,地址从 0x0000 到 0x07ff 的部分是数据区,地址从 0x0800 到 0x083f 的部分是备用区或 OOB 区,具体示意如下。作为 NAND 转储样本的概述。
cawan% hexdump -C -n 2112 ./[email protected]
00000000 20 54 56 4e 00 02 00 00 a0 ac 00 00 ff ff ff ff | TVN............|
00000010 55 aa 55 aa 2e 00 00 00 20 02 00 b0 00 00 00 01 |U.U..... .......|
00000020 64 02 00 b0 18 00 00 c0 20 02 00 b0 18 00 00 01 |d....... .......|
00000030 aa 55 aa 55 01 00 00 00 aa 55 aa 55 01 00 00 00 |.U.U.....U.U....|
00000040 28 18 00 b0 4a d8 dc 53 08 18 00 b0 14 80 00 00 |(...J..S........|
00000050 aa 55 aa 55 01 00 00 00 aa 55 aa 55 01 00 00 00 |.U.U.....U.U....|
00000060 aa 55 aa 55 01 00 00 00 00 18 00 b0 76 04 03 00 |.U.U........v...|
00000070 aa 55 aa 55 01 00 00 00 04 18 00 b0 21 00 00 00 |.U.U........!...|
00000080 aa 55 aa 55 01 00 00 00 04 18 00 b0 23 00 00 00 |.U.U........#...|
00000090 aa 55 aa 55 01 00 00 00 aa 55 aa 55 01 00 00 00 |.U.U.....U.U....|
000000a0 aa 55 aa 55 01 00 00 00 04 18 00 b0 27 00 00 00 |.U.U........'...|
000000b0 aa 55 aa 55 01 00 00 00 aa 55 aa 55 01 00 00 00 |.U.U.....U.U....|
000000c0 aa 55 aa 55 01 00 00 00 20 18 00 b0 00 00 00 00 |.U.U.... .......|
000000d0 24 18 00 b0 00 00 00 00 1c 18 00 b0 00 40 00 00 |$............@..|
000000e0 18 18 00 b0 32 03 00 00 10 18 00 b0 06 00 00 00 |....2...........|
000000f0 04 18 00 b0 27 00 00 00 aa 55 aa 55 01 00 00 00 |....'....U.U....|
00000100 aa 55 aa 55 01 00 00 00 aa 55 aa 55 01 00 00 00 |.U.U.....U.U....|
00000110 04 18 00 b0 2b 00 00 00 04 18 00 b0 2b 00 00 00 |....+.......+...|
00000120 04 18 00 b0 2b 00 00 00 18 18 00 b0 32 02 00 00 |....+.......2...|
00000130 1c 18 00 b0 81 47 00 00 1c 18 00 b0 01 44 00 00 |.....G.......D..|
00000140 04 18 00 b0 20 00 00 00 34 18 00 b0 20 88 88 00 |.... ...4... ...|
00000150 aa 55 aa 55 01 00 00 00 18 02 00 b0 08 00 00 00 |.U.U............|
00000160 60 31 00 b8 00 80 00 00 a0 31 00 b8 00 80 00 00 |1.......1......|
00000170 2c 02 00 b0 00 01 00 00 2c 02 00 b0 00 01 00 00 |,.......,.......|
00000180 2c 02 00 b0 00 01 00 00 00 00 00 00 00 00 00 00 |,...............|
00000190 13 00 00 ea 14 f0 9f e5 10 f0 9f e5 0c f0 9f e5 |................|
000001a0 08 f0 9f e5 04 f0 9f e5 00 f0 9f e5 04 f0 1f e5 |................|
000001b0 20 03 00 00 78 56 34 12 78 56 34 12 78 56 34 12 | ...xV4.xV4.xV4.|
000001c0 78 56 34 12 78 56 34 12 78 56 34 12 78 56 34 12 |xV4.xV4.xV4.xV4.|
000001d0 00 02 00 00 a0 ac 00 00 80 b5 00 00 a0 ac 00 00 |................|
000001e0 de c0 ad 0b 00 00 0f e1 1f 00 c0 e3 d3 00 80 e3 |................|
000001f0 00 f0 29 e1 bc d0 9f e5 07 d0 cd e3 00 00 a0 e3 |..).............|
00000200 70 05 00 eb 00 40 a0 e1 01 50 a0 e1 02 60 a0 e1 |[email protected].....|
00000210 04 d0 a0 e1 8c 00 4f e2 00 90 46 e0 06 00 50 e1 |......O...F...P.|
00000220 06 00 00 0a 06 10 a0 e1 5c 30 1f e5 03 20 80 e0 |........\0... ..|
00000230 00 06 b0 e8 00 06 a1 e8 02 00 50 e1 fb ff ff 3a |..........P....:|
00000240 74 00 9f e5 74 10 9f e5 00 20 a0 e3 01 00 50 e1 |t...t.... ....P.|
00000250 02 00 00 2a 00 20 80 e5 04 00 80 e2 fa ff ff ea |...*. ..........|
00000260 00 00 9f e5 00 f0 a0 e1 54 06 00 00 a0 ac 00 00 |........T.......|
00000270 a0 ac 00 00 a0 ac 00 00 00 00 a0 e3 17 0f 07 ee |................|
00000280 17 0f 08 ee 10 0f 11 ee 23 0c c0 e3 87 00 c0 e3 |........#.......|
00000290 02 00 80 e3 01 0a 80 e3 10 0f 01 ee 0e c0 a0 e1 |................|
000002a0 0a 00 00 eb 0c e0 a0 e1 0e f0 a0 e1 00 00 a0 e1 |................|
000002b0 e8 d0 1f e5 fe ff ff eb 00 80 00 bc a0 ae 00 00 |................|
000002c0 80 b7 00 00 00 00 a0 e1 00 00 a0 e1 00 00 a0 e1 |................|
000002d0 68 00 9f e5 00 10 e0 e3 00 10 80 e5 00 00 0f e1 |h...............|
000002e0 c0 00 80 e3 00 f0 21 e1 54 00 9f e5 54 10 9f e5 |......!.T...T...|
000002f0 00 10 80 e5 50 00 9f e5 50 10 9f e5 00 10 80 e5 |....P...P.......|
00000300 4c 00 9f e5 05 14 a0 e3 00 10 80 e5 44 00 9f e5 |L...........D...|
00000310 44 10 9f e5 00 10 80 e5 03 2a a0 e3 01 20 52 e2 |D........*... R.|
00000320 fd ff ff 1a 20 00 9f e5 30 10 9f e5 00 10 80 e5 |.... ...0.......|
00000330 01 2b a0 e3 01 20 52 e2 fd ff ff 1a 0e f0 a0 e1 |.+... R.........|
00000340 24 21 00 b8 04 10 00 b0 84 00 04 40 04 02 00 b0 |$!.........@....|
00000350 ff 0f 00 00 08 02 00 b0 0c 02 00 b0 24 4f 00 00 |............$O..|
00000360 fc 0f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000370 00 00 51 e3 1f 00 00 0a 01 30 a0 e3 00 20 a0 e3 |..Q......0... ..|
00000380 01 00 50 e1 19 00 00 3a 01 02 51 e3 00 00 51 31 |..P....:..Q...Q1|
00000390 01 12 a0 31 03 32 a0 31 fa ff ff 3a 02 01 51 e3 |...1.2.1...:..Q.|
000003a0 00 00 51 31 81 10 a0 31 83 30 a0 31 fa ff ff 3a |..Q1...1.0.1...:|
000003b0 01 00 50 e1 01 00 40 20 03 20 82 21 a1 00 50 e1 |..P...@ . .!..P.|
000003c0 a1 00 40 20 a3 20 82 21 21 01 50 e1 21 01 40 20 |..@ . .!!.P.!.@ |
000003d0 23 21 82 21 a1 01 50 e1 a1 01 40 20 a3 21 82 21 |#!.!..P...@ .!.!|
000003e0 00 00 50 e3 23 32 b0 11 21 12 a0 11 ef ff ff 1a |..P.#2..!.......|
000003f0 02 00 a0 e1 0e f0 a0 e1 04 e0 2d e5 c9 1c 00 eb |..........-.....|
00000400 00 00 a0 e3 00 80 bd e8 03 50 2d e9 d7 ff ff eb |.........P-.....|
00000410 06 50 bd e8 90 02 03 e0 03 10 41 e0 0e f0 a0 e1 |.P........A.....|
00000420 03 50 2d e9 09 00 00 eb 06 50 bd e8 90 02 03 e0 |.P-......P......|
00000430 03 10 41 e0 0e f0 a0 e1 00 00 a0 e1 00 00 a0 e1 |..A.............|
00000440 00 00 a0 e1 00 00 a0 e1 00 00 a0 e1 00 00 a0 e1 |................|
00000450 00 00 51 e3 01 c0 20 e0 42 00 00 0a 00 10 61 42 |..Q... .B.....aB|
00000460 01 20 51 e2 27 00 00 0a 00 30 b0 e1 00 30 60 42 |. Q.'....0...0B|
00000470 01 00 53 e1 26 00 00 9a 02 00 11 e1 28 00 00 0a |..S.&.......(...|
00000480 0e 02 11 e3 81 11 a0 01 08 20 a0 03 01 20 a0 13 |......... ... ..|
00000490 01 02 51 e3 03 00 51 31 01 12 a0 31 02 22 a0 31 |..Q...Q1...1.".1|
000004a0 fa ff ff 3a 02 01 51 e3 03 00 51 31 81 10 a0 31 |...:..Q...Q1...1|
000004b0 82 20 a0 31 fa ff ff 3a 00 00 a0 e3 01 00 53 e1 |. .1...:......S.|
000004c0 01 30 43 20 02 00 80 21 a1 00 53 e1 a1 30 43 20 |.0C ...!..S..0C |
000004d0 a2 00 80 21 21 01 53 e1 21 31 43 20 22 01 80 21 |...!!.S.!1C "..!|
000004e0 a1 01 53 e1 a1 31 43 20 a2 01 80 21 00 00 53 e3 |..S..1C ...!..S.|
000004f0 22 22 b0 11 21 12 a0 11 ef ff ff 1a 00 00 5c e3 |""..!.........\.|
00000500 00 00 60 42 0e f0 a0 e1 00 00 3c e1 00 00 60 42 |..B......<...B|
00000510 0e f0 a0 e1 00 00 a0 33 cc 0f a0 01 01 00 80 03 |.......3........|
00000520 0e f0 a0 e1 01 08 51 e3 21 18 a0 21 10 20 a0 23 |......Q.!..!. .#|
00000530 00 20 a0 33 01 0c 51 e3 21 14 a0 21 08 20 82 22 |. .3..Q.!..!. ."|
00000540 10 00 51 e3 21 12 a0 21 04 20 82 22 04 00 51 e3 |..Q.!..!. ."..Q.|
00000550 03 20 82 82 a1 20 82 90 00 00 5c e3 33 02 a0 e1 |. ... ....\.3...|
00000560 00 00 60 42 0e f0 a0 e1 04 e0 2d e5 6d 1c 00 eb |..B......-.m...|
00000570 00 00 a0 e3 04 f0 9d e4 00 00 a0 e1 00 00 a0 e1 |................|
00000580 00 00 a0 e1 00 00 a0 e1 00 00 a0 e1 00 00 a0 e1 |................|
00000590 20 30 52 e2 20 c0 62 e2 30 02 a0 41 31 03 a0 51 | 0R. .b.0..A1..Q|
000005a0 11 0c 80 41 31 12 a0 e1 0e f0 a0 e1 20 30 52 e2 |...A1....... 0R.|
000005b0 20 c0 62 e2 11 12 a0 41 10 13 a0 51 30 1c 81 41 | .b....A...Q0..A|
000005c0 10 02 a0 e1 0e f0 a0 e1 20 30 52 e2 20 c0 62 e2 |........ 0R. .b.|
000005d0 30 02 a0 41 51 03 a0 51 11 0c 80 41 51 12 a0 e1 |0..AQ..Q...AQ...|
000005e0 0e f0 a0 e1 2d de 4d e2 00 40 a0 e3 6c 31 9f e5 |[email protected]..|
000005f0 0d 00 a0 e1 00 30 8d e5 04 30 8d e5 1c 40 8d e5 |.....0...0...@..|
00000600 bc d2 8d e5 30 40 8d e5 50 40 8d e5 d1 01 00 eb |[email protected]@......|
00000610 1c 30 9d e5 04 00 53 e1 02 00 00 0a 04 10 a0 e1 |.0....S.........|
00000620 8a 0f 8d e2 33 ff 2f e1 8a 0f 8d e2 01 10 a0 e3 |....3./.........|
00000630 ca 1a 00 eb 00 00 50 e3 46 00 00 1a 70 04 00 eb |......P.F...p...|
00000640 38 42 9d e5 3c 52 9d e5 04 00 a0 e1 05 10 a0 e1 |8B..
本文的样本 NAND dump 实际上源自一个真实的工业产品的物理 NAND dump。如前所述,这个样本将被用作一个实际的案例研究,演示分析过程中的每一步,直至完整的文件系统被提取和恢复。我们先从使用 DumpFlash 工具开始,尝试识别 NAND 芯片的 ID 代码。然而,这一尝试并未成功,输出结果如下所示:
这种情况可能是由于 NAND dump 中的 ID 代码丢失或被修改成了不常见的值。
cawan% python2.7 dumpflash.py -i./[email protected]
PageSize: 0x200
OOBSize: 0x10
PagePerBlock: 0x20
BlockSize: 0x4000
RawPageSize: 0x210
FileSize: 0x10800000
PageCount: 0x84000
那么,我们就先不考虑 DumpFlash 工具产生的错误输出,而是返回到 MT29F2G08ABAEAWP 的技术规格表所提供的数据。让我们特别关注一下第一个"页面"的 64 字节大小的 OOB 区域。
00000800 ff ff 00 00 ff ff ff ff ff ff ff ff ff ff ff ff |................|
00000810 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
00000820 f6 89 f7 79 e5 60 c9 e0 d6 e3 ed cb 9c b0 f9 f0 |...y...........|
00000830 1f da d4 a4 9c d4 1b e0 e0 90 cc 85 d8 d2 e2 80 |................|
基于目前的观察,我们可以提出两个假设。第一,OOB 的前 32 字节可能是一个常量。第二,后 32 字节可能是用于错误纠正的 ECC。为了验证这两个假设,我们可以查看第二个"页面"的 OOB 区域。具体展示如下,
cawan% hexdump -v -C -n $((2112*2)) ./[email protected] | tail -n 5
00001040 ff ff 00 00 ff ff ff ff ff ff ff ff ff ff ff ff |................|
00001050 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
00001060 8f ce f4 8b 1c 26 38 00 bd 61 a0 c7 48 c4 d3 60 |.....&8..a..H..|
00001070 d2 1b 46 ab 53 8f 41 f0 8d 18 2b 3b 8d 54 21 50 |..F.S.A...+;.T!P|
是的,OOB 的前 32 字节在第二个"页面"中似乎并未发生变化。那么在第三个"页面"中呢?这是我们需要进一步确认的问题。
cawan% hexdump -v -C -n $((2112*3)) ./[email protected] | tail -n 5
00001880 ff ff 00 00 ff ff ff ff ff ff ff ff ff ff ff ff |................|
00001890 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
000018a0 01 8b bb 0a bb 54 88 50 7e 0e b9 9a c2 7b bd 40 |.....T.P~....{.@|
000018b0 dd 63 cb 9a e3 5a bc 70 65 ca 16 7a 50 dc 60 e0 |.c...Z.pe..zP..|
如果在第三个"页面"中 OOB 的前 32 字节仍然没有变化,那么接下来我们将观察下一个块的第一个"页面"中的情况。我们需要验证这个部分是否保持一致,或者有所变化。
cawan% hexdump -C -v -n $((2112*64+2112)) ./[email protected] | \
tail -n 5
00021800 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
00021810 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
00021820 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
00021830 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
确实,遇到一个空白页面,我们不能轻易下结论。取样应当更广泛,这样才能更精确地得出结论。因此,让我们进行更为全面和深入的检查,以确保我们的假设具有足够的证据支持。
############################### check_const.py ###############################
input_file = open("[email protected]","rb")
suspect_const = \
b'\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' + \
b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'
blank = \
b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' + \
b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'
page_count = 0
diff_count = 0
while 1:
data = input_file.read(2112)
if len(data) == 0:
break
oob_first_32_bytes = data[2048:2048+32]
page_count += 1
if len(data) == 2112 and oob_first_32_bytes != blank:
if oob_first_32_bytes != suspect_const:
diff_count += 1
print("diff_count: %d page_count: %d\n" % (diff_count, page_count))
##################################### end ####################################
输出结果为:
cawan% python3.8 check_const.py
diff_count: 0 page_count: 131072
因此,通过全面的检查,我们可以有充分的信心断言,所有"页面"的 OOB 的前 32 字节是一个常量。接下来,让我们验证第二个假设,即 OOB 的后 32 字节是否是用于错误纠正的 ECC。以下是我们在前 4 个"页面"的 OOB 中所观察到的疑似 ECC 的部分。
00000820 f6 89 f7 79 e5 60 c9 e0 d6 e3 ed cb 9c b0 f9 f0 |...y...........|
00000830 1f da d4 a4 9c d4 1b e0 e0 90 cc 85 d8 d2 e2 80 |................|
00001060 8f ce f4 8b 1c 26 38 00 bd 61 a0 c7 48 c4 d3 60 |.....&8..a..H..|
00001070 d2 1b 46 ab 53 8f 41 f0 8d 18 2b 3b 8d 54 21 50 |..F.S.A...+;.T!P|
000018a0 01 8b bb 0a bb 54 88 50 7e 0e b9 9a c2 7b bd 40 |.....T.P~....{.@|
000018b0 dd 63 cb 9a e3 5a bc 70 65 ca 16 7a 50 dc 60 e0 |.c...Z.pe..zP..|
000020e0 43 a9 36 70 be b0 5e 90 1c 4f c1 ad 19 54 4d 20 |C.6p..^..O...TM |
000020f0 b8 6a 20 ba 32 c2 74 80 76 73 45 10 64 3e 38 c0 |.j .2.t.vsE.d>8.|
输出结果看起来符合我们的预期,它提供了额外的线索,表明 OOB 的疑似 ECC 部分是如何被系统实际使用的。对于每一个"页面",似乎 32 字节的疑似 ECC 部分可以被进一步划分为四个各 8 字节的 ECC 部分。这种判断的依据是每 8 字节疑似 ECC 的最后 4 位始终为零,如下所示:
f6 89 f7 79 e5 60 c9 e0
d6 e3 ed cb 9c b0 f9 f0
1f da d4 a4 9c d4 1b e0
e0 90 cc 85 d8 d2 e2 80
8f ce f4 8b 1c 26 38 00
bd 61 a0 c7 48 c4 d3 60
d2 1b 46 ab 53 8f 41 f0
8d 18 2b 3b 8d 54 21 50
01 8b bb 0a bb 54 88 50
7e 0e b9 9a c2 7b bd 40
dd 63 cb 9a e3 5a bc 70
65 ca 16 7a 50 dc 60 e0
43 a9 36 70 be b0 5e 90
1c 4f c1 ad 19 54 4d 20
b8 6a 20 ba 32 c2 74 80
76 73 45 10 64 3e 38 c0
^
0
既然一个"页面"包含四个 ECC,我们就可以合理地推断出,一个大小为 2048 字节的"页面"的数据部分可以被划分为四个 512 字节的"子页面"。每一个"子页面"都被其对应的 ECC 保护。其顺序如下所示:
f6 89 f7 79 e5 60 c9 e0 <- ECC of the 1st "sub-page" in 1st "page"
d6 e3 ed cb 9c b0 f9 f0 <- ECC of the 2nd "sub-page" in 1st "page"
1f da d4 a4 9c d4 1b e0 <- ECC of the 3rd "sub-page" in 1st "page"
e0 90 cc 85 d8 d2 e2 80 <- ECC of the 4th "sub-page" in 1st "page"
8f ce f4 8b 1c 26 38 00 <- ECC of the 1st "sub-page" in 2nd "page"
bd 61 a0 c7 48 c4 d3 60 <- ECC of the 2st "sub-page" in 2nd "page"
d2 1b 46 ab 53 8f 41 f0 <- ECC of the 3st "sub-page" in 2nd "page"
8d 18 2b 3b 8d 54 21 50 <- ECC of the 4st "sub-page" in 2nd "page"
01 8b bb 0a bb 54 88 50 <- ECC of the 1st "sub-page" in 3rd "page"
7e 0e b9 9a c2 7b bd 40 <- ECC of the 2st "sub-page" in 3rd "page"
dd 63 cb 9a e3 5a bc 70 <- ECC of the 3st "sub-page" in 3rd "page"
65 ca 16 7a 50 dc 60 e0 <- ECC of the 4st "sub-page" in 3rd "page"
43 a9 36 70 be b0 5e 90 <- ECC of the 1st "sub-page" in 4th "page"
1c 4f c1 ad 19 54 4d 20 <- ECC of the 2st "sub-page" in 4th "page"
b8 6a 20 ba 32 c2 74 80 <- ECC of the 3st "sub-page" in 4th "page"
76 73 45 10 64 3e 38 c0 <- ECC of the 4st "sub-page" in 4th "page"
^
0
当我们说每个 ECC 的最后 4 位都是零时,这可能表明 ECC 的长度是 8*8=64-4=60 位。顺便说一下,这里很重要的一点是 ECC 的长度通常以位数表示。让我们通过检查每个 ECC 的最后 4 位是否始终为零,来确认所有的 ECC 是否都是 60 位大小。
########################### check_ecc_last_4bit.py ###########################
input_file = open("[email protected]","rb")
suspect_const = \
b'\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' + \
b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'
blank = \
b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' + \
b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'
masking = b'\x00\x00\x00\x00\x00\x00\x00\x0f'
page_count = 0
diff_count = 0
while 1:
data = input_file.read(2112)
if len(data) == 0:
break
oob_1st_32_bytes = data[2048:2048+32]
oob_2nd_32_bytes = data[2048+32:2048+64]
page_count += 1
if len(data) == 2112 and oob_1st_32_bytes != blank:
for i in range(4):
last_4_bits = bytes([a & b for a, b in \
zip(oob_2nd_32_bytes[i*8:i*8+8], masking)])
if last_4_bits[7] != 0:
diff_count += 1
print("diff_count: %d page_count: %d\n" % (diff_count, page_count))
##################################### end #####################################
输出结果为:
cawan% python3.8 check_ecc_last_4bit.py
diff_count: 0 page_count: 131072
有了如此令人信服的结果,我们可以合理地断定 ECC 的长度是 60 位。
现在,让我们简要地了解一下黑客对 ECC 算法的概述。通常来说,常用的实现方式有三种:汉明码、里德-所罗门码(RS)和二进制 BCH 码。然而,由于汉明码只能纠正单个位的错误,而 RS 码在给定的错误纠正中需要更多的冗余码,因此二进制 BCH 码是现代 ECC 实现中最常用的。因此,这里假设使用的 ECC 实现是二进制 BCH 码。此外,二进制 BCH 码的一些特殊特性可以帮助进一步识别 ECC 的实现。首个特性是,对于所有的零数据,无论其大小如何,对应的二进制 BCH 码中的 ECC 也应该全部为零。我们将通过使用 bchlib 来展示这个特性。请明确,此阶段所有的参数都只是为了演示,实际的参数将会在分析过程中逐步推导出来。让我们先来看看第一个特性。
############################## test_bchlib_01.py #############################
import bchlib
BCH_POLYNOMIAL = 8219
BCH_BITS = 4
bch = bchlib.BCH(BCH_POLYNOMIAL, BCH_BITS)
data = bytearray(b'\x00'*512)
ecc = bch.encode(data)
for i in ecc:
print("%X" % i, end='')
print("")
##################################### end ####################################
bchlib 用于二进制 BCH 编码和解码任务。
bchlib 是用于执行二进制 BCH 编码和解码任务的库。它需要指定两个参数,即 BCH_POLYNOMIAL 和 BCH_BITS。BCH_POLYNOMIAL 涉及到将要使用的本原多项式,而 BCH_BITS 则涉及到 ECC 能够纠正的数据中的最大位错误数量。关于这两个参数的详细信息将在后面的二进制 BCH 实现部分进行讨论,因为它们对揭示 ECC 和数据之间的秘密关联至关重要。现在,让我们对 bchlib 进行初步了解,并研究二进制 BCH 的第一个特性。以下是 test_bchlib_01.py 的输出结果:
cawan% python3.8 test_bchlib_01.py
0000000
BCH 编码输出的 512 字节的零确实是 3.5 字节的零。那么 512 字节的 0xFF 呢?让我们检查一下。
############################## test_bchlib_02.py #############################
import bchlib
BCH_POLYNOMIAL = 8219
BCH_BITS = 4
bch = bchlib.BCH(BCH_POLYNOMIAL, BCH_BITS)
data = bytearray(b'\xFF'*512)
ecc = bch.encode(data)
for i in ecc:
print("%X" % i, end='')
print("")
##################################### end ####################################
输出结果为:
cawan% python3.8 test_bchlib_02.py
D7EC33C6695380
输出结果并非全部为 0xFF,这是合理的。否则,如果将 512 字节的 0xFF 进行 BCH 编码后得到 7 字节的 0xFF,那么很难与空白的"页面"区分开来。现在,让我们继续探讨关于零填充的第二个特性。现在的问题是,如果将 32 字节的零附加到 512 字节的 0xFF 后会发生什么?让我们来验证一下。
############################## test_bchlib_03.py #############################
import bchlib
BCH_POLYNOMIAL = 8219
BCH_BITS = 4
bch = bchlib.BCH(BCH_POLYNOMIAL, BCH_BITS)
data = bytearray(b'\xFF'*512 + b'\x00'*32)
ecc = bch.encode(data)
for i in ecc:
print("%X" % i, end='')
print("")
##################################### end ####################################
输出结果为:
cawan% python3.8 test_bchlib_03.py
BCE3B0AE479EB0
看起来,零填充的数据和非零填充的数据的 BCH 编码输出是不同的,前提是数据不全为零。然而,这并不是固有的 BCH 编码器的特性。固有的 BCH 编码器应该会对零填充的数据和非零填充的数据生成完全相同的输出。虽然这种特性可能会引起某种差异,但应该避免这样的情况。解决固有特性引起的这种问题的常见方法是在进行 BCH 编码之前,对整个数据的位顺序进行反转。因此,合理的假设是 bchlib 应该遵循这种方法,但是如何验证呢?嗯,在做出这种假设的同时,对于以 512 字节的 0xFF 附加 32 字节的零的数据,这意味着实际被 bchlib 进行 BCH 编码的数据实际上是 32 字节的零附加在 512 字节的 0xFF 之前。因此,如果是这种情况,零附加数据的 BCH 编码输出应该与非零附加数据的输出相同。让我们来验证一下。
############################## test_bchlib_04.py #############################
import bchlib
BCH_POLYNOMIAL = 8219
BCH_BITS = 4
bch = bchlib.BCH(BCH_POLYNOMIAL, BCH_BITS)
data1 = bytearray(b'\x00'*32 + b'\xFF'*512)
ecc1 = bch.encode(data1)
data2 = bytearray(b'\xFF'*512)
ecc2 = bch.encode(data2)
print("Zeros Prepended:")
for i in ecc1:
print("%X" % i, end='')
print("")
print("Nothing Prepended:")
for i in ecc2:
print("%X" % i, end='')
print("")
##################################### end ####################################
正如预期的那样,两个 BCH 编码的输出都完全相同,输出结果如下所示:
cawan% python3.8 test_bchlib_04.py
Zeros Prepended:
D7EC33C6695380
Nothing Prepended:
D7EC33C6695380
这里需要注意一个重要的点。如果输入数据的位顺序被反转,那么 BCH 编码输出也应该以位顺序反转的形式呈现。感谢 bchlib 在默认模式下实现了这一点。现在,另一个问题出现了,是否有可能保持将要进行 BCH 编码的输入数据的位顺序不变?是的,通过在将输入数据传递给 bchlib 编码器之前首先对其进行位顺序反转,是可以保持输入数据的位顺序不变的。当然,BCH 编码输出也应该相应地进行位顺序反转。让我们通过示例来展示这一点。
############################## test_bchlib_05.py #############################
import bchlib
BCH_POLYNOMIAL = 8219
BCH_BITS = 4
bch = bchlib.BCH(BCH_POLYNOMIAL, BCH_BITS)
data = bytearray(b'\xFF'*511 + b'\xAA')
data_reverse_bit = b''
for i in range(0, len(data)):
data_reverse_bit += bytes([int("{:08b}".format(data[i])[::-1],2)])
data_reverse_bit = data_reverse_bit[::-1]
ecc = bch.encode(data_reverse_bit)
ecc_reverse_bit = b''
for i in range(0, len(ecc)):
ecc_reverse_bit += bytes([int("{:08b}".format(ecc[i])[::-1],2)])
ecc_reverse_bit = ecc_reverse_bit[::-1]
for i in ecc_reverse_bit:
print("%X" % i, end='')
print("")
##################################### end ####################################
在这个 test_bchlib_05.py 中,为了避免数据的对称性,特意将整个 512 字节的数据输入的最后一个字节从 0xFF 改为 0xAA(位序颠倒后的 0b111111 仍为 0b111111)。现在,让我们看看输出结果:
cawan% python3.8 test_bchlib_05.py
72FFA2590ECDB
所以,如果一切都正确的话,如果在这 512 字节的数据输入上附加 32 字节的零,并得到 BCH 编码,输出也应该等于 72FFA2590ECDB。让我们来验证一下。
############################## test_bchlib_06.py #############################
import bchlib
BCH_POLYNOMIAL = 8219
BCH_BITS = 4
bch = bchlib.BCH(BCH_POLYNOMIAL, BCH_BITS)
data = bytearray(b'\xFF'*511 + b'\xAA' + b'\x00'*32)
data_reverse_bit = b''
for i in range(0, len(data)):
data_reverse_bit += bytes([int("{:08b}".format(data[i])[::-1],2)])
data_reverse_bit = data_reverse_bit[::-1]
ecc = bch.encode(data_reverse_bit)
ecc_reverse_bit = b''
for i in range(0, len(ecc)):
ecc_reverse_bit += bytes([int("{:08b}".format(ecc[i])[::-1],2)])
ecc_reverse_bit = ecc_reverse_bit[::-1]
for i in ecc_reverse_bit:
print("%X" % i, end='')
print("")
##################################### end ####################################
完美,输出结果与预期完全一致,如下所示:
cawan% python3.8 test_bchlib_06.py
72FFA2590ECDB
通过研究二进制 BCH 的一些特性,我们对 bchlib 有了一个"初步了解",这已经足够了。以黑客的角度总结从这个"初步了解"中学到的两个要点。首先,如果输入数据全部为零,则输出也将全部为零。其次,如果输入数据填充了任意大小的零,输出结果将与未附加零的输入数据相同。回到 NAND dump 的情景,这两个要点引发了一个灵光乍现的思路。如果 60 位的 BCH 编码 ECC 以全零的形式存在,那么相应的"子页面"中的 512 字节数据也应该全部为零。如果是这样,那么意味着进行 BCH 编码的数据要么没有添加填充,要么添加的填充全为零。如果不是这样,那么添加的填充就不全为零。听起来有点混乱吗?让我们在 NAND dump 中找到一个"BCH 编码 ECC 为全零"的"子页面"来加以说明。通过示例来解释这个概念将更加清晰明了。
########################### check_all_zeros_ecc.py ###########################
input_file = open("[email protected]","rb")
oob_const = b'\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' + \
b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'
zeros_ecc = b'\x00\x00\x00\x00\x00\x00\x00\x00'
page_cnt = 0
positive_cnt = 0
while 1:
data = input_file.read(2112)
if len(data) == 0:
break
oob_1st_32_bytes = data[2048:2048+32]
oob_2nd_32_bytes = data[2048+32:2048+32+32]
if len(data) == 2112 and oob_1st_32_bytes == oob_const:
for i in range(0, 4):
ecc = oob_2nd_32_bytes[i*8:i*8+8]
if ecc == zeros_ecc:
positive_cnt += 1
print("Page Num: %d, Address: 0x%X" % (page_cnt, page_cnt*2112))
break
if positive_cnt == 1:
break
page_cnt += 1
print("Completed")
##################################### end ####################################
让我们看看是否有 "页 "能满足条件,如果有,就显示 "页 "的编号和它在第一个找到的项目的地址。输出结果如下所示,
cawan% python3.8 check_all_zeros_ecc.py
Page Num: 256, Address: 0x84000
Completed
很好,第一个发现的项目在地址 0x84000 处。让我们在十六进制视图中显示整个 “页面”。
cawan% hexdump -C -v -n $((0x84000+2112)) [email protected] \
| tail -n $((0x840/16+1))
00084000 76 3d f5 33 62 61 75 64 72 61 74 65 3d 31 31 35 |v=.3baudrate=115|
00084010 32 30 30 00 62 6f 6f 74 61 72 67 73 3d 6d 65 6d |200.bootargs=mem|
00084020 3d 36 34 4d 20 63 6f 6e 73 6f 6c 65 3d 74 74 79 |=64M console=tty|
00084030 53 30 2c 31 31 35 32 30 30 20 75 62 69 2e 6d 74 |S0,115200 ubi.mt|
00084040 64 3d 32 20 72 6f 6f 74 3d 75 62 69 30 3a 75 62 |d=2 root=ubi0:ub|
00084050 69 66 73 20 72 77 20 72 6f 6f 74 66 73 74 79 70 |ifs rw rootfstyp|
00084060 65 3d 75 62 69 66 73 20 69 6e 69 74 3d 2f 6c 69 |e=ubifs init=/li|
00084070 6e 75 78 72 63 00 62 6f 6f 74 63 6d 64 3d 6e 62 |nuxrc.bootcmd=nb|
00084080 6f 6f 74 2e 65 20 30 78 37 46 43 30 20 30 20 30 |oot.e 0x7FC0 0 0|
00084090 78 32 30 30 30 30 30 3b 20 62 6f 6f 74 6d 20 30 |x200000; bootm 0|
000840a0 78 37 46 43 30 00 62 6f 6f 74 64 65 6c 61 79 3d |x7FC0.bootdelay=|
000840b0 31 00 65 74 68 61 63 74 3d 65 6d 61 63 00 65 74 |1.ethact=emac.et|
000840c0 68 61 64 64 72 3d 30 30 3a 30 30 3a 30 30 3a 31 |haddr=00:00:00:1|
000840d0 31 3a 36 36 3a 38 38 00 69 70 61 64 64 72 3d 31 |1:66:88.ipaddr=1|
000840e0 39 32 2e 31 36 38 2e 38 2e 32 30 33 00 6d 74 64 |92.168.8.203.mtd|
000840f0 70 61 72 74 73 3d 6d 74 64 70 61 72 74 73 3d 6e |parts=mtdparts=n|
00084100 61 6e 64 30 3a 32 6d 28 75 2d 62 6f 6f 74 29 2c |and0:2m(u-boot),|
00084110 34 6d 28 6b 65 72 6e 65 6c 29 2c 31 36 6d 28 75 |4m(kernel),16m(u|
00084120 62 69 66 73 29 2c 33 32 6d 28 61 70 70 6c 69 63 |bifs),32m(applic|
00084130 61 74 69 6f 6e 29 2c 33 32 6d 28 62 61 63 6b 75 |ation),32m(backu|
00084140 70 29 2c 2d 28 64 61 74 61 29 00 6e 65 74 6d 61 |p),-(data).netma|
00084150 73 6b 3d 32 35 35 2e 32 35 35 2e 30 2e 30 00 72 |sk=255.255.0.0.r|
00084160 6f 6f 74 76 65 72 3d 4c 59 30 43 2d 30 36 30 31 |ootver=LY0C-0601|
00084170 2d 52 54 30 30 2d 48 30 53 30 2d 32 31 30 31 32 |-RT00-H0S0-21012|
00084180 37 2d 30 30 00 73 65 72 76 65 72 69 70 3d 31 39 |7-00.serverip=19|
00084190 32 2e 31 36 38 2e 38 2e 34 00 73 74 64 65 72 72 |2.168.8.4.stderr|
000841a0 3d 73 65 72 69 61 6c 00 73 74 64 69 6e 3d 73 65 |=serial.stdin=se|
000841b0 72 69 61 6c 00 73 74 64 6f 75 74 3d 73 65 72 69 |rial.stdout=seri|
000841c0 61 6c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |al..............|
000841d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000841e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000841f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084200 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084210 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084220 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084230 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084240 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084250 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084260 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084270 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084280 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084290 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000842a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000842b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000842c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000842d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000842e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000842f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084300 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084310 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084320 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084330 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084340 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084350 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084360 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084370 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084380 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084390 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000843a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000843b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000843c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000843d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000843e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000843f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084400 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084410 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084420 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084430 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084440 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084450 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084460 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084470 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084480 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084490 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000844a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000844b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000844c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000844d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000844e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000844f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084500 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084510 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084520 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084530 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084540 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084550 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084560 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084570 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084580 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084590 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000845a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000845b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000845c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000845d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000845e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000845f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084600 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084610 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084620 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084630 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084640 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084650 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084660 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084670 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084680 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084690 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000846a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000846b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000846c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000846d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000846e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000846f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084700 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084710 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084720 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084730 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084740 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084750 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084760 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084770 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084780 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084790 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000847a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000847b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000847c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000847d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000847e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
000847f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084800 ff ff 00 00 ff ff ff ff ff ff ff ff ff ff ff ff |................|
00084810 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
00084820 3b 8d c6 e5 19 b2 24 50 00 00 00 00 00 00 00 00 |;.....$P........|
00084830 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00084840
从这个"页面"中我们可以得出什么结论呢?很明显,一个大小为 2048 字节的"页面"的数据部分被分成了四个 512 字节大小的部分,我在本文开头将其称为"子页面"。在这个"页面"中,第一个"子页面"从 0x84000 到 0x841ff 开始,其中包含非零数据,其 BCH 编码 ECC 为 3b8dc6e519b22450。接下来的三个"子页面"都包含全零数据,其 BCH 编码 ECC 都是全零。换句话说,这三个"子页面"中的每个 512 字节的零数据要么直接进行 BCH 编码,要么添加了一定数量的零(仅零),以生成全零的 ECC。因此,一旦在后续部分的讨论中逐渐揭示出其他 BCH 编码参数,就可以很容易地恢复 ECC 和数据之间的秘密关联。因此,第二、第三和第四个"子页面"在一个"页面"中已经明确,对于其他所有的"页面",情况通常也是类似的。然而,第一个"子页面"的填充方案仍然不确定,除非找到一个所有四个 ECC 都是全零的"页面"。让我们尝试一下。
####################### check_all_zeros_in_all_ecc.py ########################
input_file = open("[email protected]","rb")
oob_const = \
b'\xff\xff\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' + \
b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'
zeros_ecc = \
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + \
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
page_cnt = 0
while 1:
data = input_file.read(2112)
if len(data) == 0:
break
oob_1st_32_bytes = data[2048:2048+32]
oob_2nd_32_bytes = data[2048+32:2048+32+32]
if len(data) == 2112 and oob_1st_32_bytes == oob_const:
if oob_2nd_32_bytes[0:32] == zeros_ecc[0:32]:
print("Page Num: %d, Address: 0x%X" % (page_cnt, page_cnt*2112))
break
page_cnt += 1
print("Completed")
##################################### end ####################################
让我们寻找任何预期的 “page”.然而,输出是意外的,如下所示:
cawan% python3.8 check_all_zeros_in_all_ecc.py
Completed
不管怎么说,现在先把未解决的部分放一放,我们会在下一节再回来。现在,让我们对二进制 BCH 的实现有一个简单的黑客概述,是的,完全从黑客的角度出发,不是学术性的。
一般来说,BCH 编解码器需要一个原始多项式,以便推导出一个生成器多项式,用于代码生成。Gallois 场顺序将决定 BCH 编解码器可使用的原始多项式的数量。多项式可以用整数或比特形式的二进制来表示。整数或二进制位的设定位代表所选原始多项式的给定数量级的系数。听起来很迷惑吗?让我们举个例子。
0x201B
|
V
0b0010000000011011
|
V
0b 0 0 1 0 0 0 0 0 0 0 0 1 1 0 1 1
^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
| | | | | | | | | | | | | | | |
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
对于 0x201B 的十六进制表示,它可以用比特形式的二进制表示为 0b0010000000011011。这个数字中的每一个设定位都将反映出给定数量级的系数,形成一个原始多项式。对于 0x201B 的情况,第 0 位、第 1 位、第 3 位、第 4 位和第 13 位是设定位。所以,原始多项式是:
x^13 + x^4 + x^3 + x^1 + 1
是的,每个设置位的位置反映了所选择的数量级,最大的设置位位置被定义为原始多项式的度数。同样,对于 0x201B 的情况,它的度数是 13。在大多数情况下,度数被称为 m,以代表 Gallois Field 顺序,因此对于 0x201B 的情况,可以表示为 m=13。为了保护一个以比特为单位的一定数量的数据,这个数字应该小于 2^m。例如,为了保护 512 字节的数据,以比特为单位的数据长度为 512*8=4096,这个数字通常被称为 k,因此,以 k=4096 的形式书写更为合适。所以,2^m 的数字应该大于 4096,那么 m 应该大于 log(4096)/log(2)=12,m 至少应该是 13。同样,对于 0x201B 的情况,由于其 m 是 13,那么它适合用于保护 512 字节大小的数据。0x201B 的十六进制数字在十进制中是什么?它是 8219,听起来很熟悉吧?是的,它被用在 "第一眼 "bchlib 部分,用来定义变量 BCH_POLYNOMIAL。
在谈论数据保护时,人们必须谈论保护强度。保护强度是指如果数据出了问题,那么数据最多可以容忍多少比特的错误,以便恢复到正确的状态。所以,当有人提到 t=4 时,这意味着 ECC 最多可以容忍 4 比特的错误。好了,现在对 m、k 和 t 都很清楚了。让我们继续讨论 ECC 的长度,这通常被称为奇偶校验位的大小。对于 BCH,奇偶校验位的大小等于 mt。因此,给定 m=13,k=4096,t=4,因为 2m=213=8192,大于 k=4096,所以产生 BCH 编码的 ECC 的奇偶校验位的大小为 mt=13*4=52 位是合适的,没有任何差异。还记得上一部分从 NAND 转储分析中发现的 ECC 大小吗? 是的,它是 60 位(8 个字节扣除最后 4 位的零)。
好了,无聊的东西现在变得有趣了。让我们看看通过这个小线索可以推断出什么。要保护的数据大小是 512 字节,也就是 4096 比特。m 至少应该是 13,所以 2m=213=8192,这足以保护 4096 比特的数据。由于奇偶校验位的数量是 60,各自的系数是 1,2,3,4,5,6,10,12,15,20,30,和 60。通过给定 m*t=60,和 m>=13,(m,t)的可能组合为 (15,4),(20,3),(60,1)。虽然 t=4 是大多数 ECC 的 BCH 实现的常见方法,但 m=15 和 t=4 的组合是最有可能的。其他两个组合(20,3)和(60,1)不仅不现实,而且还严重超标。在这个阶段,假设 m=15 和 t=4,应该选择哪个原始多项式?让我们参考一下[4]中所述的原始多项式列表。对于 15 度来说,候选项如下所示。
x^15 + x^1 + 1
x^15 + x^4 + 1
x^15 + x^7 + 1
x^15 + x^7 + x^6 + x^3 + x^2 + x^1 + 1
x^15 + x^10 + x^5 + x^1 + 1
x^15 + x^10 + x^5 + x^4 + 1
x^15 + x^10 + x^5 + x^4 + x^2 + x^1 + 1
x^15 + x^10 + x^9 + x^7 + x^5 + x^3 + 1
x^15 + x^10 + x^9 + x^8 + x^5 + x^3 + 1
x^15 + x^11 + x^7 + x^6 + x^2 + x^1 + 1
x^15 + x^12 + x^3 + x^1 + 1
x^15 + x^12 + x^5 + x^4 + x^3 + x^2 + 1
x^15 + x^12 + x^11 + x^8 + x^7 + x^6 + x^4 + x^2 + 1
x^15 + x^14 + x^13 + x^12 + x^11 + x^10 + x^9 + x^8 + x^7 + x^6 + \
x^5 + x^4 + x^3 + x^2+1
那么,第一个候选项应该被选中,这就是:
x^15 + x^1 + 1
如前所述,多项式可以用二进制位的形式,如下所示:
0b1000000000000011
在十六进制中,它是 0x8003,在十进制中是 32771。所以,回到 bchlib,BCH_POLYNOMIAL 和 BCH_BITS,它们都应该分别设置为 32771 和 4。
现在,假设没有人会天真到不先对整个数据输入进行位序反转就进行 BCH 编码,让我们试试第一 "页 "没有任何填充的 BCH 编码。
###################### bch_encoding_without_padding.py #######################
import bchlib
import binascii
BCH_POLYNOMIAL = 32771
BCH_BITS = 4
bch = bchlib.BCH(BCH_POLYNOMIAL, BCH_BITS)
input_file = open("./[email protected]", "rb")
page = input_file.read(2112)
ECC = page[2048+32:2048+32+32]
for i in range(0, 4):
ecc_generated = bch.encode(page[i*512:i*512+512])
print("\nSub-page: %d" % i)
print("ECC Ori:", end=' ')
print(ECC[i*8:i*8+8].hex().upper())
print("ECC Generated:", end=' ')
print(ecc_generated.hex().upper())
if ECC[i*8:i*8+8] == ecc_generated:
print("Match !")
else:
print("Wrong !")
print("\nCompleted")
##################################### end ####################################
输出结果如下:
cawan% python3.8 bch_encoding_without_padding.py
Sub-page: 0
ECC Ori: F689F779E560C9E0
ECC Generated: 8DE136AAF3E03F90
Wrong !
Sub-page: 1
ECC Ori: D6E3EDCB9CB0F9F0
ECC Generated: 6C6CF320EFAD8660
Wrong !
Sub-page: 2
ECC Ori: 1FDAD4A49CD41BE0
ECC Generated: 1058EAC213313D70
Wrong !
Sub-page: 3
ECC Ori: E090CC85D8D2E280
ECC Generated: B36A94B537E14BA0
Wrong !
Completed
四个 "子页 "中没有一个产生正确的 ECC。所以,在进行 BCH 编码之前,"子页 "应该被填充一定数量的零。让我们尝试在 "子页 "上填充 1 到 32 个字节的零来进行 BCH 编码。
#################### bch_encoding_with_zeros_padding.py ######################
import bchlib
import binascii
BCH_POLYNOMIAL = 32771
BCH_BITS = 4
bch = bchlib.BCH(BCH_POLYNOMIAL, BCH_BITS)
input_file = open("./[email protected]", "rb")
page = input_file.read(2112)
ECC = page[2048+32:2048+32+32]
found_flag = 0
for i in range(0, 4):
print("\nSub-page: %d" % i)
print("ECC Ori:", end=' ')
print(ECC[i*8:i*8+8].hex().upper())
for j in range(1, 33):
padding = b'\x00'*j
ecc_generated = bch.encode(page[i*512:i*512+512]+padding)
if ECC[i*8:i*8+8] == ecc_generated:
print("ECC Generated:", end=' ')
print(ecc_generated.hex().upper())
print("Match !", end=' ')
print("Zeros padded number: %d" % j)
found_flag = 1
break
if found_flag == 0:
print("Wrong !")
found_flag = 0
print("\nCompleted")
#################################### end ####################################
让我们去运行检查。输出结果如下:
cawan% python3.8 bch_encoding_with_zeros_padding.py
Sub-page: 0
ECC Ori: F689F779E560C9E0
Wrong !
Sub-page: 1
ECC Ori: D6E3EDCB9CB0F9F0
ECC Generated: D6E3EDCB9CB0F9F0
Match ! Zeros padded number: 24
Sub-page: 2
ECC Ori: 1FDAD4A49CD41BE0
ECC Generated: 1FDAD4A49CD41BE0
Match ! Zeros padded number: 24
Sub-page: 3
ECC Ori: E090CC85D8D2E280
ECC Generated: E090CC85D8D2E280
Match ! Zeros padded number: 24
Completed
因此,对于一个 "页 "中的四个 “子页”,除了第一个 "子页 "之外,第二个、第三个和第四个 "子页 "在被 BCH 编码之前都被填充了 24 个字节的零,以便分别产生正确的 ECC。
然而,第一个 "子页 "仍然是加密的,这需要调整一下。由于其余的 "子页 "都是用 24 个字节的零填充的,那么第一个 "子页 "很可能是用 24 个字节的非零数据填充的。这应该是与某种 "元数据 "有关的东西,它对 "页 "本身有描述作用。还记得 OOB 的前 32 字节吗?让我们再检查一下。
cawan% hexdump -C -v -n $((2112-32)) [email protected] | tail -n 3
00000800 ff ff 00 00 ff ff ff ff ff ff ff ff ff ff ff ff |................|
00000810 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
00000820
在 0x802 和 0x803 的两个字节的零有点奇怪。那么,24 个零字节的前几个字节是否有可能被这里的一些字节所取代?让我们试着逐个替换 24 字节的零填充物,直到整个 24 字节的填充物变为:
ffff0000ffffffffffffffffffffffffffffffffffffffff
让我们试一试。
####################### bch_encoding_of_1st_subpage.py #######################
import bchlib
import binascii
BCH_POLYNOMIAL = 32771
BCH_BITS = 4
bch = bchlib.BCH(BCH_POLYNOMIAL, BCH_BITS)
input_file = open("./[email protected]", "rb")
page = input_file.read(2112)
subpage = page[0:512]
ECC = page[2048+32:2048+32+8]
paddingx = \
b'\xFF\xFF\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF' + \
b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF'
padding0 = \
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + \
b'\x00\x00\x00\x00\x00\x00\x00\x00'
data_input = subpage + padding0
data_input = bytearray(data_input)
for i in range(0, 24):
data_input[512+i] = paddingx[i]
ecc_generated = bch.encode(data_input)
if ecc_generated == ECC:
print("Match !")
print("Padding:", end=' ')
print(data_input[512:].hex().upper())
break
print("\nCompleted")
#################################### end ####################################
让我们来运行它。瞧,找到了填充模式,如下所示:
cawan% python3.8 bch_encoding_of_1st_subpage.py
Match !
Padding: FFFF00000000000000000000000000000000000000000000
Completed
完美。现在,ECC 和数据之间的秘密联系被完全揭开了。作为结论,对于一个 "页 "中的每个 “子页”,第一个 "子页 "必须由 24 个字节的填充物填充,其中包括 2 个字节的 0xFF 和 22 个字节的零,然后再进行 BCH 编码以产生正确的 ECC。对于第二、第三和第四个 "子页 "的情况,分别只需要 24 个字节的全零填充就可以产生正确的 ECC。因此,通过对整个 NAND 转储的所有 "页 "进行类似的 BCH 解码,所有的比特错误都得到了完美的修复。之后,每个 "页 "中的 64 个字节的 OOB 应该被删除,并生成一个新的 NAND 转储,在 "页 "中的连续数据没有任何比特错误,我把它重命名为 cawan_output.bin,如下所示,
####################### NAND_dump_fix_bit_erros_ecc.py #######################
import bchlib
BCH_POLYNOMIAL = 32771
BCH_BITS = 4
input_file = open("./[email protected]", "rb")
output_file = open("./cawan_output.bin", "wb")
pad_sub0 = \
b'\xFF\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + \
b'\x00\x00\x00\x00\x00\x00\x00\x00'
pad_subx = \
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + \
b'\x00\x00\x00\x00\x00\x00\x00\x00'
bch = bchlib.BCH(BCH_POLYNOMIAL, BCH_BITS)
count = 0
error_cnt = 0
while 1:
page = input_file.read(2112)
if len(page) != 2112:
break
for i in range(0, 4):
data, ecc = page[512*i:512*i+512], page[2048+32+i*8:2048+32+i*8+8]
if i == 0:
data_padded = data + pad_sub0
else:
data_padded = data + pad_subx
data_padded = bytearray(data_padded)
bitflips = bch.decode_inplace(data_padded, ecc)
if bitflips == 0:
output_file.write(data_padded[:512])
elif bitflips > 0:
error_cnt += 1
output_file.write(data_padded[:512])
elif bitflips == -1:
output_file.write(data_padded[:512])
count += 1
print("Sub-page with error count: %d\n" % error_cnt)
print("Completed.")
#################################### end ####################################
那么,有 20 个 "子页面 "的比特错误已经被 ECC 修复,如下所示:
cawan% python3.8 NAND_dump_fix_bit_erros_ecc.py
Sub-page with error count: 20
Completed.
通过知识的武装,任何合适的普通工具都可以成为黑客攻击的武器。不要傻傻地固执地相信一个专有的、特殊的、商业的、甚至是自动化的工具可以在不需要任何领域的知识的情况下如期工作。所以,现在固件已经准备好了,让我们继续进行固件分析。
作为一种常见的方法,让我们从 binwalk 开始,期待着好运从天而降。先看看 binwalk 的输出,如下所示:
cawan% binwalk cawan_output.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
963584 0xEB400 CRC32 polynomial table, little endian
966688 0xEC020 CRC32 polynomial table, little endian
970868 0xED074 LZO compressed data
2097152 0x200000 uImage header, header size: ...
2097216 0x200040 Linux kernel ARM boot executable zImage ...
2115956 0x204974 gzip compressed data, maximum compression, ...
6291456 0x600000 UBI erase count header, version: 1, ...
它看起来很有趣。正如本文标题所述,我们只对 UBI 图像进行分析。下面是在地址 0x600000 处检测到的 UBI 头的完整描述:
UBI erase count header,
version: 1,
EC: 0x1,
VID header offset: 0x800,
data offset: 0x1000
在 0x600000,版本 1,擦除次数是 1,这意味着它是一个新的 NAND 闪存,或者至少它只是被重新格式化了,这个标题真的很有意义。在这之后,卷 ID 头是 0x800 或 2048 的十进制,远离 0x600000,这是 NAND 闪存的一个常见方法。这里要强调的是一件重要的事情。新生成的 NAND 转储被定义为逻辑 NAND 转储,它被去除 OOB,每个 "页 "的大小为 2048 字节。因此,这确实是一种常见的方法,将卷 ID 头定位在离 UBI 头一个 "页面 "的地方。然后,实际的数据是离 0x600000 的 0x1000 或 4096(十进制),换句话说,它是离卷 ID 头的另一个 “页”。这也是 NAND 闪存的一种常见方法。那么,有什么东西可以作为午餐?让我们试着用 binwalk 来提取它,输入众所周知的参数-Me。冗长的输出似乎很有说服力。让我们进入存放提取的文件的目录,如下所示:
cawan% cd _cawan_output.bin.extracted
cawan% ls
204974 _204974.extracted 600000.ubi ED074.lzo ubifs-root
由于 ubifs-root 目录已经生成,让我们进入该目录。
cawan% cd ubifs-root
cawan% ls
1941946494 3823591600
又发现了两个目录。让我们通过使用 tree 命令检查每个目录。
cawan% tree -L 2 1941946494
1941946494
ubifs
bin
dev
etc
home
lib
linuxrc -> bin/busybox
mnt
proc
root
sbin
sys
tmp
usr
var
work
15 directories, 1 file
cawan% tree -L 3 3823591600
3823591600
app
1 directory, 0 files
好吧,似乎文件系统是在 1941946494 的目录下提取的。然而,对于 3823591600,它是一个空目录。让我们进一步看看。
cawan% cd 1941946494
cawan% cd ubifs
cawan% ls
bin dev etc home lib linuxrc mnt proc root sbin sys tmp usr \
var work
cawan% cd etc
cawan% ls
fstab HOSTNAME inittab pointercal profile~ ts.conf
group inetd.conf networks ppp services vsftpd.conf
gshadow init.d passwd profile shadow
cawan% cat fstab
cawan% ls -la fstab
-rw-rw-r-- 1 user user 186 Mar 30 2015 fstab
cawan% cat fstab | xxd
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000060: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000090: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000b0: 0000 0000 0000 0000 0000 ..........
嗯,一定是文件系统的提取出了问题。看来免费的午餐并不是真的免费。让我们进一步寻找原因吧。
不要误入歧途,这对于一个铁杆黑客来说,确实不在正轨。在谈论分析的时候,整个过程的每一步都应该是严格控制的,可追踪的,可解释的,这也适用于固件分析。让我们再从头开始用 dd,手动制作 UBI 镜像。
cawan% dd if=./cawan_output.bin of=./ubi.bin bs=1 skip=$((0x600000))
262144000+0 records in
262144000+0 records out
262144000 bytes (262 MB, 250 MiB) copied, 281.069 s, 933 kB/s
cawan% file ubi.bin
ubi.bin: UBI image, version 1
生成 ubi.bin 真的要花点时间。现在,让我们在十六进制视图中验证 UBI 头、卷 ID 头和数据的开始。
cawan% hexdump -C -n $((2048*3)) ./600000.ubi
00000000 55 42 49 23 01 00 00 00 00 00 00 00 00 00 00 01 |UBI#............|
00000010 00 00 08 00 00 00 10 00 73 bf c0 7e 00 00 00 00 |........s..~....|
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000030 00 00 00 00 00 00 00 00 00 00 00 00 01 9f 6b b3 |..............k.|
00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
00000800 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
*
00001800
让我们解释一下 UBI 头的数据结构,如下所示:
struct ubi_ec_hdr {
__be32 magic;
__u8 version;
__u8 padding1[3];
__be64 ec;
__be32 vid_hdr_offset;
__be32 data_offset;
__be32 image_seq;
__u8 padding2[32];
__be32 hdr_crc;
}
头部的魔法是 “UBI#”,大小为 4 个字节,接着是版本号 1,大小为 1 个字节。在 3 个字节的填充之后,是所谓的擦除计数器,其缩写为 ec,表示该块已被擦除多少次。关于这一点,有一点背景知识,可能对黑客不友好。NAND 闪存有一定数量的寿命。对闪存中的同一位置进行的每一次擦除操作,都会减少寿命。所以,一旦达到了寿命数,这个地方就会变得无用。UBI 将 NAND 闪存划分为 “块”,由若干 "页 "组成。以 MT29F2G08ABAEAWP 为例,一个 "块 "包括 64 个 “页”,每个 "页 "的大小为 2048 字节。因此,监测所有 "块 "的使用次数以避免数据丢失是至关重要的。因此,当一个 "块 "的使用次数达到一定的触发水平时,该 "块 "中的全部数据必须被重新定位到另一个状态良好的 "块 "中。虽然物理 "块 "的重新定位会影响 "块 "的顺序或次序,但它需要某种抽象来以逻辑的方式管理物理 “块”。通过在高层确保逻辑 "块 "的顺序或次序,逻辑 "块 "可以特别被重新映射到相应的物理 “块”。这样的抽象被正式称为 “磨损均衡”。那么,所谓的已用计数与 UBI 中的擦除计数是相同的,或者说在磨损均衡中的磨损计数。UBI 负责通过以最适当的方式管理逻辑 "块 "来提供这样一种损耗平衡机制。让我们回到 UBI 头的 8 字节的 ec 项。ec 是 1 意味着它被格式化了 1 次。在 ec 之后,是 4 个字节的 volume ID,从 UBI 头的开始偏移,它是 0x800,大约是 1 "页 "大小。卷 ID 后面是 4 个字节的数据偏移量,是 0x1000,这是从卷 ID 开始的另一个 “页面”。在数据偏移的旁边是另外 4 个字节,代表图像序列,用于识别各自的 UBI 块属于哪一个 UBIFS,以便构建文件系统。因此,UBIFS 确实是黑客应该关注的实际文件系统。在这之后,有 32 个字节的填充,最后,是 UBI 头的 CRC 校验,共 4 个字节。
现在,让我们检查 UBI 图像中存在多少个 UBIFS。
############################ check_ubifs_count.py ############################
input_file = open("./600000.ubi", "rb")
count = 0
img_seq = b''
tmp_seq = b''
while 1:
block = input_file.read(2048*64)
if len(block) != 2048*64:
break
if block[0:4] == b'\x55\x42\x49\x23':
img_seq = block[24:28]
if img_seq != tmp_seq:
print("0x", end='')
print(img_seq.hex().upper(), end=' -> ')
print("%d" % int(img_seq.hex(),16))
tmp_seq = img_seq
count += 1
print("\nCompleted.")
#################################### end ####################################
输出结果如下:
cawan%% python3.8 check_ubifs_count.py
0x73BFC07E -> 1941946494
0xE3E760B0 -> 3823591600
0x9F61AB77 -> 2673978231
0x49F558F2 -> 1240815858
Completed.
听起来很熟悉吗?那是当然。1941946494 和 3823591600 是被 binwalk 用来命名存放提取文件的文件夹。那另外两个呢?这肯定是 binwalk 在提取 UBI 镜像的过程中出了问题。在继续前进之前,让我们试着估计一下 UBI 镜像中使用的数据大小。首先要澄清一件事。每当 UBI 擦除块被使用时,它应该带有有效的卷 ID 头,而魔法是 “UBI!”。
请注意,"UBI 擦除块 "这个术语实际上是逻辑 UBI 块的正式术语。
############################ check_data_inuse.py #############################
input_file = open("./600000.ubi", "rb")
data_inuse = 0
UBI_hdr = b'\x55\x42\x49\x23'
VID_hdr = b'\x55\x42\x49\x21'
while 1:
block = input_file.read(2048*64)
if len(block) != 2048*64:
break
if block[0:4] == UBI_hdr and block[2048:2048+4] == VID_hdr:
data_inuse += 2048*64
print("Data size in use: %d" % data_inuse)
print("\nCompleted.")
#################################### end ####################################
输出结果如下:
cawan% python3.8 check_data_inuse.py
Data size in use: 40239104
Completed.
不错,它的大小约为 40MB,包括一些难以精确估计的额外空间。现在,是时候谈谈如何从 UBI 图像中提取 UBIFS 了。因为这是关于根据图像序列号重新安排 UBI 擦除块的问题,用一个众所周知的工具包,UBI 阅读器来试试也无妨,让我们看看结果。
cawan% ubireader_extract_images ubi.bin
cawan% ls
cawan_output.bin ubi.bin ubifs-root
cawan% cd ubifs-root
cawan% ls
ubi.bin
cawan% cd ubi.bin
cawan% ls
img-1240815858_vol-data.ubifs img-2673978231_vol-backup.ubifs
img-1941946494_vol-ubifs.ubifs img-3823591600_vol-app.ubifs
cawan% ls -la
total 145212
drwxrwxr-x 2 user user 4096 May 29 16:46 .
drwxrwxr-x 3 user user 4096 May 29 16:46 ..
-rw-rw-r-- 1 user user 100438016 May 29 16:46 img-1240815858_vol-data.ubifs
-rw-rw-r-- 1 user user 11935744 May 29 16:46 img-1941946494_vol-ubifs.ubifs
-rw-rw-r-- 1 user user 27299840 May 29 16:46 img-2673978231_vol-backup.ubifs
-rw-rw-r-- 1 user user 9015296 May 29 16:46 img-3823591600_vol-app.ubifs
很好。完全没有错误提示,4 个 UBIFS 被提取出来了。记得估计使用中的数据大小约为 40MB?有理由认为名称为 img-1240815858_vol-data.ubifs 的 UBIFS 有问题。其余的 3 个 UBIFS 应该处于良好状态,因为它们的总大小约为 40MB 以上。
让我们再次尝试使用 UBI 阅读器工具包从 UBIFS 中提取文件。 从 img-1941946494_vol-ubifs.ubifs 开始,如下所示:
cawan% ubireader_extract_files img-1941946494_vol-ubifs.ubifs
Extracting files to: ubifs-root
decompress Warn: LZO Error: EResult.LookbehindOverrun
_process_reg_file Warn: inode num:693 path:<...> :can't concat NoneType to bytearray
decompress Warn: LZO Error: EResult.InputOverrun
_process_reg_file Warn: inode num:592 path:<...> :can't concat NoneType to bytearray
decompress Warn: LZO Error: EResult.LookbehindOverrun
_process_reg_file Warn: inode num:587 path:<...> :can't concat NoneType to bytearray
decompress Warn: LZO Error: EResult.InputOverrun
...
...
...
cawan% ls
img-1240815858_vol-data.ubifs img-2673978231_vol-backup.ubifs ubifs-root
img-1941946494_vol-ubifs.ubifs img-3823591600_vol-app.ubifs
cawan% cd ubifs-root
cawan% ls
bin dev etc home lib linuxrc mnt proc root sbin sys tmp usr \
var work
After getting a huge number of error prompt, it seems a file system is
generated. Is that the same thing as what was being generated by binwalk
earlier ? Let's check.
cawan% cd etc
cawan% cat fstab
cawan% ls -la fstab
-rw-rw-r-- 1 user user 186 Mar 30 2015 fstab
cawan% cat fstab | xxd
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000060: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000090: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000000b0: 0000 0000 0000 0000 0000 ..........
该死的,确实是同样的事情。似乎 ubireader_extract_files 不能完全解释 UBIFS 并生成正确的文件。其他两个 UBIFS 呢?让我们检查一下。
cawan% ubireader_extract_files img-2673978231_vol-backup.ubifs
Extracting files to: ubifs-root
index Fatal: LEB: 110 at 13998336, Node size smaller than expected.
cawan% ubireader_extract_files img-3823591600_vol-app.ubifs
Extracting files to: ubifs-root
index Fatal: LEB: 58 at 7461120, Node size smaller than expected.
对不起,这次是致命的错误,没有生成任何东西。由于这是来自真实设备的 NAND 转储,功能齐全,而且所有的比特错误都已被修复,UBIFS 应该相应地工作。它应该以另一种方式进行,通过使用 nandsim 来模拟 NAND 芯片,使其与 MTD 相关联。在大多数的黑客文献中,在谈到 nandsim 时,一个标准的传统方法是通过 nandsim 将整个 UBI 图像复制到模拟的 MTD 设备中,并用一些参数 modprobeubi 驱动,ubi 驱动就可以自己处理 UBI 图像 blob。让我们对这个问题发表一些看法。正如前面提到的,UBI 擦除块是为了在 UBI 层实现损耗均衡。由于 UBI 擦除块是以逻辑形式存在的,它们通常在物理上是不连续的,就像 NAND dump 的情况一样。因此,与其依赖 UBI 驱动进行额外的块重映射操作,这可能会在仿真模式下导致所有的错误,不如先用 ubireader_extract_images 在离线模式下预处理 UBI 图像。ubireader_extract_images 的输出已经是 UBIFS 的形式,这是实际的文件系统,就像 squashfs, jffs2, yaffs2, 或 CRAMFS 做的那样。
换句话说,通过直接处理 UBIFS,获得错误的机会将被减少到最低。无论如何,先用标准的常规方法也无妨。让我们开始抓取低悬的果实。为了仿真 NAND 芯片,我们应该知道该芯片的 ID 代码。通过参考 MT29F2G08ABAEAWP 的数据手册,前 4 个字节是 0x2c、0xda、0x90 和 0x95。有了这样的信息,就可以进行 nandsim 了。
cawan% sudo modprobe nandsim first_id_byte=0x2c second_id_byte=0xda
third_id_byte=0x90 fourth_id_byte=0x95
cawan% cat /proc/mtd
dev: size erasesize name
mtd0: 10000000 00020000 "NAND simulator partition 0"
cawan% sudo mtdinfo -a
Count of MTD devices: 1
Present MTD devices: mtd0
Sysfs interface supported: yes
mtd0
Name: NAND simulator partition 0
Type: nand
Eraseblock size: 131072 bytes, 128.0 KiB
Amount of eraseblocks: 2048 (268435456 bytes, 256.0 MiB)
Minimum input/output unit size: 2048 bytes
Sub-page size: 512 bytes
OOB size: 64 bytes
Character device major/minor: 90:0
Bad blocks are allowed: true
Device is writable: true
因为现在它被认为是低悬的果实,所以先忽略显示的参数。现在,让我们把 UBI 映像文件 dd 到/dev/mtd0。
cawan% sudo dd if=ubi.bin of=/dev/mtd0 bs=2048
128000+0 records in
128000+0 records out
262144000 bytes (262 MB, 250 MiB) copied, 2.7339 s, 95.9 MB/s
Done. Now, modprobe the ubi driver.
cawan% sudo modprobe ubi mtd=0,2048
modprobe: ERROR: could not insert 'ubi': Invalid argument
对不起,对于这个 NAND 转储来说,低垂的果实其实并不低。让我们以正确的方式进行,就像先前所建议的那样。让我们从头开始,先对 nandsim 进行 rmmod,然后再对 nandsim 进行 modprobe。
cawan% sudo rmmod nandsim
cawan% sudo modprobe nandsim first_id_byte=0x2c second_id_byte=0xda
third_id_byte=0x90 fourth_id_byte=0x95
嗯,这里没有什么特别之处。mtdinfo-a 的输出也没什么特别的,因为它只是关于 MT29F2G08ABAEAWP 的参数。唯一需要确保的是/dev/mtd0 被创建。之后,使用 ubiformat 和正确的参数将模拟的 NAND 闪存显示为与 NAND dump 中使用的 UBI 规范兼容的 UBI,如下所示:
cawan% sudo ubiformat -s 2048 -O 2048 /dev/mtd0
ubiformat: mtd0 (nand), size 268435456 bytes (256.0 MiB), \
2048 eraseblocks of 131072 bytes (128.0 KiB), min. I/O size 2048 bytes
libscan: scanning eraseblock 2047 -- 100 % complete
ubiformat: 2048 eraseblocks are supposedly empty
ubiformat: formatting eraseblock 2047 -- 100 % complete
让我们来解释一下 ubiformat 的两个强制性的输入参数。-s 也被称为子页面大小,它是用于 UBI 头文件的最小 i/o 单位。将其设置为 2048,可以防止 UBI 将整个 2048 字节分割成更小的子页单位。接下来,-O 是卷 ID 头的偏移。把它设置为 2048,意味着卷 ID 头应该从 UBI 擦除块的开始处开始 1 页或 2048 字节。
请注意,如果不指定这两个参数的正确数字,或者把所有的东西都保留为默认值,在下面的步骤中就会出现错误。让我们进一步对 UBI 驱动进行调制检测。
cawan% sudo modprobe ubi
cawan%
没有错误提示,就当它成功了。现在,使用 ubiattach 创建一个 UBI 设备文件,该文件与/dev/mtd0 关联工作,如下所示:
cawan% sudo ubiattach -p /dev/mtd0 -O 2048
UBI device number 0, \
total 2048 LEBs (260046848 bytes, 248.0 MiB), \
available 2002 LEBs (254205952 bytes, 242.4 MiB), \
LEB size 126976 bytes (124.0 KiB)
同样,输入参数-O 2048 对于指定卷 ID 头偏移量为离 UBI 擦写块 2048 字节至关重要,这与 ubiformat 类似。确保逻辑擦写块(LEB)的大小是 126976 字节是非常重要的。为什么?因为 Eraseblock 的大小是 2048*64=131072,在减去 2 个各 2048 字节的页面(一个是 UBI 头,一个是卷 ID 头)后,LEB 的大小就变成了 131072-2048-2048=126976。所以,它们是相互匹配的。否则,在下面的步骤中也会出现错误。
一个新的 UBI 设备文件被创建为/dev/ubi0,可以通过使用 ubinfo 检查其细节,如下所示,
cawan% sudo ubinfo /dev/ubi0 -a
ubi0
Volumes count: 0
Logical eraseblock size: 126976 bytes, 124.0 KiB
Total amount of logical eraseblocks: 2048 (260046848 bytes, 248.0 MiB)
Amount of available logical eraseblocks: 2002 (254205952 bytes, 242.4 MiB)
Maximum count of volumes 128
Count of bad physical eraseblocks: 0
Count of reserved physical eraseblocks: 40
Current maximum erase counter value: 0
Minimum input/output unit size: 2048 bytes
Character device major/minor: 237:0
现在,一个与 NAND 转储中的 UBI 镜像规格完全相同的 UBI 环境正在准备中。让我们创建一个有足够存储空间的卷来承载 ubireader_extract_images 所创建的 UBIFS,如下所示:
cawan% sudo ubimkvol -N volume1 -s 50MiB /dev/ubi0
Volume ID 0, \
size 413 LEBs (52441088 bytes, 50.0 MiB), \
LEB size 126976 bytes (124.0 KiB), dynamic, name "volume1", alignment 1
好了,一个名为 "volume1 "的 50MB 大小的新卷已经成功创建,同时使用 ubimkvol 创建了一个新的设备文件/dev/ubi0_0。 现在,是时候通过使用 ubiupdatevol 让 volume1 承载一个 UBIFS。让我们先从 img-1941946494_vol-ubifs.ubifs 开始,如下所示:
cawan% ls -la
total 145216
drwxrwxr-x 3 user user 4096 May 30 01:40 .
drwxrwxr-x 3 user user 4096 May 29 16:46 ..
-rw-rw-r-- 1 user user 100438016 May 29 16:46 img-1240815858_vol-data.ubifs
-rw-rw-r-- 1 user user 11935744 May 29 16:46 img-1941946494_vol-ubifs.ubifs
-rw-rw-r-- 1 user user 27299840 May 29 16:46 img-2673978231_vol-backup.ubifs
-rw-rw-r-- 1 user user 9015296 May 29 16:46 img-3823591600_vol-app.ubifs
drwxrwxr-x 2 user user 4096 May 30 01:40 ubifs-root
cawan% sudo ubiupdatevol /dev/ubi0_0 img-1941946494_vol-ubifs.ubifs
cawan%
到目前为止,一切工作都很完美,没有任何一个错误。让我们看看低垂的果实,它不是那么低,现在是否可用。
cawan% mkdir /tmp/nand
cawan% sudo mount -t ubifs /dev/ubi0_0 /tmp/nand
cawan% cd /tmp/nand
cawan% ls
bin dev etc home lib linuxrc mnt proc root sbin sys tmp usr \
var work
希望这与上一节中 ubireader_extract_files 生成的东西不是一回事。让我们来验证一下。
cawan% cd etc
cawan% cat fstab
proc /proc proc defaults 0 0
none /var/shm shm defaults 0 0
sysfs /sys sysfs defaults 0 0
none /tmp tmpfs defaults 0 0
多么神奇的时刻。让我们用另外两个 UBIFS 试试。
cawan% sudo umount /tmp/nand
cawan% sudo ubiupdatevol /dev/ubi0_0 img-2673978231_vol-backup.ubifs
cawan% sudo mount -t ubifs /dev/ubi0_0 /tmp/nand
cawan% ls /tmp/nand
14x8.hzk dat.ini flat_backup libplat.so ParaAutoNet.db ParamUniq.db
acmet driver_gwzd.ko gsmMuxd lyzd ParamMeter.db ppp
check.ini factory icons.bmp manuf.xin ParamOther.db seting.ini
chs.bin filecheck libacmet.so metproto.so ParamTerm.db startup.sh
cawan% sudo umount /tmp/nand
cawan% sudo ubiupdatevol /dev/ubi0_0 img-3823591600_vol-app.ubifs
cawan% sudo mount -t ubifs /dev/ubi0_0 /tmp/nand
cawan% ls /tmp/nand
14x8.hzk driver_gwzd.ko libacmet.so manuf.xin startup.sh
check.ini filecheck libplat.so metproto.so tmt_info.log
chs.bin gsmMuxd lyzd ppp updateinfo.xin
dat.ini icons.bmp lyzd.xzip seting.ini
最后结论为,整个文件系统托管在三个不同的 UBIFS 中,已经完全提取成功了。
CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享