Welcome to the 21th day of the Advent of Radare!
Binary diffing is a key skill for reverse engineers. It’s especially useful for identifying patches, vulnerabilities, or dynamic behaviors in memory, which is particularly valuable for game hackers.
In this post, we’ll explore binary diffing from a different
perspective by finding changing values at runtime, focusing on the
cw
command. By the end, you’ll learn how to apply these
concepts to hack games in real time, similar to how hackers used tools
like “Game Genie” or “Action Replay” in the 90s.
We’ve previously explored radiff2’s capabilities for binary
comparison in another advent post. Today, we’ll focus on other memory
comparison features available inside the radare2 shell under the
c
subcommands.
The c
stands for comparison, particularly the
‘cw’ command which is used for watching changes in memory
areas.
The workflow for using comparison watches typically begins with the ‘/v’ command to search for specific values in memory, followed by setting up comparison watchers on addresses of interest.
As the program executes, these watchers continuously track changes, allowing analysts to filter out irrelevant results while maintaining focus on significant memory locations. The combination of value searching and comparison watching creates a powerful toolkit for understanding how programs manipulate data in memory.
This integration of static and dynamic analysis capabilities makes radare2 an effective tool for both binary and memory analysis, suitable for a wide range of reverse engineering tasks.
Let’s practice with the cw
command to understand how it
works. Note that you may want to use the latest r2 version from git to
get the same output. Feel free to suggest better ways to interact with
these commands by submitting patches, discussing them in the chat, or
creating issues.
Let’s start by opening our target program: /bin/ls
. This
time we’ll enable io.cache
to modify values without
overwriting the file on disk.
$ r2 -e io.cache=true /bin/ls
Add a couple of comparison watchers:
[0x100003a58]> cw
0x100003a58
old: 7f2303d5
cur: 7f2303d5
0x100003a5c
old: fc6fbaa9
cmd: fc6fbaa9
[0x100003a58]> wv4 1234
[0x100003a58]> cw
0x100003a58
old: 7f2303d5
cmd: d2040000
0x100003a5c
old: fc6fbaa9
cmd: fc6fbaa9
[0x100003a58]> cwu
[0x100003a58]> cw
0x100003a58 modified
old: 7f2303d5 => new: d2040000
cmd: d2040000
0x100003a5c
old: fc6fbaa9 => new: fc6fbaa9
cmd: fc6fbaa9
[0x100003a58]>
Checking the help of the cw
command will explain why we
need to use cwu
to get the internal value updated. In fact,
we don’t even need to define any command in the comparison watcher for
it to work correctly.
If we want to run cwu
more frequently without having to
type it manually, we can simply set the cmd.prompt
variable
like this:
[0x100c1cf38]> e cmd.prompt
.dr*
[0x100c1cf38]> 'e cmd.prompt=.dr*;cwu
[0x100c1cf38]> e cmd.prompt
.dr*;cwu
[0x100c1cf38]>
Some tips on the topic:
.dr*
: r2 sets this automatically during debugging to
create flags for register values. When working in static mode, these
won’t be set'
: using a single quote makes the REPL ignore the
;
charactercmd.visual
contains commands that will be executed in
visual modeIdentifying data location can be complex. For small programs, dumping the entire process memory is feasible, but this approach becomes impractical for large applications. Different techniques may be needed to identify the most promising memory regions.
When running in debugger mode, we can use the dm
and
dmm
commands to display the memory map layout of the target
process. Some memory regions may be labeled as [heap]
,
depending on the operating system. Generally, we want to focus on
user-allocated pages with read-write permissions. Here’s an example
output from macOS:
$ r2 -d ls
[0x10466f868]> dm~u rw-
0x0000000104310000 - 0x0000000104314000 - usr 16K u rw- ls /bin/ls ; map.ls.rw_
0x000000010469c000 - 0x00000001046a4000 - usr 32K u rw- dyld /usr/lib/dyld
0x00000001046a4000 - 0x00000001046a8000 - usr 16K u rw- dyld /usr/lib/dyld
0x00000001046a8000 - 0x00000001046ac000 - usr 16K u rw- dyld /usr/lib/dyld
0x00000001046ac000 - 0x00000001046f4000 - usr 288K u rw- dyld /usr/lib/dyld ; map.dyld.rw_
0x000000016b300000 - 0x000000016bafc000 - usr 8.0M u rw- 0b_copy_userrwx ? ; map._b_copy_userrwx.rw_
0x00000001e5fb0000 - 0x00000001e5fd4000 - usr 144K u rw- 0e_copy_1 ? ; map._e_copy_1.rw_
0x00000001e5fd4000 - 0x00000001e6000000 - usr 176K u rw- 0f_copy_1 ? ; map._f_copy_1.rw_
0x00000001e6000000 - 0x00000001e7e4c000 - usr 30.3M u rw- 10_copy_1 ? ; map._0_copy_1.rw_
0x00000001e7e94000 - 0x00000001e8f7c000 - usr 16.9M u rw- 12_copy_1 ? ; map._2_copy_1.rw_
0x0000000270978000 - 0x0000000272000000 - usr 22.5M u rw- 18_copy_1 ? ; map._8_copy_1.rw_
0x0000000272000000 - 0x0000000273cbc000 - usr 28.7M u rw- 19_copy_1 ? ; map._9_copy_1.rw_
0x0000000273cbc000 - 0x0000000274a14000 - usr 13.3M u rw- 1a_copy_1 ? ; map._a_copy_1.rw_
[0x10466f868]>
Programs commonly store important values such as scores, health
points, or ammunition as dwords
(32-bit values) in memory.
Here’s how to locate and monitor these values using radare2.
To search for a specific value (e.g., 100) in memory:
[0x20000000]> /v4 100
This command will often return many false positives. To narrow down the search, we can set search boundaries:
[0x20000000]> e search.in=range # Restrict search to a specific range
[0x20000000]> e search.from=0x1e5fb0000 # Starting address
[0x20000000]> e search.to=0x1e8f7c000 # Ending address
Since most data in programs is aligned in memory for performance reasons, we can further refine our search by specifying alignment:
[0x20000000]> e search.align=4 # Align to 4 bytes (common for 32-bit values)
After identifying potential addresses, we can monitor changes to these values using comparison watchers:
[0x100003a58]> cw $$ 4 @@/v4 100
0x100010589 hit1_0 64000000
0x100000598 hit1_1 64000000
Breaking down this command: - cw
: Creates a comparison
watcher - $$
: Represents the current offset (search hit
location) - 4
: Size of the value to watch (4 bytes for
dwords) - @@/v4 100
: Applies the command to all locations
where value 100 was found
Use cwu
while the program is running to check for value
changes. This helps identify which memory location actually contains the
value you’re interested in by showing both old and new values.
The cw
command accepts an optional argument to specify
how to display the monitored value:
[0x100003a58]> cw $$ 4 pd 1 @@/v4 100 # Show as disassembly
[0x100003a58]> cw $$ 4 px 16 @@/v4 100 # Show as hex dump
This is particularly useful when: - Working with structured data
(using pf
) - Needing different number representations
(decimal, hex) - Analyzing surrounding memory context
Some debugger primitives allow us to use watchpoints, which are hardware-based mechanisms to pause the debugger when a specific memory area is accessed (read or written). This helps us identify which parts of the code are accessing these memory regions, leading to better code understanding.
The command to set watchpoints in the radare2 debugger is:
[0x00000000]> dbw?
Usage: dbw <addr> <r/w/rw> # Add watchpoint
[0x00000000]>
Note that this feature depends on the target hardware and debugger implementation, so it won’t work in all cases. If watchpoints aren’t available, consider these alternatives:
The limitation of modifying page protections is that we can’t target a specific memory address; instead, it affects the entire page (typically 4KB or 64KB in size). This results in many false positives. However, we can work around this by:
pd 1@r:PC
dmp
dc
[0x00000000]> dmp?
Usage: dmp Change page permissions
| dmp [addr] [size] [perms] change permissions
| dmp [perms] change dbg.map permissions
[0x00000000]>
Radare2 provides several comparison tools that help analyze differences between memory blocks.
The cu
command compares data between the current
offset and a specified target address. It’s
particularly useful for in-binary diffing, allowing you to identify
byte-level differences within a single file.
To compare two sections within the same binary:
> cu section1 @ section2
This command compares the bytes in section2 with those in section1, highlighting any differences at each offset.
By default, cu
compares data according to the current
block size. You can modify the block size using the b
command to specify how many bytes to compare.
For example, to set a block size of 32 bytes:
[0x1000073c8]> b 32
[0x1000073c8]> cu $$+32 @ sym.imp.puts
- 0x100007718 110000b031a20691300240f9110a1fd7 ....1...0.@..... !
+ 0x100007738 110000b031e20691300240f9110a1fd7 ....1...0.@..... !
- 0x100007728 110000b031c20691300240f9110a1fd7 ....1...0.@..... !
+ 0x100007748 110000b031020791300240f9110a1fd7 ....1...0.@..... !
[0x1000073c8]>
NOTE that the output will look much nicer if you have colors enabled in your terminal ;D
This command compares the first 32 bytes of sym.imp.puts in the PLT with the same address plus 32. Helping us understand the address patterns that change on the relocated pointers. We may be seeing some red and green colors.
The @!
operator will change the block size with the
number specified. This operator works the same way as @
for
temporal seeks. See the following snippet exemplifies the syntax:
[0x1000073c8]> cu $$+32 @ sym.imp.puts @!32
- 0x100007718 110000b031a20691300240f9110a1fd7 ....1...0.@..... !
+ 0x100007738 110000b031e20691300240f9110a1fd7 ....1...0.@..... !
- 0x100007728 110000b031c20691300240f9110a1fd7 ....1...0.@..... !
+ 0x100007748 110000b031020791300240f9110a1fd7 ....1...0.@..... !
[0x1000073c8]>
Thanks to the orthogonal design of radare2, we can use all the knowledge we learned from static analysis or debugging, replacing the IO layer with any other one, such as a remote GDB connection to a Gameboy emulator or a remote iOS game running on a jailbroken iPhone connecting through the Frida server via USB using the r2frida plugin.
r2frida works locally on all major platforms and architectures and
can be used as a debugger backend when using the dL io
command. The main difference is that Frida is designed
to work as a tracer, meaning that the process won’t stop and resume, and
we can manipulate memory and code at runtime without depending on
traditional debugger primitives.
Note that all r2frida commands are implemented in TypeScript in the
agent that runs inside the target process. This means we need to use the
io-system interface from r2 by prefixing the commands with the
:
character.
To list maps, use :dm
instead of dm
. To
search for values inside the process, use :/x
instead of
/x
, and so on.
However, the IO backend won’t prevent us from using any of our
familiar r2 commands like cu
or wtf
to compare
memory and write portions of memory to disk.
Additionally, we can use :dtf
to trace functions and log
the pointers returned by malloc with this
one-liner:
[0x10461b5d4]> :dtf malloc %p
Check the help message to understand why we use %p
for
the format string here:
[0x1008d35d4]> :dtf?
Usage: dtf [format] || dtf [addr] [fmt]
^ = trace onEnter instead of onExit
% = format return value (only on onLeave)
+ = show backtrace on trace
p/x = show pointer in hexadecimal
c = show value as a string (char)
i = show decimal argument
z = show pointer to string
w = show pointer to UTF-16 string
a = show pointer to ANSI string
h = hexdump from pointer (optional length, h16 to dump 16 bytes)
H = hexdump from pointer (optional position of length argument, H1 to dump args[1] bytes)
s = show string in place
Z = untrusted null terminated string (like z)
S = pointer to string
O = show pointer to ObjC object
:dtf [addr] [fmt] add a trace parsing arguments using a format string
[0x1008d35d4]>
If you want to learn more about hacking games with r2ai and decai I encourage you to watch this presentation from #r2con2024.
Your challenge today is about patching a simple game in memory.
The following base64 block contains the source code for a simple game I wrote specifically for this post. It should compile on any UNIX system with just libc and a C compiler.
The game will remind you of some classics where you collect coins
while avoiding enemies and using the coins to shoot at them to avoid
being killed. To simplify things, this game isn’t real-time but rather
turn-based - enemies move only when you press a key. This makes it
easier to use the debugger without having to interrupt the process and
experiment with special features available in radare2, such as accessing
process memory using mmap:// on proc/pid/mem, or using the
ptrace://
IO plugin instead of the dbg://
URI
handler.
This allows you to read and write process memory without stopping execution. Feel free to use any techniques you know to get extra points or lives!
dm
/ dm.
to find the heap mapwtf
command at different points
in the game/v4
and search.align=4
to find the
little-endian value of coins or livespf
to format the heap structure containing the
pointerscw
commands on memory addresses to compare
value changes at different game stateswv4
to modify coins/lives as desired to hack the
gameTo unpack the encoded blob and compile the source:
$ base64 -D | xz -d > gamecoin.c
$ gcc gamecoin.c
$ ./a.out
You can build this game using -O3
and even
strip
to remove symbol information, adjusting the
difficulty based on your skill level and knowledge.
/Td6WFoAAATm1rRGBMCvCcgYIQEcAAAAAAAAAJl2jf3gDEcEp10AF+B84GE2v2RRbcn5sn2MpsmuWcH1
X9rNRRaw5bRncOKT+VPo5ct6ClMPJ6FG7iYZDrk23J1xr+6/gQK53zjzS36RYQW3Hh1agrNdAun8cS/i
x0o2c0+PJeApgEFpkLLOycFZo0/Jnd9+tu+E0ipLip0rEs3y8rzTQ2U7gxRMT35ORUgmSkZNQBSq8iiW
+5dC9BQK7zSGK9StyyMBVdNHpIFc24UWYzjpGVxlwb0vI5W74bDQnL0CBt15++6561q7F5GyV7eAUYzG
e+YzJAhJRHxSo+hIymXywpRukf+CJpuJFlsf0nHxZwaZhX5ozAEHDjn+73f2v0F37vc4BDQso4toKz/Y
aJhgfyahqPKuZ2gD7pOGWvkH3Tz8ZaoOtOsFsVFHcgqOl2hXl30OdHHhTt7jzS1Z0w1adCh8VBfNXSYP
BQRpRvxc9NohhQPa5AS6+hQWYmNFEd8ak5rTdQ52kizLxvl/iAiRJsUCGwURJlxw0JYlVLQY+gPxw/mM
wLfrwk2pxETfqkQbSFwWHmSr+A48Qes8YCaO6MuFCXxNvNU9LSWIBTxInrmiouAhrD5WZUNNUNm1G8tl
EfCXTqblpYVUHEE0qyeqzljheY5vTLTwfk9tPaSsLt0HY5JaagSIw3gYJH+Lha+a7wEe6aqgQc3N8Uaa
OgTg45oRT2516rxp9JylImHGrFtt6zXf82Qqbqit7ibGb/CXSZvCCWIwwo5EctCysPyfBb2dpsC/W7Fb
fB1AN1sLUal1yNPxm4uD2eYPtvieYUNBBF7E2QAJ6quopYuqIt/Xcsb0kWeO10jplYFQRAmm/rWh7pxw
1GfQXVzwd43sBFa1kMFgg6zjaqjEuSip+qp6HKHy7UbUBMwG8G56gTD3vQNhP8L4ZfiETYABP8q8THU9
FCz+4JhUlY/HraeWZpxgrESOeUPybPLjehdgxIs2GggNir5CfWfXhf7Fg9QxkZ+10WGr9PxNAgRz0V2C
UiR5gWRqhxn2n+JkaCI9VSyfCiy3EeWvu2/j/tVWJQpIdu+ibJFErIKiaebFH7Wc9aaaQSgbVulkSN/Z
wDx4BEELyIgA1a1/GkEL5HgBgyynqYhjIPXMk28lXpGr8BNQ0XAji/WA9Gc5AFG0+UFWKuh/r7/rHQsw
fB9k80NfSiw8FCnCclQrnqZRUys0+9EO4Y0LScqufnmDVMN0uUDV62eKx0jA4JHnLdRJYYWshb9LEg+g
UcGMfE7y9/obL022Wv6Z5pLCHWQyGVS93lGLWVFQqswaUbfiBdQXKw9rXHyazC11tab39bYCMSTNIWSh
+mzV1vFz634cI4XqWzl0hQHsNYwVAXMmiMHzQRellJMpy9sTr+aETJCM3i+QrJE1EicaftcHWw/tWJ/M
sRuT545z0jbGKdorOr9KA+OmTKEykMfqqDSVy6uwl5z5twDAFCgcoVrK9F53A6LEb8GRdXISbsFPjyvU
E/3y2xREhcpKq0/f0PbsF6KdIvosKwbW7oXc2czXjoF0+swZE2b2K6lD/bU/9iA9zeJ7d4sJoMsRDufV
mYLJUqCAD+hmh1srOu+Cg7fesslWOKX81DRlZ8mqAAC+VRr1HVS1igABywnIGAAA+mymFLHEZ/sCAAAA
AARZWg==
From binary diffing with radiff2
to runtime memory
comparison and data patching, Radare2 empowers reverse engineers with
versatile tools. Experiment with these techniques and share your
game-hacking adventures!
Check out this repo for further ideas on the topic, and see you tomorrow in another low level adventure!