Skip to content
CVE-2022

CVE-2022

Sploitus • September 28, 2026

##

# CVE-2022-25636 повышение привилегий в netfilter ядра

Воздействие: в модуле ядра netfilter присутствует запись за границы кучи; при наличии SYS_ADMIN возможет подъём привилегий.

Уязвимость находится в модуле ядра netfilter, код в трёх .ko файлах.

При запуске через qemu возникают проблемы — модули не загружаются. Используется двухсторонняя отладка через vmware.

В ubuntu 21.10 можно вручную заменить ядро:

apt-get install linux-image-5.13.0-30-generic

Затем удалить исходное ядро и скомпилировать exp:

git clone

Результат повышения привилегий (успешность менее 50%):

![image-20220318151010142](

Уязвимая функция — `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,

/* nft_flow_rule_destroy() releases the reference on this device. */

entry = &flow->rule->action.entries[ctx->num_actions++];// выход за границы

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,

while (nft_expr_more(rule, expr)) {// вычисление num_actions на основе переданного правила

if (expr->ops->offload_flags & NFT_OFFLOAD_F_ACTION)

num_actions++;// учитываются только элементы с флагом NFT_OFFLOAD_F_ACTION

flow = nft_flow_rule_alloc(num_actions);// выделение памяти под num_actions (структура переменной длины)

ctx = kzalloc(sizeof(struct nft_offload_ctx), GFP_KERNEL);

if (!expr->ops->offload) {// вызов offload по количеству правил

err = expr->ops->offload(ctx, flow, 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. В итоге превышает размер массива , что приводит к выходу за границы.

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_reg regs[NFT_REG32_15 + 1];

* nft_dup_netdev_offload/nft_fwd_netdev_offload

Ссылка:

В этом письме объясняется, как использовать netfilter с помощью библиотек C libmnl и libnftnl. Ключевой момент для триггера уязвимости — наличие флага `NFT_OFFLOAD_F_ACTION` у добавляемых правил. Только правила, добавленные через `nftnl_expr_alloc("immediate");`, имеют этот флаг:

for(int i = 0; i rule->action.entries[ctx->num_actions++];// выход за границы

entry->dev = dev; // запись адреса кучи по фиксированному смещению, dev — struct net_device

Что касается структуры `struct flow_rule`, так как она переменной длины, размер выделяемой памяти определяет возможность эксплуатации (успех).

* Если передаётся только одно правило, размер структуры равен 0x70, что относится к kmalloc-128 (0x80). При выходе за границы указатель dev будет записан по смещению 0x88, т.е. на 0x8 байт за границей.

* Если передать два правила, размер структуры будет 0xC0, что точно соответствует kmalloc-192 (0xC0). При выходе за границы указатель dev будет записан по смещению 0xd8, т.е. на 0x18 байт за границей; при двух выходах за границы — по смещению 0x18 + 0x50 и т.д.

unsigned int used_keys; /* каждый бит означает наличие одного идентификатора ключа */

unsigned short int offset[FLOW_DISSECTOR_KEY_MAX];

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] ( Выполняется heap spray сообщениями размером 0x1040; из-за структуры сообщение делится на две части: вторая часть msg имеет длину 0x70, плюс заголовок — всё это выделяется через kmalloc-128. Затем одно сообщение освобождается, освобождая одну область kmalloc-128. После этого с помощью структуры `flow_rule` с одним правилом (также kmalloc-128) пытаемся занять освобождённую вторую часть сообщения, формируя следующую компоновку кучи:

![image-20220320175028938](

Если `flow_rule` получает только что освобождённую структуру `msg_msgseg`, то с высокой вероятностью он оказывается рядом с другими разбросанными `msg_msgseg`. При одном выходе за границы по смещению 0x8 будет записан указатель на кучу `net_device` (указатель dev). Достаточно принять все разбросанные сообщения, чтобы прочитать этот указатель, завершив утечку адреса для последующей эксплуатации без паники.

### UAF с помощью setxattr для утечки kaslr

Далее необходимо утечь kaslr и получить базовый адрес ядра. Используется тот же метод: `msg_msg` \+ heap spray. Разбрасывается много сообщений kmalloc-192, на этот раз цель — первая часть `msg_msg`. Затем аналогично освобождается одно из них и выделяется `flow_rule`, чтобы постараться сформировать следующую компоновку кучи:

![image-20220318184800730](

На этот раз используется структура `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))

void security_msg_msg_free(struct msg_msg *msg)

call_void_hook(msg_msg_free_security, msg);

Таким образом, при приёме только что разбросанных сообщений будет освобождён указатель `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`:

char name[IFNAMSIZ]; // изменяется для проверки правильности замены

const struct net_device_ops *netdev_ops;// инициализируется; используется для утечки адреса ядра

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:

![image-20220318151010142](

Письмо:

Документация автора:

exp:

Extracted Entities

Attack Types (1)

Platforms (1)

Tools (1)