Is a LaunchCodeRequirement Time-Of-Check/Time-Of-Use protected?

In the LightweightCodeRequirements framework, there is a LaunchCodeRequirement object which can be used as a requirement object for a Process for example.

What I don't understand (I admit my macOS low-level knowledge is limited) is that how can this be used in a secure way that doesn't fall victim of a Time-of-Check/Time-of-Use issue.

e.g.

  • I specify a LaunchCodeRequirement via Process.launchRequirement for my process, let's say /usr/local/bin/mycommandlinetool.
  • The LaunchCodeRequirement specifies my development team and a developer ID certificate.
  • The process must be started in some form, before a SecCode/SecTask object can be created, rather than a SecStaticCode object (which only guarantees its validity checks to be intact as long as the file is not modified).
  • But if the process was started, then I have no tools in my set to prevent it from executing its initialization code or similar. Then, by the time I'm able to check via SecCode/SecTask functions the LaunchCodeRequirement, I might have already ran malicious code - if mycommandlinetool was maliciously replaced.

Or does the operating system use a daemon to copy the executable specified for Process to a secure location, then creates the SecStaticCode object, assesses the LaunchCodeRequirement and if passed, launches the executable from that trusted location (which would make sure it is immutable for replacement by malicious actors)?

I have a hard time understanding how this works under the hood - if I remember correctly these are private APIs.

Answered by DTS Engineer in 852129022

You typically tackle this by applying a launch requirement to the launch itself. If you’re using Process [1], do this by setting the launchRequirement property on the Process object. This is of type LaunchCodeRequirement, which you create using the LightweightCodeRequirements file.

This should be resilient to TOC/TOU issues. When Process runs the executable, the kernel opens the file, runs the requirement check against the file, and then launches the file. If someone substitutes a file before the open, it gets caught by the check. If they substitute it after the open, their version isn’t executed.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] This is NSTask in Objective-C. It can do the same thing as Process, although the mechanics are a bit different.

Accepted Answer

You typically tackle this by applying a launch requirement to the launch itself. If you’re using Process [1], do this by setting the launchRequirement property on the Process object. This is of type LaunchCodeRequirement, which you create using the LightweightCodeRequirements file.

This should be resilient to TOC/TOU issues. When Process runs the executable, the kernel opens the file, runs the requirement check against the file, and then launches the file. If someone substitutes a file before the open, it gets caught by the check. If they substitute it after the open, their version isn’t executed.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] This is NSTask in Objective-C. It can do the same thing as Process, although the mechanics are a bit different.

Thank you for your answer!

When Process runs the executable, the kernel opens the file, runs the requirement check against the file, and then launches the file.

This is completely new to me, I admit, I was unaware of these being separate operations.

Now I looked into this more, and I see there are separate functions on Darwin-level for opening a file open() and launching a file exec..(). [I guess?]

However I'm not seeing a function for launching an opened file (as you said above) in Darwin - my Internet search results do bring up fexecve but that appears to be unsupported in Darwin.

Am I poking at the right place or should I just stop going further?

I'm interested in this topic because I wonder if I can implement Process/launchRequirement in some secure way before macOS 14.4.

fexecve but that appears to be unsupported in Darwin.

Right.

On macOS it’s better to use posix_spawn rather than the old school fork > exec* sequence. That’s because macOS programs typically have more than one thread, and the fork > exec* sequence presents serious challenges in a threaded environment.

Process (and NSTask in Objective-C) follows this guidance.

posix_spawn is a kernel call. It can do things that you can’t do from user space.

posix_spawn supports attributes (posix_spawnattr_t) and that’s how Process passes the LWCR to the posix_spawn and hence to the kernel. That attribute is not API, although you can see it in the Darwin open source.

WARNING The Darwin open source is full of implementation details. Do not encode such implementations details into a shipping product.

I wonder if I can implement Process/launchRequirement in some secure way before macOS 14.4.

Not easily.

The canonical way to do such things is to copy the executable to a privileged location, check the signature there, and then run it. That may or may not be feasible, depending on your specific situation.

The other way to slice this is to start the process suspended (POSIX_SPAWN_START_SUSPENDED), check it’s signature, and then resume or kill it depending on the result. However, that’s tricky for at least two reasons:

  • The process suspends at the first instruction of the dynamic linker. I’ve never explored whether the main executable is available at that point, but I suspect not. You could let the dynamic linker run up to the point where it maps the map executable, but there’s no easy way to do that.
  • This is basically debugging infrastructure, so it probably won’t work if the program has the hardened runtime enable (unless it happens to be signed with com.apple.security.get-task-allow).

Overall, I suspect that you’d be better off focusing on the future rather than worrying about macOS 13 users.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Is a LaunchCodeRequirement Time-Of-Check/Time-Of-Use protected?
 
 
Q