Защита программы Ulead COOL 3D v3.5 (trial) — Архив WASM.RU

Все статьи

Защита программы Ulead COOL 3D v3.5 (trial) — Архив WASM.RU

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

  Объект исследования: программа 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
 ...

2002-2013 (c) wasm.ru