К вопросу о защите программ

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

Автор: Евгений Каснерик
Теги:
защита программ
Добавлено: 02 Августа 2018 08:00:21 Добавил: Андрей Ковальчук Нравится 0
Добавить
Комментарии:
Нету комментариев для вывода...