
Перевод на испанский язык CVE-2022-1015 и 1016, обнаруженных и задокументированных Дэвидом.
Этот README.md является переводом блога Дэвида. Дэвид обнаружил CVE-1015 и CVE-1016 в ядре Linux. Вы можете посетить его веб-сайт, чтобы прочитать оригинальный документ.
Вот его социальные сети:
Опубликовано 2 апреля 2022 года.
Эти проблемы должны быть эксплуатируемыми в конфигурациях по умолчанию самой новой версии Ubuntu и RHEL. Я написал свою Proof of Concept (PoC) для CVE-2022-1015, нацелившись на версию ядра 5.16-rc3 от Arch Linux.
Этот документ предназначен для людей, обладающих базовыми знаниями о ядре Linux с точки зрения функциональности и безопасности. Я постарался сделать этот документ дружелюбным для людей, не имеющих знаний о сетевом стеке, чтобы сделать его доступным для широкой аудитории.
Вот руководство по чтению:
В середине февраля программа безопасности Google объявила о продолжении своей программы вознаграждений kCTF, предлагая награды от $31,337 до 91,337 долларов за эксплойт в ядре Linux, способный повысить привилегии до пользователя root из непривилегированных процессов в песочнице nsjail.
Будучи бедным студентом, это, очевидно, привлекло моё внимание. Это был мой первый поиск уязвимости в «реальном мире», но в своих приключениях с CTF со своей командой я ознакомился с ядром Linux с точки зрения безопасности. После долгих часов с очень малым, близким к нулю прогрессом (но с большим знанием о Linux), мне удалось найти некоторые уязвимости в модуле nf_tables.
К сожалению, в конечном итоге я понял, что этот модуль не был включён в правила kCTF от Google (поэтому я не получил никакого вознаграждения за эти две уязвимости). Но, очевидно, я всё равно сообщил о них и написал эксплойт LPE (локальное повышение привилегий) для CVE-2022-1015.
Итак, вы решили, что найдёте несколько уязвимостей в Linux. Что теперь? Linux — это огромный проект, и довольно легко не увидеть лес за деревьями (вы так сосредотачиваетесь на деталях, что теряете общее видение ситуации). Что ещё хуже, многие части не документированы, и вам нужно прочитать много кода, чтобы понять, что происходит.
Я начал с попытки получить детальное представление о модели безопасности Linux. Найти баг — это одно; но найти хороший баг — совсем другое. В конце концов, не все баги созданы равными:
FS_USERNS_MOUNT; в этом случае вы можете монтировать их в пространстве имён пользователя.CAP_SYS_ADMIN или CAP_NET_ADMIN.
/proc/config.gz. Модули могут быть загружаемыми (=m) или скомпилированными отдельно и загружаемыми во время выполнения (=y).Эти ограничения помогают нам понять границы файловых систем, в которых мы можем искать уязвимости. Я думаю, что хорошая идея — потратить время на планирование атаки на желаемую цель.
Я уже извлёк урок из предыдущего пункта. Как я упоминал, модуль nf_tables не был загружен в экземпляре, предоставленном kCTF. Я мог бы понять это с самого начала и избавить себя от разочарования :p. С другой стороны, вероятно, вы не читали бы этот блог сейчас, если бы я понял это раньше; думаю, в конце концов всё сложилось хорошо.
Объяснение того, почему COS, форк Linux от Google, оптимизированный для контейнеров, не имел nf_tables, можно найти здесь и здесь.
После оценки вышеупомянутых пунктов я решил, что мой лучший путь для начала, вероятно, — посмотреть на исходный код сети. Многие интересные функции там требуют CAP_NET_ADMIN, но, как я упоминал, на самом деле это не проблема. Напротив, я подозреваю, что компоненты, требующие специальных возможностей, обычно менее безопасны, поскольку у разработчиков ядра может быть ложное чувство безопасности.
Я также приложил усилия, чтобы выбрать файловую систему, о которой хочу узнать больше; таким образом, даже если вы не найдёте ни одного бага, вы всё равно сможете узнать много интересного.
Я исследовал множество сетевых файловых систем, но не нашёл ничего важного. После навигации по подкаталогу net/ я наткнулся на модуль nf_tables. Он показался мне немного сложным, поэтому я решил потратить время на его изучение.
Netfilter (net/netfilter) — довольно большая подсистема сетевых файлов в ядре. Вкратце, netfilter размещает хуки через сетевые модули, в которых другие модули могут регистрировать обработчики. Когда достигается хук, управление передаётся этим обработчикам, и они могут работать со своей соответствующей структурой сетевых пакетов. Обработчики могут принимать, отбрасывать и изменять пакеты.
После нескольких часов изучения API nf_tables (net/netfilter/nf_tables_api.c), чтобы точно понять, как он работает, я решил взглянуть на логическую проверку записей, отправляемых пользователем, и нашёл некоторое подозрительное поведение. Подумав, не схожу ли я с ума, я написал небольшой PoC (Proof of Concept), чтобы попытаться активировать найденную мной уязвимость: уязвимость, известную как OOB или выход за границы, которая позволяет читать и писать в памяти стека.
После того как я нашёл способ утечки адресов ядра, взять под контроль указатель памяти было довольно легко. После небольшого ROP (return-oriented programming) шелл с привилегиями 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)
{
/* 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 */
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;
/* 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;
}
Варианты `*_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;
Действительно ли мы можем? `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
Вызовы функций выровнены довольно хорошо. Важные операции находятся в `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) и использовать их.
Если мы сможем определить, какую власть может дать нам наш 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
С этого момента будем использовать значения регистров, близкие к `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.
input, hook активируется в контексте softirq соответствующего сетевого устройства со стеком softirq.output, hook активируется в контексте syscall (системный вызов) send* со стеком syscall.Протокол, который мы используем.
Думаю, вы можете получить множество вариаций стеков вызовов, используя различные комбинации протоколов, интерфейсов и расположений hook'ов. На данный момент мы будем использовать chain hook, настроенный как output, с пакетом UDP.

Раскладка стека и выходы за пределы границ в nft_do_chain, когда отправленный UDP-пакет достигает hook, настроенного на output
Чтобы создать стабильный эксплойт, сначала нам нужно будет выполнить утечку адреса образа ядра.
Адрес образа ядра содержит 9 бит энтропии (меры неопределённости, существующей при множестве сообщений, из которого будет получено только одно), что означает, что существует 512 различных позиций, в которые может быть загружено ядро. В зависимости от сценария вашей атаки существует вероятность 1 к 512, что атака сработает правильно; но было бы лучше, если бы мы могли получить на основе этого более стабильный эксплойт.
Самый простой шаг — попытаться использовать нашу возможность чтения за пределами границ, которую нам дал nft_bitwise, чтобы скопировать часть данных из стека в наши регистры. Поскольку общий диапазон, который мы можем прочитать, имеет длину 0x7c байт, существует довольно большая вероятность того, что адрес ядра находится там.

Выход за пределы границ nft_bitwise
Сегодня наш день! Их две:``` gef➤ x/bx 0xffffffff815b49c1 0xffffffff815b49c1 <import_iovec+49>: 0xc9 gef➤ x/bx 0xffffffff819ac3ec 0xffffffff819ac3ec <copy_msghdr_from_user+92>: 0xba
Записать это в регистры — одно дело, а извлечь их — другое. После исследования выяснилось, что не существует простого способа напрямую читать регистры, пока выполняется `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 с подходящими значениями, пока не получите достаточно информации, чтобы определить её самостоятельно.

Однако остаются некоторые предостережения. Например, полученный пакет также может быть сброшен без какого-либо предупреждения. Чтобы смягчить это, мы можем добавить снижение шума, для чего нам понадобятся *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)]
Теперь, когда мы получили утечку, произвольное выполнение кода должно быть очень лёгким. Выход за границы при записи в 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/kallsymsrequest_module