System Integrity Protection: The misunderstood setting
For the number of years I’ve been in the macOS community, one fact has always stayed consistent: Developers and users don’t understand what System Integrity Protection really is. Thus in today’s blog post, I want to clear up some misconceptions about this setting in macOS and propose better ways for developers to manage this setting.
Terminology Used:
- macOS Kernel is known as XNU (X is not Unix).
- boot.efi is macOS’s boot loader on Intel systems, which loads the XNU kernel.
- NVRAM is a type of low-level storage on a motherboard separate from the operating system drive, primarily used to store global environment variables that are retained across reboots and drive swaps. Commonly used by Apple for boot.efi and XNU configuration.
Table of Contents:
What is System Integrity Protection
System Integrity Protection, generally abbreviated as SIP, is an OS-level setting in macOS that controls many security aspects. Introduced in OS X 10.11, El Capitan, the goal of this setting was to reduce the abuse seen with root level access, namely protected task tracking, arbitrary driver loading and protected filesystem edits. Instead, users are required to manually reboot into macOS’s recovery environment and disable SIP before performing sensitive tasks in the OS.
SIP sits on the kernel level, specifically handled by XNU’s Configurable Security Restriction stack (abbreviated as CSR). Configuration is read either via the csr-active-config
NVRAM variable on Intel-based systems, or via lp-sip0
entry in the Device Tree on Apple Silicon-based systems.
- Interesting notes:
- On Intel-based systems, XNU doesn’t read
csr-active-config
directly. Instead boot.efi detects the value, and passes it to the kernel.- Due to this, boot.efi will actually strip bit 0x10 (AppleInternal) on production level machines before passing it on.
- On Apple Silicon-based systems, XNU directly reads the SIP configuration from the booted Device Tree, allowing for per-volume SIP configuration.
- Primary benefit for this is allowing special development OS installs, while keeping important data on the SIP protected install
- Compare this to Intel, 1 SIP value is used for all macOS installs booted.
- On Intel-based systems, XNU doesn’t read
The raw SIP value is a UINT32
integer, which is treated as a bit field by the Operating System.
- But what is a bit field?
- A bit field is a single value that distinctly assigns meaning to bit ranges. In case of the SIP bit field, every one of the 32 bits has a distinct boolean meaning
How XNU uses this bit field is to determine what privileged actions can be used in the OS. The following is a breakdown of valid SIP options in macOS 13.0, found in csr.h:
- A very common misconception with SIP is that it is a simple on or off state, however SIP is actually a cumulation of settings used by the OS.
CSR_ALLOW_UNTRUSTED_KEXTS = 0x1
CSR_ALLOW_UNRESTRICTED_FS = 0x2
CSR_ALLOW_TASK_FOR_PID = 0x4
CSR_ALLOW_KERNEL_DEBUGGER = 0x8
CSR_ALLOW_APPLE_INTERNAL = 0x10
CSR_ALLOW_UNRESTRICTED_DTRACE = 0x20 // Formerly known as CSR_ALLOW_DESTRUCTIVE_DTRACE
CSR_ALLOW_UNRESTRICTED_NVRAM = 0x40
CSR_ALLOW_DEVICE_CONFIGURATION = 0x80
CSR_ALLOW_ANY_RECOVERY_OS = 0x100
CSR_ALLOW_UNAPPROVED_KEXTS = 0x200
CSR_ALLOW_EXECUTABLE_POLICY_OVERRIDE = 0x400
CSR_ALLOW_UNAUTHENTICATED_ROOT = 0x800
An example of this bit field can be seen in OpenCore Legacy Patcher’s GUI, which has a breakdown of SIP settings, and how to calculate the correct bit field. For example, 0x803
breaks down into 0x1
, 0x2
and 0x800
:
SIP Breakdown
Now let’s try to break down each setting and give a brief description:
CSR_ALLOW_UNTRUSTED_KEXTS
- Introduction: 10.11 - El Capitan
- Description: Allows unsigned kernel drivers to be installed and loaded
CSR_ALLOW_UNRESTRICTED_FS
- Introduction: 10.11 - El Capitan
- Description: Allows unrestricted file system access
CSR_ALLOW_TASK_FOR_PID
- Introduction: 10.11 - El Capitan
- Description: Allows tracking processes based off a provided process ID
CSR_ALLOW_KERNEL_DEBUGGER
- Introduction: 10.11 - El Capitan
- Description: Allows attaching low level kernel debugger to system
CSR_ALLOW_APPLE_INTERNAL
- Introduction: 10.11 - El Capitan
- Description: Allows Apple Internal feature set (primarily for Apple development devices)
CSR_ALLOW_UNRESTRICTED_DTRACE
- Introduction: 10.11 - El Capitan
- Description: Allows unrestricted dtrace usage
CSR_ALLOW_UNRESTRICTED_NVRAM
- Introduction: 10.11 - El Capitan
- Description: Allows unrestricted NVRAM write
CSR_ALLOW_DEVICE_CONFIGURATION
- Introduction: 10.11 - El Capitan
- Description: Allows custom device trees (primarily for iOS devices)
- Note: This is based off speculation, currently little public info on what uses this bit provides
CSR_ALLOW_ANY_RECOVERY_OS
- Introduction: 10.12 - Sierra
- Description: Skip BaseSystem Verification, primarily for custom recoveryOS images
CSR_ALLOW_UNAPPROVED_KEXTS
- Introduction: 10.13 - High Sierra
- Description: Allows unapproved kernel driver installation/loading
- Note: Current use-case unknown
CSR_ALLOW_EXECUTABLE_POLICY_OVERRIDE
- Introduction: 10.14 - Mojave
- Description: Allows override of executable policy
- Note: Current use-case unknown
CSR_ALLOW_UNAUTHENTICATED_ROOT
- Introduction: 11 - Big Sur
- Description: Allows custom APFS snapshots to be booted (primarily for modified root volumes)
Misuse of SIP
Now that we know what SIP is and how it works, we’ll now move onto how developers have been misusing this setting and how we can improve the landscape.
Boolean treatment with csrutil
The most common way developers query the SIP status in macOS is through the userspace tool /usr/bin/csrutil
. This is by far the worst way to determine SIP status, solely because it assumes SIP is a simple on/off system. csrutil
’s biggest flaw is that it has a very small range of what it accepts as “disabled”, which is defined in csr.h:
#define CSR_DISABLE_FLAGS (CSR_ALLOW_UNTRUSTED_KEXTS | \
CSR_ALLOW_UNRESTRICTED_FS | \
CSR_ALLOW_TASK_FOR_PID | \
CSR_ALLOW_KERNEL_DEBUGGER | \
CSR_ALLOW_APPLE_INTERNAL | \
CSR_ALLOW_UNRESTRICTED_DTRACE | \
CSR_ALLOW_UNRESTRICTED_NVRAM)
If a user simply wants to edit the root volume, for example with OpenCore Legacy Patcher, they’ll only want to toggle bits 0x1, 0x2 and 0x800. However with csrutil
’s logic, you’ve entered an “unknown” configuration:
If we look to some common projects checking SIP status, we’ll see the software will fail to recognize a system as “patchable” since there’s no System Integrity Protection status: disabled
string in the output.
Hard coding SIP values
Another common issue we’ll see is developers hard coding SIP’s value. For example, SIP would be considered “disabled” with the value of 0xFF
in El Capitan. However with Sierra, Apple added CSR_ALLOW_ANY_RECOVERY_OS
which shifted SIP to 0x1FF
. And this goes on and on as new OS updates add new SIP settings.
Where this becomes an issue is developers assuming SIP is only valid with 0xFF
for the bits they require. But if a user updated macOS and got a new SIP bit, suddenly the software’s hard coded assumption is now incorrect.
Reliance on NVRAM
The final major issue is developer’s reliance on NVRAM querying for SIP. While conceptually this is fine, practically this can end up in some noteworthy edge cases:
- Running
nvram -c
clears the hardware’s NVRAM variables, yet XNU still has an active SIP value. - boot.efi and XNU can and will strip SIP bits it deems “unsupported” (ex. 0x10 (AppleInternal) being stripped on production systems).
- NVRAM is unused for SIP on Apple Silicon systems (as mentioned earlier, stored in Device Tree under
lp-sip0
).
How to properly query SIP
Now that we’ve gone over what SIP is and how it’s been misused, we’ll now discuss how developers can improve this.
The main key take aways I want everyone to remember:
- System Integrity Protection is not a single setting, but instead a cumulation of multiple.
- Each setting has it’s own uses, avoid having unused bits required for your software checks.
- Check in with the kernel for SIP status, not generic variables and utilities.
With this in mind, let me introduce you to XNU’s exposed syscalls for SIP status:
int
csr_get_active_config(csr_config_t *config)
{
return __csrctl(CSR_SYSCALL_GET_ACTIVE_CONFIG, config, sizeof(csr_config_t));
}
This snippet of code gives us the ability to directly query what XNU is actively using as their SIP value.
Now how does one implement this cleanly?
Below I’ll provide 2 examples, one in Objective-C and another in Python through a simple module:
Objective-C implementation
With Objective-C, you’ll simply need to load the libSystem.dylib
library and query for the csr_get_active_config()
entry:
+ (int)getCurrentSip {
NSString *sip_path = @"/usr/lib/libSystem.dylib";
NSString *sip_function = @"csr_get_active_config";
// Get the function pointer
void *libSystem = dlopen(sip_path.UTF8String, RTLD_LAZY);
if (!libSystem) {
NSLog(@"ParseSip: Error loading libSystem.dylib");
return -1;
};
void *csr_get_active_config = dlsym(libSystem, sip_function.UTF8String);
if (!csr_get_active_config) {
NSLog(@"ParseSip: Error loading csr_get_active_config");
return -1;
};
// Call the function
int (*csr_get_active_config_ptr)(uint32_t *) = (int (*)(uint32_t *))csr_get_active_config;
uint32_t sip_int = 0;
int err = csr_get_active_config_ptr(&sip_int);
if (err) {
NSLog(@"ParseSip: Error calling csr_get_active_config");
return -1;
};
dlclose(libSystem);
NSLog(@"ParseSip: Current SIP value: %d", sip_int);
kDetectedSIPValue = [NSNumber numberWithInt:sip_int];
return sip_int;
}
Once you’ve gotten the active SIP value, you can next iterate over the bits and validate against what you require:
+ (BOOL)canRootPatch:(int)sip_int {
NSLog(@"ParseSip: Checking if SIP value can root patch: %d", sip_int);
NSNumber *kernel_major_version = [OSDetection getKernelMajorVersion];
if ([kernel_major_version intValue] == kMacOSBigSur || [kernel_major_version intValue] == kMacOSMonterey || [kernel_major_version intValue] == kMacOSVentura) {
// - CSR_ALLOW_UNTRUSTED_KEXTS - 0x1
// - CSR_ALLOW_UNRESTRICTED_FS - 0x2
// - CSR_ALLOW_UNAUTHENTICATED_ROOT - 0x800
if ((sip_int & 0x2) && (sip_int & 0x800)) {
if ([kernel_major_version intValue] == kMacOSVentura)) {
if !(sip_int & 0x1) {
NSLog(@"ParseSip: SIP value cannot root patch");
return NO;
}
}
NSLog(@"ParseSip: SIP value can root patch");
return YES;
}
} else if ([kernel_major_version intValue] == kMacOSMojave || [kernel_major_version intValue] == kMacOSCatalina) {
// - CSR_ALLOW_UNTRUSTED_KEXTS - 0x1
// - CSR_ALLOW_UNRESTRICTED_FS - 0x2
if ((sip_int & 0x1) && (sip_int & 0x2)) {
NSLog(@"ParseSip: SIP value can root patch");
return YES;
}
}
NSLog(@"ParseSip: SIP value cannot root patch");
return NO;
}
Python implementation
The logic for Python is actually quite similar to the Objective-C approach, however I wanted to make a more “out of the box” solution that developers could easily integrate into their projects.
Thus I developed the py_sip_xnu
module, where you can easily import the library and invoke get_sip_status()
. This returns an SIP object that you can easily detect SIP’s value, a breakdown of each bit as well as a simple booleans for common operations such as root volume editing.
- Project can also be found on PyPI, and easily installed via
pip3 install py_sip_xnu
.
import py_sip_xnu
sip_config = py_sip_xnu.SipXnu().get_sip_status()
'''
sip_config = {
'value': 0,
'breakdown': {
'csr_allow_untrusted_kexts': False,
'csr_allow_unrestricted_fs': False,
'csr_allow_task_for_pid': False,
'csr_allow_kernel_debugger': False,
'csr_allow_apple_internal': False,
'csr_allow_unrestricted_dtrace': False,
'csr_allow_unrestricted_nvram': False,
'csr_allow_device_configuration': False,
'csr_allow_any_recovery_os': False,
'csr_allow_unapproved_kexts': False,
'csr_allow_executable_policy_override': False,
'csr_allow_unauthenticated_root': False
},
'can_edit_root': False,
'can_write_nvram': False,
'can_load_arbitrary_kexts': False
}
'''
However for those wanting to manually implement this, the process is fairly straight forward.
To query the raw SIP value:
def __detect_sip_status(self):
# https://github.com/khronokernel/py_sip_xnu/blob/1.0.3/py_sip_xnu.py#L209-L235
if self.xnu_major < self._XnuOsVersion.OS_EL_CAPITAN:
# Assume unrestricted
return 65535
libsys = CDLL(self.lib_system_path)
result = c_uint(0)
error = libsys.csr_get_active_config(byref(result))
if error != 0:
raise Exception("Error while detecting SIP status: %d" % error)
print("csr_active_config: %d" % result.value)
return result.value
Once you’ve gotten the raw SIP value, simply use the bitwise &
operator to check against important bits:
def __sip_can_edit_root(self):
# https://github.com/khronokernel/py_sip_xnu/blob/1.0.3/py_sip_xnu.py#L237-L252
# 0x2 - CSR_ALLOW_UNRESTRICTED_FS
# 0x800 - CSR_ALLOW_UNAUTHENTICATED_ROOT
if self.sip_status & 0x2:
if self.xnu_major < self._XnuOsVersion.OS_BIG_SUR:
return True
if self.sip_status & 0x800:
return True
return False
Above logic based off of pudquick’s concept.