*preroll music* [filler, please remove in amara] 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. [filler, please remove in amara] *applause* [filler, please remove in amara] 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. [filler, please remove in amara] *applause* [filler, please remove in amara] 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* [filler, please remove in amara] Subtitles created by many many volunteers and the c3subtitles.de team. Join us, and help us!