In our spare time, we hunt for bugs in various pieces of software. Thus, when winding down from a project recently, we decided it might be fun to audit one of our own laptops to see if we can locate a local privilege escalation (LPE) vulnerability in the software we use every day. This blog post describes the process for:
- scanning a MacBook Pro to find a target,
- conducting dynamic analysis with dtruss,
- analyzing the binary to determine the root cause, and
- bypassing the binary’s restrictions to obtain root privileges.
The write-up and the code for the vulnerability described in this blog post can be found on our NotQuite0DayFriday repository here: https://github.com/grimm-co/NotQuite0DayFriday/tree/master/2020.03.17-vmware-fusion
Finding Targets
LPE vulnerabilities come in many shapes and sizes, such as kernel vulnerabilities, vulnerabilities in privileged services, and vulnerabilities in SUID binaries. As described in our previous blog post, kernel exploits have additional complexities to ensure they don’t accidentally crash the computer being exploited. On the other hand, SUID binaries are a much more compelling target as they run as root regardless of the user that executes the binary and vulnerabilities in SUID binaries will rarely crash the computer. Additionally, it is not uncommon for third-party vendors to implement SUID binary helper programs without taking the necessary security precautions. SUID binaries must protect themselves from local attackers, and thus must take into account a substantially different threat model than most software. When choosing a target for the challenge, we started by searching the laptop for SUID binaries with a simple find command:
Figure 1. Third-party SUID Applications on the author’s MacBook Pro
We chose to focus on SUID binaries within the /Applications directory to ensure we would only find SUID binaries in third-party applications. The SUID binaries included with the base system are generally of a higher quality, and thus less likely to have many shallow bugs. As such, we proceeded with analyzing the found VMware Fusion and VirtualBox SUID binaries.
A Nice Surprise
We decided to trace the executables with dtruss, a dtrace helper utility. Dtruss will show us the syscalls that a binary executes, similar to strace on Linux.
# Copy the SUID binaries to a single directory (and remove spaces)
bash-3.2$ find /Applications/ -perm +6000 -exec /bin/sh -c 'cp "{}" $(basename "{}"|sed "s/ /_/g")' \;
# Run each of the binaries in dtruss and record their syscalls
bash-3.2$ for i in *; do sudo dtruss -f ./$i 2>&1 | tee /tmp/trace_$i.txt; done
Next, we looked through each of the syscalls lists for indicators of the mistakes typically made within SUID binaries. Since SUID binaries are run as root, they must ensure that they only load trusted libraries and only run trusted executables. As such, we used grep to look for any execve syscalls and any open syscalls with the filename ending in dylib (the extension for a library on macOS):
Figure 2. The execve and open calls in the Third-party SUID Applications
Looking at the results, we noticed that the Open VMWare Fusion Services and Open VMware USB Arbitrator Service executables, which we’ve copied to a subfolder in a home directory, are trying to run non-existent executables. Also the vmware-authd, vmware-vmx, vmware-vmx-debug, and vmware-vmx-stats executables are trying to load a non-existent libcrypto.1.0.2.dylib library. From the list of executed syscalls, it appeared that the VMWare SUID binaries are having trouble locating the other VMWare libraries and executables.
A Journey Through IDA
In order to figure out what was happening in these binaries, we began analyzing them in IDA. The two Opener daemons follow a similar code path, so we will only describe the Open VMware USB Arbitrator Service daemon here. The Hex-Rays decompiler output shown below is the executable’s main function.
Figure 3. The main function of the Open VMware USB Arbitrator Service daemon
This function makes three main function calls. The Location_Init function looks up the tool name “usbarb-opener” in a list of tools (shown below). The number associated with the tool name is saved to the NumberOfUpCounts global variable which is later used in the LocationGetRoot function.
Figure 4. The list of tools in the Open VMware USB Arbitrator Service daemon
Similarly, the LocationGetRoot function looks up the “usbarbDaemonFile” string in a separate list to find the relative path to the VMware USB Arbitrary Service executable.
Figure 5. The tool path prefix list in the Open VMware USB Arbitrator Service daemon
Then the LocationGetRoot function, as shown below, calls the HostInfo_GetModulePath function. This function is a simple wrapper around _NSGetExecutablePath and returns the file path of the main executable. When the executable is in the normal VMware directory, this will return the string “/Applications/VMware Fusion.app/Contents/Library/services/Open VMware USB Arbitrator Service”. Next, it calls resolve_realpath, a wrapper around the realpath function, to resolve any symbolic links in the file path. The function then uses the while loop to repeatedly remove the last four slashes in the file path, where four is taken from the global variable NumberOfUpCounts, set in Location_Init previously. Thus, it is finding the third parent directory of the executable’s directory. In a typical execution, this while loop will produce the string “/Applications/VMware Fusion.app”. Lastly, the function returns the combined parent directory and relative path to produce the string, “/Applications/VMware Fusion.app/Contents/Library/services/VMware USB Arbitrator Service”.
Figure 6. The directory traversal bug in the Open VMware USB Arbitrator Service daemon
The final call in the main function is to serviceImpl, which uses the returned path to start the USB service without performing any other verification on the path.
Fooling _NSGetExecutablePath and realpath
As we saw in the previous section, the two Opener daemons will try to run a program relative to the Opener daemon’s path. Thus, if we can find a way to run the Opener daemon from another location, we can make the SUID daemon execute an arbitrary program. In the dtruss example, we copied the Opener daemons to the home directory. However, when a SUID binary is copied, it loses its SUID status. Additionally, unprivileged users lack the ability to move the executable. Furthermore, symbolic links will be resolved by the realpath call, and thus they will not trick the programs into using the symbolic link’s location. However, unlike Linux, macOS allows unprivileged users to create hard links to SUID executables. As hard links are not resolved by realpath, the Opener daemons will be fooled into using an incorrect directory.
GRIMM’s NotQuite0DayFriday repository on GitHub contains two exploits for this vulnerability, which create the expected directory layout, compile a payload, hard link to the SUID Openers, and then execute the hard link to run the payload as root. The example payload for this exploit starts a netcat listener as root on TCP port 3333.
Figure 7. Example output of the bug being used to obtain root privileges
Conclusion
SUID binaries provide an easy way for developers to allow unprivileged users to call their privileged application. However, without adequate security, they can provide the means for an attacker to escalate privileges and takeover a computer. As such, SUID binaries should be avoided whenever possible and heavily audited when required. This post illustrates how a slight deviation from the expected behavior resulted in a directory traversal vulnerability that an attacker could exploit to obtain root privileges.
Want to join us and exploit some binaries? We’re hiring. Need help auditing or exploiting your binaries? Feel free to contact us.