[Перевод] Обход SSH Keystroke Obfuscation
В OpenSSH версии 9.5 были добавлены меры предотвращения keystroke timing‑атаки за счет анализа трафика. Патч включал добавление обфускации таймингов нажатий клавиш в клиенте SSH. Согласно информации о релизе, эта функция «пытается скрыть тайминги между нажатиями за счет отправки трафика взаимодействия через фиксированные интервалы времени (по умолчанию — 20 мс), когда отправляется малое количество данных». Также отправляются фейковые chaff‑пакеты после последнего нажатия, что значительно усложняет анализ трафика, скрывая реальные нажатия среди потока искусственных пакетов. Эта функция может настраиваться с помощью опции ObscureKeystrokeTiming
в конфигурации SSH.
Я исследовал влияние использования анализа задержек между нажатиями клавиш для определения отправляемых внутри сессии SSH команд клиентом в рамках моей бакалаврской диссертации. В ходе этого я обнаружил способ обхода этого метода обфускации, работающий вплоть до текущего релиза. Я оповестил разработчиков 24 апреля и получил ответ от самого Дэмьена Миллера (разработчик, добавивший данный патч), но, к сожалению, дальнейшая переписка была проигнорирована. Отсюда и публикация данной статьи.
Существующая проблема
Прошлые реализации протокола SSH раскрывали значительное количество метаданных, особенно при использовании в интерактивном режиме. Несмотря на то, что метаданные полностью зашифрованы, они могут быть использованы для нарушения конфиденциальности сессии. Проще говоря, каждый раз, когда вы нажимаете клавишу в интерактивной сессии SSH, это нажатие упаковывается, дополняется байтами, шифруется и отправляется серверу. Затем оно возвращается обратно сервером. Это означает, что каждое нажатие может быть явно идентифицировано и отмечено по времени, что открывает возможности для атак на основе анализа задержек между нажатиями клавиш с целью определения, что именно вводил клиент. Анализ также может быть усилен добавлением дополнительного контекста — например, ответов сервера и других метаданных, что я детально проанализировал в рамках написания статьи для университета, которая будет опубликована после оценки.
Показанное выше можно увидеть с помощью Wireshark с фильтром для SSH. Но для упрощения процесса я написал инструмент SSHniff для автоматизации извлечения метаданных. Я также добавил Jupyter notebooks, где показано, как перехваченные задержки могут быть использованы для определения передаваемых UNIX‑команд, используя такие алгоритмы, как Dynamic Time Warp (DTW) и/или Time Series Forests. Подробная информация об этом собрана в отдельной статье.
Обфускация in a Nutshell
И хотя раньше этот вектор атаки игнорировался, превентивные меры были впервые добавлены в октябре прошлого года (2023). Идея заключалась в том, чтобы спрятать реальные нажатия клавиш среди фейковых пакетов, которые выглядели бы одинаково для внешнего наблюдателя. Это те самые chaff‑пакеты. Также все исходящие пакеты передаются через равные промежутки времени, примерно 20 мс. Эти chaff‑пакеты на самом деле представляют собой пакеты SSH2_MSG_PING
и SSH2_MSG_PONG
, имеющие одинаковый размер с пакетами нажатий клавиш. При каждом нажатии эти пакеты массово отправляются, пряча последующие нажатия клавиш. Они также отправляются на протяжении заданного количества времени после последнего нажатия.
Обнаружение обхода
Частью моей диссертации было рассмотрение эффективности введённых превентивных мер OpenSSH. И хотя я ожидал, что данный вектор атаки будет полностью закрыт, после загрузки вывода Wireshark в SSHniff я обнаружил, что определённые пакеты всё ещё значительно выделяются среди сотен пакетов с задержкой в 20 мс.
В этой сессии я выполнял команду uptime. Как можно заметить, для каждой буквы в uptime есть соответствующий пик на графике, но стоит отметить, что первое реальное нажатие имеет задержку в 0 мс, а последний пик соответствует нажатию Enter, давая нам в итоге 7 нажатий.
Для подтверждения я выполнил несколько дополнительных команд и наблюдал аналогичное поведение. netstat ‑tlpn
, например, содержит 13 символов. Нажатие Enter в данном случае было опущено.
Я понял, что эти пики на графиках возникли из‑за того, что SSHniff пропускал по 3 пакета (что и приводило к задержке примерно в 60 мс для каждого пика). Это было вызвано реализацией инструмента — он ищет только пакеты определенного размера K, который соответствует размеру пакетов нажатий клавиш. Это означало, что среди chaff‑пакетов некоторые пакеты были больше или меньше ожидаемого размера, что приводило к их пропуску.
Это заставило меня подробнее рассмотреть вывод Wireshark. Действительно, для каждого нажатия после вызова chaff реальные нажатия создавали более крупные пакеты (а также два ответа от сервера), что позволяло явно их определить. Я использовал упомянутые ранее методы, такие как DTW, чтобы проверить, могут ли эти выделяющиеся пакеты быть использованы для определения отправляемой команды, и это сработало. Это подтвердило, что эти пакеты содержат реальные нажатия клавиш.
Больше информации о анализе задержек вы можете найти в разделе «Keystroke Latency Analysis» этой статьи.
Далее я уведомил разработчиков OpenSSH и продолжил исследование.
Большие пакеты
Выделяющиеся пакеты были примерно в два раза больше «нормальных» нажатий (и, соответственно, в два раза больше chaff‑пакетов). Я говорю «примерно», потому что точный размер зависит от используемого шифрования и других факторов.
Chaff‑пакеты имеют длину 102 байта, что в обычных условиях совпадало бы с размером обычных нажатий при использовании данного набора шифров (тех, которые SSHniff
также отфильтровал бы). Поскольку первый отправляемый клиентом пакет из тройки больше (138 байт), все три пакета пропадают и приводят к упомянутым ранее пикам на графике.
Что интересно, первоначальное нажатие клавиши отправляется «нормально», без упаковки в более объемный пакет. Большие пакеты начинают появляться только после начала chaff‑флуда. Похожим образом, если дождаться завершения флуда перед вводом следующего нажатия, это приводит к обычной паре пакетов.
Вербозный вывод OpenSSH
Я скомпилировал OpenSSH v9.7
с флагом DPACKET_DEBUG
для получения более вербозного вывода. В созданной сессии я выполнил команду whoami
. Далее будет показано, как клиент создает и упаковывает нажатия клавиш.
Начиная с первоначального нажатия, которое, как и было упомянуто ранее, отправляется пакетом обычного размера, после чего отправляются chaff‑пакеты.
debug1: packet_start[94]
plain: buffer len = 15
0000: 00 00 00 00 00 5e 00 00 00 00 00 00 00 01 77 .....^........w
debug1: send: len 20 (includes padlen 5, aadlen 4)
encrypted: buffer len = 36
0000: c7 e4 05 07 10 e1 f3 4b 24 ba 61 e8 fe 6e 0b 01 .......K$.a..n..
0016: 79 50 4e af 6a 96 31 5e ff fa ec bf 2b 3b 91 42 yPN.j.1^....+;.B
0032: a7 14 64 a6 ..d.
debug3: obfuscate_keystroke_timing: starting: interval ~20ms
debug1: input: packet len 20
debug1: partial packet: block 8, need 16, maclen 0, authlen 16, aadlen 4
read_poll enc/full: buffer len = 36
0000: d7 47 be 72 2c e8 e7 e1 ae 38 fe a2 f4 e9 e0 04 .G.r,....8......
0016: 9c 00 fd 1d 41 f8 d9 6e 61 4b 90 4e a4 e6 2c 30 ....A..naK.N..,0
0032: 92 92 42 54 ..BT
debug1: input: padlen 5
debug1: input: len before de-compress 10
фТип пакета 94 — SSH2_MSG_CHANNEL_DATA
, заданный в ssh2.h, хранит в себе одиночное нажатие. Мы также можем наблюдать начало обфускации с интервалом в ~20 мс. Этот пакет имеет длину в 36 байт после шифрования, что совпадает с выводом Wireshark при просмотре длины полезной нагрузки TCP.
Далее идет эхо от сервера, читаемое клиентом:
read/plain[94]:
buffer len = 9
0000: 00 00 00 00 00 00 00 01 77 ........w
debug1: received packet type 94
w
Давайте теперь посмотрим на chaff‑пакеты, идущие за первым нажатием. Вот что отправляет клиент:
debug1: packet_start[192]
plain: buffer len = 15
0000: 00 00 00 00 00 c0 00 00 00 05 50 49 4e 47 21 ..........PING!
debug1: send: len 20 (includes padlen 5, aadlen 4)
encrypted: buffer len = 36
0000: d7 cf 6b 64 25 d6 40 89 68 eb 4d 6c a0 cb de e6 ..kd%.@.h.Ml....
0016: d0 b5 14 81 c4 57 6f c4 3a 82 eb 55 44 d2 b4 9d .....Wo.:..UD...
0032: 3b 58 12 ac ;X..
debug1: input: packet len 20
debug1: partial packet: block 8, need 16, maclen 0, authlen 16, aadlen 4
read_poll enc/full: buffer len = 36
0000: 88 40 00 52 0e 4b fc eb 89 f7 72 1f d6 a4 3f dd .@.R.K....r...?.
0016: 0b dd 27 19 0e a8 84 f7 74 6f 43 e7 8c eb 16 9e ..\'.....toC.....
0032: 37 4e 89 95 7N..
debug1: input: padlen 5
debug1: input: len before de-compress 10й
Мы видим, что это SSH2_MSG_PING, и, что самое важное, его длина также составляет 36 байт, что идеально совпадает с длиной реального нажатия. Отправляется несколько подобных PING‑пакетов, после каждого из которых сервер отправляет PONG (SSH2_MSG_PONG), длина каждого из которых также равна 36 байт.
read/plain[193]:
buffer len = 9
0000: 00 00 00 05 50 49 4e 47 21 ....PING!
debug1: received packet type 193
debug1: Received SSH2_MSG_PONG len 5
Пока что все выглядит так, как и задумывалось. Но после того как мы доходим до второго реального нажатия, а именно буквы h
, дело принимает другой оборот.
В первую очередь формируется пакет, как и раньше:
debug1: packet_start[94]
plain: buffer len = 15
0000: 00 00 00 00 00 5e 00 00 00 00 00 00 00 01 68 .....^........h
debug1: send: len 20 (includes padlen 5, aadlen 4)
encrypted: buffer len = 36
0000: c3 22 ea f0 f5 47 15 db 95 c9 64 ec e6 66 40 a2 .\"...G....d..f@.
0016: d2 fc 71 e2 59 35 c3 a7 85 90 4c b9 7f 17 fd 65 ..q.Y5....L....e
0032: 97 54 c3 e6
Длина такая же, но, что интересно, отладочные строки partial packet
и read_poll
отсутствуют, поскольку пакет еще не отправлен. Затем идет формирование PING‑пакета до того, как нажатие клавиши отправляется:
debug1: packet_start[192]
plain: buffer len = 15
0000: 00 00 00 00 00 c0 00 00 00 05 50 49 4e 47 21 ..........PING!
debug1: send: len 20 (includes padlen 5, aadlen 4)
encrypted: buffer len = 72
0000: c3 22 ea f0 f5 47 15 db 95 c9 64 ec e6 66 40 a2 .\"...G....d..f@.
0016: d2 fc 71 e2 59 35 c3 a7 85 90 4c b9 7f 17 fd 65 ..q.Y5....L....e
0032: 97 54 c3 e6 c6 59 df 64 eb c8 ba d4 f7 ed 5a 88 .T...Y.d......Z.
0048: 53 13 da 7e 7f 1d 63 9d dd 23 40 b4 b9 67 6e f3 S..~..c..#@..gn.
0064: 76 12 66 1b 89 5b 5a 21 v.f..[Z!
debug1: input: packet len 20
debug1: partial packet: block 8, need 16, maclen 0, authlen 16, aadlen 4
read_poll enc/full: buffer len = 36
0000: 47 3e ca 40 05 b8 a8 5b 1d 1a 2b bd bd c6 d5 35 G>.@...[..+....5
0016: d1 dc 56 f2 28 8a c4 07 df cb 73 e1 fb cc 0a 9e ..V.(.....s.....
0032: 20 73 c7 97 s..
debug1: input: padlen 5
debug1: input: len before de-compress 10
Здесь мы видим отсутствовавшие ранее строки partial packet
и read_poll
. Кроме того, из‑за объединения пакетов зашифрованная длина составляет 72 байта вместо ожидаемых 36.
И, наконец, мы получаем два ответа от сервера: сначала PONG
, а затем ответ на нажатие клавиши h
.
read/plain[193]:
buffer len = 9
0000: 00 00 00 05 50 49 4e 47 21 ....PING!
debug1: received packet type 193
debug1: Received SSH2_MSG_PONG len 5
debug1: input: packet len 20
debug1: partial packet: block 8, need 16, maclen 0, authlen 16, aadlen 4
read_poll enc/full: buffer len = 36
0000: ec 9f ef a2 55 7e c3 4c f8 75 08 a9 8d 45 7e 14 ....U~.L.u...E~.
0016: 1f 55 b1 44 6e ea c7 f9 c9 ef ed ef 33 42 a7 29 .U.Dn.......3B.)
0032: 67 84 fa 94 g...
debug1: input: padlen 5
debug1: input: len before de-compress 10
read/plain[94]:
buffer len = 9
0000: 00 00 00 00 00 00 00 01 68 ........h
debug1: received packet type 94
h
Вот так выглядят пики троек пакетов при вербозном дебаг‑выводе. Это также объясняет больший размер пакетов и дублирование ответов сервера, поскольку реальные нажатия упаковываются вместе с PING‑пакетами, что приводит к удвоению размера пакета и вызывает двойной ответ сервера.
SSHniff
В духе старого доброго подхода «PoC or GTFO», я написал отвратительный, но рабочий патч для SSHniff, который, если определяет версию SSH выше 9.4, предполагает использование обфускации и применяет обходной метод. Имейте в виду, что это действительно непотребный код, который отравляет своим существованием мой текстовый редактор, но его должно быть достаточно, чтобы показать, что текущая реализация обфускации абсолютно прозрачна.
Вот пример выполнения SSHniff на перехваченной SSH-сессии, использовавшей обфускацию: я выполнял команды iptables -S
, whoami
, ls -al
, опечатался exi
, и наконец exit
. Вы можете проверить это самостоятельно, PCAP приложен тут.
┃╭─────────────────Client─────────────────╮ ╭─────────────────Server─────────────────╮
┃│ 192.168.0.19:55932 │ │ 192.168.0.16:22 │
┃│ e42184b06d45385a906f0803d04c83da │----->│ aae6b9604f6f3356543709a376d7f657 │
┃│ SSH-2.0-OpenSSH_9.7 │ │ SSH-2.0-OpenSSH_9.7 │
┃╰────────────────────────────────────────╯ ╰────────────────────────────────────────╯
┣━ tcp.seq ─ Latency μs ─ Type
┣ [4450] ─ ( 0) ─ Keystroke
┣ [4774] ─ ( 177182) ─ Keystroke
┣ [5026] ─ ( 119630) ─ Keystroke
┣ [5170] ─ ( 60477) ─ Keystroke
┣ [5530] ─ ( 182991) ─ Keystroke
┣ [5638] ─ ( 36727) ─ Keystroke
┣ [5998] ─ ( 175786) ─ Keystroke
┣ [6142] ─ ( 59886) ─ Keystroke
┣ [6394] ─ ( 119464) ─ Keystroke
┣ [6646] ─ ( 117633) ─ Keystroke
┣ [7078] ─ ( 219396) ─ Keystroke
┣╮ [10858] ─ ( 3478329) ─ Enter
┃╰─╼[236]
┣━
┣ [10858] ─ ( 0) ─ Keystroke
┣ [11290] ─ ( 238980) ─ Keystroke
┣ [11470] ─ ( 80064) ─ Keystroke
┣ [11650] ─ ( 79103) ─ Keystroke
┣ [11902] ─ ( 122768) ─ Keystroke
┣ [12226] ─ ( 158690) ─ Keystroke
┣╮ [15034] ─ ( 3324090) ─ Enter
┃╰─╼[204]
┣━
┣ [15034] ─ ( 0) ─ Keystroke
┣ [15322] ─ ( 162362) ─ Keystroke
┣ [15502] ─ ( 81398) ─ Keystroke
┣ [15682] ─ ( 83084) ─ Keystroke
┣ [15862] ─ ( 79398) ─ Keystroke
┣ [16114] ─ ( 123489) ─ Keystroke
┣╮ [18598] ─ ( 1363393) ─ Enter
┃╰─╼[3116]
┣━
┣ [18598] ─ ( 0) ─ Keystroke
┣ [18922] ─ ( 164250) ─ Keystroke
┣ [19210] ─ ( 144942) ─ Keystroke
┣╮ [22522] ─ ( 1822534) ─ Enter
┃╰─╼[256]
┣━
┣ [22522] ─ ( 0) ─ Keystroke
┣ [22846] ─ ( 162024) ─ Keystroke
┣ [23134] ─ ( 149977) ─ Keystroke
┣ [23458] ─ ( 158038) ─ Keystroke
┣╮ [27350] ─ ( 204709) ─ Enter
┃╰─╼[272]
┣━
┃
┣━━━━
Как видите, нажатия клавиш легко извлекаются и готовы для обработки инструментами анализа.
Анализ задержки нажатий
Это не часть первоначальной цели статьи по описанию обхода обфускации, но это должно помочь понять влияние утечки метаданных протоколом SSH и необходимость подобных мер. Это также дополнит картину самого процесса выполнения атаки.
Чтобы показать, как метаданные SSH могут быть использованы для нарушения конфиденциальности, я продемонстрирую PoC использования SSHniff для извлечения нажатий клавиш и получения команд.
Результат работы Wireshark может быть передан инструменту, что даст нам следующий вывод:
Помимо другой информации, инструмент показывает любую последовательность нажатий, выполненных в рамках сессии, а также относительную задержку в микросекундах, порядковый номер TCP и предполагаемый тип нажатия. Используя размер пакетов, мы можем различать нажатия клавиш, например Backspace, Enter и горизонтальные стрелки, что является еще одной важной точкой в анализе трафика. В рамках показанной сессии было напечатано только exit
, после чего нажата клавиша Enter.
Инструмент также может сериализовать данные для дальнейшего построения графиков и обработки. Я подготовил этот proof of concept, используя Jupyter notebook и небольшой датасет, собранный для моей диссертации.
Ритмичные команды
Для наиболее полного ознакомления с исследованием, ознакомьтесь с Jupyter notebook в репозитории SSHniff. Если вас интересует только обход обфускации, ищите раздел «Patch Analysis» в самом низу.
По сути, команды могут иметь определенные «профили» или ритмы при вводе, которые можно определить по их задержке. На графике ниже показан пример, когда я вводил команду sudo apt upgrade
18 раз:
Датасет, собранный другими участниками, также подтверждает это поведение, хотя некоторые команды идентифицируются легче, чем другие:
Используя такие алгоритмы, как Euclidean Distance или DTW, перехваченная последовательность нажатий клавиш может быть соотнесена с командами в датасете, вычисляя «похожесть» последовательности.
Вот так может выглядеть последовательность, полученная SSHniff:
Некоторые результаты показаны в следующей таблице: