Поиск:

Обзор защиты NevoSoft

Отечественные дистрибьюторы игровых программ мыслят шаблонно и не делают выводов из своих и чужих просчётов. По крайней мере, такое впечатление сложилось у меня после недавнего изучения защиты игр NevoSoft, которое заняло немногим больше времени, чем писалась эта краткая статья.

Честно говоря, здесь я не планирую детально разбирать механизм защиты, будут показаны лишь основные моменты, поскольку (как показал опыт предыдущей заметки про Alawar и письма читателей) технические детали в программировании неинтересны обычным пользователям. Те из читателей, для кого представляет интерес сам процесс изучения защиты, смогут проследить весь путь исследования по приведённым ниже выводам. Особого труда это не составит, справятся даже новички.

Для наглядного примера к статье возьмём первую попавшуюся игру от производителя, я скачал «Остаться в живых», объявленную на сайте хитом сезона. Устанавливаем игру обычным способом:

10.jpg


Скачиваем с сайта программу-загрузчик, запускаем, задаём ей путь для установки и ждём, пока скачается собственно сама игра:

11.jpg

12.jpg

Для чистоты эксперимента, после установки игры следует перезагрузиться, чтобы элементы защиты NevoSoft корректно запустились и мы имели дело с полноценно защищённой версией игры. Первое, что бросается в глаза после перезагрузки – в области уведомлений появился значок с фирменной эмблемой и в автозагрузку добавилась программа непонятного назначения. При запуске игры появляется типичное окно с уведомлением о временном ограничении и всякой рекламной шелухой от производителя.

Посмотрев на картинки – переходим к самой сути нашей задачи. В общем, механизм защиты здесь чем-то похож на Alawar, только без малейших намёков на серьёзный подход к делу. Alawar хотя бы пытался ограничивать доступ к оболочке игры посредством использования ASProtect в своих дистрибутивах, а здесь и этого нет. В основе лежит навесная защита и оболочка, контролирующая запуск программ. В отличие от игр производства Alawar, где эта оболочка («wrapper») прилагается к каждой игре, – здесь она единая для всех, устанавливается в систему раз и навсегда при инсталляции любой игры от Nevosoft. Исполнимый файл 'drm.exe' в автозагрузке, создающий значок в трее и время от времени показывающий рекламу, по совместительству является этой самой оболочкой-часовым, проверяющим ограничения, ведущим триальный отчет времени и всё такое прочее. Его грозное название, видимо, было призвано отпугнуть непрошенных исследователей потенциальным наличием сложных криптозащит, но на поверку оказалось, что ничего такого здесь нет и в принципе, достаточно отладчика и дизассемблера. Реверсирование защиты Nevosoft сводится к изучению алгоритмов в этом исполнимом файле.

Алгоритм защиты состоит в следующем:

1) Установленная игра запускается ярлыком, указывающим на 'drm.exe' с уникальным идентификатором игры в параметре. Оболочка обращается к внутренней базе данных и по идентификатору игры ищет соответствующие ей параметры: бесплатное время для пользователя, исполнимый файл игры, параметры защиты исполнимого файла и прочую информацию. В результате проверок загружается файл игры, в процессе загрузки с него снимается защита и ему передается управление, одновременно с этим включается таймер бесплатной игры, если игра не «куплена». По завершении процесса игры, оболочка записывает текущее значение оставшегося бесплатного времени в базу. Естественно, как только значение бесплатного времени для игры в базе будет равно нулю, оболочка больше не даст запустить игру;

2) Внутренняя база данных представляет собой обычную БД SQLite3, зашифрованную XOR-ом по ключевому массиву размером 256 байт, хранящемуся в теле оболочки. Данные ключевого массива статичны и в целях совместимости не меняются на протяжении времени (здесь и ниже используются фрагменты кода на языке Delphi):

bKeyArraySize = 256;
 
arKeyDecrypt: array[ 0..bKeyArraySize-1 ] of Byte = (
 $69, $f7, $23, $a3, $a2, $5f, $86, $8d, $c6, $43, $6d, $5b, $35, $c9, $7b, $ff,
 $7f, $a6, $a5, $75, $89, $89, $d8, $ee, $77, $f2, $d3, $22, $e4, $7a, $f1, $b4,
 $5a, $45, $5a, $d0, $72, $a3, $f4, $65, $b2, $5c, $ff, $8b, $5a, $db, $61, $e3,
 $f1, $eb, $15, $a8, $22, $9b, $a7, $5c, $ae, $27, $77, $5c, $bb, $43, $87, $0c,
 $86, $23, $5d, $53, $31, $cf, $7a, $7e, $3f, $2a, $da, $0b, $1f, $b6, $dc, $48,
 $a4, $e1, $f4, $85, $53, $d6, $2c, $50, $28, $20, $f9, $84, $dd, $93, $0d, $f3,
 $79, $df, $07, $ad, $a3, $de, $60, $62, $b1, $cb, $45, $5d, $32, $4d, $bb, $38,
 $9c, $52, $de, $05, $18, $31, $d1, $ba, $5d, $e9, $68, $cb, $f1, $67, $57, $54,
 $2a, $0d, $6e, $d2, $7f, $27, $35, $05, $82, $c9, $b2, $f9, $ce, $2f, $6a, $d1,
 $e8, $d3, $39, $b2, $23, $19, $e6, $fc, $7e, $6f, $4b, $5e, $b8, $c7, $c4, $ac,
 $56, $6b, $71, $55, $3e, $4b, $ba, $9e, $cf, $ae, $99, $67, $8e, $0a, $1f, $e9,
 $f4, $0e, $e4, $2c, $e1, $fe, $32, $ab, $d5, $f5, $36, $2f, $65, $bd, $92, $1a,
 $8d, $a1, $54, $d1, $f8, $e5, $16, $34, $9a, $65, $6a, $d8, $76, $a5, $f5, $e5,
 $f2, $a1, $be, $e7, $c9, $af, $2a, $76, $bb, $ec, $8c, $e2, $49, $6c, $89, $67,
 $33, $34, $fb, $91, $51, $d1, $af, $43, $a1, $e3, $21, $b6, $2d, $1a, $64, $3d,
 $9e, $3f, $53, $52, $ba, $c4, $44, $ec, $76, $3b, $79, $69, $91, $72, $bd, $d2 
);

Всякий раз, когда оболочке нужно прочитать или записать данные в БД, база расшифровывается в память, с ней производятся необходимые манипуляции, производится шифрование и результат в кладётся обратно на диск. Сама шифрованная база хранится в скрытом файле 'base.db' в каталоге «Application Data» текущего пользователя:

13.jpg

Стоит заметить, что оболочка блокирует файл базы данных от чтения/записи сторонними программами (держит файл открытым эксклюзивно) и при любых манипуляциях с файлом базы сначала нужно завершить процесс 'drm.exe'.

При желании, мы можем расшифровать базу с помощью такого алгоритма:

function DecryptNevoDatabase( lpSrcFile, lpDestFile: PChar) : Boolean;
// here I won't using MMF (memory-mapped files) due to approximately
// small files size (suppose that the Nevosoft DB isn't bigger than 12-15Mb)
var
  hFileRead, hFileWrite: THandle;
  i: Byte; iNumRead, iNumWrite: Integer;
  Buffer: array[ 0..bKeyArraySize-1 ] of Byte;
 
begin
  Result:= false;
 
  hFileRead:= CreateFile( lpSrcFile, GENERIC_READ, 0, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0 );
  if ( hFileRead = INVALID_HANDLE_VALUE ) then Exit;
 
  hFileWrite:= CreateFile( lpDestFile, GENERIC_READ + GENERIC_WRITE, 0, nil, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0 );
  if ( hFileWrite = INVALID_HANDLE_VALUE ) then Exit;
 
  try
    // decrypt file
    repeat
 
      if ( ReadFile( hFileRead, Buffer, bKeyArraySize, Cardinal( iNumRead ), nil ) ) then
      begin
        for i:= 0 to iNumRead - 1 do
          // where is all street magic happens.. hehe
          Buffer[i]:= Buffer[i] xor arKeyDecrypt[i];
        if ( iNumRead > 0 ) then
        begin
          if ( not ( WriteFile( hFileWrite, Buffer[0], iNumRead, Cardinal( iNumWrite ), nil ) ) ) then Exit;
          // checking bytes count to be written
          if ( iNumWrite <> iNumRead ) then Exit;
        end;
      end
      else iNumRead:= 0;
 
    until ( iNumRead < bKeyArraySize );
 
    // setting the flag
    Result:= true;
 
  finally
    CloseHandle( hFileWrite );
    CloseHandle( hFileRead );
  end;
 
end;

После расшифровки можно смотреть структуру и данные, хранящиеся в базе, при помощи любого вьювера SQLite, например, SQLite Browser (картинка кликабельна):

14.jpg

Можно даже отредактировать данные (триальный лимит игры), зашифровать полученный файл базы тем же ключом и заменить оригинальную базу своей – оболочка подхватит наши изменения. Кстати, это является одним из вариантов обхода защиты – получение «вечного» триала, но это некрасивое решение, «грязный хак».

3) Секция кода исполнимого файла игры, в которой находится точка входа PE EXE, побайтно зашифрована XOR-ом с массивом длиной 1024 байта, хранящимся в поле 'crpt_inf' в записи базы данных. Иными словами, по идентификатору игры оболочка загружает её исполнимый файл (получая имя файла из поля 'exec'), считывает из БД ключевой массив из поля 'crpt_inf' и производит расшифровку EP-секции ключевым массивом, после чего передает управление загруженному файлу.

Расшифровать секцию и получить «чистый» исполнимый файл игры можно так:

// decrypt PE file section
function PEDecrypt_Section( lpPE, lpISH: Pointer; wSectionIdx: Word; lpCryptoData: PChar ): Boolean;
var
  ish: IMAGE_SECTION_HEADER;
  a, b: Byte; i, j: DWord;
 
begin
  Result:= true; // STUB
 
  Move( Pointer( DWord( lpISH ) + DWord( ( wSectionIdx - 1 ) * SizeOf( IMAGE_SECTION_HEADER ) ) )^, ish, SizeOf( ish ) );
  j:= 0;
 
  for i:= 0 to ish.SizeOfRawData - 1 do
  begin
    Move( Pointer( DWord( lpPE ) + ish.PointerToRawData + i )^, a, 1 );
 
    Move( Pointer( DWord( lpCryptoData ) + j )^, b, 1 );
    // we explicitly know, that crypto array always has lenght of 1024 bytes
    if ( j < 1023 ) then Inc( j )
    else j:= 0;
 
    // no, no, no, - david blaine..
    a:= a xor b;
    Move( a, Pointer( DWord( lpPE ) + ish.PointerToRawData + i )^, 1 );
  end;
 
  AddListBoxItem_Sep( '  Распаковка успешно завершена.');
  AddListBoxItem( ' ' );
  SendMessage( GetDlgItem( hMainDlg, ID_LIST_LOG ), WM_VSCROLL, SB_BOTTOM, 0 );
 
end;
 
...
 
// unwrap executable file's EP section
function Unwrap_ExecutableFile( lpFileName, lpCryptoData: PChar ): Boolean;
var
  hFile, hMapFile: THandle; lpMap: Pointer;
  e_magic: Word;  inh: IMAGE_NT_HEADERS;
  lpOffset: DWord; wEPSection: Word; strSaveFile: string;
 
begin
  Result:= false;
 
  AddListBoxItem( '  Укажите имя файла для расшифрованного файла... ');
  SendMessage( GetDlgItem( hMainDlg, ID_LIST_LOG ), WM_VSCROLL, SB_BOTTOM, 0 );
 
  if ( Get_SaveFileName( hMainDlg, nil, 'Исполнимые файлы (*.exe)'+#0+'*.exe'+#0#0, strSaveFile ) ) then
  begin
    AddListBoxItem( '  Файл ' + strSaveFile );
    AddListBoxItem_Sep( '  Производится распаковка исполнимого файла...');
    CopyFile( lpFileName, PChar( strSaveFile ), false );
 
    hFile:= CreateFile( PChar( strSaveFile ), GENERIC_READ + GENERIC_WRITE, 0, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0 );
    if ( hFile = INVALID_HANDLE_VALUE ) then
    begin
      AddListBoxItem_Sep( '  Ошибка открытия результирующего файла!');
      Exit;
    end;
 
    hMapFile:= CreateFileMapping( hFile, nil, PAGE_READWRITE, 0, 0, nil );
    CloseHandle( hFile );
    if ( hMapFile = 0 ) then
    begin
      AddListBoxItem_Sep( '  Ошибка записи в результирующий файл!');
      Exit;
    end;
 
    lpMap:= MapViewOfFile( hMapFile, FILE_MAP_READ + FILE_MAP_WRITE, 0, 0, 0 );
    CloseHandle( hMapFile );
    if ( lpMap = nil ) then
    begin
      AddListBoxItem_Sep( '  Ошибка открытия результирующего файла!');
      Exit;
    end;
 
    // doing some checks (if this file really win32 PE EXE)
    Move( lpMap^, e_magic, SizeOf( e_magic ) ); // 'MZ'
    if ( e_magic = IMAGE_DOS_SIGNATURE  ) then
    begin
      // lpOffset now points on PE headers
      lpOffset:= DWord( lpMap ) + PDword( DWord( lpMap ) + $3C )^;
      // read 'nt headers'
      Move( Pointer( lpOffset )^ , inh, SizeOf( inh ) );
      if ( inh.Signature = IMAGE_NT_SIGNATURE ) then  // 'PE00'
      // seems to valid win32 PE header
      begin
 
        // retrieving EP section index (1- based)
        wEPSection:= PESectionFromRVA( Pointer( lpOffset ), inh.OptionalHeader.AddressOfEntryPoint );
        if ( wEPSection > 0 ) then
        begin
          // lpOffset now points to array of 'image secton headers'
          lpOffset:= lpOffset + SizeOf( IMAGE_NT_HEADERS );
          Result:= PEDecrypt_Section( lpMap, Pointer( lpOffset ), wEPSection, lpCryptoData );
        end
        else
          AddListBoxItem_Sep( '  Ошибка определения исполнимого файла (IMAGE_NT_SIGNATURE)!');
 
      end
      else
        AddListBoxItem_Sep( '  Ошибка проверки исполнимого файла (IMAGE_NT_SIGNATURE)!');
 
    end
    else
      AddListBoxItem_Sep( '  Ошибка проверки исполнимого файла (IMAGE_DOS_SIGNATURE)!');
 
  UnmapViewOfFile( lpMap );
 
  end
  else
    AddListBoxItem_Sep( '  Операция отменена пользователем. ');
 
end;


В целом, данная защита не сложнее используемой в Alawar, но чисто технически здесь много рутинных операций (и дело не ограничивается правкой двух байт). В связи с этим решено было написать утилиту, автоматизирующую процесс исследования (собственно, куски кода, приведённые выше, взяты из неё):

15.jpg
Скачать утилиту можно здесь; исходники (на Delphi 7) здесь.

Учтите, что данная программа представлена лишь для образовательных целей и ни в коем случае не может использоваться для изготовления пиратских копий игр; прочтите на сайте отказ от ответственности.

Дискуссия

Сергей Сальцев, 14/09/2010 13:22

Добрй день, Денис!

Честно говоря не пробовал запускать программу, смотрел только код. Но видно, что меняется только одна секция .text. На самом деле изменяемых секций две. Вторая всегда размером в 3FH. Её надо дополнитьельно создать. Часто и без неё программа работает, но, к примеру Шоколатор 2 без неё запускается, работает, но при выходе происходит сбой, помогает только перезагрузка.

PS: WriteProcessMemory, так же показывает, что процессов замены два.

Денис Фатеев, 15/09/2010 14:30

Может быть, защита уже изменилась; я смотрел примерно десяток игр от Невософт, но везде меняется одна секция. Брейки на 'WriteProcessMemory' я не ставил, поскольку не видел в том необходимости.

Вообще странно, конечно. Получается, что производитель оригинальной игры линкует лишнюю секцию мусора? Иначе, как объяснить её присутствие в незащищенном/расшифрованном EXE? Производители пытаются интерактивно взаимодействовать с wrapper-ом, используя секцию в памяти как буфер?

Как вернусь из отпуска, посмотрю на «Шоколатор 2» :-)

Сергей Сальцев, 05/10/2010 12:50

Возможно я спутал название игры (смотрел много игр) но повторно сломанный Шоколатора 2 прекрасно работал и без дополнительной секции. Но вообще-то процессов WriteProcessMemory 2. К примеру в игре Супер Корова

WriteProcessMemory 1 hProcess = 0000045C Address = 401000 Buffer = 0C4A0020 BytesToWrite = B9000 — WriteProcessMemory 2 hProcess = 0000045C Address = 140000 Buffer = drm.004A21A8 BytesToWrite = 3F

В данном случае полностью заменяется 1 сеция, но это не обязательно, .text может быть рарбита на несколько секций и заменяться будет какая угодно. Честно говоря не задумывался зачем нужна новая секция, ведь получается, что всё работает и без неё.

Денис Фатеев, 05/10/2010 13:24

Я тоже посмотрел Шоколатора 2, видел два вызова 'WriteProcessMemory()', но дальше с 'drm.exe' копать не стал (проверил, что работает с одной секцией и «забил» на детали).

Renegade1979, 19/06/2011 22:57

Скачал только что «Потерянные Души» с их сайта, распаковывал только StraySouls.exe, потом вынес всю DRM и файл .db, папку с игрой зипнул и унёс маме на ноут, и ещё услал сестре в Италию. У мамы пока работает. thanx bro

Дмитрий, 02/12/2011 02:16

Всё работает отлично, СПАСИБО!!! ОГРОМММММММНОЕЕЕЕЕ!!!

Алина, 09/12/2011 02:38

подскажите, пожалуйста, что делать??при запуске игры выдает ошибку CANNOT OPEN DATABASE похоже я что-то напутала с base.db , но что именно понять не могу я в этом плохо разбираюсь.

Денис Фатеев, 09/12/2011 13:18

Если вы меняли там что-то вручную, произвольным образом, то неудивительно. Общий совет: сохраните данные игры, удалите повреждённый 'base.db' и переустановите игру заново. Рабочий 'base.db' появится автоматически.

Алина, 09/12/2011 18:54

Спасибо большое, я все исправила, только оболочка игр качается в папку не drm как было раньше ,а в Nevosoft.Games.

Евгений, 27/12/2011 11:05

Подскажите плиз а почему при разшифровки БД пишет ошибка расшифровки БД!?

Денис Фатеев, 27/12/2011 11:36

Укажите точный текст ошибки.

Евгений, 27/12/2011 13:51

Извените что побеспокоил вас. все теперь нормально. ваша програмка работает на ура! спасибо огромное. А почему была ошибка расшифровки я так и не понял. :-\

Electronic Arts, 05/02/2012 23:26

Вопрос автору статьи…

как вы выяснили что база зашифрована XOR-ом по ключевому массиву размером 256 байт?? а исполняемый файл - массивом длиной 1024 байта??

я просто этому хочу научиться….

заранее благодарю

Денис Фатеев, 06/02/2012 13:04

Исследовал wrapper, ставил брейки. На каком-то моменте шло обращение к внешнему файлу (это можно и FileMon/ProcessMon отследить). При этом было чтение из ресурсов, редактором ресурсов можно посмотреть на запрошенные данные. Это был ключевой массив. Оттуда же и размер виден, для перестраховки можно поставить брейк и посмотреть, сколько байт читается из ресурсов. Их будет 256 или 1024 (учетверенный исходный массив, типа оптимизация чтения-записи).

Касательно ключевого массива из БД, там чисто эмпирически. Посмотрел на нескольких игрушках, размер данных в поле 'crpt_inf' всегда был 1024 байт. Где-то во wrapper-e это значение прописано явно. Или размером поля в SQLite прописано, сейчас точно не помню.

Введите ваш комментарий

 
© 2009–2011 Денис Фатеев (Danger)
Копирование контента без указания автора преследуется сотрудниками ада.
Recent changes RSS feed
Valid XHTML 1.0
Valid CSS