Reverse Engineer Faster with Radare2 Signatures
Radare2 (r2) is an open-source, unix-like, reverse engineering framework–one with a large and diverse group of contributors. As a result, it has many commonly overlooked features, such as r2 signatures
In this blog post, you will learn how r2 signatures–or “zignatures”–can save time reversing statically compiled stripped binaries.
New to r2?
If you love code golf, command lines, and vim, then you will enjoy r2. R2 has a similar learning curve–getting started is tough, but worth it. For those who like clicking things, or would rather get a running start, check out r2’s official GUI Cutter. Either way, you should be able to follow along with the post.
Statically compiled, stripped executables
An executable needs to know the name of a function in order to look it up in an external library. This means a decent disassembler can parse an executable import table to easily discover all the needed external functions. This quickly gives a reverse engineer context as to what an executable does and where it does it.
Statically compiling an executable builds the external libraries into the executable–which means those library functions don’t need to be looked up because the executable knows exactly where they are. When the executable is stripped, the function names are removed and the reverse engineer has to put in a lot more work to get some context into what a program is doing.
The Challenge
Imagine we are tasked with reversing a statically compiled and stripped ELF file. Let’s start by trying to find some libc function calls.
A quick warmup: what function is being called here?
The lea
instruction is preparing the first argument to a function call. The rdi
register will then point to a format string. Since the fcn.00408b50
function accepts the format string as the first argument, it is probably printf. We don’t need to confirm; we can quickly label it sym.printf_prbly
with the command afn sym.printf_prbly @fcn.00408b50
.
Now we can move on with reversing. We also know that if this particular function shows up again, we will immediately have some context into the code surrounding it.
That one was easy. How about this next one: can you tell what function is being called?
A single stack variable (var_8h) is being passed to the function fcn.0040f840. Could this be malloc
? The return value in rax
is clobbered right after the function call, so no. What libc function could this be? Is it even libc? We need more information.
Opening the fcn.0040f840
function in the visual block mode (VV
command) is a bit overwhelming. There are 36 basic blocks cascading downward with 55 connecting edges (function info via afi
command). It’s a mess and will take some time to figure out all the logic. Time spent reversing this function may be wasted if it is a false path.
Reversing is all about using tricks to avoid doing more work than necessary. So, instead of diving into the assembly, let’s use signatures from r2.
First, we need a signature database file. We can quickly create such a file using the rasign2 utility.
Note: I know–I’m cheating here. I’m already aware of which libc version is being used because I compiled the binary myself. When it comes to learning, it is best to work in a known environment. We will see why shortly. I address how to do this–without cheating–in a later section.
Great, now we have a /tmp/libc_zigs.sdb
file with our signatures in it. Let’s just load them up (zo
) and try to match the current function (z.
).
No matches!?! With the exact same libc binary file!?!?! This must be a bug!!! Maybe, or maybe something else is going on.
Relax, we are reverse engineers, we can figure this out and learn as a result.
Let’s name the unknown function “sym.unknown” and make a signature for it. From there we can do some manual comparisons and figure out what’s going on.
In the above command we sent the output of z*
into r2’s internal grep ~
. This way we only see the signatures for the sym.unkown
function. Each signature has a type. You can see the types with the za??
command.
By default, r2 tries to match all signature types but you can change that with flags:
Apparently, none of the zignatures were an exact match. Maybe one was close, though.
We will try and do matching ourselves. Take a closer look at the `g` format signature above. This is the “graph metrics” signature. Here it is again:
See the numbers 36 for nbbs? That is the number of basic blocks we have. The edges value is the number of edges connecting the blocks. Are you wondering how many libc functions match just those two pieces of information? We have libc zignatures already loaded, so we can print all of them and grep (~) for “nbbs=36 edges=55”.
How about that! We only have two matches; one is our unknown function, and the other is fclose. The fclose function does take in one parameter and its return value is often ignored. If we want, we can further verify by manually comparing the unknown function to fclose in our libc.so file.
Finding the correct libc version
So, I cheated in this example. Since I compiled the target binary, I had the same version of libc at hand. This improved the ability to match the unknown function.
What can be done if you don’t know which libc version that was used?
- Obtain a nice list of libc candidates. This is not too hard. Linux has repos where things are stored in order. Check out https://github.com/niklasb/libc-database.
- Find the closest match to our binary. We can create a zignature for a known libc function in our static binary, such as the printf we found earlier. From there, we can search our libc database for the closest match. You should keep track of what metrics match best.
- Make zignatures for that libc database and use the same metrics to search for unknown functions.
The above is probably best handled with r2pipe. Whenever you find yourself running the same commands over and over again and wanting to quickly automate it, r2pipe should be your first thought.
Why did r2 fail to match?
Since this test was with the very same libc version, I was surprised that r2 failed to find a match. I figured static compiling would copy over the fclose bytes and then fix relocations.
It turns out a few instructions got mixed up.
You can quickly find where things are wrong by comparing the “bytes pattern” signatures with each other. I wrote a quick python script that does a logical AND of the byte pattern with the mask and the signature, and then a logical XOR with the two signatures. This showed there were several differences. The first difference is 84 bytes in, so we can skip there in r2 with s+84
. I opened files in two different terminals and compared them.
Here are the instructions:
Static file:
Libc file:
Computers are jerks. For some reason those two instructions were flipped in the statically compiled executable. Switching these instructions does not break the algorithm. The state of the CPU will be the same regardless of which instruction comes first.
It’s these little things that break the signature matching and make parsing algorithms with computers so difficult. It’s these little things that make people hate reverse engineering and think their tool does not work–but it’s also these little things that provide an opportunity to learn more and gain a greater appreciation for the complexity of computers and algorithms.
Conclusion
Zignatures contain basic metadata about the function. Some of the information is general, such as the number of basic blocks; other signatures are more specific, like the “bytes pattern.” Understanding this information can help you to quickly search known libraries for potential matches to an unknown function and save you a lot of reversing time.
It’s good to be aware, however, that matches will not always be perfect–computers are much more complicated than that. The data provided by Radare2 allows you to quickly perform matches in a way you wish. If you come up with a better matching algorithm, consider adding it r2. It will run faster as a built-in function. Sharing also means it will be maintained and kept in r2 with regression tests.
Hopefully this blog post helped you learn a bit more about Radar2 and how r2 zignatures can be leveraged in demasking common functions.
About Hurricane Labs
Hurricane Labs is a dynamic Managed Services Provider that unlocks the potential of Splunk and security for diverse enterprises across the United States. With a dedicated, Splunk-focused team and an emphasis on humanity and collaboration, we provide the skills, resources, and results to help make our customers’ lives easier.
For more information, visit www.hurricanelabs.com and follow us on Twitter @hurricanelabs.
