Malicious Installer Plugins

In 2017, Patrick Wardle tweeted something that really piqued my interest:


Essentially, this allows a user to leverage a signed and native macOS application to execute unsigned code. There is great potential for abuse here. Running malicious code in this manner gives an attacker the ability to blend into the environment and inherit the access/permissions of that process. Abusing a feature of the operating system for code execution is familiar territory if you’ve followed the “Living off the land” research led by Matt Graeber (@mattifestation) from a few years ago. This particular feature that Patrick described, is called an installer plugin.

Installer plugins can extend functionality of the installer app by adding a custom install pane (or window) to an install session. These plugins have full access to the Objective C runtime and Cocoa framework. Installer plugins cannot be used with component packages - packages created with the pkgbuild command line tool. Installer plugins can be utilized with product archives, or metapackages that are usually distributed through the mac app store. Let’s review the components that are included with a product archive:

Creating a Product Archive w/ an Installer Plugin on macOS

So let’s walkthrough creating a product archive with an installer plugin. You’ll need to create a component package before creating a product archive. For this example, we’ll create a payloadless component package (a package that only contains scripts). This can be accomplished with macOS or Linux. If you’re interesting in utilizing Linux, refer to Empire, which contains the dependencies and code required to generate component packages. For brevity, we’ll stick with macOS.

  1. Check if you have Xcode command line tools installed with the xcode-select -p command. If they are installed, the full path to the developer directory within the Xcode app will be shown.

  2. If command line tools aren’t installed, you can install them through Xcode or through a simple bash command: xcode-select –install.

  3. Once command line tools are installed, create a new directory. Within that directory, create a file with just the bash shebang on the first line and then exit 0. The script should be named postinstall or preinstall, it doesn’t matter in this instance.

  4. Now use this command to build the component package: pkgbuild –identifier –version x.x –scripts /path/to/scripts/directory </path/to/output/file>

  5. Now that we have the component package, we can build the installer plugin. An example plugin can be found here. This will simply pop a message box once the user enters the custom install pane. Use the Xcode Installer plugin template, replace the existing code, and then build the project.

    plugin template

  6. Create a new folder and then copy both the compiled bundle and the InstallerSections.plist files to the new folder. copy files

  7. Modify the InstallerSections.plist file to look like the image below. There isn’t a specific advantage to using this order. You can adjust it to fit your narrative: InstallerSectionsPlist

  8. Build your product archive with the following command: *productbuild –identifier com.nopayload.package –version x.x –package /path/to/component/pkg –plugins /path/to/plugins/directory .*

  9. Now proceed to open the archive. You will initially see a prompt warning you that this package will run a program and you should only install applications from trusted sources. Select continue and click through the install panes until you arrive at the messagebox pane. Just as we expected, you will see an alert appear. First Prompt

    Malware Oh No

To verify that our bundle has been loaded, we can use TaskExplorer. As Patrick stated in his tweet, our bundle is copied to a temporary location before being loaded into the installer app, albeit a randomized location.


Great, let’s move on to a more practical application. There is one issue with our method of execution. Once the install session is finished and the user clicks the ‘close’ button, the installer application will exit, killing whatever code execution we had. There is a technical hurdle that we need to climb over in order to turn this into a fully weaponized payload. We’ll need to find a way to stop the application from exiting and then hide the UI once the user clicks the close button.

In order to gain control over the Installer application, we can utilize a well known technique in Objective C, called swizzling. Swizzling involves changing the implementation of a method at runtime. In this scenario, we’ll need to swizzle the closeWindow method of the InstallerSectionController protocol. This is the method called once the user clicks close on the summary installer pane. Fortunately, a user on stackoverflow provided a method for setting protocol method implementations. Once we replace the method implementation for the target function, we can include logic to hide the app from the user and then execute a Python Empire payload in a background thread. An example gist of this particular plugin is available here and you can view a demo of its execution below.

Defensive Mitigations/Detections

Pkg files are commonly used by third-party developers to distribute applications outside of the mac app store, or install software updates. I’m not sure how frequently installer plugins are used, so your mileage may vary when developing detections based solely on the presence of an installer plugin within a product archive. You could also consider using the fsevents API to monitor for bundles being dropped onto the file system. However, the plugin is copied to a random location each time the installer is executed. Thus, rendering any detection methods based on file system changes quite difficult. The install.log file is kept in the path /var/log/ . When a plugin is loaded by the installer app, an entry is created by the installer app.


That entry will only provide information as to whether an installer plugin was loaded, without any specifics in regards to its location or name. The pkgutil command line utility can be utilized to inspect the contents of a product archive. Expand a product archive with the –expand flag to decompress the archive and view its contents. Note that the plugins directory is not compressed. Also, pkgutil can be used to list all installed packages for a host, by package ID. The –pkg-info flag will provide additional details about a particular package, including the version, volume, location, and install time.

Additionally, I’ve explored solutions that are automated and more suitable for enterprise environments. Osquery contains two tables that may provide the information we need to build detections. The process_open_files table contains information for every file descriptor held by a particular process. The process_memory_map table contains information that pertains to memory mapped files in every process. Surprisingly, there weren’t any artifacts for the installer plugin in either table.

Unfortunately, I have yet to find a solution to monitor dylib load or bundle load events in a specific process. Hopefully the methods presented above will provide a starting point for developing mitigations.

Query 1

Query 2

Written by Chris Ross on 08 February 2018