© 2002 Евгений Каснерик
Как известно, многие рекомендации по совершенствованию программ, созданных с применением 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’ов, но это и в самом деле не так страшно, как может показаться. Во всяком случае, за те пять лет, что применяется описанный метод, это никогда не составляло серьезной проблемы.
|
Программирование для чайников.
|