近期做的一个项目,由于客户对安全性要求比较高,涉及到文件系统校验的问题,起初是在ramdisk中挂载rootfs后对所有重要的文件检查sha256,但是随着rootfs的逐步增大,发现校验花费的时间太长,竟然达到十几秒,于是就想改用一种方案,首先想到的是整个rootfs校验后在挂载,这样肯定是比一个文件一个文件校验要来得快些,但是项目中用的是nandflash,并不是EMMC,所以文件系统只能采用支持坏块管理的ubifs,于是这里就存在问题了,ubi层负责逻辑块到物理块的映射,也就是说在物理存储上块不一定是逻辑连续的,这样在ubifs还没有挂载之前读取整个镜像然后整体校验肯定是行不通的,一个解决办法是读取ubi卷设备而不是mtd设备,这样的话是可行的,但是一种更好的办法是采用dm-verity,但是这个是Linux项目,并不是Android项目,通过对dm-verity移植,这里记录一下对dm-verity的理解。
(资料图片)
dm-verity是什么?
它是dm(device mapper)的一个target,是一个虚拟块设备,专门用于文件系统的校验
+------------------------------+
| fs |
+------------------------------+
|
↓
+-------------------------------+
| dm-verity |
+-------------------------------+
|
↓
+-------------------------------+
| block driver |
+--------------------------------+
|
↓
+-------------------------------+
| block device |
+-------------------------------+
fs在挂载的时候直接指定dm-verity设备,也就是fs直接交互的设备是dm-verity,dm-verity调用真正的块驱动去读取对应的块,并计算hash值和hash-tree中对应的hash值进行比较,如果相等,则说明块没有被篡改,返回块数据给fs,如果不相等,则说明块被篡改,根据mode是返回EIO,或者直接重启。
首先通过ioctl去crt(create)一个dm-verity设备,通过传入参数指定这个创建的dm-verity设备的一些特性,传入的参数包括verity-table,当然也可以使用命令直接创建:
Set up a device: # dmsetup create vroot --readonly --table \ "0 2097152 verity 1 /dev/sda1 /dev/sda2 4096 4096 262144 1 sha256 "\ "4392712ba01368efdf14b05c76f9e4df0d53664630b5d48632ed17a137f39076 "\ "1234000000000000000000000000000000000000000000000000000000000000"
verity-table的内容如下:
40 def build_verity_table(block_device, data_blocks, root_hash, salt):41 table = "1 %s %s %s %s %s %s sha256 %s %s"42 table %= ( block_device,43 block_device,44 BLOCK_SIZE,45 BLOCK_SIZE,46 data_blocks,47 data_blocks + (METADATA_SIZE / BLOCK_SIZE),48 root_hash,49 salt)50 return table
block_device描述了该dm-verity设备对应了那个底层的块设备,第二个block_device指定了hash-tree存在于哪个块设备上,对于我这个项目就是/dev/ubiblock0_0,BLOCK_SIZE描述了多大一个块对应一个hash,一般都4k, data_blocks描述了有多少个4k的块,data_blocks + (METADATA_SIZE / BLOCK_SIZE)表示hash-tree在对应块设备上的偏移,由此来找到hash-tree,root_hash为hash-tree的根hash。
dm-verity工作在块设备之上,所以这里是/dev/ubiblock0_0,于是就不能再用ubifs 了,因为ubifs工作在卷设备之上,而/dev/ubi0_0是一个字符设备,所以只能采用工作在块设备之上的文件系统,我这里采用了squashfs,因为它比较简单。
dm-verity的工作原理
通过前面的描述,很容易理解dm-verity的工作过程,就拿我这个项目来说,squashfs需要读取某个块时,调用dm-verity读取对应的块,dm-verity根据verity-table中block_device,调用block_device读取对应的块,读取到块的内容后dm-verity会算出块的sha256,然后跟verity-hash-tree中相对应的hash值进行比较,如果相等,则说明该块没有被修改过,一切正常。
为何dm-verify支持所有的文件系统?
该项目在选用dm-verity之前,我一直都在怀疑dm-verity是否支持ubifs,通过前面的描述,如果你对dm-verity的工作原理足够理解的话,你就会发现,dm-verity跟文件系统是无关的,只要文件系统是工作在块设备之上的,所以ubifs是不可以的,工作在块设备之上的文件系统都是可以的,dm-verity是对逻辑块校验hash值,产生hash-tree的时候也是根据文件系统镜像来产生的(然后除ubifs之外,不存在逻辑块的概念,但是可以类似将它看出逻辑块直接等于物理块),至于逻辑块到物理块直接是怎样映射,dm-verity根本就不需要关心。
dm-verity为何这么快?
了解了原理之后这个就很好回答了,因为dm-verity并不需要在挂载前对所有的块进行校验,而是在使用的过程中用到哪个块就校验哪个块的hash值,这样对于像android一个分区几个G来说优势就显得更加明显了。
dm-verity是如何保证安全的?
前面说过每个block都在hash-tree中记录了对应的hash值,这样就能防止别人篡改block的内容了,但是如果黑客把block改了之后,重新计算hash把hash-tree中对应的hash值也改了呢,这样就能神不知鬼不觉了,所以必须要有一种机制防止hash-tree被篡改,hash-tree是这样一种结构,所有的block对应的hash值放在最底层,也就是第0层,如下图:
第1层的hash值由下面一层的hash值计算得到,除了第0层,其他的层hash值都不对应物理上block的hash值,它们存在的意义只是为了构建hash链,防止hash篡改,这样第0层的hash值改变了的话,上层对应的hash值也需要修改,也就是说根hash也需要修改,所以只需要一种机制能保证root-hash不被篡改就行了。
Android中采用的方法是算root-hash的签名,verity-table中保存了root-hash,对verity-table进行签名,它们的存储分布如下:
在Android中,系统进入ramdisk后,由/system/core/fs_mgr/ 负责dm-verity设备的创建,verity-table的校验,这里涉及到的一些知识是:
1.如何知道哪些分区需要校验?
fs_mgr通过读取fstab文件,其中记录了哪些分区需要校验
2.如何知道需要校验的分区中verity-table的位置?
这是用户空间(/system/core/fs_mgr/ )的工作,android的做法是通过读取文件系统的超级块(Superblock,简称SB),里面记录了文件系统的大小,verity-table紧挨着文件系统镜像之后
3.签名的key存放在哪里?
这里指的是public key
verity-table-metadata主要是为了校验verity-table的合法性,android的格式为:
def build_metadata_block(verity_table, signature): table_len = len(verity_table) block = struct.pack("II256sI", MAGIC_NUMBER, VERSION, signature, table_len) block += verity_table block = block.ljust(METADATA_SIZE, "\x00") return block
verity-table的校验是在用户空间(/system/core/fs_mgr/ )中完成的,校验合法之后会将verity-table传给kernel使用。
上面说的这些只是Android的一套,自己实现的话没必要完全按照它的来,比如说verity-table和hash-tree没有必要放在分区中,可以放在ramdisk中,签名和校验RSA2014可以自己实现,public key存放的位置可以自己决定,如放在ramdisk中或放在OTP中。
dm-verity异常处理
dm-verity签名校验失败后会怎么做呢?下面是Android的做法:
在metadata分区中会记录dm-verity的状态,提示是否挂载,同时在dm-verity设备创建时也会指定mode,dm-verity在内核中块hash校验失败后不同的mode表现的行为不一样。
// Verity modesenum verity_mode { VERITY_MODE_EIO = 0, VERITY_MODE_LOGGING = 1, VERITY_MODE_RESTART = 2, VERITY_MODE_LAST = VERITY_MODE_RESTART, VERITY_MODE_DEFAULT = VERITY_MODE_RESTART};static int load_verity_table(struct dm_ioctl *io, char *name, uint64_t device_size, int fd, char *table,int mode){ ... if (mode == VERITY_MODE_EIO) { //对于比较老的内核dm-verity驱动,是不支持mode的,当block hash校验不过时总是cause an I/O error for corrupted blocks // allow operation with older dm-verity drivers that are unaware // of the mode parameter by omitting it; this also means that we // cannot use logging mode with these drivers, they always cause // an I/O error for corrupted blocks strcpy(verity_params, table); } else if (snprintf(verity_params, bufsize, "%s %d", table, mode) < 0) { return -1; } ... ioctl(fd, DM_TABLE_LOAD, io);}
指定mode后,kernel中碰到校验不过的块的处理:
/* * Handle verification errors. */static int verity_handle_err(struct dm_verity *v, enum verity_block_type type, unsigned long long block){ ... out: if (v->mode == DM_VERITY_MODE_LOGGING) return 0; if (v->mode == DM_VERITY_MODE_RESTART) kernel_restart("dm-verity device corrupted"); return 1;}
记录一下移植的过程中踩过的坑:
在移植的过程中发现Android6.0是有bug的,在产生hash-tree的时候:
image_size = os.stat(out_file).st_size
由于img是sparse过后的,所以这里的大小肯定是不对的,正确的做法应该是先unsparse,然后再计算大小。
另外这里采用的是ubiblock:
ubiblock --create /dev/ubi0_0
mtd--->ubi------>ubi vol----->ubiblock
另外还可以采用gluebi:
mtd---->ubi---->ubi vol--->mtd--->mtdblock
ubiblock比较简单,缺点是只读,在挂载时必须指定为只读:
mount -t squashfs /dev/ubiblock0_0 /mnt -o ro
生成烧录镜像的过程:
rootfs dir---------mksquash------------>rootfs.squashfs-----------ubinize-------------->rootfs.ubi
把rootfs.ubi烧进去即可。
最终实现的效果如下:
1月份,乌鲁木齐局集团公司运输煤炭122661万吨,其中疆煤外运48261万吨,同比分别增长195%、395%。 乌鲁木齐局集团公司建立健全两级保更多
2023-02-16 10:02:01近日,记者从人民银行贵阳中心支行2023年一季度新闻通气会上获悉,截至2022年末,贵州对碳减排、煤炭清洁利用、交通物流、科技创新、设备更多
2023-02-16 09:54:3631个省区市2023年政府工作报告近期陆续出炉,均为能源产业转型发展绘出了新路线图。记者梳理发现,传统能源大省积极转型的同时加强保供,更多
2023-02-16 09:54:16当前国际能源供需形势严峻复杂,能源供应持续紧张、价格大幅波动,我国经济深度融入世界经济,国内能源供应必然受到国际能源供需变化和价更多
2023-02-16 10:09:29据CCTD了解,近期煤炭市场价格持续走跌,5000K报价一度跌破800元吨,不仅远低于铁路发运到港成本,且较进口煤的价差也在快速收窄。 进更多
2023-02-16 09:59:02记者从山西省发改委获悉,2022年,山西省用电量平稳增长,全省全社会用电量27208亿千瓦时,比上年增长43%。 分产业看,2022年,第一产更多
2023-02-16 08:59:141月,国家能源集团新疆能源有限责任公司生产煤炭740万吨,超计划20万吨,同比增长156%;自产煤销量736万吨,超计划16万吨,同比增长155%。更多
2023-02-16 09:06:18记者从国家矿山安监局山西局近日召开的2023年工作会议上了解到,今年山西增产保供压力持续加大及各种风险叠加,全省矿山安监系统将紧紧围更多
2023-02-16 09:16:27国家统计局2月14日公布的数据显示,2月上旬全国煤炭价格全面下跌。各煤种具体价格变化情况如下: 无烟煤(洗中块,挥发份≤8%)价格1更多
2023-02-15 10:05:212022年,在多方面因素的综合作用下,国际市场煤炭需求旺盛,各大关键煤炭价格指数一度达到近年来的最高点。业内人士与专业机构预测,与石更多
2023-02-15 10:00:10