深入理解dm-verity机制

近期做的一个项目,由于客户对安全性要求比较高,涉及到文件系统校验的问题,起初是在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,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层,如下图:

深入理解dm-verity机制_第1张图片

第1层的hash值由下面一层的hash值计算得到,除了第0层,其他的层hash值都不对应物理上block的hash值,它们存在的意义只是为了构建hash链,防止hash篡改,这样第0层的hash值改变了的话,上层对应的hash值也需要修改,也就是说根hash也需要修改,所以只需要一种机制能保证root-hash不被篡改就行了。

Android中采用的方法是算root-hash的签名,verity-table中保存了root-hash,对verity-table进行签名,它们的存储分布如下:

深入理解dm-verity机制_第2张图片

在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中。

dm-verity异常处理

dm-verity签名校验失败后会怎么做呢?下面是Android的做法:

这里写图片描述

在metadata分区中会记录dm-verity的状态,提示是否挂载,同时在dm-verity设备创建时也会指定mode,dm-verity在内核中块hash校验失败后不同的mode表现的行为不一样。

// Verity modes
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
};
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烧进去即可。

最终实现的效果如下:

深入理解dm-verity机制_第3张图片

你可能感兴趣的:(安全,Android)