Шифрование файлов отвечающих за
активацию Windows в Whistler build 2462

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

Для повторения моих действий по раскриптовке кода этих файлов понадобятся:

 

  1. Windows Whistler build 2462. Я использовал пакет Advanced Server.

  2. Диск Customer Support Diagnostics, на котором лежат файлы с отладочной информацией для этого билда Whistler. С него требуется только файл NTOSKRNL.PDB, нужный для запуска NtIce. Так же можно оттуда взять файлы WINLOGON.PDB и LICDLL.PDB, но можно обойтись и без них.

  3. Numega SoftIce не ниже 4.2.1 (Build 57). Списать его можно с http://numega.com/drivercentral/icecentral.asp.

  4. Hiew 6.65 или другой hex-редактор "знающий" про формат PE-файлов.

  5. PEditor by yoda & M.o.D.

  6. Любой дампер памяти, например IceDump или встроенный в мой старенький Windows Game Cracker.

  7. Interactive Disassembler 4.xx

  8. Visual C++ 6.0 или любой другой компилятор C++ для Win32.

  9. UPX последней версии, в случае если лень самому устанавливать правильные параметры PE-заголовка.

 

Да, чуть не забыл. Если вы не обладаете лицензией на этот билд Whistler, а так же на перечисленные выше программы, хотите использовать приведенные тут сведения для взлома активационного кода или во вред кому-либо, - то ни в коем случае не читайте эту статью.

 

Настройка системы

 

Для начала нужно подготовить NtIce для работы на этом билде ОС. Для этого:

 

  1. Устанавливаем RETAIL-символы с диска Customer Support Diagnostics (на полную установку потребуется около 700 Мб места)

  2. Запускаем Symbol Loader и в нём делаем Open Module на NTOSKRNL.EXE

  3. Нажимаем Translate. В каталоге %windir%\System32 должен появиться файл NTOSKRNL.NMS

  4. Добавляем в %windir%\System32\Drivers\WINICE.DAT строку NTSYMBOLS=ON

  5. Грохаем установленные в первом пункте файлы из каталога %windir%\Symbols.

 

После этих действий NtIce начинает запускаться нормально, хотя и проглюковывает временами. 

Так же рекомендую временно отключить Windows File Protection.

 

Приступим...

 

После установки Whistler, обнаруживаем что в меню Start появился ярлык Activate Windows. Если глянуть его свойства, то обнаруживаем что он указывает на программу "%SYSTEMROOT%\system32\oobe\msoobe.exe /A". Эта программа просто выводит HTML-ки, лежащие в каталоге %windir%\system32\actsetup, а основная работа по регистрации происходит в каком-то ActiveX. После недолгих поисков обнаруживаем файл LICDLL.DLL, который в себе содержит много интересных строк, в частности "Windows Product Activation". Такую же строку содержит в себе файл WINLOGON.EXE. Если кому интересно, то тут лежит его Type Library переведенная в понятный человеку вид с помощью программы OLE View из пакета Visual Studio.

Первая мысль которая приходит в голову - это загнать LICDLL.DLL в IDA, загрузить в неё LICDLL.PDB и посмотреть на текст функции

unsigned long DepositConfirmationId([in] BSTR bstrVal);

IDA после загрузки символьной информации из PDB-файла (Edit->Plugins->Load PDB file) обозвала её 
"__DepositConfirmationId@CLicenseAgent@@QAEKPAG@Z". После недолгого копания в её коде обнаруживаем вызов странно выглядящей функции:

   sub_66F09970 proc near
          pushf
          pusha
          push offset dword_66F09D1C    ; начало данных использующихся при расшифровке
          push 1                        ; 1 == расшифровать и выполнить
          call sub_66EF6F2D             ; процедура расшифровки
          add esp, 4                    ; сюда управление никогда не доходит
          jmp eax
    sub_66F09970 endp
    ; ──────────────────────────────────────
          dd 3E14F323h, 3E9DE06Dh, 448E72A5h  ; зашифрованая процедура, причём все адреса
          db 0Bh                              ; в ней хранятся в открытом виде
          dd offset loc_66F231CA
          ; [skipped]
          db 81h
          dd offset __imp__GetFileSize@8
          dd 0CB8D9229h, 9082F800h, 619168E1h,
          db 9Dh
          dd offset __imp__GetLastError@0
          ; [skipped]
    dword_66F09D1C dd 3A2F11F1h,          
          ; [skipped]
          dd 68AF958h, 399B2CF2h, 6B06B24Bh, 0FFFFFFFFh

А рядом с ней видим много подобных функций. Создается впечатление, что их код как-то странно зашифрован, причём адреса переменных и функций остались не тронутыми. Наверняка процедура sub_66EF6F2D отвечает за расшифровку кода (я сразу же переименовал её в Decrypt); передаваемый ей параметр (в этом случае dword_66F09D1C) указывает на массив DWORD-ов, необходимых для расшифровки, заканчивающийся 0FFFFFFFFh, а сам зашифрованый код начинается сразу после команды jmp eax.

Пора смотреть этот код в отладчике.

Чтобы не пробираться через дебри кода MSOOBE.EXE, я решил для простоты написать свою программу, загружающую LICDLL и вызывающую из неё одну из зашифрованных функций. К счастью LICDLL экспортирует три из них под номерами 123, 124 и 125. Вот или вот текст программы, вызывающей одну из этих функций. 

Кстати, если вызвать функцию 124 и перезагрузиться, то счётчик дней до необходимости активации Windows сбросится на 14. Правда такое сбрасывание срабатывает всего несколько раз - при каждой перезагрузке после вызова этой функции Windows добавляет один ключ с названием вроде EntryHash-PRCRFTFJWDC123 в реестр HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\WPA и после того как таких ключей становится 7 штук сброс перестает работать.

 Итак, компилируем программу, в SoftIce пишем "I3HERE ON" и запускаем её. В случае с функцией с номером 123, процедура расшифровки находится по адресу 66EF3E2D (я её назвал Decrypt1). Зачем-то микрософтовцы в код программы вставили две совершенно одинаковые процедуры расшифровки, различающиеся только адресами используемых ими глобальных переменных. Кстати, в файле WINLOGON.EXE этих функций так же две.

Полную трассировку кода функции Decrypt или Decrypt1 (они одинаковые) приводить не буду - она сильно громоздка и содержит много команд типа:

              lea ecx, ds:70FEFEF4h
              sub ecx, [ebp+var_20] ; почти всегда эти две
              add ecx, [ebp+var_28] ; ячейки содержат одинаковые значения
              sub ecx, 0A0FA144h
              jmp ecx               ; обычно ECX указывает на следующую после JMP команду

Наверное таким образом микрософтовцы пытались запутать дизассемблер, но в случае с IDA им это не очень удалось, хотя и мешает анализу кода. Так же в коде присутствует множество вызовов функций синхронизации типа WaitForSingleObject, так что наверняка основная часть расшифровки происходит в параллельном потоке.

После недолгой трассировки натыкаемся на такой код:

              mov edx, [esp+24h]
              mov ecx, [esp]
              mov [esp], edx        ; этот код меняет на стеке значения  
              mov [esp+24h], ecx    ; регистров EIP и EAX
              popf
              popa
              retn

В моём случае он находился по адресу 66EF7762. В регистр ECX записывается адрес расшифрованного кода на который будет передано управление после выполнения RETN.

 Расшифрованый код выглядит примерно так:

;                                db ____ dup(0)
;                                jmp 00361EFC       ; зачем нужны эти переходы скажу позже
;                                db ____ dup(0)
;                                jmp 00361EFC
;                                db ____ dup(0)
;00361EFC 90                     nop                ; <- вот сюда передается управление
;00361EFD 6652                   push    dx
;00361EFF 52                     push    edx
;00361F00 6651                   push    cx
;00361F02 E8CC1EB966             call    sub_66EF3DD3   ; эта процедура имеет какое-то отношение к установке регистров
 00361F07 55                     push    ebp            ; это начало расшифрованного кода
 00361F08 8BEC                   mov     ebp,esp
 00361F0A 51                     push    ecx
 00361F0B 51                     push    ecx
 00361F0C 8365F800               and     dword ptr [ebp-00008h],00000h
 00361F10 8365FC00               and     dword ptr [ebp-00004h],00000h
 00361F14 8D45FC                 lea     eax,[ebp-00004h]
 00361F17 50                     push    eax
 00361F18 8D45F8                 lea     eax,[ebp-00008h]
 00361F1B 50                     push    eax
 00361F1C FF7508                 push    dword ptr [ebp+00008h]
 00361F1F E8DF50BB66             call    _ValidateDigitalPid@12 ; тут кончается расшифрованый кусок кода
;00361F24 9C                     pushfd 
;00361F25 60                     pushad 
;00361F26 6808000000             push    00000008           ; 8 == расшифровать следующий кусок кода
;00361F2B E8FD1EB966             call    Decrypt            ; 66EF3E2D
;00361F30 FFE0                   jmp     near eax

Команды не отсносящиеся к расшифрованому коду я пометил ';'. В начале расшифрованного блока находится несколько нулевых байтов, потом две команды перехода на начало кода, и снова несколько нулевых байтов. Код всегда начинается с команды NOP - удобное место вставить туда int 3 ;). Сразу после команды NOP идёт вызов процедуры sub_66EF3DD3. Она как-то связана с установкой значения регистров в случае если одна зашифрованная процедура вызывает другую. В конце расшифрованного кода идёт вызов процедуры Decrypt с параметром 8. В случае конца процедуры происходит вызов процедуры Decrypt с параметром 0Ah.

В общем случае расшифрованый код в памяти имеет такой вид:

Начало расшифрованного блока кода (пролог):

        nop
        несколько команд PUSH засылающих в стек 8 байт
        call    66EF3DD3 или 66EF6ED3

после чего следует расшифрованый код, для расшифровки и выполнения следующего блока кода заканчивающийся так:

        pushfd 
        pushad 
        push    00000008       
        call    Decrypt или Decrypt1
        jmp     near eax

или в случае если достигнут конец процедуры, он имеет вид:

        pushfd 
        pushad 
        push    0000000A
        call    Decrypt или Decrypt1
        ret     <кол-во байтов параметров процедуры>
        pushfd            ; а на этот код управление никогда не передается
        pushad
        push    00000006  ; что эта константа значит - не знаю
        call    Decrypt или Decrypt1
        ret

В файле WINLOGON.EXE в двух или трёх местах бывает что после вызова PUSH 0Ah; CALL Decrypt; RET n идёт продолжение кода процедуры. Это бывает в случае особо громоздких процедур содержащих команды RET в середине своего кода.

Итак, вроде ясно как выглядит расшифрованый код процедуры в памяти. Остается придумать как его сдампить и вставить в файл. Вот тут и начинаются трудности:

 

  1. Каждый раз код расшифровывается в новый участок памяти.

  2. Код расшифровывается частями, причём расшифровка следующего участка зависит от значения регистра EIP.

  3. Кроме начала кода нужно ещё найти его конец, плюс отсеить вызов служебных процедур раскриптовщика.

 

От первой особенности я думал избавиться перехватом VirtualAlloc, однако оказалось что и в пределах выделенного участка памяти расшифрованый код оказывается каждый раз в новом месте, так что от этого пришлось отказаться. Второе ограничение обойти можно простым JMP на адрес с командой PUSHFD. Третью проблему можно решить анализируя расшифрованый код.

Я придумал следующее. Надо пропатчить LICDLL.DLL в памяти таким образом, чтобы перед передачей управления на расшифрованый участок кода вызывалась моя процедура, записывающая этот участок в файл, после чего не выполняя его код она должна "попросить" расшифровать следующий участок. И так до тех пор пока не будет достигнут конец функции.

Так как мне не удалось придумать способа отказаться от анализа расшифрованного кода, я достал старенький текст дизассемблера сделаного ещё в 1992 году Robin Hilliard и незначительно исправленого мной для поддержки 32-битного кода и написал небольшую программу, которая перехватывала код, передающий управление на расшифрованый участок, дизассемблировала его построчно одновременно ищя то место, где передаётся управление на процедуру расшифровывающую следующий участок кода. Первоначального варианта этой программы у меня не сохранилось. В последнем варианте программы добавилась возможность поиска всех зашифрованых участков кода LICDLL.DLL по сигнатуре с их автоматической расшифровкой. Архив программы можно взять тут, он в себе содержит такие файлы:

    make.bat      - файл для компиляции программы
    test.exe      - откомпилированная программа
    licdll_decr   - расшифрованый код всех найденых процедур.
                    Он получен вызовом "test.exe >licdll_decr"
    2ASM.C        - текст дизассемблера
    Table.c       - таблица кодов команд для дизассемблера
    BMSearch.cpp  - реализация процедуры поиска заданного набора байтов в памяти
    BMSearch.h    - заголовочный файл для этой процедуры
    SYMBOLS.CPP   - таблица символов для того, чтобы в дизассемблере имена функций имели
                    нормальный вид. Получен из .map файла сгенерированного IDA
    TEST.CPP      - собственно сама программа

Все комментарии в тексте программ я по привычке писал на английском.

Для нормальной работы программы в один каталог с ней надо положить файл LICDLL.DLL, предварительно исправив в нём атрибуты секции .TEXT на C0000040 (то есть сказав что в неё можно писать).

 

Расшифровка LICDLL.DLL

 

Этот вариант программы только выводит расшифрованый текст процедур на экран. Расскажу что и как он делает. 

Основные действия порисходят в файле TEST.CPP. Он:

 

1. Загружает LICDLL.DLL в память

2. Правит код процедур Decrypt и Decrypt1 в LICDLL, передающий управление на расшифрованый код таким образом чтобы он вызывал мою процедуру MyGoToCode. До исправления их код был:

              mov edx, [esp+24h]
              mov ecx, [esp]
              mov [esp], edx 
              mov [esp+24h], ecx
              popf
              popa
              retn

Он находится по адресам 66EF4645 и 66EF7762 в LICDLL.DLL. Он исправляется на:

              push offset MyGoToCode
              retn

3. Ищет все зашифрованые процедуры. Они всегда начинаются с команд

    9C                      pushf
    60                      pusha
    68 XX XX XX XX          push offset данные_для_расшифровки
    68 01 00 00 00          push 1
    E8 XX XX XX XX          call Decrypt или Decrypt1

то есть я просто делаю поиск байтов 9C 60 68.

4. Для каждой найденой процедуры вызывает DoDecrypt.

 

Функция DoDecrypt просто вызывает найденую зашифрованую процедуру. Код процедуры Decrypt/Decrypt1 в LICDLL.DLL расшифровывает её в памяти и передает управление на мою функцию MyGoToCode. В ней я запоминаю то место куда передается управление и вызываю функцию Disassemble.

В функции Disassemble происходит построчное дизассемблирование кода с отсевом вызова служебных функций. 

Процедура Disassemble отсеивает код относящийся к прологу или эпилогу (ставит в начале строки ';'), а так же отыскивает начало кода эпилога и говорит функции MyGoToCode передать на него управление для расшифровки следующего участка кода. 

Конец функции Disassemble определяет не верно, если команды PUSH 0Ah; CALL Decrypt находятся не в конце, а в середине функции. Такой код встречается в двух местах в файле WINLOGON.EXE. Я такие места правил руками. Возможно если подобное шифрование будет встречаться в будущих продуктах Микрософт, я исправлю эту ошибку.

Код Disassemble имеет несколько громоздкий вид, так как он писался на скорую руку. Самый простой способ понять что он делает - это посмотреть на результат выводимый им на экран. Увидеть его можно в файле licdll_decr из этого архива.

В файле licdll_decr в архиве находится расшифрованый код процедур. Его уже можно попробовать вручную вставить в LICDLL.DLL.

 

Правим WINLOGON.EXE

 

Как получить работоспособную расшифрованую версию программы, которую можно загнать в IDA для анализа или пропатчить я расскажу на примере файла WINLOGON.EXE. Именно этот файл проверяет истечение 14ти дней до необходимости активизировать продукт, а так же истечения срока годности beta-версий Windows.

Если попробовать загнать нерасшифрованый WINLOGON.EXE в IDA, то IDA выдаст с ошибку "Can't find translation for virtual address FF047F78, continue__", после чего вылетит сообщив "Disk positioning error: Invalid argument (handler: 9, position: -16482440)". Эта ошибка исчезает после того как подпортить в файле таблицу импорта. Для этого запускаем HIEW32 (именно 32-битную версию HIEW, ДОС-версия валится с сообщением о нехватке памяти под импорт) ищем в нем байты 78 7F 04  и забиваем их а так же ещё пару следующих строк нулями. После этого файл загружается в IDA нормально. Естественно запустить его после таких изменений не удастся. 

Чтобы получить максимум полезной информации о функциях в WINLOGON рекомендую загрузить его PDB-файл.

В случае с файлом WINLOGON процедуры Decrypt и Decrypt1 имеют адреса 10043С0 и 10074C0. Адреса команд передающих управление на расшифрованый участок кода 1004BD8 и 1007CD8. Тут можно взять программу, которая выдает на экран расшифрованый код процедур из WINLOGON. Её текст точно такой же как и для LICDLL.DLL за исключением этих четырёх адресов. Так же как в случае с LICDLL надо подправить флаги для секции .TEXT в WINLOGON чтобы можно было в неё писать. В архиве лежит WINLOGON.EXE с уже исправлеными атрибутами секций.

Я немного изменил программу выдающую на экран дизассемблированный текст WINLOGON чтобы она выдавала только коды команд без их декомпиляции, и в случае вызовов процедур писала настоящий адрес вызываемой процедуры а не относительно адреса команды CALL (на кой-то чёрт в Intel-овских процессорах адрес вызываемых процедур и команд перехода указывается относительно текущего значения EIP). В таком виде расшифрованный код проще обработать для вставки назад в WINLOGON.EXE.

Измененную программу можно списать отсюда. Текст её файла TEST.CPP стал таким. Изменения коснулись только процедуры Disassemble.

После запуска программы получаем файл winlogon_decr, содержимое которого больше не зависит от того в какое место памяти был расшифрован код процедур. Теперь можно этот код записать в файл WINLOGON.EXE.

Для этого я написал небольшую программу. Её текст состоит из одного файла. Программа загружает WINLOGON.EXE как DLL, читает файл winlogon_decr и расшифрованый код процедур кладёт в массив Unpacked, настраивая при этом адреса в командах CALL и заменяя вызовы служебных процедур на NOP. Этот массив в последствии надо будет вставить в файл WINLOGON.EXE как новую секцию. Так же программа исправляет в памяти код вызова зашифрованных процедур чтобы он передавал управление на их расшифрованые версии. Передачу управления я делаю с помощью команд PUSH адрес; NOP; RET. NOP я вставляю как место для INT 3 в целях отладки.

После запуска программа просит сдампить секцию .TEXT файла WINLOGON.EXE и массив Unpacked (на экран выводятся его адрес и размер). Далее исправленую секцию .TEXT кладём на её старое место, а массив Unpacked с помощью PEDitor добавляем в конец файла как новую секцию. 

Однако после проделаных действий полученый WINLOGON не будет воспринят системой как правильный. Сперва надо исправить его PE-заголовок файла (PEDitor не все поля прописывает как надо). Я в таких случаях пользуюсь программой UPX. Надо запаковать WINLOGON командой UPX -F WINLOGON.EXE (-F значит FORCE, то есть принудительно запаковать некорректный PE-файл), а после этого распаковать его по UPX -D WINLOGON.EXE. После этого надо запустить PEDitor и восстановить правильное CRC у файла.

Если сейчас взять WINLOGON, записать его поверх старого и перезагрузиться, то после появления окна "Press CTRL-ALT-DEL for logon" WINLOGON выполнит недопустимую операцию. Это из-за того, что код WINLOGON был расшифрован без учёта одной особенности. Иногда при условном переходе, адрес перехода указывает за пределы расшифрованого кода на одну из команд JMP про которые я говорил выше. Такие места я правил руками. Они все имеют вид 83 fe 10 (cmp esi,10h; jc куда-то - 2 места) или 83 fe 08 (cmp esi,08 - в одном месте). На  несколько команд ниже того места куда указывает JC находится команда XOR EDX,EDX. Надо исправить адрес перехода таким образом чтобы он указывал на этот XOR.

Исправленую и полностью работающую версию WINLOGON можно взять тут. В этой версии я специально не трогал код ответственный за активацию Windows. Ведь все кто читают этот текст и так имеют лицензионные версии Whistler ;). Однако замечу, что "избавление" Windows от необходимости активации после 14ти дней достигается изменением всего одного байта в WINLOGON.EXE и исправления его CRC в PE-заголовке.

 

P.S. Если кого интересуют подробности, или будут найдены неточности в этой статье, то со мной можно связаться по ICQ# 70241285.

 

(c) май 2001, mamaich.