Pull to refresh

CRACKL@B Contest 2010. Анализ первого задания

Reading time 8 min
Views 9.5K

Заканчивался 2010 год, шли глобальные реформы на ресурсе. Это были смутные времена. И в это суровое время зарождается идея о создание местного турнира. Эта идея была очень радостно воспринята местным сообществом. Спустя некоторое время, было создано 3 задания (хотя планировалось 5), были выбраны члены жюри и система оценивания. И так, это началось.

Предисловие

Первым заданием был KeyGenMe автор PE_Kill. Задачей было написание KeyGen’a, который должен был генерировать множество ключей для одного имени. Вот собственно оригинальное описание от автора:
Вот вам KeyGenMe. Название говорит само за себя — необходимо написать генератор ключей,
которые успешно будут проходить проверку в KeyGenMe. Для любого имени генератор должен
выдавать множество уникальных серийныйх номеров. KeyGenMe упакован простеньким самодельным
пакером.

Хотелось бы предупредить. KeyGenMe не так сложен как может показаться, но не тривиален
и требует нестандартного подхода. Небольшим плюсом будет предоставленный распакованный
вариант KeyGenMe.

Удачи!


Анализ

Как видно с описания кейгенми накрыт самодельным пакером, который заставляет некоторые антивирусы выдавать ложное срабатывание. Ну, я не заморачивался с ним и снял его с помощь QU. После чего открыл распакованный файл в OllyDbg и увидел по коду на EP (Entry point), что кейгенми написан на Delphi. Зная это, я решил засунуть кейгенми в IDR (Interactive Delphi Reconstructor), который бы распознал мне VCL и Delphi run-time функции. Это нужно только для двух вещей:
  • создание .map файла для последующего импорта его в OllyDbg;
  • нахождения функции обрабатывающей нажатия кнопки проверки.

После того как закончился полный анализ файла создаем .map файл.


И находим адрес обработчика нажатия кнопки.


Для использования .map файла в OllyDbg понадобится установить плагин mapimp. По умолчанию .map файл сохраняется в туже папку, с которой открывали программу и с таким же именем, как и у программы, если всё сделано правильно и плагин установлен, то при открытии кейгенми в ольке должно появиться сообщение с предложением загрузить .map файл, жмем «Да» и всё отлично. Если же что-то не так, то можно его загрузить вручную через главное меню.
Все приготовления выполнены. Переходим в OllyDbg на адрес функции обработки нажатия кнопки проверки, который нужно было запомнить/записать/скопировать с IDR. И мы видим, как вначале получается имя и серийник (оно не столь важно), а вот самое интересное идет дальше.

А именно TKeyEngine.Create создается экземпляр класса TKyeEngine по логике вещей этот класс и есть сердце всей регсхемы. За ней идет две функции результат второй участвует в одном с условий следующем сразу за ее вызовом. Вот в них всё и заключено. Итак, немного подробней о каждой из них.

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

Она также будет дальше и в дочерних функциях. Потому я просто нашел поиском по маске адрес первой встречи и этот адрес стал отправной точкой. В скрипте будем искать отсюда и до рассвета конца, пока не будет заноплены все эти участки (ищет и правда долго, несколько минут, ну на оптимизацию одиночной операции не был расчет, т.ч. и так сойдет).
MOV addr, 0046BED9

@loop:
FIND addr, #EB033BC?7?#
CMP $RESULT, 0
JE @exit
MOV addr, $RESULT
FILL addr, 5, 90
ADD addr, 5
JMP @loop

@exit:

RET

Ну, а перед тем как встретить эту обфускацию было пару действий – это то, что с имени делается массив в 16 байт для последующих «нехитрых» манипуляций, люди с не окрепшей психикой, которые не видели столько кода, который нужно было проанализировать, были в ужасе. Но «не так страшен черт, как его малюют». При более детальном анализе выяснилось, что она не несет глубокой смысловой нагрузки. Я вот об этом большом цикле со свитчем в 10001 * 16 итераций.

Его достаточно рипнуть. Правда этот рипнутый кусок кода будет овер 12к строк, но после чистки его от нопов после деобфускации останется чуть меньше 6к строк. Но перед тем как его рипнуть можно его привести в более корректный вид. Т.к. вся регсхема работает с экземпляром класса, то и этот массив является где-то внутри с сдвигом на 5 байт относительно указателя на этот экземпляр. Поэтому нужно поправить все оффсеты для передачи в наш рипнутый код только массив, без излишеств. Для этого я написал вот такой скрипт.
MOV addr, 0046BED9

@loop:
OPCODE addr
CMP $RESULT_2, 3
JB @next
MOV temp, addr
ADD temp, $RESULT_2
SUB temp, 2
MOV val, [temp], 1
CMP val, 43, 1
JE @ok
CMP val, 53, 1
JNE @next

@ok:
INC temp
MOV val, [temp], 1
SUB val, 5
MOV [temp], val, 1

@next:
ADD addr, $RESULT_2
CMP addr, 004707A1
JA @exit
JMP @loop

@exit:

RET

После чего можно всё это дело рипать. Запихнуть это все с помощью асм-вставки удалось, но возникли некие траблы, которые я не смог решить за приемлемое время, потому было решено вынести это все в асм файл как отдельную функцию и подключить его к проекту, что сразу решило все проблемы. Есть еще две запинки по рипнутому коду, там используется локальная переменная размером в один байт и находящаяся не в начале стека, а как раз и не туда и не сюда ebp-5, можно было бы заменить это всё, но я снова пошел по пути наименьшего сопротивления выделил под по локальные нужды стек в 8 байт (типа с учетом выравнивания). А вторая запинка это то, что там еще юзается вызов еще одной ф-ии, но она небольшая всего 2 команды, рипаем/переписываем вручную (даже не знаю, что быстрее) и заменяем имена вызовов на свое. Вот и всё, четверть пути позади.

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

Первое это будет проверка на длину и на расположение дефисов в нем, с чего можно будет узнать, что серийник имеет форму:
ХХХХХ-ХХХХХ-ХХХХХ-ХХХХХ-ХХХХХ-ХХХХХ-ХХХХХ-ХХХХХ


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

Погуглив этот алфавит было выяснено, что это Base32, функция декодирования. После которой мы имеем массив размерностью 25 байт, и который потом разбивается на три части, первая – массив в 16 байт, вторая – значение в 1 байт, третья – массив в 8 байт.

typedef struct _KEY {
	unsigned char SomeBytes[16];
	unsigned char SomeCount;
	unsigned int  CipherText[2];
} KEY, *LPKEY;

Как можно заметить третий элемент структуры у меня имеет осмысленное имя, забегая наперед скажу, что там должна лежать зашифрованная строка, которая сравнивается с эталонной строкой в самой последней проверке. Итак, проверка серийника и его разложение на составные закончено. При выходе с этой функции будет проверка на то чтобы KEY.SomeCount был больше 32, если так, то мы идем дальше. Дальше идет небольшой цикл, в котором идет swap по-WORD’ово KEY.SomeBytes и массива, который получили с имени, оно служит больше для растягивания кода, т.к. оно не несет никакого логического смысла, т.к. дальше идет работа с этими же массива и с соответственными их элементами, первый с первым, второй со вторым, и т.д.
А дальше идет еще один свитч. У тех у кого первый не отбил всё желание, то второй отсеял еще больше людей. Сам по себе он прост, для каждого значения байта идет вызов функции по одному и тому же прототипу, но вот функции у всех разные.

С этого места подробней. Итак, в игру снова вступает массив, который был получен с имени (скажу по секрету, что он будет участвовать дальше еще в одной функции). Он будет служить как некий pcode, в зависимости от того какое значение байта такое и будет выполнятся действие. А элементы массива KEY.SomeBytes будут служить как input value в эти функции. А те функции, в которые оно передаются имеют подобную незамысловатую логику, байт, который подается в ее разбирается по-битово, в зависимости от того установлен тот или иной бит выполняются небольшой ряд действий над отдельным байтом.

В каждой такой функции для каждого бита свои действия, но самое важное это то, что при наличии одного определенного бита результат всех этих манипуляций помещается в определенную позицию в KEY.CipherText и инкрементируется отдельный счетчик, который при условии того, что он больше восьми (т.е. было произведено 8 замен в KEY.CipherText) завершает функцию с ошибкой. Также есть еще один счетчик, который увеличивается при каждой операции, т.е. если установлен бит, кроме самой первой, когда получаем байт, над которым и происходят все эти телодвижения. При анализе этого всего была выявлена уязвимость, которая позволяла полностью игнорировать все эти манипуляции. Она заключается в том, что если бит отвечающий за запись в KEY.CipherText не установлен, то KEY.CipherText не будет никогда меняться, что очень здорово, это позволяет оставить KEY.CipherText в девственном виде, в таком, в каком мы его задали.

И того имеется, что нужно построить таблицу масок для каждого значения байта, в котором не будет учитываться этот бит. Также нужно не допускать установления бита, при котором получается байт в начале, это нужно для удобного подсчитывания бит для второго счетчика (он нам нужен для одной проверки). Чтобы избежать этой рутиной работы по поиску бит для каждого значения байта мной был написан небольшой скрипт, который находит нужные биты, а также создает с них нужные маски и записывает в таблицу.
MOV addr, 004707FC

ALLOC 100
CMP $RESULT, 0
JE @error
MOV table, $RESULT

PUSHA

MOV c, 0

@loop:
FIND addr, #751190909090900FB643260FB6440326884304#
CMP $RESULT, 0
JE @exit
MOV cl, [$RESULT - 1], 1
MOV addr, $RESULT
ADD addr, 13
FIND addr, #751E90909090900FB643260FB65304885403269090909090FE4326#
CMP $RESULT, 0
JE @exit
MOV bl, [$RESULT - 1], 1
OR bl, cl
NOT bl
MOV [table + c], bl, 1

MOV addr, $RESULT
ADD addr, 20

INC c
CMP c, 100
JE @exit
JMP @loop

@exit:

POPA

DM table, 100, "table.bin"
FREE table, 100

@error:

RET

После этого цикла идет сравнение счетчика бит с значением KEY.SomeCount, если равны, тогда мы уже на финишной прямой. Далее идет вызов функции в которую передается KEY.CipherText как данные и массив с имени как ключ, внутри будет еще несколько вхождений пока не доберемся до нужной функции, по константам в ней было сразу ясно, что это что-то с семейства алгоритмов TEA, по тому в каком месте используется дельта, было определено, что это что-то с XTEA. Чем оно и оказалось, только с небольшим изменением оригинального алгоритма.

Как видно со скрина, я разбирал каждый шаг, чтобы найти в каком месте была произведена модификация. Она заключалась в том, что было изменена в двух местах положение скобок, т.е. приоритет действий.
Оригинал:
void xtea_decipher(unsigned int num_rounds, uint32_t *v, uint32_t const *k) {
    unsigned int i;
    uint32_t v0=v[0], v1=v[1], delta=0x9E3779B9, sum=delta*num_rounds;
    for (i=0; i < num_rounds; i++) {
        v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + k[(sum>>11) & 3]);
        sum -= delta;
        v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + k[sum & 3]);
    }
    v[0]=v0; v[1]=v1;
}

Модифицированный:
void xtea_decipher(unsigned int num_rounds, uint32_t *v, uint32_t const *k) {
    unsigned int i;
    uint32_t v0=v[0], v1=v[1], delta=0x9E3779B9, sum=delta*num_rounds;
    for (i=0; i < num_rounds; i++) {
        v1 -= ((((v0 << 4) ^ (v0 >> 5)) + v0) ^ sum) + k[(sum>>11) & 3];
        sum -= delta;
        v0 -= ((((v1 << 4) ^ (v1 >> 5)) + v1) ^ sum) + k[sum & 3];
    }
    v[0]=v0; v[1]=v1;
}


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

Итог

Для написания кейгена нам понадобится: рипнутый код для создания массива с имени, реализация Base32Encoding, реализация модифицированного XTEA, таблица масок и немного дописать своего кода. Сперва дамп таблицы переводим в массив, который можно вставить в код, это каждый может сделать как ему удобнее. Алгоритм генерации серийника имеет следующий вид:
  1. Name => name_array[16]
  2. Morf(name_array)
  3. KEY.SomeBytes[i] = random() & table[name_array[i]]
  4. KEY.SomeCount = bitscount(KEY.SomeBytes)
  5. KEY.CipherText = xtea_encode(“CRACKLAB”)
  6. SerialBase = Base32Encode(KEY)
  7. Serial = InsertDefis(SerialBase)


Мой кейген: github.com/reu-res/CRACKLAB-Contest-2010

В следующей статье я разберу второе задание, которое будет пожалуй интересней этого. Внимание СПОЙЛЕР! Там вас ждет разбор ВМ (ну автор так заявил), реализация которой отличается от обыденных реализаций, которые похожи по структуре друг на друга. До встречи.
Tags:
Hubs:
+65
Comments 14
Comments Comments 14

Articles