CVE-2022-2588

tags: CVE Note MCL poc DirtyCred

** RESERVED ** This candidate has been reserved by an organization or individual that will use it when announcing a new security problem. When the candidate has been publicized, the details for this candidate will be provided.

affact kernel [v3.17, v?)

以下 code 都是以 linux kernel v5.16.15 為例

Abstract

Dirty Cred 是一個提權漏洞,能夠繞過 kernel 中 cred 的檢查機制,使用時需搭配能夠寫入 heap 相關的漏洞。

Background

Out Of Bound Write (OOB)

越界寫入,寫到了本來不應該寫入的位置。

單純的 OOB 無法利用,原因是我們需要有寫入,否則只有 read 也只能洩漏資料

Use After Free (UAF)

對於 free 掉的記憶體,卻還是拿來使用

UAF
#include <stdio.h> #include <stdlib.h> typedef struct person { long int height, weight, age; } person_t; int main(void) { person_t *p1, *p2; p1 = (person_t*) malloc(sizeof(person_t)); p1->age = 18; printf("Before free, p1->age: %ld\n", p1->age); free(p1); # p1 已經 free 掉了,但還是拿來用 printf("After free, p1->age: %ld\n", p1->age); return 0; }
$ gcc UAF.c -o UAF
$ ./UAF
> Before free, p1->age: 18
> After  free, p1->age: 18

Double Free

對於同一塊記憶體 free 兩次,導致 free list 連續兩塊是同樣地址,導致下次 malloc 取出的 chunk 同時也存在於 free list 中

double free (on glibc 2.35)
#include <stdio.h> #include <stdlib.h> int main(void) { unsigned long int *p1, *p2, *p3[7]; p1 = (unsigned long int*) malloc(0x20); p2 = (unsigned long int*) malloc(0x20); // 塞滿 tcache for (int i=0;i<7;i++) p3[i] = (unsigned long int*) malloc(0x20); for (int i=0;i<7;i++) free(p3[i]); // fast bin 無檢查 free(p1); free(p2); free(p1); return 0; }

Linux File

struct file 描述了 file 的基本訊息,其中 struct cred 是表示 file 的權限,如果能夠修改到,等同可以串改整個 file 的權限。

file 結構只保存已經被打開的 file 的訊息,因此沒有 open 權限的不會有 file 結構

kernel code

struct file

struct file { union { struct llist_node fu_llist; struct rcu_head fu_rcuhead; } f_u; struct path f_path; struct inode *f_inode; /* cached value */ const struct file_operations *f_op; /* * Protects f_ep, f_flags. * Must not be taken from IRQ context. */ spinlock_t f_lock; enum rw_hint f_write_hint; atomic_long_t f_count; unsigned int f_flags; fmode_t f_mode; struct mutex f_pos_lock; loff_t f_pos; struct fown_struct f_owner; # 權限相關 const struct cred *f_cred; struct file_ra_state f_ra; u64 f_version; #ifdef CONFIG_SECURITY void *f_security; #endif /* needed for tty driver, and maybe others */ void *private_data; #ifdef CONFIG_EPOLL /* Used by fs/eventpoll.c to link all the hooks to this file */ struct hlist_head *f_ep; #endif /* #ifdef CONFIG_EPOLL */ struct address_space *f_mapping; errseq_t f_wb_err; errseq_t f_sb_err; /* for syncfs */ } __randomize_layout __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */

Linux Credentials

存放 task 中的權限訊息,例如 SUID SGID SBIT,如果能夠修改到這個結構,等同可以串改整個 task 的權限。

kernel code

struct cred

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 */ unsigned securebits; /* SUID-less security management */ kernel_cap_t cap_inheritable; /* caps our children can inherit */ kernel_cap_t cap_permitted; /* caps we're permitted */ kernel_cap_t cap_effective; /* caps we can actually use */ kernel_cap_t cap_bset; /* capability bounding set */ kernel_cap_t cap_ambient; /* Ambient capability set */ #ifdef CONFIG_KEYS unsigned char jit_keyring; /* default keyring to attach requested * keys to */ struct key *session_keyring; /* keyring inherited over fork */ struct key *process_keyring; /* keyring private to this process */ struct key *thread_keyring; /* keyring private to this thread */ struct key *request_key_auth; /* assumed request_key authority */ #endif #ifdef CONFIG_SECURITY void *security; /* LSM security */ #endif struct user_struct *user; /* real user ID subscription */ struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */ struct ucounts *ucounts; struct group_info *group_info; /* supplementary groups for euid/fsgid */ /* RCU deletion */ union { int non_rcu; /* Can we skip RCU deletion? */ struct rcu_head rcu; /* RCU deletion hook */ }; } __randomize_layout;

slab & slob & slub

(Todo)

slab

主要解決 buddy system 一次都是要 1 個 page 大小的問題,在原先 buddy system 上再多上 slab 機制,讓小於 page 大小 的空間都由 slab 去分配。

slob

可以把它想成 slab 的縮小版,專門用於 memory 比較小的設備,ex: IoT,因此內容比較精簡,但更容易記憶體碎片化

slub

slab 的加強版,解決了 slab 的問題,為目前 linux 預設的 slab allocator

  1. 每個 node 有 3 個 list,分別記錄空閒、部分空閒、非空閒 slab,當來不及回收時,三個 list 紀錄的會長時間留在 slab 管理器中,因此 slub 只保留部分空閒 slab
  2. 每個 cpu 紀錄的是 object 的地址,這些 object 可能來自不同的 slab,不利於 slab 的回收,slub 改成紀錄一個實際可用的 slub,不影響其他 slub 的回收
  3. shared共享链表可能导致一个slab持有较多slab,无法即使释放给伙伴系统。slub去掉了该链表。可见,slub出现的主要目的是为了减少slab的数量,提高内存的使用率。同时,出于对内存使用率的极致追求,slub去除了slab的着色做法,取而代之是slub复用,通过slub复用减轻cache冲突的情况

Methology

此攻擊手法的關鍵點在於unprivileged credential 換成 privileged credential,至於如何做到,需要根據搭配的漏洞加以構造 payload。

OOB write

將原先指向 unprivileged 的 struct cred 透過修改 pointer 的方式,改指向 privileged 的 struct cred,但 page 是 4k(0x1000) 對齊,所有只有 1/16 的成功率。

UAF

  1. 如果 UAF 發生在 privileged credential 上,那就只要 free 掉要攻擊的 unprivileged credential 然後換成 privileged credential 就好
  2. 如果 UAF 發生在 unprivileged credential 上,那需搭配 invalid write 的漏洞

Double Free

  1. allocate 一個 vulnerable object(ptr1 和 ptr2 都指向它)
  2. free ptr1(ptr2 指向它)
  3. allocate 另一個 vulnerable object(new_ptr1, new_ptr2, ptr2 都指向它)
  4. free ptr2(new_ptr1, new_ptr2 都指向它)
  5. 清掉整條 memory cache
  6. 讓這條 memory cache 是 allocate credential object 的
  7. free new_ptr2
  8. allocate 新的 credential object(vuln)

接下來的手法就和 UAF 如出一轍

此外還有個重點,因為 Dirty Cred 需要在檢查 cred 與寫入 data 之間,將 unprivileged credential 換成 privileged credential,而此步需要花時間做,因此若能延長這中間的時間,勢必可以提高攻擊成功機率。

Extending time window

Userfaultfd

userfaultfd 是 linux 提供給 user space 處理 page fault 的機制,因此我們可以自行造成 page fault,並且自己處理,那麼就能自行拖延時間了。

但在 linux kernel v4.13 做了一點修改,在 Dirty Cred 所需要用的的 vfs_writev()
,將權限檢查移到了從 user space 拿資料之後,導致無法再利用。

kernel code

fs/read_write.c
v4.12

ssize_t vfs_writev(struct file *file, const struct iovec __user *vec, unsigned long vlen, loff_t *pos, int flags) { // 權限檢查 if (!(file->f_mode & FMODE_WRITE)) return -EBADF; if (!(file->f_mode & FMODE_CAN_WRITE)) return -EINVAL; // 寫入 return do_readv_writev(WRITE, file, vec, vlen, pos, flags); }

v4.13
fs/read_write.c

ssize_t vfs_writev(struct file *file, const struct iovec __user *vec, unsigned long vlen, loff_t *pos, int flags) { struct iovec iovstack[UIO_FASTIOV]; struct iovec *iov = iovstack; struct iov_iter iter; ssize_t ret; ret = import_iovec(WRITE, vec, vlen, ARRAY_SIZE(iovstack), &iov, &iter); if (ret >= 0) { file_start_write(file); ret = do_iter_write(file, &iter, pos, flags); file_end_write(file); kfree(iov); } return ret; }

在 linux kernel v5.11 開始,禁止了 userfaultfd,因為他們認為在 page fault 本該就是 kernel space 該處理的事情,拉到 user space 會有安全上的疑慮。 reference

Lock in filesystem

當 process 對檔案進行寫入時,會將 inode 上鎖,防止另一個 process 也要寫入,但是權限的判斷是不會上鎖的,因此可以有如下操作:

  1. 第一支 process 大量寫入文件,讓 lock 時間長一點
  2. 第二支 process 試圖寫內容進文件,觸發權限檢查
  3. 第三支 process 利用漏洞將替換掉整個 struct file
kernel code

以 ext4 為例
ext4_file_operations

const struct file_operations ext4_file_operations = { .llseek = ext4_llseek, .read_iter = ext4_file_read_iter, // 看 write .write_iter = ext4_file_write_iter, .iopoll = iocb_bio_iopoll, .unlocked_ioctl = ext4_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = ext4_compat_ioctl, #endif .mmap = ext4_file_mmap, .mmap_supported_flags = MAP_SYNC, .open = ext4_file_open, .release = ext4_release_file, .fsync = ext4_sync_file, .get_unmapped_area = thp_get_unmapped_area, .splice_read = generic_file_splice_read, .splice_write = iter_file_splice_write, .fallocate = ext4_fallocate, };

ext4_file_write_iter

static ssize_t ext4_file_write_iter(struct kiocb *iocb, struct iov_iter *from) { struct inode *inode = file_inode(iocb->ki_filp); if (unlikely(ext4_forced_shutdown(EXT4_SB(inode->i_sb)))) return -EIO; #ifdef CONFIG_FS_DAX if (IS_DAX(inode)) return ext4_dax_write_iter(iocb, from); #endif if (iocb->ki_flags & IOCB_DIRECT) return ext4_dio_write_iter(iocb, from); else return ext4_buffered_write_iter(iocb, from); }

ext4_buffered_write_iter

static ssize_t ext4_buffered_write_iter(struct kiocb *iocb, struct iov_iter *from) { ssize_t ret; struct inode *inode = file_inode(iocb->ki_filp); if (iocb->ki_flags & IOCB_NOWAIT) return -EOPNOTSUPP; ext4_fc_start_update(inode); inode_lock(inode); ret = ext4_write_checks(iocb, from); if (ret <= 0) goto out; current->backing_dev_info = inode_to_bdi(inode); ret = generic_perform_write(iocb->ki_filp, from, iocb->ki_pos); current->backing_dev_info = NULL; out: // 在寫入之前都會先做 lock inode_unlock(inode); ext4_fc_stop_update(inode); if (likely(ret > 0)) { iocb->ki_pos += ret; ret = generic_write_sync(iocb, ret); } return ret; }

Filesystem in Userspace (FUSE)

FUSE 是一個 unix 類的介面,可以讓沒有權限編 kernel 的使用者建立自己的檔案系統,那麼使用者就能註冊一個 handler,來指定對文件的操作。

Conclusion

Dirty Cred 並不是屬於程式邏輯的 bug,而是利用 race condition。利用時需要搭配其餘與 heap 相關的漏洞才能達成,所以利用層面廣,但是也是有些限制在的。

目前可利用的 structure 如下圖,其中星號是有 struct file,十字架是有 struct cred

目前搭配 CVE 如下圖,其中星號是有同時有 double freeUAF,十字架是有 OOB

Poc

  • 攻擊流程
    1. 攻擊者在 kernel heap 上有多個 unprivileged task
    2. 用其他漏洞將其中一個 task free 掉,等待其他 privileged task 填入
    3. 等到 linux kernel 將一個 privileged task 填入

如果是攻擊 file,就在權限檢查後將 struct file 換掉,變成去寫入另一個檔案
如果是攻擊 cred,將 struct cred 換掉後,變成 privileged cred 即可

  • 使用限制
    1. 攻擊者操作 linux os 的權限
    2. 有一個 heap 的漏洞

Reference

DirtyCred