Xnu remote doublefree via data race in ipcomp input path Vulnerability / Exploit
/
/
/
Exploits / Vulnerability Discovered : 2019-10-09 |
Type : dos |
Platform : macos
This exploit / vulnerability Xnu remote doublefree via data race in ipcomp input path is for educational purposes only and if it is used you will do on your own risk!
[+] Code ...
=== Summary ===
This report describes a bug in the XNU implementation of the IPComp protocol
(https://tools.ietf.org/html/rfc3173). This bug can be remotely triggered by an
attacker who is able to send traffic to a macOS system (iOS AFAIK isn't
affected) *over two network interfaces at the same time*.
=== Some basics to provide context ===
IPComp is a protocol for compressing the payload of IP packets.
The XNU implementation of IPComp is (going by the last public XNU release)
enabled only on X86-64; ARM64 doesn't seem to have the feature enabled at all
(look for ipcomp_zlib in config/MASTER.x86_64 and config/MASTER.arm64). In other
words, it's enabled on macOS and disabled on iOS.
While IPComp is related to IPsec, the IPComp input path processes input even
when the user has not configured any IPsec stuff on the system.
zlib requires fairly large buffers for decompression and especially for
compression. In order to avoid allocating such buffers for each packet, IPComp
uses two global z_stream instances "deflate_stream" and "inflate_stream".
If IPComp isn't used, the buffer pointers in these z_stream instances remain
NULL; only when IPComp is actually used, the kernel will attempt to initialize
the buffer pointers.
As far as I can tell, the IPComp implementation of XNU has been completely
broken for years, which makes it impossible to actually reach the decompression
code. ipcomp_algorithm_lookup() is responsible for allocating global buffers for
the compression and decompression code; however, all of these allocations go
through deflate_alloc(), which (since xnu-1228, which corresponds to macOS 10.5
from 2007) calls _MALLOC() with M_NOWAIT. _MALLOC() leads to kalloc_canblock(),
which, if the M_NOWAIT flag was set and the allocation is too big for a kalloc
zone (size >= kalloc_max_prerounded), immediately returns NULL. On X86-64,
kalloc_max_prerounded is 0x2001; both deflateInit2() and inflateInit2() attempt
allocations bigger than that, causing them to fail with Z_MEM_ERROR, as is
visible with dtrace when observing the system's reaction to a single incoming
IPComp packet [empty lines removed]:
(On iOS, the kalloc() limit seems to be higher, so if IPComp was built there,
the input path might actually work?)
=== main bug description ===
IPComp uses a single global `static z_stream inflate_stream` for decompressing
all incoming packets. This global is used without any locking. While processing
of packets from a single interface seems to be single-threaded, packets arriving
on multiple ethernet interfaces at the same time (or on an ethernet interface
and a non-ethernet interface) can be processed in parallel (see
dlil_create_input_thread() and its caller for the precise threading rules).
Since zlib isn't designed for concurrent use of a z_stream, this leads to memory
corruption.
If IPComp actually worked, I believe that this bug would lead to
things like out-of-bounds reads, out-of-bounds writes and use-after-frees.
However, since IPComp never actually manages to set up the compression and
decompression state, the bug instead manifests in the code that, for every
incoming IPComp packet, attempts to set up the deflate buffers and tears down
the successfully allocated buffers because some of the allocations failed:
```
int ZEXPORT
deflateInit2_(z_streamp strm, int level, int method, int windowBits,
int memLevel, int strategy, const char *version,
int stream_size)
{
[...]
if (memLevel < 1 || memLevel > MAX_MEM_LEVEL || method != Z_DEFLATED ||
windowBits < 8 || windowBits > 15 || level < 0 || level > 9 ||
strategy < 0 || strategy > Z_FIXED) {
return Z_STREAM_ERROR;
}
if (windowBits == 8) windowBits = 9; /* until 256-byte window bug fixed */
s = (deflate_state *) ZALLOC(strm, 1, sizeof(deflate_state));
if (s == Z_NULL) return Z_MEM_ERROR;
strm->state = (struct internal_state FAR *)s;
[...]
s->window = (Bytef *) ZALLOC(strm, s->w_size, 2*sizeof(Byte));
s->prev = (Posf *) ZALLOC(strm, s->w_size, sizeof(Pos));
s->head = (Posf *) ZALLOC(strm, s->hash_size, sizeof(Pos));
s->lit_bufsize = 1 << (memLevel + 6); /* 16K elements by default */
BSD process name corresponding to current thread: kernel_task
Boot args: -zp -v keepsyms=1
Mac OS version:
18F132
Kernel version:
Darwin Kernel Version 18.6.0: Thu Apr 25 23:16:27 PDT 2019; root:xnu-4903.261.4~2/RELEASE_X86_64
Kernel UUID: 7C8BB636-E593-3CE4-8528-9BD24A688851
Kernel slide: 0x0000000012400000
Kernel text base: 0xffffff8012600000
__HIB text base: 0xffffff8012500000
System model name: Macmini7,1 (Mac-XXXXXXXXXXXXXXXX)
```
=== Repro steps ===
You'll need a Mac (I used a Mac mini) and a Linux workstation.
Stick two USB ethernet adapters into the Mac.
Make sure that your Linux workstation has two free ethernet ports; if it
doesn't, also stick USB ethernet adapters into your workstation.
Take two ethernet cables; for both of them, stick one end into the Linux
workstation and the other end into the Mac.
Set up static IP addresses for both interfaces on the Linux box and the Mac. I'm
using:
- Linux, first connection: 192.168.250.1/24
- Mac, first connection: 192.168.250.2/24
- Linux, second connection: 192.168.251.1/24
- Mac, second connection: 192.168.251.2/24
On the Linux workstation, ping both IP addresses of the Mac, then dump the
relevant ARP table entries:
```
$ ping -c1 192.168.250.2
PING 192.168.250.2 (192.168.250.2) 56(84) bytes of data.
64 bytes from 192.168.250.2: icmp_seq=1 ttl=64 time=0.794 ms
[...]
$ ping -c1 192.168.251.2
PING 192.168.251.2 (192.168.251.2) 56(84) bytes of data.
64 bytes from 192.168.251.2: icmp_seq=1 ttl=64 time=0.762 ms
[...]
$ arp -n | egrep '192\.168\.25[01]'
192.168.250.2 ether aa:aa:aa:aa:aa:aa C eth0
192.168.251.2 ether bb:bb:bb:bb:bb:bb C eth1
$
```
On the Linux workstation, build the attached ipcomp_uaf.c and run it:
After something like a second, you should be able to observe that the Mac panics.
I have observed panics via double-free and via null deref triggered by the PoC.
(Stop the PoC afterwards, otherwise it'll panic again as soon as the network
interfaces are up.)
(The PoC also works if you use broadcast addresses as follows: ```
# ./ipcomp_uaf eth0 ff:ff:ff:ff:ff:ff 0.0.0.0 255.255.255.255 eth1 ff:ff:ff:ff:ff:ff 0.0.0.0 255.255.255.255
```)
=== Fixing the bug ===
I believe that by far the best way to fix this issue is to rip out the entire
feature. Unless I'm missing some way for the initialization to succeed, it looks
like nobody can have successfully used this feature in the last few years; and
apparently nobody felt strongly enough about that to get the feature fixed.
At the same time, this thing is remote attack surface in the IP stack, and it
looks like it has already led to a remote DoS bug in the past - the first search
result on bing.com for both "ipcomp macos" and "ipcomp xnu" is
<https://www.exploit-db.com/exploits/5191>.
In case you decide to fix the bug in a different way, please note:
- I believe that this can *NOT* be fixed by removing the PR_PROTOLOCK flag from
the entries in `inetsw` and `inet6sw`. While removal of that flag would cause
the input code to take the domain mutex before invoking the protocol handler,
IPv4 and IPv6 are different domains, and so concurrent processing of
IPv4+IPComp and IPv6+IPComp packets would probably still trigger the bug.
- If you decide to fix the memory allocation of IPComp so that the input path
works again (please don't - you'll never again have such a great way to prove
that nobody is using that code), I think another bug will become reachable:
I don't see anything that prevents unbounded recursion between
ip_proto_dispatch_in() and ipcomp4_input() using an IP packet with a series
of IPComp headers, which would be usable to cause a kernel panic via stack
overflow with a single IP packet.
In case you want to play with that, I wrote a PoC that generates packets with
100 such headers and attached it as ipcomp_recursion.c.
(The other IPv6 handlers for pseudo-protocols like IPPROTO_FRAGMENT seem to
avoid this problem by having the )
Proof of Concept:
https://github.com/offensive-security/exploitdb-bin-sploits/raw/master/bin-sploits/47479.zip
Xnu remote doublefree via data race in ipcomp input path