From arc-probe
Provides reverse engineering reference for ARC Probe: safe memory read/write, address validation heuristics, instruction patching (NOP/INT3/RET), and binary identification. For process probing sessions.
npx claudepluginhub vzco/arc-probe --plugin arc-probeThis skill uses the workspace's default tool permissions.
Reverse engineering reference — techniques, patterns, and fallback strategies for common tasks.
Controls ARC Probe GUI via HTTP bridge on localhost:9996: send memory probe commands (read, pattern scan), navigate tabs, mutate Zustand stores like createStruct. Use to drive ARC Probe programmatically.
Performs depth-first reverse engineering on Ghidra binaries, answering questions like function behavior, crypto usage, or C2 addresses via iterative analysis and database improvements.
Hunts vulnerabilities in x64dbg debuggees: analyzes imports/exports, triages I/O attack surfaces, tests bugs like overflows/wraps, generates PoCs.
Share bugs, ideas, or general feedback.
Reverse engineering reference — techniques, patterns, and fallback strategies for common tasks.
Load this when starting an investigation session. It covers the "what to do when X doesn't work" cases.
Three methods, use whichever is available:
probe.exe "<command>" → JSON to stdout127.0.0.1:9998 — send command\n, receive JSONPOST http://localhost:9996 with {"action":"probe","command":"..."} (requires GUI)Always expect failure. Every probe_read_* call can return an error if the address is invalid. ARC Probe wraps all reads in SEH so the target won't crash, but you need to handle errors.
Address validation heuristics:
Reading large regions:
probe_dump for visual inspection (max 256 bytes, includes ASCII)probe_read for raw hex when you need to parse specific bytesWrite commands:
write <addr> <hex> — raw bytes (for instruction patching)write_int <addr> <value> — 32-bit integerwrite_float <addr> <value> — 32-bit floatwrite_ptr <addr> <value> — 8-byte pointerRules:
uint8 (1 byte) or int32 (4 bytes) — check the schemaInstruction patching:
90 (1 byte per NOP, use 9090909090 for a 5-byte CALL)CC (software breakpoint)C3 (force immediate return)Common pitfalls:
write_int to a uint8 field → overwrites 3 adjacent bytesWhen you have an address and don't know what's there:
Hex dump first: probe_dump address=<addr> size=128
4D 5A at the start? It's a PE header (module base)Check if it's a C++ object: Read first 8 bytes
probe_rtti_resolve to get the class nameCheck if it's a string: probe_read_string address=<addr>
probe_strings_at address=<addr> wide=true for UTF-16Check which module it belongs to: Compare address against module base+size ranges from probe_modules
Understanding calling conventions is critical for interpreting register state at breakpoints.
Arguments: RCX, RDX, R8, R9, then stack (left to right) Return value: RAX (64-bit), EAX (32-bit), XMM0 (float/double) Caller-saved (volatile): RAX, RCX, RDX, R8, R9, R10, R11 Callee-saved (non-volatile): RBX, RBP, RDI, RSI, R12, R13, R14, R15
What this means in practice:
this pointer for methods)this pointer: Always RCX for non-static member functions. So mov eax, [rcx+0x354] means "read field at offset 0x354 from the object".
Virtual function calls:
mov rax, [rcx] ; load vtable from this (RCX)
call qword ptr [rax+0x28] ; call vtable entry at index 5 (0x28 / 8 = 5)
Getters (return a field value):
mov eax, [rcx+0x354] ; 32-bit field at +0x354
ret
or
movss xmm0, [rcx+0x40] ; float field at +0x40
ret
Setters (write a field value):
mov [rcx+0x354], edx ; write second arg (EDX) to field at +0x354
ret
Boolean flags:
movzx eax, byte ptr [rcx+0x103] ; read 1-byte boolean at +0x103
test al, al
jz skip
Bitfield test:
test dword ptr [rcx+0x48], 0x4 ; test bit 2 of flags at +0x48
jnz has_flag
Array access:
movsxd rax, edx ; sign-extend index (EDX) to 64-bit
lea rcx, [rcx+rax*4+0x100] ; base + index*stride + offset → int32 array at +0x100
Linked list traversal:
.loop:
mov rcx, [rcx+0x8] ; next = current->next (offset 0x8)
test rcx, rcx ; while (current != NULL)
jnz .loop
Process memory is dynamic. Addresses change because:
Fix: Don't store absolute addresses long-term. Instead:
Common causes:
Fix: Always check for NULL at each step of the chain. Read the chain multiple times to confirm stability.
Fix: Try a broader approach — set the breakpoint on the function prologue instead of a specific instruction. Or use probe_watch to poll for value changes first, then narrow down.
Fix: Look for a known instruction pattern to re-synchronize. Function prologues are recognizable. Or disassemble from a known-good address (like an export or vtable entry) and follow the control flow forward.
Alternative approaches:
If you're inspecting a Source 2 game (CS2, Deadlock, Dota 2):
CreateInterface pattern:
Most Source 2 DLLs export CreateInterface. Walking the interface registry gives you pointers to engine subsystems.
probe_interfaces_list module=<dll>
Entity system:
Schema system:
All networked classes have schema metadata at runtime. Use probe_rtti_scan to discover all classes, then probe_rtti_hierarchy to understand inheritance.
Common base classes:
Key offsets pattern: