Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2022-1015-1016 — Перевод на испанский язык CVE-2022-1015 и 1016, обнаруженных и задокументированных Дэвидом. | Kitploit
Инструменты/GitHubGitHub/zanezhub/cve-2022-1015-1016
Повышение привилегийАнализ уязвимостейЭксплуатацияОбучение и ОбразованиеЭксплуатация Бинарных Файлов
GitHubzanezhub/cve-2022-1015-1016

CVE-2022-1015-1016

Перевод на испанский язык CVE-2022-1015 и 1016, обнаруженных и задокументированных Дэвидом.

Репозиторий
14 лет назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться
Сайт

CVE-2022-1015 и CVE-2022-1026

Этот README.md является переводом блога Дэвида. Дэвид обнаружил CVE-1015 и CVE-1016 в ядре Linux. Вы можете посетить его веб-сайт, чтобы прочитать оригинальный документ.

Вот его социальные сети:

  • Twitter
  • Github

Анализ двух новых уязвимостей Linux в nf_tables

Опубликовано 2 апреля 2022 года.

  • CVE-2022-1015 позволяет выполнить доступ за пределами границ (out-of-bounds), вызванный недостаточной проверкой входных аргументов, что может привести к удалённому выполнению кода и локальному повышению привилегий.
  • CVE-2022-1016 связан с плохой инициализацией переменных, размещённых в стеке, что может быть использовано для утечки большого разнообразия данных ядра в пространство пользователя (userspace).

Эти проблемы должны быть эксплуатируемыми в конфигурациях по умолчанию самой новой версии Ubuntu и RHEL. Я написал свою Proof of Concept (PoC) для CVE-2022-1015, нацелившись на версию ядра 5.16-rc3 от Arch Linux.

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

Вот руководство по чтению:

  • Если вы здесь просто для того, чтобы почитать об уязвимости, начните с Раздела 4.
  • Если вы также хотите немного контекста о подсистеме ядра, начните с Раздела 2.
  • Если вам интересен ещё больший контекст, прочитайте весь документ.

1. Контекст

В середине февраля программа безопасности Google объявила о продолжении своей программы вознаграждений kCTF, предлагая награды от $31,337 до 91,337 долларов за эксплойт в ядре Linux, способный повысить привилегии до пользователя root из непривилегированных процессов в песочнице nsjail.

Будучи бедным студентом, это, очевидно, привлекло моё внимание. Это был мой первый поиск уязвимости в «реальном мире», но в своих приключениях с CTF со своей командой я ознакомился с ядром Linux с точки зрения безопасности. После долгих часов с очень малым, близким к нулю прогрессом (но с большим знанием о Linux), мне удалось найти некоторые уязвимости в модуле nf_tables.

К сожалению, в конечном итоге я понял, что этот модуль не был включён в правила kCTF от Google (поэтому я не получил никакого вознаграждения за эти две уязвимости). Но, очевидно, я всё равно сообщил о них и написал эксплойт LPE (локальное повышение привилегий) для CVE-2022-1015.

1.1 Определение цели и стратегия аудита

Итак, вы решили, что найдёте несколько уязвимостей в Linux. Что теперь? Linux — это огромный проект, и довольно легко не увидеть лес за деревьями (вы так сосредотачиваетесь на деталях, что теряете общее видение ситуации). Что ещё хуже, многие части не документированы, и вам нужно прочитать много кода, чтобы понять, что происходит.

Я начал с попытки получить детальное представление о модели безопасности Linux. Найти баг — это одно; но найти хороший баг — совсем другое. В конце концов, не все баги созданы равными:

  • Если баг требует привилегий root, то не существует значимого ограничения безопасности (если только не включена подпись модулей ядра).
    • Некоторые вещи, которые приходят мне на ум, — это многие модули (виртуальных) файловых систем. Только изначальный пользователь root может монтировать эти файловые системы. Исключением является vfs, где указано FS_USERNS_MOUNT; в этом случае вы можете монтировать их в пространстве имён пользователя.
  • Если к багу нельзя получить доступ через системные вызовы, он, вероятно, не будет эксплуатируемым.
    • Это относится ко многим драйверам оборудования, поскольку у вас нет физического доступа к машине. Низкоуровневые сетевые драйверы всё ещё могут быть хорошей целью, если вы можете, например, отправлять данные через Bluetooth или 802.11.ac.
    • Очевидно, это зависит от сценария, в котором вы находитесь.
  • Многие баги требуют CAP_SYS_ADMIN или CAP_NET_ADMIN.
    • Пространства имён пользователей включены по умолчанию, так что это не проблема.
    • В противном случае сначала вам придётся повысить привилегии до root в пространстве имён пользователя внутри контейнера.
  • Не все модули будут присутствовать на вашей цели.
    • Linux — это исключительно хорошо настраиваемый программный продукт, поэтому все конфигурации могут различаться множеством способов.
    • К конфигурации ядра обычно можно получить доступ через /proc/config.gz. Модули могут быть загружаемыми (=m) или скомпилированными отдельно и загружаемыми во время выполнения (=y).
    • Вы можете использовать и , но они не всегда надёжны, поскольку модули могут динамически загружаться в ядро (, ).

Эти ограничения помогают нам понять границы файловых систем, в которых мы можем искать уязвимости. Я думаю, что хорошая идея — потратить время на планирование атаки на желаемую цель.

Я уже извлёк урок из предыдущего пункта. Как я упоминал, модуль nf_tables не был загружен в экземпляре, предоставленном kCTF. Я мог бы понять это с самого начала и избавить себя от разочарования :p. С другой стороны, вероятно, вы не читали бы этот блог сейчас, если бы я понял это раньше; думаю, в конце концов всё сложилось хорошо.

Объяснение того, почему COS, форк Linux от Google, оптимизированный для контейнеров, не имел nf_tables, можно найти здесь и здесь.

1.2 nf_tables: почему?

После оценки вышеупомянутых пунктов я решил, что мой лучший путь для начала, вероятно, — посмотреть на исходный код сети. Многие интересные функции там требуют CAP_NET_ADMIN, но, как я упоминал, на самом деле это не проблема. Напротив, я подозреваю, что компоненты, требующие специальных возможностей, обычно менее безопасны, поскольку у разработчиков ядра может быть ложное чувство безопасности.

Я также приложил усилия, чтобы выбрать файловую систему, о которой хочу узнать больше; таким образом, даже если вы не найдёте ни одного бага, вы всё равно сможете узнать много интересного.

Я исследовал множество сетевых файловых систем, но не нашёл ничего важного. После навигации по подкаталогу net/ я наткнулся на модуль nf_tables. Он показался мне немного сложным, поэтому я решил потратить время на его изучение.

2. Введение в netfilter

Netfilter (net/netfilter) — довольно большая подсистема сетевых файлов в ядре. Вкратце, netfilter размещает хуки через сетевые модули, в которых другие модули могут регистрировать обработчики. Когда достигается хук, управление передаётся этим обработчикам, и они могут работать со своей соответствующей структурой сетевых пакетов. Обработчики могут принимать, отбрасывать и изменять пакеты.


4. CVE-2022-1015

После нескольких часов изучения API nf_tables (net/netfilter/nf_tables_api.c), чтобы точно понять, как он работает, я решил взглянуть на логическую проверку записей, отправляемых пользователем, и нашёл некоторое подозрительное поведение. Подумав, не схожу ли я с ума, я написал небольшой PoC (Proof of Concept), чтобы попытаться активировать найденную мной уязвимость: уязвимость, известную как OOB или выход за границы, которая позволяет читать и писать в памяти стека.

После того как я нашёл способ утечки адресов ядра, взять под контроль указатель памяти было довольно легко. После небольшого ROP (return-oriented programming) шелл с привилегиями root стал реальностью.

4.1 Root

Каждый раз, когда процедура init выражения должна разобрать запись из пользовательского сообщения netlink, вызывается процедура nft_parse_register_load или nft_parse_register_store в зависимости от того, является ли это исходной записью или записью назначения. Я добавил несколько комментариев:```c int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len) {

root@kitploit:~
/* Given a netlink attribute and the length
 * that is required to read the requested data,
 * write a register index to `sreg` or return
 * an error on failure. */

u32 reg;
int err;


reg = nft_parse_register(attr);
err = nft_validate_register_load(reg, len);
if (err < 0)
    return err;

/* Write resulting index to the nft_expr.data structure. */
*sreg = reg;
return 0;

}


static unsigned int nft_parse_register(const struct nlattr attr) { / Convert a register to an index in nft_regs */

root@kitploit:~
unsigned int reg;

/* Get specified register from netlink attribute */
reg = ntohl(nla_get_be32(attr));

switch (reg) {
/* If it's 0 to 4 inclusive,      
 * it's an OG 16-byte register and we need to
 * multiply the index by 4 (4*4=16) */
case NFT_REG_VERDICT...NFT_REG_4:
    return reg * NFT_REG_SIZE / NFT_REG32_SIZE;

/* Else we subtract 4, since we need to account
 * for the OG registers above. */
default:
    return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00;
}

/* So supplied values of 1, 2, 3, 4 map to
 * OG 16-byte registers, with indices 4, 8,
 * 12, 16
 * Supplied values of 5, 6, 7 overlap the verdict,
 * 8,9,10,11   overlap with OG register 1
 * 12,13,14,15 overlap with OG register 2
 * etc. */

}


static int nft_validate_register_load(enum nft_registers reg, unsigned int len) { /* We can never read from the verdict register, * so bail out if the index is 0,1,2,3 */ if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) return -EINVAL;

root@kitploit:~
/* Invalid operation, bail out */
if (len == 0)
    return -EINVAL;

/* If there would be an OOB access whenever
 * `reg` is taken as index and `len` bytes are read,
 * bail out.
 * sizeof_field(struct nft_regs, data) == 0x50 */
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data)) 
    return -ERANGE;

return 0;

}

root@kitploit:~
Варианты `*_store` практически идентичны, за исключением того, что при определённых условиях они позволяют записывать в *verbdict*.

После пересмотра последней валидации здесь что-то действительно не так:```c
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))

Похоже, это переполнение целого числа, не так ли? Если мы можем сделать так, чтобы reg содержало некоторое значение, умноженное на 4, которое вызовет переполнение при добавлении len, мы можем удовлетворить условия. В nft_parse_register_load последний значащий байт reg всё ещё записывается в указатель u8 *sreg, попадая в наш nft_expr, который затем используется как индекс.```c *sreg = reg;

root@kitploit:~
Действительно ли мы можем? `reg` — это `enum nft_registers` в валидации маршрутизации, в любом случае. Мы можем передавать значения в диапазоне от `0x00000001` до `0xfffffffb` включительно, диапазон `nft_parse_register`; но будет ли `reg` 32-битным значением в `nft_validate_register_load`? Известно, что компиляторы могут уменьшать *enum types*, если меньший тип может представить все значения. Давайте получим второе мнение.

Взято из руководства GCC:```
The integer type compatible with each enumerated type (C90 6.5.2.2, C99 and C11 6.7.2.2).
Normally, the type is unsigned int if there are no negative values 
in the enumeration, otherwise int. If -fshort-enums is specified,
then if there are negative values it is the first 
of signed char, short and int that can represent all the values, 
otherwise it is the first of unsigned char, unsigned short and unsigned int 
that can represent all the values.

On some targets, -fshort-enums is the default; this is determined by the ABI.

TL;DR? Зависит от ABI и возможной степени оптимизации. Мне не удалось найти конкретных доказательств того, включена ли эта опция по умолчанию в сборках Linux.

Но ассемблер никогда не лжёт. Давайте взглянем:```objdump.x86asm 0000000000001b60 <nft_parse_register_load>: 1b60: e8 00 00 00 00 call 1b65 <nft_parse_register_load+0x5> 1b65: 55 push rbp 1b66: 8b 47 04 mov eax,DWORD PTR [rdi+0x4] 1b69: 0f c8 bswap eax 1b6b: 89 c7 mov edi,eax 1b6d: 8d 48 fc lea ecx,[rax-0x4] 1b70: c1 e7 04 shl edi,0x4 1b73: 48 89 e5 mov rbp,rsp 1b76: c1 ef 02 shr edi,0x2 1b79: 83 f8 04 cmp eax,0x4 1b7c: 89 f8 mov eax,edi 1b7e: 0f 47 c1 cmova eax,ecx 1b81: 85 d2 test edx,edx 1b83: 74 13 je 1b98 <nft_parse_register_load+0x38> 1b85: 83 f8 03 cmp eax,0x3 1b88: 76 0e jbe 1b98 <nft_parse_register_load+0x38> 1b8a: 8d 14 82 lea edx,[rdx+rax*4] 1b8d: 83 fa 50 cmp edx,0x50 1b90: 77 0d ja 1b9f <nft_parse_register_load+0x3f> 1b92: 88 06 mov BYTE PTR [rsi],al 1b94: 5d pop rbp 1b95: 31 c0 xor eax,eax 1b97: c3 ret
1b98: b8 ea ff ff ff mov eax,0xffffffea 1b9d: 5d pop rbp 1b9e: c3 ret
1b9f: b8 de ff ff ff mov eax,0xffffffde 1ba4: 5d pop rbp 1ba5: c3 ret

root@kitploit:~
Вызовы функций выровнены довольно хорошо. Важные операции находятся в `1b8a`:```objdump.x86asm
lea    edx, [rdx+rax*4]
cmp    edx, 0x50
ja     1b9f <nft_parse_register_load+0x3f>
mov    BYTE PTR [rsi], al

rax — это результат ntf_parse_register, rdx — предоставленная len, а rsi — указатель sreg. Сомнений больше нет.

nft_parse_register_store ведёт себя так же. Пока регистры живут в stack, наша OOB-уязвимость, очевидно, будет относительной к stack. Это хорошо, потому что, если повезёт, мы сможем перезаписывать память и возвращать её напрямую.

В качестве примера уязвимого входа: регистр 0xfffffffb и длина 0x20 дадут 0xfffffffb * 4 + 0x20 = 0x0c < 0x50. После проверки (u8)0xfffffffb = 0xfb будет записано в *sreg.

Хотя есть одна проблема: существуют ли выражения, которые позволяют использовать длину, способную вызвать overflow при выполнении сложения? Немного поискав, я обнаружил, что nft_bitwise и nft_payload позволяют задавать собственную длину от 0x00 до 0xff. Многие другие выражения, похоже, имеют статические длины, которые очень малы.

Пока это выглядит многообещающе. Следующий шаг — взять эти exploit primitives (обобщённая возможность, получаемая в ходе exploit) и использовать их.

4.2 Изучаем exploit primitives

Если мы сможем определить, какую власть может дать нам наш exploit, эксплуатировать эту уязвимость будет проще. Так что наберитесь терпения: мы посмотрим немного арифметики.

Для нашего overflow при умножении регистра можно использовать три точки, так как оно умножается на 4 = 2^2: 2^32 - 1, 2^31 - 1 и 2^30 - 1 (соответственно 0xffffffff, 0x7fffffff и 0x3fffffff). Эти значения могут уменьшаться, пока при добавлении нашей максимально допустимой длины после умножения на четыре не перестанет происходить overflow. Ещё один момент: мы не можем использовать значения больше 0xfffffffb, как уже упоминалось ранее.

При заданной конкретной длине наименее значимые байты значений, которые могут вызвать overflow с этой длиной, образуют наш диапазон OOB-индексов, которые мы можем использовать.

В конце концов, не важно, какие точки overflow используются. Возьмём, например, следующие значения с LSB (младший значащий бит) равным 0xf0:``` 0xfffffff0 * 4 = 0xffffffc0 0x7ffffff0 * 4 = 0xffffffc0 0x3ffffff0 * 4 = 0xffffffc0

root@kitploit:~
С этого момента будем использовать значения регистров, близкие к `0x7fffffff`.

Ранее мы уже говорили о `nft_payload` и `nft_bitwise`. Некоторые свойства этих выражений:

* `nft_payload` может выполнять только *OOB*-записи, тогда как `nft_bitwise` может выполнять *OOB*-записи и *OOB*-чтения.

* `nft_payload` может выполнять *OOB*-записи до 0xff байт произвольных данных.

* `nft_bitwise` на самом деле может записывать только до `0x40` байт произвольных данных и читать только `0x40` байт данных, находящихся в *стеке* пространства регистров.
  
  * `nft_bitwise` требует `sreg` и `dreg`, которые должны проходить проверку с одним и тем же значением длины.
  
  * У нас есть только `0x40` байт пространства регистров, поэтому мы хотим либо читать, либо записывать из пространства регистров, но мы не можем пройти проверку с длиной больше `0x40`.

Мы можем использовать большее значение длины для `nft_bitwise`, но это означает, что `sreg` и `dreg` должны выходить за границы, что было бы не очень полезно для наших целей. Поэтому пока будем работать с длиной `0x40`.

Учитывая всё это, какие типы *эксплойтов* мы сможем использовать?

`nft_bitwise` имеет максимальную длину `0x40`. Это означает, что значение регистра, умноженное на четыре, должно быть не менее `0xffffffc0`. Наибольшее значение, которое мы можем получить при умножении на четыре, — это `0xfffffffb`, и поскольку `0xfffffffb + 0x40 = 0x3b <= 0x50`, это пройдёт проверку.

`0x7ffffff0 * 4 = 0xffffffc0`: нижняя граница — `0xf0`.
`0x7fffffff * 4 = 0xfffffffb`: верхняя граница — `0xff`.

Переводя в [*смещения байтов*](https://es.wikipedia.org/wiki/Offset_(inform%C3%A1tica)):```
0xc1 * 4        = 0x304
0xeb * 4 + 0xff = 0x4ab

nft_payload может писать за пределами границ через смещения [0x304, 0x4ab] от struct nft_regs.

Теперь, когда всё это уже прояснено, что на самом деле находится в стеке по этим смещениям?

Рутина nft_do_chain может вызываться по многим путям кода. Существует множество факторов, которые изменят форму стека перед стековым кадром (кадр стека) nft_do_chain:

  • Будь то chain hook с типом input или output.

    • Если chain hook настроен как input, hook активируется в контексте softirq соответствующего сетевого устройства со стеком softirq.
    • Если chain hook настроен как output, hook активируется в контексте syscall (системный вызов) send* со стеком syscall.
  • Протокол, который мы используем.

    • Отправка сырого IP-пакета будет иметь совсем иной стек вызовов, чем, например, пакет UDP.

Думаю, вы можете получить множество вариаций стеков вызовов, используя различные комбинации протоколов, интерфейсов и расположений hook'ов. На данный момент мы будем использовать chain hook, настроенный как output, с пакетом UDP.

Диаграмма стека с output и UDP

Раскладка стека и выходы за пределы границ в nft_do_chain, когда отправленный UDP-пакет достигает hook, настроенного на output

4.3 Фильтрация информации из побочного канала (side-channel)

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

Адрес образа ядра содержит 9 бит энтропии (меры неопределённости, существующей при множестве сообщений, из которого будет получено только одно), что означает, что существует 512 различных позиций, в которые может быть загружено ядро. В зависимости от сценария вашей атаки существует вероятность 1 к 512, что атака сработает правильно; но было бы лучше, если бы мы могли получить на основе этого более стабильный эксплойт.

Самый простой шаг — попытаться использовать нашу возможность чтения за пределами границ, которую нам дал nft_bitwise, чтобы скопировать часть данных из стека в наши регистры. Поскольку общий диапазон, который мы можем прочитать, имеет длину 0x7c байт, существует довольно большая вероятность того, что адрес ядра находится там.

Выход за пределы границ nft_bitwise

Выход за пределы границ nft_bitwise

Сегодня наш день! Их две:``` gef➤ x/bx 0xffffffff815b49c1 0xffffffff815b49c1 <import_iovec+49>: 0xc9 gef➤ x/bx 0xffffffff819ac3ec 0xffffffff819ac3ec <copy_msghdr_from_user+92>: 0xba

root@kitploit:~
Записать это в регистры — одно дело, а извлечь их — другое. После исследования выяснилось, что не существует простого способа напрямую читать регистры, пока выполняется `nft_do_chain`.

В моём первоначальном отчёте в [email protected] мне сообщили о выражении `nft_dynset` от мейнтейнера netfilter, которое поддерживает [*dynamic sets*](https://en.wikipedia.org/wiki/Dynamic_set) — нечто вроде базы данных, которая может записывать и считывать в рамках различных выполнений `nft_do_chain`. Очевидно, `nft_payload` также умеет сам записывать в пакет, я этого не осознавал.

Вместо этого я решил продолжить свою [*side-channel attack*](https://es.wikipedia.org/wiki/Ataque_de_canal_lateral). Из-за природы `nf_tables` можно вызывать побочные эффекты. Более того, можно сказать, что это даже не побочные эффекты, а основные эффекты.

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

1. Создать UDP *socket*, который принимает пакеты на `127.0.0.1:9999`:
* Он должен принимать пакеты в отдельном потоке.

* На каждый полученный пакет должно отправляться обратное сообщение.
2. Добавьте правило, которое:
   
   1. Копирует адрес ядра в регистры с помощью `nft_bitwise`.
   
   2. Использует `nft_cmp_expr` для сравнения адреса с константой.
   
   3. Сбрасывает пакет, если вычисленное сравнение истинно.

3. Отправьте UDP-пакет на `127.0.0.1:9999`
   
   1. Мы можем определить немного информации об адресе ядра в зависимости от того, получили ли мы ответное сообщение.

4. Повторяйте шаги 2 и 3 с подходящими значениями, пока не получите достаточно информации, чтобы определить её самостоятельно.

![](https://assets.kitploit.com/production/public/readmes/35022/71fabf101d1c962c5434bff7babccd3f294cd3d278765f1b03121429c5417b12.png)

Однако остаются некоторые предостережения. Например, полученный пакет также может быть сброшен без какого-либо предупреждения. Чтобы смягчить это, мы можем добавить снижение шума, для чего нам понадобятся *base chain* и *auxiliary regular chain*.

Правило в базовой цепочке:

| #   | Выражение            | Аргументы                                                                                               | Комментарий                                                                                               |
| --- | -------------------- | -------------------------------------------------------------------------------------------------------- |:---------------------------------------------------------------------------------------------------------- |
| 0   | `nft_payload`        | base=NFT_PAYLOAD_TRANSPORT_HEADER<br/>offset=offsetof(udphdr, dport)<br/>len=sizeof_field(udphdr, dport) | Записать порт назначения пакета в регистр 8.                                                               |
| 1   | `nft_cmp_expr`       | op=NFT_CMP_EQ<br/>sreg=8<br/>data=9999                                                                   | Сравнить порт назначения с `9999` и вернуть `NFT_BREAK`, если результат не равен.                          |
| 2   | `nft_payload`        | base=NFT_PAYLOAD_INNER_HEADER<br/>offset=0<br/>len=8                                                     | Записать первые восемь байт пакета в регистр 8.                                                            |
| 3   | `nft_cmp_expr`       | op=NFT_CMP_EQ<br/>sreg=8<br/>data=0xdeadbeef0badc0de                                                     | Сравнить первые восемь байт с магическим значением и вернуть `NFT_BREAK`, если они не равны.               |
| 4   | `nft_immediate_expr` | verdict=NFT_JUMP<br/>chain=aux_chain                                                                     | Поскольку правило всё ещё выполняется, условия должны совпадать, и вызывается наша *auxiliary chain*.      |

Правило во вспомогательной цепочке:

| #   | Выражение       | Аргументы                                                              | Комментарий                                                                                                                                                                                  |
| --- | --------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0   | `nft_bitwise`   | op=NFT_BITWISE_RSHIFT<br/>data=SHIFT_AMT<br/>dreg=OOB_OFFSET<br/>sreg=8 | Записывает адрес ядра в регистры, используя чтение за пределами границ, со сдвигом на биты `SHIFT_AMT`, чтобы получить нужный байт адреса в правильный регистр.                             |
| 1   | `nft_cmp`       | op=NFT_CMP_GT<br/>sreg=ADDRESS_OFFSET<br/>data=COMPARAND                | Сравнить байты адреса ядра с `COMPARAND`, вернуть `NFT_BREAK`, если результат не равен.                                                                                                      |
| 2   | `nft_immediate` | verdict=NFT_DROP                                                        | Сбросить пакет, если байт адреса больше `COMPARAND`.                                                                                                                                         |

Проверяя порт назначения и сравнивая первые восемь байт внутреннего *header* с магическим значением, мы можем активировать побочные эффекты для любых нужных нам пакетов.

Динамически изменяя `COMPARAND`, мы можем выполнить бинарный поиск, чтобы найти байт адреса ядра за `0(log(n))` операций. Динамически изменяя `SHIFT_AMT` на следующие кратные восьми, мы можем перейти к следующему байту памяти и начать заново.

#### 4.3.1 Псевдокод фильтрации

Немного кода на Python для фильтрации адреса памяти. Забавно, что я легко мог бы реализовать это на Python. Помните, что вам не всегда нужно писать свои эксплойты для ядра на C :p```python
'''
Asumimos que un hilo secundario está recibiendo
paquetes UDP en 127.0.0.1:9999 y todo lo relacionado
con nf_tables ya está configurado
p. ej. table, base y auxiliary chain
'''

def leak_byte(pos):
    s = socket.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
    s.settimeout(200) # 200ms debería ser más que suficiente
    s.bind(("127.0.0.1", 1234))

    # buscar los límites
    low = 0, high = 255

    while True:
        mid = (low + high) // 2

        # si encontramos el valor, lo regresamos 
        if low == high:
            s.close()
            return mid

        set_leak_rule(SHIFT_AMT=pos*8, COMPARAND=mid)

        # Enviar el paquete y activar la auxiliary chain
        s.sendto(pack(0xdeadbeef0badc0de), ("127.0.0.1", 9999))

        # El hilo secundario regresa a 127.0.0.1:1234
        res = s.recvfrom(0x2000)

        if not res:
            '''
            nuestro paquete fue soltado
            ya que no se regresó nada en los 200ms
            lo que significa que 

            byte to leak >= mid
            el byte a filtrar es mayor o igual a mid (127)
            '''
            low = mid
        else:
            '''
            [sanity check o prueba de cordura]

            se usa para evaluar rápidamente si 
            el valor a calcular es siquiera posible

            https://es.wikipedia.org/wiki/Prueba_de_cordura
            '''

            if res != b"MSG_OK":
                print("Something went wrong")
                return None

            '''
            Nuestro paquete fue aceptado, lo que
            significa que 

            byte to leak < mid
            byte a filtrar es menor a mid (127)
            '''
            high = mid - 1

leak_bytes = lambda: [leak_byte(i*8) for i in range(4)]

4.4 Произвольное выполнение кода (Arbitrary code execution)

Теперь, когда мы получили утечку, произвольное выполнение кода должно быть очень лёгким. Выход за границы при записи в nft_payload должен позволить записать цепочку RoP в стек, верно?

Нет. Нам не очень повезло, по крайней мере с этим конкретным ядром. Выход за границы при записи в nft_payload почти полностью выравнивается со стековым фреймом процедуры udp_sendmsg. Адрес udp_sendmsg находится по смещению +0x2f8 относительно регистров, это расположение слишком низкое, чтобы до него можно было дотянуться с помощью nft_payload или nft_bitwise (мы можем начать запись со смещения +0x304, так близко...). Адрес inet_sendmsg находится по смещению +0x4a8. Технически мы можем до него дотянуться (и перезаписать три младших байта), но там есть stack canary (техника, используемая для обнаружения переполнения буфера стека до того, как может произойти выполнение вредоносного кода) по адресу +0x0458, который нам также нужно перезаписать, чтобы добиться этого. Это, очевидно, приведёт к краху ядра, так что такой вариант не подходит.

Мне удалось применить этот метод на другой сборке ядра, но, похоже, проделать то же самое с ядром, которое я использую для этого блога, будет немного сложнее.

Теперь, возможно, мы сможем немного заняться хитроумным взломом стекового фрейма, чтобы перезаписать локальные переменные в udp_sendmsg. Также можно попытаться перезаписать verdict chain pointer, используя значение из регистров, например 0x7fffff00 (думаю, это могла бы быть классная техника; учитывая задачу).

Давайте попробуем изменить base chain hook, который мы используем. Мы использовали цепочку output, что если поменять её на input?

Диаграмма выхода за границы в nft_do_chain, если отправленный UDP-пакет достигает input hook

Так выглядит уже немного лучше! Мы можем перезаписать обратный адрес фрейма __netif_receive_skb_one_core (смещение +0x328), который возвращается в __netif_receive_skb. Поскольку он находится относительно близко к уровню нашего выхода за границы в nft_payload, мы можем заставить наш индекс OOB (выход за границы) указывать прямо на этот обратный адрес, минуя stack canary по смещению +0x310. Смещение +0x328 соответствует индексу 0xca.

Чтобы активировать перезапись обратного адреса, мы создаём новую цепочку input в таблице и добавляем в неё правило с nft_payload, который записывает 0xff байт из внутреннего заголовка пакета в индекс 0xca. Затем отправляем пакет с полезной нагрузкой, и бум.

🥳 🥳 🥳 🥳 🥳

Скачать инструмент
/proc/modules
/proc/kallsyms
например
request_module
  • Если вы не уверены, напишите небольшую программу, которая попытается взаимодействовать с модулем.