├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── bin └── .gitkeep ├── deject-docs ├── deject ├── __init__.py ├── main.py └── plugins.py ├── flake.lock ├── flake.nix ├── nix ├── default.nix └── overlays │ ├── chepy │ └── default.nix │ ├── default.nix │ ├── dotnetfile │ └── default.nix │ ├── libmagic │ └── default.nix │ ├── mwcp │ └── default.nix │ └── netstruct │ └── default.nix ├── poetry.lock ├── pyproject.toml ├── scripts ├── __init__.py ├── agenttesla_behaviour.py ├── bulk_extractor.py ├── c3_check.py ├── cobaltstrike_check.py ├── dmg_hashes.py ├── elf_hashes.py ├── elf_imports.py ├── elf_parser.py ├── extractors │ ├── cobaltstrike │ │ ├── 1768.json │ │ └── 1768.py │ ├── kaitai │ │ ├── asn1_der.py │ │ ├── elf.py │ │ ├── mach_o.py │ │ ├── mach_o_fat.py │ │ ├── microsoft_pe.py │ │ └── windows_minidump.py │ └── poshc2Parser │ │ └── poshc2parser.py ├── helpers.py ├── inspect_mmaps.py ├── list_bofs.py ├── list_dlls.py ├── list_exes.py ├── list_libs.py ├── macho_fat_parser.py ├── macho_parser.py ├── malwareconfigextract.py ├── minidump_parser.py ├── ole-tools │ ├── oledump.py │ ├── plugin_dridex.py │ ├── plugin_http_heuristics.py │ ├── plugin_metadata.py │ ├── plugin_msg.py │ ├── plugin_msg_summary.py │ ├── plugin_msi_info.py │ └── plugin_vbaproject.py ├── ole_analytics.py ├── pdf-tools │ ├── pdf-parser.py │ ├── pdfid.ini │ ├── pdfid.py │ ├── pdftojpg.py │ ├── pdftool.py │ └── plugin_triage.py ├── pdf_analytics.py ├── pdf_image.py ├── pdf_modified.py ├── pdf_object.py ├── pdf_triage.py ├── pe_exports.py ├── pe_hashes.py ├── pe_hashlookup.py ├── pe_imports.py ├── pe_packed.py ├── pe_parser.py ├── pe_sections.py ├── pe_signatures.py ├── poshc2_check.py ├── test_plugin.py ├── yarascan.py └── zeek.py ├── settings.yml ├── setup.sh └── tests ├── data ├── hello ├── hello.exe ├── hello.macho └── hello.pdf ├── test_elf.py ├── test_file_type.py ├── test_help.py ├── test_list.py ├── test_macho.py ├── test_main.py ├── test_parsers.py ├── test_pdf.py ├── test_pe.py └── test_yara.py /.gitignore: -------------------------------------------------------------------------------- 1 | *__pycache__* 2 | *.DMP 3 | 4 | # Python 5 | .cache 6 | .coverage 7 | build 8 | htmlcov 9 | *.egg* 10 | *.pyc 11 | *.pytest* 12 | .venv 13 | *.mypy_cache 14 | 15 | # Vim 16 | *.sw* 17 | app.log 18 | 19 | # Pychar 20 | *.idea* 21 | 22 | #vscode 23 | .vscode 24 | .VSCode* 25 | dist 26 | docs 27 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "scripts/extractors/MalwareConfigExtractor"] 2 | path = scripts/extractors/MalwareConfigExtractor 3 | url = https://github.com/c3rb3ru5d3d53c/mwcfg-modules.git 4 | [submodule "scripts/extractors/RelayRumbler"] 5 | path = scripts/extractors/RelayRumbler 6 | url = https://github.com/ajpc500/RelayRumbler 7 | [submodule "scripts/yara-rules"] 8 | path = scripts/yara-rules 9 | url = https://github.com/elastic/protections-artifacts.git 10 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | exclude: ^scripts/(yara-rules|extractors|pdf-tools|ole-tools|)/|^tests/|^scripts/test_plugin.py 7 | - id: end-of-file-fixer 8 | exclude: ^scripts/(yara-rules|extractors|pdf-tools|ole-tools|)/|^tests/|^scripts/test_plugin.py 9 | - id: debug-statements 10 | exclude: ^scripts/(yara-rules|extractors|pdf-tools|ole-tools|)/|^tests/|^scripts/test_plugin.py 11 | - id: check-yaml 12 | exclude: ^scripts/(yara-rules|extractors|pdf-tools|ole-tools|)/|^tests/|^scripts/test_plugin.py 13 | - id: check-toml 14 | exclude: ^scripts/(yara-rules|extractors|pdf-tools|ole-tools|)/|^tests/|^scripts/test_plugin.py 15 | - repo: https://github.com/asottile/add-trailing-comma 16 | rev: v3.1.0 17 | hooks: 18 | - id: add-trailing-comma 19 | exclude: ^scripts/(yara-rules|extractors|pdf-tools|ole-tools|)/|^tests/|^scripts/test_plugin.py 20 | - repo: https://github.com/hhatto/autopep8 21 | rev: v2.3.2 22 | hooks: 23 | - id: autopep8 24 | exclude: ^scripts/(yara-rules|extractors|pdf-tools|ole-tools|)/|^tests/|^scripts/test_plugin.py 25 | - repo: https://github.com/pycqa/flake8 26 | rev: 7.1.2 27 | hooks: 28 | - id: flake8 29 | args: ["--ignore=E501,E722,W605"] 30 | exclude: ^scripts/(yara-rules|extractors|pdf-tools|ole-tools|)/|^tests/|^scripts/test_plugin.py 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:24.04 2 | 3 | SHELL ["/bin/bash","-c"] 4 | 5 | WORKDIR /deject 6 | 7 | ENV DEBIAN_FRONTEND=noninteractive 8 | 9 | RUN apt update ; apt upgrade -y \ 10 | && apt install git curl build-essential libffi-dev python3 python3-dev python3-pip libtool libssl-dev swig libfuzzy-dev libewf-dev libexpat1 openssl nix -y 11 | 12 | RUN echo 'deb http://download.opensuse.org/repositories/security:/zeek/xUbuntu_24.04/ /' >> /etc/apt/sources.list.d/security:zeek.list ; \ 13 | curl -fsSL "https://download.opensuse.org/repositories/security:zeek/xUbuntu_24.04/Release.key" | gpg --dearmor | tee /etc/apt/trusted.gpg.d/security_zeek.gpg > /dev/null ; \ 14 | apt update ; apt install zeek-6.0 -y 15 | 16 | RUN nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs \ 17 | && echo "filter-syscalls = false" >> /etc/nix/nix.conf \ 18 | && nix-channel --update \ 19 | && nix-env --install --attr nixpkgs.bulk_extractor nixpkgs.radare2 20 | 21 | RUN ln -sf /usr/bin/python3 /usr/bin/python \ 22 | && curl -fsSL https://raw.githubusercontent.com/python-poetry/install.python-poetry.org/main/install-poetry.py | python3 23 | 24 | ENV PATH="/nix/var/nix/profiles/default/bin:/root/.local/bin:$PATH" 25 | 26 | RUN echo "export PATH=$PATH" >> ~/.bashrc 27 | 28 | COPY . /deject 29 | 30 | RUN poetry install --compile 31 | 32 | ENTRYPOINT ["poetry","run","deject"] 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all default arm64 amd64 poetry flake install-deps 2 | 3 | default: 4 | sudo docker build -t deject . 5 | 6 | all: multi 7 | 8 | arm64: 9 | sudo docker build -t deject . --platform linux/arm64 10 | 11 | amd64: 12 | sudo docker build -t deject . --platform linux/amd64 13 | 14 | multi: 15 | sudo docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 16 | $(MAKE) amd64 17 | $(MAKE) arm64 18 | 19 | poetry: 20 | poetry install --compile 21 | 22 | install-deps: 23 | sudo apt install git curl build-essential libffi-dev python3 python3-dev python3-pip libtool libssl-dev swig libfuzzy-dev libewf-dev libexpat1 openssl 24 | 25 | flake: 26 | sudo nix build . 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEJECT - Memory dump and Sample analysis tool 2 | 3 | --- 4 | 5 | ## Dependencies 6 | This project has the following dependencies that cannot be installed via Python: 7 | * Poetry - Dependency management for Python (https://python-poetry.org/) 8 | * Radare2/Rizin - Reverse Engineering Framework (https://rada.re/ / https://rizin.re/) 9 | * libfuzzy-dev 10 | 11 | Required for M2Crypto: 12 | * libssl-dev 13 | * swig 14 | * python3-dev 15 | * gcc 16 | 17 | For the Zeek plugin: 18 | * [Zeek](https://github.com/zeek/zeek) 19 | 20 | For the Bulk Extractor plugin: 21 | * [Bulk Extractor](https://github.com/simsong/bulk_extractor) 22 | 23 | **NB**: Support for Rizin is still new and has not been fully tested. 24 | 25 | ## Installation 26 | 27 | Clone the repository with GIT using the following command: 28 | 29 | `git clone --recurse-submodules https://github.com/WithSecureLabs/deject.git` 30 | 31 | In the deject folder run: 32 | 33 | `poetry install` 34 | 35 | This should install the Python dependencies and create a new virtual environment for Deject. 36 | Run Deject by typing the following command in the Deject directory: 37 | `poetry run deject` 38 | 39 | ## Building with Nix 40 | 41 | This project contains `flake.nix` file, which means that following outputs can be produced: 42 | ``` 43 | ├───devShells 44 | │ └───x86_64-linux 45 | │ └───default: development environment 'nix-shell' 46 | └───packages 47 | └───x86_64-linux 48 | ├───default: package 'python3.11-deject-0.4.0' 49 | └───deject: package 'python3.11-deject-0.4.0' 50 | ``` 51 | 52 | ### devShell 53 | 54 | `devShell` is, as the name suggest, dev-friendly environment, with all the required dependencies, to build and continue development of this project. 55 | This also creates a 'temporary' shell, with the built package provided, added to that given devShell PATH. 56 | 57 | In order to do that, run the following in Deject's root dir: 58 | 59 | `nix develop` 60 | 61 | > no other information is required, as there's only one devShell associated with this flake 62 | 63 | ### binary output 64 | 65 | If you want to build a binary of this project, using Nix, run the following inside Deject's root dir: 66 | 67 | `nix build` 68 | 69 | > no other information is required in this case neither, as both outputs for 'packages' are identical, as seen in the output of `nix flake show` above 70 | 71 | This will create a directory `result`, and the deject binary will be located under `./result/bin/deject`. 72 | 73 | ## Tests 74 | To run the tests, to check that Deject is working correct, use the following command in the Deject directory: 75 | 76 | `poetry run pytest` 77 | 78 | ## M2Crypto Install 79 | If the above command fails on the M2Crypto Python package, install the following dependancies: 80 | `libssl-dev swig python3-dev gcc` 81 | (these are the package names for Debian, if using RedHat names might be different.) 82 | 83 | ## Zeek Install 84 | Install Zeek from via a package manager (https://docs.zeek.org/en/master/install.html) or from source (https://github.com/zeek/zeek). 85 | Run `ln -s /path/to/zeek bin/zeek` to link the Zeek binary in the `bin` directory for the Zeek plugin to find it. 86 | This is only needed if you want to run the Zeek plugin to analyse pcap files. 87 | 88 | ## Basic Usage 89 | 90 | To list the available plugins: `poetry run deject plugins` 91 | 92 | In the deject folder run `poetry run deject run ` 93 | 94 | To run only a single plugin use the `--include ` option. 95 | 96 | Some plugins require an argument, place this after the memory dump, such as: 97 | 98 | `--include pe_hashes ` 99 | 100 | To provide an argument starting with a `-` or more than one argument to the application, use quotes: 101 | * `--include cobaltstrike_check " -J "` 102 | * `--include pe_sections "carve .text"` 103 | 104 | ## Dockerfile 105 | To provide a unified environment a Dockerfile is provided. 106 | 107 | Buildx is the suggested client, install buildx from https://docs.docker.com/build/install-buildx/ (documentation: https://github.com/docker/buildx#linux-packages). (On Debian run `apt-get install docker-buildx-plugin`) 108 | Running `docker buildx install` makes Buildx the default build client (this only needs to be done once.) 109 | 110 | ``` 111 | docker buildx install 112 | docker build --tag deject . 113 | cd dir/with/malware 114 | docker run -v "$PWD":/work --tty deject --include pdf_object /work/ 115 | ``` 116 | 117 | ## Malware Samples 118 | If you want to test Deject but don't have any malware, you can download malware samples from: 119 | https://github.com/jstrosch/malware-samples 120 | Beware that these are live samples, use at your own risk. 121 | 122 | ## Generating Documentation 123 | Documentation can be generated using Doxygen (https://github.com/doxygen/doxygen) by using the following command: 124 | ``` 125 | doxygen deject-docs 126 | ``` 127 | This will output HTML pages to the `docs/` directory. 128 | 129 | ## Settings 130 | 131 | ### VTKEY 132 | For plugins that require a VirusTotal API key, set a `VT_KEY` environment variable: 133 | ``` 134 | set VT_KEY= 135 | ``` 136 | 137 | ### Yara Rules 138 | The default Yara rule repository is located at `scripts/yara-rules`. To use a different set of Yara rules, set the `RULES` environment variable: 139 | ``` 140 | set RULES= 141 | ``` 142 | 143 | ### Zeek 144 | The default location for Zeek is the `bin/` directory. This can be changed using the `ZEEK_PATH` environment variable: 145 | ``` 146 | set ZEEK_PATH=` 147 | ``` 148 | You will need to install Zeek separately. 149 | 150 | ### Bulk Extractor 151 | The default location for Bulk Extractor is the `bin/` directory. This can be changed using the `BULK_PATH` environment variable: 152 | ``` 153 | set BULK_PATH= 154 | ``` 155 | You will need to install Bulk Extractor separately. 156 | 157 | ## Useful Links 158 | 159 | * https://www.forrest-orr.net/post/masking-malicious-memory-artifacts-part-ii-insights-from-moneta 160 | * https://github.com/jstrosch/malware-samples 161 | 162 | ## Acknowledgements 163 | * [Didier StevenS](https://github.com/DidierStevens/DidierStevensSuite) (1768.py and pdftool/pdfid/pdf-parser) 164 | * [Chepy](https://github.com/securisec/chepy) 165 | * [mwcfg-modules](https://github.com/c3rb3ru5d3d53c/mwcfg-modules/tree/master) 166 | * [Malduck](https://github.com/CERT-Polska/malduck) 167 | * [Radare2](https://github.com/radareorg/radare2)/[Rizin](https://github.com/rizinorg/rizin) 168 | * [Yara](https://github.com/virustotal/yara) 169 | * [KaitaiStruct](https://github.com/kaitai-io/kaitai_struct) 170 | * [Protections Artifacts](https://github.com/elastic/protections-artifacts) (Elastic) 171 | * [pefile](https://github.com/erocarrera/pefile) 172 | * [dc3-mwcp](https://github.com/dod-cyber-crime-center/DC3-MWCP) 173 | * [minidump](https://github.com/skelsec/minidump/) 174 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WithSecureLabs/deject/286751faa1791ae7f8ca74c205e94b28b474b4a0/bin/.gitkeep -------------------------------------------------------------------------------- /deject/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WithSecureLabs/deject/286751faa1791ae7f8ca74c205e94b28b474b4a0/deject/__init__.py -------------------------------------------------------------------------------- /deject/main.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief This is the main Deject application. 3 | """ 4 | from pathlib import Path 5 | from typing import List 6 | import r2pipe 7 | import concurrent.futures 8 | 9 | try: 10 | import rzpipe 11 | except OSError: 12 | pass 13 | 14 | import typer 15 | from tabulate import tabulate 16 | 17 | import scripts 18 | from deject.plugins import Deject 19 | 20 | app = typer.Typer(name="Deject") 21 | 22 | 23 | def check_path(file_path: str) -> Path: 24 | """Check the file path provided exists on the filesystem.""" 25 | path = Path(file_path) 26 | if not path.exists(): 27 | typer.secho(f"Path: '{file_path}' not found", fg=typer.colors.RED) 28 | raise typer.Abort() 29 | return path.resolve() 30 | 31 | 32 | def check_r2_installed(file_path: Path): 33 | """Provide error message if r2 is not installed on operating system.""" 34 | try: 35 | r = r2pipe.open(str(file_path), flags=["-2"]) 36 | except Exception: 37 | try: 38 | r = rzpipe.open(str(file_path), flags=["-2"]) 39 | except Exception as err: 40 | typer.secho( 41 | f"{err}. Please install either Rizin or Radare2 for your distribution", fg=typer.colors.RED, 42 | ) 43 | raise typer.Abort() 44 | return r 45 | 46 | 47 | def pretty_print(results: dict): 48 | """Print the output of the plugins in a table""" 49 | typer.secho( 50 | tabulate(results["rows"], results["header"], tablefmt="fancy_grid"), 51 | fg=typer.colors.GREEN, 52 | ) 53 | 54 | 55 | @app.command() 56 | def plugins(): 57 | """List available plugins for deject""" 58 | try: 59 | plugins = scripts.names() 60 | except KeyError: 61 | typer.secho("No plugins found!", fg=typer.colors.RED, bold=True) 62 | raise typer.Abort() 63 | plugin_docs = [] 64 | for plugin in plugins: 65 | plugin_docs.append([plugin, scripts.docs(plugin)]) 66 | 67 | headers = ["Plugin", "Description"] 68 | 69 | typer.secho( 70 | tabulate(plugin_docs, headers=headers, tablefmt="fancy_grid"), 71 | fg=typer.colors.GREEN, 72 | bold=True, 73 | ) 74 | 75 | 76 | @app.command() 77 | def run( 78 | file_path: str = typer.Argument(...), 79 | file_type: str = typer.Option( 80 | "", "--file-type", "-t", help="Run plugins for a file type. Examples include dmp, pe, pcap, pdf", 81 | ), 82 | quiet: bool = typer.Option(False, "--quiet", "-q"), 83 | save: bool = typer.Option(False, "--save", "-s"), 84 | noasync: bool = typer.Option( 85 | False, "--no-async", "-na", help="Do not use concurrent for running plugins", 86 | ), 87 | exclude: List[str] = [], 88 | include: List[str] = [], 89 | plugin_args: str = typer.Argument(False), 90 | ): 91 | """Execute the available selection of plugins.""" 92 | 93 | memory_dump_path = check_path(file_path) 94 | r2 = check_r2_installed(memory_dump_path) 95 | Deject.create( 96 | memory_dump=memory_dump_path, quiet=quiet, 97 | plugin_args=plugin_args, r2=r2, 98 | ) 99 | pre_include = ["pe_exports", "list_libs", "pe_hashlookup"] 100 | 101 | if file_type == "pe": 102 | pre_include = [ 103 | "pe_packed", "pe_exports", "pe_hashes", "pe_imports", "pe_sections", "malwareconfigextract", "poshc2_check", 104 | "c3_check", "cobaltstrike_check", "agenttesla_behaviour", "list_libs", "pe_hashlookup", "pe_parser", 105 | ] 106 | if file_type == "dmp": 107 | pre_include = [ 108 | "list_exes", "list_dlls", "minidump_parser", "inspect_mmaps", "malwareconfigextract", "poshc2_check", 109 | "c3_check", "cobaltstrike_check", 110 | ] 111 | if file_type == "dmg": 112 | pre_include = ["dmg_hashes", "pe_hashlookup"] 113 | if file_type == "elf": 114 | pre_include = [ 115 | "pe_exports", "pe_packed", "elf_imports", 116 | "elf_hashes", "list_libs", "pe_hashlookup", "elf_parser", 117 | ] 118 | if file_type == "macho": 119 | pre_include = [ 120 | "pe_exports", "elf_imports", "dmg_hashes", 121 | "list_libs", "pe_hashlookup", "macho_parser", 122 | ] 123 | if file_type == "pdf": 124 | pre_include = ["pdf_modified", "pdf_analytics", "pdf_triage"] 125 | if file_type == "ole": 126 | pre_include = ["ole_analytics"] 127 | if file_type == "pcap": 128 | pre_include = ["zeek"] 129 | if len(include) > 0: 130 | include = include[0].split(',') 131 | include = [ 132 | plugin for plugin in scripts.names() 133 | if plugin in list(include) 134 | ] 135 | if len(exclude) > 0: 136 | exclude = exclude[0].split(',') 137 | if exclude[0] == "*": 138 | pre_include = [] 139 | pre_include = [ 140 | plugin for plugin in pre_include if plugin not in list(exclude) 141 | ] 142 | include.extend(pre_include) 143 | plugins_to_run = set(include) 144 | 145 | if not plugins_to_run: 146 | typer.secho( 147 | "No plugins selected to run", 148 | fg=typer.colors.RED, bold=True, 149 | ) 150 | raise typer.Abort() 151 | 152 | table = [ 153 | ["file", memory_dump_path.name], 154 | ["file type", file_type], 155 | ["quiet mode", quiet], 156 | ["save dumps", save], 157 | ["excluded plugins", exclude], 158 | ["plugin args", plugin_args], 159 | ] 160 | typer.secho( 161 | "\n ######################### [ EXECUTING DEJECT ] ##########################\n", 162 | fg=typer.colors.CYAN, 163 | ) 164 | typer.secho( 165 | tabulate(table, headers=["Deject", "Setting"], tablefmt="fancy_grid"), 166 | fg=typer.colors.CYAN, 167 | ) 168 | typer.secho( 169 | f"Running the selected plugins: {plugins_to_run}\n", fg=typer.colors.CYAN, 170 | ) 171 | 172 | if not noasync: 173 | concurrent_plugin_names = [ 174 | "yarascan", "minidump_parser", "malwareconfigextract", 175 | "zeek", "macho_parser", "macho_fat_parser", "elf_parser", "pe_parser", "pe_hashlookup", 176 | ] 177 | if to_run := list(set(concurrent_plugin_names).intersection(plugins_to_run)): 178 | with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: 179 | concurrent_plugins = { 180 | executor.submit( 181 | scripts.run, plugin, 182 | ): plugin for plugin in to_run 183 | } 184 | for future in concurrent.futures.as_completed(concurrent_plugins): 185 | plugin_name = concurrent_plugins[future] 186 | try: 187 | con_res = future.result() 188 | except Exception as e: 189 | typer.secho( 190 | f"{plugin_name} generated an exception: {e}", 191 | ) 192 | else: 193 | typer.secho( 194 | f"\n[output of plugin: {plugin_name}]", fg=typer.colors.YELLOW, bold=True, 195 | ) 196 | if isinstance(con_res, dict): 197 | pretty_print(con_res) 198 | else: 199 | typer.secho(con_res) 200 | 201 | plugins_to_run = plugins_to_run - set(concurrent_plugin_names) 202 | with typer.progressbar(plugins_to_run) as progress: 203 | for plugin in progress: 204 | typer.secho( 205 | f"\n[running plugin: {plugin}]", fg=typer.colors.YELLOW, bold=True, 206 | ) 207 | # if the plugin returns a dict then we will use it to pretty print the result of the execution 208 | res = scripts.run(plugin) 209 | if isinstance(res, dict): 210 | pretty_print(res) 211 | else: 212 | typer.secho(res) 213 | return 0 214 | 215 | 216 | @app.command() 217 | def help(plugin: str = typer.Argument(...)): 218 | """Show plugin help""" 219 | if plugin in scripts.names(): 220 | imported = getattr(__import__("scripts", fromlist=[plugin]), plugin) 221 | try: 222 | imported.help() 223 | except Exception: 224 | typer.secho(f"No help for plugin: {plugin}.", fg=typer.colors.RED) 225 | else: 226 | typer.secho(f"Plugin ({plugin}) does not exist!", fg=typer.colors.RED) 227 | 228 | return 0 229 | 230 | 231 | if __name__ == "__main__": 232 | app() 233 | -------------------------------------------------------------------------------- /deject/plugins.py: -------------------------------------------------------------------------------- 1 | # plugins.py 2 | """! 3 | @brief This file is used to run plugin files, from the "scripts" folder. 4 | """ 5 | from typing import Any, Dict 6 | from pathlib import Path 7 | import functools 8 | import importlib 9 | from collections import namedtuple 10 | from importlib import resources 11 | 12 | 13 | class Deject: 14 | def __init__(self) -> None: 15 | """Blocks direct instantiation of a class object instance. 16 | 17 | Deject is designed as a 'singleton', the run command sets up the class attributes via the create method, 18 | import Deject in a plugin will give a plugin access to those attributes. 19 | """ 20 | raise TypeError("singleton class") 21 | file_path: Any 22 | r2_handler: Any 23 | quiet: bool 24 | plugin_args: Any 25 | 26 | @classmethod 27 | def create(cls, memory_dump: Path, quiet: bool = False, plugin_args: Any = False, r2: Any = None): 28 | """Modify the state of the Deject class instance. 29 | 30 | Used by the Deject run command. 31 | """ 32 | cls.file_path = str(memory_dump) 33 | cls.r2_handler = r2 34 | cls.quiet = quiet 35 | cls.plugin_args = plugin_args 36 | return cls 37 | 38 | @staticmethod 39 | def plugin(func): 40 | """Decorator for registering a new plugin to the Deject application. 41 | 42 | Attached to Deject class so it's also imported with Deject to save a separate import and keep 43 | the overhead for writing a compatible plugin to the minimum. 44 | """ 45 | package, _, plugin = func.__module__.rpartition(".") 46 | pkg_info = _PLUGINS.setdefault(package, {}) 47 | pkg_info[plugin] = Plugin(name=plugin, func=func) 48 | return func 49 | 50 | 51 | # Functions for dynamic loading of plugins from scripts folder 52 | # also utilises the /scripts/__init__.py to work 53 | Plugin = namedtuple("Plugin", ("name", "func")) 54 | _PLUGINS: Dict[str, Any] = {} 55 | 56 | 57 | def names(package): 58 | """List all plugins in one package""" 59 | _import_all(package) 60 | return sorted(_PLUGINS[package]) 61 | 62 | 63 | def get(package, plugin): 64 | """Get a given plugin""" 65 | _import(package, plugin) 66 | return _PLUGINS[package][plugin].func 67 | 68 | 69 | def call(package, plugin, *args, **kwargs): 70 | """Call the given plugin""" 71 | plugin_func = get(package, plugin) 72 | return plugin_func(*args, **kwargs) 73 | 74 | 75 | def doc(package, plugin): 76 | """Call the given plugin""" 77 | plugin_func = get(package, plugin) 78 | return plugin_func.__doc__ 79 | 80 | 81 | def _import(package, plugin): 82 | """Import the given plugin file from a package""" 83 | importlib.import_module(f"{package}.{plugin}") 84 | 85 | 86 | def _import_all(package): 87 | """Import all plugins in a package""" 88 | files = ( 89 | resource.name for resource in resources.files( 90 | package, 91 | ).iterdir() if resource.is_file() 92 | ) 93 | plugins = [f[:-3] for f in files if f.endswith(".py") and f[0] != "_"] 94 | for plugin in plugins: 95 | _import(package, plugin) 96 | 97 | 98 | def names_factory(package): 99 | """Create a names() function for one package""" 100 | return functools.partial(names, package) 101 | 102 | 103 | def get_factory(package): 104 | """Create a get() function for one package""" 105 | return functools.partial(get, package) 106 | 107 | 108 | def call_factory(package): 109 | """Create a call() function for one package""" 110 | return functools.partial(call, package) 111 | 112 | 113 | def doc_factory(package): 114 | """Create __doc__ function for one package""" 115 | return functools.partial(doc, package) 116 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1740828860, 6 | "narHash": "sha256-cjbHI+zUzK5CPsQZqMhE3npTyYFt9tJ3+ohcfaOF/WM=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "303bd8071377433a2d8f76e684ec773d70c5b642", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Memory dump and Sample analysis tool"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | }; 7 | 8 | outputs = inputs: let 9 | inherit (inputs.nixpkgs) lib; 10 | 11 | supportedSystems = ["x86_64-linux"]; 12 | genSystems = lib.genAttrs supportedSystems; 13 | 14 | pkgsFor = system: 15 | import inputs.nixpkgs { 16 | inherit system; 17 | overlays = [(import ./nix/overlays)]; 18 | }; 19 | in { 20 | packages = genSystems (system: let 21 | pkgs = pkgsFor system; 22 | in rec { 23 | default = pkgs.python311Packages.callPackage ./nix {}; 24 | deject = default; 25 | }); 26 | 27 | devShells = genSystems (system: let 28 | pkgs = pkgsFor system; 29 | in { 30 | default = pkgs.mkShell { 31 | packages = with pkgs; [ 32 | python3 33 | inputs.self.packages.${system}.default 34 | ]; 35 | }; 36 | }); 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /nix/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | buildPythonPackage, 4 | fetchFromGitHub, 5 | poetry-core, 6 | radare2, 7 | ssdeep, 8 | openssl, 9 | util-linux, 10 | file, 11 | # python 12 | exceptiongroup, 13 | hexdump, 14 | kaitaistruct, 15 | malduck, 16 | minidump, 17 | pdf2image, 18 | pefile, 19 | pycryptodome, 20 | python-magic, 21 | r2pipe, 22 | requests, 23 | rzpipe, 24 | tabulate, 25 | telfhash, 26 | typer, 27 | yara-python, 28 | oletools, 29 | olefile, 30 | # from overlay 31 | chepy, 32 | dotnetfile, 33 | netstruct, 34 | libmagic, 35 | mwcp, 36 | pyyaml, 37 | }: let 38 | yara-rules = fetchFromGitHub { 39 | owner = "elastic"; 40 | repo = "protections-artifacts"; 41 | rev = "cb45629514acefc68a9d08111b3a76bc90e52238"; 42 | hash = "sha256-ZKpnNgFlS6L9jzjB7lz8WL3poqd25hW2g5cZ3lJZTcI="; 43 | }; 44 | 45 | relayRumbler = fetchFromGitHub { 46 | owner = "ajpc500"; 47 | repo = "RelayRumbler"; 48 | rev = "57b70f50ad305c73efc91acbba418c361ebc665b"; 49 | hash = "sha256-ntV4sEFruUtZYu+rtwROm2kJunDSmeW26DHk9qmQyy0="; 50 | }; 51 | 52 | malware-config-extractor = fetchFromGitHub { 53 | owner = "c3rb3ru5d3d53c"; 54 | repo = "mwcfg-modules"; 55 | rev = "3f7702d1d5896bb14d8cbb401a5f155002f6698a"; 56 | hash = "sha256-fSPyFOz0QDZdSSZDMuzzRfjPcHOKJJRsOjQHS6+rMuI="; 57 | }; 58 | in 59 | buildPythonPackage { 60 | pname = "deject"; 61 | version = "0.5.0"; 62 | 63 | format = "pyproject"; 64 | 65 | src = ../.; 66 | 67 | preConfigure = '' 68 | substituteInPlace pyproject.toml \ 69 | --replace 'ipaddress = "^1.0.23"' "" 70 | ''; 71 | 72 | nativeBuildInputs = [ 73 | poetry-core 74 | ]; 75 | 76 | propagatedBuildInputs = [ 77 | openssl.dev 78 | radare2 79 | ssdeep 80 | util-linux 81 | file 82 | 83 | exceptiongroup 84 | hexdump 85 | kaitaistruct 86 | malduck 87 | minidump 88 | pdf2image 89 | pefile 90 | pycryptodome 91 | python-magic 92 | r2pipe 93 | requests 94 | rzpipe 95 | tabulate 96 | telfhash 97 | typer 98 | yara-python 99 | pyyaml 100 | 101 | chepy 102 | dotnetfile 103 | netstruct 104 | libmagic 105 | mwcp 106 | olefile 107 | oletools 108 | ]; 109 | 110 | postInstall = let 111 | scripts = "$out/lib/python3.11/site-packages/scripts"; 112 | in '' 113 | mkdir ${scripts} 114 | cp -r scripts/{extractors,pdf-tools} ${scripts}/ 115 | cp -r ${yara-rules} ${scripts}/yara-rules 116 | 117 | cp -r ${malware-config-extractor} ${scripts}/extractors/MalwareConfigExtractor 118 | cp -r ${relayRumbler} ${scripts}/extractors/RelayRumbler 119 | ''; 120 | 121 | meta = with lib; { 122 | description = "Memory dump and Sample analysis tool"; 123 | homepage = "https://github.com/WithSecureLabs/deject"; 124 | license = licenses.gpl3Plus; 125 | }; 126 | } 127 | -------------------------------------------------------------------------------- /nix/overlays/chepy/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | fetchurl, 4 | buildPythonPackage, 5 | poetry-core, 6 | }: 7 | buildPythonPackage { 8 | pname = "chepy"; 9 | version = "7.2.0"; 10 | 11 | src = fetchurl { 12 | url = "https://files.pythonhosted.org/packages/cb/34/64261d7284e8e488bedad78e34f4ccaa95c990ddb8c2890e5363a41ba237/chepy-7.2.0.tar.gz"; 13 | hash = "sha256-f/mvLyO+fm7ge5gCBuJpbfC+TDSCuSbTuvevHqC+Bbg="; 14 | }; 15 | 16 | doCheck = false; 17 | 18 | nativeBuildInputs = [ 19 | poetry-core 20 | ]; 21 | 22 | meta = with lib; { 23 | description = "Chepy is a python lib/cli equivalent of the awesome CyberChef tool"; 24 | homepage = "https://github.com/securesec/chepy"; 25 | license = licenses.gpl3Plus; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /nix/overlays/default.nix: -------------------------------------------------------------------------------- 1 | final: prev: { 2 | pythonPackagesExtensions = 3 | prev.pythonPackagesExtensions 4 | ++ [ 5 | ( 6 | python-final: python-prev: let 7 | c = python-final.callPackage; 8 | in { 9 | chepy = c ./chepy {}; 10 | dotnetfile = c ./dotnetfile {}; 11 | mwcp = c ./mwcp {}; 12 | netstruct = c ./netstruct {}; 13 | libmagic = c ./libmagic {}; 14 | 15 | minidump = python-prev.minidump.overrideAttrs (self: super: { 16 | src = final.fetchurl { 17 | url = "https://files.pythonhosted.org/packages/26/4b/bc695b99dc7d77d28223765c3ee5a31d34fd2850c52eb683ccdd1206067d/minidump-0.0.24.tar.gz"; 18 | hash = "sha256-964JuUTzsXzPXOzGb5/1p6RbBTR0oTrrAS9MkgRHBDc="; 19 | }; 20 | }); 21 | } 22 | ) 23 | ]; 24 | } 25 | -------------------------------------------------------------------------------- /nix/overlays/dotnetfile/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | fetchFromGitHub, 4 | buildPythonPackage, 5 | poetry-core, 6 | pefile 7 | }: 8 | buildPythonPackage rec { 9 | pname = "dotnetfile"; 10 | version = "0.2.4"; 11 | 12 | src = fetchFromGitHub { 13 | owner = "pan-unit42"; 14 | repo = "dotnetfile"; 15 | rev = "v${version}"; 16 | hash = "sha256-+MfxJeN/IOI6Ev8kgzFVSzESXi8TcUkrCF4f0kBHMqk="; 17 | }; 18 | 19 | doCheck = false; 20 | 21 | nativeBuildInputs = [ 22 | poetry-core 23 | ]; 24 | 25 | propagatedBuildInputs = [ 26 | pefile 27 | ]; 28 | 29 | meta = with lib; { 30 | description = "Portable Executable reader module"; 31 | homepage = "Portable Executable reader module"; 32 | license = licenses.mit; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /nix/overlays/libmagic/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | fetchurl, 4 | buildPythonPackage, 5 | poetry-core, 6 | }: 7 | buildPythonPackage { 8 | pname = "libmagic"; 9 | version = "1.0"; 10 | 11 | src = fetchurl { 12 | url = "https://files.pythonhosted.org/packages/83/86/419ddfc3879b4565a60e0c75b6d19baec48428cbc2f15aca5320b3d136f6/libmagic-1.0.tar.gz"; 13 | hash = "sha256-ZJ8c5/t8knlrrbuBJVXkqSY1HaT1zfgugQtc03Gu340="; 14 | }; 15 | 16 | doCheck = false; 17 | 18 | nativeBuildInputs = [ 19 | poetry-core 20 | ]; 21 | 22 | meta = with lib; { 23 | description = "libmagic bindings using FFL (ctypes)"; 24 | homepage = "https://bitbucket.org/xmonader/pymagic-dev"; 25 | license = licenses.gpl3Plus; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /nix/overlays/mwcp/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | fetchurl, 4 | buildPythonPackage, 5 | poetry-core, 6 | }: 7 | buildPythonPackage { 8 | pname = "mwcp"; 9 | version = "3.13.1"; 10 | 11 | src = fetchurl { 12 | url = "https://files.pythonhosted.org/packages/fa/78/4924c25f40b92854800da8e3d94d10da8a78ca46e7ea0b013effd48b7209/mwcp-3.13.1.tar.gz"; 13 | hash = "sha256-Muh97OddtZgBtXkuudLJZ9wGEL76M6S/kJ4FHE5yxBs="; 14 | }; 15 | 16 | doCheck = false; 17 | 18 | nativeBuildInputs = [ 19 | poetry-core 20 | ]; 21 | 22 | meta = with lib; { 23 | description = "DC3 Malware Configuration Parser"; 24 | homepage = "https://github.com/dod-cyber-crime-center/DC3-MWCP"; 25 | license = licenses.mit; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /nix/overlays/netstruct/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | fetchurl, 4 | buildPythonPackage, 5 | poetry-core, 6 | }: 7 | buildPythonPackage { 8 | pname = "netstruct"; 9 | version = "1.1.2"; 10 | 11 | src = fetchurl { 12 | url = "https://files.pythonhosted.org/packages/b8/eb/460b09c71d65ea3ea7ff89271207935c44e30aa558b64f5102441f129191/netstruct-1.1.2.zip"; 13 | hash = "sha256-cLalxz9bvHq1ewGTaWQq37NN2K9BuUjEAM6V+VK335o="; 14 | }; 15 | 16 | doCheck = false; 17 | 18 | nativeBuildInputs = [ 19 | poetry-core 20 | ]; 21 | 22 | meta = with lib; { 23 | description = "struct-like module for Python designed to make it a bit easier to send and received packed binary data"; 24 | homepage = "https://github.com/stendec/netstruct"; 25 | license = licenses.asl20; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "deject" 3 | version = "0.5.0" 4 | description = "A reverse engineering toolbox" 5 | authors = ["Aubrey Thomas "] 6 | license = "GPL-3.0-only" 7 | readme = "README.md" 8 | include = [ 9 | { path = "scripts/*" }, 10 | { path = "bin" }, 11 | ] 12 | 13 | [tool.poetry.scripts] 14 | deject = "deject.main:app" 15 | 16 | [tool.poetry.dependencies] 17 | python = "^3.10" 18 | r2pipe = "^1.4.2" 19 | tabulate = "^0.9.0" 20 | yara-python = "^4.0.2" 21 | netstruct = "^1.1.2" 22 | ssdeep = "^3.4" 23 | requests = "^2.27.1" 24 | pefile = "^2024.8.26" 25 | malduck = "^4.3.0" 26 | pycryptodome = "^3.14.1" 27 | mwcp = "^3.5.0" 28 | typer = "^0.15.1" 29 | exceptiongroup = "^1.0.0-rc.7" 30 | libmagic = "^1.0" 31 | python-magic = "^0.4.26" 32 | rzpipe = "^0.6.0" 33 | #ipaddress = "^1.0.23" 34 | hexdump = "^3.3" 35 | dotnetfile = {git = "https://github.com/pan-unit42/dotnetfile.git"} 36 | kaitaistruct = "^0.10" 37 | pdf2image = "^1.16.3" 38 | chepy = "^7.2.0" 39 | telfhash = "^0.9.8" 40 | minidump = "^0.0.24" 41 | setuptools = "^75.0.0" 42 | olefile = "^0.47" 43 | oletools = "^0.60.2" 44 | pre-commit = "^4.1.0" 45 | 46 | [build-system] 47 | requires = ["poetry-core"] 48 | build-backend = "poetry.core.masonry.api" 49 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- 1 | from deject import plugins 2 | 3 | names = plugins.names_factory(__package__) 4 | run = plugins.call_factory(__package__) 5 | docs = plugins.doc_factory(__package__) 6 | -------------------------------------------------------------------------------- /scripts/agenttesla_behaviour.py: -------------------------------------------------------------------------------- 1 | """! @brief This plugin is used to get behaviour from the Virus Total sandbox. To run the plugin, type 2 | poetry run deject run --include agenttesla_behaviour \. This plugin has no arguments and needs the 3 | VT_KEY environment variable set.""" 4 | 5 | from deject.plugins import Deject 6 | import scripts.helpers as utils 7 | import hashlib 8 | from typer import secho, colors 9 | 10 | 11 | # Data to extract from the SMTP fields 12 | INTERESTING_SMTP_DATA = ['smtp_from', 'smtp_to', 'subject'] 13 | 14 | 15 | @Deject.plugin 16 | def agenttesla(): 17 | """Get AgentTesla behaviour""" 18 | filename = Deject.file_path 19 | with open(filename, "rb") as f: 20 | data = f.read() 21 | sha256 = hashlib.sha256(data).hexdigest() 22 | sha1 = hashlib.sha1(data).hexdigest() 23 | secho(f"SHA1: {sha1}") 24 | extract_interesting_information(sha256, filename) 25 | 26 | 27 | def extract_interesting_information(hash, filename): 28 | """Retrieves Virus Total sandbox information for a given file.""" 29 | vt_key = utils.Settings().getSetting("vt_key") 30 | if vt_key == "": 31 | secho("VT API key is unset, please set VT_KEY environment variable to use this plugin.", fg=colors.RED) 32 | else: 33 | vt = utils.virustotal(vt_key) 34 | report = vt.getBehavior(hash) 35 | result = {} 36 | c2s = [] 37 | injected_processes = [] 38 | registry_keys = [] 39 | file_writes = [] 40 | services_started = [] 41 | 42 | for sandbox in report: 43 | if attributes := sandbox.get('attributes'): 44 | if services := attributes.get('services_started'): 45 | services_started.extend(services) 46 | 47 | if smtp_conversations := attributes.get('smtp_conversations'): 48 | for conversation in smtp_conversations: 49 | c2_conversation = {} 50 | c2_conversation['c2_type'] = 'smtp' 51 | c2_conversation['data'] = { 52 | key: conversation.get(key, f'no {key}') for key in INTERESTING_SMTP_DATA 53 | } 54 | c2s.append(c2_conversation) 55 | 56 | if http_conversations := attributes.get('http_conversations'): 57 | for conversation in http_conversations: 58 | c2_conversation = {} 59 | if 'telegram' in conversation['url'].lower(): 60 | c2_conversation['c2_type'] = 'telegram' 61 | if c2_conversation.get('response_status_code'): 62 | c2_conversation['data'] = { 63 | 'url': conversation['url'], 64 | 'request_method': conversation['request_method'], 65 | 'status_code': conversation['response_status_code'], 66 | } 67 | else: 68 | c2_conversation['data'] = { 69 | 'url': conversation['url'], 70 | 'request_method': conversation['request_method'], 71 | } 72 | c2s.append(c2_conversation) 73 | if ip_traffic := attributes.get('ip_traffic'): 74 | for ip in ip_traffic: 75 | c2_conversation = {} 76 | c2_conversation['c2_type'] = ip['transport_layer_protocol'] 77 | c2_conversation['data'] = { 78 | 'destination': ip['destination_ip'], 79 | 'port': ip['destination_port'], 80 | } 81 | c2s.append(c2_conversation) 82 | 83 | if processes_injected := attributes.get('processes_injected'): 84 | injected_processes.extend(processes_injected) 85 | if registry_keys_set := attributes.get('registry_keys_set'): 86 | registry_keys.extend(registry_keys_set) 87 | if files_written := attributes.get('files_dropped'): 88 | file_writes.extend(files_written) 89 | 90 | if len(c2s) > 0: 91 | result['c2_conversations'] = c2s 92 | if len(injected_processes) > 0: 93 | result['injected_processes'] = injected_processes 94 | if len(registry_keys) > 0: 95 | result['registry_keys_written'] = registry_keys 96 | if len(file_writes) > 0: 97 | result['files_written'] = file_writes 98 | if len(services_started) > 0: 99 | result['services_started'] = set(services_started) 100 | 101 | if result: 102 | secho(result, fg=colors.GREEN) 103 | # parsing failed 104 | else: 105 | secho(f"No information found for {filename}", fg=colors.RED) 106 | 107 | 108 | def help(): 109 | print(""" 110 | Agent Tesla Plugin 111 | SYNOPSIS 112 | This plugin is used to get behaviour from the Virus Total sandbox. To run the plugin, type 113 | poetry run deject run --include agenttesla_behaviour . This plugin has no arguments and needs the 114 | VT_KEY environment variable set with a VT API key. 115 | """) 116 | -------------------------------------------------------------------------------- /scripts/bulk_extractor.py: -------------------------------------------------------------------------------- 1 | """! @brief This plugin runs Bulk Extractor on a file. To run the plugin, type 2 | poetry run deject run --include bulk_extractor \. This plugin has no arguments 3 | and will save output to a directory 'extracted/' in the location of the file. 4 | Download Bulk_Extractor and link or copy the binary to bin/ in Deject's root directory.""" 5 | 6 | from deject.plugins import Deject 7 | import os 8 | from scripts.helpers import helpers, Settings 9 | 10 | 11 | @Deject.plugin 12 | def bulk_extractor(): 13 | """Use bulk_extractor on the memory dump.""" 14 | filename = Deject.file_path 15 | filepath = os.path.dirname(Deject.file_path) 16 | bulk = f"{os.path.dirname(os.path.realpath(__file__))}/../bin/bulk_extractor" 17 | if not os.path.exists(bulk): 18 | if Settings().getSetting("bulk_path"): 19 | bulk = Settings().getSetting("bulk_path") 20 | else: 21 | bulk = "bulk_extractor" 22 | helpers.bin_exec( 23 | helpers, [bulk, "-o", f"{filepath}/extracted", "-0", filename], 24 | ) 25 | 26 | 27 | def help(): 28 | print(""" 29 | Bulk Extractor Plugin 30 | SYNOPSIS 31 | 32 | Runs Bulk Extractor on the file supplied, with default arguments. This plugin takes no additional arguments. 33 | Download Bulk_Extractor and link or copy the binary to bin/ in Deject's root directory. 34 | """) 35 | -------------------------------------------------------------------------------- /scripts/c3_check.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Check if the memory dump or executable is C3. 3 | @details Uses RelayRumbler (https://github.com/ajpc500/RelayRumbler) to extract 4 | configuration from a C3 payload.""" 5 | from deject.plugins import Deject 6 | from pathlib import Path 7 | from scripts.helpers import helpers 8 | from sys import exit 9 | 10 | 11 | @Deject.plugin 12 | def c3_check(): 13 | """Check if the dump corresponds to a F-Secure C3 injected one and extract the config""" 14 | config_dir = Path("./scripts/extractors/RelayRumbler") 15 | filename = Deject.file_path 16 | args = "-f" 17 | # let's look for yara rules in the config dir 18 | rules, scripts = helpers.get_rules(helpers, config_dir) 19 | if len(scripts) == 0: 20 | print("[Error!] Unable to find parsing script") 21 | exit(1) 22 | # for now it is enough that just one rule matches. In future this can be refined if needed 23 | for script in scripts: 24 | helpers.script_exec(helpers, script, filename, args) 25 | return 26 | 27 | 28 | def help(): 29 | print(""" 30 | C3 Plugin 31 | 32 | SYNOPSIS 33 | 34 | Uses RelayRumbler (https://github.com/ajpc500/RelayRumbler) to extract 35 | configuration from a C3 payload. This plugin has no additional arguments. 36 | """) 37 | -------------------------------------------------------------------------------- /scripts/cobaltstrike_check.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Uses 1768.py to check if the memory dump or PE has Cobalt Strike configuration. 3 | """ 4 | from deject.plugins import Deject 5 | from pathlib import Path 6 | from scripts.helpers import helpers 7 | 8 | 9 | @Deject.plugin 10 | def cobaltstrike_check(): 11 | """Check if the dump corresponds to a cs injected one and extract the config""" 12 | process = ["python", Path("./scripts/extractors/cobaltstrike/1768.py")] 13 | filename = [Deject.file_path] 14 | arg = Deject.plugin_args 15 | if arg == "False": 16 | helpers.bin_exec(helpers, process + filename) 17 | else: 18 | helpers.bin_exec(helpers, process + arg.strip().split(" ") + filename) 19 | 20 | 21 | def help(): 22 | print(""" 23 | Cobalt Strike Check Plugin 24 | 25 | SYNOPSIS 26 | 27 | Uses 1768.py (https://github.com/DidierStevens/DidierStevensSuite/blob/master/1768.py) to extract 28 | configuration from a Cobalt Strike payload. 29 | """) 30 | -------------------------------------------------------------------------------- /scripts/dmg_hashes.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief print DMG TLSH hash 3 | """ 4 | from deject.plugins import Deject 5 | import tlsh 6 | 7 | 8 | @Deject.plugin 9 | def dmg_hashes(): 10 | """Print TLSH hash of a DMG file""" 11 | with open(Deject.file_path, "rb") as f: 12 | data = f.read() 13 | hashes = tlsh.hash(data) 14 | res = { 15 | "header": ["Filename", "TLSH"], 16 | "rows": [[Deject.file_path, hashes]], 17 | } 18 | return res 19 | 20 | 21 | def help(): 22 | print(""" 23 | DMG Hashes plugin 24 | SYNOPSIS 25 | Print the TLSH of a DMG file. 26 | There are no additional arguments for this plugin. 27 | """) 28 | -------------------------------------------------------------------------------- /scripts/elf_hashes.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief print ELF hashes (TELFHASH) 3 | """ 4 | from deject.plugins import Deject 5 | from telfhash import telfhash 6 | 7 | 8 | @Deject.plugin 9 | def elf_hashes(): 10 | """Print TELFHASH of ELF""" 11 | hashes = telfhash(Deject.file_path) 12 | hashes = list(hashes[0].values()) 13 | res = {"header": ["Filename", "Telfhash", "message"], "rows": [hashes]} 14 | return res 15 | 16 | 17 | def help(): 18 | print(""" 19 | ELF Hashes plugin 20 | SYNOPSIS 21 | Print the TELFHash of an ELF file. 22 | There are no additional arguments for this plugin. 23 | """) 24 | -------------------------------------------------------------------------------- /scripts/elf_imports.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Uses radare/rizin to extract imports for an ELF file. 3 | """ 4 | from deject.plugins import Deject 5 | from typer import secho, colors 6 | 7 | 8 | @Deject.plugin 9 | def pe_imports(): 10 | """List imports in an ELF file.""" 11 | imports = Deject.r2_handler.cmdj("iij") 12 | if imports is None: 13 | secho("No imports detected in the file, this might be a bug!", fg=colors.RED) 14 | return 15 | rows = [] 16 | for imp in imports: 17 | rows.append([imp["name"]]) 18 | res = {"header": ["Name"], "rows": rows} 19 | 20 | return res 21 | 22 | 23 | def help(): 24 | print(""" 25 | ELF Imports plugin 26 | SYNOPSIS 27 | Use Radare2/Rizin with the 'iij' command to list imports in an ELF file. 28 | There are no additional arguments for this plugin. 29 | """) 30 | -------------------------------------------------------------------------------- /scripts/elf_parser.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief uses Kaitai (https://github.com/kaitai-io/kaitai_struct_compiler) to parse ELF files. 3 | @details Can supply a section name to extract out of the ELF. 4 | By default runs elf_parser_misc() 5 | """ 6 | from deject.plugins import Deject 7 | from kaitaistruct import KaitaiStream, BytesIO 8 | from scripts.extractors.kaitai.elf import Elf 9 | from enum import Flag 10 | 11 | 12 | class Flags(Flag): 13 | PF_X = 0x1 14 | PF_W = 0x2 15 | PF_R = 0x4 16 | PF_MASKOS = 0x0ff00000 17 | PF_MASKPROC = 0xf0000000 18 | 19 | 20 | @Deject.plugin 21 | def elf_parser(): 22 | """Used to parse information from a ELF file""" 23 | with open(Deject.file_path, "rb") as hfile: 24 | try: 25 | data = Elf(KaitaiStream(BytesIO(hfile.read()))) 26 | except Exception: 27 | try: 28 | hfile.seek(0) 29 | data = Elf(KaitaiStream(BytesIO(b"\x7f" + hfile.read()))) 30 | except Exception: 31 | raise Exception( 32 | f"Could not open file {Deject.file_path} as an Elf file.", 33 | ) 34 | args = str(Deject.plugin_args).split(" ") 35 | match args[0]: 36 | case "extract": 37 | result = elf_parser_extract(data, args[1]) 38 | case "sections": 39 | result = elf_parser_sections(data) 40 | case "headers": 41 | result = elf_parser_program_headers(data) 42 | case _: 43 | result = elf_parser_misc(data) 44 | return result 45 | 46 | 47 | def elf_parser_misc(data): 48 | """Parses an ELF file, to extract section names, entrypoint and flags""" 49 | rows = [] 50 | rows.append(["ABI Version", data.abi]) 51 | rows.append(["Architecture", data.bits]) 52 | rows.append(["Flags", data.header.flags]) 53 | rows.append(["Section Names", data.header.section_names.entries]) 54 | rows.append(["Entrypoint", data.header.entry_point]) 55 | t = {"header": ["Key", "Value"], "rows": rows} 56 | return t 57 | 58 | 59 | def elf_parser_sections(data): 60 | """Extract Section Header information""" 61 | rows = [] 62 | for section in data.header.section_headers: 63 | perms = "" 64 | rows.append(["Section Name", section.name]) 65 | rows.append(["Section Type", section.type]) 66 | rows.append(["Entry Size", section.entry_size]) 67 | rows.append(["Section Address", hex(section.addr)]) 68 | if section.linked_section: 69 | rows.append(["Linked Sections", section.linked_section.name]) 70 | rows.append(["TLS", section.flags_obj.tls]) 71 | flags = elf_parser_flag_lookup(section.flags) 72 | rows.append(["Flags", "\n".join(flags)]) 73 | if section.flags_obj.write: 74 | perms += "W" 75 | if section.flags_obj.exec_instr: 76 | perms += "X" 77 | if perms != "": 78 | rows.append(["Permissions", perms]) 79 | rows.append(["Allocated", section.flags_obj.alloc]) 80 | rows.append(["Strings", section.flags_obj.strings]) 81 | t = {"header": ["Key", "Value"], "rows": rows} 82 | return t 83 | 84 | 85 | def elf_parser_program_headers(data): 86 | """Extract Program Header information""" 87 | rows = [] 88 | for header in data.header.program_headers: 89 | perms = "" 90 | rows.append(["Header Type", header.type]) 91 | rows.append(["Header Address", hex(header.vaddr)]) 92 | rows.append(["Header Filesz", hex(header.filesz)]) 93 | rows.append(["Header Memsz", hex(header.memsz)]) 94 | rows.append(["Flags", header.flags_obj.value]) 95 | if header.flags_obj.read: 96 | perms += "R" 97 | if header.flags_obj.write: 98 | perms += "W" 99 | if header.flags_obj.execute: 100 | perms += "X" 101 | if perms != "": 102 | rows.append(["Permissions", perms]) 103 | t = {"header": ["Key", "Value"], "rows": rows} 104 | return t 105 | 106 | 107 | def elf_parser_flag_lookup(data): 108 | flags = [] 109 | for i in [e.value for e in Flags]: 110 | try: 111 | if data & i > 0: 112 | flags.append(Flags(data & i).name) 113 | except ValueError: 114 | continue 115 | return flags 116 | 117 | 118 | def elf_parser_extract(data, args): 119 | """Extracts sections from an ELF.""" 120 | for section in data.header.section_headers: 121 | if section.name == args or section.name == f".{args}": 122 | return section.body 123 | 124 | 125 | def help(): 126 | print(""" 127 | ELF Parser plugin 128 | 129 | SYNOPSIS "[options]" 130 | 131 | Uses the ELF parser from Kaitai to read information from an ELF file. 132 | If extract and a section name is added, extract data from that section (enclose options in quotes). 133 | """) 134 | -------------------------------------------------------------------------------- /scripts/extractors/kaitai/asn1_der.py: -------------------------------------------------------------------------------- 1 | # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild 2 | 3 | import kaitaistruct 4 | from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO 5 | from enum import Enum 6 | 7 | 8 | if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): 9 | raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) 10 | 11 | class Asn1Der(KaitaiStruct): 12 | """ASN.1 (Abstract Syntax Notation One) DER (Distinguished Encoding 13 | Rules) is a standard-backed serialization scheme used in many 14 | different use-cases. Particularly popular usage scenarios are X.509 15 | certificates and some telecommunication / networking protocols. 16 | 17 | DER is self-describing encoding scheme which allows representation 18 | of simple, atomic data elements, such as strings and numbers, and 19 | complex objects, such as sequences of other elements. 20 | 21 | DER is a subset of BER (Basic Encoding Rules), with an emphasis on 22 | being non-ambiguous: there's always exactly one canonical way to 23 | encode a data structure defined in terms of ASN.1 using DER. 24 | 25 | This spec allows full parsing of format syntax, but to understand 26 | the semantics, one would typically require a dictionary of Object 27 | Identifiers (OIDs), to match OID bodies against some human-readable 28 | list of constants. OIDs are covered by many different standards, 29 | so typically it's simpler to use a pre-compiled list of them, such 30 | as: 31 | 32 | * 33 | * 34 | * 35 | 36 | .. seealso:: 37 | Source - https://www.itu.int/itu-t/recommendations/rec.aspx?rec=12483&lang=en 38 | """ 39 | 40 | class TypeTag(Enum): 41 | end_of_content = 0 42 | boolean = 1 43 | integer = 2 44 | bit_string = 3 45 | octet_string = 4 46 | null_value = 5 47 | object_id = 6 48 | object_descriptor = 7 49 | external = 8 50 | real = 9 51 | enumerated = 10 52 | embedded_pdv = 11 53 | utf8string = 12 54 | relative_oid = 13 55 | sequence_10 = 16 56 | printable_string = 19 57 | ia5string = 22 58 | sequence_30 = 48 59 | set = 49 60 | def __init__(self, _io, _parent=None, _root=None): 61 | self._io = _io 62 | self._parent = _parent 63 | self._root = _root if _root else self 64 | self._read() 65 | 66 | def _read(self): 67 | self.type_tag = KaitaiStream.resolve_enum(Asn1Der.TypeTag, self._io.read_u1()) 68 | self.len = Asn1Der.LenEncoded(self._io, self, self._root) 69 | _on = self.type_tag 70 | if _on == Asn1Der.TypeTag.printable_string: 71 | self._raw_body = self._io.read_bytes(self.len.result) 72 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 73 | self.body = Asn1Der.BodyPrintableString(_io__raw_body, self, self._root) 74 | elif _on == Asn1Der.TypeTag.sequence_10: 75 | self._raw_body = self._io.read_bytes(self.len.result) 76 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 77 | self.body = Asn1Der.BodySequence(_io__raw_body, self, self._root) 78 | elif _on == Asn1Der.TypeTag.set: 79 | self._raw_body = self._io.read_bytes(self.len.result) 80 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 81 | self.body = Asn1Der.BodySequence(_io__raw_body, self, self._root) 82 | elif _on == Asn1Der.TypeTag.sequence_30: 83 | self._raw_body = self._io.read_bytes(self.len.result) 84 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 85 | self.body = Asn1Der.BodySequence(_io__raw_body, self, self._root) 86 | elif _on == Asn1Der.TypeTag.utf8string: 87 | self._raw_body = self._io.read_bytes(self.len.result) 88 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 89 | self.body = Asn1Der.BodyUtf8string(_io__raw_body, self, self._root) 90 | elif _on == Asn1Der.TypeTag.object_id: 91 | self._raw_body = self._io.read_bytes(self.len.result) 92 | _io__raw_body = KaitaiStream(BytesIO(self._raw_body)) 93 | self.body = Asn1Der.BodyObjectId(_io__raw_body, self, self._root) 94 | else: 95 | self.body = self._io.read_bytes(self.len.result) 96 | 97 | class BodySequence(KaitaiStruct): 98 | def __init__(self, _io, _parent=None, _root=None): 99 | self._io = _io 100 | self._parent = _parent 101 | self._root = _root if _root else self 102 | self._read() 103 | 104 | def _read(self): 105 | self.entries = [] 106 | i = 0 107 | while not self._io.is_eof(): 108 | self.entries.append(Asn1Der(self._io)) 109 | i += 1 110 | 111 | 112 | 113 | class BodyUtf8string(KaitaiStruct): 114 | def __init__(self, _io, _parent=None, _root=None): 115 | self._io = _io 116 | self._parent = _parent 117 | self._root = _root if _root else self 118 | self._read() 119 | 120 | def _read(self): 121 | self.str = (self._io.read_bytes_full()).decode(u"UTF-8") 122 | 123 | 124 | class BodyObjectId(KaitaiStruct): 125 | """ 126 | .. seealso:: 127 | Source - https://learn.microsoft.com/en-us/windows/win32/seccertenroll/about-object-identifier 128 | """ 129 | def __init__(self, _io, _parent=None, _root=None): 130 | self._io = _io 131 | self._parent = _parent 132 | self._root = _root if _root else self 133 | self._read() 134 | 135 | def _read(self): 136 | self.first_and_second = self._io.read_u1() 137 | self.rest = self._io.read_bytes_full() 138 | 139 | @property 140 | def first(self): 141 | if hasattr(self, '_m_first'): 142 | return self._m_first 143 | 144 | self._m_first = self.first_and_second // 40 145 | return getattr(self, '_m_first', None) 146 | 147 | @property 148 | def second(self): 149 | if hasattr(self, '_m_second'): 150 | return self._m_second 151 | 152 | self._m_second = (self.first_and_second % 40) 153 | return getattr(self, '_m_second', None) 154 | 155 | 156 | class LenEncoded(KaitaiStruct): 157 | def __init__(self, _io, _parent=None, _root=None): 158 | self._io = _io 159 | self._parent = _parent 160 | self._root = _root if _root else self 161 | self._read() 162 | 163 | def _read(self): 164 | self.b1 = self._io.read_u1() 165 | if self.b1 == 130: 166 | self.int2 = self._io.read_u2be() 167 | 168 | if self.b1 == 129: 169 | self.int1 = self._io.read_u1() 170 | 171 | 172 | @property 173 | def result(self): 174 | if hasattr(self, '_m_result'): 175 | return self._m_result 176 | 177 | self._m_result = (self.int1 if self.b1 == 129 else (self.int2 if self.b1 == 130 else self.b1)) 178 | return getattr(self, '_m_result', None) 179 | 180 | 181 | class BodyPrintableString(KaitaiStruct): 182 | def __init__(self, _io, _parent=None, _root=None): 183 | self._io = _io 184 | self._parent = _parent 185 | self._root = _root if _root else self 186 | self._read() 187 | 188 | def _read(self): 189 | self.str = (self._io.read_bytes_full()).decode(u"ASCII") 190 | 191 | 192 | 193 | -------------------------------------------------------------------------------- /scripts/extractors/kaitai/mach_o_fat.py: -------------------------------------------------------------------------------- 1 | # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild 2 | 3 | import kaitaistruct 4 | from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO 5 | 6 | 7 | if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): 8 | raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) 9 | 10 | from scripts.extractors.kaitai.mach_o import MachO 11 | class MachOFat(KaitaiStruct): 12 | """This is a simple container format that encapsulates multiple Mach-O files, 13 | each generally for a different architecture. XNU can execute these files just 14 | like single-arch Mach-Os and will pick the appropriate entry. 15 | 16 | .. seealso:: 17 | Source - https://opensource.apple.com/source/xnu/xnu-7195.121.3/EXTERNAL_HEADERS/mach-o/fat.h.auto.html 18 | """ 19 | def __init__(self, _io, _parent=None, _root=None): 20 | self._io = _io 21 | self._parent = _parent 22 | self._root = _root if _root else self 23 | self._read() 24 | 25 | def _read(self): 26 | self.magic = self._io.read_bytes(4) 27 | if not self.magic == b"\xCA\xFE\xBA\xBE": 28 | raise kaitaistruct.ValidationNotEqualError(b"\xCA\xFE\xBA\xBE", self.magic, self._io, u"/seq/0") 29 | self.num_fat_arch = self._io.read_u4be() 30 | self.fat_archs = [] 31 | for i in range(self.num_fat_arch): 32 | self.fat_archs.append(MachOFat.FatArch(self._io, self, self._root)) 33 | 34 | 35 | class FatArch(KaitaiStruct): 36 | def __init__(self, _io, _parent=None, _root=None): 37 | self._io = _io 38 | self._parent = _parent 39 | self._root = _root if _root else self 40 | self._read() 41 | 42 | def _read(self): 43 | self.cpu_type = KaitaiStream.resolve_enum(MachO.CpuType, self._io.read_u4be()) 44 | self.cpu_subtype = self._io.read_u4be() 45 | self.ofs_object = self._io.read_u4be() 46 | self.len_object = self._io.read_u4be() 47 | self.align = self._io.read_u4be() 48 | 49 | @property 50 | def object(self): 51 | if hasattr(self, '_m_object'): 52 | return self._m_object 53 | 54 | _pos = self._io.pos() 55 | self._io.seek(self.ofs_object) 56 | self._raw__m_object = self._io.read_bytes(self.len_object) 57 | _io__raw__m_object = KaitaiStream(BytesIO(self._raw__m_object)) 58 | self._m_object = MachO(_io__raw__m_object) 59 | self._io.seek(_pos) 60 | return getattr(self, '_m_object', None) 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /scripts/extractors/kaitai/windows_minidump.py: -------------------------------------------------------------------------------- 1 | # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild 2 | 3 | import kaitaistruct 4 | from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO 5 | from enum import Enum 6 | 7 | 8 | if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): 9 | raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) 10 | 11 | class WindowsMinidump(KaitaiStruct): 12 | """Windows MiniDump (MDMP) file provides a concise way to store process 13 | core dumps, which is useful for debugging. Given its small size, 14 | modularity, some cross-platform features and native support in some 15 | debuggers, it is particularly useful for crash reporting, and is 16 | used for that purpose in Windows and Google Chrome projects. 17 | 18 | The file itself is a container, which contains a number of typed 19 | "streams", which contain some data according to its type attribute. 20 | 21 | .. seealso:: 22 | Source - https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_header 23 | """ 24 | 25 | class StreamTypes(Enum): 26 | unused = 0 27 | reserved_0 = 1 28 | reserved_1 = 2 29 | thread_list = 3 30 | module_list = 4 31 | memory_list = 5 32 | exception = 6 33 | system_info = 7 34 | thread_ex_list = 8 35 | memory_64_list = 9 36 | comment_a = 10 37 | comment_w = 11 38 | handle_data = 12 39 | function_table = 13 40 | unloaded_module_list = 14 41 | misc_info = 15 42 | memory_info_list = 16 43 | thread_info_list = 17 44 | handle_operation_list = 18 45 | token = 19 46 | java_script_data = 20 47 | system_memory_info = 21 48 | process_vm_counters = 22 49 | ipt_trace = 23 50 | thread_names = 24 51 | ce_null = 32768 52 | ce_system_info = 32769 53 | ce_exception = 32770 54 | ce_module_list = 32771 55 | ce_process_list = 32772 56 | ce_thread_list = 32773 57 | ce_thread_context_list = 32774 58 | ce_thread_call_stack_list = 32775 59 | ce_memory_virtual_list = 32776 60 | ce_memory_physical_list = 32777 61 | ce_bucket_parameters = 32778 62 | ce_process_module_map = 32779 63 | ce_diagnosis_list = 32780 64 | md_crashpad_info_stream = 1129316353 65 | md_raw_breakpad_info = 1197932545 66 | md_raw_assertion_info = 1197932546 67 | md_linux_cpu_info = 1197932547 68 | md_linux_proc_status = 1197932548 69 | md_linux_lsb_release = 1197932549 70 | md_linux_cmd_line = 1197932550 71 | md_linux_environ = 1197932551 72 | md_linux_auxv = 1197932552 73 | md_linux_maps = 1197932553 74 | md_linux_dso_debug = 1197932554 75 | def __init__(self, _io, _parent=None, _root=None): 76 | self._io = _io 77 | self._parent = _parent 78 | self._root = _root if _root else self 79 | self._read() 80 | 81 | def _read(self): 82 | self.magic1 = self._io.read_bytes(4) 83 | if not self.magic1 == b"\x4D\x44\x4D\x50": 84 | raise kaitaistruct.ValidationNotEqualError(b"\x4D\x44\x4D\x50", self.magic1, self._io, u"/seq/0") 85 | self.magic2 = self._io.read_bytes(2) 86 | if not self.magic2 == b"\x93\xA7": 87 | raise kaitaistruct.ValidationNotEqualError(b"\x93\xA7", self.magic2, self._io, u"/seq/1") 88 | self.version = self._io.read_u2le() 89 | self.num_streams = self._io.read_u4le() 90 | self.ofs_streams = self._io.read_u4le() 91 | self.checksum = self._io.read_u4le() 92 | self.timestamp = self._io.read_u4le() 93 | self.flags = self._io.read_u8le() 94 | 95 | class ThreadList(KaitaiStruct): 96 | """ 97 | .. seealso:: 98 | Source - https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_thread_list 99 | """ 100 | def __init__(self, _io, _parent=None, _root=None): 101 | self._io = _io 102 | self._parent = _parent 103 | self._root = _root if _root else self 104 | self._read() 105 | 106 | def _read(self): 107 | self.num_threads = self._io.read_u4le() 108 | self.threads = [] 109 | for i in range(self.num_threads): 110 | self.threads.append(WindowsMinidump.Thread(self._io, self, self._root)) 111 | 112 | 113 | 114 | class LocationDescriptor(KaitaiStruct): 115 | """ 116 | .. seealso:: 117 | Source - https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_location_descriptor 118 | """ 119 | def __init__(self, _io, _parent=None, _root=None): 120 | self._io = _io 121 | self._parent = _parent 122 | self._root = _root if _root else self 123 | self._read() 124 | 125 | def _read(self): 126 | self.len_data = self._io.read_u4le() 127 | self.ofs_data = self._io.read_u4le() 128 | 129 | @property 130 | def data(self): 131 | if hasattr(self, '_m_data'): 132 | return self._m_data 133 | 134 | io = self._root._io 135 | _pos = io.pos() 136 | io.seek(self.ofs_data) 137 | self._m_data = io.read_bytes(self.len_data) 138 | io.seek(_pos) 139 | return getattr(self, '_m_data', None) 140 | 141 | 142 | class MinidumpString(KaitaiStruct): 143 | """Specific string serialization scheme used in MiniDump format is 144 | actually a simple 32-bit length-prefixed UTF-16 string. 145 | 146 | .. seealso:: 147 | Source - https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_string 148 | """ 149 | def __init__(self, _io, _parent=None, _root=None): 150 | self._io = _io 151 | self._parent = _parent 152 | self._root = _root if _root else self 153 | self._read() 154 | 155 | def _read(self): 156 | self.len_str = self._io.read_u4le() 157 | self.str = (self._io.read_bytes(self.len_str)).decode(u"UTF-16LE") 158 | 159 | 160 | class SystemInfo(KaitaiStruct): 161 | """"System info" stream provides basic information about the 162 | hardware and operating system which produces this dump. 163 | 164 | .. seealso:: 165 | Source - https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_system_info 166 | """ 167 | 168 | class CpuArchs(Enum): 169 | intel = 0 170 | arm = 5 171 | ia64 = 6 172 | amd64 = 9 173 | unknown = 65535 174 | def __init__(self, _io, _parent=None, _root=None): 175 | self._io = _io 176 | self._parent = _parent 177 | self._root = _root if _root else self 178 | self._read() 179 | 180 | def _read(self): 181 | self.cpu_arch = KaitaiStream.resolve_enum(WindowsMinidump.SystemInfo.CpuArchs, self._io.read_u2le()) 182 | self.cpu_level = self._io.read_u2le() 183 | self.cpu_revision = self._io.read_u2le() 184 | self.num_cpus = self._io.read_u1() 185 | self.os_type = self._io.read_u1() 186 | self.os_ver_major = self._io.read_u4le() 187 | self.os_ver_minor = self._io.read_u4le() 188 | self.os_build = self._io.read_u4le() 189 | self.os_platform = self._io.read_u4le() 190 | self.ofs_service_pack = self._io.read_u4le() 191 | self.os_suite_mask = self._io.read_u2le() 192 | self.reserved2 = self._io.read_u2le() 193 | 194 | @property 195 | def service_pack(self): 196 | if hasattr(self, '_m_service_pack'): 197 | return self._m_service_pack 198 | 199 | if self.ofs_service_pack > 0: 200 | io = self._root._io 201 | _pos = io.pos() 202 | io.seek(self.ofs_service_pack) 203 | self._m_service_pack = WindowsMinidump.MinidumpString(io, self, self._root) 204 | io.seek(_pos) 205 | 206 | return getattr(self, '_m_service_pack', None) 207 | 208 | 209 | class ExceptionRecord(KaitaiStruct): 210 | """ 211 | .. seealso:: 212 | Source - https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_exception 213 | """ 214 | def __init__(self, _io, _parent=None, _root=None): 215 | self._io = _io 216 | self._parent = _parent 217 | self._root = _root if _root else self 218 | self._read() 219 | 220 | def _read(self): 221 | self.code = self._io.read_u4le() 222 | self.flags = self._io.read_u4le() 223 | self.inner_exception = self._io.read_u8le() 224 | self.addr = self._io.read_u8le() 225 | self.num_params = self._io.read_u4le() 226 | self.reserved = self._io.read_u4le() 227 | self.params = [] 228 | for i in range(15): 229 | self.params.append(self._io.read_u8le()) 230 | 231 | 232 | 233 | class MiscInfo(KaitaiStruct): 234 | """ 235 | .. seealso:: 236 | Source - https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_misc_info 237 | """ 238 | def __init__(self, _io, _parent=None, _root=None): 239 | self._io = _io 240 | self._parent = _parent 241 | self._root = _root if _root else self 242 | self._read() 243 | 244 | def _read(self): 245 | self.len_info = self._io.read_u4le() 246 | self.flags1 = self._io.read_u4le() 247 | self.process_id = self._io.read_u4le() 248 | self.process_create_time = self._io.read_u4le() 249 | self.process_user_time = self._io.read_u4le() 250 | self.process_kernel_time = self._io.read_u4le() 251 | self.cpu_max_mhz = self._io.read_u4le() 252 | self.cpu_cur_mhz = self._io.read_u4le() 253 | self.cpu_limit_mhz = self._io.read_u4le() 254 | self.cpu_max_idle_state = self._io.read_u4le() 255 | self.cpu_cur_idle_state = self._io.read_u4le() 256 | 257 | 258 | class Dir(KaitaiStruct): 259 | """ 260 | .. seealso:: 261 | Source - https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_directory 262 | """ 263 | def __init__(self, _io, _parent=None, _root=None): 264 | self._io = _io 265 | self._parent = _parent 266 | self._root = _root if _root else self 267 | self._read() 268 | 269 | def _read(self): 270 | self.stream_type = KaitaiStream.resolve_enum(WindowsMinidump.StreamTypes, self._io.read_u4le()) 271 | self.len_data = self._io.read_u4le() 272 | self.ofs_data = self._io.read_u4le() 273 | 274 | @property 275 | def data(self): 276 | if hasattr(self, '_m_data'): 277 | return self._m_data 278 | 279 | _pos = self._io.pos() 280 | self._io.seek(self.ofs_data) 281 | _on = self.stream_type 282 | if _on == WindowsMinidump.StreamTypes.memory_list: 283 | self._raw__m_data = self._io.read_bytes(self.len_data) 284 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 285 | self._m_data = WindowsMinidump.MemoryList(_io__raw__m_data, self, self._root) 286 | elif _on == WindowsMinidump.StreamTypes.misc_info: 287 | self._raw__m_data = self._io.read_bytes(self.len_data) 288 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 289 | self._m_data = WindowsMinidump.MiscInfo(_io__raw__m_data, self, self._root) 290 | elif _on == WindowsMinidump.StreamTypes.thread_list: 291 | self._raw__m_data = self._io.read_bytes(self.len_data) 292 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 293 | self._m_data = WindowsMinidump.ThreadList(_io__raw__m_data, self, self._root) 294 | elif _on == WindowsMinidump.StreamTypes.exception: 295 | self._raw__m_data = self._io.read_bytes(self.len_data) 296 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 297 | self._m_data = WindowsMinidump.ExceptionStream(_io__raw__m_data, self, self._root) 298 | elif _on == WindowsMinidump.StreamTypes.system_info: 299 | self._raw__m_data = self._io.read_bytes(self.len_data) 300 | _io__raw__m_data = KaitaiStream(BytesIO(self._raw__m_data)) 301 | self._m_data = WindowsMinidump.SystemInfo(_io__raw__m_data, self, self._root) 302 | else: 303 | self._m_data = self._io.read_bytes(self.len_data) 304 | self._io.seek(_pos) 305 | return getattr(self, '_m_data', None) 306 | 307 | 308 | class Thread(KaitaiStruct): 309 | """ 310 | .. seealso:: 311 | Source - https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_thread 312 | """ 313 | def __init__(self, _io, _parent=None, _root=None): 314 | self._io = _io 315 | self._parent = _parent 316 | self._root = _root if _root else self 317 | self._read() 318 | 319 | def _read(self): 320 | self.thread_id = self._io.read_u4le() 321 | self.suspend_count = self._io.read_u4le() 322 | self.priority_class = self._io.read_u4le() 323 | self.priority = self._io.read_u4le() 324 | self.teb = self._io.read_u8le() 325 | self.stack = WindowsMinidump.MemoryDescriptor(self._io, self, self._root) 326 | self.thread_context = WindowsMinidump.LocationDescriptor(self._io, self, self._root) 327 | 328 | 329 | class MemoryList(KaitaiStruct): 330 | """ 331 | .. seealso:: 332 | Source - https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_memory64_list 333 | """ 334 | def __init__(self, _io, _parent=None, _root=None): 335 | self._io = _io 336 | self._parent = _parent 337 | self._root = _root if _root else self 338 | self._read() 339 | 340 | def _read(self): 341 | self.num_mem_ranges = self._io.read_u4le() 342 | self.mem_ranges = [] 343 | for i in range(self.num_mem_ranges): 344 | self.mem_ranges.append(WindowsMinidump.MemoryDescriptor(self._io, self, self._root)) 345 | 346 | 347 | 348 | class MemoryDescriptor(KaitaiStruct): 349 | """ 350 | .. seealso:: 351 | Source - https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_memory_descriptor 352 | """ 353 | def __init__(self, _io, _parent=None, _root=None): 354 | self._io = _io 355 | self._parent = _parent 356 | self._root = _root if _root else self 357 | self._read() 358 | 359 | def _read(self): 360 | self.addr_memory_range = self._io.read_u8le() 361 | self.memory = WindowsMinidump.LocationDescriptor(self._io, self, self._root) 362 | 363 | 364 | class ExceptionStream(KaitaiStruct): 365 | """ 366 | .. seealso:: 367 | Source - https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_exception_stream 368 | """ 369 | def __init__(self, _io, _parent=None, _root=None): 370 | self._io = _io 371 | self._parent = _parent 372 | self._root = _root if _root else self 373 | self._read() 374 | 375 | def _read(self): 376 | self.thread_id = self._io.read_u4le() 377 | self.reserved = self._io.read_u4le() 378 | self.exception_rec = WindowsMinidump.ExceptionRecord(self._io, self, self._root) 379 | self.thread_context = WindowsMinidump.LocationDescriptor(self._io, self, self._root) 380 | 381 | 382 | @property 383 | def streams(self): 384 | if hasattr(self, '_m_streams'): 385 | return self._m_streams 386 | 387 | _pos = self._io.pos() 388 | self._io.seek(self.ofs_streams) 389 | self._m_streams = [] 390 | for i in range(self.num_streams): 391 | self._m_streams.append(WindowsMinidump.Dir(self._io, self, self._root)) 392 | 393 | self._io.seek(_pos) 394 | return getattr(self, '_m_streams', None) 395 | 396 | 397 | -------------------------------------------------------------------------------- /scripts/extractors/poshc2Parser/poshc2parser.py: -------------------------------------------------------------------------------- 1 | from chepy import Chepy 2 | import sys 3 | import os 4 | import argparse 5 | import r2pipe 6 | import rzpipe 7 | 8 | def parsePosh(filepath): 9 | try: 10 | r = r2pipe.open(filepath, flags=["-2"]) 11 | except: 12 | r = rzpipe.open(filepath, flags=["-2"]) 13 | strings = r.cmdj("izzj") 14 | payload = "" 15 | f = open(filepath, "rb").read() 16 | if f.startswith(b"MZ"): 17 | for hit in range(len(strings)): 18 | if strings[hit]["size"] == 4088 and strings[hit]["type"] == "utf16le": 19 | payload = payload + strings[hit]["string"] 20 | hit = hit + 1 21 | else: 22 | for hit in range(len(strings)): 23 | payload = payload + strings[hit]["string"] 24 | hit = hit + 1 25 | try: 26 | output = ( 27 | Chepy(payload) 28 | .remove_nullbytes() 29 | .regex_search(r"[a-zA-Z0-9+=]{200,}") 30 | .from_base64() 31 | .remove_nullbytes() 32 | .decode("Latin-1") 33 | .regex_search(r"[a-zA-Z0-9/\\+=]{100,}") 34 | .from_base64() 35 | .gzip_decompress() 36 | .o 37 | .decode() 38 | ) 39 | if len(output): 40 | print(output) 41 | else: 42 | print("Could not extract the configuration.") 43 | except AttributeError as e: 44 | print(f"{str(e)}") 45 | except Exception as e: 46 | print(f"File might not be PoshC2. {str(e)}") 47 | 48 | if __name__ == "__main__": 49 | parser = argparse.ArgumentParser( 50 | description="Extract PoshC2 configuration" 51 | ) 52 | parser.add_argument( 53 | "--file", "-f", required=True, help="Path to suspected PoshC2 sample" 54 | ) 55 | args = parser.parse_args() 56 | if not os.path.exists(args.file): 57 | print(f"[!] Input file does not exist: {args.file}") 58 | sys.exit(1) 59 | 60 | parsePosh(args.file) 61 | -------------------------------------------------------------------------------- /scripts/helpers.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief This file contains helper classes and functions. 3 | @details If a function is used for multiple plugins, it 4 | might be best to add it here for easier maintenance. 5 | """ 6 | 7 | 8 | class helpers: 9 | """! 10 | @brief Helper class for helper functions. 11 | """ 12 | import yara 13 | import subprocess 14 | 15 | def yara_exec(self, rule, fi): 16 | """! @brief run Yara on files 17 | @param[in] rule Rule File 18 | @param[in] fi Filename""" 19 | 20 | rules_file = open(rule, "r") 21 | 22 | yara_data = open(fi, "rb") 23 | 24 | rule = self.yara.compile(source=rules_file.read()) 25 | matches = rule.match(data=yara_data.read()) 26 | 27 | rules_file.close() 28 | yara_data.close() 29 | 30 | return matches 31 | 32 | def script_exec(self, script, filename, arg): 33 | """! @brief run a python script file on a file 34 | @details runs subprocess.call([python3, script, arg, filename]) 35 | @param[in] script Script file 36 | @param[in] filename File to pass to script 37 | @param[in] arg Arguments to the script (optional)""" 38 | if len(arg) > 0: 39 | self.subprocess.call(["python3", script, arg, filename]) 40 | else: 41 | self.subprocess.call(["python3", script, filename]) 42 | 43 | def bin_exec(self, process): 44 | """! @brief run a program on a file 45 | @details runs subprocess.call(process) after building the process list in the plugin 46 | @param[in] process Process list for a subprocess.call()""" 47 | self.subprocess.call(process) 48 | 49 | def get_rules(self, config_dir): 50 | """! @brief collect yara rules from a directory 51 | @param[in] config_dir rules repo""" 52 | rules = [] 53 | scripts = [] 54 | for f in config_dir.rglob("*"): 55 | if f.suffix == ".yara" or f.suffix == ".yar": 56 | rules.append(str(f)) 57 | if f.suffix == ".py": 58 | scripts.append(str(f)) 59 | return rules, scripts 60 | 61 | 62 | class Settings: 63 | """! 64 | @brief Setting class to read the settings.yml file. 65 | """ 66 | import yaml 67 | import os 68 | import sys 69 | import logging 70 | 71 | settings_file = "settings.yml" 72 | 73 | def __init__(self) -> None: 74 | self.cfg = {} 75 | self.logger = self.logging.getLogger(__class__.__name__) 76 | 77 | with open(self.settings_file, "r") as ymlfile: 78 | try: 79 | configuration = self.yaml.load( 80 | ymlfile, Loader=self.yaml.FullLoader, 81 | ) 82 | for item, conf in configuration.items(): 83 | self.cfg[item] = conf 84 | except IOError: 85 | self.logger.error( 86 | f"Could not open {self.settings_file}", 87 | ) 88 | self.sys.exit(1) 89 | 90 | for key in self.cfg["env_variables"]: 91 | try: 92 | self.cfg[key] = self.os.environ[self.cfg["env_variables"][key]] 93 | except: 94 | self.cfg[key] = "" 95 | 96 | self._cfg = self.cfg 97 | 98 | def getSetting(self, setting): 99 | return self.cfg[setting] 100 | 101 | def getIndex(self, indexname): 102 | return self._indices[indexname] 103 | 104 | def getEnvVariable(self, varname): 105 | try: 106 | return self._envvariables[varname] 107 | except KeyError: 108 | self.logger.error( 109 | f"Env Variable does not exist: {varname}", 110 | ) 111 | return None 112 | 113 | 114 | class virustotal: 115 | """! 116 | @brief Virus Total class to handle Virus Total API calls. 117 | """ 118 | # VT Interactions 119 | import logging 120 | import requests 121 | import json 122 | 123 | settings = Settings() 124 | DOWNLOAD_URL = "https://www.virustotal.com/api/v3/files/" 125 | MATCHES = {} 126 | 127 | def __init__(self, VT_KEY): 128 | self.VT_KEY = VT_KEY 129 | self.logger = self.logging.getLogger(__class__.__name__) 130 | 131 | def getBehavior(self, hash): 132 | behavior_lookup = self.requests.get( 133 | self.DOWNLOAD_URL + hash + "/behaviours", 134 | headers={"x-apikey": self.VT_KEY}, 135 | ) 136 | if str(behavior_lookup.status_code) != "200": 137 | self.logger.error( 138 | f"VT API Behaviour lookup Status code was non-200: {behavior_lookup.status_code} - {behavior_lookup.reason}", 139 | ) 140 | return {} 141 | behavior_data = self.json.loads(behavior_lookup.content)["data"] 142 | return behavior_data 143 | -------------------------------------------------------------------------------- /scripts/inspect_mmaps.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Inspect mapped regions of memory, especially for privately mapped rwx or r-x regions.""" 3 | from deject.plugins import Deject 4 | import math 5 | import collections 6 | import pefile 7 | import ssdeep 8 | import binascii 9 | from hashlib import md5 10 | from pathlib import Path 11 | from typer import secho, colors 12 | 13 | 14 | savedir = Path("./save") 15 | 16 | 17 | def get_rich_header_hash(content): 18 | try: 19 | pe = pefile.PE(data=content) 20 | if not hasattr(pe, "RICH_HEADER") or pe.RICH_HEADER is None: 21 | return "" 22 | 23 | return md5(pe.RICH_HEADER.clear_data).hexdigest() 24 | except: 25 | return "DOS Magic Header not found" 26 | 27 | 28 | def entropy(data): 29 | e = 0 30 | counter = collections.Counter(data) 31 | length = len(data) 32 | for count in counter.values(): 33 | p_x = count / length 34 | e += - p_x * math.log2(p_x) 35 | return round(e, 5) 36 | 37 | 38 | def inspct_mmaps(mmaps): 39 | 40 | pages = [] 41 | 42 | for mmap in mmaps: 43 | map_type = mmap["name"].split(' ')[2].split('=')[1] 44 | map_addr = hex(abs(int(mmap["address"]))) 45 | if mmap["flags"] == "rwx" and int(map_type, 16) == int("0x20000", 16): 46 | pages.append([map_addr, hex(mmap["size"]), mmap["flags"]]) 47 | elif mmap["flags"] == "r-x" and int(map_type, 16) == int("0x20000", 16): 48 | pages.append([map_addr, hex(mmap["size"]), mmap["flags"]]) 49 | return pages 50 | 51 | 52 | def enrich(maps): 53 | for page in maps: 54 | 55 | content = Deject.r2_handler.cmd("p8 {} @ {}".format(page[1], page[0])) 56 | i = maps.index(page) 57 | maps[i].append([]) 58 | if "4d5a" in content: 59 | maps[i][-1].append("MZ") 60 | if "5045" in content: 61 | maps[i][-1].append("PE") 62 | 63 | maps[i].append(entropy(content)) 64 | maps[i].append(ssdeep.hash(binascii.unhexlify(content.strip()))) 65 | maps[i].append(get_rich_header_hash(bytes.fromhex(content.strip()))) 66 | return maps 67 | 68 | 69 | @Deject.plugin 70 | def mmaps_report(): 71 | """Look for privately mapped mempages with RWX permissions.""" 72 | mmaps = Deject.r2_handler.cmdj("imj") 73 | mmap_cands = inspct_mmaps(mmaps) 74 | if len(mmap_cands) != 0: 75 | secho("Privately mapped memory pages with interesting permissions found!", fg=colors.GREEN) 76 | enriched = enrich(mmap_cands) 77 | res = { 78 | "header": [ 79 | "vaddr", "size", "perms", "headers", 80 | "entropy", "ssdeep", "Rich Header Hash", 81 | ], "rows": enriched, 82 | } 83 | return res 84 | else: 85 | secho("No privately mapped RWX pages could be found!", fg=colors.RED) 86 | 87 | 88 | def help(): 89 | print(""" 90 | Inspect MMaps Plugin 91 | SYNOPSIS 92 | Inspect the Memory Maps for a memory dump, especially for RX or RWX permissions. 93 | This uses Radare2/Rizin with the 'imj' command. 94 | This plugin takes no additional arguments. 95 | """) 96 | -------------------------------------------------------------------------------- /scripts/list_bofs.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Search the memory dump for InvokeBof and try and find the EXE or DLL invoked. 3 | @details InvokeBof is used in (https://github.com/CCob/BOF.NET) and could indicate 4 | if additional tooling has been invoked from the C2. 5 | However, if a BOF is expected and this does not pick it up, manual analysis is needed. 6 | """ 7 | from deject.plugins import Deject 8 | 9 | 10 | @Deject.plugin 11 | def list_bofs(): 12 | """check for InvokeBof in the memory dump""" 13 | sections = Deject.r2_handler.cmd( 14 | r"izzz | grep \"InvokeBof\" -C 5 | grep \"\.exe$\"", 15 | ) 16 | sections += Deject.r2_handler.cmd( 17 | r"izzz | grep \"InvokeBof\" -C 5 | grep \"\.dll$\"", 18 | ) 19 | if sections is None: 20 | print("InvokeBof not found in Dump. Potentially BOF free!") 21 | return 22 | rows = [] 23 | # check for duplicates 24 | for d in sections.split("\n"): 25 | if d != "": 26 | exe = d.split(" ")[9] 27 | section = d.split(" ")[7] 28 | rows.append([exe, section]) 29 | 30 | res = {"header": ["Exe Name", "Memory Section"], "rows": rows} 31 | 32 | return res 33 | 34 | 35 | def help(): 36 | print(""" 37 | InvokeBof Plugin 38 | SYNOPSIS 39 | This plugin will search a memory dump for 'InvokeBof' and try and find an EXE or DLL around the 40 | location, to see what could have been loaded and run. 41 | This plugin takes no additional arguments. 42 | """) 43 | -------------------------------------------------------------------------------- /scripts/list_dlls.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief List DLLs in a memory dump and check for duplicates. 3 | @details Duplicates could indicate an anomaly. 4 | """ 5 | from deject.plugins import Deject 6 | import ssdeep 7 | import binascii 8 | 9 | 10 | def extract(sections): 11 | dlls = [] 12 | for s in sections: 13 | if s["name"][-4:] == ".dll": 14 | dlls.append(s) 15 | return dlls 16 | 17 | 18 | @Deject.plugin 19 | def list_dlls(): 20 | """List the dlls in the memory dump. Highlight if there are duplicates.""" 21 | sections = Deject.r2_handler.cmdj("iSj~dll") 22 | if sections is None: 23 | print("No dlls detected in the dump, this might be a bug!") 24 | return 25 | dlls = extract(sections) 26 | rows = [] 27 | 28 | # check for duplicates 29 | dlls1 = set() 30 | for i in dlls: 31 | dlls1.add(i["name"]) 32 | if len(dlls) > len(dlls1): 33 | print("Duplicate dlls detected!") 34 | matches = [element for element in dlls if element in dlls1] 35 | print(matches) 36 | 37 | for d in dlls: 38 | content = Deject.r2_handler.cmd( 39 | "p8 {} @ {}".format(d['size'], d['vaddr']), 40 | ) 41 | sshash = ssdeep.hash(binascii.unhexlify(content.strip())) 42 | if (d["name"].split("\\")[2].casefold() != "system32".casefold()) and (d["name"].split("\\")[2].casefold() != "syswow64".casefold()): 43 | rows.append([ 44 | d["name"], hex(d["vaddr"]), 45 | hex(d["size"]), "X", sshash, 46 | ]) 47 | elif not Deject.quiet: 48 | rows.append([ 49 | d["name"], hex(d["vaddr"]), 50 | hex(d["size"]), " ", sshash, 51 | ]) 52 | 53 | res = { 54 | "header": [ 55 | "Name", "vaddr", "size", 56 | "anomalous", "ssdeep", 57 | ], "rows": rows, 58 | } 59 | 60 | return res 61 | 62 | 63 | def help(): 64 | print(""" 65 | List DLLs plugin 66 | SYNOPSIS 67 | 68 | This plugin will print DLLs from a memory dump using Radare/Rizin 'iSj~dll'. 69 | It will also print duplicated DLLs in the memory dump. 70 | This plugin has no additional arguments. 71 | """) 72 | -------------------------------------------------------------------------------- /scripts/list_exes.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief List executable files in a memory dump. 3 | @details More than one executable in a memory dump is suspicious 4 | and could indicate injection. 5 | """ 6 | from deject.plugins import Deject 7 | import ssdeep 8 | import binascii 9 | 10 | 11 | def extract(sections): 12 | exes = [] 13 | for s in sections: 14 | if s["name"][-4:] == ".exe": 15 | exes.append(s) 16 | return exes 17 | 18 | 19 | @Deject.plugin 20 | def list_exes(): 21 | """List the executables in the memory dump, having more than 1 is a good anomaly indicator.""" 22 | 23 | sections = Deject.r2_handler.cmdj("iSj") 24 | if sections is None: 25 | print("No exe(s) detected in the dump, this might be a bug!") 26 | return 27 | exes = extract(sections) 28 | rows = [] 29 | if len(exes) > 1: 30 | print("More than 1 exe found!") 31 | for exe in exes: 32 | content = Deject.r2_handler.cmd( 33 | "p8 {} @ {}".format(exe['size'], exe['vaddr']), 34 | ) 35 | sshash = ssdeep.hash(binascii.unhexlify(content.strip())) 36 | rows.append([exe["name"], hex(exe["vaddr"]), hex(exe["size"]), sshash]) 37 | 38 | res = {"header": ["Name", "vaddr", "size", "ssdeep"], "rows": rows} 39 | 40 | return res 41 | 42 | 43 | def help(): 44 | print(""" 45 | List EXEs plugin 46 | SYNOPSIS 47 | This will list executable files in a memory dump, using Radare2/Rizin with 'iSj' looking for '.exe' 48 | in the section name. 49 | This plugin takes no additional arguments. 50 | """) 51 | -------------------------------------------------------------------------------- /scripts/list_libs.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief A plugin to list libraries in a memory dump or PE file. 3 | """ 4 | from deject.plugins import Deject 5 | 6 | 7 | @Deject.plugin 8 | def lis_libs(): 9 | """List libraries from a PE file or memory dump""" 10 | libs = Deject.r2_handler.cmdj("ilj") 11 | results = {"header": ["Library"], "rows": []} 12 | for lib in libs: 13 | results["rows"].append([lib.strip().split(" ")[-1]]) 14 | 15 | return results 16 | 17 | 18 | def help(): 19 | print(""" 20 | List Libraries plugin 21 | 22 | SYNOPSIS 23 | 24 | This plugin is used to list libraries in a PE file or memory dump by running Radare/Rizin with the 'ilj' command. 25 | """) 26 | -------------------------------------------------------------------------------- /scripts/macho_fat_parser.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief uses Kaitai (https://github.com/kaitai-io/kaitai_struct_compiler) to parse Mach-o Fat files. 3 | @details Can supply a section name to extract out of the Mach-o Fat. 4 | By default runs macho_parser_misc() from macho_parser plugin 5 | """ 6 | from deject.plugins import Deject 7 | from scripts.extractors.kaitai.mach_o_fat import MachOFat 8 | import scripts.macho_parser 9 | 10 | 11 | @Deject.plugin 12 | def macho_fat_parser(): 13 | """Used to parse information from a Mach-o file""" 14 | data = MachOFat.from_file(Deject.file_path) 15 | args = str(Deject.plugin_args).split(" ") 16 | if args[0] == "False": 17 | return f"Please select a CPU Arch from the following: {[x.cpu_type.name for x in data.fat_archs]})" 18 | args.append("") 19 | for archs in data.fat_archs: 20 | if archs.cpu_type.name == args[0]: 21 | match args[1]: 22 | case "extract": 23 | result = scripts.macho_parser.macho_parser_extract( 24 | archs.object, args[2], 25 | ) 26 | case "sections": 27 | result = scripts.macho_parser.macho_parser_sections( 28 | archs.object, 29 | ) 30 | case _: 31 | result = scripts.macho_parser.macho_parser_misc( 32 | archs.object, 33 | ) 34 | return result 35 | 36 | 37 | def help(): 38 | print(""" 39 | Mach-o Fat Parser plugin 40 | 41 | SYNOPSIS "[options]" 42 | 43 | Select the Architecture to get information about, running without the architechure will print available architectures in the file. 44 | 45 | Uses the Mach-o Fat parser from Kaitai to read information from a Mach-o Fat file. 46 | If extract and a section name is added, extract data from that section (enclose additional options in quotes). 47 | """) 48 | -------------------------------------------------------------------------------- /scripts/macho_parser.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief uses Kaitai (https://github.com/kaitai-io/kaitai_struct_compiler) to parse Mach-o files. 3 | @details Can supply a section name to extract out of the Mach-o. 4 | By default runs macho_parser_misc() 5 | """ 6 | from deject.plugins import Deject 7 | from scripts.extractors.kaitai.mach_o import MachO 8 | from enum import Flag 9 | 10 | 11 | class Flags(Flag): 12 | MH_NOUNDEFS = 0x1 13 | MH_INCRLINK = 0x2 14 | MH_DYLDLINK = 0x4 15 | MH_BINDATLOAD = 0x8 16 | MH_PREBOUND = 0x10 17 | MH_SPLIT_SEGS = 0x20 18 | MH_LAZY_INIT = 0x40 19 | MH_TWOLEVEL = 0x80 20 | MH_FORCE_FLAT = 0x100 21 | MH_NOMULTIDEFS = 0x200 22 | MH_NOFIXPREBINDING = 0x400 23 | MH_PREBINDABLE = 0x800 24 | MH_ALLMODSBOUND = 0x1000 25 | MH_SUBSECTIONS_VIA_SYMBOLS = 0x2000 26 | MH_CANONICAL = 0x4000 27 | MH_WEAK_DEFINES = 0x8000 28 | MH_BINDS_TO_WEAK = 0x10000 29 | MH_ALLOW_STACK_EXECUTION = 0x20000 30 | MH_ROOT_SAFE = 0x40000 31 | MH_SETUID_SAFE = 0x80000 32 | MH_NO_REEXPORTED_DYLIBS = 0x100000 33 | MH_PIE = 0x200000 34 | MH_DEAD_STRIPPABLE_DYLIB = 0x400000 35 | MH_HAS_TLV_DESCRIPTORS = 0x800000 36 | MH_NO_HEAP_EXECUTION = 0x1000000 37 | MH_APP_EXTENSION_SAFE = 0x02000000 38 | MH_NLIST_OUTOFSYNC_WITH_DYLDINFO = 0x04000000 39 | MH_SIM_SUPPORT = 0x08000000 40 | MH_DYLIB_IN_CACHE = 0x80000000 41 | 42 | 43 | class SegFlags(Flag): 44 | SG_HIGHVM = 0x1 45 | SG_FVMLIB = 0x2 46 | SG_NORELOC = 0x4 47 | SG_PROTECTED_VERSION_1 = 0x8 48 | SG_READ_ONLY = 0x10 49 | 50 | 51 | @Deject.plugin 52 | def macho_parser(): 53 | """Used to parse information from a Mach-o file""" 54 | data = MachO.from_file(Deject.file_path) 55 | args = str(Deject.plugin_args).split(" ") 56 | match args[0]: 57 | case "extract": 58 | result = macho_parser_extract(data, args[1]) 59 | case "sections": 60 | result = macho_parser_sections(data) 61 | case _: 62 | result = macho_parser_misc(data) 63 | 64 | return result 65 | 66 | 67 | def macho_parser_misc(data): 68 | """Parses information from a Mach-o file, such as Entrypoint and Sections""" 69 | rows = [] 70 | rows.append(["Magic", data.magic]) 71 | rows.append(["Architecture", data.header.cputype]) 72 | rows.append(["File Type", data.header.filetype]) 73 | rows.append(["Flags (raw)", hex(data.header.flags)]) 74 | flags = macho_parser_flags_lookup(data.header.flags) 75 | rows.append(["Flags", "\n".join(flags)]) 76 | rows.append(["Number of Load Commands", data.header.ncmds]) 77 | rows.append(["Size of Load Commands", data.header.sizeofcmds]) 78 | t = {"header": ["Key", "Value"], "rows": rows} 79 | return t 80 | 81 | 82 | def macho_parser_sections(data): 83 | rows = [] 84 | dylibs = [] 85 | for command in data.load_commands: 86 | if isinstance(command.body, data.DylinkerCommand): 87 | rows.append(["Dylinker Command", command.body.name.value]) 88 | if isinstance(command.body, data.EntryPointCommand): 89 | rows.append(["Section Type", command.type.name]) 90 | rows.append(["Entrypoint Offset", command.body.entry_off]) 91 | rows.append(["Stack Size", command.body.stack_size]) 92 | if isinstance(command.body, data.DylibCommand): 93 | rows.append(["Section Type", command.type.name]) 94 | dylibs.append(command.body.name) 95 | if isinstance(command.body, data.SegmentCommand64): 96 | for section in command.body.sections: 97 | rows.append(["Section Flags (raw)", hex(section.flags)]) 98 | rows.append([ 99 | "Section Flags", "\n".join( 100 | macho_parser_seg_flags_lookup(section.flags), 101 | ), 102 | ]) 103 | rows.append(["Section Type", command.type.name]) 104 | rows.append(["Section Name", section.sect_name]) 105 | rows.append(["Segment Name", section.seg_name]) 106 | rows.append(["Size", section.size]) 107 | rows.append(["Address", hex(section.addr)]) 108 | if isinstance(command.body, data.SegmentCommand): 109 | for section in command.body.sections: 110 | rows.append(["Section Flags (raw)", hex(section.flags)]) 111 | rows.append([ 112 | "Section Flags", "\n".join( 113 | macho_parser_seg_flags_lookup(section.flags), 114 | ), 115 | ]) 116 | rows.append(["Section Type", command.type.name]) 117 | rows.append(["Section Name", section.sect_name]) 118 | rows.append(["Segment Name", section.seg_name]) 119 | rows.append(["Size", section.size]) 120 | rows.append(["Address", hex(section.addr)]) 121 | rows.append(["Dylib Commands", '\n'.join(dylibs)]) 122 | t = {"header": ["Key", "Value"], "rows": rows} 123 | return t 124 | 125 | 126 | def macho_parser_flags_lookup(data): 127 | flags = [] 128 | for i in [e.value for e in Flags]: 129 | try: 130 | if data & i > 0: 131 | flags.append(Flags(data & i).name) 132 | except ValueError: 133 | continue 134 | return flags 135 | 136 | 137 | def macho_parser_seg_flags_lookup(data): 138 | flags = [] 139 | for i in [e.value for e in SegFlags]: 140 | try: 141 | if data & i > 0: 142 | flags.append(SegFlags(data & i).name) 143 | except ValueError: 144 | continue 145 | return flags 146 | 147 | 148 | def macho_parser_extract(data, args): 149 | """Extracts sections from a Mach-o file""" 150 | try: 151 | for command in data.load_commands: 152 | if isinstance(command.body, data.SegmentCommand64): 153 | for section in command.body.sections: 154 | if section.sect_name == args or section.sect_name == f"__{args}": 155 | if isinstance(section.data, data.SegmentCommand64.Section64.StringList): 156 | return section.data.strings 157 | if isinstance(section.data, data.SegmentCommand64.Section64.PointerList): 158 | return section.data.items 159 | return section.data 160 | except UnicodeDecodeError: 161 | raise Exception( 162 | f"Could not decode data for section {args}!", 163 | ) 164 | 165 | 166 | def help(): 167 | print(""" 168 | Mach-o Parser plugin 169 | 170 | SYNOPSIS "[options]" 171 | 172 | Uses the Mach-o parser from Kaitai to read information from a Mach-o file. 173 | If extract and a section name is added, extract data from that section (enclose additional options in quotes). 174 | """) 175 | -------------------------------------------------------------------------------- /scripts/malwareconfigextract.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief uses malduck (https://github.com/CERT-Polska/malduck) and 3 | mwcfg (https://github.com/c3rb3ru5d3d53c/mwcfg/) to extract configurations from files. 4 | @details mwcfg is a submodule located "./scripts/extractors/MalwareConfigExtractor" 5 | if a new parser is not available, update the submodule. 6 | """ 7 | from deject.plugins import Deject 8 | import hashlib 9 | import pkgutil 10 | import magic 11 | from malduck.extractor import ExtractManager, ExtractorModules 12 | 13 | 14 | @Deject.plugin 15 | def extract_config_worker(): 16 | """Try to extract configs from known Malware Families, such as ASyncRAT and Dridex""" 17 | filename = Deject.file_path 18 | with open(filename, "rb") as f: 19 | data = f.read() 20 | md5 = hashlib.md5(data).hexdigest() 21 | sha1 = hashlib.sha1(data).hexdigest() 22 | sha256 = hashlib.sha256(data).hexdigest() 23 | modules = ExtractorModules("./scripts/extractors/MalwareConfigExtractor") 24 | ext = ExtractManager(modules=modules) 25 | ext.push_file(filename) 26 | 27 | result = { 28 | 'type': magic.from_buffer(data), 29 | 'mime': magic.from_buffer(data, mime=True), 30 | 'md5': md5, 31 | 'sha1': sha1, 32 | 'sha256': sha256, 33 | 'configs': ext.config, 34 | } 35 | return str(result) 36 | 37 | 38 | def get_modules(): 39 | result = '' 40 | modules = [ 41 | name for _, name, _ in pkgutil.iter_modules( 42 | ["./scripts/extractors/MalwareConfigExtractor"], 43 | ) 44 | ] 45 | for module in modules: 46 | result = result + '

' + module + '

\n' 47 | return result 48 | 49 | 50 | def help(): 51 | print(""" 52 | Malware Config Extract plugin 53 | SYNOPSIS 54 | This plugin uses MalDuck and MWCFG to check for malware configurations in a file. 55 | There are no additional arguments to this plugin. 56 | """) 57 | -------------------------------------------------------------------------------- /scripts/minidump_parser.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief uses Kaitai (https://github.com/kaitai-io/kaitai_struct_compiler) to parse MDMP files. 3 | @details Can supply "sysinfo", "processdata", "misc" to get information out of the MDMP. 4 | By default runs sysinfo. 5 | Note: uses match/case in Python, needs Python 3.10 minimum. 6 | """ 7 | from deject.plugins import Deject 8 | from scripts.extractors.kaitai.windows_minidump import WindowsMinidump 9 | from datetime import datetime, timezone 10 | from enum import Flag 11 | 12 | 13 | class Flags1(Flag): 14 | MINIDUMP_MISC1_PROCESS_ID = 0x1 15 | MINIDUMP_MISC1_PROCESS_TIMES = 0x2 16 | MINIDUMP_MISC1_PROCESSOR_POWER_INFO = 0x4 17 | MINIDUMP_MISC3_PROCESS_INTEGRITY = 0x10 18 | MINIDUMP_MISC3_PROCESS_EXECUTE_FLAGS = 0x20 19 | MINIDUMP_MISC3_TIMEZONE = 0x40 20 | MINIDUMP_MISC3_PROTECTED_PROCESS = 0x80 21 | MINIDUMP_MISC4_BUILDSTRING = 0x100 22 | MINIDUMP_MISC5_PROCESS_COOKIE = 0x200 23 | 24 | 25 | class Flags(Flag): 26 | MiniDumpNormal = 0x0 27 | MiniDumpWithDataSegs = 0x1 28 | MiniDumpWithFullMemory = 0x2 29 | MiniDumpWithHandleData = 0x4 30 | MiniDumpFilterMemory = 0x8 31 | MiniDumpScanMemory = 0x10 32 | MiniDumpWithUnloadedModules = 0x20 33 | MiniDumpWithIndirectlyReferencedMemory = 0x40 34 | MiniDumpFilterModulePaths = 0x80 35 | MiniDumpWithProcessThreadData = 0x100 36 | MiniDumpWithPrivateReadWriteMemory = 0x200 37 | MiniDumpWithoutOptionalData = 0x400 38 | MiniDumpWithFullMemoryInfo = 0x800 39 | MiniDumpWithThreadInfo = 0x1000 40 | MiniDumpWithCodeSegs = 0x2000 41 | MiniDumpWithoutAuxiliaryState = 0x4000 42 | MiniDumpWithFullAuxiliaryState = 0x8000 43 | MiniDumpWithPrivateWriteCopyMemory = 0x10000 44 | MiniDumpIgnoreInaccessibleMemory = 0x20000 45 | MiniDumpWithTokenInformation = 0x40000 46 | MiniDumpWithModuleHeaders = 0x80000 47 | MiniDumpFilterTriage = 0x100000 48 | MiniDumpWithAvxXStateContext = 0x200000 49 | MiniDumpWithIptTrace = 0x400000 50 | MiniDumpScanInaccessiblePartialPages = 0x800000 51 | MiniDumpFilterWriteCombinedMemory = 0x1ffffff 52 | MiniDumpValidTypeFlags = 0x1ffffff 53 | 54 | 55 | @Deject.plugin 56 | def minidump_parser(): 57 | """This function allows for parsing MDMP files.""" 58 | data = WindowsMinidump.from_file(Deject.file_path) 59 | match Deject.plugin_args: 60 | case "sysinfo": 61 | result = minidump_parser_sysinfo(data) 62 | case "processdata": 63 | result = minidump_parser_process_data(data) 64 | case "modulelist": 65 | result = minidump_parser_module_list(data) 66 | case "memoryinfo": 67 | result = minidump_parser_memory_info(data) 68 | case "token": 69 | result = minidump_parser_token(data) 70 | case "functiontable": 71 | result = minidump_parser_function_table(data) 72 | case "threadnames": 73 | result = minidump_parser_thread_names(data) 74 | case "handledata": 75 | result = minidump_parser_handle_data(data) 76 | case "threadlist": 77 | result = minidump_parser_thread_list(data) 78 | case "mem64": 79 | result = minidump_parser_mem64(data) 80 | case "misc": 81 | result = minidump_parser_misc(data) 82 | case _: 83 | result = minidump_parser_sysinfo(data) 84 | return result 85 | 86 | 87 | def minidump_parser_sysinfo(data): 88 | """This function is used to parse information from a minidump file""" 89 | rows = [] 90 | for stream in data.streams: 91 | streamTypeStr = str(stream.stream_type) 92 | if streamTypeStr == "StreamTypes.system_info": 93 | rows.append(["Architecture", stream.data.cpu_arch]) 94 | rows.append(["OS Version", stream.data.os_ver_major]) 95 | rows.append(["OS Platform", stream.data.os_platform]) 96 | rows.append(["OS Build", stream.data.os_build]) 97 | rows.append(["OS Reserved", stream.data.reserved2]) 98 | rows.append(["OS Type", stream.data.os_type]) 99 | rows.append(["Number of CPUs", stream.data.num_cpus]) 100 | 101 | t = {"header": ["Key", "Value"], "rows": rows} 102 | return t 103 | 104 | 105 | def minidump_parser_process_data(data): 106 | """This function is used to parse process data from a minidump file""" 107 | memRanges = [] 108 | replacements = [b"\x00", b"\x0f", b"\x1e", b"\x7f", b"\x10"] 109 | for stream in data.streams: 110 | streamTypeStr = str(stream.stream_type) 111 | if streamTypeStr == "StreamTypes.memory_list": 112 | for memoryRange in stream.data.mem_ranges: 113 | memRanges.append(memoryRange.addr_memory_range) 114 | processMemoryData = memoryRange.memory.data 115 | processMemoryDataDecoded = processMemoryData.decode( 116 | u"UTF-16-LE", errors="replace", 117 | ) 118 | processMemoryDataLength = memoryRange.memory.len_data 119 | for i in replacements: 120 | rep = i.decode(u"UTF-16-LE", errors="replace") 121 | processMemoryDataDecoded.replace(rep, '').replace('\n', '') 122 | if '.exe' in processMemoryDataDecoded or '.dll' in processMemoryDataDecoded: 123 | return f"Memory Range: {memoryRange}\n" + \ 124 | f"Length:{processMemoryDataLength}\n" + \ 125 | f"Process Data: {processMemoryDataDecoded.strip()}" 126 | 127 | 128 | def minidump_parser_module_list(data): 129 | """This function is used to parse the module list data from a minidump file""" 130 | for stream in data.streams: 131 | replacements = [b"\x00", b"\x0f", b"\x1e", b"\x7f", b"\x10"] 132 | streamTypeStr = str(stream.stream_type) 133 | if streamTypeStr == "StreamTypes.module_list": 134 | moduleListDecoded = stream.data.decode("utf-8", errors="ignore") 135 | for i in replacements: 136 | rep = i.decode(u"UTF-16-LE", errors="replace") 137 | moduleListDecoded.replace(rep, '').replace('\n', '') 138 | return f"Process Module list: {moduleListDecoded.strip()}" 139 | 140 | 141 | def minidump_parser_token(data): 142 | """This function is used to parse the token data from a minidump file""" 143 | for stream in data.streams: 144 | replacements = [b"\x00", b"\x0f", b"\x1e", b"\x7f", b"\x10"] 145 | streamTypeStr = str(stream.stream_type) 146 | if streamTypeStr == "StreamTypes.token": 147 | tokenDecoded = stream.data.decode("utf-8", errors="ignore") 148 | for i in replacements: 149 | rep = i.decode(u"UTF-16-LE", errors="replace") 150 | tokenDecoded.replace(rep, '').replace('\n', '') 151 | return f"Token: {tokenDecoded.strip()}" 152 | 153 | 154 | def minidump_parser_function_table(data): 155 | """This function is used to parse the function table data from a minidump file""" 156 | for stream in data.streams: 157 | streamTypeStr = str(stream.stream_type) 158 | replacements = [b"\x00", b"\x0f", b"\x1e", b"\x7f", b"\x10"] 159 | if streamTypeStr == "StreamTypes.function_table": 160 | functionTableDecoded = stream.data.decode("utf-8", errors="ignore") 161 | for i in replacements: 162 | rep = i.decode("utf-8", errors="ignore") 163 | functionTableDecoded.replace(rep, '').replace('\n', '') 164 | return f"Process Function table: {functionTableDecoded[0].strip()}" 165 | 166 | 167 | def minidump_parser_thread_names(data): 168 | """This function is used to parse thread data from a minidump file""" 169 | for stream in data.streams: 170 | streamTypeStr = str(stream.stream_type) 171 | if streamTypeStr == "StreamTypes.thread_names": 172 | threadNamesDecoded = stream.data.decode("utf-8", errors="ignore") 173 | return f"Thread Names: {threadNamesDecoded}" 174 | 175 | 176 | def minidump_parser_handle_data(data): 177 | """This function is used to parse handle data from a minidump file""" 178 | for stream in data.streams: 179 | streamTypeStr = str(stream.stream_type) 180 | if streamTypeStr == "StreamTypes.handle_data": 181 | handleDataDecoded = stream.data.decode("utf-8", errors="ignore") 182 | return f"Process Handle Data: {handleDataDecoded}" 183 | 184 | 185 | def minidump_parser_thread_list(data): 186 | """This function is used to parse thread list data from a minidump file""" 187 | replacements = [b"\x00", b"\x0f", b"\x1e", b"\x7f", b"\x10"] 188 | for stream in data.streams: 189 | streamTypeStr = str(stream.stream_type) 190 | if streamTypeStr == "StreamTypes.thread_list": 191 | for thread in stream.data.threads: 192 | threadMemoryDataDecoded = thread.stack.memory.data.decode( 193 | "utf-8", errors="ignore", 194 | ) 195 | for i in replacements: 196 | rep = i.decode("utf-8", errors="ignore") 197 | threadMemoryDataDecoded.replace(rep, '').replace('\n', '') 198 | return f"Total Threads: {stream.data.num_threads}\n" + \ 199 | f"Thread Environment Block(TEB): {thread.teb} \n" + \ 200 | f"Thread ID: {thread.thread_id} \n" + \ 201 | f"Suspended: {thread.suspend_count} \n" + \ 202 | f"Thread memory range: {thread.stack.addr_memory_range} \n" + \ 203 | f"Data length (Bytes): {thread.stack.memory.len_data} \n" + \ 204 | f"Data (Decoded): {threadMemoryDataDecoded.strip()} \n" 205 | 206 | 207 | def minidump_parser_mem64(data): 208 | """This function is used to parse mem64 data from a minidump file""" 209 | for stream in data.streams: 210 | streamTypeStr = str(stream.stream_type) 211 | if streamTypeStr == "StreamTypes.memory_64_list": 212 | return f"Memory x64 list (raw): {stream.data}\n" 213 | 214 | 215 | def minidump_parser_misc(data): 216 | """This function is used to parse misc_info from a minidump file""" 217 | rows = [] 218 | for stream in data.streams: 219 | streamTypeStr = str(stream.stream_type) 220 | if streamTypeStr == "StreamTypes.misc_info": 221 | rows.append([ 222 | "Process Dump Creation Time", datetime.fromtimestamp( 223 | data.timestamp, timezone.utc, 224 | ).strftime("%Y-%m-%d %H:%M:%S"), 225 | ]) 226 | rows.append(["Checksum", data.checksum]) 227 | creationtime = datetime.fromtimestamp( 228 | stream.data.process_create_time, timezone.utc, 229 | ).strftime("%Y-%m-%d %H:%M:%S") 230 | rows.append(["Process ID", stream.data.process_id]) 231 | rows.append(["Process Creation Time", creationtime]) 232 | rows.append([ 233 | "Process Kernel Time", datetime.fromtimestamp( 234 | stream.data.process_kernel_time, timezone.utc, 235 | ).strftime("%H:%M:%S"), 236 | ]) 237 | rows.append([ 238 | "Process User Time", datetime.fromtimestamp( 239 | stream.data.process_user_time, timezone.utc, 240 | ).strftime("%H:%M:%S"), 241 | ]) 242 | rows.append(["Flags1 (raw)", hex(stream.data.flags1)]) 243 | rows.append([ 244 | "Flags1", "\n".join( 245 | minidump_parser_flags1_lookup(stream.data.flags1), 246 | ), 247 | ]) 248 | rows.append(["Flags (raw)", hex(data.flags)]) 249 | rows.append( 250 | ["Flags", "\n".join(minidump_parser_flags_lookup(data.flags))], 251 | ) 252 | t = {"header": ["Key", "Value"], "rows": rows} 253 | return t 254 | 255 | 256 | def minidump_parser_memory_info(data): 257 | for stream in data.streams: 258 | streamTypeStr = str(stream.stream_type) 259 | if streamTypeStr == "StreamTypes.memory_info_list": 260 | return f"Memory info list (raw): {stream.data}\n" 261 | 262 | 263 | def minidump_parser_flags1_lookup(data): 264 | """This function is used to lookup the Flags1 enum values""" 265 | flags1 = [] 266 | for i in [e.value for e in Flags1]: 267 | try: 268 | if data & i > 0: 269 | flags1.append(Flags1(data & i).name) 270 | except ValueError: 271 | continue 272 | return [flag1 for flag1 in flags1 if flag1 is not None] 273 | 274 | 275 | def minidump_parser_flags_lookup(data): 276 | """This function is used to lookup the Flags enum values""" 277 | flags = [] 278 | for i in [e.value for e in Flags]: 279 | try: 280 | if data & i > 0: 281 | flags.append(Flags(data & i).name) 282 | except ValueError: 283 | continue 284 | return [flag for flag in flags if flag is not None] 285 | 286 | 287 | def help(): 288 | print(""" 289 | Minidump Parser Plugin 290 | SYNOPSIS [sysinfo|processdata|misc] 291 | Parses a MDMP file using Kaitai and returns Streams from the memory dump. 292 | """) 293 | -------------------------------------------------------------------------------- /scripts/ole-tools/plugin_dridex.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | __description__ = 'Dridex plugin for oledump.py' 4 | __author__ = 'Didier Stevens' 5 | __version__ = '0.0.9' 6 | __date__ = '2015/11/06' 7 | 8 | """ 9 | 10 | Source code put in public domain by Didier Stevens, no Copyright 11 | https://DidierStevens.com 12 | Use at your own risk 13 | 14 | History: 15 | 2015/02/12: start, based on sample 6beaa39b2a1d3d896c5e2fd277c227dd 16 | 2015/02/16: added OlFdL0IOXbF, based on sample f1c80a738722554b91452c59adb2f27d 17 | 2015/02/19: added NewQkeTzIIHM, based on sample d927f8cff07f87c3c3f748604ab35896 18 | 2015/02/25: 0.0.4 added Xor FF, based on sample f3c3fbeed637cccc7549636b7e0f7cdb 19 | 2015/02/26: 0.0.5 added Step2, based on sample 33c5ad38ad766d4e748ee3752fc4c292 20 | 2015/04/08: 0.0.6 added KALLKKKASKAJJAS, based on sample 491A146F5DE3592C7D959E2869F259EF 21 | 2015/04/09: 0.0.7 used KALLKKKASKAJJAS, based on sample 14C2795BCC35C3180649494EC2BC7877 22 | 2015/04/08: 0.0.8 added GQQSfwKSTdAvZbHNhpfK, based on sample 39B38CE4E2E8D843F88C3DF9124527FC 23 | 2015/11/06: 0.0.9 added chr support; added IpkfHKQ2Sd, based on sample 0E73D64FBDF6C87935C0CFF9E65FA3BE 24 | 25 | Todo: 26 | """ 27 | 28 | import re 29 | import binascii 30 | import array 31 | 32 | def RoV(InputStringToBeDecrypted): 33 | strTempText = InputStringToBeDecrypted 34 | strText = strTempText 35 | strDecryptedText = "" 36 | strText = strText[:len(strText) - 4] 37 | strText = strText[-(len(strText) - 4):] 38 | strText, nCharSize = Extract_Char_Size(strText) 39 | strText, nEncKey = Extract_Enc_Key(strText, nCharSize) 40 | nTextLenght = len(strText) 41 | for nCounter in range(0, len(strText), nCharSize): 42 | strChar1 = strText[nCounter:nCounter + nCharSize] 43 | nChar = aYP(strChar1) 44 | nChar2 = nChar / nEncKey 45 | strChar2 = chr(nChar2) 46 | strDecryptedText = strDecryptedText + strChar2 47 | return strDecryptedText.strip() 48 | 49 | def Extract_Char_Size(strText): 50 | nLeft = len(strText) / 2 51 | strLeft = strText[:nLeft] 52 | nRight = len(strText) - nLeft 53 | strRight = strText[-nRight:] 54 | strKeyEnc = strLeft[-2:] 55 | strKeySize = strRight[:2] 56 | strKeyEnc = yiK(strKeyEnc) 57 | strKeySize = yiK(strKeySize) 58 | nKeyEnc = int(strKeyEnc) 59 | nKeySize = int(strKeySize) 60 | nCharSize = nKeySize - nKeyEnc 61 | strText = strLeft[:len(strLeft) - 2] + strRight[-(len(strRight) - 2):] 62 | return (strText, nCharSize) 63 | 64 | def yiK(cString): 65 | strTempString = "" 66 | for strChar1 in cString: 67 | if strChar1.isdigit(): 68 | strTempString = strTempString + strChar1 69 | else: 70 | strTempString = strTempString + "0" 71 | return strTempString 72 | 73 | def aYP(strTempText): 74 | strText = "" 75 | strTempText = strTempText.strip() 76 | for strChar1 in strTempText: 77 | if strChar1.isdigit(): 78 | strText = strText + strChar1 79 | return int(strText) 80 | 81 | def Extract_Enc_Key(strText, nCharSize): 82 | strEncKey = "" 83 | nLenght = len(strText) - nCharSize 84 | nLeft = nLenght / 2 85 | strLeft = strText[:nLeft] 86 | nRight = nLenght - nLeft 87 | strRight = strText[-nRight:] 88 | strEncKey = strText[nLeft:nLeft + nCharSize] 89 | strEncKey = yiK(strEncKey) 90 | nEncKey = int(strEncKey.strip()) 91 | strText = strLeft + strRight 92 | return (strText, nEncKey) 93 | 94 | def MakePositive(value1, value2): 95 | while value1 < 0: 96 | value1 += value2 97 | return value1 98 | 99 | def OlFdL0IOXbF(InputData, NumKey): 100 | return ''.join([chr(MakePositive(ord(c), 256) - NumKey) for c in InputData]) 101 | 102 | def NewQkeTzIIHM(InputData): 103 | return ''.join([chr(ord(c) - 13) for c in InputData]) 104 | 105 | def lqjWjFO(strData, strKey): 106 | result = '' 107 | for iIter in range(len(strData)): 108 | if iIter < len(strKey): 109 | result += chr(ord(strData[iIter]) - ord(strKey[iIter])) 110 | else: 111 | result += chr(ord(strData[iIter]) - ord(strKey[iIter % (len(strKey) - 1)])) 112 | return result 113 | 114 | def Xor(data, key): 115 | return ''.join([chr(ord(c) ^ key) for c in data]) 116 | 117 | def Step(data, step): 118 | result = '' 119 | for iIter in range(0, len(data), step): 120 | result += data[iIter] 121 | return result 122 | 123 | def ContainsString(listStrings, key): 124 | for aString in listStrings: 125 | if key.lower() in aString.lower(): 126 | return True 127 | return False 128 | 129 | def IsHex(value): 130 | return re.match(r'^([0-9a-f][0-9a-f])+$', value, re.IGNORECASE) != None 131 | 132 | def KALLKKKASKAJJAS(strKey, strData): 133 | result = '' 134 | encoded = binascii.a2b_hex(strData) 135 | for iIter in range(len(encoded)): 136 | result += chr(ord(encoded[iIter]) ^ ord(strKey[(((iIter + 1) % len(strKey)))])) 137 | return result 138 | 139 | def GQQSfwKSTdAvZbHNhpfK(strData, strKey): 140 | result = '' 141 | dX = {x:0 for x in range(256)} 142 | Y = 0 143 | for iIter in range(256): 144 | Y = (Y + dX[iIter] + ord(strKey[iIter % len(strKey)])) % 256 145 | dX[iIter] = iIter 146 | for iIter in range(len(strData)): 147 | Y = (Y + dX[Y] + 1) % 256 148 | result += chr(ord(strData[iIter]) ^ dX[dX[(Y + dX[Y]) % 254]]) 149 | 150 | return result 151 | 152 | def IpkfHKQ2Sd(secret, key): 153 | aTable = array.array('i', [0] * (285 + 1)) 154 | aSecret = array.array('i', [0] * len(secret)) 155 | keyLength = len(key) - 1 156 | for iIter in range(0, 255 + 1): 157 | aTable[iIter] = iIter 158 | for iIter in range(256, 285 + 1): 159 | aTable[iIter] = iIter ^ 256 160 | for iIter in range(1, 6 + 1): 161 | aTable[iIter + 249] = ord(key[keyLength - iIter]) 162 | aTable[iIter - 1] = ord(key[iIter - 1]) ^ (255 - ord(key[keyLength - iIter])) 163 | 164 | bCondition = False 165 | indexKey = 0 166 | indexTable = 0 167 | for iIter in range(0, len(secret) - 1 + 1): 168 | if indexKey > keyLength: 169 | indexKey = 0 170 | if indexTable > 285 and bCondition == False: 171 | indexTable = 0 172 | bCondition = not bCondition 173 | if indexTable > 285 and bCondition == True: 174 | indexTable = 5 175 | bCondition = not bCondition 176 | aSecret[iIter] = ord(secret[iIter]) ^ (aTable[indexTable] ^ ord(key[indexKey])) 177 | indexKey = indexKey + 1 178 | indexTable = indexTable + 1 179 | return ''.join(map(chr, aSecret)) 180 | 181 | class cDridexDecoder(cPluginParent): 182 | macroOnly = True 183 | name = 'Dridex decoder' 184 | 185 | def __init__(self, name, stream, options): 186 | self.streamname = name 187 | self.stream = stream 188 | self.options = options 189 | self.ran = False 190 | 191 | def Analyze(self): 192 | self.ran = True 193 | 194 | oREString = re.compile(r'"([^"\n]+)"') 195 | foundStrings = oREString.findall(self.stream) 196 | oREChr = re.compile(r'((chr[w\$]?\(\d+\)(\s*[&+]\s*)?)+)', re.IGNORECASE) 197 | oREDigits = re.compile(r'\d+') 198 | for foundTuple in oREChr.findall(self.stream.replace('_\r\n', '')): 199 | chrString = ''.join(map(lambda x: chr(int(x)), oREDigits.findall(foundTuple[0]))) 200 | if chrString != '': 201 | foundStrings.append(chrString) 202 | 203 | for DecodingFunction in [RoV, lambda s:OlFdL0IOXbF(s, 61), NewQkeTzIIHM, lambda s:Xor(s, 0xFF), lambda s:Step(s, 2)]: 204 | result = [] 205 | for foundString in foundStrings: 206 | try: 207 | result.append(DecodingFunction(foundString)) 208 | except: 209 | pass 210 | 211 | if ContainsString(result, 'http'): 212 | return result 213 | 214 | foundStringsSmall = [foundString for foundString in foundStrings if len(foundString) <= 10] 215 | foundStringsLarge = [foundString for foundString in foundStrings if len(foundString) > 10] 216 | for foundStringSmall in foundStringsSmall: 217 | for DecodingFunction in [lqjWjFO, GQQSfwKSTdAvZbHNhpfK, IpkfHKQ2Sd]: 218 | result = [] 219 | for foundStringLarge in foundStringsLarge: 220 | try: 221 | result.append(DecodingFunction(foundStringLarge, foundStringSmall)) 222 | except: 223 | pass 224 | 225 | if ContainsString(result, 'http:'): 226 | return result 227 | 228 | foundStringsHex = [foundString for foundString in foundStrings if IsHex(foundString)] 229 | foundStringsNotHex = [foundString for foundString in foundStrings if not IsHex(foundString)] 230 | for foundStringNotHex in foundStringsNotHex: 231 | for DecodingFunction in [KALLKKKASKAJJAS]: 232 | result = [] 233 | for foundStringHex in foundStringsHex: 234 | try: 235 | result.append(DecodingFunction(foundStringNotHex, foundStringHex)) 236 | except: 237 | pass 238 | 239 | if ContainsString(result, 'http'): 240 | return result 241 | 242 | for foundStringHex1 in foundStringsHex: 243 | for DecodingFunction in [KALLKKKASKAJJAS]: 244 | result = [] 245 | for foundStringHex2 in foundStringsHex: 246 | try: 247 | result.append(DecodingFunction(foundStringHex1, foundStringHex2)) 248 | except: 249 | pass 250 | 251 | if ContainsString(result, 'http'): 252 | return result 253 | 254 | return [] 255 | 256 | AddPlugin(cDridexDecoder) 257 | -------------------------------------------------------------------------------- /scripts/ole-tools/plugin_http_heuristics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | __description__ = 'HTTP Heuristics plugin for oledump.py' 4 | __author__ = 'Didier Stevens' 5 | __version__ = '0.0.13' 6 | __date__ = '2020/08/16' 7 | 8 | """ 9 | 10 | Source code put in public domain by Didier Stevens, no Copyright 11 | https://DidierStevens.com 12 | Use at your own risk 13 | 14 | History: 15 | 2014/11/12: start 16 | 2014/11/13: added HTTP filter 17 | 2014/11/14: added unencoded http string detection 18 | 2014/11/15: changed name and plugin interface 19 | 2014/11/21: changed interface: added options 20 | 2014/12/12: added BruteforceDecode 21 | 2015/02/02: 0.0.3 added base64 22 | 2015/02/09: bugfix BruteforceDecode when empty string; added StringsPerLine 23 | 2015/02/16: 0.0.4 added rot13 24 | 2015/02/25: 0.0.5 joined lines ending with _ for Chr analysis 25 | 2015/03/18: 0.0.6 also handle empty strings 26 | 2015/03/23: 0.0.7 fixed regression bug Heuristics 27 | 2015/04/01: 0.0.8 added PreProcess 28 | 2016/12/11: 0.0.9 added iOffset loop 29 | 2018/10/13: 0.0.10 changed XOR logic, added options (-e -k) 30 | 2019/11/05: 0.0.11 Python 3 support 31 | 2020/01/24: 0.0.12 added option -c 32 | 2020/01/25: Python 3 bugfix; deduping of result 33 | 2020/08/16: 0.0.13 added option -s 34 | 35 | Todo: 36 | """ 37 | 38 | import re 39 | import binascii 40 | import codecs 41 | 42 | def ReplaceFunction(match): 43 | try: 44 | return '(%d)' % eval(match.group(0)) 45 | except: 46 | return match.group(0) 47 | 48 | keywords = ['http:', 'https:'] 49 | extendedkeywords = ['msxml', 'adodb', 'shell', 'c:\\', 'cmd', 'powershell'] 50 | 51 | def StartsWithHTTP(str): 52 | tosearch = str.lower() 53 | for keyword in keywords: 54 | if tosearch.startswith(keyword): 55 | return True 56 | return False 57 | 58 | def ContainsHTTP(str): 59 | tosearch = str.lower() 60 | for keyword in keywords: 61 | if keyword in tosearch: 62 | return True 63 | return False 64 | 65 | class cHTTPHeuristics(cPluginParent): 66 | macroOnly = True 67 | name = 'HTTP Heuristics plugin' 68 | 69 | def __init__(self, name, stream, options): 70 | self.streamname = name 71 | self.streamOriginal = stream 72 | self.stream = stream 73 | self.options = options 74 | self.ran = False 75 | self.CheckFunction = StartsWithHTTP 76 | 77 | def Heuristics(self, data, noDecode=False): 78 | if self.CheckFunction(data): 79 | return data 80 | if self.CheckFunction(data[::-1]): 81 | return data[::-1] 82 | if noDecode: 83 | return data 84 | try: 85 | decoded = binascii.a2b_hex(data).decode() 86 | return self.Heuristics(decoded, True) 87 | except: 88 | if not re.compile(r'^[0-9a-zA-Z/+=]+$').match(data): 89 | return data 90 | try: 91 | decoded = binascii.a2b_base64(data).decode() 92 | return self.Heuristics(decoded, True) 93 | except: 94 | return data 95 | 96 | # bruteforce XOR; if we have more than 250 strings, split in short strings (< 10) = keys and long strings = ciphertext 97 | def BruteforceDecode(self, strings): 98 | ciphertexts = [] 99 | keys = [] 100 | result = [] 101 | 102 | if len(strings) >= 250: 103 | for string1 in strings: 104 | if len(string1) >= 10: 105 | ciphertexts.append(string1) 106 | else: 107 | keys.append(string1) 108 | else: 109 | ciphertexts = strings 110 | keys = strings 111 | for key in keys: 112 | if key != '': 113 | for ciphertext in ciphertexts: 114 | for iOffset in range(2): 115 | cleartext = '' 116 | for iIter in range(len(ciphertext)): 117 | cleartext += chr(ord(ciphertext[iIter]) ^ ord(key[(iIter + iOffset)% len(key)])) 118 | result.append(self.Heuristics(cleartext)) 119 | 120 | return result 121 | 122 | def Strings(self): 123 | return re.compile(r'"([^"]+)"').findall(self.stream) 124 | 125 | # Concatenate all strings found on the same line 126 | def StringsPerLine(self): 127 | result = [] 128 | oREString = re.compile(r'"([^"]*)"') 129 | 130 | for line in self.stream.split('\n'): 131 | stringsConcatenated = ''.join(oREString.findall(line)) 132 | if stringsConcatenated != '': 133 | result.append(stringsConcatenated) 134 | 135 | return result 136 | 137 | def PreProcess(self, options): 138 | self.stream = re.sub(r'(\(\s*(\d+|\d+\.\d+)\s*[+*/-]\s*(\d+|\d+\.\d+)\s*\))', ReplaceFunction, self.streamOriginal) 139 | 140 | if options.space: 141 | self.stream = self.stream.replace(' ', '') 142 | 143 | def AnalyzeSub(self): 144 | global keywords 145 | 146 | oParser = optparse.OptionParser() 147 | oParser.add_option('-e', '--extended', action='store_true', default=False, help='Use extended keywords') 148 | oParser.add_option('-k', '--keywords', type=str, default='', help='Provide keywords (separator is ,)') 149 | oParser.add_option('-c', '--contains', action='store_true', default=False, help='Check if string contains keyword') 150 | oParser.add_option('-s', '--space', action='store_true', default=False, help='Ignore space characters') 151 | (options, args) = oParser.parse_args(self.options.split(' ')) 152 | 153 | self.PreProcess(options) 154 | 155 | if options.extended: 156 | keywords = keywords + extendedkeywords 157 | 158 | if options.keywords != '': 159 | keywords = options.keywords.split(',') 160 | 161 | if options.contains: 162 | self.CheckFunction = ContainsHTTP 163 | 164 | result = [] 165 | 166 | oREChr = re.compile(r'((chr[w\$]?\(\d+\)(\s*&\s*)?)+)', re.IGNORECASE) 167 | oREDigits = re.compile(r'\d+') 168 | for foundTuple in oREChr.findall(self.stream.replace('_\r\n', '')): 169 | chrString = ''.join(map(lambda x: chr(int(x)), oREDigits.findall(foundTuple[0]))) 170 | if chrString != '': 171 | result.append(self.Heuristics(chrString)) 172 | 173 | oREHexBase64 = re.compile(r'"([0-9a-zA-Z/+=]+)"') 174 | for foundString in oREHexBase64.findall(self.stream): 175 | if foundString != '': 176 | result.append(self.Heuristics(foundString)) 177 | 178 | oREHTTP = re.compile(r'"(http[^"]+)"') 179 | for foundString in oREHTTP.findall(self.stream): 180 | if foundString != '': 181 | result.append(foundString) 182 | 183 | resultHttp = [line for line in result if self.CheckFunction(line)] 184 | 185 | if resultHttp == []: 186 | resultHttp = [line for line in self.BruteforceDecode(result) if self.CheckFunction(line)] 187 | 188 | if resultHttp == []: 189 | resultHttp = [codecs.encode(line, 'rot-13') for line in self.Strings() if ContainsHTTP(codecs.encode(line, 'rot-13'))] 190 | else: 191 | return resultHttp 192 | 193 | if resultHttp == []: 194 | resultHttp = [line for line in self.StringsPerLine() if ContainsHTTP(line)] 195 | else: 196 | return resultHttp 197 | 198 | if resultHttp == []: 199 | return result 200 | else: 201 | return resultHttp 202 | 203 | def Analyze(self): 204 | self.ran = True 205 | 206 | return set(self.AnalyzeSub()) 207 | 208 | AddPlugin(cHTTPHeuristics) 209 | -------------------------------------------------------------------------------- /scripts/ole-tools/plugin_metadata.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | __description__ = 'metadata plugin for oledump.py' 4 | __author__ = 'Didier Stevens' 5 | __version__ = '0.0.2' 6 | __date__ = '2022/10/27' 7 | 8 | """ 9 | 10 | Source code put in public domain by Didier Stevens, no Copyright 11 | https://DidierStevens.com 12 | Use at your own risk 13 | 14 | History: 15 | 2015/02/28: start 16 | 2022/04/25: start again for second propertyset (VSTO files) 17 | 2022/04/26: refactor 18 | 2022/09/24: 0.0.2 added ParseASN 19 | 2022/09/25: continue 20 | 2022/10/09: added PropertySetSystemIdentifier 21 | 2022/10/27: added options: option -s 22 | 23 | Todo: 24 | implement remaining types 25 | """ 26 | 27 | import datetime 28 | 29 | try: 30 | from pyasn1.codec.der import decoder as der_decoder 31 | except ImportError: 32 | print(' Signature present but error importing pyasn1 module') 33 | try: 34 | from pyasn1_modules import rfc2315 35 | except ImportError: 36 | print(' Signature present but error importing pyasn1_modules module') 37 | 38 | # [MS-OLEPS].pdf 39 | # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oleps/bf7aeae8-c47a-4939-9f45-700158dac3bc 40 | 41 | DICTIONARY_PROPERTY_IDENTIFIER = 0x00000000 42 | CODEPAGE_PROPERTY_IDENTIFIER = 0x00000001 43 | LOCALE_PROPERTY_IDENTIFIER = 0x80000000 44 | BEHAVIOR_PROPERTY_IDENTIFIER = 0x80000003 45 | 46 | # https://www.cryptosys.net/pki/manpki/pki_distnames.html 47 | def PrintComponents(obj): 48 | dOIDs = { 49 | '2.5.4.3': 'CN', 50 | '2.5.4.4': 'SN', 51 | '2.5.4.5': 'SERIALNUMBER', 52 | '2.5.4.6': 'C', 53 | '2.5.4.7': 'L', 54 | '2.5.4.8': 'S', # or ST 55 | '2.5.4.9': 'STREET', 56 | '2.5.4.10': 'O', 57 | '2.5.4.11': 'OU', 58 | '2.5.4.12': 'T', # or or TITLE 59 | '2.5.4.42': 'G', # or GN 60 | '1.2.840.113549.1.9.1': 'E', 61 | '0.9.2342.19200300.100.1.1': 'UID', 62 | '0.9.2342.19200300.100.1.25': 'DC', 63 | } 64 | 65 | result = [] 66 | for component1 in obj.components[0]: 67 | for component2 in component1: 68 | oid = list(component2.values())[0].prettyPrint() 69 | value = list(component2.values())[1][2:] 70 | result.append('%s=%s' % (dOIDs.get(oid, oid), value)) 71 | return ','.join(result) 72 | 73 | def ParseASN(data): 74 | position = data.find(b'\x30') 75 | signature = data[position:] 76 | 77 | contentInfo, _ = der_decoder.decode(signature, asn1Spec=rfc2315.ContentInfo()) 78 | contentType = contentInfo.getComponentByName('contentType') 79 | contentInfoMap = { 80 | (1, 2, 840, 113549, 1, 7, 1): rfc2315.Data(), 81 | (1, 2, 840, 113549, 1, 7, 2): rfc2315.SignedData(), 82 | (1, 2, 840, 113549, 1, 7, 3): rfc2315.EnvelopedData(), 83 | (1, 2, 840, 113549, 1, 7, 4): rfc2315.SignedAndEnvelopedData(), 84 | (1, 2, 840, 113549, 1, 7, 5): rfc2315.DigestedData(), 85 | (1, 2, 840, 113549, 1, 7, 6): rfc2315.EncryptedData() 86 | } 87 | content, _ = der_decoder.decode(contentInfo.getComponentByName('content'), asn1Spec=contentInfoMap[contentType]) 88 | serialNumber = content['signerInfos'][0]['issuerAndSerialNumber']['serialNumber'] 89 | issuer = None 90 | subject = None 91 | for f in content['certificates']: 92 | if f['certificate']['tbsCertificate']['serialNumber'] == serialNumber: 93 | certificate = f['certificate']['tbsCertificate'] 94 | issuer = PrintComponents(certificate['issuer']) 95 | subject = PrintComponents(certificate['subject']) 96 | return issuer, subject 97 | 98 | class cCONSTANTS(object): 99 | def __init__(self): 100 | self.dCONSTANTS = {} 101 | 102 | def AddValue(self, name, value): 103 | self.dCONSTANTS[name] = value 104 | 105 | def Eval(self): 106 | for key, value in self.dCONSTANTS.items(): 107 | exec('%s = %d' % (key, value), globals()) 108 | 109 | def Lookup(self, value, default=None): 110 | for key, value2 in self.dCONSTANTS.items(): 111 | if value == value2: 112 | return key 113 | return default 114 | 115 | oCONSTANTS = cCONSTANTS() 116 | oCONSTANTS.AddValue('VT_EMPTY', 0x0000) 117 | oCONSTANTS.AddValue('VT_NULL', 0x0001) 118 | oCONSTANTS.AddValue('VT_I2', 0x0002) 119 | oCONSTANTS.AddValue('VT_I4', 0x0003) 120 | oCONSTANTS.AddValue('VT_R4', 0x0004) 121 | oCONSTANTS.AddValue('VT_R8', 0x0005) 122 | oCONSTANTS.AddValue('VT_CY', 0x0006) 123 | oCONSTANTS.AddValue('VT_DATE', 0x0007) 124 | oCONSTANTS.AddValue('VT_BSTR', 0x0008) 125 | oCONSTANTS.AddValue('VT_ERROR', 0x000A) 126 | oCONSTANTS.AddValue('VT_BOOL', 0x000B) 127 | oCONSTANTS.AddValue('VT_DECIMAL', 0x000E) 128 | oCONSTANTS.AddValue('VT_I1', 0x0010) 129 | oCONSTANTS.AddValue('VT_UI1', 0x0011) 130 | oCONSTANTS.AddValue('VT_UI2', 0x0012) 131 | oCONSTANTS.AddValue('VT_UI4', 0x0013) 132 | oCONSTANTS.AddValue('VT_I8', 0x0014) 133 | oCONSTANTS.AddValue('VT_UI8', 0x0015) 134 | oCONSTANTS.AddValue('VT_INT', 0x0016) 135 | oCONSTANTS.AddValue('VT_UINT', 0x0017) 136 | oCONSTANTS.AddValue('VT_LPSTR', 0x001E) 137 | oCONSTANTS.AddValue('VT_LPWSTR', 0x001F) 138 | oCONSTANTS.AddValue('VT_FILETIME', 0x0040) 139 | oCONSTANTS.AddValue('VT_BLOB', 0x0041) 140 | oCONSTANTS.AddValue('VT_STREAM', 0x0042) 141 | oCONSTANTS.AddValue('VT_STORAGE', 0x0043) 142 | oCONSTANTS.AddValue('VT_STREAMED_OBJECT', 0x0044) 143 | oCONSTANTS.AddValue('VT_STORED_OBJECT', 0x0045) 144 | oCONSTANTS.AddValue('VT_BLOB_OBJECT', 0x0046) 145 | oCONSTANTS.AddValue('VT_CF', 0x0047) 146 | oCONSTANTS.AddValue('VT_CLSID', 0x0048) 147 | oCONSTANTS.AddValue('VT_VERSIONED_STREAM', 0x0049) 148 | oCONSTANTS.AddValue('VT_VECTOR', 0x1000) 149 | oCONSTANTS.AddValue('VT_ARRAY', 0x2000) 150 | oCONSTANTS.Eval() 151 | 152 | dVTNumbers = { 153 | VT_I2: ' len(self.attributeNames): 231 | if identifier == LOCALE_PROPERTY_IDENTIFIER: 232 | return 'LocaleProperty' 233 | elif identifier == CODEPAGE_PROPERTY_IDENTIFIER: 234 | return 'CodePageProperty' 235 | elif identifier == BEHAVIOR_PROPERTY_IDENTIFIER: 236 | return 'BehaviorProperty' 237 | else: 238 | return '%04x' % identifier 239 | else: 240 | return self.attributeNames[identifier - 1] 241 | 242 | def ParseTypedPropertyValue(self, oStructTypedPropertyValue): 243 | type, padding = oStructTypedPropertyValue.Unpack('') 282 | 283 | def AnalyzePropertySet(self, propertySetNumner, guid, offset, options): 284 | self.result.append('PropertySet %d' % propertySetNumner) 285 | self.result.append('-------------') 286 | 287 | data = self.stream[offset:] 288 | oStruct = cStruct(data) 289 | size, numProperties = oStruct.Unpack('') == 'FMTID_SummaryInformation': 292 | self.attributeNames = olefile.OleMetadata.SUMMARY_ATTRIBS 293 | elif dGUIDs.get(guid, '') == 'FMTID_DocSummaryInformation': 294 | self.attributeNames = olefile.OleMetadata.DOCSUM_ATTRIBS 295 | else: 296 | self.attributeNames = [] 297 | self.result.append('Property Set GUID: %s (%s)' % (guid, dGUIDs.get(guid, ''))) 298 | self.result.append('Number of properties: %d' % numProperties) 299 | for iIter in range(numProperties): 300 | identifier, offset = oStruct.Unpack('') 320 | if attributeName == 'version': 321 | extra = ' 0x%08x' % propertyValue 322 | if attributeName == 'dig_sig' and options.signature: 323 | issuer, subject = ParseASN(propertyValue[1]) 324 | if issuer != None and subject != None: 325 | self.result.append(' %s: issuer: %s subject: %s' % (attributeName, issuer, subject)) 326 | else: 327 | self.result.append(' %s: %s%s' % (attributeName, propertyValue[0], extra)) 328 | else: 329 | self.result.append(' %s: %s%s' % (attributeName, propertyValue, extra)) 330 | else: 331 | self.result.append(' %s: 0x%04x %s' % (attributeName, type, propertyValue)) 332 | 333 | def Analyze(self): 334 | oParser = optparse.OptionParser() 335 | oParser.add_option('-s', '--signature', action='store_true', default=False, help='Parse signature') 336 | (options, args) = oParser.parse_args(self.options.split(' ')) 337 | 338 | self.result = [] 339 | 340 | oStruct = cStruct(self.stream) 341 | try: 342 | byteOrder, version = oStruct.Unpack(' 0: 112 | self.dURLs[self.streamIndexCounter] = urls 113 | 114 | def PostProcess(self): 115 | oParser = optparse.OptionParser() 116 | oParser.add_option('-j', '--json', action='store_true', default=False, help='Produce JSON output') 117 | oParser.add_option('-J', '--jsonattachmentoutput', action='store_true', default=False, help='Produce JSON output for attachments') 118 | oParser.add_option('-b', '--body', action='store_true', default=False, help='Print body') 119 | oParser.add_option('-H', '--header', action='store_true', default=False, help='Print header') 120 | (options, args) = oParser.parse_args(self.options.split(' ')) 121 | 122 | sha256 = hashlib.sha256(self.data).hexdigest() 123 | if options.json: 124 | jsondata = {'sha256': sha256, 'subject': self.subject, 'date': self.date, 'to': self.to, 'from': self.from_, 'attachments': [{'index': key, 'inline': value.inline, 'hidden': value.hidden, 'longfilename': value.longfilename, 'mimetag': value.mimetag, 'size': len(value.data), 'sha256': hashlib.sha256(value.data).hexdigest(), 'magichex': binascii.b2a_hex(value.data[:4]).decode()} for key, value in self.dAttachments.items()]} 125 | print(json.dumps(jsondata)) 126 | elif options.jsonattachmentoutput: 127 | oMyJSONOutput = cMyJSONOutput() 128 | for index in self.dAttachments.keys(): 129 | extra = [] 130 | if self.dAttachments[index].inline: 131 | extra.append('inline') 132 | if self.dAttachments[index].hidden: 133 | extra.append('hidden') 134 | oMyJSONOutput.AddIdItem(index, IFF(extra != [], ','.join(extra) + ':', '') + self.dAttachments[index].longfilename, self.dAttachments[index].data) 135 | print(oMyJSONOutput.GetJSON()) 136 | else: 137 | print('Sample email: sha256 %s' % sha256) 138 | try: 139 | print('Header stream index: %d' % self.headerStreamIndex) 140 | except AttributeError: 141 | pass 142 | print('Subject: %s' % self.subject) 143 | print('Date: %s' % self.date) 144 | print('To: %s' % self.to) 145 | print('From: %s' % self.from_) 146 | print('Body stream index: %d' % self.bodyStreamIndex) 147 | for index in self.dAttachments.keys(): 148 | print('Attachment %d (stream index %d)%s%s: %s %s %d %s' % (index, self.dAttachments[index].streamIndex, IFF(self.dAttachments[index].inline, ', inline', ''), IFF(self.dAttachments[index].hidden, ', hidden', ''), self.dAttachments[index].longfilename, self.dAttachments[index].mimetag, len(self.dAttachments[index].data), hashlib.sha256(self.dAttachments[index].data).hexdigest())) 149 | if len(self.dURLs) > 0: 150 | print('URLs:') 151 | for key, value in self.dURLs.items(): 152 | print(' Stream %d%s:' % (key, IFF(key == self.bodyStreamIndex, ' (body stream)', ''))) 153 | for url in value: 154 | print(' %s' % url.decode('latin')) 155 | if options.body: 156 | print('Body:') 157 | print(self.body) 158 | if options.header: 159 | print('Header:') 160 | print(self.header) 161 | 162 | AddPlugin(cMSG) 163 | -------------------------------------------------------------------------------- /scripts/ole-tools/plugin_msi_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | __description__ = 'MSI summary plugin for oledump.py' 4 | __author__ = 'Didier Stevens' 5 | __version__ = '0.0.3' 6 | __date__ = '2023/04/01' 7 | 8 | """ 9 | 10 | Source code put in public domain by Didier Stevens, no Copyright 11 | https://DidierStevens.com 12 | Use at your own risk 13 | 14 | History: 15 | 2023/02/25: start 16 | 2023/02/26: continue 17 | 2023/02/26: 0.0.2 continue 18 | 2023/02/28: continue 19 | 2023/03/23: 0.0.2 added indicator and cCab 20 | 2023/04/01: 0.0.3 detect signature streams, repr streamname, chosenhash 21 | 22 | Todo: 23 | """ 24 | 25 | import optparse 26 | 27 | # https://download.microsoft.com/download/4/d/a/4da14f27-b4ef-4170-a6e6-5b1ef85b1baa/[ms-cab].pdf 28 | class cCab(object): 29 | 30 | def __init__(self, data): 31 | self.data = data 32 | oStruct = cStruct(data) 33 | oCFHeader = oStruct.UnpackNamedtuple('= 0x3800 and number < 0x4800: 46 | return code[(number - 0x3800) & 0x3F] + code[((number - 0x3800) >> 6) & 0x3F] 47 | elif number >= 0x4800 and number <= 0x4840: 48 | return code[number - 0x4800] 49 | else: 50 | return character 51 | 52 | def StreamToRows(data, columns): 53 | rowsize = 0 54 | for column in columns: 55 | rowsize += struct.calcsize(column) 56 | cntRows = int(len(data) / rowsize) 57 | # print('Number of rows: %d' % cntRows) 58 | oData = cStruct(data) 59 | dColumns = {index: [] for index, column in enumerate(columns)} 60 | for index, column in enumerate(columns): 61 | for row in range(cntRows): 62 | dColumns[index].append(oData.Unpack(column)) 63 | table = [] 64 | for rowIndex in range(cntRows): 65 | row = [] 66 | for column in range(len(columns)): 67 | row.append(dColumns[column][rowIndex]) 68 | table.append(row) 69 | return table 70 | 71 | def MagicSub(data): 72 | if data[:2] == b'MZ': 73 | result = 'PE File' 74 | elif data[:4] == b'MSCF': 75 | result = 'CAB File' 76 | elif data[:4] == b'\xff\xd8\xff\xe0': 77 | result = 'JPEG' 78 | elif data[:4] == b'\x00\x00\x01\x00': 79 | result = 'ICO' 80 | elif data[:4] == b'\x00\x00\x02\x00': 81 | result = 'CUR' 82 | elif data[:2] == b'BM': 83 | result = 'BMP' 84 | else: 85 | result = repr(data[:8]) 86 | return result 87 | 88 | def Magic(data): 89 | hashvalue, hashname = CalculateChosenHash(data) 90 | return '%s %s: %s' % (MagicSub(data), hashname, hashvalue) 91 | 92 | class cStrings(object): 93 | def __init__(self, dStreams): 94 | oStringsData = cStruct(dStreams['!_StringData'][0]) 95 | oStringsPool = cStruct(dStreams['!_StringPool'][0]) 96 | self.dStrings = {} 97 | self.dReferenceCounterStored = {} 98 | self.dReferenceCounter = {} 99 | self.codepage, self.unknownSuspectFormatIdentifier = oStringsPool.Unpack(' 0: 102 | counter += 1 103 | size, referenceCountStored = oStringsPool.Unpack(' 0: 105 | stringBytes = oStringsData.GetBytes(size) 106 | else: 107 | stringBytes = b'' 108 | self.dStrings[counter] = stringBytes 109 | self.dReferenceCounterStored[counter] = referenceCountStored 110 | 111 | def Get(self, index): 112 | self.dReferenceCounter[index] = self.dReferenceCounter.get(index, 0) + 1 113 | return self.dStrings[index] 114 | 115 | # https://doxygen.reactos.org/db/de4/msipriv_8h.html 116 | def ParseColumnAttributes(number): 117 | if number < 0x8000: 118 | return '' 119 | result = [] 120 | if number & 0x2000 == 0x2000: 121 | result.append('Key:Y') 122 | else: 123 | result.append('Key:N') 124 | if number & 0x1000 == 0x1000: 125 | result.append('Nullable:Y') 126 | else: 127 | result.append('Nullable:N') 128 | rest = number & 0x0FFF 129 | dTypes = { 130 | 0x104: 'DoubleInteger', 131 | 0x502: 'Integer', 132 | # 0xD48: 'Identifier', 133 | # 0xDFF: 'Condition', 134 | # 0xFFF: 'Text', 135 | 0xD: 'String', 136 | 0xF: 'StringLocalized', 137 | 0x9: 'Binary', 138 | } 139 | if rest in dTypes: 140 | result.append('Type:%s' % dTypes[rest]) 141 | else: 142 | type = (number & 0x0F00) >> 8 143 | result.append('Type:%s' % dTypes.get(type, '%x' % type)) 144 | result.append('Length:%02x' % (number & 0x00FF)) 145 | return ' '.join(result) 146 | 147 | def ColumnFormats(dTable): 148 | result = [] 149 | for value in dTable.values(): 150 | value1 = value[1] & 0x0FFF 151 | value2 = value[1] & 0x0FF 152 | if value1 >= 0x0800: 153 | result.append(' 0: 220 | tableName = oStrings.Get(oTables.Unpack(' MD5: %s' % binascii.b2a_hex(hash)) 274 | if streamName == '!CustomAction': 275 | customAction = row[1] & 0x3F 276 | if customAction in dCustomActionTypes: 277 | values.append(b' -> %d: %s' % (customAction, dCustomActionTypes[customAction])) 278 | print(b','.join(values)) 279 | print() 280 | 281 | if options.verbose: 282 | print('String table stats:') 283 | print('Stored reference count = 0:') 284 | for key, value in oStrings.dReferenceCounterStored.items(): 285 | if value == 0: 286 | print('%d %s' % (key, oStrings.dStrings[key])) 287 | print('Unreferenced:') 288 | for key, value in oStrings.dReferenceCounter.items(): 289 | if value == 0: 290 | print('%d %s' % (key, oStrings.dStrings[key])) 291 | print('Compare:') 292 | for key, value in oStrings.dReferenceCounterStored.items(): 293 | if not key in oStrings.dReferenceCounter: 294 | pass 295 | elif value != oStrings.dReferenceCounter[key]: 296 | print('%d %d %d %s' % (key, value, oStrings.dReferenceCounter[key], oStrings.dStrings[key])) 297 | print() 298 | 299 | print('Remaining streams:') 300 | for key, [data, index] in self.dStreams.items(): 301 | if key not in streamsProcessed: 302 | filetype = MagicSub(data) 303 | if filetype in ['BMP', 'JPEG', 'ICO', 'CUR']: 304 | indicator = ' ' 305 | elif filetype in ['PE File', 'CAB File']: 306 | indicator = '!' 307 | elif key in ['\x05SummaryInformation', '\x05DocumentSummaryInformation', '\x05DigitalSignature', '\x05MsiDigitalSignatureEx']: 308 | indicator = ' ' 309 | else: 310 | indicator = '?' 311 | print('%2d %s %8d %s %s' % (index, indicator, len(data), repr(key), Magic(data))) 312 | if filetype == 'CAB File': 313 | try: 314 | oCab = cCab(data) 315 | for item in oCab.files: 316 | print(' %8d %s' % (item.cbFile, repr(item.szName))) 317 | except: 318 | print(' error parsing CAB file') 319 | 320 | AddPlugin(cMSI) 321 | -------------------------------------------------------------------------------- /scripts/ole_analytics.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Uses oledump.py and initially runs info. Can be used with arguments to allow for additional plugins, such as plugin_msi_info or plugin_vbaproject. 3 | """ 4 | from deject.plugins import Deject 5 | from pathlib import Path 6 | from scripts.helpers import helpers 7 | 8 | 9 | @Deject.plugin 10 | def ole_analytics(): 11 | """Use oledump to analyse an OLE file, initially runs info""" 12 | script = Path("./scripts/ole-tools/oledump.py") 13 | filename = Deject.file_path 14 | if Deject.plugin_args == "False": 15 | helpers.bin_exec(helpers, ["python", script, filename]) 16 | else: 17 | args = str(Deject.plugin_args) 18 | helpers.bin_exec( 19 | helpers, [ 20 | "python", script, "--plugindir=./scripts/ole-tools/", 21 | ] + args.strip().split(" ") + [filename], 22 | ) 23 | return 24 | 25 | 26 | def help(): 27 | print(""" 28 | OLE Analytics Plugin 29 | SYNOPSIS [arguments] 30 | Uses oledump and initially runs the info command. 31 | Arguments to this plugin will be passed to oledump, this includes using additional plugins. 32 | Add plugins to the ole-tools folder. 33 | """) 34 | -------------------------------------------------------------------------------- /scripts/pdf-tools/pdfid.ini: -------------------------------------------------------------------------------- 1 | [keywords] 2 | /URI 3 | -------------------------------------------------------------------------------- /scripts/pdf-tools/pdftojpg.py: -------------------------------------------------------------------------------- 1 | from pdf2image import convert_from_path, convert_from_bytes 2 | import argparse 3 | parser = argparse.ArgumentParser() 4 | parser.add_argument("pdf") 5 | parser.add_argument("output") 6 | args = parser.parse_args() 7 | pdf = args.pdf 8 | out = args.output 9 | images = convert_from_path(pdf) 10 | i=0 11 | if not str(out).endswith('/'): 12 | out += '/' 13 | for image in images: 14 | i+=1 15 | image.save(''+str(out)+''+str(i)+'.jpg','JPEG') 16 | -------------------------------------------------------------------------------- /scripts/pdf-tools/plugin_triage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | #2014/09/30 4 | #2015/08/12 added options; changed scoring: /ObjStm 0.75; obj/endobj or stream/endstream discrepancy: 0.50 5 | #2015/08/13 added instructions 6 | #2017/10/29 added /URI 7 | 8 | class cPDFiDTriage(cPluginParent): 9 | onlyValidPDF = False 10 | name = 'Triage plugin' 11 | 12 | def __init__(self, oPDFiD, options): 13 | self.options = options 14 | self.oPDFiD = oPDFiD 15 | 16 | def Score(self): 17 | for keyword in ('/JS', '/JavaScript', '/AA', '/OpenAction', '/AcroForm', '/JBIG2Decode', '/RichMedia', '/Launch', '/EmbeddedFile', '/XFA', '/Colors > 2^24'): 18 | if keyword in self.oPDFiD.keywords and self.oPDFiD.keywords[keyword].count > 0: 19 | return 1.0 20 | if self.options != '--io': 21 | for keyword in ('/ObjStm', ): 22 | if keyword in self.oPDFiD.keywords and self.oPDFiD.keywords[keyword].count > 0: 23 | return 0.75 24 | for keyword in ('/URI', ): 25 | if keyword in self.oPDFiD.keywords and self.oPDFiD.keywords[keyword].count > 0: 26 | return 0.6 27 | if self.oPDFiD.keywords['obj'].count != self.oPDFiD.keywords['endobj'].count: 28 | return 0.5 29 | if self.oPDFiD.keywords['stream'].count != self.oPDFiD.keywords['endstream'].count: 30 | return 0.5 31 | return 0.0 32 | 33 | def Instructions(self, score): 34 | if score == 1.0: 35 | return 'Sample is likely malicious and requires further analysis' 36 | 37 | if score == 0.75: 38 | return '/ObjStm detected, analyze sample with pdfid-objstm.bat' 39 | 40 | if score == 0.5: 41 | return 'Sample is likely not malicious but requires further analysis' 42 | 43 | if score == 0.6: 44 | return 'Sample is likely not malicious but could contain phishing or payload URL' 45 | 46 | if score == 0.0: 47 | return 'Sample is likely not malicious, unless you suspect this is used in a targeted/sophisticated attack' 48 | 49 | return '' 50 | 51 | AddPlugin(cPDFiDTriage) 52 | -------------------------------------------------------------------------------- /scripts/pdf_analytics.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Uses pdf-parser.py to display stats of keywords in a PDF. 3 | """ 4 | from deject.plugins import Deject 5 | from pathlib import Path 6 | from scripts.helpers import helpers 7 | 8 | 9 | @Deject.plugin 10 | def pdf_analytics(): 11 | """Use pdf-parser to search keywords, this uses "-a -O".""" 12 | script = Path("./scripts/pdf-tools/pdf-parser.py") 13 | filename = Deject.file_path 14 | helpers.bin_exec(helpers, ["python", script, "-a", "-O", filename]) 15 | return 16 | 17 | 18 | def help(): 19 | print(""" 20 | PDF Analytics Plugin 21 | SYNOPSIS 22 | Uses pdf-parser with the '-a -O' arguments to list stats of keywords used in a PDF. 23 | This plugin takes no additional arguments. 24 | """) 25 | -------------------------------------------------------------------------------- /scripts/pdf_image.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Uses pdf2image to save the PDF as a JPEG. 3 | """ 4 | from deject.plugins import Deject 5 | from pathlib import Path 6 | from scripts.helpers import helpers 7 | import os 8 | 9 | 10 | @Deject.plugin 11 | def pdf_image(): 12 | """Convert a PDF to an image using the pdftoimage file. Arguments are [output].""" 13 | script = Path("./scripts/pdf-tools/pdftojpg.py") 14 | filename = Deject.file_path 15 | filepath = os.path.dirname(Deject.file_path) 16 | if Deject.plugin_args == "False": 17 | helpers.bin_exec(helpers, ["python", script, filename, filepath]) 18 | print(f"[+] Saved image to {filepath}") 19 | else: 20 | args = str(Deject.plugin_args).split(" ") 21 | helpers.bin_exec(helpers, ["python", script, filename, args[0]]) 22 | print(f"[+] Saved image to {args[0]}") 23 | return 24 | 25 | 26 | def help(): 27 | print(""" 28 | PDF Image plugin 29 | SYNOPSIS [output] 30 | Uses pdftojpg.py to convert a PDF to a JPEG file. 31 | If an output location is provided, the files are saved in that location. 32 | Default is saving the images in the same location as the file. 33 | """) 34 | -------------------------------------------------------------------------------- /scripts/pdf_modified.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Uses pdftool.py to check if the PDF has been modified. 3 | @details Can also extract the modifications if the ID of the modification supplied. 4 | A filename where the modification is saved is optional.""" 5 | from deject.plugins import Deject 6 | from pathlib import Path 7 | import os 8 | from scripts.helpers import helpers 9 | 10 | 11 | @Deject.plugin 12 | def pdf_modified(): 13 | """Check if a PDF file has been modified. Dump the modification with " [filename]".""" 14 | script = Path("./scripts/pdf-tools/pdftool.py") 15 | filename = Deject.file_path 16 | args = "iu" 17 | if Deject.plugin_args == "False": 18 | helpers.script_exec(helpers, script, filename, args) 19 | else: 20 | addargs = str(Deject.plugin_args).split(" ") 21 | select = int(addargs[0]) 22 | if len(addargs) == 1: 23 | helpers.bin_exec( 24 | helpers, ["python", script, args, f"-s {select}", filename], 25 | ) 26 | else: 27 | helpers.bin_exec( 28 | helpers, [ 29 | "python", script, args, f"-s {select}", "-o", os.path.expanduser( 30 | addargs[1].strip(), 31 | ), filename, 32 | ], 33 | ) 34 | return 35 | 36 | 37 | def help(): 38 | print(""" 39 | PDF Modified plugin 40 | SYNOPSIS [id] [output] 41 | Uses pdftool.py to check if the PDF has been modified. 42 | Without the ID number, all modifications for the PDF are displayed. 43 | Can also extract the modifications if the ID of the modification supplied. 44 | A filename where the modification is saved is optional. 45 | """) 46 | -------------------------------------------------------------------------------- /scripts/pdf_object.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Use pdf-parser to extract an object from a PDF. Run with object ID as the argument. 3 | """ 4 | from deject.plugins import Deject 5 | from pathlib import Path 6 | from scripts.helpers import helpers 7 | 8 | 9 | @Deject.plugin 10 | def pdf_object(): 11 | """Use pdf-parser to parse objects. Uses "-o -c -w" options.""" 12 | script = Path("./scripts/pdf-tools/pdf-parser.py") 13 | filename = Deject.file_path 14 | object = Deject.plugin_args 15 | helpers.bin_exec( 16 | helpers, [ 17 | "python", script, 18 | "-o", object, "-c", "-w", filename, 19 | ], 20 | ) 21 | return 22 | 23 | 24 | def help(): 25 | print(""" 26 | PDF Object plugin 27 | SYNOPSIS 28 | Uses pdf-parser.py to extract an object from the PDF file. 29 | Can also extract the modifications if the ID of the modification supplied. 30 | A filename where the modification is saved is optional. 31 | """) 32 | -------------------------------------------------------------------------------- /scripts/pdf_triage.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Runs PDFiD on a PDF file with the triage plugin. 3 | """ 4 | from deject.plugins import Deject 5 | from pathlib import Path 6 | from scripts.helpers import helpers 7 | 8 | 9 | @Deject.plugin 10 | def pdf_image(): 11 | """Runs PDFiD on a PDF file with the triage plugin.""" 12 | script = Path("./scripts/pdf-tools/pdfid.py") 13 | filename = Deject.file_path 14 | if Deject.plugin_args == "False": 15 | helpers.bin_exec( 16 | helpers, [ 17 | "python", script, '-p', 18 | './scripts/pdf-tools/plugin_triage', filename, 19 | ], 20 | ) 21 | else: 22 | args = str(Deject.plugin_args) 23 | helpers.bin_exec( 24 | helpers, 25 | ["python", script] + args.strip().split(" ") + [filename], 26 | ) 27 | return 28 | 29 | 30 | def help(): 31 | print(""" 32 | PDF Triage plugin 33 | SYNOPSIS [arguments] 34 | Uses PDFiD with triage plugin (default). Additional arguments can be passed to PDFiD or additional plugins can be used. 35 | Place additional plugins in ./scripts/pdf-tools/. 36 | """) 37 | -------------------------------------------------------------------------------- /scripts/pe_exports.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief List exports of a PE file. 3 | @details This could be useful if analysing a DLL. 4 | """ 5 | from deject.plugins import Deject 6 | 7 | 8 | @Deject.plugin 9 | def pe_exports(): 10 | """List exports in a PE file.""" 11 | exports = Deject.r2_handler.cmdj("iEj") 12 | if exports is None: 13 | print("No exports detected in the file, this might be a bug!") 14 | return 15 | rows = [] 16 | for exp in exports: 17 | rows.append([exp["name"]]) 18 | res = {"header": ["Name"], "rows": rows} 19 | 20 | return res 21 | 22 | 23 | def help(): 24 | print(""" 25 | PE Exports plugin 26 | SYNOPSIS 27 | Use Radare2/Rizin with the 'iEj' command to list exports in a PE file or memory dump. 28 | There are no additional arguments for this plugin. 29 | """) 30 | -------------------------------------------------------------------------------- /scripts/pe_hashes.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Get imphash, Rich hash, TLSH and ssdeep of executable and output Virus Total links. 3 | @details Can also support a memory dump with the base address of the memory region to extract 4 | in decimal. 5 | """ 6 | from deject.plugins import Deject 7 | import pefile 8 | import ssdeep 9 | from typer import secho, colors 10 | import tlsh 11 | from hashlib import md5 12 | 13 | 14 | def get_rich_header_hash(pe): 15 | if not hasattr(pe, "RICH_HEADER") or pe.RICH_HEADER is None: 16 | return "" 17 | 18 | return md5(pe.RICH_HEADER.clear_data).hexdigest() 19 | 20 | 21 | @Deject.plugin 22 | def pe_hashes(): 23 | """Get imphash, Rich hash, TLSH and ssdeep of executable (if given a memory dump, pass in the base address of memory to extract with Radare2)""" 24 | filename = Deject.file_path 25 | with open(filename, "rb") as f: 26 | data = f.read() 27 | try: 28 | pe = pefile.PE(data=data) 29 | except: 30 | base_addr = Deject.plugin_args 31 | if base_addr == "False": 32 | secho( 33 | "Please enter a decimal base address of a section to extract! Run:\npoetry run deject run --include pe_hashes /path/to/dump.dmp .", 34 | fg=colors.RED, 35 | ) 36 | return 37 | sections = Deject.r2_handler.cmdj("iSj") 38 | if sections is None: 39 | secho("No sections detected in the dump, this might be a bug!", fg=colors.RED) 40 | return 41 | sect = 0 42 | size = 0 43 | for section in sections: 44 | if section["vaddr"] == int(base_addr): 45 | sect, size = hex(int(section["vaddr"])), section["size"] 46 | if size == 0: 47 | secho( 48 | f"Section {base_addr} not found in dump! Try using inspect_mmaps if this was an injected process.", fg=colors.RED, 49 | ) 50 | return 51 | try: 52 | pe = bytes.fromhex(Deject.r2_handler.cmd(f"p8 {size} @{sect}")) 53 | pe = pefile.PE(data=pe) 54 | except Exception as err: 55 | secho(f"Failed to read PE file from dump: {err}", fg=colors.RED) 56 | return 57 | if hasattr(pe, "get_imphash"): 58 | imp = pe.get_imphash() 59 | if len(imp) > 0: 60 | print(f"https://www.virustotal.com/gui/search/imphash:{imp}") 61 | if hasattr(pe, "get_rich_header_hash"): 62 | header_hash = pe.get_rich_header_hash() 63 | if len(header_hash) > 0: 64 | print( 65 | f"https://www.virustotal.com/gui/search/rich_pe_header_hash:{header_hash}", 66 | ) 67 | else: 68 | header_hash = get_rich_header_hash(pe) 69 | if len(header_hash) > 0: 70 | print( 71 | f"https://www.virustotal.com/gui/search/rich_pe_header_hash:{header_hash}", 72 | ) 73 | print( 74 | f'https://www.virustotal.com/gui/search/ssdeep%253A%2522{ssdeep.hash(data).replace("/", "%252F").replace(":", "%253A").replace("+", "%252B")}%2522', 75 | ) 76 | print(f"https://www.virustotal.com/gui/search/tlsh:{tlsh.hash(data)}") 77 | 78 | 79 | def help(): 80 | print(""" 81 | PE Hashing Plugin 82 | SYNOPSIS [baseaddress] 83 | This plugin will printout VirusTotal URLs for Rich PE Header, SSDEEP and TLSH hashes. 84 | If the input is a MDMP file a base address for the executable is needed, in decimal format. 85 | The plugin does not require a Virus Total API key, as the Virus Total API is not used. 86 | """) 87 | -------------------------------------------------------------------------------- /scripts/pe_hashlookup.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Search hashes on CIRCL hashlookup. 3 | @details CIRCL hashlookup (https://circl.lu/services/hashlookup/) is used to check if a hash is known 4 | in a database. 5 | """ 6 | from deject.plugins import Deject 7 | import requests 8 | import hashlib 9 | 10 | # TODO: Do we want to keep requests or to change to the offline Bloom filter? 11 | # This could also be an option, with "offline" being an argument. 12 | 13 | 14 | @Deject.plugin 15 | def hashlookup(): 16 | """Use hashlookup.circl.lu to check if the sha1 hash of a file is known""" 17 | with open(Deject.file_path, "rb") as f: 18 | data = f.read() 19 | filehash = hashlib.sha1(data).hexdigest() 20 | r = requests.get(f'https://hashlookup.circl.lu/lookup/sha1/{filehash}') 21 | if r.status_code == 200: 22 | return f"{r.json()}" 23 | elif r.status_code == 404: 24 | return "Hash not found!" 25 | else: 26 | return "Hash format incorrect, this might be a bug." 27 | 28 | 29 | def help(): 30 | print(""" 31 | Hashlookup plugin 32 | SYNOPSIS 33 | 34 | This plugin uses hashlookup.circl.lu to lookup a SHA1 hash of the file passed to Deject. 35 | There are no additional arguments for this plugin. 36 | """) 37 | -------------------------------------------------------------------------------- /scripts/pe_imports.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Uses radare/rizin to extract imports for a PE file. 3 | """ 4 | from deject.plugins import Deject 5 | from typer import secho, colors 6 | 7 | 8 | @Deject.plugin 9 | def pe_imports(): 10 | """List imports in a PE file.""" 11 | imports = Deject.r2_handler.cmdj("iij") 12 | if imports is None: 13 | secho("No imports detected in the file, this might be a bug!", fg=colors.RED) 14 | return 15 | rows = [] 16 | for imp in imports: 17 | try: 18 | rows.append([imp["name"], imp["libname"]]) 19 | except: 20 | rows.append([imp["name"]]) 21 | res = {"header": ["Name", "Library"], "rows": rows} 22 | 23 | return res 24 | 25 | 26 | def help(): 27 | print(""" 28 | PE Imports plugin 29 | SYNOPSIS 30 | Use Radare2/Rizin with the 'iij' command to list imports in a PE file or memory dump. 31 | There are no additional arguments for this plugin. 32 | """) 33 | -------------------------------------------------------------------------------- /scripts/pe_packed.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Check the PE file to see if it has packed sections. 3 | """ 4 | from deject.plugins import Deject 5 | from typer import secho, colors 6 | 7 | 8 | @Deject.plugin 9 | def pe_packed(): 10 | """Run checks against the PE file to see if sections are packed""" 11 | sections = Deject.r2_handler.cmdj("iSj entropy") 12 | rows = [] 13 | secho("Checking if packed...", fg=colors.BRIGHT_BLUE) 14 | for section in sections.get('sections'): 15 | packed = "" 16 | if section.get('entropy') is not None: 17 | if float(section.get('entropy')) > 6: 18 | packed = "X" 19 | if Deject.quiet: 20 | rows.append( 21 | [section.get("name"), section.get("entropy"), packed], 22 | ) 23 | if not Deject.quiet: 24 | rows.append([section.get("name"), section.get("entropy"), packed]) 25 | res = {"header": ["Section", "Entropy", "Packed"], "rows": rows} 26 | return res 27 | 28 | 29 | def help(): 30 | print(""" 31 | PE Packed plugin 32 | SYNOPSIS 33 | Reads a PE file and checks if the sections in the file are packed. 34 | This plugin takes no additional arguments. 35 | """) 36 | -------------------------------------------------------------------------------- /scripts/pe_parser.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief uses Kaitai (https://github.com/kaitai-io/kaitai_struct_compiler) to parse PE files. 3 | @details Can supply a section name to extract out of the PE. 4 | By default runs pe_parser_misc() 5 | """ 6 | from deject.plugins import Deject 7 | from scripts.extractors.kaitai.microsoft_pe import MicrosoftPe 8 | from datetime import datetime, timezone 9 | from enum import Flag 10 | import ssdeep 11 | import binascii 12 | import os 13 | 14 | 15 | class PeCharact(Flag): 16 | IMAGE_FILE_RELOCS_STRIPPED = 0x1 17 | IMAGE_FILE_EXECUTABLE_IMAGE = 0x2 18 | IMAGE_FILE_LINE_NUMS_STRIPPED = 0x4 19 | IMAGE_FILE_LOCAL_SYMS_STRIPPED = 0x8 20 | IMAGE_FILE_AGGRESSIVE_WS_TRIM = 0x10 21 | IMAGE_FILE_LARGE_ADDRESS_AWARE = 0x20 22 | IMAGE_FILE_BYTES_REVERSED_LO = 0x80 23 | IMAGE_FILE_32BIT_MACHINE = 0x100 24 | IMAGE_FILE_DEBUG_STRIPPED = 0x200 25 | IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP = 0x400 26 | IMAGE_FILE_NET_RUN_FROM_SWAP = 0x800 27 | IMAGE_FILE_SYSTEM = 0x1000 28 | IMAGE_FILE_DLL = 0x2000 29 | IMAGE_FILE_UP_SYSTEM_ONLY = 0x4000 30 | IMAGE_FILE_BYTES_REVERSED_HI = 0x8000 31 | 32 | 33 | class SectCharact(Flag): 34 | IMAGE_SCN_TYPE_NO_PAD = 0x8 35 | IMAGE_SCN_CNT_CODE = 0x20 36 | IMAGE_SCN_CNT_INITIALIZED_DATA = 0x40 37 | IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x80 38 | IMAGE_SCN_LNK_OTHER = 0x100 39 | IMAGE_SCN_LNK_INFO = 0x200 40 | IMAGE_SCN_LNK_REMOVE = 0x800 41 | IMAGE_SCN_LNK_COMDAT = 0x1000 42 | IMAGE_SCN_GPREL = 0x8000 43 | IMAGE_SCN_MEM_PURGEABLE = 0x20000 44 | IMAGE_SCN_MEM_16BIT = 0x20000 45 | IMAGE_SCN_MEM_LOCKED = 0x40000 46 | IMAGE_SCN_MEM_PRELOAD = 0x80000 47 | IMAGE_SCN_ALIGN_1BYTES = 0x100000 48 | IMAGE_SCN_ALIGN_2BYTES = 0x200000 49 | IMAGE_SCN_ALIGN_4BYTES = 0x300000 50 | IMAGE_SCN_ALIGN_8BYTES = 0x400000 51 | IMAGE_SCN_ALIGN_16BYTES = 0x500000 52 | IMAGE_SCN_ALIGN_32BYTES = 0x600000 53 | IMAGE_SCN_ALIGN_64BYTES = 0x700000 54 | IMAGE_SCN_ALIGN_128BYTES = 0x800000 55 | IMAGE_SCN_ALIGN_256BYTES = 0x900000 56 | IMAGE_SCN_ALIGN_512BYTES = 0xA00000 57 | IMAGE_SCN_ALIGN_1024BYTES = 0xB00000 58 | IMAGE_SCN_ALIGN_2048BYTES = 0xC00000 59 | IMAGE_SCN_ALIGN_4096BYTES = 0xD00000 60 | IMAGE_SCN_ALIGN_8192BYTES = 0xE00000 61 | IMAGE_SCN_LNK_NRELOC_OVFL = 0x1000000 62 | IMAGE_SCN_MEM_DISCARDABLE = 0x2000000 63 | IMAGE_SCN_MEM_NOT_CACHED = 0x4000000 64 | IMAGE_SCN_MEM_NOT_PAGED = 0x8000000 65 | IMAGE_SCN_MEM_SHARED = 0x10000000 66 | IMAGE_SCN_MEM_EXECUTE = 0x20000000 67 | IMAGE_SCN_MEM_READ = 0x40000000 68 | IMAGE_SCN_MEM_WRITE = 0x80000000 69 | 70 | 71 | class DllCharact(Flag): 72 | IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA = 0x20 73 | IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE = 0x40 74 | IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY = 0x80 75 | IMAGE_DLLCHARACTERISTICS_NX_COMPAT = 0x100 76 | IMAGE_DLLCHARACTERISTICS_NO_ISOLATION = 0x200 77 | IMAGE_DLLCHARACTERISTICS_NO_SEH = 0x400 78 | IMAGE_DLLCHARACTERISTICS_NO_BIND = 0x800 79 | IMAGE_DLLCHARACTERISTICS_APPCONTAINER = 0x1000 80 | IMAGE_DLLCHARACTERISTICS_WDM_DRIVER = 0x2000 81 | IMAGE_DLLCHARACTERISTICS_GUARD_CF = 0x4000 82 | IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE = 0x8000 83 | 84 | 85 | @Deject.plugin 86 | def pe_parser(): 87 | """Used to parse information from a PE file""" 88 | data = MicrosoftPe.from_file(Deject.file_path) 89 | args = str(Deject.plugin_args).split(" ") 90 | match args[0]: 91 | case "extract": 92 | result = pe_parser_extract(data, args[1]) 93 | case "certificate": 94 | result = pe_parser_certificate(data) 95 | case "sections": 96 | result = pe_parser_sections(data) 97 | case "datadirs": 98 | result = pe_parser_datadirs(data) 99 | case "extract_datadir": 100 | result = pe_parser_extract_datadir(data, args[1]) 101 | case _: 102 | result = pe_parser_misc(data) 103 | return result 104 | 105 | 106 | def pe_parser_misc(data): 107 | """Parses information from a PE file""" 108 | rows = [] 109 | try: 110 | rows.append(["Architecture", data.pe.coff_hdr.machine]) 111 | rows.append(["PE Format", data.pe.optional_hdr.std.format]) 112 | rows.append([ 113 | "PE Characteristics (raw)", hex( 114 | data.pe.coff_hdr.characteristics, 115 | ), 116 | ]) 117 | characts = pe_parser_pe_char_lookup(data.pe.coff_hdr.characteristics) 118 | rows.append(["PE Characteristics", "\n".join(characts)]) 119 | rows.append(["Subsystem", data.pe.optional_hdr.windows.subsystem]) 120 | rows.append([ 121 | "Timestamp", datetime.fromtimestamp( 122 | data.pe.coff_hdr.time_date_stamp, timezone.utc, 123 | ).strftime("%Y-%m-%d %H:%M:%S"), 124 | ]) 125 | rows.append([ 126 | "DLL Characteristics (raw)", hex( 127 | data.pe.optional_hdr.windows.dll_characteristics, 128 | ), 129 | ]) 130 | dllcharacts = pe_parser_dll_char_lookup( 131 | data.pe.optional_hdr.windows.dll_characteristics, 132 | ) 133 | rows.append(["DLL Characteristics", "\n".join(dllcharacts)]) 134 | if image64 := getattr(data.pe.optional_hdr.windows, "image_base_64", False): 135 | rows.append(["Base Address", hex(image64)]) 136 | else: 137 | rows.append([ 138 | "Base Address", hex( 139 | data.pe.optional_hdr.windows.image_base_32, 140 | ), 141 | ]) 142 | rows.append( 143 | [ 144 | "Entrypoint", hex( 145 | data.pe.optional_hdr.std.address_of_entry_point, 146 | ), 147 | ], 148 | ) 149 | rows.append([ 150 | "Base of Code", hex( 151 | data.pe.optional_hdr.std.base_of_code, 152 | ), 153 | ]) 154 | if baseData := getattr(data.pe.optional_hdr.std, "base_of_data", False): 155 | rows.append(["Base of Data", hex(baseData)]) 156 | if isinstance(data.pe.certificate_table, data.CertificateTable): 157 | for cert in data.pe.certificate_table.items: 158 | rows.append(["Type", cert.certificate_type]) 159 | rows.append(["Revision", cert.revision]) 160 | rows.append(["Length", cert.length]) 161 | except AttributeError: 162 | pass 163 | t = {"header": ["Key", "Value"], "rows": rows} 164 | return t 165 | 166 | 167 | def pe_parser_extract(data, args): 168 | """Extracts sections from a PE file""" 169 | for section in data.pe.sections: 170 | if section.name == args or section.name == f".{args}": 171 | return section.body.decode(u"UTF-8", errors="ignore") 172 | 173 | 174 | def pe_parser_certificate(data): 175 | """Extracts a certificate from a PE file""" 176 | for cert in data.pe.certificate_table.items: 177 | return cert.certificate_bytes 178 | 179 | 180 | def pe_parser_datadirs(data): 181 | """Get DataDir information from a PE file""" 182 | rows = [] 183 | for ddir in [ 184 | 'architecture', 'base_relocation_table', 'bound_import', 'certificate_table', 'clr_runtime_header', 'debug', 'delay_import_descriptor', 185 | 'exception_table', 'export_table', 'global_ptr', 'iat', 'import_table', 'load_config_table', 'resource_table', 'tls_table', 186 | ]: 187 | rows.append([ 188 | ddir, getattr(data.pe.optional_hdr.data_dirs, ddir).size, hex( 189 | getattr(data.pe.optional_hdr.data_dirs, ddir).virtual_address, 190 | ), 191 | ]) 192 | t = {"header": ["DataDir", "Size", "Virtual Address"], "rows": rows} 193 | return t 194 | 195 | 196 | def pe_parser_extract_datadir(data, datadir): 197 | if hasattr(data.pe.optional_hdr.windows, "image_base_64"): 198 | base = int(data.pe.optional_hdr.windows.image_base_64) 199 | else: 200 | base = int(data.pe.optional_hdr.windows.image_base_32) 201 | content = Deject.r2_handler.cmd( 202 | "p8 {} @ {}".format( 203 | getattr(data.pe.optional_hdr.data_dirs, datadir).size, hex( 204 | base + int( 205 | getattr( 206 | data.pe.optional_hdr.data_dirs, 207 | datadir, 208 | ).virtual_address, 209 | ), 210 | ), 211 | ), 212 | ) 213 | sshash = ssdeep.hash(binascii.unhexlify(content.strip())) 214 | filepath = os.path.dirname(Deject.file_path) 215 | f = open(filepath + "/" + datadir, "wb") 216 | f.write(binascii.unhexlify(content.strip())) 217 | f.close() 218 | return f"SSDEEP Hash: {sshash}" + \ 219 | f"[+] Saved output of {datadir} to {filepath}/{datadir}" 220 | 221 | 222 | def pe_parser_sections(data): 223 | """Extracts section information from a PE file""" 224 | rows = [] 225 | for section in data.pe.sections: 226 | rows.append(["Section", section.name]) 227 | rows.append(["Virtual Size", section.virtual_size]) 228 | rows.append(["Raw Data Size", section.size_of_raw_data]) 229 | rows.append(["Virtual Address", hex(section.virtual_address)]) 230 | rows.append([ 231 | "Section Characteristics (raw)", 232 | hex(section.characteristics), 233 | ]) 234 | rows.append([ 235 | "Section Characteristics", "\n".join( 236 | pe_parser_sect_char_lookup(section.characteristics), 237 | ), 238 | ]) 239 | t = {"header": ["Key", "Value"], "rows": rows} 240 | return t 241 | 242 | 243 | def pe_parser_pe_char_lookup(data): 244 | characteristics = [] 245 | for i in [e.value for e in PeCharact]: 246 | try: 247 | if data & i > 0: 248 | characteristics.append(PeCharact(data & i).name) 249 | except ValueError: 250 | continue 251 | return characteristics 252 | 253 | 254 | def pe_parser_sect_char_lookup(data): 255 | characteristics = [] 256 | for i in [e.value for e in SectCharact]: 257 | try: 258 | if data & i > 0: 259 | characteristics.append(SectCharact(data & i).name) 260 | except ValueError: 261 | continue 262 | return characteristics 263 | 264 | 265 | def pe_parser_dll_char_lookup(data): 266 | characteristics = [] 267 | for i in [e.value for e in DllCharact]: 268 | try: 269 | if data & i > 0: 270 | characteristics.append(DllCharact(data & i).name) 271 | except ValueError: 272 | continue 273 | return characteristics 274 | 275 | 276 | def help(): 277 | print(""" 278 | PE Parser plugin 279 | 280 | SYNOPSIS "[options]" 281 | 282 | Uses the PE parser from Kaitai to read information from a PE file. 283 | If extract and a section name is added, extract data from that section (enclose in quotes). 284 | Specifyng "certificate" as an option will extract the certificate, if available in the PE file. 285 | """) 286 | -------------------------------------------------------------------------------- /scripts/pe_sections.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief This plugin handles sections of a PE file and can carve or show only abnormal sections. 3 | @details Without arguments, this plugin will show all sections of a PE file and generate an SSDEEP hash of the section. 4 | Carving of sections is also possible by running the plugin with the arguments "carve ", for example 5 | poetry run deject run --include pe_sections \ "carve .text". The carved section is saved to the same 6 | location as the file. 7 | """ 8 | from deject.plugins import Deject 9 | import ssdeep 10 | import binascii 11 | import os 12 | from typer import secho, colors 13 | 14 | # list of normal sections 15 | NORMALSEC = [ 16 | ".text", ".pdata", ".rsrc", ".reloc", ".data", ".rdata", 17 | ".bss", ".sbss", ".sdata", ".sxdata", ".tls", ".xdata", ".vsdata", 18 | ] 19 | 20 | 21 | @Deject.plugin 22 | def pe_sections(): 23 | """Check the sections of a PE file and give a fuzzy hash of the section. 24 | Use "carve " to carve a section to disk.""" 25 | rows = [] 26 | sections = Deject.r2_handler.cmdj("iSj") 27 | args = Deject.plugin_args.split(" ") 28 | if args[0] == "carve": 29 | for sec in sections: 30 | if sec['name'] == args[1]: 31 | content = Deject.r2_handler.cmd( 32 | "p8 {} @ {}".format(sec['size'], sec['vaddr']), 33 | ) 34 | sshash = ssdeep.hash(binascii.unhexlify(content.strip())) 35 | filepath = os.path.dirname(Deject.file_path) 36 | f = open(filepath + "/" + sec["name"], "wb") 37 | f.write(binascii.unhexlify(content.strip())) 38 | f.close() 39 | secho( 40 | f"[+] Saved section {sec['name']} to {filepath}", fg=colors.GREEN, 41 | ) 42 | rows.append([ 43 | sec['name'], hex(sec['vaddr']), 44 | hex(sec['vsize']), sshash, 45 | ]) 46 | else: 47 | for sec in sections: 48 | if Deject.quiet: 49 | if sec['name'] in NORMALSEC: 50 | continue 51 | content = Deject.r2_handler.cmd( 52 | "p8 {} @ {}".format(sec['size'], sec['vaddr']), 53 | ) 54 | sshash = ssdeep.hash(binascii.unhexlify(content.strip())) 55 | rows.append([ 56 | sec['name'], hex(sec['vaddr']), 57 | hex(sec['vsize']), sshash, 58 | ]) 59 | res = {"header": ["Name", "vaddr", "size", "ssdeep"], "rows": rows} 60 | return res 61 | 62 | 63 | def help(): 64 | print(""" 65 | PE Sections plugin 66 | SYNOPSIS "carve " 67 | Shows all sections of a PE file. 68 | Quiet mode will only return abnormal section names. 69 | If "carve " is passed, the section will be carved out of the PE file. 70 | Remember the quotes. 71 | SSDEEP hashes for the sections are also returned. 72 | """) 73 | -------------------------------------------------------------------------------- /scripts/pe_signatures.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Get Digital Signatures from a PE file and display using OpenSSL. 3 | """ 4 | from deject.plugins import Deject 5 | import binascii 6 | from scripts.helpers import helpers 7 | import tempfile 8 | 9 | 10 | @Deject.plugin 11 | def pe_signatures(): 12 | """Get digital signatures out of PE files""" 13 | filename = Deject.file_path 14 | info = Deject.r2_handler.cmdj("ij") 15 | bits = info["bin"]["bits"] 16 | Deject.r2_handler.cmd(f"on {filename}") 17 | if bits == 64: 18 | size = Deject.r2_handler.cmd( 19 | "pfv.pe_nt_image_headers64.optionalHeader.dataDirectory[4].size @ pe_nt_image_headers64", 20 | ) 21 | virtualaddress = Deject.r2_handler.cmd( 22 | "pfv.pe_nt_image_headers64.optionalHeader.dataDirectory[4].virtualAddress @ pe_nt_image_headers64", 23 | ) 24 | else: 25 | size = Deject.r2_handler.cmd( 26 | "pfv.pe_nt_image_headers32.optionalHeader.dataDirectory[4].size @ pe_nt_image_headers32", 27 | ) 28 | virtualaddress = Deject.r2_handler.cmd( 29 | "pfv.pe_nt_image_headers32.optionalHeader.dataDirectory[4].virtualAddress @ pe_nt_image_headers32", 30 | ) 31 | output = Deject.r2_handler.cmd( 32 | f"p8 {size.strip()} @ {virtualaddress.strip()}+8", 33 | ) 34 | with tempfile.NamedTemporaryFile(mode="wb") as cert: 35 | cert.write(binascii.unhexlify(output.strip())) 36 | helpers.bin_exec( 37 | helpers, [ 38 | "openssl", "pkcs7", "-inform", "DER", "-print_certs", "-text", "-in", cert.name, 39 | ], 40 | ) 41 | Deject.r2_handler.cmd(f"o {filename}") 42 | 43 | 44 | def help(): 45 | print(""" 46 | PE Signatures plugin 47 | SYNOPSIS 48 | Extracts digital signatures from a PE file and uses OpenSSL to display them. 49 | """) 50 | -------------------------------------------------------------------------------- /scripts/poshc2_check.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Extract PoshC2 (https://github.com/nettitude/PoshC2) configurations from memory dumps or PE files. 3 | @details If a file or memory dump is expected to be PoshC2, run this plugin to extract the configuration. 4 | This uses poshc2parser.py and Chepy (https://github.com/securisec/chepy). 5 | """ 6 | from deject.plugins import Deject 7 | from pathlib import Path 8 | from scripts.helpers import helpers 9 | import warnings 10 | warnings.filterwarnings( 11 | "ignore", r"invalid escape sequence", category=SyntaxWarning, 12 | ) 13 | 14 | 15 | @Deject.plugin 16 | def poshc2_check(): 17 | """Check if the dump corresponds to a PoshC2 injected one and extract the config""" 18 | script = Path("./scripts/extractors/poshc2Parser/poshc2parser.py") 19 | filename = Deject.file_path 20 | arg = "-f" 21 | helpers.script_exec(helpers, script, filename, arg) 22 | 23 | 24 | def help(): 25 | print(""" 26 | PoshC2 Check plugin 27 | SYNOPSIS 28 | Run the PoshC2 parser on a memory dump or PE file and extract the configuration. 29 | The plugin uses the Chepy module. 30 | This plugin takes no additional arguments. 31 | """) 32 | -------------------------------------------------------------------------------- /scripts/test_plugin.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief A test plugin to show how a new plugin can be created. 3 | """ 4 | from deject.plugins import Deject 5 | 6 | #@Deject.plugin 7 | def hash_something(): 8 | """Function does nothing, this is just a docstr for demonstration purposes.""" 9 | print("Hashing something") 10 | t = {"header": ["first", "second", "third"], "rows": [["a", "b", "c"], [1, 2, 3], [4, 5, 6]]} 11 | return t 12 | -------------------------------------------------------------------------------- /scripts/yarascan.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Use yara-python to scan a file or memory dump. 3 | """ 4 | 5 | from pathlib import Path 6 | from deject.plugins import Deject 7 | from scripts.helpers import helpers, Settings 8 | import os 9 | 10 | rules_path = Path("./scripts/yara-rules/yara/rules") 11 | 12 | 13 | @Deject.plugin 14 | def run_yara(): 15 | """Run a set of yara rules on the memory dump.""" 16 | yara_dir = Settings().getSetting("yara_rule") 17 | rule_path = rules_path 18 | if yara_dir != "": 19 | rule_path = Path(os.path.expanduser(yara_dir)) 20 | matches = [] 21 | 22 | for rule in rule_path.iterdir(): 23 | if rule.suffix == ".yara" or rule.suffix == ".yar": 24 | rule = str(rule) 25 | try: 26 | match = helpers.yara_exec(helpers, rule, Deject.file_path) 27 | except: 28 | raise Exception(f"Error in rule {rule}") 29 | if len(match) > 0: 30 | matches.append(match) 31 | 32 | if len(matches) != 0: 33 | return matches 34 | 35 | 36 | def help(): 37 | print(""" 38 | Yara Scan plugin 39 | SYNOPSIS 40 | Run Yara Python on a file. By default this uses rules located in the yara-rules directory. 41 | To change the rules used during the scan, set the RULES environment variable to where the 42 | .yar files reside. 43 | This plugin has no additional arguments. 44 | """) 45 | -------------------------------------------------------------------------------- /scripts/zeek.py: -------------------------------------------------------------------------------- 1 | """! 2 | @brief Run Zeek over a PCAP file to extract information 3 | @details Zeek output is placed in the same location as the pcap file. 4 | Arguments can be passed to the Zeek binary by placing them after the file 5 | name, in quotes. For example, 6 | poetry run deject run --include zeek test.pcap " -C " will run zeek with checksums 7 | turned off. 8 | Download Zeek and link or copy the binary to bin/ in Deject's root directory. 9 | """ 10 | from deject.plugins import Deject 11 | import os 12 | from scripts.helpers import helpers, Settings 13 | 14 | 15 | @Deject.plugin 16 | def zeek(): 17 | """Use zeek on a PCAP.""" 18 | filename = Deject.file_path 19 | filepath = os.path.dirname(Deject.file_path) 20 | zeek = f"{os.path.dirname(os.path.realpath(__file__))}/../bin/zeek" 21 | if not os.path.exists(zeek): 22 | if Settings().getSetting("zeek_path"): 23 | zeek = Settings().getSetting("zeek_path") 24 | else: 25 | zeek = "zeek" 26 | process = [zeek, "-r", filename, f"Log::default_logdir={filepath}"] 27 | args = Deject.plugin_args 28 | if args == "False": 29 | helpers.bin_exec(helpers, process) 30 | else: 31 | helpers.bin_exec(helpers, process + args.strip().split(" ")) 32 | return f"[+] Zeek output for {filename} has been saved to {filepath}" 33 | 34 | 35 | def help(): 36 | print(""" 37 | Zeek plugin 38 | SYNOPSIS [arguments] 39 | Arguments are passed to the Zeek binary. 40 | 41 | To run the help for the Zeek binary, run poetry run deject run --include zeek test.pcap " --help ". 42 | 43 | Zeek output is placed in the same location as the pcap file. 44 | Arguments can be passed to the Zeek binary by placing them after the file 45 | name, in quotes. For example, 46 | poetry run deject run --include zeek test.pcap " -C " will run zeek with checksums 47 | turned off. 48 | Download Zeek and link or copy the binary to bin/ in Deject's root directory. 49 | """) 50 | -------------------------------------------------------------------------------- /settings.yml: -------------------------------------------------------------------------------- 1 | env_variables: 2 | vt_key: VT_KEY 3 | yara_rule: RULES 4 | zeek_path: ZEEK_PATH 5 | bulk_path: BULK_PATH 6 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo apt update && sudo apt upgrade -y && sudo apt install autoconf automake flex gcc g++ libssl-dev zlib1g-dev libexpat1-dev libxml2-dev dpkg-dev openssl patch wget bison git libewf-dev -y 3 | git clone --recursive https://github.com/simsong/bulk_extractor.git 4 | pushd bulk_extractor && ./bootstrap.sh && ./configure && make 5 | popd 6 | mv bulk_extractor/src/bulk_extractor bin/ 7 | sudo apt update ; apt upgrade -y 8 | sudo apt install git curl build-essential libffi-dev python3 python3-dev python3-pip libtool libssl-dev swig libfuzzy-dev libexpat1 -y 9 | echo 'deb http://download.opensuse.org/repositories/security:/zeek/xUbuntu_22.04/ /' | sudo tee /etc/apt/sources.list.d/security:zeek.list 10 | curl -fsSL https://download.opensuse.org/repositories/security:zeek/xUbuntu_22.04/Release.key | sudo gpg --dearmor > /etc/apt/trusted.gpg.d/security_zeek.gpg 11 | sudo apt update ; sudo apt install zeek -y 12 | curl -fsSL https://github.com/radareorg/radare2/releases/download/5.8.8/radare2_5.8.8_amd64.deb -o radare2_5.8.8_amd64.deb 13 | sudo dpkg -i radare2_5.8.8_amd64.deb 14 | curl -fsSL https://raw.githubusercontent.com/python-poetry/install.python-poetry.org/main/install-poetry.py | python3 15 | ln -s /opt/zeek/bin/zeek bin/zeek 16 | poetry install --compile 17 | -------------------------------------------------------------------------------- /tests/data/hello: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WithSecureLabs/deject/286751faa1791ae7f8ca74c205e94b28b474b4a0/tests/data/hello -------------------------------------------------------------------------------- /tests/data/hello.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WithSecureLabs/deject/286751faa1791ae7f8ca74c205e94b28b474b4a0/tests/data/hello.exe -------------------------------------------------------------------------------- /tests/data/hello.macho: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WithSecureLabs/deject/286751faa1791ae7f8ca74c205e94b28b474b4a0/tests/data/hello.macho -------------------------------------------------------------------------------- /tests/data/hello.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WithSecureLabs/deject/286751faa1791ae7f8ca74c205e94b28b474b4a0/tests/data/hello.pdf -------------------------------------------------------------------------------- /tests/test_elf.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import deject.main 3 | 4 | def test_elf_hashes(): 5 | assert deject.main.run("tests/data/hello","","","",["*"],["elf_hashes"],"") == 0 6 | 7 | def test_elf_imports(): 8 | assert deject.main.run("tests/data/hello","","","",["*"],["elf_imports"],"") == 0 9 | 10 | def test_elf_parser(): 11 | assert deject.main.run("tests/data/hello","","","",["*"],["elf_parser"],"") == 0 12 | -------------------------------------------------------------------------------- /tests/test_file_type.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import deject.main 3 | 4 | def test_file_type_elf(): 5 | assert deject.main.run("tests/data/hello","elf","","",[],[],"") == 0 6 | 7 | def test_file_type_pe(): 8 | assert deject.main.run("tests/data/hello.exe","pe","","",[],[],"") == 0 9 | 10 | def test_file_type_pdf(): 11 | assert deject.main.run("tests/data/hello.pdf","pdf","","",[],[],"False") == 0 12 | -------------------------------------------------------------------------------- /tests/test_help.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import deject.main 3 | 4 | def test_help(): 5 | for script in deject.main.scripts.names(): 6 | assert deject.main.help(script) == 0 7 | -------------------------------------------------------------------------------- /tests/test_list.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import deject.main 3 | 4 | def test_list_bofs(): 5 | assert deject.main.run("tests/data/hello.exe","","","",["*"],["list_bofs"],"") == 0 6 | 7 | def test_list_dlls(): 8 | assert deject.main.run("tests/data/hello.exe","","","",["*"],["list_dlls"],"") == 0 9 | 10 | def test_list_exes(): 11 | assert deject.main.run("tests/data/hello.exe","","","",["*"],["list_exes"],"") == 0 12 | 13 | def test_list_libs(): 14 | assert deject.main.run("tests/data/hello.exe","","","",["*"],["list_libs"],"") == 0 15 | -------------------------------------------------------------------------------- /tests/test_macho.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import deject.main 3 | 4 | def test_macho_parser(): 5 | assert deject.main.run("tests/data/hello.macho","","","",["*"],["macho_parser"],"False") == 0 6 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import deject.main 3 | 4 | def test_main(): 5 | assert deject.main.run("tests/data/hello","","","",[],[],"") == 0 6 | -------------------------------------------------------------------------------- /tests/test_parsers.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import deject.main 3 | 4 | def test_cobaltstrike_check(): 5 | assert deject.main.run("tests/data/hello","","","",["*"],["cobaltstrike_check"],"") == 0 6 | 7 | def test_poshc2_check(): 8 | assert deject.main.run("tests/data/hello","","","",["*"],["poshc2_check"],"") == 0 9 | 10 | def test_c3_check(): 11 | assert deject.main.run("tests/data/hello","","","",["*"],["c3_check"],"") == 0 12 | 13 | def test_malwareconfigextract_check(): 14 | assert deject.main.run("tests/data/hello.exe","","","",["*"],["malwareconfigextract"],"") == 0 -------------------------------------------------------------------------------- /tests/test_pdf.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import deject.main 3 | 4 | def test_pdf_modified(): 5 | assert deject.main.run("tests/data/hello.pdf","","","",["*"],["pdf_modified"],"False") == 0 6 | 7 | def test_pdf_triage(): 8 | assert deject.main.run("tests/data/hello.pdf","","","",["*"],["pdf_triage"],"False") == 0 9 | 10 | def test_pdf_analytics(): 11 | assert deject.main.run("tests/data/hello.pdf","","","",["*"],["pdf_analytics"],"False") == 0 12 | -------------------------------------------------------------------------------- /tests/test_pe.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import deject.main 3 | 4 | def test_pe_hashes(): 5 | assert deject.main.run("tests/data/hello.exe","","","",["*"],["pe_hashes"],"") == 0 6 | 7 | def test_pe_imports(): 8 | assert deject.main.run("tests/data/hello.exe","","","",["*"],["pe_imports"],"") == 0 9 | 10 | def test_pe_sections(): 11 | assert deject.main.run("tests/data/hello.exe","","","",["*"],["pe_sections"],"") == 0 12 | 13 | def test_pe_checks(): 14 | assert deject.main.run("tests/data/hello.exe","","","",["*"],["pe_packed"],"") == 0 15 | 16 | def test_pe_hashlookup(): 17 | assert deject.main.run("tests/data/hello.exe","","","",["*"],["pe_hashlookup"],"") == 0 18 | 19 | def test_pe_exports(): 20 | assert deject.main.run("tests/data/hello.exe","","","",["*"],["pe_exports"],"") == 0 21 | 22 | def test_pe_signatures(): 23 | assert deject.main.run("tests/data/hello.exe","","","",["*"],["pe_signatures"],"") == 0 24 | 25 | def test_pe_parser(): 26 | assert deject.main.run("tests/data/hello.exe","","","",["*"],["pe_parser"],"") == 0 -------------------------------------------------------------------------------- /tests/test_yara.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import deject.main 3 | 4 | def test_yarascan(): 5 | assert deject.main.run("tests/data/hello","","","",["*"],["yarascan"],"") == 0 6 | --------------------------------------------------------------------------------