An Insecurity in Apple's Security Framework?
...or why it's important to initialize your pointers!
05/02/2018
love these blog posts? support my tools & writing on patreon :)
An Interesting Crash
Recently I released Do Not Disturb (DND), a new security utility designed to detect "evil maid" attacks.
One of the best ways to compromise a computer is with physical access. Many of us have likely left our laptops unattended (perhaps in a hotel room while traveling?). I figured it would be nice to know if somebody attempted to hack it...hence, created Do Not Disturb.
Interested in the inspiration for creating 'Do Not Disturb'? (it may or may not involve a Tinder date in Moscow, with a Russian 'spy')...
Read:
One of the neat features of DND is its ability to pair with an iOS device for remote alerting and tasking:
Created by Digita Security (an enterprise macOS security company I recently co-founded with friends) the companion app allows one to receive remote alerts when somebody tampers with your laptop and respond by:
- dismissing the alert
- taking a picture via the Mac's webcam
- fully shutting down the Mac
This iOS companion application is available in the official iOS App Store:
It order to utilize the remote alerting and tasking capabilities of the iOS application, one has to pair the iOS device with a Mac that is protected by DND. This is done by scanning a QR code, generated by the macOS DND app:
This QR code contains cryptographic keying information used to secure communications between the Mac and the phone. As part of this setup for end-to-end encryption, the code on the Mac invokes several Sec* Apple APIs. These APIs are designed to allow developers to perform actions related to cryptography such as dealing with certificate authorities, generating and storing keys etc. etc.
For an interesting read on this topic that illustrates how these API can be used, check out:
The DND code that interfaces with Apple's security APIs is contained in a Swift framework, created by Digita. In this code, Apple's security APIs such as SecIdentityCopyPrivateKey and SecKeyCopyPublicKey are invoked:
guard SecIdentityCopyPrivateKey(self, &privKey) == errSecSuccess
else {
return nil
}
return privKey
...
guard let key = privateKey,
let cert = certificate else {
return false
}
if let pubKey = SecKeyCopyPublicKey(key) {
....
}
Do Not Disturb was launched smoothly! However the other day, I noticed a few crash reports...
A Crash...But Why?
The DND Swift framework uses Sentry.io which is open-source error reporting framework that "helps developers monitor and fix crashes." When a crash occurs, Sentry generates a simple crash report and submits it.
Over the last week or two, I noticed a small number of reports (even on recent versions of macOS) that all showed a crash occurring at the same location...within Apple's Sec* APIs:
OS Version: macOS 10.13.4 (17E202)
Report Version: 104
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: SEGV_NOOP at 0x0000000000000001
Crashed Thread: 6
Application Specific Information:
Attempted to dereference garbage pointer 0x1.
Originated at or in a subcall of -[FrameworkInterface initIdentity:]
Thread 6 Crashed:
0 Security 0xfffe7192df0e SecError
1 Security 0xfffe7189b668 SecCDSAKeyCopyPublicKey(OpaqueSecKeyRef*)
2 Security 0xfffe71751f69 SecKeyCopyPublicKey
3 dnd 0x107464fc1 SecIdentity.deleteIdentity()
4 dnd 0x10743d87d DNDIdentity.deleteIdentity(deleteAssociatedCA:)
5 Do Not Disturb 0x2073e765c -[FrameworkInterface initIdentity:]
6 Do Not Disturb 0x2073ec727 -[UserComms qrcRequest:]
After a brief triage of the code within the Swift Framework revealed no (obvious) errors, I decided to dig into the Apple Sec* APIs as I suspected a bug in Apple's code!
These Sec* APIs are implemented within Apple's Security.framework: /System/Library/Frameworks/Security.framework/Versions/Current/Security.
I decided to start at location of the crash, which was located within the SecError function at the ASLR'd address 0xfffe7192df0e.
Disassembling the Security.framework, specifically the SecError function revealed the following instruction that was responsible for the crash: mov rdx, qword [r11]
_SecError:
000000000026feaf mov r11, rsi
000000000026ff06 test r11, r11
000000000026ff09 je leave
...
000000000026ff0e mov rdx, qword [r11]
From this disassembly we see the 2nd argument to the SecError function (RSI) is moved into the R11 register, tested to ensure it's not NULL, then dereferenced.
Recall the error report:
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: SEGV_NOOP at 0x0000000000000001
Attempted to dereference garbage pointer 0x1.
If a value of 0x1 (which is not NULL) is dereferenced, yes, this definitely will crash ... as 0x1 is not clearly a valid memory address!
So now we know the immediate cause of the crash - but the question becomes why? In other words, why is Apple's SecError function crashing?
Luckily Apple Security framework is open source! You can grab the latest version here. Having source code makes the analysis fairly straight forward.
Let's start by looking at the code for the SecError function (/OSX/utilities/src/SecCFError.c):
bool SecError(OSStatus status, CFErrorRef *error, CFStringRef format, ...)
{
if (status == 0) return true;
if (error) {
va_list args;
CFIndex code = status;
CFErrorRef previousError = *error;
*error = NULL;
va_start(args, format);
SecCFCreateErrorWithFormatAndArguments(code, kSecErrorDomain, previousError,
error, NULL, format, args);
va_end(args);
}
return false;
}
Note it's second argument, a variable named 'error' is a pointer to a CFErrorRef. It's checked to make sure it's non-NULL, and if so, dereferenced:
if (error) {
...
CFErrorRef previousError = *error;
}
Look familiar? Yes! This is source code that matches the buggy code we saw in disassembly - at the location of the faulting (crashing) instruction.
Clearly somebody is calling SecError with an invalid CFErrorRef pointer (e.g. a 0x1 instead of a valid memory address).
Recall in the stack backtrace in the crash report, the SecError function (with the invalid value for the CFErrorRef pointer) is invoked by the SecCDSAKeyCopyPublicKey function.
Here's the relevant code from the SecCDSAKeyCopyPublicKey function (/OSX/libsecurity_keychain/lib/SecKey.cpp):
static SecKeyRef SecCDSAKeyCopyPublicKey(SecKeyRef privateKey)
{
CFErrorRef *error;
BEGIN_SECKEYAPI(SecKeyRef, NULL)
//rest of function's code
END_SECKEYAPI
}
The BEGIN_SECKEYAPI and END_SECKEYAPI macros are defined in /OSX/libsecurity_keychain/lib/SecBridge.h:
#define BEGIN_SECKEYAPI(resultType, resultInit) \
resultType result = resultInit; try {
extern "C" bool SecError(OSStatus status, CFErrorRef *error, CFStringRef format, ...);
#define END_SECKEYAPI }\
catch (const MacOSError &err) { SecError(err.osStatus(), error, CFSTR("%s"), \
err.what()); result = NULL; } \
catch (const CommonError &err) { \
if (err.osStatus() != CSSMERR_CSP_INVALID_DIGEST_ALGORITHM) { \
OSStatus status = SecKeychainErrFromOSStatus(err.osStatus()); \
if (status == errSecInputLengthError) status = errSecParam; \
SecError(status, error, CFSTR("%s"), err.what()); result = NULL; } \
} \
catch (const std::bad_alloc &) { SecError(errSecAllocate, error, \
CFSTR("allocation failed")); result = NULL; } \
catch (...) { SecError(errSecInternalComponent, error, CFSTR("internal error")); \
result = NULL; } \
return result;
This may look a little confusing (it was to me) and messy, but basically what the macros do is just ensure all the code within the SecCDSAKeyCopyPublicKey function is wrapped in a try/catch.
Specially, the BEGIN_SECKEYAPI macro opens the try block: resultType result = resultInit; try {. This macro also extern "C"s the SecError function.
As noted on StackOverflow:
"extern "C" makes a function-name in C++ have 'C' linkage (compiler does not mangle the name) so that client C code can link to (i.e use) your function using a 'C' compatible header file that contains just the declaration of your function. Your function definition is contained in a binary format (that was compiled by your C++ compiler) that the client 'C' linker will then link to using the 'C' name."
The END_SECKEYAPI macro ends the SecCDSAKeyCopyPublicKey function by containing various catch blocks... note that these call the SecError function, with the 'error' pointer.
The crash report stack backtrace contains the ASLR'd address (0xfffe7189b668) of the instruction in SecCDSAKeyCopyPublicKey that immediately proceeds the actual instruction that was responsible for the call into the SecError API (that then directly led to the crash):
Thread 6 Crashed:
0 Security 0xfffe7192df0e SecError
1 Security 0xfffe7189b668 SecCDSAKeyCopyPublicKey(OpaqueSecKeyRef*)
Back in our disassembler, we can find this instruction. Note that immediately preceding it, is a call, as expected, to SecError:
00000000001dd637 call _SecKeychainErrFromOSStatus
00000000001dd63c cmp eax, 0xfffef774
00000000001dd641 mov r14d, 0xffffffce
00000000001dd647 cmovne r14d, eax
00000000001dd64b mov rax, qword [rbx]
00000000001dd64e mov rdi, rbx
00000000001dd651 call qword [rax+0x10]
00000000001dd654 mov rcx, rax
00000000001dd657 lea rdx, qword [cfstring__s]
00000000001dd65e xor eax, eax
00000000001dd660 mov edi, r14d
; this call to 'SecError' crashes!
00000000001dd663 call _SecError
00000000001dd668 jmp loc_1dd69d
In source code this corresponds in the chunk of code in the END_SECKEYAPI macro:
if (err.osStatus() != CSSMERR_CSP_INVALID_DIGEST_ALGORITHM) { \
OSStatus status = SecKeychainErrFromOSStatus(err.osStatus()); \
if (status == errSecInputLengthError) status = errSecParam; \
SecError(status, error, CFSTR("%s"), err.what()); result = NULL; } \
}
Recall that the crash occurs within the SecError function, since it is invoked within an invalid CFErrorRef pointer. Looking at the code, it should be (somewhat) clear what the issue is....do you see it? If not, ask yourself:
"What is the value of the CFErrorRef *error"?
...ok that was kind of a trick question, as the answer is: who knows!?!
Looking again at the code within the SecCDSAKeyCopyPublicKey function, we can see the pointer is declared, but never initialized:
static SecKeyRef SecCDSAKeyCopyPublicKey(SecKeyRef privateKey)
{
CFErrorRef *error;
BEGIN_SECKEYAPI(SecKeyRef, NULL)
...
....this means its value will be whatever happens to be on the stack at the time of the call to SecError. For example, it could be 0x0, 0x1, or anything else! Opps!
To summarize, when any error occurs within the SecCDSAKeyCopyPublicKey function, a catch block will be invoked, which will in turn call the SecError function with the uninitialized CFErrorRef pointer. When SecError deferences this uninitialized pointer an unhandled EXC_BAD_ACCESS/SIGSEGV exception will be raised by the CPU, and the app will crash:
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: SEGV_NOOP at 0x0000000000000001
Crashed Thread: 6
Application Specific Information:
Attempted to dereference garbage pointer 0x1.
....thanks Apple!
Let's take a closer look at this in a 'live' debugging session.
To simplify the process, I extracted the SecCDSAKeyCopyPublicKey into a new Xcode project:
...I also added a manual throw to trigger the catch block(s) in the END_SECKEYAPI macro (which in turn invoke SecError with the unitialized pointer).
Compiling this code, I set a breakpoint on the SecError function then ran the code:
(lldb) b SecError
Breakpoint 1: where = Security`SecError, address = 0x00007fff5a297ea0
Process 2944 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
Security`SecError
(lldb) x/i $pc
Security`SecError:
-> 0x7fff5a297ea0 <+0>: pushq %rbp
Once it broke on the SecError function, we can use the 'register read' command to examine the arguments passed to the function:
We are interested in 2nd argument which is passed in via the RSI register. This is supposed to be the address of a CFErrorRef ... instead its uninitialized, and just happens to be a 0x1.
Single-stepping thru the instructions of SecError, we come to the faulting instruction: 0x7fff5a297f0e movq (%r11), %rdx. The CFErrorRef * has been moved into the R11 register, where it's about to be dereferenced:
(lldb) reg read $r11
r11 = 0x0000000000000001
Of course as 0x1 is not a valid memory address...so b00m, we crash:
(lldb) x/i $pc
movq (%r11), %rdx
(lldb)ni
...
Thread 1: EXC_BAD_ACCESS (code=1, address=0x1)
Conclusion
Often when crashes occur in your app, it's totally your fault...unless you're writing code on macOS - then it may be Apple's!
I'm not kidding, this happens often(ish). For example, I previously blogged about my user-mode ransomware detection tool triggering a kernel panic!
Read:
This bug is particularly amusing to me as it's within Apple's Security.framework...so you'd think such code would be well audited ("secure"). But no!
I mean, if you just look at the source within Apple's own IDE, the IDE explicitly identifies the bug - and even suggests a fix:
...really, bug hunting doesn't really get any easier than this!
The fix of course is simple: just set the CFErrorRef * to NULL! (Recall SecError cleanly handles the case for when the pointer has been initialized to NULL).
Interestingly Apple correctly initializes the CFErrorRef * to NULL is various other Sec* functions:
static size_t
SecCDSAKeyGetBlockSize(SecKeyRef key) {
CFErrorRef *error = NULL;
BEGIN_SECKEYAPI(size_t,0)
static CFIndex
SecCDSAKeyGetAlgorithmId(SecKeyRef key) {
CFErrorRef *error = NULL;
BEGIN_SECKEYAPI(CFIndex, 0)
Unsurprisingly in other places though, Apple does not initialize the pointer - meaning those functions are equally susceptible to the bug:
static Boolean
SecCDSAKeyIsEqual(SecKeyRef key1, SecKeyRef key2) {
CFErrorRef *error;
BEGIN_SECKEYAPI(Boolean, false)
Finally, you're probably wondering if this bug is exploitable. That is to say, does it pose a security risk to Mac users?
In short, IMHO, I think unlikely. However, uninitialized variables (especially pointers) have been exploited to before...so, who knows?? Basically if an attacker can 'pollute' the stack, say with a value that points to memory they control, the uninitialized pointer may become 'initialized' with this value. An attacker controlled-pointer is never a good thing!
Interested in research on the exploitation of uninitialized variables?
Read:
Well that's a wrap! Hopefully Apple gets around to patching this bug. Even if it doesn't pose a security risk, clearly code within in their 'Security' framework should be more robust!
...and remember folks, always initialize your pointers and head compiler warnings!
love these blog posts & tools? you can support them via patreon! Mahalo :)