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/workflows/CI%20build/badge.svg)](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