The context: PasivRober
I spent a few hours this week trying to reverse engineer the PasivRober malware sample. I had the writeup by Kandji and the sample provided by Objective-See organization on GitHub.
The malware was hidden inside multiple nested .pkg
installers, and I realized that there was no clear guide on how to extract payloads from malicious package installers. This is why I wanted to write this post - to explain the basics of how to extract payloads from malicious macOS package installers.
Today we'll talk about package installers, aka .pkg
files, and how to properly extract their contents for analysis.
What this post will and won't cover
What we will do:
- Extract payload and scripts from package installers
- Understand the structure of macOS package installers
- Learn how to handle nested packages and complex installer structures
What we won't do:
- Reverse engineer the PasivRober malware (the Kandji post is the gold standard for that)
- Provide a full reverse engineering guide for package installers
This post focuses on the technical process of extracting package installer contents, which you can reuse with other malware.
The PasivRober sample
First, I unzipped the malware sample which contains three files:
- A README file which contains a link to the writeup
- A binary named
wsus
- A package installer named
zmn2.0.6839.pkg
I'll understand later that the binary is contained in the package installer...
As I am in an isolated environment and also because I am curious to see what it looks like, I tried to open it with the classic double-click, but...
I am curious to understand how the operating system recognized it. Looking at the Xprotect logs, I can see that macOS performed a background analysis and detected it as malware. But maybe in another post...
The .pkg
is now in the bin. I'll have to find another way to interact with it.
I would like to check the content of the installer. I naturally right-click, but I don't find the usual "Show Package Contents" option available. We need an alternative solution.
Note: I'll learn later that the "Show Package Contents" is reserved for installers that are "legacy" (or "bundled").
Manual package content extraction
If you are familiar with the macOS ecosystem, you probably already know that there is a pkgutil
CLI.
There are a bunch of commands I would like to try. Let's start with --check-signature
:
pkgutil --check-signature zmn2.0.6839.pkg Package "zmn2.0.6839.pkg": Status: revoked signature Signed with a trusted timestamp on: 2024-04-19 10:28:49 +0000 Certificate Chain: 1. Developer ID Installer: weihui chen (QPV7YX8YQ9) ..
The second command I am interested in is the --payload-files
command:
pkgutil --payload-files zmn2.0.6839.pkg . ./Library ./Library/program.pkg ./Library/.temp ./Library/.temp/update_config_arm ./Library/.temp/update_config ./Library/LaunchDaemons ./Library/LaunchDaemons/com.myam.plist
We have a first view of the Library
folder files contained in the package. We can already notice that there
is another installer named program.pkg
. We can also see that there is an update_config
program, hard to say
if it is a script or an executable at a glance. Finally, we have a .plist
file which is probably related to a
LaunchDaemon, probably used for persistence.
Now, I would like to extract those files and start my analysis. There is one last pkgutil
command that will
help me to do that:
pkgutil --expand zmn2.0.6839.pkg zmn2_pkg
The --expand
should have expanded the package into zmn2_pkg
. So let's inspect the content of it:
tree zmn2_pkg/ zmn2_pkg/ ├── Distribution ├── Resources │ └── en.lproj │ └── Localizable.strings └── config.pkg ├── Bom ├── PackageInfo ├── Payload └── Scripts ├── postinstall └── preinstall 5 directories, 7 files
First surprise, we don't find files and folder we saw in the previous command. This is due to the
fact that this command simply extract the content of the package, while the --payload-files
represent file that will be installed on the machine after the installer has finished.
Also, we have two nested installers here:
zmn2.0.6839.pkg
which is a distribution installerconfig.pkg
which is a component package (the nested one)
There is a third one called "bundled" or legacy.
What is the difference between these installers?
Distribution installer vs Component installer
Before we start diving into installers, I'll skip the legacy one, as Apple started to deprecate them.
Distribution
As the high-level package we met is a distribution one, let's start with that.
The most important thing to know about distribution packages is that they can contain other packages.
Let me show you an example:
Here, you can select the package you would like to install.
Another particularity of the distribution package is that it can contain additional resources to customize the experience of the user during installation, like a license for instance.
Component
The component package installer has two parts:
- Payload: all the files that will be installed on the Mac. It could be a binary for instance, an application or a script.
- Scripts: post and pre-install scripts (we had pre and postflight in legacy package installers)
- PackageInfo: this is a plist file that contains information like where the package should be installed, the identifier and the version.
It could have one or the other or both.
Note: Distribution installers could also contain Scripts and Payload.
Flat!
At the beginning of my research, I read somewhere (sorry for the lack of precision) that these packages were called "flat packages". But what does flat mean?
The creator of the "Packages" application also wrote documentation about the flat package format.
"This file is actually a xar archive."
I did not need to interact with xar
directly, but I think that it is good to have it in mind.
Now let's try to extract the payload.
Extract Payload
First, let's try to see what the Payload itself is:
file zmn2_pkg/config.pkg/Payload zmn2_pkg/config.pkg/Payload: gzip compressed data, from Unix, original size modulo 2^32 80451072
So let's decompress it with gzip
. We'll use the following flags:
-d
for decompress-c
for sending output tostdout
We'll redirect stdout
to the Payload.cpio
file:
gzip -dc Payload > Payload.cpio
You'll notice that I used the .cpio
extension. Indeed, if you try to see what kind of file it is:
file Payload.cpio Payload.cpio: ASCII cpio archive (pre-SVR4 or odc)
... you'll find that this is a cpio
archive. According to Wikipedia:
cpio is a general file archiver utility and its associated file format. It is primarily installed on Unix-like computer operating systems. The software utility was originally intended as a tape archiving program as part of the Programmer's Workbench (PWB/UNIX), and has been a component of virtually every Unix operating system released thereafter.
Darwin comes with its cpio
command too:
where cpio /usr/bin/cpio
We'll redirect the output of the Payload.cpio
to the input of the cpio
command by using the -i
flag:
cpio -i < Payload.cpio 157131 blocks
Now I have a /Library
folder. Let's see what we have inside:
tree Library/ Library/ ├── LaunchDaemons │ └── com.myam.plist └── program.pkg
We are getting closer as we can see file names output by the pkgutil --payload-files
we previously used.
Now that I am writing this post, I realize that the --payload-files
was doing a kind of recursive job to extract each nested .pkg
and Payload
.
Undocumented pkgutil
commands
There is not a lot of content around pkg installers, but I found one really nice talk that mentioned some undocumented methods. You can watch this macOS application packaging 101 video by Rich Trouton for more details.
Let's try to find them by ourselves using the strings
command:
strings /usr/sbin/pkgutil @(#)PROGRAM:pkgutil PROJECT:pkgutil-860 INSTALL_TARGET_VOLUME help debug terse ... flatten flatten-full // Bingo! expand expand-full // Bingo!
We have two strings which seem to be flags. If we take the latter two, it seems that the --expand
command
we saw earlier has a full
version.
Let's try that out:
pkgutil --expand-full zmn2.0.6839.pkg zmn2_pkg
Let's see what we have in there:
tree zmn2_pkg/ zmn2_pkg/ ├── Distribution ├── Resources │ └── en.lproj │ └── Localizable.strings └── config.pkg ├── Bom ├── PackageInfo ├── Payload │ └── Library │ ├── LaunchDaemons │ │ └── com.myam.plist │ └── program.pkg └── Scripts ├── postinstall └── preinstall 8 directories, 8 files
Now that I am writing this post, it makes a lot of sense as the --payload-files
was able to give me
the list of all nested files.
All nested files? We see here that the program.pkg
is still not expanded...
The last hidden pkg
pkgutil --expand-full program.pkg program_pkg
program_pkg/ ├── Distribution ├── Resources │ └── en.lproj │ └── Localizable.strings └── program.pkg ├── Bom ├── PackageInfo ├── Payload │ └── Library │ └── protect │ └── wsus │ ├── Config │ │ ├── CN │ │ │ ├── LocList.xml │ │ │ ├── MapString.Ini │ │ │ └── Plugin.ini │ │ ├── HtmlStyle │ │ │ ├── ChatInstall │ │ │ │ ├── SmartViewer │ │ │ │ │ └── SmartViewer.exe │ │ │ │ ├── install.bat │ │ │ │ └── uninstall.bat │ │ │ ├── chat.css │ │ │ ├── chatMsg.js │ │ │ ├── images │ │ │ │ ├── recive-bottom-other.gif │ │ │ │ ├── [... truncated] │ │ │ │ └── send-top.gif │ │ │ ├── style.css │ │ │ ├── test.mp4 │ │ │ └── test.wav │ │ └── TableCfg.xml │ ├── Version.ini │ ├── bin │ │ ├── apse │ │ ├── center │ │ ├── com.myam.plist │ │ ├── goed │ │ ├── libCrashError.dylib │ │ ├── libDB.dylib │ │ ├── libFileSystem.dylib │ │ ├── libFmpExportDll.dylib │ │ ├── libIMKeyTool.dylib │ │ ├── libLogManager.dylib │ │ ├── libNTQQRobber.dylib │ │ ├── libPluginSDK.dylib │ │ ├── libQQRobber.dylib │ │ ├── libUtility.dylib │ │ ├── libWXRobber.dylib │ │ ├── libXml.dylib │ │ ├── libfun.dylib │ │ ├── liblz4.dylib │ │ ├── libuchardet.dylib │ │ ├── libwxworks.dylib │ │ ├── lipo │ │ ├── plugins │ │ │ ├── zero_1.0.gz │ │ │ ├── [...truncated] │ │ │ └── zero_6.0.gz │ │ ├── update_config │ │ └── wsus │ └── bin_arm │ ├── [... Same as /bin] │ └── wsus └── Scripts └── postinstall
I think that now we have extracted all that we need to start our analysis...
Bom: the forgotten part of this post?
This post was mainly focused on the extraction of the payload and scripts, which is why I did not detail much about it.
According to the flat package format documentation, this is a "Bill of materials".
file Bom Bom: Mac OS X bill of materials (BOM) file
Let's see which commands could be related to bom
:
man -k bom
Which outputs:
bom(5) - bill of materials lsbom(8) - list contents of a bom file mkbom(8) - create a bill-of-materials file PPI::Token::BOM(3pm) - Tokens representing Unicode byte order marks
First, I am curious to understand what it means. Let's take a look at the bom
man page:
The Mac OS X Installer uses a file system "bill of materials" to determine which files to install, remove, or upgrade.
Not sure it would help much, but you can still inspect the content of it with the lsbom
command.
Before we come to a conclusion, let's explore a nice tool.
Suspicious Package: the easy way
It was really interesting to go through these concepts and new commands. It allowed me to better understand how a payload could be hidden in a package installer.
That being said, there is a simpler way to gather all this data without all the pain we had so far.
This is the time to introduce: "Suspicious Package".
You can install it through:
brew install --cask suspicious-package
Then you can open it from the location where your package is:
open /Applications/Suspicious\ Package.app/
Once open, you should see:
You can select the installer of your choice:
Here you'll see a summary of the installer, plus code signing information. There is also an "All files" tab:
From there you can also open nested installers:
And finally, an "All scripts" tab where you can inspect pre and post-installation scripts:
Conclusion
In this post, we've covered the essential techniques for extracting and analyzing malicious macOS package installers. Here's what we learned:
Key takeaways:
- Package installers can contain nested structures that hide malicious payloads
pkgutil
provides powerful commands for inspection and extraction- The
--expand-full
command is crucial for handling complex nested packages - Suspicious Package offers a user-friendly alternative for quick analysis
These techniques are fundamental for malware analysis on macOS. Whether you're analyzing PasivRober or any other malicious package installer, the same extraction process applies. The tools and commands we covered will help you understand what's inside these packages.
For deeper malware analysis, remember to check out the Kandji writeup on PasivRober and the macOS application packaging video for a deep dive into package installers.
References: