Разгадайка. Пишем собственный деобфускатор для JavaScript

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
В этой статье я расскажу, как самостоятельно обходить обфускацию JavaScript в тех случаях, когда не помогают даже нестандартные деобфускаторы. Мы рассмотрим метод исследования запутанного кода и напишем свой собственный деобфускатор.

Среди читателей наверняка есть те, кому тема скриптовой обфускации гораздо милее ассемблера и прочего низкоуровневого колдунства. Именно этому вопросу была посвящена, например, моя недавняя статья «JSFuck. Разбираем уникальный метод обфускации JS-кода» или упоминаемая в ней обзорная статья про обфускаторы на Хабре. Но что делать, если перечисленные в этой статье кастомные деобфускаторы не помогают? В таком случае обфускацию придется обходить самостоятельно, и сейчас я расскажу, как это делается.

Используем автоматические деобфускаторы​

Для примера возьмем некое браузерное JavaScript-приложение. Объем его составляет около трех мегабайт, примерно три четверти из которых занимает жестко обфусцированный код, начинающийся так, как показано на следующем скриншоте.
1725682160119.jpeg


А заканчивается этот код вот так.
1725682188795.jpeg


Если ты уже успел ознакомиться с упомянутой выше статьей, характерные имена идентификаторов (_0x58cd18, _0x2f8935_0x321d33, _0x1e0595) должны были натолкнуть тебя на мысль, что код запутан обфускатором obfuscator.io. Однако попытка деобфускации его стандартным онлайн‑деобфускатором при любых настройках не приносит положительного результата: читаемый код в правом окне просто не появляется.
1725682288962.jpeg


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


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

INFO

Забегая вперед, скажу, что перечисленными выше инструментами мы не исчерпали все средства автоматической деобфускации. Например, можно попытаться оптимизировать код через нейросеть Llama. Автоматически деобфусцировать подобный код умеет проект webcrack, но давай все‑таки сделаем вид, что с использованием автоматических средств у нас ничего не вышло, — так намного интереснее!​

Деобфусцируем код вручную​

Для начала натравим на сырой код JS Beautifier, дабы придать ему читабельность. Пробежавшись по теперь уже структурированному коду, обращаем внимание на многочисленные вызовы функций с десятью шестнадцатеричными константами в виде параметров:
JavaScript: Скопировать в буфер обмена
Код:
_0x4a0111(0xa0c, 0xc0b, 0x13ef, 0x1e3e, 0x15e2, 0x1a29, 0x1b08, 0x94b, 0x968, 0x753)
_0x114a88(-0x6d, 0x126c, 0x621, 0xa59, -0x5f2, 0x72a, 0x6cf, 0x8a, 0xbf9, -0x4b4)
_0x27e22f(0xbf2, 0x670, 0xe4, 0x132e, 0x1267, 0xbf9, -0xb6, 0x697, 0x51f, 0x6da)
_0x1e51ce(0xe58, 0x1b5e, 0x2457, 0x191a, 0x224a, 0x133c, 0xf61, 0x1c11, 0x128d, 0xc77)
_0x33055f(0x16f4, 0x1704, 0xbaf, 0x231d, 0x163e, 0x161a, 0xca1, 0x15ba, 0x1c3f, 0x1649)
_0x1b164c(0x485, -0x398, 0x1e0, 0xf51, 0xcdd, 0x2de, 0xfea, 0x82f, -0x54a, 0x37)
...
Логично предположить, что таким образом зашифрованы константы, которые первым делом надо перевести в нормальный читаемый вид. Как обычно, начнем с конца. Код заканчивается следующим фрагментом:
JavaScript: Скопировать в буфер обмена
Код:
} catch (_0x321d33) {
        console[_0x1e0595(0x4f3, 0x854, 0x1210, 0x19e1, 0x3ca, 0x992, 0x665, 0xf98, 0x185b, 0x1073)](_0x321d33);
    }
});
Если мыслить логически, это console.log(_0x321d33), то есть _0x1e0595(0x4f3, 0x854, 0x1210, 0x19e1, 0x3ca, 0x992, 0x665, 0xf98, 0x185b, 0x1073) == "log". Попробуем провернуть этот фарш назад: ищем в коде строку function _0x1e0595:
JavaScript: Скопировать в буфер обмена
Код:
function _0x1e0595(_0x4bc581, _0x4ecbba, _0x1d5a39, _0x50dcae, _0x403da8, _0x4ad34e, _0x2b446e, _0x3b51da, _0x44854e, _0x1491c6) {
    return _0x340121(_0x1491c6 - 0x5ff, _0x4ecbba - 0xb8, _0x1d5a39 - 0xb2, _0x50dcae - 0x81, _0x403da8 - 0x80, _0x4ad34e - 0x1b, _0x2b446e - 0x99, _0x44854e, _0x44854e - 0x72, _0x1491c6 - 0x10f);
Как видишь, эта хрень ссылается на другую субхрень по имени _0x340121. Ищем и ее тоже:
JavaScript: Скопировать в буфер обмена
Код:
function _0x340121(_0x5ae465, _0x101079, _0x1d662f, _0x55f16c, _0x4029db, _0x3a7a06, _0x1d53e1, _0x5b0eb3, _0x4c47fe, _0x445726) {
    return _0x3a86(_0x5ae465 - -0x26c, _0x5b0eb3);
}
Эта субхрень, в свою очередь, ссылается на протохрень под именем _0x3a86, которая, по счастью, последняя (точнее, первая) в этой цепочке:
JavaScript: Скопировать в буфер обмена
Код:
function _0x3a86(_0x37610f, _0x5cbb3a) {
    const _0x1214fd = _0x5e2d();
    return _0x3a86 = function(_0x3aa59b, _0x1ad7b1) {
        _0x3aa59b = _0x3aa59b - (-0x10 * -0x80 + 0x69 * -0x1 + 0xa * -0xb5);
        _0x4072cc = _0x1214fd[_0x3aa59b];
        return _0x4072cc;
    }, _0x3a86(_0x37610f, _0x5cbb3a);
}
Пока что все просто. Осталось найти массив строковых констант, возвращаемый _0x5e2d:
JavaScript: Скопировать в буфер обмена
Код:
function _0x5e2d(){
  const _0x552e21=['t?id=','mjSUq','wcYjB','pljgg','ct:\x20<','accou','nlMqS',
                   ...
                   'se,\x22s','xESCJ'];
  _0x5e2d=function(){
      return _0x552e21;
  };
  return _0x5e2d();
}
Итак, похоже, мы вычленили минимальный фрагмент кода, отвечающий за генерацию обфусцированных строк через функцию _0x1e0595:
JavaScript: Скопировать в буфер обмена
Код:
function _0x5e2d(){
  const _0x552e21=['t?id=','mjSUq','wcYjB','pljgg','ct:\x20<','accou','nlMqS',
                   ...
                   'se,\x22s','xESCJ'];
  _0x5e2d=function(){
      return _0x552e21;
  };
  return _0x5e2d();
}
function _0x3a86(_0x37610f, _0x5cbb3a) {
    const _0x1214fd = _0x5e2d();
    return _0x3a86 = function(_0x3aa59b, _0x1ad7b1) {
        _0x3aa59b = _0x3aa59b - (-0x10 * -0x80 + 0x69 * -0x1 + 0xa * -0xb5);
        _0x4072cc = _0x1214fd[_0x3aa59b];
        return _0x4072cc;
    }, _0x3a86(_0x37610f, _0x5cbb3a);
}
function _0x340121(_0x5ae465, _0x101079, _0x1d662f, _0x55f16c, _0x4029db, _0x3a7a06, _0x1d53e1, _0x5b0eb3, _0x4c47fe, _0x445726) {
    return _0x3a86(_0x5ae465 - -0x26c, _0x5b0eb3);
}
function _0x1e0595(_0x4bc581, _0x4ecbba, _0x1d5a39, _0x50dcae, _0x403da8, _0x4ad34e, _0x2b446e, _0x3b51da, _0x44854e, _0x1491c6) {
    return _0x340121(_0x1491c6 - 0x5ff, _0x4ecbba - 0xb8, _0x1d5a39 - 0xb2, _0x50dcae - 0x81, _0x403da8 - 0x80, _0x4ad34e - 0x1b, _0x2b446e - 0x99, _0x44854e, _0x44854e - 0x72, _0x1491c6 - 0x10f);
}
В дальнейшем нам предстоит автоматизировать поиски кода для каждой аналогичной функции (хоть их и много, но они однотипные). А пока что мы просто попытаемся убедиться, что всё сделали правильно. Жмем в браузере F12 и вставляем найденный фрагмент кода в консоль, пробуя вычислить выражение _0x1e0595(0x4f3, 0x854, 0x1210, 0x19e1, 0x3ca, 0x992, 0x665, 0xf98, 0x185b, 0x1073).

В этот момент мы убеждаемся, что возвращаемая строка не log, а, наоборот, awal, хотя строка log в исходном массиве тоже присутствует. Значит, мы где‑то облажались в расчетах или авторы обфускатора нас хитро обдурили, хотя счастье было так близко...

Посмотрим на код более внимательно. Верхний фрагмент кода, начиная с комментария IT IS NOT SAFE TO MAKE CHANGES IN THE CODE BELOW, хитро перемешивает массив строковых констант _0x552e21 после его инициализации.
1725682687999.jpeg


Занятно, что в условии while мы обнаруживаем знакомую нам по JSFuck конструкцию (!![])==true. При внимательном рассмотрении отмечаем, что такие константы вместе с обратным вариантом (![])==false щедро раскиданы по обфусцированному коду. Делаем себе заметку на будущее поменять их в коде глобальной заменой, после чего вставляем фрагмент, показанный на предыдущем скриншоте, в начало нашего «ядерного кода» и снова делаем тест. На этот раз все сходится, результат правильный: _0x1e0595(0x4f3, 0x854, 0x1210, 0x19e1, 0x3ca, 0x992, 0x665, 0xf98, 0x185b, 0x1073) == "log".

На этом интересная и увлекательная исследовательская часть заканчивается и начинается кодинг, хоть и несложный, но довольно рутинный.

Пишем деобфускатор​

План построения деобфускатора у нас будет следующий.

По образу и подобию описанного выше процесса препарирования функции _0x1e0595 полностью формируем «ядро» функций, которые будут декодировать строковые константы. Для этого ищем все функции, соответствующие вот такому шаблону:
JavaScript: Скопировать в буфер обмена
Код:
function _0x??????(_0x??????, _0x??????, _0x??????, _0x??????, _0x??????, _0x??????, _0x??????, _0x??????, _0x??????, _0x??????) {
        return _0x3a86(??????, ??????);
    }
Примерная реализация этого действия через регулярные выражения на JavaScript выглядит так:
JavaScript: Скопировать в буфер обмена
Код:
var reg = /function (_0x[a-f0-9]*)\(_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*\)\{return _0x3a86\([^)]*\);\}/g;
var functions = [], found,names=[];
while (found = reg.exec(string)) {
    functions.push(found[0]);
    names.push(found[1]);
}
Здесь string — исходный код, на выходе получаем functions — массив кода функций, которые надо добавить к «ядерному» коду и одновременно убрать из кода исходного; names — список имен этих функций.

Ищем функции вида
JavaScript: Скопировать в буфер обмена
Код:
function _0x??????(_0x??????, _0x??????, _0x??????, _0x??????, _0x??????, _0x??????, _0x??????, _0x??????, _0x??????, _0x??????) {
        return <Name1>(??????, ??????,??????, ??????,??????, ??????,??????, ??????,??????, ??????);
    }
Здесь Name1 — имя функции, из списка names, полученного в пункте 1. Код выглядит следующим образом:
JavaScript: Скопировать в буфер обмена
Код:
var reg = /function (_0x[a-f0-9]*)\(_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*\)\{return _0x3a86\([^)]*\);\}/g;
var functions = [], found,names=[];
while (found = reg.exec(string)) {
    functions.push(found[0]);
    names.push(found[1]);
}
var functions1 = [], names1=[];
for (var i=0;i<names.length;i++)
{
    var reg1 = new RegExp("function (_0x[a-f0-9]*)\\(_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*\\)\\{return "+names[i]+"\\([^)]*\\);\\}", "g");
    while (found = reg1.exec(string)) {
       functions1.push(found[0]);
       names1.push(found[1]);
     }
}
Получаем новый список функций functions1 и их имен names1, которые тоже добавляем в ядро и убираем из исходного кода.

Повторяем поиск функции, каждый раз подставляя вместо списка names полученный на предыдущем этапе список names1 до тех пор, пока на очередном шаге список не опустеет. Итоговый код выглядит примерно так:
JavaScript: Скопировать в буфер обмена
Код:
var reg = /function (_0x[a-f0-9]*)\(_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*\)\{return _0x3a86\([^)]*\);\}/g;
var functions = [], found,names=[];
while (found = reg.exec(string)) {
    functions.push(found[0]);
    names.push(found[1]);
}
var names2=names.slice();
while (true)
{
  var functions1 = [], names1=[];
  for (var i=0;i<names2.length;i++)
 {
    var reg1 = new RegExp("function (_0x[a-f0-9]*)\\(_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*,_0x[a-f0-9]*\\)\\{return "+names2[i]+"\\([^)]*\\);\\}", "g");
    while (found = reg1.exec(string)) {
       functions1.push(found[0]);
       names1.push(found[1]);
        functions.push(found[0]);
        names.push(found[1]);
     }
  }
  if (names1.length==0) break;
  names2=names1.slice();
}
На выходе functions — это все паразитические функции, сгенерированные обфускатором, а names — их имена.

Теперь, когда «ядро» сформировано, мы просто перебираем все исчисляемые выражения вида
JavaScript: Скопировать в буфер обмена
<Name1>(??????,??????,??????,??????,??????,??????,??????,??????,??????,??????)
Name1 — имя функции из полученного на предыдущих шагах списка names. Их мы вычисляем при помощи вот такого кода:
JavaScript: Скопировать в буфер обмена
Код:
var expressions=[];
var values=[];
for (var i=0;i<names.length;i++)
 {
    var reg1 = new RegExp(names[i]+"\\([^)]*,[^)]*,[^)]*,[^)]*,[^)]*,[^)]*[^)]*,[^)]*,[^)]*,[^)]*\\)", "g");
    while (found = reg1.exec(string)) {
       var test=found[0];
       var value=undefined;
       try
       {
         value=eval(test);
         expressions.push(found[0]);
         values.push(value);
        } catch (err) {}
       }
  }
На выходе мы получаем массив expressions, в котором содержатся исчисляемые выражения вида _0x4a0111(0xa0c, 0xc0b, 0x13ef, 0x1e3e, 0x15e2, 0x1a29, 0x1b08, 0x94b, 0x968, 0x753) и соответствующие им константы. Нам остается просто заменить в обфусцированном коде первые вторыми обычной глобальной заменой.

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

Разумеется, это далеко не полная деобфускация исходного приложения. В стремлении к совершенству можно свернуть выражения вида Class["MethodName"] в Class.MethodName. Вот чуть более продвинутый вариант кода в Object.MethodName:
JavaScript: Скопировать в буфер обмена
Код:
const _0x17ef7d = {};
_0x17ef7d[MethodName]
Напоследок можно выполнить несколько словарных преобразований следующего вида:
JavaScript: Скопировать в буфер обмена
Код:
const _0x45618a = {
            'aJqDi': function(_0x2b031d, _0x1fc5a3) {
                return _0x2b031d(_0x1fc5a3);
            },
            'FDTmk': function(_0x542b4c, _0x55bd01) {
                return _0x542b4c(_0x55bd01);
            },
В итоге мы получим близкий к читабельному код, в котором только исходные имена переменных и функций будут безвозвратно потеряны.
1725682982233.jpeg


Выводы​

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

Взято: ТУТ
 
Сверху Снизу