All examples assume you have linked against libspecter.a and included specter.h.
-L<path> -lspectre -lc++ -framework Foundation -framework Security
Platform note:
makebuilds for both iOS (aarch64-apple-ios) and macOS (aarch64-apple-darwin). Usemake iosormake macosto build a single target.
C++ gets typed template wrappers for mem_read/mem_write automatically. In C, pass sizeof(T) explicitly.
- Error Codes
- Initialization
- Inline Hooks
- Patches
- Memory Read / Write
- Image & Symbol API
- Hardware Breakpoints
- Shellcode Loading
- Pattern Scanning
- Memory Protection
- Image Enumeration
- Patch Introspection
- Memory Backup
- Segment / Section Querying
- Common Patterns
Every function returns int32_t. Zero means success; negative values are errors.
| Code | Name | Meaning |
|---|---|---|
0 |
MEM_OK |
Success |
-1 |
MEM_ERR_GENERIC |
Catch-all / unexpected error |
-2 |
MEM_ERR_NULL |
Required pointer argument was NULL |
-3 |
MEM_ERR_NOT_FOUND |
Image / symbol / hook not found |
-4 |
MEM_ERR_EXISTS |
Hook already installed at address |
-5 |
MEM_ERR_ALLOC |
Memory allocation failed |
-6 |
MEM_ERR_PROTECT |
vm_protect / Mach VM call failed |
-7 |
MEM_ERR_PATCH |
Stealth-write or verify failed |
-8 |
MEM_ERR_RELOC |
Instruction relocation failed |
-9 |
MEM_ERR_THREAD |
Thread suspend / resume failed |
-10 |
MEM_ERR_SYMBOL |
dlsym / symbol resolution failed |
-11 |
MEM_ERR_RANGE |
Branch target out of ±128 MB range |
-12 |
MEM_ERR_EMPTY |
Empty instruction / patch data |
-13 |
MEM_ERR_HW_LIMIT |
Hardware breakpoint limit exceeded |
-14 |
MEM_ERR_SCAN_PATTERN |
Invalid scan pattern |
-15 |
MEM_ERR_SCAN_ACCESS |
Scan hit unreadable memory |
-16 |
MEM_ERR_SCAN_REGION |
Image region enumeration failed |
-17 |
MEM_ERR_MACHO |
Mach-O segment/section parse error |
Helper macro for quick prototyping:
#define CHECK(expr) \
do { int32_t _rc = (expr); \
if (_rc != MEM_OK) { printf(#expr " failed: %d\n", _rc); return; } \
} while (0)Call mem_init once before any RVA-based operation. image_name is matched as a substring against the full dylib path reported by dyld.
// Minimal
CHECK(mem_init("MyApp", NULL));
// With base address
uintptr_t base;
CHECK(mem_init("MyApp", &base));
printf("MyApp at 0x%lx\n", base);To look up a different image without changing the RVA target:
uintptr_t uikit_base;
CHECK(mem_get_image_base("UIKitCore", &uikit_base));An inline hook overwrites the first bytes of the target function with a branch to your replacement. A trampoline is allocated with the relocated original instructions so you can call back into the original.
typedef int (*AddFn)(int, int);
static AddFn g_orig_add;
static int hooked_add(int a, int b) {
printf("add(%d, %d)\n", a, b);
return g_orig_add(a, b);
}
void install(void) {
CHECK(mem_init("MyApp", NULL));
uint64_t handle;
uintptr_t trampoline;
CHECK(mem_hook_install(0x12340, (uintptr_t)hooked_add, &trampoline, &handle));
g_orig_add = (AddFn)trampoline;
}// C++ equivalent
using AddFn = int(*)(int, int);
static AddFn g_orig;
static uint64_t g_handle;
static int hooked_add(int a, int b) {
return g_orig(a, b) + 1;
}
void install() {
CHECK(mem_init("MyApp", nullptr));
uintptr_t trampoline;
CHECK(mem_hook_install(0x12340, reinterpret_cast<uintptr_t>(hooked_add),
&trampoline, &g_handle));
g_orig = reinterpret_cast<AddFn>(trampoline);
}Resolves the target via dlsym automatically.
typedef void *(*MallocFn)(size_t);
static MallocFn g_orig_malloc;
static void *hooked_malloc(size_t size) {
printf("malloc(%zu)\n", size);
return g_orig_malloc(size);
}
void hook_malloc(void) {
uint64_t handle;
uintptr_t trampoline;
CHECK(mem_hook_symbol("malloc", (uintptr_t)hooked_malloc, &trampoline, &handle));
g_orig_malloc = (MallocFn)trampoline;
}When you already have the ASLR-slid address. Removal is by address, not handle.
uintptr_t trampoline;
CHECK(mem_hook_install_at(0x10012340, (uintptr_t)my_hook, &trampoline));
// ...
CHECK(mem_hook_remove_at(0x10012340));Places the trampoline in existing NOP padding in the target image's __TEXT segment instead of a new mmap allocation. Invisible to scanners looking for anonymous executable pages.
uint64_t handle;
uintptr_t trampoline;
// By RVA
CHECK(mem_hook_install_cave(0x12340, (uintptr_t)my_hook, &trampoline, &handle));
// By absolute address
CHECK(mem_hook_install_cave_at(0x10012340, (uintptr_t)my_hook, &trampoline, &handle));Requires a NOP region of at least 256 bytes within ±128 MB of the target. Returns
MEM_ERR_ALLOCif none is found.
// By handle (for mem_hook_install / _symbol / _cave variants)
CHECK(mem_hook_remove(handle));
// By address (for mem_hook_install_at)
CHECK(mem_hook_remove_at(target_addr));Removal restores original bytes, frees the trampoline, and resumes any suspended threads.
size_t count = mem_hook_count();
int32_t hooked = mem_hook_is_hooked(0x10012340); // 1 or 0
uintptr_t addrs[64];
size_t total;
mem_hook_list(addrs, 64, &total);
for (size_t i = 0; i < total && i < 64; i++)
printf("hook at 0x%lx\n", addrs[i]);Patches overwrite arbitrary bytes and save the originals for revert. All writes go through the stealth mach_vm_remap path.
Spaces in the hex string are ignored.
uintptr_t patch_addr;
// NOP (1F 20 03 D5)
CHECK(mem_patch_apply(0xABCD0, "1F2003D5", &patch_addr));
// RET (C0 03 5F D6)
CHECK(mem_patch_apply(0xABCD0, "C0035FD6", NULL));
// Multiple instructions
CHECK(mem_patch_apply(0xABCD0, "1F2003D5 1F2003D5 C0035FD6", NULL));uint8_t nop[4] = {0x1F, 0x20, 0x03, 0xD5};
uintptr_t addr_out;
CHECK(mem_patch_apply_at(0x100123F0, nop, sizeof(nop), &addr_out));Writes payload to a nearby NOP region and installs a single B instruction at the target (4-byte footprint).
CHECK(mem_patch_apply_cave(0xABCD0,
"E0031FAA" // MOV X0, XZR
"C0035FD6", // RET
NULL));CHECK(mem_patch_revert(patch_addr));C — pass sizeof(T) explicitly:
uint32_t value;
CHECK(mem_read(0x100123F0, &value, sizeof value));
float speed = 5.0f;
CHECK(mem_write(0x100123F4, &speed, sizeof speed));C++ — template wrappers deduce the size:
uint32_t value;
CHECK(mem_read(0x100123F0, &value));
float speed = 5.0f;
CHECK(mem_write(0x100123F4, speed));Requires mem_init to have been called.
uint32_t flags;
CHECK(mem_read_rva(0xABCD0, &flags, sizeof flags));
uint32_t new_flags = 0;
CHECK(mem_write_rva(0xABCD0, &new_flags, sizeof new_flags));Follows a chain of pointer dereferences. The result is the final address, not the data at it.
// *(*(base + 0x10) + 0x28) + 0x8
uintptr_t offsets[] = {0x10, 0x28, 0x8};
uintptr_t result;
CHECK(mem_read_pointer_chain(0x100200000, offsets, 3, &result));mem_write / mem_write_rva use plain memcpy and will fault on executable pages. Use mem_write_bytes for the mach_vm_remap + icache flush path:
uint8_t nop[] = {0x1F, 0x20, 0x03, 0xD5};
CHECK(mem_write_bytes(0x100123F0, nop, sizeof nop));// Image base lookup
uintptr_t base;
CHECK(mem_get_image_base("UIKitCore", &base));
// Symbol resolution (cached after first call)
uintptr_t addr;
CHECK(mem_resolve_symbol("malloc", &addr));
// Manually cache a symbol
mem_cache_symbol("_my_func", 0x100099ABC);
// Clear cache (e.g. after dlopen/dlclose)
mem_clear_symbol_cache();Intercepts execution using ARM64 debug registers and Mach exception handling. No code is modified at the target address. Maximum 6 concurrent breakpoints (mem_brk_max_breakpoints()).
static void replacement(void) {
printf("intercepted\n");
// WARNING: calling the original directly would re-trigger the breakpoint.
// For call-original patterns, prefer inline hooks (mem_hook_install).
}
void install(void) {
CHECK(mem_init("MyApp", NULL));
printf("%d/%d breakpoints in use\n",
mem_brk_active_count(), mem_brk_max_breakpoints());
// By RVA
uint64_t handle;
CHECK(mem_brk_install(0x12340, (uintptr_t)replacement, &handle));
CHECK(mem_brk_remove(handle));
// By absolute address
CHECK(mem_brk_install_at(0x10012340, (uintptr_t)replacement, &handle));
CHECK(mem_brk_remove_at(0x10012340));
}Hardware breakpoints vs inline hooks:
- Breakpoints leave zero code footprint — ideal for read-only pages or integrity-checked code.
- Inline hooks let you call the original easily via the trampoline.
- Breakpoints are limited to 6 concurrent addresses.
Loads raw ARM64 machine code into executable memory with optional symbol relocations.
static const uint8_t nop_ret[] = {
0x1F, 0x20, 0x03, 0xD5, // NOP
0x1F, 0x20, 0x03, 0xD5, // NOP
0xC0, 0x03, 0x5F, 0xD6, // RET
};
uintptr_t sc_addr;
CHECK(mem_shellcode_load(nop_ret, sizeof nop_ret,
NULL, NULL, 0, // no relocations
0, 0, // no near hint, reserved
&sc_addr));
// Call it
((void (*)(void))sc_addr)();
// Free
CHECK(mem_shellcode_free(sc_addr));Patch 64-bit symbol addresses into the shellcode at specified offsets before execution.
// Shellcode that calls malloc via a relocated pointer at offset 12
static uint8_t sc[] = {
0x49, 0x00, 0x00, 0x58, // LDR X9, #8
0x20, 0x01, 0x3F, 0xD6, // BLR X9
0xC0, 0x03, 0x5F, 0xD6, // RET
0x00, 0x00, 0x00, 0x00, // .quad placeholder (offset 12)
0x00, 0x00, 0x00, 0x00,
};
size_t offsets[] = {12};
const char *symbols[] = {"malloc"};
uintptr_t sc_addr;
CHECK(mem_shellcode_load(sc, sizeof sc,
offsets, symbols, 1,
0, 0,
&sc_addr));If your shellcode uses B/BL instructions that need to reach within ±128 MB of a specific target:
CHECK(mem_shellcode_load(sc, sizeof sc,
offsets, symbols, 1,
0x100123F0, // allocate near this address
0, &sc_addr));Scan memory ranges or entire loaded images for byte patterns. IDA-style patterns use hex bytes separated by spaces, with ?? as a wildcard.
uintptr_t results[64];
size_t count;
CHECK(mem_scan_pattern(start, size, "DE AD ?? EF", results, 64, &count));
for (size_t i = 0; i < count && i < 64; i++)
printf("match at 0x%lx\n", results[i]);uintptr_t results[64];
size_t count;
CHECK(mem_scan_image("MyApp", "1F 20 03 D5 C0 03 5F D6", results, 64, &count));uintptr_t addr;
CHECK(mem_scan_find_first(start, size, "DE AD ?? EF", &addr));
// Or in an entire image:
CHECK(mem_scan_image_first("MyApp", "DE AD ?? EF", &addr));uint8_t pattern[] = {0xDE, 0xAD, 0x00, 0xEF};
const char *mask = "xx?x";
uintptr_t results[64];
size_t count;
CHECK(mem_scan_raw(start, size, pattern, mask, sizeof pattern,
results, 64, &count));Subsequent calls with the same parameters return cached results.
uintptr_t results[64];
size_t count;
CHECK(mem_scan_cached(start, size, "DE AD ?? EF", results, 64, &count));
// Clear when patterns may have changed:
mem_scan_clear_cache();Query and modify memory page protection flags.
int32_t prot;
CHECK(mem_get_protection(0x100004000, &prot));
printf("prot flags: %d\n", prot);
// Quick boolean checks (return 1 or 0)
if (mem_is_readable(0x100004000))
printf("readable\n");
if (mem_is_writable(0x100004000))
printf("writable\n");
if (mem_is_executable(0x100004000))
printf("executable\n");uintptr_t region_addr;
size_t region_size;
int32_t prot;
CHECK(mem_get_region_info(0x100004000, ®ion_addr, ®ion_size, &prot));
printf("region: 0x%lx size: 0x%zx prot: %d\n", region_addr, region_size, prot);
// Find region containing or after an address:
CHECK(mem_find_region(0x100004000, ®ion_addr, ®ion_size, &prot));#include <mach/vm_prot.h>
CHECK(mem_protect(page_addr, page_size, VM_PROT_READ | VM_PROT_WRITE));size_t count;
mem_get_all_regions(NULL, 0, &count); // query count first
mem_region_t *regions = malloc(count * sizeof(mem_region_t));
mem_get_all_regions(regions, count, &count);
for (size_t i = 0; i < count; i++)
printf("0x%lx size=0x%zx prot=%d\n",
regions[i].address, regions[i].size, regions[i].protection);
free(regions);List all loaded dynamic libraries and their base addresses.
// Get total count
size_t count;
CHECK(mem_image_count(&count));
printf("%zu images loaded\n", count);
// List all images
mem_image_t *images = malloc(count * sizeof(mem_image_t));
CHECK(mem_image_list(images, count, &count));
for (size_t i = 0; i < count; i++) {
char name[512];
if (mem_image_name(images[i].index, name, sizeof name) == MEM_OK)
printf("[%u] 0x%lx %s\n", images[i].index, images[i].base, name);
}
free(images);Query details about active patches.
// Count and list active patches
size_t count = mem_patch_count();
uintptr_t addrs[64];
size_t total;
mem_patch_list(addrs, 64, &total);
// Inspect a specific patch
for (size_t i = 0; i < total && i < 64; i++) {
size_t sz;
mem_patch_size(addrs[i], &sz);
uint8_t orig[64], patch[64], curr[64];
mem_patch_orig_bytes(addrs[i], orig, sz);
mem_patch_patch_bytes(addrs[i], patch, sz);
mem_patch_curr_bytes(addrs[i], curr, sz);
// Compare curr vs patch to detect external modification
if (memcmp(curr, patch, sz) != 0)
printf("patch at 0x%lx was externally modified\n", addrs[i]);
}Standalone memory backup and restore, independent of the patch system. Useful for backing up a region before making manual modifications.
// Create a backup
uint64_t handle;
CHECK(mem_backup_create(0x100004000, 64, &handle));
// ... modify memory however you want ...
// Query backup info
uintptr_t backed_addr;
size_t backed_size;
mem_backup_address(handle, &backed_addr);
mem_backup_size(handle, &backed_size);
// Read original and current bytes
uint8_t orig[64], curr[64];
mem_backup_orig_bytes(handle, orig, 64);
mem_backup_curr_bytes(handle, curr, 64);
// Restore original bytes
CHECK(mem_backup_restore(handle));
// Destroy when no longer needed (does not restore)
CHECK(mem_backup_destroy(handle));Query Mach-O segments and sections of loaded images.
// Get a segment (e.g., __TEXT)
mem_segment_t text;
CHECK(mem_get_segment("MyApp", "__TEXT", &text));
printf("__TEXT: 0x%lx - 0x%lx (%zu bytes)\n", text.start, text.end, text.size);
// Get a specific section within a segment
mem_segment_t text_text;
CHECK(mem_get_section("MyApp", "__TEXT", "__text", &text_text));
printf("__text: 0x%lx - 0x%lx (%zu bytes)\n",
text_text.start, text_text.end, text_text.size);
// Useful for targeted scanning
uintptr_t results[256];
size_t count;
mem_scan_pattern(text_text.start, text_text.size, "C0 03 5F D6",
results, 256, &count);
printf("found %zu RET instructions in __text\n", count);void ensure_hooked(uintptr_t addr) {
if (mem_hook_is_hooked(addr))
return;
mem_hook_install_at(addr, (uintptr_t)my_hook, NULL);
}void cleanup(uint64_t *handles, size_t count) {
for (size_t i = 0; i < count; i++)
mem_hook_remove(handles[i]);
}typedef struct { float x, y, z; } Vec3;
uintptr_t offsets[] = {0x20, 0x8};
uintptr_t vec_ptr;
if (mem_read_pointer_chain(game_obj, offsets, 2, &vec_ptr) == MEM_OK) {
Vec3 pos;
mem_read(vec_ptr, &pos, sizeof pos);
printf("%.2f %.2f %.2f\n", pos.x, pos.y, pos.z);
}typedef int32_t (*CheckFn)(void *);
static CheckFn g_orig_check;
static uint64_t g_handle;
static int32_t hooked_check(void *ctx) {
int32_t result = g_orig_check(ctx);
return result == 0 ? 1 : result; // patch return value
}
void setup(void) {
CHECK(mem_init("MyApp", NULL));
uintptr_t tramp;
CHECK(mem_hook_install(0xDEAD0, (uintptr_t)hooked_check, &tramp, &g_handle));
g_orig_check = (CheckFn)tramp;
}uint8_t expected[] = {0x1F, 0x20, 0x03, 0xD5};
uint8_t actual[4];
if (mem_read(patch_addr, actual, 4) == MEM_OK) {
if (memcmp(actual, expected, 4) != 0)
printf("patch was reverted externally\n");
}