В прошлый раз я рассказывал об исследовании прошивки встроенного контроллера моего ноутбука. Сегодня я займусь исследованием прошивки Wi-Fi-модуля, подключённого к тому же ноутбуку.



Чип Realtek RTL8821AE


Wi-Fi-чип, о котором идёт речь, расположен на плате формата M.2, которую можно подключить к соответствующему разъёму моего ноутбука. Среди возможностей этого чипа, кроме прочих, можно отметить поддержку Wi-Fi-частот 2,4 Ггц и 5 Ггц. Обмен данными с этим чипом ведётся по PCIe.

Найти прошивку чипа несложно: она, в Linux, загружается, при запуске системы, из /lib/firmware/rtlwifi. На самом деле, существует два варианта прошивки. Одна — для обычного использования (rtl8821aefw_29.bin), и ещё одна, немного меньшего размера, для режима Wake on WLAN (rtl8821aefw_wowlan.bin). В этом режиме компьютер может быть «разбужен» по беспроводной сети. Файл ..._29.bin загружается по команде ip link set dev wlan0 up, файл ..._wowlan.bin — по команде ip link set dev wlan0 down. Это — не постоянные прошивки, они лишь загружаются в RAM чипа.

У этого чипа есть и Wi-Fi-драйвер более высокого уровня, находящийся в ядре Linux и написанный программистами Realtek. Это не особенно много говорит о его качестве, однако это, всё же, код, созданный в Realtek.

В том же чипе реализованы возможности Bluetooth, работать с ними можно по USB 2.0. Похоже, что в ядре реализована функция Bluetooth Coexistence, которая позволяет Wi-Fi- и Bluetooth-оборудованию не мешать друг другу в ходе работы.

Все взаимодействия с RTL8821AE производятся через отображаемую память. А именно, имеется конфигурационная область размером 4 Кб, поддерживающая чтение и запись. Есть ещё TX- и RX-буферы размером по 64 Кб, используемые для отправки и получения пакетов.

Базовая структура прошивки


Если судить по прошивке, то она, очевидно, написана для устройства, основанного на микроконтроллере 8051 (и так оно, конечно, и есть). Правда, она загружается не по адресу 0x0000. Программа at51 base, непосредственно проанализировавшая образ прошивки, возвращает значение 0x3fe0. Оказалось, что тут имеется заголовок размером 0x20 байтов, в результате сама прошивка загружается по адресу 0x4000.

Код, начинающийся по адресу 0x4000, как и в других подобных прошивках, выполняет переход к главной функции. В манере, типичной для компилятора KEIL, в коде осуществляется переход к функции ?C_STAR, которая выполняет инициализацию статических переменных, сгенерированных компилятором Keil C(X)51. После этого осуществляется переход к функции MAIN.

Эта функция MAIN выглядит несколько странно. Она выделяет какой-то объём памяти, потом помещает в стек адрес, настраивает таймер, а после этого возвращается к адресу, который только что поместила в стек. Оказалось, что Realtek использует ядро RTX51 Tiny. Это — весьма компактное ядро реального времени, которое осуществляет управление задачами и сигналами. Ядро отслеживает состояние задач и, при переключении между ними, перегруппировывает стеки. Программа at51 libfind позволила узнать о том, где расположены различные библиотеки KEIL, и, в рамках этого исследования, нашла функции ядра RTX51, вроде OS_SWITH_TASK или _ISR_SEND_SIGNAL.

В обычной прошивке имеется две задачи (и одна, дополнительная, которая используется только для инициализации). Одна из них активируется лишь для особых сигналов, а вторая занимается выполнением бесконечного цикла.

Хотя и нет общедоступного даташита, документирующего регистры ввода/вывода со стороны контроллера 8051, выяснить сведения о большинстве из них было до приятного просто. Драйвер Linux определяет регистры в конфигурационном пространстве со стороны хоста. Если посмотреть на то, к каким регистрам не обращается 8051, можно заметить, что они близки к тем регистрам, которые не определены в коде драйвера при отображении на адресное пространство XDATA со смещением 0x0. После этого можно просто, для импорта всех нужных имён в Ghidra, обработать файл reg.h, в котором имеются определения регистров.

Создание дампа Mask ROM


Прошивка загружается по адресу 0x4000, но в ней при этом вызываются функции, адреса которых находятся ниже данного адреса. Это говорит о том, что на чипе имеется память для постоянного хранения информации, Mask ROM, ответственная, кроме прочего, за загрузку прошивки на чип. Незнание того, что это за функции, усложняет процесс реверс-инжиниринга прошивки, поэтому хорошо было бы найти возможность взглянуть на содержимое Mask ROM.

Легче всего сделать это путём модификации прошивки и копирования байтов из области CODE в регистр конфигурационного пространства, так как нам уже известно то, как осуществляется отображение соответствующей памяти. Но тут имеется одна проблема. А именно, возникает такое ощущение, что последние два байта прошивки представляют собой контрольную сумму. Значит — нам надо знать о том, как вычисляется эта контрольная сумма. Но вычисляется она в коде, находящемся в Mask ROM, поэтому нельзя просто посмотреть код, который её вычисляет. Правда, к моему счастью, с помощью программы delsum удалось быстро выяснить, что перед нами — простая 16-битная контрольная сумма, при вычислении которой используется XOR (которая является эквивалентом CRC с параметром poly=0x0001).

Из пользовательского пространства обычно нельзя выполнять операции чтения/записи в применении к конфигурационному пространству. Поэтому мне понадобилось перекомпилировать ядро ради добавления в него поддержки этой возможности с помощью /dev/mem. Я решил сделать это, запустив ядро внутри виртуальной машины и настроив в ней доступ к Wi-Fi-карте с использованием VFIO, так как перекомпиляция ядра занимает у меня слишком много времени, а стандартная конфигурация, в общем-то, работает только в виртуальных машинах. (Я, конечно, знаю, что modconfig — это вещь, но использование виртуальной машины хорошо подходит для стандартной конфигурации, это не отнимает особенно много времени).

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

Я решил не тратить на это слишком много времени и просто внести в код адрес, по которому осуществляется чтение данных. Команда перехода, находящаяся по адресу 0x4000, была модифицирована так, чтобы осуществлять копирование байта с жёстко заданного в коде адреса в неиспользуемый регистр конфигурационного пространства (REG_ROM_VERSION). После этого управление передаётся функции MAIN. После каждого прочитанного байта прошивка модифицируется, в неё вносится новый адрес, после чего модуль ядра перезагружается с использованием новой прошивки. Скорость чтения данных при таком подходе составляет примерно 3 байта в секунду. На то, чтобы прочесть всё, что нужно, ушла пара часов, но это — вероятно, меньше, чем ушло бы на попытки догадаться о том, почему прошивка не отвечает на H2C-команды (и ещё я мог заниматься другими делами во время чтения данных).

Прошивка Bluetooth-модуля


Сначала я не понял одну вещь, которая теперь кажется мне очевидной. Речь идёт о том, что для Bluetooth-модуля чипа имеется отдельная прошивка (она хранится в /lib/firmware/rtlbt). Эта прошивка, правда, написана не для 8051-устройства, при этом я не смог сходу разобраться в том, на какую архитектуру она рассчитана. Для того чтобы облегчить себе решение этой задачи, я написал специальную программу (я, пожалуй, слишком часто даю ссылки на инструменты собственной разработки, но дело в том, что я делаю их, «затачивая» под конкретную задачу, так как мне приходится решать подобные задачи снова и снова). Эта программа сравнивает разные редакции прошивки, используя попарное выравнивание материалов, в результате одинаковые разделы кода, находящиеся в разных редакциях прошивки по разным адресам, выводятся рядом друг с другом.


Байты 0x0065 в двух местах

В данном случае важно обратить внимание на то, что во многих местах вставлены байты 00 65, а после них оба адреса выровнены по 4-байтовым границам, что говорит о том, что тут используется некая форма выравнивания данных в памяти. Конечно, для целей выравнивания идеально подошла бы инструкция NOP. Поэтому я поискал сочетание 0065 NOP, но ничего не нашёл. А вот поиск конструкции 6500 NOP (обратный порядок следования байтов) привёл к нескольким интересным результатам, которые указывают на то, что MIPS16e (расширение MIPS, позволяющее сжимать поток команд) использует этот код операции для NOP. И эта находка способствовала правильному дизассемблированию кода прошивки и превращению его во что-то осмысленное.

Правда, Bluetooth-прошивка не особенно меня интересовала (да и написана она для модуля, который основан не на 8051), поэтому я на этом её исследование закончил и вернулся к анализу прошивки Wi-Fi-модуля. Пожалуй, дело тут, в основном, в том, что у меня нет Bluetooth-устройств (за исключением, возможно, моего Android 4.4-мобильника, которому уже 8 лет), поэтому, если бы я решил лучше разобраться с Bluetooth-модулем, у меня возникли бы сложности с тестированием.

Как драйвер отправляет пакеты


То, как работают Wi-Fi-сети, описано в наборе стандартов IEEE 802.11. Если взглянуть на эти стандарты, там будет около 3500 страниц плотного текста, набитого аббревиатурами. Мне, правда, вроде как даже нравится читать спецификации и стандарты, и это, по крайней мере — для меня, не особенно большая проблема.

Кадры 802.11 играют примерно такую же роль, что и Ethernet-кадры, но это — не только кадры, рассчитанные на перенос данных. Среди них имеется широкое разнообразие управляющих кадров, решающих различные задачи, вроде подключения к точке доступа, аутентификации, управления питанием и так далее. Кадры данных (и некоторые управляющие кадры) могут быть зашифрованы с использованием различных шифров и протоколов.

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

В кадрах данных 802.11 имеется LLC-заголовок, который, в большинстве случаев, нужен лишь для указания типа протокола инкапсулированных в кадре данных (EtherType).

При отправке пакета драйвер размещает весь кадр в буфере TX, а так же размещает там заголовок размером 40 байт, идущий перед ним. Заголовок сообщает Wi-Fi-модулю сведения о размере кадра, о том, нужно ли использовать шифрование, о скорости, с которой нужно передать кадр, и другие подобные сведения. В кадре 802.11 ещё имеется поле Duration (Длительность), которое указывает на то, сколько времени занимает отправка пакета. Его содержимое тоже вычисляется с использованием аппаратных ресурсов.

Устройство имеет 8 очередей для различных целей, которые могут быть указаны в заголовке. Одна — для сигнальных кадров, ещё одна — для других специальных целей, ещё несколько — для передачи обычных кадров. Вне зависимости от того, о какой именно очереди идёт речь, пакеты помещают в буфер TX с использованием циклического алгоритма с выравниванием по 256-байтной границе.

О применении технологии Realtek RealWoW


В Linux-драйвере есть функция rtl8821ae_set_fw_rsvdpagepkt, которая вызывается при загрузке новой прошивки. Она загружает кадры, вроде сигнальных кадров и кадров данных, содержащих ARP-ответы, в верхнюю область буфера TX, которая считается драйвером зарезервированной областью. Смещение этих кадров после этого отправляется прошивке посредством каких-то H2C-команд.

Этот механизм, по-видимому, используется и для целей WoWLAN, что позволяет прошивке отвечать на ARP-запросы даже тогда, когда хост находится в «спящем» состоянии, но IP-пакеты при этом могут быть отправлены на Wi-Fi-чип. Ещё WoWLAN-прошивка отвечает за GTK-рукопожатие, когда точка доступа отправляет новый групповой ключ шифрования всем устройствам для целей широковещательной передачи данных при подключении устройства к сети или отключении его от неё.

Для того чтобы узнать о том, как прошивка использует сведения о смещениях, нужно взглянуть на H2C-команду, которая отвечает за сохранение смещений. Она записывает их в некие места адресного пространства XDATA, в результате, если найти места, в которых есть ссылки на эти области, можно выяснить то, какая именно часть прошивки ответственна за отправку кадров.

Оказалось, что в буфере TX имеется 256-байтное окно, расположенное в адресном пространстве XDATA (0xfc00-0xfcff), и что старшие 8 бит адреса могут быть установлены с помощью регистра XDATA, доступного по адресу 0xfd10. И, аналогично, доступ к буферу RX можно получить, обратившись к области XDATA 0xfb00-0xfbff со старшими 8 битами, находящимися по адресу 0xfd11.

При отправке пакета сложно точно понять то, что конкретно его отправляет, так как в ходе этой процедуры происходит множество событий. Прошивка устанавливает некоторые поля в заголовке TX и, кроме того, устанавливает некоторые биты в заголовке 802.11, она выясняет, зашифрованы ли данные для того чтобы узнать смещение содержимого пакета. В случае с ARP-пакетами, кроме того, производится проверка, например, EtherType и установка целевого адреса из ответа (и, конечно, процесс разбора пакетов отличается ещё большим уровнем сложности). Самое главное в деле отправки пакета, похоже, делается путём установки REG_TXPKTBUF_MGQ_BDNY в старшие 8 бит адреса буфера TX (обратите внимание на то, что пакеты выровнены по 256-байтной границе) и путём последующей записи 0x20 в REG_CPU_MGQ_INFORMATION + 3.

Если взглянуть на прошивку WoWLAN, то можно выяснить, что имеется и ещё одна H2C-команда, выполняемая позже, которая, по видимому, производит ещё какие-то настройки для дополнительных пакетов. В коде разбора пакетов есть место, где проводится проверка на наличие байта 0x45 в начале содержимого кадра данных. Именно этот байт находится в начале IPv4-пакетов. Затем код разбора пакетов проверяет наличие адреса назначения и UDP-порта, заданных вышеупомянутой H2C-командой, и проверяет данные с использованием шаблона, учитывая особенности буфера TX. Если всё совпадает — прошивка, указывает 0x30 в качестве причины «пробуждения» системы по сети, после чего «будит» хост.

В исходном коде Linux-драйвера эта причина названа FW_WOW_V2_REALWOW_V2_WAKEUPPKT (её обработка там, на самом деле, не производится). А теперь зададимся вопросом о том, что же такое RealWoW. Пожалуй, ответ на все вопросы нам должна дать страничка сайта Realtek, посвящённая этой технологии.


Страница realwow.realtek.com

В Linux-драйвере, к сожалению, технология Realtek RealWoW не реализована (и не вполне ясен механизм получения новых ID от Realtek), поэтому я не смог насладиться удовольствием экспериментов с RealWoW.

Но анализ прошивки позволил узнать о том, что работа этой технологии, вероятно, основана на регулярной отправке UDP-пакетов (предоставляемых драйвером) на сервер, который в ответ отправляет нечто вроде ACK-пакета. Когда на сайте вводится корректный ID, сервер отправляет на устройство UDP-пакет, который «будит» это устройство (а он может это сделать, так как имеется открытое соединение с этим устройством благодаря регулярной отправке UDP-пакетов). После получения такого пакета Wi-Fi-прошивка «будит» устройство через PCIe.

Кейлоггер, основанный исключительно на возможностях микроконтроллера Intel 8051


Теперь, после того, как мы немного разобрались с прошивкой, подумаем о том, как внести в неё какие-нибудь изменения. В предыдущем материале я занимался анализом прошивки EC моего ноутбука. Не попробовать ли нам наладить «общение» между двумя контроллерами, основанными на Intel 8051? Такие контроллеры, в конце концов, любят друг с другом «разговаривать», иначе не существовало бы интерфейса USB.

Мой план заключается в том, чтобы EC получал бы сведения о нажатиях на клавиши клавиатуры ноутбука, а после этого отправлял бы их Wi-Fi-чипу, который передавал бы их куда-то по сети.

Мы, рассматривая задачу с точки зрения EC, не сможем воспользоваться DMA, так как он находится за шиной LPC, то есть, полагается на возможности ISA DMA, что просто ужасно, и, кроме того, может обращаться лишь к младшим 16 Мб памяти. В результате механизм взаимодействия контроллеров, основанный на DMA, нас не интересует.

Взглянем на схему связей компонентов платы из предыдущего материала.


Схема связей компонентов платы (оригинал)

Теперь мне хотелось бы обратить ваше внимание на то, что линии EC_TX и EC_RX (они находятся в правом нижнем углу между EC и RTL8821AE) не подключены к Wi-Fi-чипу, подключаемому к плате с использованием интерфейса M.2. Но тут можно обратить внимание на то, что соединение EC_RX, кроме того, подключено к линии rfkill(bt) через другой пин (и на пути к CPU тут имеется резистор, в результате EC может обойти CPU, не вызвав при этом короткого замыкания). Как насчёт того, чтобы использовать это соединение для передачи данных?

После кое-каких тестов мне удалось выяснить, что воздействие на эту линию мешает работе Wi-Fi. Оказалось, правда, что это — не такая уж и большая проблема. Если её, после использования, снова переводят в низкий уровень, то хост и прошивка видят лишь потерю некоторых пакетов.

Так как тут имеется лишь одна линия, контроллеры надо синхронизировать каким-то способом, не предусматривающим использование выделенной шины синхронизирующих импульсов. Я решил прибегнуть к UART-подходу, то есть — передавать биты один за другим с некоторым интервалом.

В EC уже используется 1 мс-таймер, поэтому я просто пропатчил код таймера, организовав проверку на наличие новых данных в 16-байтном буфере клавиатуры и отправку данных через пин EC_RX со скоростью 1 бит/мс.

На стороне Wi-Fi возникла проблема с выполнением кода прошивки. Оказалось, что выполнение этого кода просто останавливается после короткого времени бездействия ради экономии энергии. Вместо того, чтобы обращаться к коду драйвера Realtek для того чтобы выяснить, как работает система управления питанием, я прибегнул к другому методу, который заключается в регулярной записи данных в конфигурационное пространство, что, похоже, поддерживает устройство во включённом состоянии.

Прошивка Wi-Fi-чипа тоже должна каким-то образом ориентироваться во времени. На чипе имеются стандартные периферийные устройства контроллера Intel 8051, но они, похоже, не генерируют прерывания, в результате их нужно опрашивать в главном цикле. Быстрый эксперимент позволил выяснить, что речь идёт о том, что частота его срабатываний примерно равна 6,67 МГц, что говорит о том, что Intel 8051 использует источник тактовых импульсов с частотой 80 МГц. Эту догадку подтверждает упоминание такой частоты в исходном коде драйвера.

Ещё один эксперимент позволил выяснить, что воздействие на rfkill(bt) приводит к изменению бита №3 REG_GPIO_PIN_CTRL_2. В результате в моём распоряжении оказалось всё, что нужно для реализации другой стороны UART-соединения. Я просто настроил таймер так, чтобы он генерировал переполнение каждые 1/3 мс, что дало более высокую частоту для обеспечения правильной работы UART. После того, как передан один байт, EC, перед передачей следующего байта, некоторое время бездействует.

Пока EC ждёт момента передачи следующего байта, Wi-Fi-чип отправляет UDP-пакет, содержащий ранее принятый им байт, на заданный IP-адрес и порт. Делается это через незашифрованное Wi-Fi-соединение.

Хотя тут, вероятно, можно применить шифрование, это потребовало бы дополнительных усилий: нужно было бы обновлять номер последовательности и, если пакеты передавал бы и хост, нужно было бы просматривать буфер TX для выяснения того, что именно передаётся в некий момент времени. Это, кроме того, привело бы к невозможности отправки некоторых пакетов с хоста из-за дублирования номеров последовательности. Прошивка, возможно, могла бы осуществлять поиск незашифрованных Wi-Fi-сетей, находящихся поблизости, подключаться к ним и отправлять данные с использованием DNS, что обычно позволяет обходить порталы, на которых может потребоваться авторизация.

Далее, я запустил небольшую программу на Raspberry Pi. Эта программа принимает UDP-пакеты и преобразует PS/2-коды в события uinput. В результате получилось, что клавиатура ноутбука стала играть роль и клавиатуры для Raspberry Pi.

Итоги


Самое забавное в этом моём проекте то, что он, фактически, представляет собой кейлоггер, в ходе работы которого не используется выполнение кода на CPU. Для обеспечения его функционирования достаточно перепрошить EC и заменить прошивку Wi-Fi-модуля, сделав так, чтобы при его запуске загружалась бы модифицированная версия прошивки.

Пользовались ли вы технологией Realtek RealWoW?

Комментарии (2)


  1. Kiano
    16.08.2021 08:53
    +4

    Спасибо, интересное наблюдение и анализ. По большому счёту, целая ниша для дата стилинга, воровства, по сути от неё можно защититься, но это если знать. До прочтения не задумывался даже...


  1. RarogCmex
    16.08.2021 15:01
    +1

    Интересно, можно ли звуковую карту так программировать. Аппаратное эхо и дилей, все дела... :)