Hi everybody, my name is Ryan Maggio. I am a recent graduate from the Louisiana State University and today I’m going to be presenting a project called Seance that was worked on by myself, Andrew Case, Aisha Ali-Gombe and Golden Richard. And it’s a tool for, if you have a memory forensics tool that you’re trying to maintain, Seance will verify compatibility with an updated version of some software that you’re trying to analyse to make sure that your tool still works.
So, here’s just a quick overview of the presentation. I’m going to go over the motivation which I just explained as verifying compatibility of memory forensics tools with new versions of software.
I’m going to go over the internals of Seance, so the tools that it was built on, the fact that it’s two separate components, there’s an API and a controlling script, and then lastly I’m going to go over the data that we tested on, which is—we had two different test sets. One is a collection of Objective-C binaries, and the second is a collection of Windows networking stack binaries.
And I’ll go over the results of applying sounds to both of those types of data. And then lastly, I’m briefly going to go over future work and plans.
So, like I said, Seance is a tool for verifying the compatibility of a forensics tool that you’re trying to maintain and a target binary. This is a historically time-consuming and error-prone task, there’s a lot of manual reverse engineering required, existing automated efforts are largely based on data structure reconstruction and Seance is as well, but we tried to improve on some of the previous work because they can be a little bit brittle or have some holes that we try and fill.
Another real motivating factor on this was that compatibility-breaking changes have caused real world issues. Andrew took a look at the Volatility issues that were raised on GitHub and found several instances of problems like this, specifically with the Windows networking stack has caused quite a few problems.
And then, from the more academic side, previous projects that we worked on focused on malware analysis, and we had applied some emulation techniques using a unicorn to try and automate and help with those types of problems, and we wanted to see if similar techniques would work in this problem space as well.
Although, one thing that we had encountered was that some particular malicious behaviour from different code hooks would be missed by emulation, so we wanted to try using symbolic execution to explore different programme paths and not miss any behaviour, because we expected the kind of code that we’d be looking at here to maybe be even more complicated than some of the code hooks that we were encountering with malware.
So, Seance is this tool that we built for addressing the previous problems. It’s built on top of an existing binary analysis platform called angr. Angr has a lot of features and we don’t really use all of them, but we mainly use it for symbolic execution, we use it for control flow graph generation, we use it for instrumenting the execution that we’re going to be doing, and yeah, it’s very handy.
It’s Python-based and Seance as well is Python-based, and the idea is to try and explore, kind-of, the true behavior of the code that we’re trying to analyse here, which is, like I was saying, is exploring multiple paths through the programme to get a better idea of what’s going on than just concrete execution or emulation would have you see.
And part of this is getting more detailed execution data, so producing a control flow graph, recording all the memory accesses and getting concrete results about what was written or read from where. And the idea is, as I said, to try and answer questions about tool compatibility, but it is potentially useful in other domains as well, which is something I’ll get at with future work.
So, first off, like I said, Seance has two components, so I’m going to go over how the API component works. So the API has… that’s where a lot of the functionality for Seance is contained. A note here that it doesn’t integrate with Volatility, like some of our previous work like HookTracer that’s emulation- and unicorn-based integrate directly with Volatility, Seance is a bit more generally applicable. It can work with a wide variety of tools and not just Volatility, so a direct integration there doesn’t really make sense.
But yeah, so the API. It has four angr callbacks that were kind-of like helper functions for instrumenting the execution, and they assume that the main controller code is already instantiated in angr project. But yeah, so these callbacks are for memory reads and writes and register reads and writes.
And generally what each of them do is check if execution is ongoing, check that the target of the read or write can be concretised and that it’s not just some unconstrained symbolic value, and then it records a bunch of data about what the read or the write was.
Another useful feature of the API is that it contains logic for handling stashes. So, stashes are how angr collects final states from execution, and it’ll group them into categories, such as errored, or it found the thing that you were telling it to find, or stuff like that. So the API that we wrote has logic for filtering through all those stashes and associating this list of memory accesses with the correct state from certain stashes and concretising values that were symbolic, but can be concretised.
And then we also use it to look at all of the basic block addresses that were visited and using this to generate a permit-list for control flow graph generation. And then the last two things that the API handles, broadly, are generating control flow graph and printing out a nice human-readable representation of the graph.
And then, the other half of this Seance code is the controlling code. And so, this makes use of the binary to do what we want to do here. What it takes as input is the binder that you’re trying to analyse, the symbol that you’re trying to particularly pick out of the binary. And this one’s a little bit loose; you can specify the symbol name, you can specify an address if you somehow know the address of the symbol you want to look at. There’s a little bit of things you can play around here, but generally it’s fine to think of it as just the symbol.
And then, if you have a database from previous Seance runs that you want to do comparisons against, you can also provide that as input, and what you get out is a control flow graph of the symbol that you’re trying to analyse, you’re going to get some detailed execution data, as I kind-of explained before what that entails, and then you’re going to get a list of offsets that were referenced, and by this I mean offsets from certain memory addresses, or offsets from memory addresses that were accessed via pointers.
And then the last bit here that it does is the post-processing, where it takes the output from this previous step and it compares it against the database, if you provided one, to let you know what changed and where, and the nature of the changes. And it will update the database with the information that you just got from the new version of the binary.
So, as I said, we tested on two different sets of data. The first was a collection of 21 Objective-C binaries. We picked Objective-C for our first test because it’s open source, so we could quickly and correctly verify that Seance is doing what we think it was doing and that everything is working right.
But additionally, we picked it because there were a lot of important algorithms and data structures in Objective-C that, for instance, Volatility makes heavy use of for some of the analysis that it does, and these things are also frequently abused by malware so we wanted to get a good real-world example of something that we would want to be looking at with Seance.
And then lastly, the research efforts into Objective-C in this way are a little bit dated at this point, so we wanted to, kind-of, freshen that up a bit.
And the second test data serves as a complement to this, and that’s the Windows networking stack, several binaries from that. And we chose it because it’s, unlike Objective-C, closed source, so we wanted to show that Seance can still work, even when source code is not available, because it doesn’t use the source code, it just uses the binary. And also that it can work even when debug information is not published.
We also chose to target the networking stack in particular because often network activities are not only central to investigations, but they could serve as the first indicator that something is wrong. That some infection is present, is frequently found through network activity. And then lastly, the Windows networking stack is updated often and causes real issues. As I said, when Andrew was looking through the Volatility issue tracker, a lot of the issues raised were “a new update to the networking stack has broken some aspect of Volatility”.
So here’s just a really quick example. This is the truncated output of Seance on the function TcpConnectTimeout, which is found in the Windows networking stack. On the left, you see the output from Seance for a particular run and on the right, you see the control flow graph that was generated as well for this particular part of this particular function.
And I’m not going to get too detailed exactly right now, but if you just want a brief sanity check on this, you can see it like, okay, well, Seance is detecting that offsets from RBP at 10, 18 and 72 were detected. And you can see that right here at the beginning. So, just, immediate sniff test, it seems to be working fine.
But was that actually helpful? That was a bit of a jumbled output. You might want to look a little bit more carefully, so that function targets a data structure called TCP_ENDPOINT that is a very complicated nested structure. Its members, like AddrInfo, the second member, is actually a pointer to an AddrInfo structure, which has members which are, again, pointers to other structures.
So it’s not quite a very straightforward data structure, but despite this you can see that the offsets were actually correctly recognised, and I just highlighted some of the important ones that we’re going to be looking at here.
So, yeah, you could see this AddrInfo structure, or member, is present at offset 18 and, at an offset from those at offset 0 and 10, are other members that we care about. And if you look down here at the Traced Pointer Accesses, you can see that the thing that RBP points to points again to another location and offsets 10 and 0 from that are referenced. And then if you look back at RBP, offset 18 is referenced from that.
So, it looks like Seance traced correctly the accesses into the members that we care about, not only into TCP_ENDPOINT, but into the AddrInfo member as well. And this is exactly what we’ve been hoping to see.
As an example from Objective-C, we’re looking at the method_getImplementation function, which references this implementation member of this method structure. The function is very small, it’s quick, it just does a comparison and then returns either 0 or the member of this method structure that we care about.
You can see the control flow graph actually is correct, it seems to be doing exactly a check for zero. If there’s not something that was passed into it, it just returns zero. And then, otherwise, it accesses the parameter that was passed in at offset 10 which is, again, exactly what we expected to see and exactly what we wanted.
And then lastly, here is the results for the database matching functionality of Seance. So what we did was we took those 21 Objective-C binaries that we had and separated them into two different groups.
We picked five at random that were going to serve as the test group and then we used the rest of them to generate a database to mimic what an investigator might have on hand if they’re an experienced, seasoned investigator that’s come across versions of these binaries before. And then what we did was, we compared each of the test binaries against this database to see if Seance would actually detect changes that the investigator might care about.
So, there’s a lot going on in this table. To just quickly go over it, the leftmost column, that’s the structure and function column, lists the, on the top of it, is the structure that we care about, and then below that is the function that accesses it. So, for example, we care about the NXHashTable structure and then the NXEmptyHashTable function is the function that we’re analysing here.
And we’re doing that because what’s found at this next column, the parameter and register column, that’s the parameter of the function is an NXHashTable pointer, and the register that it’s passed in on is RDI. And then the three columns at the end are the results of the groups that we found which matched. So, the exact match column says that all of the offsets that Seance detected matched exactly among versions, for instance, in the first row, 10.14 through 10.15.6.
So those form this group that, as you can see, it’s a series of consecutive versions where the behaviour was exactly the same. The next column, offset match, is the group of binary versions where specifically the offset from RDI, or the set of offsets from RDI, was the same. And, as displayed on the table, that was all of them. All versions of Objective-C that we analysed accessed this particular member of NXHashTable in exactly the same way.
And then, this last column, CFG match, is a little bit more involved, and this is the group of binary versions where the offsets are the same, but the CFG generation that we did, we also do a comparison where we get some high-level information about the control flow graph, and we compare that as well. And in the CFG match column, we share the groups where the offsets were used the same, but the underlying control flow graph has changed, so that indicates that maybe the algorithm or the function that’s being used to handle this data is different, and that’s something that an investigator might want to see.
And in most cases, this can come up if the offset changed, or if the CFG changed. If the offset changed, the CFG likely changed, and if the CFG changed, the offset likely changed as well. But one case where that was not the case is this Objective-C object structure and the getClass function where we found that the offsets actually never changed for this ID parameter, but for some of the versions, the control flow graph would in fact change. So, Seance did correctly detect this, and yeah, it’s displayed here.
So, what’s left to do, right? You might notice that I just presented the results for the Objective-C database construction and testing. Objective-C didn’t really have any cases where we needed to trace the access from pointers from the parameters that were passed in pointed to, and, as such, we hadn’t had that logic written when we constructed that database, but the Windows networking stack did have a few cases, like the example that I showed with the nested data structure. And so, the database comparison logic doesn’t take that into account yet, so that needs to be done so that we can build more robust databases and do more robust comparisons.
Additionally, I’d like to make this code publicly available, but first I’ve got to clean it up a little bit. And second, I would like to kind of containerise the environment. It’s a little bit tricky to get everything, all the different dependencies, working correctly. I wrote up some detailed instructions on the GitLab, but I’d like to have it done so that, if somebody doesn’t want to mess with all that, they don’t have to.
And then lastly, I’d like to have it integrate a little bit closely with Volatility, maybe taking in V types instead of previous Seance runs to be able to construct that database, or if there’s some expected change to a data structure to automatically detect such a thing and emit what would be correct patched V types automatically, and save the investigator a little bit more work. And that’s all I’ve got for you today. Any questions?