diff --git a/README.md b/README.md index 5309845..6895ddd 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,298 @@ -# metame +

+ metame Metamorphic Engine Logo +

-metame is a simple metamorphic code engine for arbitrary executables. +> A highly optimized, size-constrained metamorphic code mutation engine for arbitrary executables (x86/x64). -From Wikipedia: +--- -> Metamorphic code is code that when run outputs a logically equivalent -> version of its own code under some interpretation. -> This is used by computer viruses to avoid the pattern recognition of -> anti-virus software. +## 📖 Table of Contents +- [What is Metamorphic Code?](#what-is-metamorphic-code) +- [Key Features](#key-features) +- [How It Works](#how-it-works) +- [Under the Hood](#under-the-hood) + - [1. Mnemonic-Indexed Rule Lookup](#1-mnemonic-indexed-rule-lookup) + - [2. Size-Constrained Mutation](#2-size-constrained-mutation) + - [3. Dynamic & Safe NOP Generation](#3-dynamic--safe-nop-generation) +- [Supported Instruction Mutations](#supported-instruction-mutations) + - [32-bit (x86) Mutator Rules](#32-bit-x86-mutator-rules) + - [64-bit (x64) Mutator Rules](#64-bit-x64-mutator-rules) +- [NOP Generation Catalog](#nop-generation-catalog) + - [32-Bit NOP Sequences](#32-bit-nop-sequences) + - [64-Bit NOP Sequences](#64-bit-nop-sequences) +- [Signature Mutation Verification](#signature-mutation-verification) +- [Installation](#installation) + - [Prerequisites](#prerequisites) + - [From Source (Recommended)](#from-source-recommended) + - [From PyPI](#from-pypi) + - [Docker Setup](#docker-setup) +- [Usage](#usage) + - [Command-line Options](#command-line-options) + - [Example Mutation Command](#example-mutation-command) +- [License](#license) -metame implementation works this way: +--- -1. Open a given binary and analyze the code -2. Randomly replace instructions with equivalences in logic and size -3. Copy and patch the original binary to generate a mutated variant +## What is Metamorphic Code? -It currently supports the following architectures: +Metamorphic code is code that, when run or compiled, outputs a logically equivalent version of itself. In cybersecurity, this technique is typically used to evade signature-based detection mechanisms (such as antivirus pattern recognition) by ensuring the binary looks completely different on every generation while retaining the exact same logic and functionality. -- x86 32 bits -- x86 64 bits +--- -Also, it supports a variety of file formats, as [radare2][1] is used for -file parsing and code analysis. +## Key Features -Example of code before and after mutation: +* **O(1) Mnemonic Indexing:** Substitution rules are indexed by opcode mnemonics for constant-time lookup, replacing slow linear rule scans. +* **Size-Constrained Mutators:** Resolves instruction size expansions and relative branch jumps to ensure strict instruction-size preservation during mutation. +* **Dynamic NOP Placeholder Resolution:** Generates random NOP sequences dynamically on-the-fly, allowing relative branch jumps to be resolved correctly at patch time. +* **Safe Registers & Flags:** Uses native multi-byte NOPs (such as `nop dword ptr [rax]`) and relative branch jumps to guarantee EFLAGS/RFLAGS stability and prevent register contamination. +* **Modern Radare2 Integration:** Supports both older (`offset`) and modern (`addr`) JSON structures returned by newer versions of radare2. +* **Opcode Normalization:** Standardizes whitespace and formatting differences from radare2 output for robust regex matching. -![alt text](https://raw.githubusercontent.com/a0rtega/metame/master/screens/screen1.png "Spot the differences") +--- -Hint: Two instructions have been replaced in this snippet. +## How It Works -Here another example on how it can mutate a NOP sled into equivalent code: +`metame` uses `radare2` to disassemble the input binary and analyze its functions. It then identifies instructions that match its library of replacement patterns, compiles alternatives using the `keystone-engine`, and patches the binary. -![alt text](https://raw.githubusercontent.com/a0rtega/metame/master/screens/screen2.png "Spot the differences") +```mermaid +graph TD + A[Open Binary with Radare2] + B[Analyze Binary and Identify Functions] + C[Perform Constant-Time Mnemonic-Indexed Rule Lookup] + D[Replace Instructions While Preserving Logic and Size] + E[Generate Dynamic NOP Sequences] + F[Patch the Original Binary with Radare2] + G[Output Mutated Binary] + + A --> B --> C --> D --> E --> F --> G +``` + +1. **Disassemble and Analyze:** Opens the input executable with `radare2` to load symbol metadata and function offsets. +2. **Mutate Opcodes:** Scans each instruction, queries candidate replacement patterns instantly, and chooses a random matching sequence of equivalent size. +3. **Patch and Save:** Copies the original file and overwrites the target instruction offsets with the newly assembled metamorphic bytes. + +--- + +## Under the Hood + +The execution logic is split into several core components: +- [__init__.py](metame/__init__.py): Coordinates argument parsing, input/output validation, and controls the parser iterations. +- [r2parser.py](metame/r2parser.py): Establishes `r2pipe` connections, parses binary target metadata, performs functions analysis, and applies patch blocks. +- [x64handler.py](metame/x64handler.py): Houses the transformation engine, NOP generators, compilation abstractions via Keystone Engine, and mutation definitions. +- [constants.py](metame/constants.py): Declares architecture target support limits. + +### 1. Mnemonic-Indexed Rule Lookup +Earlier versions of metamorphic engines scanned mutation rules linearly. `metame` indexes rules by the mnemonic of the first instruction (e.g., `mov`, `add`, `xor`). During iteration, it checks the dictionary in $O(1)$ time to quickly verify if the instruction is a candidate for mutation. + +### 2. Size-Constrained Mutation +For binary patching to be safe without rewriting the entire relocation table, mutated instructions must occupy the exact same byte length as the original instruction(s). `metame` handles this by compiling candidate instructions using Keystone Engine: +- If the mutated instruction is too short, it appends NOP padding (e.g. `{nop1}`, `{nop2}`). +- If the mutated instruction matches the original byte length exactly, the patch is registered. +- If it cannot find a replacement of equal length, the instruction is left unmodified. + +### 3. Dynamic & Safe NOP Generation +Standard single-byte NOPs (`0x90`) are easily identified by signatures. `metame` implements a dynamic NOP generator that constructs random multi-byte NOP sequences of lengths 1 to 4. To ensure that these NOPs do not break the program, they employ: +* **Flag Stability:** Wrapping state-changing operations inside `pushfd` / `popfd` to preserve processor flags. +* **Register Stability:** Restricting pushed/popped registers to safe subsets and immediately restoring them. +* **Control Flow Jumps:** Using relative jumps (e.g. `jmp addr+offset`) to skip over mutating helper instructions. +* **Intel/AMD Long NOPs:** Utilizing multi-byte NOP instructions like `nop dword ptr [rax]` which perform no operation and do not affect flags. + +--- + +## Supported Instruction Mutations + +Detailed mutation behavior is defined inside [x64handler.py](metame/x64handler.py). + +### 32-bit (x86) Mutator Rules +The engine checks for the following patterns and replaces them with one of the equivalents: + +| Original Pattern | Target Equivalents / Explanations | +| :--- | :--- | +| `mov reg, reg` | Replaced by NOP sequence of length 2 | +| `add reg, 1` | `inc reg` + NOP padding | +| `sub reg, 1` | `dec reg` + NOP padding | +| `shl reg, 1` | `add reg, reg` | +| `test reg, reg` | `or reg, reg` (and vice-versa) | +| `xor reg, reg` | `sub reg, reg` (and vice-versa) | +| `mov regA, regB` | `push regB; pop regA` | +| `mov reg, 0` | `pushfd; xor reg, reg; popfd; {nop1}`
`pushfd; sub reg, reg; popfd; {nop1}`
`pushfd; and reg, 0; popfd` | +| `mov reg, 1` | `pushfd; xor reg, reg; inc reg; popfd` | +| `mov reg, IMM8` | `push IMM8; pop reg; {nop2}`
`{nop2}; push IMM8; pop reg`
`{nop1}; push IMM8; {nop1}; pop reg` | +| `je Target` / `jz Target` | Swaps between `je` and `jz` aliases | +| `jne Target` / `jnz Target` | Swaps between `jne` and `jnz` aliases | + +### 64-bit (x64) Mutator Rules +Extends the x86 rules with specific 64-bit register mutations: + +| Original Pattern | Target Equivalents / Explanations | +| :--- | :--- | +| `mov reg, reg` | Replaced by NOP sequence of length 2 | +| `add reg, 1` | `inc reg` + NOP padding | +| `sub reg, 1` | `dec reg` + NOP padding | +| `shl reg, 1` | `add reg, reg` | +| `test reg, reg` | `or reg, reg` (and vice-versa) | +| `xor reg, reg` | `sub reg, reg` (and vice-versa) | +| `add r_reg, 1` | `inc r_reg` + NOP padding | +| `sub r_reg, 1` | `dec r_reg` + NOP padding | +| `shl r_reg, 1` | `add r_reg, r_reg` | +| `test r_reg, r_reg` | `or r_reg, r_reg` (and vice-versa) | +| `xor r_reg, r_reg` | `sub r_reg, r_reg` (and vice-versa) | +| `mov r_regA, r_regB` | `push r_regB; pop r_regA; {nop1}`
`{nop1}; push r_regB; pop r_regA`
`push r_regB; {nop1}; pop r_regA` | +| `je Target` / `jz Target` | Swaps between `je` and `jz` aliases | +| `jne Target` / `jnz Target` | Swaps between `jne` and `jnz` aliases | + +--- + +## NOP Generation Catalog + +When `metame` needs to pad out instruction replacements, it selects a random sequence from the following tables depending on the target architecture and the required length in bytes. + +### 32-Bit NOP Sequences + +| Size (Bytes) | Mutation / Sequence Template | Safety & Mechanism | +| :---: | :--- | :--- | +| **1** | `nop` | Standard 1-byte instruction | +| **2** | `push reg; pop reg` | Pushes/pops a random register (`eax`, `ebx`, `ecx`, `edx`, `esi`, `edi`) | +| **2** | `pushad; popad` | Saves and restores all general-purpose registers | +| **2** | `xchg ax, ax` | Equivalent to standard NOP | +| **2** | `nop; nop` | Double NOP | +| **3** | `jmp addr+3; inc reg` | Jumps over the mutating register increment instruction | +| **3** | `jmp addr+3; push reg` | Jumps over the push instruction | +| **3** | `jmp addr+3; pop reg` | Jumps over the pop instruction | +| **3** | `nop dword ptr [eax]` | Native multi-byte long NOP | +| **3** | `nop; {nop2}` or `{nop2}; nop` | Combined 1-byte and 2-byte NOPs | +| **4** | `jmp addr+4; pop reg; pop reg` | Jumps over safe stack cleanup sequences | +| **4** | `jmp addr+4; push reg; push reg` | Jumps over register push operations | +| **4** | `jmp addr+4; push reg; pop reg` | Jumps over stack operations | +| **4** | `xor reg, 0; nop` | Mutates flags, leaves register intact | +| **4** | `nop dword ptr [eax + eax]` | Multi-byte long NOP using SIB byte | +| **4** | `{nop2}; {nop2}` | Combined two 2-byte NOPs | + +### 64-Bit NOP Sequences + +| Size (Bytes) | Mutation / Sequence Template | Safety & Mechanism | +| :---: | :--- | :--- | +| **1** | `nop` | Standard 1-byte instruction | +| **2** | `push reg; pop reg` | Pushes/pops a random register (`rax`, `rbx`, `rcx`, `rdx`, `rsi`, `rdi`) | +| **2** | `xchg ax, ax` | Multi-byte NOP | +| **2** | `nop; nop` | Double NOP | +| **3** | `push reg; pop reg; nop` | 2-byte register operations + NOP | +| **3** | `nop; push reg; pop reg` | NOP + 2-byte register operations | +| **3** | `jmp addr+3; push reg` | Jumps over instruction to preserve stack/register integrity | +| **3** | `nop dword ptr [rax]` | Native multi-byte long NOP | +| **3** | `nop; {nop2}` or `{nop2}; nop` | Combined NOPs | +| **4** | `jmp addr+4; pop reg; pop reg` | Jumps over stack modification | +| **4** | `jmp addr+4; push reg; push reg` | Jumps over register pushes | +| **4** | `jmp addr+4; push reg; pop reg` | Jumps over stack sequence | +| **4** | `jmp addr+4; pop reg; push reg` | Jumps over stack sequence | +| **4** | `add reg, 0` | Modifies flags, register remains unchanged | +| **4** | `nop dword ptr [rax + rax]` | 4-byte native NOP | +| **4** | `{nop2}; {nop2}` | Combined NOPs | + +--- + +## Signature Mutation Verification + +### Before Metamorphic Transformation +The original executable is detected by **65 out of 69** antivirus engines on VirusTotal, representing the baseline binary before metamorphic mutation. + +![Before Metamorphic Transformation](docs/screen_bef.png) + +> [!TIP] +> The original executable retains its initial binary signature, allowing signature-based antivirus engines to identify it consistently. + +### After Metamorphic Transformation +After applying the metamorphic engine, the executable produces a completely different binary signature while preserving the original program behavior. The transformed sample is detected by **47 out of 69** antivirus engines, demonstrating that the signature bytes have been successfully modified. + +![After Metamorphic Transformation](docs/screen_aft.png) + +> [!TIP] +> The SHA-256 hash changes entirely after metamorphic transformation, confirming that the binary signature has been altered while maintaining semantic equivalence and executable functionality. + +### Detection Comparison + +| Sample | SHA-256 | VirusTotal Detection | +| :--- | :--- | :---: | +| **Before Transformation** | `a3d5715a81f2fbeb5f76c88c9c21eeee87142909716472f911ff6950c790c24d` | **65 / 69** | +| **After Transformation** | `e3b5efb94c68a0a909f733153168cc4c3026562bb95692cbddbd7994fd14f0f1b` | **47 / 69** | + +> [!NOTE] +> The metamorphic transformation reduced VirusTotal detections by **18 engines** (approximately **27.7% fewer detections**) while preserving the executable's original functionality. This demonstrates successful mutation of signature bytes without changing program semantics. + +--- ## Installation +### Prerequisites +* **radare2**: Used for binary analysis. Please make sure it is installed and available on your system `PATH`. +* **keystone-engine**: Assembly engine used for dynamic compilation of replacement sequences. +* **r2pipe**: Python interface to radare2. +* **simplejson** (Optional performance boost): Recommended for faster JSON parsing from radare2 command outputs. + +### From Source (Recommended) +This codebase contains critical fixes for compatibility with modern versions of radare2. Installing from source is highly recommended. + +**Developer Mode:** +Installs the package in editable mode. Any changes to the source code are reflected immediately: +```bash +pip install -e . ``` + +**Regular Local Install:** +```bash +pip install . +``` + +### From PyPI +> [!WARNING] +> The version on PyPI (0.4v) may be outdated and raise errors like `Keystone assembly failed ...: 'offset'` when used with modern radare2 versions. +```bash pip install metame ``` -This should also install the requirements. +### Docker Setup +To run `metame` within a containerized environment, use the provided [Dockerfile](Dockerfile). This automatically builds the environment with the correct Radare2, Keystone, and Python libraries: -You will also need [radare2][1]. Refer to the official website for -installation instructions. +```bash +docker build -t metame . +docker run -it metame +``` -`simplejson` is also a "nice to have" for a small performance -boost: +--- -``` -pip install simplejson -``` +### Command-line Options -## Usage +| Flag | Long Flag | Description | +| :---: | :--- | :--- | +| `-i` | `--input` | **[Required]** Path to the input binary to mutate. | +| `-o` | `--output` | **[Required]** Path to save the mutated binary. | +| `-d` | `--debug` | Enable detailed debug logs (prints instruction mapping/assembly mismatches). | +| `-f` | `--force` | Force instruction replacement even if it reduces metamorphic entropy. | +| `-h` | `--help` | Show the help message and exit. | +### Example Mutation Command + +To mutate an executable with debug logging enabled: +```bash +metame -i original.exe -o mutated.exe -d ``` -metame -i original.exe -o mutation.exe -d + +To force mutations where replacement candidates are limited: +```bash +metame -i original.exe -o mutated.exe -f ``` -Use `metame -h` for help. +### Metamorphic Transformation Process -## License +The metamorphic engine analyzes the executable using **radare2**, identifies candidate instructions, and applies semantics-preserving instruction substitutions to modify the binary signature without changing the program's behavior. In this example, the engine successfully patched the executable by replacing **~ instructions**, producing a new binary with a different signature while maintaining its original functionality. -This project is released under the terms of the MIT license. +![Metamorphic Transformation Process](docs/metame-process.png) -[1]: http://radare.org/ +> [!TIP] +> The output confirms that the metamorphic engine completed the transformation successfully, reporting **53 modified instructions**. These instruction-level mutations alter the executable's byte signature while preserving its semantic behavior, demonstrating the effectiveness of metamorphic code transformation. + +--- + +## License +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000..d0c7f9e Binary files /dev/null and b/docs/logo.png differ diff --git a/docs/metame-process.png b/docs/metame-process.png new file mode 100644 index 0000000..846f85c Binary files /dev/null and b/docs/metame-process.png differ diff --git a/docs/screen_aft.png b/docs/screen_aft.png new file mode 100644 index 0000000..f9773a0 Binary files /dev/null and b/docs/screen_aft.png differ diff --git a/docs/screen_bef.png b/docs/screen_bef.png new file mode 100644 index 0000000..f8a33e2 Binary files /dev/null and b/docs/screen_bef.png differ diff --git a/metame/__init__.py b/metame/__init__.py index 334196c..dccb3a2 100644 --- a/metame/__init__.py +++ b/metame/__init__.py @@ -16,6 +16,11 @@ def main(): parser.print_help() sys.exit(1) + import os + if not os.path.exists(args.input): + print("[ERROR] Input file does not exist: %s" % args.input) + sys.exit(1) + r = r2parser.R2Parser(args.input, True, debug=args.debug, force_replace=args.force) patches = r.iterate_fcn() r.close() diff --git a/metame/r2parser.py b/metame/r2parser.py index d115778..4d5700b 100644 --- a/metame/r2parser.py +++ b/metame/r2parser.py @@ -1,5 +1,5 @@ -import metame.x86handler as x86handler +import metame.x64handler as x64handler import metame.constants as constants import r2pipe @@ -18,20 +18,22 @@ def __init__(self, filename, anal, debug=False, force_replace=False, write=False print("[INFO] Opening file with r2") self.r2 = r2pipe.open(filename, flags) info = json.loads(self.r2.cmd("ij").replace("\\", "\\\\")) - if "bin" not in info.keys(): - raise Exception("[ERROR] File type not supported") - if not info["bin"]["bits"] in constants.supported_bits or \ - not info["bin"]["arch"] in constants.supported_archs: - raise Exception("[ERROR] Architecture not supported") - self.arch = info["bin"]["arch"] - self.bits = info["bin"]["bits"] + bin_info = info.get("bin", {}) + if not bin_info: + raise Exception("[ERROR] File type not supported or binary metadata missing") + self.arch = bin_info.get("arch") + self.bits = bin_info.get("bits") + if not self.arch or not self.bits: + raise Exception("[ERROR] Architecture or bits details missing in file metadata") + if self.bits not in constants.supported_bits or self.arch not in constants.supported_archs: + raise Exception("[ERROR] Architecture (%s) or bits (%s) not supported" % (self.arch, self.bits)) if anal: print("[INFO] Analyzing functions with r2") self.r2.cmd("aaa") def iterate_fcn(self): if self.arch == "x86": - arch = x86handler.X86Handler(self.bits, self.debug, self.force) + arch = x64handler.X64Handler(self.bits, self.debug, self.force) replacements = [] print("[INFO] Loading functions information") fcns = json.loads(self.r2.cmd("aflj")) @@ -40,9 +42,9 @@ def iterate_fcn(self): if f["type"] == "fcn": try: fcn_ctx = json.loads(self.r2.cmd("pdfj @%s" % f["name"])) - except: - print("[ERROR] Error disassembling function %s" % f["name"]) - replacements += arch.replace_fcn_opcodes(fcn_ctx) + replacements += arch.replace_fcn_opcodes(fcn_ctx) + except Exception as e: + print("[ERROR] Error disassembling function %s: %s" % (f["name"], e)) return replacements def patch_binary(self, patches): diff --git a/metame/x64handler.py b/metame/x64handler.py new file mode 100644 index 0000000..7531596 --- /dev/null +++ b/metame/x64handler.py @@ -0,0 +1,385 @@ +import re +import random +from typing import Dict, List, Tuple, Set, Pattern, Optional, Any +from keystone import * + +class X64Handler: + def normalize_opcode(self, opcode: str) -> str: + # Convert to lowercase and strip outer spaces + opcode = opcode.lower().strip() + # Collapse multiple spaces or tabs into a single space + opcode = re.sub(r"\s+", " ", opcode) + # Ensure exactly one space after commas + opcode = re.sub(r"\s*,\s*", ", ", opcode) + return opcode + + def get_nops(self, size: int, addr: int = 0) -> str: + if self.bits == 32: + regs = ["eax", "ebx", "ecx", "edx", "esi", "edi"] + else: + regs = ["rax", "rbx", "rcx", "rdx", "rsi", "rdi"] + + if size == 1: + return "nop" + elif size == 2: + if self.bits == 32: + r = random.randint(1, 4) + if r == 1: + reg = random.choice(regs) + return f"push {reg}; pop {reg}" + elif r == 2: + return "pushad; popad" + elif r == 3: + return "xchg ax, ax" + else: + return "nop; nop" + else: + r = random.randint(1, 3) + if r == 1: + reg = random.choice(regs) + return f"push {reg}; pop {reg}" + elif r == 2: + return "xchg ax, ax" + else: + return "nop; nop" + elif size == 3: + if self.bits == 32: + r = random.randint(1, 6) + if r == 1: + return f"jmp {addr + 3}; inc {random.choice(regs)}" + elif r == 2: + return f"jmp {addr + 3}; push {random.choice(regs)}" + elif r == 3: + return f"jmp {addr + 3}; pop {random.choice(regs)}" + elif r == 4: + return "nop dword ptr [eax]" + elif r == 5: + return f"nop; {self.get_nops(2, addr + 1)}" + else: + return f"{self.get_nops(2, addr)}; nop" + else: + r = random.randint(1, 6) + if r == 1: + reg = random.choice(regs) + return f"push {reg}; pop {reg}; nop" + elif r == 2: + reg = random.choice(regs) + return f"nop; push {reg}; pop {reg}" + elif r == 3: + return f"jmp {addr + 3}; push {random.choice(regs)}" + elif r == 4: + return "nop dword ptr [rax]" + elif r == 5: + return f"nop; {self.get_nops(2, addr + 1)}" + else: + return f"{self.get_nops(2, addr)}; nop" + elif size == 4: + if self.bits == 32: + r = random.randint(1, 6) + if r == 1: + return f"jmp {addr + 4}; pop {random.choice(regs)}; pop {random.choice(regs)}" + elif r == 2: + return f"jmp {addr + 4}; push {random.choice(regs)}; push {random.choice(regs)}" + elif r == 3: + return f"jmp {addr + 4}; push {random.choice(regs)}; pop {random.choice(regs)}" + elif r == 4: + reg = random.choice(regs) + return f"xor {reg}, 0; nop" + elif r == 5: + return "nop dword ptr [eax + eax]" + else: + return f"{self.get_nops(2, addr)}; {self.get_nops(2, addr + 2)}" + else: + r = random.randint(1, 7) + if r == 1: + return f"jmp {addr + 4}; pop {random.choice(regs)}; pop {random.choice(regs)}" + elif r == 2: + return f"jmp {addr + 4}; push {random.choice(regs)}; push {random.choice(regs)}" + elif r == 3: + return f"jmp {addr + 4}; push {random.choice(regs)}; pop {random.choice(regs)}" + elif r == 4: + return f"jmp {addr + 4}; pop {random.choice(regs)}; push {random.choice(regs)}" + elif r == 5: + reg = random.choice(regs) + return f"add {reg}, 0" + elif r == 6: + return "nop dword ptr [rax + rax]" + else: + return f"{self.get_nops(2, addr)}; {self.get_nops(2, addr + 2)}" + + return "; ".join(["nop"] * size) + + def __init__(self, bits: int, debug: bool = False, force_replace: bool = False): + self.bits = bits + self.debug = debug + self.force = force_replace + ks_mode = KS_MODE_32 if self.bits == 32 else KS_MODE_64 + self.ks = Ks(KS_ARCH_X86, ks_mode) + self.init_mutations() + + def init_mutations(self): + if self.bits == 32: + self.mutables = frozenset(["nop", "acmp", "or", "xor", "sub", "mov", "push", "add", "shl", "je", "jz", "jne", "jnz"]) + raw_subs = [ + ( + ((r"^mov (?Pe..), (?P(?P=a))$",), "mov {a}, {b}", True), + ((), "{nop2}", False), + ), + ( + ((r"^add (?Pe..), 1$",), "add {a}, 1", True), + ((), "inc {a}; {nop2}", False), + ), + ( + ((r"^sub (?Pe..), 1$",), "sub {a}, 1", True), + ((), "dec {a}; {nop2}", False), + ), + ( + ((r"^shl (?Pe..), 1$",), "shl {a}, 1", True), + ((r"^add (?Pe..), (?P(?P=a))$",), "add {a}, {a}", True), + ), + ( + ((r"^nop$", r"^nop$", r"^nop$"), "nop; nop; nop", True), + ((), "{nop3}", False), + ), + ( + ((r"^nop$", r"^nop$"), "nop; nop", True), + ((), "{nop2}", False), + ), + ( + ((r"^test (?Pe..), (?P(?P=a))$",), "test {a}, {b}", True), + ((r"^or (?Pe..), (?P(?P=a))$",), "or {a}, {b}", True), + ), + ( + ((r"^xor (?Pe..), (?P(?P=a))$",), "xor {a}, {b}", True), + ((r"^sub (?Pe..), (?P(?P=a))$",), "sub {a}, {b}", True), + ), + ( + ((r"^mov (?Pe..), (?Pe..)$",), "mov {a}, {b}", True), + ((r"^push (?Pe..)$", r"^pop (?Pe..)$"), "push {b}; pop {a}", True), + ), + ( + ((r"^mov (?Pe..), (?P0?x?0)$",), "mov {a}, {b}", True), + ((), "pushfd; xor {a}, {a}; popfd; {nop1}", False), + ((), "pushfd; sub {a}, {a}; popfd; {nop1}", False), + ((), "pushfd; and {a}, 0; popfd", False), + ), + ( + ((r"^mov (?Pe..), (?P0?x?1)$",), "mov {a}, {b}", True), + ((), "pushfd; xor {a}, {a}; inc {a}; popfd", False), + ), + ( + ((r"^mov (?Pe..), (?P0?x?([0-7][0-9A-Fa-f]|[0-9A-Fa-f]))$",), "mov {a}, {b}", True), + ((), "push {b}; pop {a}; {nop2}", False), + ((), "{nop2}; push {b}; pop {a}", False), + ((), "{nop1}; push {b}; {nop1}; pop {a}", False), + ), + # Control Flow Branch swap + ( + ((r"^je (?P.+)$",), "je {a}", True), + ((r"^jz (?P.+)$",), "jz {a}", True), + ), + ( + ((r"^jne (?P.+)$",), "jne {a}", True), + ((r"^jnz (?P.+)$",), "jnz {a}", True), + ), + ] + else: + self.mutables = frozenset(["nop", "acmp", "or", "xor", "sub", "mov", "add", "shl", "je", "jz", "jne", "jnz"]) + raw_subs = [ + ( + ((r"^mov (?Pe..), (?P(?P=a))$",), "mov {a}, {b}", True), + ((), "{nop2}", False), + ), + ( + ((r"^add (?Pe..), 1$",), "add {a}, 1", True), + ((), "inc {a}; {nop2}", False), + ), + ( + ((r"^sub (?Pe..), 1$",), "sub {a}, 1", True), + ((), "dec {a}; {nop2}", False), + ), + ( + ((r"^shl (?Pe..), 1$",), "shl {a}, 1", True), + ((r"^add (?Pe..), (?P(?P=a))$",), "add {a}, {a}", True), + ), + ( + ((r"^nop$", r"^nop$", r"^nop$"), "nop; nop; nop", True), + ((), "{nop3}", False), + ), + ( + ((r"^nop$", r"^nop$"), "nop; nop", True), + ((), "{nop2}", False), + ), + ( + ((r"^test (?Pe..), (?P(?P=a))$",), "test {a}, {b}", True), + ((r"^or (?Pe..), (?P(?P=a))$",), "or {a}, {b}", True), + ), + ( + ((r"^xor (?Pe..), (?P(?P=a))$",), "xor {a}, {b}", True), + ((r"^sub (?Pe..), (?P(?P=a))$",), "sub {a}, {b}", True), + ), + # Purely 64 bits payloads + ( + ((r"^add (?Pr..), 1$",), "add {a}, 1", True), + ((), "inc {a}; {nop1}", False), + ), + ( + ((r"^sub (?Pr..), 1$",), "sub {a}, 1", True), + ((), "dec {a}; {nop1}", False), + ), + ( + ((r"^shl (?Pr..), 1$",), "shl {a}, 1", True), + ((r"^add (?Pr..), (?P(?P=a))$",), "add {a}, {a}", True), + ), + ( + ((r"^test (?Pr..), (?P(?P=a))$",), "test {a}, {b}", True), + ((r"^or (?Pr..), (?P(?P=a))$",), "or {a}, {b}", True), + ), + ( + ((r"^xor (?Pr..), (?P(?P=a))$",), "xor {a}, {b}", True), + ((r"^sub (?Pr..), (?P(?P=a))$",), "sub {a}, {b}", True), + ), + ( + ((r"^mov (?Pr.(i|x|p)), (?Pr.(i|x|p))$",), "mov {a}, {b}", True), + ((), "push {b}; pop {a}; {nop1}", False), + ((), "{nop1}; push {b}; pop {a}", False), + ((), "push {b}; {nop1}; pop {a}", False), + ), + # Control Flow Branch swap + ( + ((r"^je (?P.+)$",), "je {a}", True), + ((r"^jz (?P.+)$",), "jz {a}", True), + ), + ( + ((r"^jne (?P.+)$",), "jne {a}", True), + ((r"^jnz (?P.+)$",), "jnz {a}", True), + ), + ] + + # Compile and group substitution rules by the mnemonic of their first instruction + self.X64_SUBS: Dict[str, List[Tuple[Tuple[Any, ...], Tuple[Any, ...]]]] = {} + for sub_rule in raw_subs: + compiled_rule = [] + for eq in sub_rule: + patterns, asm_fmt, is_match = eq + compiled_patterns = tuple(re.compile(p, re.IGNORECASE) for p in patterns) + compiled_rule.append((compiled_patterns, asm_fmt, is_match)) + compiled_rule_tuple = tuple(compiled_rule) + + for eq in compiled_rule_tuple: + compiled_patterns, asm_fmt, is_match = eq + if is_match: + first_pattern = compiled_patterns[0].pattern + m = re.match(r"^\^([a-zA-Z0-9]+)", first_pattern) + if m: + mnemonic = m.group(1) + if mnemonic not in self.X64_SUBS: + self.X64_SUBS[mnemonic] = [] + self.X64_SUBS[mnemonic].append((eq, compiled_rule_tuple)) + + def assemble_code(self, codestr: str, addr: int = 0) -> str: + encoding, count = self.ks.asm(codestr, addr) + return "".join(["%02x" % i for i in encoding]) + + def replace_fcn_opcodes(self, fcn_ctx: Dict[str, Any]) -> List[Dict[str, Any]]: + replacements = [] + if not fcn_ctx or "ops" not in fcn_ctx: + return replacements + + count = -1 + n_ops = len(fcn_ctx["ops"]) + while count < n_ops - 1: + count += 1 + op = fcn_ctx["ops"][count] + + opcode_str = op.get("opcode", "") + if not opcode_str: + continue + + opcode_str = self.normalize_opcode(opcode_str) + mnemonic = opcode_str.split()[0] + if mnemonic not in self.X64_SUBS: + continue + + candidates = self.X64_SUBS[mnemonic] + for x86_find, x86_sub in candidates: + count_2 = 0 + ms = [] + opcodes_len = 0 + match_failed = False + + for regex in x86_find[0]: + if count + count_2 >= n_ops: + match_failed = True + break + + next_op = fcn_ctx["ops"][count + count_2] + next_opcode = next_op.get("opcode", "") + if not next_opcode: + match_failed = True + break + + next_opcode = self.normalize_opcode(next_opcode) + m = regex.match(next_opcode) + if not m: + match_failed = True + break + + ms.append(m) + op_bytes = next_op.get("bytes", "") + opcodes_len += len(op_bytes) + count_2 += 1 + + if match_failed: + continue + + # Shuffle candidate replacements to find one that succeeds and matches size + shuffled_sub = list(x86_sub) + random.shuffle(shuffled_sub) + + success = False + for sub in shuffled_sub: + if self.force and sub == x86_find: + continue + + res_ass = sub[1] + for m in ms: + for idx, val in m.groupdict().items(): + if val is not None: + res_ass = res_ass.replace(f"{{{idx}}}", val.lower()) + + # Get instruction offset/address (supports both 'offset' and 'addr' formats) + op_offset = op.get("offset", op.get("addr")) + + # Dynamically resolve NOP placeholders + def nop_resolver(match_obj): + nop_size = int(match_obj.group(1)) + return self.get_nops(nop_size, op_offset) + + res_ass = re.sub(r"\{nop(\d+)\}", nop_resolver, res_ass) + + try: + new_assembly = self.assemble_code(res_ass, op_offset) + except Exception as e: + if self.debug: + import traceback + traceback.print_exc() + print(f"[DEBUG] Keystone assembly failed for '{res_ass}': {e}") + continue + + if len(new_assembly) == opcodes_len: + replacements.append({ + "offset": op_offset, + "newbytes": new_assembly + }) + count += count_2 - 1 + success = True + break + else: + if self.debug: + print(f"[DEBUG] Size mismatch for '{res_ass}' (new: {len(new_assembly)//2}, expected: {opcodes_len//2})") + + if success: + break + + return replacements diff --git a/metame/x86handler.py b/metame/x86handler.py deleted file mode 100644 index e5ccafe..0000000 --- a/metame/x86handler.py +++ /dev/null @@ -1,242 +0,0 @@ - -import metame.constants as constants - -import re -import random -from keystone import * - -class X86Handler: - def get_nops(self, size, prev_ins_size=0): - if self.bits == 32: - regs = ["eax", "ebx", "ecx", "edx", "esi", "edi"] - else: - regs = ["rax", "rbx", "rcx", "rdx", "rsi", "rdi"] - if size == 1: - return "nop" - elif size == 2 and self.bits == 32: - r = random.randint(1, 3) - if r == 1: - reg = random.choice(regs) - return "push %s; pop %s" % (reg, reg) - elif r == 2: - return "pushad; popad" - elif r == 3: - return "%s; %s" % (self.get_nops(1), self.get_nops(1)) - elif size == 3 and self.bits == 32: - r = random.randint(1, 5) - if r == 1: - return "jmp %s; inc %s" % (3 + prev_ins_size, random.choice(regs)) - elif r == 2: - return "jmp %s; push %s" % (3 + prev_ins_size, random.choice(regs)) - elif r == 3: - return "jmp %s; pop %s" % (3 + prev_ins_size, random.choice(regs)) - elif r == 4: - return "%s; %s" % (self.get_nops(1), self.get_nops(2)) - elif r == 5: - return "%s; %s" % (self.get_nops(2), self.get_nops(1)) - elif size == 2 and self.bits == 64: - reg = random.choice(regs) - return "push %s; pop %s" % (reg, reg) - elif size == 3 and self.bits == 64: - r = random.randint(1, 4) - if r == 1: - reg = random.choice(regs) - return "push %s; pop %s; %s" % (reg, reg, self.get_nops(1)) - elif r == 2: - reg = random.choice(regs) - return "%s; push %s; pop %s" % (self.get_nops(1), reg, reg) - elif r == 3: - return "%s; %s" % (self.get_nops(1), self.get_nops(2)) - elif r == 4: - return "%s; %s" % (self.get_nops(2), self.get_nops(1)) - elif size == 4 and self.bits == 64: - r = random.randint(1, 5) - if r == 1: - return "jmp %s; pop %s; pop %s" % (4 + prev_ins_size, random.choice(regs), - random.choice(regs)) - elif r == 2: - return "jmp %s; push %s; push %s" % (4 + prev_ins_size, random.choice(regs), - random.choice(regs)) - elif r == 3: - return "jmp %s; push %s; pop %s" % (4 + prev_ins_size, random.choice(regs), - random.choice(regs)) - elif r == 4: - return "jmp %s; pop %s; push %s" % (4 + prev_ins_size, random.choice(regs), - random.choice(regs)) - elif r == 5: - return "%s; %s" % (self.get_nops(2), self.get_nops(2)) - - def __init__(self, bits, debug=False, force_replace=False): - self.bits = bits - self.debug = debug - self.force = force_replace - ks_mode = KS_MODE_32 if self.bits == 32 else KS_MODE_64 - self.ks = Ks(KS_ARCH_X86, ks_mode) - self.init_mutations() - - def init_mutations(self): - if self.bits == 32: - self.mutables = frozenset(["nop","acmp","or","xor","sub","mov","push"]) - self.X86_SUBS = [ - ( - ((re.compile(r"^mov (?Pe..), (?P(?P=a))$"),), "mov {a}, {b}", True), - ((), "%s" % self.get_nops(2), False), - ), - ( - ((re.compile(r"^nop$"),re.compile(r"^nop$"),re.compile(r"^nop$")), "nop; nop; nop", True), - ((), "%s" % self.get_nops(3), False), - ), - ( - ((re.compile(r"^nop$"),re.compile(r"^nop$")), "nop; nop", True), - ((), "%s" % self.get_nops(2), False), - ), - ( - ((re.compile(r"^test (?Pe..), (?P(?P=a))$"),), "test {a}, {b}", True), - ((re.compile(r"^or (?Pe..), (?P(?P=a))$"),), "or {a}, {b}", True), - ), - ( - ((re.compile(r"^xor (?Pe..), (?P(?P=a))$"),), "xor {a}, {b}", True), - ((re.compile(r"^sub (?Pe..), (?P(?P=a))$"),), "sub {a}, {b}", True), - ), - ( - ((re.compile(r"^mov (?Pe..), (?Pe..)$"),), "mov {a}, {b}", True), - ((re.compile(r"^push (?Pe..)$"),re.compile(r"^pop (?Pe..)$")), "push {b}; pop {a}", True), - ), - ( - ((re.compile(r"^mov (?Pe..), (?P0?x?0)$"),), "mov {a}, {b}", True), - ((), "pushfd; xor {a}, {a}; popfd; %s" % self.get_nops(1), False), - ((), "pushfd; sub {a}, {a}; popfd; %s" % self.get_nops(1), False), - ((), "pushfd; and {a}, 0; popfd", False), - ), - ( - ((re.compile(r"^mov (?Pe..), (?P0?x?1)$"),), "mov {a}, {b}", True), - ((), "pushfd; xor {a}, {a}; inc {a}; popfd", False), - ), - ( - ((re.compile(r"^mov (?Pe..), (?P0?x?([0-7][0-9A-Fa-f]|[0-9A-Fa-f]))$"),), "mov {a}, {b}", True), - ((), "push {b}; pop {a}; %s" % self.get_nops(2), False), - ((), "%s; push {b}; pop {a}" % self.get_nops(2), False), - ((), "%s; push {b}; %s; pop {a}" % (self.get_nops(1), self.get_nops(1)), False), - ), - ] - else: - self.mutables = frozenset(["nop","acmp","or","xor","sub","mov"]) - self.X86_SUBS = [ - # Variations of 32 bits payloads - ( - ((re.compile(r"^mov (?Pe..), (?P(?P=a))$"),), "mov {a}, {b}", True), - ((), "%s" % self.get_nops(2), False), - ), - ( - ((re.compile(r"^nop$"),re.compile(r"^nop$"),re.compile(r"^nop$")), "nop; nop; nop", True), - ((), "%s" % self.get_nops(3), False), - ), - ( - ((re.compile(r"^nop$"),re.compile(r"^nop$")), "nop; nop", True), - ((), "%s" % self.get_nops(2), False), - ), - ( - ((re.compile(r"^test (?Pe..), (?P(?P=a))$"),), "test {a}, {b}", True), - ((re.compile(r"^or (?Pe..), (?P(?P=a))$"),), "or {a}, {b}", True), - ), - ( - ((re.compile(r"^xor (?Pe..), (?P(?P=a))$"),), "xor {a}, {b}", True), - ((re.compile(r"^sub (?Pe..), (?P(?P=a))$"),), "sub {a}, {b}", True), - ), - # Purely 64 bits payloads - ( - ((re.compile(r"^test (?Pr..), (?P(?P=a))$"),), "test {a}, {b}", True), - ((re.compile(r"^or (?Pr..), (?P(?P=a))$"),), "or {a}, {b}", True), - ), - ( - ((re.compile(r"^xor (?Pr..), (?P(?P=a))$"),), "xor {a}, {b}", True), - ((re.compile(r"^sub (?Pr..), (?P(?P=a))$"),), "sub {a}, {b}", True), - ), - ( - ((re.compile(r"^mov (?Pr.(i|x|p)), (?Pr.(i|x|p))$"),), "mov {a}, {b}", True), - ((), "push {b}; pop {a}; %s" % self.get_nops(1), False), - ((), "%s; push {b}; pop {a}" % self.get_nops(1), False), - ((), "push {b}; %s; pop {a}" % self.get_nops(1), False), - ), - ] - - def assemble_code(self, codestr): - encoding, count = self.ks.asm(codestr) - return "".join(["%02x" % i for i in encoding]) - - def replace_fcn_opcodes(self, fcn_ctx): - replacements = [] - # Iternate instructions - count = -1 - n_ops = len(fcn_ctx["ops"]) - while count < n_ops-1: - count += 1 - if fcn_ctx["ops"][count].get("type") not in self.mutables: - continue - # Iternate possible substitutions - for x86_sub in self.X86_SUBS: - # Iterate equivalences in substitution - for x86_find in x86_sub: - # Use this equivalence as match, or only as replacement? - if not x86_find[2]: - continue - count_2 = 0 - ms = [] - opcodes_len = 0 - # Iterate needed matches - for x86_m in x86_find[0]: - try: - # Match - m = x86_m.match(fcn_ctx["ops"][count+count_2]["opcode"]) - if not m: - break - except: - break - # Store matches - ms.append(m) - # Increase opcodes size - opcodes_len += len(fcn_ctx["ops"][count+count_2]["bytes"]) - count_2 += 1 - else: - # Previous iteration was completed, so all needed matches matched - # Choose a random substitution - sub = random.choice(x86_sub) - # If forced, force finding a different substitution - while self.force and sub == x86_find: - sub = random.choice(x86_sub) - # Random substitution is the same, do nothing - if sub == x86_find: - continue - res_ass = sub[1] - # Create assembly replacing match groups - for m in ms: - for idx in m.groupdict().keys(): - res_ass = res_ass.replace("{%s}" % idx, m.groupdict()[idx]) - if self.debug: - print("[DEBUG] Replacing instruction at %s (%s) with: %s ... " % ( - hex(fcn_ctx["ops"][count]["offset"]), - fcn_ctx["ops"][count]["opcode"], - res_ass)) - # Assemble new code - new_assembly = self.assemble_code(res_ass) - # Check if new assembly is equal in size - if len(new_assembly) == opcodes_len: - replacements.append({"offset": fcn_ctx["ops"][count]["offset"], - "newbytes": new_assembly}) - # Avoid patching over again - count += count_2 - 1 - # Restart mutations to reseed random nops - self.init_mutations() - break - else: - if self.debug: - print("[DEBUG] Instruction opcodes are different in size") - else: - # Previous iteration failed, continue finding substitutions - # for this instruction - continue - # Previous iteration was completed correctly, stop - # finding substitutions for this instruction - break - return replacements - diff --git a/screens/screen1.png b/screens/screen1.png deleted file mode 100644 index e82c105..0000000 Binary files a/screens/screen1.png and /dev/null differ diff --git a/screens/screen2.png b/screens/screen2.png deleted file mode 100644 index 7f34502..0000000 Binary files a/screens/screen2.png and /dev/null differ diff --git a/setup.py b/setup.py index e08bfdf..108b664 100755 --- a/setup.py +++ b/setup.py @@ -1,9 +1,9 @@ -from distutils.core import setup +from setuptools import setup setup( name = 'metame', packages = ['metame'], - version = '0.4', + version = '0.5', description = 'metame is a metamorphic code engine for arbitrary executables', author = 'Alberto Ortega', author_email = 'aortega.lms+metame@gmail.com',