b

DiscoverSearch
About
My stuff
Towards Neural Decompilation
2019·arXiv
Abstract
Abstract

We address the problem of automatic decompilation, converting a program in low-level representation back to a higher-level human-readable programming language. The problem of decompilation is extremely important for security researchers. Finding vulnerabilities and understanding how malware operates is much easier when done over source code.

The importance of decompilation has motivated the construction of hand-crafted rule-based decompilers. Such decompilers have been designed by experts to detect specific control-flow structures and idioms in low-level code and lift them to source level. The cost of supporting additional languages or new language features in these models is very high.

We present a novel approach to decompilation based on neural machine translation. The main idea is to automatically learn a decompiler from a given compiler. Given a compiler from a source language S to a target language T, our approach automatically trains a decompiler that can translate (decompile) T back to S. We used our framework to decompile both LLVM IR and x86 assembly to C code with high success rates. Using our LLVM and x86 instantiations, we were able to successfully decompile over 97% and 88% of our benchmarks respectively.

Given a low-level program in binary form or in some intermediate representation, decompilation is the task of lifting that program to human-readable high-level source code.

Fig. 1 provides a high-level example of decompilation. The input to the decompilation task is a low-level code snippet, such as the one in Fig. 1(a). The goal of Decompilation is to generate a corresponding equivalent high-level code. The C code snippet of Fig. 1(b) is the desired output for Fig. 1(a).

There are many uses for decompilation. The most common is for security purposes. Searching for software vulnerabilities and analyzing malware both start with

image

Figure 1. Example input (a) and output (b) of decompilation.

understanding the low-level code comprising the program. Currently this is done manually by reverse engineering the program. Reverse engineering is a slow and tedious process by which a specialist tries to understand what a program does and how it does it. Decompilation can greatly improve this process by translating the binary code to a more readable higher-level code.

Decompilation has many applications beyond security. For example, porting a program to a new hardware architecture or operating system is easier when source code is available and can be compiled to the new environment. Decompilation also opens the door to application of source-level analysis and optimization tools.

Existing Decompilers Existing decompilers, such as Hex-Rays [2] and Phoenix [34], rely on pattern matching to identify the high-level control-flow structure in a program. These decompilers try to match segments of a program’s control-flow graph (CFG) to some patterns known to originate from certain control-flow structures (e.g. if-then-else or loops). This approach often fails when faced with non-trivial code, and uses goto statements to emulate the control-flow of the binary code. The resulting code is often low-level, and is really assembly transliterated into C (e.g. assigning variables to temporary values/registers, using gotos, and using low-level operations rather than high-level constructs provided by the language). While it is usually semantically equivalent to the original binary code, it is hard to read, and in some cases less efficient, prohibiting recompilation of the decompiled code.

There are goto-free decompilers, such as DREAM++ [36, 37], that can decompile code without resorting to using gotos in the generated code. However, all existing decompilers, even goto-free ones, are based on hand-crafted rules designed by experts, making decompiler development slow and costly.

Even if a decompiler from a low-level language  Llow toa high-level language  Lhiдhexists, given a new language L′hiдh, it is nontrivial to create a decompiler from  Llowto  L′hiдhbased on the existing decompiler. There is no guarantee that any of the existing rules can be reused for the new decompiler.

Neural Machine Translation Recent years have seen tremendous progress in Neural Machine Translation (NMT) [16, 22, 35]. NMT systems use neural networks to translate a text from one language to another, and are widely used on natural languages. Intuitively, one can think of NMT as encoding an input text on one side and decoding it to the output language on the other side (see Section 3 for more details). Recent work suggests that neural networks are also effective in summarizing source code [9, 11–13, 20, 21, 28, 29].

Recently, Katz et al. [23] suggested using neural networks, specifically RNNs, for decompilation. Their approach trains a model for translating binary code directly to C source code. However, they did not compensate for the differences between natural languages and programming languages, thus leading to poor results. For example, the code they generate often cannot be compiled or is not equivalent to the original source code. Their work, however, did highlight the viability of using Neural Machine Translation for decompilation, thus supporting the direction we are pursuing. Section 8 provides additional discussion of [23].

Our Approach We present a novel automatic neural decompilation technique, using a two-phased approach. In the first phase, we generate a templated code snippet which is structurally equivalent to the input. The code template determines the computation structure without assignment of variables and numerical constants. Then, in the second phase, we fill the template with values to get the final decompiled program. The second phase is described in Section 5.

Our approach can facilitate the creation of a decompiler from  Llow to Lhiдhfrom every pair of languages for which a compiler from  Lhiдh to Llow exists.

The technique suggested by [23] attempted to apply NMT to binary code as-is, i.e. without any additional steps and techniques to support the translation. We recognize that for a trainable decompiler, and specifically an NMT-based decompiler, to be useful in practice, we need to augment it with programming-languages knowledge (i.e. domain-knowledge). Using domain-knowledge we can make translations simpler and overcome many shortcomings of the NMT model. This insight is implemented in our approach as our canonicalization step (Section 4.3, for simplifying translations) and template filling (Section 5, for overcoming NMT shortcomings).

Our technique is still modest in its abilities, but presents a significant step forward towards trainable decompilers and in the application of NMT to the problem of decompilation. The first phase of our approach borrows techniques from natural language processing (NLP) and applies them to programming languages. We use an existing NMT system to translate a program in a lowerlevel language to a templated program in a higher-level language.

Since we are working on programming languages rather than natural languages, we can overcome some major pitfalls for traditional NMT systems, such as training data generation (Section 4.2) and verification of translation correctness (Section 4.4). We incorporate these insights to create a decompilation technique capable of self-improvement by identifying decompilation failures as they occur, and triggering further training as needed to overcome such failures.

By using NMT techniques as the core of our decompiler’s first phase, we avoid the manual work required in traditional decompilers. The core of our technique is language-agnostic requiring only minimal manual intervention (i.e., implementing a compiler interface).

One of the reasons that NMT works well in our setting is the fact that, compared to natural language, code has a more repetitive structure and a significantly smaller vocabulary. This enables training with significantly fewer examples than what is typically required for NLP [26] (See Section 6).

Mission Statement Our goal is to decompile short snippets of low-level code to equivalent high-level snippets. We aim to handle multiple languages (e.g. x86 assembly and LLVM IR). We focus on code compiled using existing off-the-shelf compilers (e.g. gcc [1] and clang [4]), with compiler optimizations enabled, for the purpose of finding bugs and vulnerabilities in benign software. More specifically, we do not attempt to handle hand-crafted assembly as is often found in malware.

Many previous works aimed to use decompilation as a mean of understanding the low-level code, and thus focused mostly on code readability. In addition to readability, we place a great emphasis on generating code that is correct (i.e., can be compiled without further modifications) and equivalent to the given input.

We wish to further emphasize that the goal of our work is not to outperform existing decomopilers (e.g., Hex-Rays [2]). Many years of development have been invested in such decompilers, resulting in mature and well-tested (though not yet perfect) tools. Rather, we wish to shed light on trainable decompilation, and NMTbased decompilation in particular, as a promising alternative approach to traditional decompilation. This new approach holds the advantage over existing decompilers not in its current results, but in its potential to handle new languages, features, compilers, and architectures with minimal manual intervention. We believe this ability will play a vital role as decompilation will become more widely used for finding vulenrabilities.

Main Contributions The paper makes the following contributions:

A significant step towards neural decompilation by combining ideas from neural machine translation (NMT) and program analysis. Our work brings this promising approach to decompilation closer to being practically useful and viable.

A decompilation framework that automatically generates training data and checks the correctness of translation using a verifier.

A decompilation technique that is applicable to many pairs of source and target languages and is mostly independent of the actual low-level source and high-level target languages used.

An implementation of our technique in a framework called TraFix (short for TRAnslate and FIX) that, given a compiler from  Lhiдh to Llowautomatically learns a decompiler from  Llow to Lhiдh.

An instantiation of our framework for decompilation of C source code from LLVM intermediate representation (IR) [3] and x86 assembly. We used these instances to evaluate our technique on decompilation of small simple code snippets.

An evaluation showing that our framework decompiles statements in both LLVM IR and x86 assembly back to C source code with high success rates. The evaluation demonstrates the framework’s ability to successfully self-advance as needed.

In this section we provide an informal overview of our approach.

2.1 Motivating Example

Consider the x86 assembly example of Fig. 1(a). Fig. 2 shows the major steps we take for decompiling that example.

The first step in decompiling a given input is applying canonicalization. In this example, for the sake of simplicity, we limited canonicalization to only splitting numbers to digits (Section 4.3.1), thus replacing 14 with 1 4, resulting in the code in block (1). This code is provided to the decompiler for translation.

The output of our decompiler’s NMT model is a canonicalized version of C, as seen in block (2). In this example, output canonicalization consists of splitting numbers to digits, same as was applied to the input, and printing the code in post-order (Section 4.3.2), i.e. each operator appears after its operands. We apply un-canonicalization to the output, which converts it from post-order to in-order, resulting in the code in block (3). The output of un-canonicalization might contain decompilation errors, thus we treat it as a code template. Finally, by comparing the code in block (3) with the original input in Fig. 1, we fill the template (i.e. by determining the correct numeric values that should appear in the code, see Section 5), resulting in the code in block (4). The code in block (4) is then returned to the user as the final output.

For further details on the canonicalizations applied by the decompiler, see Section 4.3.

2.2 Decompilation Approach

Our approach to decompilation consists of two complementary phases: (1) Generating a code template that, when compiled, matches the computation structure of the input, and (2) Filling the template with values and constants that result in code equivalent to the input.

2.2.1 First Phase: Obtaining a Template

Fig. 3 provides a schematic representation of this phase.

At the heart of our decompiler is the NMT model. We surround the NMT model with a feedback loop that allows the system to determine success/failure rates and improve itself as needed by further training.

Denote the input language of our decompiler as  Llowand the output language as  Lhiдh, such that the grammar of both languages is known. Given a dataset of input statements in  Llowto decompile, and a compiler from  Lhiдh to Llow, the decompiler can either start from scratch, with an empty model, or from a previously trained model. The decompiler translates each of the input statements to  Lhiдh. For each statement, the NMT model generates a few translations that it deemed to be most likely. The decompiler then evaluates the generated translation. It compiles each suggested translation from Lhiдhto  Llowusing existing of-the-shelf compilers. The compiled translations are compared against the original input statement in  Llowand classified as successful translations or failed translations. At this phase, the translations are code templates, not yet actual code, thus the comparison focuses on matching the computation structure. A failed translation therefore does not match the structure of the input, and cannot produce code equivalent to the input in phase 2. We denote input statements for which there was no successful translation

image

Figure 2. Steps for decompiling x86 assembly to C: (1) canonicalized x86 input, (2) NMT output, (3) templated output, (4) final fixed output.

image

Figure 3. Schematic overview of the first phase of our decompiler

as failed inputs. Successful translations are passed to the second phase and made available to the user.

The existence of failed inputs triggers a retraining session. The training dataset and validation dataset (used to evaluate progress during training) are updated with additional samples, and the model resumes training using the new datasets. This feedback loop, between the failed inputs and the model’s training session, drives the decompiler to improve itself and keep learning as long as it has not reached its goal. These iterations will continue until a predetermined stop condition as been met, e.g. a significant enough portion of the input statements were decompiled successfully. It also allows us to focus training on aspects where the model is weaker, as determined by the failed inputs.

The well-defined structure of programming languages allows us to make predictable and reversible modifications to both the input and output of the NMT model. These modifications are referred to as canonicalization and un-canonicalization, and are aimed at simplifying the translation problem. These steps rely on domain specific knowledge and do not exist in traditional NMT systems for natural languages. Section 4.3 motivates and describes our canonicalization methods.

Updating the Datasets After each iteration we update the dataset used for training. Retraining without doing so would lead to over-fitting the model to the existing dataset, and will be ineffective at teaching the model to handle new inputs.

We update the dataset by adding new samples obtained from two sources:

Failed translations – We compile failed translations from  Lhiдhto  Llowand use them as additional training samples. Training on these samples serves to teach the model the correct inputs for these translations, thus reducing the chances that the model will generate these translations again in future iterations.

Random samples – we generate a predetermined number of random code samples in  Lhiдh and com-pile these samples to  Llow.

The validation dataset is updated using only random samples. It is also shuffled and truncated to a constant size. The validation dataset is translated and evaluated many times during training. Thus truncating it prevents the validation overhead from increasing.

2.2.2 Second Phase: Filling the Template

The first phase of our approach produces a code template that can lead to code equivalent to the input. The goal of the second phase is to find the right values for instantiating actual code from the template. Note that the NMT model provides initial values. We need to verify that these values are correct and replace them with appropriate values if they are wrong.

This step is inspired by the common NLP practice of delexicalization [18]. In NLP, using delexicalization, some words in a sentence would be replaced with placeholders (e.g. NAME1 instead of an actual name). After translation these placeholders would be replaced with values taken directly from the input.

Similarly, we use the input statement as the source for the values needed for filling our template. Unlike delexicalization, it is not always the case that we can take a value directly from the input. In many cases, and usually due to optimizations, we must apply some transformation to the values in the input in order to find the correct value to use.

In the example of Fig. 2, the code contains two numeric values which we need to “fill” – 14 and 2. For each of this values we need to either verify or replace it. The case of 14 is relatively simple as the NMT provided a correct initial value. We can determine that by comparing 14 in the output to 14 in the original input. For 2, however, copying the value 2 from the input did not provide the correct output. Compiling the output with the value 2 would result in the instruction sall 1, %eax rather than the desired sall 2, %eax. We thus replace 2 with a variable N and try to find the right value for N. To get the correct value, we need to apply a transformation to the input. Specifically, if the input value is x, the relevant transformation for this example is  N = 2x, resulting in N = 4 that, when recompiled, yields the desired output. Therefore we replace 2 with 4, resulting in the code in Fig. 2(4).

Section 5 further elaborates on this phase and provides additional possible transformations.

Current Neural Machine Translation (NMT) models follow a sequence-to-sequence paradigm introduced in [14]. Conceptually, they have two components, an encoder and a decoder. The encoder encodes an arbitrary length sequence of tokens  x1, ...,xnover alphabet A into a sequence of vectors, where each vector represents a given input token  xiin the context in which it appears. The decoder then produces an arbitrary length sequence of tokens  y1, ...,ymfrom alphabet B, conditioned on the encoded vectors. The sequence  y1, ...,ymis generated a token at a time, until generating an end-of-sequence token. When generating the ith token, the model considers the previously generated tokens as well as the encoded input sequence. An attention mechanism is used to choose which subset of the encoded vectors to consider at each generation step. The generation procedure is either greedy, choosing the best continuation symbol at each step, or uses beam-search to develop several candidates in parallel. The NMT system (including the encoder, decoder and attention mechanism) is trained over many input-output sequence pairs, where the goal of the training is to produce correct output sequences for each input sequence. The encoder and the decoder are implemented as recurrent neural networks (RNNs), and in particular as specific flavors of RNNs called LSTM [19] and GRU [14] (we use LSTMs in this work). Refer to [30] for further details on NMT systems.

In this section we describe the algorithm of our decompilation framework using NMT. First, in Section 4.1, we describe the algorithm at a high level. We then describe the realization of operations used in the algorithm such as canonicalization (Section 4.3), the evaluation of the resulting translation (Section 4.4), and the stopping condition (Section 4.5).

4.1 Decompiler Algorithm

Our framework implements the process depicted by Fig. 3. This process is also formally described in Algorithm 1. The algorithm uses a Dataset data structure which holds pairs (x,y) of statements such that  x ∈ Lhiдh, y ∈ Llow, and y is the output of compiling x.

The framework takes two inputs: (1) a set of statements for decompilation, and (2) a compiler interface. The output is a set of successfully decompiled statements.

Decompilation starts with empty sets for training and validation and canonicalizes (Section 4.3) the input set. It then iteratively extends the training and validation sets (Section 4.2), trains a model on the new sets and attempts to translate the input set. Each translation is then recompiled and evaluated against the original input (Section 4.4 and Section 5). Successful translations are then put in a Success set, that will eventually be returned to the user. Failed translations are put in a Failed set that will be used to further extend the training set. The framework repeats these steps as long as the stopping condition was not reached (Section 4.5).

4.2 Generating Samples

To generate samples for our decompiler to train on, we generate random code samples from a subset of the C programming language. This is done by sampling the grammar of the language. The samples are guaranteed to be syntactically and grammatically correct. We then compile our code samples using the provided compiler. Doing so results in a dataset of matching pairs of statement, one in C and the other in  Lll, that can be used by the model for training and validation.

image

We note that, alternatively, we could use code snippets from publicly available code repositories as training samples, but these are less likely to cover uncommon coding patterns.

4.3 Improving Translation Performance with Canonicalization

It is possible to improve the performance of NMT models without intervening in the actual model. This can be achieved by manipulating the inputs in ways that simplify the translation problem. In the context of our work, we refer to these domain-specific manipulations as canonicalization.

Following are two forms of canonicalization used by our implementation:

movl 1234 , X1 (a) Original input movl 1 2 3 4 , X1 (b) Input after splitting numbers to digits

image

Figure 4. Reducing vocabulary by splitting numbers to digits

4.3.1 Reducing Vocabulary Size

The vocabulary size of the samples provided to the model, either for training or translating, directly affects the performance and efficiency of the model. In the case of code, a large portion of the vocabulary is devoted to numerical constants and names (such as variable names, method names, etc.).

Names and numbers are usually considered “uncommon” words, i.e. words that do not appear frequently. Descriptive variable names, for example, are often used within a single method but are not often reused in other methods. This results in a distinctive vocabulary, consisting largely of uncommon words, and leading to a large vocabulary.

We observe that the actual variable names do not matter for preserving the semantics of the code. Furthermore, these names are actually removed as part of the stripping process. Therefore, we replace all names in our samples with generic names (e.g. X1 for a variable). This allows for more reuse of names in the code, and therefore more examples from which the model can learn how to treat such names. Restoring informative descriptive names in source code is a known and orthogonal research problem for which several solutions exist (e.g. [10, 17, 32]).

Numbers cannot be handled in a similar way. Their values cannot be replaced with generic values, since that would alter the semantic meaning of the code. Furthermore, essentially every number used in the samples becomes a word in the vocabulary. Even limiting the values of numbers to some range  [1−K]would still result in K different words.

To deal with the abundance of numbers we take inspiration from NMT for natural languages. Whenever an NMT model for NL encounters an uncommon word, instead of trying to directly translate that word, it falls back to a sub-word representation (i.e. process the word as several symbols). Similarly, we split all numbers in our samples to digits. We train the model to handle single digits and then fuse the digits in the output into numbers. Fig. 4 provides an example of this process on a simple input. Using this process, we reduce the portion of the vocabulary dedicated to numbers to only 10 symbols, one per digit. Note that this reduction comes at the expense of prolonging our input sentences.

image

Figure 5. Example of code structure alignment

Alternative Method for Reducing Vocabulary Size We observe that, in terms of usage and semantic meaning, all numbers are equivalent (other than very few specific numbers that hold special meaning, e.g. 0 and 1). Thus, as an alternative to splitting numbers to digits, we tried replacing all numbers with constants (e.g. N1, N2, ...). Similarly to variable names, the purpose of this replacement was to increase reuse of the relevant words while reducing the vocabulary. When applying these replacements to our input statements, we maintained a record of all applied replacements. After translation, we used this record to restore the original values to the output.

This approach worked well for unoptimized code, but failed on optimized code. In unoptimized code there is a direct correlation between constants in high-level and low-level code. That correlation allowed us to restore the values in the output. In optimized code, compiler optimizations and transformations break that correlation, thus making it impossible for us to restore the output based on the kept record.

4.3.2 Order Transformation

Most high-level programming languages write code in-order, i.e. an operator appears between its 2 operands. On the other hand, low-level programming languages, which are ”closer” to the hardware, often use post-order, i.e. both operands appear before the operator.

The code in Fig. 5 demonstrates this difference. Fig. 5a shows a simple statement in C and Fig. 5c the x86 assembly obtained by compiling it. The different colors represent the correlation between the different parts of the computation.

Intuitively, if one was charged with the task of translating a statement, it would be helpful if both input and output shared the same order. Having a shared order simplifies ”planning” the output by localizing the dependencies to some area of the input rather than spreading them across the entire input.

Similarly, NMT models often perform better when when the source and target languages follow similar word orders, even though the model reads the entire input before generating any output. We therefore modify the structure of the C input statements to post-order to create a better correlation with the output. Fig. 5b shows the code obtained by canonicalizing the code in Fig. 5a.

After translation, we can easily parse the generated post-order code using a simple bottom-up parser to obtain the corresponding in-order code.

4.4 Evaluating Translations

We rely on the deterministic nature of compilation as the basis of this evaluation. After translating the inputs, for each pair of input i and corresponding translation t (i.e. the decompiled code), we recompile t and compare the output to i. This allows us to keep track of progress and success rates, even when the correct translation is not known in advance.

Comparing computation structure After the first step of our decompiler, the structure of computation in the decompiled program should match the one of the original program. We therefore compare the original program and the templated program from decompilation by comparing their program dependence graphs. We convert each code snippet to its corresponding Program Dependence Graph (PDG). The nodes of the graph are the different instructions in the snippet. The graph contains 2 types of edges: data dependency edges and control dependency edges. A data dependency edge from node  n1to node  n2means that  n2uses a value set by n1. A control dependency between  n1 and n2means that execution of  n2depends on the outcome of  n1. Fig. 6b shows an example of a program dependence graph for the code in Fig. 6a. Solid arrows in the graph represent data dependencies between code lines and dashed arrows represent control dependencies. Since line 2 uses the variable x which was defined in line 1, we have an arrow from 1 to 2. Similarly, line 8 uses the variable z which can be defined in either line 4 or line 6. Therefore, line 8 has a data dependency on both line 4 and line 6. Furthermore, the execution of lines 4 and 6 is dependent on the outcome of line 3. This dependency is represented by the dashed arrows from 3 to 4 and 6.

We extend the PDG with nodes “initializing” the different variables in the code. These nodes allow us to maintain a separation between the different variables.

We then search for an isomorphism between the 2 graphs, such that if nodes  n and n′ are matched by the isomorphism it is guaranteed that either 1. both n and n′correspond to variables, 2. both n and  n′correspond to numeric constants, or 3. n and  n′correspond to the

image

Figure 6. Example of Program Dependence Graph. Solid arrows for data dependencies, dashed arrows for control dependencies.

same operator (e.g. addition, substraction, branching, etc...).

If such an isomorphism exists, we know that both code snippets implement the same computation structure. The snippets might still differ in the variable or numeric constants they use. However, the way the snippets use these variables and constants is equivalent in both snippets. Thus, if we could assign the correct variables and constants to the code, we would get an identical computation in both snippets. We consider translations that reach this point as a successful template and attempt to fill the template as described in Section 5. A translation is determined fully successful only if filling the template (Section 5) is also successful.

This kind of evaluation allows us to overcome instruction reordering, variable renaming, minor translation errors and small modifications to the code (often due to optimizations).

4.5 Stopping Decompilation

Our framework terminates the decompilation iterations when 1 of 3 conditions is met:

1. Sufficient results: given a percentage threshold p, after each iteration the framework checks the number of test samples that remain untranslated and stops when at least p% of the initial test set was successfully decompiled.

2. No more progress: The framework keeps track of the amount of remaining test samples. When the framework detects that that number has not changed in x iterations, meaning no progress was made during these iterations, it terminates. Such cases highlight samples that are too difficult for our decompiler to handle

3. Iteration limit: given some number n, we can terminate the decompilation process after n iterations have finished. This criteria is optional and can be left empty, in which case only the first 2 conditions apply.

4.6 Extending the Language

An important feature of our framework is that we can focus the training done in the first phase to language features exhibited by the input. Essentially, we can start by “learning” to decompile a subset of the high-level language.

Learning to decompile some subset s of the high-level language takes time and resources. Therefore, given a new input dataset, utilizing another subset of the language  s′, we would like to reuse what we have learned from s.

Because the vocabulary of  s′is not necessarily contained in the vocabulary of s, i.e.  vocab(s′) ⊈ vocab(s), we have implemented a dynamic vocabulary extension mechanism in our framework. When the framework detects that the current vocabulary is not the same as the vocabulary used for previous training sessions, it creates a new model and partially initializes it using value from a previously trained model. This allow us to add support for new tokens in the language without starting from scratch.

Note that all tokens are equivalent in the eyes of the NMT model. Specifically, the model does not know that a variable is different from a number or an operator. It only learns a difference between the tokens from the different contexts in which they appear. Therefore, using this mechanism, we can extend the language supported by the decompiler with new operators, features and constructs, as needed. For example, starting from a subset of the language containing only arithmetic expressions, we can easily add if statements to the subset without losing any previous progress we’ve made while training on arithmetic expressions.

The extension mechanism is also used during training on a specific language subset. At each iteration, our framework generates new training samples to extend the existing training set. These new samples can, for example, contain new variables/numbers that weren’t previously part of the vocabulary, thus requiring an extension of the vocabulary.

It is important to note that in a real-world use-case we don’t expect training sessions to be frequent. Additional training should only applied when dealing with new features, a new language or with relatively harder samples than previous samples. We expect the majority of decompilation problems to be solved using an existing model.

In Section 4, we saw how the decompiler takes a low-level program and produces a high-level templated program, where some constant assignments require filling. In this section, we describe how to fill the parameters in the templated program.

5.1 Motivation

From our experimentation with applying NMT models to code, we learned that NMT performs well at generating correct code structure. We also learned that NMT has difficulties with constants and generating/predicting the right ones. This is exhibited by many cases where the proposed translation differs from an exact translation by only a numerical constant or a variable.

The use of word embeddings in NMT is a major contributor to these translation errors. A word embedding is essentially a summary of the different contexts in which that word appears. It is very common in NLP for identifying synonyms and other interchangeable words. For example, assume we have an NMT model for NLP which trains on the sentence “The house is blue”. While training, the model will learn that different colors often appear in similar contexts. The model can then generalize what it has learned from “The house is blue” and apply that to the sentence “The house is green” which it has never encountered before. In practice, word embeddings are numerical vectors, and the distance between the embeddings of words that appear in similar contexts will be smaller than the distance between embeddings of words that do not appear in similar contexts. The model itself does not operate on the actual words provided by the user. It instead translates the input to embeddings and operates on those vectors.

Since we are dealing with code rather than natural languages, we have many more “interchangeable” words to handle. During training all numerical values appear in the same contexts, resulting in very similar (if not identical) embeddings. Thus, the model is often unable to distinguish between different numbers. Therefore, while word embeddings are still useful for generalizing from training examples, using embeddings in our case results in translation errors when constants are involved.

Due to the above we have decided to treat the output of the NMT model not as a final translation but as a template that needs filling. The 1st phase of our decompilation process verifies that the computation structure resulting from recompiling the translation matches that of the input. If that is the case, any differences are most likely the result of using incorrect constants. The 2nd phase of our decompilation process deals with correcting any such false constants.

Given that the computation structure of our translation and the input is the same, errors in constants can be found in variable names and numeric values. In the first phase, as part of comparing the computation structure, we also verify that there are no cases where a variable should have been a numeric value or vice versa. That means we can treat these two cases in isolation.

We note that since we are dealing with low-level languages, in which there are often no variable names to begin with, using the correct name is inconsequential. In the case of variables, all that matter is that for each variable in the input there exists a variable in the translation that is used in exactly the same manner. This requirement is already fulfilled by matching the computation structure (Section 4.4).

5.2 Finding assignments for constants

We focus on correcting errors resulting from using wrong numeric values. Denoting the input as i, the translation as t and the result of recompiling the translation as r, there are three questions that we need to address:

Which numbers in r need to change? and to which other numbers? Since the NMT model was trained on code containing numeric values and constants, the generated translation also contains such values (generated directly by the model) and constants (due to the numeric abstraction step we describe in Section 4.3.1), and replaced with their original values. We use these numbers as an initial suggestion as to which values should be used.

As explained in Section 4.4, we compare r and i by building their corresponding program dependence graphs and looking for an isomorphism between the graphs. If such an isomorphism is found, it essentially entails a mapping from nodes in one graph to nodes in the other. Using this mapping we can search for pairs of nodes  nrand  ni such that nr ∈ ris mapped to  ni ∈ i, both nodes are numeric values, but  nr! = ni. Such nodes highlight which numbers need to be changed (nr) and to which other numbers (ni).

Which numbers in t affect which numbers in r? Note that although we know that  n ∈ ris wrong and to be fixed, we cannot apply the fix directly. Instead we need to apply a fix to t that will result in the desired fix to r. The first step towards achieving that is to create a mapping from numbers in t to numbers in r such that changing  nt ∈ tresults in a change to  nr ∈ r.

By making small controlled changes to t we can observe how r is changed. We find some number  nt ∈ t, replace it with  n′tresulting in  t ′and recompile it to get  r ′. We then compare  r and r ′ to verify that the change we made maintains the same low-level computation structure. If that is the case, we identify all number  nr ∈ r that werechanged and record those as affected by  nt.

How do we enact the right changes in t? At this point we know which number  nt ∈ twe should change and we know the target value  niwe want to have instead of  nr ∈ r. All we need to determine now is how to correctly modify  ntto end up with  ni.

The simple case is such that  nt == nr, which means whatever number we put in t is copied directly to r and thus we simply need replace  nt with ni.

However, due to optimizations (some applied even when using -O0), numbers are not always copied as is. Following are three examples we encountered in our work with x86 assembly.

Replacing numbers in conditions Assuming x is a variable of type int, given the code if (x >= 5), it is compiled to assembly equivalent to if (x > 4), which is semantically identical but is slightly more efficient.

Division/Multiplication by powers of 2 These operations are often replaced with semantically equivalent shift operations. For example, Division by 8 would be compiled as shift right by 3.

Implementing division using multiplication Since division is usually considered the most expensive operation to execute, when the divisor is known at compilation time, it is more efficient implement the division using a sequence of multiplication and shift operations. For example, calculating x/3 can be done as  (x ∗1431655766) >> 32because  1431655766 ≈ 232/3.

We identified a set of common patterns used to make such optimizations in common compilers. Using these patterns, we generate candidate replacements for  nt. Wetest each replacement by applying it to t, recompiling and checking whether the affected values  nr ∈ r are nowequal to their  ni ∈ icounterparts.

We declare a translation as successful only if an appropriate fix can be found for all incorrect numeric values and constants.

In this section we describe the evaluation of our decompilation technique and present our results.

6.1 Implementation

We implemented our technique in a framework called TraFix. Our framework takes as input an implementation of our compiler interface and uses it to build a decompiler. The resulting decompiler takes as input a set of sentences in a low-level language  Llow, translates the sentences and outputs a corresponding set of sentences in a high-level language  Lhiдh, specifically C in our implementation. Each sentence represents a sequence of statements in the relevant language.

image

Figure 7. Example of code structure alignment

Our implementation uses the NMT implementation provided by DyNmt [6] with slight modifications. DyNmt implements the standard encoder-decoder model for NMT using DyNet [31], a dynamic neural network toolkit.

Compiler Interface The compiler interface consists of a set of methods encapsulating usage of the compiler and representation specific information (e.g. how does the compiler represent numbers in the assembly?). The core of the api consists of: (1) A compile method that takes a sequence of C statements and returns the sequence of statements in  Llowresulting from compiling it (the returned code is“cleaned up”by removing parts of it that don’t contribute any useful information); and (2) An Instruction class that describes the effects of different instructions, which is used for building a PDG during translation evaluation (Section 4.4).

We implemented such compiler interfaces for compilation (1) from C to LLVM IR, and (2) from C to x86 assembly. Fig. 7 shows the result of compiling the simple C statement of Fig. 7a using both compilers.

6.2 Benchmarks

We evaluate TraFix using random C snippets sampled from a subset of the C programming language. Each snippet is a sequence of statements, where each statement is either an assignment of an expression to a variable, an if condition (with or without an else branch), or a while loop. Expressions consist of numbers, variables, binary operator and unary operators. If and while statements are composed using a condition – a relational operator between two expression – and a sequence of statements which serves as the body. We limit each sequence of statements to at most 5. Table 1 provides the formal grammar from which the benchmarks are sampled.

All of our benchmarks were compiled using the compiler’s default optimizations. Working on optimized code

image

Table 1. Grammar for experiments. Terminals are underlined

introduces several challenges, as mentioned in Section 5.2, but is crucial for the practicality of our approach. Note that we didn’t strip the code after compilation. However, our ”original” C code that we compile is already essentially stripped since our canonicalization step abstracts all names in the code.

During benchmark generation we make sure that there is no overlap between the Training dataset, Validation dataset and our Test dataset (used as input statements to the decompiler).

Evaluating Benchmarks Despite holding the ground-truth for our test set (the C used to generate the set), we decided not to compare the decompiled code to the ground-truth. We observe that, in some cases, different C statements could be compiled to the same low-level code (e.g. the statements x = x + 1 and x++). We decided to evaluate them in a manner that allows for such occurrences and is closer to what would be applied in a real use-case. We, thus, opted to evaluate our benchmarks by recompiling the decompiled code and comparing it against the input, as described in Section 4.4.

6.3 Experimental Design and Setup

We ran several experiments of TraFix. For each experiment we generated 2,000 random statements to be used as the test set. TraFix was configured to generate an initial set of 10,000 training samples and an additional 5,000 training samples at each iteration. An additional 1,000 random samples served as the validation set. There is no overlap between the test set and the training/validation sets. We decided, at each iteration, to drop half of the training samples from the previous iteration. This serves to limit the growth of the training set (and thus the training time), and assigns a higher weight to samples obtained through recent failures compared to older samples. Each iteration was limited to 2,000 epochs. In practice, our experiments never reached this limit. No iteration of our experiments with LLVM and x86 exceeded more than 140 epochs (and no more than 100 epochs when excluding the first iteration). For each test input we generated 5 possible translations using beam-search. We stopped each experiment when it has successfully translated over 95% of the test statements or when no progress was made for the last 10 iterations.

Recall that the validation set is periodically translated during training and used to evaluate training progress. TraFix is capable of stopping a training session early (before the epoch limit was reached) if no progress was observed in the last consecutive k validation sessions. Intuitively, this process detects when the model has reached a stable state close enough to the optimal state that can be reached on the current training set. In our experiments a validation session is triggered after processing 1000 batches of training samples (each batch containing 32 samples) and k was set to 10. All training sessions were stopped early, before reaching the epochs limit.

The NMT model consists of a single layer each for the encoder and decoder. Each layer consists of 100 nodes and the word embedding size was set to 300.

We ran our experiments on Amazon AWS instances. Each instance is of type r5a.2xlarge – a Linux machine with 8 Intel Xeon Platinum 8175M processors, each operating at 2.5GHz, and 64GiB of RAM, running Ubuntu 16.04 with GCC [1] version 5.4.0 and Clang [4] version 3.8.0.

We executed our experiments as a single process using only a single CPU, without utilizing a GPU, in order to mimic the scenario of running the decompiler on an end-user’s machine. This configuration highlights the applicability of our approach such that it can be used by many users without requiring specialized hardware.

6.4 Results

6.4.1 Estimating Problem Hardness

As a measure of problem complexity, we first evaluated our decompiler on several different subsets of C using only a single iteration. The purpose of these measurements is to estimate how difficult a specific grammar is going to be for our decompiler.

We used 8 different grammars for these measurement. Each grammar is building upon the previous one, meaning that grammar i contains everything in grammar  i − 1and adds a new grammar feature (the only exception is grammar 4 which does not contain unary operators). The grammars are:

1. Only assignments of numbers to variables 2. Assignments of variables to variables 3. Computations involving unary operators 4. Computations involving binary operators

image

Figure 8. Success rate of x86 decompiler after a single iteration on various grammars, with compiler optimization enabled and disabled

5. Computations involving both operators types 6. If branches 7. While loops 8. Nested branches and loops

Fig. 8 shows the success rate, i.e. percentage of successfully decompiled inputs, for the different grammars, of decompiling x86 assembly with and without compiler optimizations. Note that measured success rates are after only a single iteration of our decompilation algorithm (Section 4.1).

As can be expected, the success rate drop as the complexity of the grammar increases. That means that for more complicated grammar, our decompiler will require more iterations and/or more training data to reach the same performance level as on simpler grammars.

As can also be expected, and as can be observed from the figure, decompiling optimized code is a slightly more difficult problem for our decompiler compared to unoptimized code. Although optimizations reduce our success rate by a few percents (at most 5% in our experiments), it seems that the decisive factor for the hardness of the decompilation problem is the grammar complexity, not optimizations.

Recall that, given a compiler, our framework learns the inverse of that compiler. That means that, in the eyes of the decompiler, optimizations are “transparent”. Optimizations only cause the decompiler to learn more complex patterns than it would have learned without optimizations, but don’t increase the number of patterns learned nor the vocabulary handled. Grammar complexity, on the other hand, increases both the number and complexity of the patterns the decompiler needs to learn and handle, and the vocabulary size, thus making the decompilation task much harder to learn.

We emphasize that enabling/disabling compiler optimizations in our framework required no changes to the

image

Table 2. Statistics of iterative experiments of LLVM IR

framework. The only change necessary was adding the appropriate flags in the compiler interface.

6.4.2 Iterative Decompilation

In our second set of experiments we allowed each experiment to execute iteratively to observe the effects of multiple iterations on our decompilation success rates.

We implemented and evaluated 2 instances of our framework: from LLVM IR to C, and from x86 assembly to C.

We ran each experiments 5 times using the configuration described in Section 6.3. We allowed each experiment to run until it reached either a success rate of95% or 6 iterations. The results reported below are averaged over all 5 experiments.

Decompiling LLVM IR Out of the 5 experiments we conducted using our LLVM IR instance, 3 reached the goal of 95% success rate after a single iteration. The other 2 experiments required one additional iteration to reach that goal. Table 2 reports average statistics for these two iterations. The columns epochs, train time and translate time report averages for each iteration (i.e. average of measurements from 5 experiments for the 1st iteration and from only 2 experiments for the 2nd iteration). The successful translations column reports the overall success rate, not just the successes in that specific iteration.

The statistics in the table demonstrate that our LLVM decompiler performed exceptionally well, even though it was decompiling optimized code snippets (which are traditionally considered harder to handle).

On average, Our LLVM experiments successfully decompiled 97% of the benchmarks, before autonomously terminating. These include benchmarks consisting of up to 845 input tokens and 286 output tokens. We intentionally set the goal lower than 100%. Setting it higher than 95% and allowing our instances to run for further iterations would take longer but would also lead to a higher overall success rate.

The timing measurements reported in the table highlight that the majority of execution time is spent on training the NMT model. Translation is very fast, taking only a few seconds per input, as witnessed by the first iteration. The execution time of our translation evaluation (including parsing each translation into a PDG, comparing with the input PDG, and attempting to fill the templates correlating to the translations) is extremely

image

Table 3. Statistics of iterative experiments of x86 asembly

image

Figure 9. Cummulative success rate of x86 decompiler as a function of how many iteration the decompiler performed

low, taking only a couple of minutes for the entire set of benchmarks.

These observations are important due to the expected operating scenario of our decompiler. We expect the majority of inputs to be resolved using a previously trained model. Retraining an NMT model should be done only when the language grammar is extended or when significantly difficult inputs are provided. Thus, in normal operations, the execution time of the decompiler, consisting of only translation and evaluation, will be mere seconds.

Decompiling x86 Assembly Table 3 provides statistics of our x86 experiments. All of these experiments terminated when they reached the iterations limit which was set to 6.

Fig. 9 visualizes the successful translations column. The figure plots our average success rate as a function of the number of completed iterations. It is evident that with each iteration the success rate increases, eventually reaching over 88% after 6 iterations. Overall, our decompiler successfully handled samples of up 668 input tokens and 177 output tokens.

Our decompilation success rates on x86 were lower than that of LLVM, terminating at around 88%. This correlates with the nature of x86 assembly, which has smaller vocabulary than that of LLVM IR. The smaller vocabulary shortens overall training times, but also results in longer dependencies and meaningful patterns that are harder to deduce and learn.

We note that, in case of a traditional decompiler, bridging the remaining gap of 13% failure rate would require a team of developers crafting additional rules and patterns. Using our technique this can be achieved by allowing the decompiler to train longer and on more training data.

7.1 Limitations

Manual examination of our results from Section 6.4 revealed that currently our main limitation is input length. There was no threshold such that inputs longer than the threshold would definitely fail. We observed both successful and failed long inputs, often of the same length. We did however observe a correlation between input length and a reduced success rate. As the length of an input increases, it becomes more likely to fail.

We found no other outstanding distinguishing features, in the code structure or used vocabulary, that we could claim are a consistent cause of failures.

This limitation stems from the NMT model we used. long inputs are a known challenge for existing NMT systems [26]. NMT for natural languages is usually limited to roughly 60 words [26]. Due to nature of code (i.e. limited vocabulary, limited structure) we can handle inputs much longer than typical natural language sentences (668 words for x86 and 845 words for LLVM ). Regardless, this challenge also applies to us, resulting in poorer results when handling longer inputs. As the field of NMT evolves to better handle long inputs, so would our results improve.

To verify that this limitation is not due to our specific implementation, we created another variant of our framework. This new variant is based on TensorFlow [5, 8] rather than DyNet. Experimenting with this variant, we got similar results as those reported in Section 6.4, and ultimately reached the same conclusion — the observed limitation on input length is inherent to using NMT.

7.1.1 Other Decompilation Failures

Though we do not consider this a limitation, another aspect that could be improved is our template filling phase. Our manual analysis identified some possibilities for improving our second phase – the template filling phase (Section 5).

The first type of failure we have observed is the result of constant folding – a compiler optimization that replaces computations involving only constants with their results. Fig. 10 demonstrates this kind of failure. Given the C code in Fig. 10a, the compiler determines that  63∗5

image

Figure 10. Example of decompilation failure

image

Figure 11. Failure due to redundant number

can be replaced with 315. Therefore, the x86 assembly in Fig. 10b contains the constant 315. Using the code of Fig. 10b as input, our decompiler suggests the C code in Fig. 10c.

Note that the decompiler suggested code that is identical in structure to the input. The first phase of our decompiler handled this example correctly, resulting in a matching code template. The failure occured in the second phase, in which we were unable to find the appropriate numerical values. This failure occurs because our current implementation attempts to find a value for each number independently from other numbers in the code. Essentially, this resulted in floating-point numbers which were deemed unacceptable by the decompiler because our benchmarks use only integers.

This kind of failure can be mitigated by either (1) applying constant folding to the high-level decompiled code, (2) allowing the template to be filled with floating point numbers (which was disabled since the benchmarks contained only integers), or (3) encoding the code as constraints and using a theorem prover to find appropriate assignments to constants.

A similar example is found in Fig. 11. We left the suggested translation in this example as constants to simplify the example. One can see that the suggested translation in Fig. 11b is structurally identical to the expected output in Fig. 11a, up to the addition of N11. This example was not considered a matching code template by our implementation, because any value for N11 other than 0 results in a different computation structure. However, if N11 = 0, we get an exact match between the suggested translation and the expected output. Using a

image

Figure 12. Failure due to incorrect operator

theorem prover based template filling algorithm could detect that and assign the appropriate values to the constants, including N11, resulting in equivalent code.

Fig. 12 shows another kind of failure. In this example the difference between the expected output and suggested translation is a + that was replaced with  −. Currently only variable names and numeric constants are treated as template parameters. This kind of difference can be overcome by considering operators as template parameters as well. Since the number of options for each operator type (unary, binary) is extremely small, we could try all options for filling these template parameters.

7.2 Framework Tradeoffs

There are a few tradeoffs that should be taken into account when using our decompilation framework:

Iterations limit – Applying an iterations limit allows to tradeoff decompilation success rates for a shortened decompilation time and would make sense in environments with limited resources (time, budget, etc.). On the other hand, setting the limit too low will prevent the decompiler from reaching its full potential and will result in low successful translations rate.

Training set size – In our experiments we initialized the training set to 10,000 random samples and generated additional 5,000 new random samples each iteration. As we increase the training set size, so do the training time and memory consumption increase. Using too many initial training samples would be wasteful in case of relatively simple test samples, in which a shorter training session, with fewer training samples, might suffice. On the other hand, using too few samples would result in many training sessions when dealing with harder test samples. This is also applicable when setting the number of random samples added at each iteration. Furthermore, rather than always generating a constant number of samples, one can dynamically decide the number of samples to generate based on some measure of progress (i.e. generate fewer samples when progressing at a higher rate).

Patience – the patience parameter determines how many iterations to wait before terminating due to not observing any progress. Setting this parameter

to high would result in wasted time. This is because any training performed since the last time we observed progress would essentially have been in vain. On the other hand, it is possible for the model to make no progress for a few iterations only to resume progressing once it generates the training samples it needed. Setting the patience parameter too low might cause the decompiler to stop before it can reach its full potential.

7.3 Extracting Rules

As mentioned in Section 1, traditional decompilers rely heavily on pattern matching. Development of such decompilers depends on hand-crafted rules and patterns, designed by experts to detect specific control-flow structures. Hand-crafting rules is slow, expensive and cumbersome. We observe that the successful decompilations produced by our decompiler can be re-templatized to form rules that can be used by traditional decompilers, thus simplifying traditional decompiler development. Appendix A provides examples of such rules.

7.4 Evaluating Readability

Measuring the readability of our translations requires a user study, which we did not perform. However, note that given some training set, a model trained on that set will generate code that is similar to what it was trained on. Thus, the readability of our translations stems from the readability of our training samples. Our translations are as readable as the training samples we generated. This was also verified by an empirical sampling of our results. Therefore, given readable code as training samples, we can surmise that any decompiled code we generate and output will also be readable.

Decompilation The Hex-Rays decompiler [2] was considered the state of the art in decompilation, and is still considered the de-facto industry standard. Schwartz et al. [34] presented the Phoenix decompiler which improved upon Hex-Rays using new analysis techniques and iterative refinement, but was still unable to guarantee goto-free code (since goto instructions are rarely used in practice, they should not be part of the decompiler output). Yakdan et al. [36, 37] introduced Dream, and its predecessor Dream++, taking a significant step forward by guaranteeing goto-free code. RetDec [7], short for Retargetable Decompiler, is an open-source decompiler released in December 2017 by Avast, aiming to be the first ”generic” decompiler capable of supporting many architectures, languages, ABIs, etc.

While previous work made significant improvements to decompilation, all previous work fall under the title of rule-based decompilers. Rule-based decompilers require manually written rules and patterns to detect known control-flow structures. These rules are very hard to develop, prone to errors and usually only capture part of the known control-flow structures. According to data published by Avast, it took a team of 24 developers 7 years to develop RetDec. This data emphasizes that traditional decompiler development is extremely difficult and time consuming, supporting our claim that the future of decompilers lies in approaches that can avoid this step. Our technique removes the burden of rule writing from the developer, replacing it with an automatic, neural network based approach that can autonomously extract relevant patterns from the data.

Katz et al. [23] suggested the first technique to use NMT for decompilation. While they set out to solve the same problem, in practice they provide a solution to a different and significantly easier problem - producing source-level code that is readable, without any guarantees for equivalence, not semantic or even syntactic. Further, the code they generate is not guaranteed to compile (and does not in practice). Because their code does not compile nor is equivalent, if you apply our evaluation criteria to their results, their accuracy would be at most 3.8%. Further, beyond the cardinal difference in the problem itself, they have the following limitations:

They can only operate on code compiled with a special version of Clang which they modified for their purposes.

All of their benchmarks are compiled without optimizations. We apply the compiler’s default optimizations to all of our benchmarks.

They limit their input to 112 tokens and output to 88 tokens. This limits their input to single statements. We successfully decompiled x86 benchmarks of up to 668 input tokens and 177 output tokens. Each of our samples contains several statements.

Their methodology is flawed as they do not control for overlaps between the training and test sets. We verify that there is no such overlap in our sets.

Modeling Source Code Modeling source code using various statistical models has seen a lot of interest for various applications.

Srinivasan et al. [21] used LSTMs to generate natural language descriptions for C# source code snippets and SQL queries. Allamanis et al. [11] generated descriptions for Java source code using convolutional neural networks with attention. Hu et al. [20] tackled the same problem by neural networks with a structured based traversal of the abstract syntax tree, aimed at better representing the structure of the code. Loyola et al. [28] took a similar approach for generating descriptions of changes in source code, i.e. translates commits to source code repositories to commit messages. The success presented by these papers highlights that neural networks are useful for summarizing code, and supports the use of neural networks for decompilation.

Another application of source code modeling is for predicting names for variable, methods and classes. Raychev et al. [33] used conditional random fields (CRFs) to predict variable names in obfuscated JavaScript code. He et al. [17] also used CRFs but for the purpose of predicting debug information in stripped binaries, focusing on names and types of variables. Allamanis et al. [9] used neural language models to predict variable, method and class names. Allamanis et al. relied on word embeddings to determine semantically similar names. We consider this problem as orthogonal to our own. Given a semantically equivalent source code produced by our decompiler, these techniques could be used to supplement it with variable names, etc.

Chen et al. [15] used neural networks to translate code between high-level programming languages. This problem resembles that of decompilation, but is infact simpler. Translating low-level languages to high-level languages, as we do, is more challenging. The similarities between high-level languages are more prevalent than between high-level and low-level languages. Furthermore, translating source code to source code directly bypasses many challenges added by compilation and optimizations.

Levy et al. [27] used neural networks to predict alignment between source code and compiled object code. Their results can be useful in improving our second phase, i.e. filling the template and correcting errors. Specifically, their alignment prediction can be utilized to pinpoint location in the source code that lead to errors.

Katz et al. [24, 25] used statistical language models for modeling of binary code and aspects of program structure. Based on a combination of static analysis and simple statistical language models they predict targets of virtual function calls [24] and inheritance relations between types [25]. Their work further highlights that these techniques can deduce high-level information from low-level representation in binaries.

We address the problem of decompilation — converting low-level code to high-level human-readable source code. Decompilation is extremely useful to security researchers as the cost of finding vulnerabilities and understanding malware drastically drops when source code is available.

A major problem of traditional decompilers is that they are rule-based. This means that experts are needed for hand-crafting the rules and patterns used for detecting control-flow structures and idioms in low-level code and lift them to source level. As a result decompiler development is very costly.

We presented a new approach to the decompilation problem. We base our decompiler framework on neural machine translation. Given a compiler, our framework automatically learns a decompiler from it. We implemented an instance of our framework for decompiling LLVM IR and x86 assembly to C. We evaluated these instances on randomly generated inputs with a high success rates.

[1] 1987. GCC, the GNU Compiler Collection. https://gcc.gnu. org/.

[2] 1998. The IDA Pro disassembler and debugger. http://www. hex-rays.com/idapro/.

[3] 2002. The LLVM compiler infrastructure project. http: //llvm.org.

[4] 2007. clang: a C language family frontend for LLVM. https: //clang.llvm.org/.

[5] 2015. TensorFlow. https://www.tensorflow.org.

[6] 2017. DyNMT, a DyNet based nueral machine transaltion. https://github.com/roeeaharoni/dynmt-py.

[7] 2017. Retargetable Decompiler. https://retdec.com/.

[8] Mart´ın Abadi, Paul Barham, Jianmin Chen, Zhifeng Chen, Andy Davis, Jeffrey Dean, Matthieu Devin, Sanjay Ghemawat, Geoffrey Irving, Michael Isard, Manjunath Kudlur, Josh Levenberg, Rajat Monga, Sherry Moore, Derek Gordon Murray, Benoit Steiner, Paul A. Tucker, Vijay Vasudevan, Pete Warden, Martin Wicke, Yuan Yu, and Xiaoqiang Zhang. 2016. TensorFlow: A system for large-scale machine learning. (2016).

[9] Miltiadis Allamanis, Earl T. Barr, Christian Bird, and Charles Sutton. 2015. Suggesting Accurate Method and Class Names. In Proceedings of the 2015 10th Joint Meeting on Foundations of Software Engineering.

[10] Miltiadis Allamanis, Earl T. Barr, Christian Bird, and Charles Sutton. 2015. Suggesting Accurate Method and Class Names. In Proceedings of the 10th Joint Meeting on Foundations of Software Engineering.

[11] Miltiadis Allamanis, Hao Peng, and Charles A. Sutton. 2016. A Convolutional Attention Network for Extreme Summarization of Source Code. CoRR (2016). http://arxiv.org/abs/ 1602.03001

[12] Miltiadis Allamanis, Daniel Tarlow, Andrew D. Gordon, and Yi Wei. 2015. Bimodal Modelling of Source Code and Natural Language. In Proceedings of the 32Nd International Conference on International Conference on Machine Learning -Volume 37.

[13] Matthew Amodio, Swarat Chaudhuri, and Thomas W. Reps. 2017. Neural Attribute Machines for Program Generation. CoRR (2017). http://arxiv.org/abs/1705.09231

[14] Dzmitry Bahdanau, Kyunghyun Cho, and Yoshua Bengio. 2014. Neural Machine Translation by Jointly Learning to Align and Translate. CoRR (2014).

[15] Xinyun Chen, Chang Liu, and Dawn Song. 2018. Tree-to-tree Neural Networks for Program Translation. CoRR (2018).

[16] KyungHyun Cho, Bart van Merrienboer, Dzmitry Bahdanau, and Yoshua Bengio. 2014. On the Properties of Neural Machine Translation: Encoder-Decoder Approaches. CoRR (2014).

[17] Jingxuan He, Pesho Ivanov, Petar Tsankov, Veselin Raychev, and Martin Vechev. 2018. Debin: Predicting Debug Information in Stripped Binaries. In Proceedings of the ACM SIGSAC

Conference on Computer and Communications Security.

[18] Matthew Henderson, Blaise Thomson, and Steve J. Young. 2014. Robust dialog state tracking using delexicalised recurrent neural networks and unsupervised adaptation. IEEE Spoken Language Technology Workshop (2014).

[19] Sepp Hochreiter and J˜Aijrgen Schmidhuber. 1997. Long ShortTerm Memory. Neural Computation (1997).

[20] Xing Hu, Yuhan Wei, Ge Li, and Zhi Jin. 2017. CodeSum: Translate Program Language to Natural Language. CoRR (2017). http://arxiv.org/abs/1708.01837

[21] Srinivasan Iyer, Ioannis Konstas, Alvin Cheung, and Luke Zettlemoyer. 2016. Summarizing Source Code using a Neural Attention Model. In Proceedings of the 54th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers).

[22] Nal Kalchbrenner and Phil Blunsom. 2013. Recurrent Continuous Translation Models. In Proceedings of the Conference on Empirical Methods in Natural Language Processing.

[23] D. S. Katz, J. Ruchti, and E. Schulte. 2018. Using recurrent neural networks for decompilation. In IEEE 25th International Conference on Software Analysis, Evolution and Reengineering.

[24] Omer Katz, Ran El-Yaniv, and Eran Yahav. 2016. Estimating Types in Binaries Using Predictive Modeling. In Proceedings of the 43rd Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages.

[25] Omer Katz, Noam Rinetzky, and Eran Yahav. 2018. Statistical Reconstruction of Class Hierarchies in Binaries. In Proceedings of the Twenty-Third International Conference on Architectural Support for Programming Languages and Operating Systems.

[26] Philipp Koehn and Rebecca Knowles. 2017. Six Challenges for Neural Machine Translation. (2017).

[27] Dor Levy and Lior Wolf. 2017. Learning to Align the Source Code to the Compiled Object Code. In Proceedings of the 34th International Conference on Machine Learning.

[28] Pablo Loyola, Edison Marrese-Taylor, and Yutaka Matsuo. 2017. A Neural Architecture for Generating Natural Language Descriptions from Source Code Changes. CoRR (2017). http: //arxiv.org/abs/1704.04856

[29] Chris J. Maddison and Daniel Tarlow. 2014. Structured Generative Models of Natural Source Code. CoRR (2014). http://arxiv.org/abs/1401.0514

[30] Graham Neubig. 2017. Neural Machine Translation and Sequence-to-sequence Models: A Tutorial. CoRR (2017).

[31] Graham Neubig, Chris Dyer, Yoav Goldberg, Austin Matthews, Waleed Ammar, Antonios Anastasopoulos, Miguel Ballesteros, David Chiang, Daniel Clothiaux, Trevor Cohn, Kevin Duh, Manaal Faruqui, Cynthia Gan, Dan Garrette, Yangfeng Ji, Lingpeng Kong, Adhiguna Kuncoro, Gaurav Kumar, Chaitanya Malaviya, Paul Michel, Yusuke Oda, Matthew Richardson, Naomi Saphra, Swabha Swayamdipta, and Pengcheng Yin. 2017. DyNet: The Dynamic Neural Network Toolkit. arXiv preprint arXiv:1701.03980 (2017).

[32] Veselin Raychev, Martin Vechev, and Andreas Krause. 2015. Predicting Program Properties from ”Big Code”. In Proceedings of the 42nd Annual Symposium on Principles of Pro- gramming Languages (POPL ’15).

[33] Veselin Raychev, Martin Vechev, and Andreas Krause. 2015. Predicting Program Properties from ”Big Code”. In Proceedings of the 42Nd Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages.

[34] Edward J. Schwartz, JongHyup Lee, Maverick Woo, and David Brumley. 2013. Native x86 Decompilation Using Semanticspreserving Structural Analysis and Iterative Control-flow Structuring. In Proceedings of the 22Nd USENIX Conference on Security.

[35] Ilya Sutskever, Oriol Vinyals, and Quoc V. Le. 2014. Sequence to Sequence Learning with Neural Networks. In Advances in Neural Information Processing Systems 27: Annual Conference on Neural Information Processing System.

[36] K. Yakdan, S. Dechand, E. Gerhards-Padilla, and M. Smith. 2016. Helping Johnny to Analyze Malware: A UsabilityOptimized Decompiler and Malware Analysis User Study. In IEEE Symposium on Security and Privacy (SP).

[37] Khaled Yakdan, Sebastian Eschweiler, Elmar GerhardsPadilla, and Matthew Smith. 2015. No More Gotos: Decompilation Using Pattern-Independent Control-Flow Structuring and Semantic-Preserving Transformations. In 22nd Annual Network and Distributed System Security Symposium, NDSS.

image


Designed for Accessibility and to further Open Science