
सीवीई-2022-2590 का गहन विश्लेषण, साझा मेमोरी (shmem) में एक Linux कर्नेल रेस कंडीशन, जो userfaultfd और FOLL_FORCE/COW बायपास के माध्यम से केवल-पठनीय पेजों पर मनमाना लेखन सक्षम बनाता है।
लिनक्स कर्नेल संस्करण: Linux/x86 6.0.0-rc1 (commit 37887783b3fef877bf34b8992c9199864da4afcb)
यह कमजोरी हमलावर को UFFDIO_CONTINUE का उपयोग करके फंक्शन can_follow_write_pte को संतुष्ट करके केवल-पढ़ने योग्य साझा मेमोरी पेज पर मनमानी सामग्री लिखने की अनुमति देती है, जो FOLL_FORCE, FOLL_COW और pte_dirty की जाँच करता है।
/*
* 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));
}
उपरोक्त फंक्शन यह निर्धारित करता है कि pte लिखने योग्य है या नहीं।
pte लिखने योग्य माना जाता है यदि यह निम्नलिखित शर्तों में से एक को पूरा करता है:
शर्त 2 को संतुष्ट करने के लिए, निम्नलिखित तीन फ्लैग सेट करने होंगे:
FOLL_FORCE
static ssize_t mem_rw(struct file *file, char __user *buf,
size_t count, loff_t *ppos, int write)
{
...
flags = FOLL_FORCE | (write ? FOLL_WRITE : 0);
while (count > 0) {
size_t this_len = min_t(size_t, count, PAGE_SIZE);
if (write && copy_from_user(page, buf, this_len)) {
copied = -EFAULT;
break;
}
this_len = access_remote_vm(mm, addr, page, this_len, flags); // __get_user_pages with FOLL_FORCE on
if (!this_len) {
if (!copied)
copied = -EIO;
break;
}
...
}
उपरोक्त फंक्शन /proc/<pid>/mem पर पढ़ने/लिखने के ऑपरेशन करता है।
इस फंक्शन का उपयोग करके, आप FOLL_FORCE फ्लैग चालू करके __get_user_pages फंक्शन तक पहुँच सकते हैं।
हालाँकि FOLL_FORCE का उपयोग ptrace में किया जाना है, इसकी आवश्यकता निम्नलिखित कमिट में सिद्ध हुई है: https://github.com/torvalds/linux/commit/f511c0b17b081562dca8ac5061dfa86db4c66cc2
FOLL_COW
/*
* mmap_lock must be held on entry. If @locked != NULL and *@flags
* does not include FOLL_NOWAIT, the mmap_lock may be released. If it
* is, *@locked will be set to 0 and -EBUSY returned.
*/
static int faultin_page(struct vm_area_struct *vma,
unsigned long address, unsigned int *flags, bool unshare,
int *locked)
{
...
/*
* The VM_FAULT_WRITE bit tells us that do_wp_page has broken COW when
* necessary, even if maybe_mkwrite decided not to set pte_write. We
* can thus safely do subsequent page lookups as if they were reads.
* But only do so when looping for pte_write is futile: in some cases
* userspace may also be wanting to write to the gotten user page,
* which a read fault here might prevent (a readonly page might get
* reCOWed by userspace write).
*/
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags |= FOLL_COW;
return 0;
}
FOLL_COW के साथ OR ऑपरेशन faultin_page फंक्शन में उपयोग किया जा सकता है, जो डर्टी COW के पैच का हिस्सा है।
static long __get_user_pages(struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *locked)
{
...
retry:
/*
* If we have a pending SIGKILL, don't keep faulting pages and
* potentially allocating memory.
*/
if (fatal_signal_pending(current)) {
ret = -EINTR;
goto out;
}
cond_resched();
page = follow_page_mask(vma, start, foll_flags, &ctx); // can_follow_write_pte
if (!page || PTR_ERR(page) == -EMLINK) {
ret = faultin_page(vma, start, &foll_flags,
PTR_ERR(page) == -EMLINK, locked); // flags |= FOLL_COW
switch (ret) {
case 0:
goto retry; // try follow page again
case -EBUSY:
case -EAGAIN:
ret = 0;
fallthrough;
case -EFAULT:
case -ENOMEM:
case -EHWPOISON:
goto out;
}
BUG();
} else if (PTR_ERR(page) == -EEXIST) {
...
}
FOLL_COW फ्लैग चालू करके can_follow_write_pte फंक्शन तक पहुँचने के लिए, आपको __get_user_pages फंक्शन के अंदर faultin_page फंक्शन को कॉल करना होगा, retry लेबल पर रीडायरेक्ट करना होगा, और फिर से faultin_page फंक्शन को कॉल करना होगा।
pte_dirty
यह शर्त निम्नलिखित कमिट के कारण संतुष्ट की जा सकती है: https://github.com/torvalds/linux/commit/9ae0f87d009ca6c4aab2882641ddfc319727e3db
/*
* Install PTEs, to map dst_addr (within dst_vma) to page.
*
* This function handles both MCOPY_ATOMIC_NORMAL and _CONTINUE for both shmem
* and anon, and for both shared and private VMAs.
*/
int mfill_atomic_install_pte(struct mm_struct *dst_mm, pmd_t *dst_pmd,
struct vm_area_struct *dst_vma,
unsigned long dst_addr, struct page *page,
bool newly_allocated, bool wp_copy)
{
...
_dst_pte = mk_pte(page, dst_vma->vm_page_prot);
_dst_pte = pte_mkdirty(_dst_pte); // set pte dirty unconditionally
if (page_in_cache && !vm_shared)
writable = false;
...
}
उपरोक्त पैच के कारण, केवल-पढ़ने योग्य साझा मेमोरी पेज के लिए बिना शर्त गंदी अवस्था में pte स्थापित करना संभव है।
यदि वे तीनों फ्लैग संतुष्ट होते हैं, तो can_follow_write_pte फंक्शन true लौटाएगा।
static struct page *follow_page_pte(struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd, unsigned int flags,
struct dev_pagemap **pgmap)
{
...
// true && !true == false
if ((flags & FOLL_WRITE) && !can_follow_write_pte(pte, flags)) {
pte_unmap_unlock(ptep, ptl);
return NULL;
}
page = vm_normal_page(vma, address, pte); // get read-only shared memory page
...
out:
pte_unmap_unlock(ptep, ptl);
return page;
no_page:
pte_unmap_unlock(ptep, ptl);
if (!pte_none(pte))
return NULL;
return no_page_table(vma, flags);
}
फंक्शन follow_page_pte केवल-पढ़ने योग्य साझा मेमोरी पेज लौटाता है।
निम्नलिखित PoC द्वारा सिद्ध की गई रेस परिदृश्य है, जिसे बाद में प्रस्तुत किया जाएगा।
| madvise और पढ़ना | UFFDIO_CONTINUE ioctl | pwrite |
|---|---|---|
| madvise // पेज को जैप करें | ||
| shmem_fault // पढ़ने की खराबी | ||
| handle_userfault | ||
| userfaultfd_continue | ||
| mcontinue_atomic_pte | ||
| ret = shmem_getpage(inode, pgoff, &page, SGP_NOALLOC); // get page | ||
| mfill_atomic_install_pte | ||
| _dst_pte = pte_mkdirty(_dst_pte); // pte को गंदा बनाएँ | ||
| set_pte_at(dst_mm, dst_addr, dst_pte, _dst_pte); // pte स्थापित करें | mem_rw | |
| access_remote_vm // FOLL_FORCE के साथ | ||
| __get_user_pages | ||
| can_follow_write_pte // FOLL_COW नहीं, 0 लौटाएँ | ||
| faultin_page | ||
| flags | ||
| retry: | ||
| follow_page_pte | ||
| madvise // पेज को जैप करें | ||
| shmem_fault // पढ़ने की खराबी | ||
| handle_userfault | ||
| userfaultfd_continue | ||
| mcontinue_atomic_pte | ||
| ret = shmem_getpage(inode, pgoff, &page, SGP_NOALLOC); // get page | ||
| mfill_atomic_install_pte | ||
| _dst_pte = pte_mkdirty(_dst_pte); // pte को गंदा बनाएँ | ||
| set_pte_at(dst_mm, dst_addr, dst_pte, _dst_pte); // pte स्थापित करें | ||
| can_follow_write_pte // 1 लौटाएँ | ||
| copy_to_user(buf, page, this_len) // केवल-पढ़ने वाले पेज पर सामग्री लिखें |
madvise और पढ़ने की रूटीन दो बार निष्पादित की जाती हैं; पहला निष्पादन __get_user_pages के retry को प्रेरित करता है, और दूसरा निष्पादन follow_page_pte को उस pte द्वारा इंगित पेज लौटाने का मार्गदर्शन करता है जिसे mfill_atomic_install_pte द्वारा गंदा बनाया गया है।
https://www.openwall.com/lists/oss-security/2022/08/15/1
आप डेविड हिल्डेनब्रांड के रिप्रोड्यूसर को उपरोक्त लिंक पर देख सकते हैं।