Chrome Headless против cloudflare JS challenge

Автоматизация сбора информации с различных ресурсов — обычная задача для людей разных сфер деятельности. Жаль, что не всегда бывает достаточно сделать простой GET запрос и разобрать полученный html. Веб-сайты, с которых собираются данные, принимают защитные меры для предотвращения автоматизированных запросов. Одной из таких мер является использование cloudflare. Сегодня мы посмотрим, как cloudflare выявляет ботов через javascript и коснёмся темы деобфускации скриптов. Данный материал будет неким дополнением к этому посту.

За основу возьмём рандомный сайт сайт потому что кажется, что там клаудфлеер настроен решительно, ведь при заходе мне ещё нужно кликнуть по капче «cloudflare turnstile»:

9c60a6b4ba8295e0e753a2a82d41946a.png

Пробуем сделать обычный GET запрос:

const { gotScraping } = require('got-scraping');

gotScraping('https://esfaucets.com/', {
  method: 'get',
  useHeaderGenerator: false,
  headers: {
    'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
    'accept-encoding': 'gzip, deflate, br',
    'accept-language': 'ru-RU,ru;q=0.9',
    'cache-control': 'no-cache',
    'sec-ch-ua': '"Not_A Brand";v="99", "Google Chrome";v="109", "Chromium";v="109"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"Windows"',
    'sec-fetch-dest': 'document',
    'upgrade-insecure-requests': '1',
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
  },
}).then(response => {
  console.log(response.body);
})

Видим похожий HTML:

response

ca454b81734ac1289f18f96b4fc86a20.png

Чего-то не хватает…

Бежим в devtools, смотрим последний запрос после прокрутки всех скриптов и видим, что клаудфлеер в ответ нам прислал куки:

devtools

cac1bb3f6ebb0a53b7b8a92a4e98170f.png

Устанавливаем куки в заголовок, пробуем повторить запрос и видим html нашей страницы:

gotScraping('https://esfaucets.com/', {
  method: 'get',
  useHeaderGenerator: false,
  headers: {
    'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
    'accept-encoding': 'gzip, deflate, br',
    'accept-language': 'ru-RU,ru;q=0.9',
    'cache-control': 'no-cache',
    'cookie': '__cf_bm=dTN3MAm6iH.wkD88zG7_.Q.alXDWj8WdmwqRJVlm5uA-1675618593-0-AdrvgMdAtvCcdOvx6JTww5K+jGDKEMw7x4uSZ2t9RCc6COMyo/LB5en7c/2mj3o0BvaaAsB9fR6KaTIPd8t7KGN2X1J1XJ/PGa2bIDZUAUlJIeMjMR0Hc0uEexo3kldB+ycfvhD7Lg807gh+Z58ipTY=; cf_clearance=V3TVYDLDgOd9E.8CzxdYz54aDdHnaFtfexpOIcpSL5U-1675619126-0-250; laravel_session=eyJpdiI6InpHQzU1bVJ3cnF3aUxRRXV4SGhXR1E9PSIsInZhbHVlIjoidTYyK1Nwb3VJRjBmM3ZueEgxQWhrMjQrRHBiSU9LakxJY2laUEVwczBsY0FNbEhjZFRqcUY3NUx2azdtWUZJYnhlS1hzR05CY0VyWVg2K1JOYkRaTFBwWWZaZkRxZnVLbEN3U1JxSG94dWc5dXJqY3pcL05PZURhOFJ3a2FIb0hZIiwibWFjIjoiZTVmY2Q4NjdmZDVlN2M0MjcxZGYyOWU5YTExYTFlY2Q5NzdiMmJmNDY2YWU1ZDU4N2JlYzZhMWM2Y2UzNjZlNSJ9',
    'pragma': 'no-cache',
    'sec-ch-ua': '"Not_A Brand";v="99", "Google Chrome";v="109", "Chromium";v="109"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"Windows"',
    'sec-fetch-dest': 'document',
    'sec-fetch-mode': 'navigate',
    'sec-fetch-site': 'none',
    'sec-fetch-user': '?1',
    'upgrade-insecure-requests': '1',
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36',
  },
  http2: true
}).then(response => {
  console.log(response.body);
})

response

305fe9d23bf8fea4625f66f77b25dc59.png

Очевидно, нам нужно получить данные куки, чтобы использовать их в своих запросах. Но они приходят после отправки payload-ов, которые в свою очередь сгенерировались скриптами:

c9ed1f2f00e81c4331660310bf1212bd.png

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

Тем не менее, в данной статье хотелось бы обсудить первый вариант с выполнением скриптов. Есть разные подходы к тому, как это можно сделать:

  • Можно использовать подобие JSDOM в NodeJS, дабы дополнить «окружение» тем, что требуют скрипты, но в таком варианте есть минусы:

    • Среда NodeJS открывает доступ к файловой системе, и, если вы что-то упустите, то с вашим ПК может пойти что-то не так. Это не касается клаудфлеера, но может коснуться вас, если вы возьмёте какой-нибудь скрипт из браузера и решите выполнить его в другом месте.

    • JSDOM написан на JavaScript. Манипуляции с DOM и свои реализации браузерных апи будут работать медленно. Медленнее, чем в обычном браузере.

    • Cloudflare использует свою капчу, а иногда и h-капчу, а это довольно-таки серьёзные приложения, которые многого требуют. Слишком долго реализовывать такую обёртку.

  • Обычный браузер с лёгкостью выполнит все скрипты. Например, тот браузер, что находится в playwright или в подобных библиотеках

    • Headless режим браузера. Такой браузер не отрисовывает графический интерфейс, поэтому побыстрее и легче. Звучит получше, следовательно это наш вариант для получения кук.

Проверки антиботов нацелены на выявление автоматизируемых решений. В этом заключается проблема использования headless-браузера. Выходит, нам нужно как-то замаскировать наш браузер под обычный. Но вариантов выявить бота с клиентской стороны через JavaScript так много, что всевозможные варианты проверок никогда не предусмотреть, а значит нам нужно понимать, какие именно проверки делает cloudflare. Чтобы с этим разобраться, нам нужно изучить скрипты, но вот не задача, они обфусцированы…
В общем, ковыряя devtools, видим такие интересности:

devtools

53251fbdee400a81009b1381d49c0bdc.gif

При отправке запроса используется что-то из JSON. Благо в этом объекте два стандартных метода — parse и stringify. А ещё используется eval или new Function(…)(), и скрипт там выглядит интересно, но непонятно…

Для начала попробуем посмотреть, как используется JSON. Для этого используем метод addInitScript() в playwright, который выполняет пользовательский код перед загрузкой всех остальных скриптов:

const playwright = require('playwright');

const browserOptions = {
  ignoreHTTPSErrors: true,
  headless: false,
};

(async () => {
  const browser = await playwright.chromium.launch(browserOptions);
  const context = await browser.newContext({
    colorScheme: 'dark'
  });

  const page = await context.newPage();

  await context.addInitScript(`
    const jsonStringify = JSON.stringify;
    JSON.stringify = function() {
      console.log(arguments);
      return jsonStringify.apply(null, arguments);
    }
  `);

  await page.goto('https://esfaucets.com/');
})();

Видим в консоли очень любопытный объект:

fingerprint

e84500e5864b679801f66dc87e9e2921.png

Из-за сбора плагинов становится понятно, что здесь наш «браузерный отпечаток». Скорее всего он кодируется и отправляется в качестве payload.

Если мы попробуем нажать на капчу в «голом» playwright, то попадём в так называемый «cloudflare endless loop». Нас не пропускают. Видимо, клаудфлеер понял, что используется автоматизация.

Один из самых примитивных способов обнаружения автоматизации является проверка свойcтва navigator.webdriver. Давайте его переопределим и посмотрим что получится:

await context.addInitScript(`
  Object.defineProperty(Object.getPrototypeOf(navigator), 'webdriver', {
    get: () => false
  })
`);

Неожиданный и приятный результат

7a884a99164e5e7265003fdc477b4f6d.gif

Примечание

На самом деле такой способ переопределения очень легко обнаружить:

Object.getOwnPropertyDescriptor(
  Object.getPrototypeOf(navigator), 'webdriver'
).get.toString()
//'function get webdriver() { [native code] }' в реальном браузере
// '() => false' у нас

Navigator.prototype.webdriver
// Uncaught TypeError: Illegal invocation в реальном браузере
// false у нас

Object.getOwnPropertyDescriptor(
  Object.getPrototypeOf(navigator), 'webdriver'
).get.name
// 'get webdriver'
// 'get'

И так далее и тому подобное… Тем не менее, нашего переопределения достаточно для клаудфлеера, чтобы автоматизируемый браузер «сошёл за своего».

Но наша задача более интересная, ведь мы хотим, чтобы клаудфлеер пропустил headless-браузер. Обычным изменением свойства webdriver этого добиться не выйдет, поэтому давайте изучим скрипт из eval. Мы можем немного поразвлекаться с дебаггером, чтобы уловить суть проблемы:

debugger

3872f979e9634e561329e2df51265789.gif

За всякими вызовами вида x[y(z)] стоят строки, и это основная проблема для понимания происходящего. Давайте исправим данный недуг с помощью статического анализа скрипта.

Деобфускация

В этом слове нет ничего криминального. JS-скрипты — это полноценные программы, которые выполняются в нашем браузере на нашем компьютере и без нашего ведома. Мы имеем право знать, что делают такие программы.

Для того, чтобы взаимодействовать с текстом скрипта, нам желательно его преобразовать в более удобное представление. Таковым представлением является абстрактное синтаксическое дерево (AST). Мы используем парсер babel, чтобы получить AST, затем побродим по узлам, что-то где-то заменим, а следом сгенерируем новый код из полученного дерева. (Если вы совсем незнакомы с AST трансформациями, то можете почитать это, это и это).

Давайте начнём с чего-то малого. Вставляем наш скрипт в astexplorer и видим подобные выражения:

f110034154c59ab0247eef0d93707673.png

Результатом вычисления таких выражений является обычное число. Почему бы нам сразу не вычислить эти узлы?

Что для этого нужно сделать?

  1. Найти узел

  2. Вычислить выражение

  3. Заменить узел

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

Поиск узла

b83177628527f1dfdeb17d2212791cb3.gif

Что мы здесь видим: тип нужного узла — BinaryExpression, дети которого являются NumericLiteral. Пишем:

const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
const fs = require('fs');

const srcCode = fs.readFileSync('srcCode.js', { encoding: 'utf-8' });
const ast = parse(srcCode);

traverse(ast, {
  BinaryExpression(path) {
    const { node } = path;
    if (t.isNumericLiteral(node.left) && t.isNumericLiteral(node.right)) {
      path.replaceWith(t.numericLiteral(path.evaluate().value));
    }
  }
});

Дословно получилось так: В BinaryExpression, если node.left это число и node.right тоже число, то заменяем текущий узел числом. Метод evaluate() очень удобно применять для таких простых случаев. Можно было взять числа слева и справа, и в зависимости от оператора выполнить какое-то действие, а можно было сгенерировать код из текущего поддерева через generate(node).code и выполнить его через eval(). Но использование path.evaluate () самый простой вариант.

Кстати, в простых случаях мы можем посмотреть результат прямо в astexplorer. Нужно всего лишь включить Transform:

50aedc1a3b23c7899a617af044d99ec3.png

Теперь можем перенести получившийся код в главное окно и предпринять следующий шаг. Мне также не нравятся последовательности вида (number1, number2):

ba5e99f57ef9d451d66d6406b66c43ae.png

Результатом вычисления такого выражения с оператором , является последнее число. Изучая дерево понимаем, что требуется: нужны узлы, тип которых SequenceExpression и все входящие в него expressions являются числами. Пишем:

// ...
traverse(ast, {
	SequenceExpression(path) {
    const { node } = path;
    let isAllNumbers = true;
    for (let i = 0; i < node.expressions.length; ++i) {
      if (!types.isNumericLiteral(node.expressions[i])) {
        isAllNumbers = false;
        break;
      }
    }
    if (isAllNumbers) {
      path.replaceWith(t.numericLiteral(path.evaluate().value));
    }
  }
});

Грубо говоря, проходимся по всему массиву node.expressions, и, если хоть один узел не является NumericLiteral, то ничего не трогаем, иначе меняем узел уже привычным для нас способом.

Отличный результат!

И глазу приятнееИ глазу приятнее

Теперь переходим к тем самым строкам. Начнём с «внешних». В начале скрипта мы видим массив с чудным именем _, а в коде используются строки из этого массива:

029d6aa2bb2d8726ce5c5f87075df013.png

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

AST

3645d2e49370a93dc8a9268b3f3de538.png
  • Достать сам массив.

  • Пройтись по всем MemberExpression, у которых node.object.name это _, а node.property это число.

  • Заменить MemberExpression на StringLiteral, который мы получим из массива.

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

AST

7d2511e9feb50499792c9170356cf913.png
const mainArray = [];

traverse(ast, {
  AssignmentExpression(path) {
    const { node } = path;
    if (
      t.isMemberExpression(node.left) &&
      node.left.property.name === '_'
    ) {
      const elementsArray = node.right.elements;

      elementsArray.forEach((el) => {
        mainArray.push(el.value);
      });

      path.stop();
    }
  },
});

Результат

f20eacaea2987ef12bd67f0def67f656.png

Массив получен, осталось дело за малым:

// ... 

traverse(ast, {
  MemberExpression(path) {
    const { node } = path;

    if (
      node.object.name === '_' &&
      t.isNumericLiteral(node.property)
    ) {
      path.replaceWith(
        t.stringLiteral(mainArray[node.property.value])
      );
    }
  },
});

Проще и быть не может!Проще и быть не может!

Бежим по всем MemberExpression, у которых object.name равен имени нашего массива _, а property это число.

Скрипт приобретает человекочитаемый вид, но это ещё не всё. Оказывается, мало заменить только эти строки, ведь есть ещё другие, которые мы видели во время отладки:

e1edba0ebdbcb92c5e3dcabe063baca0.png

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

Обфусцированный код

12968e56bab172aab45b438664865b10.gif

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

88a7b2e024c86a9bfe4e75aa528bfa87.png

Оператор , делает из неё IIFE, а она в свою очередь выполняет i.push(i.shift()). Такая строчка возьмёт элемент из начала массива и положит его в конец:

const array = [1, 2, 3];
array.push(array.shift());
console.log(array); // [2, 3, 1]

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

Смещение

4c8478a1f67d04e57a16afb9655a4e57.png

Вы заметили как изящно работает функция «Go to definition» в VS Code? Нажимаешь F12 и тебя перетаскивает к нужной переменной. Красота! Эта красота доступна нам поскольку JavaScript реализует лексическую область видимости, а значит, просто посмотрев на код, мы можем понять как разрешить любую переменную.

Babel довольно-таки умён. И он прекрасно поддерживает работу с областями видимости (scope). По крайней мере, для данный задачки нам его функционала хватит за глаза.

Деобфускация строк

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

AST

e755e8460f89d49d053b3099aba8e27f.png

Рассматривая скриншот, вы явно про себя уже всё проговорили: «Нам требуются MemberExpression«ы, внутри которых происходит вызов функции (CallExpression), а у этого вызова всего один аргумент и этот аргумент — число».

Проще простого:

traverse(this._ast, {
  MemberExpression: (path) => {
    const { node } = path;
    if (t.isCallExpression(node.property) &&
      node.property.arguments.length === 1 &&
      t.isNumericLiteral(node.property.arguments[0])
    ) {
      let callNode = node.property;

      const scopeData = {
        array: [],
      };
	}
  },
});

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

Каждый path в babel имеет свойство scope, которое описывает текущую область видимости:

const currentScope = path.scope; // текущая scope
const scopeUid = currentScope.uid; // идентификатор
const parentScope = currentScope.parent; // родительская scope

currentScope.traverse(currentScope.block, { // Можем обойти scope, как поддерево
	// visitor
})

У каждой scope есть свой уникальный идентификатор uid. Но надо понимать следующую вещь:

d4ffad35bbbf1b9e58103d2e295f23b5.png

Допустим, наш path наткнулся на нужный MemberExpression window[getStringByIndex(0)]. Это подходящая для нас обфусцированная строка. Но это scopeUid = 3, а наш массив находится в scopeUid = 1. Поэтому для поиска нужного массива нам нужно найти для начала ближайшую scope, в которой есть массив. И здесь нам уже нужно выступить в роли интерпретатора, который ищет какой-нибудь объект. Если объекта нет в нашей области видимости, то мы идём по цепочке областей видимости в обратную сторону и проверяем наличие объекта в родительской области, а если нет и там, то в родительской родительской и так далее…

Теперь поймём каким условиями удовлетворяет искомый массив:

AST

64fdfa07068d2d3149a15791e991444c.png

Массив находится в узле CallExpression, который вызывается с MemberExpression, у которого property === 'split', а object.type === StringLiteral

function FindClosestScopeWithArray(scope) {
	const result = {
		array: [],
		scope: {}
	}

	scope.traverse(scope.block, {
      CallExpression(path) {
        const { node } = path;
        if (
          node.callee &&
          t.isMemberExpression(node.callee) &&
          !node.callee.computed &&
          t.isStringLiteral(node.callee.object) &&
          t.isIdentifier(node.callee.property) &&
          node.callee.property.name === 'split'
        ) {
          const delimiter = node.arguments[0].value;
          result.array = node.callee.object.value.split(delimiter);
					result.scope = scope;
          path_.stop();
        }
      },
    });

}

Всё просто: находим разделитель, разбиваем с его помощью строку и массив готов. Но… Массив бы был готов, если бы он был в нужной области видимости. Следовательно, если мы ещё не забрали массив, то должны обратиться к родильской области видимости:

FindClosestScopeWithArray(scope) {
  const result = {
    array: [],
    scope: {}
  }

  while(scope) {
    scope.traverse(scope.block, {
      CallExpression(path) {
        const { node } = path;
        if (
          ...
        ) {
          const delimiter = node.arguments[0].value;
          result.array = node.callee.object.value.split(delimiter);
          result.scope = scope;
          path.stop();
        }
      },
    });
      
    if (result.array.length === 0) { // Если массив ещё пуст, значит в текущей scope
                                     // не было нужного узла CallExpression
                                     // => переходим в родительскую scope
        scope = scope.parent;
    } else {
        return result;
    }
  }
}

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

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

Функция перемешивания

!(function (getArray, shuffleOffset, getStringFromArray, array, j) {
  for (getStringFromArray = b, array = getArray(); !![]; )
    try {
      if (
        ((j =
          (-parseInt(getStringFromArray(158)) / 1) * (parseInt(getStringFromArray(197)) / 2) +
          -parseInt(getStringFromArray(192)) / 3 +
          -parseInt(getStringFromArray(175)) / 4 +
          (parseInt(getStringFromArray(163)) / 5) * (parseInt(getStringFromArray(168)) / 6) +
          (-parseInt(getStringFromArray(187)) / 7) * (parseInt(getStringFromArray(194)) / 8) +
          parseInt(getStringFromArray(174)) / 9 +
          parseInt(getStringFromArray(152)) / 10),
        j === shuffleOffset)
      )
        break;
      else array.push(array.shift());
    } catch (k) {
      array.push(array.shift());
    }
})(a, 910768);

Если у нас есть массив и функция getString, которая учитывает смещение индекса при взятии строки из массива, то мы может переписать цикл перемешивания в более читаемый вид:

while(true) {
  try {
    const j = parseInt... parseInt... parseInt... parseInt... parseInt... parseInt...;
    if (j === shuffleOffset) {
      break;
    } else {
      array.push(array.shift());
    }
  } catch(e) {
    array.push(array.shift());
  }
}

В общем, какая-то странная магия, но чтобы эту магию «завести» нам нужен массив, getStringFromArray() и shuffleOffset. А вот эту бурду parseInt... parseInt... parseInt... parseInt мы возьмём из дерева, то есть получим узел, сгенерируем из него код и выполним eval. Приступаем.

Для начала поиск: всё нужное нам находится в полученной области видимости от функции FindClosestScopeWithArray, значит проблем поиском не возникнет.

Ищем shuffleOffset

4cf08fe205587db99f6a0e26d914a8cb.png
const getShuffleOffset = (scope) => {
  let result;
  scope.traverse(scope.block, {
    CallExpression(path) {
      const node = path.node;
      if (
        node.arguments.length === 2 &&
        t.isNumericLiteral(node.arguments[1]) &&
        t.isIdentifier(node.arguments[0]) &&
        node.arguments[0].name === 'a'
      ) {
        result = node.arguments[1].value;
        path.stop();
      }
    },
  });
  return result;
};

Ищем offset

6897e31597a49fbcd7ea2ef4289fd868.png
const getOffsetForGetString = (scope) => {
  let result;
  scope.traverse(scope.block, {
    ReturnStatement(path) {
      const node = path.node;
      if (
        node.argument &&
        t.isSequenceExpression(node.argument) &&
        t.isAssignmentExpression(node.argument.expressions[0]) &&
        t.isBinaryExpression(node.argument.expressions[0].right) &&
        node.argument.expressions[0].right.operator === '-' &&
        !isNaN(node.argument.expressions[0].right.right.value)
      ) {
        result = node.argument.expressions[0].right.right.value;
        path.stop();
      }
    },
  });
  return result;
};

Ищем выражение paseInt…paseInt…paseInt…paseInt…paseInt…

f70581ed17250779235a0b20367b3b59.png
const getComparisonExpr = (scope) => {
  let result;
  scope.traverse(scope.block, {
    IfStatement(path) {
      const node = path.node;
      if (
        t.isSequenceExpression(node.test) &&
        t.isAssignmentExpression(node.test.expressions[0]) &&
        t.isBinaryExpression(node.test.expressions[0].right) &&
        t.isBinaryExpression(node.test.expressions[0].right.left) &&
        t.isBinaryExpression(node.test.expressions[0].right.left.left)
      ) {
        result = generate(node.test.expressions[0].right).code; // берём поддерево node.test.expressions[0].right и генерируем из него код
        path.stop();
      }
    },
  });
  return result;
};

Ничего сложного нет. Просто смотрим на дерево и пишем условия. Нудно, но не сложно. Функция generate() пакета @babel/generator позволяет из дерева снова получить код. В итоге в переменной сomparisonExpr у нас будет лежать строка: -parseInt(x(158)) / 1 * (parseInt(x(197)) / 2) + -parseInt(x(192)) / 3 + -parseInt(x(175)) / 4 + ...

Во фрагментах строки вида x(%number) x — функция getStringFromArray, которая учитывает смещение при взятии элемента из массива, а его мы уже нашли, следовательно реализовать такую функцию труда не составит:

const getStringFromArray = (index) => {
	return scopeData.array[index - offset];
}

Да, во всём коде используется оператор -. Спасибо, cloudflare, что упрощаешь нам жизнь.

Теперь не составит труда перемешать массив:

function ShuffleArray(scope, scopeData) {
  const shuffleOffset = getShuffleOffset(scope);
  const offset = getOffsetForGetString(scope);
  const сomparisonExpr = getComparisonExpr(scope).replaceAll(
    /parseint\(\w+\(/gi,
    'parseInt(getStringFromArray('
  );

  const getStringFromArray = (index) => {
    return scopeData.array[index - offset];
  };

  scopeData.getString = getStringFromArray;

  eval(`
    while(true) {
      try {
        const f = ${сomparisonExpr};
        if (f === ${shuffleOffset}) break;
        else scopeData.array.push(scopeData.array.shift());
      } catch(e) {
        scopeData.array.push(scopeData.array.shift());
      }
    }
  `);
}

Примечание к коду

Мы хотим выполнить eval, поэтому нам нужно заменить фрагменты x(%number) на название нашей функции getStringFromArray:

e559a7dae5ff8c3ea328ab85d6f9a31d.png

В итоге в ShuffleArray массив как-то перемешивается, а в scopeData появляется функция getString(), которая вернёт элемент по индексу.

Наш финальный обход получается таким:

const scopeUidToData = new Map();
traverse(ast, {
  MemberExpression: (path) => {
    const { node } = path;
    if (
      t.isCallExpression(node.property) &&
      node.property.arguments.length === 1 &&
      t.isNumericLiteral(node.property.arguments[0])
    ) {
      let callNode = node.property;

      const currentScope = path.scope;

      const arrayAndScope = FindClosestScopeWithArray(currentScope);

      const scopeData = {
        array: arrayAndScope.array,
      };
      const requiredScope = arrayAndScope.scope;

      if (scopeUidToData.has(requiredScope.uid)) {
        const scopeData = scopeUidToData.get(requiredScope.uid);
        node.property = t.stringLiteral(scopeData.getString(parseInt(callNode.arguments[0].value)));
        return;
      }

      ShuffleArray(requiredScope, scopeData);

      scopeUidToData.set(requiredScope.uid, scopeData);
      node.property = t.stringLiteral(
        scopeData.getString(parseInt(callNode.arguments[0].value))
      );
    }
  },
});

Я добавил один Map() (scopeUid → scopeData), чтобы по 100 раз не перемешивать массив. После перемешивания мы вставляем пару scopeUid-scopeData в словарь, а потом проверяем есть ли у нас уже готовый массив в данной области видимости.

Результат (с желтым багом) не заставил себя долго ждать:

19704b04d7bd764a21baad4801bd139b.png

Однако, мы можем заметить небольшой недочёт в нашей деобфускации. Мы заменяли CallExpression«ы внутри MemberExpression«ов, в итоге не всё заменилось как надо. Исправим наш последний обход следующим образом:

const scopeUidToData = new Map();
traverse(ast, {
  CallExpression: (path) => {
    const { node } = path;
    if (
      node.arguments.length == 1 &&
      t.isNumericLiteral(node.arguments[0])
    ) {
      const currentScope = path.scope;

      const arrayAndScope = FindClosestScopeWithArray(currentScope);

      const scopeData = {
        array: arrayAndScope.array,
      };
      const requiredScope = arrayAndScope.scope;

      if (scopeUidToData.has(requiredScope.uid)) {
        const scopeData = scopeUidToData.get(requiredScope.uid);
        try {
          path.replaceWith(
            t.stringLiteral(scopeData.getString(parseInt(node.arguments[0].value)))
          )
        } catch(e) {}
        return;
      }

      ShuffleArray(requiredScope, scopeData);

      scopeUidToData.set(requiredScope.uid, scopeData);
      try {
        path.replaceWith(
          t.stringLiteral(scopeData.getString(parseInt(node.arguments[0].value)))
        )
      } catch(e) {}
    }
  },
});

Проблема в том, что теперь сложно привязаться к правильным CallExpression«ам, потому что это обычный вызов функции с числом. Не всегда требуется заменять строку в данном случае. Приходится использовать try-catch, чтобы в случае неуспешной замены узла обход продолжался. Это можно побороть, но не хотелось бы уже на этом акцентироваться.

Далее можно заняться украшательством, например, заменить foo["bar"] на foo.bar. У babel есть уже готовый плагин для такого. Ту же процедуру может выполнить какой-нибудь минификатор по типу UglifyJS. Но можно воспользоваться народным творчеством и написать так:

const validIdentifierRegex =
    /^(?!(?:do|if|in|for|let|new|try|var|case|else|enum|eval|false|null|this|true|void|with|break|catch|class|const|super|throw|while|yield|delete|export|import|public|return|static|switch|typeof|default|extends|finally|package|private|continue|debugger|function|arguments|interface|protected|implements|instanceof)$)[$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u08a0\u08a2-\u08ac\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097f\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58\u0c59\u0c60\u0c61\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d60\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191c\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19c1-\u19c7\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1ce9-\u1cec\u1cee-\u1cf1\u1cf5\u1cf6\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fcc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua697\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790-\ua793\ua7a0-\ua7aa\ua7f8-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa80-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc][$A-Z\_a-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u08a0\u08a2-\u08ac\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097f\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58\u0c59\u0c60\u0c61\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d60\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191c\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19c1-\u19c7\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1ce9-\u1cec\u1cee-\u1cf1\u1cf5\u1cf6\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fcc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua697\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790-\ua793\ua7a0-\ua7aa\ua7f8-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa80-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc0-9\u0300-\u036f\u0483-\u0487\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u0669\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7\u06e8\u06ea-\u06ed\u06f0-\u06f9\u0711\u0730-\u074a\u07a6-\u07b0\u07c0-\u07c9\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u08e4-\u08fe\u0900-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962\u0963\u0966-\u096f\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09cb-\u09cd\u09d7\u09e2\u09e3\u09e6-\u09ef\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a66-\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2\u0ae3\u0ae6-\u0aef\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b66-\u0b6f\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0be6-\u0bef\u0c01-\u0c03\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0c66-\u0c6f\u0c82\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0ce6-\u0cef\u0d02\u0d03\u0d3e-\u0d44\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0d62\u0d63\u0d66-\u0d6f\u0d82\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0df2\u0df3\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0e50-\u0e59\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0ed0-\u0ed9\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e\u0f3f\u0f71-\u0f84\u0f86\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102b-\u103e\u1040-\u1049\u1056-\u1059\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f-\u109d\u135d-\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b4-\u17d3\u17dd\u17e0-\u17e9\u180b-\u180d\u1810-\u1819\u18a9\u1920-\u192b\u1930-\u193b\u1946-\u194f\u19b0-\u19c0\u19c8\u19c9\u19d0-\u19d9\u1a17-\u1a1b\u1a55-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1b00-\u1b04\u1b34-\u1b44\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1b82\u1ba1-\u1bad\u1bb0-\u1bb9\u1be6-\u1bf3\u1c24-\u1c37\u1c40-\u1c49\u1c50-\u1c59\u1cd0-\u1cd2\u1cd4-\u1ce8\u1ced\u1cf2-\u1cf4\u1dc0-\u1de6\u1dfc-\u1dff\u200c\u200d\u203f\u2040\u2054\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua620-\ua629\ua66f\ua674-\ua67d\ua69f\ua6f0\ua6f1\ua802\ua806\ua80b\ua823-\ua827\ua880\ua881\ua8b4-\ua8c4\ua8d0-\ua8d9\ua8e0-\ua8f1\ua900-\ua909\ua926-\ua92d\ua947-\ua953\ua980-\ua983\ua9b3-\ua9c0\ua9d0-\ua9d9\uaa29-\uaa36\uaa43\uaa4c\uaa4d\uaa50-\uaa59\uaa7b\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uaaeb-\uaaef\uaaf5\uaaf6\uabe3-\uabea\uabec\uabed\uabf0-\uabf9\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\ufe33\ufe34\ufe4d-\ufe4f\uff10-\uff19\uff3f]*$/;

traverse(ast, {
  MemberExpression(path) {
    const { computed, object, property } = path.node;
    if (
      !computed ||
      !t.isStringLiteral(property) ||
      !validIdentifierRegex.test(property.value)
    ) {
      return;
    }

    path.replaceWith(
      t.MemberExpression(object, t.identifier(property.value), false)
    );
  },
});

Вот уже и человеческий видВот уже и человеческий вид

Теперь, наконец, можно перейти к изучению кода.

«Деобфускатор»

const { parse } = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
const fs = require('fs');

const srcCode = fs.readFileSync('srcCode.js', { encoding: 'utf-8' });
const ast = parse(srcCode);

traverse(ast, {
  BinaryExpression(path) {
    const { node } = path;
    if (t.isNumericLiteral(node.left) && t.isNumericLiteral(node.right)) {
      path.replaceWith(t.numericLiteral(path.evaluate().value));
    }
  },
});

traverse(ast, {
  SequenceExpression(path) {
    const { node } = path;
    let isAllNumbers = true;
    for (let i = 0; i < node.expressions.length; ++i) {
      if (!t.isNumericLiteral(node.expressions[i])) {
        isAllNumbers = false;
        break;
      }
    }
    if (isAllNumbers) {
      path.replaceWith(t.numericLiteral(path.evaluate().value));
    }
  },
});

const mainArray = [];

traverse(ast, {
  AssignmentExpression(path) {
    const { node } = path;
    if (t.isMemberExpression(node.left) && node.left.property.name === '_') {
      const elementsArray = node.right.elements;

      elementsArray.forEach((el) => {
        mainArray.push(el.value);
      });

      path.stop();
    }
  },
});

traverse(ast, {
  MemberExpression(path) {
    const { node } = path;

    if (node.object.name === '_' && t.isNumericLiteral(node.property)) {
      path.replaceWith(t.stringLiteral(mainArray[node.property.value]));
    }
  },
});

function FindClosestScopeWithArray(scope) {
  const result = {
    array: [],
    scope: {},
  };

  while (scope) {
    scope.traverse(scope.block, {
      CallExpression(path) {
        const { node } = path;
        if (
          node.callee &&
          t.isMemberExpression(node.callee) &&
          !node.callee.computed &&
          t.isStringLiteral(node.callee.object) &&
          t.isIdentifier(node.callee.property) &&
          node.callee.property.name === 'split'
        ) {
          const delimiter = node.arguments[0].value;
          result.array = node.callee.object.value.split(delimiter);
          result.scope = scope;
          path.stop();
        }
      },
    });

    if (result.array.length === 0) {
      // Если массив ещё пуст, значит в текущей scope не было нужного узла CallExpression => переходим в родительскую scope
      scope = scope.parent;
    } else {
      return result;
    }
  }
}

const getShuffleOffset = (scope) => {
  let result;
  scope.traverse(scope.block, {
    CallExpression(path) {
      const node = path.node;
      if (
        node.arguments.length === 2 &&
        t.isNumericLiteral(node.arguments[1]) &&
        t.isIdentifier(node.arguments[0]) &&
        node.arguments[0].name === 'a'
      ) {
        result = node.arguments[1].value;
        path.stop();
      }
    },
  });
  return result;
};

const getOffsetForGetString = (scope) => {
  let result;
  scope.traverse(scope.block, {
    ReturnStatement(path) {
      const node = path.node;
      if (
        node.argument &&
        t.isSequenceExpression(node.argument) &&
        t.isAssignmentExpression(node.argument.expressions[0]) &&
        t.isBinaryExpression(node.argument.expressions[0].right) &&
        node.argument.expressions[0].right.operator === '-' &&
        !isNaN(node.argument.expressions[0].right.right.value)
      ) {
        result = node.argument.expressions[0].right.right.value;
        path.stop();
      }
    },
  });
  return result;
};

const getComparisonExpr = (scope) => {
  let result;
  scope.traverse(scope.block, {
    IfStatement(path) {
      const node = path.node;
      if (
        t.isSequenceExpression(node.test) &&
        t.isAssignmentExpression(node.test.expressions[0]) &&
        t.isBinaryExpression(node.test.expressions[0].right) &&
        t.isBinaryExpression(node.test.expressions[0].right.left) &&
        t.isBinaryExpression(node.test.expressions[0].right.left.left)
      ) {
        result = generate(node.test.expressions[0].right).code; // берём поддерево node.test.expressions[0].right и генерируем из него код
        path.stop();
      }
    },
  });
  return result;
};

function ShuffleArray(scope, scopeData) {
  const shuffleOffset = getShuffleOffset(scope);
  const offset = getOffsetForGetString(scope);
  const сomparisonExpr = ge
    
            

© Habrahabr.ru