diff --git a/package-lock.json b/package-lock.json index f0e04629..e65171c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "9.11.14", + "version": "9.11.17", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e33738dc..c38fde80 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "9.11.14", + "version": "9.11.17", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", diff --git a/src/core/lib/FileSignatures.mjs b/src/core/lib/FileSignatures.mjs index 07f3c101..e3b63f23 100644 --- a/src/core/lib/FileSignatures.mjs +++ b/src/core/lib/FileSignatures.mjs @@ -280,7 +280,7 @@ export const FILE_SIGNATURES = { 9: 0x0, 10: [0x0, 0x1] }, - extractor: null + extractor: extractICO }, { name: "Radiance High Dynamic Range image", @@ -2933,6 +2933,32 @@ export function extractBMP(bytes, offset) { } +/** + * ICO extractor. + * + * @param {Uint8Array} bytes + * @param {number} offset + */ +export function extractICO(bytes, offset) { + const stream = new Stream(bytes.slice(offset)); + + // Move to number of files there are. + stream.moveTo(4); + + // Read the number of files stored in the ICO + const numberFiles = stream.readInt(2, "le"); + + // Move forward to the last file header. + stream.moveForwardsBy(8 + ((numberFiles-1) * 16)); + const fileSize = stream.readInt(4, "le"); + const fileOffset = stream.readInt(4, "le"); + + // Move to the end of the last file. + stream.moveTo(fileOffset + fileSize); + return stream.carve(); +} + + /** * WAV extractor. * @@ -3075,15 +3101,127 @@ export function extractSQLITE(bytes, offset) { export function extractPListXML(bytes, offset) { const stream = new Stream(bytes.slice(offset)); - // Find closing tag () - stream.continueUntil([0x3c, 0x2f, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x3e]); - stream.moveForwardsBy(8); + let braceCount = 0; + + // Continue to the first ( 0 && stream.hasMore()) { + if (stream.readInt(1) === 0x3c) { + + // If we hit an . + if (stream.getBytes(7).join("") === [0x2f, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x3e].join("")) { + braceCount--; + } else { + stream.moveBackwardsBy(7); + } + } + } stream.consumeIf(0x0a); return stream.carve(); } +/** + * OLE2 extractor. + * + * @param {Uint8Array} bytes + * @param {number} offset + * @returns {Uint8Array} + */ +export function extractOLE2(bytes, offset) { + const stream = new Stream(bytes.slice(offset)); + const entries = [ + [[0x52, 0x00, 0x6f, 0x00, 0x6f, 0x00, 0x74, 0x00, 0x20, 0x00, 0x45, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x72, 0x00, 0x79], 19, "Root Entry"], + [[0x57, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x6b, 0x00, 0x62, 0x00, 0x6f, 0x00, 0x6f, 0x00, 0x6b], 15, "Workbook"], + [[0x43, 0x00, 0x75, 0x00, 0x72, 0x00, 0x72, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x20, 0x00, 0x55, 0x00, 0x73, 0x00, 0x65, 0x00, 0x72], 23, "Current User"], + [[0x50, 0x00, 0x6f, 0x00, 0x77, 0x00, 0x65, 0x00, 0x72, 0x00, 0x50, 0x00, 0x6f, 0x00, 0x69, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x20, 0x00, 0x44, 0x00, 0x6f, 0x00, 0x63, 0x00, 0x75, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x74], 37, "PowerPoint Document"], + [[0x57, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x64, 0x00, 0x44, 0x00, 0x6f, 0x00, 0x63, 0x00, 0x75, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x74], 23, "WordDocument"], + [[0x44, 0x00, 0x61, 0x00, 0x74, 0x00, 0x61], 7, "Data"], + [[0x50, 0x00, 0x69, 0x00, 0x63, 0x00, 0x74, 0x00, 0x75, 0x00, 0x72, 0x00, 0x65, 0x00, 0x73], 15, "Pictures"], + [[0x31, 0x00, 0x54, 0x00, 0x61, 0x00, 0x62, 0x00, 0x6c, 0x00, 0x65], 11, "1Table"], + [[0x05, 0x00, 0x53, 0x00, 0x75, 0x00, 0x6d, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x72, 0x00, 0x79, 0x00, 0x49, 0x00, 0x6e, 0x00, 0x66, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x74, 0x00, 0x69, 0x00, 0x6f, 0x00, 0x6e], 37, "SummaryInformation"], + [[0x05, 0x00, 0x44, 0x00, 0x6f, 0x00, 0x63, 0x00, 0x75, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x6e, 0x00, 0x74, 0x00, 0x53, 0x00, 0x75, 0x00, 0x6d, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x72, 0x00, 0x79, 0x00, 0x49, 0x00, 0x6e, 0x00, 0x66, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x6d, 0x00, 0x61, 0x00, 0x74, 0x00, 0x69, 0x00, 0x6f, 0x00, 0x6e], 53, "DocumentSummaryInformation"], + [[0x43, 0x00, 0x6f, 0x00, 0x6d, 0x00, 0x70, 0x00, 0x4f, 0x00, 0x62, 0x00, 0x6a], 13, "Comp Obj"], + [[0x01, 0x00], 2, "Entry"] + ]; + let endianness = "le"; + + // Move to endianess field. + stream.moveForwardsBy(28); + if (stream.readInt(2, endianness) === 0xfffe) + endianness = "be"; + + // Calculate the size of the normal sectors. + const sizeOfSector = 2 ** stream.readInt(2, endianness); + + // Move to root directory offset field. + stream.moveTo(48); + + // Read root directory offset. + const rootStuff = stream.readInt(4, endianness); + + // Calculate root directory offset. + let total = 512 + (rootStuff * sizeOfSector); + stream.moveTo(total); + + // While valid directory entries. + let found = true; + while (found) { + found = false; + + // Attempt to determine what directory entry it is. + for (const element of entries) { + + // If the byte pattern matches. + if (stream.getBytes(element[1]).join("") === element[0].join("")) { + stream.moveBackwardsBy(element[1]); + found = true; + + // Move forwards by the size of the comp obj. + if (element[2] === "Comp Obj") { + + // The size of the Comp Obj entry - 128. Since we add 128 later. + total += 128 * 6; + stream.moveTo(total); + } else if (element[2] === "Entry") { + + // If there is an entry move backwards by 126 to then move forwards by 128. Hence a total displacement of 2. + stream.moveBackwardsBy(126); + } + break; + } + stream.moveBackwardsBy(element[1]); + } + + // If we have found a valid entry, move forwards by 128. + if (found) { + + // Every entry is at least 128 in size, some are bigger which is dealt with by the above if statement. + total += 128; + stream.moveForwardsBy(128); + } + } + + // Round up to a multiple of 512. + total = Math.ceil(total / 512) * 512; + + stream.moveTo(total); + return stream.carve(); +} + + /** * GZIP extractor. * diff --git a/src/core/operations/Gunzip.mjs b/src/core/operations/Gunzip.mjs index 07b1d6c2..ef487b06 100644 --- a/src/core/operations/Gunzip.mjs +++ b/src/core/operations/Gunzip.mjs @@ -5,9 +5,9 @@ */ import Operation from "../Operation.mjs"; -import zlibAndGzip from "zlibjs/bin/zlib_and_gzip.min.js"; +import gunzip from "zlibjs/bin/gunzip.min.js"; -const Zlib = zlibAndGzip.Zlib; +const Zlib = gunzip.Zlib; /** * Gunzip operation @@ -42,8 +42,8 @@ class Gunzip extends Operation { * @returns {File} */ run(input, args) { - const gunzip = new Zlib.Gunzip(new Uint8Array(input)); - return new Uint8Array(gunzip.decompress()).buffer; + const gzipObj = new Zlib.Gunzip(new Uint8Array(input)); + return new Uint8Array(gzipObj.decompress()).buffer; } } diff --git a/src/core/operations/Gzip.mjs b/src/core/operations/Gzip.mjs index 5f9fa474..093ae6a4 100644 --- a/src/core/operations/Gzip.mjs +++ b/src/core/operations/Gzip.mjs @@ -6,9 +6,9 @@ import Operation from "../Operation.mjs"; import {COMPRESSION_TYPE, ZLIB_COMPRESSION_TYPE_LOOKUP} from "../lib/Zlib.mjs"; -import zlibAndGzip from "zlibjs/bin/zlib_and_gzip.min.js"; +import gzip from "zlibjs/bin/gzip.min.js"; -const Zlib = zlibAndGzip.Zlib; +const Zlib = gzip.Zlib; /** * Gzip operation @@ -73,12 +73,15 @@ class Gzip extends Operation { options.filename = filename; } if (comment.length) { - options.flags.fcommenct = true; + options.flags.comment = true; options.comment = comment; } - - const gzip = new Zlib.Gzip(new Uint8Array(input), options); - return new Uint8Array(gzip.compress()).buffer; + const gzipObj = new Zlib.Gzip(new Uint8Array(input), options); + const compressed = new Uint8Array(gzipObj.compress()); + if (options.flags.comment && !(compressed[3] & 0x10)) { + compressed[3] |= 0x10; + } + return compressed.buffer; } } diff --git a/src/web/waiters/RecipeWaiter.mjs b/src/web/waiters/RecipeWaiter.mjs index ba0e7b11..4f8290f4 100755 --- a/src/web/waiters/RecipeWaiter.mjs +++ b/src/web/waiters/RecipeWaiter.mjs @@ -51,6 +51,7 @@ class RecipeWaiter { } }.bind(this), onSort: function(evt) { + this.updateZIndices(); if (evt.from.id === "rec-list") { document.dispatchEvent(this.manager.statechange); } @@ -149,6 +150,19 @@ class RecipeWaiter { } + /** + * Sets the z-index property on each operation to make sure that operations higher in the list + * have a higher index, meaning dropdowns are not hidden underneath subsequent operations. + */ + updateZIndices() { + const operations = document.getElementById("rec-list").children; + for (let i = 0; i < operations.length; i++) { + const operation = operations[i]; + operation.style.zIndex = 100 + operations.length - i; + } + } + + /** * Handler for favourite dragover events. * If the element being dragged is an operation, displays a visual cue so that the user knows it can @@ -466,6 +480,7 @@ class RecipeWaiter { log.debug(`'${e.target.querySelector(".op-title").textContent}' added to recipe`); this.triggerArgEvents(e.target); + this.updateZIndices(); window.dispatchEvent(this.manager.statechange); } diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index c54fa7ef..51676386 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -41,6 +41,8 @@ import "./tests/DateTime.mjs"; import "./tests/ExtractEmailAddresses.mjs"; import "./tests/Fork.mjs"; import "./tests/FromDecimal.mjs"; +import "./tests/Gzip.mjs"; +import "./tests/Gunzip.mjs"; import "./tests/Hash.mjs"; import "./tests/HaversineDistance.mjs"; import "./tests/Hexdump.mjs"; diff --git a/tests/operations/tests/Gunzip.mjs b/tests/operations/tests/Gunzip.mjs new file mode 100644 index 00000000..70b67a31 --- /dev/null +++ b/tests/operations/tests/Gunzip.mjs @@ -0,0 +1,58 @@ +/** + * Gunzip Tests. + * + * @author n1073645 [n1073645@gmail.com] + * + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "Gunzip: No comment, no checksum and no filename", + input: "1f8b0800f7c8f85d00ff0dc9dd0180200804e0556ea8262848fb3dc588c6a7e76faa8aeedb726036c68d951f76bf9a0af8aae1f97d9c0c084b02509cbf8c2c000000", + expectedOutput: "The quick brown fox jumped over the slow dog", + recipeConfig: [ + { + op: "From Hex", + args: ["None"] + }, + { + op: "Gunzip", + args: [] + } + ] + }, + { + name: "Gunzip: No comment, no checksum and filename", + input: "1f8b080843c9f85d00ff66696c656e616d65000dc9dd0180200804e0556ea8262848fb3dc588c6a7e76faa8aeedb726036c68d951f76bf9a0af8aae1f97d9c0c084b02509cbf8c2c000000", + expectedOutput: "The quick brown fox jumped over the slow dog", + recipeConfig: [ + { + op: "From Hex", + args: ["None"] + }, + { + op: "Gunzip", + args: [] + } + ] + }, + { + name: "Gunzip: Has a comment, no checksum and has a filename", + input: "1f8b08186fc9f85d00ff66696c656e616d6500636f6d6d656e74000dc9dd0180200804e0556ea8262848fb3dc588c6a7e76faa8aeedb726036c68d951f76bf9a0af8aae1f97d9c0c084b02509cbf8c2c000000", + expectedOutput: "The quick brown fox jumped over the slow dog", + recipeConfig: [ + { + op: "From Hex", + args: ["None"] + }, + { + op: "Gunzip", + args: [] + } + ] + } +]); diff --git a/tests/operations/tests/Gzip.mjs b/tests/operations/tests/Gzip.mjs new file mode 100644 index 00000000..c9b2b8ca --- /dev/null +++ b/tests/operations/tests/Gzip.mjs @@ -0,0 +1,89 @@ +/** + * Gzip Tests. + * + * @author n1073645 [n1073645@gmail.com] + * + * @copyright Crown Copyright 2019 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "Gzip: No comment, no checksum and no filename", + input: "The quick brown fox jumped over the slow dog", + expectedOutput: "0dc9dd0180200804e0556ea8262848fb3dc588c6a7e76faa8aeedb726036c68d951f76bf9a0af8aae1f97d9c0c084b02509cbf8c2c000000", + recipeConfig: [ + { + op: "Gzip", + args: ["Dynamic Huffman Coding", "", "", false] + }, + { + op: "Drop bytes", + args: [0, 10, false] + }, + { + op: "To Hex", + args: ["None"] + } + ] + }, + { + name: "Gzip: No comment, no checksum and has a filename", + input: "The quick brown fox jumped over the slow dog", + expectedOutput: "636f6d6d656e74000dc9dd0180200804e0556ea8262848fb3dc588c6a7e76faa8aeedb726036c68d951f76bf9a0af8aae1f97d9c0c084b02509cbf8c2c000000", + recipeConfig: [ + { + op: "Gzip", + args: ["Dynamic Huffman Coding", "comment", "", false] + }, + { + op: "Drop bytes", + args: [0, 10, false] + }, + { + op: "To Hex", + args: ["None"] + } + ] + }, + { + name: "Gzip: Has a comment, no checksum and no filename", + input: "The quick brown fox jumped over the slow dog", + expectedOutput: "636f6d6d656e74000dc9dd0180200804e0556ea8262848fb3dc588c6a7e76faa8aeedb726036c68d951f76bf9a0af8aae1f97d9c0c084b02509cbf8c2c000000", + recipeConfig: [ + { + op: "Gzip", + args: ["Dynamic Huffman Coding", "", "comment", false] + }, + { + op: "Drop bytes", + args: [0, 10, false] + }, + { + op: "To Hex", + args: ["None"] + } + ] + }, + { + name: "Gzip: Has a comment, no checksum and has a filename", + input: "The quick brown fox jumped over the slow dog", + expectedOutput: "66696c656e616d6500636f6d6d656e74000dc9dd0180200804e0556ea8262848fb3dc588c6a7e76faa8aeedb726036c68d951f76bf9a0af8aae1f97d9c0c084b02509cbf8c2c000000", + recipeConfig: [ + { + op: "Gzip", + args: ["Dynamic Huffman Coding", "filename", "comment", false] + }, + { + op: "Drop bytes", + args: [0, 10, false] + }, + { + op: "To Hex", + args: ["None"] + } + ] + }, +]);