NAND存储器转储分析 - 使用ECC修复位错误与UBI镜像固件分析
2023-7-11 15:14:11 Author: www.4hou.com(查看原文) 阅读量:10 收藏

一、 简介

这篇研究论文将通过黑客的视角,详细阐述如何操作 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  |[email protected]|
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  |[email protected]|
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  |[email protected] . .!..P.|
000003c0  a1 00 40 20 a3 20 82 21  21 01 50 e1 21 01 40 20  |[email protected] . [email protected] |
000003d0  23 21 82 21 a1 01 50 e1  a1 01 40 20 a3 21 82 21  |#[email protected] .!.!|
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  |[email protected]|
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..<R..........|
00000650  46 ff ff eb 04 10 a0 e1  0e a6 a0 e3 00 b0 a0 e1  |F...............|
00000660  0a 08 a0 e3 41 ff ff eb  04 10 a0 e1 00 70 a0 e1  |....A........p..|
00000670  ec 00 9f e5 3d ff ff eb  04 10 a0 e1 00 90 a0 e1 |....=...........|
00000680  0a 08 a0 e3 5f ff ff eb  01 00 a0 e1 05 10 a0 e1  |...._...........|
00000690  36 ff ff eb 00 60 a0 e1  24 00 00 ea 3c 12 9d e5 |6......$...<...|
000006a0  38 02 9d e5 31 ff ff eb  8a 4f 8d e2 bc 52 9d e5 |8...1....O...R..|
000006b0  50 10 a0 e3 00 20 a0 e3  90 07 03 e0 04 00 a0 e1  |P.... ..........|
000006c0  0f e0 a0 e1 34 f0 95 e5  04 00 a0 e1 0f e0 a0 e1  |....4...........|
000006d0  08 f0 95 e5 ff 00 50 e3  01 90 89 12 12 00 00 1a  |......P.........|
000006e0  0e 00 00 ea bc 42 9d e5  38 02 9d e5 d4 51 94 e5 |.....B..8....Q..|
000006f0  3c 12 9d e5 00 00 55 e3  05 00 00 0a 1b ff ff eb  |<.....U.........|
00000700  04 10 a0 e1 0a 20 a0 e1  90 67 23 e0 8a 0f 8d e2  |..... ...g#.....|
00000710  35 ff 2f e1 3c 32 9d e5  01 60 86 e2 03 a0 8a e0  |5./.<2.........|
00000720  0b 00 56 e1 ee ff ff 3a  00 60 a0 e3 01 70 87 e2  |..V....:....p..|
00000730  09 00 57 e1 d8 ff ff 9a  1c 30 9d e5 00 00 53 e3  |..W......0....S.|
00000740  02 00 00 0a 8a 0f 8d e2  00 10 e0 e3 33 ff 2f e1  |............3./.|
00000750  0e 36 a0 e3 33 ff 2f e1  2d de 8d e2 1e ff 2f e1  |.6..3./.-...../.|
00000760  00 d0 00 b0 ff cf 11 00  f0 40 2d e9 02 60 d3 e5  |[email protected]|
00000770  00 40 d3 e5 00 00 d2 e5  01 c0 d3 e5 02 50 d2 e5  |[email protected]|
00000780  01 30 d2 e5 00 40 24 e0  03 c0 2c e0 05 60 26 e0  |[email protected]$...,..&.|
00000790  ff 00 04 e2 06 30 8c e1  03 30 90 e1 01 70 a0 e1  |.....0...0...p..|
000007a0  03 00 a0 01 f0 80 bd 08  ac 50 a0 e1 0c 30 25 e0  |.........P...0%.|
000007b0  55 30 03 e2 55 00 53 e3  28 00 00 1a a0 30 20 e0  |U0..U.S.(....0 .|
000007c0  55 30 03 e2 55 00 53 e3  24 00 00 1a a6 30 26 e0  |U0..U.S.$....0&.|
000007d0  54 30 03 e2 54 00 53 e3  20 00 00 1a 80 20 a0 e1  |T0..T.S. .... ..|
000007e0  00 31 a0 e1 20 30 03 e2  40 20 02 e2 03 20 82 e1  |.1.. [email protected] ... ..|
000007f0  80 10 04 e2 80 31 a0 e1  01 20 82 e1 10 30 03 e2  |.....1... ...0..|
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  |................|
00000840

本文的样本 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~....{[email protected]|
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~....{[email protected]|
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,因为 2^m=2^13=8192,大于 k=4096,所以产生 BCH 编码的 ECC 的奇偶校验位的大小为 mt=13*4=52 位是合适的,没有任何差异。还记得上一部分从 NAND 转储分析中发现的 ECC 大小吗? 是的,它是 60 位(8 个字节扣除最后 4 位的零)。

好了,无聊的东西现在变得有趣了。让我们看看通过这个小线索可以推断出什么。要保护的数据大小是 512 字节,也就是 4096 比特。m 至少应该是 13,所以 2^m=2^13=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:

该死的,确实是同样的事情。似乎 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 中,已经完全提取成功了。

参考文献:

源地址:Lian Security

如若转载,请注明原文地址


文章来源: https://www.4hou.com/posts/MKom
如有侵权请联系:admin#unsafe.sh