В оборванных DNS-записях, позволяющих захватить субдомен, нет ничего нового. Инструменты аудита для пен-тестеров, такие как can-i-take-over-xyz, существуют уже много лет, в то время как ресурсы, такие как MDN, отлично справляются с задачей информирования разработчиков и операционных групп об этой проблеме.

К сожалению, сотни отчетов об ошибках указывают на то, что этот класс уязвимостей далек от устранения.

Что случилось?

Недавно мы обнаружили захват поддоменом оборванной записи DNS на johndoe.example.com:

  1. Сайт GitHub Pages (например, johndoe.github.io) был настроен на использование собственного доменного имени.
  2. Запись DNS johndoe.example.com была создана как псевдоним или CNAME для johndoe.github.io.
  3. Конфигурация пользовательского доменного имени была позже удалена с сайта GitHub Pages, когда ее использование было прекращено. Однако запись DNS для johndoe.example.com не была удалена. Это оставило висящий DNS CNAME для johndoe.github.io.
  4. Злоумышленник создал свой собственный сайт GitHub Pages и настроил его для использования личного доменного имени johndoe.example.com. Все ссылки и трафик на johndoe.example.com теперь передаются контенту, контролируемому этим злоумышленником.

Как только нам стало известно о проблеме, мы посоветовали удалить запись DNS CNAME. Это остановило кровотечение, но не сказало нам, что задумал злоумышленник.

Почему они удосужились захватить субдомен? Возможно, они читали файлы cookie, установленные в домене верхнего уровня, выполняли межсайтовые сценарии и обходили политики безопасности контента (CSP), использовали поддомен для обхода белых списков переадресации или…?

Они нацеливались конкретно на example.com?

Учитывая, что это был захват поддомена веб-сайта GitHub Pages, полное содержимое сайта было легко найти. Используя функцию поиска GitHub, мы нашли репозиторий «tiodiatavo», принадлежащий пользователю GitHub @stepard.

В репозитории было около 10 900 HTML-файлов с содержанием на арабском языке по целому ряду тем, включая рецепты, компьютерное оборудование, визы, партнерский маркетинг и Lorem Ipsum.

В каждом файле HTML был тег script для необычного файла JavaScript, размещенного на домене .ru. Большинство JS-файлов минимизированы с помощью UglifyJS, Babel Minify и т. д., поэтому JS нередко трудно читать, но этот JS-файл был намеренно запутан. Среди примерно 10 900 HTML-файлов в репозитории мы определили 10 уникальных доменов, используемых для обслуживания этого файла:

  • https://bo.datingsvr.ru/trd
  • https://ct.dominikpers.ru/trd
  • https://de.datingvr.ru/trd
  • https://dr.dietaforlove.ru/trd
  • https://ew.dionwars.ru/trd
  • https://js.ekb-tv.ru/trd
  • https://nnm.eburi.ru/trd
  • https://rt.coronafly.ru/trd
  • https://td.dzeroki.ru/trd
  • https://to.darkandlight.ru/trd

Кроме того, у пользователя GitHub @stepard было 157 репозиториев. Мы извлекли их все и обнаружили, что 87 репозиториев имеют схожий HTML-контент. Для этих 87 репозиториев мы определили домен, к которому они принадлежат, и обнаружили, что 77 все еще находятся в активном поглощении субдоменов.

Мы сообщили об учетной записи в GitHub, и они быстро закрыли ее.

Что же делает запутанный JavaScript?

Количество обнаруженных поглощений субдоменов делает маловероятным, что злоумышленник специально нацеливался на example.com, но мы хотели бы знать наверняка. Нам нужно выяснить, что делает этот запутанный JavaScript.

Существует множество доступных инструментов, которые украшают сценарии. Функция формат в Chrome DevTools — отличное начало для облегчения чтения скриптов с использованием пробелов и новых строк, но она не пытается упростить код. Другие инструменты, такие как javascript-unobfuscator и JS NICE, пытаются пойти еще дальше, выполняя деобфускацию, статистическое переименование и вывод типов.

К сожалению, ни один из этих инструментов не справился с этим JS-файлом особенно хорошо. Хуже того, когда мы попытались заменить запутанный JS-скрипт на локально размещенную «улучшенную» версию, чтобы с ней было проще работать, мы получили прекрасную О, Snap! уведомление от Chrome. Улучшение скрипта останавливает его работу и постоянно приводит к сбою нашего браузера.

Чтобы понять почему, нам пришлось вернуться к ручной деобфускации. Мы открыли Chrome DevTools и приостановили выполнение JavaScript после загрузки страницы еще до того, как произошел сбой.

Прежде чем мы продолжим, вы можете получить код на моем GitHub, если хотите продолжить. Исходный обфусцированный код JS, промежуточный улучшенный вывод JS для каждого шага деобфускации и мой скрипт Python для деобфускации — все это доступно.

Пройдясь по коду, мы заметили, что он застревает в бесконечном цикле как часть функции «ySQKnz»:

  • Скрипт будет зацикливаться, пока значение _0x5ac416 меньше, чем _0x30c102.
  • На каждой итерации значение _0x5ac416 увеличивается на единицу.
  • На каждой итерации к массиву также добавляется случайное значение, а значение _0x30c102 сбрасывается до длины этого массива.
  • По сути, и _0x5ac416, и _0x30c102 увеличиваются на единицу при каждой итерации. В результате условие завершения цикла никогда не достигается.
  • Этот бесконечный цикл продолжает увеличивать размер массива _0x30c102 и, таким образом, увеличивает потребление памяти Chrome до тех пор, пока размер массива не станет чрезмерным и Chrome не решит закрыть вкладку браузера... Ой, да что там. Щелчок!

Если мы вернемся вверх по стеку вызовов, мы сможем найти логику ветвления внутри функции «jipTwl», которая условно вызывает функцию «ySQKnz» (то есть наш бесконечный цикл). Мы наивно пытались сократить это и вообще избежать вызова «ySQKnz», но код просто бесконечно зацикливается в другом месте… по крайней мере, это больше не приводит к сбою браузера?!

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

Сложный способ деобфускации JavaScript

Давайте пересмотрим наш подход... Можем ли мы достаточно упростить код, чтобы его было легче понять, и в то же время не запускать механизм защиты от несанкционированного доступа?

Деобфускация №1: упрощение

Для начала мы можем идентифицировать некоторые значения Truthy, оцениваемые в логических контекстах. Замена их их результирующими логическими значениями должна сделать сценарий немного проще для понимания:

  • !![]становится true
  • ![]становится false

Мы также можем видеть некоторую конкатенацию строк, которая не нужна и может быть легко удалена:

  • «a» + «b» становится «ab»
  • «a» + «b» становится «ab»

Язык JavaScript позволяет использовать средства доступа к свойствам с использованием скобочной или точечной записи. В примере кода используется запись в квадратных скобках, но мы находим точечную запись более удобной для глаз:

  • obj[‘property’] становится obj.property

Наконец, любые целочисленные литералы записываются в шестнадцатеричной системе счисления (с основанием 16), что для большинства из нас не является естественным. Вместо этого давайте преобразуем их в десятичные числа (с основанием 10).

  • 0x10 становится 16

Деобфускация № 2: преобразование выражений в числа

Код имеет большое количество алгебраических выражений без переменных. Учитывая, что в этих выражениях нет переменных, их значение никогда не может измениться, поэтому мы можем заменить их их постоянным значением:

  • -5354+-3878*-2+2402*-1 должно стать 0

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

  • _0x2782e9- -0x362 должен не стать _0x2782e+875

Вместо того, чтобы усложнять наши регулярные выражения, мы запускаем этап предварительной обработки, чтобы добавить постфикс подчеркивания ко всем идентификаторам функций и переменных. Мы также удаляем из них префикс 0x, чтобы избежать неправильного толкования их как шестнадцатеричных чисел:

  • _0x2782e9 становится _2782e9_

В результате этой предварительной обработки мы теперь можем более легко вычислять алгебраические выражения и преобразовывать шестнадцатеричные целочисленные литералы в десятичные:

  • _2782e9_- -0x362 теперь правильно заменяется на _2782e9_+866

Деобфускация №3: кодирование массива строк

Вредоносные файлы JS часто состоят из 3 частей:

  • Обычно они начинаются с зашифрованного и/или сдвинутого массива строк.
  • Затем следует функция, которая инициализирует скрипт, расшифровывая или возвращая этот массив в исходное состояние.
  • Далее следуют фактические сценарии полезной нагрузки.

Похоже, что этот или подобный паттерн присутствует и здесь. Мы видим массив _0x2084_ со строковыми значениями:

Есть только 3 ссылки на массив _0x2084_. Одна ссылка является параметром функции ниже, которая переупорядочивает элементы в массиве (см. операторы push и shift). Эта функция не возвращает и не присваивает новое значение, поэтому мы знаем, что она изменяет массив на месте.

Две другие ссылки берут значения из массива для некоторой дополнительной обработки, предположительно типа декодирования или расшифровки. Обе эти ссылки расположены внутри функции с идентификатором _1c40_.

Существует 15 вызовов функции _1c40_, но все они кажутся относительно простыми функциями-оболочками или вспомогательными функциями.

Есть несколько интересных вещей, которые следует отметить в отношении этих оберток:

  • Они часто идут одинаковыми парами, что просто раздувает код и затрудняет его понимание.
  • Эти оболочки выполняют простое сложение или вычитание одного из аргументов.
  • Функция-оболочка принимает 4 аргумента, но фактически используются только 2.
    Обратите внимание, что используемый аргумент несовместим; это не всегда первый аргумент, как показано на снимке экрана выше.

Мы можем полностью удалить эти функции оболочки, если заменим любые вызовы оболочки встроенным вызовом функции _1c40_. В рамках этого процесса:

  • Мы «поднимаем» операцию сложения или вычитания из функции-оболочки и корректируем соответствующий аргумент в вызове замененной функции _1c40_.
  • Мы опускаем два неиспользуемых аргумента.
  • Мы переименовали _2084_ в _strings_ для ясности.
  • Мы также переименовываем _1c40_ в _decode_

Деобфускация № 4: кодирование строкового массива на стероидах

Удаление 15 функций-оболочек выявило несколько новых функций с той же сигнатурой. Функции обертки с двойной оберткой! Мы также встраиваем их. И затем мы находим больше функций-оболочек с тройной и даже четырехкратной оболочкой.

Деобфускация № 5. Упрощение декодирования

В результате наших усилий по деобфускации теперь у нас есть прямые вызовы функции _decode_ по всему коду.

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

Первое, что мы обнаруживаем, это то, что из первого аргумента функции _decode_ немедленно вычитается 327, а затем он используется в качестве индекса для массива _strings_. Мы проделаем аналогичную операцию «подъема», как и раньше. Мы удаляем вычитание внутри функции _decode_ и вместо этого корректируем значение первого аргумента каждого вызова на _decode_.

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

Деобфускация № 6. Удаление декодирования

Все вызовы _decode_ теперь легко понять, поскольку передается одно небольшое числовое значение, которое используется для поиска элемента в массиве _strings_. Не лучше ли вообще избавиться от этих вызовов и заменить их фактическим строковым значением, которое представляет вызов?

В деобфускации №3 мы говорили о функции сдвига. Ранее мы определили, что это изменение массива _strings_ на месте с помощью операций push и shift. После всей нашей работы эта функция сдвига теперь выглядит следующим образом:

Мы могли бы попытаться выяснить, что именно это делает, но нам это не нужно. Вместо этого мы заставим движок JavaScript Chrome сделать всю тяжелую работу и восстановить массив _strings_ за нас.

Мы делаем это, вводя оператор debugger в код сразу после функции сдвига. Когда мы затем загрузим страницу с открытым Chrome DevTools, выполнение JS на этом этапе остановится. Затем в консоли Chrome DevTools мы оцениваем следующее выражение, чтобы создать новый временный массив со всеми _strings_значениямиужедекодированными.

strings_rotated = _strings_.map (функция (_str, _idx) { return _decode_ (_idx); })

Массив _strings_ и функцию сдвига теперь можно полностью удалить. Вместо этого мы можем заменить каждый вызов _decode_ правильным строковым литералом.

  • _4c86b3_ += _decode_(88) становится _4c86b3_ += ’; безопасный

Устранение запутанности № 7: снова упростить

После встраивания всех строковых литералов снова возвращаются те же шаблоны из деобфускации №1. Мы можем видеть, что доступ к свойствам осуществляется с использованием записи в квадратных скобках, ненужной конкатенации строк и т. д. Мы уже знаем, как это исправить…

Деобфускация №7: возвращение консоли

Теперь, когда мы можем более легко читать код, становится легко идентифицировать этот раздел кода, который относится к различным методам, доступным в классе JS console и которые используются для ведения журнала или отладки.

JavaScript, будучи динамическим языком, позволяет изменять все свойства и методы всех объектов, в том числе встроенных в браузер. Вредоносные JS-скрипты часто используют это для замены методов в классе console, чтобы предотвратить инструментирование вредоносного кода операторами console.log.

Мы просмотрели код, связанный с ключевыми словами log, warn, info и т. д., приведенный выше, и обнаружили, что вызов _1b8bb4_, по-видимому, делает именно это, но с изрядной степенью запутанности. в.

Код для _1b8bb4_ запутан с помощью метода, называемого выравниванием потока управления. Короче говоря, каждое действие, которое необходимо выполнить, было перемещено в другой пункт case оператора switch в случайном порядке. Затем поддерживается переменная _GGAKL, которая определяет список предложений и порядок их выполнения. Затем код перебирает это и выполняет каждое предложение по мере необходимости.

На самом деле все это не имеет значения, так как мы можем просто удалить весь этот фрагмент кода, чтобы вернуть браузеру поведение console по умолчанию.

Деобфускация № 8. Отключение защиты от несанкционированного доступа (наконец-то!)

Снова взглянув на код, мы замечаем почти идентичный паттерн сглаживания потока управления, который обсуждался ранее:

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

С некоторой помощью Chrome DevTools мы обнаружили, что это проверка на защиту от несанкционированного доступа! Проверка вызывает toString для функции, которая возвращает фактическое определение кода JS для этой функции. Затем он запускает регулярное выражение, чтобы проверить, соответствует ли это определение кода ожидаемому.

Если файл JS был «украшен», внутри определения функции вводятся дополнительные пробелы и символы новой строки. Таким образом, вызов toString возвращает другой код JS, который больше не соответствует этому регулярному выражению.

Проверка защиты от несанкционированного доступа запускается вызовом _41a7ac_, поэтому мы просто удаляем ее и связанный с ней код.

Деобфускация № 9: переименовать

Теперь у нас осталось только одно определение класса в нашем файле. Весь остальной код был упрощен и удален. Мы видим, что это точка входа в фактическое поведение. Определен класс Task2. У него есть статический метод с именем com2 (который немного трудно читать снова из-за скобок).

Сама фактическая логика снова запутана с помощью выравнивания потока управления. Мы видим свойство IZOXR с порядком выполнения предложений. Мы можем идентифицировать другие свойства, скрывающие различные строковые литералы и функции, обертывающие основные арифметические действия и операции.

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

Деобфускация № 10: сделай сам

Последний шаг — просто ручной рефакторинг кода с использованием любой полуприличной IDE.

  • Мы удаляем все недоступные блоки кода.
  • Мы заменяем любые вызовы controlFlow.invokeFnWithXArgs фактическими вызовами функций.
  • Мы делаем то же самое для функций controlFlow.or, controlFlow.plus_a и т. д. и заменяем их эквивалентным операндом или выражением.
  • Мы извлекаем оператор switch из структуры выравнивания потока управления.
  • Выполняем базовую уборку и форматирование

Ну, это было невесело. Что делает необфусцированный JavaScript?

После всех усилий у нас теперь есть чистый исходный код JavaScript и мы можем легко проверить его функциональность… и это, мягко говоря, разочарование.

Сценарий захватывает первую часть document.title на основе двух разделителей (или весь заголовок, если в нем не найдены разделители).

Сценарий ищет document.referrer, чтобы выяснить, перешел ли посетитель на текущую страницу по клику в одной из следующих поисковых систем:

  • Google.*
  • поиск.yahoo.*
  • bing.com
  • поиск.аол.*
  • спросите.com
  • альтависта.*
  • поиск.lycos.*
  • alltheweb.*
  • яндекс.*
  • nova.rambler.* и search.rambler.*
  • гого.*
  • идти.почта.*
  • нигма.*

Если посетитель пришел из одной из этих поисковых систем, то cookie «opos» устанавливается со значением «1». Затем пользователь немедленно перенаправляется на https://js.ekb-tv.ru/trds?q={document_title_prefix}.

Если у посетителя есть существующий файл cookie «opos» со значением «1», то есть если он ранее попадал сюда из поисковой системы, тогда файл cookie обновляется, и пользователь немедленно перенаправляется аналогичным образом.

Если посетитель попадает на эту страницу естественным образом, скрипт ничего не делает.

Одна любопытная часть заключается в том, что в коде, кажется, есть регулярные выражения, чтобы иметь возможность анализировать то, что пользователь искал в упомянутых поисковых системах. Это звучит как потенциально интересная точка данных для сбора, но мы нигде не видим, чтобы это делалось. Это ошибка? Это частично реализованная или удаленная функция? Мы никогда не узнаем.

На что они перенаправляют?

Для того, чтобы узнать больше, нам нужно проверить содержимое и поведение https://js.ekb-tv.ru/trds, но это уже другая длинная история. следующая запись в блоге: Хотели бы вы получить бесплатный iPhone с этим?

Между тем, будьте осторожны, если у вас нет достаточного количества инструментов аудита для обнаружения висячих DNS-записей или фактических поглощений поддоменов (да, они разные, и у вас должны быть возможности обнаруживать и то, и другое!), вы, вероятно, в конечном итоге будете ужалены.

Пост скриптум

В ходе этого расследования мы обнаружили, что файл JS был запутан с помощью инструмента https://obfuscator.io/, созданного Тимофеем Качаловым, который поддерживает еще несколько методов запутывания и вариаций, не встречавшихся в этом анализе.

Обновление №1

После проведения этого анализа мы обнаружили инструмент реактор сдвига, предназначенный для обратного проектирования запутанного JavaScript с использованием AST, который может быть более подходящим, чем подход с регулярными выражениями, который мы использовали здесь.