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

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

Пробуем сделать обычный 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

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

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

devtools

Устанавливаем куки в заголовок, пробуем повторить запрос и видим 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

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

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

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

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

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

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

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

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

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

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

devtools

При отправке запроса используется что-то из 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

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

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

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

await context.addInitScript(`
  Object.defineProperty(Object.getPrototypeOf(navigator), 'webdriver', {
    get: () => false
  })
`);
Неожиданный и приятный результат

Примечание

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

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

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

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

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

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

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

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

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

  1. Найти узел

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

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

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

Поиск узла

Что мы здесь видим: тип нужного узла - 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:

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

Результатом вычисления такого выражения с оператором , является последнее число. Изучая дерево понимаем, что требуется: нужны узлы, тип которых 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, то ничего не трогаем, иначе меняем узел уже привычным для нас способом.

Отличный результат!
И глазу приятнее
И глазу приятнее

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

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

AST

  • Достать сам массив.

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

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

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

AST

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])
      );
    }
  },
});
Проще и быть не может!
Проще и быть не может!

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

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

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

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

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

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

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

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

Смещение

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

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

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

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

AST

Рассматривая скриншот, вы явно про себя уже всё проговорили: “Нам требуются 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. Но надо понимать следующую вещь:

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

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

AST

Массив находится в узле 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
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
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…
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:

В итоге в 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 в словарь, а потом проверяем есть ли у нас уже готовый массив в данной области видимости.

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

Однако, мы можем заметить небольшой недочёт в нашей деобфускации. Мы заменяли 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 = 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());
      }
    }
  `);
}

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) {}
    }
  },
});

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)
    );
  },
});


fs.writeFileSync('deobfuscatedCode.js', generate(ast).code);

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

У клаудфлеера ещё много манипуляций с AST, но строки - это то, из-за чего невозможно глазами понять происходящее. Остальные преобразования - пыль в глаза, даже не песок. Сейчас я могу спокойно разбираться в каждом case этого огромного switch. Спасибо, клаудфлеер, что разделил все тесты на отдельный блоки. Очень удобно! Такой методы обфускации называется “Control Flow Flattening”.

Я не буду подробно разбирать каждую строчку, лишь акцентирую внимание на те моменты, из-за которых headless-браузер попадает в бесконечный цикл проверок.

performance.memory

function f(r, j) {
  (r = p),
    (j = Object.getOwnPropertyNames(
      Object.getPrototypeOf(window.performance.memory)
    ).filter(function (k, s) {
      return (s = r), k !== 'constructor';
    })),
    j.map(function (k, t) {
      (t = r), c.push(window.performance.memory[k].toString(36));
    });
}

function i() {
  if ((f(), h++, h === 3)) return void e();
  setTimeout(i, 55);
}
Для людей
const container = [];

const checkMemory = () => {
	const memoryInfoProps = Object.getOwnPropertyNames(
	  Object.getPrototypeOf(window.performance.memory)
	).filter(prop => {
	  return prop !== 'constructor';
	}); // ['totalJSHeapSize', 'usedJSHeapSize', 'jsHeapSizeLimit']
	
	memoryInfoProps.map(prop => {
      container.push(window.performance.memory[prop].toString(36));
	});
}

setTimeout(checkMemory, 55);
setTimeout(checkMemory, 110);
setTimeout(checkMemory, 165);

Для наглядности я убрал перевод в 36-ричную систему счисления:

Обычный браузер

Headless
Значения статичны всегда и у всех одинаковые
Значения статичны всегда и у всех одинаковые

plugins

Как мы видели выше, клаудфлеер собирает плагины, а в headless браузере их нет, поэтому их тоже придётся эмулировать. Но правильно эмулировать плагины через js - это задачка с двумя звёздочками. И мы снова вынуждены поблагодарить клаудфлеер за прекрасную возможность написать эмуляцию плагинов методом “тяп да ляп, тяп ляп, шаляй валяй, кое как, как-нибудь”, но следует не забыть про некоторые нюансы:

Код клаудфлеера
var __pl = navigator.plugins.item(4294967296);

            var __n = '';

            if (__pl) {
              __n = __pl.name;
            }

            DMXvQpNm[DMXvQpNm[0]] *=
              +(
                navigator.plugins.item(4294967297) ===
                navigator.plugins.item(1)
              ) *

Это очень старый детект на переопределение функции item(). В реализации Chromium данная функция принимает тип unsigned, чей максимум 4294967295. Если диапазон превысить, то 4294967297 превратится в 1. Это и проверяет клаудфлеер с помощью строки navigator.plugins.item(4294967297) === navigator.plugins.item(1).

toString()'s

Не стоит забывать о том, что клаудфлеер может вызвать toString() у переопределенных функций и заподозрить эмуляцию. И он это делает:

(c = function (j, k, A) {
    return (
      (A = b),
      k instanceof j.Function &&
        j.Function.prototype.toString
          .call(k)
          .indexOf('[native code]') > 0
    );
  }),

Вы поняли, да?) Грубо говоря, если я сделаю так:

const jsonStringify = JSON.stringify;
JSON.stringify = function() {
  '[native code]';
  return jsonStringify.apply(null, arguments);
}

Клаудфлеер ничего не заподозрит.

window.chrome

В некоторых версиях скрипта клаудфлеер проверяет это свойство, которое в headless моде не работает:

Код клаудфлеера
...
for (var m = 0; m < 9; m++) {
      k = new Array();
      s = 0;
      v = 0;
      m2 = 0;
      ns = 0;
      pt = -1;

      if (window.chrome && window.chrome.csi) {
        pt = window.chrome.csi().pageT;
      }

      if (
        window.performance.timing &&
        window.performance.timing.navigationStart
      ) {
        ns = window.performance.timing.navigationStart;
      }

      for (var j = 0; j < 10; j++) {
        k.push(Date.now() - ns - pt);
      }
			
			...
    }

Придётся тоже эмулировать. Опять же, каких-то специализированных проверок на эмуляцию я не нашёл, поэтому достаточно заполнить объект window.chrome также, как он заполнен в вашем браузере.

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

overrides.js
Object.defineProperty(Navigator.prototype, 'webdriver', {
  get() {
    return false;
  },
});

(function chrome() {
  window.chrome = {};
  window.chrome.app = {
    InstallState: {
      DISABLED: 'disabled',
      INSTALLED: 'installed',
      NOT_INSTALLED: 'not_installed',
    },
    RunningState: {
      CANNOT_RUN: 'cannot_run',
      READY_TO_RUN: 'ready_to_run',
      RUNNING: 'running',
    },
    getDetails: () => {
      '[native code]';
    },
    getIsInstalled: () => {
      '[native code]';
    },
    installState: () => {
      '[native code]';
    },
    get isInstalled() {
      return false;
    },
    runningState: () => {
      '[native code]';
    },
  };

  window.chrome.runtime = {
    OnInstalledReason: {
      CHROME_UPDATE: 'chrome_update',
      INSTALL: 'install',
      SHARED_MODULE_UPDATE: 'shared_module_update',
      UPDATE: 'update',
    },
    OnRestartRequiredReason: {
      APP_UPDATE: 'app_update',
      OS_UPDATE: 'os_update',
      PERIODIC: 'periodic',
    },
    PlatformArch: {
      ARM: 'arm',
      ARM64: 'arm64',
      MIPS: 'mips',
      MIPS64: 'mips64',
      X86_32: 'x86-32',
      X86_64: 'x86-64',
    },
    PlatformNaclArch: {
      ARM: 'arm',
      MIPS: 'mips',
      MIPS64: 'mips64',
      X86_32: 'x86-32',
      X86_64: 'x86-64',
    },
    PlatformOs: {
      ANDROID: 'android',
      CROS: 'cros',
      FUCHSIA: 'fuchsia',
      LINUX: 'linux',
      MAC: 'mac',
      OPENBSD: 'openbsd',
      WIN: 'win',
    },
    RequestUpdateCheckStatus: {
      NO_UPDATE: 'no_update',
      THROTTLED: 'throttled',
      UPDATE_AVAILABLE: 'update_available',
    },
    connect() {
      '[native code]';
    },
    sendMessage() {
      '[native code]';
    },
    id: undefined,
  };

  let startE = Date.now();
  window.chrome.csi = function () {
    '[native code]';
    return {
      startE: startE,
      onloadT: startE + 281,
      pageT: 3947.235,
      tran: 15,
    };
  };

  window.chrome.loadTimes = function () {
    '[native code]';
    return {
      get requestTime() {
        return startE / 1000;
      },
      get startLoadTime() {
        return startE / 1000;
      },
      get commitLoadTime() {
        return startE / 1000 + 0.324;
      },
      get finishDocumentLoadTime() {
        return startE / 1000 + 0.498;
      },
      get finishLoadTime() {
        return startE / 1000 + 0.534;
      },
      get firstPaintTime() {
        return startE / 1000 + 0.437;
      },
      get firstPaintAfterLoadTime() {
        return 0;
      },
      get navigationType() {
        return 'Other';
      },
      get wasFetchedViaSpdy() {
        return true;
      },
      get wasNpnNegotiated() {
        return true;
      },
      get npnNegotiatedProtocol() {
        return 'h3';
      },
      get wasAlternateProtocolAvailable() {
        return false;
      },
      get connectionInfo() {
        return 'h3';
      },
    };
  };
})();

(function plugins() {
  const plugin0 = Object.create(Plugin.prototype);

  const mimeType0 = Object.create(MimeType.prototype);
  const mimeType1 = Object.create(MimeType.prototype);
  Object.defineProperties(mimeType0, {
    type: {
      get: () => 'application/pdf',
    },
    suffixes: {
      get: () => 'pdf',
    },
  });

  Object.defineProperties(mimeType1, {
    type: {
      get: () => 'text/pdf',
    },
    suffixes: {
      get: () => 'pdf',
    },
  });

  Object.defineProperties(plugin0, {
    name: {
      get: () => 'Chrome PDF Viewer',
    },
    description: {
      get: () => 'Portable Document Format',
    },
    0: {
      get: () => {
        return mimeType0;
      },
    },
    1: {
      get: () => {
        return mimeType1;
      },
    },
    length: {
      get: () => 2,
    },
    filename: {
      get: () => 'internal-pdf-viewer',
    },
  });

  const plugin1 = Object.create(Plugin.prototype);
  Object.defineProperties(plugin1, {
    name: {
      get: () => 'Chromium PDF Viewer',
    },
    description: {
      get: () => 'Portable Document Format',
    },
    0: {
      get: () => {
        return mimeType0;
      },
    },
    1: {
      get: () => {
        return mimeType1;
      },
    },
    length: {
      get: () => 2,
    },
    filename: {
      get: () => 'internal-pdf-viewer',
    },
  });

  const plugin2 = Object.create(Plugin.prototype);
  Object.defineProperties(plugin2, {
    name: {
      get: () => 'Microsoft Edge PDF Viewer',
    },
    description: {
      get: () => 'Portable Document Format',
    },
    0: {
      get: () => {
        return mimeType0;
      },
    },
    1: {
      get: () => {
        return mimeType1;
      },
    },
    length: {
      get: () => 2,
    },
    filename: {
      get: () => 'internal-pdf-viewer',
    },
  });

  const plugin3 = Object.create(Plugin.prototype);
  Object.defineProperties(plugin3, {
    name: {
      get: () => 'PDF Viewer',
    },
    description: {
      get: () => 'Portable Document Format',
    },
    0: {
      get: () => {
        return mimeType0;
      },
    },
    1: {
      get: () => {
        return mimeType1;
      },
    },
    length: {
      get: () => 2,
    },
    filename: {
      get: () => 'internal-pdf-viewer',
    },
  });

  const plugin4 = Object.create(Plugin.prototype);
  Object.defineProperties(plugin4, {
    name: {
      get: () => 'WebKit built-in PDF',
    },
    description: {
      get: () => 'Portable Document Format',
    },
    0: {
      get: () => {
        return mimeType0;
      },
    },
    1: {
      get: () => {
        return mimeType1;
      },
    },
    length: {
      get: () => 2,
    },
    filename: {
      get: () => 'internal-pdf-viewer',
    },
  });

  const pluginArray = Object.create(PluginArray.prototype);

  pluginArray['0'] = plugin0;
  pluginArray['1'] = plugin1;
  pluginArray['2'] = plugin2;
  pluginArray['3'] = plugin3;
  pluginArray['4'] = plugin4;

  let refreshValue;

  Object.defineProperties(pluginArray, {
    length: {
      get: () => 5,
    },
    item: {
      value: (index) => {
        if (index > 4294967295) {
          index = index % 4294967296;
        }
        switch (index) {
          case 0:
            return plugin3;
          case 1:
            return plugin0;
          case 2:
            return plugin1;
          case 3:
            return plugin2;
          case 4:
            return plugin4;
          default:
            break;
        }
      },
    },
    refresh: {
      get: () => {
        return refreshValue;
      },
      set: (value) => {
        refreshValue = value;
      },
    },
    namedItem: {
      value: function namedItem(name) {
        '{ [native code] }';
        switch (name) {
          case 'PDF Viewer':
            return plugin3;
          case 'Chrome PDF Viewer':
            return plugin0;
          case 'Chromium PDF Viewer':
            return plugin1;
          case 'Microsoft Edge PDF Viewer':
            return plugin2;
          case 'WebKit built-in PDF':
            return plugin4;
          default:
            return undefined;
        }
      },
    },
  });

  Object.defineProperty(Object.getPrototypeOf(navigator), 'plugins', {
    get: () => {
      '[native code]';
      return pluginArray;
    },
  });

  Object.getOwnPropertyDescriptor(
    Object.getPrototypeOf(navigator),
    'plugins'
  ).get.toString = function toString() {
    return 'function get plugins() { [native code] }';
  };
})();


(function performance_memory() {
  const jsHeapSizeLimitInt = 4294705152;

  const total_js_heap_size = 35244183;
  const used_js_heap_size = [
    17632315, 17632315, 17632315, 17634847, 17636091, 17636751,
  ];

  let counter = 0;

  let MemoryInfoProto = Object.getPrototypeOf(performance.memory);
  Object.defineProperties(MemoryInfoProto, {
    jsHeapSizeLimit: {
      get: () => {
        return jsHeapSizeLimitInt;
      },
    },
    totalJSHeapSize: {
      get: () => {
        return total_js_heap_size;
      },
    },
    usedJSHeapSize: {
      get: () => {
        if (counter > 5) {
          counter = 0;
        }
        return used_js_heap_size[counter++];
      },
    },
  });
})();

Alert!

Не нужно всерьёз применять данный код для других антиботов. У вас ничего не получится. Каждая строчка скрипта overrides.js очень просто детектится.

Куки получены?

Код
const playwright = require('playwright');

const browserOptions = {
  ignoreHTTPSErrors: true,
  args: [
    '--lang=ru-RU,ru;q=0.9',
    '--disable-sync',
    '--disable-features=IsolateOrigins,site-per-process',
  ],
  defaultViewport: {
    width: 1920,
    height: 1080,
  },
  headless: true,
};

(async () => {
  const browser = await playwright.chromium.launch(browserOptions);
  const context = await browser.newContext({
    colorScheme: 'dark',
    userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.120 Safari/537.36"
  });

  const page = await context.newPage();

  page.on('response', async (response) => {
    const headers = await response.allHeaders();
    if (
      headers['set-cookie'] &&
      headers['set-cookie'].includes('cf_clearance')
    ) {
      context.cookies().then(cookies => {
        console.log(cookies);
      })
    }
  });

  await context.addInitScript({ path: 'overrides.js' });

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

  await page
    .frameLocator(
      'iframe[title="Widget containing a Cloudflare security challenge"]'
    )
    .getByText('Подтвердите, что вы человек')
    .click();
})();

Headless OFF

Headless ON

Метод, разобранный в данной статье, поможет переписать решение и под puppeteer, и под selenium и так далее… Я хотел лишь продемонстрировать один из подходов к тому, как это можно сделать.

Комментарии (6)


  1. Saturnych
    00.00.0000 00:00
    +8

    Огонёк!


  1. Tatikoma
    00.00.0000 00:00
    +2

    По версии кода для человеков, взял пример с памятью и попросил ChatGPT поправить нейминг переменных, результат:

    function retrieveMemoryProperties(memoryPrototype, filteredProperties) {
        (memoryPrototype = p),
        (filteredProperties = Object.getOwnPropertyNames(
            Object.getPrototypeOf(window.performance.memory)
        ).filter(function(propertyName, currentPrototype) {
            return (currentPrototype = memoryPrototype), propertyName !== 'constructor';
        })),
        filteredProperties.map(function(propertyName, currentMemoryPrototype) {
            (currentMemoryPrototype = memoryPrototype), memoryPropertiesArray.push(window.performance.memory[propertyName].toString(36));
        });
    }
    
    function callRetrieveMemoryProperties() {
        if ((retrieveMemoryProperties(), callCounter++, callCounter === 3)) return void callUnknownFunction();
        setTimeout(callRetrieveMemoryProperties, 55);
    }

    ИИ так же дал описание того что делает функция:

    It's difficult to say exactly what this code does without knowing what the other variables, such as p, c, and h, are and what the e function does. However, I can tell you what this code appears to do in a general sense.

    The retrieveMemoryProperties function takes two arguments, memoryPrototype and filteredProperties. memoryPrototype is assigned the value of p, and filteredProperties is assigned the result of filtering the property names of the prototype of the window.performance.memory object. The filtered properties are those whose names are not equal to 'constructor'.

    The filtered properties are then mapped over, and for each property, the value of memoryPrototype is assigned the value of p, and the value of the property is pushed onto an array called memoryPropertiesArray as a base-36 string.

    The callRetrieveMemoryProperties function calls the retrieveMemoryProperties function, increments a counter callCounter, and checks if it's equal to 3. If it is, it calls an unknown function callUnknownFunction. If not, it sets a timeout to call callRetrieveMemoryProperties again in 55 milliseconds.

    Кажется с подсказками ИИ разбирать такой код может быть проще.


    1. rastvl Автор
      00.00.0000 00:00
      +1

      Кажется с подсказками ИИ разбирать такой код может быть проще.

      Наверное, поначалу да, но со временем потребность в этом отпадает, если честно) Когда постоянно читаешь подобного рода скрипты, то как-то без особых проблем понимаешь происходящее. Но, как ещё один из вариантов применения ChatGPT, почему нет. Возьму на заметку, спасибо.
      А ещё, чтобы лучше разбираться в техниках минификации и не впадать в ступор при виде !0 вместо true, стоит погуглить(или поспрашивать у ChatGPT) "JavaScript golfing". Например, можно почитать что-то подобное.


  1. ChinaTown
    00.00.0000 00:00
    +2

    Я так понимаю, мало кто дочитал до конца, но проделанная тут работа просто божественна!


  1. Desprit
    00.00.0000 00:00

    Это очень круто, спасибо! По работе часто сталкиваюсь с защитой Cloudflare, давным-давно были рабочие аддоны для того же Scrapy, чтобы handshake этот проделывать, но они уже давно не работают. Стоит чему-то подобному получить огласку, и CF выкатывают обновление :) В закладки!


  1. Dangetsu-PK
    00.00.0000 00:00

    Отличная статья! Не знал что хитрую проверку плагинов можно обойти...