В предыдущей части статьи я попытался рассказать о том, как можно извлекать текстовую информацию из различных приложений, используя оконные функции WinAPI. Такой подход может быть полезен в тех случаях, когда нет прямого доступа к хранилищу данных, и (или) заблокирована возможность копирования текста в буфер обмена. Во второй части я хочу рассказать, как разработчику защитить свои программы от подобных методов доступа.
Но прежде чем перейти к методам защиты окон, я сделал некоторые доработки в предыдущей версии программы.
Самым слабым местом в программе была необходимость использования инструмента, предоставляющего отладочную информацию об окнах и оконных классах. В моем случае это был WinSight32. При его использовании приходилось затрачивать достаточно много времени на поиск окна, содержащего интересующий меня текст. Кроме того, не было полной уверенности в том, что найденное окно являлось именно тем, которое требовалось найти. Если вы помните, поиск нужного окна производился по имени класса, а, следовательно, требовалась ручная проверка правильности выбора.
Ввиду этого я решил написать собственный анализатор окон, который выводил бы список всех дочерних окон, позволял выбирать нужное окно из этого списка и подставлял его параметры в код, служащий для извлечения текста. Естественно, что рассматриваемый список должен выводить информацию не обо всех существующих в данный момент времени окнах, а лишь о дочерних окнах одной единственной формы, которую пользователь будет выбирать самостоятельно.
Я не буду детально описывать весь процесс разработки, а лишь обращу внимание на ключевые моменты. Готовую программу вместе с исходным кодом вы сможете найти по адресу
http://mda-delphi.ru/win_db2/widbb.rar.
Для вывода списка окон я использовал компонент TListBox. Среди значимых параметров отслеживаемых окон я выделил имя класса, хэндл окна, текст заголовка и длину текста.
Выбор анализируемой формы я реализовал уже известным вам способом, с помощью TTimer. Формирование списка я закрепил за специальной кнопкой. Ниже приведен обработчик события OnClick для этой кнопки.
procedure TForm1.Button2Click(Sender: TObject);
Function FindNeedWindow(wHandle: HWND; LPARAM: Integer): BOOL; Stdcall;
Var TextLen: Integer;
NewCol: TListColumn;
NewListItem: TListItem;
var S, Txt:array[0..255] of char;
a:string;
Begin
Result := True;
if LPARAM=2 then
Begin
GetClassName(wHandle, S, 256);
NewListItem:=Form1.ListView1.Items.Add;
NewListItem.Caption:=s;
NewListItem.SubItems.Add(IntToStr(wHandle));
GetWindowText(wHandle,Txt,256);
NewListItem.SubItems.Add(txt);
NewListItem.SubItems.Add(IntToStr (GetWindowTextLength(wHandle)));
if not IsWindow(wHandle) then
Result := False;
End;
End;
begin
SetForegroundWindow(FGW);
Timer1.Enabled:=false;
wndNeedEdit := -1;
wndNeedHandle := -1;
ListView1.Items.Clear;
EnumChildWindows(fgw, @FindNeedWindow, 2);
Timer1.Enabled:=true;
end;
Тело основной процедуры практически ничем не отличается от того примера, который был приведен в первой части статьи. Исключением явилось только то, что теперь ищутся лишь дочерние окна обрабатываемой формы, а не все окна приложения. А вот функция FindNeedWindow была несколько переработана. Теперь в процессе перебора окон я получил имя класса окна, хэндл, заголовок окна и длину текста, а полученные результаты добавил в соответствующие поля списка. В коде процедуры я использовал процедуру GetWindowText. Если окном назначения владеет текущий процесс, то GetWindowText отсылает сообщение WM_GETTEXT указанному окну. В противном случае, если у окна задан заголовок (Caption), GetWindowText возвращает текст этого заголовка. Если же заголовок не задан, то возвращаемым значением будет пустая строка.
Также я добавил кнопку «Текст окна». Ее назначение отображать текст выбранного в списке окна в специально созданном для этого компоненте (Memo3).
Код, закрепленный за кнопкой, выглядит следующим образом:
procedure TForm1.Button3Click(Sender: TObject);
// вывод текста окна
var
cb : DWord;
AHandle: HWND;
res: string;
begin
AHandle:=StrToIntDef(ListView1.Selected.SubItems[0],-1);
cb := SendMessage(AHandle, WM_GETTEXTLENGTH, 0, 0);
SetLength(Res, cb);
if cb > 0 then
SendMessage(AHandle, WM_GETTEXT, cb+1, LParam(@Res[1]));
Memo3.Lines.Clear;
Memo3.Lines.Add(Res);
end;
Комментарии излишни, аналогичный код был использован в первой части статьи. Вывод текста нужен для того, что бы достоверно убедится, тому ли окну следует отсылать сообщения.
Помимо поиска окна по имени его класса, в обновленной версии программы я организовал прямое обращение к окну по хэндлу. Это оказалось полезным, несмотря на то, что хэндл окна может меняться. Дело в том, что хэндл закрепляется за окном в момент его создания. Структура же большинства приложений такова, что все экранные формы создаются в момент запуска программы. При этом они не обязательно видны на экране. Методы Show и Close лишь показывают и прячут экранные формы, но не уничтожают их, а соответственно хэндлы форм остаются неизменными на протяжении всего сеанса работы приложения и их можно использовать для обращения к окну.
Вот пример организации классического Delphi приложения.
program Project1;
uses
Forms,
Unit1 in 'Unit1.pas' {Form1},
Unit2 in '..\Unit2.pas' {Form2};
{$R *.res}
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.CreateForm(TForm2, Form2);
Application.Run;
end.
Как вы видите, в момент старта приложения формы уже созданы. Хотя, конечно, они могут создаваться и уничтожаться в ходе работы программы, в этом случае обращение к окну по хэндлу не будет приемлемым.
В программе я реализовал возможность выбора метода поиска, как окна формы, так и окна элемента управления, отображающего текст. Подстановку найденных значений я также автоматизировал (кнопка «Выбор»).
Кроме того, некоторые параметры, такие как количество записей, обрабатываемых за один проход, время паузы, а также сочетания клавиш, передаваемые окну до и после отсылки сообщения, я сделал настраиваемыми. Сохранение полученной информации я реализовал с помощью TClientDataSet, что также, на мой взгляд, придало гибкости приложению.
Теперь моя программа получила возможность извлекать содержимое самых разнообразных баз без изменения кода, что, собственно, от нее и требовалось.
Возможно, интерфейс программы не покажется вам интуитивно понятным, поэтому я приведу пошаговую инструкцию по использованию программы.
1. Запустить обрабатываемый справочник и установить последовательность нажимаемых клавиш, для навигации по списку. Сделать активной форму, отображающую искомый текст (это может быть и главная форма справочника).
2. Переключится с формы отображения текста на программу, и нажать кнопку «Анализ».
3. В появившемся списке выбрать окно, отображающее текст, проверить правильность выбора с помощью кнопки «Текст окна».
4. Настроить критерии поиска окна вывода текста (имя класса или хэндл) и поиска окна формы, выводящей текст (имя класса, заголовок окна или хэндл) и нажать кнопку выбор. Соответствующие значения должны автоматически занестись в нужные поля.
5. Перейти на вторую страницу и внести в верхнее и нижнее поля ввода комбинации клавиш, необходимые для навигации. Таблица обозначений клавиш приводится ниже.
6. Указать файл для сохранения полученных данных, а также числовые параметры.
7. Активировать обрабатываемую программу и позиционировать курсор на первой записи в списке. Нажать кнопку «Пуск» и дождаться результата выполнения.
8. Повторить пункт 7 до тех пор, пока не будут обработаны все записи.
9. Перейти на третью страницу и просмотреть результат работы. Результаты будут сохранены в выбранный в 6-м пункте файл автоматически при закрытии программы.
Список клавиш, которые можно использовать приведен ниже.
Поддерживаемые модификаторы:
+ = Shift
^ = Control
% = Alt
При необходимости модифицировать группу символов, следует использовать круглые скобки. Например, '+abc' сместит только 'a', в то время как '+(abc)' сместит все три символа.
Поддерживаемые специальные символы
~ = Enter
( = Начало модифицируемой группы(смотри выше)
) = Конец модифицируемой группы (смотри выше)
{ = Начало названия клавиши (смотри выше)
} = Конец названия клавиши (смотри выше)
Поддерживаемые названия клавиш (необходимо заключать в фигурные скобки):
BKSP, BS, BACKSPACE
BREAK
CAPSLOCK
CLEAR
DEL
DELETE
DOWN
END
ENTER
ESC
ESCAPE
F1
F2
F3
F4
F5
F6
F7
F8
F9
F10
F11
F12
F13
F14
F15
F16
HELP
HOME
INS
LEFT
NUMLOCK
PGDN
PGUP
PRTSC
RIGHT
SCROLLLOCK
TAB
UP
Теперь давайте все же рассмотрим методы защиты окон. Говоря об ОС Windows, следует понимать, что окна используют практически все прикладные программы. Отсюда может возникнуть предположение о том, что защитить приложение от методов, аналогичных тому который описан в статье практически невозможно.
Тем не менее, способы защиты существуют, и я хочу рассмотреть один из них. Он основывается на технологии subclassing. Согласно трактовке ABBY Lingvo, subclassing в программировании для Windows - метод, позволяющий приложению перехватывать и обрабатывать сообщения, посланные некоторому окну прежде, чем оно сможет обработать его. Такое определение, на мой взгляд, достаточно точно отражает суть. Используя subclassing, мы можем переопределять реакцию окна на полученное сообщение. Давайте рассмотрим пример, который я создал для иллюстрации работы этой методики.
На главной форме нового приложения я разместил два компонента TMemo (Memo1 и Memo2) и один компонент TLabel (Label1).
В секции Private формы я описал две переменные и процедуру.
private
{ Private declarations }
OldWndProc, NewWndProc: Pointer;
i: integer;
procedure NewWinProcedure(var Msg: TMessage);
Событие OnCreate формы я обработал следующим образом:
procedure TForm1.FormCreate(Sender: TObject);
begin
NewWndProc:=MakeObjectInstance( NewWinProcedure);
OldWndProc:=Pointer(SetWindowLong( Memo2.Handle, gwl_wndProc, Cardinal(NewWndProc)));
i:=0;
Label1.Caption:=’Сообщение WM_GETTEXT было получено '+IntToStr(i)+' раз(а)';
end;
А текст процедуры NewWinProcedure выглядит так:
procedure TForm1.NewWinProcedure(var Msg: TMessage);
begin
if msg.Msg=WM_GETTEXT then
begin
Inc(i);
Label1.Caption:=’Сообщение WM_GETTEXT было получено '+IntToStr(i)+' раз(а)';
Memo2.Lines.Clear;
end
else
msg.Result:=CallWindowProc(OldWndProc, Memo2.Handle,Msg.Msg, Msg.WParam, MSg.LParam);
end;
Процедура обработки сообщений принимаемых окнами определена в Windows по умолчанию. И разработчик не знает, каким образом это делается. Но, используя SetWindowLong с параметром gwl_wndProc можно получить адрес этой процедуры. Более того, SetWindowLong позволяет задать адреспользовательской процедуры обработки сообщений для конкретного окна. Как вы заметили, OldWndProc и NewWndProc являются именно указателями. А NewWinProcedure и есть та процедура, в которой я переопределил обработку сообщения для окна компонента Memo2. Собственно, переопределил я только обработчик сообщения WM_GETTEXT. В тех случаях, когда окно получает любое другое сообщение, вызывается стандартная процедура обработки сообщения. Для этого используется функция
CallWindowProc.
Теперь, «натравив» свою программу на главную форму нового приложения, я получил ожидаемый результат. Если в списке выбран первый элемент TMemo, то при нажатии кнопки «Текст окна» содержимое этого элемента отобразится в нижней части формы, как и было при работе с энциклопедией. Однако при перемещении на второй TMemo, его текст уже не выводится. Зато каждый раз после нажатия кнопки изменяется текст метки Label1, что свидетельствует о получении окном компонента Memo2 сообщения WM_GETTEXT. Метка Label1 показывает, сколько раз окно Memo2 получило интересующее нас сообщение.
Замечу, что subclassing в Delphi можно реализовать и другими способами. Например, можно было создать наследника TMemo или обработать событие OnMessage всего приложения. Тем не менее, я выбрал именно такое решение, и, в принципе, достиг требуемого результата. Мне показалось, что постоянно переопределять механизм обработки сообщения для каждого элемента управления с целью блокировать возможность получения текста его окна – несколько неудобно. Идеальным выходом было бы написание собственного компонента, реализующего такую функцию. На основе приведенного примера сделать это оказалось уже совсем не сложно.
Вот текст несложного компонента, реализующего описанный прием.
unit SubClassingSample;
interface
uses
Windows, Messages, SysUtils, Classes, Controls;
type
TSubClassingSample = class(TComponent)
private
{ Private declarations }
fWinControl: TWinControl;
fOldWndProc, fNewWndProc: Pointer;
fActive : boolean;
function GetWinControl: TWinControl;
procedure SetWinControl(const Value: TWinControl);
procedure NewWinProcedure(var Msg: TMessage);
function GetActive: boolean;
procedure SetActive(const Value: boolean);
protected
{ Protected declarations }
public
constructor Create(AOwner: TComponent); override;
{ Public declarations }
published
{ Published declarations }
property WinControl: TWinControl read GetWinControl write SetWinControl;
property Active: boolean read GetActive write SetActive;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Samples', [TSubClassingSample]);
end;
{ TSubClassingSample }
constructor TSubClassingSample.Create(AOwner: TComponent);
begin
inherited;
end;
function TSubClassingSample.GetActive: boolean;
begin
result:=fActive;
end;
function TSubClassingSample.GetWinControl: TWinControl;
begin
result:=fWinControl;
end;
procedure TSubClassingSample.NewWinProcedure(var Msg: TMessage);
begin
if msg.Msg<>WM_GETTEXT then
msg.Result:=CallWindowProc(fOldWndProc, fWinControl.Handle,Msg.Msg, Msg.WParam, MSg.LParam);
end;
procedure TSubClassingSample.SetActive(const Value: boolean);
begin
fActive:=value;
if Assigned(fWinControl) then
if fActive then
begin
fNewWndProc:=MakeObjectInstance( NewWinProcedure);
fOldWndProc:=Pointer(SetWindowLong( fWinControl.Handle, gwl_wndProc, Cardinal(fNewWndProc)));
end;
end;
procedure TSubClassingSample.SetWinControl( const Value: TWinControl);
begin
fWinControl:=value;
end;
end.
Использование такого компонента может существенно упростить практическую реализацию переопределения реакции окна на сообщение при разработке приложения. Однако следует понимать, что злоупотреблять подобными методами нельзя. Ведь изначально разработчиками закладывается некое стандартное поведение оконных элементов управления, о тонкостях которого прикладной программист может и не подозревать. Зачастую принудительное изменение такого поведения приводит к непредсказуемым последствиям. Так если присвоить свойству Active описанного выше компонента в режиме разработки (Desighn time), то Delphi закрывается без предупреждения. Очевидно, что такое поведение является следствием переопределения обработки сообщений. Что бы избежать подобных артефактов мне пришлось в процедуре SetActive выполнить дополнительную проверку.
If not (csDesigning in ComponentState) thenSetWindowLong будет вызываться только тогда, когда приложение находится не в режиме разработки.
Подводя итог, я хотел бы сказать, что в прикладных программах, работающих под Windows, вывод информации посредствам окон является наиболее распространенным и простым способом. И не смотря на то, что описанный метод доступа к информации не слишком распространен, его можно реально использовать. Это касается не только текстовой информации. Теоретически подобным образом можно извлекать графическую информацию или получать доступ к буферу обмена…