preroll music
Herald: Our next speaker for today is a computer science PhD student at UC Santa
Barbara. He is a member of the Shellfish Hacking Team and he's also the organizer
of the IECTF Hacking Competition. Please give a big round of applause to Nilo
Redini.
applause
Nilo: Thanks for the introduction, hello to everyone. My name is Nilo, and today
I'm going to present you my work Koronte: identifying multi-binary vulnerabilities
in embedded firmware at scale. This work is a co-joint effort between me and
several of my colleagues at University of Santa Barbara and ASU. This talk is going
to be about IoT devices. So before starting, let's see an overview about IoT
devices. IoT devices are everywhere. As the research suggests, they will reach the
20 billion units by the end of the next year. And a recent study conducted this
year in 2019 on 16 million households showed that more than 70 percent of homes
in North America already have an IoT network connected device. IoT devices make
everyday life smarter. You can literally say "Alexa, I'm cold" and Alexa will
interact with the thermostat and increase the temperature of your room. Usually the
way we interact with the IoT devices is through our smartphone. We send a request
to the local network, to some device, router or door lock, or we might send the
same request through a cloud endpoint, which is usually managed by the vendor of
the IoT device. Another way is through the IoT hubs, smartphone will send the request
to some IoT hub, which in turn will send the request to some other IoT devices. As
you can imagine, IoT devices use and collect our data and some data is more
sensitive than other. For instance, think of all the data that is collected by my
lightbulb or data that is collected by our security camera. As such, IoT devices can
compromise people's safety and privacy. Things, for example, about the security
implication of a faulty smartlock or the brakes of your smart car. So the question
that we asked is: Are IoT devices secure? Well, like everything else, they are not.
OK, in 2016 the Mirai botnet compromised and leveraged millions of IoT devices to
disrupt core Internet services such as Twitter, GitHub and Netflix. And in 2018,
154 vulnerabilities affecting IoT devices were published, which represented an
increment of 15% compared to 2017 and an increase of 115% compared to 2016. So then
we wonder: So why is it hard to secure IoT devices? To answer this question we have
to look up how IoT devices work and they are made. Usually when you remove all the
plastic and peripherals IoT devices look like this. A board with some chips laying
on it. Usually you can find the big chip, the microcontroller which runs the
firmware and one or more peripheral controllers which interact with external
peripherals such as the motor of, your smart lock or cameras. Though the design
is generic, implementations are very diverse. For instance, firmware may run on
several different architectures such as ARM, MIPS, x86, PowerPC and so forth. And
sometimes they are even proprietary, which means that if a security analyst wants to
understand what's going on in the firmware, he'll have a hard time if he
doesn't have the vendor specifics. Also, they're operating in environments with
limited resources, which means that they run small and optimized code. For
instance, vendors might implement their own version of some known algorithm in an
optimized way. Also, IoT devices manage external peripherals that often use custom
code. Again, with peripherals we mean like cameras, sensors and so forth. The
firmware of IoT devices can be either Linux based or a blob firmware, Linux
based are by far the most common. A study showed that 86% of firmware are based on
Linux and on the other hand, blobs firmware are usually operating systems and
user applications packaged in a single binary. In any case, firmware samples are
usually made of multiple components. For instance, let's say that you have your
smart phone and you send a request to your IoT device. This request will be received
by a binary which we term as body binary, which in this example is an webserver. The
request will be received, parsed, and then it might be sent to another binary code,
the handler binary, which will take the request, work on it, produce an answer,
send it back to the webserver, which in turn would produce a response to send to
the smartphone. So to come back to the question why is it hard to secure IoT
devices? Well, the answer is because IoT devices are in practice very diverse. Of
course, there have been various work that have been proposed to analyze and secure
firmware for IoT devices. Some of them using static analysis. Others using
dynamic analysis and several others using a combination of both. Here I wrote
several of them. Again at the end of the presentation there is a bibliography with
the title of these works. Of course, all these approaches have some problems. For
instance, the current dynamic analysis are hard to apply to scale because of the
customized environments that IoT devices work on. Usually when you try to
dynamically execute a firmware, it's gonna check if the peripherals are connected and
are working properly. In a case where you can't have the peripherals, it's gonna be
hard to actually run the firmware. Also current static analysis approaches are
based on what we call the single binary approach, which means that binaries from a
firmware are taken individually and analysed. This approach might produce many
false positives. For instance, so let's say again that we have our two binaries.
This is actually an example that we found on one firmware, so the web server will
take the user request, will parse the request and produce some data, will set
this data to an environment variable and eventually will execute the handle binary.
Now, if you see the parsing function contains a string compare which checks if
some keyword is present in the request. And if so, it just returns the whole
request. Otherwise, it will constrain the size of the request to 128 bytes and
return it. The handler binary in turn when spawned will receive the data by doing a
getenv on the query string, but also will getenv on another environment variable
which in this case is not user controlled and they user cannot influence the content
of this variable. Then it's gonna call function process_request. This function
eventually will do two string copies. One from the user data, the other one from the
log path on two different local variables that are 128 bytes long. Now in the first
case, as we have seen before, the data can be greater than 128 bytes and this string
copy may result in a bug. While in the second case it will not. Because here we
assume that the system handles its own data in a good manner. So throughout this
work, we're gonna call the first type of binary, the setter binary, which means
that it is the binary that takes the data and set the data for another binary to be
consumed. And the second type of binary we called them the getter binary. So the
current bug finding tools are inadequate because other bugs are left undiscovered
if the analysis only consider those binaries that received network requests or
they're likely to produce many false positives if the analysis considers all of
them individually. So then we wonder how these different components actually
communicate. They communicate through what are called interprocess communication,
which basically it's a finite set of paradigms used by binaries to communicate
such as files, environment variables, MMIO and so forth. All these pieces are
represented by data keys, which are file names, or in the case of the example
before here on the right, it's the query string environment variable. Each binary
that relies on some shared data must know the endpoint where such data will be
available, for instance, again, like a file name or like even a socket endpoint
or the environment variable. This means that usually, data keys are coded in the
program itself, as we saw before. To find bugs in firmware, in a precise manner, we
need to track how user data is introduced and propagated across the different
binaries. Okay, let's talk about our work. Before you start talking about Karonte, we
define our threat model. We hypotesized that attacker sends arbitrary requests
over the network, both LAN and WAN directly to the IoT device. Though we said
before that sometimes IoT device can communicate through the clouds, research
showed that some form of local communication is usually available, for
instance, during the setup phase of the device. Karonte is defined as a static
analysis tool that tracks data flow across multiple binaries, to find
vulnerabilities. Let's see how it works. So the first step, Karonte find those
binaries that introduce the user input into the firmware. We call these border
binaries, which are the binaries, that basically interface the device to the
outside world. Which in the example is our web server. Then it tracks how a data is
shared with other binaries within the firmware sample. Which we'll understand in
this example, the web server communicates with the handle binary, and builds what we
call the BDG. BDG which stands for binary dependency graph. It's basically a graph
representation of the data dependencies among different binaries. Then we detect
vulnerabilities that arise from the misuse of the data using the BDG. This is an
overview of our system. We start by taking a packed firmware, we unpack it. We find
the border binaries. Then we build the binary dependency graph, which relies on a
set of CPFs, as we will see soon. CPF stands for Communication Paradigm Finder.
Then we find the specifics of the communication, for instance, like the
constraints applied to the data that is shared through our module multi-binary
data-flow analysis. Eventually we run our insecure interaction detection module,
which basically takes all the information and produces alerts. Our system is
completely static and relies on our static taint engine. So let's see each one of
these steps, more in details. The unpacking procedure is pretty easy, we use
the off-the-shelf firmware unpacking tool binwalk. And then we have to find the
border binaries. Now we see that border binaries basically are binaries that
receive data from the network. And we hypotesize that will contain parsers to
validate the data that they received. So in order to find them, we have to find
parsers which accept data from network and parse this data. To find parsers we rely
on related work, which basically uses a few metrics and define through a number
the likelihood for a function to contain parsing capabilities. These metrics that
we used are number of basic blocks, number of memory comparison operations and number
of branches. Now while these define parsers, we also have to find if a binary
takes data from the network. As such, we define two more metrics. The first one, we
check if binary contains any network related keywords as SOAP, http and so
forth. And then we check if there exists a data flow between read from socket and a
memory comparison operation. Once for each function, we got all these metrics, we
compute what is called a parsing score, which basically is just a sum of products.
Once we got a parsing score for each function in a binary, we represent the
binary with its highest parsing score. Once we got that for each binary in the
firmware we cluster them using the DBSCAN density based algorithm and consider the
cluster with the highest parsing score as containing the set of border binaries.
After this, we build the binary dependency graph. Again the binary dependency graph
represents the data dependency among the binaries in a firmware sample. For
instance, this simple graph will tell us that a binary A communicates with binary C
using files and the same binary A communicates with another binary B using
environment variables. Let's see how this works. So we start from the identified
border binaries and then we taint the data compared against network related keywords
that we found and run a static analysis, static taint analysis to detect whether
the binary relies on any IPC paradigm to share the data. If we find that it does,
we establish if the binary is a setter or a getter, which again means that if the
binary is setting the data to be consumed by another binary, or if the binary
actually gets the data and consumes it. Then we retrieve the employed data key
which in the example before was the keyword QUERY_STRING. And finally we scan
the firmware sample to find other binaries that may rely on the same data keys and
schedule them for further analysis. To understand whether a binary relies on any
IPC, we use what we call CPFs, which again means communication paradigm finder. We
design a CPF for each IPC. And the CPFs are also used to find the same data keys
within the firmware sample. We also provide Karonte with a generic CPF to
cover those cases where the IPC is unknown. Or those cases were the vendor
implemented their own versions of some IPC. So for example they don't use the
setenv. But they implemented their own setenv. The idea behind this generic CPF
that we call the semantic CPF is that data keys has to be used as index to set, or to
get some data in this simple example. So let's see how the BDG algorithm works. We
start from the body binary, which again will start from the server request and
will pass the URI and we see that here. it runs a string comparison against some
network related keyword. As such, we taint the variable P. And we see that the
variable P is returned from the function to these two different points. As such, we
continue. And now we see that data gets tainted and the variable data, it's passed
to the function setenv. At this point, the environment CPF will understand that
tainted data is passed, is set to an environment variable and will understand
that this binary is indeed the setter binary that uses the environment. Then we
retrieve the data key QUERY_STRING and we'll search within the firmware sample
all the other binaries that rely on the same data key. And it will find that this
binary relies on the same data key and will schedule this for further analysis.
After this algorithm we build the BDG by creating edges between setters and getters
for each data key. The multi binary data flow analysis uses the BDG to find and
propagate the data constraints from a setter to a getter. Now, through this we
apply only the least three constraints, which means that ideally between two
program points, there might be an infinite number of parts and ideally in theory an
infinite amount of constraints that we can propagate to the setter binary to the
getter binary. But since our goal here is to find bugs, we only propagate the least
strict set of constraints. Let's see an example. So again, we have our two
binaries and we see that the variable that is passed to the setenv function is data,
which comes from two different parts from the parse URI function. In the first case,
the data that its passed is unconstrained one in the second case, a line 8 is
constrained to be at most 128 bytes. As such, we only propagate the constraints of
the first guy. In turn, the getter binary will retrieve this variable from the
environment and set the variable query. Oh, sorry. Which in this case will be
unconstrained. Insecure interaction detection run a static taint analysis and
check whether tainted data can reach a sink in an unsafe way. We consider as
sinks memcpy like functions which are functions that implement semantically
equivalent memcyp, strcpy and so forth. We raise alert if we see that there is a
dereference of a tainted variable and if we see there are comparisons of tainted
variables in loop conditions to detect possible DoS vulnerabilities. Let's see an
example again. So we got here. We know that our query variable is tainted and
it's unconstrained. And then we follow the taint in the function process_request,
which we see will eventually copy the data from q to arg. Now we see that arg is 128
bytes long while q is unconstrained and therefore we generate an alert here. Our
static taint engine is based on BootStomp and is completely based on symbolic
execution, which means that the taint is propagated following the program data
flow. Let's see an example. So assuming that we have this code, the first
instruction takes the result from some seed function that might return for
instance, some user input. And in a symbolic world, what we do is we create a
symbolic variable ty and assign to it a tainted variable that we call TAINT_ty,
which is the taint target. The next destruction X takes the value ty plus 5
and a symbolic word. We just follow the data flow and x gets assigned TAINT_ty
plus 5 which effectively taints also X. If at some point X is overwritten with some
constant data, the taint is automatically removed. In its original design,
BootStomp, the taint is removed also when data is constrained. For instance, here we
can see that the variable n is tainted but then is constrained between two values 0
and 255. And therefore, the taint is removed. In our taint engine we have two
additions. We added a path prioritization strategy and we add taint dependencies.
The path prioritization strategy valorizes paths that propagate the taint and
deprioritizes those that remove it. For instance, say again that some user input
comes from some function and the variable user input gets tainted. Gets tainted and
then is passed to another function called parse. Here, if you see there are possibly
an infinite number of symbolic parts in this while. But only 1 will return tainted
data. While the others won't. So the path prioritization strategy valorizes this
path instead of the others. This has been implemented by finding basic blocks within
a function that return a nonconstant data. And if one is found, we follow its return
before considering the others. Taint dependencies allows smart untaint
strategies. Let's see again the example. So we know that user input here is
tainted, is then parsed and then we see that it's length is checked and stored in
a variable n. Its size is checked and if it's higher than 512 bytes, the function
will return. Otherwise it copies the data. Now in this case, it might happen that if
this strlen function is not analyzed because of some static analysis input
decisions, the taint tag of cmd might be different from the taint tag of n and in
this case, though, and gets untainted, cmd is not untainted and the strcpy can raise,
sorry, carries a false positive. So to fix this problem. Basically we create a
dependency between the taint tag of n and the taint tag of cmd. And when n gets
untainted, cmd gets untainted as well. So we don't have more false positives. This
procedure is automatic and we find functions that implement streamlined
semantically equivalent code and create taint tag dependencies. OK. Let's see our
evaluation. We ran 3 different evaluations on 2 different data sets. The first one
composed by 53 latest firmware samples from seven vendors and a second one 899
firmware gathered from related work. In the first case, we can see that the total
number of binaries considered are 8.5k, few more than that. And our system
generated 87 alerts of which 51 were found to be true positive and 34 of them were
multibinary vulnerabilities, which means that the vulnerability was found by
tracking the data flow from the setter to the getter binary. We also ran a
comparative evaluation, which basically we tried to measure the effort that an
analyst would go through in analyzing firmware using different strategies. In
the first one, we consider each and every binary in the firmware sample
independently and run the analysis for up to seven days for each firmware. The
system generated almost 21000 alerts. Considering only almost 2.5k binaries. In
the second case we found the border binaries, the parsers and we statically
analyzed only them, and the system generated 9.3k alerts. Notice that in this
case, since we don't know how the user input is introduced, like in this
experiment, we consider every IPC that we find in the binary as a possible source of
user input. And this is true for all of them. In the third case we ran the BDG but
we consider each binaries independently. Which means that we don't propagate
constraints and we run a static single corner analysis on each one of them. And
the system generated almost 15000 alerts. Finally, we run Karonte and the generated
alerts were only 74. We also run a larger scale analysis on 899 firmware samples.
And we found that almost 40% of them were multi binary, which means that the network
functionalities were carried on by more than one binary. And the system generated
1000 alerts. Now, there is a lot going on in this table, like details are on the
paper. Here in this presentation I just go through some as I'll motivate. So we found
that on average, a firmware contains 4 border binaries. A BDG contains 5 binaries
and some BDG have more than 10 binaries. Also, we plot some statistics and we found
that 80% of the firmware were analysed within a day, as you can see from the top
left figure. However, experiments presented a great variance which we found
was due to implementation details. For instance we found that angr would take
more than seven hours to build some CFGs. And sometimes they were due to a high
number of data keys. Also, we found that the number of paths, as you can see from
this second picture from the top, the number of paths do not have an impact on
the total time. And as you can see from the bottom two pictures, performance not
heavily affected by firmware size. Firmware size here we mean the number of
binaries in a firmware sample and the total number of basic blocks. So let's see
how to run Karonte. The procedure is pretty straightforward. So first you get a
firmware sample. You create a configuration file containing information
of the firmware sample and then you run it. So let's see how. So this is an
example of a configuration file. It contains the information, but most of them
are optional. The only ones that are not are this one: Firmware path, that is the
path to your firmware. And this too, the architecture of the firmware and the base
address if the firmware is a blob, is a firmware blob. All the other fields are
optional. And you can set them if you have some information about the firmware. A
detailed explanation of all of these fields are on our GitHub repo. Once you
set the configuration file, you can run Karonte. Now we provide a Docker
container, you can find the link on our GitHub repo. And I'm gonna run it, but
it's not gonna finish because it's gonna take several hours. But all you have to do
is merely... typing noises just run it on the configuration file and it's gonna
do each step that we saw. Eventually I'm going to stop it because it's going to
take several hours anyway. Eventually it will produce a result file that... I ran
this yesterday so you can see it here. There is a lot going on here. I'm just
gonna go through some important like information. So one thing that you can see
is that these are the border binaries that Karonte found. Now, there might be some
false positives. I'm not sure how many there are here. But as long as there are
no false negatives or the number is very low, it's fine. It's good. In this case,
wait. Oh, I might have removed something. All right, here, perfect. In this case,
this guy httpd is a true positive, which is the web server that we were talking
before. Then we have the BDG. In this case, we can see that Karonte found that
httpd communicates with two different binaries, fileaccess.cgi and cgibin. Then
we have information about the CPFs. For instance, here we can see that. Sorry. So
we can see here that httpd has 28 data keys. And that the semantics CPF found 27
of them and then there might be one other here or somewhere that I don't see .
Anyway. And then we have a list of alerts. Now, thanks. Now, some of those may be
duplicates because of loops, so you can go ahead and inspect all of them manually.
But I wrote a utility that you can use, which is basically it's gonna filter out
all the loops for you. Now to remember how I called it. This guy? Yeah. And you can
see that in total it generated, the system generated 6... 7... 8 alerts. So let's see
one of them. Oh, and I recently realized that the path that I'm reporting on the
log. It's not the path from the setter binary to the getter binary, to the sink.
But it's only related to the getter binary up to the sink. I'm gonna fix this in the
next days and report the whole paths. Anyway. So here we can see that the key
content type contains user input and it's passed in an unsafe way to the sink
address at this address. Now. And the binary in question is called
fileaccess.cgi. So we can see what happens there. keyboard noises If you see here,
we have a string copy that copies the content of haystack to destination,
haystack comes basically from this getenv. And if you see destination comes as
parameter from this function and return and these and this by for it's as big as
0x68 bytes. And this turned out to be actually a positive. OK. So in summary, we
presented a strategy to track data flow across different binaries. We evaluated
our system on 952 firmware samples and some takeaways. Analyzing firmware is not
easy and vulnerabilities persist. We found out that firmware are made of
interconnected components and static analysis can still be used to efficiently
find vulnerabilities at scale and finding that communication is key for precision.
Here's a list of bibliography that I use throughout the presentation and I'm gonna
take questions.
applause
Herald: So thank you, Nilo, for a very interesting talk. If you have questions,
we have three microphones one, two and three. If you have a question, please go
head to the microphone and we'll take your question. Yes. Microphone number two.
Q: Do you rely on imports from libc or something like that or do you have some
issues with like statically linked binaries, stripped binaries or is it all
semantic analysis of a function? Nilo: So. Okay. We use angr. So for
example, if you have an indirect call, we use angr to figure out, what's the target?
And to answer your question like if you use libc some CPFs do, for instance, then
environment CPF do any checks, if the setenv or getenv functions are called. But
also we use the semantic CPF, which basically in cases where information are
missing like there is no such thing as libc or some vendors reimplemented their
own functions. We use the CPF to actually try to understand the semantics of the
function and understand if it's, for example, a custom setenv.
Q: Yeah, thanks. Herald: Microphone number three.
Q: In embedded environments you often have also that the getter might work on a DMA,
some kind of vendor driver on a DMA. Are you considering this? And second part of
the question, how would you then distinguish this from your generic IPC?
Because I can imagine that they look very similar in the actual code.
Nilo: So if I understand correctly your question, you mention a case of MMIO where
some data is retrieved directly from some address in memory. So what we found is
that these addresses are usually hardcoded somewhere. So the vendor knows that, for
example, from this address A to this address B if some data is some data from
this peripheral. So when we find that some hardcoded address, like we think that this
is like some read from some interesting data.
Q: Okay. And this would be also distinguishable from your sort of CPF, the
generic CPF would be distinguishable... Nilo: Yeah. Yeah, yeah.
Q: ...from a DMA driver by using this fixed address assuming.
Nilo: Yeah. That's what the semantic CPF does, among the other things.
Q: Okay. Thank you. Nilo: Sure.
Herald: Another question for microphone number 3.
Q: What's the license for Karonte? Nilo: Sorry?
Q: I checked the software license, I checked the git repository and there is no
license like at all. Nilo: That is a very good question. I
haven't thought about it yet. I will. Herald: Any more questions from here or
from the Internet? Okay. Then a big round of applause to Nilo again for your talk.
postroll music
Subtitles created by many many volunteers and the c3subtitles.de team. Join us, and help us!
asdf