По просьбе @petrucho выкладываю здесь свое решение крэкми с занятия в МТУ 25.10.
1. Описание
Дан файл crackserial_linux. При запуске запрашивает имя юзера и серийный номер, в случае верного ввода выводит Good Job, в противном случае - Try Again. Необходимо разобрать алгоритм проверки вводимых данных
Для решения задачи будут использоваться:- IDA 32-bit
Для дизассемблирования
- g++
Для компиляции кейгена
Поскольку это исполнемый 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, генерирующий правильный серийный номер по заданном имени пользователя:
Code:
#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. Разбором и подробным описанием занимаюсь впервые, огромная просьба, подскажите, на какие моменты следует обратить внимание, может, что то было сделано неверно, хотелось бы улучшать качество работы.