Comparing C C decompilers

Comparing C C decompilers


compiling is a process of generating source code from a binary file. A good decompiler would be a powerful tool in analysis and security. While compiling is a very widespread process one would think that a reverse process would be similarly developed. This proved wrong as compiling differs between compilers and platforms, but also binary files are obfuscated and highly optimized so most of the higher-level abstractions are lost. While searching for a C/C++ decompiler I found four: RekoDecompiler, Snowman, Boomerang and RecStudio. In this post I will showcase some common code snippets and how the decompilers handle them.

To compile I used g++ with options �std=c++11 and �o0. The �o0 flag turns off optimization, this is used so that C/C++ decompilers get a fighting chance versus Java decompilers. Also much of my code has __asm__(nop); commands which are direct assembly instructions for no operation so that it would be more apparent which part of code is compiled to which assembly instructions.

The performance of each decompiler on five different examples is shown in Table 1. Decompilers are graded on a 5 point scale (--,-,-+,+,++) where -- is a very negative, - is a negative, -+ is a neutral, + is a positive and ++ is a very positive grade. In the following paragraphs we will explain what each column represents and how decompilers behaved.

Data Types
Arithmetic statements
Functions
If, Case and Loops
Class
RekoDecompiler
-
++
-+
-+
-
RecStudio
++
++
+
++
++
Boomerang
-
-
-
-
-
Snowman
+
+
++
-+
-
Table 1. Comparison of different decompilers on several tests

All the tests as well as outputs of decompilers used in this blog can be found on my GitHub page.
In the repository there is a test folder where you will find 5 subfolders corresponding to each column of Table 1. Those folders contain source code I used. In the dekomp folder are decompiled files grouped by decompiler and then by the type of the test.

C/C++ decompilers have a common problem of not being able to faithfully reconstruct data types used. This is caused by the way machine code handles data. All data is stored in 32-bit containers which are treated equally, the instructions used on the container define the data in it. This makes it exceptionally difficult for decompilers to reconstruct higher-level data types used. Best case scenario is a list of integers and void pointers with a defined size which are used in the correct parts of the code, while the worst case is just instructions run over registers and stack offset pointers which have no variables tied to them. Variable names are never reconstructed as such information isnt compiled. Decompilers which recognize variables give them arbitrary names. This helps with code comprehension but since names are usually just numbered variation of the same name its easy to get lost.

Arithmetic statements are easier to reconstruct. Some decompilers attempt to cluster simple commands into formulas while others leave them as list of functions over a register. This is not the case for floating point numbers as they have a separate set of instructions which many decompilers dont recognize well. These instructions are usually returned as __asm__(<code>); most frequently fld.

Decompilers usually faithfully reconstruct the number of functions and the number of their parameters because of the strict stack/call paradigm used for function calls by C/C++ compilers. Functions included from standard librarys are usually also named. In some examples I only decompiled the test code I wrote so definitions of included functions are not present.

If, case and loop statements are compound statements which every programmer uses. If statements are decompiled truthfully although decompilers have a problem with reconstructing comparisons between variables. This is caused by their inability to differentiate variables from hard coded data. Decompilers which didnt successfully reconstruct variables substitute them for values at the variables reserved memory address. Loop statements are usually reconstructed truthfully, with the exception that the for statement is replaced by the equivalent while statement. Case statements are substituted for a series of if else statements.

Classes are a staple of modern programming. Classes in C++ are defined as structures and are usually placed on the stack. This makes it very hard for decompilers to reconstruct them, and none are successful but there are good attempts.

Boomerang was the worst as it didnt produce good code for almost anything but I had much problems with getting it to work so I am probably using it wrong. I had most success decompiling with RecStudio and had good results with Snowman. RekoDecompiler was somewhat worse but it did handle floating point numbers the best.

As a final note some decompilers decompile the whole binary with all the extra system operations and included files while others can be made to only decompile the user generated code. To find the main function more easily user generated instructions usually begin at memory address 0x401410 and most decompilers insert comments with the starting memory address of a function at the beginning of the definition.

download file now