nf_tables入门


基本知识

TODO

基本数据结构

nft_table:https://elixir.bootlin.com/linux/v5.10/source/include/net/netfilter/nf_tables.h#L1074

可以看到这里的chains、sets、objectst都只是双链表,甚至没有指明类型,因此笔者猜测这个table上应该是可以挂上不同类型的set的,🤔。

nft_set:https://elixir.bootlin.com/linux/v5.10/source/include/net/netfilter/nf_tables.h#L430

nft_set_elem:https://elixir.bootlin.com/linux/v5.10/source/include/net/netfilter/nf_tables.h#L234

实操

这里先根据这个exp来学习:https://github.com/randorisec/CVE-2022-34918-LPE-PoC/blob/main/src/nf_tables.c ,这里主要提供了三个函数接口:

void create_table(int sock, const char *name);

void create_set(int sock, const char *set_name, uint32_t set_keylen, uint32_t data_len, const char *table_name, uint32_t id);

void add_elem_to_set(int sock, const char *set_name, uint32_t set_keylen, const char *table_name, uint32_t id, uint32_t data_len, uint8_t *data) ;

让我们逐一来调用并调试,😊!

准备工作

https://github.com/randorisec/CVE-2022-34918-LPE-PoC的`src`目录下的`nf_tables.c`、`netlink.c`两个c文件,以及其`inc`目录下的`log.h nf_tables.hnetlink.h三个头文件和我们的exp.c`文件放到一起,然后通过如下命令进行编译:

gcc ./exp.c ./nf_tables.c ./netlink.c -o test --static -masm=intel

就可以调用这三个接口函数了!😊

首先我们记得在自己的exp.c中创建新的namespace,然后创建一个socket返回文件描述符:

#include <sys/mman.h>
#include <sys/socket.h>
#include <linux/if_packet.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <netinet/if_ether.h>

void err_exit(char *s){
perror(s);
exit(-1);
}
void unshare_setup(void)
{
char edit[0x100];
int tmp_fd;

if(unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET))
err_exit("FAILED to create a new namespace");

tmp_fd = open("/proc/self/setgroups", O_WRONLY);
write(tmp_fd, "deny", strlen("deny"));
close(tmp_fd);

tmp_fd = open("/proc/self/uid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", getuid());
write(tmp_fd, edit, strlen(edit));
close(tmp_fd);

tmp_fd = open("/proc/self/gid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", getgid());
write(tmp_fd, edit, strlen(edit));
close(tmp_fd);
}

#include <sys/socket.h>
#include <linux/netlink.h>

int main(){
save_status();
bindCore(0);
unshare_setup();

int sock;
if ((sock = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_NETFILTER)) < 0) {
perror("socket");
}
printf("[+] Netlink socket created\n");
}

创建table

之后调用create_table创建一个table:

#include <sys/socket.h>
#include <linux/netlink.h>

int main(){
save_status();
bindCore(0);
unshare_setup();

int sock;
if ((sock = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_NETFILTER)) < 0) {
perror("socket");
}
printf("[+] Netlink socket created\n");

create_table(sock, "my_table");


}

table的分配点在这里,函数为nf_tables_newtablehttps://elixir.bootlin.com/linux/v5.10/source/net/netfilter/nf_tables_api.c#L1028

其cache是kmalloc-512:(笔者这个版本是5.10,没有cg隔离,但是GFP_KERNEL无所谓)

table_name有一个任意大小的分配:

创建set

然后是调用create_set函数给我们的table添加一个set:

int main(){
//......
create_table(sock, "my_table");
create_set(sock, "my_set", 0x30, 0x30, "my_table", 0);
}

第一个参数是sock;

第二个参数是新创建的set的名字;

第三个参数是keylen;

第四个参数是datalen;

第五个参数是table的名字,我们的set要挂到table上;

第六个参数是id,可能是以后用于在table上查找set用的;

set的添加位于这个函数:https://elixir.bootlin.com/linux/v5.10/source/net/netfilter/nf_tables_api.c#L4103

首先是检查一堆flag,以及desc,这里就涉及我们传入的klen、dlen:

set的分配在这里:https://elixir.bootlin.com/linux/v5.10/source/net/netfilter/nf_tables_api.c#L4265

可以看到这个分配来自kmalloc,然后size和udlen是两个变量,经过调试发现这个sizeof(*set)是0x100;size是0xe0;(这里如何改变长度以后再看)

之后会有如下几个操作:

首先拷贝udata数据,然后初始化set的各成员,调用ops->init(这个在CVE-2022-34918中可能会用到,这里先按下不表),之后nft_trans_set_add干了啥咱也不知道,反正最后会将这个新创建的set挂到table的sets链表上,table的引用计数use+1。

expr的分配

源码就在创建set的末尾:https://elixir.bootlin.com/linux/v5.10/source/net/netfilter/nf_tables_api.c#L4281

gc_init调试结果如下:

创建elem

相应代码如下:

int main(){

//......

create_table(sock, "my_table");
create_set(sock, "my_set@@", 0x30, 0x30, "my_table", 0);
//void add_elem_to_set(int sock, const char *set_name, uint32_t set_keylen, const char *table_name, uint32_t id, uint32_t data_len, uint8_t *data)
add_elem_to_set(sock, "my_set@@", 0x30, "my_table", 0, 0x30, "AAAAAAAABBBBBBBB");

}

这里有个巨大的坑:

在add_elem_to_set中,使用的是set_str8_attr来设置set_name,这样子就会在netlink的消息中生成一个固定的8字节的字符串,netlink的消息中会有4字节的元数据,后边跟上具体内容的,然后这里固定是8,总长度就是12,最后解包出来得到8,然后和6字节的“my_set”作比较就会出错。

虽然create_set中也是用的同样的函数:

但是问题是在内核中创建set的时候会以字符串的角度copy这个name,然后不会管你netlink里的长度,遇到00就不复制了。🤔

调用链如下:

nf_tables_newsetelem -> nft_add_set_elem->``

tmpl

首先关注tmpl,其结构定义如下:

其相关的函数有nft_set_ext_add_lengthnft_set_ext_add

len代表具体数据的长度,nft_set_ext_types[id].len则代表

add_length要加元数据+content,而add则只加一个固定长度;

然后在nft_add_set_elem函数中就会添加tmpl:

NFT_SET_EXT_FLAGS (可选,根据flags)
NFT_SET_EXT_KEY(必选)
NFT_SET_EXT_KEY_END (可选)
NFT_SET_EXT_TIMEOUT(可选)
NFT_SET_EXT_EXPR(可选)
NFT_SET_EXT_OBJREF(可选)
NFT_SET_EXT_DATA(可选)
NFT_SET_EXT_USERDATA(可选)

总之就是每添加一个扩展元素,tmpl就要在对应的offset数组中记录当前长度(也就是这个新增的元素在ext中的偏移),然后更新tmpl的总长度(增加元素的len,和types数组中的len)

elem

elem的创建位置在这里:https://elixir.bootlin.com/linux/v5.10/source/net/netfilter/nf_tables_api.c#L5069

ext结构体定义如下:

参考

https://github.com/randorisec/CVE-2022-34918-LPE-PoC/blob/main/src/nf_tables.c

https://github.com/randorisec/CVE-2022-34918-LPE-PoC


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