• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья CVE-2022-34918 Брандмауэр Linux трещит по швам

вавилонец

CPU register
Пользователь
Регистрация
17.06.2021
Сообщения
1 116
Реакции
1 265
ОРИГИНАЛЬНАЯ СТАТЬЯ
ПЕРЕВЕДЕНО СПЕЦИАЛЬНО ДЛЯ xss.pro
$600 на SSD для Solidity hacking by Jolah Milovsky---> 0x5B1f2Ac9cF5616D9d7F1819d1519912e85eb5C09

В нашей предыдущей статье Еще одна ошибка в Netfilter » я описал уязвимость, обнаруженную в подсистеме netfilter ядра Linux. Во время моего расследования я обнаружил странное сравнение, которое не полностью защищает копию в буфере. Это привело к переполнению буфера кучи, которое использовалось для получения привилегий суперпользователя в Ubuntu 22.04.

Небольшой скачок в прошлое


В последнем эпизоде мы достигли предела в nft_set структуре ( /include/net/netfilter/nf_tables.h).

Код:
struct nft_set {
    struct list_head        list;
    struct list_head        bindings;
    struct nft_table        *table;
    possible_net_t          net;
    char                          *name;
    u64                            handle;
    u32                            ktype;
    u32                            dtype;
    u32                           objtype;
    u32                             size;
    u8                               field_len[NFT_REG32_COUNT];
    u8                               field_count;
    u32                                use;
    atomic_t                     nelems;
    u32                             ndeact;
    u64                             timeout;
    u32                            gc_int;
    u16                             policy;
    u16                             udlen;
    unsigned char           *udata;
    /* runtime data below here */
    const struct nft_set_ops    *ops ____cacheline_aligned;
    u16                              flags:14,
                                       genmask:2;
    u8                               klen;
    u8                               dlen;
    u8                              num_exprs;
    struct nft_expr         *exprs[NFT_SET_EXPR_MAX];
    struct list_head        catchall_list;
    unsigned char           data[]
        __attribute__((aligned(__alignof__(u64))));
};

В nft_setс одержится много данных, некоторые другие поля этой структуры могут быть использованы для получения лучшего примитива записи. Я решил поискать по полям длины ( udlen, klenа также dlen), потому что может быть полезно выполнить некоторые переполнения.


Исследование кода и аномалий


Изучение различных подходов к полю dlen, звонок в memcpyфункция (1)в nft_set_elem_init( /net/netfilter/nf_tables_api.c) держи мое внимание.

Код:
void *nft_set_elem_init(const struct nft_set *set,
const struct nft_set_ext_tmpl *tmpl,
const u32 *key, const u32 *key_end,
const u32 *data, u64 timeout, u64 expiration, gfp_t gfp)
{
struct nft_set_ext *ext;
void *elem;

elem = kzalloc(set->ops->elemsize + tmpl->len, gfp);                <===== (0)
if (elem == NULL)
return NULL;

ext = nft_set_elem_ext(set, elem);
nft_set_ext_init(ext, tmpl);

if (nft_set_ext_exists(ext, NFT_SET_EXT_KEY))
memcpy(nft_set_ext_key(ext), key, set->klen);
if (nft_set_ext_exists(ext, NFT_SET_EXT_KEY_END))
memcpy(nft_set_ext_key_end(ext), key_end, set->klen);
if (nft_set_ext_exists(ext, NFT_SET_EXT_DATA))
memcpy(nft_set_ext_data(ext), data, set->dlen);                 <===== (1)

...

return elem;
}

Этот вызов подозрительный, поскольку используются два разных объекта. Буфер назначения хранится в nft_set_extобъект, ext, тогда как размер копии извлекается из nft_set объект. Объект ext динамически размещается в (0)с elem и размер, зарезервированный для него, tmpl->len. Я хотел проверить, что значение, хранящееся в set->dlen используется для вычисления значения, хранящегося в tmpl->len.


Не то место


nft_set_elem_init называется )внутри функции nft_add_set_elem( /net/netfilter/nf_tables_api.c), который отвечает за добавление элемента в набор сетевых фильтров.

Код:
static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set,

const struct nlattr *attr, u32 nlmsg_flags)
{
struct nlattr *nla[NFTA_SET_ELEM_MAX + 1];
struct nft_set_ext_tmpl tmpl;
struct nft_set_elem elem;                                                   <===== (2)
struct nft_data_desc desc;

...

if (nla[NFTA_SET_ELEM_DATA] != NULL) {
err = nft_setelem_parse_data(ctx, set, &desc, &elem.data.val,           <===== (3)
nla[NFTA_SET_ELEM_DATA]);
if (err < 0)
goto err_parse_key_end;

...

nft_set_ext_add_length(&tmpl, NFT_SET_EXT_DATA, desc.len);              <===== (4)
}

...

err = -ENOMEM;
elem.priv = nft_set_elem_init(set, &tmpl, elem.key.val.data,                <===== (5)
elem.key_end.val.data, elem.data.val.data,
timeout, expiration, GFP_KERNEL);
if (elem.priv == NULL)
goto err_parse_data;

...
Как вы можете заметить, set->dlen не используется для резервирования места для данных, связанных с идентификатором NFT_SET_EXT_DATA, вместо этого desc.len (5). desc инициализируется внутри функции nft_setelem_parse_data( /net/netfilter/nf_tables_api.c)

Код:
static int nft_setelem_parse_data(struct nft_ctx *ctx, struct nft_set *set,
struct nft_data_desc *desc,
struct nft_data *data,
struct nlattr *attr)
{
int err;

err = nft_data_init(ctx, data, NFT_DATA_VALUE_MAXLEN, desc, attr);          <===== (6)
if (err < 0)
return err;

if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) {             <===== (7)
nft_data_release(data, desc->type);
return -EINVAL;
}

return 0;
}

Прежде всего, data а также desc заполнены nft_data_init( /net/netfilter/nf_tables_api.c) по данным, предоставленным пользователем. Важнейшей частью является проверка между desc->lenа также set->dlen в, это происходит только в том случае, если данные, связанные с добавленным элементом, имеют тип, отличный от NFT_DATA_VERDICT.


Однако, set->dlenконтролируется пользователем при создании нового набора. Единственное ограничение состоит в том, что set->dlenдолжен быть меньше 64 байт, а тип данных должен отличаться от NFT_DATA_VERDICT. Более того, когда desc->typeравно NFT_DATA_VERDICT, desc->lenравен 16 байтам.

Добавление элемента типа NFT_DATA_VERDICT в набор с типом данных NFT_DATA_VALUE обычно приводят к desc->len отличается от set->dlen. Следовательно, можно выполнить переполнение буфера кучи в nft_set_elem_init в. Это переполнение буфера может быть расширено до 48 байт.


Оставайся здесь ! Я скоро вернусь !


Тем не менее, это не стандартное переполнение буфера, когда пользователь может напрямую управлять переполняющимися данными. В этом случае случайные данные будут скопированы из выделенного буфера.
Если мы проверим вызов nft_set_elem_init (5), можно заметить, что скопированные данные извлекаются из локальной переменной elem, который является nft_set_elem объект.

Код:
struct nft_set_elem elem;                                                   <===== (2)

...

elem.priv = nft_set_elem_init(set, &tmpl, elem.key.val.data,                <===== (5)
elem.key_end.val.data, elem.data.val.data,
timeout, expiration, GFP_KERNEL);
nft_set_elem( /net/netfilter/nf_tables.h) используются для хранения информации о новых элементах во время их создания.


#define NFT_DATA_VALUE_MAXLEN   64

struct nft_verdict {
u32             code;
struct nft_chain        *chain;
};

struct nft_data {
union {
u32 data[4];
struct nft_verdict verdict;
};
} __attribute__((aligned(__alignof__(u64))));

struct nft_set_elem {
union {
u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
struct nft_data val;
} key;
union {
u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
struct nft_data val;
} key_end;
union {
u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
struct nft_data val;
} data;
void            *priv;
};
Как видите, 64 байта зарезервированы для временного хранения данных, связанных с новым элементом. Однако в него записывается не более 16 байт. elem.dataкогда срабатывает переполнение буфера. Поэтому при переполнении используются случайные байты.


Наконец, не так уж случайно


В этот момент я обнаружил переполнение буфера без контроля над данными, используемыми для повреждения. Создание эксплойта с неконтролируемым переполнением буфера — непростая задача.

elem.data используемый в переполнении не инициализирован

Давайте посмотрим на вызывающую функцию, возможно, ранее вызванная функция может помочь контролировать данные, используемые для переполнения. nft_add_set_elem вызывается nf_tables_newsetelem( /net/netfilter/nf_tables_api.c) для каждого элемента, который пользователь хочет добавить в набор.


Код:
static int nf_tables_newsetelem(struct sk_buff *skb,
const struct nfnl_info *info,
const struct nlattr * const nla[])
{
...

nla_for_each_nested(attr, nla[NFTA_SET_ELEM_LIST_ELEMENTS], rem) {
err = nft_add_set_elem(&ctx, set, attr, info->nlh->nlmsg_flags);
if (err < 0) {
NL_SET_BAD_ATTR(extack, attr);
return err;
}
}
}

nla_for_each_nested используется для перебора атрибутов, отправленных пользователем, поэтому пользователь может контролировать количество выполняемых итераций. А также nla_for_each_nested использует только макросы и встроенные функции, поэтому вызов nft_add_set_elemможет непосредственно сопровождаться другим вызовом nft_add_set_elem. Это очень полезно, потому что позволяет использовать данные предыдущего элемента в переполнении, т.к. elem.dataне инициализируется. Кроме того, можно игнорировать рандомизацию расположения стека. Следовательно, способ управления переполнением не зависит от компиляции ядра.

Следующая схема суммирует различные этапы elem.dataвнутри стека для создания управляемого переполнения.

1663866348513.png


Случайные данные сохраняются в стеке, добавляя новый элемент с NFT_DATA_VALUEданные ведут к данным, контролируемым пользователем, в стеке. Наконец, добавление второго элемента с NFT_DATA_VERDICT данные вызовут переполнение буфера, а остаток данных последнего элемента будет скопирован во время переполнения.

Выбор кеша

Последнее, что мы не обсудили перед разработкой моей стратегии эксплуатации, — это кеш, в котором происходит переполнение. elem, размещенный в (0), зависит от различных опций, выбранных пользователем, как показано в предыдущем фрагменте функции nft_add_set_elem, его размер может варьироваться. Есть несколько вариантов, которые можно использовать для его увеличения, например, NFT_SET_ELEM_KEYа также NFT_SET_ELEM_KEY_END. Они позволяют резервировать два буфера длиной до 64 байт в elem. Таким образом, это переполнение явно может произойти в нескольких кешах. elem размещен на Ubuntu 22.04 с флагом GFP_KERNEL. Таким образом, соответствующие кеши kmalloc-{64,96,128,192}.
Теперь осталось только выровнять elemна размер объекта кеша, чтобы выполнить наилучшее переполнение. Следующая схема представляет собой построение elemвыровнять его на 64 байта.

1663866381777.png


Мы использовали следующую конструкцию, чтобы ориентироваться на kmalloc-64кеш:
  • 20 байт для заголовка объекта
  • 28 байт заполнения через NFT_SET_ELEM_KEY
  • 16 байт для хранения данных элемента типа NFT_DATA_VERDICT.

Дай мне утечку


Теперь, когда можно контролировать переполнение данных, следующим шагом будет поиск способа извлечения базы KASLR. Поскольку переполнение произошло только в kmalloc-xтайники, классика msg_msgобъекты не могут быть использованы для утечки информации, так как они размещены в kmalloc-cg-xтайники.

Мы посмотрели на user_key_payload( /include/keys/user-type.h) объекты, обычно используемые для хранения конфиденциальной пользовательской информации в пространстве ядра, представляют собой хорошую альтернативу. Они похожи на msg_msg объекты по своей структуре: заголовок с размером объекта, затем буфер с пользовательскими данными.
Код:
struct user_key_payload {
struct rcu_head rcu;        /* RCU destructor */
unsigned short  datalen;    /* length of this data */
char        data[] __aligned(__alignof__(u64)); /* actual data */
};
Эти объекты размещаются внутри функции user_preparse( /security/keys/user_defined.c)

Код:
int user_preparse(struct key_preparsed_payload *prep)
{
struct user_key_payload *upayload;
size_t datalen = prep->datalen;

if (datalen <= 0 || datalen > 32767 || !prep->data)
return -EINVAL;

upayload = kmalloc(sizeof(*upayload) + datalen, GFP_KERNEL);                <===== (6)
if (!upayload)
return -ENOMEM;

/* attach the data */
prep->quotalen = datalen;
prep->payload.data[0] = upayload;
upayload->datalen = datalen;
memcpy(upayload->data, prep->data, datalen);                                <===== (7)
return 0;
}
Распределение, сделанное в (6)учитывает длину данных, предоставленных пользователем. Затем данные сохраняются сразу после заголовка с вызовом memcpyв (7) Заголовок user_key_payloadобъекты имеют длину 24 байта, следовательно, их можно использовать для распыления нескольких кэшей, kmalloc-32к kmalloc-8k.


Как с msg_msgобъекты, цель состоит в том, чтобы перезаписать поле datalenс большим значением, чем исходное. При извлечении сохраненной информации поврежденный объект вернет больше данных, чем изначально было предоставлено пользователем.


Однако у этого спрея есть существенный недостаток. Количество выделенных объектов ограничено. sysctlпеременная kernel.keys.maxkeysопределяет ограничение на количество разрешенных ключей в наборе ключей пользователя. Более того, kernel.keys.maxbytesограничивает количество хранимых байтов в связке ключей. Значения по умолчанию для этих переменных очень низкие. Они показаны ниже для Ubuntu 22.04.
Код:
kernel.keys.maxbytes = 20000
kernel.keys.maxkeys = 200

Утечка — это хорошо, а утечка с пользой — еще лучше


Поскольку я нашел способ утечки информации, следующим шагом будет поиск интересной информации. Работа в рамках kmalloc-64 кеш показался лучшим, это кеш с наименьшими объектами, где может произойти переполнение. Следовательно, может произойти утечка большего количества объектов.

Код:
percpu_ref_data( /include/linux/percpu-refcount.h) объекты также размещаются в этом кэше. Они представляют интерес, поскольку содержат два полезных типа указателей.


struct percpu_ref_data {
    atomic_long_t       count;
percpu_ref_func_t *release;
percpu_ref_func_t *confirm_switch;
bool            force_atomic:1;
bool            allow_reinit:1;
struct rcu_head rcu;
struct percpu_ref   *ref;
};
Эти объекты хранят указатели на функции (поля releaseа также confirm_switch), которые можно использовать для вычисления базы KASLR или базы модулей при их утечке, а также указателя на динамически размещаемый объект (поле ref) полезен для вычисления базы физической карты.


Такие объекты выделяются во время вызова percpu_ref_init( /lib/percpu-refcount.c).
Код:
int percpu_ref_init(struct percpu_ref *ref, percpu_ref_func_t *release,
unsigned int flags, gfp_t gfp)
{
struct percpu_ref_data *data;
   
    ...
   
data = kzalloc(sizeof(*ref->data), gfp);
   
    ...
   
data->release = release;
data->confirm_switch = NULL;
data->ref = ref;
ref->data = data;
return 0;
}
Самый простой способ выделить percpu_ref_dataобъектов заключается в использовании io_uring_setupсистемный вызов ( /fs/io_uring.c). А для того, чтобы запрограммировать освобождение такого объекта, достаточно простого вызова метода closeдостаточно системного вызова.


Выделение percpu_ref_data объект выполняется во время инициализации io_ring_ctx объект ( /fs/io_uring.c) внутри функции io_ring_ctx_alloc( /fs/io_uring.c).

Код:
static __cold struct io_ring_ctx *io_ring_ctx_alloc(struct io_uring_params *p)
{
struct io_ring_ctx *ctx;
   
    ...
   
if (percpu_ref_init(&ctx->refs, io_ring_ctx_ref_free,
                PERCPU_REF_ALLOW_REINIT, GFP_KERNEL))
goto err;

    ...
}
В качестве io_uring интегрирован в ядро Linux, утечка io_ring_ctx_ref_free( /fs/io_uring.c) позволяет вычислить базу KASLR.


Во время моего расследования некоторые неожиданные percpu_ref_data объекты были в утечке но с адресом функции io_rsrc_node_ref_zero( /fs/io_uring.c) в поле release. Проанализировав происхождение этих предметов, я понял, что они тоже происходят из io_uring_setup системный вызов. Этот хороший побочный эффект io_uring_setupsyscall позволил исправить утечку в моем эксплойте.

Я (G) корень​


Теперь, когда можно получить утечку полезной информации, необходим хороший примитив записи для повышения привилегий.
Несколько недель назад Лам Джун Ронг из Starlabs опубликовал статью , в которой описывается новый способ эксплуатации CVE-2021-41073. Он представляет новый примитив записи, атаку разъединения. Он основан на list_del. После повреждения list_head с двумя адресами, один адрес сохраняется по другому.

Как и в статье Л.Дж. Ронга, цель для list_headкоррупция в моем подвиге simple_xattrобъект.

Код:
struct simple_xattr {
struct list_head list;
char *name;
    size_t size;
char value[];
};
Для работы этого метода необходимо знать, какой объект был поврежден. В другом случае удаление случайных элементов из списка приводит к ошибке в обходе списка. Элементы в списке обозначаются своими именами.
Чтобы идентифицировать поврежденный объект, я выполняю трюк с nameполе: размещение nameпри длине, достаточной для резервирования 256 байтов, младший значащий байт возвращаемого адреса равен нулю. порядком байтов, такие как x86_64 , позволяют нам просто стереть младший значащий байт name после двух указателей в list_head. Следовательно, можно приготовить listполе для примитива записи и в то же время идентифицировать поврежденный объект, усекая его имя. Единственное требование состоит в том, чтобы все имена имели одинаковый конец.
Следующая схема резюмирует построение названия спрея с simple_xattr объекты.

1663866427739.png



Используя этот примитив записи, можно редактировать modprobe_pathс дорожкой в /tmp/папка. Это позволяет запускать любую программу с привилегиями root и пользоваться оболочкой root!
Запуск poc

Примечания​

Этот метод эксплуатации основан на гипотезе о том, что конкретный адрес отображается в пространстве ядра, что не всегда так. Таким образом, эксплойт не является полностью надежным, но все же имеет хорошие шансы на успех. Вторым недостатком атаки с отключением связи является паника ядра, которая возникает после завершения эксплойта. Этого можно было бы избежать, найдя объекты, которые могут остаться в памяти ядра в конце процесса эксплойта.

Конец истории​

Об этой уязвимости было сообщено команде безопасности Linux, и ей был присвоен код CVE-2022-34918. Они предложили патч, который я протестировал и рассмотрел, и он был выпущен в дереве основной ветки в рамках коммита 7e6bc1f6cabcd30aba0b11219d8e01b952eacbb6 .

Вывод​

Подводя итог, я обнаружил переполнение буфера кучи в подсистеме Netfilter ядра Linux. Эта уязвимость может быть использована для повышения привилегий в Ubuntu 22.04. Исходный код эксплойта доступен на нашем GitHub .
Я хотел бы поблагодарить RandoriSec за предоставленную мне возможность провести исследование уязвимостей внутри ядра Linux во время моей стажировки, а также мою исследовательскую группу за их советы.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх