Главная » Статьи » Крэкинг

Защита исполняемых файлов от искажений
Автор: Алексей Кирюшкин
The RSDN Group

Источник: RSDN Magazine #3-2003
Вступление
Идея
Что дает и чего не дает данный способ

Реализация

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

Вступление

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

  • В книге исходники приведены не полностью, не хватает одного заголовочного файла.
  • Используется CRC16
  • Собственно реализация, безусловно, рабочая, но, IMHO, уж больно “мутная”.

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

Вступление

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

  • В книге исходники приведены не полностью, не хватает одного заголовочного файла.
  • Используется CRC16
  • Собственно реализация, безусловно, рабочая, но, IMHO, уж больно “мутная”.

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

Идея

Для тех, кто не читал Лу Гринзоу (фи:( ) приведу идею метода. Для того, чтобы при подсчёте 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;
}
  • Достоинства: Все очень просто
  • Недостатки: Если известно, что файл защищен данным способом пересчитать для измененного файла контрольную сумму очень просто – место хранения и функция расчета контрольной суммы - стандартные.

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

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

Реализация

В демонстрационном проекте (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 кода

  • Используем отображения контролируемого файла в память, чтобы иметь возможность работать с ним, как с обычной последовательностью байтов.
  • Применим алгоритм стандартной библиотеки C++ search, чтобы найти в нем нашу метку, обозначающую место хранения CRC.
  • Проверим, что после этой метки еще осталось место для записи 4-х байт CRC кода.
  • Повторим поиск от конца метки до конца файла, чтобы убедиться в том, что в файле случайно не образовалось еще одной такой же последовательности байт, т.к. в этом случае нам не удастся определить правильное место для хранения 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 алгоритмам обнаружения ошибок”
Категория: Крэкинг | Добавил: -=Hellsing=- (06.07.2009)
Просмотров: 3973 | Рейтинг: 0.0/0
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]