Юрий Писарев дата публикации 05-05-2003 13:28 Пример работы с "чужими" процессами — компонент TMemoryInspector
Содержание:
Компонент предназначен для доступа к адресному пространству чужого процесса. Позволяет читать память процесса, записывать данные любой длины в память процесса и замораживать данные любой длины в памяти процесса. Можно работать одновременно с любым количеством запущенных на компьютере процессов.
Принцип работы компонента |
Чтобы получить доступ к процессу, компонент использует динамическую библиотеку. Устанавливается ловушка типа 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;
Описание атрибутов защиты страниц памяти:
- apPageReadOnly: Разрешено только чтение страницы
- apPageReadWrite: Разрешены только чтение страницы и запись на странице
- apPageWriteCopy: Разрешена только запись на странице, которая приводит к предоставлению копии страницы, после чего этот флаг убирается
- apPageExecute: Разрешено только исполнение содержимого страницы
- apPageExecuteRead: Разрешены только чтение страницы и исполнение содержимого страницы
- apPageExecuteReadWrite: Нет ограничений
- apPageExecuteWriteCopy: Нет ограничений, любые операции приводят к предоставлению копии страницы, после чего этот флаг убирается
- apPageNoAccess: Нет доступа
Описание специальные атрибутов защиты страниц памяти:
- spPageGuard: Попытка доступа к содержимому страницы вызывает исключение, после чего этот флаг убирается
- spPageNoCache: Отключает кэширование группы страниц памяти
Тип страниц регионов памяти:
- ptMemImage: Указывает что страницы региона памяти отображены на EXE или DLL файл, спроецированный в память
- ptMemMapped: Указывает что страницы региона памяти отображены на файл данных, спроецированный в память
- ptMemPrivate: Указывает что страницы региона памяти отображены на страничный файл памяти
Структура TReadOptions:
- Поле ChangeProtect обозначает будут ли производиться попытки получить доступ к защищенным блокам памяти. Защищенными считаются те блоки памяти, атрибуты которых не определены полями ProhibitedProtect, PermittedProtect, ProhibitedSpecialProtect и ProhibitedPageType
- Поле ProhibitedProtect определяет запрещенный набор атрибутов страниц памяти. Любой блок памяти, имеющий страницы с один из таких атрибутов, будет проигнорирован
- Поле PermittedProtect определяет разрешенный набор атрибутов страниц памяти
- Поле ProhibitedSpecialProtect определяет запрещенный набор специальных атрибутов страниц памяти. Любой блок памяти, имеющий страницы с один из таких атрибутов, будет проигнорирован
- Поле ProhibitedPageType определяет запрещенные типы страниц памяти. Любой блок памяти, имеющий страницы таких типов, будет проигнорирован
Значение свойства ReadOptions по умолчанию настроено оптимальным образом.
Как уже было сказано, вся память редактируемого процесса разбита на регионы и блоки. Когда библиотека читает память, она берет ее по кусочкам из каждого блока, а потом склеивает воедино и передает в компонент. Таким образом, каждый байт полученной памяти принадлежит какому-то блоку в том процессе, где он был взят. Так вот, если у вас есть, например, память какого-то процесса размером, скажем, в 10 мегабайт, и вы обнаружили в этой памяти нужное вам число, адрес которого 100 байт, то вы, естественно, хотите изменить его.
Можно поступить несколькими способами:
- Первый способ – это передать соответствующим методам записи адрес этого числа – 100 байт, то есть локальный адрес, а также новое число, на которое вы хотите изменить старое. Этот способ рассмотрим несколько позже.
- Второй способ – это получить блок памяти в редактируемом процессе, которому соответствует число по локальному адресу 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 нужно только для методов записи. Тем не менее, я кратко опишу эти поля:
- AllocationBase: Начальный адрес региона памяти
- BaseAddress: Начальный адрес блока памяти
- AllocationProtect: Атрибуты защиты региона памяти, присвоенные ему по время резервирования
- Protect: Атрибуты защиты блока памяти
- SpecialProtect: Специальные атрибуты защиты блока памяти
- PageState: Состояние страниц блока памяти
- RegionSize: Размер блока памяти
- PageType: Тип физической памяти страниц блока
Все подготовительные работы для записи числа по адресу 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 имеют тип:
- Pointer; ссылка на величину записи
- TByteArray = array of Byte; массив байт неограниченной длины
- String; длинная строка
В итоге я приведу полный код примера, в котором требуется записать в память процесса число по локальному адресу 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 байта в исходный блок памяти. Ниже приведен список функций первого типа:
- function Write(LocalAddress: Longword; Buffer: TShareBuffer; Length: Longword = 0): Boolean; overload; virtual;
- function WriteByte(LocalAddress: Longword; Value: Byte): Boolean; overload; virtual;
- function WriteWord(LocalAddress: Longword; Value: Word): Boolean; overload; virtual;
- function WriteLongword(LocalAddress: Longword; Value: Longword): Boolean; overload; virtual;
- function WriteInt64(LocalAddress: Longword; Value: Int64): Boolean; overload; virtual;
- function WriteSingle(LocalAddress: Longword; Value: Single): Boolean; overload; virtual;
- function WriteDouble(LocalAddress: Longword; Value: Double): Boolean; overload; virtual;
- function WriteExtended(LocalAddress: Longword; Value: Extended): Boolean; overload; virtual;
- function WriteString(LocalAddress: Longword; Value: ShortString): Boolean; overload; virtual;
- function WriteBuffer(LocalAddress: Longword; Value: Pointer; Length: Longword): Boolean; overload; virtual;
- function WriteBuffer(LocalAddress: Longword; Value: TByteArray): Boolean; overload; virtual;
- function WriteBuffer(LocalAddress: Longword; Value: string): Boolean; overload; virtual;
Список этих функций соответствует уже рассмотренному списку функций, есть лишь некоторая разница лишь в параметрах. Параметр 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;
…
Технология заморозки значений практически ничем не отличается от технологии записи на уровне блока памяти:
- function Freeze(Elapse: Longword; MemoryRegion: TRegion; Start, Beginning: Longword; Buffer: TShareBuffer; Length: Longword = 0): Boolean; overload; virtual;
- function FreezeByte(Elapse: Longword; MemoryRegion: TRegion; Start, Beginning: Longword; Value: Byte): Boolean; virtual;
- function FreezeWord(Elapse: Longword; MemoryRegion: TRegion; Start, Beginning: Longword; Value: Word): Boolean; virtual;
- function FreezeLongword(Elapse: Longword; MemoryRegion: TRegion; Start, Beginning: Longword; Value: Longword): Boolean; virtual;
- function FreezeInt64(Elapse: Longword; MemoryRegion: TRegion; Start, Beginning: Longword; Value: Int64): Boolean; virtual;
- function FreezeSingle(Elapse: Longword; MemoryRegion: TRegion; Start, Beginning: Longword; Value: Single): Boolean; virtual;
- function FreezeDouble(Elapse: Longword; MemoryRegion: TRegion; Start, Beginning: Longword; Value: Double): Boolean; virtual;
- function FreezeExtended(Elapse: Longword; MemoryRegion: TRegion; Start, Beginning: Longword; Value: Extended): Boolean; virtual;
- function FreezeString(Elapse: Longword; MemoryRegion: TRegion; Start, Beginning: Longword; Value: ShortString): Boolean; virtual;
- function FreezeBuffer(Elapse: Longword; MemoryRegion: TRegion; Start, Beginning: Longword; Value: Pointer; Length: Longword): Boolean; overload; virtual;
- function FreezeBuffer(Elapse: Longword; MemoryRegion: TRegion; Start, Beginning: Longword; Value: TByteArray): Boolean; virtual;
- function FreezeBuffer(Elapse: Longword; MemoryRegion: TRegion; Start, Beginning: Longword; Value: string): Boolean; virtual;
Как видно, список этих функций отличается от списка функций записи только одним дополнительным параметром. Параметр 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;
…
Компонент состоит из нескольких частей. Первая часть это, собственно, сам компонент и набор необходимых вторичных компонентов. Вторая часть это используемая компонентом библиотека. Несколько слов о вторичных компонентах и файлах:
- Компонент TMemoryManager предназначен для получения информации о доступных процессах и их окнах, а также для чтения памяти и записи в память
- Компонент TFileManager предназначен для создания файла, отображаемого в память и дальнейшей работы с таким файлом
- Файл MemUtils содержит некоторые общие типы и данные
Порядок установки компонента:
- Установить компоненты TMemoryManager, TFileManager, файл MemUtils
- Получить файл Mi.dll и переместить его в директорию, где находится пакет с установленными компонентами
- Установить компонент TMemoryInspector
Необходимо, чтобы еще одна копия файла Mi.dll находилась в одной директории с исходными файлами программы, использующей компонент.
Скачать: MemUtils.zip (209 K; обновление от 31.05.04)
Архив содержит:
- компонент TMemoryInspector
- компонент TMemoryManager
- компонент TFileManager
- файл MemUtils
- библиотеку Mi
А так же, дополнительную информацию и пример по использованию этого компонента.
Юрий Писарев
[Взаимодействие с 'чужими' процессами/приложениями] [Работа с памятью] [WM_NULL]
Обсуждение материала [ 27-08-2004 18:04 ] 18 сообщений |