Обзор защиты 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) здесь.

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