Reverse Engineering Tasks In Radare2 With r2pipe

12125921862?profile=RESIZE_400xSentinel Labs reports that in a previous post in this series, we looked at powering up radare2 with aliases and macros to make our work more productive. Still, sometimes we need the ability to automate more complex tasks, extend our analyses by bringing in other tools, or process files in batches.  Most reverse engineering platforms have some scripting engine to help achieve this kind of heavy lifting, and radare2 does, too.  In this article, researchers learn how to drive radare2 with r2pipe and tackle three challenges common to RE automation: decrypting strings, applying comments, and processing files in batches.[1]

Scripting radare2 with C, Go, Swift, Perl, Python, Ruby…

No matter what language you’re most comfortable working in, there’s a good chance that r2pipe supports it.  There are 22 supported languages, though they are not all supported equally.

12125922054?profile=RESIZE_584xProgramming languages supported by radare2’s r2pipe

C, NodeJS, Python, and Swift are the most well-supported languages, but I use Go for speed and brevity, and it lets me hack scripts together rather haphazardly to achieve what is needed.  When scripting your reversing sessions, there’s little need to worry about the niceties of programming style or convention as we would when shipping code for production or other purposes.  Although performance can be improved by doing things in one language rather than another, that was rarely needed to worry about in practice in my reversing work.

All that’s a preamble to saying that you can – and probably should! – write better scripts than those shown here, but these examples will serve as a good introduction to how you can easily hack your way around problems thanks to r2’s shell integration to get a working solution without worrying too much about “the right” or “the best” way to do it.

Automated String Decryption in OSX.Fairytale - Let's use a sample of OSX.Fairytale to illustrate automated string decryption.  Though Sentinel used Go, you can easily apply the same techniques in whatever other language you prefer.

Like many simple malware families, Fairytale encrypts strings with a combination of base64 and a hard-coded XOR key. In this case, the XOR key is 0x30.

12125921891?profile=RESIZE_584xOSX.Fairytale uses 0x30 as a hard-coded key for XOR decryption

Once researchers determined the XOR key, there were various simple ways to decrypt a given string or even the whole binary (e.g., cyberchef, or writing your own decryption function), but our eventual aim is to add comments to the disassembly (as well as learn a few useful tricks), so Sentinel took a different approach.  Note that radare2 comes with a useful little tool called rahash2, which among other things, can decrypt strings.  Below is an example you can run on the command line:

% rahash2 -D base64 -s 'H1JZXh9cUUVeU1hTRFw=' | rahash2 -D xor -S 0x30 -

/bin/launchctl%

Sentinel could have easily made this into a function in our .zshrc file.  However, one drawback with that approach is r2 won’t let us call such functions from the r2 prompt. We can solve that by creating a standalone executable and saving it in our path, like so:

#!/bin/zsh

if [ "$#" -eq 2 ]; then

          echo $(rahash2 -D xor -S $1 -s $2)

elif [ "$#" -eq 3 ]; then

          echo $(rahash2 -D base64 -s $3 | rahash2 -D xor -S $2 -)

elif [ "$#" -eq 1 ]; then

          echo "

                      # USAGE:

                              # rxorb

                              # rxorb 0x30 "\|YRBQBI"

                              # Use '-b' to base64 decode the string before the xor

                              # rxorb -b 0x30 FXAffFlSQlFCSR98UUVeU1hxV1VeREMfFXAeQFxZQ0Q=

                    "

else

          echo "INPUT ERROR, type 'rxorb help' for help."

fi

Saving this as /usr/local/bin/rxorb and giving it executable permissions (e.g., via chmod +X) will now make this available to us both on the command line and from within r2, once we open a new shell and new r2 session.

12125922081?profile=RESIZE_584xCalling rxorb from within r2 to decrypt individual strings

Great, now there is a general string decryption tool that we can feed a string, a key, and cipher text, and we can specify whether the cipher needs to be base64 decoded before being XOR’d with the given key.  This alone will take care of a lot of use cases!  However, while this works well for manual decryption, it becomes tedious for anything more than a few strings.  What would be much better is if everyone could type one command that would iterate over encrypted strings in the binary and either print out all the decrypted text or comment on the code where the string is referenced.  Ideally, Sentinel’s solution should give the option to do both.

Let’s see how one can implement that by leveraging radare2’s scripting engine, r2pipe (aka r2p).

Building the Script

We’ll call the Go program “decode.go”, and the first part of it requires importing the r2pipe package from github.

package main                                            

import (

  "fmt"

  "github.com/radareorg/r2pipe-go"

)

 

var r2p, _ = r2pipe.NewPipe("")     // Declare r2p as a global

 

func check(err error) {

     if err != nil {

          panic(err)

     }

}

After the imports, Sentinel declares a global variable r2p, which provides a pipe to the r2 instance when we call it from within an r2 session.  This global will allow one to send and receive commands to the r2 session.  Sentinel also implemented a generic error function for use throughout the code.

Now implement a decrypt function.  Researchers could (and probably should) write a native version of this, but since we already have a decrypt function using rahash2 above, we’ll reuse that.  This will also allow one to see and solve some other common challenges we might face in other scenarios.

func decryptStrAtLoc(loc string, key string) {

     bytes := fmt.Sprintf("ps @ %s", loc)              // [1] 

     str, err := r2p.Cmd(bytes)

     check(err)

     decodeCmd := fmt.Sprintf("!rxorb -b %s %s > /tmp/rxorb.txt", key, str) // [2]

     r2p.Cmd(decodeCmd)

}

The decryptStrAtLoc() function does most of the work in our program.  As parameters, it takes an address and the XOR key.  Researchers chose not to return the decrypted string to the caller but instead consume it within the function. An explanation will be provided shortly.

For each command we want to pass to the r2 session, format the command as a string, then pass the command to r2p.  Thus, [1] formats a command that returns the bytes at the current address as a string.  At [2], format a command that decodes the string by passing it to the rxorb utility written earlier.  As r2pipe’s Go implementation doesn’t support easy capture of stderr and stdout, write this to a temporary file, which will be used in the next part of the code.  If chosen to implement the XOR decryption natively in our code, a researcher could have avoided that, but seeing how to deal with stdout when using r2pipe and Go is a useful exercise for other scripts.

func writeCommentAtLoc(loc string) {

     readCmd := fmt.Sprintf("CCu `!cat -v /tmp/rxorb.txt | sed 's/\\(.*\\)/\"\\1\"/g'` @ %s", loc)   

     r2p.Cmd(readCmd)                                 

}

The decoded string is now sitting in a file in /tmp.  In the function above we do two things with one command: read the string into a buffer and write it out as a comment at the disassembly address in the file under analysis.  The sed code is another work around for wrapping the string in quotes so that any special characters in the string do not get interpreted by the r2 shell when one passes it back.

func printCommentAtLoc(loc string) {

     pdCmd := fmt.Sprintf("pd 1 @ %s", loc)   // [3]

     pdStr, _ := r2p.Cmd(pdCmd)

     fmt.Println(pdStr)

}

Next implemented as a function that will print out the disassembly along with the commented string to the r2 prompt.  At [3], the “pd 1” command tells r2 to print one disassembly line from the given address.

Finally, implement was the main() function that will call all this code and handle cleaning up the temporary file now that it is finished.

func main() {

     key := "0x30"

     addr, err := r2p.Cmd("s")                             // [4] 's' = return current address

     check(err)

     decryptStrAtLoc(addr, key)

     writeCommentAtLoc(addr)

     printCommentAtLoc(addr)

 

     delCmd := fmt.Sprintf("!rm /tmp/rxorb.txt")  // clean up the temp file

     r2p.Cmd(delCmd)

     if err != nil {

           fmt.Println(err)

     }

     defer r2p.Close()

}

Note that at [4], due to the simplicity of the command, was just supplied the command directly to r2p.Cmd rather than format a separate string.  The entire script can be found here.

Using the Script - To use the script, build the decode.go program and take a note of the output path.  Open an r2 session with the target binary and at the prompt type:

#!pipe /usr/local/bin/godec/decode # change the path to suit

If you hit return, you’ll likely see an error and then some disassembly.

12125922677?profile=RESIZE_584xThe script returns an error from sed

That’s because the script was executed while located at an address that does not contain any strings to consume.  Find an encrypted string and try again.  The r2 command izz~== will output any strings in the binary that contain “==” – a common padding for base64-encoded strings.

12125922870?profile=RESIZE_584xExecuting izz~== at the r2 prompt

Now seek to location 0x100016bdb to test our decryption program.

12125922882?profile=RESIZE_584xThe decoder has appended a comment containing the decrypted string, which looks like the beginning of a LaunchAgent or LaunchDaemon plist.  Try it again, feeding it all the strings that contain “==” in one go.  Try this:

#!pipe /usr/local/bin/godec/decode @@=`izz~==[2]`

Here’s an example of the output:

12125923082?profile=RESIZE_584x12125922492?profile=RESIZE_584xSince the #!pipe command is awkward to remember and type out every time, you might want to create an alias and/or macro for that.

$dec=#!pipe /usr/local/bin/godec/decode

(script x;  #!pipe $0)

The $dec alias allows to call this particular script easily, while the script macro allows a pass in any script path as an argument to the #!pipe command.  Note that we didn’t decode all encrypted strings in the binary.  Important to note that overall strings (including non-encrypted ones) with something like $dec @@=`izz~cstring`, but that will lead to errors.  The right way to approach this would be to add code to our program that determines whether the string at the current address is a valid base64 encoded string.  

This script could also do with some other improvements: passing the key as an argument would make it more reusable, and of course, there are many points where we lazily use r2 to shell out rather than using Go’s os package, but for now, this simple script will handle the job it was intended for and is simple to repurpose or build on.

Running a Script Without an Interactive radare2 Prompt - Sometimes a researcher just needs to run a script and get the results without needing an interactive r2 prompt.  You can tell r2 to execute a script on a binary, either before or after loading the binary, with the -i and -I flags, respectively.  The -q option will tell r2 to quit after running the script.

r2 -Iq <script file> <binary>

Do the same thing with commands, aliases, and macros directly without using a script, using the -c option. For example, this will print out the result of the meta macro without leaving you in an r2 session:

r2 -qc ".(meta)" /bin/ls

Batch Processing Files with a radare2 Script - To process several files without having to start an r2 session for each one, you can pass the file path to your script as an argument when you call r2pipe as follows:

func main() {

          args := os.Args

          if len(args) < 2 || len(args) > 2 {

                    fmt.Printf("Usage: Provide path to a binary.")

                    os.Exit(1)

          }

 

          argPath := os.Args[1]

          r2p, err := r2pipe.NewPipe(argPath)

          check(err)

          defer r2p.Close()

          r2p.Cmd("aaa") // run analysis

         

          // do your stuff

          // write results to file or stdout

}

Now process all files in a folder from the command line with something like:

% for i in ./*; do my_r2pipe_script $i; done

Conclusion - In this post, we’ve learned several useful skills. We’ve seen how to automate tasks like grabbing disassembly, adding comments, and decoding strings, and we have navigated some of the complexities of dealing with stdout when using Go to drive r2pipe.  Sentinel looked at how to pass file paths as arguments and how to run scripts, commands, and macros without opening an interactive radare2 session.  With a good understanding of the r2 commands explored throughout this series, you should now be able to adapt these skills to other automation tasks readily.

 

This article is presented at no charge for educational and informational purposes only.

Red Sky Alliance is a Cyber Threat Analysis and Intelligence Service organization.  For questions, comments, or assistance, please get in touch with the office directly at 1-844-492-7225, or feedback@redskyalliance.com

Weekly Cyber Intelligence Briefings:

Weekly Cyber Intelligence Briefings:

REDSHORTS - Weekly Cyber Intelligence Briefings

https://attendee.gotowebinar.com/register/5504229295967742989

[1] https://www.sentinelone.com/labs/automating-string-decryption-and-other-reverse-engineering-tasks-in-radare2-with-r2pipe/

E-mail me when people leave their comments –

You need to be a member of Red Sky Alliance to add comments!