BugTraq.Ru
Русский BugTraq
http://www.bugtraq.ru/library/programming/uleadtrial.html

Техника анализа программных защит на примере Ulead COOL 3D v3.5
Chingachguk /HI-TECH
Опубликовано: dl, 12.04.03 07:57

Что собираемся делать ?

  Объект исследования: программа Ulead COOL 3D, триальная защита

  Инструменты: Soft-Ice (под '98), немного мозгов

  Цель: изучение win32-программ, обладающих защитой от отладки

  Примечание: ничего особо нового, простая техника, SEH, etc...

  Ниже я собираюсь исследовать защиту этой проги, которая позволяет пользователю использовать ее функциональность только в течении календарного месяца (30 дней) или купить возможность ее использования неограниченно во времени (вид входного окошка после инсталляции, инсталлятор UC3D35TBYB_E.exe):

 

  Что она вообще делает ? Примерно - это какой-то графический редактор (вид после нажатия кнопки "TRY"):

 

 Таким образом в первом приближении сразу после инсталляции данная программа позволяет полноценно работать, но в стартовом окошке напоминает о том, что до конца использования осталось 30 дней ("Remaining Days - 30") и предлагает, видимо, купить разрешение на полное использование ("BUY").

 Поисследуем реакцию программы на изменение системной даты. Поменяем текущую дату на один день вперед:

 

 Ага, программа предупреждает нас, что осталось на 1 день меньше. Если перевести дату ровно на месяц вперед, то кнопка "TRY" становится недоступна и число оставшихся дней равно 0:

 

  Кнопка "TRY" недоступна. Попробуем поймать программу на том, как она закрывает для доступа кнопку и поисследовать, чем она при этом руководствуется. Win32-приложение обычно делает это при помощи API-функции EnableWindow:

 EnableWindow(Id_Объекта_Окна,0/1-закрыть/открыть Объект)

 Устанавливаем на машине дату таковой, чтобы программа решила, что осталось 0 дней для использования, устанавливаем breakpoint в SoftIce:

 bpx EnableWindow

  запускаем программу, получаем break и видим в отладчике место вызова (оказалось, код принадлежит некоей xsystem.dll, которую можно найти в том каталоге, куда инсталлировалась программа Ulead COOL 3D):

026D7F5F:
  mov  edi,User32!EnableWindow
  push edi
  mov  edi,esp
  pushad
  lea  esi,[ebp+2C63h]
  lodsb
  cmp  al,90h
  jnz  026D7F8A
  push 03E8h
  push dword ptr [edi+8]
  call [ebp+2B50h]
  push 0 ; FALSE - disable button "TRY"
  push eax
  call [ebp+2B54h] ; Точка вызова EnableWindow
026D7F8A:
  popad
  ...

  Итак, программа анализирует первый байт некоторой структуры (lea esi,[ebp+2C63h], lodsb, cmp al,90h), сравнивает его с 90h, если он оказывается таковым, то кнопка "TRY" становится недоступной (переход на метку 026D7F8A). Попробуем проверить гипотезу о том, что все, что нужно для "освобождения" программы от проверки даты - это сделать прямо здесь достпуной кнопку "TRY". Попробуем сделать это прямо в отладчике: установим break на адрес 026D7F5F:

 bpx LoadLibraryA ; Ждем загрузки xsystem.dll

  ...

 bc 0 ; clear breakpoint at LoadLibraryA

 bpx 026D7F5F ; Set breakpoint

  Ctrl+D...

  и попытаемся дождаться срабатывания break'а и "руками" поменять в отладчике значение регистра al сразу после lodsb: но вместо того, чтобы получить break на этом (026D7F5F) адресе, мы получаем GPF... Видно (в Sice'е), что машина пытается исполнять какой-то мусор в xsystem.dll.

  ...Так мы выяснили, что программа не только контролирует системную дату, но и сопротивляется нашим попыткам ее трейсить ;) Наверное, программа как-то динамически модифицирует свой код - например по адресу 026D7F5F - и break, который мы пытаемся установить, мы устанавливаем в то место, которое впоследствии будет изменено. Скажем, самораспаковывающийся или саморасшифровывающийся код может быть причиной тому. Попробуем как-то обойти это ограничение и вспоминаем про то, что программа неминуемо должна узнать системную дату - ведь ей необходимо ее сравнить с чем-то типа даты инсталляции. Win32-приложение обычно делает это при помощи API-функции GetSystemTime:

  GetSystemTime(offset структуры типа SYSTEMTIME)

SYSTEMTIME STRUCT wYear WORD ? wMonth WORD ? wDayOfWeek WORD ? wDay WORD ? wHour WORD ? wMinute WORD ? wSecond WORD ? wMilliseconds WORD ? SYSTEMTIME ENDS

  Устанавливаем break на эту API-функцию:

 bpx GetSystemTime

  И обнаруживаем, что вызов произошел из следующего места (опять xsystem.dll):

026D6DE3: lea esi,[ebp-100h] push esi call [ebp-4] ; call GetSystemTime pop ebx 026D6DEE: movzx edx,word ptr [esi] ; d esi -> D3 07 03 00 05 00 1C ; edx= 07D3h shl edx,8 mov dl,[esi+02] ; Месяц shl edx,8 mov dl,[esi+06] ; День ; edx=07D3031C push edx

  Уже видно, как программа работает с датой: код по адресу ~026D6DE3 получает косвенным call'ом дату от Win, собирает в 32 битах edx год (07D3h=2003), месяц (03) и день (1Ch=28) - 28 марта 2003 года в данном случае, делает что-то еще и управление попадает к коду в ~026D7F5F, который уже знает, в валидном ли интервале дат мы находимся и закрывает кнопку "TRY", если это не так. Но мы помним, что нужно все же попробовать подменить результат манипуляций с датой прямо перед принятием решения о disable кнопки "TRY". Ранее нам сделать это не удалось - видимо, после загрузки dll в память ее (?-пока не знаем) код самомодифицируется и мешает ставить полноценные break'и. Однако есть надежда, что в момент выполнения кода по адресу ~026D6DE3 - кода, спрашивающего дату у Win - код в ~026D7F5F расшифрован и доступен для отладки. Повторяем попытку установить break на 026D7F5F, но уже после того, как мы попали в 026D6DE3 и опять получаем GPF ! Почему, ведь код уже явно виден в отладчике ? Может, дело в том, что адрес API EnableWindow прописывается динамически прямо в команду mov edi,const_addr ? Пробуем установить break на команду загрузки регистра esi адресом структуры (lea esi,[ebp+2C63h]) и ... оказываемся в нужном месте:

026D7F5F:
  mov  edi,User32!EnableWindow
  ...
026D7F68:
  lea  esi,[ebp+2C63h]
  lodsb ; al = 90h если запрещенный период, иначе всегда 6Ah
  cmp  al,90h
  jnz  026D7F8A
  push 03E8h
  push dword ptr [edi+8]
  call [ebp+2B50h]
  push 0 ; FALSE - disable button "TRY"
  push eax
  call [ebp+2B54h] ; Точка вызова EnableWindow
026D7F8A:
  popad
  ...

  Ура !? Что ж, проверим: модифицируем (в отладчике) регистр al после лодсб и отпускаем программу...

 

  ...Так, кнопка "TRY" доступна (хотя Remaining Days = 0). Жмем ее и видим всего лишь вот этот MessageBox:

 

  Вот и познакомились ;) Оказывается, защиту зовут "xLok". А разрешить жать на кнопку - не значит получить доступ к функциональности ;(

  Но что если данными для проверки валидности даты для кода в ~026D7F5F является не байт, а слово или даже что-то большее ? На такую мысль может навести пара команд "lea esi,[ebp+2C63h], lodsb" вместо "cmp byte ptr [ebp+2C63h],90h", хотя это может быть "приколом" компиллятора. Внимательнее поизучаем байты в структуре, адресуемой по [ebp+2C63h] для двух дат: для даты в интервале работы и вне ее:

Работа возможна: 6A 01 FF 73 08 FF 95 54 ... Работа запрещена: 90 90 FF 73 08 E8 9D 00 ...

  Установим дату вне интервала работы, опять остановимся в отладчике в точке 026D7F68, поменяем "неверные" байты на "верные" (90 90 FF ... -> 6A 01 FF ... ) и "отпустим" программу. MessageBox'а от xLok'а больше нет, но есть банальный GPF...

  Следовательно, с большой вероятностью можно заключить, что защита выполняет (динамически) расшифровку какой-то части рабочего кода и снятие всех ее проверок после расшифровки приводит к неизбежному краху, так как этот код расшифровывается неверно вне интервала работы программы. Видимо, защита использует дату (точнее, не саму дату, а некоторую функцию от нее, которая постоянна на интервале работы программы) как ключ шифрования части своего кода. Проверим это: в этом случае будет достаточно подменить системную дату, которую защита спрашивает у Win. В отладчике это легко сделать: опять устанавливаем break на GetSystemTime, возвращаемся в xsystem.dll к коду в 026D6DEE и "патчим" значение структуры SYSTEMTIME (или значение edx чуть ниже) датой инсталляции... Все OK, программа отлично работает и ничего не запрещает.

  Здорово, вроде бы осталось поменять команды инициализации edx (код в 026D6DEE и ниже) на что-то вроде таких:

026D6DEE: ; Было так: ; movzx edx,word ptr [esi] ; d esi -> D3 07 03 00 05 00 1C ; shl edx,8 ; mov dl,[esi+02] ; Месяц ; shl edx,8 ; mov dl,[esi+06] ; День ; А будет так: mov edx,Const_Valid_Date nop ... nop push edx

  и все будет работать ? Пробуем найти опкоды оригинальных инструкций в DLL:

Команды Опкод movzx edx,w [esi] 0F B7 16 shl edx,8 C1 E2 08 mov dl,[esi+2] 8A 56 02 ...
и не находим их ! Этого следовало ожидать еще тогда, когда стало ясно, что код самомодифицирующийся. Что делать дальше ? Может быть, имеет смысл попробовать найти расшифровшик, проанализировать его алгоритм и "пропатчить" нужные байты в соответствии с алгоритмом ? Пусть алгоритм дешифровщика состоит в наложении xor-последовательности на байты DLL:
Команды Опкод Шифро-Опкод в DLL (Опкод) xor (Шифро-Опкод в DLL) movzx edx,w [esi] 0F B7 16 B5 2A 1A BA 9D 0C shl edx,8 C1 E2 08 C4 45 2E 05 A7 26 ...

  В этом случае не составило бы труда подготовить те "новые" опкоды, которые мы собираемся поместить на место старых - нужно выполнить над ними операцию с той же xor-последовательностью - как это делает пока неизвестный нам расшифровщик:

Команды Опкод_новый (Опкод) xor (Шифро-Опкод в DLL) Результат mov edx,07D3031Ch BA 1C 03 D3 07 BA 9D 0C 05 A7 00 81 0F... nop ...

  Помещаем в файл xsystem.DLL по смещению (в файле) 6DEE подготовленные таким образом байты и пытаемся смотреть, что же сделал с ними расшифровщик - а в результате видим такой вот MessageBox:

 

  Да, защите удалось не только скрыть результат работы расшифровщика (если он вообще отработал) но и определить модификацию собственного кода. Неплохо ! Как же ему это удалось ? Попробуем "отловить" его на обращении к памяти, содержащей измененные коды:

 bpm 026D6DEE

  и, действительно, наблюдаем следующий код:

026D510C: push esi push ecx push eax ... 026D5115: mov ecx,49A0h xor edx,edx xor eax,eax @@GetCRC: lodsb ; esi=026D6DEFh add edx,eax dec ecx jnz @@GetCRC ... xchg edx,eax ; eax->1FBFFAh pop edx pop ecx pop esi add dword ptr [esp],5 026В515A: ret

  Очень интересный код ! Похоже на подсчет CRC (возвращает в eax), причем регион подсчета CRC включает и критичные для нас адреса в ~026D6DEE. Следовательно, прежде чем воевать с их расшифровщиком, нам нужно победить подсчет контрольной суммы. Открыты ли байты подпрограммы подсчета CRC в DLL ?

026D510C: Команда / Опкод push esi / 56h push ecx / 51h push eax / 52h ...

  Ищем их в файле DLL по смещению 510Ch и находим ! Следовательно, мы можем без особой боязни заменить оригинальную подпрограмму подсчета CRC на что-то вроде:

@@NewCRCProc: mov eax,1FBFFAh add dword ptr [esp],5 ret @@EndOfNewCRC: db (026D515Bh-026D510Ch) - (@@EndOfNewCRC-@@NewCRCProc) dup(90h) @@NewCRCProcDone:

  При этом мы вернем нужное где-то дальше CRC (в eax) без всякого подсчета - что зря процессор гонять, хитро вернемся назад (add dword ptr [esp],5) и забьем все остальное место nop'ами. Патчим таким образом DLL и получаем ... GPF ! Оба-на, защита перестала детектировать сосбтвенную модификацию но и программа "рухнула" ;( Неужели мы неправильно составили новый код подсчета CRC ?... Стоп, а ведь мы еще раньше поменяли байты, относящиеся к получению даты (в ~026D6DEE) ! Установим break на API GetSystemTime, вернемся к коду получения даты в DLL и видим, что расшифровщик явно сделал не то, что мы ожидали:

Команды_новые Результат работы расшифровщика / Опкод 026D6DEE: mov edx,07D3031Ch sbb byte ptr [esi] / 1D 82 1E 1D nop mov bl,7Fh / B3 7F ...

  Следовательно, алгоритм расшифровщика производит расшифровку байт в ~026D6DEE в зависимости от исходных байт. Т.е. это не простое наложение XOR-последовательности; расшифровка каждого байта зависит от того, как был расшифрованы все предидущие. Алгоритм расшифровщика нам по-прежнему неизвестен и мы не можем даже пытаться осуществлять атаку на его шифр. Откатим пока эти изменения, оставив лишь получение CRC и продолжим следить в отладчике за ходом выполнения программы защиты после возврата из подпрограммы подсчета CRC:

026D5366: sub eax,1FBfFAh ; если пришло верное CRC=1FBfFAh, то в eax будет 0 pop ebx jz 026D5382 ... 026D5382: mov [ebp+3C16h],ebx mov edx,[ebp+3C36h] push dword ptr fs:[0] mov fs:[0],esp push eax push ebp jmp 026D53A1 ... 026D53A1: pushfd invalid (0F1h) db 0BEh,0F0h,...

  Если продолжить в отладчике пошагово трассировать код с 026D53A2, то налетаешь на GPF. Но сразу бросаются в глаза явно вычурные для win32-кода команды работы с сегментными регистрами (mov fs:[0],esp). Если уж приложение взялось за это, то явно не с добрыми намерениями ;) На самом деле таким образом в windows пользовательским программам дозволяется установить свой обработчик исключительных ситуаций и в случае таковых получить управление и попытаться их обработать (интересно, кому-нибудь удавалось обрабатывать их инчае как выдать сообщение "Программа выполнила ... и будет закрыта" ?...). Далее мы увидим, что иногда такой обработчик может выполнять некоторые "полезные" действия.

  В двух словах о таких обработчиках. Эта штука называется "SEH" - structure exeption handler. Для того, чтобы обработчик правильно получил управление, необходимо:

- поместить в стек смещение обработчика; - сохранить смещение старого обработчика на том же стеке; - записать по селектору из FS, смещению 0 содержимое esp

  Сам обработчик вызвается в C-формате, при этом получая кучу параметров - указатель на структуру регистров в момент исключения и т.п.:

SEHproc proc C pExcept: dword, pFrame: dword, pContext: dword, pDispatch: dword PrintException pExcept ... ret SEHproc endp

  Но самое главное - это то, что он может вернуться в достаточно произвольное место кода (включая и вызвавшее исключение), при этом уведомив диспечер о том, что исключение им обработано (например, mov eax, ExceptionContinueExecution перед выходом) и то, что он имеет право записи в сегмент команд (это необходимо для исправления "дефектного" кода). Таким образом, обработчик исключения защиты явно собирается именно этим заниматься: подать управление на неверный опкод, поймать expeption, дешифровать некоторые байты, вернуться опять на неверный опкод (не обязательно первый), и так до тех пор, пока необходимый код не будет полностью расшифрован.

  Где же защита разместила свой обработчик ? Нет нужды копаться в командах, предшествующих коду установки SEH (~026D5382), можно просто посмотреть стек в этот момент (скажем, после выполнения mov fs:[0],esp):

 d esp

  Оказыватся, адрес у обработчика равен 026D5040, а вот и он сам:

026D5040: pushad mov edx,0C8EC918Bh call 026D504B 026D504B: pop edi ; edi-> 026D504B, получение текущего смещения mov esi,36h add esi,edi ; esi=026D5081h mov ecx,94h push esi sub esi,9 mov edi,esi xor eax,eax 026D5061: lodsb xor eax,edx rol edx,5 imul edx,edx,0FB712715h add eax,0AB358CDFh stosb dec ecx jnz 026D5061 026D5078: db 0F4h,041h,0ECh,076h,03Ah... 026D5081: ...

  Итак, обработчик SEH сразу после получения управления начинает что-то расшифровывать с адреса 026D5078 и ниже довольно незамысловатым алгоритмом, немного странно реализованным (вместо add eax,0AB358CDFh достаточно add al,0DFh). Размер байт для дешифровки невелик (mov ecx,94h). Дожидаемся возможности установить break в 026D5078 - после первого же stosb там появлется pop esi и после срабатывания break'а видим весь расшифрованный код:

026D5078: pop esi mov eax,[esp+28h] mov [eax+4],esi ; <-026D5081h popad 026D5081: push ebp mov ebp,esp push esi,edi,ebx,ecx,edx mov eax,[ebp+10h] mov edi,[eax+0B8h] 026D5093: mov edx,2 026D5097: jmp 026D5099 026D5099: call 026D509E 026D509E: pop ebx ; ebx<-текущее смещение 026D509E add ebx,0FFFFFFE3h ; ebx=026D5081h add edx,[eax+0B4h] mov [ebx+12h],edx ... mov ebx,0BB51B5E3h test esi,esi jz 026D50CE xor [esi],bl 026D50CE: cmp byte ptr [edi],09Dh jz 026D50DF xor [edi],bl or dword ptr [eax+0C0h],100h 026D50DF: xor ebx,ebx mov [eax+4],ebx, mov [eax+8],ebx, mov [eax+10h],ebx mov dword ptr [eax+18h],101h mov [edx],edi xor eax,eax push eax, push eax, dec eax, push eax, mov eax,ofs Kernel!FlushInstructionCashe call eax xor eax,eax pop edx,ecx,ebx,edi,esi,ebp ret ; Последняя расшифрованная команда 026D510Ch:

  Что же любопытного в раскрывшемся коде ? Прежде всего обратим внимание на характерный код в 026D5081 - push ebp, mov ebp,esp. Это явно оформлено начало какой-то процедуры, и она заканчивается в 026D510B. Командами

mov eax,[esp+28h] mov [eax+4],esi ; <-026D5081h

  декриптор определил адрес нового обработчика SEH - теперь это будет код в 026D5081. При следующих исключениях всегда будет вызываться именно эта процедура, что-то расшифровывающая командами:

xor [esi],bl 026D50CE: cmp byte ptr [edi],09Dh jz 026D50DF xor [edi],bl or dword ptr [eax+0C0h],100h 026D50DF:

  Но в данный момент код получения даты (в ~026D6DEE) еще не расшифрован. Может быть, только что расшифрованная SEH-подпрограмма 026D5081 и есть ее декриптор ? Устанавливаем break на начало 026D5081:

 bpx 026D5081

  и наблюдаем, как раз за разом управление переходит к SEH-обработчику. Он явно расшифровывает какой-то код и хотелось бы остановить программу в тот момент, когда он полностью закончит свою работу. Довольно утомительно было бы жать Ctrl-D столько раз, поэтому я написал небольшой код, заменяющий собой распаковщик SEH-обработчика:

; Новые команды по адресу 026D5040 (первоначальный SEH-обработчик) @@NewBytes: pushad call @@GetOfsNewBytes @@GetOfsNewBytes: pop esi add esi,offset @@ArtBytes - @@GetOfsNewBytes mov eax,[esp+28h] mov [eax+4],esi popad db 0EBh ; jmp на адрес 026D5081 db (@@NewBytes+40h) - $ @@ArtBytes: pushad call @@GetOfs @@GetOfs: pop eax inc word ptr [eax+(offset CounterCalls - offset @@GetOfs)] mov ax,word ptr [eax+(offset CounterCalls - offset @@GetOfs)] popad db 0EBh ; jmp на адрес 026D5081 db (@@NewBytes+40h) - $ CounterCalls dw 0 @@DoneNewBytes: LastNops db (@@DoneOldBytes - @@OldBytes) dup (90h) ; nop only

  Новый код в отличии от старого не декриптует SEH-обработчик (это можно сделать прямо в файле xsystem.dll и не обременять код графического редактора дешифрацией), а устанавливает новый адрес SEH-обработчика - это будет подпрогрммка @@ArtBytes, которая только лишь вычисляет число call'ов SEH'а и передает jmp'ом управление на старый адрес 026D5081. Теперь в Sice'е можно ставить условный break на команде mov ax,word ptr [eax+(offset CounterCalls - offset @@GetOfs)]:

 bpx IF (AX==0x100)

  Заранее, конечно, число call'ов SEH'а неизвестно, но простым методом деления "отрезка" пополам можно довольно быстро добраться до конца работы SEH-обработчика. Трейсим последний вызов по SEH-обработчика и видим, что:

  - Даже когда он закончил свою работу, код получения даты (в ~026D6DEE) нерасшифрован;

  - Последний свой xor SEH-обработчик выполнил с адресом 026D540Ch:

xor [esi],bl ; edi=026D540Ch, там 75h, bl=3Eh 026D50CE: cmp byte ptr [edi],09Dh ...

  Таким образом, SEH-обработчик-дешифровщик не расшифровал код получения даты. Но что за байты появились по адресу 026D540Ch ?...

026D540C: popfd call 026D5412 ; get current offset 026D5412: pop esi add esi,02Eh mov edi,esi mov ecx,0CF9h mov edx,9548E9F7h 026D5425: lodsd xor eax,edx add eax,54ADF121h xor eax,ecx rol edx,5 imul edx,edx,70C967BEh xor edx,ecx stosd dec ecx jnz 026D5425 jmp 026D5450 026D5440: ...

  О, старые знакомые ! :) Опять переносимый (call 026D5412, pop esi) код дешифрует нижележащие байты. Ставим break в конец цикла, дожидаемся его окончания и с нетерпением смотрим ... нет, не байты после 026D5440, а байты получения даты - ~026D6DEE. Они расшифрованы, теперь - ура - мы знаем кто их декриптует и этот кто-то (026D540C - последний дешифровщик) крайне мал, чтобы возится с его расшифровщиком (обработчик SEH'а) - проще вбить его, уже нам известный на то же место, где сейчас лежит его зашифрованный собрат. Только тогда нужно будет дезактивировать его расшифровщик (обработчик SEH'а) - а то он все испортит ;) Как же это сделать ? Вернемся к моменту инсталляции SEH'а:

... 026D5382: mov [ebp+3C16h],ebx mov edx,[ebp+3C36h] push dword ptr fs:[0] mov fs:[0],esp push eax push ebp jmp 026D53A1 ... 026D53A1: pushfd invalid (0F1h) db 0BEh,0F0h,...

  Что будет, если вместо invalid-команды, намеренно сделанной для генерации исключения, сделать переход на дешифровщик в 026D540C, который теперь, как мы планируем, будет записан нами в открытом виде прямо в DLL ? SEH-обработчик явно не вызовется, осталось опасность потерять стек при таком прыжке. Но парная команда к pushfd - последняя команда перед вызовом SEH-обработчика через исключение - popfd:

026D540C: popfd call 026D5412 ; get current offset ...

  как будто бы явно написана разработчиками защиты с целью восстановить состояние процесса после отработки SEH'а. Так и поступим. Остался последний момент: где разместить код, который поменяет пресловутые команды получения даты на одну спокойную mov edx,Const_Valid_Date и парочку nop'ов ?

  Ниже привожу свое решение, возможно немного надуманное. Поскольку этот код хотелось бы разместить после отработки расшифровщика 026D540C, т.е. тогда, когда уже код от 026D5440 и далее расшифрован, то он должен быть после последней команды расшифровывающего цикла:

026D540C: ... dec ecx jnz 026D5425 ; Здесь должен быть код, вставляющий команду mov edx,Const_Valid_Date jmp 026D5450 026D5440: ...

  Вместо двух байт короткого jmp'а сложновато сделать такое, но если немного пооптимизировать код расшифровщика и вспомнить, что после удаления старого кода подсчета CRC осталась куча места, то можно написать следующее:

  Этой командой мы закроем выполнение SEH'а:

; Этот jump мы поместим на место всяких invalid'ов, чтобы SEH не вызывался ; Сам SEH-обработчик оставим в покое @@NewJumpAt53A2: jmp $+(540Ch-53A2h) @@NewJumpAt53A2Done:

  Это код нового, открытого декриптора по адресу 026D540C:

@@Decryptor2: popfd call @@GetOfsDecryptor2 @@GetOfsDecryptor2: pop esi ;; db 081h,0C6h,02Eh,000h,000h,000h ; add esi,2Eh - в оригинале ;; был именно вариант из 6-ти байт add esi,2Eh mov edi,esi mov ecx,0CF9h mov edx,9548E9F7h @@DecryptLast: lodsd xor eax,edx add eax,54ADF121h xor eax,ecx rol edx,5 imul edx,edx,70C967BEh xor edx,ecx stosd ;; dec ecx ;; jnz @@DecryptLast loop @@DecryptLast ; Переходим на кусок кода, который разместили внутри подпрограммы ; подсчета CRC mov si,510Ch+(offset @@PatchAt026D6DEE-offset @@NewCRCProc) jmp esi @@Decryptor2Done:

  Это новый код подсчета CRC вместе с подпрограммой модификации кода получения даты в 026D6DEE:

; ; New subprogram 026D510Ch - calculate CRC + patch code at 026D6DEE ; @@NewCRCProc: mov eax,1FBFFAh add dword ptr [esp],5 ret @@PatchAt026D6DEE: ; ; Необходимо: ; 1. Пропатчить байты в ~026D6DEE - подменить дату ; 2. Пропатчить байты в ~026D6E0B - подменить дату для вычисления Remaining Days ; 3. Вернуться в 026D5450 ; ; Сделать mov edx,07D3031Ch ; Year + Month + Day -> 0BAh,01Ch,003h,0D3h,007h mov si,6DF7h mov byte ptr [esi],0BAh ; mov edx,... @@PatchCommand: mov dword ptr [esi+1],07D3031Ch ; Year + Month + Day mov byte ptr [esi+5],90h ; nop ; Пропатчить получение стратовой даты для вычисления Remaining Days - ; - сделать mov edx,07D3031Ch вместо xor eax,7895ADEBh по адресу 6E0Bh mov si,6E0Bh mov byte ptr [esi],0B8h ; mov eax,... @@PatchCommandRemainingDays: mov dword ptr [esi+1],07D3031Ch ; Year + Month + Day ; Возвращаемся mov si,5450h jmp esi

  Вот вроде бы и все ;) Хотя далее есть еще несколько пугающих кусков типа:

026D6C52: lea esi,[ebx-100h] ... call 026D7623 int 62h ; !!! add eax,0FCfC4C4h ...

  которые заставили меня лишний раз заглянуть в Ральфа Брауна, но особой опасности они не представляют ;)

Немного извращений или граффити

  После реализации подмены системной даты и многократных смен системной даты в целях выяснинения особенностей защиты программы можно наблюдать интересный эффект:

 

  xsystem.dll уже изменена, все работает, но вот с числом оставшихся дней что-то не то. Как бы не мешает, но некрасиво... Любопытен тот факт, что обратная замена исходной DLL дела не меняет: число оставшихся дней меняется, но как-то "криво". Есть и такой "баг": если даже взять нетронутую версию программы, перевести дату на некоторое число дней вперед, запустить программу, а следом вернуть дату на место, то при первом запуске программа не позволяет работать (число оставшихся дней равно 0, кнопка "TRY" недоступна).

  Анализ кода xsystem.dll показал, что после того, как системная дата получена программой, выполняется следующий код:

026D6E03: mov ecx,eax mov eax,[ebp+3202h] xor eax,7895ADEBh test eax,eax ...

  - из некоторой области памяти достатется число, которое "расшифровывается" xor-ом и далее (код здесь не приводится) трактуется как дата начала работы программы (инсталляции). Поэтому для завершенности изменений в программе можно поменять команду "xor eax,7895ADEBh" на "mov eax,Const_Valid_Date". В этом случае программа будет выдавать 30 дней до конца trial'а. Но можно немного поизвращаться и оставить число Remaining Days вот таким:

 

 

Исходный текст патчера

  Ниже приводится текст почти полный текст программы, выполняющей описанные выше действия. Некоторые вспомогательные подпрограммы типа GetCL пропущены.

; *** ; Патч проги Ulead COOL 3D 3.5 ;) ; *** .386 .model flat,stdcall option casemap :none ; case sensitive include \masm32\include\windows.inc include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib ; ------------------------------------ ; Read text at end of module for usage GetCL PROTO :DWORD,:DWORD ; ------------------------------------ .data? bRead dd ? ; Байт прочитано bWrite dd ? ; Байт записано hConsole dd ? .code start: cld ; Получаем хэндл консоли invoke GetStdHandle,STD_OUTPUT_HANDLE mov dword ptr hConsole,eax ; *** ; Открываем лог ; *** .data LogFileName db "patch_xsystem01.log",0 .data? LogDescr dd ? .code ; Открыть лог, если есть он уже invoke CreateFile,offset LogFileName,GENERIC_WRITE,FILE_SHARE_WRITE+FILE_SHARE_READ,\ 0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0 mov LogDescr,eax ; Save file descriptor inc eax jnz @@LogFileOpen ; Видимо, нет. Тогда попробовать создать invoke CreateFile,offset LogFileName,GENERIC_WRITE,FILE_SHARE_WRITE+FILE_SHARE_READ,\ 0,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0 mov LogDescr,eax ; Save file descriptor inc eax jnz @@LogFileOpen .data OpenErrorMsg db "Log file not created !",0Ah,0 .code push offset OpenErrorMsg call Write_Log jmp Exit @@LogFileOpen: ; Встать в конец лога invoke SetFilePointer,LogDescr,0,NULL,FILE_END jmp @@SkipBytes ; ; New subprogram 026D510Ch - calculate CRC + patch code at 026D6DEE ; @@NewCRCProc: mov eax,1FBFFAh add dword ptr [esp],5 ret @@PatchAt026D6DEE: ; ; Необходимо: ; 1. Пропатчить байты в ~026D6DEE - подменить дату ; 2. Вернуться в 026D5450 ; ; Сделать mov edx,07D3031Ch ; Year + Month + Day -> 0BAh,01Ch,003h,0D3h,007h mov si,6DF7h mov byte ptr [esi],0BAh ; mov edx,... @@PatchCommand: mov dword ptr [esi+1],07D3031Ch ; Year + Month + Day mov byte ptr [esi+5],90h ; nop ; Пропатчить получение стратовой даты для вычисления Remaining Days - ; - сделать mov edx,07D3031Ch вместо xor eax,7895ADEBh по адресу 6E0Bh mov si,6E0Bh mov byte ptr [esi],0B8h ; mov eax,... @@PatchCommandRemainingDays: mov dword ptr [esi+1],07D3031Ch ; Year + Month + Day ; Возвращаемся mov si,5450h jmp esi @@EndOfNewCRC: db (026D515Bh-026D510Ch) - (@@EndOfNewCRC-@@NewCRCProc) dup(90h) @@NewCRCProcDone: @@Decryptor2: popfd call @@GetOfsDecryptor2 @@GetOfsDecryptor2: pop esi ;; db 081h,0C6h,02Eh,000h,000h,000h ; add esi,2Eh add esi,2Eh mov edi,esi mov ecx,0CF9h mov edx,9548E9F7h @@DecryptLast: lodsd xor eax,edx add eax,54ADF121h xor eax,ecx rol edx,5 imul edx,edx,70C967BEh xor edx,ecx stosd ;; dec ecx ;; jnz @@DecryptLast loop @@DecryptLast ;; jmp $+(5450h-543Eh) mov si,510Ch+(offset @@PatchAt026D6DEE-offset @@NewCRCProc) jmp esi @@Decryptor2Done: @@NewJumpAt53A2: jmp $+(540Ch-53A2h) @@NewJumpAt53A2Done: @@SkipBytes: ; ; Читаем дату, которую надо будет записать в файл ; .data Year dw 2003 Month db 03 Day db 28 .data? PatchDate db 128 dup(?) .code invoke GetCL,1,offset PatchDate cmp eax,1 jz @@DatePresent .data NoDateMsg db 0Ah,"No specifed date in command line ! Patch with 2003/03/28",0Ah,0 .code @@NoDateSpec: push offset NoDateMsg call Write_Log jmp @@DateDone @@DatePresent: mov esi,offset PatchDate call Str_Len test ecx,ecx jz @@NoDateSpec cld ; Year lodsd mov ebx,eax mov ecx,4 xor ebp,ebp @@GetYear: imul ebp,ebp,10 movzx eax,bl shr ebx,8 sub al,'0' add ebp,eax loop @@GetYear mov eax,ebp mov Year,ax lodsb ; Month xor eax,eax xor ebx,ebx lodsw sub ax,'00' mov bl,ah mov ah,10 mul ah add bl,al mov Month,bl lodsb ; Day xor eax,eax xor ebx,ebx lodsw sub ax,'00' mov bl,ah mov ah,10 mul ah add bl,al mov Day,bl ; Display patched date .data PatchDateHead db 0Ah,"Patch date: " PatchDateStr db "????/??/??",0Ah,0h .code movzx eax,word ptr Year mov ebx,1000 mov edi,offset PatchDateStr call DecChar movzx eax,byte ptr Month mov ebx,10 mov edi,offset PatchDateStr+5 call DecChar movzx eax,byte ptr Day mov ebx,10 mov edi,offset PatchDateStr+8 call DecChar push offset PatchDateHead call Write_Log @@DateDone: ; ; Открываем DLL ; .data IniFileName db "xsystem.dll",0 .data? IniDescr dd ? .code invoke CreateFile,offset IniFileName,GENERIC_WRITE or GENERIC_READ,\ FILE_SHARE_READ,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0 mov IniDescr,eax ; Save file descriptor cmp eax,0ffffffffh jnz @@DestFileOpen .data DestFileNotOpenMsg db "xsystem.dll not open (not exist ?)! Process aborted !",0Ah,0 .code push offset DestFileNotOpenMsg call Write_Log jmp Exit @@DestFileOpen: ; ; Патчим DLL ; ; ; Новая процедура без CRC ; ; Встать в нужное место invoke SetFilePointer,IniDescr,0510Ch,NULL,FILE_BEGIN ; Копируем новый код в сегмент данных для установки даты .data? Data_@@NewCRCProc db (@@NewCRCProcDone-@@NewCRCProc) dup(?) .code mov ecx,@@NewCRCProcDone-@@NewCRCProc mov esi,offset @@NewCRCProc mov edi,offset Data_@@NewCRCProc lea ebx,[edi+(@@PatchCommand-@@NewCRCProc)+3] lea edx,[edi+(@@PatchCommandRemainingDays-@@NewCRCProc)+3] rep movsb mov ax,Year shl eax,16 mov ah,Month mov al,Day mov [ebx],eax .data YearCurr dw 2005 MonthCurr db 04 DayCurr db 13 .code mov ax,YearCurr shl eax,16 mov ah,MonthCurr mov al,DayCurr mov [edx],eax ; Записываем новый код подсчета CRC invoke WriteFile,IniDescr,offset Data_@@NewCRCProc,(026D515Bh-026D510Ch),offset bWrite,NULL cmp dword ptr bWrite,(026D515Bh-026D510Ch) jz @@PatchCRCOK .data DestFileNotPatchedCRCMsg db "xsystem.dll (CRC) not patched: I/O error !",0Ah,0 .code push offset DestFileNotPatchedCRCMsg call Write_Log @@PatchCRCOK: ; ; Записать декриптор2 в незашифрованном виде (в 026D540Ch) ; ; Встать в нужное место invoke SetFilePointer,IniDescr,0540Ch,NULL,FILE_BEGIN invoke WriteFile,IniDescr,offset @@Decryptor2,(5440h - 540Ch),offset bWrite,NULL cmp dword ptr bWrite,(5440h - 540Ch) jz @@WriteDec2OK .data DestFileNotWriteDec2Msg db "xsystem.dll: decryptor2 not writed I/O error !",0Ah,0 .code push offset DestFileNotWriteDec2Msg call Write_Log @@WriteDec2OK: ; ; Записать переход на новый декриптор (2) ; ; Встать в нужное место invoke SetFilePointer,IniDescr,053A2h,NULL,FILE_BEGIN mov ebp,offset @@NewJumpAt53A2Done - offset @@NewJumpAt53A2 invoke WriteFile,IniDescr,offset @@NewJumpAt53A2,ebp,offset bWrite,NULL cmp dword ptr bWrite,ebp jz @@WriteDecJump2OK .data DestFileNotWriteDecJump2Msg db "xsystem.dll: decryptor2(jump) not writed I/O error !",0Ah,0 .code push offset DestFileNotWriteDecJump2Msg call Write_Log @@WriteDecJump2OK: ; *** ; Выход из программы: закрытие дескрипторов и т.п. ; *** Exit: .data EndWorkMsg db 0Ah,"All done... ;)",0Ah,0 .code push offset EndWorkMsg call Write_Log ; Закрыть открытые файлы cmp dword ptr IniDescr,-1 jz @@SkipCloseIniFile invoke CloseHandle,IniDescr @@SkipCloseIniFile: ; Закрыть лог invoke CloseHandle,LogDescr invoke ExitProcess,0 ; *** ; Подпрограмма записи в лог и на консоль конца строки (0Ah) ; *** Write_EndStr_ToLog proc .data EndStr db 0Ah,0 .code push offset EndStr call Write_Log ret Write_EndStr_ToLog endp ; *** ; Подпрограмма записи в лог и на консоль одновременно ; *** ; [ebp+8] - адрес строки для записи Write_Log proc push ebp mov ebp,esp ; Получить длину строки mov esi,dword ptr [ebp+8] ; Адрес строки call Str_Len test ecx,ecx jz @@ExitWrite_Log ; Запись на консоль push ecx push NULL push offset bWrite push ecx ; Длина строки push dword ptr [ebp+8] ; Адрес строки push hConsole call WriteFile pop ecx ; Запись в протокол push NULL push offset bWrite push ecx ; Длина строки push dword ptr [ebp+8] ; Адрес строки push LogDescr call WriteFile @@ExitWrite_Log: pop ebp ret 4 Write_Log endp ; *** ; Get lenght of string ; esi=addr of string; result: ecx=length ; *** Str_Len proc xor ecx,ecx push eax push esi @@GetStrLen: lodsb test al,al jz @@ExitStrLen inc ecx jmp @@GetStrLen @@ExitStrLen: pop esi pop eax ret Str_Len endp ; ************* DecChar: Подпрограмма форматирования строки из числа *********** ; eax=number to digit, edi=offset result string in format 00000000(@n[ebx]) ; ebx=начальный делитель DecChar proc pushad pushfd cld @GetDec: xor edx,edx div ebx add al,'0' stosb push edx mov eax,ebx xor edx,edx mov ebx,10 div ebx mov ebx,eax pop eax test ebx,ebx jnz @GetDec popfd popad ret DecChar endp ...


обсудить  |  все отзывы (0)

[23221; 49; 8.89]




  Copyright © 2001-2024 Dmitry Leonov Design: Vadim Derkach