diff --git a/package-lock.json b/package-lock.json index b374df4b..297fa0d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { + "@alexaltea/capstone-js": "^3.0.5", "@astronautlabs/amf": "^0.0.6", "@babel/polyfill": "^7.12.1", "@blu3r4y/lzma": "^2.3.3", @@ -161,6 +162,11 @@ "worker-loader": "^3.0.8" } }, + "node_modules/@alexaltea/capstone-js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@alexaltea/capstone-js/-/capstone-js-3.0.5.tgz", + "integrity": "sha512-HWa4d5vblYc3OEJ9MpcXFo0gV/oDLTI5iH7ng80Gs3/Wo3lcYvB14gDDwSr9So1F+fuwIET8meo6TxTezEyqTg==" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", diff --git a/package.json b/package.json index 9191ab6f..4d134e90 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "worker-loader": "^3.0.8" }, "dependencies": { + "@alexaltea/capstone-js": "^3.0.5", "@astronautlabs/amf": "^0.0.6", "@babel/polyfill": "^7.12.1", "@blu3r4y/lzma": "^2.3.3", diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 434c8bb6..20d9a395 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -548,6 +548,7 @@ "Chi Square", "P-list Viewer", "Disassemble x86", + "Disassemble ARM", "Pseudo-Random Number Generator", "Generate De Bruijn Sequence", "Generate UUID", diff --git a/src/core/operations/DisassembleARM.mjs b/src/core/operations/DisassembleARM.mjs new file mode 100644 index 00000000..6e9a1b18 --- /dev/null +++ b/src/core/operations/DisassembleARM.mjs @@ -0,0 +1,195 @@ +/** + * @author MedjedThomasXM + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import { isWorkerEnvironment } from "../Utils.mjs"; + +/** + * Disassemble ARM operation + */ +class DisassembleARM extends Operation { + + /** + * DisassembleARM constructor + */ + constructor() { + super(); + + this.name = "Disassemble ARM"; + this.module = "Shellcode"; + this.description = "Disassembles ARM machine code into assembly language.

Supports ARM (32-bit), Thumb, and ARM64 (AArch64) architectures using the Capstone disassembly framework.

Input should be in hexadecimal."; + this.infoURL = "https://wikipedia.org/wiki/ARM_architecture_family"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Architecture", + "type": "option", + "value": ["ARM (32-bit)", "ARM64 (AArch64)"] + }, + { + "name": "Mode", + "type": "option", + "value": ["ARM", "Thumb", "Thumb + Cortex-M", "ARMv8"] + }, + { + "name": "Endianness", + "type": "option", + "value": ["Little Endian", "Big Endian"] + }, + { + "name": "Starting address (hex)", + "type": "number", + "value": 0 + }, + { + "name": "Show instruction hex", + "type": "boolean", + "value": true + }, + { + "name": "Show instruction position", + "type": "boolean", + "value": true + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + async run(input, args) { + const [ + architecture, + mode, + endianness, + startAddress, + showHex, + showPosition + ] = args; + + // Remove whitespace from input + const hexInput = input.replace(/\s/g, ""); + + // Validate hex input + if (!/^[0-9a-fA-F]*$/.test(hexInput)) { + throw new OperationError("Invalid hexadecimal input. Please provide valid hex characters only."); + } + + if (hexInput.length === 0) { + return ""; + } + + if (hexInput.length % 2 !== 0) { + throw new OperationError("Invalid hexadecimal input. Length must be even."); + } + + // Convert hex string to byte array + const bytes = []; + for (let i = 0; i < hexInput.length; i += 2) { + bytes.push(parseInt(hexInput.substr(i, 2), 16)); + } + + if (isWorkerEnvironment()) { + self.sendStatusMessage("Loading Capstone disassembler..."); + } + + // Dynamically import capstone to avoid loading the large library until needed + const cs = (await import("@alexaltea/capstone-js/dist/capstone.min.js")).default; + + // Determine architecture constant + let arch; + if (architecture === "ARM64 (AArch64)") { + arch = cs.ARCH_ARM64; + } else { + arch = cs.ARCH_ARM; + } + + // Determine mode constant + let modeValue = cs.MODE_LITTLE_ENDIAN; + + if (architecture === "ARM (32-bit)") { + switch (mode) { + case "ARM": + modeValue = cs.MODE_ARM; + break; + case "Thumb": + modeValue = cs.MODE_THUMB; + break; + case "Thumb + Cortex-M": + modeValue = cs.MODE_THUMB | cs.MODE_MCLASS; + break; + case "ARMv8": + modeValue = cs.MODE_ARM | cs.MODE_V8; + break; + default: + modeValue = cs.MODE_ARM; + } + } else { + // ARM64 only has one mode (ARM mode is default for ARM64) + modeValue = cs.MODE_ARM; + } + + // Add endianness + if (endianness === "Big Endian") { + modeValue |= cs.MODE_BIG_ENDIAN; + } + + if (isWorkerEnvironment()) { + self.sendStatusMessage("Disassembling..."); + } + + let disassembler; + try { + disassembler = new cs.Capstone(arch, modeValue); + } catch (e) { + throw new OperationError(`Failed to initialise Capstone disassembler: ${e}`); + } + + let instructions; + try { + instructions = disassembler.disasm(bytes, startAddress); + } catch (e) { + disassembler.close(); + throw new OperationError(`Disassembly failed: ${e}`); + } + + // Format output + const output = []; + for (const insn of instructions) { + let line = ""; + + if (showPosition) { + // Format address as hex with 0x prefix + const addrHex = "0x" + insn.address.toString(16).padStart(8, "0"); + line += addrHex + " "; + } + + if (showHex) { + // Format instruction bytes as hex + const bytesHex = insn.bytes.map(b => b.toString(16).padStart(2, "0")).join(""); + line += bytesHex.padEnd(16, " ") + " "; + } + + line += insn.mnemonic; + if (insn.op_str) { + line += " " + insn.op_str; + } + + output.push(line); + } + + disassembler.close(); + + return output.join("\n"); + } + +} + +export default DisassembleARM; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index f147e9e7..1ca15ac0 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -60,6 +60,7 @@ import "./tests/Crypt.mjs"; import "./tests/CSV.mjs"; import "./tests/DateTime.mjs"; import "./tests/DefangIP.mjs"; +import "./tests/DisassembleARM.mjs"; import "./tests/DropNthBytes.mjs"; import "./tests/ECDSA.mjs"; import "./tests/ELFInfo.mjs"; diff --git a/tests/operations/tests/DisassembleARM.mjs b/tests/operations/tests/DisassembleARM.mjs new file mode 100644 index 00000000..fcd3e33a --- /dev/null +++ b/tests/operations/tests/DisassembleARM.mjs @@ -0,0 +1,131 @@ +/** + * Disassemble ARM tests. + * @author MedjedThomasXM + * @copyright Crown Copyright 2024 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "Disassemble ARM: ARM32 basic - MOV instruction", + input: "0000a0e3", + expectedOutput: "0x00000000 0000a0e3 mov r0, #0", + recipeConfig: [ + { + "op": "Disassemble ARM", + "args": ["ARM (32-bit)", "ARM", "Little Endian", 0, true, true] + } + ], + }, + { + name: "Disassemble ARM: ARM32 - Multiple instructions", + input: "04e02de5 00d04be2", + expectedOutput: "0x00000000 04e02de5 str lr, [sp, #-4]!\n0x00000004 00d04be2 sub sp, fp, #0", + recipeConfig: [ + { + "op": "Disassemble ARM", + "args": ["ARM (32-bit)", "ARM", "Little Endian", 0, true, true] + } + ], + }, + { + name: "Disassemble ARM: ARM32 Thumb mode", + input: "80b500af", + expectedOutput: "0x00000000 80b5 push {r7, lr}\n0x00000002 00af add r7, sp, #0", + recipeConfig: [ + { + "op": "Disassemble ARM", + "args": ["ARM (32-bit)", "Thumb", "Little Endian", 0, true, true] + } + ], + }, + { + name: "Disassemble ARM: ARM64 basic - MOV instruction", + input: "e0031faa", + expectedOutput: "0x00000000 e0031faa mov x0, xzr", + recipeConfig: [ + { + "op": "Disassemble ARM", + "args": ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true] + } + ], + }, + { + name: "Disassemble ARM: ARM64 - Multiple instructions", + input: "fd7bbfa9 fd030091", + expectedOutput: "0x00000000 fd7bbfa9 stp x29, x30, [sp, #-0x10]!\n0x00000004 fd030091 mov x29, sp", + recipeConfig: [ + { + "op": "Disassemble ARM", + "args": ["ARM64 (AArch64)", "ARM", "Little Endian", 0, true, true] + } + ], + }, + { + name: "Disassemble ARM: Custom starting address", + input: "0000a0e3", + expectedOutput: "0x00001000 0000a0e3 mov r0, #0", + recipeConfig: [ + { + "op": "Disassemble ARM", + "args": ["ARM (32-bit)", "ARM", "Little Endian", 4096, true, true] + } + ], + }, + { + name: "Disassemble ARM: Hide instruction hex", + input: "0000a0e3", + expectedOutput: "0x00000000 mov r0, #0", + recipeConfig: [ + { + "op": "Disassemble ARM", + "args": ["ARM (32-bit)", "ARM", "Little Endian", 0, false, true] + } + ], + }, + { + name: "Disassemble ARM: Hide instruction position", + input: "0000a0e3", + expectedOutput: "0000a0e3 mov r0, #0", + recipeConfig: [ + { + "op": "Disassemble ARM", + "args": ["ARM (32-bit)", "ARM", "Little Endian", 0, true, false] + } + ], + }, + { + name: "Disassemble ARM: Hide both hex and position", + input: "0000a0e3", + expectedOutput: "mov r0, #0", + recipeConfig: [ + { + "op": "Disassemble ARM", + "args": ["ARM (32-bit)", "ARM", "Little Endian", 0, false, false] + } + ], + }, + { + name: "Disassemble ARM: Empty input", + input: "", + expectedOutput: "", + recipeConfig: [ + { + "op": "Disassemble ARM", + "args": ["ARM (32-bit)", "ARM", "Little Endian", 0, true, true] + } + ], + }, + { + name: "Disassemble ARM: Input with whitespace", + input: "00 00 a0 e3", + expectedOutput: "0x00000000 0000a0e3 mov r0, #0", + recipeConfig: [ + { + "op": "Disassemble ARM", + "args": ["ARM (32-bit)", "ARM", "Little Endian", 0, true, true] + } + ], + }, +]);