Думаю, каждый читатель журнала знает, какой важный ивент прошел в ноябре прошедшего года. Да, конечно же, я о конференции ZeroNights. Это было невероятно крутое мероприятие, хорошо организованное, с классными докладами и уникальными воркшопами. Но сейчас немного о другом. Дело в том, что на этом мероприятии была отдельная секция, где можно было что-нибудь поломать, например «Вдоль и поперек» от PentestIT и «Сломай меня» от «Лаборатории Касперского». «Вдоль и поперек» проходил прямо во время конференции, а пропускать из-за него доклады и воркшопы не очень хотелось, поэтому там я не участвовал. Зато на задание от ЛК «Сломай меня» дали время и после конференции. Вот ему-то я и решил посвятить вечер выходного дня.

slomay-featured

WARNING

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

Вникаем в задачу

Что собой представляет задание? Обычный исполняемый файл, при запуске которого видно два поля для ввода — электронная почта и серийный номер (рис. 1). Вводишь туда свою почту и номер, который ты каким-то образом добыл, и после этого при нажатии на кнопку Check появляется окошко, сообщающее, что ты молодец и серийный номер валидный, задание успешно решено и можно отсылать результаты в ЛК. Следовательно, для успешного решения задания потребуется найти правильную пару «email — серийный номер».

Рис. 1. Интерфейс нашего крякми
Рис. 1. Интерфейс нашего крякми

Первичный анализ

Я начал разбор задания с того, что решил просто пропатчить экзешник. Открыл IDA и первым делом во вкладке Strings нашел строку, которая вываливается при неудачном вводе серийника: «Fail, Serial is invalid !!!». Видим, что на нее всего одна перекрестная ссылка, смело идем по ней и попадаем в функцию, которая нам выкидывает окно о неудачной попытке ввода (см. рис. 2).

Рис. 2. Локализация места проверки ключа
Рис. 2. Локализация места проверки ключа

Даже беглого взгляда на листинг хватает, чтобы понять, что от условного перехода jnz (и, естественно, от сравнения test eax eax) зависит, какой код будет выполняться: тот, который скажет, что все хорошо, или тот, который отправит нас лесом. Проверим гипотезу: запускаем выполнение программы и, дойдя до условного перехода, меняем флаг ZF на 0. Выполняем условный переход, и о чудо, что мы видим?! Все сработало. Следовательно, чтобы пропатчить программу, нужно поменять условный переход на безусловный. Правда, есть одна неувязочка: по заданию нужно найти именно работающий серийник. Не вопрос — все будет.

Разбор логики хеширования

При дальнейшем анализе этого участка кода становится очевидно, что функция sub_4012D0 возвращает в регистре eax значение, которое нам и нужно. Для удобства как-нибудь назовем функцию, например cmp_serial. Ставим на ее вызове брейкпоинт, смотрим, что передается туда в качестве параметров, и мы лицезрим, что туда отправляется ссылка на строку с введенным мылом, серийным номером, длина серийника и длина email’а. Сомнений нет, это нужная нам функция! Жмем F7 и идем вглубь! Видим внутренности cmp_serial и тут же легким нажатием F5 переходим в окно псевдокода. Hex-rays делает анализ существенно легче, так что воспользуемся им. Сразу же бросается в глаза первая проверка 004012ED cmp eax, 12h, из нее делаем вывод, что длина серийника должна быть 18 символов. Далее (с 004012F8 по 00401315) идет обычная проверка, что введенные символы являются цифрами или символами латинского алфавита.

Первые выводы

Но начнем анализ этой функции с конца, так как нам выяснить, когда она возвращает единицу. Давай внимательно посмотрим на кусок кода, представленный на рис. 3 (полученный с помощью hex-rays). На первый взгляд там целых шесть условий и пять переменных v7, v8, v13, v15, v17, от которых зависит, что вернет функция. Но на самом деле это не так! Если вручную оттрассировать данный цикл, то мы увидим, что return 1 выполняется тогда и только тогда, когда v15 == 1. Значение данной переменной присваивается вот здесь: v15 = v8 == v13 ? v17[v14] == 1: v17[v14] == 0. То есть v15 есть результат сравнения v17[v14] == 1 или v17[v14] == 0. Если копнуть глубже, становится видно, что переменные v7, v8, v13 — что-то типа составного счетчика, который изменяется в ходе выполнения цикла. Ну и остается массив v17, который, для того чтобы функция вернула единицу, должен состоять из значений 0 и 1 и иметь следующий вид:v17 = {100010001} (данное значение выявлено путем длительной трассировки и игры с параметрами).

Рис. 3. Место успешного выхода из функции cmp_serial
Рис. 3. Место успешного выхода из функции cmp_serial

Зная это, мы можем поставить бряк перед циклом, а затем вводить разные серийники и смотреть, как изменяется переменная v17. Аккуратно подбирая значения, можно легко найти одну из коллизий. Но, так как легкие пути нам неинтересны, мы будем писать полноценный кейген. Зная, от какой переменной зависит успех операции и какое именно значение переменная должна принимать, давай выясним, где же ей присваивается значение. Единственное место, где используется эта переменная, — это функция sub_401170(v9, v11, (int)v17), в которую она передается в качестве параметра (рис. 4). На основании этого можно сделать заключение, что именно в этой функции на основе переменных v9 и v11 формируется нужная нам переменная v17. Тут стоит отметить, что нельзя всецело доверять hex-rays, например, данную функцию изначально он определил с одним параметром (sub_401170( (int)v17)), но после того, как я прошелся по ней под отладчиком, декомпилер исправился. Да и пройтись хотя бы один раз по интересующей функции и проверить, как все выполняется в ассемблере, попутно сравнивая с hex-rays, все же стоит — можно увидеть что-нибудь интересное. Окинув взглядом функцию cmp_serial (рис. 4), понимаем, что v9 и v11 используются по очереди с самого начала и до функции sub_401170(v9, v11, (int)v17), так что есть смысл теперь анализировать эту функцию с того места, откуда начинается формирование нужных нам переменных.

Рис. 4. Место в функции `cmp_serial`, где находится логика хеширования серийника
Рис. 4. Место в функции `cmp_serial`, где находится логика хеширования серийника

Первая функция sub_401000(a1, v4) принимает наш email и его длину. Посмотрим, что там происходит. А там два цикла, в первом массив заполняется значениями от 0 до 256 (массив, судя по всему, объявленный глобально, так как в параметрах не передавался, а доступ к нему из этой функции есть). Во втором же цикле массив как-то перемешивается, при этом используется email. Затем обращение к различным функциям, часть из которых системные, а часть нет. Вторые как раз представляют для нас очень большой интерес. Поэтому дальше постараемся разобраться, что делает каждая функция.

Разоблачение функций

Что делает функция sub_401000(a1, v4), мы разобрались. Идем далее. Вот это: v9 = sub_401BA2(9u) похоже на вызов какой-то системной функции. Почему? Хороший вопрос. Во-первых, здесь первый раз используется переменная v9, во-вторых, принимает в параметре число 9, то есть количество элементов новоиспеченного массива. И в-третьих, чтобы убедиться окончательно, ставим hardware breakpoint на чтение/запись на область памяти переменной v9, точнее только на первый байт. Найти эту область памяти и поставить туда бряк лучше всего, когда стоишь на sub_401AC5(v9, 9, a3, 9). В IDA есть окно, показывающее состояние стека, так как v9 передается первым параметром, то ее адрес будет лежать на вершине стека, пройдем по нему и поставим бряк.

Стек вызова
Стек вызова

Потом заново запускаем приложение. Если бряк сработал и не просто сработал, а IDA показала нам, что мы находимся в ntdll.dll, то, скорее всего, это системная функция, в данном случае выделяющая память под массив (конечно, это может быть юзерская функция, которая вызывает системную, но это точно так же легко проверяется, и суть от этого не меняется). Итак, мы выяснили, что sub_401BA2(9u) выделяет память для массива v9. Что же делает следующая процедурка: sub_401AC5(v9, 9, a3, 9)? Судя по тому, что она принимает в качестве параметров указатель на созданный массив и указатель на серийный номер, то, скорее всего, она копирует в v9 первые девять символов серийника. Смотрим значение v9 до выполнения и после — как оказалось, наши подозрения подтвердились. Затем сразу же выполняется функция, принимающая указатель на массив v9. Вот она —sub_401070(v9). Выясним, что же она делает. Для этого перезапускаем отладчик в IDA и перед заходом в функцию ставим бряк на области данных переменной v9. Делаем один шаг, нажимая F8, и смотрим на ту область памяти, где до этого была первая половина символов нашего серийника. И мы видим набор чисел. Ну что ж, это как раз то, что нам нужно! В этой процедурке происходит хеширование серийного номера. Как? Это мы поймем потом, сейчас нужно восстановить полную картину. Следующая строчка тоже, судя по всему, выделяет память для массива: v10 = sub_401A87(9, 1) (предлагаю тебе самому разобраться, почему так). А затем hex-rays нам четко показывает, что идет простое перемешивание массива. Ну вот, с первой половиной разобрались. И глядя дальше, видим, что абсолютно то же самое происходит и со второй половиной серийного номера.

Алгоритм хеширования

Целостная картина почти сложилась, осталось два недостающих пазла, а точнее функции. Первая, которая хеширует серийный номер sub_401070(v9), вторая выдает нам заветную переменную v17 - sub_401170(v9, v11, (int)v17). Давай разберемся с первой. Заходим в нее, открываем псевдокод, и беглого взгляда хватает, чтобы понять, что вся эта писанина в 30 строк, где v9 хешируется на основе того самого глобального массива из функции sub_401000(a1, v4) (назовем этот массив buf), может уместиться в три строки:

for (int i = 0; i < 9; i++)    
{
    V9[i] = buf[(V9[i] - 8 * (V9[i] >> 4)) % 16];
}

Ну вроде становится яснее. Теперь рассмотрим последнюю функцию. Открываем sub _401170(v9, v11, (int)v17) в псевдокоде, ожидаемо код малочитабельный, но, если немного подшаманить, у нас выходит довольно понятный цикл:

do
{
    v17[i * 3 + 0] = (v11[6] * v9[i * 3 + 2] % 7 + (v11[3] * v9[i * 3 + 1] % 7 + (v11[0] * v9[i * 3 + 0] % 7) % 7) % 7) % 7;

    v17[i * 3 + 1] = (v11[7] * v9[i * 3 + 2] % 7 + (v11[4] * v9[i * 3 + 1] % 7 + (v11[1] * v9[i * 3 + 0] % 7) % 7) % 7) % 7;

    v17[i * 3 + 2] = (v11[8] * v9[i * 3 + 2] % 7 + (v11[5] * v9[i * 3 + 1] % 7 + (v11[2] * v9[i * 3 + 0] % 7) % 7) % 7) % 7;

    i++;

} while (i < 3);

Отсюда видно, что даже если мы знаем, какой должна быть переменная v17, то нас это не спасает, так как идет хеширование с потерей информации. А теперь давай сделаем паузу и подведем промежуточные итоги. Известно: - значение переменной v17 из функции sub_401170; - mail и, следовательно, buf. Нужно найти серийник (вытащить из функции sub_401070); для этого необходимо знать еще валидные хеши от v9 и v11, являющиеся результатом работы функции sub_401070 и затем передающиеся в sub_401170. Наш план действий таков: 1. Из функции sub_401170 вытаскиваем хеши от v9 и v11. 2. Из функции sub_401070 вытаскиваем серийник.

Пишем кейген

Приступаем к первому пункту — из функции sub_401170 вытаскиваем хеши от v9 и v11. Как говорилось выше, хеширование идет с потерей информации, и просто провести все действия в обратном порядке не получится. Что же делать в таком случае? Капитан очевидность нам сразу же подскажет, что нужно брутить, и будет прав. Но напрашивается вопрос, для чего мы все это городили, если можно было брутить с самого начала? Дело в том, что если брутить совсем вслепую, то имеем: 10 цифр + 26 больших и столько же маленьких английских букв, итого 62 различных символа, 18 — длина серийника, 62^18 = 1,83 * 10^18. Конечно, это самый пессимистичный вариант, но, даже если время перебора сократится в 10 000 раз, нам не будет от этого легче. В этой ситуации нужно внимательно проанализировать зависимость v17 от v9 и v11. Я сделал так: отдельно реализовал функцию sub_401170(v9, v11, (int)v17) и в цикле под отладчиком менял значения переменных v9, v11 так, чтобы значение v17 подходило под маску {x,y,y,y,x,y,y,y,x} (главное — добиться такого рисунка, а дальше уже можно и перебрать, так, чтобы было {1,0,0,0,1,0,0,0,1}). После пары часов и литра кофе мне надоело хаотично менять значения, и я решил попробовать очевидный вариант — v9 и v17 присвоил вот такое значение: {1,0,0,0,1,0,0,0,1}. И, о чудо, это было то, что нужно! Если еще посидеть, то можно выяснить, что подходят также значения {0,1,0,1,0,0,0,0,1},{0,0,1,0,1,0,1,0,0}. При желании можно еще накопать коллизий, но мы остановимся на первом варианте. Итак, на данном этапе у нас вырисовывается алгоритм генерации серийного номера, представленный на рис. 5.

Рис. 5. Алгоритм проверки серийного ключа
Рис. 5. Алгоритм проверки серийного ключа

Перебираем значения v9 по маске {x,y,y,y,x,y,y,y,x}, на место x и y подставляем значения из первых 16 элементов массива buf. Почему? Вспоминая, как реализована функция sub_401070(v9), где хешируются v9 и v11, понимаем, что в v9 входит множество значений первых 16 элементов массива buf. То есть количество вариантов 16^2 = 256, с учетом того, сколько там коллизий, перебор пройдет значительно быстрее. Далее выполняем реализованную функцию sub_401170(v9, v9, (int)v17) (первая и вторая часть серийного номера равны), и если переменная v17 равна {1,0,0,0,1,0,0,0,1}, то хеш верный и ищем на его основе наш серийник.

Рис. 6. Алгоритм подбора переменной `v9`
Рис. 6. Алгоритм подбора переменной `v9`

После того как получаем хеш v9, нужно достать из него серийный номер. Делается это так:

  • совершаем обратную перестановку;
  • каждый элемент v9[i] сравнивается с первыми 16 элементами массива buff[];
  • если предыдущее условие выполняется, то подбираем такое нужное число
    for (int i = 0; i < 256; i++)
    {
        a = (i - (8 * (i / 16))) % 16;
    
        if (indexV9 == a)
        {
            // Проверка на печатаемые символы  
            if ((i >= 48 && i <= 57)||(i >= 65 && i <= 90)||(i >= 97 && i<=122))
            {
                resultX = i;
            }
        }
    }
    

Финишная прямая

Ну вот, собственно говоря, и все. Соединяем весь код и компилируем свой кейген. Если лень писать или нужно что-то подглядеть, то проект, написанный на шарпе, лежит на dvd.xakep.ru. Все оказалось не так сложно, как предполагалось вначале. Надо сказать, это было хорошее задание: на нем можно потренироваться в чистой защите, без траты времени на то, чтобы найти нужное место или dll, распаковать и так далее. Но в то же время и не оторванное от жизни, а показывающее, примерно как устроена защита.

DVD

Исходные коды получившегося генератора серийных номеров ждут тебя на dvd.xakep.ru

Если ты успешно освоил данное задание (а желательно еще и парочку похожих), то можно сказать, что основными приемами реверсинга ты овладел. Теперь из качественных преград могут попасться упакованные программы или нашпигованные антиотладочными приемами. Но если немного запастись терпением, кофе и чуть-чуть усердия, то и это тоже окажется по силам. Так что вперед! Желаю удачи в освоении искусства реверсинга, побольше терпения и интересных задач!

Отключаем ASLR

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

  1. Глобально отключить поддержку ASLR для всей системы. Для этого надо в реестре в ветке HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management создать ключ MoveImages (DWORD) и присвоить ему значение 0. После этого перезагрузить систему.
  2. Отключить рандомизацию адресного пространства только для исследуемого файла. Для этого надо будет в опциональном заголовке исполняемого файла изменить значение поля DllCharacteristics с 8140 на 8100. Сделать это можно, например, с помощью PETools (PETools -> Tools -> PE Editor (выбираем файл) -> Optional Header -> Dll Flags).

После этого можно будет спокойно исследовать файл под отладчиком.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    4 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии