 So this is Apple's predicament, MS predicate exploitation on macOS and iOS. My name is Austin Emmett, and I also go by alkali. Get into it. So a little bit more information about me, I'm currently a vulnerability researcher at Vigilant Labs, though the research that I'm going to be presenting today was done while I worked at the Trellix Advanced Research Center. I'm also the author of the Radius 2 symbolic execution framework, which uses Redar2 to easily, quickly and easily perform symbolic execution and taint analysis on binaries from many different architectures. Check that out if it's something you're interested in. I'm Alkaline Sec on Twitter, I mean X, and I'm Alkaline Infosec Exchange on Macedon. All right. Let's talk about where this all sort of began with the forced entry exploit. I've been obsessed with NS Predicate since I read the second part of the Project Zero's blog post about the forced entry attack, which was a exploit chain targeting iOS in 2021. The first post covered an iMessage exploit that sent a fake GIF, that was actually a PDF, which exploited an integer overflow in the JBig2 image compression codec. And it was this absolutely incredible exploit that used the operations on groups of pixels to create an entire virtual machine. The ultimate purpose of this virtual machine was to create and evaluate an NS Predicate, which sent another NS Predicate to an unsandbox process. While the iMessage exploit received most of the attention, I was fascinated by the sandbox escape and these NS Predicates, these tiny little strings that were capable of defeating all of iOS security. So why was I so fascinated? The answer is because iOS hacking is hard. It's very hard. iOS is a hard target because it has common mitigations like ASLR, address space layout randomization, though on iOS it's a little bit limited, which we'll get to later. But more importantly, iOS also has other less common mitigations like strict code signing, which prevent any dynamically generated code from being executed. Only code signed with an Apple approved certificate can be run on the device in its normal form of operation. Also every form of scripting has either been removed like Apple script or heavily sandboxed like the JavaScript engine. Additionally modern iPhones and Macs have PAC or pointer authentication codes, and these are codes made up of some of the previously unused top bits of pointers. And special instructions set and check them in order to prevent code reuse methods like RAP or return oriented programming. So if a function pointer on like the stack or the heap is overwritten, these bits will no longer match their expected values and an exception will be raised when it's called via one of those special branch instructions. Finally apps run in a sandbox with permissions restricted to only what the app needs to function. Access to sensitive content is often done through IPC with more privileged service processes. So all this makes life very difficult for hackers who really want a way to reliably perform arbitrary operations dynamically, ideally outside of any sandbox. And it turns out that this is exactly what NSPrettyKits get us. So in order to understand a little bit more about the security of iOS we have to understand Objective C. It's the language that most of the user space code is written in, although a lot of new code is written in Swift, there's still all of these old frameworks that are written basically all in Objective C. And it's a superset of C with object oriented programming concepts added in a way similar to small talk, which is based on message passing where methods are invoked dynamically at runtime by name. These names are called selectors in the parlance of Objective C. And methods can be added and removed at runtime as well as have their types changed. It's a very dynamic language, at least the object oriented part of it. It's also possible to access properties and call methods without arguments using strings concatenated with periods. These strings are called key paths. And they're going to be important in the context of NSPrettyKits. This is just like a basic hello world Objective C program. It's got nearly all the elements that we're going to care about though. So we have this NSString, which is the basic string class in Objective C, and we call string by appending string a method on the string hello with the argument world to get the NSString hello world. It's pretty simple stuff. Then we can use the key path string, uppercase string, .utf8 string to get a C string pointer, which we can print with printf. And the apps in front of the quotes are just like an easy way to create constant NSStrings. So we're nine slides in, and I haven't talked about what the hell an NSPrettyKit is yet, so we should probably get into that. The Apple documentation says they are a definition of logical conditions for constraining a search for a fetch or for in-memory filtering. This definition kind of sucks. There are simply strings that are used to filter objects and arrays, the majority of their use. So if you have a student object, you might select students in array with predicates like grade equal seven or first name like one and age less than 16. These strings are actually format strings, so you can use percent escaped strings like percent sign D to include a number that is passed to a predicate with format in order to initialize the NSPrettyKit. It's kind of like printf, you know. The NSPrettyKits also implement NSCoding, which means they can be serialized, and that allows them to be sent via IPC to other processes or even remotely to other devices. And the important thing about NSPrettyKits is that they're ubiquitous in iOS and macOS first party and third party code. They're used absolutely everywhere. They're intertwined with all of the software on these devices. So we have a quick explainer of XPC. So to expand on that last point, let's discuss XPC. It's a common form of inter-process communication, where one process can call methods on a remote object in another process. When XPC is used to call a remote method, these arguments are passed by the sender. They're serialized or archived is what the serialization process is called on iOS and macOS and sent to the target where they are deserialized. And it's common to see NSPrettyKits arguments used to filter the results of these remote calls. And that's in order to minimize the number of objects that need to be serialized on the sender end and deserialized on the receiver end. This is foreshadowing. Just in case it wasn't already abundantly clear, it's going to become very important that NSPrettyKits are being sent all around via XPC. So here's an example of an NSPrettyKits in action, just a short little program here where we have an array containing some filenames and we use the predicate path extension equals pi to filter it, and the resulting array just contains that single entry script.pi. To get a little bit deeper into the actual structure of an NSPrettyKit, that string is parsed by code generated with flex in the foundation framework into pieces called NSExpressions and NSPrettyKit operators. So in this example from CodeColorist, a predicate name equals apple is parsed into three pieces. The NSKeyPathExpression name equals equals NSEqualityPrettyKit operator and the NSConstant value contains the single constant string apple. And the KeyPathExpression is also a function expression, which means it can be further broken down into an operand, which is self, showing that it acts on the objects being filtered, this selector, which is value for key, and that KeyPathSpecifier, which is just name. This is all a bit complicated, but it's not really necessary to understand this AST structure. More important for us is the question of what can an NSPrettyKit do? And the answer to that question is anything, sort of. It used to be anything, but now there's a big asterisk, since apple has taken many steps to make NSPrettyKits much less powerful, less useful for exploitation. In fact, nearly all the techniques I'm going to talk about today no longer work as they're going to be presented, but we'll take a look at NSPrettyKits as they were at this time and at the limits of what could be done with them. So while at first glance NSPrettyKits don't look very interesting, they're actually a powerful scripting language. As CodeColorist said in his blog post CenoEval, NSPrettyKits are essentially the eval function for Objective-C. And this scripting capability comes largely from the function keyword, which allows any method to be called on any object with any number of arbitrary arguments. CodeColorist also discovered that the cast keyword could be used with the class as the second argument in order to get a reference to any class, essentially functioning like NSClassFromString. It's just a function, you know, if you've programmed much Objective-C. One interesting thing to note is that this functionality is both intended and very old. Function expressions were added in macOS 10.5 around the year 2007, and that means that they've been in iOS since the very beginning. This is official Apple documentation showing how to use function expressions, so they were never a secret. With the cast class trick, it was also possible to use these helpful classes like CN file services, which had the DLSim method to get a pack signed address to any exported function. This address could be called with NSInvocation, which has a method invoke using imp, and that could be used to call the function with any number of arbitrary arguments as well. So effectively, both the object-oriented methods of Objective-C could be used, and also any exported simple C function could be called without restriction. That means that anything that could be done in a normal Objective-C program could be done completely dynamically within this NSPredicate string. And this allows it to completely sidestep ASLR, code signing, and pack all those mitigations that I talked about in the beginning that makes iOS so secure. So this was beautiful for hackers, not for people that wanted their devices secure. So the syntax of NSPredicate, sorry. So until now, the only NSPredicates we've seen have been like first name equals one, so it's understandable if you're confused about how NSPredicates could be used as a scripting language. But they actually have a surprisingly rich syntax that's capable of representing all the concepts that we would want in a scripting language. And that includes variables and variable setting using these dollar sign strings. There's also function and keypath expressions that we've talked about a little bit. But there's also this important type of function expression, which is those that don't need to use the function keyword, like now and some. And these are selectors on the NSPredicate utilities class. And they provide these kind of useful auxiliary methods for within a predicate. But we'll see that it's possible to use any valid selector within the syntax. And that's going to be useful for both getting a reference to the NSPredicate utilities class and we'll see some other uses of it later, too. Then there are NSAgrate expressions, which are meant to be used for arrays, which we can also use to create a series of expressions that will be evaluated sequentially, which will effectively act as the lines of our script. Finally, subquery and ternary expressions can be used to create loops and conditionals, respectively, for control flow. And additionally, all the normal arithmetic and bitwise math operators can be used in NSPredicates. And we'll see that become super useful for calculating addresses. This is a Py Objective C script, so it's just Objective C bindings for Python bindings for Objective C, and it evaluates an NSPredicate script. So this script makes two variables, standard in, standard out, and proc info, which is a file handle and the process information, respectively. And these objects are instantiated using the cast class trick. It then uses a ternary expression as an if statement, so that if the process name is Python, it prints out its Python to standard out. All these expressions are wrapped in brackets so that they form a single NS aggregate expression, and each line here is evaluated sequentially. So for a more complicated example, it's probably a little hard to see, but this is a brain fuck interpreter made entirely within the evaluation of a single NS expression. And it uses all the things that we've seen so far, it's just a little larger. It uses the cast class trick again to get those file handle references to standard in and out. It uses ternary expressions for control flow, so it checks whether the program is ended or what the current operation is. And it evaluates itself recursively in order to perform unbounded loops. So instead of doing this recursive evaluation, I could have used a subquery expression, but then it would have been bounded. So it was easier to use a recursive evaluation in this case to do the looping. So before forest entry, these NS predicate scripts were essentially unrestricted with the exception of predicates that were sent over XPC. And these predicates were often limited using something called NS predicate visitors, implementations of a protocol that has methods to check what the components of an untrusted NS predicate were and if they were safe to evaluate. These implementations use the expression type property to check what those expressions were, and if they were a function or a key path, then it would potentially only allow certain specific instances of those expressions. So a limited set of selectors or a limited set of key paths. However, without these restrictions, NS predicates were basically arbitrary code execution, and they were used as such in that forest entry sandbox escape. Now that we understand a little bit more about NS predicates and their capabilities, we can understand that forest entry sandbox escape. That crazy J big 2 VM made a fake object in memory that when deallocated evaluated a series of NS function expressions. And these expressions cleaned up after the initial exploit by deleting that fake gift file, and they crafted another payload, another array of objects that was sent to the unsandboxed comm center process. These objects were chosen such that when the target DCLized them, they would immediately evaluate a new NS predicate, which collected a bunch of device information before downloading and evaluating another NS predicate payload. After forest entry and the increased visibility of code colors earlier blog posts, a few new restrictions were placed on NS predicate objects. Two denialists were added, which prevented the use of a number of inherently dangerous classes and methods, with particular focus on methods, classes and methods that allowed performing arbitrary method implications and initializing arbitrary objects. The cast class trick was forbidden and calling class methods as opposed to instance methods of objects was prevented. However, it's important to note that most of these changes only affected first party Apple apps and processes. NS predicates evaluated in third party apps had a much smaller denialist of classes and methods. And for the most part, just as powerful as before. This difference was implemented through a single flag value called predicate security flags that was set for Apple processes when the class NS predicate utilities was initialized. So here we can see some of that denialist. This is the denialist of classes. So all these classes were prevented from being used and includes things like NS bundle, which is used to load other shared libraries and NS coder, which prevents serializing new arbitrary objects. Again, trying to limit the number of the different objects that you could use within an NS predicate. Additionally, Apple removed the CN file services DLCM method that was used both in NS predicate and slop exploits. So that's select or oriented programming. It was just another type of exploitation technique on iOS. And this method had proved to be very helpful for attackers. So it was unfortunate for us when it was removed. Relatedly, NS invocation was hardened. And it was also included in that list of forbidden classes and made generally more difficult to use for exploits. However, as we shall see, the denialists were way, way, way too small. And the fact that security was enforced based on this single writable flag value made it incredibly vulnerable. It was incredibly fragile. The get value method of NS value wasn't on the forbidden list and could be used to perform arbitrary writes overwriting any address with any desired value. And this meant that the security flag could just be unset by simply calling get value on the number zero with the address of the security flag as the argument. Importantly, even though iOS has ASLR, libraries are all split by the same amount in every process with the slide being chosen at startup. And so if you already had code execution in one process, you could just, you knew all the addresses of everything. Additionally, there are many different ways to leak the address from within a predicate itself. So it's trivial to use an offset from a known address to find that flag 100% reliably. Similarly, the length of the denialists could just be set to zero. And that would remove any remaining forbidden elements because they would check the list of forbidden classes and methods. The list would be zero length and say, oh, it's not on there. So all these techniques described in the previous slides can be seen in this predicate, which disables all the security mitigations that had been introduced to predicates. First, we use the self.hash to get the address of the NSPredicate Utilities class. So that hashtag self parentheses, that gets a reference to NSPredicate Utilities, and then the .hash just converts it into an NS number for us. With this, we can use offsets from this address to get the addresses of the security flags and the lengths of the denialists. And then once we have those addresses, we can simply use getValue to overwrite them, reverting NS predicates to their previous fully unrestricted state. And here we can also see what I call the MVP of NS predicate scripts, which is non-retained object value. So this is just a method that takes an NS number and treats it as a pointer to an object instead. It's kind of like dereferencing in the context of NS predicate, and can be used to access any class by reference when the class trick is forbidden. Just super useful. However, after a tweet of mine that might or might not have influenced Apple about how easily the previous mitigations were bypassed, Apple struck back, and they added restrictions to the argument types in function expressions to exclude pointer types. Every objective C method has a signature, which is the string of characters denoting the types of arguments and the return value of the method. And in that signature, the caret and the question mark characters represent data and function pointers, respectively. And these were the types that were forbidden within function expressions. Additionally, the predicate security flags and denialist code were moved into the core foundation framework. And the predicate security flags were now placed on the heap where they could be harder to locate, so that it wasn't quite as easy to overwrite them. And many new entries were added to the denialists. Fortunately, for me, Apple overlooked the asterisk type, which refers to C string pointers. This type really shouldn't exist. It should just be denoted as caret C. But it worked out for me here that it does exist. And this means that we could simply achieve the same kind of arbitrary write using get C string instead of the get value method. And once again, NSPredicates could perform arbitrary operations. So this is the predicate that accomplishes the same things as the previously shown. However, it uses the function set to bug predicate security scoping to unset the high security flag after setting the internal release type to three. And this is a trick that's necessary because, as I said before, the predicate security flag was moved to the heap. But luckily, NSPredicate Utilities just gave us this nice convenient method to unset it. So yeah, cool. So here is that set to bug predicate security scoping method. And this is the one that unsets the high security flag. That's on the heap. And so that flag is now stored in the third bit of the CTF predicate policy data plus 0x30 offset. But this method can only unset it if an OS variant has internal content returns true. And this is what we accomplished before by setting that internal release type to three in the previous predicate. So that's what that internal value is there as being set to three. So while I've demonstrated how to get around the limits that Apple placed on NSPredicate, I still haven't shown how arbitrary functions, arbitrary native C functions could be called. Now that the previous DLSIM gadget has been removed. Luckily, there is still at least one signed reference to DLSIM that we could get using the DLSIM func method of a class in the DVT Instruments Foundation framework. This address could be used with the apply function info method of RB stroke accumulator from the render box framework to call DLSIM and also to call all of the returned function pointers that are all signed with that zero context value. So those function pointers, those signed function pointers could be called with apply function info with up the four arbitrary arguments or more if it was okay that that fifth argument was not controlled, you could control all the rest. So this effectively bypasses PAC as even though it's not possible to sign arbitrary pointers as you would want for like a true bypass, the combination of the scripting capabilities within the NSPredicate and the ability to use any exporter function was basically more than enough to accomplish any desired goal. Unfortunately, Apple has been cracking down on all remaining references to DLSIM that you could get in this manner and this DLSIM func method is no longer available in iOS 16.5 or later. So this is that apply function method. As we can see, it uses the B-R-A-A-Z instruction to call the function pointer pass as that first argument. And this instruction authenticates and calls pointers signed with a zero context value, which is exactly what we need in order to use that reference to DLSIM and use all the function pointers that that method returns. All right, putting all the pieces together. This is an NSPredicate that calls NSLog, hmm, on iOS 16.3 after removing all the mitigations protecting Apple processes. It's probably too small to see, but the point is that it's very complicated, but it's still possible to do anything within an NSPredicate in this version. All takes some creativity, some knowledge of Objective-C and some useful classes that are already defined in the shared libraries of iOS and macOS. Now that we can do anything within an NSPredicate, similar to what was possible before forced entry, there's only one potential impediment to exploiting processes that evaluate untrusted NSPredicates. And that's the NSPredicate visitors that I covered before. And this is the most surprising thing about this research is that I found that you could just say no to NSPredicate visitors. Each daemon implements their own version of this protocol in order to only allow those expressions that it expects to be in the NSPredicates that daemon receives. And it uses the expression type to property to check what kind of expression each component of a predicate is, so that simple expressions like constant values can be allowed. And more dangerous ones like function and keypad expressions could be forbidden or limited to a certain safe subset. However, the expression type was just an integer. That was read directly from the serialized data sent by an untrusted process. And this meant that setting every expression type to zero in a malicious NSPredicate led the receiver to interpret it as only containing constant values and that bypassed all additional validation. So here's an example of an NSPredicate visitor implementation for the photo library queries. We can see that it uses the expression type to decide whether additional checks need to be made on the expression that it visits. So here it checks to see if it's a keypath expression and it gets the keypath and later checks it against a list of known good keypaths. And on the XML on the right here, this shows the serialized representation of the NS expression on the left. And we can see that the expression type is just an integer within this XML. And this is XML is controlled entirely by the sender. So it just sets whether they want. And this is the actual code of in it with coders. Probably like impossible to read. Yeah, but it's just reading out that NS expression type field and passing it to NS expression in it with expression type. You're just gonna have to trust me if you can't read it, that that's how it works. So many different daemons could be exploited using this bypass. So it includes Cordo at D, Context Store D, and they just aggregate information about user behavior on the device as well as App Store D, which can be used to install arbitrary apps. They still have to be signed though. OS Log Service, which is accessible by any app and can access any logs potentially containing sensitive information. And on iPadOS Springboard, which is the home app that it's kind of like Xplored IEXE on Windows. It's kind of like that background app. So using these vulnerabilities, malicious app could gain access to app location and notification data, including message contents, and potentially even install arbitrary apps on the target device and on paired devices. It's pretty simple to find these XPC service clients that send predicates like this one, which is CD interaction recorder by grepping the objective C headers. And that's the great thing about reversing objective C libraries and apps is that they have to contain this header information because that's how the dynamic dispatching of methods works. So here we can see many methods that take NSPredicate arguments, and these arguments are sent to the Cordo at D service. And this is a free descript, which when attached to a process that has the entitlements to communicate with Cordo at D, sends an NSPredicate that will cause a crash on access to OX4141As. It uses the method count contacts using predicate error, which takes a predicate argument and sends it to Cordo at D. And by calling this method, this function expression, and having its expression type set to zero, we can bypass the NSPredicate visitor that Cordo at D uses. On the iPad, any app can communicate with a Springboard XPC service that determines which app scene should be used for handling different types of events. And this is done using an NSPredicate provided by the app. This predicate is validated with UI target content identifier predicate validator, mouthful, but like the others, this visitor relies on the expression type value to determine whether expressions are safe, and therefore can be bypassed. By using a malicious predicate visitor, we can construct any NSPredicate and use the visitor to set all the, its expression types to be zero, denoting constant values, so we can actually use our predicate visitor to defeat other predicate visitors. This is the actual predicate that we're gonna use to exploit Springboard. So first, it uses Get C-String to perform the arbitrary rights needed to clear the denialists and set the internal release type to three, then it calls the set debug predicate security scoping, which unsets that high security flag. Finally, it uses NSFileManager, which was previously on that forbidden class list, and it uses it to copy the contents of the user notifications directory to the crash report directory so that we can copy them off the device. In a real scenario, they would be like zipped up and sent over HTTP to an attacker controlled server. There's lots of classes that'll help you do that. But just for our purposes here, it's simpler to just do this. The predicate then accepts our evil NSPredicate visitor, which sets all the expression types to zero. And finally, the predicate is assigned to the scene activation conditions, which is what sends it to Springboard to be evaluated when an event like opening the app is generated. Okay, so I think like this isn't gonna work, or it is gonna work. No, not really. Hold on one second. All right, so here is a demo of exploiting this Springboard vulnerability. So on the left, we have our script that's just waiting to receive the notifications from the malicious process. On our right, we saw that we're running iOS 16.1. And we can see that we have a notification that says this is a secret. And we're gonna open our malicious app, which has the helpful link steal notifications. And then we will see that our notifications are stolen there on the left. It's kind of small. I should have made it bigger. But it stole the notifications. Come back? Hey, yeah. All right, so the conclusion. Luckily for the security of iOS and macOS devices, Apple has finally begun to limit NS Predicates in ways that are less easily bypassed. So now the return and argument types of function expressions must be objects, even in third-party apps. And there's no flag that can be overwritten to change this. Predicates could still be used by malicious apps to bypass any kind of meaningful app store review. So they're still very useful in a context where the attacker can control that environment. And I don't think that they are looking out for this type of evaluation of dynamic NS Predicates in app store review, because it's actually something that Apple does within their own processes. So until Apple makes fundamental changes to both the behavior of function and key path expressions, they're still going to be very useful to exploitation. So for this foreseeable future, they will still be dangerous. They'll still be landmines scattered everywhere within the code of iOS and macOS, waiting to explode, destroying the effectiveness of all the amazing security mitigations on these devices. And they remain Apple's predicament. Thank you.