Создание сканера виртуальной памяти процессов — Архив WASM.RU

Все статьи

Создание сканера виртуальной памяти процессов — Архив WASM.RU

Исходный код

1. Введение
2. Идея
3. Алгоритм
4. Недостатки
5. Код
6. Источники

1. Введение

Предмет повествования состоит в изучении принципов сканирования виртуальной памяти процессов (этим занимаются такие программы как ArtMoney и др.), создании и реализации алгоритма целочисленного "универсального обманщика игр".

Задача: Найти адрес переменной в адресном пространстве целевого процесса.

Ограничения: Для простоты рассматриваются целочисленные переменные размером dword, поиск проводится по младшему байту.

2. Идея:

Организация поиска в страницах памяти, доступных для чтения и записи, целевого процесса начиная с адреса 010000h по 7FFF0000h [1]. Для получения информации о страницах памяти внешнего процесса естественно использовать VirtualQueryEx. Для чтения памяти - ReadProcessMemory.

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

Алгоритм верхнего уровня детализации для первого прохода выглядит так:

  1. Получить описание страницы памяти чужого процесса;
  2. ЕСЛИ страница принадлежит диапазону поиска то п. 3 ИНАЧЕ выход;
  3. ЕСЛИ страница не доступна для чтения и записи ТО п. 5;
  4. сохранить в список все адреса переменных, значения которых равны заданному;
  5. Перейти к следующей странице и п. 1;

Алгоритм последующих проходов:

  1. Прочитать значение переменной по адресу из списка (в памяти исследуемого процесса);
  2. ЕСЛИ значение равно заданному ТО п. 4;
  3. Удалить адрес из списка;
  4. ЕСЛИ список не закончился ТО Перейти к адресу следующей переменной и п. 1;

3. Алгоритм

Общий алгоритм программы состоит из нескольких частей:

Организация списка адресов.
Так как заранее неизвестно количество найденных переменных при первом проходе (в дальнейшем оно будет уменьшаться), логично зарезервировать некоторый объем памяти, скажем 4*VARMAX, где VARMAX - это наибольшее возможное количество найденных переменных. Это число, на практике, небесконечно и определяется, с одной стороны, временем максимально долгого возможного поиска, а с другой, - достаточностью объема для проведения поиска. В среднем хватает 10*1024*1024 переменных. Причем, следует отметить, что при начальном поиске переменных со значением 0, велика вероятность нехватки указанного объема, по нескольким причинам, в том числе и из-за чтения зарезервированных областей (еще не используемых процессом), которые возвращают 0. Множитель 4 введен исходя из размера хранимого адреса переменной равного dword. Легко видеть что нет необходимости передавать всю зарезервированную память адресов сразу (это можно сделать при первом обращении к ней), т.о. можно считать подготовку списка адресов переменных завершенной:

;резервирование с передачей физической памяти при обращении.
invoke  VirtualAlloc,0,BSIZE,MEM_RESERVE+MEM_COMMIT,PAGE_READWRITE

;ЕСЛИ не удалось выделить память, ТО выход
;(далее для краткости обработка ошибок опускается)
test    eax,eax
jz      NotEnoughMem

;ИНАЧЕ сохранение адреса начала блока памяти.
mov     [addrbuffer],eax

Примечание: здесь и далее программный код приводится в соответствии с синтаксисом flat assembler (fasm)[2].

Получение Handle процесса для чтения, записи.
Предлагается следующий путь: Сначала получим Handle окна необходимого процесса, затем получим идентификатор процесса создавшего окно, затем "откроем" процесс для проведения операций с памятью.

;ebx содержит адрес названия необходимого окна.
invoke  FindWindow,0,ebx

;создать в стеке переменную, и передать ее адрес из esp в параметры
push    eax
;для получения идентификатора процесса
invoke  GetWindowThreadProcessId,eax,esp
pop    eax ;вынуть из стека результат.

;получить Handle процесса для операций с памятью
invoke  OpenProcess,PROCESS_QUERY_INFORMATION+PROCESS_VM_OPERATION+PROCESS_VM_READ+PROCESS_VM_WRITE,0,eax
mov     [hProcess],eax ;сохранить Handle открытого процесса.

Организация считывания целевых (RW) страниц.
Итак, по порядку: получаем адрес начала и размер текушей страницы памяти, проверяем права доступа на чтение и запись. Создаем блок памяти требуемого размера в текущем адресном пространстве (для копирования), читаем блок из исследуемого процесса в созданный блок. После отработки поиска байтов освобождаем уже не нужный блок.

;Сначала [fmbi.BaseAddress] = начальному адресу 10000h,
;а [fmbi.RegionSize] = 0, BaseAddress+RegionSize = адресу первого блока
getblock:
;в дальнейшем BaseAddress+RegionSize = адресу следующего блока
mov     eax,[fmbi.BaseAddress]
add     eax,[fmbi.RegionSize]

mov     [address],eax
;прочитать состояние блока
invoke  VirtualQueryEx,[hProcess],[address],fmbi,MBIsz
cmp     [fmbi.Protect],PAGE_READWRITE ;проверить доступ
jne      getblock ;ЕСЛИ нет возможности чтения и записи, ТО перейти к новому блоку

;создать блок нужного размера блока
invoke  VirtualAlloc,0,[fmbi.RegionSize],MEM_COMMIT,PAGE_READWRITE
mov     [buffer],eax

;скопировать в него память исследуемого процесса
invoke  ReadProcessMemory,[hProcess],[fmbi.BaseAddress],eax,[fmbi.RegionSize],BytesRead

;... осушествить поиск и сохранение адресов

;освободить блок
invoke  VirtualFree,[buffer],0,MEM_RELEASE

Сохранение адресов искомых переменных.
Запустить сканирование до совпадения байта или конца строки, получить адрес совпадающего байта в пространстве искомого процесса (учитывая то, что поиск ведется в созданном блоке текущего процесса), сохранить адрес в списке.

;вход:
;	ecx = edx = количеству прочитанных байт
;	esi содержит адрес блока (в текущем адресном пространстве)
;	edi - адрес списка
;	ebx - адрес начала блока в адресном пространстве исследумого процесса

lfind:
xchg    edi,esi
repne   scasb ;поиск до совпадения al с байтом из строки edi
xchg    edi,esi

jne     getblock;ЕСЛИ ничего не найдено ТО к следующему блоку

;Адрес = Адрес начала блока в исследуемом процессе + смещение в текущем
;Смещение в текущем = количество прочитанных байт edx - оставшиеся байты ecx
neg     ecx ;изменить знак
lea     eax,[edx+ecx] ;получить смещение
dec     eax ;учесть что считаем с 0, а не с 1
add     eax,ebx ;добавить [fmbi.BaseAddress]
stosd ;сохранить адрес в список адресов

neg     ecx ;проверить последний ли это байт
jz      getblock ;ЕСЛИ да ТО перейти к следующему блоку
jmp     lfind ;ИНАЧЕ проболжать поиск в текущем блоке

Примечание: В [3] использование префиксов описано с ошибками.

Определение действительных адресов искомых переменных (дальнейший поиск).
Получить адрес из списка, прочитать память процесса, сравнить прочитанное значение с заданым, если != то удалить адрес. (Читателю предлагается самостоятельно реализовать рассмотренный подход).

4. Недостатки

Из приведенного кода легко видеть что узким местом является ReadProcessMemory. Во избежание ее использования приведенный код можно оформить в виде dll и внедрить в исследуемый процесс. В этом случае библиотека будет сканировать область памяти "своего" процесса, также упрощается основной цикл поиска, так как необходимость в пересчете адреса отпадает.

5. Код

К статье приложен исходный код и откомпилированный исполнимый модуль, который успешно прошел аппробацию на различных примерах, таких как HMM, B&W, Gothic, POP, Yager и др. Скорость нахождения, во многих случаях, была, по крайней мере, не хуже чем при использовании ArtMoney.

6. Достаточные источники:

  1. Рихтер Дж. Windows для профессионалов: создание эффективных Win32-приложений с учетом специфики 64-разрядной версии Windows/Пер. с англ. - 4-е изд. - СПб: Питер; М.: Издадельско-торговый дом "Русская Редакция", 2001.
  2. Tomasz Grysztar. Flat Assembler.
  3. Зубков С.В. Assembler. Для DOS, Windows и Unix. - М.: ДМК, 1999.


2002-2013 (c) wasm.ru