This API lets scripts inspect and modify the memory of external processes via a process userdata.
Handles are created with ref_process(...) and released with deref_process(process).
🔑 Process Handles
Userdata Type
All functions operate on a process userdata created by ref_process.
-- Get a process handle by PIDlocal proc =ref_process(1234)-- Get a process handle by namelocal proc =ref_process("notepad.exe")
If the process is not found, ref_process returns nil.
Referencing / Releasing
process =ref_process(pid_or_name) -- globalderef_process(process) -- global
ref_process(pid) – pid is a number (Windows PID).
ref_process("name.exe") – search by executable name.
deref_process(process) – releases the reference; the userdata becomes invalid.
You must also call deref_process if process:alive() returns false.
Each Lua script keeps its own list of referenced processes internally.
Always call deref_process(proc) when you’re done.
🧬 Process Info
base_address() – Image base address 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
Reads unsigned integers from addr.
Signed Integers
Reads signed integers from addr.
Floating Point
Reads floating point values from addr.
✏️ Scalar Writes
Unsigned Integers
Writes an unsigned integer at addr.
Returns true on success.
Signed Integers
Writes a signed integer at addr.
Floating Point
Writes float / double.
🔤 Strings
Read and write null-terminated strings from the remote process.
Read Strings
rs – Reads up to max_chars characters from addr.
rws – Reads a wide string and converts it to a Lua UTF-8 string.
If nothing valid is read, an empty string is returned.
Write Strings
ws – Writes text bytes plus a terminating \0.
wws – Converts text to UTF-16 and writes plus wide \0.
Returns true on success.
📦 Raw Memory (byte tables)
Use Lua tables of bytes (1..N, values 0–255) for bulk reads / writes.
rvm – Returns a new table of length size filled with bytes from addr.
wvm – Writes all bytes in byte_table to addr.
Returns true if all bytes are written.
🧮 SIMD Helpers
Convenience helpers for 16/32/64-byte SIMD vectors (raw bytes).
All values are plain bytes (0–255).
wXXX returns false if the table is too short or the write fails.
📦 Modules & Pattern Scanning
Module Lookup
name – Module name, e.g. "notepad.exe", "client.dll".
Returns base, size on success.
Returns nil if the module is not found.
Code Pattern Scan
search_start – Starting address.
search_size – Number of bytes to scan.
signature – Pattern string, e.g. "48 8B ?? ?? ?? 89"
(same format as your internal pattern scanner).
Returns:
addr – Address of the first match, or 0 if not found.
🔍 Example: Scanning Notepad (Lua)
proc:read_struct
Reads a single C-style structure from the target process using a descriptor table.
Signature
Parameters
base_address
Absolute address of the struct in the remote process.
descriptor
A Lua table describing each struct field:
Supported Types
Type
Size
Notes
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
float
f64
8
double
Returns
A Lua table containing the decoded fields:
Example – Reading IMAGE_DOS_HEADER
📘 proc:read_struct_array
Reads an array of structures from the target process.
Signature
Parameters
base_address
Address of the first element in the array.
count
Number of array elements to read.
struct_size
Size in bytes of each array element.
descriptor
Same descriptor table used with read_struct.
Returns
A 1-based Lua array, where each element is a struct table:
Example – Reading a Player Array
proc:get_all_tebs()
Returns a Lua table of TEB addresses (uint64) for threads in the target process.
Syntax
Returns
{ teb1, teb2, ... } where each teb is a virtual address in the remote process
{} if none found / enumeration fails
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:alloc_vm(size)
Allocates a block of read–write–execute (RWX) memory inside the target process and returns its base address.
Parameters
size — Size of the allocation in bytes.
Returns
Base address of the allocation on success.
0 on failure.
Notes
Allocated memory is not tied to any module and is not discoverable through helpers like module queries or pattern scans.
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 code from this region, CFG may block jumps/calls unless the target allows it.
bool proc:free_vm(address)
Frees a region previously allocated with proc:alloc_vm().
Parameters
address — The exact base address returned by alloc_vm.
Returns
true if the region was successfully freed.
false if the address is invalid or the free fails.
🔗 Export Lookup – process:get_proc_address
Resolves the address of an exported function or symbol inside a module in the target process.
module_base
Base address of the module inside the target process.
Typically obtained from:
process:base_address(), or
local base, size = process:get_module("module_name.dll").
export_name
Name of the exported function/symbol, e.g. "Sleep", "CreateFileW", "DllMain".
Returns
Absolute virtual address of the exported symbol in the target process (as a Lua integer).
0 if the export is not found or the arguments are invalid.
Notes
This walks the module’s export table only – it does not look at imports or IAT.
The returned address is suitable for:
Remote call stubs / shellcode.
Manual thunks that jump directly to an API inside the target module.
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 itself – it returns the address of the pointer stored in .rdata / IAT. You can then read or patch that pointer for IAT hooks.
module_base
Base address of the module whose imports you want to inspect/patch
(usually the main EXE or a specific DLL), e.g. from process: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) as a Lua integer.
0 if the import cannot be found or arguments are invalid.
Typical usage
Read the current imported function pointer:
Install a simple IAT hook (patch the slot to point to your stub):
Notes
Only sees functions present in the module’s static import table.
Manually resolved APIs (e.g. GetProcAddress into some custom region) will not show up here.
Designed for IAT hooks and for shellcode that wants to call through the same import slot as the module.
heap_likely_only — when true, only heap-like regions are returned; when false, all regions are returned.
Returns:
A 1-based Lua array of region tables:
Each region table contains:
start (uint64)
size (uint64)
end (uint64)
protection (int)
heap_likely (bool)
Memory Scanning Helpers
These helpers scan the target process memory by iterating over the snapshot of virtual memory regions. All scanning functions return a 1-based Lua table of matching virtual addresses:
local addr = process:find_code_pattern(search_start, search_size, signature)
function main()
log("==== LUA PROC API TEST START ====")
local proc = ref_process("notepad.exe")
if not proc then
log("[-] Failed to ref notepad.exe")
return
end
local pid = proc:pid()
local base = proc:base_address()
local peb = proc:peb()
log(string.format("[+] Referenced Notepad: pid=%d", pid))
log(string.format(" Base Address = 0x%016X", base))
log(string.format(" PEB = 0x%016X", peb))
-- DOS header
local b0 = proc:ru8(base)
local b1 = proc:ru8(base + 1)
log(string.format("Header bytes: %02X %02X", b0, b1))
-- First 16 bytes as hex
local raw = proc:rvm(base, 16)
local hex = {}
for i = 1, #raw do
hex[#hex+1] = string.format("%02X", raw[i])
end
log("DOS header (16 bytes): " .. table.concat(hex, " "))
-- Module + pattern scan
local mod_base, mod_size = proc:get_module("notepad.exe")
if mod_base then
log(string.format("Module Base = 0x%016X", mod_base))
log(string.format("Module Size = 0x%X", mod_size))
local addr = proc:find_code_pattern(mod_base, mod_size, "4D 5A")
log(string.format("Pattern '4D 5A' found at 0x%016X", addr))
else
log("[-] get_module('notepad.exe') failed")
end
-- SIMD round-trip (128-bit)
local v128 = proc:r128(base)
local ok = proc:w128(base, v128)
log("w128 ok? " .. tostring(ok))
deref_process(proc)
log("==== LUA PROC API TEST END ====")
end
local arr = proc:read_struct_array(addr, 10, 0x140, DESC)
print(arr[1].health)
local PLAYER = {
health = { offset = 0x10, type = "i32" },
armor = { offset = 0x14, type = "i32" },
pos_x = { offset = 0x30, type = "f32" },
}
local base = 0x12345678
local size = 0x140
local count = 5
local players = proc:read_struct_array(base, count, size, PLAYER)
for i, p in ipairs(players) do
log_console(string.format(
"Player %d: hp=%d armor=%d x=%.1f",
i, p.health, p.armor, p.pos_x
))
end
local proc = ref_process("notepad.exe")
if not proc then return end
local kbase, ksize = proc:get_module("KERNEL32.DLL")
if kbase then
local addr_sleep = proc:get_proc_address(kbase, "Sleep")
log(string.format("Sleep @ 0x%016X", addr_sleep))
end
deref_process(proc)
local slot = proc:get_import_rdata_address(mod_base, "Sleep")
if slot ~= 0 then
local current = proc:ru64(slot) -- current function pointer
log(string.format("IAT[Sleep] = 0x%016X", current))
end
local slot = proc:get_import_rdata_address(mod_base, "Sleep")
if slot ~= 0 then
-- 'stub_addr' is an RWX address you allocated with proc:alloc_vm(...)
local ok = proc:wu64(slot, stub_addr)
log("IAT hook applied? " .. tostring(ok))
end