diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..220442ff
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,21 @@
+.git
+.github
+.vs
+.vscode
+bin/
+obj/
+Debug/
+Release/
+publish/
+publish-net8.0/
+*.sln.user
+*.suo
+*.user
+*.pdb
+*.xml
+*.ps1
+build-beaengine-32/
+build-beaengine-64/
+build64/
+beaengine/
+deb-root/
diff --git a/.gitignore b/.gitignore
index 5d5d6e94..5b98c0f4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,5 @@ publish-netcoreapp*/
publish-net*/
launchSettings.json
*.user
+*.skill
+pr_body.md
diff --git a/De4DotCommon.props b/De4DotCommon.props
index 9a042a11..10cb099c 100644
--- a/De4DotCommon.props
+++ b/De4DotCommon.props
@@ -16,8 +16,13 @@
true
4.5.0
-
+
true
+
+
+
+
+
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..ef73dac5
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,79 @@
+# ==============================================================================
+# STAGE 1: Build BeaEngine (Native Library for ConfuserEx control flow cleaning)
+# ==============================================================================
+FROM ubuntu:22.04 AS beaengine-builder
+ENV DEBIAN_FRONTEND=noninteractive
+
+RUN apt-get update && apt-get install -y \
+ git \
+ cmake \
+ build-essential \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /src
+RUN git clone --depth 1 --branch v5.3.0 https://github.com/BeaEngine/beaengine.git
+
+RUN cmake -S beaengine -B build64 \
+ -DoptBUILD_DLL=ON \
+ -DoptHAS_OPTIMIZED=ON \
+ -DoptHAS_SYMBOLS=OFF
+
+RUN cmake --build build64
+
+# Dynamically locate and move the compiled .so to a standardized path
+RUN mkdir -p /app && \
+ SO_FILE=$(find build64 -name "libBeaEngine*.so" | head -n 1) && \
+ if [ -n "$SO_FILE" ]; then cp "$SO_FILE" /app/libBeaEngine.so; else echo "Error: libBeaEngine.so not found!" && exit 1; fi
+
+# ==============================================================================
+# STAGE 2: Build de4dotEx (.NET 8.0 Cross-Platform Release)
+# ==============================================================================
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS dotnet-builder
+WORKDIR /src
+
+# Copy all files to build
+COPY . .
+
+# Publish both de4dot and the de4dot.mcp server targeting net8.0
+RUN dotnet publish -c Release -f net8.0 -o /app/publish/de4dot de4dot/de4dot.csproj
+RUN dotnet publish -c Release -f net8.0 -o /app/publish/mcp de4dot.mcp/de4dot.mcp.csproj
+RUN rm -rf /app/publish/**/*.pdb /app/publish/**/*.xml
+
+# ==============================================================================
+# STAGE 3: Final Runtime Image (.NET 8.0 on Ubuntu / Runtime)
+# ==============================================================================
+FROM mcr.microsoft.com/dotnet/runtime:8.0 AS runtime
+WORKDIR /app
+
+# Install native dependencies required for execution (e.g. globalization, bash)
+RUN apt-get update && apt-get install -y \
+ libicu-dev \
+ bash \
+ && rm -rf /var/lib/apt/lists/*
+
+# Copy de4dot and MCP published folders
+COPY --from=dotnet-builder /app/publish/de4dot ./de4dot
+COPY --from=dotnet-builder /app/publish/mcp ./mcp
+
+# Copy compiled libBeaEngine.so from Stage 1 standardized path to system libraries & register
+COPY --from=beaengine-builder /app/libBeaEngine.so /usr/local/lib/libBeaEngine.so
+RUN ldconfig
+
+# Create symlinks for global accessibility
+RUN ln -s /app/de4dot/de4dot /usr/local/bin/de4dot && \
+ ln -s /app/mcp/de4dot.mcp /usr/local/bin/de4dot-mcp
+
+# Create a unified entrypoint script inside the final image
+RUN printf '#!/bin/bash\n\
+if [ "$1" = "--mcp" ] || [ "$1" = "-mcp" ]; then\n\
+ shift\n\
+ exec /app/mcp/de4dot.mcp "$@"\n\
+else\n\
+ exec /app/de4dot/de4dot "$@"\n\
+fi\n' > /app/entrypoint.sh && chmod +x /app/entrypoint.sh
+
+# Expose HTTP port 8080 (for MCP Web API mode)
+EXPOSE 8080
+
+ENTRYPOINT ["/app/entrypoint.sh"]
+CMD ["--help"]
diff --git a/Dockerfile.wine b/Dockerfile.wine
new file mode 100644
index 00000000..125c6702
--- /dev/null
+++ b/Dockerfile.wine
@@ -0,0 +1,78 @@
+# ==============================================================================
+# STAGE 1: Build the .NET Framework 4.8 Targets natively on Linux
+# ==============================================================================
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS dotnet-framework-builder
+WORKDIR /src
+
+# Copy all files
+COPY . .
+
+# Compile de4dot.netframework.sln targeting net48 in Release mode
+# Disable RuntimeIdentifierInference and ValidateExecutableRuntimeIdentifier to bypass architecture compatibility checks on ARM64 hosts
+RUN dotnet build -c Release \
+ -p:SolutionName=de4dot.netframework \
+ -p:RuntimeIdentifierInference=false \
+ -p:ValidateExecutableRuntimeIdentifier=false \
+ de4dot.netframework.sln
+
+# ==============================================================================
+# STAGE 2: Package into Headless Wine Runtime Environment
+# ==============================================================================
+FROM ubuntu:22.04
+ENV DEBIAN_FRONTEND=noninteractive
+
+# Install dependencies, 32-bit architecture, and Wine from standard Ubuntu repositories
+# On ARM64 (Apple Silicon Mac), we restrict standard mirrors to arm64 to prevent apt-get update 404 failures on i386 lists.
+RUN ARCH=$(dpkg --print-architecture) && \
+ if [ "$ARCH" = "arm64" ]; then sed -i 's/^deb /deb [arch=arm64] /g' /etc/apt/sources.list; fi && \
+ dpkg --add-architecture i386 && \
+ apt-get update && apt-get install -y \
+ software-properties-common \
+ wget \
+ gnupg2 \
+ cabextract \
+ xvfb \
+ unzip \
+ wine64 \
+ wine \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install winetricks
+RUN wget -O /usr/local/bin/winetricks https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks && \
+ chmod +x /usr/local/bin/winetricks
+
+# Configure wine environment
+ENV WINEARCH=win64
+ENV WINEPREFIX=/root/.wine
+ENV DISPLAY=:0
+
+# Pre-download official Wine Mono MSI into Wine's global shared directory.
+# During wineboot -u, Wine will automatically find and register this package.
+RUN mkdir -p /usr/share/wine/mono && \
+ wget -q -O /usr/share/wine/mono/wine-mono-7.0.0-x86.msi https://dl.winehq.org/wine/wine-mono/7.0.0/wine-mono-7.0.0-x86.msi
+
+# Initialize wine prefix silently using Xvfb
+RUN Xvfb :0 -screen 0 1024x768x16 & \
+ sleep 2 && \
+ wineboot -u && \
+ wineserver -w
+
+# Set working directory inside the Wine prefix or a dedicated folder
+WORKDIR /app
+
+# Copy the compiled .NET 4.8 targets directly from Stage 1 (dotnet-framework-builder)
+COPY --from=dotnet-framework-builder /src/Release/net48/ .
+
+# Create helper script to launch de4dot through Wine in a virtual frame buffer
+RUN echo '#!/bin/bash\n\
+# Launch virtual framebuffer silently in the background if not already running\n\
+Xvfb :0 -screen 0 1024x768x16 >/dev/null 2>&1 & \n\
+sleep 1\n\
+wine /app/de4dot.exe "$@"' > /usr/local/bin/de4dot && \
+ chmod +x /usr/local/bin/de4dot
+
+# Define default volume for binaries to deobfuscate
+VOLUME ["/work"]
+WORKDIR /work
+
+ENTRYPOINT ["de4dot"]
diff --git a/README.md b/README.md
index d15d95a0..14fc8eb1 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,14 @@ Binaries
Get binaries from the build server [](https://github.com/GDATAAdvancedAnalytics/de4dotEx/actions).
+Docker & MCP Server Support
+===========================
+
+de4dotEx now has fully integrated **Docker containerization** and native **Model Context Protocol (MCP)** server support:
+
+* **Docker Container:** Build a single container to run de4dotEx natively, as a local stdio MCP server, or as an HTTP Web API microservice. See the [Docker Containerization](#docker-containerization) section below for build and run instructions.
+* **MCP Server:** Integrate de4dotEx directly with AI assistants like Claude Desktop, Cursor, or Windsurf to programmatically deobfuscate binaries. See the [Native C# MCP Server](#native-c-mcp-server) section below for configuration instructions.
+
It's FREE but there's NO SUPPORT
================================
@@ -215,3 +223,195 @@ Other options
-------------
Start `de4dot` without any arguments and it will show all options.
+
+
+Docker Containerization
+=======================
+
+de4dotEx includes built-in cross-platform Docker containerization. You can build a single, lightweight container and run it in three different modes: as a standard command-line utility, as an HTTP Web API microservice, or as a containerized Stdio MCP server.
+
+We offer two different Docker strategies depending on your needs:
+1. **Strategy A (Native .NET 8.0 - Recommended):** Extremely fast, lightweight, and cross-platform. It automatically compiles the native C++ `BeaEngine` disassembler library inside the container so that advanced ConfuserEx deobfuscation works natively on Linux and macOS (ARM64/x64).
+2. **Strategy B (Wine + .NET Framework 4.8):** Emulates a full Windows environment to support dynamic JIT-hook protectors (like ILProtector and Agile.NET) which rely on Windows-native memory APIs (`VirtualAlloc`, `VirtualProtect`).
+
+Strategy A: Native .NET 8.0 Linux Container (Recommended)
+--------------------------------------------------------
+
+### 1. Build the Native Image
+Execute the following command in the root folder of the repository:
+```bash
+docker build -t de4dotex -f Dockerfile .
+```
+
+### 2. Run the Native Image
+The container has an intelligent, unified entrypoint script that automatically routes execution depending on the arguments you pass:
+
+#### A. Run as standard de4dot CLI:
+```bash
+# Run help
+docker run --rm de4dotex --help
+
+# Deobfuscate an assembly in your current directory (mounts pwd to /work)
+docker run --rm -v "$(pwd):/work" -w /work de4dotex MyObfuscatedApp.exe -o CleanApp.exe
+```
+
+#### B. Run as containerized HTTP Web API (on port 8080):
+Start the container in the background to spin up a high-performance HTTP microservice:
+```bash
+# Start the HTTP Microservice in background
+docker run -d -p 8080:8080 --name de4dotex-api de4dotex --mcp --http
+
+# Upload an obfuscated file and download the cleaned output
+curl -F "file=@MyObfuscatedApp.dll" http://localhost:8080/deobfuscate -o CleanApp.dll
+
+# Upload with custom native flags (e.g. preserve tokens, decrypt string delegates)
+curl -F "file=@MyObfuscatedApp.dll" \
+ -F "options=--preserve-tokens -str delegate" \
+ http://localhost:8080/deobfuscate -o CleanApp.dll
+```
+
+You can also use our convenient test script **`test_api.sh`** to automatically perform the upload and save the processed output next to the original file, appending `_cleaned` (e.g. `App.dll` -> `App_cleaned.dll`):
+```bash
+# Basic usage:
+./test_api.sh MyObfuscatedApp.dll
+
+# Usage with custom flags:
+./test_api.sh MyObfuscatedApp.dll "--preserve-tokens -str delegate"
+```
+
+#### C. Run as containerized Stdio MCP server (for AI clients):
+To run the containerized MCP server directly via Claude Desktop or Cursor, configure your MCP client configuration as follows:
+```json
+{
+ "mcpServers": {
+ "de4dotex-docker": {
+ "command": "docker",
+ "args": [
+ "run",
+ "-i",
+ "--rm",
+ "de4dotex",
+ "--mcp"
+ ]
+ }
+ }
+}
+```
+*(The `-i` flag is required to keep standard input open so the JSON-RPC streams can communicate securely.)*
+
+
+Strategy B: Wine-based Container (Full Windows Compatibility)
+------------------------------------------------------------
+
+This strategy configures a headless Wine environment on Ubuntu, installs the official .NET Framework runtime via `winetricks`, and executes the Windows `.NET Framework 4.8` assemblies under Wine.
+
+**Fully Automated Build:**
+Unlike older setups, this container uses a **multi-stage build** with a **Mono SDK** builder stage (`mono:6.12.0`) to automatically restore and compile the `.NET Framework 4.8` solution (`de4dot.netframework.sln`) inside the container. **You do not need to install .NET Framework or compile anything locally on your host machine!**
+
+### 1. Build the Wine Image
+Simply run this command to restore, compile, and package the complete Wine-compatible image:
+```bash
+docker build -t de4dotex-wine -f Dockerfile.wine .
+```
+
+### 2. Run the Wine Image
+Run the container using Wine. A virtual framebuffer (`Xvfb`) is configured automatically in the background to handle Wine's GUI requirements in headless environments.
+```bash
+# Run help
+docker run --rm de4dotex-wine --help
+
+# Deobfuscate an assembly requiring dynamic JIT-hook decryption (e.g. ILProtector, Agile.NET)
+docker run --rm -v "$(pwd):/work" de4dotex-wine MyProtectedApp.dll -o DecryptedApp.dll
+```
+
+
+Docker Troubleshooting & Tips
+-----------------------------
+
+### Apple Silicon / ARM64 Mac Hosts (M1 / M2 / M3)
+* **Strategy A (Native):** Runs flawlessly on ARM64 hosts. Docker automatically targets ARM64 and compiles .NET 8.0 and BeaEngine natively for your CPU architecture.
+* **Strategy B (Wine):** Because Strategy B installs standard x86 `.NET Framework 4.8` components, you **MUST** force Docker to build and run the image targeting the Intel platform (`linux/amd64`). Docker Desktop on macOS will automatically translate the CPU instructions using Rosetta 2 / QEMU:
+ ```bash
+ # Build on ARM64 Mac using Intel emulation:
+ DOCKER_BUILDKIT=0 docker build --platform linux/amd64 -t de4dotex-wine -f Dockerfile.wine .
+
+ # Run on ARM64 Mac using Intel emulation:
+ docker run --platform linux/amd64 --rm -v "$(pwd):/work" de4dotex-wine MyProtectedApp.dll
+ ```
+
+### Docker BuildKit / Buildx Errors (Legacy Builder Warnings)
+If you get a warning saying `DEPRECATED: The legacy builder is deprecated` or an error saying `BuildKit is enabled but the buildx component is missing or broken`, you can easily resolve this:
+* **Workaround 1 (Fastest):** Prefix your build command with `DOCKER_BUILDKIT=0` to temporarily bypass BuildKit:
+ ```bash
+ DOCKER_BUILDKIT=0 docker build -t de4dotex -f Dockerfile .
+ ```
+* **Workaround 2 (Recommended for macOS):** If you are on macOS and have Homebrew, install and register the missing `buildx` component:
+ ```bash
+ brew install docker-buildx
+ mkdir -p ~/.docker/cli-plugins
+ ln -sfn $(brew --prefix)/opt/docker-buildx/bin/docker-buildx ~/.docker/cli-plugins/docker-buildx
+ ```
+
+### Globalization/Localization Issues
+* Both images are configured with native globalization components (`libicu`). If you encounter encoding issues with non-ASCII or obfuscated class/method names, make sure your terminal and volume directories are UTF-8 compliant.
+
+
+Native C# MCP Server
+====================
+
+de4dotEx includes a native **Model Context Protocol (MCP)** server built on **.NET 8.0**.
+
+By running de4dotEx as an MCP server, you can connect it directly to AI assistants (such as **Claude Desktop**, **Cursor IDE**, **Windsurf**, or the **VS Code MCP Client**). This allows the AI agent to programmatically and automatically deobfuscate .NET assemblies (EXE and DLL) inside directories it is analyzing!
+
+Exposed MCP Tools
+-----------------
+
+### `deobfuscate`
+Deobfuscate and unpack a .NET assembly (EXE or DLL) using de4dotEx.
+
+**Arguments:**
+* `file_path` (string, **required**): The absolute file path of the .NET assembly to deobfuscate.
+* `output_path` (string, *optional*): Custom file path where the cleaned assembly should be written.
+* `options` (string, *optional*): Additional CLI options to pass to the engine (e.g. `"--preserve-tokens"` or `"-str delegate"`).
+
+How to Build the MCP Server
+---------------------------
+
+You can build the MCP server using the standard `dotnet` CLI:
+```bash
+dotnet publish -c Release -f net8.0 -o ./publish-net8.0-mcp de4dot.mcp
+```
+After building, the published files (including the binary and its dependencies) will be placed in the `./publish-net8.0-mcp/` directory.
+
+How to Configure Your AI Client
+-------------------------------
+
+To add de4dotEx to your AI assistant, register it in your client's MCP configuration file (typically `claude_desktop_config.json` for Claude Desktop).
+
+### 1. Claude Desktop Configuration
+Open your Claude Desktop configuration file:
+* **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
+* **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
+
+Add the following block under the `mcpServers` object:
+```json
+{
+ "mcpServers": {
+ "de4dotex": {
+ "command": "dotnet",
+ "args": [
+ "/absolute/path/to/de4dotEx/publish-net8.0-mcp/de4dot.mcp.dll"
+ ]
+ }
+ }
+}
+```
+*(Make sure to replace `/absolute/path/to/de4dotEx/` with the actual absolute path to your cloned repository.)*
+
+### 2. Cursor IDE Configuration
+1. Open Cursor Settings -> **Features** -> **MCP**.
+2. Click **+ Add New MCP Server**.
+3. Name: `de4dotex`
+4. Type: `stdio`
+5. Command: `dotnet /absolute/path/to/de4dotEx/publish-net8.0-mcp/de4dot.mcp.dll`
+}
diff --git a/build.ps1 b/build.ps1
index 58b0a085..a0259d58 100644
--- a/build.ps1
+++ b/build.ps1
@@ -7,3 +7,7 @@ Remove-Item Release\net48\*.pdb, Release\net48\*.xml, Release\net48\Test.Rename.
dotnet publish -c Release -f net8.0 -o publish-net8.0 de4dot
if ($LASTEXITCODE) { exit $LASTEXITCODE }
Remove-Item publish-net8.0\*.pdb, publish-net8.0\*.xml
+
+dotnet publish -c Release -f net8.0 -o publish-net8.0-mcp de4dot.mcp
+if ($LASTEXITCODE) { exit $LASTEXITCODE }
+Remove-Item publish-net8.0-mcp\*.pdb, publish-net8.0-mcp\*.xml
diff --git a/de4dot.blocks/cflow/BlockCflowDeobfuscator.cs b/de4dot.blocks/cflow/BlockCflowDeobfuscator.cs
index 01d05e4e..bcec2e98 100644
--- a/de4dot.blocks/cflow/BlockCflowDeobfuscator.cs
+++ b/de4dot.blocks/cflow/BlockCflowDeobfuscator.cs
@@ -54,7 +54,12 @@ protected override bool Deobfuscate(Block block) {
return false;
}
- return branchEmulator.Emulate(block.LastInstr.Instruction);
+ try {
+ return branchEmulator.Emulate(block.LastInstr.Instruction);
+ }
+ catch {
+ return false;
+ }
}
void PopPushedArgs(int stackArgs) {
diff --git a/de4dot.code/ObfuscatedFile.cs b/de4dot.code/ObfuscatedFile.cs
index dc76ee20..aa2b3570 100644
--- a/de4dot.code/ObfuscatedFile.cs
+++ b/de4dot.code/ObfuscatedFile.cs
@@ -1,803 +1,805 @@
-/*
- Copyright (C) 2011-2015 de4dot@gmail.com
-
- This file is part of de4dot.
-
- de4dot is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- de4dot is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with de4dot. If not, see .
-*/
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using dnlib.DotNet;
-using dnlib.DotNet.Emit;
-using dnlib.DotNet.Writer;
-using dnlib.PE;
-using AssemblyData;
-using de4dot.code.deobfuscators;
-using de4dot.blocks;
-using de4dot.blocks.cflow;
-using de4dot.code.AssemblyClient;
-using de4dot.code.renamer;
-
-namespace de4dot.code {
- public class ObfuscatedFile : IObfuscatedFile, IDeobfuscatedFile {
- Options options;
- ModuleDefMD module;
- IDeobfuscator deob;
- IDeobfuscatorContext deobfuscatorContext;
- AssemblyModule assemblyModule;
- IAssemblyClient assemblyClient;
- DynamicStringInliner dynamicStringInliner;
- IAssemblyClientFactory assemblyClientFactory;
- SavedMethodBodies savedMethodBodies;
- bool userStringDecrypterMethods = false;
-
- class SavedMethodBodies {
- Dictionary savedMethodBodies = new Dictionary();
-
- class SavedMethodBody {
- MethodDef method;
- IList instructions;
- IList exceptionHandlers;
-
- public SavedMethodBody(MethodDef method) {
- this.method = method;
- DotNetUtils.CopyBody(method, out instructions, out exceptionHandlers);
- }
-
- public void Restore() => DotNetUtils.RestoreBody(method, instructions, exceptionHandlers);
- }
-
- public void Save(MethodDef method) {
- if (IsSaved(method))
- return;
- savedMethodBodies[method] = new SavedMethodBody(method);
- }
-
- public void RestoreAll() {
- foreach (var smb in savedMethodBodies.Values)
- smb.Restore();
- savedMethodBodies.Clear();
- }
-
- public bool IsSaved(MethodDef method) => savedMethodBodies.ContainsKey(method);
- }
-
- public class Options {
- public string Filename { get; set; }
- public string NewFilename { get; set; }
- public string ForcedObfuscatorType { get; set; }
- public DecrypterType StringDecrypterType { get; set; }
- public List StringDecrypterMethods { get; private set; }
- public bool ControlFlowDeobfuscation { get; set; }
- public bool KeepObfuscatorTypes { get; set; }
- public bool PreserveTokens { get; set; }
- public MetadataFlags MetadataFlags { get; set; }
- public RenamerFlags RenamerFlags { get; set; }
-
- public Options() {
- StringDecrypterType = DecrypterType.Default;
- StringDecrypterMethods = new List();
- }
- }
-
- public string Filename => options.Filename;
- public string NewFilename => options.NewFilename;
- public ModuleDefMD ModuleDefMD => module;
- public INameChecker NameChecker => deob;
- public bool RenameResourcesInCode => deob.TheOptions.RenameResourcesInCode;
- public bool RemoveNamespaceWithOneType => (deob.RenamingOptions & RenamingOptions.RemoveNamespaceIfOneType) != 0;
- public bool RenameResourceKeys => (deob.RenamingOptions & RenamingOptions.RenameResourceKeys) != 0;
- public IDeobfuscator Deobfuscator => deob;
- public IDeobfuscatorContext DeobfuscatorContext {
- get => deobfuscatorContext;
- set => deobfuscatorContext = value;
- }
-
- public ObfuscatedFile(Options options, ModuleContext moduleContext, IAssemblyClientFactory assemblyClientFactory) {
- this.assemblyClientFactory = assemblyClientFactory;
- this.options = options;
- userStringDecrypterMethods = options.StringDecrypterMethods.Count > 0;
- options.Filename = Utils.GetFullPath(options.Filename);
- assemblyModule = new AssemblyModule(options.Filename, moduleContext);
-
- if (options.NewFilename == null)
- options.NewFilename = GetDefaultNewFilename();
-
- if (string.Equals(options.Filename, options.NewFilename, StringComparison.OrdinalIgnoreCase))
- throw new UserException($"filename is same as new filename! ({options.Filename})");
- }
-
- string GetDefaultNewFilename() {
- string newFilename = Path.GetFileNameWithoutExtension(options.Filename) + "-cleaned" + Path.GetExtension(options.Filename);
- return Path.Combine(Path.GetDirectoryName(options.Filename), newFilename);
- }
-
- public void Load(IList deobfuscators) {
- try {
- LoadModule(deobfuscators);
- TheAssemblyResolver.Instance.AddSearchDirectory(Utils.GetDirName(Filename));
- TheAssemblyResolver.Instance.AddSearchDirectory(Utils.GetDirName(NewFilename));
- DetectObfuscator(deobfuscators);
- if (deob == null)
- throw new ApplicationException("Could not detect obfuscator!");
- InitializeDeobfuscator();
- }
- finally {
- foreach (var d in deobfuscators) {
- if (d != deob && d != null)
- d.Dispose();
- }
- }
- }
-
- void LoadModule(IEnumerable deobfuscators) {
- var oldModule = module;
- try {
- module = assemblyModule.Load();
- }
- catch (BadImageFormatException) {
- if (!UnpackNativeImage(deobfuscators))
- throw new BadImageFormatException();
- Logger.v("Unpacked native file");
- }
- finally {
- if (oldModule != null)
- oldModule.Dispose();
- }
- }
-
- bool UnpackNativeImage(IEnumerable deobfuscators) {
- using (var peImage = new PEImage(Filename)) {
- foreach (var deob in deobfuscators) {
- byte[] unpackedData = null;
- try {
- unpackedData = deob.UnpackNativeFile(peImage);
- }
- catch {
- }
- if (unpackedData == null)
- continue;
-
- var oldModule = module;
- try {
- module = assemblyModule.Load(unpackedData);
- }
- catch {
- Logger.w("Could not load unpacked data. File: {0}, deobfuscator: {0}", peImage.Filename ?? "(unknown filename)", deob.TypeLong);
- continue;
- }
- finally {
- if (oldModule != null)
- oldModule.Dispose();
- }
- this.deob = deob;
- return true;
- }
- }
-
- return false;
- }
-
- void InitializeDeobfuscator() {
- if (options.StringDecrypterType == DecrypterType.Default)
- options.StringDecrypterType = deob.DefaultDecrypterType;
- if (options.StringDecrypterType == DecrypterType.Default)
- options.StringDecrypterType = DecrypterType.Static;
-
- deob.Operations = CreateOperations();
- }
-
- IOperations CreateOperations() {
- var op = new Operations();
-
- switch (options.StringDecrypterType) {
- case DecrypterType.None:
- op.DecryptStrings = OpDecryptString.None;
- break;
- case DecrypterType.Static:
- op.DecryptStrings = OpDecryptString.Static;
- break;
- default:
- op.DecryptStrings = OpDecryptString.Dynamic;
- break;
- }
-
- op.KeepObfuscatorTypes = options.KeepObfuscatorTypes;
- op.MetadataFlags = options.MetadataFlags;
- op.RenamerFlags = options.RenamerFlags;
-
- return op;
- }
-
- void DetectObfuscator(IEnumerable deobfuscators) {
-
- // The deobfuscators may call methods to deobfuscate control flow and decrypt
- // strings (statically) in order to detect the obfuscator.
- if (!options.ControlFlowDeobfuscation || options.StringDecrypterType == DecrypterType.None)
- savedMethodBodies = new SavedMethodBodies();
-
- // It's not null if it unpacked a native file
- if (deob != null) {
- deob.Initialize(module);
- deob.DeobfuscatedFile = this;
- deob.Detect();
- return;
- }
-
- foreach (var deob in deobfuscators) {
- deob.Initialize(module);
- deob.DeobfuscatedFile = this;
- }
-
- if (options.ForcedObfuscatorType != null) {
- foreach (var deob in deobfuscators) {
- if (string.Equals(options.ForcedObfuscatorType, deob.Type, StringComparison.OrdinalIgnoreCase)) {
- this.deob = deob;
- deob.Detect();
- return;
- }
- }
- }
- else
- deob = DetectObfuscator2(deobfuscators);
- }
-
- IDeobfuscator DetectObfuscator2(IEnumerable deobfuscators) {
- var allDetected = new List();
- IDeobfuscator detected = null;
- int detectVal = 0;
- foreach (var deob in deobfuscators) {
- this.deob = deob; // So we can call deob.CanInlineMethods in deobfuscate()
- int val;
- //TODO: Re-enable exception handler
- //try {
- val = deob.Detect();
- /*}
- catch {
- val = deob.Type == "un" ? 1 : 0;
- }*/
- Logger.v("{0,3}: {1}", val, deob.TypeLong);
- if (val > 0 && deob.Type != "un")
- allDetected.Add(deob);
- if (val > detectVal) {
- detectVal = val;
- detected = deob;
- }
- }
- deob = null;
-
- if (allDetected.Count > 1) {
- Logger.n("More than one obfuscator detected:");
- Logger.Instance.Indent();
- foreach (var deob in allDetected)
- Logger.n("{0} (use: -p {1})", deob.Name, deob.Type);
- Logger.Instance.DeIndent();
- }
-
- return detected;
- }
-
- MetadataFlags GetMetadataFlags() {
- var mdFlags = options.MetadataFlags | deob.MetadataFlags;
-
- // Always preserve tokens if it's an unknown obfuscator
- if (deob.Type == "un") {
- mdFlags |= MetadataFlags.PreserveRids |
- MetadataFlags.PreserveUSOffsets |
- MetadataFlags.PreserveBlobOffsets |
- MetadataFlags.PreserveExtraSignatureData;
- }
-
- return mdFlags;
- }
-
- public void Save() {
- Logger.n("Saving {0}", options.NewFilename);
- var mdFlags = GetMetadataFlags();
- if (!options.ControlFlowDeobfuscation)
- mdFlags |= MetadataFlags.KeepOldMaxStack;
- assemblyModule.Save(options.NewFilename, mdFlags, new PrintNewTokens(module, deob as IModuleWriterListener));
- }
-
- IList GetAllMethods() {
- var list = new List();
-
- foreach (var type in module.GetTypes()) {
- foreach (var method in type.Methods)
- list.Add(method);
- }
-
- return list;
- }
-
- public void DeobfuscateBegin() {
- switch (options.StringDecrypterType) {
- case DecrypterType.None:
- CheckSupportedStringDecrypter(StringFeatures.AllowNoDecryption);
- break;
-
- case DecrypterType.Static:
- CheckSupportedStringDecrypter(StringFeatures.AllowStaticDecryption);
- break;
-
- case DecrypterType.Delegate:
- case DecrypterType.Emulate:
- CheckSupportedStringDecrypter(StringFeatures.AllowDynamicDecryption);
- var newProcFactory = assemblyClientFactory as NewProcessAssemblyClientFactory;
- if (newProcFactory != null)
- assemblyClient = newProcFactory.Create(AssemblyServiceType.StringDecrypter, module);
- else
- assemblyClient = assemblyClientFactory.Create(AssemblyServiceType.StringDecrypter);
- assemblyClient.Connect();
- break;
-
- default:
- throw new ApplicationException($"Invalid string decrypter type '{options.StringDecrypterType}'");
- }
- }
-
- public void CheckSupportedStringDecrypter(StringFeatures feature) {
- if ((deob.StringFeatures & feature) == feature)
- return;
- throw new UserException($"Deobfuscator {deob.TypeLong} does not support this string decryption type");
- }
-
- public void Deobfuscate() {
- Logger.n("Cleaning {0}", options.Filename);
- InitAssemblyClient();
-
- for (int i = 0; ; i++) {
- byte[] fileData = null;
- DumpedMethods dumpedMethods = null;
- if (!deob.GetDecryptedModule(i, ref fileData, ref dumpedMethods))
- break;
- ReloadModule(fileData, dumpedMethods);
- }
-
- deob.DeobfuscateBegin();
- DeobfuscateMethods();
+/*
+ Copyright (C) 2011-2015 de4dot@gmail.com
+
+ This file is part of de4dot.
+
+ de4dot is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ de4dot is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with de4dot. If not, see .
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using dnlib.DotNet;
+using dnlib.DotNet.Emit;
+using dnlib.DotNet.Writer;
+using dnlib.PE;
+using AssemblyData;
+using de4dot.code.deobfuscators;
+using de4dot.blocks;
+using de4dot.blocks.cflow;
+using de4dot.code.AssemblyClient;
+using de4dot.code.renamer;
+
+namespace de4dot.code {
+ public class ObfuscatedFile : IObfuscatedFile, IDeobfuscatedFile {
+ Options options;
+ ModuleDefMD module;
+ IDeobfuscator deob;
+ IDeobfuscatorContext deobfuscatorContext;
+ AssemblyModule assemblyModule;
+ IAssemblyClient assemblyClient;
+ DynamicStringInliner dynamicStringInliner;
+ IAssemblyClientFactory assemblyClientFactory;
+ SavedMethodBodies savedMethodBodies;
+ bool userStringDecrypterMethods = false;
+
+ class SavedMethodBodies {
+ Dictionary savedMethodBodies = new Dictionary();
+
+ class SavedMethodBody {
+ MethodDef method;
+ IList instructions;
+ IList exceptionHandlers;
+
+ public SavedMethodBody(MethodDef method) {
+ this.method = method;
+ DotNetUtils.CopyBody(method, out instructions, out exceptionHandlers);
+ }
+
+ public void Restore() => DotNetUtils.RestoreBody(method, instructions, exceptionHandlers);
+ }
+
+ public void Save(MethodDef method) {
+ if (IsSaved(method))
+ return;
+ savedMethodBodies[method] = new SavedMethodBody(method);
+ }
+
+ public void RestoreAll() {
+ foreach (var smb in savedMethodBodies.Values)
+ smb.Restore();
+ savedMethodBodies.Clear();
+ }
+
+ public bool IsSaved(MethodDef method) => savedMethodBodies.ContainsKey(method);
+ }
+
+ public class Options {
+ public string Filename { get; set; }
+ public string NewFilename { get; set; }
+ public string ForcedObfuscatorType { get; set; }
+ public DecrypterType StringDecrypterType { get; set; }
+ public List StringDecrypterMethods { get; private set; }
+ public bool ControlFlowDeobfuscation { get; set; }
+ public bool KeepObfuscatorTypes { get; set; }
+ public bool PreserveTokens { get; set; }
+ public MetadataFlags MetadataFlags { get; set; }
+ public RenamerFlags RenamerFlags { get; set; }
+
+ public Options() {
+ StringDecrypterType = DecrypterType.Default;
+ StringDecrypterMethods = new List();
+ }
+ }
+
+ public string Filename => options.Filename;
+ public string NewFilename => options.NewFilename;
+ public ModuleDefMD ModuleDefMD => module;
+ public INameChecker NameChecker => deob;
+ public bool RenameResourcesInCode => deob.TheOptions.RenameResourcesInCode;
+ public bool RemoveNamespaceWithOneType => (deob.RenamingOptions & RenamingOptions.RemoveNamespaceIfOneType) != 0;
+ public bool RenameResourceKeys => (deob.RenamingOptions & RenamingOptions.RenameResourceKeys) != 0;
+ public IDeobfuscator Deobfuscator => deob;
+ public IDeobfuscatorContext DeobfuscatorContext {
+ get => deobfuscatorContext;
+ set => deobfuscatorContext = value;
+ }
+
+ public ObfuscatedFile(Options options, ModuleContext moduleContext, IAssemblyClientFactory assemblyClientFactory) {
+ this.assemblyClientFactory = assemblyClientFactory;
+ this.options = options;
+ userStringDecrypterMethods = options.StringDecrypterMethods.Count > 0;
+ options.Filename = Utils.GetFullPath(options.Filename);
+ assemblyModule = new AssemblyModule(options.Filename, moduleContext);
+
+ if (options.NewFilename == null)
+ options.NewFilename = GetDefaultNewFilename();
+
+ if (string.Equals(options.Filename, options.NewFilename, StringComparison.OrdinalIgnoreCase))
+ throw new UserException($"filename is same as new filename! ({options.Filename})");
+ }
+
+ string GetDefaultNewFilename() {
+ string newFilename = Path.GetFileNameWithoutExtension(options.Filename) + "-cleaned" + Path.GetExtension(options.Filename);
+ return Path.Combine(Path.GetDirectoryName(options.Filename), newFilename);
+ }
+
+ public void Load(IList deobfuscators) {
+ try {
+ LoadModule(deobfuscators);
+ TheAssemblyResolver.Instance.AddSearchDirectory(Utils.GetDirName(Filename));
+ TheAssemblyResolver.Instance.AddSearchDirectory(Utils.GetDirName(NewFilename));
+ DetectObfuscator(deobfuscators);
+ if (deob == null)
+ throw new ApplicationException("Could not detect obfuscator!");
+ InitializeDeobfuscator();
+ }
+ finally {
+ foreach (var d in deobfuscators) {
+ if (d != deob && d != null)
+ d.Dispose();
+ }
+ }
+ }
+
+ void LoadModule(IEnumerable deobfuscators) {
+ var oldModule = module;
+ try {
+ module = assemblyModule.Load();
+ }
+ catch (BadImageFormatException) {
+ if (!UnpackNativeImage(deobfuscators))
+ throw new BadImageFormatException();
+ Logger.v("Unpacked native file");
+ }
+ finally {
+ if (oldModule != null)
+ oldModule.Dispose();
+ }
+ }
+
+ bool UnpackNativeImage(IEnumerable deobfuscators) {
+ using (var peImage = new PEImage(Filename)) {
+ foreach (var deob in deobfuscators) {
+ byte[] unpackedData = null;
+ try {
+ unpackedData = deob.UnpackNativeFile(peImage);
+ }
+ catch {
+ }
+ if (unpackedData == null)
+ continue;
+
+ var oldModule = module;
+ try {
+ module = assemblyModule.Load(unpackedData);
+ }
+ catch {
+ Logger.w("Could not load unpacked data. File: {0}, deobfuscator: {0}", peImage.Filename ?? "(unknown filename)", deob.TypeLong);
+ continue;
+ }
+ finally {
+ if (oldModule != null)
+ oldModule.Dispose();
+ }
+ this.deob = deob;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ void InitializeDeobfuscator() {
+ if (options.StringDecrypterType == DecrypterType.Default)
+ options.StringDecrypterType = deob.DefaultDecrypterType;
+ if (options.StringDecrypterType == DecrypterType.Default)
+ options.StringDecrypterType = DecrypterType.Static;
+
+ deob.Operations = CreateOperations();
+ }
+
+ IOperations CreateOperations() {
+ var op = new Operations();
+
+ switch (options.StringDecrypterType) {
+ case DecrypterType.None:
+ op.DecryptStrings = OpDecryptString.None;
+ break;
+ case DecrypterType.Static:
+ op.DecryptStrings = OpDecryptString.Static;
+ break;
+ default:
+ op.DecryptStrings = OpDecryptString.Dynamic;
+ break;
+ }
+
+ op.KeepObfuscatorTypes = options.KeepObfuscatorTypes;
+ op.MetadataFlags = options.MetadataFlags;
+ op.RenamerFlags = options.RenamerFlags;
+
+ return op;
+ }
+
+ void DetectObfuscator(IEnumerable deobfuscators) {
+
+ // The deobfuscators may call methods to deobfuscate control flow and decrypt
+ // strings (statically) in order to detect the obfuscator.
+ if (!options.ControlFlowDeobfuscation || options.StringDecrypterType == DecrypterType.None)
+ savedMethodBodies = new SavedMethodBodies();
+
+ // It's not null if it unpacked a native file
+ if (deob != null) {
+ deob.Initialize(module);
+ deob.DeobfuscatedFile = this;
+ deob.Detect();
+ return;
+ }
+
+ foreach (var deob in deobfuscators) {
+ deob.Initialize(module);
+ deob.DeobfuscatedFile = this;
+ }
+
+ if (options.ForcedObfuscatorType != null) {
+ foreach (var deob in deobfuscators) {
+ if (string.Equals(options.ForcedObfuscatorType, deob.Type, StringComparison.OrdinalIgnoreCase)) {
+ this.deob = deob;
+ deob.Detect();
+ return;
+ }
+ }
+ }
+ else
+ deob = DetectObfuscator2(deobfuscators);
+ }
+
+ IDeobfuscator DetectObfuscator2(IEnumerable deobfuscators) {
+ var allDetected = new List();
+ IDeobfuscator detected = null;
+ int detectVal = 0;
+ foreach (var deob in deobfuscators) {
+ this.deob = deob; // So we can call deob.CanInlineMethods in deobfuscate()
+ int val;
+ //TODO: Re-enable exception handler
+ //try {
+ val = deob.Detect();
+ /*}
+ catch {
+ val = deob.Type == "un" ? 1 : 0;
+ }*/
+ Logger.v("{0,3}: {1}", val, deob.TypeLong);
+ if (val > 0 && deob.Type != "un")
+ allDetected.Add(deob);
+ if (val > detectVal) {
+ detectVal = val;
+ detected = deob;
+ }
+ }
+ deob = null;
+
+ if (allDetected.Count > 1) {
+ Logger.n("More than one obfuscator detected:");
+ Logger.Instance.Indent();
+ foreach (var deob in allDetected)
+ Logger.n("{0} (use: -p {1})", deob.Name, deob.Type);
+ Logger.Instance.DeIndent();
+ }
+
+ return detected;
+ }
+
+ MetadataFlags GetMetadataFlags() {
+ var mdFlags = options.MetadataFlags | deob.MetadataFlags;
+
+ // Always preserve tokens if it's an unknown obfuscator
+ if (deob.Type == "un") {
+ mdFlags |= MetadataFlags.PreserveRids |
+ MetadataFlags.PreserveUSOffsets |
+ MetadataFlags.PreserveBlobOffsets |
+ MetadataFlags.PreserveExtraSignatureData;
+ }
+
+ return mdFlags;
+ }
+
+ public void Save() {
+ Logger.n("Saving {0}", options.NewFilename);
+ var mdFlags = GetMetadataFlags();
+ // Always include KeepOldMaxStack to guarantee that the assembly writer
+ // doesn't crash on invalid/obfuscated stack-depth recalculations,
+ // and to ensure maximum compatibility under the .NET JIT compiler.
+ mdFlags |= MetadataFlags.KeepOldMaxStack;
+ assemblyModule.Save(options.NewFilename, mdFlags, new PrintNewTokens(module, deob as IModuleWriterListener));
+ }
+
+ IList GetAllMethods() {
+ var list = new List();
+
+ foreach (var type in module.GetTypes()) {
+ foreach (var method in type.Methods)
+ list.Add(method);
+ }
+
+ return list;
+ }
+
+ public void DeobfuscateBegin() {
+ switch (options.StringDecrypterType) {
+ case DecrypterType.None:
+ CheckSupportedStringDecrypter(StringFeatures.AllowNoDecryption);
+ break;
+
+ case DecrypterType.Static:
+ CheckSupportedStringDecrypter(StringFeatures.AllowStaticDecryption);
+ break;
+
+ case DecrypterType.Delegate:
+ case DecrypterType.Emulate:
+ CheckSupportedStringDecrypter(StringFeatures.AllowDynamicDecryption);
+ var newProcFactory = assemblyClientFactory as NewProcessAssemblyClientFactory;
+ if (newProcFactory != null)
+ assemblyClient = newProcFactory.Create(AssemblyServiceType.StringDecrypter, module);
+ else
+ assemblyClient = assemblyClientFactory.Create(AssemblyServiceType.StringDecrypter);
+ assemblyClient.Connect();
+ break;
+
+ default:
+ throw new ApplicationException($"Invalid string decrypter type '{options.StringDecrypterType}'");
+ }
+ }
+
+ public void CheckSupportedStringDecrypter(StringFeatures feature) {
+ if ((deob.StringFeatures & feature) == feature)
+ return;
+ throw new UserException($"Deobfuscator {deob.TypeLong} does not support this string decryption type");
+ }
+
+ public void Deobfuscate() {
+ Logger.n("Cleaning {0}", options.Filename);
+ InitAssemblyClient();
+
+ for (int i = 0; ; i++) {
+ byte[] fileData = null;
+ DumpedMethods dumpedMethods = null;
+ if (!deob.GetDecryptedModule(i, ref fileData, ref dumpedMethods))
+ break;
+ ReloadModule(fileData, dumpedMethods);
+ }
+
+ deob.DeobfuscateBegin();
+ DeobfuscateMethods();
if (options.StringDecrypterType == DecrypterType.Delegate || options.StringDecrypterType == DecrypterType.Emulate)
- dynamicStringInliner.LogUnusedDecrypters();
- deob.DeobfuscateEnd();
- }
-
- void ReloadModule(byte[] newModuleData, DumpedMethods dumpedMethods) {
- Logger.v("Reloading decrypted assembly (original filename: {0})", Filename);
- simpleDeobfuscatorFlags.Clear();
- using (var oldModule = module) {
- module = assemblyModule.Reload(newModuleData, CreateDumpedMethodsRestorer(dumpedMethods), deob as IStringDecrypter);
- deob = deob.ModuleReloaded(module);
- }
- InitializeDeobfuscator();
- deob.DeobfuscatedFile = this;
- UpdateDynamicStringInliner();
- }
-
- DumpedMethodsRestorer CreateDumpedMethodsRestorer(DumpedMethods dumpedMethods) {
- if (dumpedMethods == null || dumpedMethods.Count == 0)
- return null;
- return new DumpedMethodsRestorer(dumpedMethods);
- }
-
- void InitAssemblyClient() {
- if (assemblyClient == null)
- return;
-
- assemblyClient.WaitConnected();
- assemblyClient.StringDecrypterService.LoadAssembly(options.Filename);
-
- if (options.StringDecrypterType == DecrypterType.Delegate)
- assemblyClient.StringDecrypterService.SetStringDecrypterType(AssemblyData.StringDecrypterType.Delegate);
- else if (options.StringDecrypterType == DecrypterType.Emulate)
- assemblyClient.StringDecrypterService.SetStringDecrypterType(AssemblyData.StringDecrypterType.Emulate);
- else
- throw new ApplicationException($"Invalid string decrypter type '{options.StringDecrypterType}'");
-
- dynamicStringInliner = new DynamicStringInliner(assemblyClient);
- UpdateDynamicStringInliner();
- }
-
- void UpdateDynamicStringInliner() {
- if (dynamicStringInliner != null)
- dynamicStringInliner.Initialize(GetMethodTokens());
- }
-
- IEnumerable GetMethodTokens() {
- if (!userStringDecrypterMethods)
- return deob.GetStringDecrypterMethods();
-
- var tokens = new List();
-
- foreach (var val in options.StringDecrypterMethods) {
- var tokenStr = val.Trim();
- if (Utils.StartsWith(tokenStr, "0x", StringComparison.OrdinalIgnoreCase))
- tokenStr = tokenStr.Substring(2);
- if (int.TryParse(tokenStr, NumberStyles.HexNumber, null, out int methodToken))
- tokens.Add(methodToken);
- else
- tokens.AddRange(FindMethodTokens(val));
- }
-
- return tokens;
- }
-
- IEnumerable FindMethodTokens(string methodDesc) {
- var tokens = new List();
-
- SplitMethodDesc(methodDesc, out string typeString, out string methodName, out var argsStrings);
-
- foreach (var type in module.GetTypes()) {
- if (typeString != null && typeString != type.FullName)
- continue;
- foreach (var method in type.Methods) {
- if (!method.IsStatic)
- continue;
- if (method.MethodSig.GetRetType().GetElementType() != ElementType.String && method.MethodSig.GetRetType().GetElementType() != ElementType.Object)
- continue;
- if (methodName != null && methodName != method.Name)
- continue;
-
- var sig = method.MethodSig;
- if (argsStrings == null) {
- if (sig.Params.Count == 0)
- continue;
- }
- else {
- if (argsStrings.Length != sig.Params.Count)
- continue;
- for (int i = 0; i < argsStrings.Length; i++) {
- if (argsStrings[i] != sig.Params[i].FullName)
- continue;
- }
- }
-
- Logger.v("Adding string decrypter; token: {0:X8}, method: {1}", method.MDToken.ToInt32(), Utils.RemoveNewlines(method.FullName));
- tokens.Add(method.MDToken.ToInt32());
- }
- }
-
- return tokens;
- }
-
- static void SplitMethodDesc(string methodDesc, out string type, out string name, out string[] args) {
- string stringArgs = null;
- args = null;
- type = null;
- name = null;
-
- var remaining = methodDesc;
- int index = remaining.LastIndexOf("::");
- if (index >= 0) {
- type = remaining.Substring(0, index);
- remaining = remaining.Substring(index + 2);
- }
-
- index = remaining.IndexOf('(');
- if (index >= 0) {
- name = remaining.Substring(0, index);
- remaining = remaining.Substring(index);
- }
- else {
- name = remaining;
- remaining = "";
- }
-
- if (Utils.StartsWith(remaining, "(", StringComparison.Ordinal)) {
- stringArgs = remaining;
- }
- else if (remaining.Length > 0)
- throw new UserException($"Invalid method desc: '{methodDesc}'");
-
- if (stringArgs != null) {
- if (Utils.StartsWith(stringArgs, "(", StringComparison.Ordinal))
- stringArgs = stringArgs.Substring(1);
- if (stringArgs.EndsWith(")", StringComparison.Ordinal))
- stringArgs = stringArgs.Substring(0, stringArgs.Length - 1);
- args = stringArgs.Split(',');
- for (int i = 0; i < args.Length; i++)
- args[i] = args[i].Trim();
- }
-
- if (type == "")
- type = null;
- if (name == "")
- name = null;
- }
-
- public void DeobfuscateEnd() {
- DeobfuscateCleanUp();
- }
-
- public void DeobfuscateCleanUp() {
- if (assemblyClient != null) {
- assemblyClient.Dispose();
- assemblyClient = null;
- }
- }
-
- void DeobfuscateMethods() {
- if (savedMethodBodies != null) {
- savedMethodBodies.RestoreAll();
- savedMethodBodies = null;
- }
- deob.DeobfuscatedFile = null;
-
- if (!options.ControlFlowDeobfuscation) {
- if (options.KeepObfuscatorTypes || deob.Type == "un")
- return;
- }
-
- bool isVerbose = !Logger.Instance.IgnoresEvent(LoggerEvent.Verbose);
- bool isVV = !Logger.Instance.IgnoresEvent(LoggerEvent.VeryVerbose);
- if (isVerbose)
- Logger.v("Deobfuscating methods");
- var methodPrinter = new MethodPrinter();
- var cflowDeobfuscator = new BlocksCflowDeobfuscator(deob.BlocksDeobfuscators);
- foreach (var method in GetAllMethods()) {
- if (isVerbose) {
- Logger.v("Deobfuscating {0} ({1:X8})", Utils.RemoveNewlines(method), method.MDToken.ToUInt32());
- Logger.Instance.Indent();
- }
-
- int oldIndentLevel = Logger.Instance.IndentLevel;
- //TODO: Re-enable exception handler
- //try {
- Deobfuscate(method, cflowDeobfuscator, methodPrinter, isVerbose, isVV);
- /*}
- catch (Exception ex) {
- if (!CanLoadMethodBody(method)) {
- if (isVerbose)
- Logger.v("Invalid method body. {0:X8}", method.MDToken.ToInt32());
- method.Body = new CilBody();
- }
- else {
- Logger.w("Could not deobfuscate method {0:X8}. Hello, E.T.: {1}", // E.T. = exception type
- method.MDToken.ToInt32(),
- ex.GetType());
- }
- }
- finally {
- Logger.Instance.IndentLevel = oldIndentLevel;
- }*/
-
- RemoveNoInliningAttribute(method);
-
- if (isVerbose)
- Logger.Instance.DeIndent();
- }
- }
-
- static bool CanLoadMethodBody(MethodDef method) {
- try {
- var body = method.Body;
- return true;
- }
- catch {
- return false;
- }
- }
-
- bool CanOptimizeLocals() {
- // Don't remove any locals if we must preserve StandAloneSig table
- return (GetMetadataFlags() & MetadataFlags.PreserveStandAloneSigRids) == 0;
- }
-
- void Deobfuscate(MethodDef method, BlocksCflowDeobfuscator cflowDeobfuscator, MethodPrinter methodPrinter, bool isVerbose, bool isVV) {
- if (!HasNonEmptyBody(method))
- return;
-
- var blocks = new Blocks(method);
-
- int numRemovedLocals = 0;
- int oldNumInstructions = method.Body.Instructions.Count;
-
- deob.DeobfuscateMethodBegin(blocks);
- if (options.ControlFlowDeobfuscation) {
- cflowDeobfuscator.Initialize(blocks);
- try {
- cflowDeobfuscator.Deobfuscate();
- }
- catch (Exception) {
- Logger.e("Error during cflow deobfuscation of {0} ({1:X8})", Utils.RemoveNewlines(method), method.MDToken.ToUInt32());
- throw;
- }
- }
-
- if (deob.DeobfuscateOther(blocks) && options.ControlFlowDeobfuscation)
- cflowDeobfuscator.Deobfuscate();
-
- if (options.ControlFlowDeobfuscation) {
- if (CanOptimizeLocals())
- numRemovedLocals = blocks.OptimizeLocals();
- blocks.RepartitionBlocks();
- }
-
- DeobfuscateStrings(blocks);
- deob.DeobfuscateMethodEnd(blocks);
-
- blocks.GetCode(out var allInstructions, out var allExceptionHandlers);
- DotNetUtils.RestoreBody(method, allInstructions, allExceptionHandlers);
-
- if (isVerbose && numRemovedLocals > 0)
- Logger.v("Removed {0} unused local(s)", numRemovedLocals);
- int numRemovedInstructions = oldNumInstructions - method.Body.Instructions.Count;
- if (isVerbose && numRemovedInstructions > 0)
- Logger.v("Removed {0} dead instruction(s)", numRemovedInstructions);
-
- if (isVV) {
- Logger.Log(LoggerEvent.VeryVerbose, "Deobfuscated code:");
- Logger.Instance.Indent();
- methodPrinter.Print(LoggerEvent.VeryVerbose, allInstructions, allExceptionHandlers);
- Logger.Instance.DeIndent();
- }
- }
-
- bool HasNonEmptyBody(MethodDef method) => method.HasBody && method.Body.Instructions.Count > 0;
-
- void DeobfuscateStrings(Blocks blocks) {
- switch (options.StringDecrypterType) {
- case DecrypterType.None:
- break;
-
- case DecrypterType.Static:
- deob.DeobfuscateStrings(blocks);
- break;
-
- case DecrypterType.Delegate:
- case DecrypterType.Emulate:
- dynamicStringInliner.Decrypt(blocks);
- break;
-
- default:
- throw new ApplicationException($"Invalid string decrypter type '{options.StringDecrypterType}'");
- }
- }
-
- void RemoveNoInliningAttribute(MethodDef method) {
- method.IsNoInlining = false;
- for (int i = 0; i < method.CustomAttributes.Count; i++) {
- var cattr = method.CustomAttributes[i];
- if (cattr.TypeFullName != "System.Runtime.CompilerServices.MethodImplAttribute")
- continue;
- int options = 0;
- if (!GetMethodImplOptions(cattr, ref options))
- continue;
- if (options != 0 && options != (int)MethodImplAttributes.NoInlining)
- continue;
- method.CustomAttributes.RemoveAt(i);
- i--;
- }
- }
-
- static bool GetMethodImplOptions(CustomAttribute cattr, ref int value) {
- if (cattr.IsRawBlob)
- return false;
- if (cattr.ConstructorArguments.Count != 1)
- return false;
- if (cattr.ConstructorArguments[0].Type.ElementType != ElementType.I2 &&
- cattr.ConstructorArguments[0].Type.FullName != "System.Runtime.CompilerServices.MethodImplOptions")
- return false;
-
- var arg = cattr.ConstructorArguments[0].Value;
- if (arg is short) {
- value = (short)arg;
- return true;
- }
- if (arg is int) {
- value = (int)arg;
- return true;
- }
-
- return false;
- }
-
- public override string ToString() {
- if (options == null || options.Filename == null)
- return base.ToString();
- return options.Filename;
- }
-
- [Flags]
- enum SimpleDeobFlags {
- HasDeobfuscated = 0x1,
- }
- Dictionary simpleDeobfuscatorFlags = new Dictionary();
- bool Check(MethodDef method, SimpleDeobFlags flag) {
- if (method == null)
- return false;
- simpleDeobfuscatorFlags.TryGetValue(method, out var oldFlags);
- simpleDeobfuscatorFlags[method] = oldFlags | flag;
- return (oldFlags & flag) == flag;
- }
- bool Clear(MethodDef method, SimpleDeobFlags flag) {
- if (method == null)
- return false;
- if (!simpleDeobfuscatorFlags.TryGetValue(method, out var oldFlags))
- return false;
- simpleDeobfuscatorFlags[method] = oldFlags & ~flag;
- return true;
- }
-
- void Deobfuscate(MethodDef method, string msg, Action handler) {
- if (savedMethodBodies != null)
- savedMethodBodies.Save(method);
-
- Logger.v("{0}: {1} ({2:X8})", msg, Utils.RemoveNewlines(method), method.MDToken.ToUInt32());
- Logger.Instance.Indent();
-
- if (HasNonEmptyBody(method)) {
- try {
- var blocks = new Blocks(method);
-
- handler(blocks);
-
- blocks.GetCode(out var allInstructions, out var allExceptionHandlers);
- DotNetUtils.RestoreBody(method, allInstructions, allExceptionHandlers);
- }
- catch {
- Logger.v("Could not deobfuscate {0:X8}", method.MDToken.ToInt32());
- }
- }
-
- Logger.Instance.DeIndent();
- }
-
- void ISimpleDeobfuscator.MethodModified(MethodDef method) => Clear(method, SimpleDeobFlags.HasDeobfuscated);
- void ISimpleDeobfuscator.Deobfuscate(MethodDef method) => ((ISimpleDeobfuscator)this).Deobfuscate(method, 0);
-
- void ISimpleDeobfuscator.Deobfuscate(MethodDef method, SimpleDeobfuscatorFlags flags) {
- bool force = (flags & SimpleDeobfuscatorFlags.Force) != 0;
- if (method == null || (!force && Check(method, SimpleDeobFlags.HasDeobfuscated)))
- return;
-
- Deobfuscate(method, "Deobfuscating control flow", (blocks) => {
- bool disableNewCFCode = (flags & SimpleDeobfuscatorFlags.DisableConstantsFolderExtraInstrs) != 0;
- var cflowDeobfuscator = new BlocksCflowDeobfuscator(deob.BlocksDeobfuscators, disableNewCFCode);
- cflowDeobfuscator.Initialize(blocks);
- cflowDeobfuscator.Deobfuscate();
- blocks.RepartitionBlocks();
- });
- }
-
- void ISimpleDeobfuscator.DecryptStrings(MethodDef method, IDeobfuscator theDeob) =>
- Deobfuscate(method, "Static string decryption", (blocks) => theDeob.DeobfuscateStrings(blocks));
-
- void IDeobfuscatedFile.CreateAssemblyFile(byte[] data, string assemblyName, string extension) {
- if (extension == null)
- extension = ".dll";
- var baseDir = Utils.GetDirName(options.NewFilename);
- var newName = Path.Combine(baseDir, assemblyName + extension);
- Logger.n("Creating file {0}", newName);
- File.WriteAllBytes(newName, data);
- }
-
- void IDeobfuscatedFile.StringDecryptersAdded() => UpdateDynamicStringInliner();
-
- void IDeobfuscatedFile.SetDeobfuscator(IDeobfuscator deob) => this.deob = deob;
-
- public void Dispose() {
- DeobfuscateCleanUp();
- if (module != null)
- module.Dispose();
- if (deob != null)
- deob.Dispose();
- module = null;
- deob = null;
- }
- }
-}
+ dynamicStringInliner.LogUnusedDecrypters();
+ deob.DeobfuscateEnd();
+ }
+
+ void ReloadModule(byte[] newModuleData, DumpedMethods dumpedMethods) {
+ Logger.v("Reloading decrypted assembly (original filename: {0})", Filename);
+ simpleDeobfuscatorFlags.Clear();
+ using (var oldModule = module) {
+ module = assemblyModule.Reload(newModuleData, CreateDumpedMethodsRestorer(dumpedMethods), deob as IStringDecrypter);
+ deob = deob.ModuleReloaded(module);
+ }
+ InitializeDeobfuscator();
+ deob.DeobfuscatedFile = this;
+ UpdateDynamicStringInliner();
+ }
+
+ DumpedMethodsRestorer CreateDumpedMethodsRestorer(DumpedMethods dumpedMethods) {
+ if (dumpedMethods == null || dumpedMethods.Count == 0)
+ return null;
+ return new DumpedMethodsRestorer(dumpedMethods);
+ }
+
+ void InitAssemblyClient() {
+ if (assemblyClient == null)
+ return;
+
+ assemblyClient.WaitConnected();
+ assemblyClient.StringDecrypterService.LoadAssembly(options.Filename);
+
+ if (options.StringDecrypterType == DecrypterType.Delegate)
+ assemblyClient.StringDecrypterService.SetStringDecrypterType(AssemblyData.StringDecrypterType.Delegate);
+ else if (options.StringDecrypterType == DecrypterType.Emulate)
+ assemblyClient.StringDecrypterService.SetStringDecrypterType(AssemblyData.StringDecrypterType.Emulate);
+ else
+ throw new ApplicationException($"Invalid string decrypter type '{options.StringDecrypterType}'");
+
+ dynamicStringInliner = new DynamicStringInliner(assemblyClient);
+ UpdateDynamicStringInliner();
+ }
+
+ void UpdateDynamicStringInliner() {
+ if (dynamicStringInliner != null)
+ dynamicStringInliner.Initialize(GetMethodTokens());
+ }
+
+ IEnumerable GetMethodTokens() {
+ if (!userStringDecrypterMethods)
+ return deob.GetStringDecrypterMethods();
+
+ var tokens = new List();
+
+ foreach (var val in options.StringDecrypterMethods) {
+ var tokenStr = val.Trim();
+ if (Utils.StartsWith(tokenStr, "0x", StringComparison.OrdinalIgnoreCase))
+ tokenStr = tokenStr.Substring(2);
+ if (int.TryParse(tokenStr, NumberStyles.HexNumber, null, out int methodToken))
+ tokens.Add(methodToken);
+ else
+ tokens.AddRange(FindMethodTokens(val));
+ }
+
+ return tokens;
+ }
+
+ IEnumerable FindMethodTokens(string methodDesc) {
+ var tokens = new List();
+
+ SplitMethodDesc(methodDesc, out string typeString, out string methodName, out var argsStrings);
+
+ foreach (var type in module.GetTypes()) {
+ if (typeString != null && typeString != type.FullName)
+ continue;
+ foreach (var method in type.Methods) {
+ if (!method.IsStatic)
+ continue;
+ if (method.MethodSig.GetRetType().GetElementType() != ElementType.String && method.MethodSig.GetRetType().GetElementType() != ElementType.Object)
+ continue;
+ if (methodName != null && methodName != method.Name)
+ continue;
+
+ var sig = method.MethodSig;
+ if (argsStrings == null) {
+ if (sig.Params.Count == 0)
+ continue;
+ }
+ else {
+ if (argsStrings.Length != sig.Params.Count)
+ continue;
+ for (int i = 0; i < argsStrings.Length; i++) {
+ if (argsStrings[i] != sig.Params[i].FullName)
+ continue;
+ }
+ }
+
+ Logger.v("Adding string decrypter; token: {0:X8}, method: {1}", method.MDToken.ToInt32(), Utils.RemoveNewlines(method.FullName));
+ tokens.Add(method.MDToken.ToInt32());
+ }
+ }
+
+ return tokens;
+ }
+
+ static void SplitMethodDesc(string methodDesc, out string type, out string name, out string[] args) {
+ string stringArgs = null;
+ args = null;
+ type = null;
+ name = null;
+
+ var remaining = methodDesc;
+ int index = remaining.LastIndexOf("::");
+ if (index >= 0) {
+ type = remaining.Substring(0, index);
+ remaining = remaining.Substring(index + 2);
+ }
+
+ index = remaining.IndexOf('(');
+ if (index >= 0) {
+ name = remaining.Substring(0, index);
+ remaining = remaining.Substring(index);
+ }
+ else {
+ name = remaining;
+ remaining = "";
+ }
+
+ if (Utils.StartsWith(remaining, "(", StringComparison.Ordinal)) {
+ stringArgs = remaining;
+ }
+ else if (remaining.Length > 0)
+ throw new UserException($"Invalid method desc: '{methodDesc}'");
+
+ if (stringArgs != null) {
+ if (Utils.StartsWith(stringArgs, "(", StringComparison.Ordinal))
+ stringArgs = stringArgs.Substring(1);
+ if (stringArgs.EndsWith(")", StringComparison.Ordinal))
+ stringArgs = stringArgs.Substring(0, stringArgs.Length - 1);
+ args = stringArgs.Split(',');
+ for (int i = 0; i < args.Length; i++)
+ args[i] = args[i].Trim();
+ }
+
+ if (type == "")
+ type = null;
+ if (name == "")
+ name = null;
+ }
+
+ public void DeobfuscateEnd() {
+ DeobfuscateCleanUp();
+ }
+
+ public void DeobfuscateCleanUp() {
+ if (assemblyClient != null) {
+ assemblyClient.Dispose();
+ assemblyClient = null;
+ }
+ }
+
+ void DeobfuscateMethods() {
+ if (savedMethodBodies != null) {
+ savedMethodBodies.RestoreAll();
+ savedMethodBodies = null;
+ }
+ deob.DeobfuscatedFile = null;
+
+ if (!options.ControlFlowDeobfuscation) {
+ if (options.KeepObfuscatorTypes || deob.Type == "un")
+ return;
+ }
+
+ bool isVerbose = !Logger.Instance.IgnoresEvent(LoggerEvent.Verbose);
+ bool isVV = !Logger.Instance.IgnoresEvent(LoggerEvent.VeryVerbose);
+ if (isVerbose)
+ Logger.v("Deobfuscating methods");
+ var methodPrinter = new MethodPrinter();
+ var cflowDeobfuscator = new BlocksCflowDeobfuscator(deob.BlocksDeobfuscators);
+ foreach (var method in GetAllMethods()) {
+ if (isVerbose) {
+ Logger.v("Deobfuscating {0} ({1:X8})", Utils.RemoveNewlines(method), method.MDToken.ToUInt32());
+ Logger.Instance.Indent();
+ }
+
+ int oldIndentLevel = Logger.Instance.IndentLevel;
+ //TODO: Re-enable exception handler
+ //try {
+ Deobfuscate(method, cflowDeobfuscator, methodPrinter, isVerbose, isVV);
+ /*}
+ catch (Exception ex) {
+ if (!CanLoadMethodBody(method)) {
+ if (isVerbose)
+ Logger.v("Invalid method body. {0:X8}", method.MDToken.ToInt32());
+ method.Body = new CilBody();
+ }
+ else {
+ Logger.w("Could not deobfuscate method {0:X8}. Hello, E.T.: {1}", // E.T. = exception type
+ method.MDToken.ToInt32(),
+ ex.GetType());
+ }
+ }
+ finally {
+ Logger.Instance.IndentLevel = oldIndentLevel;
+ }*/
+
+ RemoveNoInliningAttribute(method);
+
+ if (isVerbose)
+ Logger.Instance.DeIndent();
+ }
+ }
+
+ static bool CanLoadMethodBody(MethodDef method) {
+ try {
+ var body = method.Body;
+ return true;
+ }
+ catch {
+ return false;
+ }
+ }
+
+ bool CanOptimizeLocals() {
+ // Don't remove any locals if we must preserve StandAloneSig table
+ return (GetMetadataFlags() & MetadataFlags.PreserveStandAloneSigRids) == 0;
+ }
+
+ void Deobfuscate(MethodDef method, BlocksCflowDeobfuscator cflowDeobfuscator, MethodPrinter methodPrinter, bool isVerbose, bool isVV) {
+ if (!HasNonEmptyBody(method))
+ return;
+
+ var blocks = new Blocks(method);
+
+ int numRemovedLocals = 0;
+ int oldNumInstructions = method.Body.Instructions.Count;
+
+ deob.DeobfuscateMethodBegin(blocks);
+ if (options.ControlFlowDeobfuscation) {
+ cflowDeobfuscator.Initialize(blocks);
+ try {
+ cflowDeobfuscator.Deobfuscate();
+ }
+ catch (Exception) {
+ Logger.e("Error during cflow deobfuscation of {0} ({1:X8})", Utils.RemoveNewlines(method), method.MDToken.ToUInt32());
+ throw;
+ }
+ }
+
+ if (deob.DeobfuscateOther(blocks) && options.ControlFlowDeobfuscation)
+ cflowDeobfuscator.Deobfuscate();
+
+ if (options.ControlFlowDeobfuscation) {
+ if (CanOptimizeLocals())
+ numRemovedLocals = blocks.OptimizeLocals();
+ blocks.RepartitionBlocks();
+ }
+
+ DeobfuscateStrings(blocks);
+ deob.DeobfuscateMethodEnd(blocks);
+
+ blocks.GetCode(out var allInstructions, out var allExceptionHandlers);
+ DotNetUtils.RestoreBody(method, allInstructions, allExceptionHandlers);
+
+ if (isVerbose && numRemovedLocals > 0)
+ Logger.v("Removed {0} unused local(s)", numRemovedLocals);
+ int numRemovedInstructions = oldNumInstructions - method.Body.Instructions.Count;
+ if (isVerbose && numRemovedInstructions > 0)
+ Logger.v("Removed {0} dead instruction(s)", numRemovedInstructions);
+
+ if (isVV) {
+ Logger.Log(LoggerEvent.VeryVerbose, "Deobfuscated code:");
+ Logger.Instance.Indent();
+ methodPrinter.Print(LoggerEvent.VeryVerbose, allInstructions, allExceptionHandlers);
+ Logger.Instance.DeIndent();
+ }
+ }
+
+ bool HasNonEmptyBody(MethodDef method) => method.HasBody && method.Body.Instructions.Count > 0;
+
+ void DeobfuscateStrings(Blocks blocks) {
+ switch (options.StringDecrypterType) {
+ case DecrypterType.None:
+ break;
+
+ case DecrypterType.Static:
+ deob.DeobfuscateStrings(blocks);
+ break;
+
+ case DecrypterType.Delegate:
+ case DecrypterType.Emulate:
+ dynamicStringInliner.Decrypt(blocks);
+ break;
+
+ default:
+ throw new ApplicationException($"Invalid string decrypter type '{options.StringDecrypterType}'");
+ }
+ }
+
+ void RemoveNoInliningAttribute(MethodDef method) {
+ method.IsNoInlining = false;
+ for (int i = 0; i < method.CustomAttributes.Count; i++) {
+ var cattr = method.CustomAttributes[i];
+ if (cattr.TypeFullName != "System.Runtime.CompilerServices.MethodImplAttribute")
+ continue;
+ int options = 0;
+ if (!GetMethodImplOptions(cattr, ref options))
+ continue;
+ if (options != 0 && options != (int)MethodImplAttributes.NoInlining)
+ continue;
+ method.CustomAttributes.RemoveAt(i);
+ i--;
+ }
+ }
+
+ static bool GetMethodImplOptions(CustomAttribute cattr, ref int value) {
+ if (cattr.IsRawBlob)
+ return false;
+ if (cattr.ConstructorArguments.Count != 1)
+ return false;
+ if (cattr.ConstructorArguments[0].Type.ElementType != ElementType.I2 &&
+ cattr.ConstructorArguments[0].Type.FullName != "System.Runtime.CompilerServices.MethodImplOptions")
+ return false;
+
+ var arg = cattr.ConstructorArguments[0].Value;
+ if (arg is short) {
+ value = (short)arg;
+ return true;
+ }
+ if (arg is int) {
+ value = (int)arg;
+ return true;
+ }
+
+ return false;
+ }
+
+ public override string ToString() {
+ if (options == null || options.Filename == null)
+ return base.ToString();
+ return options.Filename;
+ }
+
+ [Flags]
+ enum SimpleDeobFlags {
+ HasDeobfuscated = 0x1,
+ }
+ Dictionary simpleDeobfuscatorFlags = new Dictionary();
+ bool Check(MethodDef method, SimpleDeobFlags flag) {
+ if (method == null)
+ return false;
+ simpleDeobfuscatorFlags.TryGetValue(method, out var oldFlags);
+ simpleDeobfuscatorFlags[method] = oldFlags | flag;
+ return (oldFlags & flag) == flag;
+ }
+ bool Clear(MethodDef method, SimpleDeobFlags flag) {
+ if (method == null)
+ return false;
+ if (!simpleDeobfuscatorFlags.TryGetValue(method, out var oldFlags))
+ return false;
+ simpleDeobfuscatorFlags[method] = oldFlags & ~flag;
+ return true;
+ }
+
+ void Deobfuscate(MethodDef method, string msg, Action handler) {
+ if (savedMethodBodies != null)
+ savedMethodBodies.Save(method);
+
+ Logger.v("{0}: {1} ({2:X8})", msg, Utils.RemoveNewlines(method), method.MDToken.ToUInt32());
+ Logger.Instance.Indent();
+
+ if (HasNonEmptyBody(method)) {
+ try {
+ var blocks = new Blocks(method);
+
+ handler(blocks);
+
+ blocks.GetCode(out var allInstructions, out var allExceptionHandlers);
+ DotNetUtils.RestoreBody(method, allInstructions, allExceptionHandlers);
+ }
+ catch {
+ Logger.v("Could not deobfuscate {0:X8}", method.MDToken.ToInt32());
+ }
+ }
+
+ Logger.Instance.DeIndent();
+ }
+
+ void ISimpleDeobfuscator.MethodModified(MethodDef method) => Clear(method, SimpleDeobFlags.HasDeobfuscated);
+ void ISimpleDeobfuscator.Deobfuscate(MethodDef method) => ((ISimpleDeobfuscator)this).Deobfuscate(method, 0);
+
+ void ISimpleDeobfuscator.Deobfuscate(MethodDef method, SimpleDeobfuscatorFlags flags) {
+ bool force = (flags & SimpleDeobfuscatorFlags.Force) != 0;
+ if (method == null || (!force && Check(method, SimpleDeobFlags.HasDeobfuscated)))
+ return;
+
+ Deobfuscate(method, "Deobfuscating control flow", (blocks) => {
+ bool disableNewCFCode = (flags & SimpleDeobfuscatorFlags.DisableConstantsFolderExtraInstrs) != 0;
+ var cflowDeobfuscator = new BlocksCflowDeobfuscator(deob.BlocksDeobfuscators, disableNewCFCode);
+ cflowDeobfuscator.Initialize(blocks);
+ cflowDeobfuscator.Deobfuscate();
+ blocks.RepartitionBlocks();
+ });
+ }
+
+ void ISimpleDeobfuscator.DecryptStrings(MethodDef method, IDeobfuscator theDeob) =>
+ Deobfuscate(method, "Static string decryption", (blocks) => theDeob.DeobfuscateStrings(blocks));
+
+ void IDeobfuscatedFile.CreateAssemblyFile(byte[] data, string assemblyName, string extension) {
+ if (extension == null)
+ extension = ".dll";
+ var baseDir = Utils.GetDirName(options.NewFilename);
+ var newName = Path.Combine(baseDir, assemblyName + extension);
+ Logger.n("Creating file {0}", newName);
+ File.WriteAllBytes(newName, data);
+ }
+
+ void IDeobfuscatedFile.StringDecryptersAdded() => UpdateDynamicStringInliner();
+
+ void IDeobfuscatedFile.SetDeobfuscator(IDeobfuscator deob) => this.deob = deob;
+
+ public void Dispose() {
+ DeobfuscateCleanUp();
+ if (module != null)
+ module.Dispose();
+ if (deob != null)
+ deob.Dispose();
+ module = null;
+ deob = null;
+ }
+ }
+}
diff --git a/de4dot.cui/Program.cs b/de4dot.cui/Program.cs
index 2b0f54ad..93b87f6e 100644
--- a/de4dot.cui/Program.cs
+++ b/de4dot.cui/Program.cs
@@ -32,7 +32,7 @@ class ExitException : Exception {
public ExitException(int code) => this.code = code;
}
- class Program {
+ public class Program {
static IList deobfuscatorInfos = CreateDeobfuscatorInfos();
static IList LoadPlugin(string assembly) {
diff --git a/de4dot.mcp/Program.cs b/de4dot.mcp/Program.cs
new file mode 100644
index 00000000..8f6a390d
--- /dev/null
+++ b/de4dot.mcp/Program.cs
@@ -0,0 +1,274 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Hosting;
+
+namespace de4dot.mcp {
+ class Program {
+ static async Task Main(string[] args) {
+ // Check if user requested HTTP Web API mode
+ if (args.Length > 0 && (args[0] == "--http" || args[0] == "-h")) {
+ int port = 8080;
+ if (args.Length > 1 && int.TryParse(args[1], out int parsedPort)) {
+ port = parsedPort;
+ }
+ await StartHttpServer(port);
+ } else {
+ // Otherwise run in standard Stdio-based MCP mode
+ await StartMcpStdioLoop();
+ }
+ }
+
+ #region HTTP Web API Mode
+ static async Task StartHttpServer(int port) {
+ var builder = WebApplication.CreateBuilder();
+ builder.WebHost.ConfigureKestrel(options => {
+ options.ListenAnyIP(port);
+ });
+
+ var app = builder.Build();
+
+ // Root info route
+ app.MapGet("/", () => $"de4dotEx Web API is active and listening on port {port}!\n\nTo deobfuscate, make a POST request to /deobfuscate with a 'file' parameter containing your obfuscated assembly.");
+
+ // Main /deobfuscate POST file handler
+ app.MapPost("/deobfuscate", async (HttpContext context) => {
+ if (!context.Request.HasFormContentType) {
+ return Results.BadRequest("Error: Request must be multipart/form-data containing a 'file' field.");
+ }
+
+ var form = await context.Request.ReadFormAsync();
+ var file = form.Files.GetFile("file");
+
+ if (file == null || file.Length == 0) {
+ return Results.BadRequest("Error: No file uploaded under form-key 'file'.");
+ }
+
+ // 1. Create unique paths in the Temp directory
+ var tempId = Guid.NewGuid().ToString("N");
+ var tempInput = Path.Combine(Path.GetTempPath(), $"{tempId}_{file.FileName}");
+ var tempOutput = Path.Combine(Path.GetTempPath(), $"{tempId}_cleaned_{file.FileName}");
+
+ // 2. Stream uploaded file directly to disk
+ using (var stream = new FileStream(tempInput, FileMode.Create)) {
+ await file.CopyToAsync(stream);
+ }
+
+ try {
+ // 3. Build native arguments list
+ var argsList = new List { tempInput, "-o", tempOutput };
+
+ // Read optional 'options' form field and append if present
+ var extraOptions = form["options"].ToString();
+ if (!string.IsNullOrEmpty(extraOptions)) {
+ var tokens = extraOptions.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ argsList.AddRange(tokens);
+ }
+
+ // 4. Invoke de4dotEx's native engine in-process
+ int exitCode = de4dot.cui.Program.Main(argsList.ToArray());
+
+ if (exitCode != 0 || !File.Exists(tempOutput)) {
+ return Results.Problem("Deobfuscation failed inside the de4dotEx analysis engine.");
+ }
+
+ // 5. Read the processed bytes and send back as binary file stream
+ var fileBytes = await File.ReadAllBytesAsync(tempOutput);
+ return Results.File(fileBytes, "application/octet-stream", $"cleaned_{file.FileName}");
+ }
+ catch (Exception ex) {
+ return Results.Problem($"An unexpected error occurred during deobfuscation: {ex.Message}");
+ }
+ finally {
+ // 5. Always securely clean up our temp files
+ try {
+ if (File.Exists(tempInput)) File.Delete(tempInput);
+ if (File.Exists(tempOutput)) File.Delete(tempOutput);
+ }
+ catch { }
+ }
+ });
+
+ Console.WriteLine($"[de4dotEx] Starting HTTP Web API Server on port {port}...");
+ await app.RunAsync();
+ }
+ #endregion
+
+ #region Stdio MCP Mode
+ static async Task StartMcpStdioLoop() {
+ // Set input and output streams to UTF-8
+ Console.InputEncoding = Encoding.UTF8;
+ Console.OutputEncoding = Encoding.UTF8;
+
+ var reader = Console.In;
+ var writer = Console.Out;
+
+ while (true) {
+ var line = await reader.ReadLineAsync();
+ if (line == null) break;
+
+ try {
+ var jsonNode = JsonNode.Parse(line);
+ if (jsonNode == null) continue;
+
+ var method = jsonNode["method"]?.ToString();
+ var id = jsonNode["id"]?.ToString();
+
+ if (method == "initialize") {
+ SendResponse(writer, id, new {
+ protocolVersion = "2024-11-05",
+ capabilities = new {
+ tools = new { }
+ },
+ serverInfo = new {
+ name = "de4dotEx",
+ version = "1.0.0"
+ }
+ });
+ }
+ else if (method == "notifications/initialized") {
+ // Client notification. No response required.
+ }
+ else if (method == "tools/list") {
+ SendResponse(writer, id, new {
+ tools = new[] {
+ new {
+ name = "deobfuscate",
+ description = "Deobfuscate and unpack a .NET assembly (EXE or DLL) using de4dotEx",
+ inputSchema = new {
+ type = "object",
+ properties = new {
+ file_path = new {
+ type = "string",
+ description = "The absolute file path of the .NET assembly to deobfuscate."
+ },
+ output_path = new {
+ type = "string",
+ description = "Optional custom output path for the deobfuscated assembly."
+ },
+ options = new {
+ type = "string",
+ description = "Optional extra CLI arguments to pass directly to de4dotEx (e.g. '-str delegate' or '--preserve-tokens')."
+ }
+ },
+ required = new[] { "file_path" }
+ }
+ }
+ }
+ });
+ }
+ else if (method == "tools/call") {
+ var toolName = jsonNode["params"]?["name"]?.ToString();
+ if (toolName == "deobfuscate") {
+ var argsObj = jsonNode["params"]?["arguments"];
+ var filePath = argsObj?["file_path"]?.ToString();
+ var outputPath = argsObj?["output_path"]?.ToString();
+ var extraOptions = argsObj?["options"]?.ToString();
+
+ var result = ExecuteDeobfuscation(filePath, outputPath, extraOptions);
+ SendResponse(writer, id, result);
+ } else {
+ SendError(writer, id, -32601, $"Tool '{toolName}' not found.");
+ }
+ }
+ else if (id != null) {
+ SendError(writer, id, -32601, $"Method '{method}' not found.");
+ }
+ }
+ catch (Exception ex) {
+ SendError(writer, null, -32603, ex.Message);
+ }
+ }
+ }
+
+ static void SendResponse(TextWriter writer, string id, object result) {
+ var response = new {
+ jsonrpc = "2.0",
+ id = id != null ? JsonValue.Create(id) : null,
+ result = result
+ };
+ var json = JsonSerializer.Serialize(response);
+ writer.WriteLine(json);
+ writer.Flush();
+ }
+
+ static void SendError(TextWriter writer, string id, int code, string message) {
+ var response = new {
+ jsonrpc = "2.0",
+ id = id != null ? JsonValue.Create(id) : null,
+ error = new {
+ code = code,
+ message = message
+ }
+ };
+ var json = JsonSerializer.Serialize(response);
+ writer.WriteLine(json);
+ writer.Flush();
+ }
+
+ static object ExecuteDeobfuscation(string filePath, string outputPath, string extraOptions) {
+ if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) {
+ return new {
+ content = new[] {
+ new {
+ type = "text",
+ text = $"Error: File '{filePath}' does not exist or is not accessible."
+ }
+ },
+ isError = true
+ };
+ }
+
+ var argsList = new List { filePath };
+ if (!string.IsNullOrEmpty(outputPath)) {
+ argsList.Add("-o");
+ argsList.Add(outputPath);
+ }
+
+ if (!string.IsNullOrEmpty(extraOptions)) {
+ var tokens = extraOptions.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ argsList.AddRange(tokens);
+ }
+
+ var originalOut = Console.Out;
+ var originalError = Console.Error;
+
+ using (var sw = new StringWriter()) {
+ Console.SetOut(sw);
+ Console.SetError(sw);
+
+ int exitCode = 1;
+ try {
+ exitCode = de4dot.cui.Program.Main(argsList.ToArray());
+ }
+ catch (Exception ex) {
+ sw.WriteLine();
+ sw.WriteLine("Exception caught during deobfuscation execution:");
+ sw.WriteLine(ex.ToString());
+ }
+
+ Console.SetOut(originalOut);
+ Console.SetError(originalError);
+
+ string outputLog = sw.ToString();
+
+ return new {
+ content = new[] {
+ new {
+ type = "text",
+ text = $"Execution completed with exit code: {exitCode}\n\nConsole Log:\n{outputLog}"
+ }
+ },
+ isError = exitCode != 0
+ };
+ }
+ }
+ #endregion
+ }
+}
diff --git a/de4dot.mcp/de4dot.mcp.csproj b/de4dot.mcp/de4dot.mcp.csproj
new file mode 100644
index 00000000..98a14868
--- /dev/null
+++ b/de4dot.mcp/de4dot.mcp.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+
+ Exe
+
+ net8.0
+
+
+
+
+
+
+
diff --git a/de4dot.netcore.sln b/de4dot.netcore.sln
index 92a9eb36..9c6e0fc8 100644
--- a/de4dot.netcore.sln
+++ b/de4dot.netcore.sln
@@ -19,6 +19,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "de4dot.cui", "de4dot.cui\de
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "de4dot.mdecrypt", "de4dot.mdecrypt\de4dot.mdecrypt.csproj", "{5C93C5E2-196F-4877-BF65-96FEBFCEFCA1}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "de4dot.mcp", "de4dot.mcp\de4dot.mcp.csproj", "{8F940F69-122B-4EFD-A7F2-4424A325CEE1}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -135,6 +137,18 @@ Global
{5C93C5E2-196F-4877-BF65-96FEBFCEFCA1}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{5C93C5E2-196F-4877-BF65-96FEBFCEFCA1}.Release|Win32.ActiveCfg = Release|Any CPU
{5C93C5E2-196F-4877-BF65-96FEBFCEFCA1}.Release|x86.ActiveCfg = Release|Any CPU
+ {8F940F69-122B-4EFD-A7F2-4424A325CEE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8F940F69-122B-4EFD-A7F2-4424A325CEE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8F940F69-122B-4EFD-A7F2-4424A325CEE1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {8F940F69-122B-4EFD-A7F2-4424A325CEE1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {8F940F69-122B-4EFD-A7F2-4424A325CEE1}.Debug|Win32.ActiveCfg = Debug|Any CPU
+ {8F940F69-122B-4EFD-A7F2-4424A325CEE1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8F940F69-122B-4EFD-A7F2-4424A325CEE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8F940F69-122B-4EFD-A7F2-4424A325CEE1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8F940F69-122B-4EFD-A7F2-4424A325CEE1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {8F940F69-122B-4EFD-A7F2-4424A325CEE1}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {8F940F69-122B-4EFD-A7F2-4424A325CEE1}.Release|Win32.ActiveCfg = Release|Any CPU
+ {8F940F69-122B-4EFD-A7F2-4424A325CEE1}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/dotnet-deobfuscate/SKILL.md b/dotnet-deobfuscate/SKILL.md
new file mode 100644
index 00000000..5ed4d0fb
--- /dev/null
+++ b/dotnet-deobfuscate/SKILL.md
@@ -0,0 +1,84 @@
+---
+name: dotnet-deobfuscate
+description: Deobfuscate, unpack, and analyze protected .NET assemblies (EXE and DLL) using de4dotEx. Use when an AI agent needs to analyze or reverse-engineer a .NET binary that is packed, renamed, or has encrypted strings/control flow.
+---
+
+# .NET Deobfuscation with de4dotEx
+
+This skill guides you through deobfuscating, unpacking, and cleaning protected `.NET` executables (`.exe`) and libraries (`.dll`) using `de4dotEx`.
+
+---
+
+## Static Analysis Mapping (Detect It Easy)
+
+When triaging files using static analysis tools like **Detect It Easy (DiE)**, map the detections to `de4dotEx` execution as follows:
+
+| Detect It Easy (DiE) Detection | Obfuscator / Protector | de4dotEx Command / Options |
+| :--- | :--- | :--- |
+| **Protector: Confuser (1.X)** | ConfuserEx (1.X) | `de4dot ` (Auto-detect) or force: `-d un_confuser` |
+| **Protector: Babel.NET** | Babel.NET | `de4dot ` or force: `-d un_babel` |
+| **Protector: SmartAssembly** | SmartAssembly | `de4dot ` or force: `-d un_sa` |
+| **Protector: dotNET Reactor** | .NET Reactor | `de4dot ` or force: `-d un_reactor` |
+| **Protector: ILProtector** | ILProtector | `de4dot ` *(Requires Strategy B Wine Container)* |
+| **Protector: Agile.NET** | Agile.NET | `de4dot ` *(Requires Strategy B Wine Container)* |
+
+---
+
+## de4dotEx CLI / API Usage
+
+Always try **Auto-detection** first, as de4dotEx parses assembly metadata and `ConfusedByAttribute` attributes to automatically load the correct unpacker:
+
+### 1. Basic Auto-Detection (Standard Run)
+```bash
+de4dot MyAssembly.dll -o CleanAssembly.dll
+```
+
+### 2. Forcing a Specific Deobfuscator (`-d` flag)
+If auto-detection fails or you want to override:
+* **ConfuserEx:** `de4dot -d un_confuser MyAssembly.dll`
+* **.NET Reactor:** `de4dot -d un_reactor MyAssembly.dll`
+* **Babel.NET:** `de4dot -d un_babel MyAssembly.dll`
+* **SmartAssembly:** `de4dot -d un_sa MyAssembly.dll`
+
+### 3. Decrypting Strings (`-str` option)
+If the obfuscator uses dynamic delegates or emulation for string decryption:
+* **Emulation (Recommended):** `de4dot -str emulate MyAssembly.dll`
+* **Delegates:** `de4dot -str delegate MyAssembly.dll`
+* **Static:** `de4dot -str static MyAssembly.dll`
+
+### 4. Renaming & Symbol Restoration
+* **Disable Renaming:** If symbol renaming breaks references, disable it: `--rename-symbols false`
+* **Force Renaming Schema:** Force a specific renaming style: `--rename-symbols true`
+* **Preserve Metadata Tokens:** For seamless decompilation alignment: `--preserve-tokens`
+
+---
+
+## Execution Environments
+
+### Mode A: Native .NET 8.0 Docker Container (Static / Fast)
+Runs natively at peak speed. Fully supports ConfuserEx 1.X, Babel, and SmartAssembly.
+```bash
+docker run --rm -v "$(pwd):/work" -w /work de4dotex -o
+```
+
+### Mode B: Wine .NET 4.8 Container (JIT-hook / Dynamic)
+Required for protectors using native Windows memory APIs (ILProtector, Agile.NET).
+```bash
+# On Intel/AMD hosts:
+docker run --rm -v "$(pwd):/work" de4dotex-wine -o
+
+# On ARM64 Apple Silicon Mac hosts (Forces Rosetta 2 x86 translation):
+docker run --platform linux/amd64 --rm -v "$(pwd):/work" de4dotex-wine -o
+```
+
+### Mode C: Native C# Stdio MCP Server (AI Agent Integration)
+Command for registering inside your MCP client config (`claude_desktop_config.json`):
+```json
+"mcpServers": {
+ "de4dotex": {
+ "command": "dotnet",
+ "args": ["/absolute/path/to/publish-net8.0-mcp/de4dot.mcp.dll"]
+ }
+}
+```
+Exposes the **`deobfuscate`** tool with parameters: `file_path`, `output_path`, and `options` (for passing custom flags like `-str delegate`).
diff --git a/test_api.sh b/test_api.sh
new file mode 100755
index 00000000..75ee3084
--- /dev/null
+++ b/test_api.sh
@@ -0,0 +1,72 @@
+#!/bin/bash
+
+# Exit on error
+set -e
+
+# Help / Usage check
+if [ $# -lt 1 ]; then
+ echo "=========================================================================="
+ echo " de4dotEx HTTP Web API Test Client"
+ echo "=========================================================================="
+ echo "Usage: $0 [optional_extra_flags]"
+ echo ""
+ echo "Examples:"
+ echo " $0 MyProtectedApp.dll"
+ echo " $0 /path/to/Target.exe \"--preserve-tokens -str delegate\""
+ echo "=========================================================================="
+ exit 1
+fi
+
+INPUT_FILE="$1"
+EXTRA_OPTIONS="$2"
+
+# Verify input file exists
+if [ ! -f "$INPUT_FILE" ]; then
+ echo "Error: File '$INPUT_FILE' not found."
+ exit 1
+fi
+
+# Calculate directory and output filename (appending _cleaned before the extension)
+DIR=$(dirname "$INPUT_FILE")
+FILENAME=$(basename "$INPUT_FILE")
+EXTENSION="${FILENAME##*.}"
+BASENAME="${FILENAME%.*}"
+
+# Handle files with no extensions
+if [ "$BASENAME" = "$EXTENSION" ]; then
+ OUTPUT_FILE="${DIR}/${BASENAME}_cleaned"
+else
+ OUTPUT_FILE="${DIR}/${BASENAME}_cleaned.${EXTENSION}"
+fi
+
+echo "[de4dotEx] Uploading '$INPUT_FILE' to http://localhost:8080/deobfuscate..."
+if [ -n "$EXTRA_OPTIONS" ]; then
+ echo "[de4dotEx] Applying native flags: $EXTRA_OPTIONS"
+fi
+
+# Perform HTTP POST request and capture HTTP status code
+HTTP_CODE=$(curl -s -w "%{http_code}" \
+ -F "file=@${INPUT_FILE}" \
+ -F "options=${EXTRA_OPTIONS}" \
+ http://localhost:8080/deobfuscate \
+ -o "${OUTPUT_FILE}")
+
+if [ "$HTTP_CODE" -eq 200 ]; then
+ echo "------------------------------------------------------------"
+ echo "🎉 Success! Cleaned assembly saved to:"
+ echo " $OUTPUT_FILE"
+ echo "------------------------------------------------------------"
+else
+ echo "------------------------------------------------------------"
+ echo "❌ Error: Deobfuscation failed (HTTP status: ${HTTP_CODE})."
+
+ # If server returned an error payload, display it and clean up the file
+ if [ -f "${OUTPUT_FILE}" ]; then
+ echo "Server message:"
+ cat "${OUTPUT_FILE}"
+ echo ""
+ rm -f "${OUTPUT_FILE}"
+ fi
+ echo "------------------------------------------------------------"
+ exit 1
+fi