Overview
These vulnerabilities were disclosed at Black Hat Europe 2022 in the talk Knockout Win Against TCC - 20+ NEW Ways to Bypass Your MacOS Privacy Mechanisms. The technique relied on an SQLite environment variable respected by libsqlite3.dylib
which made apps using the standard SQLite system API log all the SQL queries. As such queries may contain sensitive user data normally protected by the TCC - I started researching all the problematic occurrences.
Exploitation
The exploitation here is really simple. I figured out that this technique may be used against applications like: Contacts, Mail, Notes, or iMessage. All these apps run as your user so to set the SQLITE_AUTO_TRACE
environment variable - you can use the open -b BUNDLE_ID --env SQLITE_AUTO_TRACE=1
or use launchctl setenv SQLITE_AUTO_TRACE 1
to set this for all the apps.
Take a look at some examples:
Contacts
iMessage
Notes
Decoded version:
Fix
Now I’d like to share my recently discovered trick. After the fix, we usually decompile the binary file we expect to have the patch. And what do we see? Barely readable code like the below:
[...]
loc_18723febc:
r21 = 0x1;
r2 = 0x6;
if (strncasecmp("syslog", r19, 0x6) == 0x0) {
r21 = 0x1;
r2 = 0x6;
if (CPU_FLAGS & E) {
r21 = 0x1;
r2 = 0x6;
r21 = 0x1;
}
r2 = 0x6;
}
r2 = 0x6;
r20 = r21 << 0x1;
r0 = getenv("SQLITE_AUTO_TRACE");
r19 = r0;
if (r0 != 0x0) goto loc_18723ff38;
loc_18723fef0:
r2 = 0x6;
goto loc_18723ffe0;
loc_18723ffe0:
if ((r21 | 0x0) == 0x1) {
r0 = dyld_process_is_restricted();
if (r0 == 0x0) {
*(int128_t *)0x1d669bdc0 = r20;
*(int128_t *)0x1d669bdc4 = r19;
r29 = saved_fp;
r30 = stack[-8];
r20 = var_10;
r19 = stack[-24];
r22 = var_20;
r21 = stack[-40];
r31 = r31 + 0x30;
[...]
Of course, an experienced reverse engineer will immediately see that now the libsqlite3.dylib
before turning on the logging mechanism for SQLITE_AUTO_TRACE
env var checks if the process is recstricted using dyld_process_is_restricted()
. But… look what happens when you copy and paste the decompiled C code to ChatGPT 4o.
int enableAutoLogging() {
// Variable declarations
int autoProfileEnabled = 0;
int autoTraceEnabled = 0;
const char *profileEnv;
const char *traceEnv;
// Check the environment variable for auto profiling
profileEnv = getenv("SQLITE_AUTO_PROFILE");
if (profileEnv != NULL) {
if (strncasecmp("1", profileEnv, 1) == 0 || strncasecmp("stderr", profileEnv, 6) == 0 || strncasecmp("syslog", profileEnv, 6) == 0) {
autoProfileEnabled = 1;
}
}
// Check the environment variable for auto tracing
traceEnv = getenv("SQLITE_AUTO_TRACE");
if (traceEnv != NULL) {
if (strncasecmp("1", traceEnv, 1) == 0 || strncasecmp("stderr", traceEnv, 6) == 0 || strncasecmp("syslog", traceEnv, 6) == 0) {
autoTraceEnabled = 1;
}
}
// If auto logging is enabled, check if the process is restricted
if (autoProfileEnabled || autoTraceEnabled) {
if (dyld_process_is_restricted() == 0) {
// Enable auto logging
// Assume `enable_auto_logging` is a function that enables the logging
enable_auto_logging(autoProfileEnabled, autoTraceEnabled);
} else {
// Process is restricted, log a warning message
sqlite3_log(28, "Auto logging environment variable set, but process is restricted. Ignoring.");
}
}
return 0;
}
Much better, right?