Исследование ReGet 1.3.2.

  На безрыбье и рак - щука
Народная мудрость.


К чему такой эпиграф ? А к тому, что развелось слишком много народу, считающих себя более умными, чем другие; оно, конечно, что-то в этом может быть и есть. Однако, что-то мало кто из них может написать описание препарирования программы в понятной форме
1). В общем, это их проблемы, а свою проблему в виде этой статьи я выношу на всеобщее обозрение
Отчего бы не сломать более новую версию ? - спросит меня пытливый читатель. И будет совершенно неправ. Самая главная причина в том, что я живу в той же поганой стране
2), что и авторы сей не самой плохой программы на свете, и испытытываю на собственной заднице те же прелести проживания здесь, что и они. Короче, сокамерники мы, как бы. Так что зачем же так гадить людям - им ведь тоже нужно бабки делать как-то, вот когда следующая версия ReGet появится, тогда и 1.40, может быть, посмотрю. А пока, если шило в одном месте не даёт покоя, можете сами попробовать.
Значится, программа зовётся ReGet, v 1.3.2, взять можно много где, умеет она скачивать порванные соединения по HTTP (если сервер на том конце провода двумя руками поддерживает протокол HTTP 1.1).
Всем хороша данная программа, но есть у неё маленький недостаток (как любят говорить ребята от Билли "feature") - начинает надоедать через 30 дней диалоговым окошком - дескать, зарегистрируй меня etc. А чтобы не было такого, просит зарегистрироваться. O`k.
Запускаем программу, выбираем в главном меню "?", "Регистрация ReGet"
Появляется диалоговое окно "Регистрационная информация". Заносим туда имя, свой e-mail - о-па, а что это за третье поле такое? "Регистрационный код" !
А сиё чудо где взять ? Нету ? Непорядок. Надо свою программу написать, чтоб такой генерила.
Однако, скоро сказка сказывается... Короче, налейте себе, если со вчерашнего осталось, и приступим.
Для начала, жизненно необходимо выяснить, с какого же места собственно и начинается обработка введённого пароля. Запускаем струмент - SI. Ставим стандартный набор
3)

 bpx GetWindowTextA

 bpx GetDlgItemTexA

 bpx MessageBoxA

 bpx MessageBoxIndirectA

Жмём педаль - и вот оно, всплываем по вызову GetWindowTextA; жмём F12 - и оказываемся по неважно какому адресу, зато важно, в каком модуле - mfc42. Значит, программа написана с применением MFC и это есть некая функция CWnd::DoDataExChange, в которой посредством DDX_ функций происходит извлечение введённых пользователем значенийRTFM).
Чтобы не всплывать зря ещё несколько раз на тех же граблях, отключим точку прерывания: bd 0. Жмём F12, пока не окажемся снова в модуле ReGet - адрес 0x4143B9. Самое время нажать F5 и запустить IDA Pro. Я не буду приводить листинг, который она сгенерировала для данного адреса, по следующей причине - это действительно переопределённая функция CWnd::DoDataExChange для данного конкретного диалога, в ней нет ничего интересного (несколько DDX_Text
RTFM))
Ну что же, не много полезного мы выяснили на первый раз. Придётся повторить, но на этот раз жмём F12 дальше, пока снова не окажемся в модуле ReGet - на это раз по адресу 0x414492. Вот это уже интересно. Ну-ка, что нам IDA показывает (полный листинг я опять-таки не привожу
4), только вызовы функций (комментарии, естественно, мои):

  ...

  41448D	CWnd::UpdateData

  ...

  41449C	CString::CString(CString const &)

  ...

  4144AF	CString::CString(CString const &)

  ...

  4144C2	CString::CString(CString const &)

  ...

  4144CB	call 41410F

  4144D0	add esp, 0Ch	; стандартная очистка стека от локальных переменных,

				; используемых в пользовательских функциях

  4144D3	test eax, eax         ; смотрим, в eax 0 ?

  4144D5	jz short loc_4144E2   ; а вот это что-то до боли знакомое...

  ...

  4144DB	CDialog::EndDialog

  ...

  4144E2	push 0ffffffffh

  4144E4	push 0

  4144E6	push 41Dh

  4144EB	call AfxMessageBox(uint, uint, uint)

  ...  

 

Уж сколько раз говорено было... Короче, старо как Zip, если по возврате из call 41410F регистр eax не есть 0 - мы хорошие парни, и неплохо было бы закрыть диалоговое окно вызовом CDialog::EndDialog, иначе - мы плохие парни, на нас обиделись и нужно непременно высказаться вызовом AfxMessageBox (MFC-вариант обычного MessageBox). Всё ясно, нужно препарировать процедуру 41410F
Для начала, давайте дадим ей имя позвучнее, скажем, VerifyUs. Для этого подведём курсор на строку, содержащую фразу "proc near" на 4144E2 после адреса и нажмём N и в появившемся окне дадим волю фантазии
5)
Процедура, опять же, слишком длинная и мне было утомительно приводить её здесь полностью, так что приведены только необходимые для понимания моменты:

 41410F		push eax, offset loc_42003C

 414114		call __EH_prolog

Стандартная для C++ инициализация exception frame handlerа, в стек помещается адрес функции очистки стека, которая вызовется при возникновении неперехваченного исключения и вызывается функция инициализации нового фрейма обработки исключений
Дальше какая-то не относящаяся к делу лажа, ага, вот интересное местечко:

 414136		lea ecx, [ebp+10h]

 414139		push offset a_contik_

 41413E		call CString::Find(char const *)

 414143		cmp eax, 0ffffffffh

 414146		jz short loc_41415C

По адресу a_contik_ находится строка "_contik_". Если эта строка не найдена в некоей строковой переменной класса CString (то есть, вызов CString::FindRTFM) вернул -1), выполнение переходит на 41415C. Ну-ка проверим под SI, чего это тут делается... Ага, прикол от программистов (сами посмотрите, если не лениво - считайте это домашним заданием :-). Ну ладно, поstepали дальше

 41415C		mov eax, [ebp+10h]	; загрузим строку пароля из класса CString

 41415F		xor esi, esi

 414161		cmp [eax-8], edi        ; не нулевая ли длина (выше по течению

					; была инструкция xor edi, edi по адресу 414121)

 414164		jle short loc_414188    ; если длина < 0 - переход на следующую проверку

 414166		cmp edi, 14h            ; длина > 14 ?

 414169		jge short loc_414188    ; тогда на следующую проверку

 41416B		mov cl, [esi+eax]       ; в cl - очередной байт нашего пароля

					; помните, что в строках индексация начинается с 0

 41416E		cmp cl, 20h             ; сравним с кодом пробела

 414171		jz short loc_414182     ; если оно - перход на следующую итерацию

 414173		push ecx                ; наш байт положим на стек

 414174		call sub_4140ED         ; вызов некоей любопытной функции

 414179		mov [ebp+edi-20h], al   ; и результат (который всегда возвращается

					; в регистре eax - ещё куда-то (один байт)

 41417D		mov eax, [ebp+10h]      ; и снова загрузим адрес строки пароля

 414180		pop ecx                 ; очистка стека

 414181		inc edi                 ; накинем счётчики

 414182		inc esi

 414183		cmp esi, [eax-8]        ; счетчик меньше длины строки ?

 414186		jl short loc_414166     ; если нет - следующая итерация

 414188		cmp edi, 10h            ; сравним количество обработанных байт

 41418B		jnz loc_41423B          ; если не равно 0x16 - переход довольно далеко

 ...

 41423B		...

 ...

 414242		call CString::~Cstring(void)

 ...

 41424E		call CString::~Cstring(void)

 ...

 41425A		call CString::~Cstring(void)

 41425F		xor eax, eax		; это плохой парень

 ...

		retn

Не разобраться без (нет, не без пол-литра) SoftIce. Путём нескольких прогонов под отладчиком выяснилось кое-что, что я описал в комментариях (уж придётся вам поверить моему внутреннему голосу). Просмотрев этот фрагмент даже бегло, уже можно выяснить формат "Регистрационного кода" (в дальнейшем РН) - он не должен содержать пробелов и быть в длину ровно 10h = 16 символов. Посмотрим на процедуру sub_4140ED - ей передаётся в качестве параметра байт из РН

 4140ED		movsx   eax, [esp+arg_0]	; в eax - параметр

 4140F2		push    eax                     ; его же - в стек

 4140F3		call    ds:toupper              ; преобразуем сивол в верхний регистр

 4140F9		pop     ecx                     ; очистка стека

 4140FA		mov     cl, al                  ; преобразованный символ - в cl

 4140FC		xor     eax, eax                ; обнулим eax

 4140FE		cmp     cl, byte ptr a92bc4hjkldfw5z[eax]	; сравним байт по

				; адресу адрес a92bc4hjkldfw5z + eax

 414104		jz      short locret_41410E     ; если равны - возврат из функции

 414106		inc     eax                     ; накинем счётчик

 414107		cmp     eax, 20h                ; сравним счётчик с 0x20

 41410A		jl      short loc_4140FE        ; если меньше - следующая итерация

 41410C		mov     al, 20h                 ; ничего не нашли - вернём 0x20

 41410E         retn

В строке a92bc4hjkldfw5z (как IDA иногда развозит) "9#2BC4HJKLDFW5Z$6G8MNXPQR7S3ETAU". Значит, по возможности РН должен состоять из таких символов в верхнем или нижнем регистре (не забывайте про вызов toupperRTFM)).
Замечание: инструкция movsx делает следующее: в самую младшую часть регистра загружается байт, а остальная часть регистра заполняется знаковым битом этого байта, то есть в данном случае, при использовании символов, меньших 127 (латинских букв и/или цифр) это будет всегда 0.
Сечас преобразованный символы РН лежат в буфере по адресу [ebp-20h]. Далее

 414191		movsx   eax, byte ptr [ebp-16h]	; берём 20h-16h=10ый сивол

 414195		movsx   ecx, byte ptr [ebp-13h] ; 20h-13h=13ый

 414199		imul    eax, ecx		; умножаем со знаком, результат в eax

 41419C		mov     bl, [ebp-19h]           ; 20h-19h=7ой символ

 41419F		push    eax                     ; помещаем произведение в стек

 4141A0		lea     eax, [ebp-20h]          ; в eax - адрес буфера с преобразованным РН

 4141A3		push    edi                     ; в edi - длина РН - в стек

 4141A4		push    eax                     ; помещаем в стек адрес буфера с РН

 4141A5		mov     byte ptr [ebp-19h], 42h ; 7ой символ замещаем кодом 0x42

 4141A9		call    sub_416691              ; вызываем некоторую процедуру

 4141AE		and     al, 1Fh                 ; оставляем в результате младшие 5 бит

 4141B0		add     esp, 0Ch		; очистка стека

 4141B3		cmp     bl, al                  ; как интересно

 4141B5		jnz     loc_41423B

Какую любопытную вещь мы сдесь наблюдаем ! Сильно это похоже на проверку правильности введённого РН. Не иначе как функция sub_416691 считает hash-код по нашему преобразованному РН, затем результат обрезается до 5 младших бит и сравнивается с 7мым преобразованным символом. Если они не равны - переход на уже знакомый нам адрес, возвращающий 0 (признак наличия плохих парней). Переходим на функцию 416691 и называем её, скажем, get_hash (я также обозвал и все её аргументы для большей наглядности:

 416691 key             = dword ptr  4

 416691 len             = dword ptr  8

 416691 initial_seed    = dword ptr  0Ch

 416691

 416691		mov     eax, [esp+0Ch]		; поместим в eax initial_seed

 416695		xor     ecx, ecx                ; обнулим ecx

 416697		cmp     [esp+len], ecx          ; сравним длину с 0

 41669B		not     eax                     ; инвертируем initial_seed

 41669D		jbe     short get_hash_end      ; переход, если длина меньше или равна 0

 41669F		push    esi

 4166A0 get_hash_loop:

 4166A0		mov     edx, [esp+4+key]        ; в edx - адрес переданного ключа

 4166A4		movzx   esi, al                 ; в esi - младший байт initial_seed

 4166A7		movzx   edx, byte ptr [ecx+edx]	; в edx - очередной байт ключа

 4166AB		xor     edx, esi

 4166AD		shr     eax, 8                  ; сдвигаем initial_seed вправо на 8 бит

 4166B0		mov     edx, array[edx*4]       ; в edx - dword по адресу array + edx*4

 4166B7		xor     eax, edx                ; формируем новый initial_seed

 4166B9		inc     ecx                     ; накинем счётчик

 4166BA		cmp     ecx, [esp+4+len]        ; счётчик равен длине ?

 4166BE		jb      short get_hash_loop     ; если меньше - следующая итерация

 4166C0		pop     esi                     

 4166C1 get_hash_end:

 4166C1		not     eax                     ; перед возвратом ещё раз инвертируем initial_seed

						; он и есть hash-code - результат функции

 4166C3		retn

Происходит здесь следующее: цикл по байтам ключа, число итераций цикла - длина ключа, передаваемая как второй параметр, при этом происходит изменение некоторого initial_seed. После цикла его значение инвертируется, и возвращается как результат. Попутно можно заметить, что для подсчёта hash-code используется массив dword, максимальная длина которого равна 256 элементам (или 4 * 256 = 1024 байт)
Замечание 1: инструкция movzx загружает байт в самую младшую часть регистра, а остальные биты регистра заполняет нулём.
Замечание 2: команда not, в отличие от neg, не влияет на значения флагов, поэтому можно сначала сравнить, потом инвертировать, а затем воспользоваться результатами сравнения.
Хорошо, смотрим далее:

 4141BB		lea     ecx, [ebp+8]		 ; как можно проверить в SI, грузится адрес

						 ; CString-класса с именем пользователя

 4141BE		call    CString::MakeUpper(void) ; преобразуется в верхний регистр

 4141C3		lea     ecx, [ebp+0Ch]           ; адрес CString с E-Mail

 4141C6		call    CString::MakeUpper(void) ; преобразуется в верхний регистр

 4141CB		mov     eax, [ebp+8]             ; адрес CString с именем пользователя

 4141CE		push    6347A267h                ; помещается в стек initial_seed

 4141D3		push    dword ptr [eax-8]        ; помещается в стек длина имени пользователя

 4141D6		push    eax                      ; помещается в стек адрес имени пользователя

 4141D7		call    get_hash                 ; вызов всё той же get_hash

 4141DC		add     esp, 0Ch                 ; очистка стека

 4141DF		push    eax                      ; хм, вычисленный hash снова помещается в стек

						 ; как initial_seed

 4141E0		mov     eax, [ebp+0Ch]           ; длина E-Mail

 4141E3		push    dword ptr [eax-8]        ; адрес E-Mail

 4141E6		push    eax                      ; помещается в стек

 4141E7		call    get_hash                 ; снова call get_hash

 4141EC		add     esp, 0Ch                 ; очистка стека

 4141EF		xor     ecx, ecx                 ; обнуление счётчика

 4141F1		mov     dl, al			 ; в dl - младший байт вычисленного

						 ; двойного hash-code по имени и E-Mail

 4141F3		and     dl, 1Fh                  ; оставляем младшие 5 бит

 4141F6		cmp     [ebp+ecx-20h], dl        ; сравниваем с очередным преобразованным

						 ; символом из буфера РН

 4141FA		jnz     short loc_41423B         ; если не равны - переход на приснопамятный адрес

 4141FC		shr     eax, 5                   ; сдвигаем has_code на 5 бит вправо

 4141FF		inc     ecx                      ; накидываем счетчик

 414200		cmp     ecx, 7                   ; счётчик < 7 ?

 414203		jl      short loc_4141F1         ; если да - следующий цикл

Я надеюсь, что после всех моих комментариев уже должно быть понятно, как написать Key Generator. Он должен преобразовывать имя пользователя и E-Mail в верхний регистр, затем с помощью get_hash вычисляется по ним hash, далее пятёрки бит складываются в некоторый буфер, он дополняется до 16 байт каким-нибудь значением из допустимых6), в седьмой сивол буфера помещается код 0x42, затем считаем hash по нашему заполненному таким хитрым образом буферу (опять с помощью get_hash), оставляем от него младшие 5 бит, помещаем их в 7 символ, и, наконец, делаем обратное преобразование из кодов в удобочитаемые символы.
Однако, для полноценного crackа нам необходимо иметь массив Array в нашем KeyGenerator. Размер этого массива составляет около 1 Kb. Можно ,конечно, набить его вручную, и, когда я окончательно впаду в старческий маразм, я именно так и буду поступать. А пока я написал небольшую процедуру, которая сильно облегчит мне жизнь.

int

make_array(void)

{

/* эти два адреса известны из IDA */

  const int a1 = 0x42cc98,        // адрес первого байта массива Array

            a2 = 0x42d098, 	  // 

            offset = 0x2b898;     // смещение на первый байт массива Array в .exe файле - выясняется в hex-editorе

  const char *progName = "reget.exe", // жертва

             *outFile = "out.c";      // куда выход складывать будем

  FILE *out;

  int inHandle;

  long f_pos;

  unsigned char *buffer = NULL;

  inHandle = _open(progName, _O_BINARY | _O_RDONLY);

  if (-1 == inHandle)

  {

    fprintf(stderr, "Cannot open %s\n", progName);

    return -1;

  }

  f_pos = lseek(inHandle, offset, SEEK_SET);

  if (f_pos != offset)

  {

    fprintf(stderr, "Cannot seek, error=%d\n", errno);

    close(inHandle);

    return -2;

  }

  buffer = (unsigned char *)malloc(1 + (a2 - a1));

  if (a2 - a1 != read(inHandle, buffer, a2 - a1))

  {

    fprintf(stderr, "Cannot read %d bytes from %s\n", a2 - a1, progName);

    free(buffer);

    close(inHandle);

    return -3;

  }

  close(inHandle);

  if (NULL == (out = fopen(outFile, "w")))

  {

    fprintf(stderr, "Cannot create out file %s\n", outFile);

    free(buffer);

    return -4;

  }

  fprintf(out, "const unsigned char array[] = {\n");

  for (f_pos = 0; f_pos < a2 - a1; f_pos++)

  {

    fprintf(out, "0x%x%c%c", buffer[f_pos], 

      f_pos != a2 - a1 - 1 ? ',' : ' ',

      (f_pos & 7) == 7 ? '\n': ' ');

  }

  fprintf(out, "\n};\n");

  fclose(out);

  free(buffer);

  return 0; // success

}

Note Если вы не понимаете, что делает данная (не самая сложная из писанных мною) процедура, лучше Вам бросить занятия Reverse Engeneeringом и сначала попробовать научиться простому Engeneeringу, потому что довольно часто приходится писать подобные болванки (например, в настоящий момент я занят ломкой программки XLNT (высокоуровневый сетевой коммандный язык сценариев для NT), и мне уже пришлось написать таких процедур штук примерно 6, а конца сему действу ещё и не видно...), и как Вы будете дальше жить - мне неизвестно 7)
Note 2 Функция, конечно, не Бог весть какая, тем не менее имеет смысл использовать её для выгрузки массивов данных в других проектах. Для этого нужно заменить нужными значениями a1, a2, offset, progName, и, возможно, outFile, а также заменить название и/или тип массива в первом fprintf


Кстати, чтобы сделать crack и написать этот несчастный KeyGenerator, у меня ушло около 4х с половиной часов, а на написание данного опуса - два дня...

  Ломать - не строить
Народная мудрость


1). Я лично с удовольствием прочитал бы несколько essays о взломе программ, защищённых dongles; но ведь их нету в природе :-(
Back
2). Не думаю, что ещё остались люди, живущие здесь в счастливом неведении, что местное правительство на протяжении уже 80 (восьмидесяти) лет проводит геноцид против местного населения (как они его в последнее время стали называть презрительно-брезливо - "электорат". Если же такие люди ещё остались - мне не хочется общаться с ними дабы объяснять совершенно очевидные вещи...
Back
3). На самом деле, это чрезвычайно простой случай. Я просто решил, что, поскольку программа запускается и под Windows 95, и под NT (а у меня стоит Window NT 4.0 Workstation Rus), то в ней используются функции, использующие обычный однобайтовые строки - поэтому я ловлю функции API с суффиксом A. Если бы был .exe special for NT, имело бы смысл ловить функции с суффиксом W (Wide Char).
В тяжёлых случаях можно порекомендовать следующие варианты:

ShowWindow - вызывается, когда нужно показать какое-либо окно. Так как все controls, присутсвующие в, скажем диалоговом окне, в свою очередь являются опять таки окнами, которые также должны быть show при вызове диалогого окна, то вызовов ShowWindow может быть чересчур много, например у меня множество раз отлавливался этот вызов из функции ImageList_Add в COMCTL32.DLL, так что я не стал использовать этот метод.
Запустить примочку трассирования оконных сообщений от Visual C++ - Spy++, найти в ней диалоговое окно, посмотреть в ней HWND (внутренней идентификаторы) окон и далее перехватывать либо wm_gettext, либо, если программа переопределяет оконную процедуру controlов и сама обрабатывает нажатия кнопок, wm_keydown. Того же можно достичь не выходя из SI:
task - перечисляет имена загруженных процессов (к сожалению у меня на NT эта команда выдаёт "No LDT" - я так и не смог победить, да и не смертельно это при наличии других инструментов)
hwnd имя_процесса
в появившемся списке отыскиваем что-нть типа Edit etc, затем
bmsg идентификатор_окна wm_сообщение_для_отлова
Также в особо тяжёлых случаях может помочь следующее:
Находим одним из вышеперечисленных методом идентификатор самого диалогого окна,
ставим bmsg идентификатор_окна wm_command. Всплываем по нажатию какой-либо кнопки.

Если нужно отловить не банальный MessageBox, а что-нть похлёще, могут помочь:

DialogBoxIndirectParam, причём с этой функцией вообще очень интересно. Она вызывается для создания модального диалога. Но ! Дело в том, что по крайней мере на NT есть аж три такие функции (95 под рукой нет, проверить не могу). Как вы могли ожидать, есть DialogBoxIndirectParamA для ASCII-строк, DialogBoxIndirectParamW для Wide-Chars, и ещё есть недокументированная (возможно, лишь в моей документации - я использую для помощи по Win32 API Visual C++ 5.0) - DialogBoxIndirectParamAorW - и она очень часто срабатывает в программах, написанных с применением MFC.
Для создания немодального диалога используется CreateDialogIndirectParam с аналогичным набором суффиксов
SetWindowPos - используется для изменения размера, позиции и Z-порядка окон. К ней применимы все замечания, относящиеся к ShowWindow
Можно также найти строку, которая выдаётся в диалоговом окне. Если она лежит в сегменте данных, можно просто поставить bpm адрес_строки. Но очень часто такие строки расположены в сегменте ресурсов. Малоизвестный факт, но на строки ресурса также можно ставить bpm. Краткое пояснение: все функции API для манипуляции ресурсами (LoadString, LoadResource etc) принимают в качестве первого аргумента HINSTANCE, который есть ни что иное, как указатель на начало отображенного в память .exe файла. Далее такие функции обрабатывают PE (или NE) заголовок загруженного модуля, находят сегмент ресурсов, и по переданному идентификатору находят нужный ресурс, который затем просто копируется во вновь выделенную память. Есть и свои грабли: ресурсы обычно (точнее, почти всегда) хранятся в Unicode. Я поступаю так - в IDA всегда указываю загружать секцию ресурсов, а затем помогает binary search с нужными значениями.
Если лениво выискивать адрес ресурса - можно поставить bpx на LoadString и смотреть её параметры и возвращаемые значения (и ещё неизвестно, какой способ более трудозатратный)

Если программа никак не реагирует на неправильный Serial Number, можно попробовать найти строку, которая могла бы выдаваться на правильный. Я понимаю, что звучит чересчур расплывчато и никто, кроме авторов программы ,точно не знает, как она выглядит (да и авторы, наверняка, забыли уже), но всё же это лучше, чем совсем ничего. Здесь может помочь только обострённая голодом и хронической нищетой интуиция...
Ну и если ничего из вышеперечисленного не помогло - в конце концов, меня же зовут не Иисус Христос...
RTFM)
Back
4) Ну что поделать - ленив я. Вам никогда не говорили, что лень - двигатель прогресса ?
Back
5) есть замечательное эссе на fravia о том, как можно настроить IDA. Если Вы умеете настраивать IDA, вполне вероятно, что некоторых элементов у Вас не будет, или будут какие-нть другие. Однако в последнем случае Вы, видимо, не нуждаетесь в моих подсказках о именовании имён функций, их аргументов и проч.
Back
6) Я выбрал индекс сивола "$" - интересно, что бы сказал на это доктор Фрейд ?
Back
7). Это только в западных высокохудожественных полуфантастических фильмах всё выглядит красиво и эстетично - сидит этакий хукер и ломает программу, которая представлена на гигантском дисплее в трёхмерном виде (видимо, для наглядности). В жизни, как всегда, всё значительно прозаичнее - нужно много работать и иметь неитощимое терпение - короче, чтобы достичь вершины хотя бы невысокой кочки в Reverse Engeneering, Вы должны быть совсем ненормальным человеком, например, как я...
Back


RTFM) Если вторую неделю ничего не получается, прочти ,наконец, документацию. Я не верю в то, что человека ,позавчера впервые увидевшего контупер, можно научить ломать программы "за 21 день". Для этого необходимо как минимум (кроме подразумевающегося обязательного знания ассемблера) ещё и знание Win32 API хотя бы на уровне прикладного программиста, а также знание того framework, на котором написана предполагаемая жертва взлома (Delphi VCL + M$ MFC - минимум), и соответствующих языков программирования.

Дальше


Образование на Куличках