一、Device Mapper简介
dm-verity是内核子系统的Device Mapper中的一个子模块,所以在介绍dm-verity之前先要介绍一下Device Mapper的基础知识。Device Mapper为Linux内核提供了一个从逻辑设备到物理设备的映射框架,通过它,用户可以定制资源的管理策略。当前Linux中的逻辑卷管理器如LVM2(Linux Volume Manager 2)、EVMS(Enterprise Volume Mageagement System)、dmraid等都是基于该机制实现的。
Device Mapper有三个重要的概念:映射设备(Mapped Device)、映射表、目标设备(Target Device);映射设备是一个逻辑块设备,用户可以像使用其他块设备那样使用映射设备。映射设备通过映射表描述的映射关系和目标设备建立映射。对映射设备的读写操作最终要映射成对目标设备的操作。而目标设备本身不一定是一个实际的物理设备,它可以是另一个映射设备,如此反复循环,理论上可以无限迭代下去。映射关系本质上就是表明映射设备中的地址对应到哪个目标设备的哪个地址。
Device Mapper是一个灵活的架构,映射设备映射一个或多个目标设备,每个目标设备属于一个类型,类型不同,对I/O处理不同,构造目标设备的方法也不同。映射设备可以映射多个不同类型的目标设备。
Dm-verity规定只能有两个目标设备,一个是数据设备(Data Device),另一个是哈希设备(Hash Device);
Device Mapper可参考IBM博客:https://www.ibm.com/developerworks/cn/linux/l-devmapper/
二、dm-verity简介
dm-verity是Device mapper架构下的一种目标设备类型,通过它来保障设备或设备分区的完整性,它的典型架构是如图一。
dm-verity类型的设备需要两个底层设备,一个是数据设备,顾名思义是用来存储数据,实际上就是要保障完整性的设备,另一个是哈希设备,用来存储哈希值,在校验数据设备完整性时需要。图中映射设备和目标设备是一对一关系,对映射设备的读操作被映射成对目标设备的读操作,在目标设备中,dm-verity又将读操作映射为数据设备(Data Device)的读操作。但是在读操作的结束处,dm-verity加了一个额外的校验操作,对读到的数据计算一个hash值,用这个哈希值和存储在哈希设备(Hash Device)中的值做比较,如果不同,则本次读操作被标记为错误。
假设数据设备和哈希设备中每块大小均为4KB,再假设使用hash算法SHA256,即每块数据的哈希值为32B(256bits),则哈希设备中的每块(4KB)存储有4096/32=128个哈希值。所以在layer0中一个哈希设备的块对应数据设备的128个块。到这里似乎完整了,数据设备中存储数据,哈希设备存储哈希值。在读取数据时,dm-verity还要防备哈希设备中存储的哈希值被篡改的情况。所以要加上layer1,在layer1中的每块数据对应layer0的128个块,layer1中的数据就是对layer0中的数据(hash设备和数据设备中的数据)计算hash值,如果layer1中只有一块,那么就此停止,否则继续增加layer,直到layer n只有一块。最后对layer n再计算hash值,称这个hash值为root hash。这个root hash就可以反应数据设备和hash设备的变化。通过验证root hash 就可以校验数据是否被篡改。
(图一) (图二)
三、dm-verity后续
1. dm-verity是什么 ?
它是dm(device mapper)的一个target,是一个虚拟块设备,专门用于文件系统的校验。fs在挂载的时候直接指定 dm-verity 设备,也就是fs直接交互的设备是 dm-verity,dm-verity 调用真正的块驱动去读取对应的块,并计算hash值和hash-tree中对应的hash值进行比较,如果相等,则说明块没有被篡改,返回块数据给fs,如果不相等,则说明块被篡改,根据mode是返回EIO,或者直接重启。
fs | dm-verity | block driver | block device
首先通过ioctl去crt(create)一个dm-verity设备,通过传入参数指定这个创建的dm-verity设备的一些特性,传入的参数包括verity-table,verity-table的内容如下:
def build_verity_table(block_device, data_blocks, root_hash, salt): table = "1 %s %s %s %s %s %s sha256 %s %s" table %= ( block_device, block_device, BLOCK_SIZE, BLOCK_SIZE, data_blocks, data_blocks + (METADATA_SIZE / BLOCK_SIZE), root_hash, salt) 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,因为它比较简单。
2. dm-verity的工作原理
squashfs需要读取某个块时,调用dm-verity读取对应的块,dm-verity根据verity-table中block_device,调用block_device读取对应的块,读取到块的内容后dm-verity会算出块的sha256,然后跟verity-hash-tree中相对应的hash值进行比较,如果相等,则说明该块没有被修改过,一切正常。
3. dm-verify支持所有的文件系统的原因
dm-verity跟文件系统是无关的,只要文件系统是工作在块设备之上的,所以ubifs是不可以的,工作在块设备之上的文件系统都是可以的,dm-verity是对逻辑块校验hash值,产生hash-tree的时候也是根据文件系统镜像来产生的(然后除ubifs之外,不存在逻辑块的概念,但是可以类似将它看出逻辑块直接等于物理块),至于逻辑块到物理块直接是怎样映射,dm-verity根本就不需要关心。
4. dm-verity比较快的原因
因为dm-verity并不需要在挂载前对所有的块进行校验,而是在使用的过程中用到哪个块就校验哪个块的hash值,这样对于像android一个分区几个G来说优势就显得更加明显了。
5.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值也需要修改,也就是说root-hash也需要修改,所以只需要一种机制能保证root-hash不被篡改就行了。
6. verity-table的校验
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的位置?
这是通过读取文件系统的超级块(Superblock,简称SB),里面记录了文件系统的大小,verity-table紧挨着文件系统镜像之后。
(3) 签名的key存放在哪里?
这里指的是public key,上面说的这些只是Android的一套,自己实现的话没必要完全按照它的来,比如说verity-table和hash-tree没有必要放在分区中,可以放在ramdisk中,签名和校验RSA2014可以自己实现,public key存放的位置可以自己决定,如放在ramdisk中或放在OTP中。
7. dm-verity异常处理
dm-verity签名校验失败后会怎么做呢?下面是Android的做法:
在metadata分区中会记录dm-verity的状态,提示是否挂载,同时在dm-verity设备创建时也会指定mode,dm-verity在内核中块hash校验失败后不同的mode表现的行为不一样。mode如下:
enum 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 };
指定mode后,kernel中碰到校验不过的块的处理:
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; }
参考:
深入理解dm-verity机制:https://blog.csdn.net/whuzm08/article/details/85272419
dm-verity:https://www.cnblogs.com/linhaostudy/archive/2018/01/17/7418289.html
android dm-verity 功能:https://blog.csdn.net/ee230/article/details/73348344?locationNum=6&fps=1 (有校验步骤,比较好)