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]
+ }
+ ],
+ },
+]);