Защита программы 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):
Код (Text):
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 <strong>call [ebp+2B54h] ; Точка вызова EnableWindow</strong> 026D7F8A: popad ... </code></pre> <p> Итак, программа анализирует первый байт некоторой структуры (lea esi,[ebp+2C63h], lodsb, cmp al,90h), сравнивает его с 90h, если он оказывается таковым, то кнопка "TRY" становится недоступной (переход на метку 026D7F8A). Попробуем проверить гипотезу о том, что все, что нужно для "освобождения" программы от проверки даты - это сделать прямо здесь достпуной кнопку "TRY". Попробуем сделать это прямо в отладчике: установим break на адрес 026D7F5F: <p> <strong>bpx</strong> LoadLibraryA ; Ждем загрузки xsystem.dll <p> ... <p> <strong>bc 0</strong> ; clear breakpoint at LoadLibraryA <p> <strong>bpx</strong> 026D7F5F ; Set breakpoint <p> Ctrl+D... <p> и попытаемся дождаться срабатывания break'а и "руками" поменять в отладчике значение регистра al сразу после lodsb: но вместо того, чтобы получить break на этом (026D7F5F) адресе, мы получаем GPF... Видно (в Sice'е), что машина пытается исполнять какой-то мусор в xsystem.dll. <p> ...Так мы выяснили, что программа не только контролирует системную дату, но и сопротивляется нашим попыткам ее трейсить ;) Наверное, программа как-то динамически модифицирует свой код - например по адресу 026D7F5F - и break, который мы пытаемся установить, мы устанавливаем в то место, которое впоследствии будет изменено. Скажем, самораспаковывающийся или саморасшифровывающийся код может быть причиной тому. Попробуем как-то обойти это ограничение и вспоминаем про то, что программа неминуемо должна узнать системную дату - ведь ей необходимо ее сравнить с чем-то типа даты инсталляции. Win32-приложение обычно делает это при помощи API-функции GetSystemTime: <p> GetSystemTime(offset структуры типа SYSTEMTIME) <p><code><pre> 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):
© Chingachguk / HI-TECHКод (Text):
026D6DE3: lea esi,[ebp-100h] push esi <strong>call [ebp-4] ; call GetSystemTime </strong> 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 </code></pre> <p> Уже видно, как программа работает с датой: код по адресу ~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]) и ... оказываемся в нужном месте: <p><code><pre> 026D7F5F: mov edi,User32!EnableWindow ... 026D7F68: <strong>lea esi,[ebp+2C63h]</strong> 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 ... </code></pre> <p> Ура !? Что ж, проверим: модифицируем (в отладчике) регистр al после лодсб и отпускаем программу... <p> <IMG SRC="/archive/pub/23/pics/cool3d/en_btn.jpg" HEIGHT="350" WIDTH="500"> <p> ...Так, кнопка "TRY" доступна (хотя Remaining Days = 0). Жмем ее и видим всего лишь вот этот MessageBox: <p> <IMG SRC="/archive/pub/23/pics/cool3d/alert.jpg" HEIGHT="150" WIDTH="200"> <p> Вот и познакомились ;) Оказывается, защиту зовут "xLok". А разрешить жать на кнопку - не значит получить доступ к функциональности ;( <p> Но что если данными для проверки валидности даты для кода в ~026D7F5F является не байт, а слово или даже что-то большее ? На такую мысль может навести пара команд "lea esi,[ebp+2C63h], lodsb" вместо "cmp byte ptr [ebp+2C63h],90h", хотя это может быть "приколом" компиллятора. Внимательнее поизучаем байты в структуре, адресуемой по [ebp+2C63h] для двух дат: для даты в интервале работы и вне ее: <p><code><pre> Работа возможна: 6A 01 FF 73 08 FF 95 54 ... Работа запрещена: 90 90 FF 73 08 E8 9D 00 ... </code></pre> <p> Установим дату вне интервала работы, опять остановимся в отладчике в точке 026D7F68, поменяем "неверные" байты на "верные" (90 90 FF ... -> 6A 01 FF ... ) и "отпустим" программу. MessageBox'а от xLok'а больше нет, но есть банальный GPF... <p> Следовательно, с большой вероятностью можно заключить, что защита выполняет (динамически) расшифровку какой-то части рабочего кода и снятие всех ее проверок после расшифровки приводит к неизбежному краху, так как этот код расшифровывается неверно вне интервала работы программы. Видимо, защита использует дату (точнее, не саму дату, а некоторую функцию от нее, которая постоянна на интервале работы программы) как ключ шифрования части своего кода. Проверим это: в этом случае будет достаточно подменить системную дату, которую защита спрашивает у Win. В отладчике это легко сделать: опять устанавливаем break на GetSystemTime, возвращаемся в xsystem.dll к коду в 026D6DEE и "патчим" значение структуры SYSTEMTIME (или значение edx чуть ниже) датой инсталляции... Все OK, программа отлично работает и ничего не запрещает. <p> Здорово, вроде бы осталось поменять команды инициализации edx (код в 026D6DEE и ниже) на что-то вроде таких: <p><code><pre> 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 </code></pre> <p> и все будет работать ? Пробуем найти опкоды оригинальных инструкций в DLL: <p><code><pre> Команды Опкод movzx edx,w [esi] 0F B7 16 shl edx,8 C1 E2 08 mov dl,[esi+2] 8A 56 02 ... </code></pre> и не находим их ! Этого следовало ожидать еще тогда, когда стало ясно, что код самомодифицирующийся. Что делать дальше ? Может быть, имеет смысл попробовать найти расшифровшик, проанализировать его алгоритм и "пропатчить" нужные байты в соответствии с алгоритмом ? Пусть алгоритм дешифровщика состоит в наложении xor-последовательности на байты DLL: <p><code><pre> Команды Опкод Шифро-Опкод в 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 ... </code></pre> <p> В этом случае не составило бы труда подготовить те "новые" опкоды, которые мы собираемся поместить на место старых - нужно выполнить над ними операцию с той же xor-последовательностью - как это делает пока неизвестный нам расшифровщик: <p><code><pre> Команды Опкод_новый (Опкод) xor (Шифро-Опкод в DLL) Результат mov edx,07D3031Ch BA 1C 03 D3 07 BA 9D 0C 05 A7 00 81 0F... nop ... </code></pre> <p> Помещаем в файл xsystem.DLL по смещению (в файле) 6DEE подготовленные таким образом байты и пытаемся смотреть, что же сделал с ними расшифровщик - а в результате видим такой вот MessageBox: <p> <IMG SRC="/archive/pub/23/pics/cool3d/debug_m.jpg" HEIGHT="150" WIDTH="200"> <p> Да, защите удалось не только скрыть результат работы расшифровщика (если он вообще отработал) но и определить модификацию собственного кода. Неплохо ! Как же ему это удалось ? Попробуем "отловить" его на обращении к памяти, содержащей измененные коды: <p> <strong>bpm</strong> 026D6DEE <p> и, действительно, наблюдаем следующий код: <p><code><pre> 026D510C: push esi push ecx push eax ... 026D5115: mov ecx,49A0h xor edx,edx xor eax,eax @@GetCRC: <strong>lodsb ; esi=026D6DEFh </strong> 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 </code></pre> <p> Очень интересный код ! Похоже на подсчет CRC (возвращает в eax), причем регион подсчета CRC включает и критичные для нас адреса в ~026D6DEE. Следовательно, прежде чем воевать с их расшифровщиком, нам нужно победить подсчет контрольной суммы. Открыты ли байты подпрограммы подсчета CRC в DLL ? <p><code><pre> 026D510C: Команда / Опкод push esi / 56h push ecx / 51h push eax / 52h ... </code></pre> <p> Ищем их в файле DLL по смещению 510Ch и находим ! Следовательно, мы можем без особой боязни заменить оригинальную подпрограмму подсчета CRC на что-то вроде: <p><code><pre> @@NewCRCProc: mov eax,1FBFFAh add dword ptr [esp],5 ret @@EndOfNewCRC: db (026D515Bh-026D510Ch) - (@@EndOfNewCRC-@@NewCRCProc) dup(90h) @@NewCRCProcDone: </code></pre> <p> При этом мы вернем нужное где-то дальше CRC (в eax) без всякого подсчета - что зря процессор гонять, хитро вернемся назад (add dword ptr [esp],5) и забьем все остальное место nop'ами. Патчим таким образом DLL и получаем ... GPF ! Оба-на, защита перестала детектировать сосбтвенную модификацию но и программа "рухнула" ;( Неужели мы неправильно составили новый код подсчета CRC ?... Стоп, а ведь мы еще раньше поменяли байты, относящиеся к получению даты (в ~026D6DEE) ! Установим break на API GetSystemTime, вернемся к коду получения даты в DLL и видим, что расшифровщик явно сделал не то, что мы ожидали: <p><code><pre> Команды_новые Результат работы расшифровщика / Опкод 026D6DEE: mov edx,07D3031Ch sbb byte ptr [esi] / 1D 82 1E 1D nop mov bl,7Fh / B3 7F ... </code></pre> <p> Следовательно, алгоритм расшифровщика производит расшифровку байт в ~026D6DEE в зависимости от исходных байт. Т.е. это не простое наложение XOR-последовательности; расшифровка каждого байта зависит от того, как был расшифрованы все предидущие. Алгоритм расшифровщика нам по-прежнему неизвестен и мы не можем даже пытаться осуществлять атаку на его шифр. Откатим пока эти изменения, оставив лишь получение CRC и продолжим следить в отладчике за ходом выполнения программы защиты после возврата из подпрограммы подсчета CRC: <p><code><pre> 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,... </code></pre> <p> Если продолжить в отладчике пошагово трассировать код с 026D53A2, то налетаешь на GPF. Но сразу бросаются в глаза явно вычурные для win32-кода команды работы с сегментными регистрами (mov fs:[0],esp). Если уж приложение взялось за это, то явно не с добрыми намерениями ;) На самом деле таким образом в windows пользовательским программам дозволяется установить свой обработчик исключительных ситуаций и в случае таковых получить управление и попытаться их обработать (интересно, кому-нибудь удавалось обрабатывать их инчае как выдать сообщение "Программа выполнила ... и будет закрыта" ?...). Далее мы увидим, что иногда такой обработчик может выполнять некоторые "полезные" действия. <p> В двух словах о таких обработчиках. Эта штука называется "SEH" - structure exeption handler. Для того, чтобы обработчик правильно получил управление, необходимо: <p><code><pre> - поместить в стек смещение обработчика; - сохранить смещение старого обработчика на том же стеке; - записать по селектору из FS, смещению 0 содержимое esp </code></pre> <p> Сам обработчик вызвается в C-формате, при этом получая кучу параметров - указатель на структуру регистров в момент исключения и т.п.: <p><code><pre> SEHproc proc C pExcept: dword, pFrame: dword, pContext: dword, pDispatch: dword PrintException pExcept ... ret SEHproc endp </code></pre> <p> Но самое главное - это то, что он может вернуться в достаточно произвольное место кода (включая и вызвавшее исключение), при этом уведомив диспечер о том, что исключение им обработано (например, mov eax, ExceptionContinueExecution перед выходом) и то, что он имеет право записи в сегмент команд (это необходимо для исправления "дефектного" кода). Таким образом, обработчик исключения защиты явно собирается именно этим заниматься: подать управление на неверный опкод, поймать expeption, дешифровать некоторые байты, вернуться опять на неверный опкод (не обязательно первый), и так до тех пор, пока необходимый код не будет полностью расшифрован. <p> Где же защита разместила свой обработчик ? Нет нужды копаться в командах, предшествующих коду установки SEH (~026D5382), можно просто посмотреть стек в этот момент (скажем, после выполнения mov fs:[0],esp): <p> <strong>d</strong> esp <p> Оказыватся, адрес у обработчика равен 026D5040, а вот и он сам: <p><code><pre> 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: ... </code></pre> <p> Итак, обработчик SEH сразу после получения управления начинает что-то расшифровывать с адреса 026D5078 и ниже довольно незамысловатым алгоритмом, немного странно реализованным (вместо add eax,0AB358CDFh достаточно add al,0DFh). Размер байт для дешифровки невелик (mov ecx,94h). Дожидаемся возможности установить break в 026D5078 - после первого же stosb там появлется pop esi и после срабатывания break'а видим весь расшифрованный код: <p><code><pre> 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: </code></pre> <p> Что же любопытного в раскрывшемся коде ? Прежде всего обратим внимание на характерный код в 026D5081 - push ebp, mov ebp,esp. Это явно оформлено начало какой-то процедуры, и она заканчивается в 026D510B. Командами <p><code><pre> mov eax,[esp+28h] mov [eax+4],esi ; <-026D5081h </code></pre> <p> декриптор определил адрес <strong>нового</strong> обработчика SEH - теперь это будет код в 026D5081. При следующих исключениях всегда будет вызываться именно эта процедура, что-то расшифровывающая командами: <p><code><pre> xor [esi],bl 026D50CE: cmp byte ptr [edi],09Dh jz 026D50DF xor [edi],bl or dword ptr [eax+0C0h],100h 026D50DF: </code></pre> <p> Но в данный момент код получения даты (в ~026D6DEE) еще не расшифрован. Может быть, только что расшифрованная SEH-подпрограмма 026D5081 и есть ее декриптор ? Устанавливаем break на начало 026D5081: <p> <strong>bpx</strong> 026D5081 <p> и наблюдаем, как раз за разом управление переходит к SEH-обработчику. Он явно расшифровывает какой-то код и хотелось бы остановить программу в тот момент, когда он полностью закончит свою работу. Довольно утомительно было бы жать Ctrl-D столько раз, поэтому я написал небольшой код, заменяющий собой распаковщик SEH-обработчика: <p><code><pre> ; Новые команды по адресу 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 </code></pre> <p> Новый код в отличии от старого не декриптует SEH-обработчик (это можно сделать прямо в файле xsystem.dll и не обременять код графического редактора дешифрацией), а устанавливает новый адрес SEH-обработчика - это будет подпрогрммка @@ArtBytes, которая только лишь вычисляет число call'ов SEH'а и передает jmp'ом управление на старый адрес 026D5081. Теперь в Sice'е можно ставить <stong>условный</stong> break на команде mov ax,word ptr [eax+(offset CounterCalls - offset @@GetOfs)]: <p> <strong>bpx</strong> <addr> IF (AX==0x100) <p> Заранее, конечно, число call'ов SEH'а неизвестно, но простым методом деления "отрезка" пополам можно довольно быстро добраться до конца работы SEH-обработчика. Трейсим последний вызов по SEH-обработчика и видим, что: <p> - Даже когда он закончил свою работу, код получения даты (в ~026D6DEE) нерасшифрован; <p> - Последний свой xor SEH-обработчик выполнил с адресом 026D540Ch: <p><code><pre> xor [esi],bl ; edi=026D540Ch, там 75h, bl=3Eh 026D50CE: cmp byte ptr [edi],09Dh ... </code></pre> <p> Таким образом, SEH-обработчик-дешифровщик не расшифровал код получения даты. Но что за байты появились по адресу 026D540Ch ?... <p><code><pre> 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: ... </code></pre> <p> О, старые знакомые ! <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3 :smile3:"> Опять переносимый (call 026D5412, pop esi) код дешифрует нижележащие байты. Ставим break в конец цикла, дожидаемся его окончания и с нетерпением смотрим ... нет, не байты после 026D5440, а байты получения даты - ~026D6DEE. Они расшифрованы, теперь - ура - мы знаем кто их декриптует и этот кто-то (026D540C - последний дешифровщик) крайне мал, чтобы возится с его расшифровщиком (обработчик SEH'а) - проще вбить его, уже нам известный на то же место, где сейчас лежит его зашифрованный собрат. Только тогда нужно будет дезактивировать его расшифровщик (обработчик SEH'а) - а то он все испортит ;) Как же это сделать ? Вернемся к моменту инсталляции SEH'а: <p><code><pre> ... 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,... </code></pre> <p> Что будет, если вместо invalid-команды, намеренно сделанной для генерации исключения, сделать переход на дешифровщик в 026D540C, который теперь, как мы планируем, будет записан нами в открытом виде прямо в DLL ? SEH-обработчик явно не вызовется, осталось опасность потерять стек при таком прыжке. Но парная команда к pushfd - последняя команда перед вызовом SEH-обработчика через исключение - popfd: <p><code><pre> 026D540C: popfd call 026D5412 ; get current offset ... </code></pre> <p> как будто бы явно написана разработчиками защиты с целью восстановить состояние процесса после отработки SEH'а. Так и поступим. Остался последний момент: где разместить код, который поменяет пресловутые команды получения даты на одну спокойную mov edx,Const_Valid_Date и парочку nop'ов ? <p> Ниже привожу свое решение, возможно немного надуманное. Поскольку этот код хотелось бы разместить после отработки расшифровщика 026D540C, т.е. тогда, когда уже код от 026D5440 и далее расшифрован, то он должен быть после последней команды расшифровывающего цикла: <p><code><pre> 026D540C: ... dec ecx jnz 026D5425 ; Здесь должен быть код, вставляющий команду mov edx,Const_Valid_Date jmp 026D5450 026D5440: ... </code></pre> <p> Вместо двух байт короткого jmp'а сложновато сделать такое, но если немного пооптимизировать код расшифровщика и вспомнить, что после удаления старого кода подсчета CRC осталась куча места, то можно написать следующее: <p> Этой командой мы закроем выполнение SEH'а: <p><code><pre> ; Этот jump мы поместим на место всяких invalid'ов, чтобы SEH не вызывался ; Сам SEH-обработчик оставим в покое @@NewJumpAt53A2: jmp $+(540Ch-53A2h) @@NewJumpAt53A2Done: </code></pre> <p> Это код нового, открытого декриптора по адресу 026D540C: <p><code><pre> @@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: </code></pre> <p> Это новый код подсчета CRC вместе с подпрограммой модификации кода получения даты в 026D6DEE: <p><code><pre> ; ; 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 </code></pre> <p> Вот вроде бы и все ;) Хотя далее есть еще несколько пугающих кусков типа: <p><code><pre> 026D6C52: lea esi,[ebx-100h] ... call 026D7623 <strong>int 62h ; !!!</strong> add eax,0FCfC4C4h ... </code></pre> <p> которые заставили меня лишний раз заглянуть в Ральфа Брауна, но особой опасности они не представляют ;) <p align="left"><b>Немного извращений или граффити</b> <p> После реализации подмены системной даты и многократных смен системной даты в целях выяснинения особенностей защиты программы можно наблюдать интересный эффект: <p> <IMG SRC="http://www.wasm.ru/wault/pub/23/pics/cool3d/Rem_Days.jpg" HEIGHT="350" WIDTH="500"> <p> xsystem.dll уже изменена, все работает, но вот с числом оставшихся дней что-то не то. Как бы не мешает, но некрасиво... Любопытен тот факт, что обратная замена исходной DLL дела не меняет: число оставшихся дней меняется, но как-то "криво". Есть и такой "баг": если даже взять нетронутую версию программы, перевести дату на некоторое число дней вперед, запустить программу, а следом вернуть дату на место, то при первом запуске программа не позволяет работать (число оставшихся дней равно 0, кнопка "TRY" недоступна). <p> Анализ кода xsystem.dll показал, что после того, как системная дата получена программой, выполняется следующий код: <p><code><pre> 026D6E03: mov ecx,eax <strong>mov eax,[ebp+3202h]</strong> xor eax,7895ADEBh test eax,eax ... </code></pre> <p> - из некоторой области памяти достатется число, которое "расшифровывается" xor-ом и далее (код здесь не приводится) трактуется как дата начала работы программы (инсталляции). Поэтому для завершенности изменений в программе можно поменять команду "xor eax,7895ADEBh" на "mov eax,Const_Valid_Date". В этом случае программа будет выдавать 30 дней до конца trial'а. Но можно немного поизвращаться и оставить число Remaining Days вот таким: <p> <IMG SRC="/archive/pub/23/pics/cool3d/777_date.jpg" HEIGHT="350" WIDTH="500"> <p> <p align="left"><b>Исходный текст патчера</b> <p> Ниже приводится текст почти полный текст программы, выполняющей описанные выше действия. Некоторые вспомогательные подпрограммы типа GetCL пропущены. <p><code><pre> ; *** ; Патч проги 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 ...
Защита программы Ulead COOL 3D v3.5 (trial)
Дата публикации 13 апр 2003