• Objective See
  • about
  • blog
  • malware
  • products


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:
"This Ex-NSA Hacker's App Protects Your Mac From 'Evil Maid' Attacks".


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:

"How to Use Secure Enclave and Touch ID to Protect your Keys"


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:
"Two Bugs, One Func()"


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:
  • Exploitations of Uninitialized Uses on macOS Sierra
    (Zhenquan Xu, Gongshen Liu, Tielei Wang, Hao Xu)

  • "Attacks on uninitialized local variables"
    (Halvar Flake)

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 :)


  • © 2018 objective-see llc
  • ✉
  • 
  • 
  • donate!