A Cheat Engine alternative written in Go — memory scanner, CE-style multi-session pointer scan, value freeze.
Single .exe, zero dependencies. Drop and run on any Windows machine.
Get the latest memhacker.exe from Releases.
Run as Administrator (required to read/write other process memory).
Requirements: Go 1.21+
git clone https://github.com/gs-attripathi/memhacker.git
cd memhacker
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o memhacker.exe .
Cross-compile from macOS/Linux works fine.
| Command | Description |
|---|---|
ps / list |
List all running processes |
open <pid|name> (alias attach) |
Attach to process. Supports partial name match — open surro matches SurrounDead.exe. Auto-detects 32-bit WOW64 vs 64-bit |
close (alias detach) |
Detach |
modules (alias mod) |
List loaded DLLs — shows GAME / SYSTEM / OTHER tag |
regions [all] (alias reg) |
List scannable memory regions with base/end/size. Default: writable private only. regions all shows everything |
| Command | Description |
|---|---|
type <dt> (alias dt) |
Set scan data type. Default: f32 |
Types: i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 str bytes
Scans writable private memory only by default (game values are always here). Append all to scan everything including textures/assets (slower).
| Command | Description |
|---|---|
scan exact <val> (alias s) |
Exact match. For f32/f64 uses ±1.0 tolerance automatically |
scan unknown |
Snapshot all memory to disk — use next changed/increased/decreased to filter |
scan bigger <val> |
Greater than |
scan smaller <val> |
Less than |
scan between <v1> <v2> |
In range |
scan changed |
Changed since last scan |
scan unchanged |
Unchanged since last scan |
scan increased |
Any increase |
scan decreased |
Any decrease |
scan incby <val> |
Increased by exactly this amount |
scan decby <val> |
Decreased by exactly this amount |
scan notequal <val> |
Not equal to value |
next <type> [val] (alias n) |
Filter existing results (same types as scan) |
next rel <r1> <r2> |
Relation scan for obfuscated values stored as a*real + b (scaled, offset, negated encodings). Run after scan unknown (r1 = the real value when the snapshot was taken, r2 = the real value now) or on any existing result set, RAM or disk-backed (r1 = real value at the last scan/next pass; disk sets are streamed in chunks). Argument order is chronological: old value first, current value second ("it was 100, now it's 85" → next rel 100 85); swapping them computes wrong (a, b) and later refinements will reject the real address. Keeps addresses whose change fits a plausible linear relation (slope must be an integer or 1/integer) and records per-address (a, b). Use the two-arg form once to establish, then refine with the one-arg form. |
next rel <r> |
Refine an established relation scan: change the real value in game, pass the new value, and only addresses still consistent with their same recorded (a, b) survive: a third/fourth point on one line. This is deliberately stronger than re-running the two-arg form, which re-fits a fresh line through just the last two states (and overwrites the recorded one), so something like a tick counter moving in clean multiples could keep passing re-establishes but fails a refine. A couple of one-arg passes kills coincidental matches. |
rel [n] |
List surviving relations: stored value, a, b, and the decoded real value (stored - b) / a. The row whose decoded value tracks your in-game number is the winner. Indices match the scan set (iwrite, results add work on them). |
relwrite <idx|range|list> <real> (alias relw) |
You give the REAL value you want; the tool writes the ENCODED bytes (a*real + b with that entry's recorded parameters), so the game decodes its memory as usual and displays your value. You work entirely in real values; the encoding happens under the hood. A plain iwrite on the same address would store the raw number where the game expects an encoded one and show garbage on screen. |
txscan <value> [all] |
Transform scan for values stored under a fixed reversible transform that the relation scan can't catch (it isn't a*real + b, or the value never changes by a clean ratio). One pass searches negation, bitwise NOT, byteswap, and scaling by 2/4/8/10/16/100/256/1000 of <value> simultaneously and records which transform(s) matched each address. all includes non-writable/large regions. |
txnext <value> |
Refine a transform scan: change the value in game, pass the new value, and only addresses where a recorded transform still encodes it survive (this also narrows ambiguous addresses to a single transform over a couple passes). |
txlist [n] |
List transform hits: stored value, the matching transform name(s), and the decoded real value. |
txwrite <idx|range|list> <real> (alias txw) |
Write a REAL value through the matched transform (encodes it the same way the game stores it). |
xorscan <value> [all] |
XOR pair scan for stored = real ^ key where the key sits within 64 bytes of the value (same struct). XOR isn't linear, so the relation scan is blind to it. Integer types only. Records value-address and key-address pairs; both orderings are kept and refined down. Noisier and slower than other scans (checks several nearby slots per position). |
xornext <value> |
Refine an XOR scan: change the value, and only pairs where val ^ key equals the new value survive. A couple of passes isolate the real pair. |
xorlist [n] |
List XOR pairs: value address, key address, and the decoded value (val ^ key). |
xorwrite <idx|range|list> <real> (alias xorw) |
Write a REAL value: reads the live key and writes real ^ key to the value address, so the game decodes your value. |
results [N|range|list] [addr|val] [guess|g [type] [minconf]] (alias r) |
View the scan set: pure display, no side effects. Bare results shows the top 20; results 50 / results 100-200 / results 1,3,5 select what to show. Optional addr / val sorts the displayed rows. Optional guess (or g) adds Guess + Confidence columns, same heuristic as look (f32 / f64 / i32 / i64 / i8 / ptr / zero). Guess type filter: follow guess/g with a type name to show only rows guessed as that type with confidence >= minconf (default 0.5), sorted by confidence, e.g. results 20 guess f32. The count form walks until N matches are found (caps at examining 100K rows). |
results add <N|range|list> [g <type> [minconf]] |
Copy scan rows into the result set: a separate curated set, disk-backed only (memhacker_results.bin next to the exe), deduped by address. results add 50 = top 50, results add 100-200 = those rows, results add 1-400 g i8 0.7 = only i8-guessed rows. Plain adds are captured with the active type; guess-filtered adds with their guessed type. The result set accumulates across scans and survives reset and app restarts; entries go stale after a game restart, so clear it then. |
results view [N|range|list] [addr|val|conf] [guess|g [type] [minconf]] (alias results v) |
Inspect the result set: any group of entries, with live values decoded as each entry's captured type. Read-only. Sorts: addr (address), val (value), conf (captured confidence, descending). guess re-runs the live type heuristic per entry and adds Guess + GConf columns; with a type name it filters to live matches with confidence >= minconf (default 0.5), examining the whole set by default and sorting by live confidence, e.g. results view g f32 0.7 or results view 1-100 g ptr. |
results write <idx|range|list> <val> (alias results w) |
Write a value to result-set entries, encoded with each entry's captured type. |
results freeze <idx|range|list> <val> (alias results f) |
Freeze result-set entries to a value (shows up in frozen as rs[idx]). |
results remove <idx|range|list> (alias results rm) |
Remove entries from the result set. |
results clear |
Empty the result set (deletes memhacker_results.bin). The ONLY thing that clears it; new scans and reset don't touch it. |
reset |
Clear scan results |
Optional scan keywords (append to any scan command):
scan exact 100 all <- scan all memory including read-only
scan exact 100 range 0x1000000 0x2000000 <- scan only within address range
scan exact 0 cap 500000 <- stop after 500K results
Notes:
scan unknownwrites a memory snapshot tomemhacker_scans\snapshot.snapon disk — no RAM pressure regardless of game size- Results > 1M are automatically stored to
memhacker_scans\scan_N.addr+.vals— no RAM pressure - If existing results are present,
scanasks for confirmation before clearing them - Press Ctrl+C during any scan to cancel and clear results for a fresh start
- Progress is shown every 2 seconds for both
scanandnext
| Command | Description |
|---|---|
read <addr> [type] |
Read live value at address |
look <addr> [count] (alias l) |
Dump neighbors around an address as the current data type. count = entries on each side (default 8 → 17 rows). Asymmetric forms: look <addr> before <n> (or b <n>), look <addr> after <n> (or a <n>), or both: look <addr> b 4 a 16. Output also includes a per-row Guess + Confidence (heuristic — f32 / f64 / i32 / i64 / i8 / ptr / zero) so you don't have to flip type to sniff what each field probably is. Useful for figuring out what fields sit at +4, +8 etc. relative to a found value. |
write <addr> <val> (alias w) |
Write value to address |
iread <addr> <index> (alias ir) |
Read at addr + index × sizeof(type). e.g. iread 0x1A2B3C 4 reads 4th element of array |
iwrite <idx> <val> (alias iw) |
Write to scan result by index. Supports range/list: iwrite 5 100 iwrite 5-7 100 iwrite 1,3,5 100 |
add <addr> [label] (alias a) |
Add address to address list (captures the current data type) |
iadd <idx|range|list> [label] (alias ia) |
Add scan result(s) to the address list by 1-based index. Each entry captures the current data type. iadd 1, iadd 1-3, iadd 1,3,5 hp. |
ladd <off> [<off2> ...] (alias la) |
Add offsets from the last look to the address list. Offset is in bytes, signed. Accepts decimal (+4, -8) and hex (0x10, +0x20). Multiple offsets in one command. Optional shared label after --: ladd +4 +8 -- stats. |
addrlist (aliases alist, al) |
Show address list with live values (1-based indices) |
aread <idx|range|list> (alias ar) |
Read entries from the address list, using each entry's stored type. e.g. aread 1, aread 1-3, aread 1,3,5 |
awrite <idx|range|list> <val> (alias aw) |
Write a value to address list entries. e.g. awrite 1 999, awrite 1-3 100 |
afreeze <idx|range|list> <val> (alias af) |
Freeze address list entries at a value. Uses each entry's stored type. |
aremove <idx|range|list> (alias arm) |
Remove entries from the list. Removes from highest to lowest so earlier indices don't shift. |
aclear |
Empty the address list. |
| Command | Description |
|---|---|
freeze <addr> <val> [label] (alias f) |
Freeze address at value (50ms write loop) |
ifreeze <idx> <val> (alias if) |
Freeze scan result by index. Supports range/list: ifreeze 5 100 ifreeze 5-7 100 |
unfreeze <id|range|list> (alias uf) |
Unfreeze by ID. e.g. unfreeze 3 unfreeze 1-5 unfreeze 1,3,5 |
frozen (alias fl) |
List all frozen entries |
Set a name for any address and use it in every command.
| Command | Description |
|---|---|
alias <name> <addr> |
Set alias. e.g. alias hp 0x614DD58 |
alias |
List all aliases |
unalias <name> |
Remove alias |
CE-style multi-session pointer scan. Find stable pointer chains that survive game restarts.
How it works: attach and save a pointer map snapshot each session. pscan cross-references all sessions — only chains resolving correctly in ALL sessions are returned.
| Command | Description |
|---|---|
pmap |
Build pointer map in memory only (no file saved). Use pmsave to also save + register. |
pmsave <file> <addr> |
Build pointer map + save + register session |
pmadd <addr> |
Add another address to the last session (CE-style: one pmap, multiple targets) |
pmload <file> [file2] ... |
Load one or more saved pmaps. Multiple files at once: pmload s1.pmap s2.pmap s3.pmap |
pmsessions |
List registered sessions |
pmclear |
Clear all sessions |
pmexport <src.pmap> <out.scandata> |
Export pmap to CE-compatible .scandata format. CE: Pointer Scanner → File → Load pointer map |
pscan [depth] [offset] [max] [filter] [maxOffsets] [maxAddrsPerHop] [noneg]
| Arg | Default | Description |
|---|---|---|
depth |
5 |
DFS depth. depth=5 ~1s, depth=6 ~27s, depth=7 = minutes |
offset |
8192 |
Max offset per pointer hop |
max |
100 |
Max chains to return |
filter |
exe |
exe = main exe only, game = all game DLLs, all = everything |
maxOffsets |
5 |
Max distinct hop sizes tried per node (CE's LimitToMaxOffsetsPerNode) |
maxAddrsPerHop |
0 (unlimited) |
Max non-static recursions per shared hop value. Caps the UE5/Unity case where one hop value (e.g. +0x4) has hundreds of candidate addresses all in the same actor/object pool. Static-base hits (terminal chains) are never capped — they're the chains you actually want. Try 16 or 32 on deep scans (depth 6/7) for large speedups on heavy fan-out games. |
noneg |
off | Disable negative offsets. By default both positive and negative offsets are scanned (CE's NegativeOffsets, ON). Pass noneg to skip the negative pass for ~2× speed. |
Multiple sessions run in parallel — total time = slowest session, not sum.
Confirmation step. Before the scan starts, pscan prints every setting it parsed and asks y/n. Press y (or Enter) to run, or n to walk through each field one-by-one with the current value shown in []. Blank input keeps the current value. Useful when the positional args (pscan 6 8192 100 exe 5 16) get hard to read.
Press Ctrl+C during a pscan to cancel it. Workers wind down within a fraction of a second and you're returned to the prompt with whatever partial chains were already collected (no process exit, unlike scan-cancel). Useful for deep scans (depth=6/7) that run longer than you want to wait.
After pscan, results are automatically saved to pscan_last_N.json (never overwrites). Only chains that currently resolve in the live process are shown.
Negative offsets (default ON since v2.4.1-alpha): pointer scans consider pointers whose value lands either at/before or past the target — required for game layouts where struct fields sit before the parent pointer. The neg keyword (still accepted as a no-op) is now the default. Use noneg to opt out for speed.
| Command | Description |
|---|---|
prsave <file.json> |
Save current chains to JSON |
prload <file.json> [addr] |
Load chains. If addr given, keeps only chains resolving to that address |
prverify [addr] |
Re-verify chains. Status: OK = resolves + value matches, CHANGED = resolves but value differs (new game/save), BROKEN = dead |
prlist [ok|addr] |
List chains with live address + value. prlist ok = only resolvable. prlist 0xADDR = filter by address |
prlabel <index> <label> |
Label a chain (saved with prsave) |
prwrite <index> <val> |
Follow chain, write value once |
prfreeze <index> <val> |
Follow chain, freeze value |
prmerge <f1.json> <f2.json> ... |
Offline cross-session intersection — keeps only chains appearing in ALL files. No game needed. |
| Command | Description |
|---|---|
log |
Show log file path |
loglast [N] |
Copy log (or last N lines) to Windows clipboard — paste into GitHub issues |
All scan temp files go into memhacker_scans\ folder next to the exe:
memhacker_scans\
snapshot.snap <- unknown scan snapshot (deleted after first next)
scan_1.addr <- addresses from last scan
scan_1.vals <- values from last scan
scan_2.addr <- addresses after first next
scan_2.vals
...
Safe to delete the whole memhacker_scans\ folder manually at any time.
open game.exe
scan exact 100 <- find HP (default type is f32)
next decreased <- take damage in game, filter
next decreased <- take more damage
results <- view top rows (results add 5 to stash them for testing)
write 0xADDR 999 <- or use iwrite 1 999
open game.exe
scan unknown <- snapshots all memory to disk
<- do something in game (take damage, gain gold, etc.)
next changed <- filter to addresses that changed
next changed <- filter again
results
For games that don't store the displayed number directly but as a*real + b
(scaled, offset, or negated). HP shows 100:
open game.exe
scan unknown <- snapshot while HP = 100
<- take damage in game, HP now 85
next rel 100 85 <- establish ONCE: old value first, then current
<- take more damage, HP now 70
next rel 70 <- refine (one arg from now on): same line, third point
rel <- list survivors; 'Decoded' column shows the real value
relwrite 1 999 <- you type real 999, encoded bytes get written
(game shows 999; plain iwrite would show garbage)
If the relation scan finds nothing, the value may use a non-linear encoding. Try the transform scan (static transforms) or the XOR scan (value ^ nearby key):
type i32
txscan 100 <- searches negation / NOT / byteswap / x2..x1000 at once
txnext 85 <- change value in game, refine
txlist <- 'Transform' column names the encoding, 'Decoded' the value
txwrite 1 999 <- write real 999 through the matched transform
xorscan 100 <- finds stored = 100 ^ key (key within 64 bytes)
xornext 85 <- refine after changing the value
xorwrite 1 999 <- writes 999 ^ key so the game shows 999
open game.exe
scan exact 100
next exact 85
alias hp 0x614DD58
pmsave s1.pmap hp <- session 1
restart game
scan exact 100
next exact 85
alias hp 0x72419D0
pmsave s2.pmap hp <- session 2
restart game
pmsave s3.pmap 0xNEWADDR <- session 3
pscan <- cross-reference all sessions
prsave hp_chains.json
open game.exe
prload hp_chains.json <- load + auto-verify
prwrite 1 999 <- write via chain
prfreeze 1 999 <- or freeze
The scripts/ folder contains Python scripts for game manipulation using found pointer chains.
Reads car velocity (X/Y/Z) and steering via pointer chain, modifies velocity in real-time at 60Hz.
python scripts/car_speed_rotator.py # ROTATE mode (default)
python scripts/car_speed_rotator.py drift # DRIFT mode
Keyboard controls:
LEFT/RIGHTarrow — steerUParrow — boost speedDOWNarrow — reduce speed
Fill in your pointer chain values in the CONFIG section at the top. Requires Python + Administrator.
Download commands in scripts/commands.txt.
- Requires Administrator privileges
- Supports 32-bit (WOW64) and 64-bit processes — auto-detected on attach
- Default scan type:
f32(most games store floats for HP, speed etc.) - Float
scan exactuses ±1.0 tolerance automatically — catches imprecise game values - Scan defaults to writable private memory — append
allfor full scan (much slower) - Log file:
memhacker.lognext to the exe - Verified working: SurrounDead (UE5 64-bit), Mount & Blade Warband (32-bit WOW64), a popular open-world racing game (64-bit) — pointer chains survived restarts