Talos Vulnerability Report

TALOS-2024-2010

Apple macOS ramrod arbitrary argv[0] execution vulnerability

June 12, 2024
CVE Number

CVE-2024-40800

SUMMARY

An arbitrary argv[0] execution vulnerability exists in the ramrod binary of Apple macOS version 14.5 (23F79) x86_64. An attacker can inject an arbitrary argv[0] pathname while launching the ramrod, leading to SIP bypass. Previous versions may also be affected, however, they haven’t been tested.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

Apple macOS 14.5 (23F79) x86_64

PRODUCT URLS

macOS - https://apple.com

CVSSv3 SCORE

8.2 - CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H

CWE

CWE-20 - Improper Input Validation

DETAILS

macOS is the operating system designed by Apple for their line of Mac computers, providing a graphical user interface and managing system resources and hardware.

The binary ramrod seems to be a component of the macOS recovery mechanism. This binary is signed by Apple and possesses one interesting heritable entitlement:

$ codesign -dv --entitlements - ./ramrod | grep -A2 heritable
Executable=/Users/frbenven/Downloads/POC/ramrod
Identifier=com.apple.ramrod
Format=Mach-O thin (x86_64)
CodeDirectory v=20400 size=16457 flags=0x0(none) hashes=504+7 location=embedded
Platform identifier=15
Signature size=4442
Signed Time=7 May 2024 at 05:03:53
Info.plist=not bound
TeamIdentifier=not set
Sealed Resources=none
Internal requirements count=1 size=64
    [Key] com.apple.rootless.install.heritable
    [Value]
        [Bool] true

This particular entitlement is noteworthy because of its implications: com.apple.rootless.install.heritable. This entitlement effectively allows the child processes of ramrod to bypass System Integrity Protection (SIP). From an attacker’s perspective, this is significant, but it would require the attacker to somehow force ramrod into spawning a process controlled by the attacker.

Following a redacted version of the ramrod’s main function:

uint64_t main(int32_t argc, int64_t* argv)
{
    [...]
    rax_1 = plugin_related();
    int32_t var_5d8 = 2;
    int128_t likely_kern_bootargs;
    if (rax_1 == 0)
    {
        zmm0 = {0};
        __builtin_memset(&likely_kern_bootargs, 0, 0x90);
        int64_t rcx_1;
        rcx_1 = _stat$INODE64("/usr/libexec/ramrod/plugins/devi…", &likely_kern_bootargs, zmm0) == 0;
        var_5d8 = ((int32_t)(rcx_1 * 3));
    }
    int32_t argv_2_is_server;
    if (argc == 2)
    {
        argv_2_is_server = _strcmp(argv[1], "-server");
    }
    if (((argc == 2 && argv_2_is_server != 0) || (argc != 2 && argc != 1)))
    {
        _fprintf(*(uint64_t*)___stderrp, "usage: %s [-server]", *(uint64_t*)argv);
        _exit(1);
        /* no return */
    }
    int32_t child_pid;
    int64_t var_620;
    char* child_argv[0x3];
[1] if (argc == 1)
    {
[2]     child_argv[0] = *(uint64_t*)argv;
        child_argv[1] = "-server";
        child_argv[2] = 0;
        [...]
        __builtin_memset(&likely_kern_bootargs, 0, 0x100);
        var_620 = 0x100;
        int32_t ret_sysctlbyname = _sysctlbyname("kern.bootargs", &likely_kern_bootargs, &var_620, 0, 0, zmm0_1);
        int64_t is_ramrod_debug;
        if (ret_sysctlbyname != 0)
        {
            _perror("sysctlbyname(kern.bootargs) -> 0");
        }
        else
        {
            is_ramrod_debug = _strstr(&likely_kern_bootargs, "ramrod_debug=1");
        }
        void** child_env;
        if ((ret_sysctlbyname != 0 || (ret_sysctlbyname == 0 && is_ramrod_debug == 0)))
        {
            child_env = nullptr;
        }
        if ((ret_sysctlbyname == 0 && is_ramrod_debug != 0))
        {
            _fwrite("enabling debug malloc in child p…", 0x27, 1, *(uint64_t*)___stderrp);
            child_env = &env_debug_malloc;
        }
        int32_t ret_posix_spawn;
        int512_t zmm0_2;
[3]     ret_posix_spawn = _posix_spawn(&child_pid, *(uint64_t*)argv, 0, 0, &child_argv, child_env);
        [...]
    }
    [...]
}

At [1] the code checks if the process is launched with just one argument, which corresponds to the pathname used to invoke the program.
If the program was invoked without any further arguments, the code at [3] is reached. The code at [3] calls _posix_spawn using argv[0] as the pathname for the executable and child_argv as arguments [2].
Essentially, when the program is called without any arguments, another process is spawned, using argv[0] for the pathname and -server as additional argument.
Since argv[0] can be arbitrarily set when executing ramrod, it is possible to control which program is spawned at [3], inheriting all of ramrod’s heritable entitlements. This will allow an attacker to execute an arbitrary binary with the com.apple.rootless.install.heritable entitlement, which allows for arbitrary code execution without SIP restrictions.

Note that we only tested the ramrod binary on an intel-based Mac.

TIMELINE

2024-06-12 - Vendor Disclosure
2024-07-29 - Vendor Patch Release
2024-07-30 - Public Release

Credit

Discovered by Claudio Bozzato and Francesco Benvenuto of Cisco Talos.