Search code examples
pythonmacosrootsetuid

Launch child process as root (python, setuid, MacOS)


How can I launch a child process that has root privileges?

I have a python program in MacOS that can do most of its operations as a normal user. But occasionally, triggered by some user interaction, it will need root permissions to preform a task.

For security reasons, I don't want the entire GUI app to be started and left running as root. I want only a child process with a very minimal subset of functions to run as root.

For UX reasons, I don't want to have to tell the user "Sorry, please restart this app as Administrator". I want to be able to have them stay in the GUI, get presented with a pop-up that says "Uh, you need root to do that. Please enter your password."

Of course, if my unprivileged python process attempts to become root with

setuid(0)

...then I just get a permissions error

PermissionError: [Errno 1] Operation not permitted

What can I use as an alternate to setuid() so that I can launch a new child process on a MacOS system, after escalating privilege by getting authentication from the user in the GUI?


Solution

  • I want to be able to have them stay in the GUI, get presented with a pop-up that says "Uh, you need root to do that. Please enter your password."

    This is exactly what the MacOS Security API's AuthorizationExecuteWithPrivileges() function was created for.

    You can call AuthorizationExecuteWithPrivileges() directly with python's ctypes.

    For example, consider your parent script running as your normal, non-root user. If you try to just run setuid(0), then it will fail with

    PermissionError: [Errno 1] Operation not permitted
    

    Instead, let's create another script named root_child.py, which we'll execute as root with AuthorizationExecuteWithPrivileges()

    Child (root_child.py)

    #!/usr/bin/env python3
    import os
    
    if __name__ == "__main__":
       try:
          os.setuid(9)
          print( "I am root!" )
       except Exception as e:
          print( "I am not root :'(" )
    

    Parent (spawn_root.py)

    We can execute the above root_child.py script as root from our non-root script spawn_root.py:

    import sys, ctypes, struct
    import ctypes.util
    from ctypes import byref
    
    # import some C libraries for interacting via ctypes with the MacOS API
    libc = ctypes.cdll.LoadLibrary(ctypes.util.find_library("c"))
    
    # https://developer.apple.com/documentation/security
    sec = ctypes.cdll.LoadLibrary(ctypes.util.find_library("Security"))
    
    kAuthorizationFlagDefaults = 0
    auth = ctypes.c_void_p()
    r_auth = byref(auth)
    sec.AuthorizationCreate(None,None,kAuthorizationFlagDefaults,r_auth)
    
    exe = [sys.executable,"root_child.py"]
    args = (ctypes.c_char_p * len(exe))()
    for i,arg in enumerate(exe[1:]):
        args[i] = arg.encode('utf8')
    
    io = ctypes.c_void_p()
    
    print( "running root_child.py")
    err = sec.AuthorizationExecuteWithPrivileges(auth,exe[0].encode('utf8'),0,args,byref(io))
    print( "err:|" +str(err)+ "|" )
    print( "root_child.py executed!")
    

    Example Execution

    Note that, because the credential challenge for AuthorizationExecuteWithPrivileges() comes via the GUI, you must execute this from within the GUI. If you attempt to execute the above scripts, for example, over a SSH in a tty, you'll get an error -60007, which is errAuthorizationInteractionNotAllowed and means:

    The Security Server denied authorization because no user interaction is allowed.

    user@host ~ % ./spawn_root.py 
    running root_child.py
    err:|-60007|
    root_child.py executed!
    user@host ~ % 
    

    However, if executed from the Terminal app in the GUI, then it prompts the user for their password.

    screenshot showing a pop-up window with title "Python wants to make changes" and a Username and Password field presented to the user in the GUI

    If the user successfully enters their credentials correctly, then the root_child.py script is executed with root privileges.

    user@host ~ % ./spawn_root.py 
    running root_child.py
    err:|0|
    root_child.py executed!
    

    Additional Information

    Security

    Note that AuthorizationExecuteWithPrivileges() has been deprecated by apple in-favor of an alternatve that requires you to pay them money. Unfortunately, there's some misinformation out there that AuthorizationExecuteWithPrivileges() is a huge security hole. While it's true that using AuthorizationExecuteWithPrivileges() incorrectly can cause security issues, it is not inherently insecure to use it.

    Obviously, any time you run something as root, you need to be very careful!

    AuthorizationExecuteWithPrivileges() is deprecated, but it can be used safely. But it can also be used unsafely!

    It basically boils down to: do you actually know what you're running as root? If the script you're running as root is located in a Temp dir that has world-writeable permissions (as a lot of MacOS App installers have done historically), then any malicious process could gain root access.

    To execute a process as root safely:

    1. Make sure that the permissions on the process-to-be-launched are root:root 0400 (or writeable only by root)
    2. Specify the absolute path to the process-to-be-launched, and don't allow any malicious modification of that path

    Further Reading

    1. AuthorizationExecuteWithPrivileges() Reference Documentation
    2. https://github.com/cloudmatrix/esky/blob/master/esky/sudo/sudo_osx.py
    3. https://github.com/BusKill/buskill-app/issues/14
    4. https://www.jamf.com/blog/detecting-insecure-application-updates-on-macos/