[Перевод] Хотел просто скачать ISO, в итоге написал эксплойт для ядра


Моя точка доступа TP-Link EAP770 v1 портачит загрузки по Wi-Fi, а производитель прекратил обслуживание этой модели ещё до того, как я вообще достал её из коробки. Решение проблемы — прописать два нуля в sysfs. Вот только командная оболочка TP-Link не даёт этого сделать, и мне пришлось писать эксплойт для своего же @#$%&! устройства.
Постоянный обрыв длительных загрузок
В августе 2023 года я прикупил новёхонькую точку доступа стандарта Wi-Fi 7 — TP-Link EAP770. Подключение через неё слегка глючило, видео периодически подтупливало, но меня это не особо беспокоило.
Тогда я основное время работал за машиной, подключенной по Ethernet. Если же мне нужно было куда-то отлучиться, то я по Wi-Fi синхронизировал свой домашний каталог (~) с ноутбуком через rsync и брал ноут с собой. Вот как раз при этой передаче и начали возникать косяки с файлами больше нескольких ГБ. SSH ругался на ошибки целостности (MAC), поэтому я какое-то время искал проблемы в rsync и SSH. В остальном всё работало, а смена параметров rsync ситуацию то улучшала, то ухудшала. Wireshark показывал ноль битых контрольных сумм и ноль повторных передач, а одиночный UDP-поток из случайных байтов доходил абсолютно чистым. Я верил, что TCP непременно доставит правильные байты — ведь он, чёрт возьми, для этого и создавался. Собственно, потому я был уверен, что косяк где-то в rsync или SSH. В итоге я написал для rsync обёртку, чтобы автоматически повторять передачи, и забил.
Позже я наладил трафик внутри домашней сети через Tailscale, и повторные попытки стали не нужны. WireGuard просто дропает пакет, если его аутентификация проваливается — TCP внутри туннеля видит потерю пакета и повторяет отправку. Поскольку передача заработала исправно, я снова успокоился.
Отсутствие обновлений прошивки
Как я уже писал, у меня точка доступа EAP770(EU) v1.0. Это модель на базе процессора Qualcomm IPQ9574 под управлением Linux 5.4.164 и с прошивкой 1.0.0 Build 20230605 Rel. 71641. С этой прошивкой устройство шло с завода, и, похоже, с того момента в TP-Link о нём напрочь забыли :)
Я эту штуку купил в августе 2023 года, а стандарт Wi-Fi 7 окончательно утвердили только в сентябре 2024-го. У неё нет сертификации Wi-Fi Alliance, и её покупка была явно глупым решением.
НИКОГДА НЕ ПОКУПАЙТЕ ТЕХНИКУ, ПОКА СТАНДАРТЫ НЕ БУДУТ УТВЕРЖДЕНЫ, А САМИ УСТРОЙСТВА НЕ ПРОЙДУТ СЕРТИФИКАЦИЮ.
В 2025 году я заметил, что пока все устройства спокойно себе обновляются, это чудо продолжает сидеть на своей старой прошивке. В июне того же года я обратился в поддержку, так как на сайте смог найти только v2. Как ни странно, в базе данных у них первой версии тоже не оказалось, и они попросили меня выслать фото самого устройства и его упаковки. Ну а потом прислали вот что:
Поддержка модели EAP770 Version 1 завершена. Это устройство ещё может быть в наличии у некоторых ритейлеров, но обновлений прошивки для него больше не выпускается.
КЛАСС.
Поиск причины повреждения данных
В 2026 году я решил перейти на NixOS и обнаружил, что при скачивании файла на 128 МБ из бинарного кэша Nix через Fastly по протоколу HTTP/2 постоянно возникала ошибка. При этом тот же файл спокойно скачивался по протоколу HTTP/1.1. Мне до сих пор невдомёк, чем помогал HTTP/1.1. Могу лишь предположить, что по HTTP/2 данные просто передавались быстрее. Команда git clone тоже выдавала ошибку, а HTTPS-соединения разрывались с сообщением bad record mac. Плюс в марте попытка скачать ISO-образ NixOS через wget завершилась ошибкой TLS:
2026-03-05 12:37:18 (31.6 MB/s) - Read error at byte 2722477636/3683778560 (error:0A000119:SSL routines::decryption failed or bad record mac; error:0A000139:SSL routines::record layer failure). Retrying.Я вручную переключился на HTTP/1.1 и снова забил.
В сентябре я превратил свою рабочую станцию в сборочную ферму, а сам пересел за ноутбук, который подключается к сети по Wi-Fi. При этом каждый день где-то в районе 11:00 Wi-Fi стабильно отваливался — аккурат перед дежурным созвоном. Пронаблюдав эту ситуацию несколько дней подряд, я решил снова вскрыть этот ящик Пандоры и засел за поиск причины, позвав на помощь Opus 5.5.
Спойлер: причиной отвалов оказался другой баг.
Я обвешал BPF-зондами Qualcomm-драйвер на ноутбуке и запустил утилиту apstats на точке доступа с правами root. Баг таился в закрытой прошивке самой Wi-Fi карты ноутбука; подробности читайте в апдейте в самом низу.
Я пробовал включать и отключать Wi-Fi 7, отключал GRO на клиенте и даже заменил Wi-Fi адаптер MediaTek в моём Framework 16 на модель от Qualcomm. Новый адаптер и отключение GRO вроде как слегка улучшили ситуацию, но в корне проблему не решили.
С помощью tcpdump я перехватил провалившуюся операцию скачивания и нашёл TCP-сегмент из 1440 байт, в котором было повреждено 64 байта подряд, но контрольная сумма при этом оставалась верной. Показатель TcpInCsumErrors держался на нуле. Протокол TCP принял этот сегмент как правильный и передал его на уровень TLS, который отклонил запись с ошибкой bad record mac.
Повреждённая запись не расшифровывалась, поэтому сравнить испорченные байты с ожидаемой полезной нагрузкой я не мог. Тогда я решил потестить передачу сырых данных по TCP между ПК с проводным подключением и ноутбуком с Wi-Fi. Выяснилось, что порча происходила только при одновременной работе нескольких TCP-потоков и только в направлении LAN -> Wi-Fi. Тест выявил три повреждённых блока по 64 байта в 8192 МиБ переданных данных, и каждый из этих блоков начинался с нулей.
Правильная контрольная сумма в повреждённом сегменте означала, что после повреждения её кто-то вычислил повторно. Значит, теперь нужно было понять, что именно на этом маршруте перезаписывает заголовки TCP. После замены платы MediaTek на Qualcomm проблема осталась, то есть дело не в ноутбуке. На проводных клиентах всё было чисто, а код целостности CCMP MIC на «битых» пакетах оставался корректным — значит, дело было не в кабеле и не в помехах радиоэфира. Оставался только мост между Ethernet и Wi-Fi внутри самой точки доступа. 64 байта — это как раз размер строки кэша в ядрах процессора IPQ9574, что явно указывало на баг кэша или DMA, а не на перевёрнутый бит. Единственный же компонент на этом пути, который одновременно и ускоряет трафик моста, и пересчитывает контрольные суммы — это подсистема аппаратной разгрузки Qualcomm SFE/PPE offload.
Две команды записи, требующие root
Изучив исходники драйвера, я предположил, что SFE/PPE можно отключить с помощью двух следующих настроек:
echo 0 > /sys/sfe/l2_feature
echo 0 > /sys/sfe/ppe_rfs_featureВ оба этих файла sysfs разрешена запись только из-под root, но SSH-оболочка в TP-Link запускается как uid 1 вообще без системных прав. В GUI для этого тоже настройки нет.
Официального образа прошивки для первой версии, который можно было бы пропатчить и накатить, в свободном доступе тоже не существовало, а прошивка от v2 предназначена для совсем другого железа. Я попытался залить EAP773(EU)_V1_1.0.14 от родственной модели EAP773, но устройство её отклонило. А более новые релизы прошивок для v2 вообще зашифрованы. Если рассматривать прямую прошивку чипа, то потребуется специальная прищепка (flash clip) для программатора. Но мне не хотелось заморачиваться её поиском, как и превратить эту чёртову точку доступа в кирпич.
Тратить лишние 200 баксов на замену девайса только потому, что в TP-Link не стали заморачиваться его поддержкой после продажи, я тоже не собирался. Устройство работает у меня дома, я точно знаю, какую настройку нужно подкрутить, но софт этого не позволяет.
Но стоит отдать должное TP-Link: систему они укрепили хорошо. Я не нашёл ни одного вектора уязвимости в их бинарниках: они повсюду используют белые списки и загружают для ядра только необходимые модули. Так что халявы в духе ; echo 0 > /sys/sfe/ppe_rfs_feature мне не светило. Единственным путём внутрь оставалось само ядро.
Призвав на помощь Opus 4.8, я связал в цепочку уязвимость CVE-2022-0185 и манипуляцию с буфером пайпа. CVE-2022-0185 заключается в целочисленном переполнении вниз в функции legacy_parse_param(), доступной из непривилегированного пространства имён пользователей. Эта уязвимость позволяет при записи переполнить 4-килобайтный slab-объект. А манипуляции с буфером пайпа, в свою очередь, превращают это переполнение в возможность произвольной записи в память. В открытом доступе уже были подробные разборы и примеры эксплуатации (PoC) этих уязвимостей.
При этом эксплойт должен был оставить точку доступа в рабочем режиме, так как после перезагрузки сломанный offload включается обратно. Переполнение оставляет 4-килобайтный slab в таком состоянии, когда вторая попытка уводит ядро в панику, так что на одну загрузку роутера попытка тоже одна. Я дал Opus описание базовых примитивов и поручил дорабатывать эксплойт до тех пор, пока он не заработает стабильно.
Описание эксплойта
Поскольку исходников GPL для версии v1 производитель не выложил, я взял ближайший опубликованный код от модели EAP783. Конфигурация её ядра отличалась от конфигурации в /proc/config.gz моей точки доступа чисто косметически тремя строками. В итоге я собрал с этим конфигом тестовый стенд в QEMU, наладил работу цепочки уязвимостей сначала на нём и только потом перешёл к реальному устройству.
В ядре отсутствует механизм KASLR, поэтому адреса в памяти между перезагрузками не меняются. Но вот таблицу символов kallsyms производитель обнулил, так что мне всё равно пришлось вычислять адреса во время работы системы. В сборке для QEMU ядро загружалось по физическому адресу 0x40080000, но на самом устройстве оно разворачивалось по адресу 0x42080000, из-за чего все расчёты смещались на 0x2000000. Правильный базовый адрес я в итоге нашёл в переменной конфигурации сборки в исходниках EAP783. И даже после его исправления выяснилось, что секции ядра .text, .rodata и .data на устройстве сместились каждая на свою величину относительно сборки QEMU. Так что нельзя было просто прибавить одно общее смещение ко всем адресам.
Механизм работы:
Через
unshareизолировать процесс в отдельное пространство имён, чтобы получить доступ к функции fsopen и вызвать переполнение вниз.Дефрагментировать 4-килобайтный slab с помощью пары сотен аллокаций одного размера, после чего выстроить созданные объекты в памяти так, чтобы источник переполнения, целевой ключ, сообщение SysV и кольцевые буферы пайпа оказались вплотную друг к другу.
Вызвать переполнение, выходящее в область метаданных ключа, и увеличить значение поля его длины. Теперь чтение этого ключа будет приводить к утечке данных соседних объектов, включая поле
buf[0].opsпайпа, где содержится адресanon_pipe_buf_ops.Перенаправить буфер одного пайпа на страницу кольцевого буфера другого, а затем подделать в этом втором кольце запись, используя утёкший ранее указатель ops. Функция
pipe_buf_can_merge()проверяет совпадение этих указателей, поэтому операция совмещённой записи через поддельную запись позволяет добраться до любой физической страницы, включая область образа ядра.Просканировать секцию
.dataв поискеmodprobe_path. Обычно при чтении страниц образа ядра через пайп в момент очистки буфера для зарезервированной страницы вызываетсяput_page(), что приводит к ошибке Bad page map и панике ядра. Однако функцияpipe_readвызывает только методы->confirmи->release, не проверяя подлинность таблицы ops. Поддельнаяpipe_buf_operations, где оба метода указывают наgeneric_pipe_buf_confirm(которая просто возвращает ноль и больше ничего не делает), даёт возможность сканировать и читать страницы без их освобождения. Это позволит эксплойту кусками сканировать секцию.data, пока не найдётся нужная строка.Перезаписать
modprobe_pathи запустить черезexecveфайл с «магическими байтами», которые не соответствуют ни одному бинарному формату. Ядро вызоветrequest_module(), которая запустит программу по подменному пути с глобальными правамиuid 0.Оставить дочерний процесс активным, чтобы он удерживал поддельный пайп. Если позволить ему завершиться, пайп будет уничтожен, и из-за попытки освобождения зарезервированной страницы возникнет паника ядра.
Чтобы довести эту схему до рабочего состояния, потребовалось около семнадцати часов игры в «рулетку» с Opus 4.8 за токены. За это время я успел изучить бинарники устройства в radare2, проверить включённые механизмы защиты и разобраться со всеми вылетами системы. В итоге точка доступа осталась в рабочем состоянии, а отключение аппаратной разгрузки привело к прекращению порчи данных.
Баг когерентности DMA-кэша
Драйвер qca-nss-dp от Qualcomm повторно использует пакетные буферы, забывая синхронизировать процессорный кэш для всего буфера приёма перед его возвращением в распоряжение сетевой карты (NIC). На этом железе контроллер DMA пишет напрямую в DRAM, минуя синхронизацию кэшей процессора. Из-за этого «грязная» строка кэша, оставшаяся в буфере от предыдущего использования, может быть сброшена в память уже после того, как сетевая карта приняла новый пакет, затерев часть этого пакета.
Затем подсистема SFE/PPE пересчитывает контрольные суммы для уже повреждённых данных, и клиент получает свежую контрольную сумму TCP, валидную для этого мусора. Протокол TCP принимает пакет, а вот механизмы проверки подлинности сообщений (MAC) в TLS и SSH обнаруживают искажение, выдавая ошибки bad record mac и corrupted MAC.
На архитектуре x86 оборудование само следит за согласованностью DMA с кэшами процессора, поэтому там такое упущение никак бы не навредило. Изучая исходники ядра, я обнаружил похожие проблемы с обслуживанием кэша при DMA и в других драйверах. Только пока не знаю, как протестировать их на реальном железе и кому о них сообщить.
Описание драйвера
Это внешний код NSS от Qualcomm, доступный в репозитории openwrt/qca-nss-dp. При работе по быстрому пути передачи движок SFE сбрасывает кэш для данных skb->data в объёме skb_headlen(skb) байт, что в точности составляет линейную часть исходящего пакета.
buff_addr = (dma_addr_t)virt_to_phys(skb->data);
EDMA_TXDESC_BUFFER_ADDR_SET(txd, buff_addr);
#ifdef EDMA_40BIT_SUPPORT
EDMA_TXDESC_BUFFER_ADDR_HI_SET(txd, buff_addr);
#endif
dmac_clean_range_no_dsb((void )skb->data, (void )(skb->data + buf_len));Затем он выставляет флаг skb->fast_recycled = 1, а буфер возвращает в закреплённый за процессором список повторного использования без очистки содержимого.
/*
* Если пакет пошёл по быстрому пути SFE, устанавливается
* флаг fast_recycled, чтобы любой драйвер Rx DMA, который
* позднее будет выделять буфер под этот пакет, мог избежать
* аннулирования кэша.
*/
if (likely(skb->fast_xmit) && likely(skb->is_from_recycler)) {
skb->fast_recycled = 1;
}Механизм повторного использования обнуляет структуру sk_buff вплоть до поля tail, восстанавливает флаг fast_recycled и сбрасывает указатель skb_data на значение skb->head + NET_SKB_PAD.
Когда этот буфер переиспользуется для приёма, код пути наполнения видит установленный флаг и пропускает операцию аннулирования кэша.
/*
* Аннулирование кэша для skb->data.
* Операция сброса на ядрах A73 также влечёт за собой аннулирование.
* Если пакет передаётся по быстрому пути, а значит, выделенный
* под него буфер возвращается в пул по быстрому циклу, можно быть
* уверенными, что аннулирование кэша было выполнено при прошлой передаче.
*/
if (unlikely(!skb->fast_recycled)) {
dmac_inv_range_no_dsb((void *)skb->data,
(void *)(skb->data + rx_alloc_size -
EDMA_RX_SKB_HEADROOM -
NET_IP_ALIGN));
}
skb->fast_recycled = 0;Область приёма составляет 1856 байт, или 29 строк кэша, начиная со смещения head + 192. При предыдущей передаче произошёл сброс всего двух или трёх строк, причём по другому смещению.
«Грязные» строки, оставшиеся вне этой очищенной области, могут сбрасываться в DRAM уже после того, как сетевая карта выполнит DMA-передачу, затирая принятый пакет порциями по 64 байта. Причиной такого сброса может стать обычное вытеснение строк из кэша. На arm64 к этому также приводит аннулирование невыровненной области памяти. Функция __dma_inv_area использует инструкцию dc civac (очистка и аннулирование) для частично заполненных пограничных строк кэша. После того, как «грязные» данные затёрли часть пакета в DRAM, восстановить полученные байты с помощью аннулирования кэша уже не получится.
Сетевая карта проверяет контрольную сумму исходного пакета ещё до его повреждения, а код приёма на точке доступа видит аппаратные флаги состояния и помечает пакет как CHECKSUM_UNNECESSARY. Точная последовательность операций внутри PPE скрыта в проприетарном коде драйвера и железа, но анализ сетевых пакетов показывает, что контрольная сумма вычислялась уже после повреждения данных.
Повторное использование буферов включается параметром CONFIG_SKB_RECYCLER на этапе компиляции, в рабочей среде такого переключателя нет. Отключение аппаратной разгрузки не даёт буферам уходить на быстрый путь передачи. В результате драйвер перестаёт выставлять флаг fast_recycled, а код приёма полностью аннулирует буфер кэша перед тем, как передать его сетевой карте.
Через проводной тракт приёма точки доступа проходит только трафик, скачиваемый Wi-Fi клиентами; загружаемый ими трафик через него не идёт, а обмен данными между проводными клиентами происходит исключительно через коммутатор.
Opus 5.5 нашёл баг, а потом отказался его обсуждать
Когда я перешёл от диагностики проблемы к её исправлению, Opus 5.5 начал обрывать сессии примерно на каждом втором сообщении. Причём достаточно было просто упомянуть что-то вроде «локального повышения привилегий». И хотя у меня на аккаунте включена опция Cyber Verification Program, это никак не помогло. К слову, Opus 4.8 помог реализовать эксплойт без подобных проблем.
Из-за своих защитных механизмов Opus 5.5 также обрывал сессию, когда я просил его вычитать эту статью. А вот GPT-6 Astra вычитывает и правит её без какой-либо кибер-верификации.
До отключения offload и после
Я протестировал передачу сырого TCP-трафика между теми же двумя компьютерами с включённой аппаратной разгрузкой и выключенной.
Offload | Направление | Данные | Скорость | Повреждённые блоки по 64 байта |
Включен | LAN -> Wi-Fi | 8192 МиБ | 103,4 МиБ/с | 3 |
Отключен | LAN -> Wi-Fi | Тест в течение ночи | 95,4 МиБ/с | 0 |
В этом тесте из-за её отключения скорость скачивания упала примерно на 8%. Приходится жертвовать производительностью только потому, что TP-Link не даёт мне нормально починить собственное устройство.
При включённой же разгрузке невозможно скачать большой файл по Wi-Fi одним потоком, проходят только обрубки. Когда же я её отключил, за всю ночь тестирования не побился ни один фрагмент, и с тех пор крупные файлы передаются без проблем. Ну а выгрузка с Wi-Fi клиента в LAN и без того работала исправно.
После отключения разгрузки начинает расти счётчик NOT_SUPPORT_BRIDGE в файле /sys/sfe/exceptions, так как трафик в режиме моста теперь идёт не по ускоренному сетевому тракту, а по обычному.
После перезагрузки роутера настройки сбрасываются, поэтому скрипт-наблюдатель на моём NAS периодически опрашивает устройство и перезапускает эксплойт, если разгрузка снова включилась. Если же у него это не получается, он перезагружает точку через API управления TP-Link и повторяет попытку. При этом на количество таких перезагрузок установлен жёсткий лимит.
А ведь в Qualcomm могут при желании починить свой драйвер или хотя бы выложить его в более открытом виде. В чистом OpenWrt этого бага нет, а TP-Link всё равно не выпустит новую прошивку для моего девайса. Хотя похожие баги с DMA, которые я нашёл в самом ядре Linux, заслуживают более внимательного изучения. Вернусь к ним на выходных.
Апдейт: отслеживание обрывов
Плата Qualcomm, которую я установил в свой Framework 16, именуется WCN785x, и каждые несколько часов она внезапно прекращала отправлять данные на период от 1 до 4 минут. Поскольку у меня уже был рут-доступ к точке, я запустил на EAP770 утилиту apstats параллельно с BPF-зондами на ноутбуке и зафиксировал три обрыва связи, попутно собрав логи с обеих сторон.
Всё, что точка доступа отправляла на ноутбук, до него доходило и подтверждалось сообщением ACK. Значит, радиомодуль ноутбука исправно передаёт сигналы, и приёмник точки их ловит. Вот только кадры данных с ноута почему-то не уходят.
На самом ноутбуке пакеты продолжали поступать в драйвер ath12k, и в очереди скопилось аж несколько тысяч кадров. Прошивка подтверждала обработку лишь по нескольку кадров за раз, а оставшиеся в конечном итоге вернула с сообщением CMD_REMOVE_AGED_FRAMES — так в результате этого обрыва было потеряно 10630 кадров из 26388. В остальном драйвер вёл себя исправно.
На стороне точки доступа счётчик rx_data в течение большинства тактов оставался на нуле (+0), за исключением редких всплесков. При этом ошибок приёма (RX errors) и повторных попыток зафиксировано не было — то есть кадры не покидали сетевую плату ноутбука. Затем плату внезапно «отпускало», и она выплёвывала всю накопившуюся очередь за две секунды: точка доступа зафиксировала скачки rx_data+3273, а затем +12498, в то время как ноутбук зарегистрировал сразу 7751 подтверждённых кадров.
Проблема явно была не на стороне ОС или эфира: уровень сигнала оставался от -31 до -42 дБм, фоновое сканирование не выполнялось, да и IP-стек на ноутбуке работал абсолютно нормально. Плюс я уже использую последнюю официальную прошивку — WLAN.HMT.1.1.c7-00108 UPSTREAM-3. Драйвер ath12k получает обратно только статусы завершения операций, а всё, что связано с отправкой кадра в эфир, определяется внутри закрытого бинарника amss.bin от Qualcomm. Так что дело в самой прошивке.
Я до сих пор не знаю, почему пропадает передача. Всплески трафика, который прорывался во время обрыва, похожи на работу планировщика отправки данных в стандартах 802.11ax/be — передачу на основе триггеров или механизм MU EDCA. Но таких настроек я на устройстве не нашёл, так что проверить эти предположения не получилось.
Пока что я переключил радиомодуль 5 Ггц с Wi-Fi 7 на Wi-Fi 6 (802.11ax) и оставил скрипт наблюдать за ним на 24 часа. Если обрывы прекратятся, значит, это проблема Wi-Fi 7 — тогда я останусь на Wi-Fi 6 и отправлю баг-репорт разработчикам ath12k. Если нет, то прошивка косячит в любом режиме, и мне останется либо писать баг-репорт, либо просто купить сетевую карту от Intel.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.