Process API 0.2 — процессы, запись, сканирование и профили
Новые разрешения process.handles_threads, process.write, process.scan
требуют manifest "api":"0.2". Старые пакеты 0.1 продолжают работать.
SDK имеет версию 0.3, формат IPC/выдачи остаётся 0.1. Все методы используют
обычные Windows-права для явно выбранного PID. Settings → Read through driver
или --allow-process PID --process-backend driver выбирает чтение через
установленный EasyUniversalBase с READ_INFO/READ. По умолчанию используется
Windows backend. Выбор драйвера выключен изначально, сбрасывается при смене
PID/пакета и фиксируется на время сессии; недоступный драйвер выдаёт ошибку
до вызовов скрипта, без автоматической подмены backend.
Существующие process::read_*, bytebuffer, RIP и pattern reads проходят через
тот же ProcessReader и прежние квоты: 256 операций / 1 MiB на callback,
1–65536 байт на запрос. Неполное чтение очищает весь результат скрипта и
возвращает false/last_error. PEB module reads также используют выбранный режим;
перечисление процессов/модулей system/регионов и отдельная запись остаются
обычными Windows-операциями. Повышение привилегий и переподключение после выхода
процесса не реализованы. Новый SYS собран, но не подписан/не загружен;
реальное чтение ядром пока не подтверждено. Установка драйвера — отдельный этап.
Потоки и handles
array<thread_info>@ process::enumerate_threads(uint pid);
array<handle_info>@ process::enumerate_handles(uint pid);
array<thread_info>@ process::find_orphan_threads(uint pid, const array<module_info> &in modules);
Нужны process.handles_threads и согласие Access selected PID. Потоки:id, owner_pid (uint), priority (int), start_address (uint64),start_known (bool). Toolhelp возвращает список; NtQueryInformationThread
запрашивает начальный адрес. Если его получить нельзя, start_known=false;
нулевой адрес не считается orphan. Максимум 2048 потоков, два обхода/callback,
150 мс между вызовами. Orphan — известный старт вне переданных 1–512 диапазонов
модулей; это не доказательство вредоносности. Проверяйте актуальность списка.
Handles: value (uint64), type_index, access, attributes (uint).
NtQuerySystemInformation/SystemExtendedHandleInformation фильтруется по PID.
Ядровые адреса объектов, имена и дублирование handles не предоставляются.
Тип — индекс Windows, не стабильная строка. Лимиты: один обход/callback,
8192 handles целевого процесса, системный буфер до 32 MiB и 50 мс между
вызовами. Внутренние NT-структуры могут меняться между версиями Windows.
Запись — отдельное согласие
bool process::write_enabled();
bool process::write_u8(uint pid, uint64 address, uint8 value);
bool process::write_u16(uint pid, uint64 address, uint16 value);
bool process::write_u32(uint pid, uint64 address, uint value);
bool process::write_u64(uint pid, uint64 address, uint64 value);
bool process::write_f32(uint pid, uint64 address, float value);
bool process::write_f64(uint pid, uint64 address, double value);
bool process::write_bytes(uint pid, uint64 address, const string &in bytes);
bool process::write_protected(uint pid, uint64 address, const string &in bytes, uint new_protect);
Требуется process.write, согласие на PID и дополнительная выключенная по
умолчанию кнопка Allow process writes (0.2). В CLI нужны оба флага:--allow-process PID --allow-process-write PID. PID должны совпадать.
Смена PID/содержимого пакета сбрасывает согласие. Изменить permissions/API
в работающей сессии через Reload нельзя. Хост открывает отдельный writer handle
с VM_WRITE | VM_OPERATION | QUERY_INFORMATION | SYNCHRONIZE; обычный read handle
не получает права записи. write_enabled() безопасно проверяет возможность.
Запись: 1–4096 байтов/вызов, 64 вызова и 64 KiB/callback; только committed,
не guard и в одном регионе. Простая запись требует writable-страницу.write_protected ограничен одной страницей; new_protect — PAGE_READWRITE (4)
или PAGE_EXECUTE_READWRITE (0x40). Старые права восстанавливаются и после
неудачного WriteProcessMemory; executable-запись сбрасывает instruction cache.
Нет транзакции: частичная ошибка может оставить изменённые байты, откат значений
не выполняется. Ошибка восстановления защиты обозначается URGENT.
Согласие не делает произвольные адреса корректными: записывайте только проверенные
данные и только в процесс, состояние которого разрешено менять.
Сигнатуры и RIP-relative
uint64 process::find_pattern(uint pid, const string &in module, const string &in pattern, const string &in mask);
array<uint64>@ process::find_all_patterns(uint pid, const string &in module, const string &in pattern, const string &in mask);
uint64 process::resolve_rip_relative(uint pid, uint64 instruction, uint displacement_offset, uint instruction_length);
Нужны process.scan и согласие на PID. pattern — сырые байты AngelScript string,
НЕ строка вида 48 8B ??. mask такой же длины: x точный байт, ? wildcard;
1–4096 байтов, хотя бы один x. Имя модуля или полный путь сравниваются без
учёта ASCII-регистра. Сканируются только readable committed страницы модуля;
нечитаемые разрывы не склеиваются. Обрабатываются совпадения между блоками.
Один pattern scan/callback, модуль до 256 MiB, прочитано до 64 MiB,
50 мс между вызовами, максимум 4096 совпадений. Эти чтения имеют отдельный
scan budget, не 1-MiB лимит обычных read. Нет совпадения: 0/пустой массив и
пустая ошибка; сбой/лимит: тот же результат с last_error, без частичных списков.
Первый метод возвращает первое совпадение. RIP-helper читает signed disp32,
прибавляет к адресу следующей инструкции, проверяет переполнение; длина 1–15,
все четыре байта displacement должны находиться внутри неё. Это не дизассемблер.
JSON-профили и диагностика
bool process::save_profile(uint pid, const string &in filename);
string process::diff_profiles(const string &in filename_a, const string &in filename_b);
string process::diagnostics(uint pid);
Полный профиль требует process_read, process.memory_map,process.handles_threads и согласие на PID; сохранение дополнительно требуетstorage и согласие хранения. Профиль содержит метаданные процесса, регионы,
system/PEB модули, потоки, handles, timestamp и diagnostics. Это снимок доступных
метаданных, НЕ дамп всех байтов и НЕ атомарный снимок работающего процесса.
Файлы только в data/<package-id>/profiles: lowercase имя из букв/цифр/-_
с .json, до 80 символов. Обнаруженные reparse points и выход из каталога
отклоняются; конкурентная подмена файлов другим процессом вне модели runtime.
Существующий файл не перезаписывается: используйте новое имя.
Размер файла/результата diff до 8 MiB, два profile-вызова/callback;
полный снимок также расходует квоты карты/модулей/потоков/handles.
diff_profiles требует только storage: сравнивает ранее сохранённые файлы,
возвращает JSON с same_pid и changes по секциям: added, removed,changed (before/after), сопоставляя address/base/id/value. PID reuse,
повторное использование handles и меняющиеся данные учитывайте самостоятельно.diagnostics возвращает JSON-массив, не пишет файл и не требует storage.
Проверки: module_difference, rwx_region, thread_outside_modules,unrecognized_handle_attributes (неизвестные биты attributes). Нормальная
программа также может иметь RWX или такие потоки: это эвристики, не вердикт.
Имена типов handles, их object names и универсальная классификация
«нестандартного handle» в этой версии отсутствуют.
Ошибки native операций: false/0/пустой результат и process::last_error();
отсутствующая permission/базовое согласие — исключение permission denied.
Проверяйте ошибку сразу. Native toolkit время учитывается отдельно от 20-мс
бюджета скрипта; квоты и внешний watchdog worker сохраняются. Windows вызовы
не прерываются посередине. Native x64 — проверяемая целевая архитектура
для модулей/сканирования/полных профилей; WOW64-модули не поддерживаются.
Карта памяти (RC20, API 0.1 совместим)
array<memory_region>@ process::enumerate_memory_regions(uint pid);
Метод выполняет VirtualQueryEx в изолированном worker-процессе по унаследованному дескриптору выбранного процесса. Он возвращает карту адресного пространства, а не содержимое памяти. Сам этот метод не записывает память и не открывает другой PID.
Разрешение и запуск
Добавьте "process.memory_map" в permissions файла package.json. Выберите PID в клиенте и явно включите Map selected PID (либо Read + map selected PID, если пакет также объявляет process_read). По умолчанию согласие выключено; смена PID сбрасывает его. В CLI эквивалент — run <package> --allow-process <PID>.
Разрешение независимо от process_read. Пакет только с process.memory_map получает PROCESS_QUERY_INFORMATION | SYNCHRONIZE, без PROCESS_VM_READ. Для чтения байтов дополнительно нужны process_read и соответствующее согласие. Windows может отклонить доступ; API не обходит ограничения защищённых процессов.
Переданный pid должен точно совпадать с согласованным PID. process::attached(), alive(), pid(), name() и last_error() доступны для обоих разрешений. Изменение разрешений при Reload запрещено: необходима новая сессия.
Структура memory_region
| Поле | Тип | Значение |
|---|---|---|
| address | uint64 | Начало региона |
| size | uint64 | Размер в байтах |
| state | uint | Windows: commit `0x1000`, reserve `0x2000`, free `0x10000` |
| type | uint | Windows: image `0x1000000`, mapped `0x40000`, private `0x20000`; у free — `0` |
| protection | uint | Неизменённые флаги `MEMORY_BASIC_INFORMATION.Protect` |
| readable / writable / executable | bool | Эффективные R/W/X для committed-региона |
| guard | bool | Установлен PAGE_GUARD |
Для free/reserve, PAGE_NOACCESS и guard-страниц эффективные R/W/X выключены. Write-copy считается доступной для записи. Для PAGE_EXECUTE включён X, но не R. Поле protection сохраняет исходные флаги, включая guard и модификаторы кеширования. Поля описывают страницы целевого процесса, а не права скрипта: writable не даёт API записи.
Пример
void on_load() {
if (!process::attached()) return;
array<memory_region>@ regions = process::enumerate_memory_regions(process::pid());
if (process::last_error().length() != 0) {
runtime::log(process::last_error());
return;
}
runtime::log("Regions: " + regions.length());
for (uint i = 0; i < regions.length(); ++i) {
if (regions[i].state == 0x1000 && regions[i].readable)
runtime::log("Readable: " + regions[i].address + " / " + regions[i].size);
if (i >= 8) break;
}
}
Ограничения и ошибки
Не более одного сканирования за callback, 16 384 регионов и 10 мс на сканирование. Вызов входит в общий лимит операций Process API. При ошибке PID, завершении процесса, ошибке Windows или превышении лимита возвращается пустой массив и устанавливается process::last_error(); частичная карта не выдаётся. Без объявленного разрешения или согласия возникает исключение permission denied. При успехе last_error() очищается.
Результат — самостоятельный снимок, который можно сохранить в скрипте. Изменение его полей не влияет на процесс. Карта не атомарна: другой поток целевого процесса может изменить регионы во время обхода. Для обновления запускайте новый запрос; не сканируйте карту каждый кадр без необходимости. API рассчитан на Windows x64, включая согласованные WOW64-процессы в пределах их доступного адресного пространства.
RC21 — модули и сравнение источников
array<module_info>@ process::enumerate_modules(uint pid, const string &in source);
array<module_difference>@ process::diff_modules(uint pid);
Оба метода требуют process_read и явного согласия на тот же PID. Одногоprocess.memory_map недостаточно: PEB и метаданные модулей требуют чтения.
Хост запрашивает PROCESS_VM_READ | PROCESS_QUERY_INFORMATION | SYNCHRONIZE.
Если полные query-права недоступны и пакет не требует карты памяти, хост может
сохранить старый доступ с QUERY_LIMITED_INFORMATION для обычных чтений; RC21
в этом случае может вернуть ошибку Windows. Скрипт не открывает новый процесс.
source принимает только точные строки system и peb:
system: EnumProcessModulesEx (LIST_MODULES_64BIT), GetModuleInformation,
GetModuleFileNameExW. Не закрывает возвращённые HMODULE — это не owned handles.
peb: динамически полученный NtQueryInformationProcess, ProcessBasicInformation,
затем ReadProcessMemory для PEB, Ldr, InMemoryOrderModuleList, префикса
LDR_DATA_TABLE_ENTRY и строки FullDllName. Проверяет диапазоны, циклы,
обратные связи и голову списка после обхода. Не изменяет список загрузчика.
RC21 поддерживает только native x64 процессы из x64 runtime. WOW64 отклоняется
с явной ошибкой: native и 32-битный loader domains нельзя считать одним списком.
PEB/Ldr — внутренние структуры Windows; текущие x64 offsets проверены при
сборке относительно общих полей winternl, но совместимость будущих версий
Windows не гарантируется. Подробнее: [NtQueryInformationProcess](https://learn.microsoft.com/en-us/windows/win32/api/winte…),
[PEB_LDR_DATA](https://learn.microsoft.com/en-us/windows/win32/api/winte…).
Результаты
module_info — самостоятельная value-структура:
| Поле | Тип | Значение |
|---|---|---|
| base | uint64 | Базовый адрес модуля |
| size | uint64 | SizeOfImage, байты |
| path | string | Полный путь UTF-8 |
Массив упорядочен по base; содержит максимум 512 модулей. Пути ограничены
4095 UTF-16 символами. Лимит одного обхода — 10 мс между native вызовами,
не более четырёх обходов за callback. Каждый обход входит в общий лимит
256 Process-операций; чтения PEB расходуют общий byte budget 1 MiB/callback.diff_modules расходует два обхода. Native Windows-вызовы не прерываются
этим таймером; остаётся внешний watchdog worker.
module_difference содержит:
| Поле | Тип | Значение |
|---|---|---|
| base | uint64 | Адрес, по которому сопоставлялись записи |
| kind | string | missing_system / missing_peb / metadata_mismatch |
| system_size / peb_size | uint64 | Размеры в соответствующих списках |
| system_path / peb_path | string | Пути; пустая строка, если записи нет |
Размер сравнивается точно; пути — без учёта регистра через CompareStringOrdinal
после нормализации / в \. Символьные ссылки, device paths и альтернативные
имена файла не разрешаются в канонический путь. Пустой diff при пустом
last_error означает отсутствие расхождений, а не ошибку.
Любая ошибка возвращает пустой массив и process::last_error(); частичные
списки не выдаются. Отсутствие разрешения/согласия вызывает permission denied.
Проверяйте last_error сразу после каждого вызова: следующий успешный вызов
очищает ошибку. Изменения loader list во время запроса могут приводить к ошибкам
или расхождениям — это не доказательство вмешательства. Источник system также
опирается на состояние loader и не является независимым доверенным оракулом;
см. [ограничения EnumProcessModulesEx](https://learn.microsoft.com/en-us/windows/win32/api/psapi…).
LOAD_LIBRARY_AS_DATAFILE и произвольные private mappings не считаются гарантированно
перечисляемыми модулями. Методы не ищут PE во всей памяти и не обходят защиту.
void on_load() {
if (!process::attached()) return;
uint pid = process::pid();
array<module_info>@ modules = process::enumerate_modules(pid, "system");
if (process::last_error().length() != 0) {
runtime::log(process::last_error()); return;
}
if (modules.length() > 0)
runtime::log(modules[0].path + " / " + modules[0].base + " / " + modules[0].size);
array<module_difference>@ differences = process::diff_modules(pid);
if (process::last_error().length() != 0) {
runtime::log(process::last_error()); return;
}
runtime::log("Differences: " + differences.length());
}