Разблокируем бесплатный WiFi на рейсах British Airways

в 4:16, , рубрики: sni, wifi, wireshark, интернет в самолете
Разблокируем бесплатный WiFi на рейсах British Airways - 1

Недавно я летел из Гонконга в Хитроу рейсом British Airways. Тот же самый маршрут я проделал в 2023 году, и помню, как в 14-часовом путешествии понадеялся на развлечения в самолёте. Однако на этот раз по дороге в Лондон у компании появилось интересное предложение: бесплатный WiFi для «мессенджеров» участникам «The British Airways Club».

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

После входа портал предложил мне «Начать сессию», и это действительно позволило мне общаться текстом. Я попробовал Whatsapp, Signal, Wechat и Discord. Первые три работали (но без поддержки изображений); Discord, как и ожидалось, не заработал. Совсем неплохо для бесплатного WiFi!

Откуда он знает?

Это первый вопрос, который возник у меня после проверки работы мессенджеров. На дворе 2025 год, всё должно шифроваться при передаче. Почему же система знает, что я использую Whatsapp или Discord? Поначалу я решил, что соединение как-то ограничивает ширину канала/передачу данных отдельных TCP-соединений, поэтому при отправке одного-двух сообщений они проходят, но что-то большее отклоняется.

Чтобы проверить эту гипотезу, я попробовал открыть на телефоне классический example.com. К сожалению, он не загрузился; значит, всё это устроено немного сложнее…

К счастью, у меня с собой был ноутбук, поэтому я подключился к WiFi, открыл в Devtools вкладку Network и для верности дополнил всё это Wireshark. После повторной регистрации для подключения к WiFi настало время экспериментов. При открытии чего-то наподобие example.com в Wireshark сразу после Client Hello отображался сброс TCP, поэтому я сразу подумал об SNI. Этот аспект спецификации TLS очень меня раздражал, потому что он широко используется Интернет-провайдерами в Индии для блокировки веб-сайтов (впрочем, для устранения этой проблемы есть ECH (который сам ранее был ESNI)).

Если говорить вкратце, то SNI раскрывает доменное имя КАЖДОГО веб-сайта, к которому подключается пользователь в TLS handshake, до создания туннеля! Хотя само содержимое того, что вы делаете, например, на totallynondodgywebsite.com, зашифровано, любой, следящий за трафиком, будет знать, к кому вы подключились (в том числе и Интернет-провайдеры). Я предположил, что у авиакомпании есть белый список доменов, используемых мессенджерами, и если они видят что-то другое, то сбрасывают подключение.

Примечание: когда я пытаюсь объяснять это, реакции людей сильно разнятся. Многие из моих далёких от техники друзей думают, что все действия без VPN видны кому угодно, а чуть более продвинутые знакомые считают, что видны URL (в том числе и параметры запросов), но не ответы. Кроме того, есть подмножество людей, веривших, что использование TLS означает шифрование всех данных между клиентом и сервером; они понятия не имели, что SNI создаёт утечку посещаемых ими доменов!

Проверяем нашу теорию

Хотя British Airways блокирует DNS-запросы ко всем публичным ресолверам (по крайней мере, к тем, которые я смог вспомнить), система всё-таки ресолвит любой переданный ей домен, том числе и записи MX, TXT, HTTPS. (Само по себе это может стать интересной сферой исследований, особенно потому, что DNS-ресолвинг может срабатывать до регистрации для доступа к бесплатному WiFi. Можно придумать что-нибудь наподобие произвольных поддоменов, представляющих полезную нагрузку запроса, и специальный сервер имён, возвращающий ответы через запись TXT или что-то подобное. Ну да ладно, двинемся дальше…).

Получив запись A моего личного сервера, я выполнил TLS handshake напрямую с IP-адресом без SNI. Он был сброшен British Airways; то есть отсутствие SNI тоже блокируется!

$ openssl s_client -connect 95.217.167.10:443
Connecting to 95.217.167.10
CONNECTED(00000003)
write:errno=104
---
no peer certificate available
---
No client certificate CA names sent
---
SSL handshake has read 0 bytes and written 302 bytes
Verification: OK
---
New, (NONE), Cipher is (NONE)
Protocol: TLSv1.3
This TLS version forbids renegotiation.
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---

Дальше я попробовал протестировать некоторые SNI, которые могли бы удовлетворить систему. Я помнил, что Whatsapp для чего-то использует wa.me, поэтому решил проверить его. SNI работает так: сообщает серверу, к какому хосту мы хотим подключиться, чтобы он мог предоставить нужный сертификат TLS. В моём случае мой личный сервер не имел никаких сертификатов для wa.me, но NGINX, похоже, просто игнорирует SNI, если он не существует, и возвращает первый сертификат (мне так кажется; это может быть связано и с моей конфигурацией, но особо этот вопрос я не исследовал).

Однако, по сути, если я (клиент) не возражаю, можно выполнить TLS-соединение для любого произвольного сертификата, предлагаемого мне сервером, даже если в SNI я указал домен, который не контролирую (например, wa.me).

$ openssl s_client -connect 95.217.167.10:443 -servername wa.me
Connecting to 95.217.167.10
CONNECTED(00000003)
depth=2 C=US, O=Internet Security Research Group, CN=ISRG Root X1
verify return:1
depth=1 C=US, O=Let's Encrypt, CN=R3
verify return:1
depth=0 CN=mijia.mywaifu.best
verify error:num=10:certificate has expired
notAfter=Jul 22 13:03:02 2023 GMT
verify return:1
depth=0 CN=mijia.mywaifu.best
notAfter=Jul 22 13:03:02 2023 GMT
verify return:1
---
Certificate chain
<вырезано>

Ура! Использование SNI Whatsapp позволило обманом убедить British Airways, что я пользуюсь мессенджером, поэтому был создан TLS-туннель. Так как я подключён к серверу, чтобы убедиться, что это сработало, я написал в сокете запрос HTTP/1.1, воспользовавшись заголовком хоста реального веб-сайта моего инстанса NGINX

GET / HTTP/1.1
Host: saxrag.com

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Fri, 09 May 2025 19:14:46 GMT
Content-Type: text/html
Content-Length: 4968
Last-Modified: Wed, 09 Apr 2025 07:52:54 GMT
Connection: keep-alive
ETag: "67f62756-1368"
Cache-Control: no-cache
Accept-Ranges: bytes
<вырезано>

Мне успешно удалось запросить и получить в ответе мою домашнюю страницу! Все её 5 КиБ. Неплохо! Теперь нужно расширить этот способ, чтобы можно было заходить на любой веб-сайт…

От ненависти до любви

Нет, мои отношения с SNI нельзя назвать столь клишированными; я считаю, что мы всё ещё враги, зато он позволяет нам воспользоваться восхитительными возможностями. Если мне удастся убедить систему, что я подключаюсь к wa.me, то потенциально я смогу делать в этом соединении всё, что угодно (под маской «работы с мессенджером»). Итак, мои требования таковы:

  • Установить TLS-соединение при помощи SNI wa.me

  • Туннелировать произвольный трафик через это соединение

  • Делать всё это, не владея доменом wa.me и не контролируя его

Из моего предыдущего опыта реверс-инжиниринга и тому подобного я знал, что наиболее очевидным способом будет HTTPS-прокси. Это должен быть именно HTTPS, потому что именно соединение с прокси я и буду выдавать за Whatsapp. Если TLS handshake с HTTPS-прокси будет иметь SNI wa.me, то система должна пропустить его, после чего мы сможем выполнять реальные запросы через этот прокси.

К сожалению, я находился в воздухе, поэтому у меня не было удобного доступа к Интернету для управления моими серверами, а значит, подобное я настроить не мог; придётся проделать это на праздниках и протестировать на обратном рейсе. Я мог попробовать эмулировать ограничения British Airlines и так далее, находясь на земле, но я решил рискнуть.

Подготовка

Мне удалось найти один из моих VPS, который ещё не использовал порт 443. Допустим, публичный IP был 333.333.333.333 (да, я знаю, что октеты не могут быть больше 0xFF, так я решил спрятать свой IP). Затем я настроил на нём HTTP-прокси при помощи tinyproxy. Однако так мы создаём простой HTTP-прокси, слушающий 127.0.0.1:8080.

Для добавления слоя TLS я использовал stunnel. Для настройки TLS в stunnel я просто сгенерировал через openSSL несколько подписанных мной сертификатов, использовав стандартные параметры, за исключением common name (CN), в качестве которого я указал wa.me, потому что стремился обеспечить максимальную совместимость (например, чтобы клиент не отклонял соединение из-за неожиданного SNI относительно CN, и чтобы сервер знал, какой сертификат предоставлять).

openssl req -nodes -newkey ed25519 -keyout ssl.key -x509 -days 365 -out ssl.crt

Дополнение: на самом деле, в клиенте я решил игнорировать ошибки TLS (подписанный мной сертификат), а stunnel не волновал SNI, поэтому этот (CN) был не особо важен. Но в более легальных сценариях использования он определённо необходим!

Тестируем

Чтобы убедиться, что прокси работает, как задумано, я попробовал использовать его через curl непосредственно с IP:

$ curl -x https://user:pass@333.333.333.333:443 ifconfig.co -v
*   Trying 333.333.333.333:443...
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: none
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS alert, unknown CA (560):
* SSL certificate problem: self-signed certificate
* closing connection #0
curl: (60) SSL certificate problem: self-signed certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.

Разумеется! Я просто случайным образом сгенерировал на моём VPS сертификаты, неподписанные «надёжным» CA. Что ж, мы можем попросить cURL игнорировать ошибки TLS для прокси при помощи флага --proxy-insecure, после чего всё заработает; ответом становится IP моего VPS.

Однако есть проблема — если я подключаюсь к прокси напрямую через IP, расширение SNI не задаётся, поэтому подключение будет заблокировано. Расширение SNI задаётся, когда подключение выполняется к домену, поэтому мне нужно сконфигурировать wa.me так, чтобы он указывал на 333.333.333.333. Разумеется, это можно сделать при помощи файла hosts, но в cURL есть и удобный хак в командной строке при помощи --resolve:

curl --resolve wa.me:443:333.333.333.333 -x https://username:password@wa.me ifconfig.co --proxy-insecure -v

Эта команда сообщает cURL, как ресолвить IP. При этом в Wireshark я могу видеть, что в качестве SNI установлен wa.me, и что соединение с прокси устанавливается успешно (разумеется, ошибки TLS, связанные с подписанным мной сертификатом, игнорируются). Неплохо, а теперь нужно ждать обратного полёта в Гонконг…

Тестируем на рейсе

Если я что-то напутал, то меня ждёт облом, ведь без доступа к Интернету я ничего не смогу исправить! Мой обратный рейс был в 19:35 местного времени, но я из-за раннего полёта из Эдинбурга я был на ногах уже в 04:00, после чего убивал время, исследуя рынки, дегустируя пиво и наблюдая за Гран-при Эмилии-Романьи. В заведении, которое я посетил, экраны были даже над писсуарами!

Несмотря на то, что я не спал уже примерно 16 часов, мне не терпелось проверить, сработало ли то, что я настроил. После взлёта я подключился к WiFi (на ноутбуке), зарегистрировался в программе лояльности British Airlines и активировал режим «Мессенджеры». Попробовав приведённую выше команду curl, я получил HTTP 200 от ifconfig.co с IP моего VPS; сработало! Вдобавок я попробовал при помощи cURL получить доступ к другим веб-сайтам, например, к example.com, google.com и так далее, чтобы убедиться, что всё в порядке.

Дальше мне предстояло расширить эту систему для доступа к вебу. К счастью, большинство современных браузеров поддерживает отправку трафика через HTTPS-прокси, а у Chromium даже есть флаг для отключения предупреждений о сертификатах TLS (благодаря чему он не будет жаловаться на мой подписанный сертификат, который, очевидно, не относится к настоящему wa.me).

Также мне пришлось изменить в файле hosts DNS-запись wa.me на 333.333.333.333, чтобы Chromium в TLS handshake установил в качестве SNI wa.me, но подключение выполнялось к моему VPS. Так как канал, скорее всего, будет довольно ограниченным (не только из-за того, что это Интернет в самолёте, но и из-за проксирования его через VPS в Нидерландах), я решил загрузить очень простой веб-сайт, состоящий целиком из текста: Hacker News.

Разблокируем бесплатный WiFi на рейсах British Airways - 2

Ура! Похоже, мы победили. Мне удалось зайти на HN через бесплатный WiFi British Airlines, предназначенный для мессенджеров! (Примечание: HTTP-запросы отображаются в Wireshark в незашифрованном тексте, потому что я использовал SSLKEYLOGFILE и настроил Wireshark так, чтобы он дешифровал TLS).

К сожалению, попытки загрузки более тяжёлых веб-сайтов заканчивались провалом: изображения в простых текстовых блогах загружались построчно. Что ж, по крайней мере, я испытал ностальгию по коммутируемому соединению!

Разблокируем бесплатный WiFi на рейсах British Airways - 3

Я предполагаю, что на бесплатном WiFi, наряду с проверками SNI, также обрезается ширина канала. Возможно, разработчики системы предвидели подобный способ обхода. С другой стороны, если это и есть та скорость Интернета, которая разблокируется на платном тарифе, то пользователей ждёт неприятный сюрприз...

Бонус: ECH

Выше я говорил о работе, проделанной для устранения утечек SNI: ECH. Это слишком объёмная тема для объяснения в моём посте, однако я рекомендую прочитать про него. Это довольно интересно! К тому же, вам станет понятнее данный раздел статьи.

У меня есть веб-сайт для тестирования ECH, поэтому перед рейсом я решил подготовиться получше. По сути, я создал ещё одну ECHConfig, для которой в качестве public_name указал wa.me.

В мире ECH публичный SNI нужен исключительно для того, чтобы сервер выполнил внешний ClientHello, а поскольку ECH-клиенты устанавливают публичный SNI на основании ECHConfig, я могу ввести в Firefox свой реальный домен, который всё равно будет использовать домен wa.me в качестве публичного SNI. После этого безопасно выполнится внутренний Client Hello, содержащий реальный SNI (rfc5746.mywaifu.best), а handshake завершится с «подлинным» сертификатом, подписанным CA для этого домена.

Разблокируем бесплатный WiFi на рейсах British Airways - 4

Это тоже сработало, даже без флагов игнорирования TLS, потому что реальный сертификат rfc5746.mywaifu.best был подписан «надёжным CA» (Let’s Encrypt). Что ещё интереснее, это сработало даже на нестандартном порту TLS: 7443! Не знаю точно, почему, но жаловаться не буду.

Примечание о ресолвинге ECHConfig

Обычно ECHConfig должны ресолвиться через зашифрованный DNS, например, DNS-over-HTTPS. Я думаю, что по умолчанию Firefox поступает именно так, но не уверен полностью, что именно так произошло на моём рейсе. Наверно, DoH должен был блокироваться в WiFi мессенджеров? Или, может быть, разработчики разрешили и SNI DoH, потому что новые телефоны по умолчанию используют его. Если кто-то из читателей вскоре собирается лететь на British Airlines, то проверьте и напишите мне!

SNI: не стоит слепо ему доверять

SNI (Server Name Indication), как и следует из названия — это своего рода «подсказка» от клиента серверу. Если кто-то контролирует обе стороны (клиент и сервер), то может указать любое поддельное значение, которое могут попробовать перехватить и анализировать промежуточные устройства (middlebox). Хотя эта система, к сожалению, работает в таких сферах применения, как цензура (когда провайдер или страна пытается блокировать конкретный веб-сайт), в сценариях наподобие распознавания угроз на неё полагаться нельзя; разработчики malware могут спуфить SNI при подключении к своим серверам C&C, потому что на самом деле он им не требуется, но такое будет выглядеть более невинно для промежуточных устройств.

Автор: PatientZero

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js