Deobfuscate Me или JS для мсье, которые знают толк

Deobfuscate Me — это серия запутанных фрагментов JS-кода, в которых нужно подобрать такой пароль, что функция validate вернет true. На момент написания этой заметки было 7 заданий, 6 из которых были решены. Нервов и времени было потрачено немало, однако удалось узнать немало нового про работу со строками, приведения типов и чудеса, которые можно сделать с JS.

Далее идут описания решений. Тем, кто хочет пройти самостоятельно, рекомендуется дальше не читать.

P.A.C.K.E.R. #

Первое задание представляет собой фрагмент кода зашифрованного с помощью пакера от Dean Edwards (ссылка). Распаковать код можно тут же. Проверка пароля представляет собой одну строчку. Разобраться в ней не составит труда.

jj$$ #

Второй уровень — это так же весьма известное запутывание с использованием jjencode (сам сталкивался с ним достаточно давно, но внешний вид «запакованного» кода тяжело забыть 🙂 ). Вручную конечно можно распутать этот клубочек, однако этот процесс уже автоматизировали на разных ЯП (см. поиск на github).

Validate me! #

Все. Относительно простые (если знать, где копать) задания закончились. Начинается веселье. Дается большой фрагмент кода, в котором куча вычислений, работы с DOM и прочей ерунды. Для начала надо немного «причесать» код. Берем его и кидаем в jsbeautifier.org. Там, кстати, можно и немного почистить код (см. галочку «Detect packers and obfuscators»). Не скажу, что после этого код стал намного понятней, но хотя бы появилось нормальное форматирование. В самом конце функции validate есть такой фрагмент:

return (password.substr(10,4)=="jxgz")&&f[(n2u.r4+n2u.r5+n2u.T5+n2u.R7+n2u.D0+n2u.e7+n2u.r0)](password.substr(0,10));

jxgz — это динамическая часть, которая меняется после каждого обновления страницы. То есть, ваш пароль должен заканчиваться на эти 4 символа. Остальные 10 (а всего 14) проверяются после &&. В фрагменте:

n2u.r4+n2u.r5+n2u.T5+n2u.R7+n2u.D0+n2u.e7+n2u.r0

прячется слово «validate». Получается, что в объекте f есть метод validate, в котором и проверяется «статическая» часть пароля. В теле этого метода есть один if-else блок. Интересна только ветка else. Подсчитав, что и как там выполняется и какие значения берутся, находится часть пароля (jscrambl3r — чуть ли не plain-text’ом написано). Вторая часть у нас уже есть (та, которая динамическая — не забываем).

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

eval меня полностю #

Опять «многа букав». Довольно большой фрагмент кода, который «обернут» пакером из первого задания. Идем на jsbeautifier и форматируем его (помним про галочку «Detect packers and obfuscators» — ее надо поставить). Внутри оказывается один eval (блок1), один фрагмент со знакомым нам пакером (блок2) и еще один eval, но уже не простой (блок3).

Блок1 — это «var n=32;». Просто инициализация переменной. Блок2 пока пропустим, а вот на блок3 посмотрим внимательнее. В нем есть eval, внутри которого есть еще eval. А в нем есть фрагмент вида «String.fromCharCode(…..)» с кучей аргументов (ascii-коды символов). Интересно, а что за строка прячется за этим? А находится там eval с String.fromCharCode, но уже с другим набором ascii-кодов. Заглянем и в него. Трудно поверить, но внутри снова подобная конструкция. В общем, такая вот матрешка идет еще на 3-4 уровня внутрь. А в самом конце нас ожидает малюсенькая строчка кода, в которой наш пароль сравнивается с нужным:

m = eval('j=function(n){return (password == String.fromCharCode.apply(this,[146,131,133,141,135,148,136,141,143].map(function(m){return m-n})));}')(--n);

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

Теперь вернемся к блоку2. В нем происходят какие-то манипуляции с переменной n. Ее значение можно получить просто выполнив в консоли Chrome блок1 и блок2. Пароль можно получив выполнив в консоли:

var n = 34; [146,131,133,141,135,148,136,141,143].map(function(m){return String.fromCharCode(m-n)}).join('')

Да, значение n выше указано правильное. Но до массива с кодами символов придется копать каждому самому.

Не могу назвать это задание сильно сложным. Громоздкое — да. Предыдущее мне показалось куда заковыристей.

Helo World #

То самое задание, которое не удалось осилить. Увы.

Up and Down #

function validate(username, password) {
	// very strict validation!! (browser specific)
 
	function validate_length(str, len) {
		return str.length == len;
	}
 
	function match_cases(str1, str2) {
		return str1.toUpperCase().toLowerCase() == str2.toUpperCase().toLowerCase();
	}
 
	return validate_length(password, 8) && match_cases(password, 'paflyefiab');
}

Рекомендуется проходить в Google Chrome.

После 3, 4 и 5 заданий это выглядело как-то подозрительно маленьким. Ну да — всего две небольших функции. Суть задания в том, что есть две проверки на наш пароль. По одной из них, его длина должна быть 8 символов, а потом другой — он (после преобразования toUpperCase/toLowerCase) должен соответствовать строке из 10 символов.

Так же, как и в других заданиях, тут есть «динамическая» часть пароля. И она составляет весь пароль. Однако, если присмотреться, то видно, что 10-символьная строка содержит некоторые пары символов, которые могут повторяться при обновлении страницы:

  • fl
  • fi
  • ss

Что это может дать? Само по себе — ничего. Но, надо вспомнить, как в JS работает метод toUpperCase с Unicode-символами. Некоторые символы при переводе в верхний регистр преобразуются в два символа — в FI, в FL, ß в SS. Это и есть решение. Нужно подставить эти символы в наш пароль. Проверка будет пройдена и наш ник появится в табличке.

Чудеса преобразования типов данных #

Рекомендуется проходить в Google Chrome.

function validate(username, password) {
    // JS is fun :) (also browser specific)
    p = noDups(password.toLowerCase());
    p = p.toUpperCase().replace(/[^A-Z]/g,'');
    if(p.indexOf('RSTCLA') == -1) { return false; }
    if(p.indexOf('ZNUY') == -1) { return false; }
    if(p.length != 16) { return false; }
 
    var a = new Array(parseInt(p.length/2));
    for(var i = 0; i < a.length; i++)
    {
        c = parseInt(p.charCodeAt(i*2));
        c2 = parseInt(p.charCodeAt(i*2+1));  
        a[i]=((c>c2)?(c-c2):(c2-c));;
    }
    var b = new Array(parseInt(a.length/2));
    for(var i = 0; i < b.length; i++)
    {
        c = parseInt(a[i*2]);
        c2 = parseInt(a[i*2+1]);
        b[i]=((c/c2));
    }
 
    return (b[0]!=b[0])&&(b[1]==b[1])&&((b[2])&&((b[2]%2)!=(b[2]%2))&&(b[3]==b[3]));
 
    function noDups( s ) {
      var chars = {}, rv = '';
      for (var i = 0; i < s.length; ++i) {
        if (!(s[i] in chars)) {
          chars[s[i]] = 1;
          rv += s[i];
        }
      }
      return rv;
    }
}

Самое мозгодробительное задание из всех. Наш пароль должен состоять из 16 букв без повторов. В нем должна быть последовательность «RSTCLA» (статическая) и еще одна из 4-х символов (динамическая). Это «простая» часть условия. «Сложная» выглядить так — из этой строки убираются все повторяющиеся символы (см. функцию noDups). После этого пароль переводится в upperCase, берутся ascii-коды каждой пары символов пароля и вычитаются между собой (1 и 2, 3 и 4 … 15 и 16). Модули этих разниц (8 штук) пишутся в массив A. Таким образом в этом массиве получается 8 не отрицательных чисел. Из элементов A вычисляется массив B на 4 элемента путем деления соседних элементов A:

b[i] = a[2 * i] / a[2 * i + 1];

И самый сок — в массиве B должны выполняться такие условия:

b[0] != b[0];
b[2] && ((b[2] % 2) != (b[2] % 2))

Для нулевого элемента подходит только значение NaN. А под второе условие подходит значение Infinity. Таким образом, массив B должен быть вида:

b = [ NaN, \d, Infinity, \d ]; // \d - какое-то число (но не 0)

NaN можно получить путем деления 0 на 0, а бесконечность — делением чего-либо на 0. Из этого следует, что массив А должен быть таким:

a = [ 0, 0, \d, \d, \d, 0, \d, \d ];

Видно, что «RSTCLA» будет в середине пароля. А динамическая часть — в конце.

Но как получить эти нолики, если каждый элемент массива a — это разница ascii-кодов символов нашего пароля? При чем, из пароля уже вырезаны повторы символов.

Тут снова поможет «баг» с upperCase. Первым символом нашего пароля будет — ß. Из него получится SS, которое даст первый нолик. Далее возьмем ff. Из него получится FF. Это второй нолик. А вот из чего взять третий нолик? Возможно, есть еще какой-то символ, который преобразуется в два одинаковых при его upperCase, но мне он не попался. Пришлось искать обходной путь. И он был найден. Есть такой символ — ſ. В верхнем регистре он преобразуется в S. Но вот беда — одно S уже есть в статической части RSTCLA. Надо его как-то оттуда убрать. А точнее, подменить на что-то. Как не странно, но есть символ st, который в upperCase дает ST. Его и возьмем. Получается, что «обычная» S теперь доступна для использования. Берем ее и ſ. Теперь наш пароль выглядит приблизительно так:

ßffRstCLASſWGOZ // WGOZ - динамическая часть

С этим заданием пришлось посидеть подольше, но планка была взята.

P.S.

Вот и все. Почти все задания решены. Автору спасибо за такой квест.

Оставить комментарий

Top ↑ | Main page | Back