┌──┌─┐┌──
──┘├─┘──┘ Presents
┐ ┌┐┐┌─┤ VMag, Issue 3, 1 January 1999
└─┘┘ ┘└─┘ ─────────────────────────────
╔════════════════════════════╗
║ Version: 1.0 ║
║ Release: 27-Nov-1998y ║
║ Created by: Hard Wisdom ║
╚════════════════════════════╝
─[27-Nov-1998y]─[v1.0]─────────────────────────────────────────────────────────
Первоначально созданный файл (по совету Psychomancer'a)
───────────────────────────────────────────────────────────────────────────────
Некоторые методы и особенности процесса анализа ПО
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[] Содержание
~~~~~~~~~~~~~
[0] Преамбула..............................................................___
[1] Введение в анализ программного обеспечения.............................___
[2] Некоторые методы проведения(противодействия) анализа(у) ПО.............___
[2.1] Введение в язык IDC программы IDA-Pro v3.7..........................___
[2.2] Ключеделы-ключеделы... или как проводить сверку.....................___
[2.3] Что есть код, а что данные, и где есть кто?.........................___
[Z] Заключение.............................................................___
[0] Преамбула
~~~~~~~~~~~~~
Итак, здесь вы сможете прочитать некоторое количество бессистемно
изложенной информации посвященной теме анализа ПО (некоторые называют это
Reverse-Engineering, можете использовать тот термин, который вам нравится
больше всего). Тем, у кого будут чесаться руки, в предвкушении написания ну,
например, новой защиты для некоторой программы, хочется заметить, что лучший
метод уже предложен: GNU's General Public License
[1] Виды анализа программного обеспечения (ПО)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Достаточно часто возникают проблемы поиска недостающей информации о
функционировании различного программного обеспечения. Не отрицая безусловно
важной роли документации в данном вопросе, хочется отметить большую полезность
и уместность метода Reverse-Engineering, который позволяет во всех спорных
случаях внести ясность в решаемый вопрос. Я не стану приводить всех аргументов
за и против или затрагивать моральные аспекты, оставим это неудачникам. Итак:
Можно говорить о 2х обширных группах методов: статические и динамические.
Под статическим методом понимается получение информации о программе анализом
ее кода (исходного, бинарного и т.п.) При этом все сводится, как правило, к
формулированию сложных запросов поиска требуемой информации. Например:
поиском/заменой в ДОС программе фрагмента "Out 61h,Al" на "Nop; Nop" можно
"отключить" надоедливое звучание PC Speaker (контекст команды Out 61h,Al
должен определять человек и человек должен принимать решения о проведении или
отказе от модификации). Но что будет, если звук выводится системным вызовом:
Mov Ax,0E07h; Int 10h ? Безусловно, необходимо будет выдать второй запрос на
поиск и т.п. Динамический анализ - осуществляется анализ потоков выполнения в
программе, а так же изменение входных/выходных данных (регистры, память и
т.п.). Безусловно, во всех случаях необходима некоторая минимальная информация
об исследуемой программе: для бинарного кода - целевой процессор и формат
запускаемого файла + операционное окружение; для исходных текстов - язык +
библиотечное окружение. Затем происходит постепенное уменьшение
неопределенности применительно к исследуемой программе. Хочется заметить, что
при решении многих задач анализ всего кода программы (здесь и далее понимается
как исходный текст, так и бинарная (откомпилированная) версия) определенно
избыточен, поэтому достаточно важным является как можно более близкое
начальное приближение к интересующему фрагменту кода.
[2] Некоторые методы проведения анализа ПО
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Существует большое количество средств проведения анализа ПО, впрочем,
удачными можно назвать только несколько, и то - с натяжкой. Перечислять здесь
готовые продукты, наверное, смысла не имеет, но привести несколько примеров
работы с ними - можно.
[2.1] Введение в язык IDC программы IDA-Pro v3.7
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Интерактивный дизассемблер IDA-Pro располагает внутренним макроязыком
синтаксически достаточно близко похожим на Си. Появившись достаточно давно,
только в версии 3.7 он стал достаточно устойчивым и функционально богатым. В
подтверждение своих слов могу привести 2 программы на IDC, которые нафиг
вешали мою систему под IDA v3.5 (или выдавали некорректные результаты).
─[Begin: ERRIDC_1.PAS]─────────────────────────────────────────────────────────
Program IDC_Test;
Procedure TestProc;
Var I:Integer;
Begin
WriteLn('static Test() {');
For I:=1 To 2100 Do Begin
WriteLn(' if (',I,'!=',I,') Message("(',I,') - <>\n");');
End;
WriteLn('}'); WriteLn;
End;
Begin
TestProc;
WriteLn('static main() {');
WriteLn(' Message("IDC Crazy Conditions!\n"); Test();');
WriteLn('}'); WriteLn;
End.
─[End: ERRIDC_1.PAS]───────────────────────────────────────────────────────────
─[Begin: ERRIDС_2.PAS]─────────────────────────────────────────────────────────
Program IDC_Test;
Procedure TestProc;
Var I:LongInt;
Begin
WriteLn('static Test() {');
WriteLn(' auto i;');
Write(' i=0');
For I:=0 To 31 Do Write('+(',I);
For I:=0 To 31 Do Write(')');
WriteLn(';');
WriteLn('}'); WriteLn;
End;
Begin
TestProc;
WriteLn('static main() {');
WriteLn(' Message("IDA Stack overflow!\n"); Test();');
WriteLn('}'); WriteLn;
End.
─[End: ERRIDC_2.PAS]───────────────────────────────────────────────────────────
Примеры достаточно простые (для генерации файла IDC не на экран, а в файл -
используйте перенаправление консоли, например так: ERRIDC_2.EXE >BUG2.IDC),
приводят к переполнению стека исполнителя виртуальной машины IDC или к
некорректной компиляции. Я не уверен, что версия 3.7 полностью свободна от
названных выше недостатков. Кстати, подобным методом углючить Borland Pascal
v7.0 мне не удалось. Недостатком следует назвать так же малый размер
максимальной скомпилированной программы (менее 64 кБ), а так же невозможность
отката сделанных изменений в базе дизассемблирования, так что ошибаться можно
только один раз.
Выше я уже говорил о различных методах анализа ПО. Давайте попробуем
определить размещение процедуры шифрования в исследуемом файле. Как правило, в
доморощенных методах шифрования by obscurity используется операция XOR, но
следует заметить, что XOR часто используется для очистки регистров (например
наряду с: XOR AX,AX; SUB AX,AX и проч.) Попробуем сформулировать следующий
запрос: "хочу все XOR с разными регистрами в качестве операндов!" Сделаем это
таким образом:
─[Begin: _HW_LIB.IDC]──────────────────────────────────────────────────────────
// ------------------------------------------------------Main Library
//
// ---------------------------------------------Find the XOR commands
static findxor(from) {
auto ea,sz,s,op1,op2;
ea=from; for (;;) {
s=GetMnem(ea); op1=GetOpnd(ea,0); op2=GetOpnd(ea,1);
if (s!="" && s=="xor" && op1!=op2) {
// ^^^^^^^^^^^^^^^^^ ------------------------------------ IDA Suxx
Message("Necessary XOR placed at address: "+
form("%.8lX",ea)+"h In format: "+s+" "+op1+","+op2+"\n");
Jump(ea); return ea;
} sz=ItemSize(ea); ea=ea+sz;
if (ea>=MaxEA()) return 0;
}
}
// ------------------------------------------------------------------
─[End: _HW_LIB.IDC]────────────────────────────────────────────────────────────
─[Begin: _FNDXOR.IDC]──────────────────────────────────────────────────────────
// ---------------------------To find one XOR with different operands
#include <idc.idc>
#include <_hw_lib.idc>
static main() {
Message("'FIND XOR' has been loaded. Researching. . .\n");
if (!findxor(ScreenEA()))
Message("Not found, researching process was terminated.\n");
}
// ------------------------------------------------------------------
─[End: _FNDXOR.IDC]────────────────────────────────────────────────────────────
─[Begin: _FNDXORS.IDC]─────────────────────────────────────────────────────────
// --------------------------To find all XORs with different operands
#include <idc.idc>
#include <_hw_lib.idc>
static main() {
auto i,n; n=ScreenEA(); i=MinEA();
Message("'FIND XORs' has been loaded. Researching. . .\n");
while (i=findxor(i)) i=i+ItemSize(i);
Message("No more found, researching process was terminated.\n");
Jump(n);
}
// ------------------------------------------------------------------
─[End: _FNDXORS.IDC]───────────────────────────────────────────────────────────
Внутри IDA v3.7 надо нажать клавишу <F2> и указать имя требуемого файла:
_FNDXOR.IDC для поиска одной команды или _FNDXORS.IDC для поиска сразу всех
команд. Итак, возьмем например VSERVER.VXD, который ведает шаровыми ресурсами
в Microsoft Network локальной рабочей станции под управлением W95. VSERVER.VXD
112892 bytes v4.00.1111 (от русского маздая). Итак, что мы имеем:
Compiling file 'C:\HW\LANG\IDAPRO\idc\_FNDXORS.IDC'...
Executing function 'main'...
'FIND XORs' has been loaded. Researching. . .
Necessary XOR placed at address: 00000656h In format: xor dh,[edi+9]
Necessary XOR placed at address: 0000065Ch In format: xor [edi+9],dh
Necessary XOR placed at address: 00009581h In format: xor al,cl
Necessary XOR placed at address: 00009C35h In format: xor ecx,esi
Necessary XOR placed at address: 0000DB10h!!!In format: xor bl,dl
Necessary XOR placed at address: 0000DB43h!!!In format: xor bl,dl
Necessary XOR placed at address: 00012103h In format: xor [edx],edi
Necessary XOR placed at address: 000126BCh In format: xor [esi],ebp
Necessary XOR placed at address: 000126BEh In format: xor [eax],al
Necessary XOR placed at address: 000126C7h In format: xor bl,cs:[eax+30h]
Necessary XOR placed at address: 000126CBh In format: xor [edx],dh
Necessary XOR placed at address: 000126F0h In format: xor [esi],ch
Necessary XOR placed at address: 000126F2h In format: xor [edx],esi
Necessary XOR placed at address: 000134FCh In format: xor ah,[ebp+6Eh]
Necessary XOR placed at address: 00013508h In format: xor [ebp+6Eh],esp
Necessary XOR placed at address: 00013578h In format: xor al,[eax]
Necessary XOR placed at address: 00013580h In format: xor ah,[ebp+6Eh]
Necessary XOR placed at address: 0001358Ch In format: xor al,[eax]
Necessary XOR placed at address: 00013594h In format: xor ah,[ebp+6Eh]
Necessary XOR placed at address: 000135A0h In format: xor [eax],eax
Necessary XOR placed at address: 000135A8h In format: xor [ebp+6Eh],esp
Necessary XOR placed at address: 000135B4h In format: xor [eax],eax
Necessary XOR placed at address: 000135BCh In format: xor [ebp+6Eh],esp
No more found, researching process was terminated.
Страшно? Много? Нажимая <ESC> просмотрим список: автоанализ IDA достаточно
кривой и все ложные срабатывания на текст (трактуемый как код) быстренько
пропускаем, Итак: DB10h и DB43h - очень интересно, 4 первых срабатывания -
явные фрагменты неистовства оптимизации сишного компилера ;-) Хе-хе, 30 сек. и
что мы имеем:
0000DB2F mov dl, [ebp+arg_0]
0000DB32 mov eax, [ebp+arg_4]
0000DB35 mov ecx, [ebp+arg_8]
0000DB38 LOC_DB38:
0000DB38 mov [ebp+var_1], dl
0000DB3B ror [ebp+var_1], 1
0000DB3E mov dl, [ebp+var_1]
0000DB41 mov bl, [ecx]
0000DB43 xor bl, dl
0000DB45 mov [eax], bl
0000DB47 inc ecx
0000DB48 inc eax
0000DB49 dec esi
0000DB4A jnz short LOC_DB38
Ой, ой, ой, ай да микрософт. Вот так мы храним в регистри пошифрованные пароли
на наши собственные шаровые ресурсы. Программа PWLHACK v4.0 позволяет
полностью расшифровать и посмотреть их. Ну, думаю, что на этом можно закончить.
[2.2] Ключеделы-ключеделы... или как проводить сверку
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Итак, многие из вас писали ключеделы, регистраторы и проч. Многие
отхакивали проверки нафиг (тоже разумно). Безусловно, что где тонко, там и
рвется. Никто из вас не обращал внимания на ключемейкеры? Ну ка, например:
─[Begin: LASM20R.C]────────────────────────────────────────────────────────────
#include <string.h>
#include <stdio.h>
char* MakeReg(unsigned char* UserName, unsigned char* UserId,
unsigned char* Password) {
unsigned long S1=13,S2=0;
while (*UserName) { S2+=S1*(*UserName++); S1=(S1+ 37)*3; }
while (*UserId ) { S2+=S1*(*UserId++ ); S1=(S1+197)*7; }
S2%=0xF4240; sprintf(Password,"%lu",S2); return Password;
}
void main(int argc, char* argv[]) {
char s1[256],s2[256],s3[256];
puts("(C) 25-Nov-1998y by Hard Wisdom \"LASM v2.0 KeyGen\" v1.0");
puts(" ~~~~~~~~~~~~~~~~~~");
if (argc<2) { printf("User name: "); gets(s1); s1[strlen(s1)]=0;
} else strcpy(s1,argv[1]);
if (argc<3) { printf(" User ID: "); gets(s2); s2[strlen(s2)]=0;
} else strcpy(s2,argv[2]);
printf("<Password: %s>\n",MakeReg(s1,s2,s3));
}
─[End: LASM20R.C]──────────────────────────────────────────────────────────────
Это пример ключегенератора для "Lite Assembler", великолепная вещь одного
японца, полностью совместима с MASM v6.0 и т.п. В чем основная проблема
подобных проверок регистрационных ключей? Правильно, в их (проверок)
обозримости. Никто не мешает нам работать не только с бинарным кодом (как
поступают многие), но и с исходным текстом так же. (В свое время на УКНЦ я
работал с паскалем компиляющим сквозь MACRO-11 (ассемблер), так же может
поступать Turbo C и огромное множество других продуктов, а если вспомнить еще
скремблеры? Типа PLM, SAT и проч.). Приведем пример кряк-ми, решающего
вышеописанную проблему. Хочу заметить, что это _ТОЛЬКО_ПРИМЕР_, никто вам не
запрещает выполнять параллельно 2 генерации кода (прямого - шифрование и
обратного - расшифровка). Итак, опять мусорим сырцами:
─[Begin: HWCRKMEG.C]───────────────────────────────────────────────────────────
/*******************************************************************/
/* (C) by Hard Wisdom "The CrackMe Generator from Hard Wisdom" */
/* v1.0 from 06-Apr-1998y */
/* Status: Alpha version */
/*******************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
/*-----------------------------------------------------------------*/
void str2file(FILE* f, char* s) {
fprintf(f,"%s\n",s);
}
/*-----------------------------------------------------------------*/
void simpleop(FILE* f) {
int op1,op2,op3; unsigned long op0;
op1=random(0x10); op2=random(0x10); op3=random(0x10);
op0=((unsigned long)random(RAND_MAX)<<10)+random(RAND_MAX);
switch (random(15)) {
case 0: fprintf(f," w[%d]+=0x%lX;\n",op1,op0); break;
case 1: fprintf(f," w[%d]-=0x%lX;\n",op1,op0); break;
case 2: fprintf(f," w[%d]^=0x%lX;\n",op1,op0); break;
case 3: fprintf(f," w[%d]+=w[%d]^0x%lX;\n",op1,op2,op0); break;
case 4: fprintf(f," w[%d]^=w[%d]+0x%lX;\n",op1,op2,op0); break;
case 5: fprintf(f," w[%d]-=w[%d]^0x%lX;\n",op1,op2,op0); break;
case 6: fprintf(f," w[%d]^=w[%d]-0x%lX;\n",op1,op2,op0); break;
case 7: fprintf(f," w[%d]=w[%d]^w[%d];\n",op3,op1,op2); break;
case 8: fprintf(f," w[%d]=w[%d]+w[%d];\n",op3,op1,op2); break;
case 9: fprintf(f," w[%d]=w[%d]-w[%d];\n",op3,op1,op2); break;
case 10: fprintf(f," w[%d]++;\n",op1); break;
case 11: fprintf(f," w[%d]--;\n",op1); break;
case 12: fprintf(f," w[%d]^=(w[%d]<<1)^(w[%d]>>1);\n",op3,op1,op2); break;
case 13: fprintf(f," w[%d]^=(w[%d]<<3)^(w[%d]>>3);\n",op3,op1,op2); break;
case 14: fprintf(f," w[%d]^=(w[%d]<<9)^(w[%d]>>9);\n",op3,op1,op2); break;
}
}
/*-----------------------------------------------------------------*/
void _combinedop(FILE* f, int level) {
char b_op[4]="+-^"; char u_op[8][3]={"-","~","--","++","","","",""};
int op,i,ii,n; char lspaces[256]; unsigned long op0;
op0=((unsigned long)random(RAND_MAX)<<10)+random(RAND_MAX);
for (i=0;i<=level;lspaces[i++]=' '); lspaces[i]=0;
fprintf(f,"\n%s(\n%s ",lspaces,lspaces);
op=random(0x10); i=random(8); fprintf(f,"(%sw[%d])",u_op[i],op);
n=random(10)+5; while (n--) { op=random(0x10); i=random(8); ii=random(3);
fprintf(f,"%c",b_op[ii]);
if (random(10)>8 && level<=3) _combinedop(f,level+1);
else if (random(10)>5) fprintf(f,"0x%lX",op0);
else fprintf(f,"(%sw[%d])",u_op[i],op);
} fprintf(f,"\n%s)\n%s",lspaces,lspaces);
}
void combinedop(FILE* f) {
int op; op=random(0x10); fprintf(f," w[%d]=",op);
_combinedop(f,0); fprintf(f,";\n");
}
/*-----------------------------------------------------------------*/
void trivialops(FILE* f, int mincnt, int dev) {
int i,i0; i0=random(dev+1)+mincnt; for (i=1;i<=i0;i++)
if (random(10)>8) combinedop(f); else simpleop(f);
};
/*-----------------------------------------------------------------*/
void blockop(FILE* f,int level, int mincnt, int dev) {
int i,i0,n,op,flg; i0=random(dev+1)+mincnt;
op=random(0x10); flg=random(2); n=random(14)+10;
if (flg) fprintf(f," if (w[%d] && %d) {\n",op,n);
else fprintf(f," for (i[%d]=0;i[%d]<%d;i[%d]++) {\n",level,level,n,level);
for (i=1;i<=i0;i++) {
if (random(10)>8 && level<0x0F) blockop(f,level+1,mincnt,dev);
else trivialops(f,1,0);
} fprintf(f," }\n");
}
/*-----------------------------------------------------------------*/
void funcop(FILE* f, int level) {
fprintf(f,"void flow%d(unsigned long from, unsigned long* to) {\n",level);
str2file(f," unsigned long w[0x10]; int i[0x10];");
str2file(f," w[0]=from;");
str2file(f," for (i[0]=1;i[0]<=0x0F;w[i[0]++]=w[0]);");
blockop(f,0,4,4); blockop(f,0,4,4);
blockop(f,0,4,4); blockop(f,0,4,4);
str2file(f," for (i[0]=1;i[0]<=0x0F;w[0]^=w[i[0]++]);");
str2file(f," *to=w[0];}");
}
/*-----------------------------------------------------------------*/
void maincode(FILE* f) {
int i,i0;
randomize(); i0=random(6)+10;
for (i=1;i<=i0;i++) funcop(f,i);
str2file(f,"");
str2file(f,"int check_n_get(char* name, unsigned long ser,");
str2file(f," unsigned long* res) {");
str2file(f," unsigned long hash; int i;");
str2file(f," hash=0x00FEED00; while (*name)");
str2file(f," hash+=((hash^*name)<<3)+((hash^*name)>>3),name++;");
str2file(f,"");
for (i=1;i<=i0;i++) {
fprintf(f," flow%d(hash,&hash); flow%d(hash,&hash);",i,i0-i+1);
fprintf(f," printf(\"▐\");\n");
} str2file(f,"");
str2file(f," *res=hash; return *res==ser;");
str2file(f,"}");
}
/*-----------------------------------------------------------------*/
void generate(FILE* f) {
str2file(f,"/*-----------------------*/");
str2file(f,"/*---start of crack me---*/");
str2file(f,"/*-----------------------*/");
str2file(f,"#include <stdio.h>");
str2file(f,"");
maincode(f);
str2file(f,"");
str2file(f,"void main() {");
str2file(f," int r;");
str2file(f," unsigned long ser; char name[256];");
str2file(f," printf(\" Accepted only KEYMAKERS !\\n\");");
str2file(f," printf(\"══════════════════════════════════════════════\\n\");");
str2file(f," printf(\" \\\"HW-CrackME\\\" v1.0\\n\");");
str2file(f," printf(\" ~~~~~~~~~~~~ (C) 06-Apr-1998y by Hard Wisdom\\n\");");
str2file(f," printf(\"──────────────────────────────────────────────\\n\");");
str2file(f," printf(\" ■ Registration Name: \");");
str2file(f," gets(name);");
str2file(f," printf(\" ■ Serial Number: \");");
str2file(f," scanf(\"%lX\",&ser);");
str2file(f," printf(\" ■ Checking: \");");
str2file(f," r=check_n_get(name,ser,&ser);");
str2file(f," printf(\" Ok\\n\");");
str2file(f," if (!r)");
str2file(f," printf(\" ■ Your serial number is NOT Accepted!\\n\");");
str2file(f," else");
str2file(f," printf(\" √ Well done, You hack it !!! Wise Guy.\\n\");");
str2file(f," printf(\"══════════════════════════════════════════════\\n\");");
str2file(f," printf(\" FidoNet: 2:461/133.69\\n\");");
str2file(f,"");
str2file(f,"#ifdef crackhw");
str2file(f," printf(\" ■ Real serial number is %.8lX\\n\",ser);");
str2file(f,"#endif");
str2file(f,"}");
str2file(f,"/*-----------------------*/");
str2file(f,"/*---!stop of crack me---*/");
str2file(f,"/*-----------------------*/");
}
/*-----------------------------------------------------------------*/
void main(int argc, char* argv[]) {
FILE* f;
printf("(C) 06-Apr-1998y by Hard Wisdom \"The CrackMe Generator\" v1.0\n");
printf(" ~~~~~~~~~~~~~~~~~~~~~~~\n");
if (argc<=1) {
printf("USAGE: HWCRKMEG.EXE NameOfSourceFile - to generate in...\n");
return;
};
/*-----------------------------------------------------------------*/
if ((f=fopen(argv[1],"w"))==NULL) {
printf("Can't create output file '%s'!\n",argv[1]);
return;
}; generate(f);
if (ferror(f)) {
printf("Disk I/O error occured during generation!\n");
return;
}; fclose(f); /* Generated OK */
printf("Well done! ;-) Use the TC to compile it.\n");
}
/*******************************************************************/
/* The End */
/*******************************************************************/
─[End: HWCRKMEG.C]─────────────────────────────────────────────────────────────
Я думаю, что ключедел в данном случае будет написать проблематично, а для
битхака, насколько я понимаю, тоже есть свои пути противодействия. Один
момент: компилять надо TC v2.0 и второе: генерация случайна, иногда компилер
не сможет покомпилять сгенерированный код по техническим причинам (слишком
большая глубина вложенности или слишком большой файл), это обходится
повторными перегенерациями. К сожалению, приведенный выше метод создания
исходного текста невозможно полностью излечить от непредсказуемости, но можно
автоматически проверять статистические характеристики полученного кода.
[2.3] Что есть код, а что данные, и где есть кто?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Я думаю, что все знают программу "Слово и Дело v8.0", очень неплохой
текстовый редактор Гутникова для ДОСа. Я полагаю, что многим известны так же и
некоторые крякми некоторых уважаемых людей: NOR (C) Solar Designer или NORAC
(C) Code Master. Разговор пойдет сейчас именно об этом: выполнение кода, как
это делает процессор и зачем это делают остальные (те, которые не являются
процессорами).
Я уже говорил, что в процессе анализа некоторой программы мы располагаем
определенной начальной информацией (целевой процессор выполнения, в
частности). А что, если у нас нет даже этого?! Именно, тогда большинство
утилит (SoftIce, IDA, HIEW, Sourcer e.t.c.) будут бесполезны, прийдется писать
что-то свое. Именно так появились в свое время: DeClipper, Valcyria, ReFox,
DeVB, JAD, DeInsh и проч. Хм, интересно отметить, что само по себе
использование неизвестного процессора еще не является гарантом
"невзламываемости" кода, примеров тому - масса. Рассмотрим механизм проверки
ключей программы "Слово и Дело" (W&D). Рискуя навлечь на себя ваш гнев,
приведу листинг Пи-кода проверки серийного номера целиком:
─[Begin: WD80WOW.LST]──────────────────────────────────────────────────────────
This part of code is used far away in program
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0: │ 11│ 0│ 71│142│ LOAD R2,18318
4: │ 15│ │ │ │ MOV R4,R2
5: │ 19│ │ │ │ LOAD R2,<CONST>
6: │ 13│ │ │ │ MOV R3,R2
7: │ 14│ │ │ │ MOV R2,R4
8: │ 5│ │ │ │ DEC R2
9: │ 6│ │ │ │ INC R3
10: │ 2│ │ │ │ BEQ R2==0,13
11: │ 1│ 8│ │ │ JMP 8
13: │ 11│ 0│ 1│ 87│ LOAD R2,343
17: │ 10│ │ │ │ MUL R2,R3
18: │ 13│ │ │ │ MOV R3,R2
19: │ 18│ │ │ │ SUB R2,R3
20: │ 20│ │ │ │ ROLO R4,R3,R2
21: │ 11│ 1│ 61│ 7│ LOAD R2,81159
25: │ 20│ │ │ │ ROLO R4,R3,R2
26: │ 18│ │ │ │ SUB R2,R3
27: │ 8│ │ │ │ INC R4
28: │ 3│ │ │ │ BEQ R2>0,31
29: │ 1│ 33│ │ │ JMP 33
31: │ 1│ 26│ │ │ JMP 26
33: │ 13│ │ │ │ MOV R3,R2
34: │ 11│ 1│ 62│218│ LOAD R2,81626
38: │ 23│ │ │ │ ADD V2,V3
39: │ 43│ │ │ │ NOP
40: │ 56│ │ │ │ NOP
41: │ 71│ │ │ │ NOP
42: │ 82│ │ │ │ NOP
43: │ 20│ │ │ │ ROLO R4,R3,R2
44: │ 20│ │ │ │ ROLO R4,R3,R2
45: │ 11│ 0│ 65│ 23│ LOAD R2,16663
49: │ 20│ │ │ │ ROLO R4,R3,R2
50: │ 20│ │ │ │ ROLO R4,R3,R2
51: │ 2│ │ │ │ BEQ R2==0,54
52: │ 1│ 56│ │ │ JMP 56
54: │ 1│ 60│ │ │ JMP 60
56: │ 5│ │ │ │ DEC R2
57: │ 20│ │ │ │ ROLO R4,R3,R2
58: │ 1│ 51│ │ │ JMP 51
60: │ 20│ │ │ │ ROLO R4,R3,R2
61: │ 20│ │ │ │ ROLO R4,R3,R2
62: │ 23│ │ │ │ ADD V2,V3
63: │ 22│ │ │ │ MOV R1,R2
64: │ 21│ │ │ │ HALT R1
PreCalc1
~~~~~~~~
65: │ 82│ │ │ │ NOP
66: │ 11│ 1│134│160│ LOAD R2,100000
70: │ 13│ │ │ │ MOV R3,R2
71: │ 19│ │ │ │ LOAD R2,<CONST>
72: │ 20│ │ │ │ ROLO R4,R3,R2
73: │ 19│ │ │ │ LOAD R2,<CONST>
74: │ 18│ │ │ │ SUB R2,R3
75: │ 20│ │ │ │ ROLO R4,R3,R2
76: │ 20│ │ │ │ ROLO R4,R3,R2
77: │ 18│ │ │ │ SUB R2,R3
78: │ 8│ │ │ │ INC R4
79: │ 3│ │ │ │ BEQ R2>0,82
80: │ 1│ 84│ │ │ JMP 84
82: │ 1│ 77│ │ │ JMP 77
84: │ 14│ │ │ │ MOV R2,R4
85: │ 5│ │ │ │ DEC R2
86: │ 2│ │ │ │ BEQ R2==0,89
87: │ 1│ 95│ │ │ JMP 95
89: │ 11│ 0│ 49│ 2│ LOAD R2,12546
93: │ 1│185│ │ │ JMP 185
95: │ 5│ │ │ │ DEC R2
96: │ 2│ │ │ │ BEQ R2==0,99
97: │ 1│105│ │ │ JMP 105
99: │ 11│ 0│ 37│234│ LOAD R2,9706
103: │ 1│185│ │ │ JMP 185
105: │ 5│ │ │ │ DEC R2
106: │ 2│ │ │ │ BEQ R2==0,109
107: │ 1│115│ │ │ JMP 115
109: │ 11│ 0│ 47│ 49│ LOAD R2,12081
113: │ 1│185│ │ │ JMP 185
115: │ 5│ │ │ │ DEC R2
116: │ 2│ │ │ │ BEQ R2==0,119
117: │ 1│125│ │ │ JMP 125
119: │ 11│ 0│ 58│ 1│ LOAD R2,14849
123: │ 1│185│ │ │ JMP 185
125: │ 5│ │ │ │ DEC R2
126: │ 2│ │ │ │ BEQ R2==0,129
127: │ 1│135│ │ │ JMP 135
129: │ 11│ 0│ 45│128│ LOAD R2,11648
133: │ 1│185│ │ │ JMP 185
135: │ 5│ │ │ │ DEC R2
136: │ 2│ │ │ │ BEQ R2==0,139
137: │ 1│145│ │ │ JMP 145
139: │ 11│ 0│ 31│ 10│ LOAD R2,7946
143: │ 1│185│ │ │ JMP 185
145: │ 5│ │ │ │ DEC R2
146: │ 2│ │ │ │ BEQ R2==0,149
147: │ 1│155│ │ │ JMP 155
149: │ 11│ 0│ 53│ 97│ LOAD R2,13665
153: │ 1│185│ │ │ JMP 185
155: │ 5│ │ │ │ DEC R2
156: │ 2│ │ │ │ BEQ R2==0,159
157: │ 1│165│ │ │ JMP 165
159: │ 11│ 0│ 34│218│ LOAD R2,8922
163: │ 1│185│ │ │ JMP 185
165: │ 5│ │ │ │ DEC R2
166: │ 2│ │ │ │ BEQ R2==0,169
167: │ 1│175│ │ │ JMP 175
169: │ 11│ 0│ 39│163│ LOAD R2,10147
173: │ 1│185│ │ │ JMP 185
175: │ 11│ 0│ 62│ 9│ LOAD R2,15881
179: │185│ │ │ │ NOP
180: │178│ │ │ │ NOP
181: │123│ │ │ │ NOP
182: │ 82│ │ │ │ NOP
183: │ 94│ │ │ │ NOP
184: │ 64│ │ │ │ NOP
185: │ 75│ │ │ │ NOP
186: │ 43│ │ │ │ NOP
187: │ 56│ │ │ │ NOP
188: │ 22│ │ │ │ MOV R1,R2
189: │ 21│ │ │ │ HALT R1
PreCalc2
~~~~~~~~
190: │ 42│ │ │ │ NOP
191: │ 52│ │ │ │ NOP
192: │ 87│ │ │ │ NOP
193: │ 19│ │ │ │ LOAD R2,<CONST>
194: │ 42│ │ │ │ NOP
195: │ 52│ │ │ │ NOP
196: │ 87│ │ │ │ NOP
197: │ 20│ │ │ │ ROLO R4,R3,R2
198: │ 11│ 0│ 0│ 76│ LOAD R2,76
202: │ 82│ │ │ │ NOP
203: │ 8│ │ │ │ INC R4
204: │ 75│ │ │ │ NOP
205: │ 5│ │ │ │ DEC R2
206: │ 64│ │ │ │ NOP
207: │ 6│ │ │ │ INC R3
208: │132│ │ │ │ NOP
209: │ 3│ │ │ │ BEQ R2>0,212
210: │ 1│214│ │ │ JMP 214
212: │ 1│202│ │ │ JMP 202
214: │ 12│ │ │ │ MOV R2,R3
215: │ 1│180│ │ │ JMP 180
217: │ 19│ │ │ │ LOAD R2,<CONST>
218: │ 15│ │ │ │ MOV R4,R2
219: │ 4│ │ │ │ INC R2
220: │ 6│ │ │ │ INC R3
221: │ 8│ │ │ │ INC R4
222: │ 11│ 0│ 10│ 0│ LOAD R2,2560
226: │ 20│ │ │ │ ROLO R4,R3,R2
227: │ 22│ │ │ │ MOV R1,R2
228: │ 18│ │ │ │ SUB R2,R3
229: │ 3│ │ │ │ BEQ R2>0,232
230: │ 29│ │ │ │ NOP
231: │ 21│ │ │ │ HALT R1
232: │ 52│ │ │ │ NOP
233: │ 24│ │ │ │ FUCK
234: │255│ │ │ │ NOP
─[End: WD80WOW.LST]────────────────────────────────────────────────────────────
Мнемоники, полагаю, всем понятны. При вызове интерпретатора Пи-кода на вход
передается параметр <CONST>, и на выходе командой HALT возвращается некоторый
результат. Честно говоря, в данном случае применение интерпретируемого Пи-кода
выглядит несколько странно. Автор не нашел лучшего применения, чем выполнять
XLAT по табличке. 8-() А ведь мог навернуть, так же странно выглядят участки
NOP'ов в коде, возможно это obscurity. Дабы не быть голословным приведу
напоследок окончательный вариант ключегенератора.
─[Begin: WD80K.C]──────────────────────────────────────────────────────────────
// --------------------------------------------------------
// (C) 08-Feb-1998y by Hard Wisdom "WD v8.0 KeyMaker" v2.0
// --------------------------------------------------------
#include <stdio.h>
#include <string.h>
void main(int argc, char* argv[])
{
char s[20]="";
unsigned long key; long num; int i;
unsigned int dk[10]={19559,27409,13769,2354,8486,
13576,13467,11689,14889,27106};
unsigned int da[10]={12546,9706,12081,14849,11648,
7946,13665,8922,10147,15881};
printf("(C) 08-Feb-1998y by Hard Wisdom \"WD v8.0 KeyMaker\" v2.0\n");
printf(" ~~~~~~~~~~~~~~~~~~\n");
if (argc>1) strcpy(s,argv[1]);
else {printf("Enter the product number: "); scanf("%s",&s);};
strupr(s); if (strlen(s)<3 || s[1]!='-') {
printf("Error: bad product number prefix, must be 'X-' where X is alpha!\n");
return;
}; if (s[0]!='E')
printf("Warning: generation can be incorrect, required prefix 'E-' only!\n");
strcpy(s,&s[2]); sscanf(s,"%lu",&key);
key=((key / 10)-dk[key % 10]+892)*5/4+7546;
if (key<0 || key>99999) {
printf("Error: invalid product number has been entered!\n");
return;
};
printf("Well, generating registration numbers for %s...\n",s);
for(i=1;i<11;i++){
num=key-da[i-1]; if (num<0) num=num+100000*i; else num=num+100000*(i-1);
printf(" %2d%s registration key is %6lu\n",i,i==1?"st":i==2?"nd":"th",num);
}; printf("Make your choice ;-) Software must be FREE!");
}
// --------------------------------------------------------
─[End: WD80K.C]────────────────────────────────────────────────────────────────
Весьма просто, не так ли? А ведь битхак W&D штука очень нетривиальная, я могу
только посочувствовать тому, кто возьмется поправить хотя бы байтик в коде
программы. Ну, вот, например: W&D позиционируется за конец себя в файле на
диске (по фиксированному смещению) и читает пару килобайт в таблицу
прерываний! Если файл имеет оригинальную длинну, то все в порядке, произойдет
ошибка чтения. Но что будет, если файл распаковать? Правильно, вектора
прерываний будут затерты хвостом программы. И ведь никаких явных сравнений! Но
это только начало, желающие - вооружайтесь отладчиком, там есть на что
посмотреть.
[Z] Заключение
~~~~~~~~~~~~~~
Надеюсь, что вам было интересно. Думаю, что в будущем стану продолжать
вести этот файл. Естественно, что все это и много других сопутствующих
материалов можно взять на моей домашней страничке. По идеологии данное эссе
противопоставлено своему собрату NASTYxxx.TXT (to URL: please ;-) За сим не
прощаюсь.
Home Page: www.geocities.com/SiliconValley/Hills/7827
E-Mail: [email protected]