Proc API
The Proc API lets AngelScript access and edit memory of external processes using a lightweight handle type proc_t.
🔑 Process Handles
Referencing Processes
proc_t ref_process(uint pid)
proc_t ref_process(const string &in name)pid– Windows process ID.name– Executable name ("notepad.exe","cs2.exe", etc).
Returns a proc_t handle, or 0 if the process could not be opened.
Releasing Handles
void proc_t::deref()Decrements the internal reference and clears the handle.
After calling deref(), the handle becomes invalid (further calls are no-ops).
You must also call deref() if proc_t::alive() returns false.
🧬 Process Info
uint64 proc_t::base_address() const
uint64 proc_t::peb() const
uint proc_t::pid() const
bool proc_t::alive() constbase_address()– Image base (module base) of the process.peb()– Address of the process’ PEB.pid()– Process ID.alive()- Determines whether the process is currently alive and running.
📖 Scalar Reads
All addresses are virtual addresses in the target process.
Unsigned Integers
uint8 proc_t::ru8 (uint64 addr) const
uint16 proc_t::ru16(uint64 addr) const
uint32 proc_t::ru32(uint64 addr) const
uint64 proc_t::ru64(uint64 addr) constReads unsigned integer values at addr.
Signed Integers
int8 proc_t::r8 (uint64 addr) const
int16 proc_t::r16(uint64 addr) const
int32 proc_t::r32(uint64 addr) const
int64 proc_t::r64(uint64 addr) constReads signed integer values at addr.
Floating-Point
float proc_t::rf32(uint64 addr) const
double proc_t::rf64(uint64 addr) constReads float / double from addr.
✏️ Scalar Writes
Unsigned Integers
bool proc_t::wu8 (uint64 addr, uint8 v)
bool proc_t::wu16(uint64 addr, uint16 v)
bool proc_t::wu32(uint64 addr, uint32 v)
bool proc_t::wu64(uint64 addr, uint64 v)Writes unsigned integer values.
Returns true on success.
Signed Integers
bool proc_t::w8 (uint64 addr, int8 v)
bool proc_t::w16(uint64 addr, int16 v)
bool proc_t::w32(uint64 addr, int32 v)
bool proc_t::w64(uint64 addr, int64 v)Writes signed integer values.
Floating-Point
bool proc_t::wf32(uint64 addr, float v)
bool proc_t::wf64(uint64 addr, double v)Writes float / double.
🔤 Strings
All string helpers assume null-terminated memory on the remote side (for reads) and write a terminating null when writing.
Reading Strings
string proc_t::rs (uint64 addr, int max_chars) const
string proc_t::rws(uint64 addr, int max_chars) constrs– Reads an ANSI/UTF-8 style string fromaddr.rws– Reads a UTF-16 wide string, converts to UTF-8string.
max_chars is a hard cap to avoid walking off invalid memory.
Writing Strings
bool proc_t::ws (uint64 addr, const string &in text)
bool proc_t::wws(uint64 addr, const string &in text)ws– Writestextas ANSI/UTF-8 bytes plus trailing\0.wws– Convertstextto UTF-16 and writes wide string plus trailingL'\0'.
Returns true on success.
📦 Raw Memory (byte arrays)
Uses array<uint8> for bulk reads/writes.
void proc_t::rvm(uint64 addr, uint size, array<uint8> &out out_buf)
bool proc_t::wvm(uint64 addr, const array<uint8> &in in_buf)rvm– Readssizebytes starting ataddrintoout_buf.out_bufis resized tosize.wvm– Writes bytes fromin_buftoaddr. Returnstrueif all bytes were written.
🧮 SIMD Helpers
Convenience wrappers around 16/32/64-byte vectors (__m128/__m256/__m512).
All data is exposed as raw bytes.
void proc_t::r128(uint64 addr, array<uint8> &out out16) const
void proc_t::r256(uint64 addr, array<uint8> &out out32) const
void proc_t::r512(uint64 addr, array<uint8> &out out64) const
bool proc_t::w128(uint64 addr, const array<uint8> &in in16)
bool proc_t::w256(uint64 addr, const array<uint8> &in in32)
bool proc_t::w512(uint64 addr, const array<uint8> &in in64)r128/r256/r512– Read 16/32/64 bytes into the output array (array is resized).w128/w256/w512– Write 16/32/64 bytes from the input array. Returnsfalseif the array is too small or the write fails.
📦 Modules & Pattern Scans
Module Lookup
bool proc_t::get_module(
const string &in name,
uint64 &out module_base,
uint64 &out module_size
)Looks up a module by name (e.g. "notepad.exe", "client.dll"):
On success:
module_base– Base address of the module.module_size– Size in bytes.Returns
true.
Returns
falseif the module is not found.
Code Pattern Search
uint64 proc_t::find_code_pattern(
uint64 search_start,
uint64 search_size,
const string &in signature
)Scans [search_start, search_start + search_size) for a textual pattern:
signature– Pattern string, e.g."48 8B ?? ?? ?? 89"(exact format is the same as used internally by your pattern scanner).Returns the address of the first match, or
0if not found.
🔍 Example: Scanning Notepad
int main()
{
proc_t p = ref_process("notepad.exe");
if (p.pid() == 0)
{
log("Failed to reference notepad.exe");
return 0;
}
uint64 base = p.base_address();
log("PID = " + p.pid());
log("Base = 0x" + formatUInt(base, "0H", 16));
// Read the first two bytes ("MZ")
uint8 b0 = p.ru8(base);
uint8 b1 = p.ru8(base + 1);
log("Header Bytes: "
+ formatUInt(b0, "0H", 2) + " "
+ formatUInt(b1, "0H", 2));
// Dump first 16 bytes
array<uint8> bytes;
p.rvm(base, 16, bytes);
string dump;
for (uint i = 0; i < bytes.length(); i++)
{
if (i > 0) dump += " ";
dump += formatUInt(bytes[i], "0H", 2);
}
log("DOS Header (16 bytes): " + dump);
// Find module + pattern "4D 5A" (MZ signature)
uint64 modBase, modSize;
if (p.get_module("notepad.exe", modBase, modSize))
{
log("Module Base = 0x" + formatUInt(modBase, "0H", 16));
log("Module Size = 0x" + formatUInt(modSize, "0H", 16));
uint64 addr = p.find_code_pattern(modBase, modSize, "4D 5A");
log("Pattern '4D 5A' found at 0x" + formatUInt(addr, "0H", 16));
}
else
{
log("Module not found");
}
p.deref();
return 0;
}Process Struct Helpers
These helpers let you describe a C-style structure in a dictionary and read it.
📦 Struct Descriptor (common concept)
A struct descriptor is a dictionary where each entry:
Key = field name (string)
Value = another
dictionarywith:"offset"– byte offset from struct base (int64)"type"– field type string ("u16","i32","f32", etc.)(optional)
"max_chars"for string fields
Example:
dictionary PLAYER_DESC;
void init_player_desc()
{
dictionary@ health = { {"offset", int64(0x10)}, {"type", "i32"} };
dictionary@ armor = { {"offset", int64(0x14)}, {"type", "i32"} };
dictionary@ pos_x = { {"offset", int64(0x30)}, {"type", "f32"} };
dictionary@ pos_y = { {"offset", int64(0x34)}, {"type", "f32"} };
dictionary@ pos_z = { {"offset", int64(0x38)}, {"type", "f32"} };
PLAYER_DESC.set("health", @health);
PLAYER_DESC.set("armor", @armor);
PLAYER_DESC.set("pos_x", @pos_x);
PLAYER_DESC.set("pos_y", @pos_y);
PLAYER_DESC.set("pos_z", @pos_z);
}Supported field types
"u8"
1
unsigned 8-bit
"u16"
2
unsigned 16-bit
"u32"
4
unsigned 32-bit
"u64"
8
unsigned 64-bit
"i8"
1
signed 8-bit
"i16"
2
signed 16-bit
"i32"
4
signed 32-bit
"i64"
8
signed 64-bit
"f32"
4
32-bit float
"f64"
8
64-bit double
"string"
var
UTF-8 C-string in target
"wstring"
var
UTF-16 string → UTF-8
For "string" / "wstring", you can add "max_chars" to limit how many characters are read (default ~256, clamped to 1..8192).
📘 bool proc_t::read_struct(uint64 addr, dictionary &out result, const dictionary &in desc)
bool proc_t::read_struct(uint64 addr, dictionary &out result, const dictionary &in desc)Reads one structure instance from the target process into an existing dictionary.
Signature
bool read_struct(uint64 addr,
dictionary &out result,
const dictionary &in desc) constParameters
addrAbsolute address of the struct in the remote process.resultDictionary that will be filled with the decoded fields. (Existing contents are cleared before filling.)descStruct descriptordictionarydescribed above.
Return
trueon successfalseon failure (invalid process handle, invalid descriptor, etc.)
Stored value types
To keep things simple (and consistent with your JSON API):
All integer fields are stored as
int64All float fields are stored as
doubleString fields are stored as script
string
So you can safely read with:
int64 i; result.get("field", i);
double d; result.get("field", d);
string s; result.get("field", s);You can also read into uint64 etc. – the dictionary will convert as needed.
Example – Read one player struct
void example_read_player()
{
init_player_desc();
proc_t p = ref_process("somegame.exe");
uint64 base, size;
p.get_module("somegame.exe", base, size);
dictionary player;
if (!p.read_struct(base + 0x123456, player, PLAYER_DESC))
{
log("read_struct failed");
return;
}
int64 health;
player.get("health", health);
log("Health = " + health);
}📘 bool proc_t::read_struct_array(uint64 base, uint count, uint size, array<dictionary>@ &out result, const dictionary &in desc)
bool proc_t::read_struct_array(uint64 base, uint count, uint size, array<dictionary>@ &out result, const dictionary &in desc)Reads an array of structures into an array<dictionary>.
Signature
bool read_struct_array(uint64 base,
uint count,
uint size,
array<dictionary>@ &out result,
const dictionary &in desc) constParameters
baseAddress of the first element.countNumber of elements to read.sizeSize in bytes of each element in the array (sizeof(struct)).resultarray<dictionary>that will be resized tocount. Each element will be filled with that struct’s data. If an element isnullinternally, it is created.descSame struct descriptor as forread_struct.
Return
trueon successfalseif the process is invalid,count/sizeis 0, etc.
Example – Player list
void example_read_players()
{
init_player_desc();
proc_t p = ref_process("somegame.exe");
uint64 base = 0x20000000;
uint count = 32;
uint structSize = 0x140;
array<dictionary> players;
if (!p.read_struct_array(base, count, structSize, players, PLAYER_DESC))
{
log("read_struct_array failed");
return;
}
for (uint i = 0; i < players.length(); i++)
{
dictionary@ pl = players[i];
int64 hp = 0;
double px = 0.0;
pl.get("health", hp);
pl.get("pos_x", px);
log("Player " + i + " hp=" + hp + " pos_x=" + px);
}
}🧪 Full Example Script – Read IMAGE_DOS_HEADER from notepad.exe
IMAGE_DOS_HEADER from notepad.exeThis is a complete AngelScript file you can drop into your environment. It:
Builds a DOS header descriptor
Uses
proc_t.read_structto read it fromnotepad.exePrints all key fields and the computed NT header address
dictionary DOS_HEADER;
string to_hex16(uint v)
{
const string HEX = "0123456789ABCDEF";
string hex16 = "0000";
for (int i = 0; i < 4; i++)
{
uint nibble = (v >> ((3 - i) * 4)) & 0xF;
hex16[i] = HEX[nibble];
}
return hex16;
}
string to_hex32(uint v)
{
const string HEX = "0123456789ABCDEF";
string hex32 = "00000000";
for (int i = 0; i < 8; i++)
{
uint nibble = (v >> ((7 - i) * 4)) & 0xF;
hex32[i] = HEX[nibble];
}
return hex32;
}
void init_dos_descriptor()
{
// IMAGE_DOS_HEADER (partial, but enough for inspection)
// All offsets are from base of the module
dictionary@ e_magic = { {"offset", int64(0x00)}, {"type", "u16"} };
dictionary@ e_cblp = { {"offset", int64(0x02)}, {"type", "u16"} };
dictionary@ e_cp = { {"offset", int64(0x04)}, {"type", "u16"} };
dictionary@ e_crlc = { {"offset", int64(0x06)}, {"type", "u16"} };
dictionary@ e_cparhdr = { {"offset", int64(0x08)}, {"type", "u16"} };
dictionary@ e_minalloc = { {"offset", int64(0x0A)}, {"type", "u16"} };
dictionary@ e_maxalloc = { {"offset", int64(0x0C)}, {"type", "u16"} };
dictionary@ e_ss = { {"offset", int64(0x0E)}, {"type", "u16"} };
dictionary@ e_sp = { {"offset", int64(0x10)}, {"type", "u16"} };
dictionary@ e_csum = { {"offset", int64(0x12)}, {"type", "u16"} };
dictionary@ e_ip = { {"offset", int64(0x14)}, {"type", "u16"} };
dictionary@ e_cs = { {"offset", int64(0x16)}, {"type", "u16"} };
dictionary@ e_lfarlc = { {"offset", int64(0x18)}, {"type", "u16"} };
dictionary@ e_ovno = { {"offset", int64(0x1A)}, {"type", "u16"} };
// skip e_res[4], e_oemid, e_oeminfo, e_res2[10] for brevity
dictionary@ e_lfanew = { {"offset", int64(0x3C)}, {"type", "u32"} }; // NT headers offset
DOS_HEADER.set("e_magic", @e_magic);
DOS_HEADER.set("e_cblp", @e_cblp);
DOS_HEADER.set("e_cp", @e_cp);
DOS_HEADER.set("e_crlc", @e_crlc);
DOS_HEADER.set("e_cparhdr", @e_cparhdr);
DOS_HEADER.set("e_minalloc", @e_minalloc);
DOS_HEADER.set("e_maxalloc", @e_maxalloc);
DOS_HEADER.set("e_ss", @e_ss);
DOS_HEADER.set("e_sp", @e_sp);
DOS_HEADER.set("e_csum", @e_csum);
DOS_HEADER.set("e_ip", @e_ip);
DOS_HEADER.set("e_cs", @e_cs);
DOS_HEADER.set("e_lfarlc", @e_lfarlc);
DOS_HEADER.set("e_ovno", @e_ovno);
DOS_HEADER.set("e_lfanew", @e_lfanew);
}
void dump_dos_header(dictionary@ dos, uint64 base)
{
uint64 e_magic64 = 0;
uint64 e_cblp64 = 0;
uint64 e_cp64 = 0;
uint64 e_crlc64 = 0;
uint64 e_cparhdr64 = 0;
uint64 e_minalloc64 = 0;
uint64 e_maxalloc64 = 0;
uint64 e_ss64 = 0;
uint64 e_sp64 = 0;
uint64 e_csum64 = 0;
uint64 e_ip64 = 0;
uint64 e_cs64 = 0;
uint64 e_lfarlc64 = 0;
uint64 e_ovno64 = 0;
uint64 e_lfanew64 = 0;
dos.get("e_magic", e_magic64);
dos.get("e_cblp", e_cblp64);
dos.get("e_cp", e_cp64);
dos.get("e_crlc", e_crlc64);
dos.get("e_cparhdr", e_cparhdr64);
dos.get("e_minalloc", e_minalloc64);
dos.get("e_maxalloc", e_maxalloc64);
dos.get("e_ss", e_ss64);
dos.get("e_sp", e_sp64);
dos.get("e_csum", e_csum64);
dos.get("e_ip", e_ip64);
dos.get("e_cs", e_cs64);
dos.get("e_lfarlc", e_lfarlc64);
dos.get("e_ovno", e_ovno64);
dos.get("e_lfanew", e_lfanew64);
log("[AS] ==== IMAGE_DOS_HEADER dump ====");
log("[AS] base = 0x" + to_hex32(uint(base & 0xFFFFFFFF)));
log("[AS] e_magic = 0x" + to_hex16(uint(e_magic64 & 0xFFFF)));
log("[AS] e_cblp = 0x" + to_hex16(uint(e_cblp64 & 0xFFFF)));
log("[AS] e_cp = 0x" + to_hex16(uint(e_cp64 & 0xFFFF)));
log("[AS] e_crlc = 0x" + to_hex16(uint(e_crlc64 & 0xFFFF)));
log("[AS] e_cparhdr = 0x" + to_hex16(uint(e_cparhdr64 & 0xFFFF)));
log("[AS] e_minalloc= 0x" + to_hex16(uint(e_minalloc64& 0xFFFF)));
log("[AS] e_maxalloc= 0x" + to_hex16(uint(e_maxalloc64& 0xFFFF)));
log("[AS] e_ss = 0x" + to_hex16(uint(e_ss64 & 0xFFFF)));
log("[AS] e_sp = 0x" + to_hex16(uint(e_sp64 & 0xFFFF)));
log("[AS] e_csum = 0x" + to_hex16(uint(e_csum64 & 0xFFFF)));
log("[AS] e_ip = 0x" + to_hex16(uint(e_ip64 & 0xFFFF)));
log("[AS] e_cs = 0x" + to_hex16(uint(e_cs64 & 0xFFFF)));
log("[AS] e_lfarlc = 0x" + to_hex16(uint(e_lfarlc64 & 0xFFFF)));
log("[AS] e_ovno = 0x" + to_hex16(uint(e_ovno64 & 0xFFFF)));
log("[AS] e_lfanew = 0x" + to_hex32(uint(e_lfanew64 & 0xFFFFFFFF)));
// Validate magic and compute NT header address
if ((e_magic64 & 0xFFFF) == 0x5A4D)
log("[AS] DOS magic OK ('MZ')");
else
log("[AS] DOS magic INVALID (expected 0x5A4D)");
uint64 nt_addr = base + (e_lfanew64 & 0xFFFFFFFF);
log("[AS] NT header at: 0x" + to_hex32(uint(nt_addr & 0xFFFFFFFF)));
}
void test_dos_header()
{
init_dos_descriptor();
proc_t p = ref_process("notepad.exe");
if (!p.alive())
{
log("[AS] notepad.exe not found");
return;
}
uint64 base = p.base_address();
dictionary dos;
if (!p.read_struct(base, dos, DOS_HEADER))
{
log("[AS] read_struct failed");
return;
}
dump_dos_header(@dos, base);
}
int main()
{
test_dos_header();
return 1;
}Virtual Memory Allocation
These functions let you allocate and manage RWX memory inside the target process. They are intended for advanced use; you are fully responsible for tracking and freeing any allocations you create.
🔴 Important Memory Management Notes (MUST READ!)
These points are critical for using alloc_vm safely:
Control Flow Guard If the target process has Control Flow Guard (CFG) enabled and you intend to execute code from this region, CFG may block all jumps and calls into the allocated memory. To avoid this, CFG must be fully disabled for the target process. Be aware that disabling CFG reduces the security of the device.
Allocations are not automatically freed. Every allocation you create must be manually released using
free_vm(). If you lose track of an allocation, the memory will remain reserved until the target process exits.Allocations persist for the lifetime of the target process. Memory created with
alloc_vmis automatically released only when the target process itself closes.Closing the Perception overlay will clear all allocations. When the overlay is closed, Perception will clean up every RWX allocation for all processes currently referenced. If you need an allocation to stay alive, do not close the overlay while that memory is being used.
Repeated unfreed allocations can exhaust physical memory. If you repeatedly allocate memory without freeing it, these leaks will accumulate and can eventually consume all available physical RAM, potentially degrading system performance or causing instability. If you are writing an injector make sure your inject frees the memory
If you are writing an injector, you must free allocated memory. Any injector, loader, or external tool that allocates memory through
alloc_vmis responsible for freeing it once the memory is no longer needed. Failing to do so will leave permanent physical memory leaks until the target process exits.
Functions
uint64 proc_t::alloc_vm(uint size)
bool proc_t::free_vm(uint64 address)uint64 proc_t::alloc_vm(uint size)
Allocates a block of read–write–execute (RWX) memory in the target process and returns its base address.
Parameters
size— Size of the allocation in bytes.
Returns
Base address of the allocated region on success.
0on failure.
Notes
The allocated region is not associated with any module and is not discoverable through higher-level helpers (e.g. module lookup, pattern scan helpers, etc.). You must store and manage the returned address yourself.
The memory is RWX. If the target process uses Control Flow Guard (CFG) and you intend to execute from this region, CFG may block jumps/calls into it unless the target process allows it.
bool proc_t::free_vm(uint64 address)
Frees a region previously allocated with alloc_vm.
Parameters
address— Base address previously returned byalloc_vm.
Returns
trueif the region was successfully freed.falseif the address is invalid or the free fails.
🔗 Export Lookup – get_proc_address
get_proc_addressuint64 proc_t::get_proc_address(
uint64 module_base,
const string &in export_name
) constResolves the address of an exported symbol inside a module in the target process.
module_baseBase address of the module inside the target process. Typically obtained from:proc_t::base_address(), orproc_t::get_module("module_name.dll", base, size).
export_nameName of the exported function or symbol, e.g."Sleep","CreateFileW".
Returns
Absolute virtual address of the exported symbol in the target process.
0if the export is not found or the arguments are invalid.
Notes
This walks the module’s export table only – it does not search imports or runtime-resolved addresses.
The address is directly usable for:
Call stubs / shellcode in the remote process.
Manual thunking (e.g. “jmp [resolved_addr]” from your injected code).
You are responsible for ensuring the returned address is valid / executable before calling into it.
Example
proc_t p = ref_process("notepad.exe");
uint64 kbase, ksize;
if (p.get_module("KERNEL32.DLL", kbase, ksize))
{
uint64 addrSleep = p.get_proc_address(kbase, "Sleep");
log("Sleep @ 0x" + formatUInt(addrSleep, "0H", 16));
}🧷 Import Table (IAT) Slot Lookup – get_import_rdata_address
get_import_rdata_addressuint64 proc_t::get_import_rdata_address(
uint64 module_base,
const string &in import_name
) constResolves the address of the import table entry (IAT slot) for a given imported function inside a module.
This does not return the function’s address directly – it returns the address of the pointer stored in .rdata / IAT. That pointer can then be read or patched (for IAT hooks, trampolines, etc.).
module_baseBase address of the module whose imports you want to inspect/patch (usually the main EXE or a specific DLL), obtained frombase_address()orget_module(...).import_nameName of the imported function as it appears in the import table, e.g."Sleep","MessageBoxW".
Returns
Address of the IAT entry (pointer-sized slot) for the requested import.
0if the import cannot be found or the arguments are invalid.
How to use
Read current target of the import:
uint64 slot_addr = p.get_import_rdata_address(modBase, "Sleep"); uint64 current_fn = p.ru64(slot_addr); // current function pointerInstall a basic IAT hook (patch the pointer):
uint64 slot_addr = p.get_import_rdata_address(modBase, "Sleep"); if (slot_addr != 0) { // 'stub_remote' is an RWX address you allocated in the target p.wu64(slot_addr, stub_remote); }
Notes
This only sees imports that are present in the module’s static import table.
Imports resolved manually at runtime (e.g. via
GetProcAddressinto custom memory) will not show up here.Ideal for:
Classic IAT hooks (redirecting calls to your own stub).
Finding the call site used by a module to invoke a given API, so your shellcode can jump through the same IAT entry.
Last updated