Часть 1. Прячем формы
Как известно, многие рекомендации по совершенствованию программ, созданных с
применением VCL, сводятся к простому указанию – открыть исполняемый модуль
очередным Restorator’ом и поправить то или иное место в ресурсе формы или
датамодуля. Наличие исходных текстов и какая-никакая документированность
потоковой системы VCL привели к тому, что сегодня извлечение в читабельное
представление и обратная запись содержимого ресурсов форм не является задачей,
посильной только усилиям гуру.
Вместе с тем, путем нехитрых манипуляций с исходными текстами программы можно
достаточно легко реализовать защиту ресурсов форм от постороннего просмотра,
которая к тому же окажется совершенно прозрачной при последующей разработки.
1. СОЗДАНИЕ ФИЛЬТРА ЧТЕНИЯ ДАННЫХ
Итак, единственное место во всем VCL, где происходит доступ к ресурсу формы –
это функция InternalReadComponentRes из Classes, текст которой приведен ниже:
function InternalReadComponentRes(
const ResName: string;
HInst: THandle;
var Instance: TComponent
): Boolean;
var
HRsrc: THandle;
begin { avoid possible EResNotFound exception }
if HInst = 0 then HInst := HInstance;
HRsrc := FindResource(HInst, PChar(ResName), RT_RCDATA);
Result := HRsrc <> 0;
if not Result then Exit;
with TResourceStream.Create(HInst, ResName, RT_RCDATA) do
try
Instance := ReadComponent(Instance);
finally
Free;
end;
Result := True;
end;
Суть ее действий несложна: в модуле, определяемом параметром HInst, ищем
ресурс с типом RCDATA и заданным именем. Если не находим, то возвращаем False и
на этом успокаиваемся, иначе создаем поток на данных указанного ресурса и читаем
из него данные методом ReadComponent.
В таком случае возникает вопрос, что нам мешает вклиниться между созданием
потока и чтением из него данных с тем, чтобы перед чтением нужным образом их
модифицировать? Собственно, прямых ограничений нет – нам придется лишь выполнить
одну условно сомнительную операцию – модифицировать модуль Classes, дополнив его
перед implementation следующим текстом:
type
TLoadComponentFunc = function (hInst: THandle;
const ResName: string;
var Instance: TComponent): Boolean;
var
LoadComponentFunc: TLoadComponentFunc;
а в implementation мы изменим функцию InternalReadComponentRes следующим
образом:
function InternalReadComponentRes(
const ResName: string;
HInst: THandle;
var Instance: TComponent): Boolean;
var
HRsrc: THandle;
begin { avoid possible EResNotFound exception }
if HInst = 0 then HInst := HInstance;
if not Assigned(LoadComponentFunc) then
begin
HRsrc := FindResource(HInst, PChar(ResName), RT_RCDATA);
Result := HRsrc <> 0;
if not Result then Exit;
with TResourceStream.Create(HInst, ResName, RT_RCDATA) do
try
Instance := ReadComponent(Instance);
finally
Free;
end;
Result := True;
end else Result := LoadComponentFunc(HInst, ResName, Instance);
end;
Легко увидеть, что TLoadComponentFunc по описанию совпадает с
InternalReadComponentRes и вызывается внутри нее, выступая в качестве того
самого «клина», о котором мы и говорили выше. Описание TLoadComponentFunc и
переменную, содержащую адрес обработчика мы добавляли в самом конце
interface-секции Classes с единственной целью – избежать таких изменений в
модуле, которые приводили бы к печально известному сообщению ("Unit xxx was
compiled with another version of yyy”). Практика свидетельствует, что
дописывание каких-либо новых определений в конец существующего модуля никак не
влияет на «версионную отметку» вышестоящих описаний (подробнее об этом в другой
раз, пока придется поверить на слово).
Таким образом, мы определили функцию-фильтр, которая будет вызываться при
каждой попытке доступа к ресурсу формы. Удобной особенностью является то, что
при отсутствии фильтра приложение может работать в штатном режиме, т.е. можно
спокойно вести разработку и защищать данные от случая к случаю.
Пример модуля, реализующего фильтр, тождественный VCL-ному чтению:
unit DFMLoader;
interface
uses
Classes; // Classes должны быть измененными!
implementation
uses
Windows;
function MyLoadFunc(
HInst: THandle;
const ResName: string;
var Instance: TComponent
): Boolean;
begin
with TResourceStream.Create(HInst, ResName, RT_RCDATA) do
try
Instance := ReadComponent(Instance);
finally
Free;
end;
Result := True;
end;
initialization
LoadComponentFunc := @MyLoadFunc;
finalization
LoadComponentFunc := nil;
end.
Обратим внимание на initialization и finalization. Установка фильтра помещена
в initialization с тем, чтобы для активизации нашего метода защиты было
достаточно просто подключить модуль к проекту. Изъятие фильтра на finalization
обусловлено тем, что при использовании пакетов (packages) обращение к фильтру
происходит внутри VCLXX.BPL (или RTLXX.bpl в D6), а сам фильтр располагается в
другом пакете, который может быть выгружен. Именно поэтому на выходе мы и уберем
за собой.
Лирическое отступление: в процессе тестирования описываемой защиты
первоначально зануление фильтра отсутствовало, но быстро появилось после
эффектного падения IDE на перекомпиляции модуля с защитой ;-) Кстати, IDE могло
упасть только после перекомпиляции VCLXX/RTLXX, но о том, как это делалось, тоже
не в этот раз.
2. РЕАЛИЗАЦИЯ ФИЛЬТРА
Рассмотрим реализацию простейшего фильтра (фильтры посложнее вы реализуете
сами, доверившись своему вкусу) и обработки приложения для его использования.
Над байтами оригинального ресурса проделаем следующее – увеличим значение на
единицу: операция обратимая и, следовательно, назад это фарш прокрутить можно.
При этом обратим внимание, что начало данных ресурса формы обозначено
сигнатурой "TPF0”, а после нашего преобразования там будет "UQG1” (вместо
каждого символа сигнатуры мы взяли следующий за ним по таблице ASCII). Мы
используем это обстоятельство для решения вопроса о том, каким образом читать
ресурс.
Итак, функция чтения ресурса теперь будет выглядеть так:
function MyLoadFunc(
HInst: THandle;
const ResName: string;
var Instance: TComponent
): Boolean;
const
MySignature: array[0..3] of Char = 'UQG1';
var
I: Integer;
HRsrc: THandle;
src: TResourceStream;
Stream: TMemoryStream;
begin
HRsrc := FindResource(HInst, PChar(ResName), RT_RCDATA);
Result := HRsrc <> 0;
if not Result then Exit;
src := TResourceStream.Create(HInst, ResName, RT_RCDATA);
try
if LongInt(src.Memory^) = LongInt(MySignature) then
begin
Stream := TMemoryStream.Create;
try
Stream.LoadFromStream(src);
{ расшифровываем }
for I := 0 to Stream.Size - 1 do
Dec(Byte(PChar(Stream.Memory)[I]));
{ и загружаем }
Instance := Stream.ReadComponent(Instance);
finally
Stream.Free;
end;
end else Instance := src.ReadComponent(Instance);
finally
src.Free;
end;
Result := True;
end;
А для того, чтобы защитить скомпилированный проект, напишем такую же
простенькую «защищалку»:
program protect;
{$APPTYPE CONSOLE}
uses
Windows, Classes;
const
FormSignature: array[0..3] of Char = 'TPF0';
function MyEnumProc(hModule: THandle; lpResType, lpResName: PChar;
lParam: LPARAM): BOOL; stdcall;
var
I: Integer;
Src: TResourceStream;
Dst: TMemoryStream;
begin
if DWORD(lpResName) and $FFFF0000 <> 0 then
begin
Src := TResourceStream.Create(hModule, lpResName, lpResType);
try
{ удостоверимся, что это именно ресурс формы! }
if LongInt(Src.Memory^) = LongInt(FormSignature) then
begin
Dst := TMemoryStream.Create;
try
Dst.LoadFromStream(Src);
Dst.Position := 0;
{ зашифруем }
for I := 0 to Dst.Size - 1 do
Inc(Byte(PChar(Dst.Memory)[I]));
TStrings(lParam).AddObject(lpResName, Dst);
except
Dst.Free;
raise;
end;
finally
Src.Free;
end;
end;
Result := True;
end;
procedure GetResNames(const Filename: string; Items: TStrings);
var
hModule: THandle;
begin
hModule := LoadLibraryEx(PChar(Filename), 0, LOAD_LIBRARY_AS_DATAFILE);
if hModule <> 0 then
try
EnumResourceNames(hModule, RT_RCDATA, @MyEnumProc, LPARAM(Items));
finally
FreeLibrary(hModule);
end;
end;
procedure UpdateResources(const Filename: string; Items: TStrings);
var
I: Integer;
Stream: TMemoryStream;
hUpdate: THandle;
begin
hUpdate := BeginUpdateResource(PChar(Filename), False);
if hUpdate <> 0 then
try
for I := 0 to Items.Count - 1 do
begin
Stream := Items.Objects[I] as TMemoryStream;
UpdateResource(hUpdate, RT_RCDATA, PChar(Items[I]), 0, Stream.Memory, Stream.Size);
end;
finally
EndUpdateResource(hUpdate, False);
end;
end;
var
I: Integer;
Items: TStrings;
begin
Items := TStringList.Create;
try
GetResNames(ParamStr(1), Items);
UpdateResources(ParamStr(1), Items);
finally
{ освободим временные буфера }
for I := 0 to Items.Count - 1 do
Items.Objects[I].Free;
Items.Free;
end;
end.
Что мы здесь делаем: во-первых, загружаем обрабатываемый исполняемый модуль и
находим в нем все ресурсы с данными от форм (обращая внимание на то, чтобы это
были именно данные форм путем проверки сигнатуры). Преобразованные копии этих
ресурсов вместе с именами мы складываем в список.
Затем мы освобождаем модуль и открываем его уже для изменения ресурсов
(одновременно произвести два открытия нам не дадут). Т.к. нам известны имена,
типы и содержимое обновляемых ресурсов, ничто не мешает нам заменить ресурсы и,
запустив программу, убедиться, что все работает. Осмотр же с использованием
Restorator’а более не выявляет внутри программы каких-либо форм. Что и
требовалось доказать.
3. ЧТО ОСТАЛОСЬ ЗА КАДРОМ
Во-первых, обработка большинства ошибок. Например, отсутствие на диске
исходного файла в защищалке. Или более тщательная проверка целостности и
корректности информации в шифрованных ресурсах. Для описания концепции
приведенной информации вполне достаточно, а при развитии идеи появятся иные
места для проверки.
Во-вторых, системная функция обновления ресурсов в файле есть только в
WinNT/2K/XP. Однако для целей защиты всегда можно найти машину с указанными ОС
или, перелопатив MSDN, написать полностью свое обновление ресурсов.
В-третьих, наше предположение о том, что все операции чтения ресурсов будут
проходить именно через наш фильтр, основывается исключительно на анализе
исходного кода VCL. Впрочем, кроме VCL никто данные в ресурсы форм и не пишет.
Если у вас дело обстоит не так, то уделите внимание вопросу разрешения
потенциальных конфликтов.
В-четвертых, диапазон возможностей, которые открываются в описанном подходе,
не ограничивается только преобразованием ресурсов. В фильтре вполне возможно
реализовать чтение данных через Интернет. Ну или хотя бы преобразовать их через
установленный электронный ключ… Или в самом простом случае, упаковать их
каким-либо алгоритмом (zLib, к примеру).
4. ЗАКЛЮЧЕНИЕ
Итак, что мы имеем в плюсе: сокрытие ресурсов от постороннего взгляда;
больший контроль над процессом загрузки форм; палки, установленные в колеса
декомпиляторам; прозрачность защиты – без применения защищалки приложение также
будет корректно работать.
Особая изюминка заключается в сложности написания универсальной «открывашки»
для ресурсов – даже если для версии N Вашей программы определили алгоритм
восстановления ресурсов, в версии N+1 Вы незначительно изменяете алгоритм и
делаете бесполезной предыдущий хакерский труд.
Очень удобной является привязка параметров шифрования к коду программы – при
попытке подпатчить программу велика вероятность столкнуться с нечитаемостью
ресурсов и, очевидно, недоступностью ряда возможностей программы.
А что же в минусе: необходимость модифицировать исходный текст VCL и
поддерживать затем эти изменения при переходе к новым версиям или установки
UpdatePack’ов, но это и в самом деле не так страшно, как может показаться. Во
всяком случае, за те пять лет, что применяется описанный метод, это никогда не
составляло серьезной проблемы.
Автор:
Евгений Каснерик
|