How can I pass messages between a parent process that launches a child process as root using Apple Script and stdin/stdout?
I'm writing an anti-forensics GUI application that needs to be able to do things that require root permissions on MacOS. For example, shutting down the computer.
For security reasons, I do not want the user to have to launch the entire GUI application as root. Rather, I want to just spawn a child process with root permission and a very minimal set of functions.
Also for security reasons, I do not want the user to send my application its user password. That authentication should be handled by the OS, so only the OS has visibility into the user's credentials. I read that the best way to do this with Python on MacOS is to leverage osascript
.
Unfortunately, for some reason, communication between the parent and child process breaks when I launch the child process using osascript
. Why?
Example with sudo
First, let's look at how it should work.
Here I'm just using sudo
to launch the child process. Note I can't use sudo
for my use-case because I'm using a GUI app. I'm merely showing it here to demonstrate how communication between the processes should work.
Parent (spawn_root.py)
The parent python script launches the child script root_child.py
as root using sudo
.
Then it sends it a command soft-shutdown\n
and waits for the response
#!/usr/bin/env python3
import subprocess, sys
proc = subprocess.Popen(
[ 'sudo', sys.executable, 'root_child.py' ],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True
)
print( "sending soft-shutdown command now" )
proc.stdin.write( "soft-shutdown\n" )
proc.stdin.flush()
print( proc.stdout.readline() )
proc.stdin.close()
Child (root_child.py)
The child process enters an infinite loop listening for commands (in our actual application, the child process will wait in the background for the command from the parent; it won't usually just get the soft-shutdown
command immediately).
Once it does get a command, it does some sanity checks. If it matches soft-shutdown
, then it executes shutdown -h now
with subprocess.Popen()
.
#!/usr/bin/env python3
import os, sys, re, subprocess
if __name__ == "__main__":
# loop and listen for commands from the parent process
while True:
command = sys.stdin.readline().strip()
# check sanity of recieved command. Be very suspicious
if not re.match( "^[A-Za-z_-]+$", command ):
sys.stdout.write( "ERROR: Bad Command Ignored\n" )
sys.stdout.flush()
continue
if command == "soft-shutdown":
try:
proc = subprocess.Popen(
[ 'sudo', 'shutdown', '-h', 'now' ],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True
)
sys.stdout.write( "SUCCESS: I am root!\n" )
sys.stdout.flush()
except Exception as e:
sys.stdout.write( "ERROR: I am not root :'(\n" )
sys.stdout.flush()
sys.exit(0)
continue
else:
sys.stdout.write( "WARNING: Unknown Command Ignored\n" )
sys.stdout.flush()
continue
Example execution
This works great. You can see in this example execution that the shutdown command runs without any exceptions thrown, and then the machine turns off.
user@host ~ % ./spawn_root.py
sending soft-shutdown command now
SUCCESS: I am root!
...
user@host ~ % Connection to REDACTED closed by remote host.
Connection to REDACTED closed.
user@buskill:~$
Example with osascript
Unfortunately, this does not work when you use osascript
to get the user to authenticate in the GUI.
For example, if I change one line in the subprocess call in spawn_root.py
from using sudo
to using osascript
as follows
Parent (spawn_root.py)
#!/usr/bin/env python3
import subprocess, sys
proc = subprocess.Popen(
['/usr/bin/osascript', '-e', 'do shell script "' +sys.executable+ ' root_child.py" with administrator privileges' ],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True
)
print( "sending soft-shutdown command now" )
proc.stdin.write( "soft-shutdown\n" )
proc.stdin.flush()
print( proc.stdout.readline() )
proc.stdin.close()
Child (root_child.py)
(no changes in this script, just use 'root_child.py' from above)
Example Execution
This time, after I type my user password into the prompt provided by MacOS, the parent gets stuck indefinitely when trying to communicate with the child.
user@host spawn_root_sudo_communication_test % diff simple/spawn_root.py simple_gui/spawn_root.py
sending soft-shutdown command now
Why is it that I cannot communicate with a child process that was launched with osascript
?