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()        const
  • base_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) const

Reads 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) const

Reads signed integer values at addr.


Floating-Point

float  proc_t::rf32(uint64 addr) const
double proc_t::rf64(uint64 addr) const

Reads 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) const
  • rs – Reads an ANSI/UTF-8 style string from addr.

  • rws – Reads a UTF-16 wide string, converts to UTF-8 string.

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 – Writes text as ANSI/UTF-8 bytes plus trailing \0.

  • wws – Converts text to UTF-16 and writes wide string plus trailing L'\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 – Reads size bytes starting at addr into out_buf. out_buf is resized to size.

  • wvm – Writes bytes from in_buf to addr. Returns true if 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. Returns false if 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 false if the module is not found.


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 0 if 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 dictionary with:

    • "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

Type
Size
Meaning

"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)

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) const

Parameters

  • addr Absolute address of the struct in the remote process.

  • result Dictionary that will be filled with the decoded fields. (Existing contents are cleared before filling.)

  • desc Struct descriptor dictionary described above.

Return

  • true on success

  • false on 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 int64

  • All float fields are stored as double

  • String 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)

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) const

Parameters

  • base Address of the first element.

  • count Number of elements to read.

  • size Size in bytes of each element in the array (sizeof(struct)).

  • result array<dictionary> that will be resized to count. Each element will be filled with that struct’s data. If an element is null internally, it is created.

  • desc Same struct descriptor as for read_struct.

Return

  • true on success

  • false if the process is invalid, count/size is 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

This is a complete AngelScript file you can drop into your environment. It:

  1. Builds a DOS header descriptor

  2. Uses proc_t.read_struct to read it from notepad.exe

  3. Prints 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_vm is 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_vm is 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.

    • 0 on 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 by alloc_vm.

  • Returns

    • true if the region was successfully freed.

    • false if the address is invalid or the free fails.


🔗 Export Lookup – get_proc_address

uint64 proc_t::get_proc_address(
    uint64        module_base,
    const string &in export_name
) const

Resolves the address of an exported symbol inside a module in the target process.

  • module_base Base address of the module inside the target process. Typically obtained from:

    • proc_t::base_address(), or

    • proc_t::get_module("module_name.dll", base, size).

  • export_name Name of the exported function or symbol, e.g. "Sleep", "CreateFileW".

Returns

  • Absolute virtual address of the exported symbol in the target process.

  • 0 if 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

uint64 proc_t::get_import_rdata_address(
    uint64        module_base,
    const string &in import_name
) const

Resolves 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_base Base address of the module whose imports you want to inspect/patch (usually the main EXE or a specific DLL), obtained from base_address() or get_module(...).

  • import_name Name 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.

  • 0 if 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 pointer
  • Install 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 GetProcAddress into 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