memwatch provides an interactive interface to find and modify variables in the memory of a running process. This is done by repeatedly searching the process' address space for values that follow the rules given by the user, narrowing down the result set until it includes only the desired variable. After finding this variable, memwatch can monitor it, change its value, "freeze" its value, and more. This functionality is similar to that provided by scanmem(1).
I often use memwatch to cheat in games, but have also used it in debugging to complement the features of a generic debugger such as gdb.
- Build and install phosg (https://github.com/fuzziqersoftware/phosg)
- Build and install libamd64 (https://github.com/fuzziqersoftware/libamd64)
make
sudo make install
If it doesn't work on your system, let me know. It should build and run on all recent OS X versions.
Run sudo memwatch <target_name_or_pid>
. If you give a name, memwatch will search the list of running processes to find one that matches this name (case-insensitive). If multiple processes match the name, it will ask you which one to operate on. See the man page (man memwatch
after building and installing) for more options.
Upon running memwatch, you'll see a debugger-like prompt like memwatch:48536/VirtualBoxVM 0s/0f #
. This prompt shows the pid of the attached process, the process name, the number of open searches, and the number of frozen memory regions.
Most commands can be abbreviated intuitively (e.g. read
, rd
, and r
are all the same command). There are a few unintuitive abbreviations, which are noted below.
attach [pid_or_name]
: Attaches to a new process by PID or by name. If no argument is given, attaches to a process with the same name as the currently-attached process.data <data_string>
: Parses the data string and displays the raw values returned from the parser. You can use this to test complex data strings before writing them to be sure the correct data will be written.state [field_name value]
: With no arguments, displays simple variables from memwatch's internal state. This includes variables from command-line options, like use_color and pause_target. With arguments, sets the internal variablefield_name
tovalue
.watch <command> [args...]
or! <command> [args...]
: Repeat the given command every second until Ctrl+C is pressed. Not all commands may be repeated; if the command doesn't support watching, it will execute only once.quit
orexit
: Exits memwatch.
memwatch's interactive interface implements the following commands for reading, writing, and otherwise manipulating virtual memory:
access <address> <mode>
: Changes the virtual memory protection mode on the region containingaddress
. The access mode is a string consisting of the r, w, or x characters, or some combination thereof. To remove all access to a memory region, use - for the access mode.list
: Lists memory regions allocated in the target process's address space.dump [filename_prefix]
: If a prefix is given, dumps all readable memory in the target process to files named<prefix>.address1.address2.bin
, along with an index file. If no prefix is given, only determines which regions are readable.read <address> <size> [filename] [+format]
: Reads a block ofsize
bytes fromaddress
in the target process's memory.address
andsize
are specified in hexadecimal. If a filename is given, writes the data to the given file instead of printing to the terminal.format
is a short string specifying which additional interpretations of the data should be printed. It starts with '+' and includes any of the characters 'a' (text), 'f' (float), 'd' (double), and 'r' (reverse-endian); the default is '+a'.write <address-or-result-id> <data>
: Writesdata
toaddress
in the target process's memory.address
may be preceded bys
to read the address from the current search result set, or byt
to read the address from the results of the previous invocation of thefind
command. For example, specifyings0
refers to the first result address in the current search;s1
refers to the second result, etc. Results from other searches may also be used by specifying the search name explicitly, as ins1@search-name
. See the Data Format section for information on how to specify the data string.
memwatch's interactive interface implements the following commands for searching a process's memory for variables:
find <data>
ort <data>
: Finds all occurrences ofdata
in readable regions of the target's memory. See the Data Format section for more information on how to specify the search string. Searches done with this command do not affect the current saved search results.open <type> [name]
: Opens a search for a variable of the given type in writable memory regions.type
may be suffixed with a ! to search all readable memory instead of only writable memory. See the Search Types section for valid search types. Ifname
is not given, the search will be unnamed and cannot be resumed after another search is opened. Ifpredicate
is given, perform the initial search immediately.open [name]
: Ifname
is given, reopens a previous search. Ifname
is not given, lists all open searches.close [name]
: Ifname
is given, closes the specified search. Ifname
is not given, closes the current search.fork <name> [name2]
: Ifname2
is given, makes a copy of the search namedname
asname2
. Otherwise, makes a copy of the current search asname
.search [search_name] <operator> [value]
: Reads the values of variables in the current list of results (or the named search's results, if a name is given), and filters out the results for whichnew_value operator prev_value
is false. Ifvalue
is not given, uses the value of the variable during the previous search. Valid operators are<
(less than),>
(greater than),<=
(less-or-equal),>=
(greater-or-equal),=
(equal),!=
(not equal), and$
(flag search - true if the two arguments differ in only one bit). The$
operator cannot be used in a search for a floating-point variable.search [search_name] .
: Begins a search for a variable with an unknown initial value. Once this is done, future searches can be done using the above operators.results [search_name]
orx
: Displays the current list of results. Ifsearch_name
is given, displays the results for that search.delete <spec> [spec ...]
: Deletes specific search results.spec
may be the address of a specific result to delete, or a range of addresses to delete, which is inclusive on both ends. Ranges are specified as a pair of addresses separated by a dash with no spaces. Result references likes1
are acceptable for this command as well.iterations [search_name]
: Displays the list of stored iterations for the current search, or the named search if a name is given.truncate [search_name] <count>
: Deletes all iterations except thecount
most recent from the current search, or the named search if a name is given.undo [search_name]
orcz
: Undoes the latest iteration of the current search, or the named search if a name is given.set <value>
: Writesvalue
to all addresses in the current result set.set <result-id> <value>
: Writesvalue
to one address in the current result set.result-id
should be of the forms0
,s1
, etc. (as for thewrite
command).
memwatch implements a memory freezer, which repeatedly writes values to the target's memory at a very short interval, effectively fixing the variable's value in the target process. The following commands allow manipulation of frozen variables:
freeze [+n<name>] <address-or-result-id> <data> [+d]
: Sets a freeze onaddress
with the given data.address
may refer to a search or find result, using the same syntax as for thewrite
command. The given data is written in the background approximately every 10 milliseconds. Sets the freeze name toname
if given; otherwise, sets the freeze name to the current search name (if any). If+d
is given, the freeze is initially disabled and won't take effect until enabled with theenable
command.freeze [+n<name>] <address-or-result-id> +s<size> [+d]
: Identical to the above command, but uses the data already present in the process's memory. Size is specified in hexadecimal.freeze [+n<name>] <address-or-result-id> +m<max-entries> <data> [+N<null-data>] [+d]
: Sets a freeze on an array ofmax-entries
items starting ataddress
with the given data. Ifdata
is not present in the array, the first null entry in the array is overwritten withdata
. Null entries are those whose contents are entirely zeroes, or whose contents matchnull-data
ifnull-data
is given. The size of each array element is assumed to be the size ofdata
.data
andnull-data
must have equal sizes.unfreeze [id]
: Ifid
is not given, displays the list of currently-frozen regions. Otherwise,id
may be the index, address, or name of the region to unfreeze. If a name is given and multiple regions have the same name, unfreezes all of them. If*
is given, unfreezes all regions.enable <id>
or+ <id>
: Enables the given frozen regions, so their values will be written. Values forid
are specified in the same way as for theunfreeze
command.disable <id>
or- <id>
: Disables the given frozen regions, so their values will not be written. Values forid
are specified in the same way as for theunfreeze
command.frozen [data | commands]
: Displays the list of currently-frozen regions. If run asfrozen data
, displays the data associated with each region as well. If run asfrozen commands
, displays for each frozen region a command to freeze that region (this is generally a more concise way to view frozen regions with their data).
memwatch implements experimental support for viewing and modifying execution state in the target process, implemented by the following commands:
pause
: Pauses the target process.unpause
orresume
: Unpauses the target process.signal <signum>
: Sends the Unix signalsignum
to the target process. See the signal(3) manual page for a list of signals.
memwatch supports searching for the following types of variables. Any type except 'str' may be prefixed by the letter 'r' to perform reverse-endian searches (that is, to search for big-endian values on a little-endian architecture, or vice versa).
s
,str
,string
: Search for any string. Values are specified in immediate data format (see the Data Format section for more information).f
,flt
,float
: Search for a 32-bit floating-point value.d
,dbl
,double
: Search for a 64-bit floating-point value.u8
,u16
,u32
,u64
: Search for an unsigned 8-bit, 16-bit, 32-bit, or 64-bit value.s8
,s16
,s32
,s64
: Search for a signed 8-bit, 16-bit, 32-bit, or 64-bit value.
Input data for raw data searches and the find
, write
, and freeze
commands is specified in a custom format, described here. You can try using this format with the data
command (see above). Every pair of hexadecimal digits represents one byte, with special control sequences as follows:
- Decimal integers: A decimal integer may be specified by preceding it with
#
signs (#
for a single byte,##
for a 16-bit int,###
for a 32-bit int, or####
for a 64-bit int). - Floating-point numbers: A floating-point number may be specified by preceding it with
%
signs (%
for single-precision,%%
for double-precision). - String literals: ASCII strings must be enclosed in double quotes, and unicode strings in single quotes. Within a string, the escape sequences
\n
,\r
,\t
, and\\
will be replaced with a newline, a carriage return, a tab character, and a single backslash respectively. - File contents: A string enclosed in
< >
will be treated as a filename, and will be replaced with the contents of the file in the output data. - Change of endianness: A dollar sign (
$
) inverts the endianness of the data following it. This applies to unicode string literals, integers specified with#
signs, and floating-point numbers. - Wildcard: Any data between question marks (
?
) will match any byte when searching with thefind
command or freezing array entries with thefreeze
command. This is not yet implemented for thesearch
command. - Comments: Comments are formatted in C-style blocks; anything between
/*
and*/
will be omitted from the output string, as well as anything between//
and a newline (though this format is rarely used since commands are delimited by newlines). Comments cannot be nested.
Any non-recognized characters are ignored. The initial endian-ness of the output depends on the endian-ness of the host machine: on an Intel machine, the resulting data would be little-endian.
Example data string:
/* omit 01 02 */ 03 ?04? $ ##30 $ ##127 ?"dark"? ###-1 'cold'
Resulting data (Intel):
03 04 00 1E 7F 00 64 61 72 6B FF FF FF FF 63 00 6F 00 6C 00 64 00
Resulting mask:
FF 00 FF FF FF FF 00 00 00 00 FF FF FF FF FF FF FF FF FF FF FF FF
We're playing Supaplex in DOSBox and we want to have infinite bombs. The number of bombs that we have is displayed on the screen at all times, so we can always know how many the game thinks we have. We start memwatch and open an unsigned, small integer search:
fuzziqersoftware@pointy:~$ sudo memwatch dosbox
memwatch:90732/DOSBox 0s/0f # open u8 bombs
opened new u8 search named bombs
Now we start playing a level on which there are a lot of bombs. We collect a few of them and search for the number we collected:
memwatch:90732/DOSBox 1s/0f bombs:u8 # search = 3
memwatch:90732/DOSBox 1s/0f bombs:u8(378052) #
Now we use one of the bombs, and narrow down the result set to variables that had the value 3 during the initial search but have the value 2 now:
memwatch:90732/DOSBox 1s/0f bombs:u8(378052) # s = 2
memwatch:90732/DOSBox 1s/0f bombs:u8(167) #
We use one more bomb and search again:
memwatch:90732/DOSBox 1s/0f bombs:u8(167) # s = 1
(0) 000000000C34E37C 1 (0x01)
memwatch:90732/DOSBox 1s/0f bombs:u8(1) #
This single result must be the variable that represents the number of bombs we have. Now we freeze that address at a nonzero value (s0
refers to the first result in the current search):
memwatch:90732/DOSBox 1s/0f bombs:u8(1) # freeze s0 01
region frozen
Now we have infinite bombs as long as memwatch is running, and we don't unfreeze that variable.
Paper Mario for the Nintendo 64 has a glitch whereby we can replay earlier chapters of the game, but keep all of our items and upgrades from later chapters. To trigger the glitch, we need to fall under the floor in a specific room. This is possible to do without memory editing, but very difficult to pull off without spending a lot of time setting up the necessary glitches. Instead, we can use memwatch to directly change our Y coordinate within the emulator.
In this example we're using sixtyforce, but this should work for any emulator. The Nintendo 64 uses 32-bit registers, so we assume the type we need is float
. We start memwatch, open a search, and do an initial-value search:
fuzziqersoftware@pointy:~$ sudo memwatch sixtyforce
memwatch:91372/sixtyforce 0s/0f # open float y-coord
memwatch:91372/sixtyforce 1s/0f y-coord:f # s .
memwatch:91372/sixtyforce 1s/0f y-coord:f(+) #
This creates a reference point for the next search, but doesn't generate any results yet - (+)
indicates that a reference point exists for the current search. Now we jump up onto a ledge (which should increase our Y coordinate within the game) and search for increased values:
memwatch:91372/sixtyforce 1s/0f y-coord:f(+) # s >
memwatch:91372/sixtyforce 1s/0f y-coord:f(192462) #
Unknown-value searches tend to take longer to converge to the variables we want. We jump onto ledges, jump down, jump up again, etc., repeating until we get a manageable number of search results:
memwatch:91372/sixtyforce 1s/0f y-coord:f(192462) # s <
memwatch:91372/sixtyforce 1s/0f y-coord:f(55813) # s >
memwatch:91372/sixtyforce 1s/0f y-coord:f(24824) # s >
memwatch:91372/sixtyforce 1s/0f y-coord:f(5594) # s >
memwatch:91372/sixtyforce 1s/0f y-coord:f(1336) # s >
memwatch:91372/sixtyforce 1s/0f y-coord:f(327) # s >
memwatch:91372/sixtyforce 1s/0f y-coord:f(82) # s <
memwatch:91372/sixtyforce 1s/0f y-coord:f(72) # s =
memwatch:91372/sixtyforce 1s/0f y-coord:f(9) # s >
(0) 00000001070177A0 50.000000 (0x42480000)
(1) 00000001082B60C0 50.000000 (0x42480000)
(2) 00000001082F3DC0 166.129028 (0x43262108)
(3) 00000001082F3DE4 50.000000 (0x42480000)
(4) 00000001082F4214 50.000000 (0x42480000)
(5) 00000001082F4228 50.000000 (0x42480000)
(6) 0000000108350FF4 50.000000 (0x42480000)
(7) 000000010839C580 50.000000 (0x42480000)
(8) 00000001085534E4 50.000000 (0x42480000)
memwatch:91372/sixtyforce 1s/0f y-coord:f(9) #
Now we can try changing each of these individually to see if they have any effect in the game:
memwatch:91372/sixtyforce 1s/0f y-coord:f(9) # set s0 100
wrote 4 (0x4) bytes
memwatch:91372/sixtyforce 1s/0f y-coord:f(9) # set s1 100
wrote 4 (0x4) bytes
memwatch:91372/sixtyforce 1s/0f y-coord:f(9) # set s2 100
wrote 4 (0x4) bytes
memwatch:91372/sixtyforce 1s/0f y-coord:f(9) # set s3 100
wrote 4 (0x4) bytes
memwatch:91372/sixtyforce 1s/0f y-coord:f(9) # set s4 100
wrote 4 (0x4) bytes
memwatch:91372/sixtyforce 1s/0f y-coord:f(9) # set s5 100
wrote 4 (0x4) bytes
memwatch:91372/sixtyforce 1s/0f y-coord:f(9) # set s6 100
wrote 4 (0x4) bytes
memwatch:91372/sixtyforce 1s/0f y-coord:f(9) # set s7 100
wrote 4 (0x4) bytes
memwatch:91372/sixtyforce 1s/0f y-coord:f(9) # set s8 100
wrote 4 (0x4) bytes
When we wrote to s6
, we suddenly jumped up into the air within the game - this must be our Y coordinate. Now that we know this, we go to the place where we want to fall under the floor, check the value at that place, and set it to a smaller value:
memwatch:91372/sixtyforce 1s/0f y-coord:f(9) # x
(0) 00000001070177A0 0.000000 (0x00000000)
(1) 00000001082B60C0 0.000000 (0x00000000)
(2) 00000001082F3DC0 116.129028 (0x42E84210)
(3) 00000001082F3DE4 0.000000 (0x00000000)
(4) 00000001082F4214 0.000000 (0x00000000)
(5) 00000001082F4228 0.000000 (0x00000000)
(6) 0000000108350FF4 0.000000 (0x00000000)
(7) 000000010839C580 0.000000 (0x00000000)
(8) 00000001085534E4 0.000000 (0x00000000)
memwatch:91372/sixtyforce 1s/0f y-coord:f(9) # set s6 -20
wrote 4 (0x4) bytes
This causes us to fall under the floor and trigger the beginning of the story again.