Версия для печати
Пример работы с "чужими" процессами — компонент TMemoryInspector
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=788Юрий Писарев
дата публикации 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: Отключает кэширование группы страниц памяти
Структура TReadOptions:
- ptMemImage: Указывает что страницы региона памяти отображены на EXE или DLL файл, спроецированный в память
- ptMemMapped: Указывает что страницы региона памяти отображены на файл данных, спроецированный в память
- ptMemPrivate: Указывает что страницы региона памяти отображены на страничный файл памяти
Значение свойства ReadOptions по умолчанию настроено оптимальным образом.
- Поле ChangeProtect обозначает будут ли производиться попытки получить доступ к защищенным блокам памяти. Защищенными считаются те блоки памяти, атрибуты которых не определены полями ProhibitedProtect, PermittedProtect, ProhibitedSpecialProtect и ProhibitedPageType
- Поле ProhibitedProtect определяет запрещенный набор атрибутов страниц памяти. Любой блок памяти, имеющий страницы с один из таких атрибутов, будет проигнорирован
- Поле PermittedProtect определяет разрешенный набор атрибутов страниц памяти
- Поле ProhibitedSpecialProtect определяет запрещенный набор специальных атрибутов страниц памяти. Любой блок памяти, имеющий страницы с один из таких атрибутов, будет проигнорирован
- Поле ProhibitedPageType определяет запрещенные типы страниц памяти. Любой блок памяти, имеющий страницы таких типов, будет проигнорирован
Блоки памяти Как уже было сказано, вся память редактируемого процесса разбита на регионы и блоки. Когда библиотека читает память, она берет ее по кусочкам из каждого блока, а потом склеивает воедино и передает в компонент. Таким образом, каждый байт полученной памяти принадлежит какому-то блоку в том процессе, где он был взят. Так вот, если у вас есть, например, память какого-то процесса размером, скажем, в 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 нужно только для методов записи. Тем не менее, я кратко опишу эти поля:Все подготовительные работы для записи числа по адресу 100 завершены. Теперь можно приступать к записи:
- AllocationBase: Начальный адрес региона памяти
- BaseAddress: Начальный адрес блока памяти
- AllocationProtect: Атрибуты защиты региона памяти, присвоенные ему по время резервирования
- Protect: Атрибуты защиты блока памяти
- SpecialProtect: Специальные атрибуты защиты блока памяти
- PageState: Состояние страниц блока памяти
- RegionSize: Размер блока памяти
- PageType: Тип физической памяти страниц блока
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: …
- Pointer; ссылка на величину записи
- TByteArray = array of Byte; массив байт неограниченной длины
- String; длинная строка
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. Я приведу код примера, который обсуждался в предыдущей главе, но применительно к рассматриваемым методам записи:
- 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;
… 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 обозначает частоту обновления в миллисекундах. После вызова любой из этих функций, изменяется свойство компонента, которое обозначает состояние заморозки:
- 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;
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 содержит некоторые общие типы и данные
Необходимо, чтобы еще одна копия файла Mi.dll находилась в одной директории с исходными файлами программы, использующей компонент.
- Установить компоненты TMemoryManager, TFileManager, файл MemUtils
- Получить файл Mi.dll и переместить его в директорию, где находится пакет с установленными компонентами
- Установить компонент TMemoryInspector
Ресурсы Скачать: MemUtils.zip (209 K; обновление от 31.05.04)
Архив содержит:А так же, дополнительную информацию и пример по использованию этого компонента.
- компонент TMemoryInspector
- компонент TMemoryManager
- компонент TFileManager
- файл MemUtils
- библиотеку Mi