
Introduction
Netfilter is a complicated subsystem in Linux. As its name indicated, it was used to filter network packets by some rules. Hence, netfilter provides multiple data structures (polymorphisms) and abstraction to handle different filter rules. Essentially, the netfilter places hooks throughout the regular networking modules for which other modules can register handlers. Such a design makes the whole module complex and difficult to analyze; on the other hand, the user input makes exploitation possible.
In netfilter, nf_tables
reroutes packets based on user-defined rules. In nf_tables, a table (struct nft_table
) is a container associated with a specific protocol (e.g., ip
,ip6
, arp
). Then it will be processed in different handlers when a packet hits the route.
There are many other CVEs in the netfilter: such as CVE-2022-2586. It pertains to an expression referring to deleted set, which can cause UAF.
In this post, I will show the analysis of CVE-2022-32250, which is also a UAF.
General nf_tables
architecture
Noting a filter can have many rules, a table (struct nft_table
) can house a set of chains (struct nft_chain
). The chain defines what type of network traffic it is concerned about. nft_chain
also includes a container for an ordered set of rules (struct nft_rule
). The rules are yet another housing for expressions (struct nft_expr
). All these structures are “base classes”; in practice, each structure contains a pointer to the level above it (there is a struct nft_table
pointer in chains), and there are a lot of forced type conversions.
For example in struct nft_expr
:
1 | /** |
nft_expr
is an abstract class. It will then be converted to void*
by nft_expr_priv
. nft_expr_ops
is another base class that mainly defines some functions (like init
or destroy
) for a concrete expression (very object-oriented, but in C).
Base and inherited class (part)
Base (Abstract) | Instance |
nft_set | nft_set_hash |
nft_set_rbtree | |
nft_chain | nft_chain_filter_ipv4 |
nft_chain_filter_arp | |
nft_expr | nft_cmp_expr |
nft_immediate_expr | |
nft_lookup | |
nft_dynset |
Bug analysis
The CVE-2022-32250 is essentially a use-after-free bug. It occurs when processing nft_lookup
and nft_dynset
expressions (in some abnormal way), the freed object remains in the set->binding
linked list. So there are the following steps:
- create expression (
lookup
anddynset
) - add to linked list
- free the expression
- delete it from the linked list (not found or not executable)
Create expression
The story starts from nft_expr_init
, expr
is allocated in (1).
1 | static struct nft_expr *nft_expr_init(const struct nft_ctx *ctx, |
Then it will be initialized in nf_tables_newexpr
:
1 | static int nf_tables_newexpr(const struct nft_ctx *ctx, |
Context (Caller) | Function name | Possible instance |
nf_tables_newexpr | ops->init | nft_lookup_init |
nft_dynset_init | ||
nft_objref_map_init |
Add to linked list
In ops->init
(if it actually calls nft_lookup_init
, nft_dynset_init
, or nft_objref_map_init
), it will then call nf_tables_bind_set
, where add this object to its container’s list.
binding
is a field in some expressions (e.g., nft_lookup). It is added to the set(table)’s list at (2).
1 | int nf_tables_bind_set(const struct nft_ctx *ctx, struct nft_set *set, |
Free the expression
After nft_expr_init
, there is a check at (3). When it fails, the program will go to (4) and destroy this expression.
1 | struct nft_expr *nft_set_elem_expr_alloc(const struct nft_ctx *ctx, |
In nft_expr_destroy
, we can see the free of expr
(5)
1 | void nft_expr_destroy(const struct nft_ctx *ctx, struct nft_expr *expr) |
List delete?
nf_tables_expr_destroy
is a key function to analyze and we have to determine if it has deleted the relevant pointers before we call kfree
.
1 | static void nf_tables_expr_destroy(const struct nft_ctx *ctx, |
It calls the expr’s instance of destroy function, take nft_lookup
as an example:
1 | static void nft_lookup_destroy(const struct nft_ctx *ctx, |
It then call a shared function nf_tables_destroy_set
:
1 | void nf_tables_destroy_set(const struct nft_ctx *ctx, struct nft_set *set) |
Actually, here we can find list_empty(&set->bindings)
won’t come true, because we have just added one expr
to set->bindings
at (2). Hence, it won’t do anything and returns to the last function call. Finally, it causes the UAF.
But the path-insensitive analyzer won’t stop here. He has to investigate further:
More findings
nft_set_destroy
: calls nft_expr_destroy
and set->ops->destroy
1 | static void nft_set_destroy(const struct nft_ctx *ctx, struct nft_set *set) |
Since nft_expr_destroy
will go to (5) again! Whatever, let’s focus on set->ops->destroy
. Obviously, this indirect call can have multiple instances. Take net_set_hash
and net_set_rbtree
as two cases:
1 | static void nft_hash_destroy(const struct nft_set *set) |
1 | static void nft_rbtree_destroy(const struct nft_set *set) |
Basically, these destruction functions iterate over each of these elements, remove them from the set, and then call the public function nft_set_elem_destroy
at (6).
1 | void nft_set_elem_destroy(const struct nft_set *set, void *elem, |
This is the end of analysis. It isn’t destroy_clone
in nft_lookup
; it will call nf_tables_expr_destroy
(why is it always you) again. And in the whole procedure, there is no access to set->bindings
. (no usage)
In conclusion, the destroy should clear both set
and expr,
but there is a condition list_empty(&set->bindings)
. Then the set
remains but expr
freed, and make further UAF possible (though not explicit).
Discussion
Patch
The patch is very simple: the fault status was introduced by (3). Noting expr_info.ops->type->flags
is determined before so that we can move this check before creation for expr
.
1 | diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c |
Some thoughts
- knowledge for data struct, and its functions.
- Loop analysis: element traversal (list, tree), with condition
- combine with its function name? or access patterns (summary for each function)
- challenges:
- detect this kind of bug: freed pointers remain in some list, and can further be used without check! (implicit UAF?)
- not a false alarm for the patch?
- polymorphism