R0 CREW

JavaScript Anti-Debugging Tricks

Оригинал: x-c3ll.github.io

Прошлым летом я много времени беседовал с @cgvwzq о трюках с антиотладкой в JavaScript. Мы пытались найти ресурсы или статьи, в которых эта тема была бы проанализирована, но документация оказалась плохой и в основном неполной. Вы можете найти небольшие трюки в сети, но мы не смогли найти ресурс, где были бы собраны они все. Итак … вот наши поиски.

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

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

В общем, подходами методов, показанных в этом сообщении, являются:

  • Обнаружение неожиданных условий выполнения (мы хотим выполнить их только в браузерах)
  • Обнаружение инструментов отладки (например, DevTools)
  • Контроль целостности кода (Code Integrity Controls)
  • Элементы управления целостностью потока (Flow Integrity Controls)
  • Антиэмуляция

Наша основная идея - объединить методы, показанные здесь, с обфускацией и криптографией. Код разделен на множество зашифрованных кодовых блоков, где процесс дешифрования каждого блока зависит от других ранее дешифрованных блоков. Предполагаемый поток программы - переходит от одного зашифрованного блока к другому зашифрованному блоку в известной последовательности. Если какая-либо из наших проверок обнаруживает что-то «странное», поток программы меняет свой естественный путь и достигает поддельных блоков. Итак, когда мы обнаруживаем, что кто-то отлаживает наш код, мы просто отправляем его в поддельный регион, ограждая «интересные» части от него.

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

0x01 Переопределение функций

Это далеко не самый простой и известный метод защиты от отладки кода. В JavaScript мы можем переопределять функции, которые обычно используются для извлечения информации. Например, console.log () используется для отображения в консоли информации о функциях, переменных и т. Д. Если мы переопределим эту функцию и изменим ее поведение, мы сможем скрыть определенную информацию или просто подделать ее.

Чтобы увидеть это в действии, просто запустите это внутри своего DevTools:

console.log("Hello World");
var fake = function() {};
window['console']['log'] = fake;
console.log("You can't see me!");

Вы должны увидеть следующее:

Hello World

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

console.log("Normal function");
// First we save a reference to the original console.log function
var original = window['console']['log'];
// Next we create our fake function
// Basicly we check the argument and if match we call original function with other param.
// If there is no match pass the argument to the original function
var fake = function(argument) {
    if (argument === "Ka0labs") {
        original("Spoofed!");
    } else {
        original(argument);
    }
}
// We redefine now console.log as our fake function
window['console']['log'] = fake;
// Then we call console.log with any argument
console.log("This is unaltered");
// Now we should see other text in console different to "Ka0labs"
console.log("Ka0labs");
// Aaaand everything still OK
console.log("Bye bye!");

И если все будет работать …

Normal function
This is unaltered
Spoofed!
Bye bye!

Если вы играли до этого с «hooking», это будет хорошо знакомо вам.

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

// Just a normal eval
eval("console.log('1337')");
// Now we repat the process...
var original = eval;
var fake = function(argument) {
    // If the code to be evaluated contains 1337...
    if (argument.indexOf("1337") !== -1) {
        // ... we just execute a different code
        original("for (i = 0; i < 10; i++) { console.log(i);}");
    }
    else {
        original(argument);
    }
}
eval = fake;
eval("console.log('We should see this...')");
// Now we should see the execution of a for loop instead of what is expected
eval("console.log('Too 1337 for you!')");

И … Да, мы выполнили другой код (цикл for вместо console.log со строкой «Too 1337 for you!»).

1337
We should see this...
0
1
2
3
4
5
6
7
8
9

Изменение потока нашей программы таким образом - это крутой трюк, но, как мы сказали в начале, это самый простой трюк, который можно легко обнаружить и победить. Это связано с тем, что в JavaScript каждая функция имеет метод toString (или toSource в Firefox), который возвращает свой собственный код. Поэтому нужно только проверить, был ли изменен код желаемой функции или нет. Конечно, мы можем переопределить метод toString / toSource, но мы столкнемся с той же ситуацией: function.toString.toString ().

Мы поговорим подробнее о «перехвате» и переопределении функций позже, используя другой подход, основанный на прокси-объекте.

0x02 Точки останова

Инструменты, используемые для отладки JavaScript (например, DevTools), способны блокировать выполнение скрипта в произвольной точке, чтобы помочь нам понять то, что происходит. Это делается с помощью «точек останова». Использование контрольных точек при отладке помогает вам узнать, что произошло, что происходит и что произойдет дальше, поэтому они являются одним из основных принципов отладки.

Если вы немного играли с отладчиком и семейством команд x86, вы, вероятно, знаете о инструкции 0xCC. В JavaScript у нас есть аналогичная инструкция, называемая debugger. Размещение debugger; внутри вашего кода приведет к остановке выполнения вашего скрипта, когда отладчик дойдет до этой команды. Пример:

console.log("See me!");
debugger;
console.log("See me!");

Если вы выполните этот код в DevTools, будет показано приглашение на возобновление выполнения. Пока вы не нажмете «Продолжить», сценарий будет заблокирован. И вот следующий (довольно глупый) трюк, который можно увидеть в коммерческих продуктах: просто поместить бесконечный цикл с debugger;. Некоторые браузеры предотвращают эту ситуацию, другие - нет. Но концепция лежащая внутри этого будет только слегка раздражает парня, отлаживающего ваш код. Цикл будет заваливать вас окнами, с требованием возобновить выполнение работы, поэтому мы не сможем продолжить реверсить (изучать) скрипт, пока не исправим это.

setTimeout(function() {while (true) {eval("debugger")

Другой трюк, связанный с точками останова, будет объяснен в следующем разделе.

0x03 Разница во времени

Еще один трюк, заимствованный из классических методов антиреверса, - это использовать проверки, основанные на времени. Когда сценарий выполняется с DevTools (или аналогичным), время выполнения заметно замедляется. Эта поведение может быть использовано нами, при помощи некоторого небольшого эталонного времени, которое скажет нам находимся ли мы под отладкой или нет. Этот подход можно сделать разными способами.

Например, мы можем измерить прошедшее время между двумя или более пунктами внутри кода. Если мы знаем прошедшее среднее время между этими точками в «естественных» условиях, мы можем использовать это значение как эталонное значение. Истекшее время, превышающее ожидаемое эталонное значение, будет означать, что мы находимся под отладчиком.

Другая идея, основанная на этом подходе, состоит в том, чтобы иметь некоторые функции с циклами или другим «тяжелым» кодом для которого известно время выполнения:

setInterval(function(){
  var startTime = performance.now(), check, diff;
  for (check = 0; check < 1000; check++){
    console.log(check);
    console.clear();
  }
  diff = performance.now() - startTime;
  if (diff > 200){
    alert("Debugger detected!");
  }
}, 500);

Сначала запустите этот код без, а затем с DevTools. Как вы можете видеть, мы могли бы обнаружить присутствие отладчика, потому что разница во времени была больше ожидаемой. Такой подход, с использованием эталонного времени, может быть объединен с подходом из предыдущего раздела. Таким образом мы можем получить эталонное время до и после точки останова. Если точка останова выполнена, количество потерянного времени до возобновления выполнения покажет нам наличие отладчика.

var startTime = performance.now();
    debugger;
    var stopTime = performance.now();
    if ((stopTime - startTime) > 1000) {
        alert("Debugger detected!")
    }

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

0x04 Определение DevTools (Chrome)

Впервые я увидел эту технику в этой статье на Reddit. Как сказано в сообщении:

Используемый метод заключается в реализации getter в свойстве id элемента div. Когда этот div-элемент отправляется на консоль, например console.log(div); браузер, для удобства, автоматически пытается получить идентификатор этого элемента. Следовательно, если getter выполняется после вызова console.log, это означает, что консоль открыта.

Простое доказательство концепции:

let div = document.createElement('div');
let loop = setInterval(() => {
    console.log(div);
    console.clear();
});
Object.defineProperty(div, "id", {get: () => { 
    clearInterval(loop);
    alert("Dev Tools detected!");
}});

0x05 Неявный контроль целостности потока

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

С помощью arguments.callee.caller мы можем создать трассировку стека, где мы сохраним ранее выполненные функции. Мы можем использовать эту информацию для генерации хэша, который станет seed’ом, используемым для генерации ключа для дешифрования других частей нашего JavaScript. Таким образом, мы имеем неявный контроль целостности потока, поскольку, если функция переименована или порядок выполняемых функций немного отличается, созданный хэш будет совершенно иным. Если хэш отличается, генерируемый ключ будет отличаться. Если ключ отличается, мы не можем расшифровать код. Чтобы лучше понять это, см. следующий пример:

function getCallStack() {
    var stack = "#", total = 0, fn = arguments.callee;
    while ( (fn = fn.caller) ) {
        stack = stack + "" +fn.name;
        total++
    }
    return stack
}
function test1() {
    console.log(getCallStack());
}
function test2() {
    test1();
}
function test3() {
    test2();
}
function test4() {
    test3();
}
test4();

При выполнении этого кода вы увидите строку # test1test2test3test4. Если мы изменим имя любой функции, возвращаемая строка будет отличаться. Мы можем вычислить безопасный хэш с этой строкой и использовать его позже как seed для получения ключа, используемого для дешифрования других кодовых блоков. Интересным моментом здесь является то, что, если мы не сможем расшифровать следующий блок кода из-за недействительности ключа (аналитик изменил имя функции), мы можем поймать исключение и перенаправить поток выполнения на поддельный путь.

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

0x06 Неявный контроль целостности кода

В конце раздела «0x01 Переопределение функций» мы упоминали, что мы можем получить код функции в JavaScript с помощью метода toString (). Как мы сказали, может быть полезно проверить, была ли переопределена функция, и действительно, эту самую идею можно использовать для того, чтобы узнать, был ли изменен код функции.

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

Самой красивой идеей для создания неявного контроля целостности является использование коллизий в md5. Эта идея была придумана @cgvwzq после нескольких сортов пива прошлым летом. В основном мы можем создавать функции, где собственный md5 проверяется внутри собственной функции. Чтобы выполнить проверку внутри функции, нам нужно поиграть с коллизиями (мы хотим создать нечто вроде function(){ if (md5(arguments.callee.toString() === ‘’) code_function; }.

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

О том, как создать этот тип коллизий, есть множество статей (даже появились некоторые примеры в PoC || GTFO), но первый, который я прочитал и смог повторить, был этот с PHP. Вы можете заранее скорректировать блоки, необходимые для генерации коллизий. В самом деле, вот пример, созданный @cgvwzq, так это проверка целостности содержимого функции.

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

Прокси-объекты 0x07

Прокси-объект является одним из самых полезных инструментов, внедренных в последнее время в мире JavaScript. Этот объект можно использовать для слежения внутри других объектов, изменения поведения (например, хука) или запуска действия при определенных обстоятельствах. Например, если мы хотим отслеживать каждый вызов document.createElement и регистрировать эту информацию, мы можем создать прокси-объект:

const handler = { // Our hook to keep the track
    apply: function (target, thisArg, args){
        console.log("Intercepted a call to createElement with args: " + args);
        return target.apply(thisArg, args)
    }
}
 
document.createElement = new Proxy(document.createElement, handler) // Create our proxy object with our hook ready to intercept
document.createElement('div');

Затем мы увидим, что когда мы вызываем createElement, его аргументы будут регистрироваться в консоли:

Intercepted a call to createElement with args: div

Это прекрасно! Мы можем использовать это в отладке кода через перехват некоторых известных функций (таких как strace / ltrace). Но, как мы видели в разделе «0x01 Переопределение функций», мы можем использовать этот же подход для скрытия или подделки информации или просто для запуска кода, отличного от того, что мы видим (вы можете просто заменить логику внутри хука отображения в примере). Такой способ подключения гораздо лучше, чем простое переопределение.

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

// Call a "virgin" createElement:
try {
    document.createElement.toString();
} catch(e){
    console.log("I saw your proxy!");
}

Тут все в порядке:

"function createElement() { [native code] }"
Но когда мы используем прокси…

//Then apply the hook
const handler = { 
    apply: function (target, thisArg, args){
        console.log("Intercepted a call to createElement with args: " + args);
        return target.apply(thisArg, args)
    }
}
document.createElement = new Proxy(document.createElement, handler);
 
//Call our not-so-virgin-after-that-party createElement
try {
    document.createElement.toString();
} catch(e) {
    console.log("I saw your proxy!");
}

Да, мы смогли бы обнаружить этот прокси:

I saw your proxy!

Как мы сказали: это всего лишь игра в кошки-мышки. Мы можем добавить метод toString:

const handler = { 
    apply: function (target, thisArg, args){
        console.log("Intercepted a call to createElement with args: " + args);
        return target.apply(thisArg, args)
    }
}
document.createElement = new Proxy(document.createElement, handler);
document.createElement = Function.prototype.toString.bind(document.createElement); //Add toString
//Call our not-so-virgin-after-that-party createElement
try {
    document.createElement.toString();
} catch(e) {
    console.log("I saw your proxy!");
}

Сейчас наша проверка не сработает:

"function createElement() { [native code] }"

0x07 Ограничительные среды

Как мы говорили во введении, одну из вещей, которую мы хотим осуществить, - это попытаться определить, выполняется ли код в правильной среде. То, что мы называем «правильной средой», это:

  • Код выполняется в браузере (не эмулятор, и не NodeJS, …)
  • Код выполняется в домене / ресурсе, предназначенном для него (не локальном сервере)

Например, простая проверка, которую мы можем выполнить, чтобы проверить, выполняется ли код локально, является:

// Pretty stupid idea found in commercial software
if (location.hostname === "forum.reverse4you.org" || location.hostname === "127.0.0.1" || location.hostname === "") {
    console.log("Don't run me here!")
}

Если мы запустим этот фрагмент JavaScript внутри локального html, мы увидим сообщение:

Don't run me here!

Следуя этой идее, другой вариант проверки - это обработчик, используемый для открытия документа (что-то вроде if (location.protocol == ‘file:’) {…}) или попробуйте протестировать по HTTP-запросам, если другие ресурсы (изображения, css и т. д.) доступны. Конечно, все эти методы чрезвычайно легко обойти.

Еще более интересная идея состоит в том, чтобы избежать выполнения в NodeJS (или, как мы рассказали в этой статье: измените поток на фальшивый путь). Это опасно, но я видел в реально жизни людей использующих NodeJS для решения задач JavaScript и обхода мер по борьбе с брутфорсом.

Мы можем попытаться обнаружить существование объектов, которые существуют только в контексте браузера:

//Under NodeJS
   try { 
..   console.log(window); 
   } catch(e){ 
..      console.log("NodeJS detected!!!!"); 
   }
NodeJS detected!!!!

И наоборот: в NodeJS у нас есть объекты, которые не существуют в контексте браузера:

//Under the browser
console.log(global)
Uncaught ReferenceError: global is not defined
    at <anonymous>:1:13
 
//Under NodeJS
  console.log(global)
{ console: 
   Console {
     log: [Function: bound log],...
     ...

Мы можем искать тонны метаданных, которые существуют только в браузере. Некоторые подобные идеи можно увидеть в проекте Panopticlick.

0x08 WebGL

Мы не будем говорить об анти-реверсе или обфускации внутри WebGL, потому что вы можете найти тонны информации в сети (да, да WebGL темный и полный ужасов). Вместо этого мы упомянем использование WebGL для обработки данных и взаимодействия с JavaScript, поэтому, если кто-то попытается «подражать» нашему фрагменту JavaScript, ему нужно будет включить поддержку WebGL для своего эмулятора.

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

В заключение

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

Респект и почет @cgvwzq за помощь :slight_smile:

© Translated by AlexS special for r0 Crew