macOS Security Framework and previous CVEs

Yuebin Sun(@yuebinsun2020) of Tencent Security Xuanwu Lab

Summary

COVID-19 outbreak keep me from going out,I have been researching macOS’s Security framework in the past two weeks of homeworking.

In this blog, I will try to analyze Security framework, especially Keychain, and previous vulnerabilities of the Secuirty framework。

Security Framework

Security framework is responsible for providing authentication and authorization, secure data storage and transportation, code signing, encryption/decryption services. Apps can use this services by using API of Security framework directly without knowing or caring about its implementation details.

But what are the components which composes the Security framework, and how the components collaborate with each other?

Unfortunately the official document site did not updating the architecture diagram of this framework from macOS 10.7, but I find a outdated diagram in the book 《Mac OS X Internals》, It can still be used as a reference, Not too much has changed from that time.

Keychain

Keychain play a significant role in the Security framework, passwords of WiFi and passwords of Safari autofilling are all saved and managed by Keychain.

Keychain was first introduced in Mac OS X 8.6, and was used for storing login credentials of the mail servers for PowerTalk mail system. Today, it improve a lot, many new data types are supported including many kinds of passwords, encryption keys, certificates and private notes. PowerTalk is no longer exists, but Keychain’s clients is replaced by many builtin Apps and thirdparty Apps.

Keychains in iOS and macOS are slightly different.

In iOS, there is only a single Keychain, it can be accessed when the device is unlocked, otherwise it will be locked too.

In macOS, users are allowed to create any number of Keychains for private use, Security framework provide SecKeychain{Create, Delete, Open, …} APIs for macOS users, with this API users can create, delete, open Keychain.

By default, two Keychains are already exist in macOS:

  • ~/Library/Keychains/login.keychain-db
  • /Library/Keychains/System.keychain

The login Keychain will be unlocked when user login macOS. System keychain, in contrast, is locked and encrypted, its decryption key is stored in /var/db/SystemKey, only root process can access it.

Apple offers Keychain Access.app to common users to view/search/create Keychains and Keychain Items, obtaining sensitive data, such as passwords, will trigger a authentication dialog.

How Keychain stores a new password

Example code of Apple Documentation as below shows how to store a new website password to Keychain.

1
2
3
4
5
6
7
8
static let server = "www.example.com"
let account = credentials.username
let password = credentials.password.data(using: String.Encoding.utf8)!
var query: [String: Any] = [kSecClass as String: kSecClassInternetPassword,
kSecAttrAccount as String: account,
kSecAttrServer as String: server,
kSecValueData as String: password]
let status = SecItemAdd(query as CFDictionary, nil)

The most important is SecItemAdd API, we will analyze this API step by step to see how it implements.

In the abstract, data stored in query param will deliver to Keychain Service over SecItemAdd API, service will package the data as an Keychain Item, password in query will be encrypted, and the Keychain Item will continue to be written to Keychain database at disk.

From a components perspective, SecItemAdd API is implemented by Security shared library(/System/Library/Frameworks/Security.framework/Versions/A/Security), Security shared library will be loaded into the current App process. After SecItemAdd of Security shared library are called, the query data will be forwarded to XPC Service(com.apple.securityd.xpc) over SECURITYD_XPC macro, this XPC service are hosted by secd process(/usr/libexec/secd), secd run as current user.

When data is send to secd process, according to operation, securityd_xpc_dictionary_handler(simplified for better reading) dispatch message to different internal functions. In our case, query param is passed on to _SecItemAdd directly, and there is also another important param, client of SecurityClient struct, SecurityClient is responsible for identifying client process, subsequently ACL checking is based on this struct. In addition, accessGroups field of SecurityClient is used for implementing the Shared Web Credentials(share credentials between iOS apps and their website counterparts). Apps and Web use Associated Domains Entitlement to create association, more details can read Supporting Associated Domains in Your App

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void securityd_xpc_dictionary_handler(const xpc_connection_t connection, xpc_object_t event) {
SecurityClient client = {
.task = NULL,
.accessGroups = NULL,
.musr = NULL,
.uid = xpc_connection_get_euid(connection),
.allowSystemKeychain = false,
.allowSyncBubbleKeychain = false,
.isNetworkExtension = false,
.canAccessNetworkExtensionAccessGroups = false,
};
fill_security_client(&client, xpc_connection_get_euid(connection), auditToken));
switch (operation)
{
case sec_item_add_id:
{
_SecItemAdd(query, &client, &result, &error) && result);
break;
}
// ...
}

In _SecItemAdd, query data will be translated to Sqlite3 sql statement, and in the end data will be inserted into sqlite3 database. What needs to be pointed out is that password will be encryptd before insert into database, and other non-secret fields will keep plain, this plain fields provide Keychain item searching support.

So far, inserting new website password to Keychain based on SecItemAdd API finished. The newly inserted Keychain item is save to Login Keychain, Login Keychain will be locked when user logout or machine poweroff.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static CFStringRef SecDbItemCopyInsertSQL(SecDbItemRef item, bool(^use_attr)(const SecDbAttr *attr)) {
CFMutableStringRef sql = CFStringCreateMutable(CFGetAllocator(item), 0);
CFStringAppend(sql, CFSTR("INSERT INTO "));
CFStringAppend(sql, item->class->name);
CFStringAppend(sql, CFSTR("("));
bool needComma = false;
CFIndex used_attr = 0;
SecDbForEachAttr(item->class, attr) {
if (use_attr(attr)) {
++used_attr;
SecDbAppendElement(sql, attr->name, &needComma);
}
}
CFStringAppend(sql, CFSTR(")VALUES(?"));
while (used_attr-- > 1) {
CFStringAppend(sql, CFSTR(",?"));
}
CFStringAppend(sql, CFSTR(")"));
return sql;
}

SecurityServer 与 SecurityAgent

Login Keychain is unlocked when user unlock the device, so wo do not see Keychain decryption or unlocking in the previous inserting keychain item.

However System Keychain or private Keychain(user created) will need Keychain encryption/decryption, locking/unlocing, Security Server is responsible for that.

Security Server(/usr/sbin/securityd) is a daemon service process which run as root, as the architecture diagram shows, Security Server offsers CSP/DL plugin for CDSA, namely data encryption and storage.

Security Server host service based on ucsp MIG interface, clients can access the internal Server object through mig interface. Fortunately any process can use this ucsp MIG interface.

By reading the source code, I see many features provided by Security Server:

  • Managing Security Server’s clients(Session, Connection)
  • Authentication and authrization
  • Managing Keychain databases
  • Code signature generating/verifying
  • Data encryption and decryption(ucsp_server_encrypt, ucsp_server_decrypt)
  • Key, key pair generating(ucsp_server_generateKey, ucsp_server_generateKeyPair, ucsp_server_wrapKey, ucsp_server_unwrapKey)
  • Code Signing Hosting(disappeared in 10.15, not yet in-depth analysis)

It is obvious that Security Server(securityd) has many highly privileged operations, and it manages a lot of sensitive data, run as root, so if we can find a vulnerability in this service process, it will have a big impact.

KeySteal is such a vulnerability occurred in securityd, when successfully exploited, any process can access passwords hold by Keychain.

But how can we interact with Security Server(securityd) based on MIG interface?

The ucsp MIG definition file exists in the source code of Security framework(OSX/libsecurityd/mig/ucsp.defs). Unfortunately only a few documentations about MIG can be found, and none of them can be found about interacting with Security Server certainly. In the end, I extract the code snippet which meet our needs from Linus Henze’s KeySteal Exploit, based on the snippet, I create a simple ucsp client accessing Security Server’s ucsp_server_setup interface.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#define UCSP_ARGS    gServerPort, gReplyPort, &securitydCreds, &rcode
#define ATTRDATA(attr) (void *)(attr), (attr) ? strlen((attr)) : 0

#define CALL(func) \
security_token_t securitydCreds; \
CSSM_RETURN rcode; \
if (KERN_SUCCESS != func) \
return errSecCSInternalError; \
if (securitydCreds.val[0] != 0) \
return CSSM_ERRCODE_VERIFICATION_FAILURE; \
return rcode

#define SSPROTOVERSION 20000

mach_port_t gServerPort;
mach_port_t gReplyPort;

CSSM_RETURN securityd_setup() {
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &gReplyPort);
mach_port_insert_right(mach_task_self(), gReplyPort, gReplyPort, MACH_MSG_TYPE_MAKE_SEND);
bootstrap_look_up(bootstrap_port, (char*)"com.apple.SecurityServer", &gServerPort);
ClientSetupInfo info = { 0x1234, SSPROTOVERSION };
CALL(ucsp_client_setup(UCSP_ARGS, mach_task_self(), info, "?:unspecified"));
}

int main(int argc, char *argv[])
{
mach_port_t port;
mach_port_t bootstrap_port;
task_get_bootstrap_port(mach_task_self(), &bootstrap_port);
kern_return_t kr = bootstrap_look_up(bootstrap_port, "com.apple.SecurityServer", &port);
securityd_setup();
return 0;
}

SecurityAgent

As mentioned above, Security Server is also responsible for managing authentication and authroization.

When client request Security Server to launch authentication/authroization verifying, if Security Server need to interact with the user (enter password) to verify identity, Security Server daemon will talk to Security Agent through XPC communication. The Security Agent, run as current user, pop up the interactive dialog to user, and then passwords typed by user will send back to Security Server, Security Server processing the actual verification. Throughout all the verification process, client DO NOT touch any sensitive information(such as passwords), all it can get is the verification result, true or false, yes or not. This mechanism can avoid the leakage of sensitive information and at the same time it will be transparent to the client when system add new authentication extension.

Vulnerabilities History

After we have learned some of the necessary Security framework architecture content above, let’s move on to the Security framework vulnerabilities which occured in macOS 10.14.*, and try to understand how the vulnerability works and what components the vulnerability is in.

What needs to be declared in advance is that Apple do not provide any details about the patched vulnerabilities except short title. The following are my own analysis based on the source code diff, so it also means that the results may not be entirely correct. If you find any mistakes or omissions, please don’t hesitate to point them out.

CVE-2019-8604 (fixed in 10.14.5)

By comparing the two versions of the source code, vulerability patch of CVE-2019-8604 can be found.

This vulnerablity exists in Security Server Daemon(securityd), when Security Server deal with Keychain database name, ucsp_server_setDbName of mig interface can set any length of dbname to Security Server object, ucsp_server_getDbName of interface will copy the given dbname to name parameter, the name parameter is designed to length PATH_MAX. So, if we pass a overlong dbname, ucsp_server_getDbName will trigger OOB write when memcpy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
--- a/Security-58286.251.4/securityd/src/transition.cpp
+++ b/Security-58286.260.20/securityd/src/transition.cpp

+static void checkPathLength(char const *str) {
+ if (strlen(str) >= PATH_MAX) {
+ secerror("SecServer: path too long");
+ CssmError::throwMe(CSSMERR_CSSM_MEMORY_ERROR);
+ }
+}
+

@@ -306,15 +313,16 @@ kern_return_t ucsp_server_getDbName(UCSP_ARGS, DbHandle db, char name[PATH_MAX])
{
BEGIN_IPC(getDbName)
string result = Server::database(db)->dbName();
- assert(result.length() < PATH_MAX);
+ checkPathLength(result.c_str());
memcpy(name, result.c_str(), result.length() + 1);

END_IPC(DL)
}

kern_return_t ucsp_server_setDbName(UCSP_ARGS, DbHandle db, const char *name)
{
BEGIN_IPC(setDbName)
+ checkPathLength(name);
Server::database(db)->dbName(name);
END_IPC(DL)
}

It is quite natural that the patch add a checking to length of dbname, to avode overlong dbname triggering oob memory copying. One more point to explain, both std::string and strlen can and only can be truncated by “\0”, so the way of setDbName and getDbName treating dbname is consistent.

CVE-2019-8520 (fixed in 10.14.4)

By comparing the two versions of the source code, vulerability patch of CVE-2019-8520 can be found.

This vulnerablity exists also in Security Server Daemon(securityd), when Security Server deal with authroization or authentication, if it need to interact with the user (enter password) to verify identity, it will ask for Security Agent’s help, Security Agent finally pop up a diaglog to user.

The communication between Security Server and Security Agent is based on XPC, when receiving response from Security Agent, in xpcArrayToAuthItemSet, the data(AUTH_XPC_ITEM_VALUE) and the length(AUTH_XPC_ITEM_SENSITIVE_VALUE_LENGTH) are passed by two separate fields. If providing a small data and a big length, sensitiveLength bytes of data will be copied from data to dataCopy, if sensitiveLength exceeds the actual size of data. OOB read will occurs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
--- a/Security-58286.240.4/securityd/src/agentquery.cpp
+++ b/Security-58286.251.4/securityd/src/agentquery.cpp

static void xpcArrayToAuthItemSet(AuthItemSet *setToBuild, xpc_object_t input) {
setToBuild->clear();

xpc_array_apply(input, ^bool(size_t index, xpc_object_t item) {
const char *name = xpc_dictionary_get_string(item, AUTH_XPC_ITEM_NAME);

size_t length;
const void *data = xpc_dictionary_get_data(item, AUTH_XPC_ITEM_VALUE, &length);
void *dataCopy = 0;

// <rdar://problem/13033889> authd is holding on to multiple copies of my password in the clear
bool sensitive = xpc_dictionary_get_value(item, AUTH_XPC_ITEM_SENSITIVE_VALUE_LENGTH);
if (sensitive) {
size_t sensitiveLength = (size_t)xpc_dictionary_get_uint64(item, AUTH_XPC_ITEM_SENSITIVE_VALUE_LENGTH);
+ if (sensitiveLength > length) {
+ secnotice("SecurityAgentXPCQuery", "Sensitive data len %zu is not valid", sensitiveLength);
+ return true;
+ }
dataCopy = malloc(sensitiveLength);
memcpy(dataCopy, data, sensitiveLength);
memset_s((void *)data, length, 0, sensitiveLength); // clear the sensitive data, memset_s is never optimized away
length = sensitiveLength;
} else {
dataCopy = malloc(length);
memcpy(dataCopy, data, length);
}

uint64_t flags = xpc_dictionary_get_uint64(item, AUTH_XPC_ITEM_FLAGS);
AuthItemRef nextItem(name, AuthValueOverlay((uint32_t)length, dataCopy), (uint32_t)flags);
setToBuild->insert(nextItem);
memset(dataCopy, 0, length); // The authorization items contain things like passwords, so wiping clean is important.
free(dataCopy);
return true;
});
}

The patch add a checking, to ensure that sensitiveLength is not greater than actual size of data.

CVE-2019-8526 (fixed in 10.14.4)

By comparing the two versions of the source code, vulerability patch of CVE-2019-8526 can be found.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

--- a/Security-58286.240.4/securityd/src/child.cpp
+++ b/Security-58286.251.4/securityd/src/child.cpp
@@ -57,7 +57,7 @@ ServerChild::ServerChild()
//
ServerChild::~ServerChild()
{
- mServicePort.destroy();
+ mServicePort.deallocate();


--- a/Security-58286.240.4/securityd/src/clientid.cpp
+++ b/Security-58286.251.4/securityd/src/clientid.cpp
@@ -45,14 +45,18 @@ ClientIdentification::ClientIdentification()
// Initialize the ClientIdentification.
// This creates a process-level code object for the client.
//
-void ClientIdentification::setup(pid_t pid)
+void ClientIdentification::setup(Security::CommonCriteria::AuditToken const &audit)
{
StLock<Mutex> _(mLock);
StLock<Mutex> __(mValidityCheckLock);
- OSStatus rc = SecCodeCreateWithPID(pid, kSecCSDefaultFlags, &mClientProcess.aref());
- if (rc)
- secinfo("clientid", "could not get code for process %d: OSStatus=%d",
- pid, int32_t(rc));
+
+ audit_token_t const token = audit.auditToken();
+ OSStatus rc = SecCodeCreateWithAuditToken(&token, kSecCSDefaultFlags, &mClientProcess.aref());
+
+ if (rc) {
+ secerror("could not get code for process %d: OSStatus=%d",
+ audit.pid(), int32_t(rc));
+ }
mGuests.erase(mGuests.begin(), mGuests.end());
}


--- a/Security-58286.240.4/securityd/src/csproxy.cpp
+++ b/Security-58286.251.4/securityd/src/csproxy.cpp
@@ -64,13 +64,12 @@ void CodeSigningHost::reset()
case noHosting:
break; // nothing to do
case dynamicHosting:
- mHostingPort.destroy();
- mHostingPort = MACH_PORT_NULL;
+ mHostingPort.deallocate();
secnotice("SecServer", "%d host unregister", mHostingPort.port());
break;
case proxyHosting:
Server::active().remove(*this); // unhook service handler
- mHostingPort.destroy(); // destroy receive right
+ mHostingPort.modRefs(MACH_PORT_RIGHT_RECEIVE, -1);
mHostingState = noHosting;
mHostingPort = MACH_PORT_NULL;
mGuests.erase(mGuests.begin(), mGuests.end());


--- a/Security-58286.240.4/securityd/src/process.cpp
+++ b/Security-58286.251.4/securityd/src/process.cpp
@@ -40,7 +40,7 @@
// Construct a Process object.
//
Process::Process(TaskPort taskPort, const ClientSetupInfo *info, const CommonCriteria::AuditToken &audit)
- : mTaskPort(taskPort), mByteFlipped(false), mPid(audit.pid()), mUid(audit.euid()), mGid(audit.egid())
+ : mTaskPort(taskPort), mByteFlipped(false), mPid(audit.pid()), mUid(audit.euid()), mGid(audit.egid()), mAudit(audit)
{
StLock<Mutex> _(*this);

@@ -48,6 +48,11 @@ Process::Process(TaskPort taskPort, const ClientSetupInfo *info, const CommonCri
parent(Session::find(audit.sessionId(), true));

// let's take a look at our wannabe client...
+
+ // Not enough to make sure we will get the right process, as
+ // pids get recycled. But we will later create the actual SecCode using
+ // the audit token, which is unique to the one instance of the process,
+ // so this just catches a pid mismatch early.
if (mTaskPort.pid() != mPid) {
secnotice("SecServer", "Task/pid setup mismatch pid=%d task=%d(%d)",
mPid, mTaskPort.port(), mTaskPort.pid());
@@ -55,7 +60,14 @@ Process::Process(TaskPort taskPort, const ClientSetupInfo *info, const CommonCri
}

setup(info);
- ClientIdentification::setup(this->pid());
+ ClientIdentification::setup(this->audit_token());

This is the KeySteal vulnerability that I have read paper before, vulnerablity exists in Security Server Daemon(securityd), Security Server provides a feature named Hosting Guest Code. Two problems can be seen from the vulnerability patch:

  • The first one exists in implementing Hosting Guest Code. When Security Server creeate SecCode object, it use SecCodeCreateWithPID API, this API identifies the client process by pid, so as the comment code in the patch says, there is a problem with PID Reuse. The way fix this problem is replacing SecCodeCreateWithPID with SecCodeCreateWithAuditToken, SecCodeCreateWithAuditToken API identifies the client process by audit token. Reasons and attack methods for pid reuse has already been fully explained in Samuel Groß’s 《Don’t Trust the PID!》

  • The second is the reference counting problem of mach port. CodeSigningHost::reset() calls destory() to forcibly release mach port. But the destroyed mach port may still be referenced by some data structures, and the mach port in the user-mode process itself is a Mach Port Name, it’s just a number. Since it’s number, there’s a possibility of reuse, so, UAF can be implemented if we can reoccupy it before the next use. It’s easy to fix the problem, replace destory() with a reference-counted version of deallocate().

CVE-2018-4400 (fixed in 10.14.1)

The vulnerability is described in Apple bulletin that denies service when processing S/MIME messages. Through code comparison, I got a suspected patch, and I am not sure.

1
2
3
4
5
6
7
8
9
10
11
--- a/Security-58286.200.222/OSX/libsecurity_smime/lib/smimeutil.c
+++ b/Security-58286.220.15/OSX/libsecurity_smime/lib/smimeutil.c
@@ -733,6 +733,8 @@ SecSMIMEGetCertFromEncryptionKeyPreference(SecKeychainRef keychainOrArray, CSSM_
cert = CERT_FindCertByIssuerAndSN(keychainOrArray, rawCerts, NULL, tmppoolp, ekp.id.issuerAndSN);
break;
case NSSSMIMEEncryptionKeyPref_RKeyID:
+ cert = CERT_FindCertBySubjectKeyID(keychainOrArray, rawCerts, NULL, &ekp.id.recipientKeyID->subjectKeyIdentifier);
+ break;
case NSSSMIMEEncryptionKeyPref_SubjectKeyID:
cert = CERT_FindCertBySubjectKeyID(keychainOrArray, rawCerts, NULL, ekp.id.subjectKeyID);
break;

I am not very familiar with certificate management and related data structures for the time being, so I will not analyze further.

The above are some of the identified vulnerabilities and patches that I have found so far. Because Apple open source code lags behind version updates, this vulnerabilities exists in 10.14.*.

Conclusion

This blog is what I researched and did during this time about Security Framework. The source code of Security Framework is huge and I only focused on Keychain and Security Server components that have been found to have more vulnerabilities in history。Other components like Auth will be analyzed when I have time, and, of course, I will continue to share with community through this site.

If you find something wrong above, or you are also interested in macOS, welcome to ping me @yuebinsun2020.

References

[1] Documentation of Security Framework

https://developer.apple.com/documentation/security?language=objc

[2] Apple Open Source Code

https://opensource.apple.com/

[3] KeySteal Vulnerability

https://www.pinauten.de/resources/KeySteal_OBTS_2019.pdf

[4] Keychain Wikipedia

https://en.wikipedia.org/wiki/Keychain_(software)