Метод входа в Steam довольно интересен | Indite — Медиахостинг

Метод входа в Steam довольно интересен

IT Boroda

visibility
14 Янв 2020
Как вы отправляете пароль через интернет? Вы приобретаете SSL-сертификат и позволяете TLS выполнять работу по безопасной передаче пароля от клиента к серверу. Конечно, все не так просто, как я себе представляю, но суть остается верной и выдержала испытание временем. Однако так было не всегда, и одна невероятно популярная витрина во всемирной паутине предпочитает добавить немного больше к этому дню. Я буду обсуждать уникальный метод Steam входа в систему своих пользователей и спускаться в глубокую кроличью нору захватывающих деталей реализации.

УКАЗЫВАЯ НА ОЧЕВИДНОЕ
Я нашел вопрос StackOverflow от 2013 года, спрашивающий, как безопасно отправить пароль по протоколу HTTP. Ответы довольно единодушны: получите SSL-сертификат. Вот эксперимент: настройте свой любимый прокси-сервер для захвата трафика, перейдите к сервису, которым вы часто пользуетесь, войдите в свою учетную запись (или предпочтительно одноразовую) и проверьте запросы. Вы наверняка обнаружите, что ваше имя пользователя и пароль отправляются как есть в теле HTTP-запроса. Единственная причина, по которой это работает, заключается в том, что ваше соединение с сервером шифруется с помощью TLS.

1610130334999.png

Странно думать, что раньше это было проблемой

Однако в начале 2010-х интернет был совсем другим местом, не говоря уже о многих предыдущих годах. Теперь у нас есть такие сервисы, как Let's Encrypt, которые выдают SSL-сертификаты бесплатно в течение трех месяцев с автоматическим продлением при желании. На самом деле не было большого способа обойти приобретение SSL-сертификата за деньги, но обычно с расширенным сроком действия и поддержкой. Вы, конечно, можете утверждать, что есть цена, которую нужно заплатить за безопасность и конфиденциальность ваших пользователей, но это не остановило появление таких вопросов, как тот, который я связал.

Теперь, когда мы все согласны с тем, что TLS важен, давайте переключим его. Давайте представим, что мы не можем отправить пароль по HTTPS и должны каким-то образом заставить его работать с обычным HTTP, а также обеспечить пользователям некоторый уровень безопасности. Есть Authorizationзаголовок, который стандартизирован и широко принят. Однако в сочетании с” базовой " схемой аутентификации HTTP она не обеспечивает никакой безопасности, если используется в обычном HTTP.

Существуют проверенные алгоритмы ответа на вызов, в первую очередь SRP, который предназначен для аутентификации на основе пароля без фактической отправки пароля, но вам, вероятно, придется реализовать их самостоятельно, и небольшая оплошность может нанести серьезный вред. Вы также можете отложить проверку подлинности для внешней службы. “Войдите в систему с помощью сервиса XYZ " обычно используется, но имеет свои собственные последствия. Учитывая все обстоятельства, нет ничего тривиального в том, чтобы передавать секреты через изначально небезопасную связь.

Поэтому, когда мы с другом разобрали Steam в поисках следов личной информации, я был удивлен, увидев, что страница входа в Steam не только полагается на TLS, чтобы гарантировать, что ваш пароль остается защищенным.

КРИПТО ВИШНЯ НА ВЕРШИНЕ
Опять же, возьмите свой любимый прокси-сервер для захвата трафика и перейдите на страницу входа в Steam. Введите свое имя пользователя и пароль, и вам (надеюсь) будет предложено ввести одноразовый токен, сгенерированный вашим предпочтительным методом двухфакторной аутентификации. Вы можете остановиться прямо здесь, потому что магия, на которую я хочу указать, уже произошла. Вы обнаружите, что нажатие кнопки входа в систему запускает запрос против нечетной конечной точки:/login/getrsakey, за которым следует /login/dologin.

717

Все соответствующие активы и запросы последовательно

Проверьте запрос/login/getrsakey, и вы найдете ответ в формате JSON, содержащий поля с именами, которые должны выглядеть очень знакомыми любому, кто кратко имел дело с криптографией с открытым ключом. Вам дается открытый ключ RSA, хотя точные значения могут выглядеть немного странно. Понятно, что publickey_modи publickey_expопределить модуль и показатель степени, используемые в шифровании, но первый дан в шестнадцатеричном формате, а второй, по-видимому, дан в двоичном (я вернусь к этому позже). Существует также временная метка, которая не имеет сразу узнаваемой начальной точки. Что касается цели token_gidэтого, я пока не имею ни малейшего понятия.

{
"success":true,
"publickey_mod":"c85ba44d5a3608561cb289795ac93b34d4b9b4326f9c09d1d19a9923e2d136b8...",
"publickey_exp":"010001",
"timestamp":"1260462250000",
"token_gid":"2701e0b0a4be3635"
}
Страница входа в систему тянет некоторые скрипты при загрузке. Существует основной обработчик входа, содержащийся в login.jsкотором полностью не запутан, поэтому любой может просто проанализировать его и выяснить, что он делает. Сайт также загружает некоторые дополнительные зависимости, а именно jsbn.jsи rsa.js.

Быстрый поиск имени, упомянутого в первой строкеjsbn.js, показывает, что эти два сценария принадлежат тому Ву — выпускнику Массачусетского технологического института и Стэнфорда, любящему программную инженерию и компьютерную криптографию. Они выпустили jsbn.jsи rsa.jsкак чистые JavaScript-реализации произвольных целых чисел точности и шифрования/дешифрования RSA соответственно. Вы также обнаружите, что последние обновления этих библиотек произошли в 2005 и 2013 годах, и к этой информации я вернусь позже. А пока просто имейте это в виду.

СПУСКАЯСЬ В ДЫРУ R (S)ABBIT
Так что теперь, когда у нас есть все соответствующие активы, давайте копаться login.js. Код немного запутан с большим количеством обратных вызовов и проксированных вызовов функций, но оказывается, что части интереса могут быть легко сконденсированы. По сути, сценарий можно свести к нескольким шагам, каждый из которых предполагает, что все прошло нормально на предыдущем шаге.

  1. Пользователь вводит свое имя пользователя и пароль и нажимает кнопку входа в систему.
  2. DoLogin вызывается, который проверяет, если маска входа была заполнена правильно и запускает запрос против /login/getrsakey.
  3. OnRSAKeyResponse называется. Это проверяет, правильно ли сформирован ответ.
  4. GetAuthCode называется. Он запускает некоторый специфичный для платформы код на случай, если в учетной записи пользователя активны какие-либо меры 2FA.
  5. OnAuthCodeResponse называется. Именно здесь пароль шифруется с помощью RSA, а запрос против /login/dologinподготавливается и выполняется.
  6. OnLoginResponse называется. Пользователь входит в систему и перенаправляется на витрину магазина Steam.
Приведенный ниже код OnAuthCodeResponseпоказывает, почему запрошенный открытый ключ отформатирован именно так. Начиная со строки 387 в исходном файле, модуль и Экспонента /login/getrsakeyответа передаются как есть в библиотеку RSA. Пароль пользователя затем шифруется с помощью данного открытого ключа и добавляется к запросу против /login/dologinна следующем шаге входа в систему.

var pubKey = RSA.getPublicKey(results.publickey_mod, results.publickey_exp);
var username = this.m_strUsernameCanonical;
var password = form.elements['password'].value;
password = password.replace(/[^\x00-\x7F]/g, ''); // remove non-standard-ASCII characters
var encryptedPassword = RSA.encrypt(password, pubKey);

Я скопировал исходные файлы на свой локальный компьютер, чтобы немного изучить библиотеку RSA. И модуль, и Экспонента передаются функцииRSAPublicKey, которая ведет себя как конструктор в “доклассовую” эпоху JavaScript. RSAPublicKey просто оберните оба значения в экземплярыBigInteger, предоставленные jsbn.jsскриптом. К моему удивлению, экспонента на самом деле представлена не в двоичном виде, а, как и модуль, в шестнадцатеричном. (Кроме того, оказывается0x010001, что это очень распространенный показатель шифрования в реализациях RSA.) Таким образом, теперь ясно, что шифрование пароля основано на 2048-битном RSA с показателем шифрования 65537.

let r = RSA.getPublicKey("c85ba44d5a360856..." /* insert your own long modulus here */, "010001");
console.log(r.encryptionExponent.toString()); // => "65537"
console.log(r.modulus.bitLength()); // => 2048
Переходим к timestampполю. /login/getrsakeyОтвет содержит Expiresзаголовок. Он ссылается на дату в прошлом, что означает, что ответ абсолютно не предназначен для кэширования или сохранения каким-либо образом. Если вы вернетесь назад в /login/getrsakeyтечение более длительного периода времени, вы заметите, что открытый ключ меняется очень часто, и, как таковое, его значение метки времени тоже. Это означает, что существует только ограниченный период времени, в течение которого определенный открытый ключ RSA, выпущенный Steam, может быть использован для аутентификации.

Это становится еще более очевидным при рассмотрении последующего запроса против /login/dologin. Помимо всего прочего, он содержит имя пользователя, зашифрованный пароль, а также метку времени выданного открытого ключа RSA. Попытка выполнить попытку входа в систему при изменении метки времени завершается неудачей, как и ожидалось. Но что еще более важно, невозможно повторно использовать старый открытый ключ, даже если пароль правильно зашифрован.

Я пошел еще дальше и написал простой скрипт Python для сбора открытых ключей в течение трех дней с помощью одноразовой учетной записи. Я позволяю ему работать каждые пять минут, используя cronjob. Цель состояла в том, чтобы проверить, как часто меняются открытые ключи Steam, и, надеюсь, выяснить, как timestampведет себя поле.

718

Много-много-много открытых ключей

Я обнаружил, что открытый ключ меняется каждые 12 записей, а это означает, что можно с уверенностью предположить, что они вращаются каждый час. Показатель шифрования остается прежним — никаких сюрпризов здесь нет. Однако еще более интригующей является вышеупомянутая timestampобласть. Для каждых 12 открытых ключей значение timestampувеличивается на определенную величину, а именно на 3600000000 и далее на некоторую. И более того, это число оборачивается через некоторый промежуток времени, как видно на следующем рисунке. Будьте осторожны, потому что все, что я собираюсь сказать, очень спекулятивно.

719

Поле временной метки оборачивается вокруг

Я обнаружил, что 3600000000 микросекунд равно одному часу, что заставляет меня предположить, что значение timestampполя на самом деле задается в микросекундах. Однако я уже намекал на то, что значение временной метки не увеличивается ровно на час с каждым новым открытым ключом. В своих собственных данных я заметил, что разница между двумя последовательными временными метками составляет один час плюс от 1 до 2,6 секунды, причем большинство из них находятся примерно в порядке от 1,05 до 1,25 секунды. Но тут возникает еще одна интересная возможность.

Предположим, что новый открытый ключ генерируется каждый час плюс одна секунда. Если я буду запрашивать конечную точку открытого ключа точно каждые пять минут (полностью игнорируя сетевую задержку на данный момент), то есть шанс, что я буду наблюдать один и тот же открытый ключ не 12, а 13 раз подряд. Это должно происходить всякий раз, когда запрос совпадает с генерацией нового открытого ключа. К счастью, поскольку это относится ко второму, допустимая погрешность на самом деле не безумно мала.

720

Различные цвета указывают на различные открытые ключи (не масштабировать!)

После просмотра моего собственного набора открытых ключей я обнаружил, что не поймал этот крайний случай. Это может быть плохая примета или тот факт, что я просто извергаю гипотезы и надеюсь на своего рода откровение. Кроме того, с переменным увеличением значений временных меток становится трудно предсказать, когда именно я смогу наблюдать этот крайний случай — то есть если вообще существует такой сценарий.

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

Итак, подводя итог: если все мои предположения до сих пор были правильными, то timestampполе и его временные различия между открытыми ключами невероятно озадачивают. Разве это компенсирует високосные годы? Может быть, это связано с каким-то другим видом задержки? Может быть, это ошибка реализации, что Valve просто пошел с этим? Не выражается ли оно не в микросекундах, а в чем-то более произвольном? Это чтобы держать любопытных ботаников, таких как я, почесывая их головы? Я склоняюсь к последнему.

Я знаю, что пропустил мимо ушей странную timestampокантовку поля и даже не коснулся предполагаемого назначения token_gidполя. Я считаю, что первое связано с некоторым техническим ограничением, а второе, по — видимому, является некоторым смягчением CSRF или уникальным идентификатором-полные снимки в темноте, так как я уже получил больше от этого усилия, чем я первоначально ожидал. Если вы чувствуете мотивацию копаться в этом самостоятельно и хвастаться своими открытиями, я был бы признателен, если бы вы связались со мной либо по почте в нижней части этого сайта, либо в Твиттере.

И последнее, что стоит отметить, - это то, что запрос конечной точки открытого ключа с разными именами пользователей дает разные ответы. Независимо от того, извлекаются ли открытые ключи из пула и каждому пользователю назначается разное смещение временной метки, или же они действительно генерируются на лету, все зависит от того, что происходит в воздухе. Также можно использовать любое произвольное имя пользователя в запросе против /login/getrsakey. Он не должен быть зарегистрирован в Steam. Делайте с этой информацией все, что хотите.

ЛАДНО НО … ПОЧЕМУ?
Когда я исследовал эту тему, я стал странно очарован механизмом входа в систему Steam. Теперь я знаю, что помимо использования TLS (как и положено) для входа своих пользователей, они также используют 2048-битный RSA для шифрования паролей своих пользователей с помощью какой-то вращающейся системы открытых ключей, которая должным образом аннулирует старые ключи и действует по-разному для каждого пользователя. Все эти усилия кажутся такими избыточными, когда SSL-сертификат-это все, что вам реально нужно для безопасного входа в систему ваших пользователей.

Напрашивается вопрос-почему? Зачем создавать такую странную сложную систему поверх чего-то, что прекрасно работает само по себе? У меня есть своя теория, но имейте в виду, что это именно так.

Помните даты выпуска библиотек BigIntegerи RSA? Не только это, но и страница входа в систему также содержит источники jQuery версии 1.8.3, которая была выпущена в ноябре 2012года . Все это указывает на тот простой факт, что механизм входа в систему на самом деле не изменился уже почти десять лет. И точно так же, как я упоминал в начале этого поста: интернет тогда был совершенно другим местом.

721

Му-ух! Я нашел опечатку в jQuery 1.8.3 changelog!! Какова награда за орфографические ошибки?

"HTTPS everywhere" - это постоянное усилие в современном интернете, но это был долгий и болезненный процесс, чтобы добраться туда, где мы находимся сегодня. Моя теория заключается в том, что это была, по сути, попытка Steam обеспечить уровень безопасности для пользователей в те дни, которые случайно или случайно не попали на версию SSL/TLS своего сайта входа в систему. Таким образом, даже если бы третья сторона могла обнюхать каждый бит их данных, когда они отправлялись на серверы Steam и с них, у них, по крайней мере, не было бы способа узнать свой пароль — по крайней мере, без огромных вычислительных усилий.

Я попытался связаться с сотрудником Valve,который действительно работал на витрине магазина Steam. Я вкратце изложил им свой анализ и свою теорию. Я спросил их, могут ли они подтвердить это или знают кого-то, кто был там, когда этот метод входа был задуман. Конечно, я знаю, что у кого-то, кто работает в Valve, есть дела поважнее, чем отвечать на несрочный, не деловой запрос ботаника. На момент написания статьи я все еще жду ответа и могу только высказать свое собственное обоснованное предположение. Как бы то ни было, путешествие до этого места было очень долгим, и я имею в виду очень долгим- конечно, весело. Там еще есть немного земли, чтобы покрыть, и я определенно еще не закончил.

Edit (2021/01/07): в предыдущей версии этого поста я предполагал, что увеличение timestampполя было исправлено для каждого нового открытого ключа. Я проверил его еще раз и обнаружил, что это не так, даже в данных, которые я собрал. (Я был одурачен длинной чередой повышений на один час и одну минуту.) Это было исправлено.