10 Jan Application Playpen
Overview
We have developed a methodology of managing enterprise and/or shared environment unfriendly applications that has evolved over the years at the Marriott Library or other locations on campus that we support. We affectionately call it the “Crappy App” model which we use sanitize applications with exceptional requirements.
These requirements include:
- Application has insecure permissions
- Application has hardcoded variables (usernames, paths, etc.)
- Application tries to update with every launch
- Application requires access to locations we’d prefer they not have access
- Application requires to be launched by a specific user or group
- Application requires additional configuration pre or post launch
Originally, we developed this methodology to better secure the Classic environment running in our shared environments. Classic implemented Mac OS 9 running as an application within earlier versions of Mac OS X, allowing you to use your older “Classic” applications. The Classic environment had security & state issues and some Classic applications may not function properly from a System Folder that can’t be written, provides a good security measure to prevent startup from Mac OS 9 and ensures that each user starts with a pristine System Folder. To resolve the issues but not hinder the needed functionality we developed a workaround that included placing the Classic System Folder in a read-only disk image, then hack the “Classic Startup” application to mount the image as read-write with a shadow file saved during the Mac OS X user session.
For example, the following command would mount the Classic image with a shadow file.
hdid /Classic.dmg -shadow ∼/Library/Management
This file can then be mounted, and used, but will not save any information once the user is logged out. Any application can take advantage of this method, allowing for more secure and easier to cleanup environments.
For example, the AppleScripts below will run VirtualPC with a ’shadow’ file…
do shell script "/usr/bin/hdid ’ /Applications/Virtual PC 6.0.1/Windows 98 SE.dmg’ -shadow VPCShadow’" delay 5 tell application "Finder" open file "Mac OS X:Applications:Virtual PC 6.0.1" end tell
Mike Bombich (aka Carbon Copy Cloner fame) developed a GUI application version that helped simplify and automate the steps into his “Shadow Classic” application. This application implements a shell script that will mount your read-only Classic disk image with a shadow file so that it appears to users and applications as read-write. This allows users and applications to make changes to the System Folder or applications on the disk image at will. When the next user logs in and starts Classic, the disk image is mounted again with a fresh shadow file, having none of the changes imposed by the last user.
For example, this is shell script included with the ShadowClassic application…
#!/bin/sh ## currentSettings;%classic%;%classicName%;%shadow%; if [ -e "%classic%" ]; then if [ ! -d "/Volumes/%classicName%" ]; then /bin/rm -f %shadow% /usr/bin/hdid "%classic%" -shadow %shadow% fi #/usr/bin/osascript -e "launch application \"Classic Startup\"" /System/Library/CoreServices/Classic\ Startup.app/Contents/MacOS/Real\ Classic\ Startup fi
ShadowClassic implements the equivalent commands…
cd /System/Library/CoreServices/Classic\ Startup.app/Contents/MacOS sudo mv Classic\ Startup Real\ Classic\ Startup sudo ditto ~/ShadowClassic.app/Contents/Resources/shadow ./Classic\ Startup
Then it replaces the Classic Startup executable with a shell script that mounts your Classic image with a shadow file and launches the “Real Classic Startup” application.
Why Distribute Crappy Apps?
This methodology has been extended and implemented for multiple “crappy applications”. Since we are a higher education institution, in a large decentralized research university, our department is in a campus central location, and the campus main library; we tried to meet the computing and software needs for campus which is often defined by multiple departments, colleges and groups with different and varying needs and goals. With that in mind, we have try to address these needs by sanitizing software that other environments like corporations could simply say NO to distributing and supporting in their fleet. But, as in our environment we try to make a best effort in sanitizing the “crappy app” to safely distribute.
We would strongly recommend to document and give the “crappy app” developer feedback on why their application is enterprise unfriendly and that unless its changed you will transition to another application to provide needed functionality and try to get others in the Mac Admins community to do the same (i.e., carrot). In the past, we maintained a “crappy app” shame page, but we decided that this wasn’t a priority for our group or wasn’t the most effective strategy in motivating a company to change their development methodologies and practices (i.e., stick).
Mac’s Are Secure/Resilient?
Other arguments against taking a little extra time and effort to sanitize needed “crappy apps” is that Mac are secure and one bad application won’t break the entire Apple system. But, with presentations from Apple knowledgeable persons like Patrick Wardle, who recently presented at MacTECH about the “Protecting the Garden of Eden“, Mac systems are being more and more targeted and are having increasing Common Vulnerabilities and Exposures (CVE) or publicly known cybersecurity vulnerabilities. And since Apple has implemented System Integrity Protection (SIP) in OS X El Capitan, which comprises a number of mechanisms that are enforced by the kernel including protection of system-owned files and directories against modifications. And with newer macOS versions Apple has increased scope of these mechanisms to improve privacy, security, resilience and stability of the system. Seems we should try to follow, and try to implement the principle of least privilege to justify taking a little extra effort to sanitizing “crappy applications” in our environment.
Principle of Least Privilege
The principle of least privilege requires that in a particular abstraction layer of a computing environment, every module like a process, a user, or a program must be able to access only the information and resources that are necessary for its legitimate purpose and are essential to perform its intended function. This principle is widely recognized as an important design consideration in enhancing the protection of data and functionality from faults (fault tolerance) and malicious behavior (system security).
Benefits of the principle include:
- Condensed Attack Surface
Limiting privileges for people, processes, and applications means the pathways and ingresses for exploit are also diminished.
- Reduced Malware Infection and Propagation
Least privilege helps dramatically reduce malware infection and propagation, as the malware should be denied the ability to elevate processes that allow it to install or execute.
- Improved Operational Performance
When it comes to applications and systems, restricting privileges to the minimal range of processes to perform an authorized activity reduces the chance of incompatibility issues cropping up between other applications or systems, and helps reduce the risk of downtime.
- Easier to Achieve and Prove Compliance
By constraining the activities that can possibly be performed, least privilege enforcement helps create a less complex, and thus, a more audit-friendly, environment.
The following Apple Developer documentation on “Elevating Privileges Safely” covers the basics of the Principle of Least Privilege.
Details
- Start with the Application Playpen launcher template
- Create the read-only image of the “crappy application”
- Place the image into the Application Playpen launcher application
- Update the Application Playpen launcher main.scpt AppleScript file
- Update the Application Playpen launcher icons files to match “crappy application”
- Update the Application Playpen launcher Info.plist to match “crappy application”
- Test, test and test again
Currently, we use the following AppleScript template to build the Application Playpen launcher.
Application Playpen Launcher Template
All of the logic for launching and managing configuration is handled in the /Contents/Resources/Scripts/main.scpt file.
(* This script is automatically generated. It's used to wrap applications that behave poorly. Some applications like to force updates or require world read/write permissions, which we don't generally like in our sort of environment. This allows us to keep everything locked up carefully. COPYRIGHT (c) 2015 University of Utah, Marriott Library, ITS. All rights reserved. Author: Pierce Darragh - pierce.darragh@utah.edu Revised: Sam Forester - sam.forester@utah.edu Creation Date: May 7, 2015 Last Updated: July 8, 2016 Permission to use, copy, modify, and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of The Marriott Library not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. This software is supplied as-is without expressed or implied warranties of any kind. *) global myPath global appName global diskName global appPath global imagePath global shadowPath global mountRoot global homeFolder global shadowFile global shadowFolder on run -- Set all of the locations for things. -- Most likely, you'll only ever need to change the 'appName'. If you give everything else -- similar names, it'll make your life easier. -- -- appName: the name of the application -- diskName: the name of the disk -- appPath: the path to the application in the image -- imagePath: the path to the image in this bundle -- homeFolder: the path of the current user's home folder -- shadowFolder: the directory in which the shadow file will be saved -- shadowFile: the full path to the shadow file -- mountRoot: the location to mount the volume under (change for obnoxious applications) set myPath to (path to me as Unicode text) set appName to "<TEMPLATE>" set diskName to appName set appPath to diskName & ":Applications:" & appName & ".app" set imagePath to POSIX path of (myPath & "Contents:Resources:" & appName & ".dmg") as Unicode text set homeFolder to (get path to home folder) as Unicode text set shadowFolder to (homeFolder & "Library:Application Support:" & appName) set shadowFile to POSIX path of (shadowFolder & ":" & appName & ".shadow") set mountRoot to "/Volumes" my main() end run on main() -- If the application is running, quit. -- Otherwise, launch it! if (isRunning()) then display dialog (appName & " is already running!") buttons {"Thanks for the reminder!"} default button 1 giving up after 5 with icon caution return else launch {} end if end main -- Launches the application. on launch {} -- Inform the user that this might take a moment (mounting a disk image) -- uncomment this if you want to have a dialog appear while launching the wrapped application # display dialog "This application may take a while to load. #Remember that none of your data will be saved once you log off. Back up anything you need before you leave! #-- Mac Support Group #Marriott Library IT Services" buttons {"Okay"} default button 1 with icon path to resource (appName & ".icns") in bundle (path to me) -- If the disk is not mounted, mount it. -- If after this the path to the executable exists, launch it. tell application "Finder" -- create the intermediate directories where we will be keeping the shadowfile if not (exists folder shadowFolder) then -- I'm sure there is a better way to do with with actual AppleScript, but I was unable to find out how and mkdir works just fine set dir to POSIX path of shadowFolder do shell script ("mkdir -p " & quoted form of dir) end if if not (disk diskName exists) then -- Mount the image at imagePath with: -- * -nobrowse Prevents the volume from appearing in Finder or on the Desktop. -- * -noautoopenro Makes sure we won't accidentally pop open a Finder window into the volume. -- * -noverify Ensures that hdiutil will not verify the volume. Speeds up mount times. -- * -mountroot Changes where the volume is mounted to. -- * -shadow Allows read/write access by creating a file the user can edit and shadowing what would have been the edits there. do shell script ("hdiutil attach -nobrowse -noautoopenro -noverify -mountroot " & quoted form of mountRoot & " -shadow " & quoted form of shadowFile & " " & quoted form of imagePath) end if -- Launch the app for the user too. if appPath exists then open of appPath end if end tell end launch -- Returns a boolean telling whether the application is currently running from the image. on isRunning() try set psResults to (do shell script "pgrep -f '" & binPath & "'") return true on error return false end try end isRunning
Create the Read-only Image of the “Crappy Application”
While I’m sure all of this can be done in Disk Utility, we will use the hdiutil command in the terminal to ensure everything gets set properly. The hdiutil tool uses the DiskImages framework to manipulate disk images. We’ll create the image in the ~/Library/Application Support/[CRAPPY APPLICATION] directory, which will allow the shadow file to be saved in each user’s home folder and keep or removed it based on the system’s configuration type.
For example, for student systems, we would disregard any user home folder modifications including the created shadow file on student logout. For staff, the home folder contents including the shadow file are retained on staff logout, so, modifications to a “crappy app” environment could be retained if beneficial to the staff.
Change Directory to tmp
cd /tmp
Create Crappy App Image File
To create a read/write image to install the “crappy application”, enter the following commands:
hdiutil create -size 5g -fs HFS+J -type SPARSE -volname [CRAPPY APP]
- -size
This option allows you to specify a image size of 5 GB
- -fs
This option allows you to specify the filesystem of the image such as HFS+, HFS+J (JHFS+), HFSX, JHFS+X, APFS, FAT32, ExFAT, or UDF.
- -type
This option is particular to create and is used to specify the format of empty read/write images. UDIF is the default type, but SPARSE creates a UDSP, a read/write single-file image which expands as is is filled with data.
- -volname
The newly-created filesystem will be named [CRAPPY APP]
For example…
hdiutil create -size 5g -fs HFS+J -type SPARSE -volname CrappyApp CrappyApp created: /private/tmp/CrappyApp.sparseimage
Mount Crappy App Image Volume
Next, to mount the “crappy application” image volume to begin installation process, enter the following commands:
hdiutil attach CrappyApp.sparseimage /dev/disk1 GUID_partition_scheme /dev/disk1s1 EFI /dev/disk1s2 Apple_HFS /Volumes/CrappyApp
At this point, you should have the volume mounted to the default location: /Volumes/CrappyApp
And see it mounted in Finder on the Desktop:
Install Application to Image
Now run the “crappy app” installer package, and when presented with a choice of “Where should this application be installed to?”, specify the “CrappyApp” volume. Some installers ask for a specific directory or if the “crappy app” installer doesn’t support this and it is a application package, you could simply copy it over to the mounted disk. An a application package is a file system directory that is normally displayed to the user by the Finder as if it were a single file or application.
We would recommend opening a Finder window and checking that everything seems installed properly in the mounted image. Then, go back to the terminal to finalize our image creation process by running the following commands…
hdiutil detach /Volumes/[CrappyApp] hdiutil convert [CrappyApp].sparseimage -format UDRO -o [CrappyApp].dmg
The above commands unmount or disconnect (i.e., detach ) the read/write image from the system, then converts the read/write image into a UDIF read-only image (i.e., UDRO )
mv /path/to/[CrappyApp].dmg /path/to/[CrappyApp.app]/Contents/Resources/
From inside the wrapper application, navigate to Contents/Resources/Scripts and open the main.scpt file in AppleScript Editor. We’ve tried to abstract as much of the code as possible, so all you should really need to change is the variable at the top named appName and the application-specific binary launcher location. Its given value is <TEMPLATE> . Change this value to be the name of your application.
For example, change…
set appName to "<TEMPLATE>"
To…
set appName to "CrappyApp"
The script assumes that you’ve named everything identically, so the image file is CrappyApp.dmg and it mounts to /Volumes/CrappyApp , and the real application will be at /Volumes/CrappyApp/Applications/CrappyApp.app . This might not necessarily be true and so you will need to change the appPath variable accordingly.
For example…
set myPath to (path to me as Unicode text)
Update Icon Image
To make the launcher application appear as the real application to Spotlight and to convince our users that it’s the regular application, we’ll include the original application’s icons file. Mount the disk image we made and navigate in Finder to the application’s .app bundle within the mounted volume. Inside of the bundle, look in the Contents/Resources/ directory for a file ending in ‘.icns ‘. This is an Apple icons file, note there may be more than one; so make sure you use the correct main application icon. Copy this .icns file to our launcher application and rename it with the application’s name (i.e., [CrappyApp].icns ). Then replace the applet.icns file with the your application icon file.
Property List Customizations
Inside the launcher application’s top directory, Contents , there is a property list file named Info.plist . This file contains the information describing how the launcher application presents itself to the user, such as the name, version number, and icons to display.
- CFBundleIconFile – the .icns file (“CrappyApp”)
<key>CFBundleIconFile</key> <string>applet</string>
- CFBundleIdentifier – the unique identifier of our bundle (“my.domain.CrappyAppLauncher”)
<key>CFBundleIdentifier</key> <string>com.apple.ScriptEditor.id.Crappy-App</string>
- CFBundleName – the name of the application (“CrappyApp”)
<key>CFBundleName</key> <string>CrappyApp</string>
- CFBundleShortVersionString – the version for the application. Simply copy the value from the regular application, such as “4R7 P3” or “1.58.27”, etc.
<key>CFBundleShortVersionString</key> <string>1.0</string>
Test
No matter what, you should always double- and triple-check your compiled playpen application before considering pushing it to your enterprises distribution cycle (i.e., development, testing, production).
Helper Script Elevated Privileges
Some “crappy app” implementations require administrative privileges to make modifications to effective use this methodology like for example with Xcode we add a user explicitly using Xcode to _Developer group. To aid in the setup, we created python script to help administrators manage their sudoers file to allow scripts to run with elevated privileges but allow standard users to unknowingly use these scripts called “Sudoers Manager” and made it available to the community.
There is a file located at /etc/sudoers which contains a list of rules for privilege escalation. This file allows users to run commands as other users. Most frequently, this file is used to grant super-user (“root”) privileges to non-root users. The administration of the sudoers file is intricate, and it’s important that you don’t make mistake or else you risk losing all administrative access on the given computer (unless your root account has a password that you know). Our python script simplifies & automates the process of modifying the sudoers file for your environment on a fleet of systems.
Drop-In Rules Directory
Note, that you can use the sudoers #includedir directive and then distribute sudoers rule files to /etc/sudoers.d directory as part of installation package, script or other methods. Internally, we have updated our “Sudoers Manager” to support sudoers.d with validation and automatically creating the rule files, but currently haven’t pushed the update to our public GitHub repository.
For example, you need to edit the /etc/sudoers file to allow support of the #includedir directive:
## Read drop-in files from /private/etc/sudoers.d ## (the '#' here does not indicate a comment) #includedir /private/etc/sudoers.d
Be aware that sudo will skip file names that end in `~’ or contain a `.’ character to avoid causing problems with editor or other temporary/backup files.
Validating Rules File
You can validate your sudoers rules file, before distributing or packing it by using the following command:
/usr/sbin/visudo -csf /path/to/your/sudoers_rules_file
We would recommend that these rule files should be owned by root and with permissions 0440. Then the root user can read the file but not write to it!
For example…
visudo -c /etc/sudoers: parsed OK /private/etc/sudoers.d/sudoers_rules_file: parsed OK
Mac Support
Note, some older versions of macOS might not fully support sudoers.d drop-in files. You can try manually add the #includedir directive and /etc/sudoers.d directory and see if it works properly. The above command will show you all the files sudo is parsing and show potential issues.
Example
We use sudoers.d with rules files with or custom “Tablet Driver Installer” application, which allows standard users (students, staff & faculty) take multiple versions of tablets to a system and install the correct version based on the tablet make and model.
For example, here is the python script we use with our custom “Table Driver Installer” application to install drivers with elevated permissions using sudoers and sudoers.d with rules file.
#!/usr/bin/python from subprocess import Popen, PIPE import sys def main(): path = sys.argv[1] arg = sys.argv[2] path = "installer -pkg " + "\"" + path +arg+ "\"" + " -target /" print Popen(path, shell=True, stdout=PIPE, stderr=PIPE).communicate()[0] if __name__ == "__main__": main()
Other Workarounds
Not all “crappy apps” can’t be solved using this application playpen methodology and we implement other solutions like using a location of the file system to validate & restore the pristine state of “crappy apps” location(s) at logout, startup, etc.
Examples
Here are some examples of applications that we have implemented the “crappy apps” model and use in our environment.
Xcode
Xcode is Apple’s development environment. It’s used to develop applications on Macs, iOS devices like iPhones and the Apple Watch. However, Xcode requires users to have _Developer group privileges to allow the debugger and emulators to work correctly. We give our users only the least privileges they need to get their work done and we take great pains to secure our environment. We felt giving every user _Developer rights was unnecessary and something solve using the “crappy app” methodology.
We use the following shell script named AddUIDToDevGroup.sh that adds the current user launching our Xcode “crappy app” application, then adds the user to the _Developer group and then launches the “real Xcode application. We then could remove the user from the group at user logout and/or startup.
#!/bin/sh inputUID=$1 inSudo=`/usr/bin/grep $inputUID /etc/sudoers -c` if /bin/test $inSudo -ne 0 then exit 0 else /usr/bin/dscl . merge /Groups/_developer GroupMembership $inputUID fi
NVivo
With NVivo we originally did activation as part of system maintenance, but with the amount of new & temporary systems installed & number of times maintenance was run our sales representative from QSR International, the developers of NVivo, thought that the software was very popular at the University of Utah, maybe the most popular location in the world ;-). We provided them with KeyServer statistics & logs with concurrent usage and explained our maintenance activation and shared our scripts, but they didn’t completely believe us 🙁
So, we decided to implement NVivo using our “crappy app” methodology where activation would only happen when a user explicitly launches NVivo.
For example, here is NVivo activation python script we use the the application playpen launcher:
#!/usr/bin/env python import os import subprocess import sys from management_tools import loggers def main(): # names the logger file in /var/log/management logger = loggers.FileLogger(name='activate_nvivo', level=loggers.DEBUG) logger.info("Starting up...") # Declaring path to home. home = os.path.expanduser("~") # Declaring paths to NVivo and NVivo XML. # Receives input from associated AppleScript. # Replaces "\" from path name and replaces with space so that it works with subprocess # since subprocess doesn't understand backslashes. nvivo_path = sys.argv[1] nvivo_path = nvivo_path.replace('\ ',' ') logger.debug('nvivo_path: {0!s}'.format(nvivo_path)) path_to_nvivo_xml = home + "/Library/Application\ Support/NVivo/NVivo11Activation.xml" path_to_nvivo_xml = path_to_nvivo_xml.replace('\ ',' ') logger.debug('path_to_nvivo_xml: ' + path_to_nvivo_xml) #Activate logger.info("Proceeding with activation of NVivo.") cmd = [nvivo_path, '-initialize', '[ACTIVATION CODE]', '-activate', path_to_nvivo_xml] logger.debug('cmd: {0!s}'.format(cmd)) nvivo_activation_process = subprocess.check_output(cmd) logger.debug('nvivo_activation_process: {0!s}'.format(nvivo_activation_process)) logger.info("Exiting script.") sys.exit(0) if __name__ == '__main__': main()
The user uses Finder or Spotlight and gets our “application playpen” NVivo, which mounts the image, modifies the NVivo activation xml file and then launches the “real” NVivo.
To use this script in your environment, you need to change the [ACTIVATION CODE] with your NVivo activation code.
Unity
The challenge with Unity is that it continuously updates on application launch and we wanted to support multiple versions like standard & pro on the same system or older versions of the software.
With Unity Pro we used the following python script to automatically setup the serialization in a classroom environment using the “crappy app” methodology:
#!/usr/bin/python import subprocess import sys ''' In order to use this script, the crappy app has to first be mounted. Once the crappy app is mounted, the script can be called but must be followed by an argument/variable. The two available variables are 'fullSetup' and 'reserialize'. 'fullSetup' runs Unity Pro in the background while logging in with the user's credentials (-username -password) and will enter the serial number (-serial) without any windows popping up (-batchmode). It'll quit once finished (-quit). 'reserialize' does the same as 'fullSetup' except that it doesn't run in batchmode and won't quit when it's finished. For the use of this script it is called 'reserialize' since Unity Pro has a hangup creating the Unity Pro license the first time it tries to serialize (fullSetup). To fix this, it must be serialized again (reserialize). ''' option = sys.argv[1] if option == 'fullSetup': subprocess.call("/Volumes/Unity\ Pro/Applications/Unity/Unity\ Pro.app/Contents/MacOS/Unity -quit -batchmode -serial [SERIAL NUMBER] -username '[USERNAME]' -password '[USER PASSWORD]'", shell=True) elif option == 'reserialize': subprocess.call("/Volumes/Unity\ Pro/Applications/Unity/Unity\ Pro.app/Contents/MacOS/Unity -serial [SERIAL NUMBER] -username '[USERNAME]' -password '[USER PASSWORD]'", shell=True) else: subprocess.call("""osascript -e 'tell application "System Events" to display dialog "Please make sure that your arguments match one of the following: \n\t\t\t\t\t fullSetup \n\t\t\t\t\t reserialize"' """, shell=True)
If you want to use this Unity Pro serialization script, change [SERIAL NUMBER] , [USERNAME] & [USER PASSWORD] will need to be changed for your environment.
Future
In the future, we will be moving this project to Python and Tkinter to provide user interface along with a command line equivalent. There will be a “creator” script/application that will automate the creation of the “launcher” script/application and allow including a pre or post scripts or commands that could be executed before or after launch of the “real” crappy application. We are also debating based on frequency of updates, demands and number of “crappy applications” we need to maintain, creating an AutoPkg processor to automatically build the “crappy application” launchers. The project is in development and if you are interested, please give use feedback, as this might help change our priority or motivate us to focus on this project.
No Comments