R0 CREW

Разбор crackserial_linux

По просьбе @petrucho выкладываю здесь свое решение крэкми с занятия в МТУ 25.10.

1. Описание

Дан файл crackserial_linux. При запуске запрашивает имя юзера и серийный номер, в случае верного ввода выводит Good Job, в противном случае - Try Again. Необходимо разобрать алгоритм проверки вводимых данных

Для решения задачи будут использоваться:
  1. IDA 32-bit
    [INDENT]Для дизассемблирования[/INDENT]
  2. g++
    [INDENT]Для компиляции кейгена[/INDENT]

Поскольку это исполнемый 32-х битный файл, запустим IDA и разберем его.

2. Разбор

IDA определяет формат данного файла как исполняемый файл как 32х битный ELF.
Перед нами оказывается дизассемблированный код и найденные функции. Откроем окно со строками, обнаружим там уже известные значения User, Serial и тд и перейдем в функцию sub_8048A41, где они используются и начнем разбор.

После беглого осмотра становится понятно, что функция sub_8048A41 выполняет всю логику работы программы и осуществляет обработку данных и выход из нее. Для удобства переименуем ее в main.

Дале пойдем по порядку. Обратим внимание на метку loc_8048A61. По этому адресу у нас используется строка “User”, далее вызываются оператор >> (вывода потока) с параметрами cout и этой строкой, что означает вывод на экран слова User. После этого вызывается функция getline, в результате работы которой введенные нами данные помещаются в [esp+10h] (строка 8048A89). После этого введенная нами строка перемещается в eax и вызывается функция length, возвращающая ее длину, которая помещается в [esp+1ch]. После этого полученная длина сравнивается с 2 и 30 и если она не попадает в этот диапазон, то происходит перемещение на метку loc_8048A61 и вся процедура повторяется.

Из этого запутанного описания можно сделать один простой вывод - имя пользователя должно больше 2 и меньше 30 символов в длину.

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

N.B. В языке С++ - string - это класс, поэтому все функции работы со строками имеют подобный префикс - _zStl.

Обратим внимание на строку 0848ACA. В ней вызывается функция sub_8048CC1 (адрес 8048ABB), имеющая сигнатуру istream& operator>> (ios_base& (*pf)(ios_base&)) - где pf - это так называемый манипулятор, то есть функция занимающаяся не извлечением данных из потока, а его изменением. Далее, в этот поток отправляется введенный нами серийный номер. После этого вызывается конструктор копирования (8048AF2) и вызывается функция sub_80489CD, которая в качестве параметров берет ссылку на введеный нами username и некое число, полученное в результате ввода нами серийного номера и обработанное функцией 8048СС1. Работа функции sub_80489CD определяет исход работы программы.

Отсюда следует, что нам необходимо разобрать работу двух функций - sub_80489CD и sub_8048CC1.

3. Функция sub_8048CC1

Перейдем в эту функцию. Переименуем ее в strange_func

Как мы видим, она добавляет в стэк числа 74 (4Ah), 8 и указатель на наш аргумент и вызывает функцию sub_8048C6C. Далее, кладем в eax указатель на наш аргумент и выходим из функции. Раз наш аргумент используется - идем в функцию sub_8048C6C.

Как мы видим в строках 0848C72 - 0848C78 и 0848CBC, функция берет указатель на наш аргумент, берет 12й байт и начинает производить с ним манипуляции. Однако, в конце работы нам опять возвращается по указателю значение [ebp + var_C], из чего я сделал вывод о том, что функция не влияет на вводимый нами результат. N.B. Вывод довольно поспешный, но я немного схитрил и запустил все в динамике, после чего убедился, что вводимый нами серийный номер никаким образом не меняется после работы этой функции.

Таким образом остается только одна функция

4. Функция sub_80489CD

На скриншоте уже переименованы переменный и описаны шаги работы алгоритма. На входе функция получает наш юзернейм и серийный номер. Далее, заводятся локальные переменные индекс, некое значение magic и значение для хранения промежуточного результата. После чего magic присваивается 0x4E504700, index/tmp_result - 0. Далее, по адресу 8048A21 происходит сравнение величины текущего значения индекса и длины строки-аргумента. Если индекс меньше - прыгаем на 80489EA. По этому адресу происходит взятие i-го элемента строки, xor со значением magic, умножение на 8 и вычитание (т.о. просто умножение на 7). Полученный результат записывается в magic, а потом прибавляется в tmp_result.

Далее, когда проверка на длину не проходит, то есть когда размер индекса равен длине строки, происходит проверка условия равенства второго аргумента(нашего серийного номера) и полученного tmp_result. Если они равны, то далее в программе происходит прыжок на ветку кода, выводящую Good Job.

Таким образом можно написать простой keygen, генерирующий правильный серийный номер по заданном имени пользователя:

#include <string>
#include <inttypes.h>
#include <iostream>
#include <stdint.h>
#include <stdio.h>


void secretfunc(std::string* a1);

using namespace std;



int main() 
{
    string username;
    for (int i=0; i < 2 || i > 30  ; i=username.length())
    {
        cout<<"User: ";
        cin>>username;
    }
    secretfunc(&username); 
    return 0;
}




void secretfunc(std::string* a1)
{
    uint32_t tmp_result=0;
    uint32_t MAGIC = 0x4E504700;

    for (int i=0; (*a1).length()>i;i++)
    {
        MAGIC = 7*(((*a1).at(i))^MAGIC);
        tmp_result+=MAGIC;
    }
    printf("Serial for user %s is %x\n",(*a1).c_str(),tmp_result);
    return;
}

Спасибо за внимание!

P.S. Разбором и подробным описанием занимаюсь впервые, огромная просьба, подскажите, на какие моменты следует обратить внимание, может, что то было сделано неверно, хотелось бы улучшать качество работы.