/advent

21 - Diffing for Coins

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.

Changing dwords

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.

Comparison Bytes

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:

Finding the map

Identifying 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]>

Finding values

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.

Custom Formatting

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

Watchpoints

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:

  1. Checking the instruction that raised the exception using pd 1@r:PC
  2. Restoring page permissions with dmp
  3. Single stepping once
  4. Removing the read or write bits
  5. Resuming execution with dc
[0x00000000]> dmp?
Usage: dmp   Change page permissions
| dmp [addr] [size] [perms]  change permissions
| dmp [perms]                change dbg.map permissions
[0x00000000]>

Unified Comparisons

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.

Temporary Block Size

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]>

Hacking with r2frida

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.

Challenge

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!

To 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==

Conclusion

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!