├── .gitignore ├── CapFile.js ├── Crack.js ├── Main.js ├── README.md ├── crypto-js.js ├── index.html └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | *.cap 2 | -------------------------------------------------------------------------------- /CapFile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * DESIGN 3 | * Using string-as-bytes per https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Sending_and_Receiving_Binary_Data 4 | */ 5 | 6 | /** 7 | * RESEARCH 8 | * Additional Reading and References: 9 | * 10 | * Details on WPA encryption (byte-level) - http://sid.rstack.org/pres/0810_BACon_WPA2_en.pdf 11 | * - Includes details on "caching" repeated calls to "BODY" 12 | * 13 | * HMAC-SHA1 in JS - https://github.com/Caligatio/jsSHA/tree/v2.0.1 14 | * - Try online: https://caligatio.github.io/jsSHA/ 15 | * - Text: "value" 16 | * - Key: "secret" 17 | * 18 | * PBKDF2: 19 | * - Apparently it's HMAC-SHA1() * 8192 20 | * - But see https://en.wikipedia.org/wiki/PBKDF2 21 | * - PBKDF2, DK = PBKDF2(PRF, Password, Salt, count, dkLen) 22 | * - WPA, DK = PBKDF2(HMAC−SHA1, passphrase, ssid, 4096, 256) 23 | * 24 | * 25 | * Load byte array from URL: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Sending_and_Receiving_Binary_Data 26 | * 27 | * aircrack-ng sample WPA file: http://www.aircrack-ng.org/doku.php?id=wpa_capture 28 | * Parsing .cap files: http://systemsarchitect.net/2014/03/12/parsing-binary-data-in-php-on-an-example-with-the-pcap-format/ 29 | * 30 | * http://www.willhackforsushi.com/papers/80211_Pocket_Reference_Guide.pdf 31 | * 32 | * Details on 802.11 packet structures - http://www.studioreti.it/slide/802-11-Frame_E_C.pdf 33 | * 34 | * 35 | * TODO: 36 | * - Check bytes_.length >= header.length before parsing! (chocobo-new.cap is 'truncated', gives weird results) 37 | * - Look at using ArrayBuffer + DataView internal JS structures. 38 | * - Browser compatibility (ie, chrome, safari) 39 | */ 40 | 41 | 42 | /** 43 | * Parse the given bytes of a Packet Capture (PCAP) file. 44 | * Loads result into this.globalHeader (always) and list this.packetFrames (for known frame types). 45 | * 46 | * @param bytes (string? bytes?) Raw bytes from a .cap Pcap file. 47 | * @param debug (boolean or function) If 'true': Dumps debug information to console. 48 | * If 'false': Does not dump anything to console. 49 | * If given a function, calls function with debug text. 50 | */ 51 | function CapFile(bytes, debug) { 52 | if (debug) { 53 | if (typeof debug === "boolean") { 54 | // Default debug function 55 | CapFile.debug = function(txt) { 56 | console.log("[CapFile.js] " + txt); 57 | } 58 | } 59 | else if (typeof debug === "function") { 60 | CapFile.debug = debug; 61 | } 62 | else { 63 | throw Error("Unexpected type of 'debug' option: " + (typeof debug)); 64 | } 65 | } 66 | 67 | this.bytes_ = bytes; 68 | this.byteOffset_ = 0; 69 | this.bytesTotal = this.bytes_.length; 70 | //this.bytesHex = this.getHex(0, this.bytesTotal, " "); 71 | 72 | this.parse(); 73 | 74 | delete this.bytes_; 75 | delete this.byteOffset_; 76 | }; 77 | 78 | // Configurations 79 | CapFile.useBigEndian = true; 80 | 81 | // Constants 82 | CapFile.MAGIC_NUMBER = 2712847316; 83 | CapFile.SUPPORTED_PCAP_VERSION = "2.4"; 84 | CapFile.WLAN_HEADER_TYPE = 105; 85 | CapFile.GLOBAL_HEADER_LENGTH = 24; 86 | CapFile.PACKET_HEADER_LENGTH = 16; 87 | 88 | /** 89 | * TODO: Log parse time for debug mode. 90 | */ 91 | CapFile.prototype.parse = function() { 92 | // First chunk of bytes is global header. 93 | this.byteOffset_ = 0; 94 | this.globalHeader = CapFile.GlobalHeader.call(this); 95 | 96 | // Ensure we can parse this Pcap file/ format version. 97 | if (this.globalHeader.version !== CapFile.SUPPORTED_PCAP_VERSION) { 98 | throw Error("Unsupported PCap File version (" + this.globalHeader.version + "). " + 99 | "Unable to parse."); 100 | } 101 | 102 | // Restrict parsing to WLAN types. 103 | if (this.globalHeader.headerType !== CapFile.WLAN_HEADER_TYPE) { 104 | throw Error("Unsupported (non-WLAN) Pcap file header type (" + this.globalHeader.headerType + "). " + 105 | "Unable to parse."); 106 | } 107 | 108 | // Skip past Blobal Header bytes. 109 | this.byteOffset_ += CapFile.GLOBAL_HEADER_LENGTH; 110 | 111 | // List of all identified frames in the cap file. 112 | this.packetFrames = []; 113 | 114 | var frame, skippedFrames = 0; 115 | while (this.byteOffset_ < this.bytes_.length) { 116 | frame = CapFile.WlanFrame.call(this); 117 | if (frame && frame.name !== undefined && frame.name.indexOf("Unknown") === -1) { 118 | // Only add known packet types to frames list. 119 | this.packetFrames.push(frame); 120 | } 121 | else { 122 | skippedFrames++; 123 | } 124 | } 125 | if (CapFile.debug) { 126 | var totalFrames = skippedFrames + this.packetFrames.length; 127 | CapFile.debug("Parsed " + this.packetFrames.length + " known frames out of " + totalFrames + " total frames."); 128 | } 129 | }; 130 | 131 | /** 132 | * Extract integer from bytes_ at current byteOffset_ 133 | * 134 | * @param startIndex Index of first byte (added to byteOffset_) 135 | * @param endIndex Index of last byte (added to byteOffset_) 136 | * @param useBigEndian Override configuration, expect big-endian-style byte order (default: CapFile.useBigEndian) 137 | * @param signed If integer should be signed (default: false) 138 | * 139 | * @return (int) Numeric-representation of data at byte location. 140 | */ 141 | CapFile.prototype.getInt = function(startIndex, endIndex, useBigEndian, signed) { 142 | startIndex += this.byteOffset_; 143 | endIndex += this.byteOffset_; 144 | 145 | var intResult = 0, i, x; 146 | if (useBigEndian == true || CapFile.useBigEndian) { 147 | for (i = startIndex; i < endIndex; i++) { 148 | intResult = intResult << 8; 149 | x = this.bytes_.charCodeAt(i); 150 | intResult = intResult | x; 151 | } 152 | } else { 153 | for (i = endIndex - 1; i >= startIndex; i--) { 154 | intResult = intResult << 8; 155 | x = this.bytes_.charCodeAt(i); 156 | intResult = intResult | x; 157 | } 158 | } 159 | 160 | if (!signed) { 161 | // convert to unsigned. 162 | // See http://stackoverflow.com/a/1908655 163 | intResult = (intResult >>> 0); 164 | } 165 | return intResult; 166 | }; 167 | 168 | /** 169 | * Extract hex characters from bytes_ at current byteOffset_ 170 | * 171 | * @param startIndex (int) Index of first byte (added to byteOffset_) 172 | * @param endIndex (int) Index of last byte (added to byteOffset_) 173 | * @param byteSpacer (string) Separator between bytes (default: empty string) 174 | * @param colSpacer (string) Separator between chunks of 8 bytes (default: empty string) 175 | * @param rowSpacer (string) Separator between chunks of 16 bytes (default: empty string) 176 | * 177 | * @return (string) Hex-representation of data at byte location. 178 | */ 179 | CapFile.prototype.getHex = function(startIndex, endIndex, byteSpacer, colSpacer, rowSpacer) { 180 | startIndex += this.byteOffset_; 181 | endIndex += this.byteOffset_; 182 | 183 | var byteList = [], hex, i, counter = 0; 184 | if (byteSpacer) { 185 | byteList.push(""); 186 | } 187 | 188 | // Presume Big-endian for all hex value operations. 189 | for (i = startIndex; i < endIndex; i++) { 190 | hex = this.bytes_.charCodeAt(i).toString(16); 191 | while (hex.length < 2) { 192 | hex = "0" + hex; 193 | } 194 | counter++; 195 | if (rowSpacer && counter % 16 == 0) { 196 | hex += rowSpacer; 197 | } else if (colSpacer && counter % 8 == 0) { 198 | hex += colSpacer; 199 | } 200 | byteList.push(hex); 201 | } 202 | 203 | return byteList.join(byteSpacer || ""); 204 | }; 205 | 206 | /** 207 | * Extract raw bytes from bytes_ at current byteOffset_ 208 | * 209 | * @param startIndex (int) Index of first byte (added to byteOffset_) 210 | * @param endIndex (int) Index of last byte (added to byteOffset_) 211 | * 212 | * @return (string? bytes?) Raw bytes of data at byte location. 213 | */ 214 | CapFile.prototype.getBytes = function(startIndex, endIndex) { 215 | startIndex += this.byteOffset_; 216 | endIndex += this.byteOffset_; 217 | 218 | var byteList = [], hex, i; 219 | for (i = startIndex; i < endIndex; i++) { 220 | hex = this.bytes_.charAt(i); 221 | byteList.push(hex); 222 | } 223 | if (CapFile.useBigEndian) { 224 | byteList.reverse(); 225 | } 226 | return byteList.join(""); 227 | }; 228 | 229 | /** 230 | * Parses Pcap file global header (starting at byteOffset_, presumably "0"). 231 | * 232 | * Requires reference to "this" CapFile object. 233 | * 234 | * More info on https://wiki.wireshark.org/Development/LibpcapFileFormat 235 | * 236 | * @return (object) containing: 237 | * version (string) in format . e.g. 2.4 238 | * gmtOffset (signed int) Offset between packet timestamps and GMT timezone 239 | * sigFigs (int) Accuracy of timestamps 240 | * snapshotLength (int) Length of snapshot for the capture (in bytes) 241 | * headerType (int) Link-Layer header type, e.g. LINKTYPE_IEEE802.11 = 105 (Wireless LAN) 242 | */ 243 | CapFile.GlobalHeader = function() { 244 | // Presume big endian. 245 | CapFile.useBigEndian = false; 246 | 247 | // Set global endianess based on the magic number. 248 | var magic_number = this.getInt(0, 4); 249 | if (magic_number === CapFile.MAGIC_NUMBER) { 250 | if (CapFile.debug) { 251 | CapFile.debug("Using Little-Endian byte-encoding due to magic number: " + magic_number); 252 | } 253 | } else { 254 | if (CapFile.debug) { 255 | CapFile.debug("Using Big-Endian byte-encoding due to magic number: " + magic_number); 256 | } 257 | CapFile.useBigEndian = true; 258 | magic_number = this.getInt(0, 4); 259 | if (magic_number !== CapFile.MAGIC_NUMBER) { 260 | throw Error("Can't read magic number! Got <" + magic_number + ">, but expecting <" + CapFile.MAGIC_NUMBER + ">"); 261 | } 262 | } 263 | 264 | var headers = { 265 | 266 | // Version of cap file. 267 | version: this.getInt(4, 6).toString(10) + "." + this.getInt(6, 8).toString(10), 268 | 269 | // Difference between capfile timestamps and GMT (in seconds) 270 | gmtOffset: this.getInt(8, 12, undefined, true), 271 | 272 | // The accuracy of the capfile timestamps 273 | sigFigs: this.getInt(12, 16), 274 | 275 | // Length of snapshot for the capture. 276 | // May cause "length" in PacketHeader to differ from "originalLength" 277 | snapshotLength: this.getInt(16, 20), 278 | 279 | // Link-layer header type. See http://www.tcpdump.org/linktypes.html 280 | // e.g. LINKTYPE_IEEE802_11 = 105 (for Wireless LAN) 281 | headerType: this.getInt(20, 24) 282 | 283 | }; 284 | 285 | if (CapFile.debug) { 286 | CapFile.debug("GlobalHeader (24 bytes):\n" + this.getHex(0, 24, " ", " ", "\n")); 287 | } 288 | 289 | return headers; 290 | }; 291 | 292 | /** 293 | * "Frame Builder". Parses entire frame at current bytesOffset_ location. 294 | * Only supports detailed parsing of certain 802.11 WLAN frames - see CapFile.WlanFrame.* for more info 295 | * 296 | * Requires reference to "this" CapFile object -- call using CapFile.WlanFrame.call(this); 297 | * 298 | * @return (object) Frame information containing: { 299 | * header: { 300 | * timestamp: (Date) Timestamp of the packet as a Date object 301 | * length: (int) Length of the packet/frame (in bytes) 302 | * }, 303 | * frameControl: { 304 | * version: (int) Frame version 305 | * type: (int) Frame type (0=Management, 1=Control, 2=Data) 306 | * subtype: (int) Frame subtype 307 | * flags: { 308 | * toDS: (boolean) 309 | * fromDS: (boolean) 310 | * moreFragments:(boolean) 311 | * retry: (boolean) 312 | * powerMgt: (boolean) 313 | * moreData: (boolean) 314 | * is_protected: (boolean) 315 | * order: (boolean) 316 | * } 317 | * }, 318 | * duration: (int) 319 | * addr1: (string) First address, as hex characters (no separator). 320 | * } 321 | * 322 | * CONTROL FRAMES (type:1) will not contain any additional information. 323 | * 324 | * MANAGEMENT FRAMES (type:0) and DATA FRAMES (type:2) both contain more information: { 325 | * addr2: (string) Second address, as hex characters (no separator). 326 | * addr3: (string) Second address, as hex characters (no separator). 327 | * fragmentNumber: (int) 328 | * sequenceNumber: (int) 329 | * } 330 | * 331 | * More info on MANAGEMENT frames: see CapFile.WlanFrame.Management 332 | * More info on DATA frames: see CapFile.WlanFrame.Data 333 | * 334 | */ 335 | CapFile.WlanFrame = function() { 336 | var frame = {}; 337 | 338 | // Parse header 339 | // Details on https://wiki.wireshark.org/Development/LibpcapFileFormat 340 | 341 | // Convert timestamp to Date. 342 | var timestampSec = this.getInt(0, 4); 343 | var timestampUsec = this.getInt(4, 8); 344 | var ts_usec = timestampSec * 1000; 345 | ts_usec += (timestampUsec / 1000); 346 | ts_usec += this.globalHeader.gmtOffset; 347 | frame.header = { 348 | timestamp: new Date(ts_usec), 349 | length: this.getInt(8, 12), 350 | originalLength: this.getInt(12, 16) 351 | }; 352 | 353 | // Shift to frame body. 354 | this.byteOffset_ += CapFile.PACKET_HEADER_LENGTH; 355 | 356 | // Mark where this packet ends. 357 | var endOfPacketOffset = this.byteOffset_ + frame.header.originalLength; 358 | 359 | // Parse fields that are present in all Wlan frames. 360 | // Details on https://en.wikipedia.org/wiki/IEEE_802.11#Layer_2_.E2.80.93_Datagrams 361 | var frameControlBits = this.getInt(0, 1); 362 | frame.frameControl = { 363 | version: (frameControlBits >>> 0) & 0b11, 364 | type: (frameControlBits >>> 2) & 0b11, 365 | subtype: (frameControlBits >>> 4) & 0b1111 366 | }; 367 | 368 | var frameControlFlags = this.getInt(1, 2); 369 | frame.frameControl.flags = { 370 | toDS: !!(frameControlFlags >>> 0 & 0b1), 371 | fromDS: !!(frameControlFlags >>> 1 & 0b1), 372 | moreFragments: !!(frameControlFlags >>> 2 & 0b1), 373 | retry: !!(frameControlFlags >>> 3 & 0b1), 374 | powerMgt: !!(frameControlFlags >>> 4 & 0b1), 375 | moreData: !!(frameControlFlags >>> 5 & 0b1), 376 | is_protected: !!(frameControlFlags >>> 6 & 0b1), 377 | order: !!(frameControlFlags >>> 7 & 0b1) 378 | }; 379 | 380 | frame.duration = this.getInt(2, 4); 381 | 382 | frame.addr1 = this.getHex(4, 10); 383 | 384 | // From here on, the fields may vary depending on the Frame Type (MANAGEMENT, CONTROL, DATA). 385 | if (frame.frameControl.type === CapFile.WlanFrame.Types.CONTROL) { 386 | // No other relevant data to parse. 387 | } else { 388 | // Capture similarities between Management and Data frames. 389 | frame.addr2 = this.getHex(10, 16); 390 | frame.addr3 = this.getHex(16, 22); 391 | var fragSeq = this.getInt(22, 24); 392 | frame.fragmentNumber = (fragSeq >>> 0) & 0b1111; 393 | frame.sequenceNumber = (fragSeq >>> 4) & 0b111111111111; 394 | 395 | var toDS = frame.frameControl.flags.toDS, 396 | fromDS = frame.frameControl.flags.fromDS; 397 | if (toDS && !fromDS) { 398 | frame._bssid = frame.addr1; 399 | frame._station = frame.addr2; 400 | } 401 | else if (!toDS && fromDS) { 402 | frame._bssid = frame.addr2; 403 | frame._station = frame.addr1; 404 | } 405 | else if (!toDS && !fromDS) { 406 | frame._bssid = frame.addr3; 407 | frame._station = undefined; 408 | } 409 | else if (toDS && fromDS) { 410 | // No idea 411 | frame._bssid = undefined; 412 | frame._station = undefined; 413 | } 414 | 415 | // Skip to just past the sequence number. 416 | this.byteOffset_ += 24; 417 | 418 | if (frame.frameControl.type === CapFile.WlanFrame.Types.MANAGEMENT) { 419 | // Parse frame in context of a Management Frame. 420 | CapFile.WlanFrame.Management.call(this, frame, endOfPacketOffset); 421 | } 422 | else if (frame.frameControl.type === CapFile.WlanFrame.Types.DATA) { 423 | // Parse frame in context of a Data frame. 424 | CapFile.WlanFrame.Data.call(this, frame, endOfPacketOffset); 425 | } 426 | } 427 | 428 | // Shift to end of frame. 429 | this.byteOffset_ = endOfPacketOffset; 430 | 431 | return frame; 432 | }; 433 | 434 | CapFile.WlanFrame.Types = { 435 | MANAGEMENT: 0, 436 | CONTROL: 1, 437 | DATA: 2 438 | }; 439 | 440 | /** 441 | * Adds any additional information about this Management packet to the given frame. 442 | * Only a subset of Management frames are supported. 443 | * 444 | * Focus is on getting SSID from the Management frames' "tagged parameters". 445 | * Other tagged parameters are read (and stored as Hex) but are not parsed. 446 | * 447 | * Requires reference to "this" CapFile object -- call using CapFile.WlanFrame.Management.call(this, ...); 448 | * 449 | * Increments CapFile.byteOffset_ to end of the Management frame. 450 | * 451 | * @param frame (object) Reference to the currently-parsed frame. 452 | * @param endOfPacketOffset (int) The Offset, in relation to byteOffset_, in which this packet ends. 453 | */ 454 | CapFile.WlanFrame.Management = function(frame, endOfPacketOffset) { 455 | // Management frames contain: 456 | // 1. Fixed Parameters (variable length, depends on frameControl.subtype). 457 | // 2. Tagged Parameters (variable length, defined in 'length' bytes). 458 | 459 | var fixedParamLength; 460 | if (frame.frameControl.subtype === 0) { 461 | frame.name = "Association Request"; 462 | fixedParamLength = 4; 463 | } 464 | else if (frame.frameControl.subtype === 1) { 465 | frame.name = "Association Response"; 466 | fixedParamLength = 6; 467 | } 468 | else if (frame.frameControl.subtype === 8) { 469 | frame.name = "Beacon"; 470 | fixedParamLength = 12; 471 | } 472 | else if (frame.frameControl.subtype === 5) { 473 | frame.name = "Probe Response"; 474 | fixedParamLength = 12; 475 | } 476 | else if (frame.frameControl.subtype === 11) { 477 | frame.name = "Authentication"; 478 | fixedParamLength = 6; 479 | } 480 | else if (frame.frameControl.subtype === 12) { 481 | frame.name = "Deauthentication"; 482 | fixedParamLength = 2; 483 | } 484 | else if (frame.frameControl.subtype === 13) { 485 | frame.name = "Action"; 486 | fixedParamLength = 9; 487 | } 488 | 489 | if (!fixedParamLength) { 490 | // Unable to parse tagged parameters without knowing fixed parameter length. 491 | frame.name = "Unknown Management Frame subtype (" + frame.frameControl.subtype + ")"; 492 | return; 493 | } 494 | 495 | // TODO: Parse fixed parameters. Skipping for now. 496 | this.byteOffset_ += fixedParamLength; 497 | 498 | 499 | // Parse tagged parameters. 500 | frame.taggedParameters = {}; 501 | while (this.byteOffset_ < endOfPacketOffset) { 502 | var tag = {}; 503 | var tagIndex = this.getInt(0, 1); 504 | var tagLength = this.getInt(1, 2); 505 | if (tagIndex === 0) { 506 | // SSID 507 | var tagData = this.getBytes(2, 2 + tagLength); 508 | frame.taggedParameters[tagIndex] = { 509 | name: "SSID", 510 | length: tagLength, 511 | data: tagData 512 | }; 513 | frame.description = "SSID: " + tagData; 514 | } 515 | else { 516 | // Don't care about other tags. 517 | var tagData = this.getHex(2, 2 + tagLength); 518 | frame.taggedParameters[tagIndex] = { 519 | name: "N/A", 520 | length: tagLength, 521 | data: tagData 522 | }; 523 | } 524 | // Shift to next tagged paramter (or end of packet). 525 | this.byteOffset_ += tagLength + 2; 526 | } 527 | }; 528 | 529 | /** 530 | * Adds any additional information about this Data frame to the given frame. 531 | * Only a small subset of Data frames are supported. 532 | * 533 | * Focus is on getting EAPOL (WPA handshake-related) information. 534 | * 535 | * Requires reference to "this" CapFile object -- call using CapFile.WlanFrame.Data.call(this, ...); 536 | * 537 | * Increments CapFile.byteOffset_ to end of the Data frame (if known). 538 | * Otherwise does not change CapFile.byteOffset_ 539 | * 540 | * @param frame (object) Reference to the currently-parsed frame. 541 | * @param endOfPacketOffset (int) The Offset, in relation to byteOffset_, in which this packet ends. 542 | */ 543 | CapFile.WlanFrame.Data = function(frame, endOfPacketOffset) { 544 | if ((frame.frameControl.subtype & 0b111) !== 0) { 545 | // Only support EAPOL (and EAPOL+QoS) packets. 546 | frame.name = "Unknown Data Frame subtype (" + frame.frameControl.subtype + ")"; 547 | return; 548 | } 549 | 550 | if (frame.frameControl.flags.toDS && frame.frameControl.flags.fromDS) { 551 | // toDS and fromDS are set, expect addr4 552 | frame.addr4 = this.getHex(0, 6); 553 | this.byteOffset_ += 6; 554 | } 555 | 556 | if ((frame.frameControl.subtype & 0b1000) === 8) { 557 | // QoS flag is set. Expect QoS control field. 558 | frame.qosControl = this.getHex(0, 2); 559 | this.byteOffset_ += 2; 560 | frame.name = "EAPOL (QoS)"; 561 | } 562 | else { 563 | frame.name = "EAPOL"; 564 | } 565 | 566 | if (frame.frameControl.flags.order) { 567 | // Expect (and skip) HT Control field. 568 | this.byteOffset_ += 4; 569 | } 570 | 571 | // Skip Logical-Link Control bytes 572 | this.byteOffset_ += 8; 573 | 574 | frame.bytes = this.getHex(0, endOfPacketOffset - this.byteOffset_); 575 | 576 | // Parse Data frame body -- expect 802.1x auth packet. 577 | var authVersion = this.getInt(0, 1); 578 | var authType = this.getInt(1, 2); 579 | frame.auth = { 580 | version: authVersion, // 1=802.1X-2001 581 | type: authType, // 3=Key 582 | authLength: this.getInt(2, 4, true, false), 583 | keyDescriptorType: this.getInt(4, 5), // 2=EAPOL RSN Key 584 | keyInfo: this.getInt(5, 7, true, false), 585 | keyLength: this.getInt(7, 9, true, false), 586 | replayCounter: this.getInt(9, 17, true, false), 587 | keyNonce: this.getHex(17, 49), 588 | keyIV: this.getHex(49, 65), 589 | keyRSC: this.getHex(65, 73), 590 | keyID: this.getHex(73, 81), 591 | keyMIC: this.getHex(81, 97), 592 | keyDataLength: this.getInt(97, 99, true, false) 593 | }; 594 | frame.auth.keyInfoFlags = { 595 | keyDescriptorVersion: (frame.auth.keyInfo >>> 0) & 0b111, // 2=AES Cipher, HMAC-SHA1 MIC 596 | keyType: (frame.auth.keyInfo >>> 3) & 0b1, // 1=Pairwise Key 597 | keyIndex: (frame.auth.keyInfo >>> 4) & 0b11, 598 | install: !!((frame.auth.keyInfo >>> 6) & 0b1 ), 599 | ack: !!((frame.auth.keyInfo >>> 7) & 0b1 ), 600 | mic: !!((frame.auth.keyInfo >>> 8) & 0b1 ), 601 | secure: !!((frame.auth.keyInfo >>> 9) & 0b1 ), 602 | error: !!((frame.auth.keyInfo >>> 10) & 0b1 ), 603 | request: !!((frame.auth.keyInfo >>> 11) & 0b1 ), 604 | encrypted: !!((frame.auth.keyInfo >>> 12) & 0b1 ) 605 | }; 606 | this.byteOffset_ += 99; 607 | 608 | if (frame.auth.keyDataLength > 0) { 609 | frame.auth.keyData = this.getHex(0, frame.auth.keyDataLength); 610 | this.byteOffset_ += frame.auth.keyDataLength; 611 | } 612 | 613 | // Set handshake number 614 | if (frame.frameControl.flags.fromDS) { 615 | // Either 1 or 3 616 | if (frame.auth.keyInfoFlags.mic) { 617 | frame.description = "Handshake (3 of 4)"; 618 | } else { 619 | frame.description = "Handshake (1 of 4)"; 620 | } 621 | } else { 622 | if (frame.auth.keyInfoFlags.secure) { 623 | frame.description = "Handshake (4 of 4)"; 624 | } else { 625 | frame.description = "Handshake (2 of 4)"; 626 | } 627 | } 628 | 629 | }; 630 | 631 | /** 632 | * Identify 4-way handshake(s), extract information required for calculating PMK. 633 | */ 634 | CapFile.prototype.extractPmkFields = function(givenSsid) { 635 | // Look for SSID name in previous beacons/auth packets 636 | 637 | var bssid_to_ssid = {}; 638 | var i, frame; 639 | for (i = 0; i < this.packetFrames.length; i++) { 640 | frame = this.packetFrames[i]; 641 | if (!frame.hasOwnProperty("taggedParameters") 642 | || !frame.hasOwnProperty("_bssid")) { 643 | continue; 644 | } 645 | var tags = frame.taggedParameters; 646 | if (!tags.hasOwnProperty("0")) { 647 | continue; 648 | } 649 | var tag = tags["0"]; 650 | if (!tag.hasOwnProperty("name") || tag.name !== "SSID") { 651 | continue; 652 | } 653 | if (!givenSsid || tag.data === givenSsid) { 654 | bssid_to_ssid[frame._bssid] = tag.data; 655 | } 656 | else if (CapFile.debug) { 657 | CapFile.debug("Ignoring discovered SSID <" + tag.data + "> because it does not match given SSID <" + givenSSID + ">"); 658 | } 659 | } 660 | 661 | var handshakes = []; 662 | 663 | // Iterate over all known BSSIDs 664 | var bssids = [], ssid; 665 | for (var bssid in bssid_to_ssid) { 666 | if (!bssid_to_ssid.hasOwnProperty(bssid)) { 667 | continue; 668 | } 669 | bssids.push(bssid); 670 | ssid = bssid_to_ssid[bssid]; 671 | 672 | // Look for last 3 frames of handshake 673 | if (CapFile.debug) { 674 | CapFile.debug("Looking for handshake for bssid: " + bssid + ", ssid: " + ssid); 675 | } 676 | 677 | var fc, mic, ack, install, dataLength; 678 | var hsSrcAddress, hsDstAddress, snonce, anonce, hsKeyLength, hsReplayCounter, hsMic, hsKeyDescriptorVersion; 679 | for (i = 0; i < this.packetFrames.length; i++) { 680 | frame = this.packetFrames[i]; 681 | 682 | // Filter by packet type. 683 | fc = frame.frameControl; 684 | if (fc.type !== 2 || (fc.subtype !== 0 && fc.subtype !== 8)) { 685 | // Not an EAPOL WPA data frame, skip. 686 | continue; 687 | } 688 | 689 | // Filter for the BSSID we're looking for. 690 | if (frame._bssid !== bssid) { 691 | if (CapFile.debug) { 692 | CapFile.debug("Skipping frame #" + i + ": BSSID: " + frame._bssid + " is not from " + bssid); 693 | } 694 | continue; 695 | } 696 | 697 | // Store fields used in all handshakes. 698 | mic = frame.auth.keyInfoFlags.mic; 699 | ack = frame.auth.keyInfoFlags.ack; 700 | install = frame.auth.keyInfoFlags.install; 701 | dataLength = frame.auth.keyDataLength; 702 | 703 | /* Handshake (2 of 4): 704 | * mic:true 705 | * ack:false 706 | * install:false 707 | * keyDataLength > 0) 708 | * 709 | * Extract: 710 | * - keynonce (SNonce , the nonce from STATION) 711 | */ 712 | if (mic && !ack && !install && dataLength > 0) { 713 | // Reset variables from Handshakes #3 and #4 714 | hsSrcAddress = hsDstAddress = anonce = hsKeyLength = hsReplayCounter = hsMic = hsKeyDescriptorVersion = undefined; 715 | 716 | // Extract SNonce 717 | snonce = frame.auth.keyNonce; 718 | if (CapFile.debug) { 719 | CapFile.debug("Handshake (2 of 4): Found SNonce: " + snonce); 720 | } 721 | continue; 722 | } 723 | 724 | /* Handshake (3 of 4): 725 | * mic:true 726 | * ack:true 727 | * install:true 728 | * 729 | * Extract: 730 | * - src_address (STATION) 731 | * - dst_address (AP) 732 | * - ANonce (from AP) 733 | * - replay_counter (for Handshake 4 of 4) 734 | */ 735 | if (mic && ack && install) { 736 | if (!snonce) { 737 | // Require Handshake #2 738 | continue; 739 | } 740 | 741 | // Reset variables from Handshake #4 742 | hsMic = hsKeyDescriptorVersion = undefined; 743 | 744 | // Extract variables 745 | hsSrcAddress = frame._station; 746 | hsDstAddress = frame._bssid; 747 | anonce = frame.auth.keyNonce; 748 | hsReplayCounter = frame.auth.replayCounter; 749 | hsKeyLength = frame.auth.keyLength; 750 | if (CapFile.debug) { 751 | CapFile.debug("Handshake (3 of 4): src: " + hsSrcAddress + 752 | ", dst: " + hsDstAddress + 753 | ", ANonce: " + anonce + 754 | ", counter: " + hsReplayCounter); 755 | } 756 | continue; 757 | } 758 | 759 | /* Handshake (4 of 4): 760 | * mic:true 761 | * ack:false 762 | * install:false 763 | * replay_couner: 764 | * (And/Or) key_data_length == 0 (data === undefined) 765 | * 766 | * Extract: 767 | * - MIC 768 | * - "EAPOL frame" 769 | */ 770 | if (mic && !ack && !install 771 | && hsReplayCounter && hsReplayCounter === frame.auth.replayCounter 772 | && dataLength === 0) { 773 | if (!anonce) { 774 | // Require handshake #3. 775 | continue; 776 | } 777 | 778 | hsMic = frame.auth.keyMIC; 779 | hsKeyDescriptorVersion = frame.auth.keyInfoFlags.keyDescriptorVersion; 780 | var eapolFrameBytes = frame.bytes; 781 | eapolFrameBytes = eapolFrameBytes.substring(0, eapolFrameBytes.length - 36); 782 | for (var j = 0; j < 36; j++) { 783 | eapolFrameBytes += "0"; 784 | } 785 | if (CapFile.debug) { 786 | CapFile.debug("Handshake (4 of 4): MIC: " + hsMic + ", eapolFrameBytes: " + eapolFrameBytes); 787 | } 788 | 789 | handshakes.push({ 790 | ssid: ssid, 791 | bssid: bssid, 792 | snonce: snonce, 793 | anonce: anonce, 794 | srcAddress: hsSrcAddress, 795 | dstAddress: hsDstAddress, 796 | keyLength: hsKeyLength, 797 | mic: hsMic, 798 | eapolFrameBytes: eapolFrameBytes, 799 | keyDescriptorVersion: hsKeyDescriptorVersion 800 | }); 801 | continue; 802 | } 803 | } 804 | } 805 | 806 | if (bssids.length === 0) { 807 | throw Error("No SSIDs found"); 808 | } 809 | 810 | if (handshakes.length === 0) { 811 | throw Error("No handshakes found"); 812 | } 813 | 814 | // TODO: Return all handshakes? Or just the first one? 815 | if (CapFile.debug) { 816 | CapFile.debug("Captured " + handshakes.length + " 4-way handshakes: " + JSON.stringify(handshakes)); 817 | CapFile.debug("Using first 4-way handshake captured: " + JSON.stringify(handshakes[0])); 818 | } 819 | return handshakes[0]; 820 | 821 | }; 822 | 823 | -------------------------------------------------------------------------------- /Crack.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enables cracking of handshake information (See CapFile.extractPmKFields). 3 | * 4 | * Uses Crypto-JS for HMAC-SHA1 and PBKDF2: https://github.com/brix/crypto-js 5 | * 6 | * References / Knowledge Sharing: 7 | * 8 | * http://security.stackexchange.com/questions/66008/how-exactly-does-4-way-handshake-cracking-work 9 | * - Explains how to Handshake data relates to PMK, PTK, KCK, MIC. 10 | * 11 | * https://hashcat.net/forum/thread-1745-post-9937.html#pid9937 12 | * - Extremely-detailed info on calculating PMK, PTK, and KCK. 13 | * 14 | * https://github.com/roobixx/cowpatty/blob/master/cowpatty.c 15 | * - Implementation of WPA handshake cracking in C. 16 | * - Specificaly https://github.com/roobixx/cowpatty/blob/master/cowpatty.c#L274 17 | * 18 | * http://stackoverflow.com/questions/18298387/pbkdf2-result-is-different-in-cryptojs-and-ios 19 | * - Explains that inputs to CryptoJS must be CryptoJS-encoded binary objects (with 'words' and 'sigDigits'). 20 | * 21 | * http://stackoverflow.com/questions/12018920/wpa-handshake-with-python-hashing-difficulties 22 | * - Python-implementation of handshake cracking. 23 | * - Notes that the inputs to the PRF should be *bytes*, not ascii hex characters. 24 | * 25 | * http://sid.rstack.org/pres/0810_BACon_WPA2_en.pdf 26 | * - More reading on weaknesses in WPA, possible ways to speed up cracking, etc. 27 | * 28 | * https://pyrit.wordpress.com/2011/04/16/known-plaintext-attack-against-ccmp/ 29 | * - Possible 'speedier way' to crack CCMP based on first 6 bytes of result. 30 | * - Found at http://security.stackexchange.com/a/6617 31 | */ 32 | 33 | /** 34 | * Algorithm: 35 | * 36 | * Construct PMK using 37 | * - passphrase (from user input or list) and 38 | * - SSID (from user input or beacon frame). 39 | * pmk = pbkdf2_sha1(passphrase, ssid, 4096, 256) 40 | * 41 | * Construct PTK using 42 | * - PMK (step 1) 43 | * - AP bssid, STATION bssid, ANonce, SNonce (from Handshake 3 of 4) 44 | * ... (wpa_pmk_to_ptk) 45 | * 46 | * Construct MIC we expect to see in 4-of-4 using 47 | * - PTK (Step 2) 48 | * - EAPOL Frame (Handshake 4 of 4) 49 | */ 50 | 51 | /** 52 | * Initializes WPA cracker instance. 53 | * @param handshake (object) Data from 4-way handshake (see CapFile.extractPmkFields) 54 | * @param debug (boolean or function) If 'true': Dumps debug information to console. 55 | * If 'false': Does not dump anything to console. 56 | * If given a function, calls function with debug text. 57 | */ 58 | function Crack(handshake, debug) { 59 | if (debug) { 60 | if (typeof debug === "boolean") { 61 | // Default debug function 62 | Crack.debug = function(txt) { 63 | console.log("[Crack.js] " + txt); 64 | } 65 | } 66 | else if (typeof debug === "function") { 67 | Crack.debug = debug; 68 | } 69 | else { 70 | throw Error("Unexpected type of 'debug' option: " + (typeof debug)); 71 | } 72 | } 73 | this.pbkdf2ConfigForPmk = { 74 | keySize: 64/8, 75 | iterations: 4096 76 | }; 77 | this.handshake = handshake; 78 | this.prfPrefix = this.getPrfPrefix(); 79 | } 80 | 81 | /** 82 | * Convert given string to hexadecimal characters. 83 | * @param s (string) Text to convert to hex. 84 | * @return (string) Hexadecimal-representation of "s". 85 | */ 86 | Crack.stringToHex = function(s) { 87 | var result = "", i, x; 88 | for (i = 0; i < s.length; i++) { 89 | x = s.charCodeAt(i).toString(16); 90 | while (x.length < 2) { 91 | x = "0" + x; 92 | } 93 | result += x; 94 | } 95 | return result; 96 | }; 97 | 98 | /** 99 | * Compute prefix of one of the inputs to Pseudo-Random Function (PRF, see Crack.kckFromPmk). 100 | * The "prefix" contains the "Pairwise Key Expansion", addresses, and nonces. 101 | * @return (string, hex) PRF prefix. 102 | */ 103 | Crack.prototype.getPrfPrefix = function() { 104 | var prefix = ""; 105 | prefix = Crack.stringToHex("Pairwise key expansion"); 106 | prefix += "00"; 107 | if (this.handshake.srcAddress < this.handshake.dstAddress) { 108 | prefix += this.handshake.srcAddress; 109 | prefix += this.handshake.dstAddress; 110 | } else { 111 | prefix += this.handshake.dstAddress; 112 | prefix += this.handshake.srcAddress; 113 | } 114 | if (this.handshake.snonce < this.handshake.anonce) { 115 | prefix += this.handshake.snonce; 116 | prefix += this.handshake.anonce; 117 | } else { 118 | prefix += this.handshake.anonce; 119 | prefix += this.handshake.snonce; 120 | } 121 | return prefix; 122 | }; 123 | 124 | /** 125 | * Calculates Pairwise Master Key (PMK). 126 | * Uses PBKDF2 which may take a while... 127 | * 128 | * @param key (string) The plaintext key/password (PSK). 129 | * @param ssid (string, optional) SSID (name of Wireless Access Point). Uses SSID from CapFile if not given. 130 | * @return (CryptoJS-encoded object) The PMK (256bits/32Bytes). 131 | */ 132 | Crack.prototype.pmk = function(key, ssid) { 133 | // Tribble 134 | //return CryptoJS.enc.Hex.parse("273c545d3be7e3fd4510fb5509486ba77f32c39716c4d63bf86de6b808387a77"); 135 | 136 | // Netgear 2/158 137 | //return CryptoJS.enc.Hex.parse("01b809f9ab2fb5dc47984f52fb2d112e13d84ccb6b86d4a7193ec5299f851c48"); 138 | 139 | if (Crack.debug) { 140 | Crack.debug("Constructing PMK using PDKDF2(psk:" + key + ", ssid:" + this.handshake.ssid + ")..."); 141 | } 142 | 143 | var pmk = CryptoJS.PBKDF2(key, ssid || this.handshake.ssid, this.pbkdf2ConfigForPmk); 144 | 145 | if (Crack.debug) { 146 | Crack.debug("PMK (Pairwise Master Key): " + pmk.toString()); 147 | } 148 | 149 | return pmk; 150 | }; 151 | 152 | 153 | /** 154 | * Psudo-Random Function to calculate KCK (Key-Confirmation Key) from the PTK (Pairwise Transient Key). 155 | * Computes part of PTK using the given PMK. 156 | * 157 | * Uses "prfPrefix" calculated in Crack.getPrfPrefix(). 158 | * 159 | * @param pmk (CryptoJS-encoded object) The PMK, calculated from Crack.pmk() 160 | * @return (string, hex) The KCK (first 16 bytes of the PTK). 161 | * 162 | */ 163 | Crack.prototype.kckFromPmk = function(pmk) { 164 | if (Crack.debug) { 165 | Crack.debug("Constructing KCK using handshake values, Hmac-SHA1, and the PMK..."); 166 | } 167 | 168 | // Pseudo-Random function based on http://crypto.stackexchange.com/a/33192 169 | var i = 0, ptk = "", thisPrefix; 170 | while (i < (64 * 8 + 159) / 160) { 171 | // Append the current iteration counter as a (hex) byte to the prefix. 172 | thisPrefix = this.prfPrefix + ("0" + i); 173 | 174 | thisPrefix = CryptoJS.enc.Hex.parse(thisPrefix); 175 | ptk += CryptoJS.HmacSHA1(thisPrefix, pmk).toString(); 176 | 177 | i++; 178 | } 179 | 180 | // Extract first 16 bytes (32 hex characters) of PTK to get KCK. 181 | var kck = ptk.substring(0, 32); 182 | 183 | if (Crack.debug) { 184 | Crack.debug("KCK (Key-Confirmation Key) : " + kck); 185 | } 186 | 187 | return kck; 188 | }; 189 | 190 | /** 191 | * Calculate MIC using KCK (given) and EAPOL frame bytes (from a message in the 4-way handshake). 192 | * 193 | * @param kck (string, hex) The Key-ConfirmationKey (KCK) computed from Crack.kckFromPmk(). 194 | * @return (string, hex) The expected MIC. 195 | */ 196 | Crack.prototype.micFromKck = function(kck) { 197 | kck = CryptoJS.enc.Hex.parse(kck); 198 | 199 | // NOTE: We expect the "MIC" portion of the EAPOL frame bytes to be *zeroed* out! From the 802.11 spec: 200 | // MIC(KCK, EAPOL) – MIC computed over the body of this EAPOL-Key frame with the Key MIC field first initialized to 0 201 | var bytes = CryptoJS.enc.Hex.parse(this.handshake.eapolFrameBytes); 202 | if (Crack.debug) { 203 | Crack.debug("EAPOL packet frame bytes: " + bytes.toString()); 204 | } 205 | 206 | var computedMic; 207 | if (this.handshake.keyDescriptorVersion === 1) { 208 | if (Crack.debug) { 209 | Crack.debug("Using Hmac-MD5 for computing WPA MIC..."); 210 | } 211 | computedMic = CryptoJS.HmacMD5(bytes, kck).toString(); 212 | } 213 | else if (this.handshake.keyDescriptorVersion === 2) { 214 | if (Crack.debug) { 215 | Crack.debug("Using Hmac-SHA1 for computing WPA2 MIC"); 216 | } 217 | computedMic = CryptoJS.HmacSHA1(bytes, kck).toString(); 218 | 219 | // Extract 0-128 MSB per the 802.11 spec. 220 | computedMic = computedMic.substring(0, 32); 221 | } 222 | else { 223 | throw Error("Unknown key descriptor version: " + this.handshake.keyDescriptorVersion + ", expecting '1' or '2'"); 224 | } 225 | 226 | if (Crack.debug) { 227 | Crack.debug("Computed Mic (based on PMK & KCK): " + computedMic); 228 | Crack.debug("Expected Mic (from Handshake packet): " + this.handshake.mic); 229 | } 230 | 231 | return computedMic; 232 | } 233 | 234 | Crack.prototype.tryPSK = function(psk) { 235 | var pmk = this.pmk(psk); 236 | var kck = this.kckFromPmk(pmk); 237 | var computedMic = this.micFromKck(kck); 238 | return (computedMic === this.handshake.mic); 239 | }; 240 | 241 | /** 242 | * Asserts cracking method for WPA (TKIP) works. 243 | */ 244 | Crack.test_WPA1 = function(debug) { 245 | var handshake = { 246 | ssid: "Netgear 2/158", 247 | bssid: "001e2ae0bdd0", 248 | snonce: "60eff10088077f8b03a0e2fc2fc37e1fe1f30f9f7cfbcfb2826f26f3379c4318", 249 | anonce: "61c9a3f5cdcdf5fae5fd760836b8008c863aa2317022c7a202434554fb38452b", 250 | srcAddress: "001e2ae0bdd0", 251 | dstAddress: "cc08e0620bc8", 252 | mic: "45282522bc6707d6a70a0317a3ed48f0", 253 | keyLength: 32, 254 | keyDescriptorVersion: 1, // WPA 255 | eapolFrameBytes: "0103005ffe01090020000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" 256 | // PSK is 10zZz10ZZzZ 257 | }; 258 | 259 | var c = new Crack(handshake, debug); 260 | 261 | // Assert PMK is accurate. 262 | var pmk = c.pmk("10zZz10ZZzZ"); 263 | if (pmk.toString() !== "01b809f9ab2fb5dc47984f52fb2d112e13d84ccb6b86d4a7193ec5299f851c48") { 264 | throw Error("Expected PMK does not match. Got " + pmk.toString()); 265 | } 266 | 267 | // Assert KCK is accurate. 268 | var kck = c.kckFromPmk(pmk); 269 | if (kck !== "bf49a95f0494f44427162f38696ef8b6") { 270 | throw Error("Expected KCK does not match. Got " + kck.toString()); 271 | } 272 | 273 | var mic = c.micFromKck(kck); 274 | if (mic !== "45282522bc6707d6a70a0317a3ed48f0") { 275 | throw Error("Expected MIC does not match. Got " + mic); 276 | } 277 | Crack.debug("Crack.test_WPA1 passed."); 278 | }; 279 | 280 | /** 281 | * Asserts cracking method for WPA2 (CCMP) works. 282 | */ 283 | Crack.test_WPA2 = function(debug) { 284 | var handshake = { 285 | ssid: "Tribble", 286 | bssid: "002275ecf9c9", 287 | snonce: "da12c942e9dfcbe67068438f87cd4ce49b253e3c7347bacc8f9aa4ab310e6e9f", 288 | anonce: "f5f5cd2ca691efe420224f466d3eb1633ef368ac93de64079ef4d9ca8129fa1b", 289 | srcAddress: "f4ce46629c64", 290 | dstAddress: "002275ecf9c9", 291 | replayCounter: 2925, 292 | keyLength: 16, 293 | mic: "646debf34b677fbfd78c5724dc9ea442", 294 | keyDescriptorVersion: 2, // WPA2 295 | eapolFrameBytes: "0103005f02030a00000000000000000b6dda12c942e9dfcbe67068438f87cd4ce49b253e3c7347bacc8f9aa4ab310e6e9f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" 296 | // PSK is dandelion 297 | }; 298 | 299 | var c = new Crack(handshake, debug); 300 | 301 | // Assert PMK is accurate. 302 | var pmk = c.pmk("dandelion"); 303 | if (pmk.toString() !== "273c545d3be7e3fd4510fb5509486ba77f32c39716c4d63bf86de6b808387a77") { 304 | throw Error("Expected PMK does not match. Got " + pmk.toString()); 305 | } 306 | 307 | // Assert KCK is accurate. 308 | var kck = c.kckFromPmk(pmk); 309 | //if (kck !== "dc9471429e3918be1eff0f742450d0cd") { 310 | if (kck !== "dc9471429e3918be1eff0f742450d0cd") { 311 | throw Error("Expected KCK does not match. Got " + kck.toString()); 312 | } 313 | 314 | var mic = c.micFromKck(kck); 315 | if (mic !== handshake.mic) { 316 | throw Error("Expected MIC does not match. Got " + mic); 317 | } 318 | 319 | Crack.debug("Crack.test_WPA2 passed."); 320 | }; 321 | 322 | -------------------------------------------------------------------------------- /Main.js: -------------------------------------------------------------------------------- 1 | // Debug 2 | var textArea = document.querySelector("#debug"); 3 | textArea.value = ""; 4 | 5 | function debug(txt) { 6 | if (textArea.value !== '') { 7 | textArea.value += "\n"; 8 | } 9 | textArea.value += txt; 10 | textArea.scrollTop = textArea.scrollHeight; 11 | } 12 | 13 | 14 | // Cap file viewer 15 | function prettyTime(date) { 16 | var result = "", temp; 17 | 18 | result += date.getFullYear(); 19 | result += "-"; 20 | 21 | temp = (date.getMonth() + 1).toString(); 22 | while (temp.length < 2) temp = "0" + temp; 23 | result += temp; 24 | result += "-"; 25 | 26 | temp = date.getDate().toString(); 27 | while (temp.length < 2) temp = "0" + temp; 28 | result += temp; 29 | 30 | result += "T"; 31 | 32 | temp = date.getHours().toString(); 33 | while (temp.length < 2) temp = "0" + temp; 34 | result += temp; 35 | 36 | result += ":"; 37 | 38 | temp = date.getMinutes().toString(); 39 | while (temp.length < 2) temp = "0" + temp; 40 | result += temp; 41 | 42 | result += ":"; 43 | 44 | temp = date.getSeconds().toString(); 45 | while (temp.length < 2) temp = "0" + temp; 46 | result += temp; 47 | 48 | result += "."; 49 | 50 | temp = date.getMilliseconds().toString(); 51 | while (temp.length < 3) temp = "0" + temp; 52 | result += temp; 53 | 54 | return result; 55 | } 56 | 57 | function prettyMac(mac) { 58 | var result = "", i; 59 | for (i = 0; i < mac.length; i += 2) { 60 | if (result !== "") { 61 | result = result + ":"; 62 | } 63 | result += mac.substring(i, i + 2).toUpperCase(); 64 | } 65 | return result; 66 | } 67 | 68 | function prettyFrame(frame) { 69 | var tr = document.createElement("tr"); 70 | var td = document.createElement("td"); 71 | td.className = "prettyFrame"; 72 | td.colSpan = 8; 73 | var pre = document.createElement("pre"); 74 | pre.className = "payload"; 75 | pre.textContent = JSON.stringify(frame, null, 2); 76 | td.appendChild(pre); 77 | tr.appendChild(td); 78 | return tr; 79 | } 80 | 81 | function prettyClick(row, frame) { 82 | row.addEventListener('click', function() { 83 | if (this.className === "collapsed") { 84 | this.querySelector(".expando").textContent = "-"; 85 | this.className = "expanded"; 86 | this.parentNode.insertBefore(prettyFrame(frame), this.nextSibling); 87 | this.scrollIntoView(); 88 | } else if (this.className === "expanded") { 89 | this.querySelector(".expando").textContent = "+"; 90 | document.querySelector(".prettyFrame").remove(); 91 | this.className = "collapsed"; 92 | } 93 | }, false); 94 | } 95 | 96 | function loadCapfile(capfile) { 97 | var tbody = document.querySelector("#capfileBody"); 98 | for (var i = 0; i < capfile.packetFrames.length; i++) { 99 | var frame = capfile.packetFrames[i]; 100 | var tr = document.createElement("tr"); 101 | tr.className = "collapsed"; 102 | var td; 103 | 104 | td = document.createElement("td"); 105 | td.className = "expando"; 106 | td.textContent = "+"; 107 | tr.appendChild(td); 108 | 109 | // Frame # 110 | td = document.createElement("td"); 111 | td.className = "index"; 112 | td.textContent = (i + 1); 113 | tr.appendChild(td); 114 | 115 | // Timestamp 116 | td = document.createElement("td"); 117 | td.textContent = prettyTime(frame.header.timestamp); 118 | tr.appendChild(td); 119 | 120 | // Source Address 121 | td = document.createElement("td"); 122 | td.textContent = prettyMac(frame.addr2); 123 | tr.appendChild(td); 124 | 125 | // Destination Address 126 | td = document.createElement("td"); 127 | td.textContent = prettyMac(frame.addr1); 128 | tr.appendChild(td); 129 | 130 | // Size 131 | td = document.createElement("td"); 132 | td.textContent = frame.header.length; 133 | tr.appendChild(td); 134 | 135 | // Type 136 | td = document.createElement("td"); 137 | var name = frame.name; 138 | if (frame.description) { 139 | frame.name += " - " + frame.description; 140 | } 141 | td.textContent = frame.name; 142 | tr.appendChild(td); 143 | 144 | prettyClick(tr, frame); 145 | tbody.appendChild(tr); 146 | } 147 | } 148 | 149 | // FileChooser 150 | document.querySelector('#fileChooser').addEventListener('change', function() { 151 | var reader = new FileReader(); 152 | reader.onload = function(){ 153 | var capfile = new CapFile(this.result, debug); 154 | loadCapfile(capfile); 155 | //document.querySelector('#result').innerHTML = JSON.stringify(capfile, null, 2); 156 | } 157 | reader.readAsBinaryString(this.files[0]); 158 | }, false); 159 | 160 | // Test buttons 161 | document.querySelector("#test1").addEventListener('click', function() { 162 | Crack.test_WPA1(debug); 163 | }, false); 164 | document.querySelector("#test2").addEventListener('click', function() { 165 | Crack.test_WPA2(debug); 166 | }, false); 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CapFile.js & Crack.js 2 | WLAN Pcap parser & WPA/2 PSK validator in Javascript 3 | 4 | ## CapFile.js 5 | 6 | Converts raw `.cap` or `.pcap` file bytes into structured Javascript objects. 7 | 8 | Only supports: 9 | 10 | 1. WLAN packet captures, 11 | 2. Parsing of certain frame types (Probe, Auth, EAPOL) -- other frame types are ignored. 12 | 13 | ```javascript 14 | // Get rawBytes from *somewhere*, e.g. "file" input element, external address, etc. 15 | 16 | var capfile = new CapFile(rawBytes); 17 | >>> '[CapFile.js] Using Little-Endian byte-encoding due to magic number: 2712847316' 18 | >>> '[CapFile.js] GlobalHeader (24 bytes):' 19 | >>> '[CapFile.js] d4 c3 b2 a1 02 00 04 00 00 00 00 00 00 00 00 00' 20 | >>> '[CapFile.js] ff 7f 00 00 69 00 00 00' 21 | >>> '[CapFile.js] Parsed 20 known frames out of 20 total frames.' 22 | 23 | // Metadata about .cap file 24 | capfile.globalHeader 25 | >>> Object { 26 | >>> version: "2.4", 27 | >>> gmtOffset: 0, 28 | >>> sigFigs: 0, 29 | >>> snapshotLength: 32767, 30 | >>> headerType: 105 31 | >>> } 32 | 33 | capfile.packetFrames.length 34 | >>> 20 35 | 36 | // First example packet is a "Beacon Response" containing the SSID of the Access Point. 37 | JSON.stringify(capfile.packetFrames[0], null, 2); 38 | { 39 | "header": { 40 | "timestamp": "2011-01-16T21:28:30.057Z", 41 | "length": 435, 42 | ... 43 | }, 44 | "frameControl": { 45 | "type": 0, 46 | "subtype": 5, 47 | ... 48 | }, 49 | "name": "Probe Response - SSID: NETGEAR2815", 50 | "_bssid": "002275ecf9c9", 51 | ... 52 | "taggedParameters": { 53 | "0": { 54 | "name": "SSID", 55 | "length": 7, 56 | "data": "NETGEAR2815" 57 | }, 58 | ... 59 | }, 60 | } 61 | 62 | // Last example packet is the 4th message in a 4-way handshake 63 | JSON.stringify(capfile.packetFrames[19], null, 2); 64 | { 65 | "header": { 66 | "timestamp": "2011-01-16T21:28:30.109Z", 67 | "length": 133, 68 | }, 69 | "frameControl": { 70 | "type": 2, 71 | "subtype": 8, 72 | ... 73 | }, 74 | "name": "EAPOL (QoS) - Handshake (4 of 4)", 75 | "_bssid": "002275ecf9c9", 76 | "_station": "f4ce46629c64", 77 | ... 78 | "auth": { 79 | "version": 1, // 802.1X-2001 80 | "keyInfoFlags": { 81 | "keyDescriptorVersion": 2, // WPA2 (HMAC-SHA1 MIC) 82 | "keyType": 1, 83 | "mic": true, 84 | "secure": true, 85 | ... 86 | } 87 | "keyMIC": "646debf34b677fbfd78c5724dc9ea442", 88 | "replayCounter": 2925, 89 | ... 90 | } 91 | } 92 | 93 | // Extract data from a 4-way handshake (neccessary to validate PSK in Crack.js) 94 | capfile.extractPmkFields() 95 | >>> Object { 96 | ssid: "NETGEAR2815", 97 | bssid: "002275ecf9c9", 98 | snonce: "da12c942e9dfcbe67068438f87cd4ce49b2…", 99 | anonce: "f5f5cd2ca691efe420224f466d3eb1633ef…", 100 | srcAddress: "f4ce46629c64", 101 | dstAddress: "002275ecf9c9", 102 | keyLength: 16, 103 | mic: "646debf34b677fbfd78c5724dc9ea442", 104 | eapolFrameBytes: "0103005f02030a00000000000000000b6dd…", 105 | keyDescriptorVersion: 2 // WPA2 106 | } 107 | ``` 108 | 109 | ## Crack.js 110 | 111 | ```javascript 112 | // Extract relevant fields from 4-way handshake (from CapFile.js) 113 | var handshake = capfile.extractPmkFields(); 114 | 115 | // Initialize Crack.js 116 | var crack = new Crack(handshake); 117 | 118 | // Validate the PSK ("dandelion" is the PSK used in the 4-way handshake) 119 | crack.tryPSK("dandelion") 120 | '[Crack.js] Constructing PMK using PDKDF2(psk:dandelion, ssid:NETGEAR2815)...' 121 | '[Crack.js] PMK (Pairwise Master Key): 273c545d3be7e3fd4510fb5509486ba77f32c39716c4d63bf86de6b808387a77' 122 | '[Crack.js] Constructing KCK using handshake values, Hmac-SHA1, and the PMK...' 123 | '[Crack.js] KCK (Key-Confirmation Key) : dc9471429e3918be1eff0f742450d0cd' 124 | '[Crack.js] EAPOL packet frame bytes: 0103005f02030a00000000000000000b6dda12c9...' 125 | '[Crack.js] Using Hmac-SHA1 for computing WPA2 MIC' 126 | '[Crack.js] Computed Mic (based on PMK & KCK): 646debf34b677fbfd78c5724dc9ea442' 127 | '[Crack.js] Expected Mic (from Handshake packet): 646debf34b677fbfd78c5724dc9ea442' 128 | >>> true 129 | 130 | // Validate an incorrect PSK 131 | crack.tryPSK("dandelioX") 132 | '[Crack.js] Constructing PMK using PDKDF2(psk:dandelioX, ssid:NETGEAR2815)...' 133 | '[Crack.js] PMK (Pairwise Master Key): 6dda13b1dec8170ce16b091f42c4558cdd7b26fb84de0b28ad11d049e8945a55' 134 | '[Crack.js] Constructing KCK using handshake values, Hmac-SHA1, and the PMK...' 135 | '[Crack.js] KCK (Key-Confirmation Key) : c9fa4a020370cc74dfa9a92a1412785c' 136 | '[Crack.js] EAPOL packet frame bytes: 0103005f02030a00000000000000000b6dda12c9...' 137 | '[Crack.js] Using Hmac-SHA1 for computing WPA2 MIC' 138 | '[Crack.js] Computed Mic (based on PMK & KCK): 6fbf2e9382faeeca025e00ec88f1cac4' 139 | '[Crack.js] Expected Mic (from Handshake packet): 646debf34b677fbfd78c5724dc9ea442' 140 | >>> false 141 | ``` 142 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CapFile 6 | 7 | 8 | 9 | 10 |

CapFile.js

11 | 12 |

Test cracking WPA 4-way handshake (TKIP): 13 | 14 | 15 |

Test cracking WPA2 4-way handshake (CCMP): 16 | 17 | 18 |

Debug output: 19 |

20 | 21 |

Load Pcap capture file: 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
#TimestampSource MACDest. MACBytesPacket Type
38 | 39 |


40 | 
41 |     
42 |     
43 |     
44 |     
45 |   
46 | 
47 | 


--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
 1 | * {
 2 |     font-family: Consolas,  Verdana, monospace;
 3 |     background-color: #000;
 4 |     color: #0c0;
 5 | }
 6 | html, body {
 7 |     margin: 0px;
 8 |     padding: 20px;
 9 | }
10 | 
11 | input {
12 |     background-color: #080;
13 |     color: #fff;
14 | }
15 | 
16 | textarea {
17 |     overflow: auto;
18 |     width: 100%;
19 | }
20 | 
21 | table {
22 |     border-spacing: 0px;
23 |     border-collapse: collapse;
24 | }
25 | 
26 | table th, table td {
27 |     text-align: left;
28 |     padding-right: 10px;
29 |     margin: 0px;
30 | }
31 | table th {
32 |     text-decoration: underline;
33 | }
34 | 
35 | 
36 | table tr.collapsed:hover td,
37 | table tr.expanded:hover td {
38 |     cursor: pointer;
39 |     background-color: #030;
40 | }
41 | 
42 | table td {
43 |     background-color: #010;
44 | }
45 | 
46 | td.index, th.index {
47 |     text-align: right;
48 | }
49 | 
50 | pre.payload {
51 |     padding: 5px;
52 |     margin-left: 20px;
53 |     overflow-x: scroll;
54 |     max-width: 800px;
55 |     background-color: #030;
56 | }
57 | 
58 | 


--------------------------------------------------------------------------------