CVE-2022-32250 Review - Part1
Intro
이 글은 Theori에서 작성해 주신 글을 이해하면서 작성해간 노트이다.
이 취약점은 netfilter 에서 발생하는 UAF(Use-After-Free) 취약점이며, 해당 취약점을 이용해 권한 상승을 할 수 있다.
같이 본 코드는 linux-5.15.27 버전이다.
취약점을 찾기 앞서, 포스트에서는 nftables 에 대한 간략한 설명만 해주고 넘어간다.
Netfilter Structures
Theori 의 글을 보면 다음과 같이 netfilter 의 구조를 간략하게 설명한다.
nftables have tables, chains, rules, and expressions to store and process instructions. tables contain several chains and are linked to protocols such as IP and IP6. chains include several rules and the types of network traffic information to be processed. rules contain several expressions, and the information received by chains is evaluated as rules inside chains. expressions evaluate whether the input satisfies a set of conditions.
이 부분을 좀더 자세히 알아보자
nftable 이란 뭘까? nftable 은 netfilter에서 각 네트워크 프로토콜에 대한 핸들러를 지정하는 컨테이너이다.
filter 핸들러는 각 netfilter 후크 위치에 설치되고, 해당 후크에서 핸들러가 실행될 때마다 nftable의 처리가 시작된다.
리눅스 커널 내에서는 struct nft_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;
};
nftable 은 일련의 chain 들을 가지고 있다. chain 에는 특정 트래픽에 관련되어 어떠한 처리를 할 것인지에 대한 정보를 담고 있다.
앞서 특정 트래픽에 관련되어 어떤 행동을 가질지 결정하는 정보들이 rule 이다. 즉, chain은 여러 rule 의 집합이다.
리눅스 커널 내에서는 struct nft_chain 으로 정의된다.
- include/net/netfilter/nf_table.c
struct nft_chain {
struct nft_rule *__rcu *rules_gen_0;
struct nft_rule *__rcu *rules_gen_1;
struct list_head rules;
struct list_head list;
struct rhlist_head rhlhead;
struct nft_table *table;
u64 handle;
u32 use;
u8 flags:5,
bound:1,
genmask:2;
char *name;
u16 udlen;
u8 *udata;
/* Only used during control plane commit phase: */
struct nft_rule **rules_next;
};
소스롤 보면 nft_rule 및 rules 가 리스트 형식으로 정의되어 있는 것을 알 수 있다.
rule 은 커널 내에서 struct nft_rule로 정의된다.
- include/net/netfilter/nf_table.c
struct nft_rule {
struct list_head list;
u64 handle:42,
genmask:2,
dlen:12,
udata:1;
unsigned char data[]
__attribute__((aligned(__alignof__(struct nft_expr))));
};
rule은 특정 패킷에 대한 지정자인 표현식을 가지고 있다. 그러한 설명식이 expression 이다.
expression 은 차례대로 반복되고 실행되서 실제 처리 작업을 진행한다.
즉, 특정 패킷이 들어오면 nftable에 있는 여러 chain들이 순서에 따라 처리되고, chain은 가지고 있는 rule들에 대한 처리를 시작하며, rule은 자신이 가지고 있는 expression에 지정되어있는 행동을 한다.

그럼 정확히 expression 은 어떤 표현에 대한 것을 어떻게 처리할까
expression은 커널 내에서 struct nft_expr로 정의된다.
- include/net/netfilter/nf_table.c
struct nft_expr {
const struct nft_expr_ops *ops;
unsigned char data[]
__attribute__((aligned(__alignof__(u64))));
};
ops는 expression의 구체적인 구현에 대한 포인터이고, data는 각 expression 에 대한 구조체가 들어간다.
data의 예시로, expression 중 하나인 immediate expression 의 구조체를 살펴보자.
- include/net/netfilter/nf_tables_core.h
struct nft_immediate_expr {
struct nft_data data;
u8 dreg;
u8 dlen;
};
다음은 lookup expression 의 구조체이다.
- net/netfilter/nft_lookup.c
struct nft_lookup {
struct nft_set *set;
u8 sreg;
u8 dreg;
bool invert;
struct nft_set_binding binding;
};
이렇게 expression 마다 다른 구조체가 정의 되어 있고, 그 구조체는 nft_expr의 data필드에 등록된다.
이번엔 ops 필드의 구조체인 struct nft_expr_ops를 보자.
- include/net/netfilter/nf_tables.c
struct nft_expr_ops {
void (*eval)(const struct nft_expr *expr,
struct nft_regs *regs,
const struct nft_pktinfo *pkt);
int (*clone)(struct nft_expr *dst,
const struct nft_expr *src);
unsigned int size;
int (*init)(const struct nft_ctx *ctx,
const struct nft_expr *expr,
const struct nlattr * const tb[]);
void (*activate)(const struct nft_ctx *ctx,
const struct nft_expr *expr);
void (*deactivate)(const struct nft_ctx *ctx,
const struct nft_expr *expr,
enum nft_trans_phase phase);
void (*destroy)(const struct nft_ctx *ctx,
const struct nft_expr *expr);
void (*destroy_clone)(const struct nft_ctx *ctx,
const struct nft_expr *expr);
int (*dump)(struct sk_buff *skb,
const struct nft_expr *expr);
int (*validate)(const struct nft_ctx *ctx,
const struct nft_expr *expr,
const struct nft_data **data);
bool (*gc)(struct net *net,
const struct nft_expr *expr);
int (*offload)(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
const struct nft_expr *expr);
bool (*offload_action)(const struct nft_expr *expr);
void (*offload_stats)(struct nft_expr *expr,
const struct flow_stats *stats);
const struct nft_expr_type *type;
void *data;
};
실제 expresesion 에 대한 구현 함수의 포인터들이 들어가 있는 것을 확인 할 수 있다. 여기서 size 필드는 sizeof(struct nft_expr_ops) + sizeof(<struct expr_data>) 값이다.
Vulnerability
netfilter의 모든 처리 과정을 이해한 것은 아니지만, netfilter가 사용하는 구조체에 대해서 어느정도 이해했다.
netfilter에 대해 더 자세히 정보가 필요하면 이 링크를 통해 알아보자
이제 해당 취약점이 어떻게 발생하는지 알아보자.
해당 취약점은 nf_tables_api.c 의 nft_set_elem_expr_alloc() 함수에서 시작된다.
- net/netfilter/nf_tables_api.c
struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
const struct nft_set *set,
const struct nlattr *attr)
{
struct nft_expr *expr;
int err;
expr = nft_expr_init(ctx, attr);
if (IS_ERR(expr))
return expr;
err = -EOPNOTSUPP;
if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
goto err_set_elem_expr;
if (expr->ops->type->flags & NFT_EXPR_GC) {
if (set->flags & NFT_SET_TIMEOUT)
goto err_set_elem_expr;
if (!set->ops->gc_init)
goto err_set_elem_expr;
set->ops->gc_init(set);
}
return expr;
err_set_elem_expr:
nft_expr_destroy(ctx, expr);
return ERR_PTR(err);
}
nft_set_elem_expr_alloc()함수는 nft_expr_init()함수를 호출한다.
- net/netfilter/nf_tables_api.c
static struct nft_expr *nft_expr_init(const struct nft_ctx *ctx,
const struct nlattr *nla)
{
struct nft_expr_info expr_info;
struct nft_expr *expr;
struct module *owner;
int err;
err = nf_tables_expr_parse(ctx, nla, &expr_info);
if (err < 0)
goto err1;
err = -ENOMEM;
expr = kzalloc(expr_info.ops->size, GFP_KERNEL);
if (expr == NULL)
goto err2;
err = nf_tables_newexpr(ctx, &expr_info, expr);
if (err < 0)
goto err3;
return expr;
err3:
kfree(expr);
err2:
owner = expr_info.ops->type->owner;
if (expr_info.ops->type->release_ops)
expr_info.ops->type->release_ops(expr_info.ops);
module_put(owner);
err1:
return ERR_PTR(err);
}
nft_expr_init() 함수는 nf_tables_expr_parse() 함수를 호출해 expr_info를 초기화 한 뒤, kzalloc() 함수를 이용해 expr 이라는 구조체를 할당한다.
그 후 nf_tables_newexpr() 함수에 expr_info 를 인자로 주어 expr 구조체를 초기화 한다. 이 때, nf_tables_newexpr() 함수는 expr_info->ops->init() 함수를 호출한다.
- net/netfilter/nf_tables_api.c
static int nf_tables_newexpr(const struct nft_ctx *ctx,
const struct nft_expr_info *expr_info,
struct nft_expr *expr)
{
const struct nft_expr_ops *ops = expr_info->ops;
int err;
expr->ops = ops;
if (ops->init) {
err = ops->init(ctx, expr, (const struct nlattr **)expr_info->tb);
if (err < 0)
goto err1;
}
return 0;
err1:
expr->ops = NULL;
return err;
}
struct nft_expr_info 구조체는 다음과 같이 정의된다.
- net/netfilter/nf_tables_api.c
struct nft_expr_info {
const struct nft_expr_ops *ops;
const struct nlattr *attr;
struct nlattr *tb[NFT_EXPR_MAXATTR + 1];
};
다시 짚고 넘어가기 위해 expr 의 구조체인 nft_expr를 살펴보자.
- include/net/netfilter/nf_tables.h
struct nft_expr {
const struct nft_expr_ops *ops;
unsigned char data[]
__attribute__((aligned(__alignof__(u64))));
};
ops 는 netfilter의 expression의 처리를 담당하는 함수들의 포인터이다.
data는 각 expression 에 맞는 구조체가 들어간다.
해당 취약점은 lookup 과 set_binding expression 에서 발생한다.
우리가 볼 expression 인 nft_lookup 과 nft_set_binding의 구조체는 다음과 같이 선언된다.
- net/netfilter/nft_lookup.c
struct nft_lookup {
struct nft_set *set;
u8 sreg;
u8 dreg;
bool invert;
struct nft_set_binding binding;
};
- net/netfilter/nf_tables.h
struct nft_set_binding {
struct list_head list;
const struct nft_chain *chain;
u32 flags;
};
그리고 nf_tables_newexpr() 함수에서 호출되는 expr_info->ops->init() 함수는 lookup expression 의 init 함수인 nft_lookup_init() 함수이다.
- net/netfilter/nft_lookup.c
static const struct nft_expr_ops nft_lookup_ops = {
.type = &nft_lookup_type,
.size = NFT_EXPR_SIZE(sizeof(struct nft_lookup)),
.eval = nft_lookup_eval,
.init = nft_lookup_init,
.activate = nft_lookup_activate,
.deactivate = nft_lookup_deactivate,
.destroy = nft_lookup_destroy,
.dump = nft_lookup_dump,
.validate = nft_lookup_validate,
};
- net/netfilter/nft_lookup.c
static int nft_lookup_init(const struct nft_ctx *ctx,
const struct nft_expr *expr,
const struct nlattr * const tb[])
{
struct nft_lookup *priv = nft_expr_priv(expr);
u8 genmask = nft_genmask_next(ctx->net);
struct nft_set *set;
u32 flags;
int err;
if (tb[NFTA_LOOKUP_SET] == NULL ||
tb[NFTA_LOOKUP_SREG] == NULL)
return -EINVAL;
set = nft_set_lookup_global(ctx->net, ctx->table, tb[NFTA_LOOKUP_SET],
tb[NFTA_LOOKUP_SET_ID], genmask);
if (IS_ERR(set))
return PTR_ERR(set);
err = nft_parse_register_load(tb[NFTA_LOOKUP_SREG], &priv->sreg,
set->klen);
if (err < 0)
return err;
if (tb[NFTA_LOOKUP_FLAGS]) {
flags = ntohl(nla_get_be32(tb[NFTA_LOOKUP_FLAGS]));
if (flags & ~NFT_LOOKUP_F_INV)
return -EINVAL;
if (flags & NFT_LOOKUP_F_INV) {
if (set->flags & NFT_SET_MAP)
return -EINVAL;
priv->invert = true;
}
}
if (tb[NFTA_LOOKUP_DREG] != NULL) {
if (priv->invert)
return -EINVAL;
if (!(set->flags & NFT_SET_MAP))
return -EINVAL;
err = nft_parse_register_store(ctx, tb[NFTA_LOOKUP_DREG],
&priv->dreg, NULL, set->dtype,
set->dlen);
if (err < 0)
return err;
} else if (set->flags & NFT_SET_MAP)
return -EINVAL;
priv->binding.flags = set->flags & NFT_SET_MAP;
err = nf_tables_bind_set(ctx, set, &priv->binding);
if (err < 0)
return err;
priv->set = set;
return 0;
}
nft_lookup_init() 함수는 nf_tables_bind_set() 함수를 호출한다.
- net/netfilter/nf_tables_api.c
int nf_tables_bind_set(const struct nft_ctx *ctx, struct nft_set *set,
struct nft_set_binding *binding)
{
struct nft_set_binding *i;
struct nft_set_iter iter;
if (set->use == UINT_MAX)
return -EOVERFLOW;
if (!list_empty(&set->bindings) && nft_set_is_anonymous(set))
return -EBUSY;
if (binding->flags & NFT_SET_MAP) {
/* If the set is already bound to the same chain all
* jumps are already validated for that chain.
*/
list_for_each_entry(i, &set->bindings, list) {
if (i->flags & NFT_SET_MAP &&
i->chain == binding->chain)
goto bind;
}
iter.genmask = nft_genmask_next(ctx->net);
iter.skip = 0;
iter.count = 0;
iter.err = 0;
iter.fn = nf_tables_bind_check_setelem;
set->ops->walk(ctx, set, &iter);
if (!iter.err)
iter.err = nft_set_catchall_bind_check(ctx, set);
if (iter.err < 0)
return iter.err;
}
bind:
binding->chain = ctx->chain;
list_add_tail_rcu(&binding->list, &set->bindings);
nft_set_trans_bind(ctx, set);
set->use++;
return 0;
}
nf_tables_bind_set() 함수는 bind: 라벨에서 nft_lookup 구조체의 binding 필드를 구성한다.
이렇게 nft_expr_init() 함수가 끝나면 nft_set_elem_expr_alloc() 함수로 리턴한다.
앞서 언급했듯, nft_set_elem_expr_alloc()함수가 nft_expr_init() 함수를 호출한다.
다시 nft_set_elem_expr_alloc() 함수를 보자.
- net/filter/nf_tables_api.c
struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx,
const struct nft_set *set,
const struct nlattr *attr)
{
struct nft_expr *expr;
int err;
expr = nft_expr_init(ctx, attr);
if (IS_ERR(expr))
return expr;
err = -EOPNOTSUPP;
if (!(expr->ops->type->flags & NFT_EXPR_STATEFUL))
goto err_set_elem_expr;
if (expr->ops->type->flags & NFT_EXPR_GC) {
if (set->flags & NFT_SET_TIMEOUT)
goto err_set_elem_expr;
if (!set->ops->gc_init)
goto err_set_elem_expr;
set->ops->gc_init(set);
}
return expr;
err_set_elem_expr:
nft_expr_destroy(ctx, expr);
return ERR_PTR(err);
}
nft_expr_init() 함수가 끝나고 나서 첫번째 분기를 보면 expr->ops->type->flags 값과 NFT_EXPR_STATEFUL 을 엔드한 값이 0이면 nft_expr_destroy() 함수를 호출한다.
이때, 지나온 과정 중 expr->ops->type->flags 값을 초기화 한 과정이 없어 expr->ops->type->flags 값은 0이 되고, nft_expr_destroy() 함수를 호출하게 된다.
nft_expr_destroy() 함수가 어떤일을 하는지 살펴보자.
- net/netfilter/nf_tables_api.c
void nft_expr_destroy(const struct nft_ctx *ctx, struct nft_expr *expr)
{
nf_tables_expr_destroy(ctx, expr);
kfree(expr);
}
nf_tables_expr_destroy() 함수를 호출 한 후 expr 구조체를 free 하는 것을 볼 수 있다.
nf_tables_expr_destroy() 함수가 무엇을 하는 지 보자.
- net/netfilter/nf_tables_api.c
static void nf_tables_expr_destroy(const struct nft_ctx *ctx,
struct nft_expr *expr)
{
const struct nft_expr_type *type = expr->ops->type;
if (expr->ops->destroy)
expr->ops->destroy(ctx, expr);
module_put(type->owner);
}
expr->ops->destory() 함수를 호출하는 것을 알 수 있는데, lookup expression 에서는 nft_lookup_destroy() 함수가 호출된다.
static void nft_lookup_destroy(const struct nft_ctx *ctx,
const struct nft_expr *expr)
{
struct nft_lookup *priv = nft_expr_priv(expr);
nf_tables_destroy_set(ctx, priv->set);
}
nft_lookup_destroy() 함수는 nf_tables_destroy_set() 함수를 호출하는 것을 알 수 있다. 이때 nft_expr_priv() 함수는 expr 구조체의 data 필드를 반환한다.
- include/net/netfilter/nf_tables.h
static inline void *nft_expr_priv(const struct nft_expr *expr)
{
return (void *)expr->data;
}
- net/netfilter/nf_tables_api.c
void nf_tables_destroy_set(const struct nft_ctx *ctx, struct nft_set *set)
{
if (list_empty(&set->bindings) && nft_set_is_anonymous(set))
nft_set_destroy(ctx, set);
}
EXPORT_SYMBOL_GPL(nf_tables_destroy_set);
nf_tables_destroy_set() 함수에서는 set->bindings 가 비어있으면 nft_set_destroy() 함수를 통해 set 을 초기화 한다.
lookup expression 을 nft_set_elem_expr_alloc함수를 통해 초기화 했을때, 위의 과정에서 처럼 binding 을 해주어 set-bindings 값이 비어있지 않으므로 set 은 초기화 되지 않는다.
근데 여기서 수상한 점이 하나 나온다. nft_expr_destroy() 함수를 다시보자.
void nft_expr_destroy(const struct nft_ctx *ctx, struct nft_expr *expr)
{
nf_tables_expr_destroy(ctx, expr);
kfree(expr);
}
nf_tables_expr_destory 안에서 expr->data->set 값이 destory 됐는지 확인을 하지 않고 expr 구조체를 free 하는 것을 알 수 있다!
따라서 만약에 우리가 다시 expr 을 초기화 한 후 set 값에 바인딩을 하게 된다면, use-after-free 취약점을 트리거 할 수 있다.
conclusion
처음으로 원데이를 분석해 보는데, 취약점이 유발되는 과정이 생각보다 복잡하고, 코드를 많이 공부해야 된다는 것을 실감할 수 있었다.
지금은 구조체만 어느정도 파악했지만, 실제 제로데이 취약점을 찾으려면 더 많은 부분을 공부해야 할 것이다.
천천히 한걸음씩 나아가자.
Part2 에서는 취약점의 PoC 를 작성해 나가는 것부터 써내려갈 예정이다.