TL;DR Link to heading
On macOS 10.15.2 Apple introduced the com.apple.private.security.clear-library-validation
entitlement, which is slowly replacing the previously used com.apple.security.cs.disable-library-validation
entitlement on system binaries. Although their impact is the about the same, the way they work is different. While library validation is automatically disabled using com.apple.security.cs.disable-library-validation
, with com.apple.private.security.clear-library-validation
, the application has to disable it for itself through a csops
system call.
Intro Link to heading
With the release of Big Sur, I noticed that many of the system binaries have a new entitlement, which I didn’t see before, and that is com.apple.private.security.clear-library-validation
(later it turned out that it was introduced earlier, but I didn’t know about it that time). These applications possessed the com.apple.security.cs.disable-library-validation
before, so it seemed that this is being replaced with a new one. The name suggest similarity, and testing it also confirmed, that these binaries can still load third party plugins signed by non Apple developers. This means that these entitlements have the same impact.
However their inner working is different.
The csops system call Link to heading
I didn’t dig into this new entitlement until I run into a new csops
operation code listed below, which can be found in xnu-7195.50.7.100.1/bsd/sys/codesign.h
.
#define CS_OPS_CLEAR_LV 15 /* clear the library validation flag */
csops
is a system call that can be used to perform various code signing related operation on processes. We can query the status of a process, set various flags during runtime, query its code signing blob, and so on. This was a new one that I didn’t spot before, so I started to go after it.
The description of this constant says that we can use this operation code to clear the library validation flag of a process. This means that if we can run this on a process we can load third party libraries into it if the call is successful.
This constant is only referenced inside the xnu-7195.50.7.100.1/bsd/kern/kern_proc.c
source file, which contains the source for the csops_internal
function. This is the function that will be run when the system call is made. Here is the relevant part of the source code concerning the CS_OPS_CLEAR_LV
operation.
static int
csops_internal(pid_t pid, int ops, user_addr_t uaddr, user_size_t usersize, user_addr_t uaudittoken)
{
(...)
if (pid == 0) {
pid = proc_selfpid();
}
if (pid == proc_selfpid()) {
forself = 1;
}
switch (ops) {
case CS_OPS_STATUS:
case CS_OPS_CDHASH:
case CS_OPS_PIDOFFSET:
case CS_OPS_ENTITLEMENTS_BLOB:
case CS_OPS_IDENTITY:
case CS_OPS_BLOB:
case CS_OPS_TEAMID:
case CS_OPS_CLEAR_LV:
break; /* not restricted to root */
default:
if (forself == 0 && kauth_cred_issuser(kauth_cred_get()) != TRUE) {
return EPERM;
}
break;
}
pt = proc_find(pid);
if (pt == PROC_NULL) {
return ESRCH;
}
(...)
#if CONFIG_MACF
switch (ops) {
case CS_OPS_MARKINVALID:
case CS_OPS_MARKHARD:
case CS_OPS_MARKKILL:
case CS_OPS_MARKRESTRICT:
case CS_OPS_SET_STATUS:
case CS_OPS_CLEARINSTALLER:
case CS_OPS_CLEARPLATFORM:
case CS_OPS_CLEAR_LV:
if ((error = mac_proc_check_set_cs_info(current_proc(), pt, ops))) {
goto out;
}
break;
default:
if ((error = mac_proc_check_get_cs_info(current_proc(), pt, ops))) {
goto out;
}
}
#endif
switch (ops) {
(...)
case CS_OPS_CLEAR_LV: {
/*
* This option is used to remove library validation from
* a running process. This is used in plugin architectures
* when a program needs to load untrusted libraries. This
* allows the process to maintain library validation as
* long as possible, then drop it only when required.
* Once a process has loaded the untrusted library,
* relying on library validation in the future will
* not be effective. An alternative is to re-exec
* your application without library validation, or
* fork an untrusted child.
*/
#if !defined(XNU_TARGET_OS_OSX)
// We only support dropping library validation on macOS
error = ENOTSUP;
#else
/*
* if we have the flag set, and the caller wants
* to remove it, and they're entitled to, then
* we remove it from the csflags
*
* NOTE: We are fine to poke into the task because
* we get a ref to pt when we do the proc_find
* at the beginning of this function.
*
* We also only allow altering ourselves.
*/
if (forself == 1 && IOTaskHasEntitlement(pt->task, CLEAR_LV_ENTITLEMENT)) {
proc_lock(pt);
pt->p_csflags &= (~(CS_REQUIRE_LV | CS_FORCED_LV));
proc_unlock(pt);
error = 0;
} else {
error = EPERM;
}
(...)
}
We will go through this piece by piece. There are three places where it gets checked. We will start with the first switch
case.
switch (ops) {
case CS_OPS_STATUS:
case CS_OPS_CDHASH:
case CS_OPS_PIDOFFSET:
case CS_OPS_ENTITLEMENTS_BLOB:
case CS_OPS_IDENTITY:
case CS_OPS_BLOB:
case CS_OPS_TEAMID:
case CS_OPS_CLEAR_LV:
break; /* not restricted to root */
default:
if (forself == 0 && kauth_cred_issuser(kauth_cred_get()) != TRUE) {
return EPERM;
}
break;
}
What happens here is that the system will allow non-root execution of the operations listed in the switch table, including the one, which is in the focus of our interest. This means that we can call csops
with the CS_OPS_CLEAR_LV
operation even if we are not running as root.
Let’s move on to the other switch case.
#if CONFIG_MACF
switch (ops) {
case CS_OPS_MARKINVALID:
case CS_OPS_MARKHARD:
case CS_OPS_MARKKILL:
case CS_OPS_MARKRESTRICT:
case CS_OPS_SET_STATUS:
case CS_OPS_CLEARINSTALLER:
case CS_OPS_CLEARPLATFORM:
case CS_OPS_CLEAR_LV:
if ((error = mac_proc_check_set_cs_info(current_proc(), pt, ops))) {
goto out;
}
break;
default:
if ((error = mac_proc_check_get_cs_info(current_proc(), pt, ops))) {
goto out;
}
}
#endif
Here we have a MACF policy call, using the function mac_proc_check_get_cs_info
. MACF policy calls return 0 if they are successful, and this is what being check on the condition. The mac_proc_check_get_cs_info
function is implemented inside xnu-7195.50.7.100.1/security/mac_process.c
. Let’s follow it.
int
mac_proc_check_set_cs_info(proc_t curp, proc_t target, unsigned int op)
{
kauth_cred_t cred;
int error = 0;
#if SECURITY_MAC_CHECK_ENFORCE
/* 21167099 - only check if we allow write */
if (!mac_proc_enforce) {
return 0;
}
#endif
if (!mac_proc_check_enforce(curp)) {
return 0;
}
cred = kauth_cred_proc_ref(curp);
MAC_CHECK(proc_check_set_cs_info, cred, target, op);
kauth_cred_unref(&cred);
return error;
}
This function will eventually make a MACF call using the MAC_CHECK
macro, what I already discussed in my Reversing engineering the fix of CVE-2020-9771 post. It will iterate over the MACF policy hooks, which make a check on proc_check_set_cs_info
. This is only hooked right now by the Sandbox, which we can see below.
void _hook_proc_check_set_cs_info(int arg0, int arg1) {
___bzero(&var_1A0, 0x188);
*(int32_t *)(&var_1A0 + 0xa8) = 0x4;
*(&var_1A0 + 0xb8) = arg1;
_cred_sb_evaluate(arg0, 0x65, &var_1A0, rcx, r8, r9);
return;
}
This will make an internal call to _cred_sb_evaluate
with the opcode 0x65
, I also discussed this in my previous post.
Going back to our csops
call, we can determine that a MACF policy check will be run if this operation is allowed or not for the calling process.
Assuming yes, we move on, and finally reach the point where the operation is actually implemented.
case CS_OPS_CLEAR_LV: {
/*
* This option is used to remove library validation from
* a running process. This is used in plugin architectures
* when a program needs to load untrusted libraries. This
* allows the process to maintain library validation as
* long as possible, then drop it only when required.
* Once a process has loaded the untrusted library,
* relying on library validation in the future will
* not be effective. An alternative is to re-exec
* your application without library validation, or
* fork an untrusted child.
*/
#if !defined(XNU_TARGET_OS_OSX)
// We only support dropping library validation on macOS
error = ENOTSUP;
#else
/*
* if we have the flag set, and the caller wants
* to remove it, and they're entitled to, then
* we remove it from the csflags
*
* NOTE: We are fine to poke into the task because
* we get a ref to pt when we do the proc_find
* at the beginning of this function.
*
* We also only allow altering ourselves.
*/
if (forself == 1 && IOTaskHasEntitlement(pt->task, CLEAR_LV_ENTITLEMENT)) {
proc_lock(pt);
pt->p_csflags &= (~(CS_REQUIRE_LV | CS_FORCED_LV));
proc_unlock(pt);
error = 0;
} else {
error = EPERM;
}
I think the description added by Apple is very nice, detailed and explains everything. If the requirements are satisfied it will clear the library validation flag of the target process (pt->p_csflags &= (~(CS_REQUIRE_LV | CS_FORCED_LV));
).
The condition is very strict. This operation can be called by processes only on themselves (forself == 1
) and those that has the entitlement defined by the constant CLEAR_LV_ENTITLEMENT
. This is defined in xnu-7195.50.7.100.1/bsd/sys/codesign.h
.
#define CLEAR_LV_ENTITLEMENT "com.apple.private.security.clear-library-validation"
And the loop is closed we got the entitlement which we saw before.
To sum up, we can say that a process owning the com.apple.private.security.clear-library-validation
entitlement can call the csops
system call using the CLEAR_LV_ENTITLEMENT
to clear the library validation code signing flag on itself. This works even if the process is not running as root.
SSH Link to heading
To confirm this finding I loaded ssh
, which has this entitlement, into Hopper, and verify that it uses csops
to disable library validation.
int sub_10016d41f(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
(...)
loc_10016d67b:
rax = getpid();
rax = csops(rax, 0xf, 0x0, 0x0);
if (rax == 0x0) goto loc_10016d6d6;
loc_10016d694:
rdx = 0x0;
rcx = 0x0;
rbx = 0x0;
sub_10014e556("csops(CS_OPS_CLEAR_LV) failed: %d", rax, rdx, rcx, r8, r9, stack[-136]);
(...)
Indeed we can find the function which calls csops
with the opcode 0xf
, which is the CS_OPS_CLEAR_LV
operation. There is even a nice detailed error message if that call fails.
History Link to heading
Although I only noticed this in Big Sur, this feature was introduced earlier. The detailed csops
operation to clear the library validation flag was introduced in xnu-6153.61.1
, which was used on macOS 10.15.2. However binaries that used this entitlement were only available since macOS 10.15.4, and it started with four applications: su
, screen
, login
, and passwd
.
As of Big Sur (macOS 11.0) 20 binaries posses this new entitlement, so Apple is slowly migrating over to this new method.
Why is it good? Link to heading
One might wonder what’s the benefit of using this method over the old one. I’m only speculating here, but I can see some benefits. With the new method, when applications being loaded, library validation will be enforced, which means that you can’t perform a dylib hijacking or proxying attack on such binaries. This is very common problem especially on third party apps.
You can still inject code to such apps, but only via plugins, and not through other means. Although a plugin might not be more difficult than a standard dylib, it’s still a good move towards a better design. Unfortunately, as the name suggests, it’s only available for Apple itself, if we try to use it on our own binary we get an error.
mac_vnode_check_signature: /tmp/launch: code signature validation failed fatally: When validating /tmp/launch:
Code has restricted entitlements, but the validation of its code signature failed.
Unsatisfied Entitlements:
Recap Link to heading
We saw that Apple introduced a new entitlement, com.apple.private.security.clear-library-validation
in macOS 10.15.2, which allows a process to clear the library validation flag on itself using csops
system calls. Apple is slowly migrating over applications from using the old com.apple.security.cs.disable-library-validation
entitlement to this new one, which allows a slightly more secure design.