Инструменты: отдадчик OllyDebugger v.1.1, дизассемблер IDA Pro v.5.0,
утилита
PEiD, упаковщик/распаковшик UPX v.3.Прежде всего, загрузив подопытного в
PEiD, убеждаемся, что прога упакована
UPX'ом. Для комфортной работы под дизассемблером лучше будет ее
распаковать,
благо это несложно - можно скачать распаковщик с сайта проекта, а можно
немного
порыться в папках дистрибутива самого PSY и обнаружить готовый к
употреблению
распаковщик в фолдере Compile (спасибо разработчикам).Поскольку мы не
собираемся патчить исполняемый файл, его лучше скопировать
для дизассемблерного анализа в другое место и распаковать там (upx -d
PSY.exe).
Повторно запросив PEiD по поводу соображений насчет известного персонажа
узнаем,
что он написан на Delphi 6/7 (скорее всего 7), что исключает анализ с
помощью
DeDe, зато открывает оперативный простор для использования
дизассемблера, для
удобства и ускорения работы добавим к нему отладчик.Использование именно
IDA не принципиально, но предпочтительно, хотя я и не
использовал механизм FLIRT для анализа в полном объеме (в смысле
ограничился
только сигнатурами Borland C++ Compiler, входящими в поставку Иды по
дефолту,
сигнатуры Delphi 7 было лень искать). Я не слишком большой специалист по
Delphi,
но интуиция подсказала, что окно регистрации выводится какой-либо
ООП-процедурой
из разряда CreateWindow(Form/Dialog...).По методу "тупого перебора"
перебираем функции в дизассемблере с похожими
названиями и ставим точки останова в отладчике по их адресам. Только
прежде, чем
этим заниматься, нужно посмотреть адрес точки входа распакованной
программы в
дизассемблере (00638с98) и установить на нее аппаратную точку останова в
отладчике, а эксперименты с "тоглами" проводить уже в распакованной
программе.
Буквально через 30-40 минут подобных манипуляций мы получим срабатывание
программной ВР по адресу 004A39E4, который соответствует
Forms::TApplication::CreateForm. Подсмотрев адрес возврата мы получим
функцию
основного модуля, которая показывает нам регистрационную форму -
sub_638488, для
удобства обзовем ее ViewRegistrationForm. По перекрестной ссылке видно,
что
вызывается эта функция только из стартовой процедуры, причем вызову
предшествует
проверка равенства некоторой глобальной переменной (назовем ее
RegistrationFlag)
нулю:
CODE:0063921A cmp ds:RegistrationFlag, 0 CODE:00639221 jnz short loc_63922D CODE:00639223 call ViewRegistrationForm
Инвертировав (из любопытства) флаг перед переходом в отладчике, мы увидим
главное окно программы вместо скучной формы регистрации, что укрепит нас в идее
о правильности выбранного пути, но понажимав на кнопки и пункты меню Психа
познакомимся с птицей обломинго, которая радостно сообщит, что писать генератор
кода все-таки придется, видимо проверка подлинности выполняется по всему телу
программы, что исключает (или сильно затрудняет) кряк бит-хаком.
Анализируем алгоритм преобразования кода при проверке:
CODE:00638DAD push offset aMd33333a CODE:00638DB2 lea eax, [ebp+lpNameLocal] CODE:00638DB5 push eax CODE:00638DB6 mov ecx, offset asc_63953C CODE:00638DBB mov edx, offset lpTempBuff CODE:00638DC0 mov eax, ebx CODE:00638DC2 mov esi, [eax] CODE:00638DC4 call dword ptr [esi] этот блок кода CODE:00638DC6 mov edx, [ebp+lpNameLocal] CODE:00638DC9 mov eax, ds:lpNameGlobal CODE:00638DCE call System::__linkproc__ LStrAsg(void *,void *) распихивает сер номер CODE:00638DD3 push offset a123456 CODE:00638DD8 lea eax, [ebp+lpCodeLocal] комп код и рег код по CODE:00638DDB push eax CODE:00638DDC mov ecx, offset Svrconst::_16422 CODE:00638DE1 mov edx, offset lpTempBuff CODE:00638DE6 mov eax, ebx буфферам CODE:00638DE8 mov esi, [eax] CODE:00638DEA call dword ptr [esi] CODE:00638DEC mov edx, [ebp+lpCodeLocal] CODE:00638DEF mov eax, ds:lpCodeGlobal CODE:00638DF4 call System::__linkproc__ LStrAsg(void *,void *) CODE:00638DF9 push offset a264832123910 CODE:00638DFE lea eax, [ebp+lpACodeLocal] CODE:00638E01 push eax CODE:00638E02 mov ecx, offset aAcode_4 CODE:00638E07 mov edx, offset lpTempBuff CODE:00638E0C mov eax, ebx CODE:00638E0E mov esi, [eax] CODE:00638E10 call dword ptr [esi] CODE:00638E12 mov edx, [ebp+lpACodeLocal] CODE:00638E15 mov eax, ds:lpACodeGlobal CODE:00638E1A call System::__linkproc__ LStrAsg(void *,void *)
Проследив под отладчиком значение ESI в момент вызовов узнаем, что эти строки
получает функция по адресу 00447D1C при помощи системного вызова
GetPrivateProfileStringA из файла Info.ini в главной папке программы, а строки,
передаваемые при вызове (вроде 264832-1239-10), это соответственно значения по
дефолту, что можно выяснить в MSDN.
Интересующая нас часть Info.ini выглядит так:
[PEx] InstallDir=C:\Program Files\Psychometric Expert PathDll=C:\Program Files\Psychometric Expert\Sysmod\MetodParams DefaultBase=C:\Program Files\Psychometric Expert\DData\DefaultData.pfd StartupWindow=1 RName=$єжќс ЊЄ fш % RData=26330 RTime=63125 Name=MD14545F Code=C3831715 ACode=C38317-151A-555974
Идем дальше (комментарии и имена меток мои):
CODE:00639078 mov ebx, eax CODE:0063907A mov eax, ds:lpCodeGlobal CODE:0063907F mov eax, [eax] CODE:00639081 call System::__linkproc__ DynArrayLength(void) этот блок кода проверяет длины CODE:00639086 imul ebx, eax строк комп/рег кодов и сер номера CODE:00639089 mov eax, ds:lpACodeGlobal CODE:0063908E mov eax, [eax] CODE:00639090 call System::__linkproc__ DynArrayLength(void) CODE:00639095 imul ebx, eax CODE:00639098 test ebx, ebx CODE:0063909A jle CheckRegistration CODE:006390A0 mov eax, ds:lpACodeGlobal CODE:006390A5 mov eax, [eax] CODE:006390A7 call System::__linkproc__ DynArrayLength(void) CODE:006390AC cmp eax, 0Ah CODE:006390AF jle CheckRegistration CODE:006390B5 mov eax, ds:lpNameGlobal CODE:006390BA mov eax, [eax] CODE:006390BC cmp byte ptr [eax], 4Dh CODE:006390BF jnz short loc_6390E1
Как видно критерии строк следующие: во-первых ненулевые, во-вторых длина рег
кода не менее 10 символов, в третьих проверяется значение первого символа
серийника (если он не "М", то, как будет видно далее, меняется алгоритм
получения конфигурации компьютера). Этот блок кода получает ID процессора при
помощи инструкции CPUID и складывает результат с константой.
CODE:006390C1 call GetRawCompCode CODE:006390C6 add eax, 3976Bh
Поскольку внутри себя процедура содержит большое количество незначимого кода,
вот мой вариант этой функции (она же первая функция в проекте генерации
рег кода):
INT GetRawCode(VOID) { int res _asm { mov eax,1 cpuid mov res,eax mov eax,3 cpuid add res,edx add res,0x3976b } return res }
Готовый результат приведен мной исключительно из экономии места, анализ самой
функции не представляет проблемы даже для новичка, тем более, что защитный код
уже локализован и анализировать нужно немного. В случае же равенства первого
кода серийника символу "М", RawCompCode будет равен параметру
lpVolumeSerialNumber, возвращаемому GetVolumeInformationA. Воспроизведение
этого, думаю, также не встретит каких-либо серьезных препятствий.
Далее для строк с регистрационным кодом и серийником вызывается функция,
которую я назвал ChangeSymbols, что понятно из ее алгоритма. Вот ее вызов:
CODE:0063911B mov edx, ds:lpACodeGlobal CODE:00639121 mov eax, ds:lpNameGlobal CODE:00639126 mov eax, [eax] CODE:00639128 call ChangeSymbols
А вот часть ее кода:
CODE:0052C734 mov edx, [ebx] CODE:0052C736 mov dl, [edx] CODE:0052C738 mov [eax], dl CODE:0052C73A mov eax, ebx CODE:0052C73C call System::_16779 CODE:0052C741 mov edx, [ebx] CODE:0052C743 mov dl, [edx+4] CODE:0052C746 mov [eax], dl CODE:0052C748 mov eax, ebx CODE:0052C74A call System::_16779 CODE:0052C74F mov edx, [ebp+var_8] CODE:0052C752 mov dl, [edx] CODE:0052C754 mov [eax+4], dl CODE:0052C757 lea eax, [ebp+var_8] CODE:0052C75A call System::_16779
Не нужно быть ученым, чтобы понять, что этот блок кода меняет местами символы
0 и 4 в какой-то строке (в данном случае в строке рег кода). Полная
реконструкция алгоритма для рег кода выглядит так:
VOID ChangheRegCodeSymb(LPSTR lpszBuff) { char t t=lpszBuff[0] lpszBuff[0]=lpszBuff[4] lpszBuff[4]=t t=lpszBuff[1] lpszBuff[1]=lpszBuff[3] lpszBuff[3]=t }
С заменой символов в серийнике все не так очевидно, но тоже не слишком
усложнено:
CODE:0052C784 mov eax, [ebp+var_4] CODE:0052C787 mov al, [eax+7] CODE:0052C78A cmp al, 41h CODE:0052C78C jnz short loc_52C7ED ......................................................... CODE:0052C7ED cmp al, 42h CODE:0052C7EF jnz short loc_52C850 ......................................................... ......................................................... CODE:0052C910 loc_52C910: ; CODE XREF: ChangeSymbols+1BA j CODE:0052C910 cmp al, 45h CODE:0052C912 jz short loc_52C916 CODE:0052C914 cmp al, 46h CODE:0052C916 CODE:0052C916 loc_52C916:
Замена символов в серийнике происходит в переключателе, который выбирает
алгоритм замены исходя из значения последнего символа. Полная реконструкция этой
функции:
VOID ChangeCompCodeSym(LPSTR lpszStr) { char a=lpszStr[7] char t switch(a) { case 0x41: t=lpszStr[7] lpszStr[7]=lpszStr[2] lpszStr[2]=t t=lpszStr[8] lpszStr[8]=lpszStr[10] lpszStr[10]=t break
case 0x42: t=lpszStr[2] lpszStr[2]=lpszStr[9] lpszStr[9]=t t=lpszStr[5] lpszStr[5]=lpszStr[7] lpszStr[7]=t break
case 0x43: t=lpszStr[5] lpszStr[5]=lpszStr[2] lpszStr[2]=t t=lpszStr[7] lpszStr[7]=lpszStr[10] lpszStr[10]=lpszStr[7] break
case 0x44: t=lpszStr[7] lpszStr[7]=lpszStr[10] lpszStr[10]=t t=lpszStr[5] lpszStr[5]=lpszStr[9] lpszStr[9]=t break
default: break } }
В моем случае, кстати, работает только дефолтный вариант, что обусловлено
комплектацией программы. У них их, видите ли, аж пять вариантов поставки (как у
Ford Mondeo), алгоритм смены символов в рег коде, кстати, также зависит от
комплектации.
Следующий блок кода манипулирует уже непосредственно с рег кодом:
CODE:00639132 mov eax, ds:lpACodeGlobal CODE:00639137 mov eax, [eax] CODE:00639139 mov ecx, 2 CODE:0063913E mov edx, 0Ah CODE:00639143 call System::__linkproc__ LStrCopy(void) CODE:00639148 mov eax, ds:lpACodeGlobal CODE:0063914D push eax CODE:0063914E mov eax, ds:lpACodeGlobal CODE:00639153 mov eax, [eax] CODE:00639155 mov ecx, 9 CODE:0063915A mov edx, 1 CODE:0063915F call System::__linkproc__ LStrCopy(void) CODE:00639164 mov eax, ds:lpACodeGlobal CODE:00639169 mov ecx, 1 CODE:0063916E mov edx, 7 CODE:00639173 call System::__linkproc__ LStrDelete(void)
А именно - вырезает из него первые 9 символов и удаляет из них седьмой
(видимо на этом месте ожидается незначимый символ, скорее всего дефис). Таким
образом, предварительный формат рег кода выглядит так:
хххххх-хх.......
где <x....>- символы первого поля рег кода, при выделении поля Х, символы в
нем меняются в зависимости от комплектации программы.
Далее мы видим следующую функцию (я назвал ее SerialNumHash), основная часть
кода которой выглядит так:
CODE:0063833C repeat: CODE:0063833C lea eax, [ebp+lpMulBuff] CODE:0063833F mov edx, offset dword_6383D8 CODE:00638344 call System::__linkproc__ LStrLAsg(void *,void *) CODE:00638349 mov ebx, esi CODE:0063834B test ebx, ebx CODE:0063834D jle short mul CODE:0063834F CODE:0063834F loc_63834F: CODE:0063834F lea eax, [ebp+lpMulBuff] CODE:00638352 mov edx, offset dword_6383E4 CODE:00638357 call System::__linkproc__ LStrCat(void) CODE:0063835C dec ebx CODE:0063835D jnz short loc_63834F CODE:0063835F CODE:0063835F mul: CODE:0063835F mov eax, [ebp+lpMulBuff] CODE:00638362 call Sysutils::StrToInt64(System::AnsiString) CODE:00638367 push edx CODE:00638368 push eax CODE:00638369 mov eax, [ebp+lpSerialNumLocal] CODE:0063836C mov al, [eax+esi-1] CODE:00638370 and eax, 0FFh CODE:00638375 xor edx, edx CODE:00638377 call System::__linkproc__ _llmul(void) CODE:0063837C add eax, [ebp+MinorPartRes] CODE:0063837F adc edx, [ebp+MajorPartRes] CODE:00638382 mov [ebp+MinorPartRes], eax CODE:00638385 mov [ebp+MajorPartRes], edx CODE:00638388 inc esi CODE:00638389 dec edi CODE:0063838A jnz short repeat
что в переводе на С выглядит так:
INT GetCompCodeHash(LPSTR lpszStr) { INT64 temp=1 INT64 res=0 INT64 work=0 int result, t
CHAR c[4]={0,0,0,0} LPSTR ch=(LPSTR)c int count=strlen(lpszStr) if(count>0x11)count=0x11
for(int i=0 { ch[0]=lpszStr[i] t=(int)ch[0] work=(INT64)t temp=temp*10 res=res+(work*temp) }
if(res>0xffffffff) { result=(int)(res-0xf4c11db7) } else { result=(int)res }
return result
}
Хэшем все это названо, разумеется, для краткости. Алгоритм полностью обратим.
Ну и наконец то, для чего это все делалось:
CODE:00639178 mov eax, ds:lpNameGlobal CODE:0063917D mov eax, [eax] CODE:0063917F call SerialNumHash CODE:00639184 mov ebx, eax ........................ CODE:00639194 mov ecx, ds:lpACodeGlobal CODE:0063919A mov ecx, [ecx] CODE:0063919C lea eax, [ebp+iRegCode] CODE:0063919F mov edx, offset asc_639714 CODE:006391A4 call System::__linkproc__ LStrCat3(void) CODE:006391A9 mov eax, [ebp+iRegCode] CODE:006391AC call Sysutils::StrToInt(System::AnsiString) CODE:006391B1 xor eax, ds:RawCompCode CODE:006391B7 xor ebx, eax
т.е. банально: res=iRegCode^RawCode^CompCodehash;
Далее все это элементарно складывается потетраэдно (ну разумеется не без
дельфийских наворотов со строково-целочисленными преобразованиями):
CODE:006391CB lea ecx, [ebp+lpHexXorString] CODE:006391CE mov edx, 8 CODE:006391D3 mov eax, ebx CODE:006391D5 call Sysutils::IntToHex(int,int) CODE:006391DA mov eax, [ebp+lpHexXorString] CODE:006391DD call CheckSummCalc
Для краткости, функция CheckSummCalc выглядит так:
int t=iRegCode^RawCode^CompCodehash; int sum=0;
for(int i=0;i<8;i++) { sum=sum+((t>>(i*4))&0xf); }
А вот тут из результата и получается строка, которая и сравнивается с
эталонной, взятой из рег кода, что приводит к "взводу" флага регистрации:
CODE:006391E2 lea ecx, [ebp+lpCalcChekStr] CODE:006391E5 mov edx, 2 CODE:006391EA call Sysutils::IntToHex(int,int) CODE:006391EF mov eax, [ebp+lpCalcChekStr] CODE:006391F2 mov edx, ds:lpEtalonCheckStr CODE:006391F8 call System::__linkproc__ LStrCmp(void) CODE:006391FD jnz short RestoreSEH CODE:006391FF mov ds:RegistrationFlag, 1
Таким образом, проследив откуда берется эталонная строка:
CODE:0063912D push offset lpEtalonCheckStr CODE:00639132 mov eax, ds:lpACodeGlobal CODE:00639137 mov eax, [eax] CODE:00639139 mov ecx, 2 CODE:0063913E mov edx, 0Ah CODE:00639143 call System::__linkproc__ LStrCopy(void)
получаем следующий формат рег кода:
хххххх-ххуу-.....
где х-поле - случайное 16-ричное число в строковом представлении из которого
вычисляется у-поле (двухзначное 16-ричное число в строковом представлении).
Алгоритм его генерации вполне очевиден (достаточно использовать функции,
описанные выше, при вычислении поля у).
Сгенерировав регистрационный код по описанной схеме, подставив вместо точек
пару случайных символов (например10), мы, скормим все это форме Психа и запустим
его. Он будет прекрасно пахать, откликаясь на все наши нажатия кнопок и пунктов
меню, предоставляя полный доступ к своему основному функционалу, но закрыв
его и перезагрузив мы снова увидим наглое окно с требованием регистрации. Вот
незадача...
|