nft_chain can be looked up by name, handle or ID. Let's go through the functions that do the job.
Lookup by name:
static struct nft_chain *nft_chain_lookup(struct net *net,
struct nft_table *table,
const struct nlattr *nla, u8 genmask)
{
char search[NFT_CHAIN_MAXNAMELEN + 1];
struct rhlist_head *tmp, *list;
struct nft_chain *chain;
if (nla == NULL)
return ERR_PTR(-EINVAL);
nla_strscpy(search, nla, sizeof(search));
WARN_ON(!rcu_read_lock_held() &&
!lockdep_commit_lock_is_held(net));
chain = ERR_PTR(-ENOENT);
rcu_read_lock();
list = rhltable_lookup(&table->chains_ht, search, nft_chain_ht_params);
if (!list)
goto out_unlock;
rhl_for_each_entry_rcu(chain, tmp, list, rhlhead) {
if (nft_active_genmask(chain, genmask))
goto out_unlock;
}
chain = ERR_PTR(-ENOENT);
out_unlock:
rcu_read_unlock();
return chain;
}
Lookup by handle:
static struct nft_chain *
nft_chain_lookup_byhandle(const struct nft_table *table, u64 handle, u8 genmask)
{
struct nft_chain *chain;
list_for_each_entry(chain, &table->chains, list) {
if (chain->handle == handle &&
nft_active_genmask(chain, genmask))
return chain;
}
return ERR_PTR(-ENOENT);
}
Lookup by ID:
static struct nft_chain *nft_chain_lookup_byid(const struct net *net,
const struct nft_table *table,
const struct nlattr *nla)
{
struct nftables_pernet *nft_net = nft_pernet(net);
u32 id = ntohl(nla_get_be32(nla));
struct nft_trans *trans;
list_for_each_entry(trans, &nft_net->commit_list, list) {
struct nft_chain *chain = trans->ctx.chain;
if (trans->msg_type == NFT_MSG_NEWCHAIN &&
chain->table == table &&
id == nft_trans_chain_id(trans))
return chain;
}
return ERR_PTR(-ENOENT);
}
In both nft_chain_lookup and nft_chain_lookup_byhandle, they check if the chain is active by calling nft_active_genmask. A chain will be deactivated if the user send a DELETE message for that chain. This check ensures that another object will not be able to refer to a deactivated chain. However in nft_chain_lookup_byid, the check will not be conducted. That means we can refer to a deactivated chain. But at cleanup stage, if chain->use is not 0, a warning will be issued and the chain won't be freed. We must find a way to make a reference to a deactivated chain while still satisfy the condition to free it.
Netfilter transaction will not free the deleted objects when commiting. Instead, Netfilter will run a deferred task to delete it later. Therefore, we can achieve Use-After-Free condition like this:
Batch 1:
- Create table
- Create chain victim
- Mark chain victim as deleted
- Create chain attack
- Create rule belong to attack chain, with a nft_immediate expression refer to victim by ID => victim->use == 1
- Commit the batch => Cleanup task will be queued
Batch 2:
- Mark the rule we created in the previous batch as deleted => victim->use == 0
- Wait for the cleanup task to complete => victim will be freed
- Fail the batch using some invalid input => the rule will not be marked as deleted anymore
The nft_immediate expression in the rule still refer to the freed chain. We have achieved Use-After-Free condition. From here we can spray fake chain object to leak (using nft_immediate dump function) or to execute code (need to create fake rule and fake expression as well to call expression ops). This primitive can be used multiple times reliably.
Unavailable Comments