在取证过程中经常遇到
纯软件形式的LUKS全盘加密在自解密时能够轻易被获取master key以实现全盘解密。。。
背景
全盘(分区)加密可用于保护个人隐私或产品产权保护等,它能够在牺牲较小性能情况下实现透明加密,即整个加解密过程对用户是无感的,当锁定磁盘后未授权用户将无法获取到该磁盘内文件系统及其内部所存储数据的任何信息。然而透明加密也就意味着在磁盘使用过程中密钥会一直存储于某个位置(一般都是内存),而基于内存的取证技术对这方面已经有了较深入的研究,已经存在大量工具可用于自动化内存密钥搜索,本篇以luks为例介绍其攻击与防御方式。
姿势
首先介绍下luks加密的原理,它涉及device mapper与文件系统,内核加密模块等:
文件系统
基本每种操作系统都用到了文件系统,它的作用是组织管理持久数据,也就是说没有文件系统磁盘上的文件依然可以被使用,但是无法直接使用文件系统的各种有用功能(如日志事务,访问控制,链接文件甚至查看文件名与文件修改日期等),Linux支持很多种文件系统,如etx3,fat32及btrfs等,用户层不必直接处理各种文件系统的差异因为有虚拟文件系统屏蔽底层区别,内核也不必将所有文件系统包含进去而是以内核模块的形式加载,由于device mapper机制,可以轻易实现文件系统堆叠,(所谓device mapper机制,其实很像树形管道,其底层是一个或多个真实设备,中间可以连接多个”过滤器”)如下图为在真实磁盘分区上上堆叠了LVM,RAID,dm-crypt与文件系统etx2,其中dm-crypt就是一个加解密层:
1 | Ext2 Filesystem <- top |
全盘加密
对文件加密可简单分为文件系统加密与全盘加密,前者加密的对象是文件系统中单一的文件,将会把文件作为一个整体进行加密,该方式不同文件可使用不同的密钥,并实现按需解密,缺点是未隐藏文件元信息(如文件大小,所有者,文件名等),另外可能需要空间存储额外的数据(如IV等)以及无法做到随机访问(若要访问加密后的文件的某一部分也必须将文件完整解密)。对于全盘加密,或者分区加密,它将使用透明加密的方式,只需要输入一次密码解密分区或磁盘,之后活动状态中的所有文件操作对用户来说都是透明的:
1 | 用户写->内核加密数据->写入磁盘 |
该方式的好处就是能够实现随机访问,透明操作且效率较高,能够隐藏所有数据(包括文件元数据等),而且能够轻易实现数据销毁(比如有核平技术将能轻易实现[9]),缺点就是无法只解密指定文件,整个系统只使用一个加密密钥等。在全盘加密中1.密文需要和明文占据同样大小空间,2.随机访问,所以加密的单元一般为扇区,加密用到的IV需要就地取材而不是随机生成。
LUKS
运行过程
LUKS是Linux Unified Key Setup的简称,它其实就是一种磁盘(全盘)加密格式,底层使用的是dm-crypt
进行加解密操作,前端使用cryptsetup
进行命令行控制,即它本身未实现任何密码算法,而是调用的内核提供的密码算法实现,它只是自定义了加密后分区格式,这种格式能存储加密与校验的元信息,方便密钥管理与使用等,它的基本使用方法如下:
- 将分区格式化为luks格式:
cryptsetup --cipher aes-xts-plain64 --keysize 512 --hash sha512 --iter-time 55000 luksFormat /dev/sda2
。这个过程会提示用户输入密码(该密码将会用于解密分区),输入密码后将根据指定的参数对分区进行格式化,即添加luks头等信息以及加密分区的剩余部分(用于存储数据的部分)。 - 打开加密后的分区:
cryptsetup luksOpen /dev/sda2 betamao
。此时会要求用户输入密码以解密分区,只有解密分区后才能对分区进行操作,解密后的分区将由dm映射为/dev/mapper/betamao
,之后的操作都将在该设备上进行,对其读操作会转换为都/dev/sda2
对应的加密数据,并返回其解密后的形式,写操作同理,这个操作对用户来说是透明的。 - 格式化分区:
mkfs.ext4 /dev/mapper/betamao
。解密映射后的分区是没有格式化的,为了更好的使用,一般需要根据需要将其格式化为操作系统能够识别的文件系统,比如这里为ext4,格式化后将使用文件系统的功能了(比如软连接)。 - 挂载分区:
mount /dev/mapper/betamao /home/beta/test
。像正常的添加磁盘操作一样,将分区挂载到合适的目录,就能对其进行文件读写操作了,此时可使用dmsetup table --target crypt --showkey
查看所有加密分区的信息,包括密钥。 - 锁定分区:
umount /home/beta/test;cryptsetup close /dev/sda2
或者直接关机,这样分区就被重新锁定了,只有拥有密码才能访问它里面的文件。
注:此处的分区不一定是磁盘上真实的分区,它可以是一个文件(虚拟分区),也可以是一个逻辑卷。
内部实现
AFsplitter
首先说下它,该技术为秘密共享技术,它将密钥分成任意多份,分别存储在不同的位置,只有拿到所有分割密钥才能恢复原始密钥并解密数据,最直观的方法如下图:
D为明文(或者此处的原始密钥),$S_1$到$S_{n-1}$为随机生成的数据,通过异或得到$S_n$,那么$S_1$到$S_n$的运算就能恢复出D,其中任意$S_x$受损都会影响D,这就是D的反取证分解(当然示例的分解无法扩散变化,实际使用的算法会用hash等方法来达到$S_n$某一位变化影响M的多位)。
该技术的特点是任意一份分割秘密受损将无法恢复明文,若要实现按比例恢复,可使用shamir门限方案等。
数据结构
luks的数据结构如下[4]:
- LUKS phdr头部存储的磁盘加密的元信息,比如上面命令指定的加密算法
aes
,摘要算法sha512
,迭代次数55000
以及随机盐等信息。 - KMn为密钥槽信息,luks不会直接使用用户提供的密码加密数据(因为这种密码熵太小且无法修改),它会随机生成一个主密钥用于加密磁盘数据,该主密钥将会由用户输入的密码进行PBKDF2等算法转换,用转换后的密码进行加密后存储,这样就可以实现多密钥管理,luks1提供最多8个密钥槽,允许用户最多设置8个不同的密码。
- bulk data由主密钥加密,综上一种典型的模式(TKS1)如下:
1
enc−data = encrypt(cipher−name, cipher−mode, key, original, original−length)
解密过程
用户输入passphrase经过PBKDF2(LUKS2新增bcrypt等算法支持)生成解密密钥:同时将分布在磁盘各处的分散密钥合并:1
2
3
4
5
6
7key = PBKDF2(
hash-spec, # 使用的PRF,存储在头部
passphrase, # 用户输入的解密磁盘的密码
salt, # 盐,初次随机生成后存储在luks分区的头部
iteration-count, # 迭代轮数,一般根据硬件性能自适应生成,从分区头部获得
derived−key−length # 生成的key的长度,从分区头部获得
)1
2
3
4
5
6split-material = AFsplit(
unsplit-material, # 未分割的长度
length, # 每份的长度
stripes # 分割的份数
)
unsplit-material= AFmerge(split-material, length, stripes)
D是原始数据(unsplit-material),H是hash函数(默认为sha1,由hash-spec指定),两者结果参与运算即可得到主密钥,主密钥是唯一的并且基本不会变化:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23read phdr from disk
check for correct LUKS_MAGIC and compatible version number
masterKeyLength = phdr.key−bytes
pwd = read password from user input
foreach active keysplot in phdr do as ks{
pwd−PBKDF2ed = PBKDF2(pwd, ks.salt, ks.iteration-count, masterKeyLength )
read from partion (encryptedKey , // destination
ks.key−material−offset , // s e c t o r number
masterKeyLength ∗ ks.stripes) // number of bytes
splitKey = decrypt(phdr.cipherSpec, // c i p h e r spec .
pwd−PBKDF2ed, // key
encryptedKey , // content
encrypted ) // content length
masterKeyCandidate = AFmerge (splitKey, masterkeyLength, ks.stripes)
MKCandidate−PBKDF2ed = PBKDF2( masterKeyCandidate ,
phdr.mk−digest−salt,
phdr.mk−digest−iter,
LUKS_DIGEST_SIZE)
if equal ( MKCandidate−PBKDF2ed, phdr.mk−digest) {
break loop and return masterKeyCandidate as correct master key
}
}
return error: password does not match any keysplot
对称加密模式
以AES为例,作为分组密码,它只能处理128比特大小的数据,实际使用时需要配合指定的操作模式,常见的操作模式有ECB,CBC,CTR,OFB,CFB等,而在磁盘加密上主要是使用的是CBC,LRW和XTS。其中大部分工作模式都需要额外的IV,随机IV必须被存储,这部适用于全盘加密,于是全盘加密上的IV将由数据位置等信息生成,分如下几种:
- plain:初始向量是扇区号的32位小端版本,必要时用零填充。
- plain64:初始向量是扇区号的64位小端版本,必要时用零填充。
- plain64be:初始向量是扇区号的64位大端版本,必要时用零填充。
- essiv:“加密扇区|盐初始向量”,使用salt作为密钥,使用密码对扇区号进行加密。盐是密码的散列。请注意,虽然密码算法始终与用于数据加密的算法相同,但其密钥大小取决于使用的散列算法。换句话说,虽然数据加密可以使用AES-128,但使用SHA256的ESSIV计算将使用AES-256来加密算扇区号。ESSIV将哈希算法作为选项,因此格式为essiv:hash,例如essiv:sha256。
- null:初始向量始终为零。提供与过时的loop_fish2设备的兼容性。
此处重点说明XTS-AES工作模式[19],它是XOR-Encrypt-XOR Tweakable Block Cipher with Ciphertext Stealing Advanced Encryption Standard的简称,被设计用于使用固定长度的data units的存储设备加密[11],也是当前luks默认的(推荐的)工作方式。它支持两种密钥长度256/512bits,实际上它使用的是AES128/AES256,由于引入了tweak value才使密钥长度翻倍。
- disk block或叫做data unit(如一个扇区)是一个长度不小于128bts的数据,它在之后将会被分成多个cipher block(每个128bit,即AES的分组操作),对于最后一个分组若不满足大小,将使用密文窃取技术;
- 各cipher block之间独立(即它实际使用的类似EBC工作模式,没有CBC那样的dependency);
- cipher block的加密输入包含了index信息(扇区号和块号);
1 | C← XTS-AES-blockEnc(Key, P, i, j) |
它的内部过程如下,其中乘法为模$ GF(2^{128})=x^{128} + x^{7} + x^{2} + x + 1$
上的乘法,$\alpha$
是它的一个生成元:
可扩展的,对于一个disk block的加密如下图:
可见其实XTS-AES不算一种全新的加密算法而是一种加密方案,在最下层它就是普通的AES加密,所以256比特或512比特长的密钥只是两个密钥的连接,在实际使用时对半切割就好了。
注:Ciphertext stealing(密文窃取)即加密时把倒数第二个分块的密文部分内容移动到最后一个块的明文部分,使最后一个块填充后再对最后一个块加密,这样密文和明文大小就不会有变化,解密时先解密最后一个分块,再将解密后的属于倒数第二块的密文和倒数第二块组合成一个完整块,这样就能解密倒数第二块了。(密文窃取只会影响最后两个块,其他块该咋的还咋的)
破解
思路
使用现代密码学方式加密不存在直接破译密文,但存在如下破解思路:
- 硬核方法:柯克霍夫准则说啦安全性应取决于密钥保密,那么有弱密码的话再好的设计也不管用,注意luks会将用户输入密码转换为高熵的随机密码,所以破解的过程也要做这种转换,可以使用如john等工具来生成密码并使用脚本自动测试密码是否正确,明显的这是一种极其低效的手段;也使用hashcat,其已经实现了luks解密模式,配合上宇宙第一快的hashcat能够以较快的速度破解,然而密码猜解这种天命行为毕竟不那么靠谱,只能说是最后的办法了,不过编写该解密模式的作者其中使用熵来判断是否猜解成功的思路挺帅的。
- 取巧方法:由于系统可正常启动,磁盘又是全盘加密,被加密的分区必定在加载系统前先被解密再挂载,使用cryptsetup luksDump等命令可以确认被加密的磁盘使用了luks的aes256加密,分析mbr发现使用了grub,有以下思路:
- 分析grub,既然系统都被加密了还能正常启动,肯定是在启动grub之后,将控制权转交给被加密的内核前完成的解密操作,密钥一定在grub相关的文件里(生成),这就被限定在很小的范围了,通过逆向分析即可获取密钥。
- 向系统注入shell,该操作可以在系统启动时文件解密前的各个阶段进行,比如直接hook grub,当拿到shell后就可以直接取消锁或者拿文件了。
- 当系统运行时,dump虚拟机内存,从内存中查找密钥,这种方式的可行性是假设密钥是被普通的保存在内存中而不是存放在加密硬件中的,既然使用了透明加密,密钥在磁盘被使用时一定会被存储在计算机的某个位置。
- 从虚拟机启动后提供的服务或shell登入,再进行必要的提权操作,简单的测试了弱密码发现登陆shell失败(事实上在后来的分析中也发现这种方式基本不可能成功)。
本篇陈述的内存取证方式密钥提取即第三种dump密钥。
内存中密钥识别
当前通用破解思路,都是假设密钥在内存中连续存储:
- 暴力搜索,一般密钥4字节对齐,4G的内存有2^28种可能
- 在直接暴力的基础上优化,排除熵小的区域,因为据统计程序中的代码与普通数据熵远小于MK的熵,设定阈值可以排除大部分数据,不过为了效率与准确率需要适中的统计窗口,故此方法对于MK直接用处不大。
- 计算汉明距离,AES轮密钥具有线性关系,使用汉明距离实现容错。
- 压缩试错的方法,和2,3类似,先对一块数据压缩,若压缩率不高说明该段数据随机性高即可能是密钥,效率低直接抛弃。
- 根据代码结构搜索,例如luks使用的dm-crypt会使用
struct crypt_config
存储密钥,该结构体的其他元素会有一些特征,通过特征可以定位到该结构体并进一步定位到key。 - 可以使用轮密钥去验证/修复受损的初始密钥。
内存dump
内存dump的工具非常多,windows下常见的有prodump,dumpit,process explorer或调试器等,Linux下可以直接复制内存的虚拟映射,但是此处我们需要dump的是虚拟机系统里的内存,virtualbox提供了VBoxManage管理工具可用于该操作,VMware无类似工具但是实际上挂起客户机即可拿到其内存文件,该文件位于目标虚拟机(一般也为虚拟磁盘)文件目录下,更多可参考内存取证相关文献。
例-内存AES抓取
原理
device-mapper实现了透明加密,在启用(解密)设备后,用户对文件读时会将加密后的文件解密后提交给用户层,写操作相反,整个过程都会涉及到加解密,所以密钥一定存放在某个地方,在此处为内存,那么dump内存后将能够得到密钥相关的数据了。但是转储的内存如此大如何找到密钥文件是最难的事,最直接的方法是顺序遍历内存的每一字节,并读取密钥长度的数据作为密钥去解密数据或者验证hmac,但是这是一个低效的事,直接的提高效率的办法是先通过信息熵找到疑似密码的区域(熵高出阈值)再去验证,不过有另一种更精确也能容错与自动更正内存错误(如密钥发生1bit错误时试解密的方法将会失效而下面的方式将能够成功找到密钥并修正错误)的方式,那就是根据轮密钥生成算法的属性。
以AES为例,标准的AES加密有一种分组长度(128位)及三种密钥长度(128,192,256位),三种密钥长度在实际加密过程中会对一个分组分别执行10,12,14轮加密,例如AES128会将16字节的密钥扩展为11组共176字节的轮密钥,除了第一轮其他每一轮加密用对应的那一个轮密钥,其中轮密钥只与16字节的输入密钥相关,与明文或密文无关,那么知道扩展密钥中任何连续的Nk个字(Nk代表密钥长度,如AES128的Nk为4,此处4字节为一个字)能够重新产生整个扩展密钥(比如AES128 扩展密钥444个字,只要知道其中连续的4个字就可以恢复输入的16字节的密钥),如下图,得到足够长度后能够推出整个扩展密钥。
另一方面,在AES的实现中,轮密钥的位置一般是连续的,所以只需要遍历内存,并计算一个完整的AES key schedule,那么他们应该满足轮密钥的生成关系,换句话说就是一片内存区域的数据如果能够满足轮密钥生成算法的约束那么就可以猜测该区域存储的是AES的扩展密钥,也就可以计算出该区域的AES主密钥,而且若设定容错阈值,那么可以实现小部分不匹配依然能识别的情况。
*注:尽管AES需要使用特定的操作模式才能进行实际的对称加密操作,但是显然的磁盘加密时不会将一个磁盘(或分区等)作为一个整体加密,否则无法做到随机存取,因此操作模式不会对密钥恢复造成任何影响。另一方面,由于AES用了轮常数所以可以很容易定位当前轮数。**
解密步骤
使用virtualBox导入镜像,运行,待开始初始化时执行内存转储:
1 | ~ VBoxManage debugvm aurora-universal dumpvmcore --filename=lm.raw |
之后使用findaes获取内存中的密钥:
1 | ~ findaes.exe lm.raw |
如上可能会发现很多AES密钥先记录之后逐个尝试,接着在XXX虚拟机里添加含有kali系统的虚拟磁盘(若是反过来从kali里添加XXX的虚拟磁盘似乎会出错),设置为从kali启动,此时kali会自动挂载XX的磁盘,可使用fdisk列出磁盘:
1 | ~ fdisk -l |
接着使用cryptsetup获取每个加密分区的luks加密元信息,主要关注注释部分:
1 | ~ cryptsetup luksDump /dev/sdb2 |
可见主密钥256位,通过pbkdf2函数验证内存中的aes密钥获得对应位置的主密钥:
1 | mk-salt = '' # 盐 |
直接使用主密钥可以设置新的密码:
1 | cryptsetup luksAddKey /dev/sdb2 --master-key-file <(echo '2c7bc972fa44d5c7a6a48f90e0a4cc491ff23d485b344d0bb75e70cb2d808931' | xxd -r -p) |
再输入新设置的密码即可打开加密分区(当也可以直接使用主密钥解密分区,但是此处的IV太过复杂)。
注:类似的XTS-AES会需要组合两个密钥,直接将dump的密钥连接即可,可使用脚本枚举验证HMAC。
提升
我们当前的实现中,虽然密钥隐藏起来了,但是在启动后依然会被装载入内存,之后该密钥将能够被直接从内存dump,并使用公开资料解密数据,该过程难度较低,为此已经有人做了一些研究,软件层面上大体可分为三类:
- 不修改算法,将密钥存放在不易被dump的区域,例如始终存于寄存器,存于cache里等。这是在算法的实现上做文章,不会影响原数据加解密的结果,能兼容已有加密文件,缺点是需要特定的硬件,占用宝贵的高速存储资源,最关键的是在我们的虚拟机镜像上完全失效,直接pass。
- 自修改加密算法,以AES算法为例,我们可以自定义S盒,或者改变加密轮数,亦或者修改其他操作。这样即使能从内存中dump密钥(若改变密钥扩展算法将无法从dump的内存中找到密钥),也无法直接解密数据,当然这样的缺点是算法改变后所有数据都需要重新加密,而且这是密码学的安全性转换为软件保护的安全性。
- 另一种可选的方法是使用白盒密码,白盒密码就是应对这种情况产生的,它的思路是把密钥放置在算法里面,这样既不会改变加密解密的结果也不会在内存中出现真实密钥,可惜的是当前并没有兼顾效率与安全性的白盒AES密码实现,不过我们的对手不是密码学专家,他不一定能通过数学手段破译,所以牺牲掉一定的密码学安全性,把破解转移到了软件逆向之上是有用的。
想法
提出一种方案,在一定程度上提升全盘保护的强度,思路为对抗findaes等攻击dump密钥:
- 我们的启动引导程序是GRUB2,它当前支持luks1.0,相关代码是直接集成到项目之中的(在
grub-core/disk
和grub-core/lib/libgcrypt
目录下),包括加解密部分,所以我们对luks算法的修改不会影响系统的正常功能。 - 修改工作模式部分,比如它的IV,以IV PLAIN64为例,它的原始值为扇区值:我们可以将该值进行任何任何运算,当然这里修改后所有数据需要全部重新加密,所以操作是可选的。
1
2
3
4
5
6case GRUB_CRYPTODISK_MODE_IV_PLAIN64:
iv[1] = grub_cpu_to_le32 (sector >> 32);
/* FALLTHROUGH */
case GRUB_CRYPTODISK_MODE_IV_PLAIN:
iv[0] = grub_cpu_to_le32 (sector & 0xFFFFFFFF);
break; - 修改算法部分,如上所述选择白盒密码,当前已有一些不成熟的WBDES,WBAES,WBSM4算法实现,我们可以选择一种性能符合要求的非主流算法进行实现,例如《面向智能终端的白盒密码技术研究与实现》中所描述的算法,这部分的修改不会改变加解密的结果也能防止密钥被dump,是修改的核心。更简单的,根据findaes原理,只要减少连续内存区的熵,增加重复数据,改变汉明距离即可绕过其搜索算法,所以只要修改其密钥排布就行啦~,另外也可以自己实现基于dm-crypt实现类luks结构增大难度,隐藏luks头部也是一种方法。
- 转向软件保护,各种技巧都可以用上,比如当前的密钥隐藏技巧,尽管现在已经不需要外置密钥了,还是可以藏一个假的在那里误导攻击者,至于其他保护方式就太多了,各种混淆都可以上,不过由于分区需要被自动解密,决定了全盘保护的上限,所以为了效率不必多费功夫。
内存取证加逆向分析
即使通过各种方式修改了算法,也可以通过内存取证的方式拿到关键程序与内存,并进行二进制程序逆向分析。
总结
在无专用加密设备,做纯全盘加密时,由于系统运行需要解密数据,在这种情况下只要能够物理访问磁盘就一定能够(如代码提升)获取加密密钥并解密磁盘,所以全盘加密不应该做为主要的版权数据保护手段,而应该在其他部分(如数据代码混淆,使用native库)下功夫,在全盘加密上,可以使用较小众的工具算法与模式防止现有工具直接破解,提高分析门槛。
参考
[3] luks-wiki
[5] findaes
[11] XTS磁盘加密模式
[17] IEEE_P1619
[19] IEEE Standard for CryptographicProtection of Data on Block-OrientedStorage Devices
autopsy, Elcomsoft Forensic Disk Decryptor, forensic-toolkit-ftk, volatility
《The persistence of memory: Forensic identification and extraction of cryptographic keys》, 《Lest We Remember: Cold Boot Attacks on Encryption Keys》,《Playing ‘hide and seek’with stored keys》,《Forensic Key Discovery andIdentificationFinding Cryptographic Keys in Physical Memory》,《》