Как стать автором
Обновить

Как приготовить обфускацию в JavaScript и не сжечь лабораторию: AST, babel, плагины

Уровень сложностиСредний
Время на прочтение16 мин
Количество просмотров916

Вступление

Вероятно каждый программист или компания, сталкивались с мыслями о своей крутости или хотя бы крутости своих алгоритмов 😎. Разумеется, в этом случае может возникнуть соответствующее нежелание делиться разработками с широкой аудиторией. Данная проблема минимизируется переносом части кода на сервер (если речь идёт о клиент-серверных приложениях). Однако, такой подход не всегда применим, и порой обстоятельства вынуждают нас оставлять чувствительные участки кода прямо на виду.

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

Зачем оно?

Тут не обойтись без дурацкого примера. Представим следующую гипотетическую ситуацию:

  1. Боб заходит на сайт, на котором происходит розыгрыш монитора (вот он -> :tv:). У Боба он получше, но халява всегда приятна!

  2. Заходя на сайт, в браузере исполняется JavaScript, собирающий данные об устройстве пользователя и отправляющий их на сервер. Допустим вот оно:

let w = screen.width, h = screen.height;
// Допустим тут логика с проверкой 
// или отправка этих данных на сервер для принятия решения там...
console.info(w, h);
  1. И... Боба не пускают к странице с розыгрышем! Он негодует, не понимает в чём причина. Затем он узнаёт в правилах розыгрыша, что не допускаются пользователи с уже большими и хорошими мониторами.

  2. К счастью, в школьные годы Боб посещал уроки информатики. Он решительно, нажатием на F12, открывает консоль разработчика, изучает скрипт и понимает, что организаторы, проверяют разрешение экрана. Тогда он решает участвовать с телефона… и успешно проходит проверку.

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

l=~[];l={___:++l,$$$$:(![]+"")[l],__$:++l,$_$_:(![]+"")[l],_$_:++l,$_$$:({}+"")[l],$$_$:(l[l]+"")[l],_$$:++l,$$$_:(!""+"")[l],$__:++l,$_$:++l,$$__:({}+"")[l],$$_:++l,$$$:++l,$___:++l,$__$:++l};l.$_=(l.$_=l+"")[l.$_$]+(l._$=l.$_[l.__$])+(l.$$=(l.$+"")[l.__$])+((!l)+"")[l._$$]+(l.__=l.$_[l.$$_])+(l.$=(!""+"")[l.__$])+(l._=(!""+"")[l._$_])+l.$_[l.$_$]+l.__+l._$+l.$;l.$$=l.$+(!""+"")[l._$$]+l.__+l._+l.$+l.$$;l.$=(l.___)[l.$_][l.$_];l.$(l.$(l.$$+"\""+(![]+"")[l._$_]+l.$$$_+l.__+"\\"+l.$__+l.___+"\\"+l.__$+l.$$_+l.$$$+"\\"+l.$__+l.___+"=\\"+l.$__+l.___+"\\"+l.__$+l.$$_+l._$$+l.$$__+"\\"+l.__$+l.$$_+l._$_+l.$$$_+l.$$$_+"\\"+l.__$+l.$_$+l.$$_+".\\"+l.__$+l.$$_+l.$$$+"\\"+l.__$+l.$_$+l.__$+l.$$_$+l.__+"\\"+l.__$+l.$_$+l.___+",\\"+l.$__+l.___+"\\"+l.__$+l.$_$+l.___+"\\"+l.$__+l.___+"=\\"+l.$__+l.___+"\\"+l.__$+l.$$_+l._$$+l.$$__+"\\"+l.__$+l.$$_+l._$_+l.$$$_+l.$$$_+"\\"+l.__$+l.$_$+l.$$_+".\\"+l.__$+l.$_$+l.___+l.$$$_+"\\"+l.__$+l.$_$+l.__$+"\\"+l.__$+l.$__+l.$$$+"\\"+l.__$+l.$_$+l.___+l.__+";\\"+l.__$+l._$_+l.$$__+l._$+"\\"+l.__$+l.$_$+l.$$_+"\\"+l.__$+l.$$_+l._$$+l._$+(![]+"")[l._$_]+l.$$$_+".\\"+l.__$+l.$_$+l.__$+"\\"+l.__$+l.$_$+l.$$_+l.$$$$+l._$+"(\\"+l.__$+l.$$_+l.$$$+",\\"+l.$__+l.___+"\\"+l.__$+l.$_$+l.___+");"+"\"")())();

Уверяю вас, это не абракадабра, а JavaScript! И выполняет он те же действия. Можете попробовать запустить код в консоли тут (about:blank) (или в любом другом месте).

Полагаю, что в данном случае, наш герой смирился бы с неучастием в розыгрыше и замысел организаторов остался бы нерушим!

Так к чему же всё это? Поздравляю — вы познакомились с инструментом jjencode и узнали, что такое обфускация и какую роль она может сыграть.

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

Прячем секреты. Быстро и ненадолго

Хватит теорий, давайте перейдем к примерам попрактичнее👨‍🔬. Сейчас попробуем преобразовать код с помощью обфускаций, которые вы встретите на просторах интернета с большей вероятностью. Возьмем код поинтереснее, который содержит наши "ноу-хау" операции. И крайне нежелательно, чтобы о них мог узнать каждый желающий, которому не лень тянуться до F12:

function getGpuData(){
  let cnv = document.createElement("canvas");
  let ctx = cnv.getContext("webgl");
  const rendererInfo = ctx.getParameter(ctx.RENDERER);
  const vendorInfo = ctx.getParameter(ctx.VENDOR);

  return [rendererInfo, vendorInfo]
}

function getLanguages(){
  return window.navigator.languages;
}

let data = {};
data.gpu = getGpuData();
data.langs = getLanguages();
console.log(JSON.stringify(data))

Этот код собирает данные об устройстве и браузере и выводит результат в консоль, например (будем использовать вывод как метрику работоспособности кода):

{"gpu":["ANGLE (NVIDIA, NVIDIA GeForce GTX 980 Direct3D11 vs_5_0 ps_5_0), or similar","Mozilla"],"langs":["en-US","en"]}

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

function _0x9591(_0x587f42,_0x4b4b1a){const _0x581ade=_0x581a();return _0x9591=function(_0x9591f0,_0x20e0a4){_0x9591f0=_0x9591f0-0x18a;let _0x2b716b=_0x581ade[_0x9591f0];return _0x2b716b;},_0x9591(_0x587f42,_0x4b4b1a);}const _0x5d747e=_0x9591;(function(_0x41cdc1,_0x26e305){const _0xd47419=_0x9591,_0x3c47fc=_0x41cdc1();while(!![]){try{const _0x1c6e63=-parseInt(_0xd47419(0x18c))/0x1*(parseInt(_0xd47419(0x18d))/0x2)+parseInt(_0xd47419(0x18f))/0x3+-parseInt(_0xd47419(0x18b))/0x4+-parseInt(_0xd47419(0x195))/0x5+parseInt(_0xd47419(0x196))/0x6+-parseInt(_0xd47419(0x19e))/0x7*(parseInt(_0xd47419(0x192))/0x8)+parseInt(_0xd47419(0x19a))/0x9;if(_0x1c6e63===_0x26e305)break;else _0x3c47fc['push'](_0x3c47fc['shift']());}catch(_0x2210e4){_0x3c47fc['push'](_0x3c47fc['shift']());}}}(_0x581a,0x5b85c));function _0x59d10d(){const _0x12260c=_0x9591;let _0x14403e=document[_0x12260c(0x197)](_0x12260c(0x191)),_0xf297ee=_0x14403e[_0x12260c(0x19b)](_0x12260c(0x199));const _0x16d7eb=_0xf297ee[_0x12260c(0x19f)](_0xf297ee[_0x12260c(0x198)]),_0x3174f4=_0xf297ee[_0x12260c(0x19f)](_0xf297ee[_0x12260c(0x193)]);return[_0x16d7eb,_0x3174f4];}function _0x157cda(){const _0x52b881=_0x9591;return window[_0x52b881(0x19c)][_0x52b881(0x18a)];}let _0x421797={};_0x421797[_0x5d747e(0x19d)]=_0x59d10d(),_0x421797[_0x5d747e(0x194)]=_0x157cda(),console[_0x5d747e(0x190)](JSON[_0x5d747e(0x18e)](_0x421797));function _0x581a(){const _0x3fdf5e=['webgl','15135525QqurjW','getContext','navigator','gpu','304409xUlnUb','getParameter','languages','1546148RYMKQN','14903JFRqxJ','96TioORm','stringify','817929YcOxtF','log','canvas','80ELkOfJ','VENDOR','langs','3339820dAlRZJ','3751338qfcHSk','createElement','RENDERER'];_0x581a=function(){return _0x3fdf5e;};return _0x581a();}

Вуаля! Сейчас этот код с удовольствием будет разбирать только машина. Мы с вами, вероятно, не относимся к их числу 🤔. Тем не менее, он по-прежнему работает и выдаёт тот же результат. Обратите внимание на изменения:

  1. Исчезли переносы строк и лишние пробелы.

  2. Имена переменных заменены на неинформативные вроде _0x587f42.

  3. Строки и свойства объектов преобразованы в вызовы функций, возвращающих их значения из массива по индексу. Например, document.createElement("canvas") превратилось в document[_0x12260c(0x197)](_0x12260c(0x191)), что стало возможным благодаря использованию вычисляемых свойств.

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

Ну что, все секреты спрятаны, отправляем код в прод?

Подожжите... Если существуют сервисы для обфускации кода, возможно есть и те, что могут провернуть этот фарш обратно? Несомненно 😏, и не один! Давайте попробуем использовать такой - webcrack. И посмотрим, сможем ли мы получить изначальный, читаемый код. Ниже результат использования деобфускатора:

function _0x59d10d() {
  let _0x14403e = document.createElement("canvas");
  let _0xf297ee = _0x14403e.getContext("webgl");
  const _0x16d7eb = _0xf297ee.getParameter(_0xf297ee.RENDERER);
  const _0x3174f4 = _0xf297ee.getParameter(_0xf297ee.VENDOR);
  return [_0x16d7eb, _0x3174f4];
}
function _0x157cda() {
  return window.navigator.languages;
}
let _0x421797 = {};
_0x421797.gpu = _0x59d10d();
_0x421797.langs = _0x157cda();
console.log(JSON.stringify(_0x421797));

Упс 🙊. Название переменных оно, конечно же, не вернуло, но и на этом спасибо.

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

Отчаиваемся и отдаём секреты без боя? Конечно нет! Давайте посмотрим, что мы можем предпринять ещё...

Как стать обфускатором

Обфускатор - звучит как какой-то вид мага из фентези вселенной, неправда ли? 🧙🏻‍♂️
Определенно, у кого-то получается обфусцировать код прямо во время его написания, и он является прирожденным магом. Возможно, даже, какое-то время вы сами неумышленно могли проделывать подобные заклинания. А что делать теперь, если навыки, благодаря критике "сеньор программистов" улетучились, но появилась идея, потенциально позволяющая затруднить исследование программы? Для этого случая имеет смысл обратиться к инструментам, которые взаимодеюствуют с самой структурой кода и позволяют её модифицировать. Давайте рассмотрим их.

ASTрументы

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

Для более надежной и контролируемой модификации имеет смысл привести его к абстрактной структуре, дереву (AST - abstract syntax tree), проходясь по которому мы сможем изменять интересующие нас элементы и конструкции.

Существуют различные решения для работы с JS кодом, с отличиями в конечном AST. В этой статье для этого мы будем использовать babel. Не нужно ничего устанавливать, поэксперементировать со всем можно на таком ресурсе как astexplorer.

( Если нет желания связываться с babel, то обратите внимание на shift-refactor. Он позволяет взаимодействовать с AST используя CSS селекторы. Довольно минималистичный и удобный подход для изучения и модификации кода. Но оно использует своеобразную версию AST, отличную от babel. Для этого инструмента потестировать свои CSS запросы можно на shift-query interactive demo ).

0. Работа с AST

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

function test(){ // Не тут
  let test = "some data"; // Должно стать changed
  let id = "";
  console.log(test); // Должно стать changed
}

test() // Не тут

Вставим данный код в astexplorer (выбираем сверху JavaScript и @babel/parser), там он должен отобразиться в виде AST. Можно нажать на переменную test, дабы увидеть синтаксис для данного участка кода в правом окне:

Для решения нашей задачи, мы можем написать следующий babel плагин, который будет парсить наш код и искать в нем все названия\идентификаторы (Identifier) с дальнейшим их переименованием при соблюдении определенных условий. Вставим его в нижнее левое окно в astexplorer (включаем ползунок transform и выбираем babelv7 чтобы оно появилось):

function transformCode() {
  return {
    name: "change-name",
    visitor: {
      Identifier(path) {
        // Выводим информацию о текущей ноде и окружении
        console.log(path) 
          
        // Нас интересует только название "test"
        // + оно должно находиться в функции
        if(
          path.node.name === "test" && 
          path.parent.type === "FunctionDeclaration"
        ) {
          // Переименовываем. Функция позаботится о всех ссылках 
          path.scope.rename(path.node.name, "changed")
        }
      },
    },
  };
}

module.exports = transformCode;

Вывод в консоль в этом плагине вставлен не просто так. Благодаря этому мы можем отлаживать наш плагин, изучая вывод в консоли браузера. В данном случае мы выводим информацию о всех нодах типа Identifier. В этой информации содержатся данные о самой ноде - node, родительской ноде - parent, и окружении scope (содержит созданные в текущем контексте переменные и ссылки на них):

Таким образом, в правом нижнем окне, мы можем заметить, что переменная в нашем исходном коде была успешно изменена не затронув других идентификаторов:

function test() {
  let changed = "some data"; // <-
  let id = "";
  console.log(changed); // <-
}

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

  1. Мы конвертировали код в AST с помощью babel на astexplorer.

  2. Изучив AST, мы увидели, что переменная test обозначена типом Identifier, название которой можно определить с помощью свойства name.

  3. Далее, с помощью написанного babel плагина мы обошли все идентификаторы и изменили название тех, что находятся в функции и имеют название test на changed.

1. Прячем названия функций и переменных

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

К слову, в дальнейших примерах, вы можете попробовать использовать любой JS код, заодно поотлаживаете проблемы (нет лучшего обучения, чем мучения) 🥲.

Осуществить наши планы поможет следующий плагин:

// Хранилище с использованными названиями идентификаторов
const usedIdentifiers = new Set();

// Генерирует случайную строку из алфавита `characters` длиной до 3 символов
// Сгенерированная строка должна быть уникальной (не использована ранее идентификаиторами)
function generateRndName() {
  const characters = "abcdefghijklmnopqrstuvwxyz_";
  let randomIdentifier = "";
  do {
    const length = Math.floor(Math.random() * 4); 
    for (let i = 0; i <= length; i++) {
      randomIdentifier += characters.charAt(
        Math.floor(Math.random() * characters.length)
      );
    }
  } while (usedIdentifiers.has(randomIdentifier));
  usedIdentifiers.add(randomIdentifier);
  return randomIdentifier;
}

// Проходимся по всем нодам типа `Identifier` 
// Изменяем их названия на случайные вместе со всеми ссылками
function transformCode() {
  return {
    name: "hide-names",
    visitor: {
      Identifier(path) {
        path.scope.rename(path.node.name, generateRndName());
      },
    },
  };
}

module.exports = transformCode;

Что делает данный код? Да почти тоже самое что в предыдущем примере:

  1. Мы проходимся по всем AST нодам типа Identifier;

  2. В этот раз, мы без каких-либо условий переименовываем встретившиеся нам названия на случайные с помощью функции generateRndName;

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

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

function hjj() {
  let bq = document.createElement("canvas");
  let c = bq.getContext("webgl");
  const a_x = c.getParameter(c.RENDERER);
  const nry = c.getParameter(c.VENDOR);
  return [a_x, nry];
}

function sztc() {
  return window.navigator.languages;
}

let _o = {};
_o.gpu = hjj();
_o.langs = sztc();
console.log(JSON.stringify(_o));

Можно проверить его, исполнив в консоли - после наших манипуляции, он остался рабочим! А это главное в магических деяниях обфускатора ✨.

Но что насчет качества обфускации? Как по мне - злодеяние ещё не слишком сильно: даже заменив названия, опытному программисту будет легко свиду понять предназначение данного кода. Да и толку, если с данной задачей может справиться любой минификатор для JS. Можно ли предпринять что-то более практичное и отягощающее для реверсера? Есть ещё одно заклинание...

2. Скрываем 🪄 всё!

Возможно погорячился написав "всё", но то что мы сделаем сейчас максимально скроет действия нашего кода. В данном разделе мы будем прятать строки и различные свойства объектов, дабы усложнить статический анализ, и потенциально избавить "клиента" от каких-либо затей копаться в нашем коде!

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

function transformCode(babel) {
  const { types: t } = babel;

  // Все наши свойства\строки, заменяемые в коде будут тут
  let data = [];

  return {
    name: "hide-props-strings",
    visitor: {
      //1. Находим корневую ноду `Program`
      // И вставляем в начало кода функцию, возвращающую свойства\строки по индексу
      Program(path) {
        // Тело создаваемой функции
        let funcBody = t.blockStatement([
          // Обьявляем переменную, хранящую свойства\строки
          // let data = [...]
          t.variableDeclaration("let", [
            t.variableDeclarator(t.identifier("data"), t.arrayExpression(data)),
          ]),

          // Возвращаем данные перед этим декодируя их из base64
          // return atob(data[data_index])
          t.returnStatement(
            t.callExpression(t.identifier("atob"), [
              t.memberExpression(
                t.identifier("data"),
                t.identifier("data_index"),
                true
              ),
            ])
          ),
        ]);

        // Создаём функцию `getData` с 1 аргументом `data_index`
        let func = t.functionDeclaration(
          t.identifier("getData"),
          [t.identifier("data_index")],
          funcBody
        );
        
        // Вставляем функцию в начало
        path.node.body.unshift(func);
      },

      // 2. Обходим ноды типа `MemberExpression`. Заменяем свойства на вызовы `getData`
      // Например `document.createElement` будет `document[getData(0)]
      MemberExpression({ node }) {
      // Избегаем уже "вычесленных" нод и где имеется свойство `data_index`, дабы не затронуть новую функцию `getData`
        let prop = node.property.name;
        if (node.computed) return;
        if (prop == "data_index") return;

        // Заносим данное свойство в "хранилище" в `getData`
        data.push(t.stringLiteral(btoa(prop)));

        // Заменяем свойство на вызов функции `getData` с соответствующим индексом
        node.property = t.callExpression(t.identifier("getData"), [
          t.numericLiteral(data.length - 1),
        ]);

        // Делаем свойство вычисляемым
        node.computed = true;
      },


      // 3. Обходим ноды типа `StringLiteral`. Заменяя строки на вызовы `getData`
      StringLiteral(path) {
        // Заносим строку в "хранилище" в `getData`
        data.push(t.stringLiteral(btoa(path.node.value)));

        // Создаём вызов функции `getData` с соответствующим индексом
        const c = t.callExpression(t.identifier("getData"), [
          t.numericLiteral(data.length - 1),
        ]);
        
        // Заменяем данную ноду на новосозданную
        path.replaceWith(c);
      },
    },
  };
}

module.exports = transformCode;

Я уже немного описал работу данного плагина в комментариях кода, но давайте вкратце распишем пошагово что он делает:

  1. Мы создаем массив data в который будут помещены все свойства и строки, заменяемые в коде. Данный массив будет использован в функции getData, возвращающая наши данные;

  2. Далее мы проходимся по AST и находим корневую ноду Program, благодаря которой в начало нашего кода будет вставлена функция getData (возвращающая свойства и строки по заданному индексу);

  3. Затем мы обходим ноды типа MemberExpression. Заменяем свойства на вызовы функции getData. В данном случае, конструкции типа document.createElement будут превращены в document[getData(0)], благодаря вычисляемым свойствам. Попутно мы заносим названия свойств в массив data;

  4. Наконец, мы обходим ноды типа StringLiteral, где так же заменяем строки на вызов getData с нужным индекском.

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

В результате исполнения этого плагина мы получим следующий код:

function getData(data_index) {
  let data = ["Y3JlYXRlRWxlbWVudA==", "Y2FudmFz", "Z2V0Q29udGV4dA==", "d2ViZ2w=", "Z2V0UGFyYW1ldGVy", "UkVOREVSRVI=", "Z2V0UGFyYW1ldGVy", "VkVORE9S", "bGFuZ3VhZ2Vz", "bmF2aWdhdG9y", "Z3B1", "bGFuZ3M=", "bG9n", "c3RyaW5naWZ5"];
  return atob(data[data_index]);
}

function hjj() {
  let bq = document[getData(0)](getData(1));
  let c = bq[getData(2)](getData(3));
  const a_x = c[getData(4)](c[getData(5)]);
  const nry = c[getData(6)](c[getData(7)]);
  return [a_x, nry];
}

function sztc() {
  return window[getData(9)][getData(8)];
}

let _o = {};
_o[getData(10)] = hjj();
_o[getData(11)] = sztc();
console[getData(12)](JSON[getData(13)](_o));

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

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

  • Функции, возвращающие нам наши свойства и строки так и кричат об их предназначении - getData, получить данные. Но благо этот момент можно поправить применив наш первый плагин с переименованием идентификаторов.

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

  • Функция getData является единственной, и не трудно написать скрипт, который заменит все её вызовы на исходные значение, выдернув и исполнив саму функцию.

Несмотря на все недостатки и простоту, думаю это уже можно назвать обфускацией. Но опять-таки, чем мы сейчас отличились от тех же опен-сорс обфускаторов, ведь они делают похожие вещи?
Вспомним о нашей изначальной проблеме - с теми видами обфускаций легко справлялись публичные деобфускаторы. Попробуйте теперь взять получившийся код и деобфусцировать его в webcrack (надеюсь он всё ещё не способен справиться с нашим заклинанием 😅). Можно сказать, практическая значимость была достигнута - наш "защищенный" код теперь не провернуть обратно в один клик на публичном деобфускаторе.

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

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

Давайте займемся этим, а конкретно воспользуемся:

  • Наличием единственной вызываемой функции, хранящей наши свойства и строки;

  • Вызовы этой функции не замаскированы;

Учитывая данные недостатки мы можем реализовать следующий плагин:

// Имя функции, вызываемой для получения свойств и строк
let functionName = "getData"

// Скопированная из обфусцированного кода функция
function getData_copy(data_index) {
  let data = ["Y3JlYXRlRWxlbWVudA==", "Y2FudmFz", "Z2V0Q29udGV4dA==", "d2ViZ2w=", "dG9EYXRhVVJM", "Y2FudmFz", "cGx1Z2lucw==", "bmF2aWdhdG9y", "bGVuZ3Ro", "cHVzaA==", "bmFtZQ==", "Y2FudmFz", "cGx1Z2lucw==", "bG9n", "c3RyaW5naWZ5"];
  return atob(data[data_index]);
}


function transformCode(babel) {
  const { types: t } = babel;
  return {
    name: "deobf-str-props",
    visitor: {
      // 1. Удаляем функцию `getData` из кода
      FunctionDeclaration(path){
      	if(path.node.id.name !== functionName) return
        path.remove()
      },
      
      // 2. Проходимся по всем вызовам с названием `getData`
      // Вызываем скопированную функцию с текущим аргументом
      // Заменяем вызов на полученный результат
      CallExpression(path) {
        if(path.node.callee.name !== functionName) return
        
        let index = path.node.arguments[0].value
        let str = t.stringLiteral(getData_copy(index))
		path.replaceWith(str)        
      }
    },
  };
}

module.exports = transformCode;

Опишем функционал данного плагина для деобфускации:

  1. Мы скопировали функцию getData из обфусцированного кода, исполнив которую с нужным аргументом (индексом) можно получить необходимую строку;

  2. Прошлись по всем вызовам функций getData и заменили их на результат её исполнения;

  3. И наконец, мы нашли функцию getData в AST и удалили её из кода, тк. она там больше не нужна.

В результате получаем следующий код:

function hjj() {
  let bq = document["createElement"]("canvas");
  let c = bq["getContext"]("webgl");
  const a_x = c["toDataURL"](c["canvas"]);
  const nry = c["plugins"](c["navigator"]);
  return [a_x, nry];
}

function sztc() {
  return window["push"]["length"];
}

let _o = {};
_o["name"] = hjj();
_o["canvas"] = sztc();
console["plugins"](JSON["log"](_o));

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

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

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

Заключение

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

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

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

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

Надеюсь вам полезно было получить информацию на эту тему, и вы больше не будете ругать себя или своих программистов за изначально обфусцированный код. Цените этих волшебников 🧙🏻‍♀️ ! Больше магии вы cможете найти тут😏

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Сталкивались ли вы с потребностью в обфускации в своём приложении?
45.45% Было дело5
54.55% И без неё норм6
Проголосовали 11 пользователей. Воздержался 1 пользователь.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Достаточно ли вам стандартных\публичных проектов для обфускации?
16.67% Вполне1
83.33% Хочу что-то кастомное5
Проголосовали 6 пользователей. Воздержались 4 пользователя.
Теги:
Хабы:
+5
Комментарии5

Публикации

Истории

Работа

Ближайшие события