##
# 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%):
 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) пытаемся занять освобождённую вторую часть сообщения, формируя следующую компоновку кучи:
. Достаточно принять все разбросанные сообщения, чтобы прочитать этот указатель, завершив утечку адреса для последующей эксплуатации без паники.
### UAF с помощью setxattr для утечки kaslr
Далее необходимо утечь kaslr и получить базовый адрес ядра. Используется тот же метод: `msg_msg` \+ heap spray. Разбрасывается много сообщений kmalloc-192, на этот раз цель — первая часть `msg_msg`. Затем аналогично освобождается одно из них и выделяется `flow_rule`, чтобы постараться сформировать следующую компоновку кучи:
(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:
