Proc API
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 PID
local proc = ref_process(1234)
-- Get a process handle by name
local proc = ref_process("notepad.exe")If the process is not found, ref_process returns nil.
Referencing / Releasing
process = ref_process(pid_or_name) -- global
deref_process(process) -- globalref_process(pid)–pidis 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_processifprocess:alive()returnsfalse.
Each Lua script keeps its own list of referenced processes internally. Always call
deref_process(proc)when you’re done.
🧬 Process Info
process:base_address() --> number (uint64)
process:peb() --> number (uint64)
process:pid() --> number (uint)
process:alive() --> boolbase_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
process:ru8(addr) --> uint8
process:ru16(addr) --> uint16
process:ru32(addr) --> uint32
process:ru64(addr) --> uint64Reads unsigned integers from addr.
Signed Integers
process:r8(addr) --> int8
process:r16(addr) --> int16
process:r32(addr) --> int32
process:r64(addr) --> int64Reads signed integers from addr.
Floating Point
process:rf32(addr) --> float
process:rf64(addr) --> doubleReads floating point values from addr.
✏️ Scalar Writes
Unsigned Integers
process:wu8(addr, value) --> bool
process:wu16(addr, value) --> bool
process:wu32(addr, value) --> bool
process:wu64(addr, value) --> boolWrites an unsigned integer at addr.
Returns true on success.
Signed Integers
process:w8(addr, value) --> bool
process:w16(addr, value) --> bool
process:w32(addr, value) --> bool
process:w64(addr, value) --> boolWrites a signed integer at addr.
Floating Point
process:wf32(addr, value) --> bool
process:wf64(addr, value) --> boolWrites float / double.
🔤 Strings
Read and write null-terminated strings from the remote process.
Read Strings
process:rs(addr, max_chars) --> string -- ANSI / UTF-8 style
process:rws(addr, max_chars) --> string -- UTF-16 wide -> Lua stringrs– Reads up tomax_charscharacters fromaddr.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
process:ws(addr, text) --> bool -- ANSI / UTF-8
process:wws(addr, text) --> bool -- UTF-16 widews– Writestextbytes plus a terminating\0.wws– Convertstextto 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.
process:rvm(addr, size) --> { byte1, byte2, ... }
process:wvm(addr, byte_table) --> boolrvm– Returns a new table of lengthsizefilled with bytes fromaddr.wvm– Writes all bytes inbyte_tabletoaddr. Returnstrueif all bytes are written.
🧮 SIMD Helpers
Convenience helpers for 16/32/64-byte SIMD vectors (raw bytes).
process:r128(addr) --> { b1..b16 }
process:r256(addr) --> { b1..b32 }
process:r512(addr) --> { b1..b64 }
process:w128(addr, t16) --> bool -- t16 length >= 16
process:w256(addr, t32) --> bool -- t32 length >= 32
process:w512(addr, t64) --> bool -- t64 length >= 64All values are plain bytes (0–255).
wXXX returns false if the table is too short or the write fails.
📦 Modules & Pattern Scanning
Module Lookup
local base, size = process:get_module(name)name– Module name, e.g."notepad.exe","client.dll".Returns
base, sizeon success.Returns
nilif the module is not found.
Code Pattern Scan
local addr = process:find_code_pattern(search_start, search_size, signature)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, or0if not found.
🔍 Example: Scanning Notepad (Lua)
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 ====")
endproc:read_struct
proc:read_structReads a single C-style structure from the target process using a descriptor table.
Signature
table proc:read_struct(uint64 base_address, table descriptor)Parameters
base_address
base_addressAbsolute address of the struct in the remote process.
descriptor
descriptorA Lua table describing each struct field:
field_name = {
offset = <byte offset>,
type = "<field type>"
}Supported 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
float
f64
8
double
Returns
A Lua table containing the decoded fields:
local t = proc:read_struct(addr, DESC)
print(t.health)
print(t.pos_x)Example – Reading IMAGE_DOS_HEADER
local DOS_HEADER = {
e_magic = { offset = 0x00, type = "u16" },
e_lfanew = { offset = 0x3C, type = "u32" },
}
local proc = ref_process("notepad.exe")
local base = proc:get_module("notepad.exe")
local dos = proc:read_struct(base, DOS_HEADER)
log_console(string.format("e_magic = 0x%04X", dos.e_magic))
log_console(string.format("e_lfanew = 0x%08X", dos.e_lfanew))📘 proc:read_struct_array
proc:read_struct_arrayReads an array of structures from the target process.
Signature
table proc:read_struct_array(
uint64 base_address,
integer count,
integer struct_size,
table descriptor
)Parameters
base_address
base_addressAddress of the first element in the array.
count
countNumber of array elements to read.
struct_size
struct_sizeSize in bytes of each array element.
descriptor
descriptorSame descriptor table used with read_struct.
Returns
A 1-based Lua array, where each element is a struct table:
local arr = proc:read_struct_array(addr, 10, 0x140, DESC)
print(arr[1].health)Example – Reading a Player Array
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
))
endVirtual 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:alloc_vm(uint size)
bool proc:free_vm(uint64 address)uint64 proc:alloc_vm(size)
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.
0on 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)
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
trueif the region was successfully freed.falseif the address is invalid or the free fails.
🔗 Export Lookup – process:get_proc_address
process:get_proc_addressaddr = process:get_proc_address(module_base, export_name)Resolves the address of an exported function or symbol inside a module in the target process.
module_baseBase address of the module inside the target process. Typically obtained from:process:base_address(), orlocal base, size = process:get_module("module_name.dll").
export_nameName 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).
0if 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.
Example
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)🧷 Import Table (IAT) Slot Lookup – process:get_import_rdata_address
process:get_import_rdata_addressslot_addr = process:get_import_rdata_address(module_base, import_name)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_baseBase address of the module whose imports you want to inspect/patch (usually the main EXE or a specific DLL), e.g. fromprocess:get_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) as a Lua integer.
0if the import cannot be found or arguments are invalid.
Typical usage
Read the current imported function pointer:
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)) endInstall a simple IAT hook (patch the slot to point to your stub):
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
Notes
Only sees functions present in the module’s static import table.
Manually resolved APIs (e.g.
GetProcAddressinto 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.
Last updated