Pointers

An address found by scanning is good for one run of the target. Next launch, ASLR and a different allocation order put the value somewhere else. What survives is the path to it: a static address inside a module, plus a series of offsets to follow.

Reading a chain you already know

If you have the chain — from a forum post, from a previous session, from a disassembler — you do not need a pointer scan at all:

pointer:read game.exe+0x1a2b3c 0x10 0x8 --type float
pointer:deref game.exe+0x1a2b3c 0x10 0x8

pointer:read follows the links and reads the value at the end; pointer:deref shows each link along the way, which is what you want when the chain has stopped working and you need to see where.

pointer:read --write VALUE writes at the end of the chain instead of reading.

The same walk is available inside any address expression, so these two are the same thing:

pointer:read game.exe+0x1a2b3c 0x10 0x8
memory:read [[game.exe+0x1a2b3c]+0x10]+0x8

See Addresses.

Finding a chain

pointer:scan <address> [--depth N] [--max-offset N] [--max N] [--unaligned] [--all-regions]

pointer:scan builds a map of every pointer in the target and walks it backwards from your address until it reaches a static base inside a module:

picklock [game.exe:41902]> pointer:scan #1

This is the expensive command in Picklock — minutes and hundreds of megabytes on a large target. Two knobs control the cost:

  • --depth N — how many links a chain may have (default 3). Each extra level costs a lot of time and memory.

  • --max-offset N — the largest offset to consider (default 1024). Bigger means more paths found and much more work.

Ctrl+C stops it and keeps the paths found so far.

pointer:paths lists what it found, with the usual paging flags.

Telling a real path from a coincidence

A pointer scan finds paths that reach the address right now. Most of them are accidents: a number that happens to be that address, in a structure that has nothing to do with your value. The workflow that separates them is a restart.

1. pointer:scan #1                     find candidate paths
2. pointer:save health.json            write them out
3. (restart the target, attach, scan for the value again)
4. pointer:rescan #1 health.json       keep the ones that still land on it

Step 4 is the whole point. A path that still reaches the value after a restart describes the structure; one that does not describes only that one run.

pointer:rescan without a file rescans the paths currently held.

Intersecting several runs

One restart eliminates most accidents. Two or three eliminate nearly all of them:

pointer:diff run1.json run2.json run3.json

pointer:diff keeps only the paths present in every file, compared by their portable recipe — module, module offset, offsets — rather than by absolute address, which is the only comparison that means anything across runs. The result replaces the paths currently held, so pointer:save can write it straight back out.

Two or three runs of the same target usually leave a handful of paths standing, and those are the ones worth writing into a script.

Saving and loading

pointer:save health.json     write the current paths
pointer:load health.json     read them back

The file is JSON, and it stores the portable recipe rather than the addresses, which is what lets pointer:rescan and pointer:diff compare across runs.