Введение в тему

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

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

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

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

Мы будем использовать задачу PicoCTF «Снова на стороне клиента» в категории «Веб-эксплуатация».

Описание

Подход

Это веб-страница, которую нам дают:

Проверив источник, мы обнаруживаем следующее:

<html>
<head>
<title>Secure Login Portal V2.0</title>
</head>
<body background="barbed_wire.jpeg" >
<!-- standard MD5 implementation -->
<script type="text/javascript" src="md5.js"></script>
<script type="text/javascript">
  var _0x5a46=['0a029}','_again_5','this','Password\\x20Verified','Incorrect\\x20password','getElementById','value','substring','picoCTF{','not_this'];(function(_0x4bd822,_0x2bd6f7){var _0xb4bdb3=function(_0x1d68f6){while(--_0x1d68f6){_0x4bd822['push'](_0x4bd822['shift']());}};_0xb4bdb3(++_0x2bd6f7);}(_0x5a46,0x1b3));var _0x4b5b=function(_0x2d8f05,_0x4b81bb){_0x2d8f05=_0x2d8f05-0x0;var _0x4d74cb=_0x5a46[_0x2d8f05];return _0x4d74cb;};function verify(){checkpass=document[_0x4b5b('0x0')]('pass')[_0x4b5b('0x1')];split=0x4;if(checkpass[_0x4b5b('0x2')](0x0,split*0x2)==_0x4b5b('0x3')){if(checkpass[_0x4b5b('0x2')](0x7,0x9)=='{n'){if(checkpass[_0x4b5b('0x2')](split*0x2,split*0x2*0x2)==_0x4b5b('0x4')){if(checkpass[_0x4b5b('0x2')](0x3,0x6)=='oCT'){if(checkpass[_0x4b5b('0x2')](split*0x3*0x2,split*0x4*0x2)==_0x4b5b('0x5')){if(checkpass['substring'](0x6,0xb)=='F{not'){if(checkpass[_0x4b5b('0x2')](split*0x2*0x2,split*0x3*0x2)==_0x4b5b('0x6')){if(checkpass[_0x4b5b('0x2')](0xc,0x10)==_0x4b5b('0x7')){alert(_0x4b5b('0x8'));}}}}}}}}else{alert(_0x4b5b('0x9'));}}
</script>
<div style="position:relative; padding:5px;top:50px; left:38%; width:350px; height:140px; background-color:gray">
<div style="text-align:center">
<p>New and Improved Login</p>
<p>Enter valid credentials to proceed</p>
<form action="index.html" method="post">
<input type="password" id="pass" size="8" />
<br/>
<input type="submit" value="verify" onclick="verify(); return false;" />
</form>
</div>
</div>
</body>
</html>

Мы замечаем запутанный скрипт:

<script type="text/javascript">
  var _0x5a46=['0a029}','_again_5','this','Password\\x20Verified','Incorrect\\x20password','getElementById','value','substring','picoCTF{','not_this'];(function(_0x4bd822,_0x2bd6f7){var _0xb4bdb3=function(_0x1d68f6){while(--_0x1d68f6){_0x4bd822['push'](_0x4bd822['shift']());}};_0xb4bdb3(++_0x2bd6f7);}(_0x5a46,0x1b3));var _0x4b5b=function(_0x2d8f05,_0x4b81bb){_0x2d8f05=_0x2d8f05-0x0;var _0x4d74cb=_0x5a46[_0x2d8f05];return _0x4d74cb;};function verify(){checkpass=document[_0x4b5b('0x0')]('pass')[_0x4b5b('0x1')];split=0x4;if(checkpass[_0x4b5b('0x2')](0x0,split*0x2)==_0x4b5b('0x3')){if(checkpass[_0x4b5b('0x2')](0x7,0x9)=='{n'){if(checkpass[_0x4b5b('0x2')](split*0x2,split*0x2*0x2)==_0x4b5b('0x4')){if(checkpass[_0x4b5b('0x2')](0x3,0x6)=='oCT'){if(checkpass[_0x4b5b('0x2')](split*0x3*0x2,split*0x4*0x2)==_0x4b5b('0x5')){if(checkpass['substring'](0x6,0xb)=='F{not'){if(checkpass[_0x4b5b('0x2')](split*0x2*0x2,split*0x3*0x2)==_0x4b5b('0x6')){if(checkpass[_0x4b5b('0x2')](0xc,0x10)==_0x4b5b('0x7')){alert(_0x4b5b('0x8'));}}}}}}}}else{alert(_0x4b5b('0x9'));}}
</script>

Используя средство украшения JavaScript (например, https://beautifier.io/), мы делаем его немного более читабельным для более детального изучения.

Давайте сначала посмотрим на функцию verify():

function verify() {
    checkpass = document[_0x4b5b('0x0')]('pass')[_0x4b5b('0x1')];
    split = 0x4;
    if (checkpass[_0x4b5b('0x2')](0x0, split * 0x2) == _0x4b5b('0x3')) {
        if (checkpass[_0x4b5b('0x2')](0x7, 0x9) == '{n') {
            if (checkpass[_0x4b5b('0x2')](split * 0x2, split * 0x2 * 0x2) == _0x4b5b('0x4')) {
                if (checkpass[_0x4b5b('0x2')](0x3, 0x6) == 'oCT') {
                    if (checkpass[_0x4b5b('0x2')](split * 0x3 * 0x2, split * 0x4 * 0x2) == _0x4b5b('0x5')) {
                        if (checkpass['substring'](0x6, 0xb) == 'F{not') {
                            if (checkpass[_0x4b5b('0x2')](split * 0x2 * 0x2, split * 0x3 * 0x2) == _0x4b5b('0x6')) {
                                if (checkpass[_0x4b5b('0x2')](0xc, 0x10) == _0x4b5b('0x7')) {
                                    alert(_0x4b5b('0x8'));
                                }
                            }
                        }
                    }
                }
            }
        }
    } else {
        alert(_0x4b5b('0x9'));
    }
}

Мы заметили, что функция _0x4b5b вызывается много раз с шестнадцатеричным числом (в строке) в качестве аргумента. Давайте попробуем понять, что она делает.

var _0x4b5b = function(_0x2d8f05, _0x4b81bb) {
    _0x2d8f05 = _0x2d8f05 - 0x0;
    var _0x4d74cb = _0x5a46[_0x2d8f05];
    return _0x4d74cb;
};

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

Мы видим, что первый аргумент изменяется путем вычитания из него шестнадцатеричного значения 0, вероятно, для преобразования шестнадцатеричной строки в целое число.

_0x2d8f05 = _0x2d8f05 - 0x0;

Причину мы понимаем в следующей строке:

var _0x4d74cb = _0x5a46[_0x2d8f05];

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

var _0x5a46=['0a029}','_again_5','this','Password\\x20Verified','Incorrect\\x20password','getElementById','value','substring','picoCTF{','not_this']

(Массив _0x5a46.)

По сути, функция _0x4b5b используется для получения значения массива _0x5a46 по индексу, указанному в аргументе.

Мы можем вернуться к нашей функции verify().

function verify() {
    checkpass = document[_0x4b5b('0x0')]('pass')[_0x4b5b('0x1')];
    split = 0x4;
    if (checkpass[_0x4b5b('0x2')](0x0, split * 0x2) == _0x4b5b('0x3')) {
        if (checkpass[_0x4b5b('0x2')](0x7, 0x9) == '{n') {
            if (checkpass[_0x4b5b('0x2')](split * 0x2, split * 0x2 * 0x2) == _0x4b5b('0x4')) {
                if (checkpass[_0x4b5b('0x2')](0x3, 0x6) == 'oCT') {
                    if (checkpass[_0x4b5b('0x2')](split * 0x3 * 0x2, split * 0x4 * 0x2) == _0x4b5b('0x5')) {
                        if (checkpass['substring'](0x6, 0xb) == 'F{not') {
                            if (checkpass[_0x4b5b('0x2')](split * 0x2 * 0x2, split * 0x3 * 0x2) == _0x4b5b('0x6')) {
                                if (checkpass[_0x4b5b('0x2')](0xc, 0x10) == _0x4b5b('0x7')) {
                                    alert(_0x4b5b('0x8'));
                                }
                            }
                        }
                    }
                }
            }
        }
    } else {
        alert(_0x4b5b('0x9'));
    }
}

Чтобы заполнить пробелы, мы можем подумать, что нам просто нужно посмотреть на значение, переданное в качестве аргумента _0x4b5b, и получить элемент по этому индексу в массиве _0x5a46. Однако это не так просто. Простое выполнение этого требования приводит к совершенно бессвязным утверждениям, таким как:

checkpass = document[_0x4b5b('0x0')]('pass')[_0x4b5b('0x1')];

==>

checkpass = document['0a029}']('pass')['_again_5'];

Это явно неправильно, особенно если учесть, что в массиве присутствует значение getElementById, и вместе с «document» оно имеет гораздо больше смысла.

Это должно означать, что массив перемешивается и нет возможности статически определить значения.

Вот (возможно) функция, отвечающая за перемешивание:

(function(_0x4bd822, _0x2bd6f7) {
    var _0xb4bdb3 = function(_0x1d68f6) {
        while (--_0x1d68f6) {
            _0x4bd822['push'](_0x4bd822['shift']());
        }
    };
    _0xb4bdb3(++_0x2bd6f7);
}(_0x5a46, 0x1b3));

Статический анализ нам больше не поможет, пора перейти к динамическому.

Давайте откроем инструменты разработчика в нашем браузере и попытаемся разобраться в функции verify().

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

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

Теперь мы можем увидеть значения массива из консоли:

Теперь это начинает иметь больше смысла:

checkpass = document[_0x4b5b('0x0')]('pass')[_0x4b5b('0x1')];
==>
checkpass = document.getElementById('pass').value;

checkpass в основном хранит наши входные данные.

Поскольку теперь мы хорошо понимаем сценарий, мы можем вернуться к нашей функции verify(), вооружившись знаниями, необходимыми для понимания ее поведения.

function verify() {
    checkpass = document[_0x4b5b('0x0')]('pass')[_0x4b5b('0x1')];
    split = 0x4;
    if (checkpass[_0x4b5b('0x2')](0x0, split * 0x2) == _0x4b5b('0x3')) {
        if (checkpass[_0x4b5b('0x2')](0x7, 0x9) == '{n') {
            if (checkpass[_0x4b5b('0x2')](split * 0x2, split * 0x2 * 0x2) == _0x4b5b('0x4')) {
                if (checkpass[_0x4b5b('0x2')](0x3, 0x6) == 'oCT') {
                    if (checkpass[_0x4b5b('0x2')](split * 0x3 * 0x2, split * 0x4 * 0x2) == _0x4b5b('0x5')) {
                        if (checkpass['substring'](0x6, 0xb) == 'F{not') {
                            if (checkpass[_0x4b5b('0x2')](split * 0x2 * 0x2, split * 0x3 * 0x2) == _0x4b5b('0x6')) {
                                if (checkpass[_0x4b5b('0x2')](0xc, 0x10) == _0x4b5b('0x7')) {
                                    alert(_0x4b5b('0x8'));
                                }
                            }
                        }
                    }
                }
            }
        }
    } else {
        alert(_0x4b5b('0x9'));
    }
}

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

checkpass[_0x4b5b('0x2')](0x0, split * 0x2) == _0x4b5b('0x3')
==>
checkpass.substring(0,8) == "picoCTF{"

Функция verify() разбивает переменную checkpass на подстроки и проверяет, соответствуют ли они одному из значений, хранящихся в массиве _0x5a46.

Переведя каждое предложение «если», мы получим:

checkpass.substring(0,8) == "picoCTF{"
checkpass.substring(7,9) == "{n"
checkpass.substring(8,16) == "not_this"
checkpass.substring(3,6) == "oCT"
checkpass.substring(24,32) == "0a029}"
checkpass.substring(6,11) == "F{not"
checkpass.substring(16,24) == "_again_5"
checkpass.substring(12,16) == "this"

Мы заметили некоторое совпадение подстрок, однако определить флаг достаточно легко:

picoCTF{not_this_again_50a029}.

Одна последняя вещь. Мы замечаем, что могли бы угадать флаг с самого начала, просто взглянув на значения массива _0x5a46:

var _0x5a46=['0a029}','_again_5','this','Password\\x20Verified','Incorrect\\x20password','getElementById','value','substring','picoCTF{','not_this']

Однако полезно понимать процесс деобфускации кода.

Краткое содержание

Обфускация заключается в том, чтобы сделать наш код очень трудным для чтения, обеспечивая «безопасность через неизвестность». Однако сложно не значит невозможно: разобраться в каком-то запутанном коде можно с помощью средств статического и динамического анализа.