Skip to content

r2frida

r2frida is a plugin for radare2, a reverse engineering framework. This plugin aims to join the capabilities of static analysis of radare2 and the instrumentation provided by Frida. Even though it is possible to achieve similar results using only Frida alone (when it comes to in-memory related stuff) having the power of radare2 joined to assemble patches in memory and static analysis all-in-one makes it a great tandem.

In order to get this plugin working a working radare2 setup is required. From now, radare2 will be referred to asr2. To get r2 setup in your environment, please refer to their INSTALL.md.

To setup this plugin, the simplest way is by running radare2's package manager:

r2pm -ci r2frida

r2frida commands

When searching for r2frida documentation or blogposts, it is likely that you will find commands starting with backslash \. This is the old way of running commands with r2frida, commands are now run starting with :.

Testing r2frida setup

In order to confirm that r2frida is setup properly, it simplest way of doing so is by running it against a known binary. In this case /bin/ls is a good candidate for it in unix systems:

r2 frida:///bin/ls

And then it should get into r2's shell:

$ r2 frida:///bin/ls
 -- This binary no good. Try another.
[0x00000000]> 
The next step is verifying that Frida is working and is read properly by r2frida:

[0x00000000]> :?V
{"version":"15.1.17"}

To test this out there are other commands that you could use:

[0x00000000]> :i  # information about the process and frida runtime
arch                x86
bits                64
os                  linux
pid                 9502
uid                 1000
objc                false
runtime             QJS
swift               false
java                false
mainLoop            false
pageSize            4096
pointerSize         8
codeSigningPolicy   optional
isDebuggerAttached  true
cwd                 /home/jedi
[0x00000000]> :dp  # prints the PID of the process
9502

Congratulations, your r2frida setup is now working!

Attaching to running processes.

To attach to already running processes, the name or the PID of the process is required instead of the absolute path:

For a PID: r2 frida://<PID>. For example r2 frida://1234

For a process name: r2 frida://<PROCESS_NAME>. For example r2 frida://notepad.exe

Tracing functions

r2frida allows the user to trace functions without writing instrumentation code. To do so, it is possible to use the command dt(=trace) and dtf(=trace format). :dtf allows to format the output of the traced functions, instrument onEnter blocks and display backtraces. When typing :dtf? it displays the following help:

Usage: dtf [format] || dtf [addr] [fmt]
  ^  = trace onEnter instead of onExit
  +  = 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
  O  = show pointer to ObjC object
Undocumented: Z, S
 dtf    trace format

For example, to instrument a function that receives a const char* as the first argument it is possible to do so by:

:dtf 0x7f101d1ed000 z^

z will read the first argument as a UTF-8 string and ^ traces the onEnter instead of the onLeave block. To understand how to use the tracing command it is better to do it through practical examples.

Tracing functions from imports/exports

The easiest path to instrument function is whenever these functions are easily obtainable from the process' imports and exports. Through r2frida it is possible to retrieve the address of the function that we want to instrument by using the :iE command but before going further; we will get a known binary to test with: wget.

To instrument wget with parameters in r2frida it is done this way:

r2 "frida:///wget man7.org"

Note that the unlike the previous example where r2frida was spawned this time the frida:/// block is surrounded by double quotes, this allows to pass arguments to the target binary. The target function to instrument in this case is fopen that receives the filename in the first argument and mode in second argument both as const char*'s. Once we are in the r2 shell we should have tue following console:

r2 "frida:///usr/bin/wget man7.org"
r_config_set: variable 'asm.cmtright' not found
 -- If you're having fun using radare2, odds are that you're doing something wrong.
[0x00000000]>

fopen is a function imported from libc and to be able to list the imports/exports of this module we need to position ourselves in the correct module address. To do so, we can use the :dm command to enumerate modules and their addresses:

0x0000560be7abb000 - 0x0000560be7b2e000 r-x /usr/bin/wget
0x0000560be7d2e000 - 0x0000560be7d32000 r-- /usr/bin/wget
0x0000560be7d32000 - 0x0000560be7d35000 rw- /usr/bin/wget
0x0000560be7d35000 - 0x0000560be7d3c000 rw-
[...]
0x00007ff0a9db9000 - 0x00007ff0a9df7000 r-x /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ff0a9df7000 - 0x00007ff0a9df8000 rwx /lib/x86_64-linux-gnu/libc-2.27.so
0x00007ff0a9df8000 - 0x00007ff0a9df9000 r-x /lib/x86_64-linux-gnu/libc-2.27.so
[...]

The output has been shortened, but the important bit is the first entry of libc-2.27.so. By entering the address in r2's console, we position ourselves in the module address:

[0x00000000]> 0x00007ff0a9db9000
[0x7ff0a9db9000]>

From here, it is possible to enumerate exports by using the :iE command. However, the output of :iE is huge and to filter out the results to those containing fopen the ~ operator allows to do so:

[0x7ff0a9db9000]> :iE~fopen
0x7ff0a9e45450 f _IO_file_fopen
0x7ff0a9e37de0 f fopen
0x7ff0a9e380d0 f fopencookie
0x7ff0a9e37de0 f _IO_fopen
0x7ff0a9e37de0 f fopen64
[0x7ff0a9db9000]>

0x7ff0a9e37de0 is the memory address of the libc's fopen and as mentioned before it contains two arguments that can be read as UTF8 strings. With this information it is now possible to use the :dtf: command to trace the function and print out the values of each argument:

[0x7ff0a9db9000]> :dtf 0x7ff0a9e37de0 zz^
true
[0x7ff0a9db9000]> :dc
resumed spawned process.

The :dtf command returns true signaling that the command was issued succesfully. z will display the value read as an UTF8String and the second z will do the same thing for the second argument. '^' also shows the backtrace of the function. To resume execution, the :dc does so.

I> It is possible to combine different format types for different arguments, for example it is possible to print the hexdump of the second argument instead by replacing z with h: :dtf 0x7ff0a9e37de0 zh^

When the execution is resumed, the following output is showed in our console:

Console output

The output has been filtered to include only the interesting bits, but it can be seen that each argument is interpreted correctly as a UTF8 string and displayed along their backtrace.

Tracing functions by using offsets

Unlike the previous section, this time the objective is to instrument a function that is not directly listed in the imports/exports table which is something common when tracing functions. In this situation what we need to do is to retrieve the offset to the function(s) that are to be instrumented.

To illustrate this example, the following code will be used to trace the memcmp and check functions:

// gcc check_password.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Damn_YoU_Got_The_Flag
char password[] = "\x18\x3d\x31\x32\x03\x05\x33\x09\x03\x1b\x33\x28\x03\x08\x34\x39\x03\x1a\x30\x3d\x3b";

inline int check(char* input);

int check(char* input) {
  for (int i = 0; i < sizeof(password) - 1; ++i) {
    password[i] ^= 0x5c;
  }
  return memcmp(password, input, sizeof(password) - 1);
}

int main(int argc, char **argv) {
  if (argc != 2) {
    printf("Usage: %s <password>\n", argv[0]);
    return EXIT_FAILURE;
  }
  int size_of_password = (sizeof(password) - 1);
  printf("size: %d", size_of_password);
  if (strlen(argv[1]) == (sizeof(password) - 1) && check(argv[1]) == 0) {
    puts("You got it !!");
    return EXIT_SUCCESS;
  }

  puts("Wrong");
  return EXIT_FAILURE;

}

This time the code is compiled using gcc and when opening in in r2frida and inspecting the imports/exports of the binary it is a blank slate:

[0x00000000]> :dm
0x00005591d1947000 - 0x00005591d1948000 r-x /tmp/a.out
0x00005591d1b47000 - 0x00005591d1b48000 r-- /tmp/a.out
0x00005591d1b48000 - 0x00005591d1b49000 rw- /tmp/a.out
# ...
[0x00000000]> s 0x00005591d1947000
[0x5591d1947000]> :iE
[0x5591d1947000]> :ii
[0x5591d1947000]>

When opening this binary with r2 -A to analyze it, this is the output obtained when listing functions:

$ r2 -A a.out
[0x00000610]> afl
0x00000610    1 42           entry0
0x00000640    4 50   -> 40   sym.deregister_tm_clones
0x00000680    4 66   -> 57   sym.register_tm_clones
0x000006d0    5 58   -> 51   sym.__do_global_dtors_aux
0x00000600    1 6            sym.imp.__cxa_finalize
0x00000710    1 10           entry.init0
0x000008a0    1 2            sym.__libc_csu_fini
0x000008a4    1 9            sym._fini
0x00000830    4 101          sym.__libc_csu_init
0x0000077b    7 170          main
0x0000071a    4 97           sym.check
0x00000598    3 23           sym._init
0x000005c0    1 6            sym.imp.puts
0x000005d0    1 6            sym.imp.strlen
0x000005e0    1 6            sym.imp.printf
0x00000000    2 25           loc.imp._ITM_deregisterTMCloneTable
0x000005f0    1 6            sym.imp.memcmp
0x000001a5    1 38           fcn.000001a5
[0x00000610]>

What is seen in the first column are the offsets for the functions and the ones which are of interest to us are sym.imp.memcmp and sym.check:

# offset                     function
0x0000071a    4 97           sym.check
0x000005f0    1 6            sym.imp.memcmp

After retrieving the values for both functions, the next step is spawning the binary using r2frida to calculate the memory addresses of these functions. To ensure that both the memcmp and the check function are called the binary has been spawned with the following argument:

r2 "frida:///tmp/a.out testtesttesttesttestt"

The next step is retrieving the base address of a.out which can be done by using the :dm command to list modules joint with ~ to filter out the results:

[0x00000000]> :dm~out
0x000055c05fc86000 - 0x000055c05fc87000 r-x /tmp/a.out
0x000055c05fe86000 - 0x000055c05fe87000 r-- /tmp/a.out
0x000055c05fe87000 - 0x000055c05fe88000 rw- /tmp/a.out

In this situation, the base address for the spawned process is 0x000055c05fc86000 which can be used to add the offsets of each function and get their real address. For example, for sym.check:

[0x55c05fc8671a]> 0x000055c05fc86000 + 0x0000071a

0x55c05fc8671a is the address of the sym.check function, the next step is to trace it:

[0x55c05fc8671a]> :dtf 0x55c05fc8671a z^
true

With z the value is read as a UTF8 string. The next function to instrument is memcmp:

[0x55c05fc8671a]> 0x000055c05fc86000 + 0x000005f0
[0x55c05fc865f0]>
[0x55c05fc865f0]> :dtf 0x55c05fc865f0 hh
true

Since memcmp receives two const void* parameters the tracing format that we are using here is hh to hexdump the address of both arguments. Now that both functions have been traced the execution of the process can be resumed by calling :dc:

When the execution is resumed, the argument we passed to the check function "testtesttesttesttestt" can be read when the function is traced. The hexdumps for both arguments are also printed and from there the flag can be obtained.

Disassembling functions in memory

With r2frida it is possible to analyze and disassemble functions in memory provided the right addresses, something that is very powerful to inspect what functions do to extract valuable information from them. To learn to do this, we are going to use the previous code with the known offsets of the sym.check function and the memcmp function.

Again, we open the binary the same way as before:

r2 "frida:///tmp/a.out testtesttesttesttestt"

And then set emu.str=true to view the strings obtained from emulation and place ourselves at the sym.check address:

[0x00000000]> e emu.str=true
[0x00000000]> :dm~out
0x000055bf4aefa000 - 0x000055bf4aefb000 r-x /tmp/a.out
0x000055bf4b0fa000 - 0x000055bf4b0fb000 r-- /tmp/a.out
0x000055bf4b0fb000 - 0x000055bf4b0fc000 rw- /tmp/a.out
[0x00000000]> 0x000055bf4aefa000 + 0x0000071a
[0x55bf4aefa71a]>

The next step is analyzing the function which is done by typing af @ address, in our case:

And with this , we have access to the disassembly of the function in memory and the addresses used by it.

Replacing return values (hijacking)

Intercepting and replacing (also known as hijacking) the return value of an address is possible using the :di command. The help of :di? shows:

[0x00000000]> :di?
 di intercept help
 di-1   intercept ret_1
 di0    intercept ret0
 di1    intercept ret1
 dif    intercept fun help
 dif-1  intercept fun ret_1
 dif0   intercept fun ret0
 dif1   intercept fun ret1
 difi   intercept fun ret int
 difs   intercept fun ret string
 dii    intercept ret int
 dis    intercept ret string
 div    intercept ret void

What this means is that :di-1 will replace the return value of the address with -1, :di0 will make the return value 0 and the same goes for :di1 which sets the return value to one. The same code as in the previous section is what we areusing to test this command out.

The idea is to patch the check function's return value so that it returns 0 allowing the code to return the string "You got it !!". The first thing to do to get the address of the check function:

[0x00000000]> 0x0000563b168e5000 + 0x0000071a
[0x563b168e571a]> :di0 0x563b168e571a
The address of the check function for this execution is 0x563b168e571a. The next step is to modify the return value by using the :di0 command:

[0x563b168e571a]> :di0 0x563b168e571a

And when checking the main function, the latest function called is 0x563b168e55c0 on which we are going to place a breakpoint by using the :db command to be able to see what happens:

│    │ │    0x563b168e5819      e8a2fdffff     0x563b168e55c0  ()
│    │ │    0x563b168e581e      b801000000     eax = 1
│    │ │    ; CODE XREFS from fcn.563b168e577b @ 0x563b168e57b0, 0x563b168e5810
│    └─└──> 0x563b168e5823      c9             leav
└           0x563b168e5824      c3             re
[0x563b168e577b]> :db 0x563b168e55c0

Now the execution can be resumed by using the :dc command:

[0x563b168e577b]> :dc
resumed spawned process.
[0x563b168e577b]> 0x563b168e55c0
0x563b168e580b a.out!main+0x90
    0x7f55ee0f6c87 libc.so.6!__libc_start_main+0xe7
    0x563b168e563a a.out!_start+0x2a
[0x563b168e577b]> # breakpoint was hit, resume execution again using :dc
[0x563b168e577b]> :dc
size: 21You got it !!
DetachReason: FRIDA_SESSION_DETACH_REASON_PROCESS_TERMINATED

We can see that although the process was spawned with the "testtesttesttesttestt" string instead of the correct flag it returned 0 and the code returns "You got it !!" in turn.

Allocating strings

A common use case is allocating strings on the heap, this can be done with the :dmas command. The :dmas command takes a string value and returns the address of the allocated string in the heap, for example:

[0x00000000]> :dmas r2fridarul3s
0x7f58d1436b60

The string r2fridarules is allocated at the address 0x7f58d1436b60. To keep track of the stuff that we have allocated throughout our session there command :dmal returns the list of our current allocations:

[0x00000000]> :dmal
0x7f58d1436b60  "r2fridarul3s"

Calling functions

It is possible to call functions of the process within r2frida by using the :dx command. The help for the :dx command is as follows:

[0x00000000]> :dx?
 dxc  dx call
 dxo  dx objc
 dxs  dx syscall

The :dx command is able to perform regular CALLs, Objective-C calls or syscalls. To test this feature out the following code example is used:

#include <stdio.h>

int main()
{
  FILE *fp = NULL;
  fp = fopen("sample_file.dat", "w");
  fclose(fp);
  return 0;
}

What we are going to do is to call the fopen function of this binary with a custom string, in order to do so the first step is opening it up in r2frida and the required strings for filename and mode respectively:

[0x00000000]> :dmas r2fridarul3s
0x7fd21c121cb0
[0x00000000]> :dmas w
0x7fd21e825e90
[0x00000000]> :dmal
0x7fd21c121cb0  "r2fridarul3s"
0x7fd21e825e90  "w"

Now that both strings have been allocated the next step is figuring out the address of the fopen function. This can be done as previously learned by getting the base address of the process:

[0x00000000]> :dm~out
0x000055c153ffa000 - 0x000055c153ffb000 r-x /home/jedi/fopentest/a.out
0x000055c1541fa000 - 0x000055c1541fb000 r-- /home/jedi/fopentest/a.out
0x000055c1541fb000 - 0x000055c1541fc000 rw- /home/jedi/fopentest/a.out
[0x00000000]> 0x000055c153ffa000 + 0x00000560

The result is that the address of the fopen function for this process is 0x55c153ffa560 which can now be used to call :dxc:

[0x55c153ffa560]> :dxc 0x55c153ffa560 0x7fd21c121cb0 0x7fd21e825e90
"0x7fd200000bc0"

The result 0x7fd200000bc0 is the address of the pointer to the FILE* returned by fopen. When inspecting our local folder the file is now present:

$ ls | grep r2
r2fridarul3s