使用 OOB 数据提取 NAND 闪存转储

逆向工程 二元分析 固件 手臂 闪光 倾倒
2021-06-15 05:18:41

我正在尝试提取旧随身听播放器的 NAND 闪存转储。转储是由朋友完成的,不幸的是无法重做,因为芯片已损坏。转储应该只是没有 OOB 的用户数据,但似乎出了点问题,OOB 仍然存在。我尝试了各种删除 OOB 的方法,我可以获得一些看起来合理的数据,但很多事情仍然没有解决,我无法提取正确的文件。

这是我尝试过的。

仅通过直观地查看十六进制转储,就会发现每 0x200 个字节有一个 16 字节的行以“01 00 00 00”开头,看起来不合适。 十六进制转储 我制作了一个简短的脚本来在每个 0x200 之后删除 16 个字节:

import sys
inf = open(sys.argv[1] ,"rb")
of = open(sys.argv[2],"wb")
clen = 0x200
c = inf.read(clen)
off = 0
while c:
  off = inf.tell()
  oob = inf.read(0x10)
  if oob[:4]!="\x01\x00\x00\x00":
   print " bad OOB at %08X?" % off, oob.encode('hex')
   break
  of.write(c)
  if (off&0x1000) ==0:
   print "%08X" % off
  c = inf.read(clen)


inf.close()
of.close()
print "done."

添加的健全性检查很快就会触发:

00000200
00000410
00000620
00000830
00000A40
00000C50
00000E60
 bad OOB at 00001070? 042080e4fbffffea10009fe530ff2fe1
done.

事实上,以 01 00 00 00 开头的下一行是 1240,而不是 1070。我试图解释它并重新启动,但后来我遇到了类似的问题。所以我想知道我是否遗漏了什么。完整文件是 4GB,有点重,所以这里有一些切出的块:

  1. 转储的开始(初始引导加载程序?)。偏移量 0
  2. 启动 U-boot 引导加载程序。偏移量 0x104000
  3. 内核映像的开始。偏移量 0x186000

如果您想查看整个转储,我在我们的聊天中分享了一个链接

硬件细节:设备为 Sony Walkman NWZ-A829。闪存芯片很可能是TH58NVG6D1DTG20。CPU 是 NEC MP201 (ARMv5le)。

GPL 源(U-Boot/Linux 内核)可在此处获得:http : //oss.sony.net/Products/Linux/Audio/NWZ-S715.html

最终目标是找出固件更新加密并为设备生成自定义固件。

3个回答

所以,最后我自己想通了。我会试着描述这个过程。

首先,关于 NAND 的一些背景知识:它被组织成页面,这些页面被分组为块。您可以一次读取或写入一页,但擦除(将所有位变为 1(将字节变为 FF))一次只能完成一个块(写入只能将位从 1 更改为 0,而不能将其他位更改)方法,因此要写入新数据,通常必须先擦除该块)。通常,芯片每页还有一些额外的存储空间来存储 ECC(纠错码)和/或任意“备用”数据(也称为 OOB:带外),这些数据不被视为有用存储的一部分芯片,但仍然可以使用低级函数进行读写。

在浏览十六进制转储时,我在一堆 FF 之后遇到了这个区域:

0000081FE0: FF FF FF FF FF FF FF FF │ FF FF FF FF FF FF FF FF          
0000081FF0: FF FF FF FF FF FF FF FF │ FF FF FF FF FF FF FF FF          
0000082000: 6C 4F 41 64 00 00 00 00 │ 00 00 00 00 00 00 00 00  lOAd
0000082010: 00 00 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00
0000082020: 00 00 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00
0000082030: 00 00 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00
0000082040: 00 00 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00
0000082050: 00 00 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00

它在每个 0x200 字节之后有通常的 OOB 外观的 16 字节块,但随后有所不同:

83040: 70 41 52 74 18 00 00 00 │ 48 00 00 00 90 00 00 00  pARt↑   H   
83050: A0 00 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00   
83060: 00 00 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00
....
84080: 75 42 54 74 20 00 00 00 │ 02 00 00 00 00 00 00 00  uBTt    ☻
84090: 00 00 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00
840A0: 00 00 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00
840B0: 00 00 00 00 00 00 00 00 │ 00 00 00 00 00 00 00 00

这是什么?听起来像U-Boot。让我们从 Sony 的网站上查看 U-boot 源...和宾果游戏!

 /* for Loader Management */
 #define MNG_SIG_LOADER         0x64414F6C      /* "lOAd" */
 #define TBL_SIG_PART           0x74524170      /* "pARt" */
 #define TBL_SIG_UBOOT          0x74544275      /* "uBTt" */
 #define TBL_SIG_FUKRNL         0x744B5566      /* "fUKt" */
 #define TBL_SIG_FUINRD         0x74525566      /* "fURt" */
 #define TBL_SIG_BAD                0x74444162      /* "bADt" */

 #define    MNG_SIG_KRNL            0x6C4E526B      /* "kRNl" */
 #define TBL_SIG_KRNL           0x744C4E6B      /* "kNLt" */
 #define TBL_SIG_INRD           0x74445269      /* "iRDt" */

(from icx1087_nand.c) 所以,显然这些块与“加载器管理”有关……看看我们遇到的这个代码块的引用:

/* read management table */
for( rty = 0; rty < NAND_READ_RETRY; rty++ ) {
    if( nand_search_tbl(NAND_BLK_LOADER_START, NAND_BLK_LOADER_LMT,
                        MNG_SIG_LOADER, TBL_SIG_PART, (void *)&ptbl, 0) ) {
            continue;
    }
    /* success to read Kernel information if reach here */
    break;
}

因此,它MNG_SIG_LOADER在 NAND 块 NAND_BLK_LOADER_START 中寻找(lOAd)。而且价值是...

#define NAND_BLK_LOADER_START   1

所以看起来 82000 是 NAND 块 1 的开始。如果我们进一步检查代码,我们可以看到它正在这个块的页面寻找其他签名(例如TBL_SIG_PARTTBL_SIG_FUKRNL让我们看看:

  • MNG_SIG_LOADER: 82000 (block 1 page 0?)

  • TBL_SIG_PART:83040(块 1 第 1 页?)

  • TBL_SIG_UBOOT:84080(块 1 第 2 页?)

83040-82000=1040 和 84080-83040=1040 也是!所以看起来转储中的页面大小是 1040. 和 1040*128 =82000 这意味着我们每个块有 128 个页面。实际数据可能是 0x1000 字节,0x40 是“备用”数据。所以,这解释了为什么我在每四个 0x200 扇区之后去同步:每页有 8 个扇区,但只有 0x40 字节的 OOB 数据。

我不知道为什么转储最终会有如此奇怪的结构;我认为这是因为用于转储的软件(IIRC,它是Matt Oh 的DumpFlash)在许多地方假设 512 字节的页面,并且不知何故 OOB 数据最终以块的形式传播,而不是在每个页面的末尾分组。可能它也与数据表中类似(但不相同)的东芝芯片的这段话有关

每 512Byte 需要 8 位 ECC

(但没有解释应该如何做)

无论如何,一旦我计算出页面大小的 0x1040 和每块 128 页的幻数,修复我的脚本就不难了:

NAND_PAGE_SIZE  = 0x1000
NAND_PAGE_BLK   = 128
NAND_SECTOR_PER_PAGE = 8
NAND_SECTOR_SIZE = NAND_PAGE_SIZE/NAND_SECTOR_PER_PAGE
OOBLEN = 16

def page2off(pgno):
  return pgno*0x1040 # (NAND_PAGE_SIZE + NAND_SECTOR_PER_PAGE*OOBLEN)

def read_page(inf, blkno, pgno):
 blklen = page2off(NAND_PAGE_BLK)
 fileoff = blklen*blkno + page2off(pgno)
 print "reading block %d page %d: offset %08X" % (blkno, pgno, fileoff)
 inf.seek(fileoff, 0)
 block = inf.read(blklen)
 s = ""
 soff =0
 for i  in range(NAND_SECTOR_PER_PAGE):
  print "sector %d offset %08X" % (i, soff)
  s+= block[soff:soff+NAND_SECTOR_SIZE]
  soff += NAND_SECTOR_SIZE+OOBLEN
 return s

有了这个,我可以用内核和 ramdisk 转储闪存的一部分并提取二进制文件。

我是 Rockbox 开源项目的开发人员。我们通过本质上做同样的事情(转储芯片)来解决固件升级的加密问题。

upgtool可以从 UPG 工具中提取固件,可以使用 cabextract 从固件升级安装程序中提取固件并查看文件 Data/Device/NW_WM_FW.UPG 它需要一个可以暴力破解的加密密钥(upgtool 可以这样做,如果询问 [3]),或者您可以使用scsitool向设备询问密钥我们有很多 Sony 设备的端口,NW-A820 将不被支持,因为它有一个非常旧的内核,但我们逆向工程的大部分信息可能仍然适用。

[3] upgtool的数据库里有A820的key,是蛮力找到的

只是一个快速的答案,作为未来 NAND 问题的提示。

在读取 NAND 闪存时(不关心 bga、tsop ..),并非所有程序员都会明确转储,通常它包括您提到的 OOB 数据的虚拟块。

由于 NAND 转储应该是 8 的倍数,例如 64、128、256MB……如果您转储的文件大于典型大小,例如 132MB,则必须分析二进制文件以删除虚拟块并找出模式,然后你可以开始玩二进制。