巨大的肮脏的COW CVE-2017-1000405

不完整的肮脏的COW补丁

“Dirty COW”漏洞(CVE-2016-5195)是最广为人知的漏洞之一。过去十年的每个Linux版本,包括Android,台式机和服务器都很脆弱。影响是巨大的 – 数百万用户可能容易和可靠地妥协,绕过共同的利用防御。

发布了大量有关该漏洞的信息,但未对其补丁进行详细分析。
我们Bindecy有兴趣研究这个补丁及其所有含义。令人惊讶的是,尽管臭虫已经得到了广泛的宣传,但是我们发现这个补丁并不完整。

“肮脏的牛棚”重述

首先,我们需要充分了解原始的肮脏的COW漏洞。我们将假定对Linux内存管理器有基本的了解。人才已经这样做了,我们不会恢复原来的血统细节。

原来的漏洞在get_user_pages函数中。该功能用于获取用户进程中虚拟地址后面的物理页面。调用者必须指定他打算在这些页面上执行什么样的动作(触摸,写入,锁定等等),所以内存管理员可以相应地准备页面。具体而言,当计划在私有映射内的页面上执行写入操作时,页面可能需要经历COW(写时复制)循环 – 将原始“只读”页面复制到新页面这是可写的。原始页面可以是“特权”的 – 也可以映射到其他进程中,甚至可以在修改后写回到磁盘。

现在我们来看看相关的代码__get_user_pages:

static long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
        unsigned long start, unsigned long nr_pages,
        unsigned int gup_flags, struct page **pages,
        struct vm_area_struct **vmas, int *nonblocking)
{
    // ...
    do {
        struct page *page;
        unsigned int foll_flags = gup_flags;
        // ...
        vma = find_extend_vma(mm, start);
        // ...  

retry:
        // ...
        cond_resched();
        page = follow_page_mask(vma, start, foll_flags, &page_mask);
        if (!page) {
            int ret;
            ret = faultin_page(tsk, vma, start, &foll_flags,
                    nonblocking);
            switch (ret) {
            case 0:
                goto retry;
            case -EFAULT:
            case -ENOMEM:
            case -EHWPOISON:
                return i ? i : ret;
            case -EBUSY:
                return i;
            case -ENOENT:
                goto next_page;
            }
            BUG();
        }
        // ...

next_page:
        // ...
        nr_pages -= page_increm;
    } while (nr_pages);
    return i;
}

该while环路的目标是在被请求的页面范围内获取每一页。在满足我们的要求之前,每个页面都必须有错误 – 这就是retry标签的用途。

follow_page_mask其作用是扫描页表以获得给定地址的物理页面(同时考虑到PTE权限),或者在请求不能满足的情况下失败。在follow_page_mask操作期间,PTE的自旋锁被获取 – 这保证了在我们抓取参考之前物理页面不会被释放。

faultin_page请求内存管理器以指定的权限处理给定地址中的错误(也在PTE的自旋锁中)。请注意,成功调用faultin_page锁之后,无法保证follow_page_mask下一次重试成功。另一块代码可能与我们的页面混淆。

最初的易受攻击代码位于faultin_page:

if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags &= ~FOLL_WRITE;

删除FOLL_WRITE标志的原因是考虑到FOLL_FORCE标志应用于只读VMA(当VM_MAYWRITE标志设置在VMA中)的情况。在这种情况下,pte_maybe_mkwrite函数将不会设置写入位,但是故障页面确实已经准备好写入。

如果页面在执行时经过COW循环(由VM_FAULT_WRITE标志标记)faultin_page并且VMA不可写入,FOLL_WRITE 则从下一次访问页面的尝试中移除该标志 – 将仅请求读取权限。
如果第一个follow_page_mask失败,因为该页面是只读或不存在,我们会尝试错误的。现在让我们想象,在这段时间,直到下一次尝试获取页面,我们将摆脱COW版本(例如通过使用madvise(MADV_DONTNEED))。

下一次调用faultin_page将会没有FOLL_WRITE标志,所以我们将从页面缓存中获取页面的只读版本。现在,下一次调用follow_page_mask也会发生,没有FOLL_WRITE标志,所以它将返回特权的只读页面,而不是调用者对页面的可写版本的原始请求。
基本上,上述流程是肮脏的COW漏洞 – 它允许我们写入页面的只读特权版本。引入了以下修复程序
faultin_page:

if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags |= FOLL_COW; // Instead of *flags &= ~FOLL_WRITE;

并增加了一个新的函数follow_page_mask:

/*
 * FOLL_FORCE can write to even unwritable pte's, but only
 * after we've gone through a COW cycle and they are dirty.
 */
static inline bool can_follow_write_pte(pte_t pte, unsigned int flags)
{
    return pte_write(pte) ||
        ((flags & FOLL_FORCE) && (flags & FOLL_COW) && pte_dirty(pte));
}

get_user_pages现在记住我们经历了一个COW周期的事实,而不是减少请求的权限。在下一次迭代中,只有在指定了FOLL_FORCE和FOLL_COW标志的情况下,我们才能够获得写操作的只读页面,并且PTE被标记为脏。

这个补丁假设页面的只读特权副本将永远不会有一个PTE指向它的脏位 – 一个合理的假设…或是它吗?

透明巨大页面(THP)

通常,Linux通常使用4096个字节的长页面。为了让系统管理大量的内存,我们可以增加页表项的数量,或者使用更大的页面。我们专注于第二种方法,这是在Linux中使用巨大的页面实现的。
一个巨大的页面是一个2MB的页面。利用这个功能的方法之一是通过透明巨大页面机制。虽然还有其他方法可以获得大量的页面,但它们超出了我们的范围。

内核将尝试使用巨大的页面来满足相关的内存分配。THP是可交换的和“易碎的”(即可以拆分成普通的4096个字节的页面),并且可以在匿名,shmem和tmpfs映射中使用(后两种只在更新的内核版本中才是真实的)。

通常(取决于编译标志和机器配置),默认THP支持仅用于匿名映射。Shmem和tmpfs支持可以手动打开,通常THP支持可以在系统运行时通过写入某些内核的特殊文件来打开和关闭。

一个重要的优化机会是将普通页面合并成大页面。一个名为khugepaged的特殊守护进程会不断扫描可能的候选页面,并将其合并到巨大的页面中。显然,要成为候选者,VMA必须覆盖整个对齐的2MB内存范围。

THP通过打开_PAGE_PSEPMD(页面媒体目录,比PTE电平高一级)的位来实现。因此,PMD指向一个2MB的物理页面,而不是一个PTE的目录。每次扫描页表时,必须检查pmd_trans_hugePMD是否具有该功能,以便决定PMD是指向pfn还是指向PTE的目录。在一些体系结构中,也存在大量的PUD(Page Upper Directory),导致1GB的页面。
内核2.6.38支持THP。在大多数Android设备上,THP子系统未启用。

错误🐞

深入处理与THP 有关的脏COW 补丁代码,我们可以看到,相同的逻辑can_follow_write_pte适用于庞大的PMD。can_follow_write_pmd添加了一个匹配的函数:

static inline bool can_follow_write_pmd(pmd_t pmd, unsigned int flags)
{
    return pmd_write(pmd) ||
        ((flags & FOLL_FORCE) && (flags & FOLL_COW) && pmd_dirty(pmd));
}

然而,在庞大的PMD情况下,页面可以被标记为肮脏的,而不需要经过COW循环,使用touch_pmd函数:

static void touch_pmd(struct vm_area_struct *vma, unsigned long addr,
        pmd_t *pmd)
{
    pmd_t _pmd;

    /*
     * We should set the dirty bit only for FOLL_WRITE but for now
     * the dirty bit in the pmd is meaningless.  And if the dirty
     * bit will become meaningful and we'll only set it with
     * FOLL_WRITE, an atomic set_bit will be required on the pmd to
     * set the young bit, instead of the current set_pmd_at.
     */
    _pmd = pmd_mkyoung(pmd_mkdirty(*pmd));
    if (pmdp_set_access_flags(vma, addr & HPAGE_PMD_MASK,
                pmd, _pmd,  1))
        update_mmu_cache_pmd(vma, addr, pmd);
}

这个函数是通过follow_page_mask,每次get_user_pages试图获得一个巨大的页面将被调用。显然,评论是不正确的,现在这个脏点不是没有意义的。特别是 – 当get_user_pages用来阅读一个巨大的页面时,该页面将被标记为肮脏的,而不经过COW循环,并且can_follow_write_pmd逻辑现在被破坏。

在这一点上,利用这个bug非常简单 – 我们可以使用原始的肮脏的COW竞赛的类似模式。这一次,在我们摆脱了复制版本的页面之后,我们必须对原始页面进行两次 错误的处理- 首先使其出现,然后打开脏的页面。
现在是不可避免的问题 – 这有多糟?

错误影响

为了利用这个bug,我们必须选择一个有趣的只读大页面作为写作的目标。唯一的限制是我们需要能够在丢弃之后获取它madvise(MADV_DONTNEED)。匿名巨大的页面在a之后从父进程继承fork是一个有价值的目标,但是一旦被丢弃,它们就会丢失 – 我们无法再次获取它们。
我们发现了两个不应写入的有趣目标:

  • 巨大的零页

  • 密封(只读)大页面

零页

在匿名映射发出读取错误之前,我们会得到一个称为零页的特殊物理页面。这种优化防止系统不得不在系统中分配多个归零页面,这些页面可能永远不会被写入。因此,在许多不同的进程中映射完全相同的零页面,这些进程具有不同的安全级别。

同样的原则也适用于巨大的页面 – 如果没有发生写入错误,则不需要创建另一个巨大的页面 – 反而会映射一个称为巨大零页面的特殊页面。请注意,此功能也可以关闭。

THP,shmem和密封的文件

shmem和tmpfs文件也可以使用THP进行映射。可以使用memfd_create 系统调用或mmap匿名共享映射来创建shmem文件。可以使用tmpfs的安装点(通常/dev/shm)创建tmpfs文件。根据系统配置,两者都可以用巨大的页面进行映射。

shmem文件可以被密封 – 密封文件会限制所讨论的文件所允许的操作集。这种机制允许不相互信任的进程通过共享内存进行通信,而不必采取额外的措施来处理共享内存区域的意外操作(参见man memfd_create()更多信息)。存在三种类型的印章 –

  • F_SEAL_SHRINK:文件大小不能减少

  • F_SEAL_GROW:文件大小不能增加

  • F_SEAL_WRITE:文件内容不能被修改

这些密封可以使用fcntl系统调用添加到shmem文件。

POC

我们的POC演示覆盖巨大的零页。覆盖shmem应该是同样可能的,并会导致一个替代的利用路径。
请注意,在第一次写入页面错误到零页之后,它将被替换为新的(并归零)THP。使用这个原语,我们成功地崩溃了几个进程。覆盖巨大的零页面的一个可能的后果是在大的BSS部分内部具有不正确的初始值。一个常见的易受攻击的模式将使用零值作为全局变量尚未初始化的指标。
下面的崩溃示例演示该模式。在这个例子中,Firefox的JS Helper线程产生一个NULL-deref,可能是因为布尔%rdx错误地指出对象被初始化了:

Thread 10 "JS Helper" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7fffe2aee700 (LWP 14775)]
0x00007ffff13233d3 in ?? () from /opt/firefox/libxul.so
(gdb) i r
rax            0x7fffba7ef080 140736322269312
rbx            0x0 0
rcx            0x22 34
rdx            0x7fffba7ef080 140736322269312
rsi            0x400000000 17179869184
rdi            0x7fffe2aede10 140736996498960
rbp            0x0 0x0
rsp            0x7fffe2aede10 0x7fffe2aede10
r8             0x20000 131072
r9             0x7fffba900000 140736323387392
r10            0x7fffba700000 140736321290240
r11            0x7fffe2aede50 140736996499024
r12            0x1 1
r13            0x7fffba7ef090 140736322269328
r14            0x2 2
r15            0x7fffe2aee700 140736996501248
rip            0x7ffff13233d3 0x7ffff13233d3
eflags         0x10246 [ PF ZF IF RF ]
cs             0x33 51
ss             0x2b 43
ds             0x0 0
es             0x0 0
fs             0x0 0
gs             0x0 0
(gdb) x/10i $pc-0x10
   0x7ffff13233c3: mov    %rax,0x10(%rsp)
   0x7ffff13233c8: mov    0x8(%rdx),%rbx
   0x7ffff13233cc: mov    %rbx,%rbp
   0x7ffff13233cf: and    $0xfffffffffffffffe,%rbp
=> 0x7ffff13233d3: mov    0x0(%rbp),%eax
   0x7ffff13233d6: and    $0x28,%eax
   0x7ffff13233d9: cmp    $0x28,%eax
   0x7ffff13233dc: je     0x7ffff1323440
   0x7ffff13233de: mov    %rbx,%r13
   0x7ffff13233e1: and    $0xfffffffffff00000,%r13
(gdb) x/10w $rdx
0x7fffba7ef080: 0x41414141 0x00000000 0x00000000 0x00000000
0x7fffba7ef090: 0xeef93bba 0x00000000 0xda95dd80 0x00007fff
0x7fffba7ef0a0: 0x778513f1 0x00000000

这是另一个崩溃的例子 – gdb在加载Firefox调试会话的符号时崩溃:

(gdb) r
Starting program: /opt/firefox/firefox 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Program received signal SIGSEGV, Segmentation fault.
0x0000555555825487 in eq_demangled_name_entry (a=0x4141414141414141, b=<optimized out>) at symtab.c:697
697   return strcmp (da->mangled, db->mangled) == 0;
(gdb) i s
#0  0x0000555555825487 in eq_demangled_name_entry (a=0x4141414141414141, b=<optimized out>) at symtab.c:697
#1  0x0000555555955203 in htab_find_slot_with_hash (htab=0x555557008e60, element=element@entry=0x7fffffffdb00, hash=4181413748, insert=insert@entry=INSERT) at ./hashtab.c:659
#2  0x0000555555955386 in htab_find_slot (htab=<optimized out>, element=element@entry=0x7fffffffdb00, insert=insert@entry=INSERT) at ./hashtab.c:703
#3  0x00005555558273e5 in symbol_set_names (gsymbol=gsymbol@entry=0x5555595b3778, linkage_name=linkage_name@entry=0x7ffff2ac5254 "_ZN7mozilla3dom16HTMLTableElement11CreateTHeadEv", len=len@entry=48, 
    copy_name=copy_name@entry=0, objfile=<optimized out>) at symtab.c:818
#4  0x00005555557d186f in minimal_symbol_reader::record_full (this=0x7fffffffdce0, this@entry=0x1768bd6, name=<optimized out>, 
    name@entry=0x7ffff2ac5254 "_ZN7mozilla3dom16HTMLTableElement11CreateTHeadEv", name_len=<optimized out>, copy_name=copy_name@entry=48, address=24546262, ms_type=ms_type@entry=mst_file_text, 
    section=13) at minsyms.c:1010
#5  0x00005555556959ec in record_minimal_symbol (reader=..., name=name@entry=0x7ffff2ac5254 "_ZN7mozilla3dom16HTMLTableElement11CreateTHeadEv", name_len=<optimized out>, copy_name=copy_name@entry=false, 
    address=<optimized out>, address@entry=24546262, ms_type=ms_type@entry=mst_file_text, bfd_section=<optimized out>, objfile=0x555557077860) at elfread.c:209
#6  0x0000555555696ac6 in elf_symtab_read (reader=..., objfile=objfile@entry=0x555557077860, type=type@entry=0, number_of_symbols=number_of_symbols@entry=365691, 
    symbol_table=symbol_table@entry=0x7ffff6a6d020, copy_names=copy_names@entry=false) at elfread.c:462
#7  0x00005555556970c4 in elf_read_minimal_symbols (symfile_flags=<optimized out>, ei=0x7fffffffdcd0, objfile=0x555557077860) at elfread.c:1084
#8  elf_symfile_read (objfile=0x555557077860, symfile_flags=...) at elfread.c:1194
#9  0x000055555581f559 in read_symbols (objfile=objfile@entry=0x555557077860, add_flags=...) at symfile.c:861
#10 0x000055555581f00b in syms_from_objfile_1 (add_flags=..., addrs=0x555557101b00, objfile=0x555557077860) at symfile.c:1062
#11 syms_from_objfile (add_flags=..., addrs=0x555557101b00, objfile=0x555557077860) at symfile.c:1078
#12 symbol_file_add_with_addrs (abfd=<optimized out>, name=name@entry=0x55555738c1d0 "/opt/firefox/libxul.so", add_flags=..., addrs=addrs@entry=0x555557101b00, flags=..., parent=parent@entry=0x0)
    at symfile.c:1177
#13 0x000055555581f63d in symbol_file_add_from_bfd (abfd=<optimized out>, name=name@entry=0x55555738c1d0 "/opt/firefox/libxul.so", add_flags=..., addrs=addrs@entry=0x555557101b00, flags=..., 
    parent=parent@entry=0x0) at symfile.c:1268
#14 0x000055555580b256 in solib_read_symbols (so=so@entry=0x55555738bfc0, flags=...) at solib.c:712
#15 0x000055555580be9b in solib_add (pattern=pattern@entry=0x0, from_tty=from_tty@entry=0, readsyms=1) at solib.c:1016
#16 0x000055555580c678 in handle_solib_event () at solib.c:1301
#17 0x00005555556f9db4 in bpstat_stop_status (aspace=0x555555ff5670, bp_addr=bp_addr@entry=140737351961185, ptid=..., ws=ws@entry=0x7fffffffe1d0) at breakpoint.c:5712
#18 0x00005555557ad1ef in handle_signal_stop (ecs=0x7fffffffe1b0) at infrun.c:5963
#19 0x00005555557aec8a in handle_inferior_event_1 (ecs=0x7fffffffe1b0) at infrun.c:5392
#20 handle_inferior_event (ecs=ecs@entry=0x7fffffffe1b0) at infrun.c:5427
#21 0x00005555557afd57 in fetch_inferior_event (client_data=<optimized out>) at infrun.c:3932
#22 0x000055555576ade5 in gdb_wait_for_event (block=block@entry=0) at event-loop.c:859
#23 0x000055555576aef7 in gdb_do_one_event () at event-loop.c:322
#24 0x000055555576b095 in gdb_do_one_event () at ./common/common-exceptions.h:221
#25 start_event_loop () at event-loop.c:371
#26 0x00005555557c3938 in captured_command_loop (data=data@entry=0x0) at main.c:325
#27 0x000055555576d243 in catch_errors (func=func@entry=0x5555557c3910 <captured_command_loop(void*)>, func_args=func_args@entry=0x0, errstring=errstring@entry=0x555555a035da "", 
    mask=mask@entry=RETURN_MASK_ALL) at exceptions.c:236
#28 0x00005555557c49ae in captured_main (data=<optimized out>) at main.c:1150
#29 gdb_main (args=<optimized out>) at main.c:1160
#30 0x00005555555ed628 in main (argc=<optimized out>, argv=<optimized out>) at gdb.c:32
(gdb) list
692   const struct demangled_name_entry *da
693     = (const struct demangled_name_entry *) a;
694   const struct demangled_name_entry *db
695     = (const struct demangled_name_entry *) b;
696 
697   return strcmp (da->mangled, db->mangled) == 0;
698 }
699 
700 /* Create the hash table used for demangled names.  Each hash entry is
701    a pair of strings; one for the mangled name and one for the demangled
(gdb)
链接到我们的POC

概要

这个错误展示了安全开发生命周期中补丁审计的重要性。由于肮脏的COW情况和其他以往的案例显示,甚至被炒作的漏洞可能会得到不完整的补丁。这种情况不仅适用于封闭源码软件,开源软件同样受到影响。

披露时间表
最初的报告是在22.11.17发给内核和发行版的邮件列表。答复是立即和专业的一个补丁在几天准备。该修补程序touch_pmd 只有在调用者要求写访问时才修复该功能以设置PMD条目的脏位。
感谢安全团队和发行人为维护高标准的安全所付出的时间和精力。


转载请注明出处:https://www.freearoot.com/index.php/%e5%b7%a8%e5%a4%a7%e7%9a%84%e8%82%ae%e8%84%8f%e7%9a%84cow-cve-2017-1000405.html

文章转载来源:https://medium.com/bindecy/huge-dirty-cow-cve-2017-1000405-110eca132de0