Introduction to FlashROM

半年多没写文章了,固件提取系列已经写到第11部,这一个我觉得没那么难,可以公开。

FlashROM是一款开源的Flash固件提取项目,支持多种硬件平台,SPI Flash和Parallel Flash。 这一次遇到了一款NAND的SPI Flash,型号为IS38SML01G1,车规级存储芯片。起初以为SPI Flash都是NOR芯片,于是没有看手册,飞线用编程器读取,通常的编程器无法读取,又使用RT809H读取,也失败。于是查看手册才发现是NAND Flash,目前只有REVELPROG-IS支持读写。 一开始想使用FT2232HL来读取,发现官方对该芯片的文档支持不是很好,于是使用树莓派3B。

flashchips.c文件保存了各个芯片的配置信息,设计的非常好,扩展性很强。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
.vendor = Vendor name
.name = Chip name
.bustype = Supported flash bus types (Parallel, LPC...)
.manufacture_id = Manufacturer chip ID
.model_id = Model chip ID
.total_size = Total size in (binary) kbytes
.page_size = Page or eraseblock(?) size in bytes
.tested = Test status
.probe = Probe function
.probe_timing = Probe function delay
.block_erasers[] = Array of erase layouts and erase functions
{
.eraseblocks[] = Array of { blocksize, blockcount }
.block_erase = Block erase function
}
.printlock = Chip lock status function
.unlock = Chip unlock function
.write = Chip write function
.read = Chip read function
.voltage = Voltage range in millivolt
}

根据文档,加入了38SM的配置信息。 根据文档信息,一个NAND SPI Flash有1024个Block,每个Block有64Page,一个Page有2K+64字节,其中64是冗余区。 这里的Total Size是以KB为单位,因此不需要加上冗余区的大小。电平范围根据文档设置成2.7V~3.6V。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
.vendor = "ISSI",
.name = "IS38SML01G1",
.bustype = BUS_SPI,
.manufacture_id = ISSI_NAND_ID,
.model_id = ISSI_NAND_ID_SPI,
.total_size = 131072, /* kb */
.page_size = 2048, /* bytes, actual page size is 64 */
.tested = {.probe = OK, .read = OK, .erase = NA, .write = NA},
.probe = probe_spi_rdid5,
.probe_timing = TIMING_ZERO,
.block_erasers =
{
{
.eraseblocks = { {64 * 2048, 1024} },
.block_erase = spi_block_erase_d8,
}
},
.write = NULL,
.read = spi_read_issi,
.voltage = {2700, 3600},
},

要读取Flash的内容,需要实现芯片的初始化和读取功能。因此我们只要对probe和read添加指针。下图是各个Command的定义,包括Op Code字节,寻址用的字节,填充字节和下位机返回的数据字节。数据的传输格式是MSB。

command_set

初始化需要读取ID,首先把芯片ID定义写好。Mark Code 和 Device Code有用。剩下的Communication Code 0x7F7F7F也顺便写上去吧。

jedec_id

1
2
3
#define ISSI_NAND_ID		0xC8
#define ISSI_NAND_ID_SPI 0x21
#define ISSI_38SML01G1 0x7F7F7F

FlashROM内置的probe_spi_rdid4函数用于读取JEDEC ID,即发送0x9F,但是由于该芯片发送Read ID指令需要填充一个字节,正常情况使用probe_spi_rdid读取第一个字节会变成0x00。ISSI文档的时序图简直AV画质。

read_id_timing

因此需要新建一个函数,把它命名为probe_spi_rdid5。另外再加上读取的函数spi_read_issi,它们都要在chipdrivers.h里声明

1
2
int probe_spi_rdid5(struct flashctx *flash);
int spi_read_issi(struct flashctx *flash, uint8_t *buf, unsigned int start, unsigned int len);

Read ID函数,如果第一位是0x00,那么跳过这一位继续读取。也可以发送ID的时候填充一位,就不用判断MISO的数据了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
int probe_spi_rdid5(struct flashctx *flash)
{
const struct flashchip *chip = flash->chip;
unsigned char readarr[6];
uint32_t id1;
uint32_t id2;
uint32_t bytes = 6;

if (spi_rdid(flash, readarr, bytes)) {
return 0;
}

if (!oddparity(readarr[0]))
msg_cdbg("RDID byte 0 parity violation. ");

/* Check if this is a continuation vendor ID.
* FIXME: Handle continuation device IDs.
*/

if (readarr[0] == 0x00) {
if (!oddparity(readarr[1]))
msg_cdbg("RDID byte 1 parity violation. ");
id1 = (readarr[0] << 8) | readarr[1];
id2 = readarr[2];
} else {
id1 = readarr[0];
id2 = (readarr[1] << 8) | readarr[2];
}

msg_cdbg("%s: id1 0x%02x, id2 0x%02x\n", __func__, id1, id2);

if (id1 == chip->manufacture_id && id2 == chip->model_id)
return 1;

/* Test if this is a pure vendor match. */
if (id1 == chip->manufacture_id && GENERIC_DEVICE_ID == chip->model_id)
return 1;

/* Test if there is any vendor ID. */
if (GENERIC_MANUF_ID == chip->manufacture_id && id1 != 0xff && id1 != 0x00)
return 1;

return 0;
}

另外是读取函数,首先要知道芯片数据的读取过程。NAND主控先把NAND的数据放入Cache Memory,一次只能读一页,然后NAND主控再把数据从Cache读取出来输出到上位机。

blockdiagram

因此要先发送读取指令,告诉控制器要读取哪一页。在NAND数据传输到Cache的时候,不能做其他读写操作。此时状态寄存器应该处于繁忙状态。OIP==1。

在发送完页读取指令后,应该循环发送0x0F 0xC0直到OIP==0。

status_register

因此一次完整的读取流程的CMD如下:

0x13 页读取 0x0F 0xC0 轮询状态 0x03 缓存读取

页读取根据Command定义,寻址字节长度为3,其中有一字节为无效数据,所以最大地址为0xFFFF,换算成10进制长度为65536。1024 Blocks * 64 Pages = 65536 Pages。这里的填充数据暂时先理解为[7:0],寻址数据为[23:8]

page_read

缓存读取的寻址长度为2-bytes,填充字节为1-bytes + 4-bits,因此缓存的寻址长度为12-bits既4096,文档中规定的范围是0-2112,可以理解为2048-bytes + 64-bytes (OOB area)。

page_cache_read

下面是spi_read_issi的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
int spi_read_issi(struct flashctx *flash, uint8_t *buf, unsigned int start, unsigned int len)
{
uint8_t cmd[4];
uint8_t page_read_resp[1];
unsigned int ret = 0;
unsigned int buf_off = 0;
uint8_t cache_read_cmd[4];
uint8_t get_feature_cmd[2] = {0x0f, 0xc0};

for (unsigned int address_h = 0; address_h < 256; address_h++)
{
for (unsigned int address_l = 0; address_l < 256; address_l++)
{
cmd[0] = 0x13; /* page read cmd */
cmd[1] = 0x00; /* dummy byte */
cmd[3] = (uint8_t)address_h;
cmd[2] = (uint8_t)address_l;
ret = spi_send_command(flash, sizeof(cmd), 1, cmd, page_read_resp);
/* 7-0 bits: ECC_S1, ECC_S0, P_Fail, E_Fail, WEL3, OIP */
uint8_t status[1] = {0};
int get_feature_ret = 1;

{
internal_sleep(10);
get_feature_ret = spi_send_command(flash, sizeof(get_feature_cmd), sizeof(status), get_feature_cmd, status);
}while (get_feature_ret);
/* printf("\nStatus: 0x%X, get_feature_ret:%d\n", (unsigned int)status[0], get_feature_ret); */

cache_read_cmd[0] = 0x03; /* page read cmd */
cache_read_cmd[1] = 0x00;
cache_read_cmd[2] = 0x00;
cache_read_cmd[3] = 0x00; /* dummy byte */

if (status[0] == 0)
{
int cache_read_ret = spi_send_command(flash, sizeof(cache_read_cmd), 2048, cache_read_cmd, buf + 2048 * buf_off);
ret = cache_read_ret;
} else {
printf("device busy. timeout\n");
ret = spi_send_command(flash, sizeof(get_feature_cmd), sizeof(status), get_feature_cmd, status);
}
/* Send Read */
unsigned int *buf_addr = (unsigned int *)((unsigned int)buf + 2048 * buf_off);

if (buf_addr[0] != 0xffffffff){
printf("buf_off:%d, address: 0x%x%x\nbuf_addr: 0x%X\ndata:\n", buf_off, (int) cmd[2], (int)cmd[1], (unsigned int)buf_addr);
/*int* = 4* int8 */
for (int b = 0; b < 512; b++)
{
printf("%08x", buf_addr[b]);
}
printf("\n");
}
// printf("\n");
if (ret){
printf("reading err");
break;
}

buf_off++;
}
}
return ret;
}

飞线

首先使用夹具,热风枪400℃底部加热12秒

target_device

将芯片焊在转接板上面

sop8

一开始没有考虑到WSON封装的底部会导致短路,于是重新飞线

wson8

jumping1

jumping2

接至树莓派3B reading_by_rpi

接线方式如下,HOLD要接在VCC上。

RPi header SPI flash
25 GND
24 /CS
23 SCK
21 DO
19 DI
17 VCC 3.3V (+ /HOLD, /WP)

开启SPI模拟

1
2
vi /boot/config.txt
dtparam=spi=on

加载内核模块

1
2
3
# If that fails you may wanna try the older spi_bcm2708 module instead
sudo modprobe spi_bcm2835
sudo modprobe spidev

1
flashrom -p linux_spi:dev=/dev/spidev0.0,spispeed=10000 -c IS38SML01G1 -V -r /tmp/is38_nooob.bin

reading

目标设备初始化分析

读取出来的128MB固件几乎全是0xFF,但是问了厂商,这个芯片存储了一些软件和配置信息。由于ISSI Flash文档没有写清楚页读取寻址的格式,寻址有3字节,高位字节和低位字节连续,所以有两种排列,再与填充字节组合,有4种地址格式,不知道哪一种是正确的。于是我把4种地址格式都读了一遍,只是数据的分布变化了,没有办法确定正确的分布。

flashdump1 flashdump2 flashdump3 flashdump4

完全按照文档编写的驱动,读取出来几乎全是0xFF,感觉是哪里出问题了,于是上逻辑分析仪。 只需要抓取三个通道的数据,MOSI,MISO,CLK。使用MSB,CPOL和CPHA都设为0。

spi_mode kingst_spiconf

设置成时钟上升沿触发,采样频率200MHz。 首先抓取树莓派读取Flash的的数据,符合文档规范,

读取JEDEC ID

rpi_spi_init

读取状态

rpi_status

当读取cache时,发完4个字节的数据,MISO就一直处于高电平,让人觉得是哪里不对。

rpi_spi_read_cache

再对目标设备抓取数据

logical_analyzer_probe

首先读取JEDEC ID,没问题,和树莓派的区别是只返回前两个ID。

target_init

页读取到缓存读取,首先发送读取页的指令,然后发送读取状态,等待控制器返回0,再发送缓存读取指令。这里就出问题了,后面的MISO也是输出0xFF,但是每四个字节输出,Master就会输入4个不知道什么含义的字节,循环交替。0x03可以确定是单路传输,并且在树莓派上,MOSI也不会产生数据,可以证明不是Slave发来的。

target_reading

至少证明我的代码没错,设备目前没有使用到存储芯片,

读取OOB导致的后果

一开始想把冗余区读取出来,所以响应buffer设成2112,最后在dump文件里看到了ELF的文件头,一开始以为目标设备使用了ELF文件。但是总觉得不对劲,车载网关怎么会使用Linux。

dump1

后来查看内存的地址分布,感觉是超出了堆的大小,越界读到了后面的库文件

maps

计算一下,第一段是实际堆的大小,第二段是实际读取的大小,第三段是正常读取的大小,所以不要去读冗余区。

address_calc

REVELPROG-IS开箱

时隔一个多月,购买了REVELPROG-IS,用于验证FlashROM读取的结果是否正确。

package

这是波兰制造,包装和编程器外观都还行

revelprog

电路设计很简单,一颗STM32F103的芯片,价格有点小贵,还好没被抄板子

STM32F103

验证读取结果

WSON8的烧录座还没到货,暂时飞线读取。

reading_with_prog

读取速度超慢,花了几分钟时间。不支持调整速率,还是FlashROM速度最快

reading_with_prog2

读取的数据和前面一致

result

寻址格式和第三种对应,具体顺序忘了,以后就用这个编程器读了。

flashdump5

勘误

在后续的研究中,发现这个文章存在一些坑。 Address_h 和 address_l 是在不知道读取规则的前提下假设的,因为文档没有写。两者长度都假设是8-bit。 在阅读了其他几款芯片的文档后,确定存在问题。 readpage_address 实际上这两个参数代表块地址与页地址,块数量1024,占据10位,页地址64,占据6位,加起来刚好16-bits。不同大小的芯片有不同的规则。 因此读取的结果会出现重复,加上有效数据刚好分布在存储设备的前面部分,剩余的都是无效数据,即使地址错误,读取出来的结果也不影响,最终呈现和编程器读取一致的结果。

另外给出的代码里,Status判断存在问题,不一定要为0才能读取数据。BBM LUT FULL(Look-Up Table)也可能为1,ECC Err Status可能是0x20。 status