Беспроводные девайсы сейчас повсюду (мышки, клавиатуры, звонки, даже розетки управляемые по радио). Зачастую вендоры в своих устройствах используют самописные протоколы, не задумываясь о безопасности. Так как материала в сети на эту тему не так много, давай разберем один из таких протоколов на примере беспроводной клавиатуры Logitech и напишем кейлоггер для нее.

Что нам понадобится?

Для приема сигнала в этом примере я буду использовать HackRF One. Но также можно использовать и TV-тюнер на RTL2832, который можно найти на aliexpress.com за 10-15 долларов. Наблюдать за радиочастотным спектром и искать нужный нам сигнал будем с помощью утилиты GQRX для Linux. Для исследования радиосигнала, определения типа модуляции и скорости передачи данных (baud rate) будем использовать утилиты baudline и inspectrum. В основном работать с сигналом мы будем в GNURadio. GNURadio - настоящий монстр, включающий в себя огромное количество блоков. Среди них есть фильтры, модуляторы/демодуляторы, декодеры и инструменты для визуализации сигнала.

OSINT - наше все

Осмотрим нашу “жертву”. На обратной стороне клавиатуры можно увидеть ее FCC ID, уникальный номер, по которому можно найти информацию об устройстве. Рис. 1. FCC ID устройства

В нашем случае FCC ID JNZ128177. Для поиска информации перейдем на сайт fcc.gov. Рис. 2. Поиск информации по FCC ID устройства Тут мы можем найти как выглядит наше устройство, вид модуляции и частоту, на которой клавиатура общается с USB-приемником. Рис. 3. Внешний вид клавиатуры Рис. 4. Информация о частоте и виде модуляции Мы видим, что клавиатура может работать на двух частотах: 27.095 МГц и 27.145 МГц в зависимости от выбранного канала. Эта информация пригодится нам позже. Также мы видим, что используется FSK (Frequency Shift Keying) модуляция, или частотная манипуляция.

Ищем сигнал

Для наблюдения за частотным спектром на частоте, найденной нами во время OSINT, будем использовать утилиту GQRX. Настроимся на нужную частоту.

Рис. 5. Водопадное представление спектра на нужной нам частоте

Как мы видим, спектр сильно зашумлен, и выделить среди шума информационный сигнал с клавиатуры - практически невозможно. Дело в том, что на частотах ниже 480 МГц помехи могут возникать из-за неэкранированного USB-кабеля. А в комплекте с HackRF идет как раз такой. В своем GitHub Майкл Оссман приводил некоторые требования к кабелю, который будет использоваться с HackRF. Если вкратце, то это должен быть экранированный кабель длиной менее 1.8 метров с феритовыми сердечниками на концах. Стоит отметить, что при использовании TV-тюнеров на RTL2832, подобных помех не будет, так как там нет кабеля.

После установки двух феритовых сердечников на стандартный кабель от HackRF ситуация сильно изменилась в лучшую сторону. На Рисунке 6 можно увидеть незашумленный спектр и информационный сигнал с клавиатуры.

Рис. 6. Частотный спектр после установки феритовых сердечников

Ура! Мы нашли наш сигнал. Теперь нам нужно его записать для дальнейшего анализа: определения вида модуляции и скорости передачи данных (количества символов, передаваемых за одну секунду).

Записываем и анализируем

Для записи сигнала мы составим простую схему (flowgraph) в GNURadio Companion. Схема будет содержать источник сигнала - osmocom Source и блок File Sink, необходимый для записи сигнала в файл. На Рисунке 6 ровно на частоте 27.095 МГц мы могли наблюдать сигнал, возникающий от постоянного источника питания HackRF, так называемый DC Spike. Этот сигнал не несет никакой полезной информации и может мешать нам при записи, поэтому наш источник сигнала (osmocom Source) мы настроим не соседнюю частоту 27.08 МГц. Схема должна выглядеть как на Рисунке 7.

Рис. 7. Схема для записи сигнала

Для анализа записанного сигнала, будем использовать утилиту inspectrum. Качать ее лучше с GitHub, так как версия, которая ставится через apt-get не включает в себя полезные функции, такие как inspectrum cursor, позволяющие очень просто определить скорость передачи данных (baud rate). На Рисунке 8 можно увидеть сигнал, открытый в утилите inspectrum.

Рис. 8. Анализ сигнала в inspectrum

На Рисунке 8 мы видим сигнал DC spike, ровно на той частоте, на которую настроен osmocom Source. Сигнал с клавиатуры немного сдвинут вверх по частоте. На Рисунке 8 отчетливо видно, что это FSK модуляция. С помощью inspectrum cursor можно выделить несколько символов, и рассчитать скорость передачи данных (baud rate), которая в нашем случае составляет 2.4 кГц (2400 символов в секунду).

Как работает FSK?

FSK (Frequency Shift Keying) модуляция - это такой вид модуляции, когда данные передаются путем сдвига частоты. В нашем случае используется 2-FSK (двоичная FSK), при которой частоты всего 2. Одна из частот соответствует нулю, другая соответствует единице. Как правило большей частоте соответствует единица. На Рисунке 9 можно увидеть пример передачи двоичной последовательности с использованием 2-FSK модуляции.

Рис. 9. Пример 2-FSK модуляции

Демодуляция

Мы уже определили частоту, на которой передается сигнал с нашей клавиатуры, а также скорость передачи данных и вид модуляции. Теперь нам необходимо использовать эти полученные знания и демодулировать сигнал, чтобы извлечь из него информацию. Добавим в нашу схему блок для демодуляции сигнала - Quadrature Demod, установив Gain = 1 и изменим тип входных данных блока File Sink на Float. Также, для того, чтобы выделить наш полезный сигнал, добавим фильтр нижних частот (Low Pass Filter) с частотой среза 50 кГц и шириной переходной полосы 20 кГц.

Фильтр нижних частот - фильтр, пропускающий частотный спектр сигнала ниже заданной частоты и подавляющий все, что находится выше заданной частоты.

На Рисунке 10 на простом примере можно увидеть как работает фильтр нижних частот , что такое частота среза (cutoff_freq) и ширина переходной полосы (transition_width).

Рис. 10. Фильтр нижних частот

После установки описанных выше блоков схема должна выглядеть как на Рисунке 11.

Рис. 11. Схема демодулятора

Откроем файл с записанным сигналом в baudline. Для того, чтобы правильно открыть файл, необходимо указать Sample Rate (частоту дискретизации) и Decode Format. На Рисунке 12 можно увидеть указанные при открытии файла параметры.

Рис. 12. Параметры открытия файла в baudline

Поигравшись немного с масштабом с помощью клавиш Alt + стрелочки, находим наш демодулированный сигнал в окне waveform.

Рис. 13. Демодулированный сигнал

На Рисунке 13 кроме полезного сигнала мы видим шум, который не позволит декодировать сигнал. Сам полезный сигнал с клавиатуры также выглядит довольно шумным. Для решения этих двух проблем, нам потребуется добавить пару блоков в нашу схему.

Блок Power Squelch не пропускает сигналы, амплитуда которых ниже заданного нами значения. Посмотрим на амплитуду нашего сигнала и выберем тот уровень, ниже которого находятся только ненужные шумы.

Рис. 14. Амплитуда полезного сигнала

На Рисунке 14 видно, что полезный сигнал находится на уровне выше -35 дБ. Таким образом, все, что находится ниже, мы можем отрезать. Для этого добавим блок Power Squelch cо значением Threshold равным -35dB.

Также, для того, чтобы очистить сигнал от шума, добавим еще один Low Pass Filter сразу после блока Quadrature Demod. Выберем частоту среза равную 2400 (соответствующую значению baud rate) и ширину переходной полосы, равную 1200 (соответствующую baud rate/2). Уменьшим частоту дискретизации (sample rate), установив значение Decimation = 100 в блок Low Pass Filter, чтобы разгрузить процессор.

После добавления блоков наша схема должна выглядеть как на Рисунке 15.

Рис. 15. Схема демодулятора с дополнительным Low Pass Filter и Power Squelch

Запишем сигнал еще раз, и посмотрим на него в baudline. При открытии файла важно установить значение sample rate равное 200 кГц вместо прежних 2 МГц, так как мы уменьшили sample rate в нашей схеме в 100 раз. Снова немного поигравшись с масштабом, находим наш сигнал.

Рис. 16. Демодулированный сигнал

Декодируем сигнал

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

Таким образом, нам необходимо подготовить наш сигнал, перенеся его на ось абсцисс. Для этого мы будем использовать блок Add Const.

Рис. 17. Блок Add Const

Теперь наш сигнал должен выглядеть как на Рисунке 18. Ocь абсцисс теперь проходит ровно посередине, что позволит блоку Binary Slicer преобразовать наш сигнал в последовательность нулей и единиц.

Рис. 18. Подкготовленный сигнал

Добавим блок Binary Slicer между блоками Add Const и File Sink. При этом необходимо поменять Input Type у блока File Sink на Byte.

Рис. 19. Схема с добавленным блоком Binary Slicer

Запишем сигнал и откроем файл в любом hex редакторе. Я использовал bless. В редакторе мы видим значения каждого сэмпла, при этом 00 соответствует нулю, а 01 соответствует единице. Для работы с этими значениями необходимо определить сколько сэмплов находится в одном символе. На Рисунке 20 красным цветом выделена преамбула, которая отчетливо видна на Рисунке 21. Для передачи одного символа требуется 8-9 сэмпловРис. 20. Преамбула

Если внимательно присмотреться к нашему сигналу, то становится видно, что посылка начинается с преамбулы, состоящей из последовательности чередующихся нулей и единиц, после которых идет последовательность, состоящая из трех нулей, сообщающая приемнику, о том, что посылка началась. Заканчивается посылка последовательностью из трех единиц. Также стоит заметить, что в посылке встречаются последовательности длиной T = 1, T = 1.5, T = 2, и T = 3, где T - длина одного символа.

Рис. 21. Сигнал

Наличие T = 1.5 свидетельствует об использовании кода Миллера (Miller encoding).

Код Миллера - код, в котором единица представлена, как переход на противоположный уровень в середине битового периода. Как раз такие переходы и формируют T = 1.5. Ноль, за которым идет единица представляется отсутствием перехода на противоположный уровень. Ноль, за которым идет ноль представляется переходом на противоположный уровень в начале битового периода.

Извлекаем символы

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

Прежде всего, создадим сокет и поставим его слушать на порту 9090. Принимать данные мы будем порциями по 1024 байт. Каждую такую порцию мы сразу отправляем в функцию rebuild.

def main():
    sock = socket.socket()
    sock.bind(('', 9090))
    sock.listen(1)
    conn, addr = sock.accept()
    while 1:
        data = conn.recv(1024)
        if not data: break
        rebuild(data)
    conn.close()

Функция rebuild обращается к предыдущему значению сэмпла и счетчику сэмплов. С помощью ord переводим из 01 в 1 и из 00 в 0, для дальнейшей работы с последовательностью. В samples_list теперь содержатся значения сэмплов в нормальном виде. Пробегаемся по всем сэмплам в samples_list. Если видим, что текущее значение сэмпла равно 0, а предыдущее было отлично от нуля, значит до этого момента мы наблюдали пик, длина которого current_sample_count. Передаем current_sample_count и информацию о том, пик это был или минимум в функцию recover_miller для дальнейшего анализа и сбрасываем значение current_sample_count. Точно так же мы поступаем, если наблюдаем единицу после нескольких нулей. Если же мы видим, что предыдущее и текущее значения сэмплов совпадают, значит просто увеличиваем значение current_sample_count на единицу. После того, как пробежались по всем сэмплам, очищаем samples_list.

def rebuild(data):
    global previous_sample_value
    global current_sample_count

    for sample in data:
        samples_list.append(ord(sample))

    for sample in samples_list:
        if sample == 0:
            if previous_sample_value != 0:
                recover_miller(current_sample_count, 1)
                current_sample_count = 0
            else:
                current_sample_count += 1
        elif sample == 1:
            if previous_sample_value != 1:
                recover_miller(current_sample_count, 0)
                current_sample_count = 0
            else:
                current_sample_count += 1
        previous_sample_value = sample

    del samples_list[:]

Функция recover_miller необходима для восстановления символов. Переменная previous_message_string хранит предыдущую посылку в формате строки. Анализируем длину пика или минимума и добавляем их в recovered_list с удвоенной длиной. Это нужно для того, чтобы однозначно отличать пики длиной T = 1, T = 1.5 и T = 2. Если в recovered_list мы находим шесть единиц, значит это конец посылки, ищем в этом списке начало посылки (последовательность из шести нулей). Если находим начало посылки, то сохраняем посылку в message_string, так как список не может быть ключом в словаре.

def recover_miller(current_sample_count, side):
    global previous_message_string

    if current_sample_count >= 6 and current_sample_count <=10:
        if side == 1:
            recovered_list.append("11")
        elif side == 0:
            recovered_list.append("00")

    elif current_sample_count > 10 and current_sample_count < 14:
        if side == 1:
            recovered_list.append("111")
        elif side == 0"
            recovered_list.append("000")

    elif current_sample_count >= 14 and current_sample_count < 20:
        if side == 1:
            recovered_list.append("1111")
        elif side == 0:
            recovered_list.append("0000")

    elif current_sample_count >= 20 and current_sample_count < 28:
        if side == 1:
            recovered_list.append("111111")
            if "000000" in recovered_list:
                start_index = recovered_list.index("000000")
                stop_index = recovered_list.index("111111") + 1
                message = recovered_list[start_index:stop_index]
                message_string = ''.join(message)
                print message_string
                del recovered_list[:]
        elif side == 0:
            recovered_list.append("000000")

    elif current_sample_count < 7:
        recovered_list.append("too short")
    elif current_sample_count >= 28:
        recovered_list.append("too long")
if __name__ == '__main__':
    main()

Теперь нам необходимо перенаправить данные из GNURadio на порт 9090. Для этого добавим блок TCP Sink в нашу схему, изменив Input Type на Byte. Рис. 22. Финальная схема

Запустим скрипт и flowgraph в GNURadio. На Рисунке 22 можно увидеть сообщения, отправляемые клавиатурой при нажатии на клавишу Q.

Рис. 23. Вывод кейлоггера

Можно заметить, что при нажатии на клавишу и при отжатии клавиши отправляются по два сообщения. Таким образом на Рисунке 22 зафиксированы всего 2 нажатия на клавишу Q. Теперь нам остается составить словарь с буквами и соответствующими им двоичными и добавить в наш скрипт поиск по словарю.

Запускаем наш кейлоггер и flowgraph, и видим в консоли введенные пользователем символы.

Рис. 24. Вывод кейлоггера

Заключение

Таким образом нам удалось перехватить сигнал с беспроводной клавиатуры и восстановить нажатые пользователем клавиши. В дальнейшем, можно реализовать скрипт, который будет отправлять нажатия клавиш удаленно на компьютер жертвы (keystroke injection) и осуществлять ввод команд (remote code execution). Подобные атаки возможны по причине отсутствия каких-либо защитных механизмов при передаче данных между клавиатурой и приемником. Данные о нажатии клавиш передаваются в открытом виде, на одной из двух частот, о которой устройства договорились на этапе установления соединения.

Полезные ссылки: