Введение в тему
Обфускация — это мера «безопасности через неясность», которую принимают разработчики, чтобы сделать свой код трудным для понимания людьми. Это усложняет реверс-инжиниринг кода для использования в целях эксплуатации.
Он заключается в использовании случайных строк символов в качестве имен переменных, построении ненужных условных блоков, добавлении в функции бесполезных параметров, перетасовке содержимого массивов, к которым потом будет обращаться через индексы и т.д.
Также часто в коде создаются «дыры», которые затем нужно будет «заполнить» фактическим строковым значением, которое может представлять не только строку, но и функцию, имя переменной и т. д.
Чтобы лучше понять концепцию, давайте применим практический подход и попробуем разобраться в некотором запутанном коде.
Мы будем использовать задачу 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']
Однако полезно понимать процесс деобфускации кода.
Краткое содержание
Обфускация заключается в том, чтобы сделать наш код очень трудным для чтения, обеспечивая «безопасность через неизвестность». Однако сложно не значит невозможно: разобраться в каком-то запутанном коде можно с помощью средств статического и динамического анализа.