Сообщений 7    Оценка 10 [+0/-1]         Оценить  
Система Orphus

Защита исполняемых файлов от искажений

Автор: Алексей Кирюшкин
The RSDN Group

Источник: RSDN Magazine #3-2003
Опубликовано: 07.09.2003
Исправлено: 13.03.2005
Версия текста: 1.3
Вступление
Идея
Что дает и чего не дает данный способ
Другие способы самоконтроля целостности исполняемого файла
Реализация
Поиск места для записи/ чтения CRC кода
Подсчет CRC и запись подсчитанной суммы в файл
Контроль CRC
Порядок применения CSelfSafe
Дополнительная информация по CRC

Вступление

Автор должен чистосердечно раскаяться в том, что идея данного способа использования CRC для защиты исполняемых файлов от искажения целиком и полностью украдена им из книги Лу Гринзоу "Философия программирования Windows 95/NT" (Символ, Санкт-Петербург,1997), однако просит принять во внимание следующие, смягчающие его вину обстоятельства:

Что еще мог сделать в этой ситуации русский программист? Конечно, только одно – “переписать это все нафиг” :).

Идея

Для тех, кто не читал Лу Гринзоу (фи:( ) приведу идею метода. Для того, чтобы при подсчёте CRC учитывались только данные исполняемого файла и не учитывалась сама CRC, добавим в исходные тексты программы следующую глобальную структуру данных CRC_DATA:

struct CRC_DATA
{
    BYTE label[ 16 ];  // метка (маячок) для поиска места CRC в файле
    DWORD crc;         // собственно посчитанная CRC файла
} CrcData = {{"0123456789ABCDE"}, 0}; // << ЗАДАЙТЕ ВАШУ МЕТКУ ЗДЕСЬ

Придуманная нами уникальная (в пределах файла) метка label поможет найти в исполняемом файле нашей программы место (DWORD crc), где находится рассчитанная CRC, и которое поэтому не должно учитываться при подсчёте.

ПРИМЕЧАНИЕ

В рассматриваемой далее реализации не имеет значения кратность длины метки установленному в свойствах проекта выравниванию – Struct Member Aligment, т.к. переменная CrcData.crc, как таковая, нигде в программе не используется. Она нужна только как гарантия наличия 4-х неиспользуемых байт после метки. Именно эти 4 байта будут использоваться для записи и чтения CRC. В зависимости от длины метки и используемого выравнивания они могут совпадать, а могут и не совпадать с 4-мя байтами переменной CrcData.crc.

Что дает и чего не дает данный способ

Начну с конца – использование CRC затрудняет, но не исключает полностью возможность искажения файла злоумышленником (см. например [1]), так что о 100%-й надежности определения факта искажения речь не идет. Утешимся, однако, тем, что под нашим контролем останутся искажения при передаче по каналам связи, записи/перезаписи, изменения, внесённые вирусами, а также малолетними «хацкерами» с редакторами ресурсов в руках.

Другие способы самоконтроля целостности исполняемого файла

Использование стандартного поля PE-заголовка и функции MapFileAndCheckSum

Установив в свойствах проекта опцию Set CheckSum (ключ /RELEASE) мы заставим линкер после каждой перекомпиляции рассчитывать контрольную сумму для файла и записывать ее в соответствующее поле PE-заголовка. Следующий код демонстрирует способ проверки контрольной суммы при запуске exe-файла:

// checksumm.cpp : © Павел Блудов http://www.rsdn.ru/Users/Profile.aspx?uid=507

#include "stdafx.h"
#include <tchar.h>
#include <stdio.h>
#include <windows.h>
#include <Imagehlp.h>
#pragma comment(lib, "Imagehlp.lib")

int _tmain(int argc, _TCHAR* argv[])
{
  TCHAR szFullPath[MAX_PATH];
  DWORD dwFileChecksum = 0, dwRealChecksum = 0;

  ::GetModuleFileName(::GetModuleHandle(NULL), szFullPath, MAX_PATH);
  ::MapFileAndCheckSum(szFullPath, &dwFileChecksum, &dwRealChecksum);

  tprintf(TEXT("File checksum %08X, real checksum %08X\n")
      , dwFileChecksum, dwRealChecksum);
  return 0;
}

Использование криптографии – цифровая подпись

Реализация

В демонстрационном проекте (VC7) приведены исходные тексты класса CSelfSafe, делающего для нас необходимую минимальную работу по контролю целостности файла и расчету CRC:

#pragma once

#include <sstream>
#include "filemap.h"

using namespace std;

class CSelfSafe
{
  public:
    // файл будет потом
    CSelfSafe();
    // работать с файлом, переданным через HMODULE
    CSelfSafe( HMODULE hmod );
    // работать с файлом с заданным именем
    CSelfSafe( string fname );
    // новое имя файла для работы
    void NewFile( string fname );
    void NewFile( HMODULE hmod );

    ~CSelfSafe( void );

    // получить строку с сообщением об ощибке
    string GetErrStrAnsi() const;
    // тоже для консольных приложений
    string GetErrStrOem() const;

    // подсчитать и сверить CRC для заданного файла
    BOOL CheckCRC();

    // подсчитать и записать CRC в заданного файла
    BOOL WriteCRC();

  protected:
    // имя проверяемого файла
    string filename;
    // сообщение об ошибке
    stringstream errstrm;
    // обработываемый файл, отображенный в память
    CFileMap targetfile;

    // найти место для CRC в файле
    BOOL OpenFileAndFindCRCpos( BYTE ** crcpos, BOOL toWrite = FALSE );
    // посчитать CRC файла исключая собственно значение CRC в позиции crcpos
    DWORD SynCRC( BYTE * crcpos );
};

Оставим пока в стороне конструкторы и способы задания имени контролируемого файла, начнем с основных моментов.

Поиск места для записи/ чтения CRC кода

ПРИМЕЧАНИЕ

Ситуация с двумя метками регулярно возникает для Debug-версий исполняемых файлов. Помогает полная перекомпиляция.

Ниже приведена реализация метода поиска места для CRC - OpenFileAndFindCRCpos():

// открыть файл и найти место для CRC
BOOL CSelfSafe::OpenFileAndFindCRCpos( BYTE ** crcpos, BOOL toWrite )
{
  // открываем файл, отображаем в память
  if ( !targetfile.Open( filename.c_str(), toWrite ) )
  {
    errstrm.clear();
    errstrm.str( "" );
    errstrm << "Невозможно открыть файл '" << filename << "'";

    return FALSE;
  }

  // указатель на конец файла, будет нужен несколько раз
  BYTE* file_end = targetfile.Base() + targetfile.Size();

  // ищем метку, после которой идет место для CRC
  BYTE* label_start = search( targetfile.Base(),
                      file_end,
                      CrcData.label,
                      CrcData.label + sizeof( CrcData.label ) );

  if ( label_start == file_end )
  {
    errstrm.clear();
    errstrm.str( "" );
    errstrm << "В файле '" << filename 
        << "' не найдено место хранения CRC";
    return FALSE;
  }

  // CRC - сразу после метки и смещения
  *crcpos = label_start + sizeof( CrcData.label );

  if ( ( *crcpos + sizeof( DWORD ) ) > file_end )
  {
    // при попытке записи/чтения в это место, вылетим
    // за конец файла
    errstrm.clear();
    errstrm.str( "" );
    errstrm << "Недопустимое место для хранения CRC в файле '" 
        << filename << "'";
    return FALSE;
  }

  // метка найдена, на всякий случай ищем вторую,
  // начиная сразу после первой найденной метки
  if ( search( label_start + sizeof( CrcData.label ),
         file_end,
         CrcData.label,
         CrcData.label + sizeof( CrcData.label ) ) != file_end )
  {
    // нашли две метки, это уже безобразие, метка в файле
    // должна быть одна, иначе непонятно, куда писать CRC
    errstrm.clear();
    errstrm.str( "" );
    errstrm << "В файле '" << filename
      << "' найдено 2 места для хранения CRC, должно быть только одно";
    return FALSE;
  }

  return TRUE;
}

Подсчет CRC и запись подсчитанной суммы в файл

После получения от OpenFileAndFindCRCpos() указателя на место для хранения CRC в файле нам остается только подсчитать CRC32, исключив из подсчета те самые четыре байта, в которых будет храниться CRC, и записать на это место подсчитанную сумму:

// подсчитать и записать CRC для заданного файла
BOOL CSelfSafe::WriteCRC()
{
  // указатель на CRC, сохраненную в файле
  BYTE * crcpos = NULL;
  // результат записи CRC
  BOOL ret = FALSE;

  // ищем место, куда в файл нужно записать CRC
  if ( OpenFileAndFindCRCpos( &crcpos, TRUE ) )
  {
    // нашли, пишем в это место только что подсчитанную CRC
    *reinterpret_cast<DWORD*>(crcpos) = SynCRC( crcpos );
    ret = TRUE;
  }
  else
  {
    // расшифровка ошибки дана в OpenFileAndFindCRCpos
    ret = FALSE;
  }

  // закрываем файл
  targetfile.Close();

  return ret;
}

// посчитать CRC, исключая собственно 32 бита CRC
DWORD CSelfSafe::SynCRC( BYTE * crcpos )
{
  // первая половина файла, до места, где CRC
  DWORD CRC = accumulate( targetfile.Base(),
              crcpos,
              ( DWORD ) 0, // начальное значение CRC
              UpdateCRC ); // функция подсчета CRC

  // пропустили 32 байта места хранения CRC в файле и идем дальше
  return accumulate( crcpos + sizeof( DWORD ), // место после CRC
            targetfile.Base() + targetfile.Size(), // конец
            CRC,     // CRC первой половины
            UpdateCRC ); // функция расчета
}

// пересчет CRC по таблице с учетом следующего байта
DWORD UpdateCRC( DWORD crcSoFar, const BYTE& nextByte )
{
  return ( crcSoFar >> 8 ) ^ CRCtable[ ( ( BYTE ) ( crcSoFar & 0x000000ff ) ) ^ nextByte ];
}

В данной реализации использована таблица для расчета CRC32, приведенная в [1].

Контроль CRC

Ищем место, где хранится CRC, считываем контрольную сумму, сохраненную в файле, и сравниваем с рассчитанной:

// подсчитать и сверить CRC для заданного файла
BOOL CSelfSafe::CheckCRC()
{
  // указатель на CRC, сохраненную в файле
  BYTE * crcpos = NULL;
  // результат сверки CRC
  BOOL ret = FALSE;

  // ищем место, где в файле записана CRC
  if ( OpenFileAndFindCRCpos( &crcpos ) )
  {
    // нашли, сравниваем то, что записано в файле,
    // с CRC, подсчитанной только что
    if ( *reinterpret_cast<DWORD*>(crcpos) == SynCRC( crcpos ) )
      ret = TRUE;
    else
    {
      // устанавливаем ошибку
      errstrm.clear();
      errstrm.str( "" );
      errstrm << "Файл '" << filename << "': неверное значение CRC";
      ret = FALSE;
    }
  }
  else
  {
    // расшифровка ошибки дана в OpenFileAndFindCRCpos
    ret = FALSE;
  }

  // закрываем файл
  targetfile.Close();

  return ret;
}

Порядок применения CSelfSafe

Пример использования метода дан в демонстрационном проекте.

Создайте объект класса CSelfSafe, передав ему в конструкторе или в методе NewFile() HMODULE или строку с именем контролируемого файла. В процедуре запуска exe или dll, а также, в порядке паранойи, по некоторым событиям в вашей программе вызывайте метод CheckCRC() для контроля целостности.

Естественно, после каждой перекомпиляции программы на нее надо натравливать утилитку, которая, используя тот же самый CSelfSafe, будет с помощью метода WriteCRC() подсчитывать CRC и записывать ее в нужное место. Это, пожалуй, единственное неудобство при использовании данного метода, но с другой стороны, зачем еще нужен Post Build Step? :-)

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

Тестовая программа из демонстрационного проекта при запуске с параметром (именем исполняемого файла) рассчитывает для него (файла) контрольную сумму и записывает в предназназначенное для этого место, а при запуске без параметра производит самопроверку CRC:

int _tmain( int argc, _TCHAR* argv[] )
{
  // инициализация CRTDBG для контроля утечек памяти
  // см. также
  // #define _CRTDBG_MAP_ALLOC
  // #include <crtdbg.h>
  // в stdafx.h
  _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );

  CSelfSafe selfs;

  if ( argc == 1 )
  {
    // argv[0] в данном случае использовать нельзя, если
    // программу вызовут без полного пути к selfsecureexe, 
    // и она запустится из одного из каталогов, доступных по
    // PATH, не найдем "сами себя" для самопроверки.
    char myname[_MAX_PATH];
    ::GetModuleFileName( NULL, myname, sizeof(myname) );

    selfs.NewFile( myname );
    if ( !selfs.CheckCRC() ) 
    {
      cout << selfs.GetErrStrOem() << endl;
      _getch();
      return 0;
    }
    else
    {
      cout << "CRC in file " << myname << " is cheked out!" << endl;
    }

  }
  else
  {
    selfs.NewFile( argv[1] );

    cout << "Writing CRC into file " << argv[1] << "..." << endl;
      
    if ( !selfs.WriteCRC() )
    {
      cout << selfs.GetErrStrOem() << endl;
      _getch();
      return 0;
    }
    else
    {
      cout << "CRC is written into file " << argv[1] << " !" << endl;
    }

    cout << "Checking CRC of " << argv[1] << "file..." << endl;

    if ( !selfs.CheckCRC() )
    {
      cout << selfs.GetErrStrOem() << endl;
      _getch();
      return 0;
    }
    else
    {
      cout << "CRC in file " << argv[1] << " is cheсked out!" << endl;
    }
  }

  _getch();

  return 0;
}

Дополнительная информация по CRC

  1. Anarchriz/DREAD. “CRC, и как его восстановить”
  2. Ross N. Williams. “Элементарное руководство по CRC алгоритмам обнаружения ошибок”


Эта статья опубликована в журнале RSDN Magazine #3-2003. Информацию о журнале можно найти здесь
    Сообщений 7    Оценка 10 [+0/-1]         Оценить