Techniques for Malware Analysis based on Symbolic Execution
Techniques for Malware Analysis based on Symbolic Execution
Supervisors Candidate
July, 2020
Summary
The landscape of malicious software, more commonly known as malware, grows
every year in number, popularity and financial damage. Just like we see happening
in the software industry, organizations in the cybercrime world are well coordi-
nated: they hire developers, distributors, maintainers, they advertise their product,
offering deployment services to paying customers and channels to signal bugs to fix.
Manually examining every potentially malicious executable would be unfeasible to
the least, therefore turning towards automated, fast analysis systems is becoming
more and more a requirement to be efficient in the industry and offer meaningful
results. To interfere with such automated techniques, malware developers will
often hide meaningful routines activated only if certain conditions in the execution
environment are met. These, in the literature called trigger conditions, become a
great obstacle in automated analysis systems: specific dates, directory names or
network commands that would expose the malicious nature of a sample will not
most likely be triggered in a generic execution context without prior knowledge
of their expected values, therefore leading to false negatives and, in general, to a
decrease of the analysis coverage. Consequently, designing systems to expose hidden
trigger conditions has drawn some interest in the reverse engineering and malware
analysis literature. This thesis introduces Symba, a prototype based on symbolic
execution that attempts to reveal trigger conditions in executables. Symbolic
execution, precisely, is a software analysis technique which has been introduced in
the literature a few decades of years ago but only recently – thanks to an increasing
attention from the scientific community – is being practically adopted. Its rationale
is to transform a binary executable into a set of symbols and equations binding
them, which can be at any time mathematically solved to query the executable for
interesting properties. In this work, we specifically resort to symbolic execution to
handle the problem posed by trigger conditions. By extracting these conditions
from both proof-of-concept and real world samples, consequently observing new
paths of execution revealed in automated systems, we demonstrated how applying
new analysis techniques, such as symbolic execution, on malware analysis can push
a step forward towards more intelligent systems.
ii
Acknowledgements
I wish to express my deepest gratitude to the people who helped me during the
development of this thesis. To Guglielmo Morgari, who relentlessly sit with me
countless times, listening, supporting, kindly introducing new precious ideas in my
flow of thought with the clarity and passion that has the shape of genius. Thanks to
Emanuele De Lucia, who, in the most friendly and competent way, introduced me
to the concepts of malware analysis and cybercrime research. I am truly indebted
with Fabrizio Vacca, who gave me the opportunity of working with the wonderful
laboratory guys at Telsy, where I had the privilege of growing from both a human
and professional perspective. My most sincere appreciation and admiration goes to
professor Antonio Lioy, who encouraged me towards finding a research direction
that could truly motivate me, something that would give a contribution, for how
small, to our community. A special regard goes to Cataldo Basile, who supported
me in so many ways that I could not count them. Thanks for giving me your
continuous guidance, assistance and advice, in a way that I truly feel grateful for.
Thanks to my teammates at PolitHack, where I found the motivation to start
tinkering with the amazing angr framework. I wish to acknowledge the constant
love and support of my family. To my dad and my mother, Carmelo and Gabriella,
and my two sisters, Rossana and Caterina: I would not have been able to reach
this point without you, and your tireless help and kind words. To my friends, for
being there, helping me with a beer and a chat when the road got tougher. To
Chiara, all my love and gratitude. You make every day my best day, even when I
spend it analysing disassembled and messy malware.
iii
Table of Contents
1 Introduction 1
1.1 Contribution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 Outline . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2 Requirements 6
2.1 Problem Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.2 Proposed Solution . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.3 Expected Outcomes . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3 Background 9
3.1 Windows System . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3.1.1 Windows API . . . . . . . . . . . . . . . . . . . . . . . . . . 9
3.1.2 Windows API example . . . . . . . . . . . . . . . . . . . . . 10
3.2 Windows Malware . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.2.1 Classification . . . . . . . . . . . . . . . . . . . . . . . . . . 11
3.3 Dynamic and static analysis . . . . . . . . . . . . . . . . . . . . . . 13
3.3.1 Static Analysis . . . . . . . . . . . . . . . . . . . . . . . . . 13
3.3.2 Dynamic Analysis . . . . . . . . . . . . . . . . . . . . . . . . 14
3.3.3 Advantages and Disadvantages . . . . . . . . . . . . . . . . 17
3.4 Symbolic Execution . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.4.1 Components . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.4.2 Weaknesses . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
5 Architecture design 42
5.1 Configurator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
5.2 Model Injector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
5.3 States Extractor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
5.4 TriggerSeer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
5.5 Symbolic Explorator . . . . . . . . . . . . . . . . . . . . . . . . . . 49
5.6 Solver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
6 Implementation details 52
6.1 GenericModel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
6.2 SymbaConfig . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
6.3 CFGFast . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
6.4 Exploration Techniques . . . . . . . . . . . . . . . . . . . . . . . . . 55
6.5 Simulation Manager . . . . . . . . . . . . . . . . . . . . . . . . . . 57
7 Evaluation 59
7.1 Proof of Concept . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
7.2 Paranoid fish . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
7.3 Wrathrage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
7.4 Limitations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
7.4.1 OS interaction . . . . . . . . . . . . . . . . . . . . . . . . . . 66
7.4.2 Chained triggers . . . . . . . . . . . . . . . . . . . . . . . . 67
7.4.3 Testing samples . . . . . . . . . . . . . . . . . . . . . . . . . 67
8 Related Work 69
8.1 Symbolic Execution . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
8.2 Malware Trigger Analysis . . . . . . . . . . . . . . . . . . . . . . . 70
9 Conclusions 71
9.1 Future work . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
A Code 73
Bibliography 78
vi
List of Figures
3.1 Interface of the reverse engineering tool open sourced by the NSA,
Ghidra. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3.2 Flow Graph generated by radare2 framework. . . . . . . . . . . . . 15
3.3 Customizable monitoring with Sysinternals procmon. . . . . . . . . 16
3.4 List of signatures extracted by cuckoo for a Poweliks sample. Color
indicates degree of suspiciousness. . . . . . . . . . . . . . . . . . . . 17
3.5 Diagram of how the CFG is transformed after the process of flat-
tening (http://tigress.cs.arizona.edu/transformPage/docs/
flatten/index.html). . . . . . . . . . . . . . . . . . . . . . . . . . 20
3.6 Symbolic Execution example flow, from [27] . . . . . . . . . . . . . 23
viii
Chapter 1
Introduction
Keywords: cyber security, malware, analysis, automatic malware analysis, threat
intelligence, symbolic execution
have to understand how the code is obtaining persistence in the target machine,
and which techniques are employed to defeat known analysis approaches. In short
time, they have to write a detailed report of the sample, a dissection of its features.
This kind of report is extremely valuable.
• Other malware analysts in the network will learn how to handle that family
or similar ones.
It goes without saying that these kind of detailed analysis and reports of
malware samples are not performed in a few minutes. Depending on the degree of
sophistication of samples, these tasks can require from different hours to even days
of work. In certain cases this is completely acceptable. For instance, a malware
analyst tasked with analysing complex APT samples developed by threat actors
like Fancy Bear [1] will not probably "see" multiple samples per day. However, for
companies operating in the corporate and business world, gathering and processing
data from samples being delivered daily in email attachments, honeypots, Point Of
Sales stations, the requirements are much different. It is enough to research how
many samples an engine like Virus Total processes per day to shape an idea of the
flow of this market.
How can companies without thousands of analysts cope with this volume of
samples? In the past years we have withstood an increasing attention in automation
techniques applied to malware analysis. These can be categorized as static or
dynamic, each one with their perks and disadvantages. Static analyses have a good
coverage and do not need any kind of target environment to execute the malware
in. Obfuscation and anti-reversing techniques make this kind of analysis harder,
even when scripts and not simple human eyes are used as tools of the trade. On
the other hand, dynamic techniques are inherently more capable to observe the
behaviour of code, as is the case with malware sandboxes technologies. A sandbox
is a controlled environment where malicious code can "explode" and every action
it performs on the machine is logged. File system operation, network traffic, API
calls, registry keys queried and written, everything is monitored to compute a
detailed behavioural report where interesting properties of malware samples can be
extracted and processed without human interaction.
However, modern malware will often use various approaches to detect when they
are being executed in a controlled environment and not a simple victim machine
2
Introduction
1.1 Contribution
This thesis brings the following contributions to the field of Windows Malware
Analysis.
2. We describe some novel techniques to generate and inject the potential inputs
3
Introduction
that are then chained in order to extract properties from malicious sample, to
construct the correct environment where malware has to be executed.
3. We evaluate and demonstrate the capability of Symba to extract such properties
from real malware samples and benchmarking code typically used to measure
the degree of detectability of an environment.
1.2 Outline
The rest of this thesis is organized as follows.
• Chapter 2 states the requirements of this work. Namely, the specific problem
that we identified, the ideas designed to solve it, the tools used to implement
a system matching the design, and the approach used to verify the validity of
such implementation.
• Chapter 3 gives the reader a comprehensive background of the theoretical and
technical concepts handled in this thesis. Ranging from the environment where
we have worked, the Windows OS, to known malware analysis techniques
using both static and dynamic approaches. Finally, an academic overview of
symbolic execution will be presented.
• Chapter 4 explores the capabilities of the angr framework, the symbolic exe-
cution engine employed for this thesis. Each section will highlight different
features of this library, including examples of their use aimed to facilitate
reading.
• Chapter 5 describes the design of Symba, first in its entirety, that is, how
components interact with each other. Subsequentially, every component is
"disassembled", and its role in the workflow is thoroughly characterized.
• Chapter 6 contains some details about the specific implementation of Symba.
It incorporates some notable snippets of code extracted from the tool, alongside
a few comments on their inner mechanisms.
• Chapter 7 presents the case studies where Symba has been evaluated. It
starts with a proof of concept RAT server developed by following documented
malware development techniques. Then, Symba is launched against a highly
starred project in GitHub that simulates known and less known techniques
that a malware would use to detect virtualization, Paranoid Fish. Afterwards,
Symba is tested on a real malware for which the source code has been leaked
on GitHub, named Wrathrage. The chapter is concluded with a depiction of
the system limitations in its design and implementation.
4
Introduction
• Chapter 8 describes some other works, consulted from the available literature,
that handle the problem of solving trigger conditions, working with Windows
malware and using symbolic execution.
• Chapter 9 concludes the thesis with a glance towards the future researches
that could be conducted in order to extend this piece of work.
5
Chapter 2
Requirements
• Checking the system date in malware that only activate their routines in
specific days and months.
There are two main issues with trigger conditions. First of all, the routines
hidden behind triggers are often essentially what characterize the executable as
malicious. In scenarios where analysts execute the sample to monitor its behaviour
and determine the degree of suspiciousness, without the knowledge of those triggers,
they would not notice anything malicious in the report of execution, with the risk
of falsely classifying a malware as benign.
Moreover, in situations where the examined sample has already been determined
to be a malware, revealing triggers is not a simple task. With dynamic analysis
techniques, there is a high change that the generic environment where the malware
is monitored will not satisfy any of the conditions. Turning to even more dynamic
6
Requirements
techniques like fuzzing the malware input to observe new behaviour could be useless.
Let us take the example of a server with a particular command suite that listens for
packets on the network. The chance that, by fuzzing network input to the binary,
one of the commands in the suite is found, has the same statistical features of a
complete bruteforcing attack on a password, which as known is generally limited in
effectiveness.
Therefore, extracting triggers often turns out to be a manual, time-consuming
static approach. The analyst needs to find the correct spots where a condition is
checked for, disassembling and figuring out the meaning of all the checks, even
when obfuscated between different instructions, and solve them manually. Even in
simple scenarios, where the analyst is able to extract the triggers in no more than
a couple of hours, such approach becomes impractical when applied to hundreds of
samples per day.
With these premises, we believe that the process of finding and solving triggers
in potentially malicious executables needs to be pushed towards automation.
8
Chapter 3
Background
• Base services.
• Security.
• Graphics.
• User Interface.
9
Background
• Multimedia.
• Windows shell.
• Networking.
The Base services exposes fundamental interface on the OS like file system,
processes, threads or registry handling. Security API provides programming
elements for authentication, authorization and other security related prototypes.
The Graphics subsystem provides functionality to show graphical content on
monitors, printers and generic output devices. The User Interface can be used to
create windows and controls. Multimedia offers the capability to work with video,
sound and input devices. Windows shell interface allows applications to execute
commands in the context of an OS shell. Finally, Network services provides access
to the network capabilities of the host.
The official reference for the API is the MSDN (Microsoft Developer Network),
while the bone and meat implementation of the API specifications is located in
Windows DLLs.
1 v o i d GetSystemTime (
2 LPSYSTEMTIME lpSystemTime
3 );
1 t y p e d e f s t r u c t _SYSTEMTIME {
2 WORD wYear ;
3 WORD wMonth ;
4 WORD wDayOfWeek ;
5 WORD wDay ;
6 WORD wHour ;
10
Background
7 WORD wMinute ;
8 WORD wSecond ;
9 WORD w M i l l i s e c o n d s ;
10 } SYSTEMTIME, ∗PSYSTEMTIME, ∗LPSYSTEMTIME;
The listing is clear and states which words will contain the different fields returned
by the operating system. Following the same reasoning, we lookup functions to
display message boxes in the Windows OS, and we encounter MessageBox. The
documentation of this function follows:
1 i n t MessageBox (
2 HWND hWnd,
3 LPCTSTR lpText ,
4 LPCTSTR lpCaption ,
5 UINT uType
6 );
3.2.1 Classification
Malware is a generic umbrella term used to describe many kinds of malicious,
hostile software. It can be used by cybercrime industry in order to gain money,
in APT [5] campaigns, both in simulated attacks (Red Teaming Operations) and
state actors [6] to damage or control target systems. It can be used by hacktivist
groups or as a vector of revenge towards enemies. In this wide context, researchers
11
Background
developed some criteria to classify [7] [8] [9] the multiple types of malware in several
ways. These criteria include:
• The delivery method and attack methodology. Victims can get infected simply
by visiting an evil or compromised website - drive-by distribution - or via
attachment in phishing emails that trick victim into executing them. Man-in-
the-Middle attacks where the content is injected into traffic packets, or using
exploits targeting browsers, in what is called Exploit kit [10].
• The goal or objective. Different types of malware have different scopes. Some
may have purely financial goals, while others are deployed in order to control
remotely the target machine, or exfiltrate sensitive data.
• The platform that the malware targets. Operating system, architecture, even
specific builds.
• Approach to stealth. How does the malware attempt to hide itself from user
or AV detection?
• Bankers. This usually sophisticated type of malware will hook specific APIs
or DLLs used by Web Browsers in order to inject custom content or exfiltrate
information when the unknowing user visits banking web apps. The goal is
financial, to extract card information or hijack accounts.
• Remote Access Trojans. Once infected by a RAT, the machine will establish
a tunnel on the Internet towards a Command & Control server, expecting
commands to execute on the host, such as exfiltrating data or pivoting further
in the network of the victim.
• Rootkits. Generally more advanced malware, rootkits establish their persistence
at a lower level, which can assume the form of a kernel driver, or even as code
hidden in the MBR of the hard disk. Able to hide from plenty of detection
techniques, they may survive disk formatting and OS reinstall.
• Worms. Typically infamous pieces of code like the Morris Worm and, more
recently, WannaCry [12], these malwares require almost zero interaction from
the user, since they use wide-spread and powerful vulnerabilities. For this
reason, they are able to replicate and spread in a small amount of time and
potentially deal great amount of damages to impacted systems..
Also, strings can be really good indicators of maliciousness, and in some cases
they can yield interesting information and properties of samples, like C2 IP ad-
dresses, or files searched in the system.
As previously mentioned, Windows API calls can be used as detection patterns
during static analysis; for instance, CreateRemoteThread, LoadLibrary, WriteProcessMemory
are suspicious function often used by malicious samples for DLL injection into
other processes, and they are rarely used for legitimate code.
13
Background
Figure 3.1: Interface of the reverse engineering tool open sourced by the NSA,
Ghidra.
A control Flow Graph (CFG) [17] is a directed graph where basic blocks of code
are represented by graph nodes, and flow paths - like branches - by edges. For
instance, in Symba design and implementation, CFG will be used to extract basic
blocks preceding and following a certain API function call deemed as particularily
interesting. Modern analysis frameworks implement methods and tools to graph-
ically output these kind of graphs, which provides an easier understanding and
mapping of the general features of the code, as seen in fig 3.2.
Notably, other static analysis techniques have been employed in the literature,
such as the usage of N-grams combined with API calls or opcodes, and file attributes
like size or content entropy [18].
analyst with the possibility to step through the code, instrution after instruction,
to observe in a detailed and controlled way the flow of execution of binaries under
scrutiny. While debugging can be used by developers to track down and solve
bugs - as the very etymology of the words shows - it constitutes a useful tool in
the hands of reverse engineers trying to extract knowledge and properties out of
analyzed potentially malicious samples. Incidentally, one of the already mentioned
industry standard tools for malware analysis, IDA Pro, also features a capable
debugger. Other notable mentions here are WinDBG, ImmunityDBG and x64dbg
for Windows, while the de-facto tool in the Linux environment remains GDB. Even
though debuggers take the role of manual, precise tools in the field of dynamic
analyses, other automatic approaches are used in the wild with good results. For
instance, we already mentioned API calls sequencing as a valuable tool to detect and
classify in a first quick analysis malicious samples. However, static observation of
API calls suffer from the drawback of not being able to monitor which parameters
are being passed to API calls. Moreover, some malware developers have been
obfuscating their samples by importing API function which have no real use in
15
Background
the binary, other than confusing analysts trying to extract information from the
imports table. For this reason, analysts will often use monitoring tools which will
hook API calls performed by binaries alongside their parameters, and output them
in a filterable tables. The tool of the trade, in this scenario, is ProcMon of the
Sysinternal suite (fig 3.3).
it. All the operations mentioned up to here, and more, are monitored and recorded,
and at the end of the execution, after a given timeout or upon malware termination,
they are used to build a detailed report of the execution. Modern sandboxes
also automatically extracts "suspicious" signatures - basically, using Yara rules
precompiled in the sandbox - out of the monitored facts and use them to compute
a score of maliciousness of the analysed samples. These services can be exposed
onlines in applications like app.any (https://app.any.run/), joe sandbox (https:
//www.joesecurity.org/) or VirusTotal (https://www.virustotal.com/gui/),
or can be hosted offline by the analyst in projects like the Honeynet Cuckoo Sand-
box. An example of suspicious analysis signatures as extracted by Cuckoo is shown
in figure 3.4
Figure 3.4: List of signatures extracted by cuckoo for a Poweliks sample. Color
indicates degree of suspiciousness.
them during the process of analysis. This section will not take into account the
time spent by the analyst using one approach instead of the other, given that this
measure heavily depends on the tools employed, which can greatly vary in their
capability of automating the analysis process.
Dynamic
which fake plausible responses are sometimes used, while it goes without saying that
such security compromise could negatively impact the soundness of the analysis.
Finally, the kind of described environment can be complex to design and build.
On one side, different samples require different targets. OS, architecture, even
a specific version of a particular piece of software or a particular DLL could be
needed by the sample in order to execute as expected, posing a great challenge for
systems and tools trying to analyze malware targeting different platforms such as
Linux, Windows, Android. On the other hand, it is clear how the entire process
of bringing up the virtual machine, transferring the binary file, executing it and
monitoring - via instrumentation and hooking - its capabilities, then shutting down
or resetting the machine and computing a full report can take several minutes. In
environments where thousands of samples are fed into the system on a daily basis,
performance is definitely a problem to address.
Static
Unlike dynamic analysis, as has already been mentioned, static analysis simply look
at the contents of files as they exists on disk, without executing them whatsoever. It
uses patterns, attributes, signatures and artifacts to extract meaningful information
from the binary.
Static analysis is definitely more resilient to the same issues that were defined
before with regard to dynamic analysis. It is much more efficient, since the process
of handling the virtual environment doesn’t exist here, and more cost-effective, not
requiring an infrastructure of distributed machines and the capability to handle
instrumentation, hooking, evade detection and mantaining the system secure at the
same time. Static tools can be quickly written specifically for the analyzed binary,
independently of the architecture, OS, library or networking capabilities needed.
However, this kind of analysis can be easily hindered to the point of rendering it
useless by largely available and known techniques:
• Code obfuscation. The idea behind code obfuscation [22] [23] is to transform
instructions of the original code in some way that maintains the same semantic
behavior of the former, but in a way that is much more difficult to analyze.
Notable examples are opaque predicates, sequence of code ending in a branch
that always produce the same result, in a way that would be difficult to detect
from static analysis, and control flow flattening (fig 3.5), a technique that
attepts to make it as difficult as possible to build a useful CFG out of the
binary, by bringing all the flow logic down to a list of "leaf" basic blocks and
a single (or multiple) dispatcher blocks that use and update a state variable
to jump to the relevant blocks. Both techniques have been reportedly used
in modern malware and are available in open source tools such as the LLVM
obfuscator [24].
19
Background
Figure 3.5: Diagram of how the CFG is transformed after the process
of flattening (http://tigress.cs.arizona.edu/transformPage/docs/flatten/
index.html).
• False positives / False negatives ratio. Since based on heuristics and signatures,
static analysis can flag only to a certain extent of soundness a malware as
malicious. Without executing it, it is hard - without manual intervention from
the analyst - to determine whether those signature are really malicious or just
potentially suspicious. In fact, it is entirely possible to craft harmless samples
built just in order to trigger static analysis tools for incorrect or malicious
attribution to other groups or campaigns or simply to bring confusion inside
the analysis environment.
Both analysis approaches have significant drawbacks when dealing with trigger
conditions. Statically analysing a trigger essentially mean to disassemble and
examine the binary instructions around the condition, trying to reverse engineer
the meaning of the conditions, and the values that could solve both arms of branches
dependent on it. On the other side, generic dynamic systems, as already depicted,
fall short on feeding precise inputs that the malware expects before exhibiting its
20
Background
3.4.1 Components
In every moment, a symbolic execution engine manages symbolic states. Each state
is characterized by:
• a memory store. This maps relationships between symbols and other symbols,
or symbols and concrete values. During symbolic execution, assignments and
operations on the symbols update the symbolic memory store.
• a boolean formula. This component collects, for every if statement that handles
a symbol, the conditions that have to be bound on symbols to meet each
branch. Therefore, for every different path in the execution tree, a different
boolean formula is held.
• a model checker. An SMT solver which can solve, at any time, boolean
formulas to generate concrete values, or to ensure the unsatisfiability of the
formula.
21
Background
1 void foobar ( i n t a , i n t b) {
2 int x = 1 , y = 0;
3 i f ( a != 0 ) {
4 y = 3 + x;
5 i f ( b == 0 )
6 x = 2∗( a + b) ;
7 }
8 a s s e r t ( x−y != 0 ) ;
9 }
The branches in the diagram show how a symbolic execution engine updates its
path formula and store depending on the values that the symbols simultaneously
take. At some point, a state where the assert fails is found, and the symbols are
resolved to the value of (a = 2, b = 0).
3.4.2 Weaknesses
Discussing about weak points of symbolic execution is an important part of the
analysis of the tools conducted before implementing Symba. Theoretically, symbolic
execution is capable of guaranteeing complete soundness on the analysed binary.
However, most of the times, a trade-off between quality and feasibility has to
be made. Mainly, we can sum up the issues to solve when employing symbolic
execution as follows:
• environment. A binary, and this stands in particular for malware, is not merely
a set of instructions independent from everything around. Its flow of execution
strictly depends from variables coming from other layers in the software stack:
the creation of a file, a registry key, network packets incoming on the wire. A
symbolic execution engine has to consider all these factors, to make sure they
are consistent within the analysis flow.
• path explosion. Every time a branch is encountered, the symbolic execution
engine creates a state satisfying, whenever possible, both arms. Clearly, this
grows exponentially, therefore it is unlikely that an exhaustive search of all
the states is feasible. For this reason, the engine has to take into account
optimizations, pruning, and other techniques to keep the exploration feasible.
• constraint solving. SMT solvers have the capability of solving hard combina-
tions of constraints over multiple variables. However, finely-grained non-linear
arithmetic designed to be mathematically hard to solve can cause serious
problems to efficiency.
The framework used in the backend of Symba, angr, provides solutions to manage
all the mentioned problems, but it does not, in general, automatically implements
any of them. Instead, this power is given to the user of the framework. In the
following chapter, we describe some notable features of angr and how they are
implemented and used.
23
Chapter 4
1 $ ./ test
2 This s h o u l d n ’ t be p r i n t e d i f hooked !
1 $ python t e s t s i m p r o c . py
2 ∗∗∗∗∗∗STDOUT CONTENT∗∗∗∗∗∗
24
The angr framework
The output string matches what we would expect from a ’normal’ execution of
the binary. Let us now introduce a Symbolic Procedure into the test environment,
to observe the change of behavior. The listing at A shows a new version of the
previous script, where a SimProcedure is declared and hooked to the binary. The
listed script demonstrates the interface used to implement Symbolic Procedures:
1. The class modeling the function must extend angr.SimProcedure, which is the
abstract interface exposed by angr.
2. The run method must be implemented. It will take as argument whatever
arguments the original function expects, following the calling convention of
the binary. Inside the method body, the programmer can choose what to do,
either concretely or symbolically, with the function args, including returning
values to the callee.
3. Finally, the Symbolic Procedure has to be hooked via call to hook_symbol,
replacing the symbol of the function meant to be replaced.
1 $ python t e s t s i m p r o c . py
2 The f u n c t i o n f u n c was r e p l a c e d by t h i s f u n c t i o n !
3 i n t _ a r g : <SAO <BV64 0xa>> / s t r i n g : <SAO <BV64 0 x400742>> /
o t h e r _ s t r i n g <SAO <BV64 0 x40073d>>
4 ∗∗∗∗∗∗STDOUT CONTENT∗∗∗∗∗∗
5 b’ ’
As we expected, the content of stdout is empty, which clearly shows that the
original func function has not been executed. On the other hand, we can observe
the result of the code inside our run method, where the given arguments are printed.
Later in this chapter, we will further expand this example to understand what is
the meaning of those seemingly opaque <SAO <BV64>> objects, but the reader is
invited to note that the first argument, the integer int_arg, is actually equal to 10
(0xa in hexadecimal notation), which is indeed the value passed to that function
by the main.
3. call_state. Begins at the first basic block of a function, ordering its passed
arguments depending on the calling convention.
We can see how the mentioned interfaces work with our test binary. An
interactive python console will show some information on the computed states.
The first thing will be to create the project:
1 In [ 1 ] : import angr
2 In [ 2 ] : p r o j = angr . P r o j e c t ( " . / t e s t " )
Starting with the blank state, we will see how registers and initial address are
populated:
1 In [ 2 7 ] : blank = p r o j . f a c t o r y . b l a n k _ s t a t e ( )
2 In [ 2 8 ] : hex ( blank . addr )
3 Out [ 2 8 ] : ’ 0 x4006c0 ’
26
The angr framework
4 In [ 2 9 ] : f o r r e g i n [ " rax " , " rbx " , " r s i " , " r d i " , " r c x " , " rbp " , " r s p "
]:
5 ...: p r i n t ( g e t a t t r ( blank . r e g s , r e g ) . s y m b o l i c )
6 ...:
7 True
8 True
9 True
10 True
11 True
12 True
13 False
14 In [ 3 3 ] : blank . r e g s . r s p
15 Out [ 3 3 ] : <BV64 0 x 7 f f f f f f f f f e f f f 8 >
• The address, when not specified as argument, is the one of the entry point.
• Most of the registers are symbolic, which means that they are unitialized by
the symbolic engine.
How would this compare to a state initialized as an entry state? We can analyze
this by generating one in the same way we did for the blank state:
1 In [ 3 0 ] : e n t r y = p r o j . f a c t o r y . e n t r y _ s t a t e ( )
2 In [ 3 1 ] : hex ( e n t r y . addr )
3 Out [ 3 1 ] : ’ 0 x4006c0 ’
4 In [ 3 2 ] : f o r r e g i n [ " rax " , " rbx " , " r s i " , " r d i " , " r c x " , " rbp " , " r s p "
]:
5 ...: print ( g e t a t t r ( entry . regs , reg ) . symbolic )
6 ...:
7 False
8 False
9 True
10 True
11 True
12 False
13 False
The scenario is somewhat different: in fact, more registers appear not symbolic.
The presence of a concrete RBP suggests that, in this case, some function prologue
has been executed which prepared the stack frame for execution. It is worth
mentioning that, even though we call it entry state, the address of the starting
basic block can be modified to start somewhere else.
27
The angr framework
The project has been created with a specific option unset, auto_load_libs. This
is necessary, otherwise angr would try to compute the CFG of any shared library
loaded by our binary, thus increasing enormously the complexity of the graph
and its time of computation. For instance, one could imagine the grievousness of
the task of analysing the glibc implementation. The graph is built using NetworkX
[29], a python library used to implement complex networks with a great focus on
portability and universality – nodes and edges can be anything.
In the listing below, we start exploring the capabilities of the CFG graph
generated by angr, to define the functionalities that will be used by Symba to
compute the starting points of the analysis. We will explore how to retrieve basic
blocks’ addresses, alongside their successors/predecessors nodes, specifying the
jumpkind in the case of branches.
28
The angr framework
7 Out [ 2 6 ] : ’ 0 x4006c0 ’
The interface is clear and readable. We first get the entry_node by querying for
a node matching the address of the entry point. As the output of the block attribute
states, the node matches symbol _start at address 0x4006c0 - the address of the
entry point - and its size is 42 bytes. Each block exposes, among the other things,
a capstone interface to disassemble instructions found at nodes. Capstone (http:
//www.capstone-engine.org/) is a lightweight multi-platform, multi-architecture
disassembly framework., and we can see it in action by playing a bit with the
interface:
29
The angr framework
As expected, there is only one successor node, the one calling the printf . In fact,
a function call does not "branch" the execution flow in any way. Let us explore how
the three subsequent if s in the code impact the generated CFG.
The CFG allows for querying of symbols of functions listed in the binary. The
interface is simple and clear. The listing below extracts the addresses of our
functions func1, func2 and func3 :
1 In [ 1 3 0 ] : f u n c t i o n s = c f g . kb . f u n c t i o n s
2 In [ 1 3 1 ] : a d d r e s s e s = [ ( addr , f u n c . name ) f o r addr , f u n c i n f u n c t i o n s .
i t e m s ( ) i f f u n c . name i n [ " f u n c 1 " , " f u n c 2 " , " f u n c 3 " ] ]
3 In [ 1 3 2 ] : a d d r e s s e s
4 Out [ 1 3 2 ] : [ ( 4 1 9 6 2 9 8 , ’ f u n c 1 ’ ) , ( 4 1 9 6 3 3 4 , ’ f u n c 2 ’ ) , ( 4 1 9 6 3 7 4 , ’ f u n c 3 ’ )
]
The code above simply builds a list of tuples with the form (addr, name) through
a list comprension filtered for those functions named after what we are looking
for. For the sake of brevity, and since all of those functions will have a similar
behavior in the CFG graph, we will only analyze one of those functions, func2.
Before proceeding into querying the graph, let us reason on what we would expect,
knowing the code.
Each function is called in a separate arm of an if-elseif-elseif-else structure.
Therefore, reasonably, basic blocks responsible for calling functions will not have
multiple predecessors. In fact, they are reachable only through one path in the
code. Also, for the same reason, we expect only one successor, which is, the block
executing the return statement. Is this reasoning confirmed by the graph?
1 In [ 1 5 4 ] : func2_addr = a d d r e s s e s [ 1 ] [ 0 ]
2 In [ 1 5 5 ] : func2_node = c f g . get_node ( func2_addr )
3 In [ 1 5 6 ] : if_body_node = c f g . g e t _ p r e d e c e s s o r s ( func2_node ) [ 0 ]
4 In [ 1 5 7 ] : p r e d s = c f g . g e t _ p r e d e c e s s o r s ( if_body_node )
5 In [ 1 5 8 ] : s u c c s = c f g . g e t _ s u c c e s s o r s ( if_body_node )
6 In [ 1 5 9 ] : l e n ( p r e d s ) == 1
7 Out [ 1 5 9 ] : True
8 In [ 1 6 0 ] : l e n ( s u c c s ) == 1
9 Out [ 1 6 0 ] : True
As shown clearly in the listing, regarding both the successors and predecessors
nodes of the block calling function 2, we only have one direction. As expected, by
printing the instructions of the block contained in the successor node, we obtain:
1 In [ 1 6 1 ] : p r i n t ( ’ \n ’ . j o i n ( [ r e p r ( i n s n . i n s n ) f o r i n s n i n if_body_node .
block . capstone . insns ] ) )
30
The angr framework
Following this idea, we can predict that, by querying the CFG for the predecessor
node of the if arm body, we will get a node responsible for the cmp instruction
that leads to the former. That node will have more than one successor: in fact,
depending on the value being compared, the execution flow could or could not pass
through that if arm. The listing below confirms this prediction.
1 In [ 1 6 1 ] : cmp_node = c f g . g e t _ p r e d e c e s s o r s ( if_body_node ) [ 0 ]
2 In [ 1 6 2 ] : p r i n t ( ’ \n ’ . j o i n ( [ r e p r ( i n s n . i n s n ) f o r i n s n i n cmp_node . b l o c k
. capstone . insns ] ) )
3 <CsInsn 0 x4008c7 [ 8 3 7 dbc02 ] : cmp dword p t r [ rbp − 0 x44 ] , 2>
4 <CsInsn 0 x4008cb [ 7 5 1 3 ] : j n e 0 x4008e0>
5 In [ 1 6 3 ] : c f g . g e t _ s u c c e s s o r s ( cmp_node )
6 Out [ 1 6 3 ] : [<CFGNode main+0x8b [ 1 7 ] > , <CFGNode main+0x9e [ 6 ] > ]
1 In [ 1 7 0 ] : f o r f u n c i n [ func1 , func2 , f u n c 3 ] :
2 ...: pred_addr = l i s t ( c f g . f u n c t i o n s . c a l l g r a p h . p r e d e c e s s o r s (
func ) ) [ 0 ]
3 ...: p r i n t ( pred_addr == p . l o a d e r . find_symbol ( " main " ) .
rebased_addr )
4 ...:
5 ...:
6 True
7 True
8 True
• active. As already mentioned, this stash contain states that can and will be
stepped on the next step() .
• deadended. This stash collects all states that, for some reason, cannot be
stepped anymore. For instance, states reaching termination through an exit
call.
• unsat. States that are already determined to be unsatisfiable are put in this
stash. For example, a state which would need a certain variable to be both
equal to 3 and 4 at the same time, would belong to this stash. We will, later
in this chapter, show how we can place constraints on states using the solver
backend. Requires option save_unsat.
• unconstrained. States where the RIP is controlled by the user or by another
source of symbolic input. Requires option save_unconstrained.
• pruned. With a certain option - LAZY_SOLVES - enabled, angr will not check
for state satisfiability at every step. Instead, it will delay this until absolutely
required. In this case, if a state is found to be unsatisfiable, all its previous
state, until the point it became unsat, is placed inside this stash.
32
The angr framework
A simulation manager just initialized on a state - in this case the entry state -
has a single stash, the active stash, populated with that state. One of the simplest
operation that can be performed on a simulation manager is to run it until all the
possible paths have been explored. How would the stashes look in that case?
1 In [ 9 ] : s i m g r . run ( )
2 Out [ 9 ] : <SimulationManager with 4 deadended>
3 In [ 1 0 ] : s i m g r
4 Out [ 1 0 ] : <SimulationManager with 4 deadended>
5 In [ 1 1 ] : s i m g r . deadended
6 Out [ 1 1 ] :
7 [< SimState @ 0 x1000108 >,
8 <SimState @ 0 x1000160 >,
9 <SimState @ 0 x1000160 >,
10 <SimState @ 0 x1000160 >]
As expected, the simulation manager after a run call doesn’t have active states
anymore - if it would, there would still be paths which can be explored. Therefore,
all the states are in the deadended stash, which means that they correspond to
state termination.
Is it possible to explain the meaning of the four deadended states? Let us
reconsider the source code of the test executable at A. Three of the states stopped
at the same address, as three are the functions which can be called depending on
the value given to atoi . The remaining state, instead, represents the else arm which
goes through the exit function. This can be interestingly confirmed by accessing
the history plugin of the state, which exposes descriptions of the actions performed
on the state:
1 In [ 2 8 ] : l i s t ( s . h i s t o r y . d e s c r i p t i o n s )
2 Out [ 2 8 ] :
3 <s n i p >
4 ’<SimProcedure a t o i from 0 x1000100 : 1 s a t > ’ ,
5 ’<IRSB from 0 x4008b2 : 2 s a t > ’ ,
6 ’<IRSB from 0 x4008c7 : 2 s a t > ’ ,
7 ’<IRSB from 0 x4008e0 : 2 s a t > ’ ,
8 ’<IRSB from 0 x 4 0 0 8 f d : 1 s a t > ’ ,
9 ’<IRSB from 0 x4006a0 : 1 s a t > ’ ,
33
The angr framework
• Depth First Search. Just one state is chosen to be executed and stepped
forward. The others are kept in a deferred stash until the former terminates.
• LoopSeer. For reasons linked to path explosion, for loops can hinder symbolic
execution up to the extent of impracticality . LoopSeer detects states which
go through too many loops, dumping them into a spinning stash until no
other active state is available. Other techniques, like the one presented in [31],
propose an extension of symbolic execution tailored to handle loops.
For the sake of understanding, we can apply the Explorer technique to the
example we are working on. The Explorer instructs angr to continue exploring
until some condition has been met. The condition can be represented, simply, by
an address - run until a block containing this address is executed - or by providing a
function returning a boolean, transforming the condition into a run until function
returns true.
We can observe both behaviors in the test binary. Let us suppose that an analyst
is given it, while not provided with the source code, so reverse engineering has to
be performed on the executable. The analyst identifies the three func functions,
and he identifies as well that an atoi function is used to transform the value given
34
The angr framework
fed to standard input into an integer. The Explorer technique can help the analyst
knowing which value has to return atoi in order to reach a certain function. Let
us start with func1, thereafter using the same method to solve with respect to the
other functions.
Once computed the address of the function, this can be used as it is for the
Explorer technique:
As shown in the listing above, the simulation manager, armed with the Explorer
technique, has created a new stash called found. This stash contains states that
matched the condition configured into Explorer, which, in this case, is the reach of
a precise address. Explorer stops here at the first match, but we could continue
running it, gathering all the states matching the rule into that stash.
What we do expect now, therefore, is to have, into the state in found stash, the
path needed to reach func2. First of all, let us verify the address:
1 In [ 1 4 ] : f o u n d _ s t a t e = s i m g r . found [ 0 ]
2 In [ 1 5 ] : hex ( f o u n d _ s t a t e . addr )
3 Out [ 1 5 ] : ’ 0 x4007ee ’
The address of the found state is, indeed, the address of func2. Concerning how
the analyst can retrieve the value passed into atoi to answer his starting question,
there are various ways. The easiest one is a small workaround based on the state
posix plugin. In fact, we know that the value ultimately passed to atoi is fed to the
process via stdin . The posix state plugins allows fast solving of opened files:
1 In [ 1 9 ] : f o u n d _ s t a t e . p o s i x . dumps ( 0 )
2 Out [ 1 9 ] : b ’ 02\ x f 7 \ xc7 \ x00 \ x10 \ xbc3 \ xec \ x00 \x19PB\ x00 \ x00 \ x00 \ x00 \ x00 \
x00 \ x00 \ x00 \ x00 \ x00 \ x00 \ x00 \ x00 \ x00 \ x00 \ x00 \ x00 \ x00 ’
The returned bytestring contains the answer in its first bytes: in fact, the value
02 is exactly the one needed to reach our target function. The other bytes are
35
The angr framework
just solved by default and, in this case, they’re not constrained to anything. We
can verify this by wrapping the code into a function and calling it for all target
functions:
1 In [ 2 2 ] : d e f f i n d _ a t o i _ v a l u e ( f i n d _ a d d r e s s ) :
2 ...: s i m g r = p . f a c t o r y . simulation_manager ( e n t r y _ s t a t e )
3 ...: s i m g r . e x p l o r e ( f i n d=f i n d _ a d d r e s s )
4 ...: f o u n d _ s t a t e = s i m g r . found [ 0 ]
5 ...: p r i n t ( f o u n d _ s t a t e . p o s i x . dumps ( 0 ) [ : 3 ] )
6 ...:
7 In [ 2 3 ] : f i n d _ a t o i _ v a l u e ( p . l o a d e r . find_symbol ( " f u n c 1 " ) . rebased_addr )
8 b ’ 1\ xacq ’
9 In [ 2 4 ] : f i n d _ a t o i _ v a l u e ( p . l o a d e r . find_symbol ( " f u n c 3 " ) . rebased_addr )
10 b ’ 3 | \ xe0 ’
The results show that the number in the solved stdin string matches what we
would expect. It is worth considering that we are interested, in this cases, only
to numbers until the first non-digit letter. This stems from the way atoi works, in
particular, without detecting errors in its input, but just converting as long as read
letters are digits.
What about the else arm exiting without calling functions? To solve for a value
reaching that code, another useful matching rule exposed by Explorer can be used:
the avoid parameter. The latter works the opposite with respect to find - it directs
exploration avoiding addresses or boolean functions passed to the exploration
technique. The solution to our previous question becomes:
• In this case, since no find parameter was passed, there was no active stash.
36
The angr framework
However, since we were looking for an exited state, the deadended stash
became our target.
• The dumped string doesn’t contain, at its first bytes, any digit that can be
converted. It is a valid solution - we will discuss later about constraints and
the Z3 solver - to reach the exit arm.
The approach used to retrieve the correct value has a few drawbacks. First
of all, without having specific knowledge on the entire program flow, finding the
correct bytes from the entire stdin symbolic file content would be almost impossible.
Moreover, it does not employ at all the power of the angr interface in creating
symbols and solving them. Considering this, in the following, we will implement a
more robust way to solve for values of interest, a core functionality that Symbolic
Execution frameworks expose.
A more precise approach to discover the input needed for a target function
starts from the assembly of the executable. The idea is to know, precisely, where
the fgets function will place the value that will be then passed as input to atoi . In
many modern architectures, local variables are referenced through an offset from
the register RBP. In this case, by quickly inspecting the disassembly of the main
function, we can determine that the offset is 0x30. Therefore, the buffer which
atoi will read our input from will be placed at address rbp − 0x30. Moreover, the
address where the atoi function is called is 0x4008ad. The following listing will
show how a symbol of a suitable length is created and injected into memory at
that address, for later evaluation at solve time:
1 In [ 1 8 3 ] : s t a t e = p . f a c t o r y . e n t r y _ s t a t e ( addr=0x4008ad )
2 In [ 1 8 4 ] : s t a t e . r e g s . rbp = s t a t e . r e g s . r s p + 0 x100
3 In [ 1 8 5 ] : s t a t e . r e g s . r d i = s t a t e . r e g s . rbp − 0 x30
4 In [ 1 8 6 ] : s t a t e . memory . s t o r e ( s t a t e . r e g s . rbp − 0 x30 , fgets_symbol , 4 )
5 In [ 1 8 7 ] : s i m g r = p . f a c t o r y . simulation_manager ( s t a t e )
6 In [ 1 8 8 ] : s i m g r . e x p l o r e ( f i n d=p . l o a d e r . find_symbol ( " f u n c 2 " ) .
rebased_addr )
7 Out [ 1 8 8 ] : <SimulationManager with 3 a c t i v e , 1 found>
8 In [ 1 8 9 ] : s o l _ s t a t e = s i m g r . found [ 0 ]
9 In [ 1 9 0 ] : s o l _ s t a t e . s e . e v a l ( fgets_symbol , c a s t _ t o=b y t e s )
10 Out [ 1 9 0 ] : b ’ 2\ x80 \ x d f ’
Let us consider the listing above, to explain how the solution has been found
employing the power of symbolic execution. The flow of the code can be explained
as follows:
37
The angr framework
1. A state is created, in line 1, exactly at the address of the call atoi instruction.
2. A stack frame has now to be created. In fact, when creating the state as an
entry_state, RSP is initialized with a concrete value, but RBP is not. In order
to create a suitable frame, in this case, it is enough to place RBP at 256 bytes
above RSP (line 2).
3. As previously mentioned, fgets will use as buffer the address at RBP - 0x30.
Therefore, since the same address has to be fed into atoi , we set, in line 3,
RDI - the register where the first argument is placed according to the calling
convention - equal to that address.
4. Now that the prologue has been set, it is time to actually inject the symbol.
In line 4, the previously created fgets_symbol is stored at address RBP - 0x30,
with a len of 4 bytes - more than enough in this case.
5. A simulation manager is created from the setup state, and it is executed with
the Explorer technique until func2 is called.
6. On the found state, the solver is called to evaluate the concrete solution of the
injected symbol. The obtained value is, as expected, 2.
1 In [ 2 0 5 ] : d e f s o l v e _ f o r _ f u n c ( n ) :
2 ...: def is_func ( state ) :
3 ...: r e t u r n True i f " Function {} h e r e " . format ( n ) . encode
( ) i n s t a t e . p o s i x . dumps ( 1 ) e l s e F a l s e
4 ...: s t a t e = p . f a c t o r y . e n t r y _ s t a t e ( addr=0x4008ad )
5 ...: s t a t e . r e g s . rbp = s t a t e . r e g s . r s p + 0 x100
6 ...: s t a t e . r e g s . r d i = s t a t e . r e g s . rbp − 0 x30
7 ...: f g e t s _ s y m b o l = s t a t e . s o l v e r . BVS( " f g e t s _ i n p u t " , 4 ∗ 8 )
8 ...: s t a t e . memory . s t o r e ( s t a t e . r e g s . rbp − 0 x30 , fgets_symbol ,
4)
9 ...: s i m g r = p . f a c t o r y . simulation_manager ( s t a t e )
10 ...: s i m g r . e x p l o r e ( f i n d=i s _ f u n c )
38
The angr framework
The only change in the listing above stands in the introduced is_func that checks
for the content in the symbolic stdout. If the seeked string is found, the Explorer
stops and yields the found state that is later used to solve the symbol. By launching
the function with the different integers, we obtain the desired result:
1 In [ 2 1 7 ] : for n in [ 1 , 2 , 3 ] :
2 ...: p r i n t ( "A s u i t a b l e v a l u e t o f e e d i n t o f g e t s t o r e a c h
f u n c {} i s : {} " . format ( n , s o l v e _ f o r _ f u n c ( n ) ) )
3 ...:
4 A suitable v a l u e t o f e e d i n t o f g e t s t o r e a c h f u n c 1 i s : b ’ 1\ x974O ’
5 A suitable v a l u e t o f e e d i n t o f g e t s t o r e a c h f u n c 2 i s : b ’ 2 $ \ xc60 ’
6 A suitable v a l u e t o f e e d i n t o f g e t s t o r e a c h f u n c 3 i s : b ’ 3?\ xcc \ xe7 ’
Arguably, the best way to use angr and explore its capabilities is to use an
interactive python interpreter which supports help and tab completion. For this
task, I will employ IPython. In this specific case, a breakpoint - represented by
the IPython function embed - will be placed right after the print statement in the
SimProcedure code. By calling functions in the IPython environment, we are able
to interactively explore how angr manages SimStates and memory values.
In a SimProcedure, the SimState can be accessed through the attribute self . state :
39
The angr framework
1 In [ 1 ] : s e l f . s t a t e
2 Out [ 1 ] : <SimState @ 0 x40063a>
The string representation of the state shows the current address of execution.
Value of CPU registers can be easily accessed:
Under x86_64 architecture and System V ABI, the first three parameters to
functions are passed through the registers RDI, RSI, RDX, in this particular order.
In the listing above, we see how the arguments to functions printed in A match
those contained in registers. Let us examine furthermore how to interact with
concrete values inside those registers. Considering the value of the arguments
passed to the function:
1 f u n c ( 1 0 , "ARG1" , "ARG2" ) ;
1 In [ 5 4 ] : rdi
2 Out [ 5 4 ] : <SAO <BV64 0xa>>
3 In [ 5 5 ] : state . solver . eval ( rdi )
4 Out [ 5 5 ] : 10
In the same way, we can extract the address of the second argument:
1 In [ 5 9 ] : r s i
2 Out [ 5 9 ] : <SAO <BV64 0 x400742>>
3 In [ 6 0 ] : hex ( s t a t e . s o l v e r . e v a l ( r s i ) )
4 Out [ 6 0 ] : ’ 0 x400742 ’
Memory can be accessed through the interface state .memory. So, supposing we
are to extract the first byte contained at the address in rsi, the code would be:
40
The angr framework
1 In [ 6 3 ] : addr = s t a t e . s o l v e r . e v a l ( r s i )
2 In [ 6 4 ] : s t a t e .mem[ addr ] . byte . r e s o l v e d
3 Out [ 6 4 ] : <BV8 65>
4 In [ 6 5 ] : c h r ( s t a t e . s o l v e r . e v a l ( s t a t e .mem[ addr ] . byte . r e s o l v e d ) )
5 Out [ 6 5 ] : ’A ’
Abstracting the code above to print strings at given addresses, we obtain the
following:
1 In [ 7 3 ] : d e f p r i n t _ s t r i n g ( s t a t e , addr ) :
2 ...: result = " "
3 ...: w h i l e True :
4 ...: c = c h r ( s t a t e . s o l v e r . e v a l ( s t a t e .mem[ addr ] . byte .
resolved ) )
5 ...: i f c i s not ’ \ x00 ’ :
6 ...: r e s u l t += c
7 ...: else :
8 ...: break
9 ...: addr += 1
10 ...: return r e s u l t
11 ...:
12 In [ 7 4 ] : r s i _ a d d r = s e l f . s t a t e . s o l v e r . e v a l ( r s i )
13 In [ 7 5 ] : rdx_addr = s e l f . s t a t e . s o l v e r . e v a l ( rdx )
14 In [ 7 6 ] : p r i n t _ s t r i n g ( s e l f . s t a t e , r s i _ a d d r )
15 Out [ 7 6 ] : ’ARG1 ’
16 In [ 7 7 ] : p r i n t _ s t r i n g ( s e l f . s t a t e , rdx_addr )
17 Out [ 7 7 ] : ’ARG2 ’
Indeed, the resulting strings match exactly what is passed to the function in the
test code. In line 14, the value ’ARG1’, passed as the first argument, is correctly
retrieved from the address stored in rsi. Then, the same happens with the second
argument, resulting in ’ARG2’. This example is easy to grasp, since all the variables
involved are already determined and "hard-coded".
41
Chapter 5
Architecture design
The design of symba is greatly focused on the aforementioned concept of trigger
conditions. The generic flow of the analysis is as follows:
1. The analyst chooses Win32 API function calls that are deemed as interesting.
By interesting, here, we mean function calls for which the return value can
unlock or block interesting routines - that is, functions that model ‘trigger
conditions‘. As we will see, this choice is easily handled by a script which
generates the correct configuration.
2. Symba "hooks" these function calls in the binary, basically replacing them
with functions automatically generated by the system that return a symbolic,
tainted value to the caller. We will see how these tainted value are generated
and tracked during the execution.
3. The system then builds a Control Flow Graph of the executable, extracting
basic blocks in which the hooked functions will be called. Using the CFG,
it builds a list of basic blocks preceding the extracted ones. It will be,
substantially, a list of "entry points" basic blocks.
4. Starting from each one of those entry points, Symba symbolically executes the
binary, tracking any kind of constraint posed on tracked values. Tracked values
could be memory addresses returned by the configured ‘trigger‘ functions or
other memory addresses which are directly influenced by those. The mechanism
of this tracking will be explained in detail in a following section.
6. For each taken path, Symba then collects all constraints on tainted values,
solving them using the underlining SMT solver. The computed solutions are
then stored into a structured file for further processing - e.g examined by
a human analyst or fed into a Cuckoo configuration process for automated
analysis.
Start TriggerSeer
applied to
parsed by initiates
Configurator Solver
Basically, the main rationale of this system is that malwares will interact with
the execution environment through the Win32 API. Results from these API calls
will be then probably compared and checked using simple or complex routines,
and the results will direct the execution flow towards one branch or the other. By
collecting and solving these chain of constraints each time a branch is detected for
a tainted value, the system is capable to generate values satisfying one or the other
branch arm, thus possibly unlocking new guarded paths in the malware code.
Symba is implemented in a modular fashion, so that most of the features
described can be extended and augmented without necessarily touching other parts
of the code.
In this chapter, the design of each module will be detailed. The flow diagram
of the interaction between components is described in figure 5.1. Specifically,
components are:
• Configurator.
43
Architecture design
• Model Injector.
• States Extractor.
• TriggerSeer.
• Symbolic Explorator.
• Solver.
In the rest of this chapter, each component design will be discussed in detail,
highlighting its process inside the analysis flow.
5.1 Configurator
parse
outputs
JSON configuration
The configurator is charged with the task of transforming Windows API function
names into the format used by Symba. Basically, for each name it is necessary to
extract the function signature and build a JSON file with all the relevant fields
in a default value, ready to be tweaked by the user. The documentation used to
lookup function names is the Microsoft Developer Network, considered to be the
official documentation for the Windows API. Since the precise URL depends on
44
Architecture design
the header file, and since the MSDN does not offer any public API, in order to
retrieve documentation from the MSDN a couple of steps are required.
First of all, the name of the function is searched through Google with the site
filtering keyword pointing to the MSDN site. This way, we will only retrieve pages
in the MSDN with the function name contained in the URL. Afterwards, the query
results are filtered to make sure the URL looks like an API signature documentation,
and if so, the page is downloaded. Since we download the whole page HTML,
another parsing step is required to extract exactly the information needed to build
the signatures. At last, a JSON file is built with the results. The choice of JSON as
the configuration format greatly improves the readability of the file. Moreover, it
ensures that the analyst using Symba can easily tweak and customize it, by setting
the length of a certain symbol, the name of some parameters, or to decide which one
of them should be injectable. Also, there’s another reason for having configuration
files stating which functions should be traced as trigger sources. The idea is to
create multiple JSON files, each one focused on a certain set of trigger sources, that
can quickly be switched during analysis. One could have a lightweigth . json file to
follow only a few functions that are generally deemed as interesting, like API calls
that query registry keys. Then, another file which could be named full . json will be
loaded of more functions, in order to reveal as many conditions as possible from
the binaries exhamined. The flow diagram discussed is represented in figure 5.2.
The model injector manages the technique that Symba uses to model and
represent trigger sources. Basically, functions have to be replaced with a symbolic
equivalent counterpart. At the beginning of the flow, the JSON configuration file is
loaded and function signatures and options are stored inside some data structure.
45
Architecture design
Once this data is available, Symba generates a class that matches everything in
the signature of the API function: parameters name, type and calling convention.
The real difference between the two functions stands in what happens in the
function body. Whereas the original API function would have computed the value
requested or queried the resource pointed by the executable, the only thing that
a Model will do is to select – depending on the configuration file – some specific
parameters, lookup their memory address, and inject there a suitable symbol. This
will constitute the basics of the tainting process. For each injected symbol, a unique
key built using the configuration file name, the function name and the parameter
name is stored inside a global repository of keys. From that point on, every time
that the executable will try to access in read or write, in a branch condition, the
data that has been injected, this will be reflected in the constraints of the symbol.
This allows other components that need to monitor the list of constraints associated
to a particular symbol, like the TriggerSeer or the Solver, to properly work.
Having a component that handles automatic creation of trigger sources model
from the configuration – also automatically generated – is a valuable addition
to the project. In fact, without this component, the analyst should manually
develop code to represent each modeled function, probably incurring in errors or
redundancy. Moreover, it is entirely possible to customize the generation of the
model depending on the selected function. The JSON configuration file that is
given as input to the Injector, in fact, could be extended to add new options that
further personalize different trigger sources. One notable addition to this would be
a constraints option. This option would allow analysts to use a simple grammar to
define simple constraints on the function parameters. Let us consider, for instance,
the GetUserName function. Some analyst could decide that the malware will only
look for usernames that start with the letter ’B’. So, a constraint on the lpBuffer
parameter that stores the symbol for the generated username could be placed to
constrainv the first letter to be equal to ’B’.
Binary executable
CFGFast generation
5.4 TriggerSeer
The TriggerSeer is an important component in the execution of Symba. The
previous section documented the workflow of the State Extractor, which decides
where the symbolic execution will start. The TriggerSeer handles the opposite
aspect, precisely the termination condition of the execution. The reason for which
Symba needs this hides behind the same reasons already mentioned about the
weaknesses of symbolic execution. The main idea of the TriggerSeer is to continue
47
Architecture design
step
yes
Stop execution
executing as long as the program is interacting with the tainted symbols. At some
point, after a reasonable number of states where the symbol is not fetched in neither
read or write, the execution is deemed as complete.
TriggerSeer works with two principal data structures: the counter and the
registry. It is, at its core, an object defined in angr as an Exploration Technique.
These objects receive, for each step, the active states, and they decide what to
do with those states, and whether the execution is complete. In this case, the
operation performed on state is to check, for each active state, the constraints posed
on the injected symbol for the trigger source. At the very first step, the registry
is initialized with these constraints. In the following step, the new constraints
are checked: if anyone new is found, the registry is updated and the counter is
set to zero, since the injected symbol has been fetched. Otherwise, the counter
is incremented. As soon as the counter passes a certain configured threshold, the
execution is completed.
An extension, needed for the correct execution of the exploration technique,
has been applied. In particular, since for the reasons previously mentioned, the
execution starts from the function which actually calls the trigger source function,
for the initial states new constraints on the injected symbol cannot literally be
detected, since the symbol is not there yet. Therefore, TriggerSeer actually starts
setting the counter only after the unique key bound to the injected symbol has
48
Architecture design
been inserted in the global state plugin, that is, after the symbolic model of the
tracked function has been executed.
Execute symbolically
The Symbolic Explorator is essentially the glue code that connects the other
components together. It collects the command-line parameters, it builds the
configuration from where it will extract the trigger source names. For each trigger
source name, it invokes the State Extractor to retrieve starting states for the
execution. With those states, it will initialize an execution manager to which it
will apply the TriggerSeer exploration technique with the desired threshold. As
soon as the execution is complete – with the termination conditions previously
described – the explorator retrieves all the states collected during the execution.
At this point, it will filter each state, looking only for the "triggered" ones, which
is, states that are somehow constrained on the trigger sources. Each one of those
49
Architecture design
5.6 Solver
The last component described is also the final component in the workflow of
Symba: the Solver. Its task is to take all the triggered states which are generated
from the Symbolic Explorator, and solve them, producing concrete values. For this
task, it uses an SMT solver, Microsoft Z3, as a backend. The solver is capable of
handling both linear and not linear constraints, and it can produce raw bytes or
compat numbers as a result. Constraint are identified via the unique key that has
been injected by the Model Injector when the trigger source has been replaced.
The same key will identify the results in the output log. The key is readable and it
contains the name of the configuration file, the name of the trigger source function,
and the parameter injected.
In some cases, where the symbol does not have a precise structure, the solver has
to produce raw bytes that have to be manually inspected by the analyst. However,
for precisely defined structs, custom output function can be implemented to produce
a more readable result. This will be demonstrated in the evaluation chapter, where
50
Architecture design
51
Chapter 6
Implementation details
6.1 GenericModel
By leveraging the SimProcedure interface, Symba is capable to dynamically, auto-
matically generate new models on the fly - starting from a given configuration -
without the need for the analyst to implement them from scratch, a time-consuming
and repetitive task.
Specifically, Symba contains a GenericModel class that can model the majority
of trigger sources one could be interested in tracking. The implementation can be
found in the appendix at A.
A GenericModel abstracts the creation of symbolic procedures by exposing an
interface (via __init__) where the real difference stands in the function signatures
- named fsig in the code. In run method, the unpacking syntax ∗args is used to
represent a variable list of parameters - since different API functions will have a
different number of parameters - while the field fsig .params provides the method
body with a precise list of parameters, together with its name and type. The rest of
the function will be explored further, where the mechanism of symbolic constraints
injection will be handled.
Summing up, the GenericModel symbolic procedures constitutes an "abstract"
interface for configured trigger sources, lifting the analyst from having to implement
the symbolic replacement for API functions on a one-by-one basis.
6.2 SymbaConfig
As previously mentioned, the starting point of the config generation is a list of
function names. In this case, it would be:
52
Implementation details
1 $ cat a p i _ f i l e . txt
2 GetSystemTime
3 GetDiskFreeSpaceEx
The relevant code used to query Google and the MSDN and parse through
BeautifulSoup can be found in the appendix at A.
The workflow of the script follows this path:
1 c l a s s SymbaConfig ( o b j e c t ) :
2
3 d e f __init__ ( s e l f , c o n f i g _ f i l e ) :
4 self . config_file = config_file
5 s e l f . signatures = [ ]
6 s e l f . models = [ ]
7 s e l f . _parse_json ( )
8
53
Implementation details
13 with open ( s e l f . c o n f i g _ f i l e , ’ r ’ ) a s f :
14 d = json . load ( f )
15 f o r func in d [ ’ f u n c t i o n s ’ ] :
16 params = [ param ( ∗ ∗ p ) f o r p i n f u n c [ ’ params ’ ] ]
17 s e l f . s i g n a t u r e s . append ( s i g ( f u n c [ ’ name ’ ] , params ) )
The logic is clear: the JSON is parsed in method _parse_json - line 20 - and
the signatures are extracted. Thereafter, a list of models is filled with various
instances of GenericModel, each one representing a different Trigger Source. This
data structure will be then available to the main routine of the analysis code, as
shown in:
1 def _init_configuration ( s e l f ) :
2 f o r c o n f i g i n s e l f . _config_paths :
3 f o r model i n SymbaConfig ( c o n f i g ) . models :
4 s e l f . t r i g g e r s . append ( T r i g g e r S o u r c e ( model ) )
6.3 CFGFast
First of all, we should examine why the CFG is necessary at all in the process of
extracting trigger conditions from the associated sources. In fact, a naive solution
would consist in symbolically executing the executable starting from main, thus
touching all the trigger sources functions. This solution, despite being formally
correct, has the big drawback of impracticality: as discussed in the introductory
chapter, symbolic execution brings some disadvantages to the table, exposing a few
weak points to binaries, especially in the absence of source code - in fact, while
angr works at the binary level, other notable symbolic execution engines [34] need
the source code to provide more reliability to path explosion or divergence. For all
these reasons, an approach set and forget where the executable starts from main
thru its entire codebase will most likely end up being unsuccessful.
For this reason, Symba employs a different approach to start its symbolic
execution. The idea is based upon finding references to the functions defined
as trigger sources in the CFG and beginning symbolic execution just before that.
Specifically, the algorithm works as follows:
2. The functions referenced in the CFG are searched for configured trigger sources.
4. The list such constituted is returned with the name of call_points, used later
to compute starting addresses.
It should be noted that Symba is not looking for predecessor basic blocks, but
functions. In fact, starting symbolic execution from random points inside routine
code will probably lead to inconsistencies in local variables or stack, as already
demonstrated formerly in this section.
The relevant code is shown below:
1 try :
2 i f not s e l f . c f g :
3 s e l f . c f g : CFG = s e l f . p r o j e c t . a n a l y s e s .CFG( ∗ ∗
cfg_options )
4 f o r address , function in s e l f . cfg . f u n c t i o n s . items ( ) :
5 s e l f . l . debug ( ( a d d r e s s , f u n c t i o n . name ) )
6 try :
7 i f f u n c t i o n . name i n symbols o r not symbols :
8 s e l f . l . i n f o ( f " I nt er ce pte d c a l l to { func tion .
name} " )
9 pred = next (
10 iter (
11 s e l f . cfg . functions . callgraph .
predecessors (
12 address ) ) )
13 c a l l _ p o i n t s [ f u n c t i o n . name ] = pred
14 except StopIteration :
15 pass
16 except Exception as e :
17 s e l f . l . l o g ( l o g g i n g .ERROR, f " { e } " )
18 return call_points
Another notable mention in the code above is that CFG is persisted over different
executions of Symba, so to avoid having to recompute it each time losing time.
In this scenario, it is equivalent to a Singleton. Moreover, other than used to
find calling points, this function serves another purpose: it actually searches for
trigger sources in the binary. In fact, should call_points be empty, the analysis will
not start, thus lifting the system from losing computational time over executables
where seeked functions are not called anywhere.
exploration technique is that, once starting, we could not possibly continue until
exhaustion of the program state, for reasons highlighted before. Therefore, using a
blank run() is not suggested. Moreover, we do not possess any clear rule to apply
to the Explorer technique.
The crucial feature of the needed exploration technique is therefore to know
when the simulation manager has to return. A suitable stopping condition, or
termination criterion, must ensure that, once symbolic execution has began tracking
a certain trigger source, it collects all relevant constraints placed on its produced
value to unravel new paths. So, TriggerSeer models exactly this behavior. The
general flow is described below:
2. Each time the simulation manager steps forward a state, TriggerSeer checks
all the constraints connected to the registered trigger source. If any of them
is not in the registry yet, the state is marked as recently constrained.
3. If, in a step, no state in the active stash has been recently constrained, a
counter is incremented. Otherwise, the counter is assigned zero.
4. When the counter passes a certain threshold value, TriggerSeer stops execution.
The rationale of TriggerSeer is clear: the idea is that, when producing a value
with an API function call, its resulting value will be probably used by its consumer
code right after, in a function close in the callgraph. Therefore, we continue
executing as long as we see new constraints - that is, operations and branches -
applied on our tracked value. Once for a reasonable number of states our value is not
"touched" anymore, execution can stop and we can extract meaningful conditions
from the collected constraints.
The relevant code follows below:
1
2 d e f s t e p ( s e l f , simgr , s t a s h= ’ a c t i v e ’ , ∗∗ kwargs ) :
3 i f not any (
4 s e l f . _recently_constrained ( state )
5 f o r s t a t e in simgr . s t a s h e s [ stash ] ) :
6 s e l f . _count += 1
7 else :
8 s e l f . _count = 0
9
10 <s n i p >
56
Implementation details
11
12 def complete ( s e l f , simgr ) :
13 r e t u r n s e l f . _count >= s e l f . _ t h r e s h o l d
• The step function, in this case, is part of the interface needed to implement
a custom exploration technique. It implements what happens when angr
executes step on a simulation manager.
• The complete boolean function signals to angr whether a simulation manager
should stop its execution.
• The _recently_constrained function will be discussed later when describing how
to interact with the solver state plugin.
In this section, most of the core ideas in Symba have been discussed in details,
digging down into implementation code and examples. First, fundamentals on how
to create a Control Flow Graph, explore its nodes and extract states from it have
been depicted. Those states can be used to initialize a simulation manager, the
top-level component responsible for every execution-related task, which behavior
can be modified and tweaked using built-in and custom exploration techniques,
modules which collect states in stash and regulate how states are dropped in each
stash, together with defining and respecting stopping criteria. The core of symbolic
analysis, that is, creating symbols, has been elaborated in both injecting symbol
into memory and later solving them.
1 c a l l _ a d d r = r e s [ t r i g g e r . name ]
2 b = s e l f . p r o j e c t . f a c t o r y . b l o c k ( addr=c a l l _ a d d r )
3 s t a r t _ s t a t e = s e l f . p r o j e c t . f a c t o r y . e n t r y _ s t a t e ( addr=b . addr )
4 sm = s e l f . p r o j e c t . f a c t o r y . simulation_manager ( s t a r t _ s t a t e )
5
6 sm . u s e _ t e c h n i q u e ( T r i g g e r S e e r (
7 ( t r i g g e r . model . c o n f i g , t r i g g e r . model . name ) ) )
8
9 sm . run ( )
10
11 f o r s t a t e i n sm . deadended + sm . a c t i v e + sm . u n c o n s t r a i n e d :
12 i f trigger . is_triggered ( state ) :
13 t r i g g e r . s t a t e s . append ( s t a t e )
57
Implementation details
This part of the code is almost straightforward. For each trigger source, the
saved call_addr is retrieved and used to create a new state (line 3). This simulation
manager is executed through a call to run. Actually, there are a few special things
that should be highlighted in the code:
58
Chapter 7
Evaluation
Symba has been evaluated by executing it, using simple and lightweight configu-
rations, against a proof of concept code and two real-world examples containing
trigger conditions of different kinds. For each sample, in this prototype, the re-
sults of the tool have been examined manually to extract meaningful information.
Then, the sample is executed first in a generic virtual machine with no custom
configuration, and afterwards, in a scenario where the extracted trigger conditions
are met. Interactions of real malware with the OS are monitored via different
methods, like watching debugging output or via the already mentioned Sysinternals
ProcessMonitor tool, to observe the shape of new routines in the malware execution
flow.
The three commands hide different routines that model what could happen in a
real-world scenario.
1. Command 1. Malware reads the value of the "run" registry key, searching for
installed startup programs.
2. Command 2. Malware enumerates processes currently running.
3. Command 3. Malware lists filenames in the Documents folder, possibly looking
for files to exfiltrate.
The sample has been compiled using the i686−w64−mingw32−g++ tool part of
the MinGW toolchain. The configuration file has been generated starting from the
recv API function, and the buf parameter in the signature has been configured with
inject boolean field set to true.
The whole Symba run took 4m2s, and produced four different results which you
can observe in figure 7.3. The string covered in the black square represents the
recv input where no command is executed, while the red ones represent the three
different inputs associated to the malware commands.
60
Evaluation
It is worth noting that, as demonstrated by the byte editor xxd, the command
sent to the server are composed exactly of the sequence of bytes extracted by
Symba.
7.3 Wrathrage
Wrathrage is a worm which source code has been leaked on github in the kaiserfar-
rell (https://github.com/kaiserfarrell/malware) malware repository. Once
executed in the target machine, it will try to send itself towards other nodes via
mail or other messaging systems, and it will try to cause some damage on the host
depending on the date of execution. In some dates, just a text message will be
shown to the user, while in other, unfortunate dates, the malware will attempt to
overwrite every file in the filesystem, basically breaking the victim machine up to
an unusable state.
To conduct this experiment, we have compiled the source code of wrathrage
with the MinGW toolchain, so that we had a binary that Symba could analyze.
The latter has been configured using the GetSystemTime function as trigger source
– since we are handling dates – where the lpSystemTime has been configured to be
injected, and its length has been set to 16 bytes, 8 WORDs of 2 bytes each. This
modification is reflected in Symba logs, as shown in figure 7.13. Moreover, in this
case, where the symbol has a precise and predetermined structure – it inherits
SYSTEMTIME properties – the constraint solving routine has been extended to
63
Evaluation
In order to confirm where these conditions lead to, it would be normally necessary
to execute the sample, as we did with the previous experiments, and observe the
output. However, in this case, the MinGW compiler apparently produced an invalid
binary that, in the virtual machine used for the experiments, is not executed
correctly. Nevertheless, we can quickly, for the sake of this experiment, confirm
the legitimacy of these results by consulting the source code – which in this case is
available – to inspect the function where the GetSystemTime function is called. The
relevant snippet is listed below.
1 void executePayload ( ) {
2 // payload o f t h e worm w i l l change depending o f t h e day .
3 SYSTEMTIME x ;
4 GetSystemTime(&x ) ;
5 i f ( ( x . wDay == 1 ) && ( x . wMonth == 1 1 ) ) {
6 // t h i s i s a v e r y s p e c i a l d a t e f o r me . i t p r o b a b l e r e p r e s e n t s
why i make t h i s worm
7 //why i h a t e some t h i n g s . . why . . a l o t o f t h i n g s . time t o be
a SOB .
8 f u c k A l l ( ) ; // f u c k a l l
9 }
10 else {
11 i f ( ( x . wDay == 3 ) | | ( x . wDay == 9 ) ) {
12 // i l i k e number 3 , and number 9 .
13 defaultPayload () ;
64
Evaluation
Figure 7.12: Snippet of pafish execution with trigger conditions inserted in target
environment.
14 }
This listing clearly demonstrate that Symba has been able to extract all the
relevant trigger conditions from the malware, without needing to consult the source
code, which as already mentioned is a rare luxury when dealing with malware
analysis. It represented both the condition protecting the if with an OR branch,
and the one protecting the if with an AND branch, basically uncovering all the
triggers connected to the GetSystemTime function.
7.4 Limitations
There are some limitations that could not be addressed in the implementation and
design of Symba. Some of these limitations depend inherently on the techniques
employed, while others are part of the test environment that was prepared for
65
Evaluation
Figure 7.13: Snippet from Symba logs in wrathrage analysis, showing symbol
length override.
Symba.
7.4.1 OS interaction
As already described in the background chapter, one of the greatest challenges
posed in symbolic execution comes from the interaction with the Operating System
and, more general, the software stack involved. For instances, many interaction
with the Windows API are chained as follows:
without dealing with the operating system. For Linux, angr already models the
majority of system calls and interfaces exposed by the glibc. However, the same
does not stand for the Windows OS. The support for the API that the framework
offers is not complete, and often the programmer has to implement the required
calls. In a binary-agnostic system like Symba, it is not possible to predict which
API calls will be invoked. For this reason, executing the sample from the beginning
and waiting to encounter the trigger sources is not feasible, because most likely
the execution will not be consistent, errors will be raised, or the memory usage
will grow with no control due to path explosion. In order to solve this, one should
extend the support that angr provides for Windows, or switch to another symbolic
execution engine with a tighter integration with the operating system.
• The trigger condition extracted may be hidden inside an outer trigger condition
that Symba is not tracking. For example, we may be looking to extract the
command suite for a RAT server listening on the wire, so we track functions
of the recv family. However, before actually starting the server, the malware
checks for virtualization in the target environment.
• Values used inside API function calls may not be local to the starting function,
and they could have other outer constraints posed on them. For example, a
GetUserName call could produce usernames that are checked for a particular
user, but this value is saved inside a global variable that is checked in some
other part of the code not encountered by the exploration process.
Both the issues above depend from the issues that symbolic execution yields
in executing an entire binary file end-to-end. Other approaches, like a mixed
dynamic-symbolic execution, could help solve this problem.
packets, system time, and information on the username. In this sense, they serve
well as case studies. However, a better approach to test and improve the system
could be more automated, with a flow designed like follows.
However, most of the samples incoming from these feeds, nowadays, present
some issues to this process that made unfeasible to properly setup this testbed
during the time spent working on this thesis. Most notably, malware does not come
in its ultimate, malicious form so easily. Obfuscation, packing, dynamic loading,
loading dlls from the network are just some examples of the techniques used to
hinder. Those samples resulted to be too noisy for Symba to properly extract
conditions. In order to overcome this, the tool could be best integrated into a
pre-existent sandbox framework that would use Symba to extract conditions from
the final, dumped, unpacked and unencrypted sample that would eventually go
into the hands of the malware analyst. In this direction, the usefulness of a system
like Symba stands in its capability to be able to reason over trigger conditions in
generally less time than what would take to a human, when they would both start
from a somewhat "clean" executable. We mention that some research has been
dedicated to the question of symbolic execution with respect to self-modifying code
[35] and obfuscation aimed exactly towards hindering symbolic execution [36].
68
Chapter 8
Related Work
70
Chapter 9
Conclusions
In normal scenarios, extracting trigger conditions guarding suspicious behaviour
requires manual analysis, leading to consumption of time and effort. In this work,
we present Symba, a targeted symbolic execution system that tracks and solves
appropriate inputs to unlock new and interesting behaviour in binary executables.
By combining custom techniques to extract starting and ending points, track
interesting symbols, and solve their values, symbolic execution was used in a way
that mitigated its typical weaknesses, rendering possible to implement a tool that
still completely works in the land of static analyses, with no need for a dynamic
environment that would introduce other problems in the design. The system has
been capable of extracting and solving trigger conditions for both proof of concept
code and real samples, with an average execution time of 2 minutes. Moreover,
while the system has been tested on Windows, the high adaptability of angr can be
exploited to have a single tool producing results for malwares of different platforms
and architectures. The modularity of the system makes it easy to extend and
improve its features, and the use of Python, a highly employed programming
language in the malware analysis field, as the language of choice, makes it easier to
integrate Symba in already operating infrastructures and tools.
72
Appendix A
Code
This appendix contains some snippets of code that are pointed, mostly, in the
implementation details section.
Listing A.1: Test code, version 1.
1 #i n c l u d e <s t d i o . h>
2
3 i n t func ( i n t n , char ∗ s t r i n g , char ∗ other_string ) {
4 p r i n t f ( " This s h o u l d n ’ t be p r i n t e d i f hooked ! \ n " ) ;
5 }
6 i n t main ( i n t argc , c h a r ∗ argv [ ] ) {
7 f u n c ( 1 0 , "ARG1" , "ARG2" ) ;
8 }
12 # P r i n t s output c o l l e c t e d t o s t d o u t
13 f i n a l _ s t a t e = s i m g r . deadended [ 0 ]
14 p r i n t ( " ∗∗∗∗∗∗STDOUT CONTENT∗∗∗∗∗∗ " )
15 f i n a l _ s t a t e . p o s i x . dumps ( 1 )
73
Code
12 // Choose f u n c t i o n
13 i f ( c h o i c e == 1 ) {
14 func1 (1) ;
15 }
16
17 e l s e i f ( c h o i c e == 2 ) {
18 func2 (2 , arg1 ) ;
19 }
20
21 e l s e i f ( c h o i c e == 3 ) {
22 f u n c 3 ( 3 , arg1 , a r g 2 ) ;
23 }
24 else {
25 exit (1) ;
26 }
27 }
74
Code
19 si m g r = p r o j . f a c t o r y . simulation_manager ( )
20
21 # Step u n t i l a l l s t a t e s t e r m i n a t e
22 si m g r . run ( )
23
24 # P r i n t s output c o l l e c t e d t o s t d o u t
25 f i n a l _ s t a t e = s i m g r . deadended [ 0 ]
26 p r i n t ( " ∗∗∗∗∗∗STDOUT CONTENT∗∗∗∗∗∗ " )
27 p r i n t ( f i n a l _ s t a t e . p o s i x . dumps ( 1 ) )
75
Code
4 funcs = [ l i n e . s t r i p () for l i n e in f ]
5
6 msdns = [ ]
7 f o r funcname i n f u n c s :
8 query = " s i t e : d o c s . m i c r o s o f t . com {} " . format ( funcname )
9
10 for s in search (
11 query ,
12 t l d= ’ i t ’ ,
13 l a n g= ’ en ’ ,
14 t b s= ’ 0 ’ ,
15 s a f e= ’ o f f ’ ,
16 num=2,
17 s t a r t =0,
18 s t o p =2,
19 domains=None ,
20 pause =2.0 ,
21 t p e= ’ ’ ,
22 c o u n t r y= ’ ’ ,
23 extra_params=None ,
24 user_agent=None ) :
25 i f ’ win32 / a p i / ’ i n s and funcname . l o w e r ( ) i n s :
26 msdns . append ( s )
27
28 out = {}
29 out [ ’ f u n c t i o n s ’ ] = [ ]
30 # Matter o f time and r e a d a b i l i t y man
31 f u n c t i o n s = out [ ’ f u n c t i o n s ’ ]
32
33 f o r msdn i n msdns :
34 soup = B e a u t i f u l S o u p ( r e q u e s t s . g e t ( msdn ) . t e x t , ’ html . p a r s e r ’ )
35 code = soup . f i n d ( ’ code ’ ) . g e t _ t e x t ( ) . s p l i t ( ’ \n ’ ) [ : − 2 ]
36
37 # S h a l l we b e g i n ? Let ’ s p a r s e t h i s code !
38
39 function_name = code [ 0 ] . s p l i t ( ) [ 1 ] [ : − 1 ]
40 params = [ ]
41
76
Code
77
Bibliography
[1] Benjamin Jensen, Brandon Valeriano, and Ryan Maness. «Fancy bears and
digital trolls: Cyber strategy with a Russian twist». In: Journal of Strategic
Studies (Jan. 2019), pp. 1–23. doi: 10.1080/01402390.2018.1559152 (cit.
on p. 2).
[2] David Brumley, Cody Hartwig, Zhenkai Liang, James Newsome, Dawn Song,
and Heng Yin. «Automatically Identifying Trigger-based Behavior in Mal-
ware». In: vol. 36. Jan. 1970, pp. 65–88. doi: 10.1007/978-0-387-68768-1_4
(cit. on p. 3).
[3] Yan Shoshitaishvili et al. «SOK: (State of) The Art of War: Offensive Tech-
niques in Binary Analysis». In: May 2016, pp. 138–157. doi: 10.1109/SP.
2016.17 (cit. on p. 7).
[4] Hui Xu, Zirui Zhao, Yangfan Zhou, and Michael Lyu. «On Benchmarking
the Capability of Symbolic Execution Tools with Logic Bombs». In: IEEE
Transactions on Dependable and Secure Computing PP (Dec. 2017). doi:
10.1109/TDSC.2018.2866469 (cit. on p. 8).
[5] Zainab Saud and M. Hasan Islam. «Towards Proactive Detection of Advanced
Persistent Threat (APT) Attacks Using Honeypots». In: Proceedings of the
8th International Conference on Security of Information and Networks. SIN
’15. Sochi, Russia: Association for Computing Machinery, 2015, pp. 154–
157. isbn: 9781450334532. doi: 10.1145/2799979.2800042. url: https:
//doi.org/10.1145/2799979.2800042 (cit. on p. 11).
[6] Samuel Greengard. «The New Face of War». In: Commun. ACM 53.12 (Dec.
2010), pp. 20–22. issn: 0001-0782. doi: 10.1145/1859204.1859212. url:
https://doi.org/10.1145/1859204.1859212 (cit. on p. 11).
[7] Giuseppe Primiero, Frida Solheim, and Jonathan Spring. «On Malfunction,
Mechanisms and Malware Classification». In: Philosophy & Technology (Nov.
2018). doi: 10.1007/s13347-018-0334-2 (cit. on p. 12).
78
BIBLIOGRAPHY
[8] Alan Lee, Vijay Varadharajan, and Udaya Tupakula. «On Malware Charac-
terization and Attack Classification». In: Proceedings of the First Australasian
Web Conference - Volume 144. AWC ’13. Adelaide, Australia: Australian
Computer Society, Inc., 2013, pp. 43–47. isbn: 9781921770296 (cit. on p. 12).
[9] Yunting Guo and Wenqing Fan. «Feature Collection and Selection in Malware
Classification». In: Proceedings of the 2019 International Conference on
Artificial Intelligence and Advanced Manufacturing. AIAM 2019. Dublin,
Ireland: Association for Computing Machinery, 2019. isbn: 9781450372022.
doi: 10 . 1145 / 3358331 . 3358342. url: https : / / doi . org / 10 . 1145 /
3358331.3358342 (cit. on p. 12).
[10] Ivan Nikolaev, Martin Grill, and Veronica Valeros. «Exploit Kit Website
Detection Using HTTP Proxy Logs». In: Proceedings of the Fifth Inter-
national Conference on Network, Communication and Computing. ICNCC
’16. Kyoto, Japan: Association for Computing Machinery, 2016, pp. 120–
125. isbn: 9781450347938. doi: 10.1145/3033288.3033354. url: https:
//doi.org/10.1145/3033288.3033354 (cit. on p. 12).
[11] Ondřej Pluskal. «Behavioural Malware Detection Using Efficient SVM Imple-
mentation». In: Proceedings of the 2015 Conference on Research in Adaptive
and Convergent Systems. RACS. Prague, Czech Republic: Association for Com-
puting Machinery, 2015, pp. 296–301. isbn: 9781450337380. doi: 10.1145/
2811411.2811516. url: https://doi.org/10.1145/2811411.2811516
(cit. on p. 12).
[12] Guy Martin, Saira Ghafur, James Kinross, Chris Hankin, and Ara Darzi.
«WannaCry—a year on». In: BMJ 361 (June 2018), k2381. doi: 10.1136/
bmj.k2381 (cit. on p. 13).
[13] Rami Sihwail, Khairuddin Omar, and Khairul Akram Zainol Ariffin. «A
survey on malware analysis techniques: static, dynamic, hybrid and memory
analysis». In: 8 (Jan. 2018), p. 1662. doi: 10.18517/ijaseit.8.4-2.6827
(cit. on p. 13).
[14] Roman Rohleder. «Hands-On Ghidra - A Tutorial about the Software Reverse
Engineering Framework». In: Nov. 2019, pp. 77–78. isbn: 978-1-4503-6835-3.
doi: 10.1145/3338503.3357725 (cit. on p. 13).
[15] Dhruwajita Devi and Sukumar Nandi. «Detection of Packed Malware». In:
Proceedings of the First International Conference on Security of Internet of
Things. SecurIT ’12. Kollam, India: Association for Computing Machinery,
2012, pp. 22–26. isbn: 9781450318228. doi: 10.1145/2490428.2490431. url:
https://doi.org/10.1145/2490428.2490431 (cit. on pp. 13, 20).
79
BIBLIOGRAPHY
[16] Marco Gaudesi, Andrea Marcelli, Ernesto Sanchez, Giovanni Squillero, and Al-
berto Tonda. «Malware Obfuscation through Evolutionary Packers». In: Pro-
ceedings of the Companion Publication of the 2015 Annual Conference on Ge-
netic and Evolutionary Computation. GECCO Companion ’15. Madrid, Spain:
Association for Computing Machinery, 2015, pp. 757–758. isbn: 9781450334884.
doi: 10 . 1145 / 2739482 . 2764940. url: https : / / doi . org / 10 . 1145 /
2739482.2764940 (cit. on p. 13).
[17] Andrey Mikhailov, Aleksey Hmelnov, Evgeny Cherkashin, and Igor Bychkov.
«Control flow graph visualization in compiled software engineering». In: May
2016, pp. 1313–1317. doi: 10.1109/MIPRO.2016.7522343 (cit. on p. 14).
[18] Zhuojun Ren, Guang Chen, and Wenke Lu. «Space Filling Curve Mapping
for Malware Detection and Classification». In: Proceedings of the 2020 3rd
International Conference on Computer Science and Software Engineering.
CSSE 2020. Beijing, China: Association for Computing Machinery, 2020,
pp. 176–180. isbn: 9781450375528. doi: 10.1145/3403746.3403924. url:
https://doi.org/10.1145/3403746.3403924 (cit. on p. 14).
[19] Waqas Aman. «A Framework for Analysis and Comparison of Dynamic
Malware Analysis Tools». In: International Journal of Network Security & Its
Applications 6 (Oct. 2014). doi: 10.5121/ijnsa.2014.6505 (cit. on p. 14).
[20] Ori Or-Meir, Nir Nissim, Yuval Elovici, and Lior Rokach. «Dynamic Malware
Analysis in the Modern Era—A State of the Art Survey». In: ACM Comput.
Surv. 52.5 (Sept. 2019). issn: 0360-0300. doi: 10.1145/3329786. url: https:
//doi.org/10.1145/3329786 (cit. on p. 14).
[21] Amir Afianian, Salman Niksefat, Babak Sadeghiyan, and David Baptiste.
«Malware Dynamic Analysis Evasion Techniques: A Survey». In: ACM Comput.
Surv. 52.6 (Nov. 2019). issn: 0360-0300. doi: 10.1145/3365001. url: https:
//doi.org/10.1145/3365001 (cit. on p. 18).
[22] Melissa Chua and Vivek Balachandran. «Effectiveness of Android Obfusca-
tion on Evading Anti-Malware». In: Proceedings of the Eighth ACM Confer-
ence on Data and Application Security and Privacy. CODASPY ’18. Tempe,
AZ, USA: Association for Computing Machinery, 2018, pp. 143–145. isbn:
9781450356329. doi: 10.1145/3176258.3176942. url: https://doi.org/
10.1145/3176258.3176942 (cit. on p. 19).
[23] Sebastian Schrittwieser, Stefan Katzenbeisser, Johannes Kinder, Georg Merz-
dovnik, and Edgar Weippl. «Protecting Software through Obfuscation: Can
It Keep Pace with Progress in Code Analysis?» In: ACM Comput. Surv.
49.1 (Apr. 2016). issn: 0360-0300. doi: 10 . 1145 / 2886012. url: https :
//doi.org/10.1145/2886012 (cit. on p. 19).
80
BIBLIOGRAPHY
[24] Pascal Junod, Julien Rinaldini, Johan Wehrli, and Julie Michielin. «Obfuscator-
LLVM: Software Protection for the Masses». In: Proceedings of the 1st Inter-
national Workshop on Software Protection. SPRO ’15. Florence, Italy: IEEE
Press, 2015, pp. 3–9 (cit. on p. 19).
[25] Blake Anderson, Curtis Storlie, and Terran Lane. «Improving Malware Clas-
sification: Bridging the Static/Dynamic Gap». In: Proceedings of the 5th
ACM Workshop on Security and Artificial Intelligence. AISec ’12. Raleigh,
North Carolina, USA: Association for Computing Machinery, 2012, pp. 3–
14. isbn: 9781450316644. doi: 10 . 1145 / 2381896 . 2381900. url: https :
//doi.org/10.1145/2381896.2381900 (cit. on p. 21).
[26] Cristian Cadar and Koushik Sen. «Symbolic Execution for Software Testing:
Three Decades Later». In: Commun. ACM 56.2 (Feb. 2013), pp. 82–90. issn:
0001-0782. doi: 10.1145/2408776.2408795. url: https://doi.org/10.
1145/2408776.2408795 (cit. on p. 21).
[27] Roberto Baldoni, Emilio Coppa, Daniele Cono D’elia, Camil Demetrescu,
and Irene Finocchi. «A Survey of Symbolic Execution Techniques». In: ACM
Comput. Surv. 51.3 (May 2018). issn: 0360-0300. doi: 10.1145/3182657.
url: https://doi.org/10.1145/3182657 (cit. on pp. 21–23).
[28] Gideon Redelinghuys, Willem Visser, and Jaco Geldenhuys. «Symbolic Ex-
ecution of Programs with Strings». In: Proceedings of the South African
Institute for Computer Scientists and Information Technologists Conference.
SAICSIT ’12. Pretoria, South Africa: Association for Computing Machinery,
2012, pp. 139–148. isbn: 9781450313087. doi: 10.1145/2389836.2389853.
url: https://doi.org/10.1145/2389836.2389853 (cit. on p. 22).
[29] Aric Hagberg, Pieter Swart, and Daniel Chult. «Exploring Network Structure,
Dynamics, and Function Using NetworkX». In: Jan. 2008 (cit. on p. 28).
[30] Duc-Hiep Chu, Joxan Jaffar, and Rasool Maghareh. «Symbolic Execution
for Memory Consumption Analysis». In: SIGPLAN Not. 51.5 (June 2016),
pp. 62–71. issn: 0362-1340. doi: 10.1145/2980930.2907955. url: https:
//doi.org/10.1145/2980930.2907955 (cit. on pp. 34, 69).
[31] Prateek Saxena, Pongsin Poosankam, Stephen McCamant, and Dawn Song.
«Loop-Extended Symbolic Execution on Binary Programs». In: Proceedings
of the Eighteenth International Symposium on Software Testing and Analysis.
ISSTA ’09. Chicago, IL, USA: Association for Computing Machinery, 2009,
pp. 225–236. isbn: 9781605583389. doi: 10.1145/1572272.1572299. url:
https://doi.org/10.1145/1572272.1572299 (cit. on p. 34).
81
BIBLIOGRAPHY
[32] Thanassis Avgerinos, Alexandre Rebert, Sang Kil Cha, and David Brumley.
«Enhancing Symbolic Execution with Veritesting». In: Proceedings of the 36th
International Conference on Software Engineering. ICSE 2014. Hyderabad,
India: Association for Computing Machinery, 2014, pp. 1083–1094. isbn:
9781450327565. doi: 10.1145/2568225.2568293. url: https://doi.org/
10.1145/2568225.2568293 (cit. on p. 34).
[33] Vaibhav Sharma, Michael W. Whalen, Stephen McCamant, and Willem
Visser. «Veritesting Challenges in Symbolic Execution of Java». In: SIGSOFT
Softw. Eng. Notes 42.4 (Jan. 2018), pp. 1–5. issn: 0163-5948. doi: 10.1145/
3149485.3149491. url: https://doi.org/10.1145/3149485.3149491
(cit. on p. 34).
[34] Cristian Cadar, Daniel Dunbar, and Dawson Engler. «KLEE: Unassisted
and Automatic Generation of High-Coverage Tests for Complex Systems
Programs». In: Proceedings of the 8th USENIX Conference on Operating
Systems Design and Implementation. OSDI’08. San Diego, California: USENIX
Association, 2008, pp. 209–224 (cit. on p. 54).
[35] Lian Li, Yi Lu, and Jingling Xue. «Dynamic Symbolic Execution for Polymor-
phism». In: Proceedings of the 26th International Conference on Compiler Con-
struction. CC 2017. Austin, TX, USA: Association for Computing Machinery,
2017, pp. 120–130. isbn: 9781450352338. doi: 10.1145/3033019.3033029.
url: https://doi.org/10.1145/3033019.3033029 (cit. on p. 68).
[36] Sebastian Banescu, Christian Collberg, Vijay Ganesh, Zack Newsham, and
Alexander Pretschner. «Code Obfuscation against Symbolic Execution At-
tacks». In: Proceedings of the 32nd Annual Conference on Computer Security
Applications. ACSAC ’16. Los Angeles, California, USA: Association for Com-
puting Machinery, 2016, pp. 189–200. isbn: 9781450347716. doi: 10.1145/
2991079.2991114. url: https://doi.org/10.1145/2991079.2991114
(cit. on p. 68).
[37] Thanassis Avgerinos, Sang Cha, Brent Hao, and David Brumley. «AEG:
Automatic Exploit Generation.» In: vol. 57. Jan. 2011. doi: 10.1145/2560217.
2560219 (cit. on p. 69).
[38] Nick Stephens, John Grosen, Christopher Salls, Andrew Dutcher, Ruoyu Wang,
Jacopo Corbetta, Yan Shoshitaishvili, Christopher Kruegel, and Giovanni
Vigna. «Driller: Augmenting Fuzzing Through Selective Symbolic Execution».
In: Jan. 2016. doi: 10.14722/ndss.2016.23368 (cit. on p. 69).
[39] Moritz Eckert, Antonio Bianchi, Ruoyu Wang, Yan Shoshitaishvili, Christo-
pher Kruegel, and Giovanni Vigna. «Heaphopper: Bringing Bounded Model
Checking to Heap Implementation Security». In: Proceedings of the 27th
82
BIBLIOGRAPHY
83
BIBLIOGRAPHY
84