Главная | Программы | Статьи | Заработай 100$ и более

Гостевая книга

 
Статьи: Исследование CDCheck 3.1.5.0 14.07.2005

 

Приветствую всех читателей этого документа. В этот раз меня заинтересовала программа CDCheck 3.1.5.0. Интерес вызван обыденной проблемой – после записи болванки файлы не читаются. Полез искать программы, которые могли бы проверить качество записанной информации.После поисков в сети и прочтения некоторых мнений на софт-сайтах решил попробовать именно эту программу. Итак, что именно заинтересовало – не очень большой размер, удобство использования (после пары минут боёв с GUI:), ну и сам способ проверки внушает доверие – сравнение хэшей исходных и записанных файлов. Теперь посмотрим, как выглядит способ защиты этой программы :) По поводу тутора - расписал всё очень подробно (как мне кажется), даже исходник прилепил. Писал в ужасную жару, так что не серчайте, если что-нибудь непонятно, путанно или заумно написано:)

Инструменты:
OllyDebug
DeDe
PEiD
UPX
MicroSoft Visual Studio (я использовал 7)

Ну что, поехали, как говорил один хороший человек, но почему-то полетел :)
Сначала осмотримся... Для начала смотрим, запакована ли программа и если да, то чем. PEiD уверяет, что UPX. Верим на слово и распаковываем самим UPX’ом. Смотрим компилятор – Borland C++. Теперь берём в руки Olly и начинаем копать… Запускаем и начинаем искать, с чего начать, отправную, так сказать, точку. Жмём кнопочку “О программе”. Всего одно поле ввода для регистрации. Уж и не вспомнить, когда последний раз я видел программу с одним окошком. А может за кажущейся простотой кроется что-нить этакое - привязка к железу, к винде?! Ладно, что-то я тучи сильно сгущаю. Лучше один раз глянуть, чем много гадать. Ищем вызовы привычных функций, но кроме одного GeTWindowText ничего нет. Так как компилер борландовский, пихаем экзешник в DeDe, ищем форму about и реакцию на клик по кнопке “Регистрировать”. Находим, что вызывается метод по адресу: 0041899C 55 push ebp Видим там ещё несколько интересных строк

004189DA mov eax, [ebx+$02EC]
* Reference to: controls.TControl.GetText(TControl):TCaption;
004189E0 call 0050F210

Читаем введённый текст

004189EA mov eax, [eax]
* Reference to: sysutils.StrToIntDef(S:
004189EC call 005360BC

И приводим его к типу int

Ставим бряк и начинаем следить…
Итак, с функциями, описанными выше всё понятно, смотрим дальше:

00418A05 E8867A0700 call 00490490

Первая функция, следующая сразу за считыванием номера. Может здесь будет что-то интересное…
Нет, здесь номер заносится в реестр, так что продолжаем искать…
Следом идёт 3 ещё менее интересных вызова, а вот далее вызов и следом проверка

00418A3D |.CALL unpacked.004339E0

Заходим… И в DeDe тоже… Опять несколько интересных строчек:

00433ADA 68F7395700 push $005739F7
* Reference to: GetPrivateProfileStringA()
00433ADF call 0055CBF6

и ещё ниже

00433B3A 8B00 mov eax, [eax]
* Reference to: sysutils.StrToIntDef(S:
00433B3C call 005360BC

Давайте здесь остановимся (надеюсь Вы ввели хоть какой-нибудь рег. номер :) Ставим бряк, отпускаем программу и… пролетаем. Странно. Перед этим вызовом идёт вызов функции GetPrivateProfileStringA. Она считывает из ini файла параметр ***. Как можно заметить, параметр здесь говорит сам за себя - "RegCode". Ладно, смотрим в оле ниже.

00433B54 |> MOV ECX,DWORD PTR SS:[EBP-44]

Вот этот значок (“>”) говорит, что сюда направлен прыжок. Прыжок идёт сверху, пропуская приличный кусок кода, в том числе и чтение данных из файла настройки. Логически рассуждая ( + немного экспереминетов ;) можно предположить, что мы находимся в функции проверки номера, причём номер достаётся и из реестра, и из ini файла, правда не сразу отовсюду, а в зависимости от ситуации (сначала пытается считать из реестра, если записи о номере нет, пытается из ini-файла). Сама функция не получает параметров. Странно. Немного помозговав понимаем, что работает это следующим образом – когда мы вводим номер он записывается в реестр (это мы отметили в самом начале, сразу после преобразованию к типу int), а метод проверки считывает его из registry. Не очень удобный способ конечно, но это не наши проблемы. Главное, что из этого следует - патчить (если придётся) нам будет не напряжно – всего-то в одном месте :) Продолжим осматриваться...

Любопытнейшее место:

00433B54 |> MOV ECX,DWORD PTR SS:[EBP-44]
00433B57 |. PUSH ECX ; /Arg1
00433B58 |. CALL unpacked.0049065C ; \unpacked.0049065C

Вызов функции, параметр которой наш номер. Зайдём :)

00490665 |. TEST ESI,ESI ; Проверяем, есть ли вообще номер
00490667 |. JG SHORT unpacked.0049066D ; если есть, работаем
00490669 |. XOR EAX,EAX ; иначе обнулимся
0049066B |. JMP SHORT unpacked.004906A1 ; и на выход
0049066D |> MOV EBX,2 ; цикл начинается с 2
00490672 |. JMP SHORT unpacked.00490682
00490674 |> /MOV EAX,ESI ; введённый номер
00490676 |. |CDQ
00490677 |. |IDIV EBX ; делим на EBX (счётчик цикла)
00490679 |. |TEST EDX,EDX ; Проверяем остаток
0049067B 75 JNZ SHORT unpacked.00490681 ; Если не ноль, продолжаем
0049067D |. |XOR EAX,EAX ; Иначе обнуляемся
0049067F JMP SHORT unpacked.004906A1 ; И валим
00490681 |> |INC EBX ; Прибавим EBX
00490682 |> MOV DWORD PTR SS:[EBP-4],ESI ; В стек номер
00490685 |. |FILD DWORD PTR SS:[EBP-4] ; Из стека в регистр FPU
00490688 |. |ADD ESP,-8 ; /
0049068B |. |FSTP QWORD PTR SS:[ESP] ; |Arg1 (8-byte)
0049068E |. |CALL unpacked.005492EC ; \Извлечь корень помещённого числа
00490693 |. |ADD ESP,8
00490696 |. |CALL unpacked.00547EA0 ; Корень в EAX
0049069B |. |CMP EBX,EAX ; Сравниваем корень и счётчик
0049069D |.^\JLE SHORT unpacked.00490674

Вроде довольно подробно прокомментировал, теперь надо разобраться, что же здесь делается? Это проверка нашего номера. Бросается в глаза, что цикл начинается не с 0, и даже не 1, а с 2, и при этом увеличивается. Цель – видимо избежать деления на единицу. Зачем? Чтобы не выйти сразу, т.к. условие для выхода – деление без остатка, а на единицу делится любое число. Алгоритм реализован как-то очень корявенько – зачем же каждый раз извлекать корень из номера (ну не удержался я от критики :). Но это, повторюсь, не наши проблемы. Теперь, собсно, сам алгоритм – из номера извлекается корень – это предел счётчика. Введённый номер делится на значение счётчика, если нацело – выходим с ошибкой. Остроумно. Таким свойствами обладают лишь простые числа (точнее такие числа называются простыми (они делятся лишь на себя и единицу)). Значит наш номер должен являться простым числом. Это, например, число 13. Пишем его и смотрим, что будет... Да, мы благополучно дошли до конца и в AL у нас теперь 1. Смотрим дальше:

00433B66 |. >CMP DWORD PTR SS:[EBP-44],0F4240
00433B6D |. JB unpacked.00433C19

Итак, номер должен быть больше F4240 (1000000). Ради любопытства подменим флажок C и посмотрим, что нас ждёт дальше

00433B73 |. DEC DWORD PTR SS:[EBP-44]

Номер уменьшается на единицу.

00433B76 |. MOV ECX,3DF
00433B7B |. MOV EAX,DWORD PTR SS:[EBP-44]
00433B7E |. XOR EDX,EDX
00433B80 |. DIV ECX
00433B82 |. TEST EDX,EDX
00433B84 |. JNZ SHORT unpacked.00433B99

Как можно заметить теперь номер делят на константу 3DF (991), дальше на 3E5, потом на 3F1, следом на 3F5, и наконец на 2665. Попробуйте изменяя флажки посмотреть, что будет, если наш номер будет делиться на какое-нибудь из этих чисел без остатка... Поздравления и... тип регистрации. Код типа лицензии, кстати, кладётся перед проверкой в EAX. Вот мы и осмотрелись, итог:
1) Номер должен быть простым числом
2) Номер должен быть больше F4240 (1000000)
3) Номер – 1 должен делится без остатка на одну из констант (желательно на 2665 – корпоративная лицензия :)
4) Ну и номер не должен выходить за пределы типа int


Активные действия
Итак, что делать вроде понятно, но вот как – я уже не соображаю (за окном +28,камень 48С, так ещё и кот рядом с вентиляционными отверстиями прикемарил, охлаждается в выдуваемом кулером воздухе – не отогнать :). Пока олю, DeDe и прочий инструментарий можно закрывать. Теперь надо думать, как найти номерок. Первое, что приходит в голову:
чтобы получить номер надо проделать обратные операции – то есть взять число, умножить его на 2665h и прибавить единицу. Остаётся одна проблема – получившееся число должно быть простым, а это уже сложнее. Кстати, вспомнил одну вещь – человеку, который получит простое число, длиной более не_помню_сольких (вроде в десятках измеряется) знаков вручат не_помню_сколько_много долларов :) но чё-то в районе миллиона. Я это к чему – нет алгоритма получения простых чисел, нет чёткой зависимости, последовательности (как это ещё назвать), а у нас именно такая задача и стоит, да ёще чтоб при вычитании 1 оно делилось без остатка. Мне видится один путь – написать программку, которая будет перебирать числа в поисках нужного. Итак, программа должна сделать следующее – создать массив чисел, полученных обратным алгоритмом, а потом удостовериться, что хоть одно из них простое. Я реализовал это следующим способом:
1) Задаётся число, с которого начинается поиск ключа (обязательно должно делиться на 9829)
2) Задаётся кол-во раз, сколько мы к нему будет плюсовать 9829
3) И проверяется, простое число или нет
В конце приложен сорец на С++


Послесловие
После геморрических усилий мы разобрали алгоритм, сделали ключеделалку и попутно вспомнили математику :) . Кстати, можно было обойтись и патчем. Решение, что именно патчить оставляется Вам как творческое задание для самостоятельной работы :) На счёт генератора - необязательно создавать массив (тем более толку от него в этом коде нулл), можно просто в цикле увеличивать номер на константу и сразу его проверять. Но тут вдруг получится, что валидных ключей в ближайшем диапозоне (то есть до предела типа int :) нету, и получится лёгкое подтормаживание (а может и не лёгкое :) , это если nSize убрать нах), плюс я вообще-то планировал выводить ВСЕ найденные ключи в этом диапозоне, но передумал (уж очень непостояяный я человек - мыслей слишком много :).
Короче, читайте, изучайте, ломайте (есесно всё это только в образовательных целях) . Спасибо за внимание и до встречи.
Все вопросы, замечания и пожелания в комменты. Если что-то очень важное приспичит - мыло тоже присутствует.

Автор: Ra$cal
Mail: Rascalspb [dog] mail [dot] ru
Дата: 14.07.2005

-------->Cut<--------

#include
#include
using namespace std;

// Переменные
const long Type = 9829;
long nStart = 0;
long nSize = 0;
long* nArray = NULL;

// Функции
void CreateArray ( );
long TestArray ( );
bool IsSimple ( long _Number );
void Exit ( );

void main ()
{
char Answer;
do
{
cout << "1.Generate key\n";
cout << "0.Exit\n";
cin >> Answer;

switch (Answer)
{
case '1': CreateArray ( ); break;
case '0': Exit ( ); break;
}


} while(Answer !='0');

}

void CreateArray ()
{
if(nArray!=NULL)
delete [] nArray;

cout << "\nCDCheck keygen\nEnter start number ->";
cin >> nStart;
cout << "\nEnter upper limit ->";
cin >> nSize;

nArray = new long [nSize];

for(long i=0; i < nSize; i++)
nArray[i]= nStart + Type * i + 1;

long Key = TestArray ();

if(Key < 0)
cout << "\nKey not found :( Try increase search range\n";
else
cout << "\nCongratulations! License number is " << Key << endl;

}

long TestArray ()
{
for(long i = 0; i if(IsSimple(nArray[i]))
return nArray[i];
return -1;
}

bool IsSimple (long _Number)
{
long SQRT_NUMBER = sqrt ((long double)_Number);
for (long i = 2; i < SQRT_NUMBER; i++)
if(!(_Number%i))
return false;
return true;
}

void Exit ()
{
if(nArray!=NULL)
delete [] nArray;
exit(0);

}

-------->Cut<--------