0x01. Intro

The NSPredicate was first mentioned in the iMessage 0-click exploit chain, also known as FORCEDENTRY, and it was used to escape to sandbox. Security mitigations such as PAC, code signing, sandboxing, and ASLR make exploitation more difficult in iOS.

In this article, we will break down what NSPredicate is and how it is used.

This post is based on the Black Hat USA 2023 Presentation! You can watch the full video on their YouTube.

0x02. Background Knowledge

Before that, we need to understand selector, KeyPath, and NSPredicate.

a. selector & KeyPath

selector

According to the documentation, a selector has two meanings in Objective-C:

It can be used to refer simply to the name of a method when it’s used in a source-code message to an object. It also, though, refers to the unique identifier that replaces the name when the source code is compiled.

Here is a sample code snippet in Objective-C:

#import <Foundation/Foundation.h>

// Define a simple class
@interface Greeter : NSObject

- (void)sayHello;

@end

@implementation Greeter

- (void)sayHello {
    NSLog(@"Hello from Objective-C!");
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // Create an instance of Greeter
        Greeter *greeter = [[Greeter alloc] init];
        [greeter sayHello];
    }
    return 0;
}

The ‘Greeter’ class has a ‘sayHello’ instance method, and it is allocated and initialized before calling the method. Finally, we reach the [greeter sayHello]; call. According to the Objective-C syntax, sayHello is the selector inside the brackets - this is the first meaning of a selector.

The second meaning of a selector can be found in the compiled binary. In the following code, the ‘Buyer’ class is defined with a ‘sayHello’ method, just like Greeter, but with different behavior.

#import <Foundation/Foundation.h>

// Define a simple class
@interface Greeter : NSObject

- (void)sayHello;

@end

@interface Buyer : NSObject

- (void)sayHello;

@end

@implementation Greeter

- (void)sayHello {
    NSLog(@"Hello from Objective-C!");
}

@end

@implementation Buyer

- (void)sayHello {
    NSLog(@"Goodbye from Objective-C!");
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // Create an instance of Greeter
        Greeter *greeter = [[Greeter alloc] init];
        [greeter sayHello];

        Buyer *buyer = [[Buyer alloc] init];
        [buyer sayHello];
    }
    return 0;
}

Here is the result of the decompiled binary:

100000a88  struct objc_method_entry_t method_sayHello = 
100000a88  {
100000a88      rptr_t name = selRef_sayHello
100000a8c      rptr_t types = selTypes_sayHello
100000a90      rptr_t imp = -[Buyer sayHello]
100000a94  }

100000a98  struct objc_method_list_t method_list_Greeter = 
100000a98  {
100000a98      uint32_t obsolete = 0x8000000c
100000a9c      uint32_t count = 0x1
100000aa0  }
100000aa0  struct objc_method_entry_t method_sayHello = 
100000aa0  {
100000aa0      rptr_t name = selRef_sayHello
100000aa4      rptr_t types = selTypes_sayHello
100000aa8      rptr_t imp = -[Greeter sayHello]
100000aac  }

Both classes have methods with the same name, but they perform different operations. However, the binary treats them as the same selector - that’s why the second meaning of a selector is a unique identifer at runtime.

KeyPath

In some cases, a nested class is needed during development. While a single property is easy to access, this one is not. If you access multiple properties directly, it becomes complicated, so you can use a KeyPath - a predefined path used as a variable.

b. NSPredicate

NSPredicate is defined in the Apple Developer Documentation as:

A definition of logical conditions for constraining a search for a fetch or for in-memory filtering.

NSPredicate is composed of three parts: a target, a comparison operator, and a value. The following is an example of how to use it:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSArray *people = @[
            @{@"name": @"Alice", @"age": @30},
            @{@"name": @"Bob", @"age": @25},
            @{@"name": @"Charlie", @"age": @35}
        ];
        
        // Predicate: age >= 30
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age >= %d", 30];
        
        NSArray *filtered = [people filteredArrayUsingPredicate:predicate];
        
        NSLog(@"People aged 30 or older: %@", filtered);
    }
    return 0;
}

The people array contains three dictionaries, and each one has name and age fields. The filtered array contains the dictionaries where the age is greater than or equal to a specified value, using NSPredicate.

c. XPC

XPC is commonly used in iOS and macOS to call methods in a remote process from the current process; It is an interprocess communication mechanism. Arguments and callback functions can also be passed, and NSPredicate is often used to filter the returned results.

0x02. Deep Dive

This video goes into more detail about how NSPredicate is structured. You don’t need to understand everything, but try to remember this: The leftExpression, which was referred to as the ‘target’ when explaining NSPredicate, can be either an NSFunctionExpression or an NSKeyPathExpression. That also means predicates are built using NSExpression and NSPredicateOperator instances.

As mentioned in See No Eval, function expressions enable various tricks - including PAC mitigation bypasses using methods like [CNFileServices dlsym::] and [NSInvocation invokeUsingIMP:].

a. scripting with NSPredicate

The following syntax is used to write expressions in the format field of an NSPredicate:

b. mitigations #1

In iOS 15, three limitations were added to use of NSPredicate after the disclosure of FORCEDENTRY exploit chain:

  1. Action limitations using a denylist - certain actions are restricted via a denylist in NSPredicate.
  2. CAST() restrictions - The use of “Class” as the second argument in CAST() is no longer allowed.
  3. Method call limitations - Only methods from the _NSPredicateUtilities class are allowed when invoking methods.

These changes are enforced by the global variable __predicateSecurityFlags, but they only restrict first-party apps. In addition, [CNFileServices dlsym::] was removed and a magic canary was introcued for NSInvocation, which has greatly restricted the possible ways it can be exploited.

c. bypassing #1

The above mitigations were bypassed using an arbitrary write via the -[NSValue getValue:] method, which was used to overwrite the __predicateSecurityFlags variable.

FUNCTIOn("\x00", "getValue:", $__predicateSecurityFlags)

d. mitigations & bypassing #2

To strengthen the initially insufficient mitigations, pointer-type arguments were restricted in function expressions. Nevertheless, this restriction can still be bypassed using -[NSString getCString:] in place of getValue.

FUNCTION("\x00", "getCString:", addr)

e. bypass PAC

As a replacement for the removed [CNFileServices dlsym::], the method +[DTCompanionControlServiceV2 dlsymFunc] can be used and triggered through -[RBStrokeAccumulator applyFunction:info:].

“DT” belongs to a framework related to Developer Tools, while RB appears to be part of the RenderBox framework. As this information is not publicly documented, reverse engineering my be necessary to verify it.

Although the added mitigations make exploitation more complicated, the presentation states the PAC could still be bypassed using this technique up to iOS 16.3 Beta.

0x03. Exploitation

a. CVE-2023-27937

Each daemon has its own implementation os NSPredicateVisitor, which inspect the expressionType field to detect potentailly dangerous expressions. Yet, during deserialization, the expressionType is parsed from serialized input without validating the sender process.

If all expressionType values are set to 0, the validation can be bypassed, leading to a vulnerability identified as CVE-2023-27937.


0xFF. References