Two days ago El Capitan 10.11.3 was released together with security updates for Yosemite and Mavericks. The bulletin available here describes nine security issues, most of them related to kernel or IOKit drivers. The last security issue is about a memory corruption issue on syslog that could lead to arbitratry code execution with root privileges. I was quite curious about this bug mostly because it involved syslogd, a logging daemon.
This post is about reversing the vulnerability and finding how it could be exploited. Unfortunately for us Apple is very terse on its security updates – for example they say nothing about if it is exploitable on default OS X installations or requires particular conditions. As we will see later on, this bug is not exploitable on default OS X installations.
While Apple makes available the source code for many components used in OS X, most of the time there is a significant delay so we need to use binary diffing to find out the differences between the vulnerable and updated binary. The usual tool for this purpose is BinDiff but there is also a free alternative called Diaphora made by Joxean Koret. Both tools require IDA and on this post we are going to use Diaphora. For this purpose we will need a copy of the vulnerable and patched binaries. The easiest way is to copy the syslogd binary (found at /usr/sbin/syslogd) before the updates are installed (usually it’s a good idea to have virtual machines snapshots for each version) and then after (or just extract the new binary from the update packages – El Capitan, Yosemite, Mavericks). This post will focus on Yosemite binaries.
Diaphora essentially works by generating a database and then comparing its contents. Comparing the 10.11.2 and 10.11.3 syslogd binaries gets us the following warning from Diaphora:
This means that both binaries are very similar so we should expect minimal changes between the two. Only one change is detected and its output is below.
The change is quite subtle. The original code could be something like:
reallocf(pointer, value + 4);
And the patch something like:
reallocf(pointer, value * 4 + 4);
The syslogd source package for El Capitan 10.11.2 can be downloaded here. The easiest way to try to locate this function is to grep the code using the string “add_lockdown_session: realloc failed\n”, finding a single hit inside syslogd.tproj/dbserver.c. The source code for this function is:
This makes it easier to observe the vulnerability. The patch is made on the reallocf() allocation size while the vulnerability is triggered when the fd variable is written into the lockdown_session_fds array. The allocation size used in reallocf() is wrong since it’s allocating memory just for the number of lockdown sessions instead of enough memory for each session. The following image taken from Zimperium’s analysis is a perfect illustration of the overflow and heap corruption.
At the third connection the heap corruption is happening but from my tests more connections are required to make it crash (I get most of the time crashes in different areas than Zimperium but I was also testing against OS X).
The developer of this particular piece of code made a mistake, and the fix can be as simple as adding a set of parenthesis:
C language is powerful but unforgiving of these small mistakes.
At this point we know where the vulnerability is and how it was patched. The next question is how do we reach this function? The following is the partial call graph for add_lockdown_session():
Judging by the initial function names the vulnerable function could be reached either locally (unix socket?) or remotely/locally (via TCP socket). The security bulletin mentions an attack from a local user. Looking at /System/Library/LaunchDaemons/com.apple.syslogd.plist configuration we can only observe the syslog unix socket:
This means that the default configuration in OS X is not vulnerable, unless the user changes it. Unfortunately for us Apple doesn’t mention this in the bulletin, which is indeed interesting information for example to anyone running old systems that can’t be upgraded. Let’s dig a bit deeper and understand what do we need to do to activate this feature in OS X so we can try to reproduce the vulnerability. The remote_acceptmsg_tcp() function seems like a good candidate to trace back. Looking it up on source code we will find an interesting function:
This is the function that will activate the remote feature which allows us to reach the vulnerable code. The #ifdef means that we can check the binary to see if they were compiled or not into the final binary.
The disassembly output of remote_init() shows that only remote_init_tcp() was compiled, meaning that we can reach the vulnerable code via tcp sockets, either locally or remote depending on user configuration. The remote_init_tcp() function takes care of creating and binding the listener socket and is the one calling remote_acceptmsg_tcp() we saw in the first callgraph using Grand Central Dispatch.
We still don’t know how to activate the remote feature. Next step is to see who calls remote_init(). There are two calls but the most interesting is init_modules().
The remote module support will be compiled into syslogd binary if the target is not the iOS simulator and the default enable or disable status depends on the remote_enable local variable. Its default value is zero, meaning the remote feature is disabled by default. This is another strong clue about a default OS X not being vulnerable.
Finally init_modules() is called by main(), where we can find the final clues about how to activate this feature.
Inside main we can observe interesting things and finally be sure if OS X is vulnerable on default installation or not. The first thing we can observe in the above code snippet is that the remote feature is enabled by default on the embedded OS, usually meaning iOS and AppleTV. Next there is an option -config that also enables it if the iphone option is selected. Last is the undocumented -remote command line option, which can enable the remote feature on any Apple operating system.
To activate the feature we need to edit syslogd launchd configuration file found at /System/Library/LaunchdDaemons/com.apple.syslogd.plist (usually in binary format but can be converted using plutil -convert xml1 filename). The ProgramArguments and Sockets keys need to be modified to the following:
Because launchd controls the sockets we also need to configure the socket where syslogd will be listening for the remote option (#define ASL_REMOTE_PORT 203). After we modify the plist and reload syslogd we can finally connect to port 203.
The vulnerable code path is triggered using the watch command. If we attach a debugger and insert a breakpoint in the vulnerable add_lockdown_session(), the breakpoint will never be hit when we select the watch command. This is the code inside session that calls the vulnerable function:
The WATCH_LOCKDOWN_START is only set in one place inside session:
The SESSION_FLAGS_LOCKDOWN is a flag passed on the only session argument.
And we can finally observe and conclude why the security bulletin talks about a local user:
This means that the SESSION_FLAGS_LOCKDOWN flag is only set on local connections and never on remote tcp connections, the only feature we have enabled in OS X syslogd binary. The functions who call remote_acceptmsg() show it clearly.
The conclusion is that there is no code path to trigger this bug in OS X, even if the user configures the remote feature. To test the bug and observe it in action the only way is to attach to the syslogd binary (or patch it) and remove the above condition (we can also patch inside session but here is easier). Next we just need a small tcp client that sends a few connections to the port 203 and issues the watch command. Sooner or later the syslogd binary will finally crash.
The vulnerability also doesn’t seem easy to exploit because we don’t have much control over the fd variable that is overwriting the allocated array.
One final note is that the vulnerability was only patched in El Capitan but not included in Yosemite and Mavericks security updates. While we have just seen that even on El Capitan there is no code path to the vulnerable code it is weird that the older versions weren’t also patched. Apple security policy is still confusing most of the time.
So after a long post we can finally conclude that there is nothing really interesting about this vulnerability in OS X (and also iOS given the potential barriers to exploitation). It was just an interesting reverse engineering and source code analysis exercise to understand the vulnerability impact in OS X. This exercise wouldn’t be needed if Apple just published more relevant details on its security bulletin.
Thanks to pmsac for draft post review and exploitation discussion (also to qwertyoruiop and poupas).
The tool used to generate the call graph is Understand from http://www.scitools.com. It’s a great tool for browsing and auditing large projects.