Версия для печати


Пример работы с "чужими" процессами — компонент TMemoryInspector
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=788

Юрий Писарев
дата публикации 05-05-2003 13:28

Пример работы с "чужими" процессами — компонент TMemoryInspector

Содержание:
  1. Введение
  2. Принцип работы компонента
  3. Чтение памяти процесса
  4. Блоки памяти
  1. Простые методы записи
  2. Заморозка значений
  3. Установка компонента
  4. Ресурсы

Введение

Компонент предназначен для доступа к адресному пространству чужого процесса. Позволяет читать память процесса, записывать данные любой длины в память процесса и замораживать данные любой длины в памяти процесса. Можно работать одновременно с любым количеством запущенных на компьютере процессов.

Принцип работы компонента

Чтобы получить доступ к процессу, компонент использует динамическую библиотеку. Устанавливается ловушка типа WH_CALLWNDPROC, которая реагирует на сообщения, посылаемые функцией SendMessage. После установки ловушки, компонент посылает сообщение WM_NULL целевому окну и, библиотека отображается на адресное пространство процесса (если не была отображена раньше), которому принадлежит целевое окно.

Эта технология подробно описывается в книге Джеффри Рихтера (как и ряд других технологий для тех же целей), поэтому особо подробно останавливаться не буду. Для того чтобы активировать компонент, предназначен метод:

function Activate: Boolean; virtual;
который устанавливает ловушку, создает информационный файл, отображаемый в память, в общем, делает некоторую подготовительную работу. Для того чтобы деактивировать компонент, предназначен метод:
function Deactivate: Boolean; virtual;
который убирает ловушку и закрывает все отображаемые в память файлы. Впрочем, активировать и деактивировать компонент, а также проследить его состояние можно, используя свойство:
property Active: Boolean;
Явно вызывать метод Activate не обязательно, так как при вызове методов компонента проверяется статус компонента и, если надо, производится его активация. Метод:
function UpdateWndData: Boolean; virtual;
получает информацию обо всех доступных в системе процессах и их окнах. После вызова этой функции становятся доступны следующие свойства:
property ProcessId_: THandles;
содержит дескрипторы процессов;
property ProcessSize: TIntArray;
содержит размеры процессов;
property WndHandle: THandles;
содержит дескрипторы окон процессов;
property WndClassName: TStrings;
содержит имена классов окон;
property WndText: TStrings;
содержит заголовки окон;
property ModuleFileName: TStrings;
содержит имена исполняемых файлов окон; Вышеперечисленные свойства представляет собой массивы данных, где любой элемент массива соответствует элементу любого другого массива с тем же индексом. Таким образом, все вышеперечисленные свойства имеют одинаковую длину, а элементы свойств с одинаковыми индексами относятся к одному и тому же процессу. Свойство компонента:
property Selected: Integer;
обозначает индекс выбранного элемента вышеперечисленных массивов. Это свойство можно установить исходя из, например, заголовка окна:
var
  Index: Integer;
begin
  Index := 10;
  with MemoryInspector do if WndText[Index] = ‘Microsoft Internet Explorer’ then
    Selected := Index;
end;
…
или это свойство можно установить исходя из имени класса окна:
var
  Index: Integer;
begin
  Index := 10;
  with MemoryInspector do if WndClassName[Index] = ‘IEFrame’ then
    Selected := Index;
end;
…
Зная дескриптор окна, можно получить индекс, соответствующий свойству Selected:
function GetWindowIndex(Window: THandle): Integer; virtual;

Чтение памяти процесса

Чтение памяти процесса осуществляется функцией:

function PeekData: Boolean; virtual;
Перед тем, как считывать данные процесса, необходимо установить некоторые свойства компонента. Первым делом, необходимо обновить информацию обо всех доступных в системе процессах и их окнах. После этого выбрать какое-нибудь окно целевого процесса и установить свойство Selected. В результате вызова функции PeekData данные записываются в поток памяти. Этот поток вы должны создать сами и установить ссылку на объект потока в свойстве компонента:
property StreamRef: TMemoryStream;
После того, как заданы свойства Selected и StreamRef, можно вызывать функцию PeekData. Работа функции PeekData зависит от свойства:
property UpdateMemory: Boolean;
Это свойство обозначает, будет ли перед считыванием памяти обновляться информация о регионах памяти и их блоках в выбранном процессе. Если это свойство истинно, то размер считываемой памяти будет, вероятно, изменяться. Когда память считывается первый раз, это свойство значение не имеет, библиотека обновляет информацию о памяти процесса в любом случае. В последующих вызовах функции PeekData можно либо заново обновить информацию (UpdateMemory = True), либо использовать ту информацию о памяти, которая была получена в первый раз (UpdateMemory = False). Работа функции PeekData также зависит от свойства компонента, которое определяет правила чтения памяти или записи в память:
property ReadOptions: TReadOptions;
где
TProtect = (apPageReadOnly, apPageReadWrite, apPageWriteCopy,
  apPageExecute, apPageExecuteRead, apPageExecuteReadWrite,
  apPageExecuteWriteCopy, apPageNoAccess);

TProtectSet = set of TProtect;
определяет набор атрибутов защиты страниц памяти;
TSpecial = (spPageGuard, spPageNoCache);

TSpecialProtect = set of TSpecial;
определяет набор специальных атрибутов защиты страниц памяти;
TPageType = (ptMemImage, ptMemMapped, ptMemPrivate);

TPageTypeSet = set of TPageType;
определяет тип физической памяти страниц
TReadOptions = record
  ChangeProtect: Boolean;
  ProhibitedProtect, PermittedProtect: TProtectSet;
  ProhibitedSpecialProtect: TSpecialProtect;
  ProhibitedPageType: TPageTypeSet;
end;
Описание атрибутов защиты страниц памяти: Описание специальные атрибутов защиты страниц памяти: Тип страниц регионов памяти: Структура TReadOptions: Значение свойства ReadOptions по умолчанию настроено оптимальным образом.

Блоки памяти

Как уже было сказано, вся память редактируемого процесса разбита на регионы и блоки. Когда библиотека читает память, она берет ее по кусочкам из каждого блока, а потом склеивает воедино и передает в компонент. Таким образом, каждый байт полученной памяти принадлежит какому-то блоку в том процессе, где он был взят. Так вот, если у вас есть, например, память какого-то процесса размером, скажем, в 10 мегабайт, и вы обнаружили в этой памяти нужное вам число, адрес которого 100 байт, то вы, естественно, хотите изменить его. Можно поступить несколькими способами:

Для получения блока памяти используется функция:
function TMemoryInspector.GetMemoryRegion(LocalAddress: Longword): Boolean;
Ее параметр LocalAddress это и есть адрес числа в нашем примере, по которому мы хотим получить соответствующий блок в редактируемом процессе. В результате вызова этой функции изменяются некоторые свойства компонента:
property Beginning: Integer;
Используется библиотекой и обозначает сумму размеров разрешенных блоков памяти, предшествующих полученному блоку. Значение этого свойства необходимо для некоторых методов записи.
property MemoryRegion: TRegion;

TRegion = record
  AllocationBase, BaseAddress: Pointer;
  AllocationProtect, Protect: TProtect;
  SpecialProtect: TSpecialProtect;
  PageState: TPageState;
  RegionSize: Longword;
  PageType: TPageType;
end;
Это и есть нужный нам блок памяти. Отдельные значения полей записи TRegion вряд ли вам понадобятся, так как свойство MemoryRegion нужно только для методов записи. Тем не менее, я кратко опишу эти поля: Все подготовительные работы для записи числа по адресу 100 завершены. Теперь можно приступать к записи:
function Write(MemoryRegion: TRegion; Start, Beginning: Longword; 
	Buffer: TShareBuffer; Length: Longword = 0): Boolean; overload; virtual;
function WriteByte(MemoryRegion: TRegion; Start, Beginning: Longword; 
	Value: Byte): Boolean; overload; virtual;
function WriteWord(MemoryRegion: TRegion; Start, Beginning: Longword; 
	Value: Word): Boolean; overload; virtual;
function WriteLongword(MemoryRegion: TRegion; Start, Beginning: Longword; 
	Value: Longword): Boolean; overload; virtual;
function WriteInt64(MemoryRegion: TRegion; Start, Beginning: Longword; 
	Value: Int64): Boolean; overload; virtual;
function WriteSingle(MemoryRegion: TRegion; Start, Beginning: Longword; 
	Value: Single): Boolean; overload; virtual;
function WriteDouble(MemoryRegion: TRegion; Start, Beginning: Longword; 
	Value: Double): Boolean; overload; virtual;
function WriteExtended(MemoryRegion: TRegion; Start, Beginning: Longword; 
	Value: Extended): Boolean; overload; virtual;
function WriteString(MemoryRegion: TRegion; Start, Beginning: Longword; 
	Value: ShortString): Boolean; overload; virtual;
function WriteBuffer(MemoryRegion: TRegion; Start, Beginning: Longword; 
	Value: Pointer; Length: Longword): Boolean; overload; virtual;
function WriteBuffer(MemoryRegion: TRegion; Start, Beginning: Longword; 
	Value: TByteArray): Boolean; overload; virtual;
function WriteBuffer(MemoryRegion: TRegion; Start, Beginning: Longword; 
	Value: string): Boolean; overload; virtual;
Осталось выбрать наиболее подходящую функцию для записи. Несколько слов об общих параметрах функций. Параметры MemoryRegion и Beginning это то, о чем мы только что говорили. Параметр Start это адрес начала записи в масштабе блока памяти: Start = LocalAddress – Beginning. Базовый метод записи Write требует параметр Buffer, который имеет тип:
TShareBuffer = record
  case Byte of
    0: (ByteArray: TSmallByteArray);
    1: (CharArray: TSmallCharArray);
    2: (ValueRecord: TValueRecord);
    3: (Float80: Extended);
  end;
где
TSmallByteArray = array[Byte] of Byte;

TSmallCharArray = array[Byte] of Char;

TValueRecord = record
  case Byte of
    0: (ByteArray: array[0..7] of Byte);
    1: (Signed8: Shortint);
    2: (Unsigned8: Byte);
    3: (Signed16: Smallint);
    4: (Unsigned16: Word);
    5: (Signed32: Longint);
    6: (Unsigned32: Longword);
    7: (Signed64: Int64);
    8: (Float32: Single);
    9: (Float64: Double);
  end;
Как видно, параметром Buffer может быть представлено практически любое значение, имеющее наиболее распространенный тип и небольшой размер. Если требуется записать значение, длина которого превышает размер структуры TShareBuffer, следует использовать методы типа WriteBuffer. Такие методы могут записывать значения неограниченной длины. Параметры Value методов WriteBuffer имеют тип: В итоге я приведу полный код примера, в котором требуется записать в память процесса число по локальному адресу 100: …
const
  Value: Int64 = 1000;
var
  LocalAddress, Start: Integer;
  MemoryInspector: TMemoryInspector;
  Stream: TMemoryStream;
begin
  MemoryInspector := TMemoryInspector.Create(Self);
  MemoryInspector.Parent := Self;
  with MemoryInspector do
  begin
    // Получаем информацию обо всех процессах и их окнах:
    UpdateWndData;
    // Выбираем самый первый процесс:
    Selected := 0;
    // Устанавливаем адрес:
    LocalAddress := 100;
    // Получаем блок памяти:
    GetMemoryRegion(LocalAddress);
    // Устанавливаем начало записи:
    Start := LocalAddress - Beginning;
    // Запись:
    WriteInt64(MemoryRegion, Start, Beginning, Value);
    // Память процесса можно загрузить в поток и сохранить в файл:
    Stream := TMemoryStream.Create;
    try
      StreamRef := Stream;
      PeekData;
      Stream.SaveToFile('stream.dat');
    finally
      Stream.Free;
    end;
  end;
…

Простые методы записи

Второй метод записи был только что подробно рассмотрен. Теперь пришла очередь описать первый метод, наиболее простой. Он работает немного медленнее предыдущего, но все же обладает некоторым преимуществом. Представьте себе ситуацию, вы собираетесь записать число размером, скажем 10 байт. Тот блок, в котором будет производиться запись, имеет размер, например 4096 байт, запись начинается с 4092 байта. Получается, что в регион может быть записано только 4 байта, а нужно записать 10 байт. Функции второго типа, которые были рассмотрены в предыдущей главе, в такой ситуации запишут только 4 байта из 10. Функции первого типа ведут себя иначе и в рассматриваемой ситуации сначала найдут следующий блок памяти, запишут в него неуместившиеся 6 байт, после чего запишут первые 4 байта в исходный блок памяти. Ниже приведен список функций первого типа:

Список этих функций соответствует уже рассмотренному списку функций, есть лишь некоторая разница лишь в параметрах. Параметр LocalAddress обозначает адрес начала записи в масштабе полученной памяти редактируемого процесса, т.е. в масштабе объекта потока памяти, на который ссылается свойство StreamRef. Я приведу код примера, который обсуждался в предыдущей главе, но применительно к рассматриваемым методам записи:
const
  Value: Int64 = 1000;
var
  LocalAddress: Integer;
  MemoryInspector: TMemoryInspector;
  Stream: TMemoryStream;
begin
  MemoryInspector := TMemoryInspector.Create(Self);
  MemoryInspector.Parent := Self;
  with MemoryInspector do
  begin
    // Получаем информацию обо всех процессах и их окнах:
    UpdateWndData;
    // Выбираем самый первый процесс:
    Selected := 0;
    // Устанавливаем адрес:
    LocalAddress := 100;
    // Запись:
    WriteInt64(LocalAddress, Value);
    // Память процесса можно загрузить в поток и сохранить в файл:
    Stream := TMemoryStream.Create;
    try
      StreamRef := Stream;
      PeekData;
      Stream.SaveToFile('stream.dat');
    finally
      Stream.Free;
    end;
  end;
…

Заморозка значений

Технология заморозки значений практически ничем не отличается от технологии записи на уровне блока памяти:

Как видно, список этих функций отличается от списка функций записи только одним дополнительным параметром. Параметр Elapse обозначает частоту обновления в миллисекундах. После вызова любой из этих функций, изменяется свойство компонента, которое обозначает состояние заморозки:
property Frozen: Boolean;
В любой момент времени может быть заморожено только одно значение одного процесса. Для разморозки предназначена функция:
function Unfreeze: Boolean; virtual;
Ниже приведен код пример, который рассматривался в предыдущих главах, где вместо записи мы замораживаем значение:
const
  Value: Int64 = 1000;
var
  LocalAddress, Start: Integer;
  MemoryInspector: TMemoryInspector;
  Stream: TMemoryStream;
begin
  MemoryInspector := TMemoryInspector.Create(Self);
  MemoryInspector.Parent := Self;
  with MemoryInspector do
  begin
    // Получаем информацию обо всех процессах и их окнах:
    UpdateWndData;
    // Выбираем самый первый процесс:
    Selected := 0;
    // Устанавливаем адрес:
    LocalAddress := 100;
    // Получаем блок памяти:
    GetMemoryRegion(LocalAddress);
    // Устанавливаем начало заморозки:
    Start := LocalAddress - Beginning;
    // Заморозка с интервалом обновления 500 мс:
    FreezeInt64(500, MemoryRegion, Start, Beginning, Value);

    …

    // Разморозка:
    Unfreeze;
    // Память процесса можно загрузить в поток и сохранить в файл:
    Stream := TMemoryStream.Create;
    try
      StreamRef := Stream;
      PeekData;
      Stream.SaveToFile('stream.dat');
    finally
      Stream.Free;
    end;
  end;
…

Установка компонента

Компонент состоит из нескольких частей. Первая часть это, собственно, сам компонент и набор необходимых вторичных компонентов. Вторая часть это используемая компонентом библиотека. Несколько слов о вторичных компонентах и файлах:

Порядок установки компонента: Необходимо, чтобы еще одна копия файла Mi.dll находилась в одной директории с исходными файлами программы, использующей компонент.

Ресурсы

Скачать: MemUtils.zip (209 K; обновление от 31.05.04)
Архив содержит:

А так же, дополнительную информацию и пример по использованию этого компонента.

Юрий Писарев