September 13, 2020

Внутри UAM Cloudflare

Привет ❤️

Около месяца назад (авг 2020) я изучала Cloudflare UAM (далее просто UAM), но никак не доходили руки написать пост про это.

UAM это та хрень, которая отображается на некоторое время перед тем, как можно зайти на некоторые сайты, и которая что-то проверяет. Я хотела сделать обход для нее, не используя библиотеки хедлес браузера вроде selenium/puppeteer

спойлер: у меня не получилось~
вот этот покемон

Снаружи

Про общую структуру UAM хорошо написано здесь (EN), вкратце:

  • При открытии страницы UAM грузится JS скрипт с /cdn-cgi/challenge-platform/orchestrate/captcha/v1
  • Этот скрипт что-то генерирует и делается POST запрос на /cdn-cgi/challenge-platform/generate/ov1/..., результат - еще один JS скрипт
  • Этот скрипт также выполняется и что-то делает, после чего делается запрос на тот же URL (но с другим телом и с куком cf_chl_rc_ni)
  • В конце концов, кидается POST запрос на изначальный URL с ?__cf_chl_jschl_tk__ либо страница обновится (и запрашивается новый челлендж)
Звучит несложно?... Надо всего лишь парсить жс код и кидать то что надо?...

Внутри

Страницу, содержащую UAM, задетектить несложно: код ответа всегда 503, в хедерах всегда будет Server: cloudflare, а ответ - html, содержащий элемент form.challenge-form.

Если открыть исходники страницы с UAM, в глаза сразу бросается это:

Эти параметры меняются при каждой перезагрузке страницы - очевидно, это какие-то параметры челленджа, сгенерированные сервером. Спарсить их нетрудно, нужно всего лишь найти скрипт, содержащий _cf_chl_opt и спарсить объект, посчитав открывающие/закрывающие скобки/кавычки/етц.

Почти сразу после этого грузится тот самый скрипт с челленджем:

И.... (кто бы мог подумать) там обфусцированный хлам:

Обфусцированный скрипт, часть 1

Сразу видно, что тут используется модифицированный obfuscator.io, однако массив строк записан как '...'.split(',') вместо ["a",...]. Выдает себя этот обфускатор тремя вещами: декодированием строк через b('0xXX') (фиол.), сдвигом массива строк (зел.) и использованием рандомных 5-символьных строк (красн.).

Если это добро деобфусцировать и сделать несколько ренеймов, то становится примерно понятно что там вообще происходит:

А именно:

  • Создается некоторый контекст для челленджа, в который добавляются параметры из полученного ранее объекта (challengeOptions=window._cf_chl_opt) и некоторый "лог"
  • В "лог" сразу добавляется время начала выполнения челленджа
  • Устанавливается кука cf_chl_prog=e
  • Выполняется запрос на /generate/ov/... с параметрами челленджа и какой-то стремной строкой

И вот тут появляется первая проблема: строка, которая там прибавляется - захардкожена внутри обфусцированного кода. Но ее легко найти регуляркой, если сделать допущение о том как это строка генерируется (мне кажется, так: ${Math.random()}:${unixTimeSeconds()}:${sha256(previousString + SECRET_SALT)}).

Однако кто такой этот ваш window.sendRequest()? Это функция, которая определяется все в том же обфусцированном скрипте, которая делает запрос на переданный ему урл и eval()-ит результат:

Но сразу в глаза бросается compressToEncodedURIComponent. Если загуглить, то вылезет https://github.com/pieroxy/lz-string/. Чтобы удостовериться, можно сравнить имплементацию:

в скрипте с UAM
в исходниках lz-string

Выглядит похоже!? Но что это за строка вместо keyStrUriSafe? Она не совпадает с строкой в исходниках lz-string:

Опа! Эта строка - не константа, и меняется между челенджами. То есть, чтобы правильно закодировать тело, надо как-то достать эту строку из обфусированных исходников! И вот тут начинается веселье.

Парсить JS регулярками весело

Во первых, таких сигнатур - '...'['charAt'](...) - в коде, мягко говоря, не одна. Во-вторых, эта сигнатура не всегда верна! Помнишь, этот скрипт покрывается obfuscator.io? Его Control Flow Flattening может рандомно попасть в эту функцию и вытащить эту строку в JS объект, например так:

неприятная ситуация

А может сработать Dead Code Injection, и эта функция будет выглядеть так:

тут вообще пиздец, аж 2 разных строки!

Для решения этой проблемы я взяла очень мощный инструмент под названием регулярки (fuck AST, all my homies use Regex). Объяснять сами регулярки долго и лень (ну и они очень всратые), поэтому накидаю суть, по которой я уже писала сами регулярки:

?N, где N число - номер матч-группы в регексе
?N<T>, где N число, а T тип - номер матч-группы в регексе с данным типом
^N, где N число - референс к N-й матч-группе из предыдущего регекса
^^N, где N число - референс к N-й матч-группе из пред-предыдущего регекса
  1. Найти сигнатуру объявления compressToEncodedURIComponent: ?1["compressToEncodedURIComponent"]=?2['?3']
  2. Найти сигнатуру заголовка функции '^3': function(?1) и спарсить тело этой функции
  3. Найти интересующую нас функцию (там где 2й аргумент - 6), предположив что Control Flow Flattening нету: ^^2['?1'](^1,6,function(?2){return ?3<js string>["charAt"](\2})
  4. Если нашлось - отлично, эта строка (группа 3) нас и интересует. Если нет - едем дальше и ищем почти такую же сигнатуру, но с учетом CFF: ^^2['?1'](^1,6,function(?2){return ?3['?4']["charAt"](\2)})
  5. Если не нашлось - ггвп. Если нашлось, то ищем такой ключ по всему файлу (по моему опыту, коллизий не бывает): ?1["^3"]=?2, где ?2 - либо JS строка, либо референс на другой ключ ?['?3'] (во таком случае надо повторить этап 5)

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

Все самое страшное позади? ведь так???

Обфусцированный скрипт, часть 2

Сервер возвращает (внезапно!) рандомный хлам:

мама я не хочу умирать

Который, однако, несложно расшифровать, если посмотреть на то, как это делается в коде UAM (по сути - мемы с UTF-16 строками в жсе). После дешифрования рандомный хлам становится... рандомным хламом:

совсем не хочу умирать

Здесь уже используется какой-то свой обфускатор (либо какой-то коммерческий, не встречала раньше таких), который при обфускации строк использует ключ, сгенерированный на базе Ray-ID челленджа. Если деобфусцировать это, вылезает очень интересный код, который слишком большой чтобы скринить, поэтому вот pastebin.

Скрипт содержит последовательность нескольких мини-тестов различных браузерных API, часть из которых в принципе не выполняется (доп. обфускация?), часть покрыта obfuscator.io сверху, а результаты каждого теста пишутся в созданный ранее "лог" в контексте челленджа.

Вот неполный список фич бразуера, которые проверяются:

  • Типизация у интерпретатора (мемы с +![] етц.)
  • eval()
  • Доступность windows.SHA256, которая была определена в изначальном обфусцированном скрипте (кстати что странно: имплементация SHA256 - реальная. Я бы слегка изменила алгоритм чтобы ее результаты отличались от реальной sha256)
  • navigator.* API
  • DOM API (создание, взаимодействие, удаление элементов + проверка элементов в хтмл который отдал сервер изначально)
  • Проверка наличия global и process (глобальные объекты в NodeJS)
  • Canvas API
  • WebSocket API
  • Image API
  • Cookie API
  • Errors & Stack traces (легко палит вмки)
  • и многое другое, наверное

После завершения работы этого скрипта, весь контекст (вместе с "логом") отправляется на сервер, анализируется и сервер решает что со мной делать (пустить на сайт, выдать капчу или отправить решать другой челлендж).

Капчу я даже трогать не стала, отдав ее индусам с 2captcha, так что там все достаточно просто.

Что такое жизнь

Принцип работы UAM мне очень напомнил софтварный SafetyNet, используемый в ведре, но гораздо более бюджетный (CloudFlare даже WASM не используют, всего лишь обфускаторы).

Я попыталась использовать JSDOM чтобы имитировать все эти API, но он не смогла, поэтому я забила и написала скрипт на 100 строк через puppeteer.

скрипт за полчаса на 100 строк на папетке работает, скрипт в 700 строк на который я убила пару дней с кучей 228iq мемов - нет.
вывод думайте сами~

Ну и напоследок, несколько мемов:

послание тем, кто зашел так далеко, что все хорошо и не надо вешаться
отсылка на мем из начала 2000х
нет спасибо, я лучше в вебкам~

А исходники?

Исходников не будет. Ни исходников версии без папетки, ни версии с ней. Во-первых, код говно, во-вторых, просто не хочу~.

Я и так многое описала здесь, так что удачи ❤️