Repository: ReFirmLabs/binwalk Branch: master Commit: a417b4dcf742 Files: 270 Total size: 828.3 KB Directory structure: gitextract_b67t48qd/ ├── .dockerignore ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── quality.yaml │ └── rust-release-binary.yml ├── .gitignore ├── CARGO_README.md ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── build_docker.sh ├── dependencies/ │ ├── README.md │ ├── pip.sh │ ├── requirements.txt │ ├── src.sh │ └── ubuntu.sh ├── fuzzing/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ └── main.rs ├── scripts/ │ └── binwalk-ui ├── src/ │ ├── binwalk.rs │ ├── cliparser.rs │ ├── common.rs │ ├── display.rs │ ├── entropy.rs │ ├── extractors/ │ │ ├── androidsparse.rs │ │ ├── arcadyan.rs │ │ ├── autel.rs │ │ ├── bmp.rs │ │ ├── bzip2.rs │ │ ├── cab.rs │ │ ├── common.rs │ │ ├── csman.rs │ │ ├── dahua_zip.rs │ │ ├── dmg.rs │ │ ├── dtb.rs │ │ ├── dumpifs.rs │ │ ├── dxbc.rs │ │ ├── encfw.rs │ │ ├── gif.rs │ │ ├── gpg.rs │ │ ├── gzip.rs │ │ ├── inflate.rs │ │ ├── iso9660.rs │ │ ├── jboot.rs │ │ ├── jffs2.rs │ │ ├── jpeg.rs │ │ ├── linux.rs │ │ ├── lz4.rs │ │ ├── lzfse.rs │ │ ├── lzma.rs │ │ ├── lzop.rs │ │ ├── matter_ota.rs │ │ ├── mbr.rs │ │ ├── mh01.rs │ │ ├── pcap.rs │ │ ├── pem.rs │ │ ├── png.rs │ │ ├── rar.rs │ │ ├── riff.rs │ │ ├── romfs.rs │ │ ├── sevenzip.rs │ │ ├── squashfs.rs │ │ ├── srec.rs │ │ ├── svg.rs │ │ ├── swapped.rs │ │ ├── tarball.rs │ │ ├── trx.rs │ │ ├── tsk.rs │ │ ├── ubi.rs │ │ ├── uefi.rs │ │ ├── uimage.rs │ │ ├── vxworks.rs │ │ ├── wince.rs │ │ ├── yaffs2.rs │ │ ├── zlib.rs │ │ └── zstd.rs │ ├── extractors.rs │ ├── json.rs │ ├── lib.rs │ ├── magic.rs │ ├── main.rs │ ├── signatures/ │ │ ├── aes.rs │ │ ├── android_bootimg.rs │ │ ├── androidsparse.rs │ │ ├── apfs.rs │ │ ├── arcadyan.rs │ │ ├── arj.rs │ │ ├── autel.rs │ │ ├── binhdr.rs │ │ ├── bmp.rs │ │ ├── btrfs.rs │ │ ├── bzip2.rs │ │ ├── cab.rs │ │ ├── cfe.rs │ │ ├── chk.rs │ │ ├── common.rs │ │ ├── compressd.rs │ │ ├── copyright.rs │ │ ├── cpio.rs │ │ ├── cramfs.rs │ │ ├── csman.rs │ │ ├── dahua_zip.rs │ │ ├── deb.rs │ │ ├── dkbs.rs │ │ ├── dlink_tlv.rs │ │ ├── dlke.rs │ │ ├── dlob.rs │ │ ├── dmg.rs │ │ ├── dms.rs │ │ ├── dpapi.rs │ │ ├── dtb.rs │ │ ├── dxbc.rs │ │ ├── ecos.rs │ │ ├── efigpt.rs │ │ ├── elf.rs │ │ ├── encfw.rs │ │ ├── encrpted_img.rs │ │ ├── ext.rs │ │ ├── fat.rs │ │ ├── gif.rs │ │ ├── gpg.rs │ │ ├── gzip.rs │ │ ├── hashes.rs │ │ ├── iso9660.rs │ │ ├── jboot.rs │ │ ├── jffs2.rs │ │ ├── jpeg.rs │ │ ├── linux.rs │ │ ├── logfs.rs │ │ ├── luks.rs │ │ ├── lz4.rs │ │ ├── lzfse.rs │ │ ├── lzma.rs │ │ ├── lzop.rs │ │ ├── matter_ota.rs │ │ ├── mbr.rs │ │ ├── mh01.rs │ │ ├── ntfs.rs │ │ ├── openssl.rs │ │ ├── packimg.rs │ │ ├── pcap.rs │ │ ├── pchrom.rs │ │ ├── pdf.rs │ │ ├── pe.rs │ │ ├── pem.rs │ │ ├── pjl.rs │ │ ├── pkcs_der.rs │ │ ├── png.rs │ │ ├── qcow.rs │ │ ├── qnx.rs │ │ ├── rar.rs │ │ ├── riff.rs │ │ ├── romfs.rs │ │ ├── rsa.rs │ │ ├── rtk.rs │ │ ├── seama.rs │ │ ├── sevenzip.rs │ │ ├── shrs.rs │ │ ├── squashfs.rs │ │ ├── srec.rs │ │ ├── svg.rs │ │ ├── tarball.rs │ │ ├── tplink.rs │ │ ├── trx.rs │ │ ├── ubi.rs │ │ ├── uboot.rs │ │ ├── uefi.rs │ │ ├── uimage.rs │ │ ├── vxworks.rs │ │ ├── wince.rs │ │ ├── xz.rs │ │ ├── yaffs.rs │ │ ├── zip.rs │ │ ├── zlib.rs │ │ └── zstd.rs │ ├── signatures.rs │ ├── structures/ │ │ ├── android_bootimg.rs │ │ ├── androidsparse.rs │ │ ├── apfs.rs │ │ ├── arj.rs │ │ ├── autel.rs │ │ ├── binhdr.rs │ │ ├── bmp.rs │ │ ├── btrfs.rs │ │ ├── cab.rs │ │ ├── chk.rs │ │ ├── common.rs │ │ ├── cpio.rs │ │ ├── cramfs.rs │ │ ├── csman.rs │ │ ├── deb.rs │ │ ├── dkbs.rs │ │ ├── dlink_tlv.rs │ │ ├── dlob.rs │ │ ├── dmg.rs │ │ ├── dms.rs │ │ ├── dpapi.rs │ │ ├── dtb.rs │ │ ├── dxbc.rs │ │ ├── efigpt.rs │ │ ├── elf.rs │ │ ├── ext.rs │ │ ├── fat.rs │ │ ├── gif.rs │ │ ├── gzip.rs │ │ ├── iso9660.rs │ │ ├── jboot.rs │ │ ├── jffs2.rs │ │ ├── linux.rs │ │ ├── logfs.rs │ │ ├── luks.rs │ │ ├── lz4.rs │ │ ├── lzfse.rs │ │ ├── lzma.rs │ │ ├── lzop.rs │ │ ├── matter_ota.rs │ │ ├── mbr.rs │ │ ├── mh01.rs │ │ ├── ntfs.rs │ │ ├── openssl.rs │ │ ├── packimg.rs │ │ ├── pcap.rs │ │ ├── pchrom.rs │ │ ├── pe.rs │ │ ├── png.rs │ │ ├── qcow.rs │ │ ├── qnx.rs │ │ ├── rar.rs │ │ ├── riff.rs │ │ ├── romfs.rs │ │ ├── rtk.rs │ │ ├── seama.rs │ │ ├── sevenzip.rs │ │ ├── shrs.rs │ │ ├── squashfs.rs │ │ ├── svg.rs │ │ ├── tplink.rs │ │ ├── trx.rs │ │ ├── ubi.rs │ │ ├── uefi.rs │ │ ├── uimage.rs │ │ ├── vxworks.rs │ │ ├── wince.rs │ │ ├── xz.rs │ │ ├── yaffs.rs │ │ ├── zip.rs │ │ └── zstd.rs │ └── structures.rs └── tests/ ├── arcadyan.rs ├── arj.rs ├── bmp.rs ├── bzip2.rs ├── common/ │ └── mod.rs ├── cramfs.rs ├── gzip.rs ├── jpeg.rs ├── matter_ota.rs ├── mbr.rs ├── pdf.rs ├── png.rs ├── qcow.rs ├── riff.rs ├── romfs.rs ├── sevenzip.rs ├── squashfs.rs ├── squashfs_v2.rs ├── yaffs2.rs ├── zip.rs └── zip_truncated.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ target Dockerfile build_docker.sh ================================================ FILE: .gitattributes ================================================ * text eol=lf *.md text eol=lf *.png binary *.bin binary ================================================ FILE: .github/workflows/quality.yaml ================================================ name: Quality Checks on: pull_request: branches: - "*" jobs: lychee-link-check: name: Link check runs-on: ubuntu-latest steps: - name: Code checkout uses: actions/checkout@v4 - name: Link Checker uses: lycheeverse/lychee-action@v1.8.0 with: fail: true fmt: name: Formatting (rustfmt) runs-on: ubuntu-latest strategy: fail-fast: false matrix: platform: - target: x86_64-unknown-linux-gnu steps: - name: Code checkout uses: actions/checkout@v4 - name: Install Rust toolchain (stable) uses: dtolnay/rust-toolchain@stable with: target: ${{ matrix.platform.target }} components: rustfmt - name: Formatting (rustfmt) run: cargo fmt -- --check lint: name: Lint (clippy) runs-on: ubuntu-latest strategy: fail-fast: false matrix: platform: - target: x86_64-unknown-linux-gnu steps: - name: Code checkout uses: actions/checkout@v4 - name: Install Rust toolchain (stable) uses: dtolnay/rust-toolchain@stable with: target: ${{ matrix.platform.target }} components: clippy - name: Clippy (all crates) run: cargo clippy --locked --target=${{ matrix.platform.target }} --workspace --all-targets -- -D warnings - name: Check build did not modify any files run: test -z "$(git status --porcelain)" ================================================ FILE: .github/workflows/rust-release-binary.yml ================================================ name: Release Build on: push: tags: - 'v*' jobs: release: name: Release - ${{ matrix.platform.os-name }} strategy: matrix: platform: - os-name: Linux-x86_64 runs-on: ubuntu-24.04 target: x86_64-unknown-linux-gnu - os-name: Linux-aarch64 runs-on: ubuntu-24.04-arm target: aarch64-unknown-linux-gnu - os-name: macOS-x86_64 runs-on: macOS-latest target: x86_64-apple-darwin - os-name: macOS-aarch64 runs-on: macOS-latest target: aarch64-apple-darwin - os-name: Windows-x86_64 runs-on: windows-latest target: x86_64-pc-windows-msvc - os-name: Windows-aarch64 runs-on: windows-11-arm target: aarch64-pc-windows-msvc runs-on: ${{ matrix.platform.runs-on }} steps: - name: Checkout uses: actions/checkout@v4 - name: Update CHANGELOG if: startsWith(github.ref, 'refs/tags/') id: changelog uses: requarks/changelog-action@v1 with: token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ github.ref_name }} writeToFile: true changelogFilePath: "CHANGELOG.md" - name: Build binary uses: houseabsolute/actions-rust-cross@v1 with: command: build target: ${{ matrix.platform.target }} args: "--locked --release" strip: true - name: Publish artifacts and release if: startsWith(github.ref, 'refs/tags/') uses: houseabsolute/actions-rust-release@v0 with: executable-name: binwalk target: ${{ matrix.platform.target }} release-tag-prefix: v changes-file: "CHANGELOG.md" action-gh-release-parameters: | { "draft": false, "prerelease": false, "overwrite": true, "generate_release_notes": true, "make_latest": true, "body_path": "CHANGELOG.md", "token": "${{ secrets.GITHUB_TOKEN }}" } - name: Commit CHANGELOG.md if: startsWith(github.ref, 'refs/tags/') && matrix.platform.os-name == 'Linux-x86_64' uses: stefanzweifel/git-auto-commit-action@v4 with: branch: master commit_message: 'docs: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]' file_pattern: CHANGELOG.md ================================================ FILE: .gitignore ================================================ target ================================================ FILE: CARGO_README.md ================================================ # binwalk A Rust implementation of the Binwalk firmware analysis tool. ## System Requirements Building requires the following system packages: ``` build-essential libfontconfig1-dev liblzma-dev ``` ## Example ``` use binwalk::Binwalk; // Create a new Binwalk instance let binwalker = Binwalk::new(); // Read in the data to analyze let file_data = std::fs::read("/tmp/firmware.bin").expect("Failed to read from file"); // Scan the file data and print the results for result in binwalker.scan(&file_data) { println!("{:#?}", result); } ``` ================================================ FILE: Cargo.toml ================================================ [package] name = "binwalk" version = "3.1.1" edition = "2024" authors = ["Craig Heffner "] license = "MIT" readme = "CARGO_README.md" repository = "https://github.com/ReFirmLabs/binwalk" description = "Analyzes data for embedded file types" keywords = ["binwalk", "firmware", "analysis"] [dependencies] log = "0.4.22" base64 = "0.22.1" chrono = "0.4.38" walkdir = "2.5.0" entropy = "0.4.2" colored = "3.0.0" termsize = "0.1" crc32-v2 = "0.0.5" crc32c = "0.6.8" liblzma = "0.4.2" bzip2 = "0.6.0" threadpool = "1.8.1" serde_json = "1.0" env_logger = "0.11.5" flate2 = "1.1.2" adler32 = "1.2.0" md5 = "0.8.0" miniz_oxide = "0.8.0" aho-corasick = "1.1.3" serde = { version = "1.0", features = ["derive"] } clap = { version = "4.5.16", features = ["derive"] } xxhash-rust = { version = "0.8.12", features = ["xxh32"] } hex = "0.4.3" delink = { git = "https://github.com/devttys0/delink" } plotly = { version = "0.13.1", features = ["kaleido", "kaleido_download"] } [dependencies.uuid] version = "1.17.0" features = [ "v4", # Lets you generate random UUIDs "fast-rng", # Use a faster (but still sufficiently random) RNG "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs ] [profile.release] lto = true ================================================ FILE: Dockerfile ================================================ ## Scratch build stage FROM ubuntu:25.04 AS build ARG BUILD_DIR="/tmp" ARG BINWALK_BUILD_DIR="${BUILD_DIR}/binwalk" ARG SASQUATCH_FILENAME="sasquatch_1.0.deb" ARG SASQUATCH_BASE_FILE_URL="https://github.com/onekey-sec/sasquatch/releases/download/sasquatch-v4.5.1-5/" ENV DEBIAN_FRONTEND=noninteractive ENV TZ=Etc/UTC COPY . ${BINWALK_BUILD_DIR} WORKDIR ${BINWALK_BUILD_DIR} # Pull build needs, build dumpifs, lzfse, dmg2img, vfdecrypt, and binwalk # Cleaning up our mess here doesn't matter, as anything generated in # this stage won't make it into the final image unless it's explicitly copied RUN apt-get update -y \ && apt-get upgrade -y \ && apt-get -y --no-install-recommends install \ ca-certificates \ tzdata \ curl \ git \ wget \ build-essential \ clang \ zlib1g \ zlib1g-dev \ liblz4-1 \ libsrecord-dev \ liblzma-dev \ liblzo2-dev \ libucl-dev \ liblz4-dev \ libbz2-dev \ libssl-dev \ pkg-config \ && curl -L -o "${SASQUATCH_FILENAME}" "${SASQUATCH_BASE_FILE_URL}\sasquatch_1.0_$(dpkg --print-architecture).deb" \ && git clone https://github.com/askac/dumpifs.git ${BUILD_DIR}/dumpifs \ && git clone https://github.com/lzfse/lzfse.git ${BUILD_DIR}/lzfse \ && git clone https://github.com/Lekensteyn/dmg2img.git ${BUILD_DIR}/dmg2img \ && rm ${BUILD_DIR}/dumpifs/dumpifs \ && make -C ${BUILD_DIR}/dumpifs dumpifs \ && make -C ${BUILD_DIR}/lzfse install \ && make -C ${BUILD_DIR}/dmg2img dmg2img vfdecrypt HAVE_LZFSE=1 \ && curl https://sh.rustup.rs -sSf | sh -s -- -y \ && . /root/.cargo/env \ && cargo build --release ## Prod image build stage FROM ubuntu:25.04 ARG BUILD_DIR="/tmp" ARG BINWALK_BUILD_DIR="${BUILD_DIR}/binwalk" ARG DEFAULT_WORKING_DIR="/analysis" ARG SASQUATCH_FILENAME="sasquatch_1.0.deb" ENV DEBIAN_FRONTEND=noninteractive ENV TZ=Etc/UTC COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ ENV UV_SYSTEM_PYTHON=1 UV_BREAK_SYSTEM_PACKAGES=1 WORKDIR ${BUILD_DIR} # Copy the build artifacts from the scratch build stage COPY --from=build ${BINWALK_BUILD_DIR}/${SASQUATCH_FILENAME} ${BUILD_DIR}/${SASQUATCH_FILENAME} COPY --from=build /usr/local/bin/lzfse ${BUILD_DIR}/dumpifs/dumpifs ${BUILD_DIR}/dmg2img/dmg2img ${BUILD_DIR}/dmg2img/vfdecrypt ${BINWALK_BUILD_DIR}/target/release/binwalk /usr/local/bin/ # Install dependencies, create default working directory, and remove clang & friends. # clang is needed to build python-lzo and vmlinux-to-elf, but it's not needed # afterward, so it's safe to remove and reduces the image size by ~400MB. # Those two packages could be built in the scratch stage and copied over from it, # but that would require that I untangle the Eldritch Horror that is the # pip build process, and that's not a particular monster that I'm up to slaying today. RUN apt-get update -y \ && apt-get upgrade -y \ && apt-get -y install --no-install-recommends \ ca-certificates \ tzdata \ python3 \ 7zip \ zstd \ srecord \ tar \ unzip \ sleuthkit \ cabextract \ curl \ wget \ git \ lz4 \ lzop \ unrar \ unyaffs \ zlib1g \ zlib1g-dev \ liblz4-1 \ libsrecord-dev \ liblzma-dev \ liblzo2-dev \ libucl-dev \ liblz4-dev \ libbz2-dev \ libssl-dev \ libfontconfig1-dev \ libpython3-dev \ 7zip-standalone \ cpio \ device-tree-compiler \ clang \ && dpkg -i ${BUILD_DIR}/${SASQUATCH_FILENAME} \ && rm ${BUILD_DIR}/${SASQUATCH_FILENAME} \ && CC=clang uv pip install uefi_firmware jefferson ubi-reader git+https://github.com/marin-m/vmlinux-to-elf \ && uv cache clean \ && apt-get purge clang -y \ && apt autoremove -y \ && rm -rf /var/cache/apt/archives /var/lib/apt/lists/* /bin/uv /bin/uvx \ && mkdir -p ${DEFAULT_WORKING_DIR} \ && chmod 777 ${DEFAULT_WORKING_DIR} WORKDIR ${DEFAULT_WORKING_DIR} # Run as the default ubuntu user USER ubuntu # Enable this environment variable to remove extractor top-level symlink, # as the symlink target path in the docker environment will not match that of the host. ENV BINWALK_RM_EXTRACTION_SYMLINK=1 ENTRYPOINT [ "binwalk" ] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 devttys0 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Binwalk v3 This is an updated version of the Binwalk firmware analysis tool, re-written in Rust for speed and accuracy. ![binwalk v3](images/binwalk_animated.svg) ## What does it do? Binwalk can identify, and optionally extract, files and data that have been embedded inside of other files. While its primary focus is firmware analysis, it supports a [wide variety](https://github.com/ReFirmLabs/binwalk/wiki/Supported-Signatures) of file and data types. Through [entropy analysis](https://github.com/ReFirmLabs/binwalk/wiki/Generating-Entropy-Graphs), it can even help to identify unknown compression or encryption! Binwalk can be customized and [integrated](https://github.com/ReFirmLabs/binwalk/wiki/Using-the-Rust-Library) into your own Rust projects. ## How do I get it? The easiest way to install Binwalk and all dependencies is to [build a Docker image](https://github.com/ReFirmLabs/binwalk/wiki/Building-A-Binwalk-Docker-Image). Binwalk can also be [installed](https://github.com/ReFirmLabs/binwalk/wiki/Cargo-Installation) via the Rust package manager. Or, you can [compile from source](https://github.com/ReFirmLabs/binwalk/wiki/Compile-From-Source)! ## How do I use it? Usage is _**simple**_, analysis is _**fast**_, and results are _**detailed**_: ``` binwalk DIR-890L_AxFW110b07.bin ``` ![example output](images/output.png) Use `--help`, or check out the [Wiki](https://github.com/ReFirmLabs/binwalk/wiki#usage) for more advanced options! ================================================ FILE: build_docker.sh ================================================ #!/usr/bin/env bash docker build --build-arg SCRIPT_DIRECTORY=$PWD -t binwalkv3 . ================================================ FILE: dependencies/README.md ================================================ # Binwalk Dependencies These scripts install the required Binwalk build and runtime system dependencies, except for the Rust compiler itself. Execute the appropriate script for your operating system (e.g., `ubuntu.sh` for Ubuntu). ## ubuntu.sh This script installs *all* required dependencies for Ubuntu-based systems, including the dependencies listed in `pip.sh` and `src.sh`. This should work for most Debian / Debian-based systems as well, but is only tested on Ubuntu. ## pip.sh This script installs all Python-based dependencies via `pip3`. It should be sourced by higher-level scripts (e.g., `ubuntu.sh`). ## src.sh This script builds and installs all source-based dependencies. It should be sourced by higher-level scripts (e.g., `ubuntu.sh`). ================================================ FILE: dependencies/pip.sh ================================================ #!/bin/bash # Install pip dependencies. # Requires that pip3 is already installed. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if command -v uv >/dev/null 2>&1; then uv pip install -r "$SCRIPT_DIR/requirements.txt" else pip install -r "$SCRIPT_DIR/requirements.txt" --break-system-packages fi ================================================ FILE: dependencies/requirements.txt ================================================ uefi_firmware jefferson ubi-reader lz4 zstandard git+https://github.com/marin-m/vmlinux-to-elf ================================================ FILE: dependencies/src.sh ================================================ #!/bin/bash # Install dependencies from source. # Requires that git and build tools (make, gcc, etc) are already installed. # Install dumpifs cd /tmp git clone https://github.com/askac/dumpifs.git cd /tmp/dumpifs make dumpifs cp ./dumpifs /usr/local/bin/dumpifs cd /tmp rm -rf /tmp/dumpifs # Install LZFSE utility and library cd /tmp git clone https://github.com/lzfse/lzfse.git cd /tmp/lzfse make install cd /tmp rm -rf /tmp/lzfse # Install dmg2img with LZFSE support cd /tmp git clone https://github.com/Lekensteyn/dmg2img.git cd /tmp/dmg2img make dmg2img HAVE_LZFSE=1 make install cd /tmp rm -rf /tmp/dmg2img ================================================ FILE: dependencies/ubuntu.sh ================================================ #!/bin/bash # Get the path to this script's directory, regardless of where it is run from SCRIPT_DIRECTORY=$(dirname -- "$( readlink -f -- "$0"; )") # Install dependencies from apt repository DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get -y install \ 7zip \ zstd \ srecord \ tar \ unzip \ sleuthkit \ cabextract \ curl \ wget \ git \ lz4 \ lzop \ unrar \ unyaffs \ python3-pip \ build-essential \ clang \ liblzo2-dev \ libucl-dev \ liblz4-dev \ libbz2-dev \ zlib1g-dev \ libfontconfig1-dev \ liblzma-dev \ libssl-dev \ 7zip-standalone \ cpio \ device-tree-compiler # Install sasquatch Debian package curl -L -o sasquatch_1.0.deb "https://github.com/onekey-sec/sasquatch/releases/download/sasquatch-v4.5.1-5/sasquatch_1.0_$(dpkg --print-architecture).deb" dpkg -i sasquatch_1.0.deb rm sasquatch_1.0.deb # Install Python dependencies source "${SCRIPT_DIRECTORY}/pip.sh" # Install dependencies from source source "${SCRIPT_DIRECTORY}/src.sh" ================================================ FILE: fuzzing/Cargo.toml ================================================ [package] name = "fuzz" version = "0.1.0" edition = "2024" [dependencies] afl = "*" binwalk = { path = "../" } ================================================ FILE: fuzzing/README.md ================================================ # Fuzzing Binwalk Fuzz testing for Binwalk is done through [AFL++](https://aflplus.plus). At the moment code coverage is not 100% complete, but exercises the file parsing code, which is the most problematic and error-prone. ## Fuzzer Dependencies You must have a C compiler and `make` installed, as well as the `cargo-afl` crate: ``` sudo apt install build-essentials cargo install cargo-afl ``` ## Building the Fuzzer ``` cargo afl build --release ``` ## Running the Fuzzer You must provide an input directory containing sample files for the fuzzer to mutate. You must provide an output directory for the fuzzer to save crash results to. ``` cargo afl fuzz -i input_directory -o output_directory ./target/release/fuzz ``` ================================================ FILE: fuzzing/src/main.rs ================================================ use afl::fuzz; use binwalk::Binwalk; fn main() { // AFL makes this real simple... fuzz!(|data: &[u8]| { // Initialize binwalk, no extraction let binwalker = Binwalk::new(); // Scan the data provided by AFL binwalker.scan(&data.to_vec()); }); } ================================================ FILE: scripts/binwalk-ui ================================================ #! /bin/bash - # https://unix.stackexchange.com/questions/290696/display-stdout-and-stderr-in-two-separate-streams # # This script will run binwalk in a split screen, displaying results in the top screen and debug output in the bottom screen. # The `screen` utility must be installed. # If BINWALK_PATH is not set, assume it is in the default cargo target path if [[ -z "${BINWALK_PATH}" ]]; then BINWALK_PATH="$(cd "$(dirname $0)" && pwd)/../target/release/binwalk" fi # If no RUST_LOG level is defined, default to `info` if [[ -z "${RUST_LOG}" ]]; then export RUST_LOG=info fi tmpdir=$(mktemp -d) || exit trap 'rm -rf "$tmpdir"' EXIT INT TERM HUP FIFO=$tmpdir/FIFO mkfifo "$FIFO" || exit conf=$tmpdir/conf cat > "$conf" << 'EOF' || exit split -h focus screen -t stderr sh -c 'tty > "$FIFO"; read done < "$FIFO"' focus screen -t stdout sh -c 'read tty < "$FIFO"; eval "$CMD" 2> "$tty"; echo "[Command exited with status $?, press enter to exit]"; read prompt; echo done > "$FIFO"' EOF CMD="$BINWALK_PATH $*" export FIFO CMD screen -mc "$conf" ================================================ FILE: src/binwalk.rs ================================================ //! Primary Binwalk interface. use aho_corasick::AhoCorasick; use log::{debug, error, info, warn}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::path; use uuid::Uuid; #[cfg(windows)] use std::os::windows; #[cfg(unix)] use std::os::unix; use crate::common::{is_offset_safe, read_file}; use crate::extractors; use crate::magic; use crate::signatures; /// Returned on initialization error #[derive(Debug, Default, Clone)] pub struct BinwalkError { pub message: String, } impl BinwalkError { pub fn new(message: &str) -> Self { BinwalkError { message: message.to_string(), } } } /// Analysis results returned by Binwalk::analyze #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct AnalysisResults { /// Path to the file that was analyzed pub file_path: String, /// File signature results, as returned by Binwalk::scan pub file_map: Vec, /// File extraction results, as returned by Binwalk::extract. /// HashMap key is the corresponding SignatureResult.id value in `file_map`. pub extractions: HashMap, } /// Analyze files / memory for file signatures /// /// ## Example /// /// ``` /// use binwalk::Binwalk; /// /// let target_file = "/bin/ls"; /// let data_to_scan = std::fs::read(target_file).expect("Unable to read file"); /// /// let binwalker = Binwalk::new(); /// /// let signature_results = binwalker.scan(&data_to_scan); /// /// for result in &signature_results { /// println!("Found '{}' at offset {:#X}", result.description, result.offset); /// } /// ``` #[derive(Debug, Default, Clone)] pub struct Binwalk { /// Count of all signatures (short and regular) pub signature_count: usize, /// Count of all magic patterns (short and regular) pub pattern_count: usize, /// The base file requested for analysis pub base_target_file: String, /// The base output directory for extracted files pub base_output_directory: String, /// A list of signatures that must start at offset 0 pub short_signatures: Vec, /// A list of magic bytes to search for throughout the entire file pub patterns: Vec>, /// Maps patterns to their corresponding signature pub pattern_signature_table: HashMap, /// Maps signatures to their corresponding extractors pub extractor_lookup_table: HashMap>, } impl Binwalk { /// Create a new Binwalk instance with all default values. /// Equivalent to `Binwalk::configure(None, None, None, None, None, false)`. /// /// ## Example /// /// ``` /// use binwalk::Binwalk; /// /// let binwalker = Binwalk::new(); /// ``` #[allow(dead_code)] pub fn new() -> Binwalk { Binwalk::configure(None, None, None, None, None, false).unwrap() } /// Create a new Binwalk instance. /// /// If `target_file_name` and `output_directory` are specified, the `output_directory` will be created if it does not /// already exist, and a symlink to `target_file_name` will be placed inside the `output_directory`. The path to this /// symlink is placed in `Binwalk.base_target_file`. /// /// The `include` and `exclude` arguments specify include and exclude signature filters. The String values contained /// in these arguments must match the `Signature.name` values defined in magic.rs. /// /// Additional user-defined signatures may be provided via the `signatures` argument. /// /// ## Example /// /// ``` /// # fn main() { #[allow(non_snake_case)] fn _doctest_main_src_binwalk_rs_102_0() -> Result { /// use binwalk::Binwalk; /// /// // Don't scan for these file signatures /// let exclude_filters: Vec = vec!["jpeg".to_string(), "png".to_string()]; /// /// let binwalker = Binwalk::configure(None, /// None, /// None, /// Some(exclude_filters), /// None, /// false)?; /// # Ok(binwalker) /// # } _doctest_main_src_binwalk_rs_102_0(); } /// ``` pub fn configure( target_file_name: Option, output_directory: Option, include: Option>, exclude: Option>, signatures: Option>, full_search: bool, ) -> Result { let mut new_instance = Binwalk { ..Default::default() }; // Target file is optional, especially if being called via the library if let Some(target_file) = target_file_name { // Set the target file path, make it an absolute path match path::absolute(&target_file) { Err(_) => { return Err(BinwalkError::new(&format!( "Failed to get absolute path for '{target_file}'" ))); } Ok(abspath) => { new_instance.base_target_file = abspath.display().to_string(); } } // If an output extraction directory was also specified, initialize it if let Some(extraction_directory) = output_directory { // Make the extraction directory an absolute path match path::absolute(&extraction_directory) { Err(_) => { return Err(BinwalkError::new(&format!( "Failed to get absolute path for '{extraction_directory}'" ))); } Ok(abspath) => { new_instance.base_output_directory = abspath.display().to_string(); } } // Initialize the extraction directory. This will create the directory if it // does not exist, and create a symlink inside the directory that points to // the specified target file. match init_extraction_directory( &new_instance.base_target_file, &new_instance.base_output_directory, ) { Err(e) => { return Err(BinwalkError::new(&format!( "Failed to initialize extraction directory: {e}" ))); } Ok(new_target_file_path) => { // This is the new base target path (a symlink inside the extraction directory) new_instance.base_target_file = new_target_file_path.clone(); } } } } // Load all internal signature patterns let mut signature_patterns = magic::patterns(); // Include any user-defined signature patterns if let Some(user_defined_signature_patterns) = signatures { signature_patterns.extend(user_defined_signature_patterns); } // Load magic signatures for signature in signature_patterns.clone() { // Check if this signature should be included if !include_signature(&signature, &include, &exclude) { continue; } // Keep a count of total unique signatures that are supported new_instance.signature_count += 1; // Keep a count of the total number of magic patterns new_instance.pattern_count += signature.magic.len(); // Create a lookup table which associates each signature to its respective extractor new_instance .extractor_lookup_table .insert(signature.name.clone(), signature.extractor.clone()); // Each signature may have multiple magic bytes associated with it for pattern in signature.magic.clone() { if signature.short && !full_search { // These are short patterns, and should only be searched for at the very beginning of a file new_instance.short_signatures.push(signature.clone()); break; } else { /* * Need to keep a mapping of the pattern index and its associated signature * so that when a match is found it can be resolved back to the signature from * which it came. */ new_instance .pattern_signature_table .insert(new_instance.patterns.len(), signature.clone()); // Add these magic bytes to the list of patterns new_instance.patterns.push(pattern.to_vec()); } } } Ok(new_instance) } /// Scan a file for magic signatures. /// Returns a list of validated magic signatures representing the known contents of the file. /// /// ## Example /// /// ``` /// use binwalk::Binwalk; /// /// let target_file = "/bin/ls"; /// let data_to_scan = std::fs::read(target_file).expect("Unable to read file"); /// /// let binwalker = Binwalk::new(); /// /// let signature_results = binwalker.scan(&data_to_scan); /// /// for result in &signature_results { /// println!("{:#X} {}", result.offset, result.description); /// } /// /// assert!(signature_results.len() > 0); /// ``` pub fn scan(&self, file_data: &[u8]) -> Vec { const FILE_START_OFFSET: usize = 0; let mut index_adjustment: usize = 0; let mut next_valid_offset: usize = 0; let mut previous_valid_offset = None; let available_data = file_data.len(); // A list of identified signatures, representing a "map" of the file data let mut file_map: Vec = vec![]; /* * Check beginning of file for short signatures. * These signatures are only valid if they occur at the very beginning of a file. * This is typically because the signatures are very short and they are likely * to occur randomly throughout the file, so this prevents having to validate many * false positve matches. */ for signature in &self.short_signatures { for magic in signature.magic.clone() { let magic_start = FILE_START_OFFSET + signature.magic_offset; let magic_end = magic_start + magic.len(); if file_data.len() > magic_end && file_data[magic_start..magic_end] == magic { debug!( "Found {} short magic match at offset {:#X}", signature.description, magic_start ); if let Ok(mut signature_result) = (signature.parser)(file_data, magic_start) { // Auto populate some signature result fields signature_result_auto_populate(&mut signature_result, signature); // Add this signature to the file map file_map.push(signature_result.clone()); info!( "Found valid {} short signature at offset {:#X}", signature_result.name, FILE_START_OFFSET ); // Only update the next_valid_offset if confidence is high; these are, after all, short signatures if signature_result.confidence >= signatures::common::CONFIDENCE_HIGH { next_valid_offset = signature_result.offset + signature_result.size; } // Only one signature can match at fixed offset 0 break; } else { debug!( "{} short signature match at offset {:#X} is invalid", signature.description, FILE_START_OFFSET ); } } } } /* * Same pattern matching algorithm used by fgrep. * This will search for all magic byte patterns in the file data, all at once. * https://en.wikipedia.org/wiki/Aho–Corasick_algorithm */ let grep = AhoCorasick::new(self.patterns.clone()).unwrap(); debug!("Running Aho-Corasick scan"); /* * Outer loop wrapper for AhoCorasick scan loop. This will loop until: * * 1) next_valid_offset exceeds available_data * 2) previous_valid_offset <= next_valid_offset */ while is_offset_safe(available_data, next_valid_offset, previous_valid_offset) { // Update the previous valid offset in praparation for the next loop iteration previous_valid_offset = Some(next_valid_offset); debug!("Continuing scan from offset {next_valid_offset:#X}"); /* * Run a new AhoCorasick scan starting at the next valid offset in the file data. * This will loop until: * * 1) All data has been exhausted, in which case previous_valid_offset and next_valid_offset * will be identical, causing the outer while loop to break. * 2) A valid signature with a defined size is found, in which case next_valid_offset will * be updated to point the end of the valid signature data, causing a new AhoCorasick * scan to start at the new next_valid_offset file location. */ for magic_match in grep.find_overlapping_iter(&file_data[next_valid_offset..]) { // Get the location of the magic bytes inside the file data let magic_offset: usize = next_valid_offset + magic_match.start(); // Get the signature associated with this magic signature let magic_pattern_index: usize = magic_match.pattern().as_usize(); let signature: signatures::common::Signature = self .pattern_signature_table .get(&magic_pattern_index) .unwrap() .clone(); debug!( "Found {} magic match at offset {:#X}", signature.description, magic_offset ); /* * Invoke the signature parser to parse and validate the signature. * An error indicates a false positive match for the signature type. */ if let Ok(mut signature_result) = (signature.parser)(file_data, magic_offset) { // Calculate the end of this signature's data let signature_end_offset = signature_result.offset + signature_result.size; // Sanity check the reported offset and size vs file size if signature_end_offset > available_data { info!("Signature {} extends beyond EOF; ignoring", signature.name); // Continue inner loop continue; } // Auto populate some signature result fields signature_result_auto_populate(&mut signature_result, &signature); // Add this signature to the file map file_map.push(signature_result.clone()); info!( "Found valid {} signature at offset {:#X}", signature_result.name, signature_result.offset ); // Only update the next_valid_offset if confidence is at least medium if signature_result.confidence >= signatures::common::CONFIDENCE_MEDIUM { // Only update the next_valid offset if the end of the signature reported the size of its contents if signature_result.size > 0 { // This file's signature has a known size, so there's no need to scan inside this file's data. // Update next_valid_offset to point to the end of this file signature and break out of the // inner loop. next_valid_offset = signature_end_offset; break; } } } else { debug!( "{} magic match at offset {:#X} is invalid", signature.description, magic_offset ); } } } debug!("Aho-Corasick scan found {} magic matches", file_map.len()); /* * A file's magic bytes do not always start at the beginning of a file, meaning that it is possible * that the order in which the signatures were found in the file data is not the order in which we * want to process/validate the signatures. Each signature's parser function will report the correct * starting offset for the signature, so sort the file_map by the SignatureResult.offset value. */ file_map.sort(); next_valid_offset = 0; /* * Now that signatures are in the correct order, identify and any overlapping signatures * (such as gzip files identified within a tarball archive), signatures with the same reported offset, * and any signatures with an invalid reported size (i.e., the size extends beyond the end of available file_data). */ for mut i in 0..file_map.len() { // Some entries may have been removed from the file_map list in previous loop iterations; adjust the index accordingly i -= index_adjustment; // Make sure the file map index is valid if file_map.is_empty() || i >= file_map.len() { break; } let this_signature = file_map[i].clone(); let remaining_available_size = file_data.len() - this_signature.offset; // Check if the previous file map entry had the same reported starting offset as this one if i > 0 && this_signature.offset == file_map[i - 1].offset { // Get the previous signature in the file map let previous_signature = file_map[i - 1].clone(); // If this file map entry and the conflicting entry do not have the same confidence level, default to the one with highest confidence if this_signature.confidence != previous_signature.confidence { debug!( "Conflicting signatures at offset {:#X}; defaulting to the signature with highest confidence", this_signature.offset ); // If this signature is higher confidence, invalidate the previous signature if this_signature.confidence > previous_signature.confidence { file_map.remove(i - 1); index_adjustment += 1; // Else, this signature has a lower confidence; invalidate this signature and continue to the next signature in the list } else { file_map.remove(i); index_adjustment += 1; continue; } // Conflicting signatures have identical confidence levels; defer to the previously vetted signature } else { debug!( "Conflicting signatures at offset {:#X} with the same confidence; first come, first served", this_signature.offset ); file_map.remove(i); index_adjustment += 1; continue; } // Else, if the offsets don't conflict, make sure this signature doesn't fall inside a previously identified signature's data } else if this_signature.offset < next_valid_offset { debug!( "Signature {} at offset {:#X} contains conflicting data; ignoring", this_signature.name, this_signature.offset ); file_map.remove(i); index_adjustment += 1; continue; } // If we've made it this far, make sure this signature's data doesn't extend beyond EOF and that the file data doesn't wrap around if this_signature.size > remaining_available_size || ((this_signature.offset + this_signature.size) as isize) < 0 { debug!( "Signature {} at offset {:#X} claims its size extends beyond EOF; ignoring", this_signature.name, this_signature.offset ); file_map.remove(i); index_adjustment += 1; continue; } // This signature looks OK, update the next_valid_offset to be the end of this signature's data, only if we're fairly confident in the signature if this_signature.confidence >= signatures::common::CONFIDENCE_MEDIUM { next_valid_offset = this_signature.offset + this_signature.size; } } /* * Ideally, all signatures would report their size; some file formats do not specify a size, and the only * way to determine the size is to extract the file format (compressed data, for example). * For signatures with a reported size of 0, update their size to be the start of the next signature, or EOF. * This makes the assumption that there are no false positives or false negatives. * * False negatives (i.e., there is some other file format or data between this signature and the next that * was not correctly identified) is less problematic, as this will overestimate the size of this signature, * but most extraction utilities don't care about this extra trailing data being included. * * False positives (i.e., some data inside of this signature is identified as some other file type) can cause * this signature's file data to become truncated, which will inevitably result in a failed, or partial, extraction. * * Thus, signatures must be very good at validating magic matches and eliminating false positives. */ for i in 0..file_map.len() { if file_map[i].size == 0 { // Index of the next file map entry, if any let next_index = i + 1; // By default, assume this signature goes to EOF let mut next_offset: usize = file_data.len(); // If there are more entries in the file map if next_index < file_map.len() { // Look through all remaining file map entries for one with medium to high confidence for file_map_entry in file_map.iter().skip(next_index) { if file_map_entry.confidence >= signatures::common::CONFIDENCE_MEDIUM { // If a signature of at least medium confidence is found, assume that *this* signature ends there next_offset = file_map_entry.offset; break; } } } file_map[i].size = next_offset - file_map[i].offset; warn!( "Signature {}:{:#X} size is unknown; assuming size of {:#X} bytes", file_map[i].name, file_map[i].offset, file_map[i].size ); } else { debug!( "Signature {}:{:#X} has a reported size of {:#X} bytes", file_map[i].name, file_map[i].offset, file_map[i].size ); } } debug!("Found {} valid signatures", file_map.len()); file_map } /// Extract all extractable signatures found in a file. /// /// ## Example /// /// ``` /// # fn main() { #[allow(non_snake_case)] fn _doctest_main_src_binwalk_rs_529_0() -> Result { /// use binwalk::Binwalk; /// /// let target_path = std::path::Path::new("tests") /// .join("inputs") /// .join("gzip.bin") /// .display() /// .to_string(); /// /// let extraction_directory = std::path::Path::new("tests") /// .join("extractions") /// .display() /// .to_string(); /// /// # std::fs::remove_dir_all(&extraction_directory); /// let binwalker = Binwalk::configure(Some(target_path), /// Some(extraction_directory.clone()), /// None, /// None, /// None, /// false)?; /// /// let file_data = std::fs::read(&binwalker.base_target_file).expect("Unable to read file"); /// /// let scan_results = binwalker.scan(&file_data); /// let extraction_results = binwalker.extract(&file_data, &binwalker.base_target_file, &scan_results); /// /// assert_eq!(scan_results.len(), 1); /// assert_eq!(extraction_results.len(), 1); /// assert_eq!(std::path::Path::new(&extraction_directory) /// .join("gzip.bin.extracted") /// .join("0") /// .join("decompressed.bin") /// .exists(), true); /// # std::fs::remove_dir_all(&extraction_directory); /// # Ok(binwalker) /// # } _doctest_main_src_binwalk_rs_529_0(); } /// ``` pub fn extract( &self, file_data: &[u8], file_name: impl Into, file_map: &Vec, ) -> HashMap { let file_path = file_name.into(); let mut extraction_results: HashMap = HashMap::new(); // Spawn extractors for each extractable signature for signature in file_map { // Signatures may opt to not perform extraction; honor this request if signature.extraction_declined { continue; } // Get the extractor for this signature let extractor = self.extractor_lookup_table[&signature.name].clone(); match &extractor { None => continue, Some(_) => { // Run an extraction for this signature let mut extraction_result = extractors::common::execute(file_data, &file_path, signature, &extractor); if !extraction_result.success { debug!( "Extraction failed for {} (ID: {}) {:#X} - {:#X}", signature.name, signature.id, signature.offset, signature.size ); // Calculate all available data from the start of this signature to EOF let available_data = file_data.len() - signature.offset; /* * If extraction failed, it could be due to truncated data (signature matching is not perfect ya know!) * In that case, make one more attempt, this time provide the extractor all the data possible. */ if signature.size < available_data { // Create a duplicate signature, but set its reported size to the length of all available data let mut new_signature = signature.clone(); new_signature.size = available_data; debug!( "Trying extraction for {} (ID: {}) again, this time from {:#X} - {:#X}", new_signature.name, new_signature.id, new_signature.offset, new_signature.size ); // Re-run the extraction extraction_result = extractors::common::execute( file_data, &file_path, &new_signature, &extractor, ); } } // Update the HashMap with the result of this extraction attempt extraction_results.insert(signature.id.clone(), extraction_result); } } } extraction_results } /// Analyze a data buffer and optionally extract the file contents. /// /// ## Example /// /// ``` /// # fn main() { #[allow(non_snake_case)] fn _doctest_main_src_binwalk_rs_672_0() -> Result { /// use binwalk::{Binwalk, common}; /// /// let target_path = std::path::Path::new("tests") /// .join("inputs") /// .join("gzip.bin") /// .display() /// .to_string(); /// /// let extraction_directory = std::path::Path::new("tests") /// .join("extractions") /// .display() /// .to_string(); /// /// let file_data = common::read_file(&target_path).expect("Failed to read file data"); /// /// # std::fs::remove_dir_all(&extraction_directory); /// let binwalker = Binwalk::configure(Some(target_path), /// Some(extraction_directory.clone()), /// None, /// None, /// None, /// false)?; /// /// let analysis_results = binwalker.analyze_buf(&file_data, &binwalker.base_target_file, true); /// /// assert_eq!(analysis_results.file_map.len(), 1); /// assert_eq!(analysis_results.extractions.len(), 1); /// assert_eq!(std::path::Path::new(&extraction_directory) /// .join("gzip.bin.extracted") /// .join("0") /// .join("decompressed.bin") /// .exists(), true); /// # std::fs::remove_dir_all(&extraction_directory); /// # Ok(binwalker) /// # } _doctest_main_src_binwalk_rs_672_0(); } /// ``` pub fn analyze_buf( &self, file_data: &[u8], target_file: impl Into, do_extraction: bool, ) -> AnalysisResults { let file_path = target_file.into(); // Return value let mut results: AnalysisResults = AnalysisResults { file_path: file_path.clone(), ..Default::default() }; // Scan file data for signatures debug!("Analysis start: {file_path}"); results.file_map = self.scan(file_data); // Only extract if told to, and if there were some signatures found in this file if do_extraction && !results.file_map.is_empty() { // Extract everything we can debug!( "Submitting {} signature results to extractor", results.file_map.len() ); results.extractions = self.extract(file_data, &file_path, &results.file_map); } debug!("Analysis end: {file_path}"); results } /// Analyze a file on disk and optionally extract its contents. /// /// ## Example /// /// ``` /// # fn main() { #[allow(non_snake_case)] fn _doctest_main_src_binwalk_rs_745_0() -> Result { /// use binwalk::Binwalk; /// /// let target_path = std::path::Path::new("tests") /// .join("inputs") /// .join("gzip.bin") /// .display() /// .to_string(); /// /// let extraction_directory = std::path::Path::new("tests") /// .join("extractions") /// .display() /// .to_string(); /// /// # std::fs::remove_dir_all(&extraction_directory); /// let binwalker = Binwalk::configure(Some(target_path), /// Some(extraction_directory.clone()), /// None, /// None, /// None, /// false)?; /// /// let analysis_results = binwalker.analyze(&binwalker.base_target_file, true); /// /// assert_eq!(analysis_results.file_map.len(), 1); /// assert_eq!(analysis_results.extractions.len(), 1); /// assert_eq!(std::path::Path::new(&extraction_directory) /// .join("gzip.bin.extracted") /// .join("0") /// .join("decompressed.bin") /// .exists(), true); /// # std::fs::remove_dir_all(&extraction_directory); /// # Ok(binwalker) /// # } _doctest_main_src_binwalk_rs_745_0(); } /// ``` #[allow(dead_code)] pub fn analyze(&self, target_file: impl Into, do_extraction: bool) -> AnalysisResults { let file_path = target_file.into(); let file_data = match read_file(&file_path) { Err(_) => { error!("Failed to read data from {file_path}"); b"".to_vec() } Ok(data) => data, }; self.analyze_buf(&file_data, &file_path, do_extraction) } } /// Initializes the extraction output directory fn init_extraction_directory( target_file: &str, extraction_directory: &str, ) -> Result { // Create the output directory, equivalent of mkdir -p match fs::create_dir_all(extraction_directory) { Ok(_) => { debug!("Created base output directory: '{extraction_directory}'"); } Err(e) => { error!("Failed to create base output directory '{extraction_directory}': {e}"); return Err(e); } } // Create a Path for the target file let target_path = path::Path::new(&target_file); // Build a symlink path to the target file in the extraction directory let link_target_path_str = format!( "{}{}{}", extraction_directory, path::MAIN_SEPARATOR, target_path.file_name().unwrap().to_str().unwrap() ); // Create a path for the symlink target path let link_path = path::Path::new(&link_target_path_str); if link_path.exists() { return Ok(link_target_path_str); } debug!( "Creating symlink from {} -> {}", link_path.display(), target_path.display() ); // Create a symlink from inside the extraction directory to the specified target file #[cfg(unix)] { match unix::fs::symlink(target_path, link_path) { Ok(_) => Ok(link_target_path_str), Err(e) => { error!( "Failed to create symlink {} -> {}: {}", link_path.display(), target_path.display(), e ); Err(e) } } } #[cfg(windows)] { match std::fs::hard_link(target_path, link_path) { Ok(_) => { return Ok(link_target_path_str); } Err(e) => { error!( "Failed to create hardlink {} -> {}: {}", link_path.display(), target_path.display(), e ); return Err(e); } } } } /// Returns true if the signature should be included for file analysis, else returns false. fn include_signature( signature: &signatures::common::Signature, include: &Option>, exclude: &Option>, ) -> bool { if let Some(include_signatures) = include { for include_str in include_signatures { if signature.name.to_lowercase() == include_str.to_lowercase() { return true; } } return false; } if let Some(exclude_signatures) = exclude { for exclude_str in exclude_signatures { if signature.name.to_lowercase() == exclude_str.to_lowercase() { return false; } } return true; } true } /// Some SignatureResult fields need to be auto-populated. fn signature_result_auto_populate( signature_result: &mut signatures::common::SignatureResult, signature: &signatures::common::Signature, ) { signature_result.id = Uuid::new_v4().to_string(); signature_result.name = signature.name.clone(); signature_result.always_display = signature.always_display; } ================================================ FILE: src/cliparser.rs ================================================ use clap::{CommandFactory, Parser}; #[derive(Debug, Parser)] #[command(author, version, about, long_about = None)] pub struct CliArgs { /// List supported signatures and extractors #[arg(short = 'L', long)] pub list: bool, /// Read data from standard input #[arg(short, long)] pub stdin: bool, /// Supress normal stdout output #[arg(short, long)] pub quiet: bool, /// During recursive extraction display *all* results #[arg(short, long)] pub verbose: bool, /// Automatically extract known file types #[arg(short, long)] pub extract: bool, /// Carve both known and unknown file contents to disk #[arg(short, long)] pub carve: bool, /// Recursively scan extracted files #[arg(short = 'M', long)] pub matryoshka: bool, /// Search for all signatures at all offsets #[arg(short = 'a', long)] pub search_all: bool, /// Generate an entropy graph with Plotly #[arg(short = 'E', long, conflicts_with = "extract")] pub entropy: bool, /// Save entropy graph as a PNG file #[arg(short, long)] pub png: Option, /// Log JSON results to a file ('-' for stdout) #[arg(short, long)] pub log: Option, /// Manually specify the number of threads to use #[arg(short, long)] pub threads: Option, /// Do no scan for these signatures #[arg(short = 'x', long, value_delimiter = ',', num_args = 1..)] pub exclude: Option>, /// Only scan for these signatures #[arg(short = 'y', long, value_delimiter = ',', num_args = 1.., conflicts_with = "exclude")] pub include: Option>, /// Extract files/folders to a custom directory #[arg(short, long, default_value = "extractions")] pub directory: String, /// Path to the file to analyze pub file_name: Option, } pub fn parse() -> CliArgs { let args = CliArgs::parse(); if std::env::args().len() == 1 { CliArgs::command() .print_help() .expect("Failed to print help output"); std::process::exit(0); } args } ================================================ FILE: src/common.rs ================================================ //! Common Functions use chrono::prelude::DateTime; use log::{debug, error}; use std::fs::File; use std::io::Read; /// Read a data into memory, either from disk or from stdin, and return its contents. /// /// ## Example /// /// ``` /// # fn main() { #[allow(non_snake_case)] fn _doctest_main_src_common_rs_11_0() -> Result<(), Box> { /// use binwalk::common::read_input; /// /// let file_data = read_input("/etc/passwd", false)?; /// assert!(file_data.len() > 0); /// # Ok(()) /// # } _doctest_main_src_common_rs_11_0(); } /// ``` pub fn read_input(file: impl Into, stdin: bool) -> Result, std::io::Error> { if stdin { read_stdin() } else { read_file(file) } } /// Read data from standard input and return its contents. pub fn read_stdin() -> Result, std::io::Error> { let mut stdin_data = Vec::new(); match std::io::stdin().read_to_end(&mut stdin_data) { Err(e) => { error!("Failed to read data from stdin: {e}"); Err(e) } Ok(nbytes) => { debug!("Loaded {nbytes} bytes from stdin"); Ok(stdin_data) } } } /// Read a file data into memory and return its contents. /// /// ## Example /// /// ``` /// # fn main() { #[allow(non_snake_case)] fn _doctest_main_src_common_rs_48_0() -> Result<(), Box> { /// use binwalk::common::read_file; /// /// let file_data = read_file("/etc/passwd")?; /// assert!(file_data.len() > 0); /// # Ok(()) /// # } _doctest_main_src_common_rs_48_0(); } /// ``` pub fn read_file(file: impl Into) -> Result, std::io::Error> { let mut file_data = Vec::new(); let file_path = file.into(); match File::open(&file_path) { Err(e) => { error!("Failed to open file {file_path}: {e}"); Err(e) } Ok(mut fp) => match fp.read_to_end(&mut file_data) { Err(e) => { error!("Failed to read file {file_path} into memory: {e}"); Err(e) } Ok(file_size) => { debug!("Loaded {file_size} bytes from {file_path}"); Ok(file_data) } }, } } /// Calculates the CRC32 checksum of the given data. /// /// ## Notes /// /// Uses initial CRC value of 0. /// /// ## Example /// /// ``` /// use binwalk::common::crc32; /// /// let my_data: &[u8] = b"ABCD"; /// /// let my_data_crc = crc32(my_data); /// /// assert_eq!(my_data_crc, 0xDB1720A5); /// ``` pub fn crc32(data: &[u8]) -> u32 { crc32_v2::crc32(0, data) } /// Converts an epoch time to a formatted time string. /// /// ## Example /// /// ``` /// use binwalk::common::epoch_to_string; /// /// let timestamp = epoch_to_string(0); /// /// assert_eq!(timestamp, "1970-01-01 00:00:00"); /// ``` pub fn epoch_to_string(epoch_timestamp: u32) -> String { let date_time = DateTime::from_timestamp(epoch_timestamp.into(), 0); match date_time { Some(dt) => dt.format("%Y-%m-%d %H:%M:%S").to_string(), None => "".to_string(), } } /// Get a C-style NULL-terminated string from the provided list of u8 bytes. /// Return value does not include the terminating NULL byte. fn get_cstring_bytes(raw_data: &[u8]) -> Vec { let mut cstring: Vec = vec![]; for raw_byte in raw_data { if *raw_byte == 0 { break; } else { cstring.push(*raw_byte); } } cstring } /// Get a C-style NULL-terminated string from the provided array of u8 bytes. /// /// ## Example /// /// ``` /// use binwalk::common::get_cstring; /// /// let raw_data: &[u8] = b"this_is_a_c_string\x00"; /// /// let string = get_cstring(raw_data); /// /// assert_eq!(string, "this_is_a_c_string"); /// ``` pub fn get_cstring(raw_data: &[u8]) -> String { let raw_string = get_cstring_bytes(raw_data); let string: String = match String::from_utf8(raw_string) { Err(_) => "".to_string(), Ok(s) => s.clone(), }; string } /// Returns true if the provided byte is an ASCII number /// /// ## Example /// /// ``` /// use binwalk::common::is_ascii_number; /// /// assert!(is_ascii_number(0x31)); /// assert!(!is_ascii_number(0xFE)); /// ``` pub fn is_ascii_number(b: u8) -> bool { const ZERO: u8 = 48; const NINE: u8 = 57; (ZERO..=NINE).contains(&b) } /// Returns true if the provided byte is a printable ASCII character /// /// ## Example /// /// ``` /// use binwalk::common::is_printable_ascii; /// /// assert!(is_printable_ascii(0x41)); /// assert!(!is_printable_ascii(0xFE)); /// ``` pub fn is_printable_ascii(b: u8) -> bool { const ASCII_MIN: u8 = 0x0A; const ASCII_MAX: u8 = 0x7E; (ASCII_MIN..=ASCII_MAX).contains(&b) } /// Validates data offsets to prevent out-of-bounds access and infinite loops while parsing file formats. /// /// ## Notes /// /// - `next_offset` must be within the bounds of `available_data` /// - `previous_offset` must be less than `next_offset`, or `None` /// /// ## Example /// /// ``` /// use binwalk::common::is_offset_safe; /// /// let my_data: &[u8] = b"ABCD"; /// let available_data = my_data.len(); /// /// assert!(is_offset_safe(available_data, 0, None)); /// assert!(!is_offset_safe(available_data, 4, None)); /// assert!(is_offset_safe(available_data, 2, Some(1))); /// assert!(!is_offset_safe(available_data, 2, Some(2))); /// assert!(!is_offset_safe(available_data, 1, Some(2))); /// ``` pub fn is_offset_safe( available_data: usize, next_offset: usize, last_offset: Option, ) -> bool { // If a previous file offset was specified, ensure that it is less than the next file offset if let Some(previous_offset) = last_offset { if previous_offset >= next_offset { return false; } } // Ensure that the next file offset is within the bounds of available file data if next_offset >= available_data { return false; } true } ================================================ FILE: src/display.rs ================================================ use crate::binwalk::AnalysisResults; use crate::extractors; use crate::signatures; use colored::ColoredString; use colored::Colorize; use log::error; use std::collections::HashMap; use std::io; use std::io::Write; use std::time; const DELIM_CHARACTER: &str = "-"; const DEFAULT_TERMINAL_WIDTH: u16 = 200; const COLUMN1_WIDTH: usize = 35; const COLUMN2_WIDTH: usize = 35; fn terminal_width() -> usize { let terminal_width: u16 = match termsize::get() { Some(ts) => ts.cols, None => DEFAULT_TERMINAL_WIDTH, }; terminal_width as usize } fn line_delimiter() -> String { let mut delim: String = "".to_string(); for _i in 0..terminal_width() { delim += DELIM_CHARACTER; } delim } fn center_text(text: &str) -> String { let mut padding_width: i32; let mut centered_string: String = "".to_string(); match ((terminal_width() / 2) - (text.len() / 2)).try_into() { Err(_e) => padding_width = 0, Ok(value) => padding_width = value, } if padding_width < 0 { padding_width = 0; } for _i in 0..padding_width { centered_string += " "; } centered_string += text; centered_string } fn pad_to_length(text: &str, len: usize) -> String { let mut pad_size: i32; let mut padded_string = String::from(text); match (len - text.len()).try_into() { Err(_e) => pad_size = 0, Ok(value) => pad_size = value, } if pad_size < 0 { pad_size = 0; } for _i in 0..pad_size { padded_string += " "; } padded_string } fn line_wrap(text: &str, prefix_size: usize) -> String { let mut this_line = "".to_string(); let mut formatted_string = "".to_string(); let max_line_size: usize = terminal_width() - prefix_size; for word in text.split_whitespace() { if (this_line.len() + word.len()) < max_line_size { this_line = this_line + word + " "; } else { formatted_string = formatted_string + &this_line + "\n"; for _i in 0..prefix_size { formatted_string += " "; } this_line = word.to_string() + " "; } } formatted_string = formatted_string + &this_line; formatted_string.trim().to_string() } fn print_column_headers(col1: &str, col2: &str, col3: &str) { let header_string = format!( "{}{}{}", pad_to_length(col1, COLUMN1_WIDTH), pad_to_length(col2, COLUMN2_WIDTH), col3 ); println!("{}", header_string.bold().bright_blue()); } fn print_delimiter() { println!("{}", line_delimiter().bold().bright_blue()); } fn print_header(title_text: &str) { println!(); println!("{}", center_text(title_text).bold().magenta()); print_delimiter(); print_column_headers("DECIMAL", "HEXADECIMAL", "DESCRIPTION"); print_delimiter(); } fn print_footer() { print_delimiter(); println!(); } fn print_signature(signature: &signatures::common::SignatureResult) { let decimal_string = format!("{}", signature.offset); let hexadecimal_string = format!("{:#X}", signature.offset); let display_string = format!( "{}{}{}", pad_to_length(&decimal_string, COLUMN1_WIDTH), pad_to_length(&hexadecimal_string, COLUMN2_WIDTH), line_wrap(&signature.description, COLUMN1_WIDTH + COLUMN2_WIDTH) ); if signature.confidence >= signatures::common::CONFIDENCE_HIGH { println!("{}", display_string.green()); } else if signature.confidence >= signatures::common::CONFIDENCE_MEDIUM { println!("{}", display_string.yellow()); } else { println!("{}", display_string.red()); } } fn print_signatures(signatures: &Vec) { for signature in signatures { print_signature(signature); } } fn print_extraction( signature: &signatures::common::SignatureResult, extraction: Option<&extractors::common::ExtractionResult>, ) { let extraction_message: ColoredString; match extraction { None => { extraction_message = format!( "[#] Extraction of {} data at offset {:#X} declined", signature.name, signature.offset ) .bold() .yellow(); } Some(extraction_result) => { if extraction_result.success { extraction_message = format!( "[+] Extraction of {} data at offset {:#X} completed successfully", signature.name, signature.offset ) .bold() .green(); } else { extraction_message = format!( "[-] Extraction of {} data at offset {:#X} failed!", signature.name, signature.offset ) .bold() .red(); } } } println!("{extraction_message}"); } fn print_extractions( signatures: &Vec, extraction_results: &HashMap, ) { let mut delimiter_printed: bool = false; for signature in signatures { let mut printable_extraction: bool = false; let mut extraction_result: Option<&extractors::common::ExtractionResult> = None; // Only print extraction results if an extraction was attempted or explicitly declined if signature.extraction_declined { printable_extraction = true } else if extraction_results.contains_key(&signature.id) { printable_extraction = true; extraction_result = Some(&extraction_results[&signature.id]); } if printable_extraction { // Only print the delimiter line once if !delimiter_printed { print_delimiter(); delimiter_printed = true; } print_extraction(signature, extraction_result); } } } pub fn print_analysis_results(quiet: bool, extraction_attempted: bool, results: &AnalysisResults) { if quiet { return; } // Print signature results print_header(&results.file_path); print_signatures(&results.file_map); // If extraction was attempted, print extraction results if extraction_attempted { print_extractions(&results.file_map, &results.extractions); } // Print the footer text print_footer(); } // Used by print_signature_list #[derive(Debug, Default, Clone)] struct SignatureInfo { name: String, is_short: bool, has_extractor: bool, extractor: String, description: String, } pub fn print_signature_list(quiet: bool, signatures: &Vec) { let mut extractor_count: usize = 0; let mut signature_count: usize = 0; let mut sorted_descriptions: Vec = vec![]; let mut signature_lookup: HashMap = HashMap::new(); if quiet { return; } // Print column headers print_delimiter(); print_column_headers( "Signature Description", "Signature Name", "Extraction Utility", ); print_delimiter(); // Loop through all signatures for signature in signatures { // Convenience struct for storing some basic info about each signature let mut signature_info = SignatureInfo { ..Default::default() }; // Keep track of signature name, description, and if the signature is a "short" signature signature_info.name = signature.name.clone(); signature_info.is_short = signature.short; signature_info.description = signature.description.clone(); // Keep track of which signatures have associated extractors, and if so, what type of extractor match &signature.extractor { None => { signature_info.has_extractor = false; signature_info.extractor = "None".to_string(); } Some(extractor) => { signature_info.has_extractor = true; match &extractor.utility { extractors::common::ExtractorType::External(command) => { signature_info.extractor = command.to_string(); } extractors::common::ExtractorType::Internal(_) => { signature_info.extractor = "Built-in".to_string(); } extractors::common::ExtractorType::None => error!( "An invalid extractor type exists for the '{}' signature", signature.description ), } } } // Increment signature count signature_count += 1; // If there is an extractor for this signature, increment extractor count if signature_info.has_extractor { extractor_count += 1; } // Keep signature descriptions in a separate list, which wil be sorted alphabetically for display sorted_descriptions.push(signature_info.description.clone()); // Lookup table associating signature descriptions with their SignatureInfo struct signature_lookup.insert(signature.description.clone(), signature_info.clone()); } // Sort signature descriptions alphabetically sorted_descriptions.sort_by_key(|description| description.to_lowercase()); // Print signatures, sorted alphabetically by description for description in sorted_descriptions { let siginfo = &signature_lookup[&description]; let display_line = format!( "{}{}{}", pad_to_length(&description, COLUMN1_WIDTH), pad_to_length(&siginfo.name, COLUMN2_WIDTH), siginfo.extractor ); if siginfo.is_short { println!("{}", display_line.yellow()); } else { println!("{}", display_line.green()); } } print_delimiter(); println!(); println!("Total signatures: {signature_count}"); println!("Extractable signatures: {extractor_count}"); } pub fn print_stats( quiet: bool, run_time: time::Instant, file_count: usize, signature_count: usize, pattern_count: usize, ) { const MS_IN_A_SECOND: f64 = 1000.0; const SECONDS_IN_A_MINUTE: f64 = 60.0; const MINUTES_IN_AN_HOUR: f64 = 60.0; let mut file_plural = ""; let mut units = "milliseconds"; let mut display_time: f64 = run_time.elapsed().as_millis() as f64; if quiet { return; } // Format the output time in a more human-readable manner if display_time >= MS_IN_A_SECOND { display_time /= MS_IN_A_SECOND; units = "seconds"; if display_time >= SECONDS_IN_A_MINUTE { display_time /= SECONDS_IN_A_MINUTE; units = "minutes"; if display_time >= MINUTES_IN_AN_HOUR { display_time /= MINUTES_IN_AN_HOUR; units = "hours"; } } } if file_count != 1 { file_plural = "s"; } println!( "Analyzed {file_count} file{file_plural} for {signature_count} file signatures ({pattern_count} magic patterns) in {display_time:.1} {units}" ); } pub fn print_plain(quiet: bool, msg: &str) { if !quiet { print!("{msg}"); let _ = io::stdout().flush(); } } pub fn println_plain(quiet: bool, msg: &str) { if !quiet { println!("{msg}"); } } ================================================ FILE: src/entropy.rs ================================================ use crate::common::read_input; use entropy::shannon_entropy; use plotly::layout::{Axis, Layout}; use plotly::{ImageFormat, Plot, Scatter}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone)] pub struct EntropyError; #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct BlockEntropy { pub end: usize, pub start: usize, pub entropy: f32, } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct FileEntropy { pub file: String, pub blocks: Vec, } /// Splits the supplied data up into blocks and calculates the entropy of each block. fn blocks(data: &[u8]) -> Vec { const BLOCK_COUNT: usize = 2048; let mut offset: usize = 0; let block_size = if data.len() < BLOCK_COUNT { data.len() } else { data.len() / BLOCK_COUNT }; let mut chunker = data.chunks(block_size); let mut entropy_blocks: Vec = vec![]; loop { match chunker.next() { None => break, Some(block_data) => { let mut block = BlockEntropy { ..Default::default() }; block.start = offset; block.entropy = shannon_entropy(block_data); block.end = block.start + block_data.len(); offset = block.end; entropy_blocks.push(block); } } } entropy_blocks } pub fn plot( file_path: impl Into, stdin: bool, out_file: Option, ) -> Result { let mut x: Vec = Vec::new(); let mut y: Vec = Vec::new(); let target_file: String = file_path.into(); let mut file_entropy = FileEntropy { file: target_file.clone(), ..Default::default() }; // Read in the target file data if let Ok(file_data) = read_input(&target_file, stdin) { // Calculate the entropy of each file block file_entropy.blocks = blocks(&file_data); for block in &file_entropy.blocks { x.push(block.start); x.push(block.end); y.push(block.entropy); y.push(block.entropy); } let mut plot = Plot::new(); let trace = Scatter::new(x, y); let layout = Layout::new() .title("Entropy Graph") .x_axis(Axis::new().title("File Offset")) .y_axis(Axis::new().title("Entropy").range(vec![0, 8])); plot.add_trace(trace); plot.set_layout(layout); match out_file { None => plot.show(), Some(out_file_name) => { // TODO: Switch to plotly_static, which is the recommended way to do this #[allow(deprecated)] plot.write_image(&out_file_name, ImageFormat::PNG, 2048, 1024, 1.0); } } return Ok(file_entropy); } Err(EntropyError) } ================================================ FILE: src/extractors/androidsparse.rs ================================================ use crate::common::is_offset_safe; use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::androidsparse; /// Defines the internal extractor function for extracting Android Sparse files /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::androidsparse::android_sparse_extractor; /// /// match android_sparse_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn android_sparse_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(extract_android_sparse), ..Default::default() } } /// Android sparse internal extractor pub fn extract_android_sparse( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const OUTFILE_NAME: &str = "unsparsed.img"; let mut result = ExtractionResult { ..Default::default() }; // Parse the sparse file header if let Ok(sparse_header) = androidsparse::parse_android_sparse_header(&file_data[offset..]) { let available_data: usize = file_data.len(); let mut last_chunk_offset: Option = None; let mut processed_chunk_count: usize = 0; let mut next_chunk_offset: usize = offset + sparse_header.header_size; while is_offset_safe(available_data, next_chunk_offset, last_chunk_offset) { // Parse the next chunk's header match androidsparse::parse_android_sparse_chunk_header(&file_data[next_chunk_offset..]) { Err(_) => { break; } Ok(chunk_header) => { // If not a dry run, extract the data from the next chunk if output_directory.is_some() { let chroot = Chroot::new(output_directory); let chunk_data_start: usize = next_chunk_offset + chunk_header.header_size; let chunk_data_end: usize = chunk_data_start + chunk_header.data_size; if let Some(chunk_data) = file_data.get(chunk_data_start..chunk_data_end) { if !extract_chunk( &sparse_header, &chunk_header, chunk_data, OUTFILE_NAME, &chroot, ) { break; } } else { break; } } processed_chunk_count += 1; last_chunk_offset = Some(next_chunk_offset); next_chunk_offset += chunk_header.header_size + chunk_header.data_size; } } } // Make sure the number of processed chunks equals the number of chunks reported in the sparse flie header if processed_chunk_count == sparse_header.chunk_count { result.success = true; result.size = Some(next_chunk_offset - offset); } } result } // Extract a sparse file chunk to disk fn extract_chunk( sparse_header: &androidsparse::AndroidSparseHeader, chunk_header: &androidsparse::AndroidSparseChunkHeader, chunk_data: &[u8], outfile: &str, chroot: &Chroot, ) -> bool { if chunk_header.is_raw { // Raw chunks are just data chunks stored verbatim if !chroot.append_to_file(outfile, chunk_data) { return false; } } else if chunk_header.is_fill { // Fill chunks are block_count blocks that contain a repeated sequence of data (typically 4-bytes repeated over and over again) for _ in 0..chunk_header.block_count { let mut i = 0; let mut fill_block: Vec = vec![]; // Fill each block with the repeated data while i < sparse_header.block_size { fill_block.extend(chunk_data); i += chunk_data.len(); } // Append fill block to file if !chroot.append_to_file(outfile, &fill_block) { return false; } } } else if chunk_header.is_dont_care { let mut null_block: Vec = vec![]; // Build a block full of NULL bytes while null_block.len() < sparse_header.block_size { null_block.push(0); } // Write block_count NULL blocks to disk for _ in 0..chunk_header.block_count { if !chroot.append_to_file(outfile, &null_block) { return false; } } } true } ================================================ FILE: src/extractors/arcadyan.rs ================================================ use crate::extractors::common::{ExtractionResult, Extractor, ExtractorType}; use crate::extractors::lzma::lzma_decompress; /// Defines the internal extractor for Arcadyn Obfuscated LZMA /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::arcadyan::obfuscated_lzma_extractor; /// /// match obfuscated_lzma_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn obfuscated_lzma_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(extract_obfuscated_lzma), ..Default::default() } } /// Internal extractor for Arcadyn Obfuscated LZMA pub fn extract_obfuscated_lzma( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const LZMA_DATA_OFFSET: usize = 4; const MIN_DATA_SIZE: usize = 0x100; const MAX_DATA_SIZE: usize = 0x1B0000; let mut result = ExtractionResult { ..Default::default() }; let available_data: usize = file_data.len() - offset; // Sanity check data size if available_data <= MAX_DATA_SIZE && available_data > MIN_DATA_SIZE { // De-obfuscate the LZMA data let deobfuscated_data = arcadyan_deobfuscator(&file_data[offset..]); // Do a decompression on the LZMA data (actual LZMA data starts 4 bytes into the deobfuscated data) result = lzma_decompress(&deobfuscated_data, LZMA_DATA_OFFSET, output_directory); } result } fn arcadyan_deobfuscator(obfuscated_data: &[u8]) -> Vec { const BLOCK_SIZE: usize = 32; const P1_START: usize = 0; const P1_END: usize = 4; const BLOCK1_START: usize = P1_END; const BLOCK1_END: usize = BLOCK1_START + BLOCK_SIZE; const P2_START: usize = BLOCK1_END; const P2_END: usize = 0x68; const BLOCK2_START: usize = P2_END; const BLOCK2_END: usize = BLOCK2_START + BLOCK_SIZE; const P3_START: usize = BLOCK2_END; let mut deobfuscated_data: Vec = vec![]; // Get the "parts" and "blocks" of the obfuscated header let p1 = obfuscated_data[P1_START..P1_END].to_vec(); let b1 = obfuscated_data[BLOCK1_START..BLOCK1_END].to_vec(); let p2 = obfuscated_data[P2_START..P2_END].to_vec(); let b2 = obfuscated_data[BLOCK2_START..BLOCK2_END].to_vec(); let p3 = obfuscated_data[P3_START..].to_vec(); // Swap "block1" and "block2" deobfuscated_data.extend(p1); deobfuscated_data.extend(b2); deobfuscated_data.extend(p2); deobfuscated_data.extend(b1); deobfuscated_data.extend(p3); // Nibble swap each byte in what is now "block1" for swapped_byte in deobfuscated_data .iter_mut() .take(BLOCK1_END) .skip(BLOCK1_START) { *swapped_byte = ((*swapped_byte & 0x0F) << 4) + ((*swapped_byte & 0xF0) >> 4); } let mut i: usize = BLOCK1_START; // Byte swap each byte in what is now "block1" while i < BLOCK1_END { let b1 = deobfuscated_data[i]; let b2 = deobfuscated_data[i + 1]; deobfuscated_data[i] = b2; deobfuscated_data[i + 1] = b1; i += 2; } deobfuscated_data } ================================================ FILE: src/extractors/autel.rs ================================================ use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::autel::parse_autel_header; const BLOCK_SIZE: usize = 256; /// Defines the internal extractor function for deobfuscating Autel firmware /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::autel::autel_extractor; /// /// match autel_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn autel_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(autel_deobfuscate), ..Default::default() } } /// Internal extractor for obfuscated Autel firmware /// https://gist.github.com/sector7-nl/3fc815cd2497817ad461bfbd393294cb pub fn autel_deobfuscate( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const OUTPUT_FILE_NAME: &str = "autel.decoded"; let mut result = ExtractionResult { ..Default::default() }; // Parse and validate the header if let Ok(autel_header) = parse_autel_header(&file_data[offset..]) { // Get the start and end offsets of the actual encoded data let data_start = offset + autel_header.header_size; let data_end = data_start + autel_header.data_size; // Get the encoded data if let Some(autel_data) = file_data.get(data_start..data_end) { // Interate through each block of the encoded data let mut block_iter = autel_data.chunks(BLOCK_SIZE); loop { match block_iter.next() { None => { // EOF result.size = Some(autel_header.data_size); result.success = true; break; } Some(block_bytes) => { // Decode the data block let decoded_block = decode_autel_block(block_bytes); // Write to file, if requested if output_directory.is_some() { let chroot = Chroot::new(output_directory); if !chroot.append_to_file(OUTPUT_FILE_NAME, &decoded_block) { break; } } } } } } } result } /// Block decoder for autel encoded firmware. /// block_data *must* be 256 bytes in size, or less. fn decode_autel_block(block_data: &[u8]) -> Vec { // Lookup table for encoding/decoding bytes let encoding_table: Vec<(usize, usize)> = vec![ (54, 147), (96, 129), (59, 193), (191, 0), (45, 130), (96, 144), (27, 129), (152, 0), (44, 180), (118, 141), (115, 129), (210, 0), (13, 164), (27, 133), (20, 192), (139, 0), (28, 166), (17, 133), (19, 193), (224, 0), (20, 161), (145, 0), (14, 193), (12, 132), (18, 161), (17, 140), (29, 192), (246, 0), (115, 178), (28, 132), (155, 0), (12, 132), (31, 165), (20, 136), (27, 193), (142, 0), (96, 164), (18, 133), (145, 0), (23, 132), (13, 165), (13, 148), (23, 193), (19, 132), (27, 178), (83, 137), (146, 0), (145, 0), (18, 166), (96, 148), (13, 193), (159, 0), (96, 166), (20, 129), (20, 193), (27, 132), (9, 160), (96, 148), (13, 192), (159, 0), (96, 180), (142, 0), (31, 193), (155, 0), (7, 166), (224, 0), (20, 192), (27, 132), (28, 160), (17, 149), (19, 193), (96, 132), (76, 164), (208, 0), (80, 192), (78, 132), (96, 160), (27, 144), (24, 193), (140, 0), (96, 178), (17, 141), (12, 193), (224, 0), (14, 161), (17, 141), (151, 0), (14, 132), (16, 165), (96, 137), (13, 193), (155, 0), (20, 161), (29, 141), (23, 192), (24, 132), (27, 178), (10, 133), (96, 192), (140, 0), (14, 180), (17, 133), (16, 192), (144, 0), (11, 163), (13, 141), (96, 192), (17, 132), (12, 178), (96, 141), (28, 192), (27, 132), (27, 130), (18, 141), (96, 193), (31, 132), (96, 181), (13, 140), (23, 193), (224, 0), (27, 166), (142, 0), (27, 192), (24, 132), (12, 183), (96, 133), (84, 192), (14, 132), (27, 178), (10, 140), (155, 0), (9, 132), (17, 160), (56, 133), (96, 192), (82, 132), (13, 160), (27, 137), (20, 193), (139, 0), (28, 161), (145, 0), (19, 192), (118, 132), (115, 165), (20, 132), (145, 0), (14, 132), (12, 167), (146, 0), (17, 193), (29, 132), (96, 176), (28, 144), (27, 193), (140, 0), (31, 180), (148, 0), (27, 192), (14, 132), (83, 160), (18, 137), (17, 193), (23, 132), (13, 165), (13, 145), (151, 0), (147, 0), (27, 178), (96, 137), (19, 193), (159, 0), (14, 160), (25, 148), (17, 193), (142, 0), (16, 180), (27, 136), (14, 193), (224, 0), (17, 178), (12, 144), (224, 0), (28, 132), (27, 160), (13, 141), (11, 193), (96, 132), (27, 165), (30, 140), (224, 0), (146, 0), (31, 165), (29, 129), (96, 192), (140, 0), (31, 161), (24, 145), (140, 0), (96, 132), (27, 165), (29, 140), (31, 192), (154, 0), (14, 161), (27, 145), (140, 0), (18, 132), (23, 167), (96, 140), (21, 129), (14, 132), (17, 165), (9, 137), (12, 193), (155, 0), (18, 161), (96, 141), (27, 192), (148, 0), (29, 178), (23, 133), (24, 192), (155, 0), (10, 180), (96, 133), (28, 192), (14, 132), (31, 130), (28, 129), (18, 193), (31, 132), (12, 180), (13, 144), (96, 193), (31, 132), (96, 160), (13, 141), (27, 193), (18, 132), (23, 181), (26, 140), (27, 193), (156, 0), (96, 166), (79, 141), (211, 0), (76, 132), (77, 160), (75, 133), (206, 0), (182, 0), (96, 129), (59, 133), (191, 0), (173, 0), ]; assert!(block_data.len() <= BLOCK_SIZE); let mut decoded_block: Vec = vec![]; for (i, byte) in block_data.iter().enumerate() { let encoding = encoding_table[i]; decoded_block.push(((((*byte as usize) + encoding.0) ^ encoding.1) % 256) as u8); } decoded_block } ================================================ FILE: src/extractors/bmp.rs ================================================ use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::bmp::{get_dib_header_size, parse_bmp_file_header}; /// Defines the internal extractor function for carving out GIF images /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::bmp::bmp_extractor; /// /// match bmp_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn bmp_extractor() -> Extractor { Extractor { do_not_recurse: true, utility: ExtractorType::Internal(extract_bmp_image), ..Default::default() } } pub fn extract_bmp_image( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const OUTFILE_NAME: &str = "image.bmp"; let mut result = ExtractionResult { ..Default::default() }; result.success = false; // Parse the bmp_file_header if let Ok(bmp_file_header) = parse_bmp_file_header(&file_data[offset..]) { // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapfileheader // The size of the BMP file header const BMP_FILE_HEADER_SIZE: usize = 14; // Retrieve the size of the header following the BMP file header if let Ok(bmp_header_size) = get_dib_header_size(&file_data[(offset + BMP_FILE_HEADER_SIZE)..]) { // The offset that points to the image data cannot point into the second header if bmp_file_header.bitmap_bits_offset >= (BMP_FILE_HEADER_SIZE + bmp_header_size) { // If it was parsed successfully, get the file size result.size = Some(bmp_file_header.size); result.success = true; if output_directory.is_some() { let chroot = Chroot::new(output_directory); result.success = chroot.carve_file(OUTFILE_NAME, file_data, offset, bmp_file_header.size); } } } } result } ================================================ FILE: src/extractors/bzip2.rs ================================================ use crate::common::is_offset_safe; use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use bzip2::{Decompress, Status}; /// Defines the internal extractor function for decompressing BZIP2 files /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::bzip2::bzip2_extractor; /// /// match bzip2_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn bzip2_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(bzip2_decompressor), ..Default::default() } } /// Internal extractor for decompressing BZIP2 data pub fn bzip2_decompressor( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { // Size of decompression buffer const BLOCK_SIZE: usize = 900 * 1024; // Output file for decompressed data const OUTPUT_FILE_NAME: &str = "decompressed.bin"; let mut result = ExtractionResult { ..Default::default() }; let mut bytes_written: usize = 0; let mut stream_offset: usize = 0; let bzip2_data = &file_data[offset..]; let mut decompressed_buffer = [0; BLOCK_SIZE]; let mut decompressor = Decompress::new(false); let available_data = bzip2_data.len(); let mut previous_offset = None; /* * Loop through all compressed data and decompress it. * * This has a significant performance hit since 1) decompression takes time, and 2) data is * decompressed once during signature validation and a second time during extraction (if extraction * was requested). * * The advantage is that not only are we 100% sure that this data is valid BZIP2 data, but we * can also determine the exact size of the BZIP2 data. */ while is_offset_safe(available_data, stream_offset, previous_offset) { previous_offset = Some(stream_offset); // Decompress a block of data match decompressor.decompress(&bzip2_data[stream_offset..], &mut decompressed_buffer) { Err(_) => { // Break on decompression error break; } Ok(status) => { match status { Status::RunOk => break, Status::FlushOk => break, Status::FinishOk => break, Status::MemNeeded => break, Status::Ok => { stream_offset = decompressor.total_in() as usize; } Status::StreamEnd => { result.success = true; result.size = Some(decompressor.total_in() as usize); } } // Decompressed a block of data, if extraction was requested write the decompressed block to the output file if output_directory.is_some() { let n: usize = (decompressor.total_out() as usize) - bytes_written; let chroot = Chroot::new(output_directory); if !chroot.append_to_file(OUTPUT_FILE_NAME, &decompressed_buffer[0..n]) { // If writing data to file fails, break break; } bytes_written += n; } // If everything has been processed successfully, we're done; break. if result.success { break; } } } } result } ================================================ FILE: src/extractors/cab.rs ================================================ use crate::extractors; /// Describes how to run the cabextract utility to extract MS CAB archives /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::cab::cab_extractor; /// /// match cab_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn cab_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("cabextract".to_string()), extension: "cab".to_string(), arguments: vec![extractors::common::SOURCE_FILE_PLACEHOLDER.to_string()], exit_codes: vec![0], ..Default::default() } } ================================================ FILE: src/extractors/common.rs ================================================ use crate::signatures::common::SignatureResult; use log::{debug, error, info, warn}; use serde::{Deserialize, Serialize}; use std::fs; use std::io::Write; use std::path; use std::process; use walkdir::WalkDir; #[cfg(windows)] use std::os::windows; #[cfg(unix)] use std::os::unix; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; /// This contstant in command line arguments will be replaced with the path to the input file pub const SOURCE_FILE_PLACEHOLDER: &str = "%e"; /// Return value of InternalExtractor upon error #[derive(Debug, Clone)] pub struct ExtractionError; /// Built-in internal extractors must provide a function conforming to this definition. /// Arguments: file_data, offset, output_directory. pub type InternalExtractor = fn(&[u8], usize, Option<&str>) -> ExtractionResult; /// Enum to define either an Internal or External extractor type #[derive(Debug, Default, Clone, Eq, PartialEq, Ord, PartialOrd)] pub enum ExtractorType { External(String), Internal(InternalExtractor), #[default] None, } /// Describes extractors, both external and internal #[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct Extractor { /// External command or internal function to execute pub utility: ExtractorType, /// File extension expected by an external command pub extension: String, /// Arguments to pass to the external command pub arguments: Vec, /// A list of successful exit codes for the external command pub exit_codes: Vec, /// Set to true to disable recursion into this extractor's extracted files pub do_not_recurse: bool, } /// Stores information about a completed extraction #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct ExtractionResult { /// Size of the data consumed during extraction, if known; should be populated by the constructor pub size: Option, /// Extractor success status; should be populated by the constructor pub success: bool, /// Extractor name, automatically populated by extractors::common::execute pub extractor: String, /// Set to true to disable recursion into this extractor's extracted files. /// Automatically populated with the corresponding Extractor.do_not_recurse field by extractors::common::execute. pub do_not_recurse: bool, /// The output directory where the extractor dropped its files, automatically populated by extractors::common::execute pub output_directory: String, } /// Stores information about external extractor processes. For internal use only. #[derive(Debug)] pub struct ProcInfo { pub child: process::Child, pub exit_codes: Vec, pub carved_file: String, } /// Provides chroot-like functionality for internal extractors #[derive(Debug, Default, Clone)] pub struct Chroot { /// The chroot directory passed to Chroot::new pub chroot_directory: String, } impl Chroot { /// Create a new chrooted instance. All file paths will be effectively chrooted in the specified directory path. /// The chroot directory path will be created if it does not already exist. /// /// If no directory path is specified, the chroot directory will be `/`. /// /// ## Example /// /// ``` /// use binwalk::extractors::common::Chroot; /// /// let chroot_dir = std::path::Path::new(&std::env::temp_dir()) /// .join("binwalk_unit_tests") /// .display() /// .to_string(); /// /// # std::fs::remove_dir_all(&chroot_dir); /// let chroot = Chroot::new(Some(&chroot_dir)); /// /// assert_eq!(&chroot.chroot_directory, &chroot_dir); /// assert_eq!(std::path::Path::new(&chroot_dir).exists(), true); /// # std::fs::remove_dir_all(&chroot_dir); /// ``` pub fn new(chroot_directory: Option<&str>) -> Chroot { let mut chroot_instance = Chroot { ..Default::default() }; match chroot_directory { None => { // Default path is '/' chroot_instance.chroot_directory = path::MAIN_SEPARATOR.to_string(); } Some(chroot_dir) => { // Attempt to ensure that the specified path is absolute. If this fails, just use the path as given. match path::absolute(chroot_dir) { Ok(pathbuf) => { chroot_instance.chroot_directory = pathbuf.display().to_string(); } Err(_) => { chroot_instance.chroot_directory = chroot_dir.to_string(); } } } } // Create the chroot directory if it does not exist if !path::Path::new(&chroot_instance.chroot_directory).exists() { match fs::create_dir_all(&chroot_instance.chroot_directory) { Ok(_) => { debug!( "Created new chroot directory {}", chroot_instance.chroot_directory ); } Err(e) => { error!( "Failed to create chroot directory {}: {}", chroot_instance.chroot_directory, e ); } } } chroot_instance } /// Joins two paths, ensuring that the final path does not traverse outside of the chroot directory. /// /// ## Example /// /// ``` /// use binwalk::extractors::common::Chroot; /// use std::path::MAIN_SEPARATOR; /// /// let chroot_dir = std::path::Path::new(&std::env::temp_dir()) /// .join("binwalk_unit_tests") /// .display() /// .to_string(); /// /// # std::fs::remove_dir_all(&chroot_dir); /// let chroot = Chroot::new(Some(&chroot_dir)); /// /// let dir_name = "etc"; /// let file_name = "passwd"; /// let abs_path = format!("{}{}{}{}", MAIN_SEPARATOR, dir_name, MAIN_SEPARATOR, file_name); /// let abs_path_dir = format!("{}{}", MAIN_SEPARATOR, dir_name); /// let rel_path_dir = format!("..{}..{}..{}{}", MAIN_SEPARATOR, MAIN_SEPARATOR, MAIN_SEPARATOR, dir_name); /// let abs_path_file = format!("{}{}", MAIN_SEPARATOR, file_name); /// let rel_path_file = format!("..{}..{}..{}{}", MAIN_SEPARATOR, MAIN_SEPARATOR, MAIN_SEPARATOR, file_name); /// /// let path1 = chroot.safe_path_join(&abs_path_dir, file_name); /// let expected_path1 = std::path::Path::new(&chroot_dir).join(dir_name).join(file_name); /// /// let path2 = chroot.safe_path_join(&abs_path_dir, &rel_path_file); /// let expected_path2 = std::path::Path::new(&chroot_dir).join(file_name); /// /// let path3 = chroot.safe_path_join(&rel_path_dir, &abs_path_file); /// let expected_path3 = std::path::Path::new(&chroot_dir).join(dir_name).join(file_name); /// /// let path4 = chroot.safe_path_join(&chroot_dir, &abs_path); /// let expected_path4 = std::path::Path::new(&chroot_dir).join(dir_name).join(file_name); /// /// assert_eq!(path1, expected_path1.display().to_string()); /// assert_eq!(path2, expected_path2.display().to_string()); /// assert_eq!(path3, expected_path3.display().to_string()); /// assert_eq!(path4, expected_path4.display().to_string()); /// # std::fs::remove_dir_all(&chroot_dir); /// ``` pub fn safe_path_join(&self, path1: impl Into, path2: impl Into) -> String { // Join and sanitize both paths; retain the leading '/' (if there is one) let mut joined_path: String = self.sanitize_path( &format!("{}{}{}", path1.into(), path::MAIN_SEPARATOR, path2.into()), true, ); // If the joined path does not start with the chroot directory, // prepend the chroot directory to the final joined path. // on Windows: If no chroot directory is specified, skip the operation if cfg!(windows) && self.chroot_directory == path::MAIN_SEPARATOR.to_string() { // do nothing and skip } else if !joined_path.starts_with(&self.chroot_directory) { joined_path = format!( "{}{}{}", self.chroot_directory, path::MAIN_SEPARATOR, joined_path ); } self.strip_double_slash(&joined_path) } /// Given a file path, returns a sanitized path that is chrooted inside the specified chroot directory. /// /// ## Example /// /// ``` /// use binwalk::extractors::common::Chroot; /// /// let chroot_dir = std::path::Path::new(&std::env::temp_dir()) /// .join("binwalk_unit_tests") /// .display() /// .to_string(); /// /// let file_name = "test.txt"; /// /// let chroot = Chroot::new(Some(&chroot_dir)); /// let path = chroot.chrooted_path(file_name); /// /// assert_eq!(path, std::path::Path::new(&chroot_dir).join(file_name).display().to_string()); /// ``` pub fn chrooted_path(&self, file_path: impl Into) -> String { self.safe_path_join(file_path, "".to_string()) } /// Creates a regular file in the chrooted directory and writes the provided data to it. /// /// ## Example /// /// ``` /// # fn main() { #[allow(non_snake_case)] fn _doctest_main_src_extractors_common_rs_213_0() -> Result<(), Box> { /// use binwalk::extractors::common::Chroot; /// /// let file_data: &[u8] = b"foobar"; /// /// let file_name = "created_file.txt"; /// /// let chroot_dir = std::path::Path::new("tests") /// .join("binwalk_unit_tests") /// .display() /// .to_string(); /// /// # std::fs::remove_dir_all(&chroot_dir); /// let chroot = Chroot::new(Some(&chroot_dir)); /// /// assert_eq!(chroot.create_file(file_name, file_data), true); /// assert_eq!(std::fs::read_to_string(std::path::Path::new(&chroot_dir).join(file_name))?, std::str::from_utf8(file_data)?); /// # std::fs::remove_dir_all(&chroot_dir); /// # Ok(()) /// # } _doctest_main_src_extractors_common_rs_213_0(); } /// ``` pub fn create_file(&self, file_path: impl Into, file_data: &[u8]) -> bool { let safe_file_path: String = self.chrooted_path(file_path); if !path::Path::new(&safe_file_path).exists() { match fs::write(safe_file_path.clone(), file_data) { Ok(_) => { return true; } Err(e) => { error!("Failed to write data to {safe_file_path}: {e}"); } } } else { error!("Failed to create file {safe_file_path}: path already exists"); } false } /// Carve data and write it to a new file. /// /// ## Example /// /// ``` /// # fn main() { #[allow(non_snake_case)] fn _doctest_main_src_extractors_common_rs_255_0() -> Result<(), Box> { /// use binwalk::extractors::common::Chroot; /// /// const CARVE_SIZE: usize = 6; /// /// let data: &[u8] = b"foobarJUNK"; /// /// let file_name = "carved_file.txt"; /// /// let chroot_dir = std::path::Path::new("tests") /// .join("binwalk_unit_tests") /// .display() /// .to_string(); /// /// # std::fs::remove_dir_all(&chroot_dir); /// let chroot = Chroot::new(Some(&chroot_dir)); /// /// assert_eq!(chroot.carve_file(file_name, data, 0, CARVE_SIZE), true); /// assert_eq!(std::fs::read_to_string(std::path::Path::new(&chroot_dir).join(file_name))?, std::str::from_utf8(&data[0..CARVE_SIZE])?); /// # std::fs::remove_dir_all(&chroot_dir); /// # Ok(()) /// } _doctest_main_src_extractors_common_rs_255_0(); } /// ``` pub fn carve_file( &self, file_path: impl Into, data: &[u8], start: usize, size: usize, ) -> bool { let mut retval: bool = false; if let Some(file_data) = data.get(start..start + size) { retval = self.create_file(file_path, file_data); } else { error!( "Failed to create file {}: data offset/size are invalid", file_path.into() ); } retval } /// Creates a device file in the chroot directory. /// /// Note that this does *not* create a real device file, just a regular file containing the device file info. fn create_device( &self, file_path: impl Into, device_type: &str, major: usize, minor: usize, ) -> bool { let device_file_contents: String = format!("{device_type} {major} {minor}"); self.create_file(file_path, &device_file_contents.clone().into_bytes()) } /// Creates a character device file in the chroot directory. /// /// Note that this does *not* create a real character device, just a regular file containing the text `c `. /// /// ## Example /// /// ``` /// # fn main() { #[allow(non_snake_case)] fn _doctest_main_src_extractors_common_rs_312_0() -> Result<(), Box> { /// use binwalk::extractors::common::Chroot; /// /// let chroot_dir = std::path::Path::new("tests") /// .join("binwalk_unit_tests") /// .display() /// .to_string(); /// /// let dev_major: usize = 1; /// let dev_minor: usize = 2; /// let file_name = "char_device"; /// /// # std::fs::remove_dir_all(&chroot_dir); /// let chroot = Chroot::new(Some(&chroot_dir)); /// /// assert_eq!(chroot.create_character_device(file_name, dev_major, dev_minor), true); /// assert_eq!(std::fs::read_to_string(std::path::Path::new(&chroot_dir).join(file_name))?, "c 1 2"); /// # std::fs::remove_dir_all(&chroot_dir); /// # Ok(()) /// # } _doctest_main_src_extractors_common_rs_312_0(); } /// ``` pub fn create_character_device( &self, file_path: impl Into, major: usize, minor: usize, ) -> bool { self.create_device(file_path, "c", major, minor) } /// Creates a block device file in the chroot directory. /// /// Note that this does *not* create a real block device, just a regular file containing the text `b `. /// /// ## Example /// /// ``` /// # fn main() { #[allow(non_snake_case)] fn _doctest_main_src_extractors_common_rs_345_0() -> Result<(), Box> { /// use binwalk::extractors::common::Chroot; /// /// let chroot_dir = std::path::Path::new("tests") /// .join("binwalk_unit_tests") /// .display() /// .to_string(); /// /// let dev_major: usize = 1; /// let dev_minor: usize = 2; /// let file_name = "block_device"; /// /// # std::fs::remove_dir_all(&chroot_dir); /// let chroot = Chroot::new(Some(&chroot_dir)); /// /// assert_eq!(chroot.create_block_device(file_name, dev_major, dev_minor), true); /// assert_eq!(std::fs::read_to_string(std::path::Path::new(&chroot_dir).join(file_name))?, "b 1 2"); /// # std::fs::remove_dir_all(&chroot_dir); /// # Ok(()) /// # } _doctest_main_src_extractors_common_rs_345_0(); } /// ``` pub fn create_block_device( &self, file_path: impl Into, major: usize, minor: usize, ) -> bool { self.create_device(file_path, "b", major, minor) } /// Creates a fifo file in the chroot directory. /// /// Note that this does *not* create a real fifo, just a regular file containing the text `fifo`. /// /// ## Example /// /// ``` /// # fn main() { #[allow(non_snake_case)] fn _doctest_main_src_extractors_common_rs_377_0() -> Result<(), Box> { /// use binwalk::extractors::common::Chroot; /// /// let chroot_dir = std::path::Path::new("tests") /// .join("binwalk_unit_tests") /// .display() /// .to_string(); /// /// let file_name = "fifo_file"; /// /// # std::fs::remove_dir_all(&chroot_dir); /// let chroot = Chroot::new(Some(&chroot_dir)); /// /// assert_eq!(chroot.create_fifo(file_name), true); /// assert_eq!(std::fs::read_to_string(std::path::Path::new(&chroot_dir).join(file_name))?, "fifo"); /// # std::fs::remove_dir_all(&chroot_dir); /// # Ok(()) /// # } _doctest_main_src_extractors_common_rs_377_0(); } /// ``` pub fn create_fifo(&self, file_path: impl Into) -> bool { self.create_file(file_path, b"fifo") } /// Creates a socket file in the chroot directory. /// /// Note that this does *not* create a real socket, just a regular file containing the text `socket`. /// /// ## Example /// /// ``` /// # fn main() { #[allow(non_snake_case)] fn _doctest_main_src_extractors_common_rs_401_0() -> Result<(), Box> { /// use binwalk::extractors::common::Chroot; /// /// let chroot_dir = std::path::Path::new("tests") /// .join("binwalk_unit_tests") /// .display() /// .to_string(); /// /// let file_name = "socket_file"; /// /// # std::fs::remove_dir_all(&chroot_dir); /// let chroot = Chroot::new(Some(&chroot_dir)); /// /// assert_eq!(chroot.create_socket(file_name), true); /// assert_eq!(std::fs::read_to_string(std::path::Path::new(&chroot_dir).join(file_name))?, "socket"); /// # std::fs::remove_dir_all(&chroot_dir); /// # Ok(()) /// # } _doctest_main_src_extractors_common_rs_401_0(); } /// ``` pub fn create_socket(&self, file_path: impl Into) -> bool { self.create_file(file_path, b"socket") } /// Append the provided data to the specified file in the chroot directory. /// /// If the specified file does not exist, it will be created. /// /// ## Example /// /// ``` /// # fn main() { #[allow(non_snake_case)] fn _doctest_main_src_extractors_common_rs_426_0() -> Result<(), Box> { /// use binwalk::extractors::common::Chroot; /// /// let chroot_dir = std::path::Path::new("tests") /// .join("binwalk_unit_tests") /// .display() /// .to_string(); /// /// let file_data: &[u8] = b"foobar"; /// let file_name = "append.txt"; /// /// # std::fs::remove_dir_all(&chroot_dir); /// let chroot = Chroot::new(Some(&chroot_dir)); /// /// assert_eq!(chroot.append_to_file(file_name, file_data), true); /// assert_eq!(std::fs::read_to_string(std::path::Path::new(&chroot_dir).join(file_name))?, std::str::from_utf8(file_data)?); /// # std::fs::remove_dir_all(&chroot_dir); /// # Ok(()) /// # } _doctest_main_src_extractors_common_rs_426_0(); } /// ``` pub fn append_to_file(&self, file_path: impl Into, data: &[u8]) -> bool { let safe_file_path: String = self.chrooted_path(file_path); if !self.is_symlink(&safe_file_path) { match fs::OpenOptions::new() .create(true) .append(true) .open(safe_file_path.clone()) { Err(e) => { error!("Failed to open file '{safe_file_path}' for appending: {e}"); } Ok(mut fp) => match fp.write(data) { Err(e) => { error!("Failed to append to file '{safe_file_path}': {e}"); } Ok(_) => { return true; } }, } } else { error!("Attempted to append data to a symlink: {safe_file_path}"); } false } /// Creates a directory in the chroot directory. /// /// Equivalent to mkdir -p. /// /// ## Example /// /// ``` /// use binwalk::extractors::common::Chroot; /// /// let chroot_dir = std::path::Path::new("tests") /// .join("binwalk_unit_tests") /// .display() /// .to_string(); /// /// let dir_name = "my_directory"; /// /// # std::fs::remove_dir_all(&chroot_dir); /// let chroot = Chroot::new(Some(&chroot_dir)); /// /// assert_eq!(chroot.create_directory(dir_name), true); /// assert_eq!(std::path::Path::new(&chroot_dir).join(dir_name).exists(), true); /// # std::fs::remove_dir_all(&chroot_dir); /// ``` pub fn create_directory(&self, dir_path: impl Into) -> bool { let safe_dir_path: String = self.chrooted_path(dir_path); match fs::create_dir_all(safe_dir_path.clone()) { Ok(_) => { return true; } Err(e) => { error!("Failed to create output directory {safe_dir_path}: {e}"); } } false } /// Delete a directory in the chroot directory. /// /// Equivalent to rm -rf. /// /// ## Example /// /// ``` /// use binwalk::extractors::common::Chroot; /// /// let chroot_dir = std::path::Path::new("tests") /// .join("binwalk_unit_tests") /// .display() /// .to_string(); /// /// let dir_name = "my_directory"; /// /// # std::fs::remove_dir_all(&chroot_dir); /// let chroot = Chroot::new(Some(&chroot_dir)); /// /// assert_eq!(chroot.create_directory(dir_name), true); /// assert_eq!(chroot.remove_directory(dir_name), true); /// assert_eq!(chroot.remove_directory("i_dont_exist"), true); /// # std::fs::remove_dir_all(&chroot_dir); /// ``` pub fn remove_directory(&self, dir_path: impl Into) -> bool { let safe_dir_path: String = self.chrooted_path(dir_path); match fs::exists(safe_dir_path.clone()) { Ok(dir_exists) => { if !dir_exists { return true; } } Err(e) => { error!("Failed to check if directory {safe_dir_path} exists: {e:?}"); return false; } } match fs::remove_dir_all(safe_dir_path.clone()) { Ok(_) => return true, Err(e) => error!("Failed to delete directory {safe_dir_path}: {e}"), } false } /// Set executable permissions on an existing file in the chroot directory. /// /// ## Example /// /// ``` /// use binwalk::extractors::common::Chroot; /// /// let chroot_dir = std::path::Path::new("tests") /// .join("binwalk_unit_tests") /// .display() /// .to_string(); /// /// let file_name = "runme.exe"; /// /// # std::fs::remove_dir_all(&chroot_dir); /// let chroot = Chroot::new(Some(&chroot_dir)); /// chroot.create_file(file_name, b"AAAA"); /// /// assert_eq!(chroot.make_executable(file_name), true); /// # std::fs::remove_dir_all(&chroot_dir); /// ``` #[allow(dead_code)] pub fn make_executable(&self, file_path: impl Into) -> bool { // Make the file globally executable const UNIX_EXEC_FLAG: u32 = 1; let safe_file_path: String = self.chrooted_path(file_path); match fs::metadata(safe_file_path.clone()) { Err(e) => { error!("Failed to get permissions for file {safe_file_path}: {e}"); } Ok(_metadata) => { #[cfg(unix)] { let mut permissions = _metadata.permissions(); let mode = permissions.mode() | UNIX_EXEC_FLAG; permissions.set_mode(mode); match fs::set_permissions(&safe_file_path, permissions) { Err(e) => { error!("Failed to set permissions for file {safe_file_path}: {e}"); } Ok(_) => { return true; } } } #[cfg(windows)] { return true; } } } false } /// Creates a symbolic link in the chroot directory, named `symlink_path`, which points to `target_path`. /// /// Note that both the symlink and target paths will be sanitized to stay in the chroot directory. /// Both the target path will be converted into a path relative to the symlink file path. /// /// ## Example /// /// ``` /// # fn main() { #[allow(non_snake_case)] fn _doctest_main_src_extractors_common_rs_571_0() -> Result<(), Box> { /// use binwalk::extractors::common::Chroot; /// /// let chroot_dir = std::path::Path::new("tests") /// .join("binwalk_unit_tests") /// .display() /// .to_string(); /// /// let symlink_name = "symlink"; /// let target_path = "target"; /// /// let expected_symlink_path = std::path::Path::new(&chroot_dir).join(symlink_name); /// let expected_target_path = std::path::Path::new(&chroot_dir).join(target_path); /// /// # std::fs::remove_dir_all(&chroot_dir); /// let chroot = Chroot::new(Some(&chroot_dir)); /// /// assert_eq!(chroot.create_symlink(symlink_name, target_path), true); /// assert_eq!(std::fs::canonicalize(expected_symlink_path)?.to_str(), expected_target_path.to_str()); /// # std::fs::remove_dir_all(&chroot_dir); /// # Ok(()) /// # } _doctest_main_src_extractors_common_rs_571_0(); } /// ``` pub fn create_symlink( &self, symlink_path: impl Into, target_path: impl Into, ) -> bool { let target = target_path.into(); let symlink = symlink_path.into(); // Chroot the symlink file path and create a Path object let safe_symlink = self.chrooted_path(&symlink); let safe_symlink_path = path::Path::new(&safe_symlink); // Normalize the symlink target path to a chrooted absolute path let safe_target = if target.starts_with(path::MAIN_SEPARATOR) { // If the target path is absolute, just chroot it inside the chroot directory self.chrooted_path(&target) } else { // Get the symlink file's parent directory path let relative_dir: String = match safe_symlink_path.parent() { None => { // There is no parent, or parent is the root directory; assume the root directory path::MAIN_SEPARATOR.to_string() } Some(parent_dir) => { // Got the parent directory parent_dir.display().to_string() } }; // Join the target path with its relative directory, ensuring it does not traverse outside // the specified chroot directory self.safe_path_join(&relative_dir, &target) }; // Remove the chroot directory from the target and symlink paths. // This results in each being an absolute path that is relative to the chroot directory, // e.g., '/my_chroot_dir/bin/busybox' -> '/bin/busybox'. // // Note: need at least one leading '/', so if the chroot directory is just '/', just use the string as-is. let mut safe_target_rel_path = if self.chroot_directory == path::MAIN_SEPARATOR.to_string() { safe_target.clone() } else { safe_target.replacen(&self.chroot_directory, "", 1) }; let safe_symlink_rel_path = if self.chroot_directory == path::MAIN_SEPARATOR.to_string() { safe_symlink.clone() } else { safe_symlink.replacen(&self.chroot_directory, "", 1) }; // Count the number of path separators (minus the leading one) and an '../' to the target // path for each; e.g., '/bin/busybox' -> '..//bin/busybox'. for _i in 0..safe_symlink_rel_path.matches(path::MAIN_SEPARATOR).count() - 1 { safe_target_rel_path = format!("..{}{}", path::MAIN_SEPARATOR, safe_target_rel_path); } // Add a '.' at the beginning of any paths that start with '/', e.g., '/tmp' -> './tmp'. if safe_target_rel_path.starts_with(path::MAIN_SEPARATOR) { safe_target_rel_path = format!(".{safe_target_rel_path}"); } // Replace any instances of '//' with '/' safe_target_rel_path = self.strip_double_slash(&safe_target_rel_path); // The target path is now a safely chrooted path that is relative to the symlink file path. // Ex: // // Original symlink: "/my_chroot_dir/usr/sbin/ls" is a symlink to "/bin/busybox" // Safe relative symlink: "/my_chroot_dir/usr/sbin/ls" is a symlink to "./../../bin/busybox" let safe_target_path = path::Path::new(&safe_target_rel_path); #[cfg(unix)] { match unix::fs::symlink(safe_target_path, safe_symlink_path) { Ok(_) => true, Err(e) => { error!("Failed to create symlink from {symlink} -> {target}: {e}"); false } } } #[cfg(windows)] { // let sym = match safe_target_path.is_dir() { // true => windows::fs::symlink_dir(safe_target_path, safe_symlink_path), // false => windows::fs::symlink_file(safe_target_path, safe_symlink_path), // }; match windows::fs::symlink_dir(safe_target_path, safe_symlink_path) { Ok(_) => { return true; } Err(e) => { error!( "Failed to create symlink from {} -> {}: {}", symlink, target, e ); return false; } } } } /// Returns true if the file path is a symlink. fn is_symlink(&self, file_path: &str) -> bool { if let Ok(metadata) = fs::symlink_metadata(file_path) { return metadata.file_type().is_symlink(); } false } /// Replace `//` with `/`. This is for asthetics only. fn strip_double_slash(&self, path: &str) -> String { let mut stripped_path = path.to_owned(); let single_slash = path::MAIN_SEPARATOR.to_string(); let double_slash = format!("{single_slash}{single_slash}"); while stripped_path.contains(&double_slash) { stripped_path = stripped_path.replace(&double_slash, &single_slash); } stripped_path } /// Interprets a given path containing '..' directories. fn sanitize_path(&self, file_path: &str, preserve_root_path_sep: bool) -> String { const DIR_TRAVERSAL: &str = ".."; let mut exclude_indicies: Vec = vec![]; let mut sanitized_path: String = "".to_string(); if preserve_root_path_sep && file_path.starts_with(path::MAIN_SEPARATOR) { sanitized_path = path::MAIN_SEPARATOR.to_string(); } // Split the file path on '/' let path_parts: Vec<&str> = file_path.split(path::MAIN_SEPARATOR).collect(); // Loop through each part of the file path for (i, path_part) in path_parts.iter().enumerate() { // If this part of the path is '..', don't include it in the final sanitized path if *path_part == DIR_TRAVERSAL { exclude_indicies.push(i); if i > 0 { // Walk backwards through the path parts until a non-excluded part is found, then mark that part for exclusion as well let mut j = i - 1; while j > 0 && exclude_indicies.contains(&j) { j -= 1; } exclude_indicies.push(j); } // If this part of the path is an empty string, don't include that either (happens if the original file path has '//' in it) } else if path_part.is_empty() { exclude_indicies.push(i); } } // Concatenate each non-excluded part of the file path, with each part separated by '/' for (i, path_part) in path_parts.iter().enumerate() { if !exclude_indicies.contains(&i) { #[cfg(windows)] { // on Windows: in the first loop run, we cannot really prepend a '\' to drive letters like 'C:' if sanitized_path.is_empty() { sanitized_path = path_part.to_string(); continue; } } sanitized_path = format!("{}{}{}", sanitized_path, path::MAIN_SEPARATOR, path_part); } } self.strip_double_slash(&sanitized_path) } } /// Recursively walks a given directory and returns a list of regular non-zero size files in the given directory path. #[allow(dead_code)] pub fn get_extracted_files(directory: &str) -> Vec { let mut regular_files: Vec = vec![]; for entry in WalkDir::new(directory).into_iter() { match entry { Err(_e) => continue, Ok(entry) => { let entry_path = entry.path(); // Query file metadata *without* following symlinks match fs::symlink_metadata(entry_path) { Err(_e) => continue, Ok(md) => { // Only interested in non-empty, regular files if md.is_file() && md.len() > 0 { regular_files.push(entry_path.display().to_string()); } } } } } } regular_files } /// Executes an extractor for the provided SignatureResult. pub fn execute( file_data: &[u8], file_path: &str, signature: &SignatureResult, extractor: &Option, ) -> ExtractionResult { let mut result = ExtractionResult { ..Default::default() }; // Create an output directory for the extraction if let Ok(output_directory) = create_output_directory(file_path, signature.offset) { // Make sure a defalut extractor was actually defined (this function should not be called if signature.extractor is None) match &extractor { None => { error!( "Attempted to extract {} data, but no extractor is defined!", signature.name ); } Some(default_extractor) => { let extractor_definition: Extractor; // If the signature result specified a preferred extractor, use that instead of the default signature extractor if let Some(preferred_extractor) = &signature.preferred_extractor { extractor_definition = preferred_extractor.clone(); } else { extractor_definition = default_extractor.clone(); } // Decide how to execute the extractor depending on the extractor type match &extractor_definition.utility { ExtractorType::None => { error!( "Signature {}: an extractor of type None is invalid!", signature.name ); } ExtractorType::Internal(func) => { debug!("Executing internal {} extractor", signature.name); // Run the internal extractor function result = func(file_data, signature.offset, Some(&output_directory)); // Set the extractor name to "_built_in" result.extractor = format!("{}_built_in", signature.name); } ExtractorType::External(cmd) => { // Spawn the external extractor command match spawn( file_data, file_path, &output_directory, signature, extractor_definition.clone(), ) { Err(e) => { error!( "Failed to spawn external extractor for '{}' signature: {}", signature.name, e ); } Ok(proc_info) => { // Wait for the external process to exit match proc_wait(proc_info) { Err(_) => { warn!("External extractor failed!"); } Ok(ext_result) => { result = ext_result; // Set the extractor name to the name of the extraction utility result.extractor = cmd.to_string(); } } } } } } // Populate these ExtractionResult fields automatically for all extractors result.output_directory = output_directory.clone(); result.do_not_recurse = extractor_definition.do_not_recurse; // If the extractor reported success, make sure it extracted something other than just an empty file if result.success && !was_something_extracted(&result.output_directory) { result.success = false; warn!("Extractor exited successfully, but no data was extracted"); } } } // Clean up extractor's output directory if extraction failed if !result.success { if let Err(e) = fs::remove_dir_all(&output_directory) { warn!( "Failed to clean up extraction directory {output_directory} after extraction failure: {e}" ); } } } result } /// Spawn an external extractor process. fn spawn( file_data: &[u8], file_path: &str, output_directory: &str, signature: &SignatureResult, mut extractor: Extractor, ) -> Result { let chroot = Chroot::new(None); // This function *only* handles execution of external extraction utilities; internal extractors must be invoked directly let command = match &extractor.utility { ExtractorType::External(cmd) => cmd.clone(), ExtractorType::Internal(_ext) => { error!("Tried to run an internal extractor as an external command!"); return Err(std::io::Error::other( "attempt to execute an internal extractor as an external command", )); } ExtractorType::None => { error!("An extractor command was defined, but is set to None!"); return Err(std::io::Error::other( "invalid external command of type None", )); } }; // Carved file path will be /_. let carved_file = format!( "{}{}{}_{:X}.{}", output_directory, path::MAIN_SEPARATOR, signature.name, signature.offset, extractor.extension ); info!( "Carving data from {} {:#X}..{:#X} to {}", file_path, signature.offset, signature.offset + signature.size, carved_file ); // If the entirety of the source file is this one file type, no need to carve a copy of it, just create a symlink if signature.offset == 0 && signature.size == file_data.len() { if !chroot.create_symlink(&carved_file, file_path) { return Err(std::io::Error::other( "Failed to create carved file symlink", )); } } else { // Copy file data to carved file path if !chroot.carve_file(&carved_file, file_data, signature.offset, signature.size) { return Err(std::io::Error::other("Failed to carve data to disk")); } } // Replace all "%e" command arguments with the path to the carved file for i in 0..extractor.arguments.len() { if extractor.arguments[i] == SOURCE_FILE_PLACEHOLDER { extractor.arguments[i] = carved_file.clone(); } } info!("Spawning process {} {:?}", command, extractor.arguments); match process::Command::new(&command) .args(&extractor.arguments) .stdout(process::Stdio::null()) .stderr(process::Stdio::null()) .current_dir(output_directory) .spawn() { Err(e) => { error!( "Failed to execute command {}{:?}: {}", command, extractor.arguments, e ); Err(e) } Ok(child) => { // If the process was spawned successfully, return some information about the process let proc_info = ProcInfo { child, carved_file: carved_file.clone(), exit_codes: extractor.exit_codes, }; Ok(proc_info) } } } /// Waits for an extraction process to complete. /// Returns ExtractionError if the extractor was prematurely terminated, else returns an ExtractionResult. fn proc_wait(mut worker_info: ProcInfo) -> Result { // The standard exit success value is 0 const EXIT_SUCCESS: i32 = 0; // Block until child process has terminated match worker_info.child.wait() { // Child was terminated from an external signal, status unknown, assume failure but do nothing else Err(e) => { error!("Failed to retreive child process status: {e}"); Err(ExtractionError) } // Child terminated with an exit status Ok(status) => { // Assume failure until proven otherwise let mut extraction_success: bool = false; // Clean up the carved file used as input to the extractor debug!("Deleting carved file {}", worker_info.carved_file); if let Err(e) = fs::remove_file(worker_info.carved_file.clone()) { warn!( "Failed to remove carved file '{}': {}", worker_info.carved_file, e ); }; // Check the extractor's exit status match status.code() { None => { extraction_success = false; } Some(code) => { // Make sure the extractor's exit code is an expected one if code == EXIT_SUCCESS || worker_info.exit_codes.contains(&code) { extraction_success = true; } else { warn!("Child process exited with unexpected code: {code}"); } } } // Return an ExtractionResult with the appropriate success status Ok(ExtractionResult { success: extraction_success, ..Default::default() }) } } } // Create an output directory in which to place extraction results fn create_output_directory(file_path: &str, offset: usize) -> Result { let chroot = Chroot::new(None); // Output directory will be: let output_directory = format!( "{}.extracted{}{:X}", file_path, path::MAIN_SEPARATOR, offset ); // First, remove the output directory if it exists from a previous run if !chroot.remove_directory(&output_directory) { return Err(std::io::Error::other("Directory deletion failed")); } // Create the output directory, equivalent of mkdir -p if !chroot.create_directory(&output_directory) { return Err(std::io::Error::other("Directory creation failed")); } Ok(output_directory) } /// Returns true if the size of the provided extractor output directory is greater than zero. /// Note that any intermediate/carved files must be deleted *before* calling this function. fn was_something_extracted(output_directory: &str) -> bool { let output_directory_path = path::Path::new(output_directory); debug!("Checking output directory {output_directory} for results"); // Walk the output directory looking for something, anything, that isn't an empty file for entry in WalkDir::new(output_directory).into_iter() { match entry { Err(e) => { warn!("Failed to retrieve output directory entry: {e}"); continue; } Ok(entry) => { // Don't include the base output directory path itself if entry.path() == output_directory_path { continue; } debug!("Found output file {}", entry.path().display()); match fs::symlink_metadata(entry.path()) { Err(_e) => continue, Ok(md) => { if md.len() > 0 { return true; } } } } } } false } ================================================ FILE: src/extractors/csman.rs ================================================ use crate::common::is_offset_safe; use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::csman::{CSManEntry, parse_csman_entry, parse_csman_header}; use miniz_oxide::inflate; use std::collections::HashMap; /// Defines the internal extractor function for CSMan DAT files /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::csman::csman_extractor; /// /// match csman_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn csman_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(extract_csman_dat), ..Default::default() } } /// Validate and extract CSMan DAT file entries pub fn extract_csman_dat( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const COMPRESSED_HEADER_SIZE: usize = 2; // Return value let mut result = ExtractionResult { ..Default::default() }; let mut csman_entries: Vec = Vec::new(); // Parse the CSMAN header if let Ok(csman_header) = parse_csman_header(&file_data[offset..]) { // Calulate the start and end offsets of the CSMAN entries let entries_start: usize = offset + csman_header.header_size; let entries_end: usize = entries_start + csman_header.data_size; // Get the CSMAN entry data if let Some(raw_entry_data) = file_data.get(entries_start..entries_end) { let mut entry_data = raw_entry_data.to_vec(); // If the entries are compressed, decompress it (zlib compression) if csman_header.compressed { if let Some(compressed_data) = raw_entry_data.get(COMPRESSED_HEADER_SIZE..) { match inflate::decompress_to_vec(compressed_data) { Err(_) => { return result; } Ok(decompressed_data) => { entry_data = decompressed_data.clone(); } } } } // Offsets for processing CSMAN entries in entry_data let mut next_offset: usize = 0; let mut previous_offset = None; let available_data: usize = entry_data.len(); // Loop while there is still data that can be safely parsed while is_offset_safe(available_data, next_offset, previous_offset) { // Get the next entry's data match entry_data.get(next_offset..) { None => { break; } Some(next_entry_data) => { // Parse the next entry match parse_csman_entry(next_entry_data, &csman_header.endianness) { Err(_) => { break; } Ok(entry) => { if entry.eof { // Last entry should be an EOF marker; an EOF marker should always exist. // There should be at least one valid entry. result.success = !csman_entries.is_empty(); break; } else { // Append this entry to the list of entries and update the offsets to process the next entry csman_entries.push(entry.clone()); previous_offset = Some(next_offset); next_offset += entry.size; } } } } } } // If all entries were processed successfully if result.success { // Update the reported size of data processed result.size = Some(csman_header.header_size + csman_header.data_size); // If extraction was requested, extract each entry using the entry key as the file name if output_directory.is_some() { // All files will be written inside the provided output directory let chroot = Chroot::new(output_directory); // There may be more than one entry with the same key; track the key and how many times it was encountered let mut processed_entries: HashMap = HashMap::new(); // Loop through all entries for entry in csman_entries { // File name is [key value, in ASCII hex].dat let mut file_name = format!("{:08X}.dat", entry.key); // If this key value has already been extracted, file name is [key value, in ASCII hex].dat_[count] if processed_entries.contains_key(&entry.key) { file_name = format!("{}_{}", file_name, processed_entries[&entry.key]); processed_entries.insert(entry.key, processed_entries[&entry.key] + 1); } else { processed_entries.insert(entry.key, 1); } if !chroot.create_file(&file_name, &entry.value) { result.success = false; break; } } } } } } result } ================================================ FILE: src/extractors/dahua_zip.rs ================================================ use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::signatures::zip::find_zip_eof; /// Defines the internal extractor function for carving Dahua ZIP files /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::dahua_zip::dahua_zip_extractor; /// /// match dahua_zip_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn dahua_zip_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(extract_dahua_zip), ..Default::default() } } /// Carves out a Dahua ZIP file and converts it to a normal ZIP file pub fn extract_dahua_zip( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const OUTFILE_NAME: &str = "dahua.zip"; const ZIP_HEADER: &[u8] = b"PK"; let mut result = ExtractionResult { ..Default::default() }; // Locate the end of the zip archive if let Ok(zip_info) = find_zip_eof(file_data, offset) { // Calculate total size of the zip archive, report success result.size = Some(zip_info.eof - offset); result.success = true; // If extraction was requested, carve the zip archive to disk, replacing the Dahua ZIP magic bytes // with the standard ZIP magic bytes. if output_directory.is_some() { // Start and end offsets of the data to carve let start_data = offset + ZIP_HEADER.len(); let end_data = offset + result.size.unwrap(); let chroot = Chroot::new(output_directory); // Get the data to carve match file_data.get(start_data..end_data) { None => { result.success = false; } Some(zip_data) => { // First write the normal ZIP header magic bytes to disk if !chroot.create_file(OUTFILE_NAME, ZIP_HEADER) { result.success = false; } else { // Append the rest of the ZIP archive to disk result.success = chroot.append_to_file(OUTFILE_NAME, zip_data); } } } } } result } ================================================ FILE: src/extractors/dmg.rs ================================================ use crate::extractors; /// Describes how to run the dmg2img utility to convert DMG images to MBR /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::dmg::dmg_extractor; /// /// match dmg_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn dmg_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("dmg2img".to_string()), extension: "dmg".to_string(), arguments: vec![ "-i".to_string(), // Input file extractors::common::SOURCE_FILE_PLACEHOLDER.to_string(), "-o".to_string(), // Output file "mbr.img".to_string(), ], exit_codes: vec![0, 1], ..Default::default() } } ================================================ FILE: src/extractors/dtb.rs ================================================ use crate::common::is_offset_safe; use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::dtb::{parse_dtb_header, parse_dtb_node}; use log::error; /// Defines the internal extractor function for extracting Device Tree Blobs /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::dtb::dtb_extractor; /// /// match dtb_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn dtb_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(extract_dtb), ..Default::default() } } /// Internal extractor for extracting Device Tree Blobs pub fn extract_dtb( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { let mut heirerarchy: Vec = Vec::new(); let mut result = ExtractionResult { ..Default::default() }; // Parse the DTB file header if let Ok(dtb_header) = parse_dtb_header(&file_data[offset..]) { // Get all the DTB data if let Some(dtb_data) = file_data.get(offset..offset + dtb_header.total_size) { // DTB node entries start at the structure offset specified in the DTB header let mut entry_offset = dtb_header.struct_offset; let mut previous_entry_offset = None; let available_data = dtb_data.len(); // Loop over all DTB node entries while is_offset_safe(available_data, entry_offset, previous_entry_offset) { // Parse the next DTB node entry let node = parse_dtb_node(&dtb_header, dtb_data, entry_offset); // Beginning of a node, add it to the heirerarchy list if node.begin { if !node.name.is_empty() { heirerarchy.push(node.name.clone()); } // End of a node, remove it from the heirerarchy list } else if node.end { if !heirerarchy.is_empty() { heirerarchy.pop(); } // End of the DTB structure, return success only if the whole DTB structure was parsed successfully up to the EOF marker } else if node.eof { result.success = true; result.size = Some(available_data); break; // DTB property, extract it to disk } else if node.property { if output_directory.is_some() { let chroot = Chroot::new(output_directory); let dir_path = heirerarchy.join(std::path::MAIN_SEPARATOR_STR); let file_path = chroot.safe_path_join(&dir_path, &node.name); if !chroot.create_directory(dir_path) { break; } if !chroot.create_file(file_path, &node.data) { break; } } // The only other supported node type is NOP } else if !node.nop { error!("Unknown or invalid DTB node"); break; } // Update offsets to parse the next DTB structure entry previous_entry_offset = Some(entry_offset); entry_offset += node.total_size; } } } result } ================================================ FILE: src/extractors/dumpifs.rs ================================================ use crate::extractors; /// Describes how to run the dumpifs utility to extract QNX IFS images /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::dumpifs::dumpifs_extractor; /// /// match dumpifs_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn dumpifs_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("dumpifs".to_string()), extension: "ifs".to_string(), arguments: vec![ "-x".to_string(), // Extract the image extractors::common::SOURCE_FILE_PLACEHOLDER.to_string(), ], exit_codes: vec![0], ..Default::default() } } ================================================ FILE: src/extractors/dxbc.rs ================================================ use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::dxbc::parse_dxbc_header; /// Defines the internal extractor function for carving out DXBC images /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::dxbc::dxbc_extractor; /// /// match dxbc_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn dxbc_extractor() -> Extractor { Extractor { do_not_recurse: true, utility: ExtractorType::Internal(extract_dxbc_file), ..Default::default() } } pub fn extract_dxbc_file( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const OUTFILE_NAME: &str = "shader.dxbc"; let mut result = ExtractionResult { ..Default::default() }; if let Ok(header) = parse_dxbc_header(&file_data[offset..]) { // Report success result.size = Some(header.size); result.success = true; // Do extraction, if requested if output_directory.is_some() { let chroot = Chroot::new(output_directory); result.success = chroot.carve_file(OUTFILE_NAME, file_data, offset, result.size.unwrap()); } } result } ================================================ FILE: src/extractors/encfw.rs ================================================ use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; /// Defines the internal extractor function for decrypting known encrypted firmware /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::encfw::encfw_extractor; /// /// match encfw_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn encfw_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(encfw_decrypt), ..Default::default() } } /// Attempts to decrypt known encrypted firmware images pub fn encfw_decrypt( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const OUTPUT_FILE_NAME: &str = "decrypted.bin"; let mut result = ExtractionResult { ..Default::default() }; if let Ok(decrypted_data) = delink::decrypt(&file_data[offset..]) { result.success = true; // Write to file, if requested if output_directory.is_some() { let chroot = Chroot::new(output_directory); result.success = chroot.create_file(OUTPUT_FILE_NAME, &decrypted_data); } } result } ================================================ FILE: src/extractors/gif.rs ================================================ use crate::common::is_offset_safe; use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::common::StructureError; use crate::structures::gif::{parse_gif_extension, parse_gif_header, parse_gif_image_descriptor}; /// Defines the internal extractor function for carving out GIF images /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::gif::gif_extractor; /// /// match gif_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn gif_extractor() -> Extractor { Extractor { do_not_recurse: true, utility: ExtractorType::Internal(extract_gif_image), ..Default::default() } } /// Parses and carves a GIF image from a file pub fn extract_gif_image( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const OUTFILE_NAME: &str = "image.gif"; let mut result = ExtractionResult { ..Default::default() }; // Parse the GIF header if let Ok(gif_header) = parse_gif_header(&file_data[offset..]) { // GIF data follows the gif header if let Some(gif_image_data) = file_data.get(offset + gif_header.size..) { // Determine the size of the GIF image data if let Some(gif_data_size) = get_gif_data_size(gif_image_data) { // Report success result.size = Some(gif_header.size + gif_data_size); result.success = true; // Do extraction, if requested if output_directory.is_some() { let chroot = Chroot::new(output_directory); result.success = chroot.carve_file(OUTFILE_NAME, file_data, offset, result.size.unwrap()); } } } } result } /// Returns the size of the GIF data that follows the GIF header fn get_gif_data_size(gif_data: &[u8]) -> Option { // GIF block types const EXTENSION: u8 = 0x21; const TERMINATOR: u8 = 0x3B; const IMAGE_DESCRIPTOR: u8 = 0x2C; let mut next_offset: usize = 0; let mut previous_offset = None; let available_data = gif_data.len(); // Loop through all GIF data blocks while is_offset_safe(available_data, next_offset, previous_offset) { let block_size: Result; // Get the block type of the next block match gif_data.get(next_offset) { None => break, Some(block_type) => { // Parse the block type accordingly if *block_type == IMAGE_DESCRIPTOR { block_size = parse_gif_image_descriptor(&gif_data[next_offset..]); } else if *block_type == EXTENSION { block_size = parse_gif_extension(&gif_data[next_offset..]); } else if *block_type == TERMINATOR { // Only return the GIF size if we've found a termination block. // The +1 is for the size of the block_type u8. return Some(next_offset + 1); } else { break; } } } // Check if the block was parsed successfully match block_size { Err(_) => break, Ok(this_block_size) => { // Everything looks OK, go to the next block previous_offset = Some(next_offset); next_offset += this_block_size; } } } // Something went wrong, failure None } ================================================ FILE: src/extractors/gpg.rs ================================================ use crate::extractors::common::{ExtractionResult, Extractor, ExtractorType}; use crate::extractors::inflate; /// Defines the internal extractor function for decompressing signed GPG data /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::gpg::gpg_extractor; /// /// match gpg_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn gpg_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(gpg_decompress), ..Default::default() } } /// Internal extractor for decompressing signed GPG data pub fn gpg_decompress( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { // Size of the GPG header const HEADER_SIZE: usize = 2; let mut exresult = ExtractionResult { ..Default::default() }; // Do the decompression, ignoring the GPG header let inflate_result = inflate::inflate_decompressor(file_data, offset + HEADER_SIZE, output_directory); // Check that the data decompressed OK if inflate_result.success { exresult.success = true; exresult.size = Some(HEADER_SIZE + inflate_result.size); } exresult } ================================================ FILE: src/extractors/gzip.rs ================================================ use crate::extractors::common::{ExtractionResult, Extractor, ExtractorType}; use crate::extractors::inflate; use crate::structures::gzip::parse_gzip_header; /// Defines the internal extractor function for decompressing gzip data /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::gzip::gzip_extractor; /// /// match gzip_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn gzip_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(gzip_decompress), ..Default::default() } } /// Internal extractor for gzip compressed data pub fn gzip_decompress( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { let mut exresult = ExtractionResult { ..Default::default() }; // Parse the gzip header if let Ok(gzip_header) = parse_gzip_header(&file_data[offset..]) { // Deflate compressed data starts at the end of the gzip header let deflate_data_start: usize = offset + gzip_header.size; if file_data.len() > deflate_data_start { let inflate_result = inflate::inflate_decompressor(file_data, deflate_data_start, output_directory); if inflate_result.success { exresult.success = true; exresult.size = Some(inflate_result.size); } } } exresult } ================================================ FILE: src/extractors/inflate.rs ================================================ use crate::extractors::common::Chroot; use adler32::RollingAdler32; use flate2::bufread::DeflateDecoder; use std::io::Read; #[derive(Debug, Default, Clone)] pub struct DeflateResult { pub size: usize, pub adler32: u32, pub success: bool, } /// Decompressor for inflating deflated data. /// For internal use, does not conform to the standard extractor format. pub fn inflate_decompressor( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> DeflateResult { // Size of decompression buffer const BLOCK_SIZE: usize = 8192; // Output file for decompressed data const OUTPUT_FILE_NAME: &str = "decompressed.bin"; let mut result = DeflateResult { ..Default::default() }; let mut adler32_checksum = RollingAdler32::new(); let mut decompressed_buffer = [0; BLOCK_SIZE]; let mut decompressor = DeflateDecoder::new(&file_data[offset..]); /* * Loop through all compressed data and decompress it. * * This has a significant performance hit since 1) decompression takes time, and 2) data is * decompressed once during signature validation and a second time during extraction (if extraction * was requested). * * The advantage is that not only are we 100% sure that this data is a valid deflate stream, but we * can also determine the exact size of the deflated data. */ loop { // Decompress a block of data match decompressor.read(&mut decompressed_buffer) { Err(_) => { // Break on decompression error break; } Ok(n) => { // Decompressed a block of data, update checksum and if extraction was requested write the decompressed block to the output file if n > 0 { adler32_checksum.update_buffer(&decompressed_buffer[0..n]); if output_directory.is_some() { let chroot = Chroot::new(output_directory); if !chroot.append_to_file(OUTPUT_FILE_NAME, &decompressed_buffer[0..n]) { // If writing data to file fails, break break; } } } // No data was read, end of compression stream if n == 0 { // If some data was actually decompressed, report success and the number of input bytes consumed if decompressor.total_out() > 0 { result.success = true; result.adler32 = adler32_checksum.hash(); result.size = decompressor.total_in() as usize; } // Nothing else to do, break break; } } } } result } ================================================ FILE: src/extractors/iso9660.rs ================================================ use crate::extractors; use crate::extractors::sevenzip::sevenzip_extractor; /// Describes how to run the 7z utility to extract ISO images /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::iso9660::iso9660_extractor; /// /// match iso9660_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn iso9660_extractor() -> extractors::common::Extractor { // Same as the normal 7z extractor, but give the carved file an ISO file extension. // The file extension matters, and 7z doesn't handle some ISO sub-formats correctly if the file extension is not '.iso'. let mut extractor = sevenzip_extractor(); extractor.extension = "iso".to_string(); extractor } ================================================ FILE: src/extractors/jboot.rs ================================================ use crate::common::crc32; use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::jboot::parse_jboot_sch2_header; /// Defines the internal extractor function for carving out JBOOT SCH2 kernels /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::jboot::sch2_extractor; /// /// match sch2_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn sch2_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(extract_jboot_sch2_kernel), ..Default::default() } } /// Extract the kernel described by a JBOOT SCH2 header pub fn extract_jboot_sch2_kernel( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { // Output file name const OUTFILE_NAME: &str = "kernel.bin"; let mut result = ExtractionResult { ..Default::default() }; // Get the SCH2 data if let Some(sch2_header_data) = file_data.get(offset..) { // Parse the SCH2 header if let Ok(sch2_header) = parse_jboot_sch2_header(sch2_header_data) { let kernel_start: usize = offset + sch2_header.header_size; let kernel_end: usize = kernel_start + sch2_header.kernel_size; // Validate the kernel data checksum if let Some(kernel_data) = file_data.get(kernel_start..kernel_end) { if crc32(kernel_data) == (sch2_header.kernel_checksum as u32) { // Everything checks out ok result.size = Some(sch2_header.header_size + sch2_header.kernel_size); result.success = true; if output_directory.is_some() { let chroot = Chroot::new(output_directory); result.success = chroot.carve_file( OUTFILE_NAME, file_data, kernel_start, sch2_header.kernel_size, ); } } } } } result } ================================================ FILE: src/extractors/jffs2.rs ================================================ use crate::extractors; /// Describes how to run the jefferson utility to extract JFFS file systems /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::jffs2::jffs2_extractor; /// /// match jffs2_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn jffs2_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("jefferson".to_string()), extension: "img".to_string(), arguments: vec![ "-f".to_string(), // Force overwrite if output file, for some reason, exists "-d".to_string(), // Output to jffs2-root directory "jffs2-root".to_string(), extractors::common::SOURCE_FILE_PLACEHOLDER.to_string(), ], exit_codes: vec![0, 1, 2], ..Default::default() } } ================================================ FILE: src/extractors/jpeg.rs ================================================ use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; /// Defines the internal extractor function for carving out JPEG images /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::jpeg::jpeg_extractor; /// /// match jpeg_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn jpeg_extractor() -> Extractor { Extractor { do_not_recurse: true, utility: ExtractorType::Internal(extract_jpeg_image), ..Default::default() } } /// Internal extractor for carving JPEG images to disk pub fn extract_jpeg_image( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const OUTFILE_NAME: &str = "image.jpg"; let mut result = ExtractionResult { ..Default::default() }; // Find the JPEG EOF to identify the total JPEG size if let Some(jpeg_data_size) = get_jpeg_data_size(&file_data[offset..]) { result.size = Some(jpeg_data_size); result.success = true; if output_directory.is_some() { let chroot = Chroot::new(output_directory); result.success = chroot.carve_file(OUTFILE_NAME, file_data, offset, result.size.unwrap()); } } result } /// Parses JPEG markers until the EOF marker is found fn get_jpeg_data_size(jpeg_data: &[u8]) -> Option { const SIZE_FIELD_LENGTH: usize = 2; const SOS_SCAN_AHEAD_LENGTH: usize = 2; const MARKER_MAGIC: u8 = 0xFF; const SOS_MARKER: u8 = 0xDA; const EOF_MARKER: u8 = 0xD9; let mut next_marker_offset: usize = 0; // Most JPEG markers include a size field; these do not let no_length_markers: Vec = vec![ 0x00, 0x01, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, EOF_MARKER, ]; // In a Start Of Scan block, ignore 0xFF marker magics that are followed by one of these bytes let sos_skip_markers: Vec = vec![0x00, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7]; loop { // Read the marker magic byte match jpeg_data.get(next_marker_offset) { None => { break; } Some(marker_magic) => { // Make sure this is the correct marker magic if *marker_magic != MARKER_MAGIC { break; } // Include marker magic byte in side of the marker next_marker_offset += 1; // Read the marker ID byte match jpeg_data.get(next_marker_offset) { None => { break; } Some(marker_id) => { // Include marker ID byte in the size of the marker next_marker_offset += 1; // Most markers have a 2-byte length field after the marker, stored in big-endian if !no_length_markers.contains(marker_id) { match jpeg_data .get(next_marker_offset..next_marker_offset + SIZE_FIELD_LENGTH) { None => { break; } Some(size_bytes) => { next_marker_offset += u16::from_be_bytes(size_bytes.try_into().unwrap()) as usize; } } } // Start Of Scan markers have a size field, but are immediately followed by data not included int // the size field. Need to scan all the bytes until the next valid JPEG marker is found. if *marker_id == SOS_MARKER { loop { // Get the next two bytes match jpeg_data.get( next_marker_offset..next_marker_offset + SOS_SCAN_AHEAD_LENGTH, ) { None => { break; } Some(next_bytes) => { // Check if the next byte is a marker magic byte, *and* that it is not followed by a marker escape byte if next_bytes[0] == MARKER_MAGIC && !sos_skip_markers.contains(&next_bytes[1]) { break; } else { // Go to the next byte next_marker_offset += 1; } } } } } // EOF marker indicates the end of the JPEG image if *marker_id == EOF_MARKER { return Some(next_marker_offset); } } } } } } None } ================================================ FILE: src/extractors/linux.rs ================================================ use crate::extractors; /// Describes how to run the vmlinux-to-elf utility to convert raw kernel images to ELF files /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::linux::linux_kernel_extractor; /// /// match linux_kernel_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn linux_kernel_extractor() -> extractors::common::Extractor { extractors::common::Extractor { do_not_recurse: true, utility: extractors::common::ExtractorType::External("vmlinux-to-elf".to_string()), extension: "bin".to_string(), arguments: vec![ // Input file extractors::common::SOURCE_FILE_PLACEHOLDER.to_string(), // Output file "linux_kernel.elf".to_string(), ], exit_codes: vec![0], } } ================================================ FILE: src/extractors/lz4.rs ================================================ use crate::extractors; /// Describes how to run the lz4 utility to extract LZ4 compressed files /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::lz4::lz4_extractor; /// /// match lz4_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn lz4_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("lz4".to_string()), extension: "lz4".to_string(), arguments: vec![ "-f".to_string(), // Force overwirte if, for some reason, the output file exists "-d".to_string(), // Perform a decompression extractors::common::SOURCE_FILE_PLACEHOLDER.to_string(), "decompressed.bin".to_string(), // Output file ], exit_codes: vec![0], ..Default::default() } } ================================================ FILE: src/extractors/lzfse.rs ================================================ use crate::extractors::common::{Extractor, ExtractorType, SOURCE_FILE_PLACEHOLDER}; /// Describes how to run the lzfse utility to decompress LZFSE files /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::lzfse::lzfse_extractor; /// /// match lzfse_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn lzfse_extractor() -> Extractor { const OUTPUT_FILE_NAME: &str = "decompressed.bin"; Extractor { utility: ExtractorType::External("lzfse".to_string()), extension: "bin".to_string(), arguments: vec![ "-decode".to_string(), // Do decompression "-i".to_string(), // Input file SOURCE_FILE_PLACEHOLDER.to_string(), "-o".to_string(), // Output file OUTPUT_FILE_NAME.to_string(), ], exit_codes: vec![0], ..Default::default() } } ================================================ FILE: src/extractors/lzma.rs ================================================ use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use liblzma::stream::{Action, Status, Stream}; /// Defines the internal extractor function for decompressing LZMA/XZ data /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::lzma::lzma_extractor; /// /// match lzma_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn lzma_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(lzma_decompress), ..Default::default() } } /// Internal extractor for decompressing LZMA/XZ data streams pub fn lzma_decompress( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { // Output file name const OUTPUT_FILE_NAME: &str = "decompressed.bin"; // Output buffer size const BLOCK_SIZE: usize = 8192; // Maximum memory limit: 4GB const MEM_LIMIT: u64 = 4 * 1024 * 1024 * 1024; let mut result = ExtractionResult { ..Default::default() }; // Output buffer let mut output_buf = [0; BLOCK_SIZE]; // Input compression stream let lzma_stream = &file_data[offset..]; // Instantiate a new decoder, auto-detect LZMA or XZ if let Ok(mut decompressor) = Stream::new_auto_decoder(MEM_LIMIT, 0) { // Tracks number of bytes written to disk let mut bytes_written: usize = 0; // Tracks current position of bytes consumed from input stream let mut stream_position: usize = 0; /* * Loop through all compressed data and decompress it. * * This has a significant performance hit since 1) decompression takes time, and 2) data is * decompressed once during signature validation and a second time during extraction (if extraction * was requested). * * The advantage is that not only are we 100% sure that this data is a valid LZMA stream, but we * can also determine the exact size of the LZMA data. */ loop { // Decompress data into output_buf match decompressor.process( &lzma_stream[stream_position..], &mut output_buf, Action::Run, ) { Err(_) => { // Decompression error, break break; } Ok(status) => { // Check reported status match status { Status::GetCheck => break, Status::MemNeeded => break, Status::Ok => { // Decompression OK, but there is still more data to decompress stream_position = decompressor.total_in() as usize; } Status::StreamEnd => { // Decompression complete. If some data was decompressed, report success, else break. if decompressor.total_out() > 0 { result.success = true; result.size = Some(decompressor.total_in() as usize); } else { break; } } } // Some data was decompressed successfully; if extraction was requested, write the data to disk. if output_directory.is_some() { // Number of decompressed bytes in the output buffer let n = (decompressor.total_out() as usize) - bytes_written; let chroot = Chroot::new(output_directory); if !chroot.append_to_file(OUTPUT_FILE_NAME, &output_buf[0..n]) { // If writing data to disk fails, report failure and break result.success = false; break; } // Remember how much data has been written to disk bytes_written += n; } // If result.success is true, then everything has been processed and written to disk successfully. if result.success { break; } } } } } result } ================================================ FILE: src/extractors/lzop.rs ================================================ use crate::extractors; /// Describes how to run the lzop utility to extract LZO compressed files /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::lzop::lzop_extractor; /// /// match lzop_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn lzop_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("lzop".to_string()), extension: "lzo".to_string(), arguments: vec![ "-p".to_string(), // Output to the current directory "-N".to_string(), // Restore original file name "-d".to_string(), // Perform a decompression extractors::common::SOURCE_FILE_PLACEHOLDER.to_string(), ], exit_codes: vec![0], ..Default::default() } } ================================================ FILE: src/extractors/matter_ota.rs ================================================ use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::matter_ota::parse_matter_ota_header; /// Defines the internal extractor function for extracting a Matter OTA firmware payload */ /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::matter_ota::matter_ota_extractor; /// /// match matter_ota_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn matter_ota_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(extract_matter_ota), ..Default::default() } } /// Matter OTA firmware payload extractor pub fn extract_matter_ota( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const OUTFILE_NAME: &str = "matter_payload.bin"; let mut result = ExtractionResult { ..Default::default() }; if let Ok(ota_header) = parse_matter_ota_header(&file_data[offset..]) { const MAGIC_SIZE: usize = 4; const TOTAL_SIZE_SIZE: usize = 8; const HEADER_SIZE_SIZE: usize = 4; let total_header_size = MAGIC_SIZE + TOTAL_SIZE_SIZE + HEADER_SIZE_SIZE + ota_header.header_size; result.success = true; result.size = Some(ota_header.total_size); let payload_start = offset + total_header_size; let payload_end = offset + total_header_size + ota_header.payload_size; // Sanity check reported payload size and get the payload data if let Some(payload_data) = file_data.get(payload_start..payload_end) { if output_directory.is_some() { let chroot = Chroot::new(output_directory); result.success = chroot.carve_file(OUTFILE_NAME, payload_data, 0, payload_data.len()); } } } result } ================================================ FILE: src/extractors/mbr.rs ================================================ use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::mbr::parse_mbr_image; /// Defines the internal extractor function for MBR partitions /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::mbr::mbr_extractor; /// /// match mbr_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn mbr_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(extract_mbr_partitions), ..Default::default() } } /// Validate and extract partitions from an MBR pub fn extract_mbr_partitions( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { // Return value let mut result = ExtractionResult { ..Default::default() }; let available_data = file_data.len() - offset; // Parse the MBR header if let Ok(mbr_header) = parse_mbr_image(&file_data[offset..]) { // Make sure there is at least one valid partition if !mbr_header.partitions.is_empty() { // Make sure the reported size of the MBR does not extend beyond EOF if available_data >= mbr_header.image_size { // Everything looks ok result.success = true; result.size = Some(mbr_header.image_size); // Do extraction if requested if output_directory.is_some() { // Chroot extracted files into the output directory let chroot = Chroot::new(output_directory); // Loop through each partition for (partition_count, partition) in mbr_header.partitions.iter().enumerate() { // Partition names are not unique, output file will be: "_partition." let partition_name = format!("{}_partition.{}", partition.name, partition_count); // Carve out the partition result.success = chroot.carve_file( partition_name, file_data, partition.start, partition.size, ); // If partition extraction failed, quit and report a failure if !result.success { break; } } } } } } result } ================================================ FILE: src/extractors/mh01.rs ================================================ use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::mh01::parse_mh01_header; /// Defines the internal extractor function for carving out MH01 firmware images /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::mh01::mh01_extractor; /// /// match mh01_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn mh01_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(extract_mh01_image), ..Default::default() } } /// Internal extractor for carve pieces of MH01 images to disk pub fn extract_mh01_image( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { // File names for the three portions of the MH01 firmware image const IV_FILE_NAME: &str = "iv.bin"; const SIGNATURE_FILE_NAME: &str = "signature.bin"; const ENCRYPTED_DATA_FILE_NAME: &str = "encrypted.bin"; const DECRYPTED_DATA_FILE_NAME: &str = "decrypted.bin"; let mut result = ExtractionResult { ..Default::default() }; // Get the MH01 image data if let Some(mh01_data) = file_data.get(offset..) { // Parse the MH01 header if let Ok(mh01_header) = parse_mh01_header(mh01_data) { result.size = Some(mh01_header.total_size); // If extraction was requested, do it if output_directory.is_some() { let chroot = Chroot::new(output_directory); // Try to decrypt the firmware match delink::mh01::decrypt(mh01_data) { Ok(decrypted_data) => { // Write decrypted data to disk result.success = chroot.create_file(DECRYPTED_DATA_FILE_NAME, &decrypted_data); } Err(_) => { // Decryption failture; extract each part of the firmware image, ensuring that each one extracts without error result.success = chroot.carve_file( IV_FILE_NAME, mh01_data, mh01_header.iv_offset, mh01_header.iv_size, ) && chroot.carve_file( SIGNATURE_FILE_NAME, mh01_data, mh01_header.signature_offset, mh01_header.signature_size, ) && chroot.carve_file( ENCRYPTED_DATA_FILE_NAME, mh01_data, mh01_header.encrypted_data_offset, mh01_header.encrypted_data_size, ); } } // No extraction requested, just return success } else { result.success = true; } } } result } ================================================ FILE: src/extractors/pcap.rs ================================================ use crate::common::is_offset_safe; use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::pcap::{parse_pcapng_block, parse_pcapng_section_block}; /// Defines the internal extractor function for extracting pcap-ng files /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::pcap::pcapng_extractor; /// /// match pcapng_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn pcapng_extractor() -> Extractor { Extractor { do_not_recurse: true, utility: ExtractorType::Internal(pcapng_carver), ..Default::default() } } /// Carves a pcap-ng file to disk pub fn pcapng_carver( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { // Output file name const OUTPUT_FILE_NAME: &str = "capture.pcapng"; // Pcap-NG files must have at least two blocks: a section header block and an interface description block const MIN_BLOCK_COUNT: usize = 2; // Return value let mut result = ExtractionResult { ..Default::default() }; // All pcap-ng files start with a section header; parse it if let Ok(section_header) = parse_pcapng_section_block(&file_data[offset..]) { let mut block_count: usize = 1; let available_data = file_data.len() - offset; let mut next_offset = offset + section_header.block_size; let mut previous_offset = None; // Loop through all blocks in the pcap-ng file while is_offset_safe(available_data, next_offset, previous_offset) { match file_data.get(next_offset..) { None => { break; } Some(block_data) => { // Parse the next block header match parse_pcapng_block(block_data, §ion_header.endianness) { Err(_) => { break; } Ok(block_header) => { // This block looks valid, go to the next one block_count += 1; previous_offset = Some(next_offset); next_offset += block_header.block_size; } } } } } // Must have processed the minimum number of blocks if block_count >= MIN_BLOCK_COUNT { // Everything looks OK result.size = Some(next_offset - offset); result.success = true; // Do extraction if requested if output_directory.is_some() { let chroot = Chroot::new(output_directory); result.success = chroot.carve_file(OUTPUT_FILE_NAME, file_data, offset, result.size.unwrap()); } } } result } ================================================ FILE: src/extractors/pem.rs ================================================ use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use aho_corasick::AhoCorasick; /// Defines the internal extractor function for carving out PEM keys /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::pem::pem_key_extractor; /// /// match pem_key_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn pem_key_extractor() -> Extractor { Extractor { do_not_recurse: true, utility: ExtractorType::Internal(pem_key_carver), ..Default::default() } } /// Internal extractor function for carving out PEM certs /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::pem::pem_certificate_extractor; /// /// match pem_certificate_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn pem_certificate_extractor() -> Extractor { Extractor { do_not_recurse: true, utility: ExtractorType::Internal(pem_certificate_carver), ..Default::default() } } pub fn pem_certificate_carver( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const CERTIFICATE_FILE_NAME: &str = "pem.crt"; pem_carver( file_data, offset, output_directory, Some(CERTIFICATE_FILE_NAME), ) } pub fn pem_key_carver( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const KEY_FILE_NAME: &str = "pem.key"; pem_carver(file_data, offset, output_directory, Some(KEY_FILE_NAME)) } pub fn pem_carver( file_data: &[u8], offset: usize, output_directory: Option<&str>, fname: Option<&str>, ) -> ExtractionResult { let mut result = ExtractionResult { ..Default::default() }; if let Some(pem_size) = get_pem_size(file_data, offset) { result.size = Some(pem_size); result.success = true; if let Some(outfile) = fname { if output_directory.is_some() { let chroot = Chroot::new(output_directory); result.success = chroot.carve_file(outfile, file_data, offset, result.size.unwrap()); } } } result } fn get_pem_size(file_data: &[u8], start_of_pem_offset: usize) -> Option { let eof_markers = vec![ b"-----END PUBLIC KEY-----".to_vec(), b"-----END CERTIFICATE-----".to_vec(), b"-----END PRIVATE KEY-----".to_vec(), b"-----END EC PRIVATE KEY-----".to_vec(), b"-----END RSA PRIVATE KEY-----".to_vec(), b"-----END DSA PRIVATE KEY-----".to_vec(), b"-----END OPENSSH PRIVATE KEY-----".to_vec(), ]; let newline_chars: Vec = vec![0x0D, 0x0A]; let grep = AhoCorasick::new(eof_markers.clone()).unwrap(); // Find the first end marker if let Some(eof_match) = grep .find_overlapping_iter(&file_data[start_of_pem_offset..]) .next() { let eof_marker_index: usize = eof_match.pattern().as_usize(); let mut pem_size = eof_match.start() + eof_markers[eof_marker_index].len(); // Include any trailing newline characters in the total size of the PEM file while (start_of_pem_offset + pem_size) < file_data.len() { if newline_chars.contains(&file_data[start_of_pem_offset + pem_size]) { pem_size += 1; } else { break; } } return Some(pem_size); } None } ================================================ FILE: src/extractors/png.rs ================================================ use crate::common::is_offset_safe; use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::png::parse_png_chunk_header; /// Defines the internal extractor function for carving out PNG images /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::png::png_extractor; /// /// match png_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn png_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(extract_png_image), ..Default::default() } } /// Internal extractor for carving PNG files to disk pub fn extract_png_image( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const PNG_HEADER_LEN: usize = 8; const OUTFILE_NAME: &str = "image.png"; let mut result = ExtractionResult { ..Default::default() }; // Parse all the PNG chunks to determine the size of PNG data; first chunk starts immediately after the 8-byte PNG header if let Some(png_data) = file_data.get(offset + PNG_HEADER_LEN..) { if let Some(png_data_size) = get_png_data_size(png_data) { // Total size is the size of the header plus the size of the data result.size = Some(png_data_size + PNG_HEADER_LEN); result.success = true; // If extraction was requested, extract the PNG if output_directory.is_some() { let chroot = Chroot::new(output_directory); result.success = chroot.carve_file(OUTFILE_NAME, file_data, offset, result.size.unwrap()); } } } result } fn get_png_data_size(png_chunk_data: &[u8]) -> Option { let available_data = png_chunk_data.len(); let mut png_chunk_offset: usize = 0; let mut previous_png_chunk_offset = None; // Loop until we run out of data while is_offset_safe(available_data, png_chunk_offset, previous_png_chunk_offset) { // Parse this PNG chunk header match parse_png_chunk_header(&png_chunk_data[png_chunk_offset..]) { Ok(chunk_header) => { // The next chunk header will start immediately after this chunk previous_png_chunk_offset = Some(png_chunk_offset); png_chunk_offset += chunk_header.total_size; // If this was the last chunk, then png_chunk_offset is the total size of the PNG data if chunk_header.is_last_chunk { return Some(png_chunk_offset); } } Err(_) => break, } } None } ================================================ FILE: src/extractors/rar.rs ================================================ use crate::extractors; /// Describes how to run the unrar utility to extract RAR archives /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::rar::rar_extractor; /// /// match rar_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn rar_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("unrar".to_string()), extension: "rar".to_string(), arguments: vec![ "x".to_string(), // Perform extraction "-y".to_string(), // Answer yes to all questions "-ppassword".to_string(), // Set the password to 'password' for password protected rar files extractors::common::SOURCE_FILE_PLACEHOLDER.to_string(), ], exit_codes: vec![0], ..Default::default() } } ================================================ FILE: src/extractors/riff.rs ================================================ use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::riff::parse_riff_header; /// Describes the internal RIFF image extactor /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::riff::riff_extractor; /// /// match riff_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn riff_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(extract_riff_image), do_not_recurse: true, ..Default::default() } } /// Internal extractor for carving RIFF files to disk pub fn extract_riff_image( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const OUTFILE_NAME: &str = "image.riff"; const WAV_OUTFILE_NAME: &str = "video.wav"; const WAV_TYPE: &str = "WAVE"; let mut result = ExtractionResult { ..Default::default() }; if let Ok(riff_header) = parse_riff_header(&file_data[offset..]) { result.size = Some(riff_header.size); result.success = true; if output_directory.is_some() { let chroot = Chroot::new(output_directory); let file_path: String = if riff_header.chunk_type == WAV_TYPE { WAV_OUTFILE_NAME.to_string() } else { OUTFILE_NAME.to_string() }; result.success = chroot.carve_file(file_path, file_data, offset, result.size.unwrap()); } } result } ================================================ FILE: src/extractors/romfs.rs ================================================ use crate::common::is_offset_safe; use crate::extractors::common::{ Chroot, ExtractionError, ExtractionResult, Extractor, ExtractorType, }; use crate::structures::romfs::{parse_romfs_file_entry, parse_romfs_header}; use log::warn; #[derive(Default, Debug, Clone)] struct RomFSEntry { info: usize, size: usize, name: String, offset: usize, file_type: usize, executable: bool, directory: bool, regular: bool, block_device: bool, character_device: bool, fifo: bool, socket: bool, symlink: bool, symlink_target: String, device_major: usize, device_minor: usize, children: Vec, } /// Defines the internal extractor function for extracting RomFS file systems */ /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::romfs::romfs_extractor; /// /// match romfs_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn romfs_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(extract_romfs), ..Default::default() } } /// Internal RomFS extractor pub fn extract_romfs( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { let mut result = ExtractionResult { ..Default::default() }; // Parse the RomFS header if let Ok(romfs_header) = parse_romfs_header(&file_data[offset..]) { // Calculate start and end offsets of RomFS image let romfs_data_start: usize = offset; let romfs_data_end: usize = romfs_data_start + romfs_header.image_size; // Sanity check reported image size and get the romfs data if let Some(romfs_data) = file_data.get(romfs_data_start..romfs_data_end) { // Process the RomFS file entries if let Ok(root_entries) = process_romfs_entries(romfs_data, romfs_header.header_size) { // We expect at least one file entry in the root of the RomFS image if !root_entries.is_empty() { // Everything looks good result.success = true; result.size = Some(romfs_header.image_size); // Do extraction, if an output directory was provided if output_directory.is_some() { let mut file_count: usize = 0; let root_parent = "".to_string(); // RomFS files will be extracted to a sub-directory under the specified // extraction directory whose name is the RomFS volume name. let chroot = Chroot::new(output_directory); let romfs_chroot_dir = chroot.chrooted_path(&romfs_header.volume_name); // Create the romfs output directory, ensuring that it is contained inside the specified extraction directory if chroot.create_directory(&romfs_chroot_dir) { // Extract RomFS contents file_count = extract_romfs_entries( romfs_data, &root_entries, &root_parent, &romfs_chroot_dir, ); } // If no files were extracted, extraction was a failure if file_count == 0 { result.success = false; } } } } } } result } // Recursively processes all RomFS file entries and their children, and returns a list of RomFSEntry structures fn process_romfs_entries( romfs_data: &[u8], offset: usize, ) -> Result, ExtractionError> { let mut previous_file_offset = None; let mut file_entries: Vec = vec![]; let mut processed_entries: Vec = vec![]; let ignore_file_names: Vec = vec![".".to_string(), "..".to_string()]; // Total available data let available_data = romfs_data.len(); // File data starts immediately after the image header; the offset passed in should be the end of the header let mut file_offset: usize = offset; /* * Sanity check the available file data against the offset of the next file entry. * The file offset for the next file entry will be 0 when we've reached the end of the entry list. */ while file_offset != 0 && is_offset_safe(available_data, file_offset, previous_file_offset) { // Sanity check, no two entries should exist at the same offset, if so, infinite recursion could ensue if processed_entries.contains(&file_offset) { break; } else { processed_entries.push(file_offset); } // Parse the next file entry if let Ok(file_header) = parse_romfs_file_entry(&romfs_data[file_offset..]) { // Instantiate a new RomFSEntry structure let mut file_entry = RomFSEntry { ..Default::default() }; // Populate basic info file_entry.size = file_header.size; file_entry.info = file_header.info; file_entry.name = file_header.name.clone(); file_entry.symlink = file_header.symlink; file_entry.regular = file_header.regular; file_entry.directory = file_header.directory; file_entry.file_type = file_header.file_type; file_entry.executable = file_header.executable; file_entry.block_device = file_header.block_device; file_entry.character_device = file_header.character_device; file_entry.fifo = file_header.fifo; file_entry.socket = file_header.socket; // Make file_entry.offset an offset relative to the beginning of the RomFS image file_entry.offset = file_offset + file_header.data_offset; // Sanity check the file data offset and size fields if (file_entry.offset + file_entry.size) > romfs_data.len() { warn!("Invalid offset/size specified for file {}", file_entry.name); return Err(ExtractionError); } // Don't do anything special for '.' or '..' directory entries if !ignore_file_names.contains(&file_entry.name) { // Symlinks need their target paths if file_entry.symlink { if let Some(symlink_bytes) = romfs_data.get(file_entry.offset..file_entry.offset + file_entry.size) { match String::from_utf8(symlink_bytes.to_vec()) { Err(e) => { warn!("Failed to convert symlink target path to string: {e}"); return Err(ExtractionError); } Ok(path) => { file_entry.symlink_target = path.clone(); } } } else { break; } // Device files have their major/minor numbers encoded into the info field } else if file_entry.block_device || file_entry.character_device { file_entry.device_minor = file_entry.info & 0xFFFF; file_entry.device_major = (file_entry.info >> 16) & 0xFFFF; } // Directories have children; process them if file_entry.directory { match process_romfs_entries(romfs_data, file_entry.info) { Err(e) => return Err(e), Ok(children) => file_entry.children = children, } } // Only add supported file types to the list of file entries if file_entry.directory || file_entry.symlink || file_entry.regular { file_entries.push(file_entry); } } // The next file header offset is an offset from the beginning of the RomFS image previous_file_offset = Some(file_offset); file_offset = file_header.next_header_offset; } else { // File entry header parsing failed, gtfo break; } } Ok(file_entries) } // Recursively extract all RomFS entries, returns the number of extracted files/directories fn extract_romfs_entries( romfs_data: &[u8], romfs_files: &Vec, parent_directory: &str, chroot_directory: &str, ) -> usize { let mut file_count: usize = 0; let chroot = Chroot::new(Some(chroot_directory)); for file_entry in romfs_files { let extraction_success: bool; let file_path = chroot.safe_path_join(parent_directory, &file_entry.name); if file_entry.directory { extraction_success = chroot.create_directory(&file_path); } else if file_entry.regular { extraction_success = chroot.carve_file(&file_path, romfs_data, file_entry.offset, file_entry.size); } else if file_entry.symlink { extraction_success = chroot.create_symlink(&file_path, &file_entry.symlink_target); } else if file_entry.fifo { extraction_success = chroot.create_fifo(&file_path); } else if file_entry.socket { extraction_success = chroot.create_socket(&file_path); } else if file_entry.block_device { extraction_success = chroot.create_block_device( &file_path, file_entry.device_major, file_entry.device_minor, ); } else if file_entry.character_device { extraction_success = chroot.create_character_device( &file_path, file_entry.device_major, file_entry.device_minor, ); } else { continue; } if extraction_success { file_count += 1; // Extract the children of a directory if file_entry.directory && !file_entry.children.is_empty() { file_count += extract_romfs_entries( romfs_data, &file_entry.children, &file_path, chroot_directory, ); } // Make executable files executable if file_entry.regular && file_entry.executable { chroot.make_executable(&file_path); } } else { warn!("Failed to extract RomFS file {file_path}"); } } // Return the number of files extracted file_count } ================================================ FILE: src/extractors/sevenzip.rs ================================================ use crate::extractors; /// Describes how to run the 7z utility, supports multiple file formats /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::sevenzip::sevenzip_extractor; /// /// match sevenzip_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn sevenzip_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("7zz".to_string()), extension: "bin".to_string(), arguments: vec![ "x".to_string(), // Perform extraction "-y".to_string(), // Assume Yes to all questions "-o.".to_string(), // Output to current working directory "-p''".to_string(), // Blank password to prevent hangs if archives are password protected extractors::common::SOURCE_FILE_PLACEHOLDER.to_string(), ], // If there is trailing data after the compressed data, extraction will happen but exit code will be 2 exit_codes: vec![0, 2], ..Default::default() } } ================================================ FILE: src/extractors/squashfs.rs ================================================ use crate::extractors; /// Describes how to run the sasquatch utility to extract SquashFS images /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::squashfs::squashfs_extractor; /// /// match squashfs_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn squashfs_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("sasquatch".to_string()), extension: "sqsh".to_string(), arguments: vec![extractors::common::SOURCE_FILE_PLACEHOLDER.to_string()], // Exit code may be 0 or 2; 2 indicates running as not root, but otherwise extraction is ok exit_codes: vec![0, 2], ..Default::default() } } /// Describes how to run the sasquatch utility to extract little endian SquashFS images /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::squashfs::squashfs_le_extractor; /// /// match squashfs_le_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn squashfs_le_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("sasquatch".to_string()), extension: "sqsh".to_string(), arguments: vec![ "-le".to_string(), extractors::common::SOURCE_FILE_PLACEHOLDER.to_string(), ], // Exit code may be 0 or 2; 2 indicates running as not root, but otherwise extraction is ok exit_codes: vec![0, 2], ..Default::default() } } /// Describes how to run the sasquatch utility to extract big endian SquashFS images /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::squashfs::squashfs_be_extractor; /// /// match squashfs_be_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn squashfs_be_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("sasquatch".to_string()), extension: "sqsh".to_string(), arguments: vec![ "-be".to_string(), extractors::common::SOURCE_FILE_PLACEHOLDER.to_string(), ], // Exit code may be 0 or 2; 2 indicates running as not root, but otherwise extraction is ok exit_codes: vec![0, 2], ..Default::default() } } /// Describes how to run the sasquatch-v4be utility to extract big endian SquashFSv4 images /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::squashfs::squashfs_v4_be_extractor; /// /// match squashfs_v4_be_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn squashfs_v4_be_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("sasquatch-v4be".to_string()), extension: "sqsh".to_string(), arguments: vec![extractors::common::SOURCE_FILE_PLACEHOLDER.to_string()], // Exit code may be 0 or 2; 2 indicates running as not root, but otherwise extraction is ok exit_codes: vec![0, 2], ..Default::default() } } ================================================ FILE: src/extractors/srec.rs ================================================ use crate::extractors; /// Describes how to run the srec_cat utility to convert Motorola S-records to binary /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::srec::srec_extractor; /// /// match srec_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn srec_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("srec_cat".to_string()), extension: "hex".to_string(), arguments: vec![ "-output".to_string(), "s-record.bin".to_string(), "-binary".to_string(), extractors::common::SOURCE_FILE_PLACEHOLDER.to_string(), ], exit_codes: vec![0], ..Default::default() } } ================================================ FILE: src/extractors/svg.rs ================================================ use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::svg::parse_svg_image; /// Defines the internal extractor function for carving out SVG images /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::svg::svg_extractor; /// /// match svg_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn svg_extractor() -> Extractor { Extractor { do_not_recurse: true, utility: ExtractorType::Internal(extract_svg_image), ..Default::default() } } /// Internal extractor for carving SVG images to disk pub fn extract_svg_image( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const OUTFILE_NAME: &str = "image.svg"; let mut result = ExtractionResult { ..Default::default() }; // Parse the SVG image to determine its total size if let Ok(svg_image) = parse_svg_image(&file_data[offset..]) { result.size = Some(svg_image.total_size); result.success = true; if output_directory.is_some() { let chroot = Chroot::new(output_directory); result.success = chroot.carve_file(OUTFILE_NAME, file_data, offset, result.size.unwrap()); } } result } ================================================ FILE: src/extractors/swapped.rs ================================================ use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; /// Defines the internal extractor function for u16 swapped firmware images /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::swapped::swapped_extractor_u16; /// /// match swapped_extractor_u16().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn swapped_extractor_u16() -> Extractor { Extractor { utility: ExtractorType::Internal(extract_swapped_u16), ..Default::default() } } /// Extract firmware where every two bytes have been swapped pub fn extract_swapped_u16( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const SWAP_BYTE_COUNT: usize = 2; extract_swapped(file_data, offset, output_directory, SWAP_BYTE_COUNT) } /// Extract a block of data where every n bytes have been swapped fn extract_swapped( file_data: &[u8], offset: usize, output_directory: Option<&str>, n: usize, ) -> ExtractionResult { const OUTPUT_FILE_NAME: &str = "swapped.bin"; let mut result = ExtractionResult { ..Default::default() }; if let Some(data) = file_data.get(offset..) { let swapped_data = byte_swap(data, n); result.success = !swapped_data.is_empty(); if result.success { result.size = Some(swapped_data.len()); // Write to file, if requested if output_directory.is_some() { let chroot = Chroot::new(output_directory); result.success = chroot.create_file(OUTPUT_FILE_NAME, &swapped_data); } } } result } /// Swap every n bytes of the provided data /// /// ## Example: /// /// ``` /// use binwalk::extractors::swapped::byte_swap; /// /// assert_eq!(byte_swap(b"ABCD", 2), b"CDAB"); /// ``` pub fn byte_swap(data: &[u8], n: usize) -> Vec { let chunk_size = n * 2; let mut chunker = data.chunks(chunk_size); let mut swapped_data: Vec = Vec::new(); loop { match chunker.next() { None => { break; } Some(chunk) => { if chunk.len() != chunk_size { break; } swapped_data.extend(chunk[n..].to_vec()); swapped_data.extend(chunk[0..n].to_vec()); } } } swapped_data } ================================================ FILE: src/extractors/tarball.rs ================================================ use crate::extractors; /// Describes how to run the tar utility to extract tarball archives /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::tarball::tarball_extractor; /// /// match tarball_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn tarball_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("tar".to_string()), extension: "tar".to_string(), arguments: vec![ "-x".to_string(), "-f".to_string(), extractors::common::SOURCE_FILE_PLACEHOLDER.to_string(), ], // Exit code may be 2 if attempting to create special device files fails exit_codes: vec![0, 2], ..Default::default() } } ================================================ FILE: src/extractors/trx.rs ================================================ use crate::common::crc32; use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::trx::parse_trx_header; /// Defines the internal TRX extractor /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::trx::trx_extractor; /// /// match trx_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn trx_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(extract_trx_partitions), ..Default::default() } } /// Internal extractor for TRX partitions pub fn extract_trx_partitions( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const CRC_DATA_START_OFFSET: usize = 12; let mut result = ExtractionResult { ..Default::default() }; // Get the TRX data and parse the header if let Some(trx_header_data) = file_data.get(offset..) { if let Ok(trx_header) = parse_trx_header(trx_header_data) { let crc_data_start = offset + CRC_DATA_START_OFFSET; let crc_data_end = crc_data_start + trx_header.total_size - CRC_DATA_START_OFFSET; if let Some(crc_data) = file_data.get(crc_data_start..crc_data_end) { if trx_crc32(crc_data) == trx_header.checksum { result.success = true; result.size = Some(trx_header.total_size); // If extraction was requested, carve the TRX partitions if output_directory.is_some() { let chroot = Chroot::new(output_directory); for i in 0..trx_header.partitions.len() { let next_partition: usize = i + 1; let this_partition_relative_offset: usize = trx_header.partitions[i]; let this_partition_absolute_offset: usize = offset + this_partition_relative_offset; let mut this_partition_size: usize = trx_header.total_size - this_partition_relative_offset; if next_partition < trx_header.partitions.len() { this_partition_size = trx_header.partitions[next_partition] - this_partition_relative_offset; } let this_partition_file_name = format!("partition_{i}.bin"); result.success = chroot.carve_file( &this_partition_file_name, file_data, this_partition_absolute_offset, this_partition_size, ); if !result.success { break; } } } } } } } result } fn trx_crc32(crc_data: &[u8]) -> usize { (crc32(crc_data) ^ 0xFFFFFFFF) as usize } ================================================ FILE: src/extractors/tsk.rs ================================================ use crate::extractors; /// Describes how to run the tsk_recover utility to extract various file systems pub fn tsk_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("tsk_recover".to_string()), extension: "img".to_string(), arguments: vec![ "-i".to_string(), // Set input type to "raw" "raw".to_string(), "-a".to_string(), // Only recover allocated files extractors::common::SOURCE_FILE_PLACEHOLDER.to_string(), "rootfs".to_string(), // Ouput directory ], exit_codes: vec![0], ..Default::default() } } ================================================ FILE: src/extractors/ubi.rs ================================================ use crate::extractors; /// Describes how to run the ubireader_extract_images utility to extract UBI images /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::ubi::ubi_extractor; /// /// match ubi_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn ubi_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External( "ubireader_extract_images".to_string(), ), extension: "img".to_string(), arguments: vec![extractors::common::SOURCE_FILE_PLACEHOLDER.to_string()], exit_codes: vec![0], ..Default::default() } } /// Describes how to run the ubireader_extract_files utility to extract UBIFS images pub fn ubifs_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("ubireader_extract_files".to_string()), extension: "ubifs".to_string(), arguments: vec![extractors::common::SOURCE_FILE_PLACEHOLDER.to_string()], exit_codes: vec![0], ..Default::default() } } ================================================ FILE: src/extractors/uefi.rs ================================================ use crate::extractors; /// Describes how to run the uefi-firmware-parser utility to extract UEFI images /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::uefi::uefi_extractor; /// /// match uefi_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn uefi_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("uefi-firmware-parser".to_string()), extension: "img".to_string(), arguments: vec![ "-o.".to_string(), // Output to the current working directory "-q".to_string(), // Don't print verbose output "-e".to_string(), // Extract extractors::common::SOURCE_FILE_PLACEHOLDER.to_string(), ], exit_codes: vec![0], /* * This extractor recursively pulls out all the UEFI stuff *and* leaves raw copies of the extracted data on disk. * Recursing into this data would result in double extractions for no good reason. */ do_not_recurse: true, } } ================================================ FILE: src/extractors/uimage.rs ================================================ use crate::common::crc32; use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::uimage::parse_uimage_header; /// Describes the internal extractor for carving uImage files to disk /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::uimage::uimage_extractor; /// /// match uimage_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn uimage_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(extract_uimage), ..Default::default() } } pub fn extract_uimage( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { // If no name is povided in the uImage header, use this as the output file name const DEFAULT_OUTPUT_FILE_NAME: &str = "uimage_data"; const OUTPUT_FILE_EXT: &str = "bin"; let mut result = ExtractionResult { ..Default::default() }; // Get the uImage data and parse the header if let Some(uimage_header_data) = file_data.get(offset..) { if let Ok(uimage_header) = parse_uimage_header(uimage_header_data) { let image_data_start = offset + uimage_header.header_size; let image_data_end = image_data_start + uimage_header.data_size; // Get the raw image data after the uImage header to validate the data CRC if let Some(image_data) = file_data.get(image_data_start..image_data_end) { result.success = true; result.size = Some(uimage_header.header_size); // Check the data CRC let data_crc_valid: bool = crc32(image_data) == (uimage_header.data_checksum as u32); // If the data CRC is valid, include the size of the data in the reported size if data_crc_valid { result.size = Some(result.size.unwrap() + uimage_header.data_size); } // If extraction was requested and the data CRC is valid, carve the uImage data out to a file if data_crc_valid && output_directory.is_some() { let chroot = Chroot::new(output_directory); let mut file_base_name: String = DEFAULT_OUTPUT_FILE_NAME.to_string(); // Use the name specified in the uImage header as the file name, if one was provided if !uimage_header.name.is_empty() { file_base_name = uimage_header.name.replace(" ", "_"); } let output_file = format!("{file_base_name}.{OUTPUT_FILE_EXT}"); result.success = chroot.create_file(&output_file, image_data); } } } } result } ================================================ FILE: src/extractors/vxworks.rs ================================================ use crate::common::is_offset_safe; use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::vxworks::{ VxWorksSymbolTableEntry, get_symtab_endianness, parse_symtab_entry, }; use log::error; use serde_json; /// Describes the VxWorks symbol table extractor /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::vxworks::vxworks_symtab_extractor; /// /// match vxworks_symtab_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn vxworks_symtab_extractor() -> Extractor { Extractor { do_not_recurse: true, utility: ExtractorType::Internal(extract_symbol_table), ..Default::default() } } /// Internal extractor for writing VxWorks symbol tables to JSON pub fn extract_symbol_table( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { const MIN_VALID_ENTRIES: usize = 250; const OUTFILE_NAME: &str = "symtab.json"; let mut result = ExtractionResult { ..Default::default() }; let available_data = file_data.len(); let mut previous_entry_offset = None; let mut symtab_entry_offset: usize = offset; let mut symtab_entries: Vec = vec![]; // Determine the symbol table endianness first if let Ok(endianness) = get_symtab_endianness(&file_data[symtab_entry_offset..]) { // Loop through all the symbol table entries, until we run out of data or hit an invalid entry while is_offset_safe(available_data, symtab_entry_offset, previous_entry_offset) { // Parse the symbol table entry match parse_symtab_entry(&file_data[symtab_entry_offset..], &endianness) { // Break on an invalid entry Err(_) => { break; } // Increment symtab_entry_offset to the offset of the next entry and keep a list of all processed entries Ok(entry) => { previous_entry_offset = Some(symtab_entry_offset); symtab_entry_offset += entry.size; symtab_entries.push(entry); } } } } // Sanity check the number of symbols in the symbol table; there are usualy MANY if symtab_entries.len() >= MIN_VALID_ENTRIES { result.success = true; result.size = Some(symtab_entry_offset - offset); // This is not a drill! if output_directory.is_some() { let chroot = Chroot::new(output_directory); // Convert symbol table entires to JSON match serde_json::to_string_pretty(&symtab_entries) { // This should never happen... Err(e) => { error!("Failed to convert VxWorks symbol table to JSON: {e}"); } // Write JSON to file Ok(symtab_json) => { result.success = chroot.create_file(OUTFILE_NAME, &symtab_json.clone().into_bytes()); } } } } result } ================================================ FILE: src/extractors/wince.rs ================================================ use crate::common::is_offset_safe; use crate::extractors::common::{Chroot, ExtractionResult, Extractor, ExtractorType}; use crate::structures::wince::{parse_wince_block_header, parse_wince_header}; /// Defines the internal extractor function for extracting Windows CE images /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::wince::wince_extractor; /// /// match wince_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn wince_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(wince_dump), ..Default::default() } } /// Internal extractor for extracting data blocks from Windows CE images pub fn wince_dump( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { let mut result = ExtractionResult { ..Default::default() }; // Parse the file header if let Some(wince_data) = file_data.get(offset..) { if let Ok(wince_header) = parse_wince_header(wince_data) { // Get the block data, immediately following the file header if let Some(wince_block_data) = wince_data.get(wince_header.header_size..) { // Process all blocks in the block data if let Some(data_blocks) = process_wince_blocks(wince_block_data) { // The first block entry's address should equal the WinCE header's base address if data_blocks.entries[0].address == wince_header.base_address { // Block processing was successful result.success = true; result.size = Some(wince_header.header_size + data_blocks.total_size); // If extraction was requested, extract each block to a file on disk if output_directory.is_some() { let chroot = Chroot::new(output_directory); for block in data_blocks.entries { let block_file_name = format!("{:X}.bin", block.address); // If file carving fails, report a failure to extract if !chroot.carve_file( block_file_name, wince_block_data, block.offset, block.size, ) { result.success = false; break; } } } } } } } } result } /// Stores info about each WinCE block #[derive(Debug, Default, Clone)] struct BlockInfo { pub address: usize, pub offset: usize, pub size: usize, } /// Stores info about all WinCE blocks #[derive(Debug, Default, Clone)] struct BlockData { pub total_size: usize, pub entries: Vec, } /// Process all WinCE blocks fn process_wince_blocks(blocks_data: &[u8]) -> Option { // Arbitrarily chosen, just to make sure more than one or two blocks were processed and sane const MIN_ENTRIES_COUNT: usize = 5; let mut blocks = BlockData { ..Default::default() }; let mut next_offset: usize = 0; let mut previous_offset = None; let available_data = blocks_data.len(); // Process all blocks until the end block is reached, or an error is encountered while is_offset_safe(available_data, next_offset, previous_offset) { // Parse this block's header match parse_wince_block_header(&blocks_data[next_offset..]) { Err(_) => { break; } Ok(block_header) => { // Include the block header size in the total size of the block data blocks.total_size += block_header.header_size; // A block header address of NULL indicates EOF if block_header.address == 0 { // Sanity check the number of blocks processed if blocks.entries.len() > MIN_ENTRIES_COUNT { return Some(blocks); } else { break; } } else { // Include this block's size in the total size of the block data blocks.total_size += block_header.data_size; // Add this block to the list of block entries blocks.entries.push(BlockInfo { address: block_header.address, offset: next_offset + block_header.header_size, size: block_header.data_size, }); // Update the offsets for the next loop iteration previous_offset = Some(next_offset); next_offset += block_header.header_size + block_header.data_size; } } } } None } ================================================ FILE: src/extractors/yaffs2.rs ================================================ use crate::extractors; /// Describes how to run the unyaffs utility to extract YAFFS2 file systems /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::yaffs2::yaffs2_extractor; /// /// match yaffs2_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn yaffs2_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("unyaffs".to_string()), extension: "img".to_string(), arguments: vec![ extractors::common::SOURCE_FILE_PLACEHOLDER.to_string(), "yaffs-root".to_string(), ], exit_codes: vec![0], ..Default::default() } } ================================================ FILE: src/extractors/zlib.rs ================================================ use crate::extractors::common::{ExtractionResult, Extractor, ExtractorType}; use crate::extractors::inflate; /// Size of the checksum that follows the ZLIB deflate data stream pub const CHECKSUM_SIZE: usize = 4; /// Defines the internal extractor function for decompressing zlib data /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::zlib::zlib_extractor; /// /// match zlib_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn zlib_extractor() -> Extractor { Extractor { utility: ExtractorType::Internal(zlib_decompress), ..Default::default() } } /// Internal extractor for decompressing ZLIB data pub fn zlib_decompress( file_data: &[u8], offset: usize, output_directory: Option<&str>, ) -> ExtractionResult { // Size of the zlib header const HEADER_SIZE: usize = 2; let mut exresult = ExtractionResult { ..Default::default() }; // Do the decompression, ignoring the ZLIB header let inflate_result = inflate::inflate_decompressor(file_data, offset + HEADER_SIZE, output_directory); // Check that the data decompressed OK if inflate_result.success { // Calculate the ZLIB checksum offsets let checksum_start = offset + HEADER_SIZE + inflate_result.size; let checksum_end = checksum_start + CHECKSUM_SIZE; // Get the ZLIB checksum if let Some(adler32_checksum_bytes) = file_data.get(checksum_start..checksum_end) { let reported_checksum = u32::from_be_bytes(adler32_checksum_bytes.try_into().unwrap()); // Make sure the checksum matches if reported_checksum == inflate_result.adler32 { exresult.success = true; exresult.size = Some(HEADER_SIZE + inflate_result.size + CHECKSUM_SIZE); } } } exresult } ================================================ FILE: src/extractors/zstd.rs ================================================ use crate::extractors; /// Describes how to run the zstd utility to extract ZSTD compressed files /// /// ``` /// use std::io::ErrorKind; /// use std::process::Command; /// use binwalk::extractors::common::ExtractorType; /// use binwalk::extractors::zstd::zstd_extractor; /// /// match zstd_extractor().utility { /// ExtractorType::None => panic!("Invalid extractor type of None"), /// ExtractorType::Internal(func) => println!("Internal extractor OK: {:?}", func), /// ExtractorType::External(cmd) => { /// if let Err(e) = Command::new(&cmd).output() { /// if e.kind() == ErrorKind::NotFound { /// panic!("External extractor '{}' not found", cmd); /// } else { /// panic!("Failed to execute external extractor '{}': {}", cmd, e); /// } /// } /// } /// } /// ``` pub fn zstd_extractor() -> extractors::common::Extractor { extractors::common::Extractor { utility: extractors::common::ExtractorType::External("zstd".to_string()), extension: "zst".to_string(), arguments: vec![ "-k".to_string(), // Don't delete input files (we do this ourselves) "-f".to_string(), // Force overwrite if output file, for some reason, exists (disables y/n prompts) "-d".to_string(), // Perform a decompression extractors::common::SOURCE_FILE_PLACEHOLDER.to_string(), ], exit_codes: vec![0], ..Default::default() } } ================================================ FILE: src/extractors.rs ================================================ //! # File Extractors //! //! File extractors may be internal (written in Rust, compiled into Binwalk), or external (command line utilties). //! //! While the former are generally faster, safer, and portable, the latter requires very little code to implement. //! //! Binwalk relies on various internal and external utilities for automated file extraction. //! //! ## External Extractors //! //! To implement an external extractor, you must use the `extractors::common::Extractor` struct to define: //! //! - The name of a command-line utility to run //! - What arguments to pass to it //! - What file extension the utility expects //! - Which exit codes are considered successful (the default is exit code `0`) //! //! ### Example //! //! We want to define an external extractor for a new, contrived, file type, `FooBar`. A command-line utility, //! `unfoobar`, exists, and is typically executed as such: //! //! ```bash //! unfoobar -x -f input_file.bin -o data.foobar //! ``` //! //! To define this external utility as an extractor: //! //! ```no_run //! use binwalk::extractors::common::{Extractor, ExtractorType, SOURCE_FILE_PLACEHOLDER}; //! //! /// This function returns an instance of extractors::common::Extractor, which describes how to run the unfoobar utility. //! pub fn foobar_extractor() -> Extractor { //! // Build and return the Extractor struct //! return Extractor { //! // This indicates that we are defining an external extractor, named 'unfoobar' //! utility: ExtractorType::External("unfoobar".to_string()), //! // This is the file extension to use when carving the FooBar file system data to disk //! extension: "bin".to_string(), //! // These are the arguments to pass to the unfoobar utility //! arguments: vec![ //! "-x".to_string(), // This argument tells unfoobar to extract the FooBar data //! "-o".to_string(), // Specify an output file //! "data.foobar".to_string(), // The output file name //! "-f".to_string(), // Specify an input file //! // This is a special string that will be replaced at run-time with the name of the source file //! SOURCE_FILE_PLACEHOLDER.to_string() //! ], //! // The only valid exit code for this utility is 0 //! exit_codes: vec![0], //! // If set to true, the extracted files will not be analyzed //! do_not_recurse: false, //! ..Default::default() //! }; //! } //! ``` //! //! ## Internal Extractors //! //! Internal extractors are functions that are repsonsible for extracting the data of a particular file type. //! They must conform to the `extractors::common::InternalExtractor` type definition. //! //! Like external extractors, they are defined using the `extractors::common::Extractor` struct. //! //! The internal extraction function will be passed: //! //! - The entirety of the file data //! - An offset inside the file data at which to begin processing data //! - An output directory for extracted files (optional) //! //! If the output directory is `None`, the extractor function should perform a "dry run", processing the intended file format //! as normal, but must not extract any data; this allows signatures to use the extractor function to validate potential signature //! matches without performing an actual extraction. //! //! Internal extractors must return an `extractors::common::ExtractionResult` struct. //! //! Internal extractors should use the `extractors::common::Chroot` API to write files to disk. //! The methods defined in the `Chroot` struct allow the manipulation of files on disk while ensuring that any file paths //! accessed do not traverse outside the specified output directory. //! //! ### Example //! //! ```ignore //! use binwalk::common::crc32; //! use binwalk::extractors::common::{Chroot, Extractor, ExtractionResult, ExtractorType}; //! use binwalk::structures::foobar::parse_foobar_header; //! //! /// This function *defines* an internal extractor; it is not the actual extractor //! pub fn foobar_extractor() -> Extractor { //! // Build and return the Extractor struct //! return Extractor { //! // This specifies the function extract_foobar_file as the internal extractor to use //! utility: ExtractorType::Internal(extract_foobar_file), //! ..Default::default() //! }; //! } //! //! /// This function extracts the contents of a FooBar file //! pub fn extract_foobar_file(file_data: Vec, offset: usize, output_directory: Option<&str>) -> ExtractionResult { //! //! // This will be the return value //! let mut result = ExtractionResult{..Default::default()}; //! //! // Get the FooBar file data, which starts at the specified offset //! if let Some(foobar_data) = file_data.get(offset..) { //! // Parse and validate the FooBar file header; this function is defined in the structures module //! if let Ok(foobar_header) = parse_foobar_header(foobar_data) { //! // Data CRC is calculated over data_size bytes, starting at the end of the FooBar header //! let crc_start = foobar_header.header_size; //! let crc_end = crc_start + foobar_header.data_size; //! //! if let Some(crc_data) = foobar_data.get(crc_start..crc_end){ //! // Validate the data CRC //! if foobar_header.data_crc == (crc32(crc_data) as usize) { //! // Report the total size of the FooBar file, including header and data //! result.size = Some(foobar_header.header_size + foobar_header.data_size); //! //! // If an output directory was specified, extract the contents of the FooBar file to disk //! if !output_directory.is_none() { //! // Chroot file I/O inside the specified output directory //! let chroot = Chroot::new(output_directory); //! //! // The FooBar file format is very simple: just a header, followed by the data we want to extract. //! // Carve the FooBar data to disk, and set result.success to true if this succeeds. //! result.success = chroot.carve_file(foobar_header.original_file_name, //! foobar_data, //! foobar_header.header_size, //! foobar_header.data_size); //! } else { //! // Nothing else to do, consider this a success //! result.success = true; //! } //! } //! } //! } //! } //! //! return result; //! } //! ``` pub mod androidsparse; pub mod arcadyan; pub mod autel; pub mod bmp; pub mod bzip2; pub mod cab; pub mod common; pub mod csman; pub mod dahua_zip; pub mod dmg; pub mod dtb; pub mod dumpifs; pub mod dxbc; pub mod encfw; pub mod gif; pub mod gpg; pub mod gzip; pub mod inflate; pub mod iso9660; pub mod jboot; pub mod jffs2; pub mod jpeg; pub mod linux; pub mod lz4; pub mod lzfse; pub mod lzma; pub mod lzop; pub mod matter_ota; pub mod mbr; pub mod mh01; pub mod pcap; pub mod pem; pub mod png; pub mod rar; pub mod riff; pub mod romfs; pub mod sevenzip; pub mod squashfs; pub mod srec; pub mod svg; pub mod swapped; pub mod tarball; pub mod trx; pub mod tsk; pub mod ubi; pub mod uefi; pub mod uimage; pub mod vxworks; pub mod wince; pub mod yaffs2; pub mod zlib; pub mod zstd; ================================================ FILE: src/json.rs ================================================ use log::error; use serde::{Deserialize, Serialize}; use std::fs; use std::io; use std::io::Seek; use std::io::Write; use crate::binwalk::AnalysisResults; use crate::display; use crate::entropy::FileEntropy; const STDOUT: &str = "-"; const JSON_LIST_START: &str = "[\n"; const JSON_LIST_END: &str = "\n]\n"; const JSON_LIST_SEP: &str = ",\n"; #[derive(Debug, Serialize, Deserialize)] pub enum JSONType { Entropy(FileEntropy), Analysis(AnalysisResults), } #[derive(Debug, Default, Clone)] pub struct JsonLogger { pub json_file: Option, pub json_file_initialized: bool, } impl JsonLogger { pub fn new(log_file: Option) -> JsonLogger { let mut new_instance = JsonLogger { ..Default::default() }; if log_file.is_some() { new_instance.json_file = Some(log_file.unwrap().clone()); } new_instance } pub fn close(&self) { self.write_json(JSON_LIST_END); } pub fn log(&mut self, results: JSONType) { // Convert analysis results to JSON match serde_json::to_string_pretty(&results) { Err(e) => error!("Failed to convert analysis results to JSON: {e}"), Ok(json) => { if !self.json_file_initialized { self.write_json(JSON_LIST_START); self.json_file_initialized = true; } else { self.write_json(JSON_LIST_SEP); } self.write_json(&json); } } } fn write_json(&self, data: &str) { if let Some(log_file) = &self.json_file { if log_file == STDOUT { display::print_plain(false, data); } else { // Open file for reading and writing, create if does not already exist match fs::OpenOptions::new() .create(true) .append(true) .read(true) .open(log_file) { Err(e) => { error!("Failed to open JSON log file '{log_file}': {e}"); } Ok(mut fp) => { // Seek to the end of the file and get the cursor position match fp.seek(io::SeekFrom::End(0)) { Err(e) => { error!("Failed to seek to end of JSON file: {e}"); } Ok(_) => { if let Err(e) = fp.write_all(data.as_bytes()) { error!("Failed to write to JSON log file: {e}"); } } } } } } } } } ================================================ FILE: src/lib.rs ================================================ //! Rust library for identifying, and optionally extracting, files embedded inside other files. //! //! ## Example //! //!```no_run //! use binwalk::Binwalk; //! //! // Create a new Binwalk instance //! let binwalker = Binwalk::new(); //! //! // Read in the data you want to analyze //! let file_data = std::fs::read("/tmp/firmware.bin").expect("Failed to read from file"); //! //! // Scan the file data and print the results //! for result in binwalker.scan(&file_data) { //! println!("{:#?}", result); //! } //! ``` mod binwalk; pub mod common; pub mod extractors; mod magic; pub mod signatures; pub mod structures; pub use binwalk::{AnalysisResults, Binwalk, BinwalkError}; ================================================ FILE: src/magic.rs ================================================ use crate::extractors; use crate::signatures; /// Returns a list of all supported signatures, including their "magic" byte patterns, parser functions, and any associated extractor. pub fn patterns() -> Vec { let binary_signatures: Vec = vec![ // gzip signatures::common::Signature { name: "gzip".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::gzip::gzip_magic(), parser: signatures::gzip::gzip_parser, description: signatures::gzip::DESCRIPTION.to_string(), extractor: Some(extractors::gzip::gzip_extractor()), }, // .deb signatures::common::Signature { name: "deb".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::deb::deb_magic(), parser: signatures::deb::deb_parser, description: signatures::deb::DESCRIPTION.to_string(), extractor: None, }, // 7-zip signatures::common::Signature { name: "7zip".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::sevenzip::sevenzip_magic(), parser: signatures::sevenzip::sevenzip_parser, description: signatures::sevenzip::DESCRIPTION.to_string(), extractor: Some(extractors::sevenzip::sevenzip_extractor()), }, // xz signatures::common::Signature { name: "xz".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::xz::xz_magic(), parser: signatures::xz::xz_parser, description: signatures::xz::DESCRIPTION.to_string(), extractor: Some(extractors::lzma::lzma_extractor()), }, // tarball signatures::common::Signature { name: "tarball".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::tarball::tarball_magic(), parser: signatures::tarball::tarball_parser, description: signatures::tarball::DESCRIPTION.to_string(), extractor: Some(extractors::tarball::tarball_extractor()), }, // squashfs signatures::common::Signature { name: "squashfs".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::squashfs::squashfs_magic(), parser: signatures::squashfs::squashfs_parser, description: signatures::squashfs::DESCRIPTION.to_string(), extractor: Some(extractors::squashfs::squashfs_extractor()), }, // dlob signatures::common::Signature { name: "dlob".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::dlob::dlob_magic(), parser: signatures::dlob::dlob_parser, description: signatures::dlob::DESCRIPTION.to_string(), extractor: None, }, // lzma signatures::common::Signature { name: "lzma".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::lzma::lzma_magic(), parser: signatures::lzma::lzma_parser, description: signatures::lzma::DESCRIPTION.to_string(), //extractor: Some(extractors::sevenzip::sevenzip_extractor()), extractor: Some(extractors::lzma::lzma_extractor()), }, // bmp signatures::common::Signature { name: "bmp".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::bmp::bmp_magic(), parser: signatures::bmp::bmp_parser, description: signatures::bmp::DESCRIPTION.to_string(), extractor: Some(extractors::bmp::bmp_extractor()), }, // bzip2 signatures::common::Signature { name: "bzip2".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::bzip2::bzip2_magic(), parser: signatures::bzip2::bzip2_parser, description: signatures::bzip2::DESCRIPTION.to_string(), extractor: Some(extractors::bzip2::bzip2_extractor()), }, // uimage signatures::common::Signature { name: "uimage".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::uimage::uimage_magic(), parser: signatures::uimage::uimage_parser, description: signatures::uimage::DESCRIPTION.to_string(), extractor: Some(extractors::uimage::uimage_extractor()), }, // packimg header signatures::common::Signature { name: "packimg".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::packimg::packimg_magic(), parser: signatures::packimg::packimg_parser, description: signatures::packimg::DESCRIPTION.to_string(), extractor: None, }, // crc32 constants signatures::common::Signature { name: "crc32".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::hashes::crc32_magic(), parser: signatures::hashes::crc32_parser, description: signatures::hashes::CRC32_DESCRIPTION.to_string(), extractor: None, }, // sha256 constants signatures::common::Signature { name: "sha256".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::hashes::sha256_magic(), parser: signatures::hashes::sha256_parser, description: signatures::hashes::SHA256_DESCRIPTION.to_string(), extractor: None, }, // cpio signatures::common::Signature { name: "cpio".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::cpio::cpio_magic(), parser: signatures::cpio::cpio_parser, description: signatures::cpio::DESCRIPTION.to_string(), extractor: Some(extractors::sevenzip::sevenzip_extractor()), }, // iso9660 primary volume signatures::common::Signature { name: "iso9660".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::iso9660::iso_magic(), parser: signatures::iso9660::iso_parser, description: signatures::iso9660::DESCRIPTION.to_string(), extractor: Some(extractors::iso9660::iso9660_extractor()), }, // linux kernel signatures::common::Signature { name: "linux_kernel".to_string(), short: false, magic_offset: 0, always_display: true, magic: signatures::linux::linux_kernel_version_magic(), parser: signatures::linux::linux_kernel_version_parser, description: signatures::linux::LINUX_KERNEL_VERSION_DESCRIPTION.to_string(), extractor: Some(extractors::linux::linux_kernel_extractor()), }, // linux boot image signatures::common::Signature { name: "linux_boot_image".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::linux::linux_boot_image_magic(), parser: signatures::linux::linux_boot_image_parser, description: signatures::linux::LINUX_BOOT_IMAGE_DESCRIPTION.to_string(), extractor: None, }, // linux arm zimage signatures::common::Signature { name: "linux_arm_zimage".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::linux::linux_arm_zimage_magic(), parser: signatures::linux::linux_arm_zimage_parser, description: signatures::linux::LINUX_ARM_ZIMAGE_DESCRIPTION.to_string(), extractor: None, }, // zstd signatures::common::Signature { name: "zstd".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::zstd::zstd_magic(), parser: signatures::zstd::zstd_parser, description: signatures::zstd::DESCRIPTION.to_string(), extractor: Some(extractors::zstd::zstd_extractor()), }, // zip signatures::common::Signature { name: "zip".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::zip::zip_magic(), parser: signatures::zip::zip_parser, description: signatures::zip::DESCRIPTION.to_string(), extractor: Some(extractors::sevenzip::sevenzip_extractor()), }, // Intel PCH ROM signatures::common::Signature { name: "pchrom".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::pchrom::pch_rom_magic(), parser: signatures::pchrom::pch_rom_parser, description: signatures::pchrom::DESCRIPTION.to_string(), extractor: Some(extractors::uefi::uefi_extractor()), }, // UEFI PI volume signatures::common::Signature { name: "uefi_pi_volume".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::uefi::uefi_volume_magic(), parser: signatures::uefi::uefi_volume_parser, description: signatures::uefi::VOLUME_DESCRIPTION.to_string(), extractor: Some(extractors::uefi::uefi_extractor()), }, // UEFI capsule image signatures::common::Signature { name: "uefi_capsule".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::uefi::uefi_capsule_magic(), parser: signatures::uefi::uefi_capsule_parser, description: signatures::uefi::CAPSULE_DESCRIPTION.to_string(), extractor: Some(extractors::uefi::uefi_extractor()), }, // PDF document signatures::common::Signature { name: "pdf".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::pdf::pdf_magic(), parser: signatures::pdf::pdf_parser, description: signatures::pdf::DESCRIPTION.to_string(), extractor: None, }, // ELF signatures::common::Signature { name: "elf".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::elf::elf_magic(), parser: signatures::elf::elf_parser, description: signatures::elf::DESCRIPTION.to_string(), extractor: None, }, // CramFS signatures::common::Signature { name: "cramfs".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::cramfs::cramfs_magic(), parser: signatures::cramfs::cramfs_parser, description: signatures::cramfs::DESCRIPTION.to_string(), extractor: Some(extractors::sevenzip::sevenzip_extractor()), }, // QNX IFS // TODO: The signature and extractor are untested. Need a sample IFS image. signatures::common::Signature { name: "qnx_ifs".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::qnx::qnx_ifs_magic(), parser: signatures::qnx::qnx_ifs_parser, description: signatures::qnx::IFS_DESCRIPTION.to_string(), extractor: Some(extractors::dumpifs::dumpifs_extractor()), }, // RomFS signatures::common::Signature { name: "romfs".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::romfs::romfs_magic(), parser: signatures::romfs::romfs_parser, description: signatures::romfs::DESCRIPTION.to_string(), extractor: Some(extractors::romfs::romfs_extractor()), }, // EXT signatures::common::Signature { name: "ext".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::ext::ext_magic(), parser: signatures::ext::ext_parser, description: signatures::ext::DESCRIPTION.to_string(), extractor: Some(extractors::tsk::tsk_extractor()), }, // CAB archive signatures::common::Signature { name: "cab".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::cab::cab_magic(), parser: signatures::cab::cab_parser, description: signatures::cab::DESCRIPTION.to_string(), extractor: Some(extractors::cab::cab_extractor()), }, // JFFS2 signatures::common::Signature { name: "jffs2".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::jffs2::jffs2_magic(), parser: signatures::jffs2::jffs2_parser, description: signatures::jffs2::DESCRIPTION.to_string(), extractor: Some(extractors::jffs2::jffs2_extractor()), }, // YAFFS signatures::common::Signature { name: "yaffs".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::yaffs::yaffs_magic(), parser: signatures::yaffs::yaffs_parser, description: signatures::yaffs::DESCRIPTION.to_string(), extractor: Some(extractors::yaffs2::yaffs2_extractor()), }, // lz4 signatures::common::Signature { name: "lz4".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::lz4::lz4_magic(), parser: signatures::lz4::lz4_parser, description: signatures::lz4::DESCRIPTION.to_string(), extractor: Some(extractors::lz4::lz4_extractor()), }, // lzop signatures::common::Signature { name: "lzop".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::lzop::lzop_magic(), parser: signatures::lzop::lzop_parser, description: signatures::lzop::DESCRIPTION.to_string(), extractor: Some(extractors::lzop::lzop_extractor()), }, // lzop signatures::common::Signature { name: "pe".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::pe::pe_magic(), parser: signatures::pe::pe_parser, description: signatures::pe::DESCRIPTION.to_string(), extractor: None, }, // zlib signatures::common::Signature { name: "zlib".to_string(), // The magic bytes for this signature are only 2 bytes, only match on the beginning of a file short: true, magic_offset: 0, always_display: false, magic: signatures::zlib::zlib_magic(), parser: signatures::zlib::zlib_parser, description: signatures::zlib::DESCRIPTION.to_string(), extractor: Some(extractors::zlib::zlib_extractor()), }, // gpg signed data signatures::common::Signature { name: "gpg_signed".to_string(), // The magic bytes for this signature are only 2 bytes, only match on the beginning of a file short: true, magic_offset: 0, always_display: false, magic: signatures::gpg::gpg_signed_magic(), parser: signatures::gpg::gpg_signed_parser, description: signatures::gpg::GPG_SIGNED_DESCRIPTION.to_string(), extractor: Some(extractors::gpg::gpg_extractor()), }, // pem certificates signatures::common::Signature { name: "pem_certificate".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::pem::pem_certificate_magic(), parser: signatures::pem::pem_parser, description: signatures::pem::PEM_CERTIFICATE_DESCRIPTION.to_string(), extractor: Some(extractors::pem::pem_certificate_extractor()), }, // pem public keys signatures::common::Signature { name: "pem_public_key".to_string(), short: false, magic_offset: 0, always_display: true, magic: signatures::pem::pem_public_key_magic(), parser: signatures::pem::pem_parser, description: signatures::pem::PEM_PUBLIC_KEY_DESCRIPTION.to_string(), extractor: Some(extractors::pem::pem_key_extractor()), }, // pem private keys signatures::common::Signature { name: "pem_private_key".to_string(), short: false, magic_offset: 0, always_display: true, magic: signatures::pem::pem_private_key_magic(), parser: signatures::pem::pem_parser, description: signatures::pem::PEM_PRIVATE_KEY_DESCRIPTION.to_string(), extractor: Some(extractors::pem::pem_key_extractor()), }, // netgear chk signatures::common::Signature { name: "chk".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::chk::chk_magic(), parser: signatures::chk::chk_parser, description: signatures::chk::DESCRIPTION.to_string(), extractor: None, }, // trx signatures::common::Signature { name: "trx".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::trx::trx_magic(), parser: signatures::trx::trx_parser, description: signatures::trx::DESCRIPTION.to_string(), extractor: Some(extractors::trx::trx_extractor()), }, // Motorola S-record signatures::common::Signature { name: "srecord".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::srec::srec_magic(), parser: signatures::srec::srec_parser, description: signatures::srec::SREC_DESCRIPTION.to_string(), extractor: Some(extractors::srec::srec_extractor()), }, // Motorola S-record (generic) signatures::common::Signature { name: "srecord_generic".to_string(), short: true, magic_offset: 0, always_display: false, magic: signatures::srec::srec_short_magic(), parser: signatures::srec::srec_parser, description: signatures::srec::SREC_SHORT_DESCRIPTION.to_string(), extractor: Some(extractors::srec::srec_extractor()), }, // Android sparse signatures::common::Signature { name: "android_sparse".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::androidsparse::android_sparse_magic(), parser: signatures::androidsparse::android_sparse_parser, description: signatures::androidsparse::DESCRIPTION.to_string(), extractor: Some(extractors::androidsparse::android_sparse_extractor()), }, // device tree blob signatures::common::Signature { name: "dtb".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::dtb::dtb_magic(), parser: signatures::dtb::dtb_parser, description: signatures::dtb::DESCRIPTION.to_string(), extractor: Some(extractors::dtb::dtb_extractor()), }, // ubi signatures::common::Signature { name: "ubi".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::ubi::ubi_magic(), parser: signatures::ubi::ubi_parser, description: signatures::ubi::UBI_IMAGE_DESCRIPTION.to_string(), extractor: Some(extractors::ubi::ubi_extractor()), }, // ubifs signatures::common::Signature { name: "ubifs".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::ubi::ubifs_magic(), parser: signatures::ubi::ubifs_parser, description: signatures::ubi::UBI_FS_DESCRIPTION.to_string(), extractor: Some(extractors::ubi::ubifs_extractor()), }, // cfe bootloader signatures::common::Signature { name: "cfe".to_string(), short: false, magic_offset: 0, always_display: true, magic: signatures::cfe::cfe_magic(), parser: signatures::cfe::cfe_parser, description: signatures::cfe::DESCRIPTION.to_string(), extractor: None, }, // SEAMA firmware header signatures::common::Signature { name: "seama".to_string(), short: false, magic_offset: 0, always_display: true, magic: signatures::seama::seama_magic(), parser: signatures::seama::seama_parser, description: signatures::seama::DESCRIPTION.to_string(), extractor: None, }, // compress'd signatures::common::Signature { name: "compressd".to_string(), short: true, magic_offset: 0, always_display: false, magic: signatures::compressd::compressd_magic(), parser: signatures::compressd::compressd_parser, description: signatures::compressd::DESCRIPTION.to_string(), extractor: Some(extractors::sevenzip::sevenzip_extractor()), }, // rar archive signatures::common::Signature { name: "rar".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::rar::rar_magic(), parser: signatures::rar::rar_parser, description: signatures::rar::DESCRIPTION.to_string(), extractor: Some(extractors::rar::rar_extractor()), }, // PNG image signatures::common::Signature { name: "png".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::png::png_magic(), parser: signatures::png::png_parser, description: signatures::png::DESCRIPTION.to_string(), extractor: Some(extractors::png::png_extractor()), }, // JPEG image signatures::common::Signature { name: "jpeg".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::jpeg::jpeg_magic(), parser: signatures::jpeg::jpeg_parser, description: signatures::jpeg::DESCRIPTION.to_string(), extractor: Some(extractors::jpeg::jpeg_extractor()), }, // arcadyan obfuscated lzma signatures::common::Signature { name: "arcadyan".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::arcadyan::obfuscated_lzma_magic(), parser: signatures::arcadyan::obfuscated_lzma_parser, description: signatures::arcadyan::DESCRIPTION.to_string(), extractor: Some(extractors::arcadyan::obfuscated_lzma_extractor()), }, // copyright text signatures::common::Signature { name: "copyright".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::copyright::copyright_magic(), parser: signatures::copyright::copyright_parser, description: signatures::copyright::DESCRIPTION.to_string(), extractor: None, }, // WIND kernel version signatures::common::Signature { name: "wind_kernel".to_string(), short: false, magic_offset: 0, always_display: true, magic: signatures::vxworks::wind_kernel_magic(), parser: signatures::vxworks::wind_kernel_parser, description: signatures::vxworks::WIND_KERNEL_DESCRIPTION.to_string(), extractor: None, }, // vxworks symbol table signatures::common::Signature { name: "vxworks_symtab".to_string(), short: false, magic_offset: 0, always_display: true, magic: signatures::vxworks::symbol_table_magic(), parser: signatures::vxworks::symbol_table_parser, description: signatures::vxworks::SYMTAB_DESCRIPTION.to_string(), extractor: Some(extractors::vxworks::vxworks_symtab_extractor()), }, // ecos mips exception handler signatures::common::Signature { name: "ecos".to_string(), short: false, magic_offset: 0, always_display: true, magic: signatures::ecos::exception_handler_magic(), parser: signatures::ecos::exception_handler_parser, description: signatures::ecos::EXCEPTION_HANDLER_DESCRIPTION.to_string(), extractor: None, }, // dmg signatures::common::Signature { name: "dmg".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::dmg::dmg_magic(), parser: signatures::dmg::dmg_parser, description: signatures::dmg::DESCRIPTION.to_string(), extractor: Some(extractors::dmg::dmg_extractor()), }, // riff signatures::common::Signature { name: "riff".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::riff::riff_magic(), parser: signatures::riff::riff_parser, description: signatures::riff::DESCRIPTION.to_string(), extractor: Some(extractors::riff::riff_extractor()), }, // openssl signatures::common::Signature { name: "openssl".to_string(), short: false, magic_offset: 0, always_display: true, magic: signatures::openssl::openssl_crypt_magic(), parser: signatures::openssl::openssl_crypt_parser, description: signatures::openssl::DESCRIPTION.to_string(), extractor: Some(extractors::encfw::encfw_extractor()), }, // lzfse signatures::common::Signature { name: "lzfse".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::lzfse::lzfse_magic(), parser: signatures::lzfse::lzfse_parser, description: signatures::lzfse::DESCRIPTION.to_string(), extractor: Some(extractors::lzfse::lzfse_extractor()), }, // MBR signatures::common::Signature { name: "mbr".to_string(), short: true, magic_offset: signatures::mbr::MAGIC_OFFSET, always_display: true, magic: signatures::mbr::mbr_magic(), parser: signatures::mbr::mbr_parser, description: signatures::mbr::DESCRIPTION.to_string(), extractor: Some(extractors::mbr::mbr_extractor()), }, // tp-link signatures::common::Signature { name: "tplink".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::tplink::tplink_magic(), parser: signatures::tplink::tplink_parser, description: signatures::tplink::DESCRIPTION.to_string(), extractor: None, }, // HP PJL signatures::common::Signature { name: "pjl".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::pjl::pjl_magic(), parser: signatures::pjl::pjl_parser, description: signatures::pjl::DESCRIPTION.to_string(), extractor: None, }, // JBOOT ARM firmware image signatures::common::Signature { name: "jboot_arm".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::jboot::jboot_arm_magic(), parser: signatures::jboot::jboot_arm_parser, description: signatures::jboot::JBOOT_ARM_DESCRIPTION.to_string(), extractor: None, }, // JBOOT STAG header signatures::common::Signature { name: "jboot_stag".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::jboot::jboot_stag_magic(), parser: signatures::jboot::jboot_stag_parser, description: signatures::jboot::JBOOT_STAG_DESCRIPTION.to_string(), extractor: None, }, // JBOOT SCH2 header signatures::common::Signature { name: "jboot_sch2".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::jboot::jboot_sch2_magic(), parser: signatures::jboot::jboot_sch2_parser, description: signatures::jboot::JBOOT_SCH2_DESCRIPTION.to_string(), extractor: Some(extractors::jboot::sch2_extractor()), }, // pcap-ng signatures::common::Signature { name: "pcapng".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::pcap::pcapng_magic(), parser: signatures::pcap::pcapng_parser, description: signatures::pcap::PCAPNG_DESCRIPTION.to_string(), extractor: Some(extractors::pcap::pcapng_extractor()), }, // RSA encrypted data signatures::common::Signature { name: "rsa".to_string(), short: false, magic_offset: 0, always_display: true, magic: signatures::rsa::rsa_magic(), parser: signatures::rsa::rsa_parser, description: signatures::rsa::DESCRIPTION.to_string(), extractor: None, }, // GIF image signatures::common::Signature { name: "gif".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::gif::gif_magic(), parser: signatures::gif::gif_parser, description: signatures::gif::DESCRIPTION.to_string(), extractor: Some(extractors::gif::gif_extractor()), }, // SVG image signatures::common::Signature { name: "svg".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::svg::svg_magic(), parser: signatures::svg::svg_parser, description: signatures::svg::DESCRIPTION.to_string(), extractor: Some(extractors::svg::svg_extractor()), }, // Linux ARM64 boot image signatures::common::Signature { name: "linux_arm64_boot_image".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::linux::linux_arm64_boot_image_magic(), parser: signatures::linux::linux_arm64_boot_image_parser, description: signatures::linux::LINUX_ARM64_BOOT_IMAGE_DESCRIPTION.to_string(), extractor: None, }, // FAT signatures::common::Signature { name: "fat".to_string(), short: true, magic_offset: signatures::fat::MAGIC_OFFSET, always_display: false, magic: signatures::fat::fat_magic(), parser: signatures::fat::fat_parser, description: signatures::fat::DESCRIPTION.to_string(), extractor: Some(extractors::tsk::tsk_extractor()), }, // EFI GPT signatures::common::Signature { name: "efigpt".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::efigpt::efigpt_magic(), parser: signatures::efigpt::efigpt_parser, description: signatures::efigpt::DESCRIPTION.to_string(), extractor: Some(extractors::sevenzip::sevenzip_extractor()), }, // RTK firmware header signatures::common::Signature { name: "rtk".to_string(), short: true, magic_offset: 0, always_display: false, magic: signatures::rtk::rtk_magic(), parser: signatures::rtk::rtk_parser, description: signatures::rtk::DESCRIPTION.to_string(), extractor: None, }, // AES S-Box signatures::common::Signature { name: "aes_sbox".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::aes::aes_sbox_magic(), parser: signatures::aes::aes_sbox_parser, description: signatures::aes::DESCRIPTION_AES_SBOX.to_string(), extractor: None, }, // AES Forward table signatures::common::Signature { name: "aes_forward_table".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::aes::aes_forward_table_magic(), parser: signatures::aes::aes_forward_table_parser, description: signatures::aes::DESCRIPTION_AES_FT.to_string(), extractor: None, }, // AES Reverse table signatures::common::Signature { name: "aes_reverse_table".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::aes::aes_reverse_table_magic(), parser: signatures::aes::aes_reverse_table_parser, description: signatures::aes::DESCRIPTION_AES_RT.to_string(), extractor: None, }, // AES RCON signatures::common::Signature { name: "aes_rcon".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::aes::aes_rcon_magic(), parser: signatures::aes::aes_rcon_parser, description: signatures::aes::DESCRIPTION_AES_RCON.to_string(), extractor: None, }, // Accelerated AES signatures::common::Signature { name: "aes_acceleration_table".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::aes::aes_acceleration_table_magic(), parser: signatures::aes::aes_acceleration_table_parser, description: signatures::aes::DESCRIPTION_AES_ACC.to_string(), extractor: None, }, // LUKS signatures::common::Signature { name: "luks".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::luks::luks_magic(), parser: signatures::luks::luks_parser, description: signatures::luks::DESCRIPTION.to_string(), extractor: None, }, // TP-Link RTOS signatures::common::Signature { name: "tplink_rtos".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::tplink::tplink_rtos_magic(), parser: signatures::tplink::tplink_rtos_parser, description: signatures::tplink::RTOS_DESCRIPTION.to_string(), extractor: None, }, // BIN firmware header signatures::common::Signature { name: "binhdr".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::binhdr::bin_hdr_magic(), parser: signatures::binhdr::bin_hdr_parser, description: signatures::binhdr::DESCRIPTION.to_string(), extractor: None, }, // Autel obfuscated firmware signatures::common::Signature { name: "autel".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::autel::autel_magic(), parser: signatures::autel::autel_parser, description: signatures::autel::DESCRIPTION.to_string(), extractor: Some(extractors::autel::autel_extractor()), }, // NTFS signatures::common::Signature { name: "ntfs".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::ntfs::ntfs_magic(), parser: signatures::ntfs::ntfs_parser, description: signatures::ntfs::DESCRIPTION.to_string(), extractor: Some(extractors::tsk::tsk_extractor()), }, // APFS signatures::common::Signature { name: "apfs".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::apfs::apfs_magic(), parser: signatures::apfs::apfs_parser, description: signatures::apfs::DESCRIPTION.to_string(), extractor: Some(extractors::sevenzip::sevenzip_extractor()), }, // BTRFS signatures::common::Signature { name: "btrfs".to_string(), short: false, magic_offset: 0, always_display: true, magic: signatures::btrfs::btrfs_magic(), parser: signatures::btrfs::btrfs_parser, description: signatures::btrfs::DESCRIPTION.to_string(), extractor: None, }, // WinCE signatures::common::Signature { name: "wince".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::wince::wince_magic(), parser: signatures::wince::wince_parser, description: signatures::wince::DESCRIPTION.to_string(), extractor: Some(extractors::wince::wince_extractor()), }, // Dahua ZIP signatures::common::Signature { name: "dahua_zip".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::dahua_zip::dahua_zip_magic(), parser: signatures::dahua_zip::dahua_zip_parser, description: signatures::dahua_zip::DESCRIPTION.to_string(), extractor: Some(extractors::dahua_zip::dahua_zip_extractor()), }, // DLink MH01 signatures::common::Signature { name: "mh01".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::mh01::mh01_magic(), parser: signatures::mh01::mh01_parser, description: signatures::mh01::DESCRIPTION.to_string(), extractor: Some(extractors::mh01::mh01_extractor()), }, // CSman DAT signatures::common::Signature { name: "csman".to_string(), short: true, magic_offset: 0, always_display: false, magic: signatures::csman::csman_magic(), parser: signatures::csman::csman_parser, description: signatures::csman::DESCRIPTION.to_string(), extractor: Some(extractors::csman::csman_extractor()), }, // DirectX ByteCode signatures::common::Signature { name: "dxbc".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::dxbc::dxbc_magic(), parser: signatures::dxbc::dxbc_parser, description: signatures::dxbc::DESCRIPTION.to_string(), extractor: Some(extractors::dxbc::dxbc_extractor()), }, // D-Link TLV firmware signatures::common::Signature { name: "dlink_tlv".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::dlink_tlv::dlink_tlv_magic(), parser: signatures::dlink_tlv::dlink_tlv_parser, description: signatures::dlink_tlv::DESCRIPTION.to_string(), extractor: Some(extractors::encfw::encfw_extractor()), }, // DLKE encrypted firmware signatures::common::Signature { name: "dlke".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::dlke::dlke_magic(), parser: signatures::dlke::dlke_parser, description: signatures::dlke::DESCRIPTION.to_string(), extractor: Some(extractors::encfw::encfw_extractor()), }, // SHRS encrypted firmware signatures::common::Signature { name: "shrs".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::shrs::shrs_magic(), parser: signatures::shrs::shrs_parser, description: signatures::shrs::DESCRIPTION.to_string(), extractor: Some(extractors::encfw::encfw_extractor()), }, // PKCS DER hashes signatures::common::Signature { name: "pkcs_der_hash".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::pkcs_der::der_hash_magic(), parser: signatures::pkcs_der::der_hash_parser, description: signatures::pkcs_der::DESCRIPTION.to_string(), extractor: None, }, // LogFS signatures::common::Signature { name: "logfs".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::logfs::logfs_magic(), parser: signatures::logfs::logfs_parser, description: signatures::logfs::DESCRIPTION.to_string(), extractor: None, }, // encrpted_img signatures::common::Signature { name: "encrpted_img".to_string(), short: true, magic_offset: 0, always_display: false, magic: signatures::encrpted_img::encrpted_img_magic(), parser: signatures::encrpted_img::encrpted_img_parser, description: signatures::encrpted_img::DESCRIPTION.to_string(), extractor: Some(extractors::encfw::encfw_extractor()), }, // Android boot image signatures::common::Signature { name: "android_bootimg".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::android_bootimg::android_bootimg_magic(), parser: signatures::android_bootimg::android_bootimg_parser, description: signatures::android_bootimg::DESCRIPTION.to_string(), extractor: None, }, // uboot signatures::common::Signature { name: "uboot".to_string(), short: false, magic_offset: 0, always_display: true, magic: signatures::uboot::uboot_magic(), parser: signatures::uboot::uboot_parser, description: signatures::uboot::DESCRIPTION.to_string(), extractor: None, }, // dms firmware signatures::common::Signature { name: "dms".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::dms::dms_magic(), parser: signatures::dms::dms_parser, description: signatures::dms::DESCRIPTION.to_string(), extractor: Some(extractors::swapped::swapped_extractor_u16()), }, // dkbs firmware signatures::common::Signature { name: "dkbs".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::dkbs::dkbs_magic(), parser: signatures::dkbs::dkbs_parser, description: signatures::dkbs::DESCRIPTION.to_string(), extractor: Some(extractors::encfw::encfw_extractor()), }, // known encrypted firmware signatures::common::Signature { name: "encfw".to_string(), short: true, magic_offset: 0, always_display: true, magic: signatures::encfw::encfw_magic(), parser: signatures::encfw::encfw_parser, description: signatures::encfw::DESCRIPTION.to_string(), extractor: Some(extractors::encfw::encfw_extractor()), }, // matter ota firmware signatures::common::Signature { name: "matter_ota".to_string(), short: true, magic_offset: 0, always_display: false, magic: signatures::matter_ota::matter_ota_magic(), parser: signatures::matter_ota::matter_ota_parser, description: signatures::matter_ota::DESCRIPTION.to_string(), extractor: Some(extractors::matter_ota::matter_ota_extractor()), }, // DPAPI blob data signatures::common::Signature { name: "dpapi".to_string(), short: true, magic_offset: 0, always_display: true, magic: signatures::dpapi::dpapi_magic(), parser: signatures::dpapi::dpapi_parser, description: signatures::dpapi::DESCRIPTION.to_string(), extractor: None, }, // QEMU QCOW image signatures::common::Signature { name: "qcow".to_string(), short: true, magic_offset: 0, always_display: true, magic: signatures::qcow::qcow_magic(), parser: signatures::qcow::qcow_parser, description: signatures::qcow::DESCRIPTION.to_string(), extractor: None, }, // ARJ archive signatures::common::Signature { name: "arj".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::arj::arj_magic(), parser: signatures::arj::arj_parser, description: signatures::arj::DESCRIPTION.to_string(), extractor: Some(extractors::sevenzip::sevenzip_extractor()), }, // MD5 hashes signatures::common::Signature { name: "md5".to_string(), short: false, magic_offset: 0, always_display: false, magic: signatures::hashes::md5_magic(), parser: signatures::hashes::md5_parser, description: signatures::hashes::MD5_DESCRIPTION.to_string(), extractor: None, }, ]; binary_signatures } ================================================ FILE: src/main.rs ================================================ use binwalk::AnalysisResults; use log::{debug, error, info}; use std::collections::VecDeque; use std::panic; use std::process; use std::process::ExitCode; use std::sync::mpsc; use std::thread; use std::time; use threadpool::ThreadPool; mod binwalk; mod cliparser; mod common; mod display; mod entropy; mod extractors; mod json; mod magic; mod signatures; mod structures; fn main() -> ExitCode { // File name used when reading from stdin const STDIN: &str = "stdin"; // Only use one thread if unable to auto-detect available core info const DEFAULT_WORKER_COUNT: usize = 1; // Number of seconds to wait before printing debug progress info const PROGRESS_INTERVAL: u64 = 30; // If this env var is set during extraction, the Binwalk.base_target_file symlink will // be deleted at the end of extraction. const BINWALK_RM_SYMLINK: &str = "BINWALK_RM_EXTRACTION_SYMLINK"; // Output directory for extracted files let mut output_directory: Option = None; /* * Maintain a queue of files waiting to be analyzed. * Note that ThreadPool has its own internal queue so this may seem redundant, however, * queuing a large number of jobs via the ThreadPool queue results in *massive* amounts * of unecessary memory consumption, especially when recursively analyzing many files. */ let mut target_files = VecDeque::new(); // Statistics variables; keeps track of analyzed file count and total analysis run time let mut file_count: usize = 0; let run_time = time::Instant::now(); let mut last_progress_interval = time::Instant::now(); // Initialize logging env_logger::init(); // Process command line arguments let mut cliargs = cliparser::parse(); // If --list was specified, just display a list of signatures and return if cliargs.list { display::print_signature_list(cliargs.quiet, &magic::patterns()); return ExitCode::SUCCESS; } // Set a dummy file name when reading from stdin if cliargs.stdin { cliargs.file_name = Some(STDIN.to_string()); } let mut json_logger = json::JsonLogger::new(cliargs.log); // If entropy analysis was requested, generate the entropy graph and return if cliargs.entropy { display::print_plain(cliargs.quiet, "Calculating file entropy..."); if let Ok(entropy_results) = entropy::plot(cliargs.file_name.unwrap(), cliargs.stdin, cliargs.png) { // Log entropy results to JSON file, if requested json_logger.log(json::JSONType::Entropy(entropy_results.clone())); json_logger.close(); display::println_plain(cliargs.quiet, "done."); } else { panic!("Entropy analysis failed!"); } return ExitCode::SUCCESS; } // If extraction or data carving was requested, we need to initialize the output directory if cliargs.extract || cliargs.carve { output_directory = Some(cliargs.directory); } // Initialize binwalk let binwalker = match binwalk::Binwalk::configure( cliargs.file_name, output_directory, cliargs.include, cliargs.exclude, None, cliargs.search_all, ) { Err(e) => { error!("Binwalk initialization failed: {}", e.message); return ExitCode::FAILURE; } Ok(bw) => bw, }; // If the user specified --threads, honor that request; else, auto-detect available parallelism let available_workers = cliargs.threads.unwrap_or_else(|| { // Get CPU core info match thread::available_parallelism() { // In case of error use the default Err(e) => { error!("Failed to retrieve CPU core info: {e}"); DEFAULT_WORKER_COUNT } Ok(coreinfo) => coreinfo.get(), } }); // Sanity check the number of available worker threads if available_workers < 1 { panic!("No available worker threads!"); } // Initialize thread pool debug!("Initializing thread pool with {available_workers} workers"); let workers = ThreadPool::new(available_workers); let (worker_tx, worker_rx) = mpsc::channel(); /* * Set a custom panic handler. * This ensures that when any thread panics, the default panic handler will be invoked * _and_ the entire process will exit with an error code. */ let default_panic_handler = panic::take_hook(); panic::set_hook(Box::new(move |panic_info| { default_panic_handler(panic_info); process::exit(-1); })); debug!( "Queuing initial target file: {}", binwalker.base_target_file ); // Queue the initial file path target_files.insert(target_files.len(), binwalker.base_target_file.clone()); /* * Main loop. * Loop until all pending thread jobs are complete and there are no more files in the queue. */ while !target_files.is_empty() || workers.active_count() > 0 { // If there are files waiting to be analyzed and there is at least one free thread in the pool if !target_files.is_empty() && workers.active_count() < workers.max_count() { // Get the next file path from the target_files queue let target_file = target_files .pop_front() .expect("Failed to retrieve next file from the queue"); // Spawn a new worker for the new file spawn_worker( &workers, binwalker.clone(), target_file, cliargs.stdin && file_count == 0, cliargs.extract, cliargs.carve, worker_tx.clone(), ); } // Don't spin CPU cycles if there is no backlog of files to analyze if target_files.is_empty() { let sleep_time = time::Duration::from_millis(1); thread::sleep(sleep_time); } // Some debug info on analysis progress if last_progress_interval.elapsed().as_secs() >= PROGRESS_INTERVAL { info!( "Status: active worker threads: {}/{}, files waiting in queue: {}", workers.active_count(), workers.max_count(), target_files.len() ); last_progress_interval = time::Instant::now(); } // Get response from a worker thread, if any if let Ok(results) = worker_rx.try_recv() { // Keep a tally of how many files have been analyzed file_count += 1; // Log analysis results to JSON file json_logger.log(json::JSONType::Analysis(results.clone())); // Nothing found? Nothing else to do for this file. if results.file_map.is_empty() { debug!("Found no results for file {}", results.file_path); continue; } // Print analysis results to screen if should_display(&results, file_count, cliargs.verbose) { display::print_analysis_results(cliargs.quiet, cliargs.extract, &results); } // If running recursively, add extraction results to list of files to analyze if cliargs.matryoshka { for (_signature_id, extraction_result) in results.extractions.into_iter() { if !extraction_result.do_not_recurse { for file_path in extractors::common::get_extracted_files( &extraction_result.output_directory, ) { debug!("Queuing {file_path} for analysis"); target_files.insert(target_files.len(), file_path.clone()); } } } } } } json_logger.close(); // If BINWALK_RM_SYMLINK env var was set, delete the base_target_file symlink if (cliargs.carve || cliargs.extract) && std::env::var(BINWALK_RM_SYMLINK).is_ok() { if let Err(e) = std::fs::remove_file(&binwalker.base_target_file) { error!( "Request to remove extraction symlink file {} failed: {}", binwalker.base_target_file, e ); } } // All done, show some basic statistics display::print_stats( cliargs.quiet, run_time, file_count, binwalker.signature_count, binwalker.pattern_count, ); ExitCode::SUCCESS } /// Returns true if the specified results should be displayed to screen fn should_display(results: &AnalysisResults, file_count: usize, verbose: bool) -> bool { let mut display_results: bool = false; /* * For brevity, when analyzing more than one file only display subsequent files whose results * contain signatures that we always want displayed, or which contain extractable signatures. * This can be overridden with the --verbose command line flag. */ if file_count == 1 || verbose || !results.extractions.is_empty() { display_results = true; } else { for signature in &results.file_map { if signature.always_display { display_results = true; break; } } } display_results } /// Spawn a worker thread to analyze a file fn spawn_worker( pool: &ThreadPool, bw: binwalk::Binwalk, target_file: String, stdin: bool, do_extraction: bool, do_carve: bool, worker_tx: mpsc::Sender, ) { pool.execute(move || { // Read in file data let file_data = match common::read_input(&target_file, stdin) { Err(_) => { error!("Failed to read {target_file} data"); b"".to_vec() } Ok(data) => data, }; // Analyze target file, with extraction, if specified let results = bw.analyze_buf(&file_data, &target_file, do_extraction); // If data carving was requested as part of extraction, carve analysis results to disk if do_carve { let carve_count = carve_file_map(&file_data, &results); info!("Carved {carve_count} data blocks to disk from {target_file}"); } // Report file results back to main thread if let Err(e) = worker_tx.send(results) { panic!( "Worker thread for {target_file} failed to send results back to main thread: {e}" ); } }); } /// Carve signatures identified during analysis to separate files on disk. /// Returns the number of carved files created. /// Note that unknown blocks of file data are also carved to disk, so the number of files /// created may be larger than the number of results defined in results.file_map. fn carve_file_map(file_data: &[u8], results: &binwalk::AnalysisResults) -> usize { let mut carve_count: usize = 0; let mut last_known_offset: usize = 0; let mut unknown_bytes: Vec<(usize, usize)> = Vec::new(); // No results, don't do anything if !results.file_map.is_empty() { // Loop through all identified signatures in the file for signature_result in &results.file_map { // If there is data between the last signature and this signature, it is some chunk of unknown data if signature_result.offset > last_known_offset { unknown_bytes.push(( last_known_offset, signature_result.offset - last_known_offset, )); } // Carve this signature's data to disk if carve_file_data_to_disk( &results.file_path, file_data, &signature_result.name, signature_result.offset, signature_result.size, ) { carve_count += 1; } // Update the last known offset to the end of this signature's data last_known_offset = signature_result.offset + signature_result.size; } // Calculate the size of any remaining data from the end of the last signature to EOF let remaining_data = file_data.len() - last_known_offset; // Add any remaining unknown data to the unknown_bytes list if remaining_data > 0 { unknown_bytes.push((last_known_offset, remaining_data)); } // All known signature data has been carved to disk, now carve any unknown blocks of data to disk for (offset, size) in unknown_bytes { if carve_file_data_to_disk(&results.file_path, file_data, "unknown", offset, size) { carve_count += 1; } } } carve_count } /// Carves a block of file data to a new file on disk fn carve_file_data_to_disk( source_file_path: &str, file_data: &[u8], name: &str, offset: usize, size: usize, ) -> bool { let chroot = extractors::common::Chroot::new(None); // Carved file path will be: __.raw let carved_file_path = format!("{source_file_path}_{offset}_{name}.raw",); debug!("Carving {carved_file_path}"); // Carve the data to disk if !chroot.carve_file(&carved_file_path, file_data, offset, size) { error!( "Failed to carve {} [{:#X}..{:#X}] to disk", carved_file_path, offset, offset + size, ); return false; } true } ================================================ FILE: src/signatures/aes.rs ================================================ use crate::signatures::common::{CONFIDENCE_LOW, SignatureError, SignatureResult}; /// Human readable description pub const DESCRIPTION_AES_SBOX: &str = "AES S-Box"; pub const DESCRIPTION_AES_FT: &str = "AES Forward Table"; pub const DESCRIPTION_AES_RT: &str = "AES Reverse Table"; pub const DESCRIPTION_AES_RCON: &str = "AES RCON"; pub const DESCRIPTION_AES_ACC: &str = "AES Acceleration Table"; /// AES S-box magic bytes pub fn aes_sbox_magic() -> Vec> { vec![ b"\x63\x7C\x77\x7B\xF2\x6B\x6F\xC5".to_vec(), // Forward S-Box b"\x52\x09\x6A\xD5\x30\x36\xA5\x38".to_vec(), // Reverse S-Box ] } pub fn aes_forward_table_magic() -> Vec> { vec![b"\xC6\x63\x63\xA5\xF8\x7C\x7C\x84\xEE\x77\x77\x99\xF6\x7B\x7B\x8D".to_vec()] } pub fn aes_reverse_table_magic() -> Vec> { vec![b"\x51\xF4\xA7\x50\x7E\x41\x65\x53\x1A\x17\xA4\xC3\x3A\x27\x5E\x96".to_vec()] } pub fn aes_rcon_magic() -> Vec> { vec![ b"\x01\x02\x04\x08\x10\x20\x40\x80\x1B\x36".to_vec(), b"\x01\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x08\x00\x00\x00\x10\x00\x00\x00\x20\x00\x00\x00\x40\x00\x00\x00\x80\x00\x00\x00\x1B\x00\x00\x00\x36\x00\x00\x00".to_vec(), ] } pub fn aes_acceleration_table_magic() -> Vec> { vec![ b"\xA5\x84\x99\x8D\x0D\xBD\xB1\x54\x50\x03\xA9\x7D\x19\x62\xE6\x9A".to_vec(), // combined sbox x2 b"\xC6\xF8\xEE\xF6\xFF\xD6\xDE\x91\x60\x02\xCE\x56\xE7\xB5\x4D\xEC".to_vec(), // combined sbox x3 b"\x00\x02\x04\x06\x08\x0a\x0c\x0e\x10\x12\x14\x16\x18\x1a\x1c\x1e\x20\x22\x24\x26\x28\x2a\x2c\x2e".to_vec(), // Gallois mult 2 b"\x00\x03\x06\x05\x0c\x0f\x0a\x09\x18\x1b\x1e\x1d\x14\x17\x12\x11\x30\x33\x36\x35\x3c\x3f\x3a\x39".to_vec(), // Gallois mult 3 b"\x00\x09\x12\x1b\x24\x2d\x36\x3f\x48\x41\x5a\x53\x6c\x65\x7e\x77\x90\x99\x82\x8b\xb4\xbd\xa6\xaf".to_vec(), // Gallois mult 9 b"\x00\x0b\x16\x1d\x2c\x27\x3a\x31\x58\x53\x4e\x45\x74\x7f\x62\x69\xb0\xbb\xa6\xad\x9c\x97\x8a\x81".to_vec(), // Gallois mult 11 b"\x00\x0d\x1a\x17\x34\x39\x2e\x23\x68\x65\x72\x7f\x5c\x51\x46\x4b\xd0\xdd\xca\xc7\xe4\xe9\xfe\xf3".to_vec(), // Gallois mult 13 b"\x00\x0e\x1c\x12\x38\x36\x24\x2a\x70\x7e\x6c\x62\x48\x46\x54\x5a\xe0\xee\xfc\xf2\xd8\xd6\xc4\xca".to_vec(), // Gallois mult 14 ] } /// Validates the AES S-Box pub fn aes_sbox_parser( _file_data: &[u8], offset: usize, ) -> Result { // Successful return value let result = SignatureResult { offset, description: DESCRIPTION_AES_SBOX.to_string(), confidence: CONFIDENCE_LOW, ..Default::default() }; // Nothing to do, just return success Ok(result) } /// Validates the AES Forward Table pub fn aes_forward_table_parser( _file_data: &[u8], offset: usize, ) -> Result { // Successful return value let result = SignatureResult { offset, description: DESCRIPTION_AES_FT.to_string(), confidence: CONFIDENCE_LOW, ..Default::default() }; // Nothing to do, just return success Ok(result) } /// Validates the AES Reverse Table pub fn aes_reverse_table_parser( _file_data: &[u8], offset: usize, ) -> Result { // Successful return value let result = SignatureResult { offset, description: DESCRIPTION_AES_RT.to_string(), confidence: CONFIDENCE_LOW, ..Default::default() }; // Nothing to do, just return success Ok(result) } /// Validates the AES Acceleration Table pub fn aes_acceleration_table_parser( _file_data: &[u8], offset: usize, ) -> Result { // Successful return value let result = SignatureResult { offset, description: DESCRIPTION_AES_ACC.to_string(), confidence: CONFIDENCE_LOW, ..Default::default() }; // Nothing to do, just return success Ok(result) } /// Validates the AES RCON pub fn aes_rcon_parser( _file_data: &[u8], offset: usize, ) -> Result { // Successful return value let result = SignatureResult { offset, description: "AES RCON".to_string(), confidence: CONFIDENCE_LOW, ..Default::default() }; // Nothing to do, just return success Ok(result) } ================================================ FILE: src/signatures/android_bootimg.rs ================================================ use crate::signatures::common::{ CONFIDENCE_LOW, CONFIDENCE_MEDIUM, SignatureError, SignatureResult, }; use crate::structures::android_bootimg::parse_android_bootimg_header; /// Human readable description pub const DESCRIPTION: &str = "Android boot image"; /// Android boot images always start with these bytes pub fn android_bootimg_magic() -> Vec> { vec![b"ANDROID!".to_vec()] } /// Validates the android boot image header pub fn android_bootimg_parser( file_data: &[u8], offset: usize, ) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_LOW, ..Default::default() }; if let Ok(bootimg_header) = parse_android_bootimg_header(&file_data[offset..]) { if offset == 0 { result.confidence = CONFIDENCE_MEDIUM; } result.description = format!( "{}, kernel size: {} bytes, kernel load address: {:#X}, ramdisk size: {} bytes, ramdisk load address: {:#X}", result.description, bootimg_header.kernel_size, bootimg_header.kernel_load_address, bootimg_header.ramdisk_size, bootimg_header.ramdisk_load_address, ); return Ok(result); } Err(SignatureError) } ================================================ FILE: src/signatures/androidsparse.rs ================================================ use crate::extractors::androidsparse::extract_android_sparse; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::androidsparse::parse_android_sparse_header; /// Human readable description pub const DESCRIPTION: &str = "Android sparse image"; /// Magic bytes for Android Sparse files pub fn android_sparse_magic() -> Vec> { vec![b"\x3A\xFF\x26\xED".to_vec()] } /// Parses Android Sparse files pub fn android_sparse_parser( file_data: &[u8], offset: usize, ) -> Result { // Default result, returned on success let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Do a dry-run extraction let dry_run = extract_android_sparse(file_data, offset, None); if dry_run.success { if let Some(total_size) = dry_run.size { // Dry-run went OK, parse the header to get some useful info to report if let Ok(header) = parse_android_sparse_header(&file_data[offset..]) { // Update reported size and description result.size = total_size; result.description = format!( "{}, version {}.{}, header size: {}, block size: {}, chunk count: {}, total size: {} bytes", result.description, header.major_version, header.minor_version, header.header_size, header.block_size, header.chunk_count, total_size ); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/apfs.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::apfs::{MAGIC_OFFSET, parse_apfs_header}; /// Human readable description pub const DESCRIPTION: &str = "APple File System"; /// APFS magic bytes pub fn apfs_magic() -> Vec> { vec![b"NXSB".to_vec()] } /// Validates the APFS header pub fn apfs_parser(file_data: &[u8], offset: usize) -> Result { const MBR_BLOCK_SIZE: usize = 512; // Successful return value let mut result = SignatureResult { description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; if offset >= MAGIC_OFFSET { result.offset = offset - MAGIC_OFFSET; let available_data = file_data.len() - result.offset; if let Ok(apfs_header) = parse_apfs_header(&file_data[result.offset..]) { let mut truncated_message = "".to_string(); result.size = apfs_header.block_count * apfs_header.block_size; // It is observed that an APFS contained in an EFIGPT with a protective MBR includes the MBR block in its size. // If the APFS image is pulled out of the EFIGPT, the reported size will be 512 bytes too long, but otherwise valid. if result.size > available_data { let truncated_size = result.size - available_data; // If the calculated size is 512 bytes short, adjust the reported APFS size accordingly if truncated_size == MBR_BLOCK_SIZE { result.size -= truncated_size; truncated_message = format!(" (truncated by {truncated_size} bytes)"); } } result.description = format!( "{}, block size: {} bytes, block count: {}, total size: {} bytes{}", result.description, apfs_header.block_size, apfs_header.block_count, result.size, truncated_message ); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/arcadyan.rs ================================================ use crate::extractors::arcadyan::extract_obfuscated_lzma; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; /// Human readable description pub const DESCRIPTION: &str = "Arcadyan obfuscated LZMA"; /// Obfuscated Arcadyan LZMA magic bytes pub fn obfuscated_lzma_magic() -> Vec> { vec![b"\x00\xD5\x08\x00".to_vec()] } /// Parses obfuscated Arcadyan LZMA data pub fn obfuscated_lzma_parser( file_data: &[u8], offset: usize, ) -> Result { // Magic bytes are 0x68 bytes into the actual file const MAGIC_OFFSET: usize = 0x68; // Success return value let mut result = SignatureResult { description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Sanity check on the reported offset; must be at least MAGIC_OFFSET bytes into the file if offset >= MAGIC_OFFSET { // Actual start of the Arcadyan data in the file let start_offset: usize = offset - MAGIC_OFFSET; // Do an extraction dry-run let dry_run = extract_obfuscated_lzma(file_data, start_offset, None); // If dry-run was successful, return success if dry_run.success { // Report the actual start of file data result.offset = start_offset; return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/arj.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::arj::parse_arj_header; pub const DESCRIPTION: &str = "ARJ archive data"; pub fn arj_magic() -> Vec> { vec![b"\x60\xea".to_vec()] } pub fn arj_parser(file_data: &[u8], offset: usize) -> Result { if let Ok(arj_header) = parse_arj_header(&file_data[offset..]) { let available_data = file_data.len() - offset; // Sanity check the reported ARJ header size if arj_header.header_size <= available_data { // Return success return Ok(SignatureResult { description: format!( "{}, header size: {}, version {}, minimum version to extract: {}, flags: {}, compression method: {}, file type: {}, original name: {}, original file date: {}, compressed file size: {}, uncompressed file size: {}, os: {}", DESCRIPTION, arj_header.header_size, arj_header.version, arj_header.min_version, arj_header.flags, arj_header.compression_method, arj_header.file_type, arj_header.original_name, arj_header.original_file_date, arj_header.compressed_file_size, arj_header.uncompressed_file_size, arj_header.host_os, ), offset, size: arj_header.header_size, confidence: CONFIDENCE_MEDIUM, extraction_declined: arj_header.file_type != *"comment header", ..Default::default() }); } } Err(SignatureError) } ================================================ FILE: src/signatures/autel.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::autel::parse_autel_header; /// Human readable description pub const DESCRIPTION: &str = "Autel obfuscated firmware"; /// Autel magic bytes pub fn autel_magic() -> Vec> { vec![b"ECC0101\x00".to_vec()] } /// Validates the Autel header pub fn autel_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; if let Ok(autel_header) = parse_autel_header(&file_data[offset..]) { result.size = autel_header.header_size + autel_header.data_size; result.description = format!( "{}, header size: {} bytes, data size: {}, total size: {}", result.description, autel_header.header_size, autel_header.data_size, result.size ); return Ok(result); } Err(SignatureError) } ================================================ FILE: src/signatures/binhdr.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::binhdr::parse_bin_header; /// Human readable description pub const DESCRIPTION: &str = "BIN firmware header"; /// BIN header magic bytes pub fn bin_hdr_magic() -> Vec> { vec![b"U2ND".to_vec()] } /// Validates the BIN header pub fn bin_hdr_parser(file_data: &[u8], offset: usize) -> Result { const MAGIC_OFFSET: usize = 14; // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; if offset >= MAGIC_OFFSET { result.offset = offset - MAGIC_OFFSET; if let Ok(bin_header) = parse_bin_header(&file_data[result.offset..]) { result.description = format!( "{}, board ID: {}, hardware revision: {}, firmware version: {}.{}", result.description, bin_header.board_id, bin_header.hardware_revision, bin_header.firmware_version_major, bin_header.firmware_version_minor, ); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/bmp.rs ================================================ use crate::extractors::bmp::extract_bmp_image; use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; /// Human readable description pub const DESCRIPTION: &str = "BMP image (Bitmap)"; // BMPs start with these bytes // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapfileheader // "The file type; must be 0x4d42 (the ASCII string "BM")" pub fn bmp_magic() -> Vec> { vec![b"BM".to_vec()] } // Validates BMP header pub fn bmp_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, name: "bmp".to_string(), description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // Extraction dry-run to validate the image let dry_run = extract_bmp_image(file_data, offset, None); // If it was successful, inform the user if dry_run.success { // Retrieve total file size and report it to the user if let Some(total_size) = dry_run.size { result.description = format!("BMP image, total size: {total_size}"); result.size = total_size; return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/btrfs.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::btrfs::parse_btrfs_header; /// Human readable description pub const DESCRIPTION: &str = "BTRFS file system"; /// BTRFS magic bytes pub fn btrfs_magic() -> Vec> { vec![b"_BHRfS_M".to_vec()] } /// Validates the BTRFS header pub fn btrfs_parser(file_data: &[u8], offset: usize) -> Result { // Offset of the superblock magic bytes in a BTRFS image const MAGIC_OFFSET: usize = 0x10040; // Successful return value let mut result = SignatureResult { description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // Sanity check the reported offset if offset >= MAGIC_OFFSET { // Actual offset is the location of the magic bytes minus the magic byte offset result.offset = offset - MAGIC_OFFSET; // Parse the superblock header; this also validates the superblock CRC if let Ok(btrfs_header) = parse_btrfs_header(&file_data[result.offset..]) { result.size = btrfs_header.total_size; result.description = format!( "{}, node size: {}, sector size: {}, leaf size: {}, stripe size: {}, bytes used: {}, total size: {} bytes", result.description, btrfs_header.node_size, btrfs_header.sector_size, btrfs_header.leaf_size, btrfs_header.stripe_size, btrfs_header.bytes_used, result.size ); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/bzip2.rs ================================================ use crate::extractors::bzip2::bzip2_decompressor; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; /// Human readable description pub const DESCRIPTION: &str = "bzip2 compressed data"; /// Bzip2 magic bytes; includes the magic bytes, version number, block size, and compressed magic signature pub fn bzip2_magic() -> Vec> { vec![ b"BZh91AY&SY".to_vec(), b"BZh81AY&SY".to_vec(), b"BZh71AY&SY".to_vec(), b"BZh61AY&SY".to_vec(), b"BZh51AY&SY".to_vec(), b"BZh41AY&SY".to_vec(), b"BZh31AY&SY".to_vec(), b"BZh21AY&SY".to_vec(), b"BZh11AY&SY".to_vec(), ] } /// Bzip2 header parser pub fn bzip2_parser(file_data: &[u8], offset: usize) -> Result { // Return value let mut result = SignatureResult { description: DESCRIPTION.to_string(), offset, confidence: CONFIDENCE_HIGH, ..Default::default() }; let dry_run = bzip2_decompressor(file_data, offset, None); if dry_run.success { if let Some(bzip2_size) = dry_run.size { result.size = bzip2_size; result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/cab.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::cab::parse_cab_header; /// Human readable description pub const DESCRIPTION: &str = "Microsoft Cabinet archive"; /// CAB magic bytes; includes the magic bytes and the following reserved1 header entry, which must be 0. pub fn cab_magic() -> Vec> { vec![b"MSCF\x00\x00\x00\x00".to_vec()] } /// Parses and cabinet file signature pub fn cab_parser(file_data: &[u8], offset: usize) -> Result { // Parse the CAB header if let Ok(cab_header) = parse_cab_header(&file_data[offset..]) { let available_data = file_data.len() - offset; // Sanity check the reported CAB file size if cab_header.total_size <= available_data { // Return success return Ok(SignatureResult { description: format!( "{}, file count: {}, folder count: {}, header size: {}, total size: {} bytes", DESCRIPTION, cab_header.file_count, cab_header.folder_count, cab_header.header_size, cab_header.total_size ), offset, size: cab_header.total_size, confidence: CONFIDENCE_MEDIUM, ..Default::default() }); } } Err(SignatureError) } ================================================ FILE: src/signatures/cfe.rs ================================================ use crate::signatures::common::{ CONFIDENCE_LOW, CONFIDENCE_MEDIUM, SignatureError, SignatureResult, }; /// Human readable description pub const DESCRIPTION: &str = "CFE bootloader"; /// CFE bootloader always contains this string pub fn cfe_magic() -> Vec> { vec![b"CFE1CFE1".to_vec()] } /// Validate the CFE signature pub fn cfe_parser(_file_data: &[u8], offset: usize) -> Result { // Magic bytes occur this many bytes into the bootloader const CFE_MAGIC_OFFSET: usize = 28; // Success result; confidence is set to low by default as little additional validation is performed let mut result = SignatureResult { description: DESCRIPTION.to_string(), confidence: CONFIDENCE_LOW, ..Default::default() }; // CFE signature must start at least CFE_MAGIC_OFFSET bytes into the file if offset >= CFE_MAGIC_OFFSET { // Adjust the reported starting offset accordingly result.offset = offset - CFE_MAGIC_OFFSET; // If this signature occurs at the very beginning of a file, our confidence is a bit higher if result.offset == 0 { result.confidence = CONFIDENCE_MEDIUM; } return Ok(result); } Err(SignatureError) } ================================================ FILE: src/signatures/chk.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::chk::parse_chk_header; /// Human readable description pub const DESCRIPTION: &str = "CHK firmware header"; /// CHK firmware always start with these bytes pub fn chk_magic() -> Vec> { vec![b"\x2A\x23\x24\x5E".to_vec()] } /// Parse and validate CHK headers pub fn chk_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // Parse the CHK header if let Ok(chk_header) = parse_chk_header(&file_data[offset..]) { // Calculate reported image size and size of available data let available_data: usize = file_data.len() - offset; let image_total_size: usize = chk_header.header_size + chk_header.kernel_size + chk_header.rootfs_size; // Total reported image size should be between the header size and the file size if available_data >= image_total_size && image_total_size > chk_header.header_size { // Report the size of the header and a brief description result.size = chk_header.header_size; result.description = format!( "{}, board ID: {}, header size: {} bytes, data size: {} bytes", result.description, chk_header.board_id, chk_header.header_size, chk_header.kernel_size + chk_header.rootfs_size ); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/common.rs ================================================ use crate::extractors; use serde::{Deserialize, Serialize}; /// Some pre-defined confidence levels for SignatureResult structures pub const CONFIDENCE_LOW: u8 = 0; pub const CONFIDENCE_MEDIUM: u8 = 128; pub const CONFIDENCE_HIGH: u8 = 250; /// Return value of SignatureParser upon error #[derive(Debug, Clone)] pub struct SignatureError; /// Type definition for signature parser functions /// /// ## Arguments /// /// All signature parsers are passed two arguments: a vector of u8 bytes, and an offset into that vector where the signature's magic bytes were found. /// /// ## Return values /// /// Each signature parser is responsible for parsing and validating signature candidates. /// /// They must return either a SignatureResult struct if validation succeeds, or a SignatureError if validation fails. pub type SignatureParser = fn(&[u8], usize) -> Result; /// Describes a valid identified file signature /// /// ## Construction /// /// The SignatureResult struct is returned by all SignatureParser functions upon success. /// /// The `id`, `name`, and `always_display` fields are automatically populated after being returned by a SignatureParser function, and need not be set by the SignatureParser function. /// /// At the very least, SignatureParser functions should define the `offset` and `description` fields. /// /// ## Additional Notes /// /// If a SignatureResult contains a `size` of `0` (the default value), it is assumed to extend to the beginning of the next signature, or EOF, whichever comes first. /// /// SignatureResult structs are sortable by `offset`. /// /// SignatureResult structs can be JSON serialized/deserialized with [serde](https://crates.io/crates/serde). #[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct SignatureResult { /// File/data offset where this signature starts pub offset: usize, /// A UUID uniquely identifying this signature result; auto-populated pub id: String, /// Size of the signature data, 0 if unknown pub size: usize, /// A unique name for this signature type, auto-populated from the signature definition in Signature.name pub name: String, /// One of CONFIDENCE_LOW, CONFIDENCE_MEDIUM, CONFIDENCE_HIGH; default is CONFIDENCE_LOW pub confidence: u8, /// Human readable description of this signature pub description: String, /// If true, always display this signature result; auto-populated from the signature definition in Signature.always_display pub always_display: bool, /// Set to true to disable extraction for this particular signature result (default: false) pub extraction_declined: bool, /// Signatures may specify a preferred extractor, which overrides the default extractor specified in the Signature.extractor definition #[serde(skip_deserializing, skip_serializing)] pub preferred_extractor: Option, } /// Defines a file signature to search for, and how to extract that file type #[derive(Debug, Clone)] pub struct Signature { /// Unique name for the signature (no whitespace) pub name: String, /// Set to true if this is a short signature; it will only be matched at the beginning of a file pub short: bool, /// List of magic byte patterns associated with this signature pub magic: Vec>, /// Offset of magic bytes from the beginning of the file; only relevant for short signatures pub magic_offset: usize, /// Human readable description of this signature pub description: String, /// If true, will always display files that contain this signature, even during recursive extraction pub always_display: bool, /// Specifies the signature parser to invoke for magic match validation pub parser: SignatureParser, /// Specifies the extractor to use when extracting this file type pub extractor: Option, } ================================================ FILE: src/signatures/compressd.rs ================================================ use crate::signatures::common::{ CONFIDENCE_LOW, CONFIDENCE_MEDIUM, SignatureError, SignatureResult, }; /// Human readable description pub const DESCRIPTION: &str = "compress'd data"; /// Compress'd files always start with these bytes pub fn compressd_magic() -> Vec> { vec![b"\x1F\x9D\x90".to_vec()] } /// "Validate" the compress'd header pub fn compressd_parser( _file_data: &[u8], offset: usize, ) -> Result { // Successful return value; confidence is medium since this only matches magic bytes at the beginning of a file let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_LOW, ..Default::default() }; if offset == 0 { result.confidence = CONFIDENCE_MEDIUM; } Ok(result) } ================================================ FILE: src/signatures/copyright.rs ================================================ use crate::common::get_cstring; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; /// Human readable description pub const DESCRIPTION: &str = "Copyright text"; /// Magic copyright strings to search for pub fn copyright_magic() -> Vec> { vec![ b"copyright".to_vec(), b"Copyright".to_vec(), b"COPYRIGHT".to_vec(), ] } /// Parse copyright magic candidates pub fn copyright_parser( file_data: &[u8], offset: usize, ) -> Result { // Size of "copright" string const MAGIC_SIZE: usize = 9; // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Get a NULL terminated string, starting at the "copright" text let copyright_string = get_cstring(&file_data[offset..]); // Make sure we got more than just the "copyright" string if copyright_string.len() > MAGIC_SIZE { result.size = copyright_string.len(); // Truncate copright text to 100 bytes result.description = format!("{}: \"{:.100}\"", result.description, copyright_string); return Ok(result); } Err(SignatureError) } ================================================ FILE: src/signatures/cpio.rs ================================================ use crate::common::is_offset_safe; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::cpio; /// Human readable description pub const DESCRIPTION: &str = "CPIO ASCII archive"; /// Magic bytes for CPIO archives with and without CRC's pub fn cpio_magic() -> Vec> { vec![b"070701".to_vec(), b"070702".to_vec()] } /// Parse and validate CPIO archives pub fn cpio_parser(file_data: &[u8], offset: usize) -> Result { // The last CPIO entry will have this file name const EOF_MARKER: &str = "TRAILER!!!"; let mut header_count: usize = 0; let mut result = SignatureResult { description: DESCRIPTION.to_string(), offset, confidence: CONFIDENCE_HIGH, ..Default::default() }; let mut next_header_offset = offset; let mut previous_header_offset = None; let available_data = file_data.len(); // Loop over all the available data, or until CPIO EOF, or until error while is_offset_safe(available_data, next_header_offset, previous_header_offset) { // Get the CPIO entry's raw data match file_data.get(next_header_offset..) { None => { break; } Some(cpio_entry_data) => { // Parse this CPIO entry's header match cpio::parse_cpio_entry_header(cpio_entry_data) { Err(_) => { break; } Ok(cpio_header) => { // Sanity check the magic bytes if !cpio_magic().contains(&cpio_header.magic) { break; } // Keep a tally of how many CPIO headers have been processed header_count += 1; // Update the total size of the CPIO file to include this header and its data result.size += cpio_header.header_size + cpio_header.data_size; // If EOF marker has been found, we're done if cpio_header.file_name == EOF_MARKER { // If one or fewer CPIO headers were found, consider it a false positive; // a CPIO archive should have at least one file/directory entry, and one EOF entry. if header_count > 1 { // Return the result; reported file count does not include the EOF entry result.description = format!( "{}, file count: {}", result.description, header_count - 1 ); return Ok(result); } break; } // Update the previous and next header offset values for the next loop iteration previous_header_offset = Some(next_header_offset); next_header_offset = offset + result.size; } } } } } // No EOF marker was found, or an error occurred in processing the CPIO headers Err(SignatureError) } ================================================ FILE: src/signatures/cramfs.rs ================================================ use crate::common; use crate::signatures::common::{ CONFIDENCE_HIGH, CONFIDENCE_MEDIUM, SignatureError, SignatureResult, }; use crate::structures::cramfs::parse_cramfs_header; /// Human readable description pub const DESCRIPTION: &str = "CramFS filesystem"; /// This is technically the CramFS "signature", not the magic bytes, but it's endian-agnostic pub fn cramfs_magic() -> Vec> { vec![b"Compressed ROMFS".to_vec()] } /// Parse and validate the CramFS header pub fn cramfs_parser(file_data: &[u8], offset: usize) -> Result { // Some constant relative offsets const SIGNATURE_OFFSET: usize = 16; const CRC_START_OFFSET: usize = 32; const CRC_END_OFFSET: usize = 36; let mut result = SignatureResult { offset: offset - SIGNATURE_OFFSET, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; if let Some(cramfs_header_data) = file_data.get(result.offset..) { // Parse the CramFS header; also validates that the reported size is greater than the header size if let Ok(cramfs_header) = parse_cramfs_header(cramfs_header_data) { // Update the reported size result.size = cramfs_header.size; if let Some(cramfs_image_data) = file_data.get(result.offset..result.offset + result.size) { /* * Create a copy of the cramfs image; we have to NULL out the checksum field to calculate the CRC. * This typically shouldn't be too bad on performance, CramFS images are usually relatively small. */ let mut cramfs_image: Vec = cramfs_image_data.to_vec(); // Null out the checksum field for crc_byte in cramfs_image .iter_mut() .take(CRC_END_OFFSET) .skip(CRC_START_OFFSET) { *crc_byte = 0; } // For displaying an error message in the description let mut error_message: &str = ""; // On CRC error, lower confidence and report the checksum error // (have seen partially corrupted images that still extract Ok) if common::crc32(&cramfs_image) != cramfs_header.checksum { error_message = " (checksum error)"; result.confidence = CONFIDENCE_MEDIUM; } result.description = format!( "{}, {} endian, {} files, total size: {} bytes{}", result.description, cramfs_header.endianness, cramfs_header.file_count, cramfs_header.size, error_message ); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/csman.rs ================================================ use crate::extractors::csman::extract_csman_dat; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; /// Human readable description pub const DESCRIPTION: &str = "CSman DAT file"; /// CSMAN DAT files always start with these bytes pub fn csman_magic() -> Vec> { // Big and little endian magic vec![b"SC".to_vec(), b"CS".to_vec()] } /// Validates the CSMAN DAT file pub fn csman_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; let dry_run = extract_csman_dat(file_data, offset, None); if dry_run.success { if let Some(total_size) = dry_run.size { result.size = total_size; result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/dahua_zip.rs ================================================ use crate::signatures::common::{SignatureError, SignatureResult}; use crate::signatures::zip; /// Human readable description pub const DESCRIPTION: &str = "Dahua ZIP archive"; /// Dahua ZIP file entry magic bytes pub fn dahua_zip_magic() -> Vec> { // The first ZIP file entry in the Dahua ZIP file is has "DH" instead of "PK". // Otherwise, it is a normal ZIP file. vec![b"DH\x03\x04".to_vec()] } /// Validates a Dahua ZIP file entry signature pub fn dahua_zip_parser( file_data: &[u8], offset: usize, ) -> Result { // Parse & validate the Dahua ZIP file like a normal ZIP file if let Ok(mut result) = zip::zip_parser(file_data, offset) { // Replace the normal ZIP description string with our description string result.description = result.description.replace(zip::DESCRIPTION, DESCRIPTION); return Ok(result); } Err(SignatureError) } ================================================ FILE: src/signatures/deb.rs ================================================ use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::deb::parse_deb_header; /// Human readable description pub const DESCRIPTION: &str = "Debian package file"; /// Debian archives always start with these bytes pub fn deb_magic() -> Vec> { vec![b"!\ndebian-binary\x20\x20\x20".to_vec()] } /// Validates debian archive signatures pub fn deb_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Parse the deb header if let Ok(deb_header) = parse_deb_header(&file_data[offset..]) { result.size = deb_header.file_size; // Make sure the reported size of the DEB file is sane if result.size <= (file_data.len() - offset) { result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/dkbs.rs ================================================ use crate::signatures::common::{ CONFIDENCE_HIGH, CONFIDENCE_MEDIUM, SignatureError, SignatureResult, }; use crate::structures::dkbs::parse_dkbs_header; /// Human readable description pub const DESCRIPTION: &str = "DKBS firmware header"; /// DKBS firmware magic pub fn dkbs_magic() -> Vec> { vec![b"_dkbs_".to_vec()] } /// Validates the DKBS header pub fn dkbs_parser(file_data: &[u8], offset: usize) -> Result { const MAGIC_OFFSET: usize = 7; // Successful return value let mut result = SignatureResult { description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // Sanity check the magic bytes offset if offset >= MAGIC_OFFSET { // Magic bytes occur 7 bytes into the actual firmware header result.offset = offset - MAGIC_OFFSET; // Parse the firmware header if let Ok(dkbs_header) = parse_dkbs_header(&file_data[result.offset..]) { // Calculate the total bytes available after the firmware header let available_data: usize = file_data.len() - result.offset; // Sanity check on the total reported DKBS firmware size if available_data >= (dkbs_header.header_size + dkbs_header.data_size) { // If this header starts at the beginning of the file, confidence is high if result.offset == 0 { result.confidence = CONFIDENCE_HIGH; } // Report header size and description result.size = dkbs_header.header_size; result.description = format!( "{}, board ID: {}, firmware version: {}, boot device: {}, endianness: {}, header size: {} bytes, data size: {}", result.description, dkbs_header.board_id, dkbs_header.version, dkbs_header.boot_device, dkbs_header.endianness, dkbs_header.header_size, dkbs_header.data_size ); // Return OK return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/dlink_tlv.rs ================================================ use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::signatures::openssl::openssl_crypt_parser; use crate::structures::dlink_tlv::parse_dlink_tlv_header; /// Human readable description pub const DESCRIPTION: &str = "D-Link TLV firmware"; /// TLV firmware images always start with these bytes pub fn dlink_tlv_magic() -> Vec> { vec![b"\x64\x80\x19\x40".to_vec()] } /// Validates the TLV header pub fn dlink_tlv_parser( file_data: &[u8], offset: usize, ) -> Result { // Checksum calculation includes the 8-byte header that preceeds the actual payload data const CHECKSUM_OFFSET: usize = 8; // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Parse the header if let Ok(tlv_header) = parse_dlink_tlv_header(&file_data[offset..]) { // Calculate the start and end offsets for the payload data over which the checksum is calculated let data_start = offset + tlv_header.header_size - CHECKSUM_OFFSET; let data_end = data_start + tlv_header.data_size + CHECKSUM_OFFSET; // Get the payload data and calculate the MD5 hash if let Some(payload_data) = file_data.get(data_start..data_end) { let payload_md5 = format!("{:x}", md5::compute(payload_data)); // If the MD5 checksum exists, make sure it matches if tlv_header.data_checksum.is_empty() || payload_md5 == tlv_header.data_checksum { result.size = tlv_header.header_size + tlv_header.data_size; result.description = format!( "{}, model name: {}, board ID: {}, header size: {} bytes, data size: {} bytes", result.description, tlv_header.model_name, tlv_header.board_id, tlv_header.header_size, tlv_header.data_size, ); // Check if the firmware data is OpenSSL encrypted if let Some(crypt_data) = file_data.get(offset + tlv_header.header_size..) { if let Ok(openssl_signature) = openssl_crypt_parser(crypt_data, 0) { result.description = format!("{}, {}", result.description, openssl_signature.description); } } return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/dlke.rs ================================================ use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::jboot::parse_jboot_arm_header; /// Human readable description pub const DESCRIPTION: &str = "DLK encrypted firmware"; /// DLKE encrypted firmware images always start with these bytes pub fn dlke_magic() -> Vec> { // These magic bytes are technically the ROM-ID field of a JBOOT header vec![b"DLK6E8202001".to_vec(), b"DLK6E6110002".to_vec()] } /// Validates the DLKE header pub fn dlke_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Parse the first header, which describes the size of the firmware signature if let Ok(dlke_signature_header) = parse_jboot_arm_header(&file_data[offset..]) { // Second header should immediately follow the first if let Some(dlke_crypt_header_data) = file_data .get(offset + dlke_signature_header.header_size + dlke_signature_header.data_size..) { // Parse the second header, which describes the size of the encrypted data if let Ok(dlke_crypt_header) = parse_jboot_arm_header(dlke_crypt_header_data) { result.size = dlke_signature_header.header_size + dlke_signature_header.data_size + dlke_crypt_header.header_size + dlke_crypt_header.data_size; result.description = format!( "{}, signature size: {} bytes, encrypted data size: {} bytes", result.description, dlke_signature_header.data_size, dlke_crypt_header.data_size ); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/dlob.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::dlob::parse_dlob_header; /// Human readable description pub const DESCRIPTION: &str = "DLOB firmware header"; /// DLOB firmware images always start with these bytes pub fn dlob_magic() -> Vec> { vec![b"\x5e\xa3\xa4\x17".to_vec()] } /// Validates the DLOB header pub fn dlob_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; let available_data: usize = file_data.len() - offset; if let Ok(dlob_header) = parse_dlob_header(&file_data[offset..]) { // Sanity check on the total reported DLOB size if available_data >= (dlob_header.header_size + dlob_header.data_size) { // Don't skip the DLOB contents; it's mostly just a metadata header result.size = dlob_header.header_size; result.description = format!( "{}, header size: {} bytes, data size: {}", result.description, dlob_header.header_size, dlob_header.data_size ); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/dmg.rs ================================================ use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::dmg::parse_dmg_footer; use aho_corasick::AhoCorasick; /// Human readable description pub const DESCRIPTION: &str = "Apple Disk iMaGe"; /// 4-byte magic, 4-byte version (v4), 4-byte structure size (0x0200). /// This is actually the magic bytes of the DMG footer, there is no standard header format. pub fn dmg_magic() -> Vec> { vec![b"koly\x00\x00\x00\x04\x00\x00\x02\x00".to_vec()] } /// Validates the DMG footer pub fn dmg_parser(file_data: &[u8], offset: usize) -> Result { // Confidence is set to HIGH + 1 to ensure this overrides other signatures. // DMG's typically start with compressed data, and the file should be treated // as a DMG, not just compressed data. let mut result = SignatureResult { description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH + 1, ..Default::default() }; // Parse the DMG footer if let Ok(dmg_footer) = parse_dmg_footer(&file_data[offset..]) { /* * DMG files have the following layout: * * [ image data ] [ xml data ] [ footer ] * * We can only signature reliably on the footer, which does contain the offsets and sizes of the image data and xml data. * In theory, this would allow us to calculate the starting offset, and size, of the DMG image. * * In practice, signed DMG files have additional data between the XML and the footer. This extra data appears to * be related to signing certificates and is variable in length, making the above theoretical calculations of the DMG offset * and size invalid. * * Instead, we have to search the file data for the XML property, then the correct offset can be calculated. */ // Make sure the length of image data and length of XML data are sane if (dmg_footer.data_length + dmg_footer.xml_length) <= offset { // Locate the XML data if let Some(xml_offset) = find_xml_property_list(file_data) { // Make sure the XML data comes after the image data if xml_offset >= dmg_footer.data_length { // Report the result result.size = offset + dmg_footer.footer_size; result.offset = xml_offset - dmg_footer.data_length; result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } } } } Err(SignatureError) } fn find_xml_property_list(file_data: &[u8]) -> Option { // XML data should start with this string const XML_SIGNATURE: &str = "blkx"; let grep = AhoCorasick::new(vec![XML_SIGNATURE]).unwrap(); for xml_match in grep.find_overlapping_iter(file_data) { let xml_start = xml_match.start(); let xml_end = xml_start + MIN_XML_LENGTH; if let Some(xml_data) = file_data.get(xml_start..xml_end) { if let Ok(xml_string) = String::from_utf8(xml_data.to_vec()) { if xml_string.contains(BLKX_KEY) { return Some(xml_start); } } } } None } ================================================ FILE: src/signatures/dms.rs ================================================ use crate::extractors::swapped::byte_swap; use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::dms::parse_dms_header; /// Human readable description pub const DESCRIPTION: &str = "DMS firmware image"; /// DMS firmware image magic bytes pub fn dms_magic() -> Vec> { vec![b"0><1".to_vec()] } /// Validates the DMS header pub fn dms_parser(file_data: &[u8], offset: usize) -> Result { const MIN_SIZE: usize = 0x100; const BYTE_SWAP_SIZE: usize = 2; const MAGIC_OFFSET: usize = 4; // Successful return value let mut result = SignatureResult { description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // The magic bytes start at offset 4 if offset >= MAGIC_OFFSET { result.offset = offset - MAGIC_OFFSET; if let Some(dms_data) = file_data.get(result.offset..result.offset + MIN_SIZE) { // DMS firmware images have every 2 bytes swapped let swapped_data = byte_swap(dms_data, BYTE_SWAP_SIZE); // Validate the DMS firmware header if let Ok(dms_header) = parse_dms_header(&swapped_data) { result.size = dms_header.image_size; result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/dpapi.rs ================================================ use crate::{ signatures::common::{SignatureError, SignatureResult}, structures::dpapi::parse_dpapi_blob_header, }; use super::common::CONFIDENCE_MEDIUM; /// Human readable description pub const DESCRIPTION: &str = "DPAPI blob data"; /// DPAPI blob data header will always start with these bytes pub fn dpapi_magic() -> Vec> { vec![ b"\x01\x00\x00\x00\xD0\x8c\x9d\xdf\x01\x15\xd1\x11\x8c\x7a\x00\xc0\x4f\xc2\x97\xeb" .to_vec(), ] } /// Returns success with additional details pub fn dpapi_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; if let Ok(header) = parse_dpapi_blob_header(&file_data[offset..]) { result.description = format!( "{}, header_size: {}, blob_size: {}, version: {}, provider_id: {}, master_key_version: {}, master_key_id: {}, flags: {}, description_len: {}, crypto_algorithm: {}, crypti_alg_len: {}, salt_len: {}, hmac_key_len: {}, hash_algorithm: {}, hash_alg_len: {}, hmac2_key_len: {}, data_len: {}, sign_len: {}", result.description, header.header_size, header.blob_size, header.version, header.provider_id, header.master_key_version, header.master_key_id, header.flags, header.description_len, header.crypto_algorithm, header.crypti_alg_len, header.salt_len, header.hmac_key_len, header.hash_algorithm, header.hash_alg_len, header.hmac2_key_len, header.data_len, header.sign_len ); } Err(SignatureError) } ================================================ FILE: src/signatures/dtb.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::dtb::parse_dtb_header; /// Human readable description pub const DESCRIPTION: &str = "Device tree blob (DTB)"; /// DTB files start with these magic bytes pub fn dtb_magic() -> Vec> { vec![b"\xD0\x0D\xFE\xED".to_vec()] } /// Validates the DTB header pub fn dtb_parser(file_data: &[u8], offset: usize) -> Result { // Sucessful result let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // Parse the DTB header if let Ok(dtb_header) = parse_dtb_header(&file_data[offset..]) { // Calculate the offsets of where the dt_struct and dt_strings end let dt_struct_end: usize = offset + dtb_header.struct_offset + dtb_header.struct_size; let dt_strings_end: usize = offset + dtb_header.strings_offset + dtb_header.strings_size; // Sanity check the dt_struct and dt_strings offsets if file_data.len() >= dt_struct_end && file_data.len() >= dt_strings_end { result.size = dtb_header.total_size; result.description = format!( "{}, version: {}, CPU ID: {}, total size: {} bytes", result.description, dtb_header.version, dtb_header.cpu_id, result.size ); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/dxbc.rs ================================================ use crate::signatures::common::{ CONFIDENCE_HIGH, CONFIDENCE_MEDIUM, SignatureError, SignatureResult, }; use crate::structures::dxbc::parse_dxbc_header; /// Human readable description pub const DESCRIPTION: &str = "DirectX shader bytecode"; /// DXBC file magic bytes pub fn dxbc_magic() -> Vec> { vec![b"DXBC".to_vec()] } /// Validates the DXBC header pub fn dxbc_parser(file_data: &[u8], offset: usize) -> Result { const CHUNK_SM4: [u8; 4] = *b"SHDR"; const CHUNK_SM5: [u8; 4] = *b"SHEX"; // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; if let Ok(header) = parse_dxbc_header(&file_data[offset..]) { result.confidence = CONFIDENCE_HIGH; result.size = header.size; let shader_model = if header.chunk_ids.contains(&CHUNK_SM4) { "Shader Model 4" } else if header.chunk_ids.contains(&CHUNK_SM5) { "Shader Model 5" } else { "Unknown Shader Model" }; result.description = format!("{}, {}", result.description, shader_model); return Ok(result); } Err(SignatureError) } ================================================ FILE: src/signatures/ecos.rs ================================================ use crate::signatures::common::{CONFIDENCE_LOW, SignatureError, SignatureResult}; /// Human readable description pub const EXCEPTION_HANDLER_DESCRIPTION: &str = "eCos kernel exception handler"; /// Big and little endian magic byte signatures for eCos kernel exception handlers (MIPS only) pub fn exception_handler_magic() -> Vec> { /* * eCos kernel exception handlers * * mfc0 $k0, Cause # Cause of last exception * nop # Some versions of eCos omit the nop * andi $k0, 0x7F * li $k1, 0xXXXXXXXX * add $k1, $k0 * lw $k1, 0($k1) * jr $k1 * nop */ vec![ b"\x00\x68\x1A\x40\x00\x00\x00\x00\x7F\x00\x5A\x33".to_vec(), b"\x00\x68\x1A\x40\x7F\x00\x5A\x33".to_vec(), b"\x40\x1A\x68\x00\x00\x00\x00\x00\x33\x5A\x00\x7F".to_vec(), b"\x40\x1A\x68\x00\x33\x5A\x00\x7F".to_vec(), ] } /// Parses the eCos exception handler signature pub fn exception_handler_parser( file_data: &[u8], offset: usize, ) -> Result { // Successful return value let mut result = SignatureResult { offset, description: EXCEPTION_HANDLER_DESCRIPTION.to_string(), confidence: CONFIDENCE_LOW, ..Default::default() }; let mut endianness: &str = "big"; // Detect endianess if file_data[offset] == 0 { endianness = "little"; } result.description = format!("{}, MIPS {} endian", result.description, endianness); Ok(result) } ================================================ FILE: src/signatures/efigpt.rs ================================================ use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::efigpt::parse_efigpt_header; /// Human readable description pub const DESCRIPTION: &str = "EFI Global Partition Table"; /// EFI GPT always contains these bytes pub fn efigpt_magic() -> Vec> { vec![b"\x55\xAAEFI PART".to_vec()] } /// Validates the EFI GPT header pub fn efigpt_parser(file_data: &[u8], offset: usize) -> Result { // Offset of magic bytes from the start of the MBR const MAGIC_OFFSET: usize = 0x01FE; // Successful return value let mut result = SignatureResult { description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; let available_data = file_data.len() - offset; if offset >= MAGIC_OFFSET { // MBR actually starts this may bytes before the magic bytes result.offset = offset - MAGIC_OFFSET; // Get the EFI data, including the MBR block if let Some(efi_data) = file_data.get(result.offset..) { // Parse the EFI data; this also validates CRC so if this succeeds, confidence is high if let Ok(efi_header) = parse_efigpt_header(efi_data) { // Some EFI images have been observed to define partitions that extend beyond EOF. // If that is the case, assume the EFI image extends to EOF. if efi_header.total_size > available_data { result.size = available_data; } else { result.size = efi_header.total_size; } result.description = format!("{}, total size: {}", result.description, result.size); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/elf.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::elf::parse_elf_header; /// Human readable description pub const DESCRIPTION: &str = "ELF binary"; /// ELF files start with these bytes pub fn elf_magic() -> Vec> { vec![b"\x7FELF".to_vec()] } /// Parse and validate the ELF header pub fn elf_parser(file_data: &[u8], offset: usize) -> Result { // Successful result let mut result = SignatureResult { offset, name: "elf".to_string(), description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // If the header is parsed successfully, consider it valid if let Ok(elf_header) = parse_elf_header(&file_data[offset..]) { result.description = format!( "{}, {}-bit {}, {} for {}, {} endian", result.description, elf_header.class, elf_header.exe_type, elf_header.machine, elf_header.osabi, elf_header.endianness ); return Ok(result); } Err(SignatureError) } ================================================ FILE: src/signatures/encfw.rs ================================================ use crate::signatures::common::{ CONFIDENCE_LOW, CONFIDENCE_MEDIUM, SignatureError, SignatureResult, }; use std::collections::HashMap; /// Known encrypted firmware magics and their associated make/model fn encfw_known_firmware() -> HashMap, String> { HashMap::from([ ( b"\xdf\x8c\x39\x0d".to_vec(), "D-Link DIR-822 rev C".to_string(), ), (b"\x35\x66\x6f\x68".to_vec(), "D-Link DAP-1665".to_string()), ( b"\xf5\x2a\xa0\xb4".to_vec(), "D-Link DIR-842 rev C".to_string(), ), ( b"\xe3\x13\x00\x5b".to_vec(), "D-Link DIR-850 rev A".to_string(), ), ( b"\x0a\x14\xe4\x24".to_vec(), "D-Link DIR-850 rev B".to_string(), ), ]) } /// Human readable description pub const DESCRIPTION: &str = "Known encrypted firmware"; /// Known encrypted firmware magic bytes pub fn encfw_magic() -> Vec> { encfw_known_firmware().keys().cloned().collect() } /// Parse the magic signature match pub fn encfw_parser(file_data: &[u8], offset: usize) -> Result { const MAGIC_LEN: usize = 4; // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; if let Some(magic_bytes) = file_data.get(offset..offset + MAGIC_LEN) { if encfw_known_firmware().contains_key(magic_bytes) { if result.offset != 0 { result.confidence = CONFIDENCE_LOW; } result.description = format!( "{}, {}", result.description, encfw_known_firmware()[magic_bytes] ); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/encrpted_img.rs ================================================ use crate::signatures::common::{ CONFIDENCE_LOW, CONFIDENCE_MEDIUM, SignatureError, SignatureResult, }; /// Human readable description pub const DESCRIPTION: &str = "D-Link Encrpted Image"; /// encrpted_img firmware images always start with these bytes pub fn encrpted_img_magic() -> Vec> { vec![b"encrpted_img".to_vec()] } /// Validates the encrpted_img header pub fn encrpted_img_parser( _file_data: &[u8], offset: usize, ) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // Nothing to really validate here if offset != 0 { result.confidence = CONFIDENCE_LOW; } Ok(result) } ================================================ FILE: src/signatures/ext.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::ext::parse_ext_header; /// Human readable description pub const DESCRIPTION: &str = "EXT filesystem"; /// EXT magic bytes pub fn ext_magic() -> Vec> { /* * The magic bytes for EXT are only a u16, resulting in many false positives. * These magic signatures include all possible values for the state and errors fields in the superblock, * as well as the minor version number (assumed to be 0). * This means fewer false positive matches, and less time spent validating false positives. */ vec![ b"\x53\xEF\x01\x00\x01\x00\x00\x00".to_vec(), b"\x53\xEF\x01\x00\x02\x00\x00\x00".to_vec(), b"\x53\xEF\x01\x00\x03\x00\x00\x00".to_vec(), b"\x53\xEF\x02\x00\x01\x00\x00\x00".to_vec(), b"\x53\xEF\x02\x00\x02\x00\x00\x00".to_vec(), b"\x53\xEF\x02\x00\x03\x00\x00\x00".to_vec(), ] } /// Parse the EXT signature pub fn ext_parser(file_data: &[u8], offset: usize) -> Result { // Offset inside the EXT image where the magic bytes reside const MAGIC_OFFSET: usize = 1080; let mut result = SignatureResult { description: DESCRIPTION.to_string(), offset: offset - MAGIC_OFFSET, size: 0, confidence: CONFIDENCE_MEDIUM, ..Default::default() }; if let Some(ext_data) = file_data.get(result.offset..) { if let Ok(ext_header) = parse_ext_header(ext_data) { result.size = ext_header.image_size; result.description = format!( "{} for {}, inodes: {}, block size: {}, block count: {}, free blocks: {}, reserved blocks: {}, total size: {} bytes", result.description, ext_header.os, ext_header.inodes_count, ext_header.block_size, ext_header.free_blocks_count, ext_header.reserved_blocks_count, ext_header.blocks_count, result.size ); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/fat.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::fat::parse_fat_header; /// Human readable description pub const DESCRIPTION: &str = "FAT file system"; /// Offset of magic bytes from the start of the FAT pub const MAGIC_OFFSET: usize = 0x01FE; /// FAT always contains these bytes pub fn fat_magic() -> Vec> { vec![b"\x55\xAA".to_vec()] } /// Validates the FAT header pub fn fat_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // Sanity check the magic offset if offset >= MAGIC_OFFSET { // FAT actually starts this may bytes before the magic bytes result.offset = offset - MAGIC_OFFSET; // Parse and validate the FAT header if let Some(fat_data) = file_data.get(result.offset..) { if let Ok(fat_header) = parse_fat_header(fat_data) { // Report the total size of the FAT image result.size = fat_header.total_size; // Include FAT type in the description let mut fat_type_desc: &str = "FAT12/16"; if fat_header.is_fat32 { fat_type_desc = "FAT32"; } result.description = format!( "{}, type: {}, total size: {} bytes", result.description, fat_type_desc, result.size ); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/gif.rs ================================================ use crate::extractors::gif::extract_gif_image; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::gif::parse_gif_header; /// Human readable description pub const DESCRIPTION: &str = "GIF image"; /// GIF images always start with these bytes pub fn gif_magic() -> Vec> { // https://giflib.sourceforge.net/whatsinagif/bits_and_bytes.html vec![b"GIF87a".to_vec(), b"GIF89a".to_vec()] } /// Validates the GIF header pub fn gif_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Do an extraction dry-run to validate the GIF image let dry_run = extract_gif_image(file_data, offset, None); if dry_run.success { if let Some(total_size) = dry_run.size { // Everything looks ok, parse the GIF header to report some info to the user if let Ok(gif_header) = parse_gif_header(&file_data[offset..]) { // No sense in extracting a GIF from a file if the GIF data starts at offset 0 if offset == 0 { result.extraction_declined = true; } result.size = total_size; result.description = format!( "{}, {}x{} pixels, total size: {} bytes", result.description, gif_header.image_width, gif_header.image_height, result.size ); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/gpg.rs ================================================ use crate::extractors::gpg::gpg_decompress; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; /// Human readable description pub const GPG_SIGNED_DESCRIPTION: &str = "GPG signed file"; /// GPG signed files start with these two bytes pub fn gpg_signed_magic() -> Vec> { vec![b"\xA3\x01".to_vec()] } /// Validates GPG signatures pub fn gpg_signed_parser( file_data: &[u8], offset: usize, ) -> Result { // Success result; confidence is high since this signature is only reported what it starts at the beginning of a file let mut result = SignatureResult { offset, confidence: CONFIDENCE_HIGH, description: GPG_SIGNED_DESCRIPTION.to_string(), ..Default::default() }; /* * GPG signed files are just zlib compressed files with the zlib magic bytes replaced with the GPG magic bytes. * Decompress the signed file; no output directory specified, dry run only. */ let decompression_dry_run = gpg_decompress(file_data, offset, None); // If the decompression dry run was a success, this signature is almost certianly valid if decompression_dry_run.success { if let Some(total_size) = decompression_dry_run.size { result.size = total_size; result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/gzip.rs ================================================ use crate::common; use crate::extractors::gzip::gzip_decompress; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::gzip::parse_gzip_header; /// Human readable description pub const DESCRIPTION: &str = "gzip compressed data"; /// Gzip magic bytes, plus compression type field (always 8 for deflate) pub fn gzip_magic() -> Vec> { vec![b"\x1f\x8b\x08".to_vec()] } /// Validates gzip signatures pub fn gzip_parser(file_data: &[u8], offset: usize) -> Result { // Length of the GZIP CRC located at the end of the deflate data stream const GZIP_CRC_SIZE: usize = 4; // Length of the ISIZE field located after the CRC field const GZIP_ISIZE_SIZE: usize = 4; // Do a dry-run decompression let dry_run = gzip_decompress(file_data, offset, None); // If dry-run was successful, this is almost certianly a valid gzip file if dry_run.success { // Get the size of the deflate data stream if let Some(deflate_data_size) = dry_run.size { // The dry run has already validated the header, but we want some header info to display to the user if let Ok(gzip_header) = parse_gzip_header(&file_data[offset..]) { // Original file name is optional let mut original_file_name_text: String = "".to_string(); if !gzip_header.original_name.is_empty() { original_file_name_text = format!(" original file name: \"{}\",", gzip_header.original_name); } // Total size of the gzip file is the size of the header, plus the size of the compressed data, plus the trailing CRC and ISIZE fields let total_size = gzip_header.size + deflate_data_size + GZIP_CRC_SIZE + GZIP_ISIZE_SIZE; return Ok(SignatureResult { offset, size: total_size, confidence: CONFIDENCE_HIGH, description: format!( "{},{} operating system: {}, timestamp: {}, total size: {} bytes", DESCRIPTION, original_file_name_text, gzip_header.os, common::epoch_to_string(gzip_header.timestamp), total_size, ), ..Default::default() }); } } } Err(SignatureError) } ================================================ FILE: src/signatures/hashes.rs ================================================ use crate::signatures::common::{SignatureError, SignatureResult}; /// All hash magics here are the same size const HASH_MAGIC_LEN: usize = 16; /// Human readable descriptions pub const CRC32_DESCRIPTION: &str = "CRC32 polynomial table"; pub const SHA256_DESCRIPTION: &str = "SHA256 hash constants"; pub const MD5_DESCRIPTION: &str = "MD5 hash constants"; /// CRC32 contstants pub fn crc32_magic() -> Vec> { // Order matters! See hash_endianness(). vec![ // Big endian b"\x00\x00\x00\x00\x77\x07\x30\x96\xEE\x0E\x61\x2C\x99\x09\x51\xBA".to_vec(), // Little endian b"\x00\x00\x00\x00\x96\x30\x07\x77\x2C\x61\x0E\xEE\xBA\x51\x09\x99".to_vec(), ] } /// SHA256 constants pub fn sha256_magic() -> Vec> { // Order matters! See hash_endianness(). vec![ // Big endian b"\x42\x8a\x2f\x98\x71\x37\x44\x91\xb5\xc0\xfb\xcf\xe9\xb5\xdb\xa5".to_vec(), // Little endian b"\x98\x2f\x8a\x42\x91\x44\x37\x71\xcf\xfb\xc0\xb5\xa5\xdb\xb5\xe9".to_vec(), ] } /// MD5 constants pub fn md5_magic() -> Vec> { vec![ // Big endian b"\xd7\x6a\xa4\x78\xe8\xc7\xb7\x56\x24\x20\x70\xdb\xc1\xbd\xce\xee".to_vec(), // Little endian b"\x78\xa4\x6a\xd7\x56\xb7\xc7\xe8\xdb\x70\x20\x24\xee\xce\xbd\xc1".to_vec(), ] } pub fn crc32_parser(file_data: &[u8], offset: usize) -> Result { // Just return a success with some extra description info let result = SignatureResult { description: format!( "{}, {} endian", CRC32_DESCRIPTION, hash_endianess(file_data, offset, crc32_magic()) ), offset, size: HASH_MAGIC_LEN, ..Default::default() }; Ok(result) } pub fn sha256_parser(file_data: &[u8], offset: usize) -> Result { // Just return a success with some extra description info let result = SignatureResult { description: format!( "{}, {} endian", SHA256_DESCRIPTION, hash_endianess(file_data, offset, sha256_magic()) ), offset, size: HASH_MAGIC_LEN, ..Default::default() }; Ok(result) } pub fn md5_parser(file_data: &[u8], offset: usize) -> Result { // Just return a success with some extra description info let result = SignatureResult { description: format!( "{}, {} endian", MD5_DESCRIPTION, hash_endianess(file_data, offset, md5_magic()) ), offset, size: HASH_MAGIC_LEN, ..Default::default() }; Ok(result) } /// Detects hash contstant endianess fn hash_endianess(file_data: &[u8], offset: usize, magics: Vec>) -> String { let mut endianness: String = "little".to_string(); let this_magic = &file_data[offset..offset + HASH_MAGIC_LEN]; if *this_magic == magics[0] { endianness = "big".to_string(); } endianness } ================================================ FILE: src/signatures/iso9660.rs ================================================ use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::iso9660::parse_iso_header; /// Human readable description pub const DESCRIPTION: &str = "ISO9660 primary volume"; /// ISOs start with these magic bytes pub fn iso_magic() -> Vec> { vec![b"\x01CD001\x01\x00".to_vec()] } /// Validate ISO signatures pub fn iso_parser(file_data: &[u8], offset: usize) -> Result { // Offset from the beginning of the ISO image to the magic bytes const ISO_MAGIC_OFFSET: usize = 32768; let mut result = SignatureResult { description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // We need at least ISO_MAGIC_OFFSET bytes to exist before the magic match offset if offset >= ISO_MAGIC_OFFSET { // Calculate the actual starting offset of the ISO result.offset = offset - ISO_MAGIC_OFFSET; // Parse the header, if parsing succeeds assume it's valid if let Ok(iso_header) = parse_iso_header(&file_data[result.offset..]) { result.size = iso_header.image_size; result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/jboot.rs ================================================ use crate::extractors::jboot::extract_jboot_sch2_kernel; use crate::signatures::common::{ CONFIDENCE_HIGH, CONFIDENCE_LOW, CONFIDENCE_MEDIUM, SignatureError, SignatureResult, }; use crate::structures::jboot::{ parse_jboot_arm_header, parse_jboot_sch2_header, parse_jboot_stag_header, }; /// Human readable descriptions pub const JBOOT_ARM_DESCRIPTION: &str = "JBOOT firmware header"; pub const JBOOT_STAG_DESCRIPTION: &str = "JBOOT STAG header"; pub const JBOOT_SCH2_DESCRIPTION: &str = "JBOOT SCH2 header"; /// JBOOT firmware header magic bytes pub fn jboot_arm_magic() -> Vec> { vec![b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x42\x48".to_vec()] } /// JBOOT STAG header magic bytes pub fn jboot_stag_magic() -> Vec> { vec![b"\x04\x04\x24\x2B".to_vec(), b"\xFF\x04\x24\x2B".to_vec()] } /// JBOOT SCH2 header magic bytes pub fn jboot_sch2_magic() -> Vec> { vec![ b"\x24\x21\x00\x02".to_vec(), b"\x24\x21\x01\x02".to_vec(), b"\x24\x21\x02\x02".to_vec(), b"\x24\x21\x03\x02".to_vec(), ] } /// Parse and validate the JBOOT ARM header pub fn jboot_arm_parser( file_data: &[u8], offset: usize, ) -> Result { // Magic bytes start at this offset into the header const MAGIC_OFFSET: usize = 48; // Successful return value let mut result = SignatureResult { description: JBOOT_ARM_DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // Actual header starts MAGIC_OFFSET bytes before the magic bytes let header_start = offset - MAGIC_OFFSET; if let Some(jboot_data) = file_data.get(header_start..) { if let Ok(arm_header) = parse_jboot_arm_header(jboot_data) { result.size = arm_header.header_size; result.offset = header_start; result.description = format!( "{}, header size: {} bytes, ROM ID: {}, erase offset: {:#X}, erase size: {:#X}, data flash offset: {:#X}, data size: {:#X}", result.description, arm_header.header_size, arm_header.rom_id, arm_header.erase_offset, arm_header.erase_size, arm_header.data_offset, arm_header.data_size, ); return Ok(result); } } Err(SignatureError) } /// Parse and validate a JBOOT STAG header pub fn jboot_stag_parser( file_data: &[u8], offset: usize, ) -> Result { // Successful return value let mut result = SignatureResult { offset, description: JBOOT_STAG_DESCRIPTION.to_string(), confidence: CONFIDENCE_LOW, ..Default::default() }; if let Ok(stag_header) = parse_jboot_stag_header(&file_data[offset..]) { // Sanity check on the stag header reported size; it is expected that this // type of header describes a kernel, and should not take up the entire firmware image if (offset + stag_header.header_size + stag_header.image_size) < file_data.len() { // Only report the header size, confidence in this signature is low, don't // want to skip a bunch of data on a false positive result.size = stag_header.header_size; let mut image_type: &str = "factory image"; if stag_header.is_sysupgrade_image { image_type = "system upgrade image"; } result.description = format!( "{}, {}, header size: {} bytes, kernel data size: {} bytes", result.description, image_type, stag_header.header_size, stag_header.image_size, ); return Ok(result); } } Err(SignatureError) } /// Parse and validate a JBOOT SCH2 header pub fn jboot_sch2_parser( file_data: &[u8], offset: usize, ) -> Result { // Successful return value let mut result = SignatureResult { offset, description: JBOOT_SCH2_DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; let dry_run = extract_jboot_sch2_kernel(file_data, offset, None); if dry_run.success { if let Some(total_size) = dry_run.size { if let Ok(sch2_header) = parse_jboot_sch2_header(&file_data[offset..]) { result.size = total_size; result.description = format!( "{}, header size: {} bytes, kernel size: {} bytes, kernel compression: {}, kernel entry point: {:#X}", result.description, sch2_header.header_size, sch2_header.kernel_size, sch2_header.compression, sch2_header.kernel_entry_point, ); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/jffs2.rs ================================================ use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::jffs2::{JFFS2_NODE_STRUCT_SIZE, parse_jffs2_node_header}; use aho_corasick::AhoCorasick; /// Human readable description pub const DESCRIPTION: &str = "JFFS2 filesystem"; /// JFFS2 magic bytes pub fn jffs2_magic() -> Vec> { /* * Big and little endian patterns to search for. * These assume that the first JFFS2 node will be a directory, inode, or clean marker type. * Longer signatures are less prone to false positive matches. */ vec![ b"\x19\x85\xe0\x01".to_vec(), b"\x19\x85\xe0\x02".to_vec(), b"\x19\x85\x20\x03".to_vec(), b"\x85\x19\x01\xe0".to_vec(), b"\x85\x19\x02\xe0".to_vec(), b"\x85\x19\x03\x20".to_vec(), ] } /// Parse and validate a JFFS2 image pub fn jffs2_parser(file_data: &[u8], offset: usize) -> Result { // Useful contstants const MAX_PAGE_SIZE: usize = 0x20000; const MIN_VALID_NODE_COUNT: usize = 2; const JFFS2_BIG_ENDIAN_MAGIC: &[u8; 2] = b"\x19\x85"; const JFFS2_LITTLE_ENDIAN_MAGIC: &[u8; 2] = b"\x85\x19"; let mut result = SignatureResult { size: 0, offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Parse this first JFFS2 node header to ensure correctness if let Ok(first_node_header) = parse_jffs2_node_header(&file_data[offset..]) { // The known end of JFFS2 data in the raw file data. This will be updated as we find more nodes. let mut jffs2_eof: usize = offset + roundup(first_node_header.size); // Make sure that jffs2_eof is sane if jffs2_eof < file_data.len() { // Start searching for subsequent JFFS2 nodes at the end of this node's data let grep_offset: usize = jffs2_eof; // Keep a count of how many valid nodes were found let mut node_count: usize = 1; // Determine which node magic bytes to search for based on the first node's endianness let mut node_magic = JFFS2_LITTLE_ENDIAN_MAGIC; if first_node_header.endianness == "big" { node_magic = JFFS2_BIG_ENDIAN_MAGIC; } // Need to grep for all JFFS2 nodes to figure out how big this file system really is let grep = AhoCorasick::new(vec![node_magic]).unwrap(); // Find all matching JFFS2 node magic bytes for magic_match in grep.find_overlapping_iter(&file_data[grep_offset..].to_vec()) { // Calculate the start and end of the node header inside the file data let header_start: usize = grep_offset + magic_match.start(); let header_end: usize = header_start + JFFS2_NODE_STRUCT_SIZE; // This is a false positive that is inside the node data of a previously validated node if header_start < jffs2_eof { continue; } // If we haven't found a valid header within MAX_PAGE_SIZE bytes, quit if (header_start - jffs2_eof) > MAX_PAGE_SIZE { break; } // Get the node header's raw bytes match file_data.get(header_start..header_end) { None => { break; } Some(node_header_data) => { // Parse this node's header if let Ok(this_node_header) = parse_jffs2_node_header(node_header_data) { node_count += 1; jffs2_eof = header_start + roundup(this_node_header.size); } } } } // Make sure we've processed at least a few JFFS2 nodes if node_count > MIN_VALID_NODE_COUNT { result.size = jffs2_eof - result.offset; result.description = format!( "{}, {} endian, nodes: {}, total size: {} bytes", result.description, first_node_header.endianness, node_count, result.size ); return Ok(result); } } } Err(SignatureError) } /// JFFS2 nodes are padded to a 4 byte boundary fn roundup(num: usize) -> usize { let base: f64 = 4.0; let number: f64 = num as f64; let div: f64 = number / base; (base * div.ceil()) as usize } ================================================ FILE: src/signatures/jpeg.rs ================================================ use crate::extractors::jpeg::extract_jpeg_image; use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; /// Human readable description pub const DESCRIPTION: &str = "JPEG image"; /// JPEG magic bytes pub fn jpeg_magic() -> Vec> { vec![ b"\xFF\xD8\xFF\xE0\x00\x10JFIF\x00".to_vec(), b"\xFF\xD8\xFF\xE1".to_vec(), b"\xFF\xD8\xFF\xDB".to_vec(), ] } /// Parse a JPEG image pub fn jpeg_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // Perform an extraction dry-run let dry_run = extract_jpeg_image(file_data, offset, None); // If the dry-run was a success, this is probably a valid JPEG file if dry_run.success { // Get the total size of the JPEG if let Some(jpeg_size) = dry_run.size { // Report signature result data result.size = jpeg_size; result.description = format!("{}, total size: {} bytes", result.description, result.size); // If this entire file is a JPEG, no need to extract it if offset == 0 && result.size == file_data.len() { result.extraction_declined = true; } return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/linux.rs ================================================ use crate::common::get_cstring; use crate::signatures::common::{ CONFIDENCE_LOW, CONFIDENCE_MEDIUM, SignatureError, SignatureResult, }; use crate::structures::linux::{ parse_linux_arm_zimage_header, parse_linux_arm64_boot_image_header, }; use aho_corasick::AhoCorasick; /// Human readable descriptions pub const LINUX_ARM_ZIMAGE_DESCRIPTION: &str = "Linux ARM boot executable zImage"; pub const LINUX_BOOT_IMAGE_DESCRIPTION: &str = "Linux kernel boot image"; pub const LINUX_KERNEL_VERSION_DESCRIPTION: &str = "Linux kernel version"; pub const LINUX_ARM64_BOOT_IMAGE_DESCRIPTION: &str = "Linux kernel ARM64 boot image"; /// Magic bytes for a linux boot image pub fn linux_boot_image_magic() -> Vec> { vec![b"\xb8\xc0\x07\x8e\xd8\xb8\x00\x90\x8e\xc0\xb9\x00\x01\x29\xf6\x29".to_vec()] } /// Kernel version string magic pub fn linux_kernel_version_magic() -> Vec> { vec![b"Linux\x20version\x20".to_vec()] } /// Magic bytes for a linux ARM64 boot image pub fn linux_arm64_boot_image_magic() -> Vec> { vec![b"\x00\x00\x00\x00\x00\x00\x00\x00ARMd".to_vec()] } /// Magic bytes for Linux ARM zImage pub fn linux_arm_zimage_magic() -> Vec> { vec![b"\x18\x28\x6F\x01".to_vec(), b"\x01\x6F\x28\x18".to_vec()] } /// Validate a Linux ARM zImage pub fn linux_arm_zimage_parser( file_data: &[u8], offset: usize, ) -> Result { const MAGIC_OFFSET: usize = 36; let mut result = SignatureResult { confidence: CONFIDENCE_MEDIUM, description: LINUX_ARM_ZIMAGE_DESCRIPTION.to_string(), ..Default::default() }; if offset >= MAGIC_OFFSET { result.offset = offset - MAGIC_OFFSET; if let Some(zimage_data) = file_data.get(result.offset..) { if let Ok(zimage_header) = parse_linux_arm_zimage_header(zimage_data) { result.description = format!( "{}, {} endian", result.description, zimage_header.endianness ); return Ok(result); } } } Err(SignatureError) } /// Validate a linux ARM64 boot image signature pub fn linux_arm64_boot_image_parser( file_data: &[u8], offset: usize, ) -> Result { // Magic bytes are 56 bytes into the image const MAGIC_OFFSET: usize = 0x30; let mut result = SignatureResult { confidence: CONFIDENCE_MEDIUM, description: LINUX_ARM64_BOOT_IMAGE_DESCRIPTION.to_string(), ..Default::default() }; if offset >= MAGIC_OFFSET { // Set the real starting offset result.offset = offset - MAGIC_OFFSET; // Parse and validate the header data if let Ok(image_header) = parse_linux_arm64_boot_image_header(&file_data[result.offset..]) { result.size = image_header.header_size; result.description = format!( "{}, {} endian, effective image size: {} bytes", result.description, image_header.endianness, image_header.image_size ); return Ok(result); } } Err(SignatureError) } /// Validate a linux boot image signature pub fn linux_boot_image_parser( file_data: &[u8], offset: usize, ) -> Result { // There should be the string "!HdrS" 514 bytes from the start of the magic signature const HDRS_OFFSET: usize = 514; const HDRS_EXPECTED_VALUE: &str = "!HdrS"; let result = SignatureResult { description: LINUX_BOOT_IMAGE_DESCRIPTION.to_string(), offset, size: 0, ..Default::default() }; // Calculate start and end offset of the expected !HdrS string let hdrs_start: usize = offset + HDRS_OFFSET; let hdrs_end: usize = hdrs_start + HDRS_EXPECTED_VALUE.len(); if let Some(hdrs_bytes) = file_data.get(hdrs_start..hdrs_end) { // Get the string that should equal HDRS_EXPECTED_VALUE if let Ok(actual_hdrs_value) = String::from_utf8(hdrs_bytes.to_vec()) { // Validate that the hdrs string matches if actual_hdrs_value == HDRS_EXPECTED_VALUE { return Ok(result); } } } Err(SignatureError) } /// Validate a linux kernel version signature and detect if a symbol table is present pub fn linux_kernel_version_parser( file_data: &[u8], offset: usize, ) -> Result { // Kernel version string format is expected to be something like: // "Linux version 4.9.241 (root@server2) (gcc version 10.0.1 (OpenWrt GCC 10.0.1 r12423-0493d57e04) ) #755 SMP Wed Nov 4 03:59:02 +03 2020\n" const PERIOD: u8 = 0x2E; const NEW_LINE: &str = "\n"; const AMPERSAND: &str = "@"; const PERIOD_OFFSET_1: usize = 15; const PERIOD_OFFSET_2: usize = 17; const PERIOD_OFFSET_3: usize = 18; const MIN_FILE_SIZE: usize = 100 * 1024; const MIN_VERSION_STRING_LENGTH: usize = 75; const GCC_VERSION_STRING: &str = "gcc "; let mut result = SignatureResult { offset, confidence: CONFIDENCE_LOW, ..Default::default() }; let file_size = file_data.len(); // Sanity check the size of the file; this automatically eliminates small text files that might match the magic bytes if file_size > MIN_FILE_SIZE { // Get the kernel version string let kernel_version_string = get_cstring(&file_data[offset..]); // Sanity check the length of the version string if kernel_version_string.len() > MIN_VERSION_STRING_LENGTH { // Make sure the string includes the GCC version string too if kernel_version_string.contains(GCC_VERSION_STRING) { // Make sure the string includes an ampersand if kernel_version_string.contains(AMPERSAND) { // The kernel version string should end with a new line if kernel_version_string.ends_with(NEW_LINE) { let kv_bytes = kernel_version_string.as_bytes(); // Make sure the linux kernel version has periods at the expected locations if kv_bytes[PERIOD_OFFSET_1] == PERIOD && (kv_bytes[PERIOD_OFFSET_2] == PERIOD || kv_bytes[PERIOD_OFFSET_3] == PERIOD) { // Try to locate a Linux kernel symbol table let symtab_present = has_linux_symbol_table(file_data); // If a symbol table is present, assume the entire file is a raw Linux kernel. // This is necessary for vmlinux-to-elf extraction. // Otherwise just report the kernel version string and decline extraction. if symtab_present { result.offset = 0; result.size = file_data.len(); } else { result.size = kernel_version_string.len(); result.extraction_declined = true; } // Report the result result.description = format!( "{}, has symbol table: {}", kernel_version_string.trim(), symtab_present ); return Ok(result); } } } } } } Err(SignatureError) } /// Searches the file data for a linux symbol table fn has_linux_symbol_table(file_data: &[u8]) -> bool { let mut match_count: usize = 0; // Same magic bytes that vmlinux-to-elf searches for let symtab_magic = vec![b"\x000\x001\x002\x003\x004\x005\x006\x007\x008\x009\x00"]; let grep = AhoCorasick::new(symtab_magic).unwrap(); // Grep for matches on the Linux symbol table magic bytes for _ in grep.find_overlapping_iter(file_data) { match_count += 1; } // There should be only one match match_count == 1 } ================================================ FILE: src/signatures/logfs.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::logfs::{LOGFS_MAGIC_OFFSET, parse_logfs_super_block}; /// Human readable description pub const DESCRIPTION: &str = "LogFS file system"; /// LogFS magic bytes pub fn logfs_magic() -> Vec> { vec![b"\x7A\x3A\x8E\x5C\xB9\xD5\xBF\x67".to_vec()] } /// Validates the LogFS super block pub fn logfs_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; if offset >= LOGFS_MAGIC_OFFSET { result.offset = offset - LOGFS_MAGIC_OFFSET; if let Some(logfs_sb_data) = file_data.get(result.offset..) { if let Ok(logfs_super_block) = parse_logfs_super_block(logfs_sb_data) { result.size = logfs_super_block.total_size; result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/luks.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::luks::parse_luks_header; /// Human readable description pub const DESCRIPTION: &str = "LUKS header"; /// LUKS Headers start with these bytes pub fn luks_magic() -> Vec> { vec![b"LUKS\xBA\xBE".to_vec()] } /// Parse and validate the LUKS header pub fn luks_parser(file_data: &[u8], offset: usize) -> Result { // Successful result let mut result = SignatureResult { offset, name: "luks".to_string(), description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // If the header is parsed successfully, consider it valid if let Ok(luks_header) = parse_luks_header(&file_data[offset..]) { // Version 1 and version 2 have different header fields if luks_header.version == 1 { result.description = format!( "{}, version: {}, cipher algorithm: {}, cipher mode: {}, hash fn: {}", result.description, luks_header.version, luks_header.cipher_algorithm, luks_header.cipher_mode, luks_header.hashfn ); } else { result.description = format!( "{}, version: {}, header size: {} bytes, hash fn: {}", result.description, luks_header.version, luks_header.header_size, luks_header.hashfn ); } return Ok(result); } Err(SignatureError) } ================================================ FILE: src/signatures/lz4.rs ================================================ use crate::common::is_offset_safe; use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::lz4::{parse_lz4_block_header, parse_lz4_file_header}; /// Human readable description pub const DESCRIPTION: &str = "LZ4 compressed data"; /// LZ4 files start with these magic bytes pub fn lz4_magic() -> Vec> { vec![b"\x04\x22\x4D\x18".to_vec()] } /// Validate a LZ4 signature pub fn lz4_parser(file_data: &[u8], offset: usize) -> Result { // Checksums are 4 bytes in length const CONTENT_CHECKSUM_LEN: usize = 4; let mut result = SignatureResult { offset, confidence: CONFIDENCE_MEDIUM, description: DESCRIPTION.to_string(), ..Default::default() }; // Sanity check the size of available data if let Ok(lz4_file_header) = parse_lz4_file_header(&file_data[offset..]) { // LZ4 data starts immediately after the LZ4 header if let Some(lz4_data) = file_data.get(offset + lz4_file_header.header_size..) { // Determine the size of the actual LZ4 data by processing the data blocks that immediately follow the file header if let Ok(lz4_data_size) = get_lz4_data_size(lz4_data, lz4_file_header.block_checksum_present) { // Set the size of the header and the LZ4 data result.size = lz4_file_header.header_size + lz4_data_size; // If this flag is set, an additional 4-byte checksum will be present at the end of the LZ4 data if lz4_file_header.content_checksum_present { result.size += CONTENT_CHECKSUM_LEN; } // Update description result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } } } Err(SignatureError) } /// Processes the LZ4 data blocks and returns the size of the raw LZ4 data fn get_lz4_data_size(lz4_data: &[u8], checksum_present: bool) -> Result { let mut lz4_data_size: usize = 0; let mut last_lz4_data_size = None; let available_data = lz4_data.len(); // Loop while there is still data and while the offsets are sane while is_offset_safe(available_data, lz4_data_size, last_lz4_data_size) { // Get the next block's data match lz4_data.get(lz4_data_size..) { None => { break; } Some(lz4_block_data) => { // Parse the next block's data match parse_lz4_block_header(lz4_block_data, checksum_present) { Err(_) => { break; } Ok(block_header) => { // Update offsets last_lz4_data_size = Some(lz4_data_size); lz4_data_size += block_header.header_size + block_header.data_size + block_header.checksum_size; // Only return success if a last block header is found if block_header.last_block { return Ok(lz4_data_size); } } } } } } Err(SignatureError) } ================================================ FILE: src/signatures/lzfse.rs ================================================ use crate::common::is_offset_safe; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::lzfse::parse_lzfse_block_header; /// Human readable description pub const DESCRIPTION: &str = "LZFSE compressed data"; /// LZFSE block magics pub fn lzfse_magic() -> Vec> { vec![ b"bvx-".to_vec(), b"bvx1".to_vec(), b"bvx2".to_vec(), b"bvxn".to_vec(), ] } /// Validate LZFSE signatures pub fn lzfse_parser(file_data: &[u8], offset: usize) -> Result { let mut result = SignatureResult { offset, confidence: CONFIDENCE_HIGH, description: DESCRIPTION.to_string(), ..Default::default() }; let available_data = file_data.len(); let mut next_block_offset = offset; let mut previous_block_offset = None; // Walk through all the LZFSE blocks until an end of stream block is found while is_offset_safe(available_data, next_block_offset, previous_block_offset) { // Update previous block offset value in preparation for the next loop previous_block_offset = Some(next_block_offset); // Parse the next block if let Ok(lzfse_block) = parse_lzfse_block_header(&file_data[next_block_offset..]) { next_block_offset += lzfse_block.header_size + lzfse_block.data_size; // Only return success if an end-of-stream block is found if lzfse_block.eof { result.size = next_block_offset - offset; result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/lzma.rs ================================================ use crate::extractors::lzma; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::lzma::parse_lzma_header; /// Human readable description pub const DESCRIPTION: &str = "LZMA compressed data"; /// Builds a list of common LZMA magic bytes (properties + dictionary sizes) pub fn lzma_magic() -> Vec> { let mut magic_signatures: Vec> = vec![]; // Common LZMA properties let supported_properties: Vec = vec![0x5D, 0x6E, 0x6D, 0x6C]; let supported_dictionary_sizes: Vec = vec![ 0x10_00_00_00, 0x20_00_00_00, 0x01_00_00_00, 0x02_00_00_00, 0x04_00_00_00, 0x00_80_00_00, 0x00_40_00_00, 0x00_20_00_00, 0x00_10_00_00, 0x00_08_00_00, 0x00_02_00_00, 0x00_01_00_00, ]; /* * Build a list of magic signatures to search for based on the supported property and dictionary values. * This means having a lot of LZMA signatures, but they are less prone to false positives than searching * for a more generic, but shorter, signature, such as b"\x5d\x00\x00". This results in less validation * of false positives, improving analysis times. */ for property in supported_properties { for dictionary_size in &supported_dictionary_sizes { let mut magic: Vec = vec![property]; magic.extend(dictionary_size.to_le_bytes().to_vec()); magic_signatures.push(magic.to_vec()); } } magic_signatures } /// Validate LZMA signatures pub fn lzma_parser(file_data: &[u8], offset: usize) -> Result { // Success return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Parse the LZMA header if let Ok(lzma_header) = parse_lzma_header(&file_data[offset..]) { /* * LZMA signatures are very prone to false positives, so do a dry-run extraction. * If it succeeds, we have high confidence that this signature is valid. * Else, assume this is a false positive. */ let dry_run = lzma::lzma_decompress(file_data, offset, None); // Return success if dry run succeeded if dry_run.success { if let Some(lzma_stream_size) = dry_run.size { result.size = lzma_stream_size; result.description = format!( "{}, properties: {:#04X}, dictionary size: {} bytes, compressed size: {} bytes, uncompressed size: {} bytes", result.description, lzma_header.properties, lzma_header.dictionary_size, result.size, lzma_header.decompressed_size as i64 ); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/lzop.rs ================================================ use crate::common::is_offset_safe; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::lzop::{ parse_lzop_block_header, parse_lzop_eof_marker, parse_lzop_file_header, }; /// Human readable description pub const DESCRIPTION: &str = "LZO compressed data"; /// LZOP magic bytes pub fn lzop_magic() -> Vec> { vec![b"\x89LZO\x00\x0D\x0A\x1A\x0A".to_vec()] } /// Validate an LZOP signature pub fn lzop_parser(file_data: &[u8], offset: usize) -> Result { // Success retrun value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Parse the LZOP file header if let Ok(lzop_header) = parse_lzop_file_header(&file_data[offset..]) { if let Some(lzop_data) = file_data.get(offset + lzop_header.header_size..) { // Get the size of the compressed LZO data if let Ok(data_size) = get_lzo_data_size(lzop_data, lzop_header.block_checksum_present) { // Update the total size to include the LZO data result.size = lzop_header.header_size + data_size; result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } } } Err(SignatureError) } // Parse the LZO blocks to determine the size of the compressed data, including the terminating EOF marker fn get_lzo_data_size( lzo_data: &[u8], compressed_checksum_present: bool, ) -> Result { // Technially LZOP could have one block, but this would seem uncommon const MIN_BLOCK_COUNT: usize = 2; let available_data = lzo_data.len(); let mut last_offset = None; let mut data_size: usize = 0; let mut block_count: usize = 0; // Loop until we run out of data or an invalid block header is encountered while is_offset_safe(available_data, data_size, last_offset) { // Parse the next block header match parse_lzop_block_header(&lzo_data[data_size..], compressed_checksum_present) { Err(_) => { break; } Ok(block_header) => { // Update block count, offset, and size block_count += 1; last_offset = Some(data_size); data_size += block_header.header_size + block_header.compressed_size + block_header.checksum_size; } } } // As a sanity check, make sure we processed some number of data blocks if block_count >= MIN_BLOCK_COUNT { // Process the EOF marker that should come at the end of the data blocks if let Some(eof_marker_data) = lzo_data.get(data_size..) { if let Ok(eof_marker_size) = parse_lzop_eof_marker(eof_marker_data) { data_size += eof_marker_size; return Ok(data_size); } } } Err(SignatureError) } ================================================ FILE: src/signatures/matter_ota.rs ================================================ use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::matter_ota::parse_matter_ota_header; /// Human readable description pub const DESCRIPTION: &str = "Matter OTA firmware"; /// Matter OTA firmware images always start with these bytes pub fn matter_ota_magic() -> Vec> { vec![b"\x1e\xf1\xee\x1b".to_vec()] } /// Validates the Matter OTA header pub fn matter_ota_parser( file_data: &[u8], offset: usize, ) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), ..Default::default() }; if let Ok(ota_header) = parse_matter_ota_header(&file_data[offset..]) { result.confidence = CONFIDENCE_HIGH; result.size = ota_header.header_size; result.description = format!( "{}, total size: {} bytes, tlv header size: {} bytes, vendor id: 0x{:x}, product id: 0x{:x}, version: {}, payload size: {} bytes, digest type: {}, payload digest: {}", result.description, ota_header.total_size, ota_header.header_size, ota_header.vendor_id, ota_header.product_id, ota_header.version, ota_header.payload_size, ota_header.image_digest_type, ota_header.image_digest, ); return Ok(result); } Err(SignatureError) } ================================================ FILE: src/signatures/mbr.rs ================================================ use crate::extractors::mbr::extract_mbr_partitions; use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::mbr::parse_mbr_image; /// Human readable description pub const DESCRIPTION: &str = "DOS Master Boot Record"; /// Offset of magic bytes from the start of the MBR pub const MAGIC_OFFSET: usize = 0x01FE; /// MBR always contains these bytes pub fn mbr_magic() -> Vec> { vec![b"\x55\xAA".to_vec()] } /// Validates the MBR header pub fn mbr_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // This signature is only matched at the beginning of files (see magic.rs), so this check is not strictly necessary if offset == MAGIC_OFFSET { // MBR actually starts this may bytes before the magic bytes result.offset = offset - MAGIC_OFFSET; // Do an extraction dry run let dry_run = extract_mbr_partitions(file_data, result.offset, None); // If dry run was a success, this is likely a valid MBR if dry_run.success { if let Some(mbr_total_size) = dry_run.size { // Update reported MBR size result.size = mbr_total_size; // Parse the MBR header if let Ok(mbr_header) = parse_mbr_image(&file_data[result.offset..]) { // Examine all reported partitions for partition in &mbr_header.partitions { // Carving out partitions starting at offset 0 would result in infinite recurstion during recursive extraction! if partition.start == result.offset { result.extraction_declined = true; } // Add partition info to the description result.description = format!("{}, partition: {}", result.description, partition.name); } // Add total size to the description result.description = format!("{}, image size: {} bytes", result.description, result.size); // Everything looks ok return Ok(result); } } } } Err(SignatureError) } ================================================ FILE: src/signatures/mh01.rs ================================================ use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::signatures::openssl::openssl_crypt_parser; use crate::structures::mh01::parse_mh01_header; /// Human readable description pub const DESCRIPTION: &str = "D-Link MH01 firmware image"; /// MH01 firmware images always start with these bytes pub fn mh01_magic() -> Vec> { vec![b"MH01".to_vec()] } /// Validates the MH01 header pub fn mh01_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Parse the firmware header if let Ok(mh01_header) = parse_mh01_header(&file_data[offset..]) { // The encrypted data is expected to be in OpenSSL file format, so parse that too if let Some(crypt_data) = file_data.get(offset + mh01_header.encrypted_data_offset..) { if let Ok(openssl_signature) = openssl_crypt_parser(crypt_data, 0) { result.size = mh01_header.total_size; result.description = format!( "{}, signed, encrypted with {}, IV: {}, total size: {} bytes", result.description, openssl_signature.description, mh01_header.iv, mh01_header.total_size, ); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/ntfs.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::ntfs::parse_ntfs_header; /// Human readable description pub const DESCRIPTION: &str = "NTFS partition"; /// NTFS partitions start with these bytes pub fn ntfs_magic() -> Vec> { vec![b"\xEb\x52\x90NTFS\x20\x20\x20\x20".to_vec()] } /// Validates the NTFS header pub fn ntfs_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; if let Ok(ntfs_header) = parse_ntfs_header(&file_data[offset..]) { // The reported sector count does not include the NTFS boot sector itself result.size = ntfs_header.sector_size * (ntfs_header.sector_count + 1); // Simple sanity check on the reported total size if result.size > ntfs_header.sector_size { result.description = format!( "{}, number of sectors: {}, bytes per sector: {}, total size: {} bytes", result.description, ntfs_header.sector_count, ntfs_header.sector_size, result.size ); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/openssl.rs ================================================ use crate::common::is_printable_ascii; use crate::signatures::common::{ CONFIDENCE_LOW, CONFIDENCE_MEDIUM, SignatureError, SignatureResult, }; use crate::structures::openssl::parse_openssl_crypt_header; /// Human readable description pub const DESCRIPTION: &str = "OpenSSL encryption"; /// OpenSSL crypto magic pub fn openssl_crypt_magic() -> Vec> { vec![b"Salted__".to_vec()] } /// Validate an openssl signature pub fn openssl_crypt_parser( file_data: &[u8], offset: usize, ) -> Result { // Success return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_LOW, ..Default::default() }; // Parse the header if let Ok(openssl_header) = parse_openssl_crypt_header(&file_data[offset..]) { // Sanity check the salt value if !is_salt_invalid(openssl_header.salt) { // If the magic starts at the beginning of a file, our confidence is a bit higher if offset == 0 { result.confidence = CONFIDENCE_MEDIUM; } result.description = format!("{}, salt: {:#X}", result.description, openssl_header.salt); return Ok(result); } } Err(SignatureError) } // Returns true if the salt is entirely comprised of NULL and/or ASCII bytes fn is_salt_invalid(salt: usize) -> bool { const SALT_LEN: usize = 8; let mut bad_byte_count: usize = 0; for i in 0..SALT_LEN { let salt_byte = ((salt >> (8 * i)) & 0xFF) as u8; if salt_byte == 0 || is_printable_ascii(salt_byte) { bad_byte_count += 1; } } bad_byte_count == SALT_LEN } ================================================ FILE: src/signatures/packimg.rs ================================================ use crate::signatures::common::{SignatureError, SignatureResult}; use crate::structures::packimg::parse_packimg_header; /// Human readable description pub const DESCRIPTION: &str = "PackImg firmware header"; /// PackIMG magic bytes pub fn packimg_magic() -> Vec> { vec![b"--PaCkImGs--".to_vec()] } /// Parse a PackIMG signature pub fn packimg_parser(file_data: &[u8], offset: usize) -> Result { let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), ..Default::default() }; let available_data: usize = file_data.len() - offset; // Parse the header if let Ok(packimg_header) = parse_packimg_header(&file_data[offset..]) { // Sanity check the reported data size if available_data >= (packimg_header.header_size + packimg_header.data_size) { result.size = packimg_header.header_size; result.description = format!( "{}, header size: {} bytes, data size: {} bytes", result.description, packimg_header.header_size, packimg_header.data_size ); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/pcap.rs ================================================ use crate::extractors::pcap::pcapng_carver; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; /// Human readable description pub const PCAPNG_DESCRIPTION: &str = "Pcap-NG capture file"; /// Pcap-NG files always start with these bytes pub fn pcapng_magic() -> Vec> { vec![b"\x0A\x0D\x0D\x0A".to_vec()] } /// Parses and validates the Pcap-NG file pub fn pcapng_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: PCAPNG_DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Do an extraction dry-run let dry_run = pcapng_carver(file_data, offset, None); // If dry-run was successful, this is almost certianly a valid pcap-ng file if dry_run.success { if let Some(pcap_size) = dry_run.size { // If this file is just a pcap file, no need to carve it out to yet another file on disk if offset == 0 && pcap_size == file_data.len() { result.extraction_declined = true; } // Return parser results result.size = pcap_size; result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/pchrom.rs ================================================ use crate::signatures::common::{SignatureError, SignatureResult}; use crate::structures::pchrom::parse_pchrom_header; /// Human readable description pub const DESCRIPTION: &str = "Intel serial flash for PCH ROM"; /// PCH ROM magic bytes pub fn pch_rom_magic() -> Vec> { vec![b"\x5a\xa5\xf0\x0f".to_vec()] } /// Validate a PCH ROM signature pub fn pch_rom_parser(file_data: &[u8], offset: usize) -> Result { // Magic bytes begin at this offset from the start of file const MAGIC_OFFSET: usize = 16; let mut result = SignatureResult { size: 0, offset: 0, description: DESCRIPTION.to_string(), ..Default::default() }; // Sanity check the offset where the magic bytes were found if offset >= MAGIC_OFFSET { // Set the reported starting offset of this signature result.offset = offset - MAGIC_OFFSET; // Parse the header; if this succeeds, assume it is valid if let Ok(pchrom_header) = parse_pchrom_header(&file_data[result.offset..]) { result.size = pchrom_header.header_size + pchrom_header.data_size; return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/pdf.rs ================================================ use crate::signatures::common::{SignatureError, SignatureResult}; /// Human readable description pub const DESCRIPTION: &str = "PDF document"; /// PDF magic bytes pub fn pdf_magic() -> Vec> { // This assumes a major version of 1 vec![b"%PDF-1.".to_vec()] } /// Validate a PDF signature pub fn pdf_parser(file_data: &[u8], offset: usize) -> Result { // More than enough data for our needs const MIN_PDF_SIZE: usize = 16; const NEWLINE_OFFSET: usize = 8; const MINOR_NUMBER_OFFSET: usize = 7; const ASCII_ZERO: u8 = 0x30; const ASCII_NINE: u8 = 0x39; const ASCII_NEWLINE: u8 = 0x0A; const ASCII_PERCENT: u8 = 0x25; const ASCII_CARRIGE_RETURN: u8 = 0x0D; let mut result = SignatureResult { description: DESCRIPTION.to_string(), offset, size: 0, ..Default::default() }; let newline_characters: Vec = vec![ASCII_NEWLINE, ASCII_CARRIGE_RETURN]; let pdf_header_start = offset; let pdf_header_end = pdf_header_start + MIN_PDF_SIZE; // PDF header is expected to start with something like: %PDF-1.7\n% if let Some(pdf_header) = file_data.get(pdf_header_start..pdf_header_end) { // Get the minor version number at the expected offset let version_minor: u8 = pdf_header[MINOR_NUMBER_OFFSET]; // Sanity check the minor version number if (ASCII_ZERO..=ASCII_NINE).contains(&version_minor) { // Update the result description to include the version number result.description = format!( "{}, version 1.{}", result.description, version_minor - ASCII_ZERO ); // Search the remaining bytes for new line characters followed by a percent character for byte in pdf_header[NEWLINE_OFFSET..].iter().copied() { // Any new line or carrige return byte is OK, just keep going if newline_characters.contains(&byte) { continue; // There should be a percent character } else if byte == ASCII_PERCENT { return Ok(result); // Anything else is invalid } else { break; } } } } Err(SignatureError) } ================================================ FILE: src/signatures/pe.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::pe::parse_pe_header; /// Human readable description pub const DESCRIPTION: &str = "Windows PE binary"; /// Common PE file magics pub fn pe_magic() -> Vec> { /* * This matches the first 16 bytes of a DOS header, from e_magic through e_ss. * Note that these values may differ in some special cases, but these are common ones. */ vec![ b"\x4d\x5a\x90\x00\x03\x00\x00\x00\x04\x00\x00\x00\xff\xff\x00\x00".to_vec(), b"\x4d\x5a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".to_vec(), ] } /// Validate a PE header pub fn pe_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // Parse the PE header if let Ok(pe_header) = parse_pe_header(&file_data[offset..]) { result.description = format!( "{}, machine type: {}", result.description, pe_header.machine ); return Ok(result); } Err(SignatureError) } ================================================ FILE: src/signatures/pem.rs ================================================ use crate::extractors::pem; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use base64::Engine; use base64::prelude::BASE64_STANDARD; /// Human readable descriptions pub const PEM_PUBLIC_KEY_DESCRIPTION: &str = "PEM public key"; pub const PEM_PRIVATE_KEY_DESCRIPTION: &str = "PEM private key"; pub const PEM_CERTIFICATE_DESCRIPTION: &str = "PEM certificate"; /// Public key magic pub fn pem_public_key_magic() -> Vec> { vec![ b"-----BEGIN PUBLIC KEY-----".to_vec(), b"-----BEGIN RSA PUBLIC KEY-----".to_vec(), b"-----BEGIN DSA PUBLIC KEY-----".to_vec(), b"-----BEGIN ECDSA PUBLIC KEY-----".to_vec(), ] } /// Private key magics pub fn pem_private_key_magic() -> Vec> { vec![ b"-----BEGIN PRIVATE KEY-----".to_vec(), b"-----BEGIN EC PRIVATE KEY-----".to_vec(), b"-----BEGIN RSA PRIVATE KEY-----".to_vec(), b"-----BEGIN DSA PRIVATE KEY-----".to_vec(), b"-----BEGIN OPENSSH PRIVATE KEY-----".to_vec(), b"-----BEGIN ANY PRIVATE KEY-----".to_vec(), b"-----BEGIN ENCRYPTED PRIVATE KEY-----".to_vec(), b"-----BEGIN TSS2 PRIVATE KEY-----".to_vec(), ] } /// Certificate magic pub fn pem_certificate_magic() -> Vec> { vec![b"-----BEGIN CERTIFICATE-----".to_vec()] } /// Validates both PEM certificate and key signatures pub fn pem_parser(file_data: &[u8], offset: usize) -> Result { // Enough bytes to uniquely differentiate certs from keys const MIN_PEM_LEN: usize = 26; let mut result = SignatureResult { offset, confidence: CONFIDENCE_HIGH, ..Default::default() }; /* * Build a list of magic signatures for public, prvate, and certificate PEMs. * These magics are truncated to MIN_PEM_LEN bytes, which is enough to check if * the matching signature was a public key, private key, or certificate, which is * all we need to know for displaying a useful description string. */ let mut public_magics: Vec> = vec![]; let mut private_magics: Vec> = vec![]; let mut certificate_magics: Vec> = vec![]; for public_magic in pem_public_key_magic() { public_magics.push(public_magic[0..MIN_PEM_LEN].to_vec()); } for private_magic in pem_private_key_magic() { private_magics.push(private_magic[0..MIN_PEM_LEN].to_vec()); } for cert_magic in pem_certificate_magic() { certificate_magics.push(cert_magic[0..MIN_PEM_LEN].to_vec()); } // Sanity check available data if let Some(pem_magic) = file_data.get(offset..offset + MIN_PEM_LEN) { // Check if this magic is for a PEM cert or a PEM key if public_magics.contains(&pem_magic.to_vec()) { result.description = PEM_PUBLIC_KEY_DESCRIPTION.to_string(); } else if private_magics.contains(&pem_magic.to_vec()) { result.description = PEM_PRIVATE_KEY_DESCRIPTION.to_string(); } else if certificate_magics.contains(&pem_magic.to_vec()) { result.description = PEM_CERTIFICATE_DESCRIPTION.to_string(); } else { // This function will only be called if one of the magics was found, so this should never happen return Err(SignatureError); } // Do an extraction dry-run to validate that this PEM file looks sane let dry_run = pem::pem_carver(file_data, offset, None, None); if dry_run.success { if let Some(pem_size) = dry_run.size { // Make sure the PEM data can be base64 decoded if decode_pem_data(&file_data[offset..offset + pem_size]).is_ok() { // If the file starts and end with this PEM data, no sense in carving it out to another file on disk if offset == 0 && pem_size == file_data.len() { result.extraction_declined = true; } result.size = pem_size; return Ok(result); } } } } Err(SignatureError) } /// Base64 decode PEM file contents fn decode_pem_data(pem_file_data: &[u8]) -> Result { const DELIM: &str = "--"; // Make sure the PEM data can be converted to a string if let Ok(pem_file_string) = String::from_utf8(pem_file_data.to_vec()) { let mut delim_count: usize = 0; let mut base64_string: String = "".to_string(); // Loop through PEM file lines for line in pem_file_string.lines() { // PEM begin and end delimiter strings both start with hyphens if line.starts_with(DELIM) { delim_count += 1; // Expect two delimiters: the start, and the end. If we've found both, break. if delim_count == 2 { break; } else { continue; } } // This is not a delimiter string, append the line to the base64 string to be decoded base64_string.push_str(line); } // If we found some text between the delimiters, attempt to base64 decode it if !base64_string.is_empty() { // PEM contents are base64 encoded, they should decode OK; if not, it's a false positive if let Ok(decoded_data) = BASE64_STANDARD.decode(&base64_string) { return Ok(decoded_data.len()); } } } Err(SignatureError) } ================================================ FILE: src/signatures/pjl.rs ================================================ use crate::common::get_cstring; use crate::signatures::common::{CONFIDENCE_LOW, SignatureError, SignatureResult}; /// Human readable description pub const DESCRIPTION: &str = "HP Printer Job Language data"; /// PJL files typically start with these bytes pub fn pjl_magic() -> Vec> { vec![b"\x1B%-12345X@PJL".to_vec()] } /// Parses display info for the PJL pub fn pjl_parser(file_data: &[u8], offset: usize) -> Result { // Offset to the first "@PJL" string const PJL_COMMANDS_OFFSET: usize = 9; // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_LOW, ..Default::default() }; // Get the PJL string data if let Some(pjl_command_data) = file_data.get(offset + PJL_COMMANDS_OFFSET..) { // Pull out a NULL terminated string let mut pjl_text = get_cstring(pjl_command_data); result.size = pjl_text.len(); if result.size > 0 { // For display, replace new line and carriage return characters with spaces pjl_text = pjl_text.replace("\r", " ").replace("\n", ""); result.description = format!("{}: \"{}\"", result.description, pjl_text); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/pkcs_der.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use std::collections::HashMap; /// Human readable description pub const DESCRIPTION: &str = "PKCS DER hash"; /// Returns a HashMap of the hash types and their associated signature bytes fn der_hash_lookups() -> HashMap> { HashMap::from([ ( "MD5".to_string(), b"\x30\x20\x30\x0c\x06\x08\x2a\x86\x48\x86\xf7\x0d\x02\x05\x05\x00\x04\x10".to_vec(), ), ( "SHA1".to_string(), b"\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14".to_vec(), ), ( "SHA256".to_string(), b"\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20" .to_vec(), ), ( "SHA384".to_string(), b"\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x02\x05\x00\x04\x30" .to_vec(), ), ( "SHA512".to_string(), b"\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04".to_vec(), ), ]) } /// DER hash signatures pub fn der_hash_magic() -> Vec> { der_hash_lookups().values().cloned().collect() } /// Validates the DER hash matches pub fn der_hash_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; for (name, magic) in der_hash_lookups() { let hash_start = offset; let hash_end = hash_start + magic.len(); if let Some(hash_bytes) = file_data.get(hash_start..hash_end) { if hash_bytes == magic { result.size = magic.len(); result.description = format!("{}, {}", result.description, name); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/png.rs ================================================ use crate::extractors::png::extract_png_image; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; /// Human readable description pub const DESCRIPTION: &str = "PNG image"; /// PNG magic bytes pub fn png_magic() -> Vec> { /* * PNG magic, followed by chunk size and IHDR chunk type. * IHDR must be the first chunk type, and it is a fixed size of 0x0000000D bytes. */ vec![b"\x89PNG\x0D\x0A\x1A\x0A\x00\x00\x00\x0DIHDR".to_vec()] } /// Validate a PNG signature pub fn png_parser(file_data: &[u8], offset: usize) -> Result { // Success return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Perform an extraction dry-run let dry_run = extract_png_image(file_data, offset, None); // If the dry-run was a success, this is almost certianly a valid PNG if dry_run.success { // Get the total size of the PNG if let Some(png_size) = dry_run.size { // If the start of a file PNG, there's no need to extract it if offset == 0 { result.extraction_declined = true; } // Report signature result result.size = png_size; result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/qcow.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::qcow::parse_qcow_header; pub const DESCRIPTION: &str = "QEMU QCOW Image"; pub fn qcow_magic() -> Vec> { vec![b"QFI\xFB".to_vec()] } pub fn qcow_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, name: "qcow".to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; if let Ok(qcow_header) = parse_qcow_header(file_data) { result.description = format!( "QEMU QCOW Image, version: {}, storage media size: {:#x} bytes, cluster block size: {:#x} bytes, encryption method: {}", qcow_header.version, qcow_header.storage_media_size, 1 << qcow_header.cluster_block_bits, qcow_header.encryption_method ); return Ok(result); }; Err(SignatureError) } ================================================ FILE: src/signatures/qnx.rs ================================================ use crate::signatures::common::{SignatureError, SignatureResult}; use crate::structures::qnx::parse_ifs_header; /// Human readable description pub const IFS_DESCRIPTION: &str = "QNX IFS image"; /// QNX IFS magic bytes pub fn qnx_ifs_magic() -> Vec> { /* * Assumes little endian. * Includes the magic bytes (u32) and version number (u16), which must be 1. */ vec![b"\xEB\x7E\xFF\x00\x01\x00".to_vec()] } /// Validate a QNX IFS signature pub fn qnx_ifs_parser(file_data: &[u8], offset: usize) -> Result { // Success return value let mut result = SignatureResult { offset, description: IFS_DESCRIPTION.to_string(), ..Default::default() }; let available_data: usize = file_data.len() - offset; if let Ok(ifs_header) = parse_ifs_header(&file_data[offset..]) { // Set the total size of this signature result.size = ifs_header.total_size; // Sanity check that the total size doesn't exceed the available data size if result.size <= available_data { result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/rar.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::rar::parse_rar_archive_header; use aho_corasick::AhoCorasick; use std::collections::HashMap; /// Human readable description pub const DESCRIPTION: &str = "RAR archive"; /// RAR magic bytes for both v4 and v5 pub fn rar_magic() -> Vec> { vec![b"Rar!\x1A\x07".to_vec()] } /// Validate RAR signature pub fn rar_parser(file_data: &[u8], offset: usize) -> Result { // Success return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), ..Default::default() }; let mut extra_description: String = "".to_string(); // Parse the archive header if let Ok(rar_header) = parse_rar_archive_header(&file_data[offset..]) { // Try to locate the RAR end-of-file marker if let Ok(rar_size) = get_rar_size(&file_data[offset..], rar_header.version) { result.size = rar_size; result.confidence = CONFIDENCE_MEDIUM; } else { extra_description = " (failed to locate RAR EOF)".to_string(); } result.description = format!( "{}, version: {}, total size: {} bytes{}", result.description, rar_header.version, result.size, extra_description ); return Ok(result); } Err(SignatureError) } /// Determine the size of the RAR file fn get_rar_size(file_data: &[u8], rar_version: usize) -> Result { // EOF markers for Rar v4 and v5 let eof_markers: HashMap>> = HashMap::from([ (4, vec![b"\xC4\x3D\x7B\x00\x40\x07\x00".to_vec()]), (5, vec![b"\x1d\x77\x56\x51\x03\x05\x04\x00".to_vec()]), ]); if eof_markers.contains_key(&rar_version) { // Select the appropriate EOF marker for this version let eof_marker = &eof_markers[&rar_version]; // Need to grep the file for the EOF marker let grep = AhoCorasick::new(eof_marker.clone()).unwrap(); // Search the file data for the EOF marker if let Some(eof_match) = grep.find_overlapping_iter(file_data).next() { // Accept the first match; total size is the start of the EOF marker plus the size of the EOF marker return Ok(eof_match.start() + eof_marker[0].len()); } } Err(SignatureError) } ================================================ FILE: src/signatures/riff.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::riff::parse_riff_header; /// Human readable description pub const DESCRIPTION: &str = "RIFF image"; /// RIFF file magic bytes pub fn riff_magic() -> Vec> { vec![b"RIFF".to_vec()] } /// Validate RIFF signatures pub fn riff_parser(file_data: &[u8], offset: usize) -> Result { // Success return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // Parse the RIFF header if let Ok(riff_header) = parse_riff_header(&file_data[offset..]) { // No sense in extracting an image if the entire file is just the image itself if offset == 0 && riff_header.size == file_data.len() { result.extraction_declined = true; } result.size = riff_header.size; result.description = format!( "{}, encoding type: {}, total size: {} bytes", result.description, riff_header.chunk_type, result.size ); return Ok(result); } Err(SignatureError) } ================================================ FILE: src/signatures/romfs.rs ================================================ use crate::extractors::romfs::extract_romfs; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::romfs::parse_romfs_header; /// Human readable description pub const DESCRIPTION: &str = "RomFS filesystem"; /// ROMFS magic bytes pub fn romfs_magic() -> Vec> { vec![b"-rom1fs-".to_vec()] } /// Validate a ROMFS signature pub fn romfs_parser(file_data: &[u8], offset: usize) -> Result { // Success return value let mut result = SignatureResult { description: DESCRIPTION.to_string(), offset, confidence: CONFIDENCE_HIGH, ..Default::default() }; // Do an extraction dry run let dry_run = extract_romfs(file_data, offset, None); // If the dry run was a success, everything should be good to go if dry_run.success { if let Some(romfs_size) = dry_run.size { // Parse the RomFS header to get the volume name if let Ok(romfs_header) = parse_romfs_header(&file_data[offset..]) { // Report the result result.size = romfs_size; result.description = format!( "{}, volume name: \"{}\", total size: {} bytes", result.description, romfs_header.volume_name, result.size ); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/rsa.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::common; use std::collections::HashMap; /// Human readable description pub const DESCRIPTION: &str = "RSA encrypted session key"; /// Stores defintions about each RSA key type #[derive(Debug, Default, Clone)] struct RSAKeyDefinition { // Magic bytes for this RSA key pub magic: Vec, // Size of the RSA key, in bits pub key_size: usize, // Offset of the byte indicating if the key can be used for signing/encryption pub usage_offset: usize, // Offset of the 64-bit key ID pub keyid_offset: usize, // HashMap of offsets and expected bytes pub valid_checks: HashMap>>, // Offset of the expected terminator byte, 0xD2 pub terminator_offset: usize, } /// Returns a list of RSA key definitions fn rsa_key_definitions() -> Vec { vec![ // 1024b RSA key RSAKeyDefinition { magic: b"\x84\x8C\x03".to_vec(), key_size: 1024, keyid_offset: 3, terminator_offset: 142, usage_offset: 11, valid_checks: HashMap::from([( 12, vec![ b"\x04\x00".to_vec(), b"\x03\xff".to_vec(), b"\x03\xfe".to_vec(), b"\x03\xfd".to_vec(), b"\x03\xfc".to_vec(), b"\x03\xfb".to_vec(), b"\x03\xfa".to_vec(), b"\x03\xf9".to_vec(), ], )]), }, // 2048b RSA key RSAKeyDefinition { magic: b"\x85\x01\x0c\x03".to_vec(), key_size: 2048, keyid_offset: 4, terminator_offset: 271, usage_offset: 12, valid_checks: HashMap::from([( 13, vec![ b"\x08\x00".to_vec(), b"\x07\xff".to_vec(), b"\x07\xfe".to_vec(), b"\x07\xfd".to_vec(), b"\x07\xfc".to_vec(), b"\x07\xfb".to_vec(), b"\x07\xfa".to_vec(), b"\x07\xf9".to_vec(), ], )]), }, // 3072b RSA key RSAKeyDefinition { magic: b"\x85\x01\x8c\x03".to_vec(), key_size: 3072, keyid_offset: 4, terminator_offset: 399, usage_offset: 12, valid_checks: HashMap::from([( 13, vec![ b"\x0c\x00".to_vec(), b"\x0b\xff".to_vec(), b"\x0b\xfe".to_vec(), b"\x0b\xfd".to_vec(), b"\x0b\xfc".to_vec(), b"\x0b\xfb".to_vec(), b"\x0b\xfa".to_vec(), b"\x0b\xf9".to_vec(), ], )]), }, // 4096b RSA key RSAKeyDefinition { magic: b"\x85\x02\x0c\x03".to_vec(), key_size: 4096, keyid_offset: 4, terminator_offset: 527, usage_offset: 12, valid_checks: HashMap::from([( 13, vec![ b"\x10\x00".to_vec(), b"\x0f\xff".to_vec(), b"\x0f\xfe".to_vec(), b"\x0f\xfd".to_vec(), b"\x0f\xfc".to_vec(), b"\x0f\xfb".to_vec(), b"\x0f\xfa".to_vec(), b"\x0f\xf9".to_vec(), ], )]), }, // 8192b RSA key RSAKeyDefinition { magic: b"\x85\x04\x0c\x03".to_vec(), key_size: 8192, keyid_offset: 4, terminator_offset: 1039, usage_offset: 12, valid_checks: HashMap::from([( 13, vec![ b"\x20\x00".to_vec(), b"\x1f\xff".to_vec(), b"\x1f\xfe".to_vec(), b"\x1f\xfd".to_vec(), b"\x1f\xfc".to_vec(), b"\x1f\xfb".to_vec(), b"\x1f\xfa".to_vec(), b"\x1f\xf9".to_vec(), ], )]), }, ] } /// RSA crypto magic bytes pub fn rsa_magic() -> Vec> { let mut magics: Vec> = vec![]; for key_definition in rsa_key_definitions() { magics.push(key_definition.magic.clone()); } magics } /// Validates an RSA encrypted file header pub fn rsa_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // Loop through all the known RSA key types for key_definition in rsa_key_definitions() { let magic_start: usize = offset; let magic_end: usize = magic_start + key_definition.magic.len(); // Check if these magic bytes belong to this key type if let Some(rsa_magic) = file_data.get(magic_start..magic_end) { if rsa_magic == key_definition.magic { // Parse and validate the key data match rsa_key_parser(&file_data[magic_start..], &key_definition) { Err(_) => { break; } Ok(key_info) => { result.size = key_info.data_size; result.description = format!( "{}, {} bits, can sign: {}, can encrypt: {}, key ID: {:#018X}", result.description, key_info.bits, key_info.can_sign, key_info.can_encrypt, key_info.key_id ); return Ok(result); } } } } } Err(SignatureError) } /// Stores info about a validated RSA key #[derive(Debug, Default, Clone)] struct RSAKeyInfo { pub bits: usize, pub can_sign: bool, pub can_encrypt: bool, pub key_id: usize, pub data_size: usize, } /// Parse and validate key data based on the provided key definition fn rsa_key_parser( raw_data: &[u8], key_definition: &RSAKeyDefinition, ) -> Result { // Expected constant values const TERMINATOR_BYTE: u8 = 0xD2; const ENCRYPT_ONLY: u8 = 2; const SIGN_AND_ENCRYPT: u8 = 1; const VALID_BYTES_SIZE: usize = 2; let key_id_structure = vec![("id", "u64")]; let mut result = RSAKeyInfo { ..Default::default() }; // This is the farthest offset we'll need to index into the key data let key_data_len: usize = key_definition.terminator_offset + std::mem::size_of::(); if let Some(key_data) = raw_data.get(0..key_data_len) { // Check the terminator byte if key_data[key_definition.terminator_offset] == TERMINATOR_BYTE { // Get the key ID if let Ok(key_id) = common::parse( &key_data[key_definition.keyid_offset..], &key_id_structure, "big", ) { // Report the key ID result.key_id = key_id["id"]; // Determine if this key can be used to sign or encrypt result.can_encrypt = key_data[key_definition.usage_offset] == ENCRYPT_ONLY; result.can_sign = key_data[key_definition.usage_offset] == SIGN_AND_ENCRYPT; // If a key can sign, it can also encrypt if result.can_sign { result.can_encrypt = true; } // A key that can't sign or encrypt would be useless! if result.can_sign || result.can_encrypt { // Each key has a set of fixed-size bytes that are expected to exist at certian offsets for (valid_bytes_start, valid_bytes) in key_definition.valid_checks.clone().into_iter() { // Get the bytes to validate; always a size of 2 let valid_bytes_end: usize = valid_bytes_start + VALID_BYTES_SIZE; let key_bytes = key_data[valid_bytes_start..valid_bytes_end].to_vec(); // Check the bytes in the key data against the list of expected bytes for expected_bytes in valid_bytes { // Got 'em! if key_bytes == expected_bytes { result.bits = key_definition.key_size; result.data_size = key_data_len; return Ok(result); } } } } } } } Err(SignatureError) } ================================================ FILE: src/signatures/rtk.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::rtk::parse_rtk_header; /// Human readable description pub const DESCRIPTION: &str = "RTK firmware header"; /// RTK firmware images always start with these bytes pub fn rtk_magic() -> Vec> { vec![b"RTK0".to_vec()] } /// Validates the RTK header pub fn rtk_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // Note: magic.rs enforces short=true for this signature, so offset will always be 0 let available_data = file_data.len() - offset; if let Ok(rtk_header) = parse_rtk_header(&file_data[offset..]) { // This firmware header is expected to encompass the entirety of the remaining file data if rtk_header.image_size == available_data { result.size = rtk_header.header_size; result.description = format!( "{}, header size: {} bytes, image size: {}", result.description, rtk_header.header_size, rtk_header.image_size ); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/seama.rs ================================================ use crate::signatures::common::{CONFIDENCE_LOW, SignatureError, SignatureResult}; use crate::structures::seama::parse_seama_header; /// Human readable description pub const DESCRIPTION: &str = "SEAMA firmware header"; /// SEAMA magic bytes, big and little endian pub fn seama_magic() -> Vec> { vec![ b"\x5E\xA3\xA4\x17\x00\x00".to_vec(), b"\x17\xA4\xA3\x5E\x00\x00".to_vec(), ] } /// Validate SEAMA signatures pub fn seama_parser(file_data: &[u8], offset: usize) -> Result { // Success return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_LOW, ..Default::default() }; // Parse the header if let Ok(seama_header) = parse_seama_header(&file_data[offset..]) { let total_size: usize = seama_header.header_size + seama_header.data_size; // Sanity check the reported size if file_data.len() >= (offset + total_size) { result.size = seama_header.header_size; result.description = format!( "{}, header size: {} bytes, data size: {} bytes", result.description, seama_header.header_size, seama_header.data_size ); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/sevenzip.rs ================================================ use crate::common::crc32; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::sevenzip::parse_7z_header; /// Human readable description pub const DESCRIPTION: &str = "7-zip archive data"; /// 7zip magic bytes pub fn sevenzip_magic() -> Vec> { vec![b"7z\xbc\xaf\x27\x1c".to_vec()] } /// Validates 7zip signatures pub fn sevenzip_parser(file_data: &[u8], offset: usize) -> Result { // Parse the 7z header if let Ok(sevenzip_header) = parse_7z_header(&file_data[offset..]) { // Calculate the start and end offsets that the next header CRC was calculated over let next_crc_start: usize = offset + sevenzip_header.header_size + sevenzip_header.next_header_offset; let next_crc_end: usize = next_crc_start + sevenzip_header.next_header_size; if let Some(crc_data) = file_data.get(next_crc_start..next_crc_end) { // Calculate the next_header CRC let calculated_next_crc: usize = crc32(crc_data) as usize; // Validate the next_header CRC if calculated_next_crc == sevenzip_header.next_header_crc { // Calculate total size of the 7zip archive let total_size: usize = sevenzip_header.header_size + sevenzip_header.next_header_offset + sevenzip_header.next_header_size; // Report signature result return Ok(SignatureResult { offset, size: total_size, confidence: CONFIDENCE_HIGH, description: format!( "{}, version {}.{}, total size: {} bytes", DESCRIPTION, sevenzip_header.major_version, sevenzip_header.minor_version, total_size ), ..Default::default() }); } } } Err(SignatureError) } ================================================ FILE: src/signatures/shrs.rs ================================================ use crate::signatures::common::{ CONFIDENCE_LOW, CONFIDENCE_MEDIUM, SignatureError, SignatureResult, }; use crate::structures::shrs::parse_shrs_header; /// Human readable description pub const DESCRIPTION: &str = "SHRS encrypted firmware"; /// SHRS firmware images always start with these bytes pub fn shrs_magic() -> Vec> { vec![b"SHRS".to_vec()] } /// Validates the SHRS header pub fn shrs_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_LOW, ..Default::default() }; if let Ok(shrs_header) = parse_shrs_header(&file_data[offset..]) { result.size = shrs_header.header_size + shrs_header.data_size; result.description = format!( "{}, header size: {} bytes, encrypted data size: {} bytes, IV: {}", result.description, shrs_header.header_size, shrs_header.data_size, hex::encode(shrs_header.iv), ); if offset == 0 { result.confidence = CONFIDENCE_MEDIUM; } return Ok(result); } Err(SignatureError) } ================================================ FILE: src/signatures/squashfs.rs ================================================ use crate::common::epoch_to_string; use crate::extractors::squashfs::{ squashfs_be_extractor, squashfs_le_extractor, squashfs_v4_be_extractor, }; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::squashfs::{parse_squashfs_header, parse_squashfs_uid_entry}; use std::collections::HashMap; /// Human readable description pub const DESCRIPTION: &str = "SquashFS file system"; /// All of the known magic bytes that could indicate the beginning of a SquashFS image pub fn squashfs_magic() -> Vec> { vec![ b"sqsh".to_vec(), b"hsqs".to_vec(), b"sqlz".to_vec(), b"qshs".to_vec(), b"tqsh".to_vec(), b"hsqt".to_vec(), b"shsq".to_vec(), ] } /// Responsible for parsing and validating a suspected SquashFS image header pub fn squashfs_parser(file_data: &[u8], offset: usize) -> Result { const SQUASHFSV4: usize = 4; let squashfs_compression_types = HashMap::from([ (0, "unknown"), (1, "gzip"), (2, "lzma"), (3, "lzo"), (4, "xz"), (5, "lz4"), (6, "zstd"), ]); let mut result = SignatureResult { size: 0, offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; let available_data: usize = file_data.len() - offset; // Parse the squashfs header if let Ok(squashfs_header) = parse_squashfs_header(&file_data[offset..]) { // Sanity check the reported image size if squashfs_header.image_size <= available_data { /* * To better validate SquashFS images, we want to verify at least some of the SquashFS image contents. * There are situations where the SquashFS header itself is valid and in-tact, but the data is not; for example, * gzipping a SquashFS image often leaves some of the SquashFS data uncompressed, since SquashFS images are already * compressed and the gzip utility realizes that it cannot further compress some sections. This can result in the * contents of the gzipped data containing an uncorrupted copy of the SquashFS header, while some of the SquashFS * image contents are gzipped compressed. * * The easiest field to validate seems to be the UID table pointer, which is an offset in the SquashFS image whre * the UID table resides. This table is just an array of 64-bit pointers, each one pointing to a compressed data block * which contains the actual UIDs. Validate that the UID table pointer is sane, *and* that the first 64-bit pointer * in the UID table is sane. */ // Get the offset of the UID table, an array of pointers to metadata blocks containing lists of user IDs let uid_table_start: usize = offset + squashfs_header.uid_table_start; // Validate that the UID table pointer points to a location after the end of the SquashFS header (it's usually at the end of the image) if uid_table_start > squashfs_header.header_size { // Get the UID table data if let Some(uid_entry_data) = file_data.get(uid_table_start..) { // Parse one entry from the UID table if let Ok(uid_entry) = parse_squashfs_uid_entry( uid_entry_data, squashfs_header.major_version, &squashfs_header.endianness, ) { // Make sure the first UID table entry is either 0, or falls within the bounds of the SquashFS image data if (uid_entry == 0) || (uid_entry > squashfs_header.header_size && uid_entry <= squashfs_header.image_size) { // Format the modified time into something human readable let create_date = epoch_to_string(squashfs_header.timestamp as u32); // Make sure the compression type is supported if squashfs_compression_types.contains_key(&squashfs_header.compression) { let compression_type_str = squashfs_compression_types [&squashfs_header.compression] .to_string(); // Select the appropriate extractor to use if squashfs_header.endianness == "little" { result.preferred_extractor = Some(squashfs_le_extractor()); } else if squashfs_header.major_version == SQUASHFSV4 { result.preferred_extractor = Some(squashfs_v4_be_extractor()); } else { result.preferred_extractor = Some(squashfs_be_extractor()); } result.size = squashfs_header.image_size; result.description = format!( "{}, {} endian, version: {}.{}, compression: {}, inode count: {}, block size: {}, image size: {} bytes, created: {}", result.description, squashfs_header.endianness, squashfs_header.major_version, squashfs_header.minor_version, compression_type_str, squashfs_header.inode_count, squashfs_header.block_size, squashfs_header.image_size, create_date ); return Ok(result); } } } } } } } Err(SignatureError) } ================================================ FILE: src/signatures/srec.rs ================================================ use crate::common::is_offset_safe; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use aho_corasick::AhoCorasick; /// Human readable descriptions pub const SREC_DESCRIPTION: &str = "Motorola S-record"; pub const SREC_SHORT_DESCRIPTION: &str = "Motorola S-record (generic)"; /// Generic, short signature for s-records, should only be matched at the beginning of a file pub fn srec_short_magic() -> Vec> { vec![b"S0".to_vec()] } /// This assumes a srec header with the hex encoded string of "HDR" pub fn srec_magic() -> Vec> { vec![b"S00600004844521B".to_vec()] } /// Validates a SREC signature pub fn srec_parser(file_data: &[u8], offset: usize) -> Result { // \r and \n const UNIX_TERMINATING_CHARACTER: u8 = 0x0A; const WINDOWS_TERMINATING_CHARACTER: u8 = 0x0D; let mut result = SignatureResult { offset, description: SREC_DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; let available_data = file_data.len(); // Srec lines, and hence the last line of an s-record, should end with a new line or line feed let terminating_characters = [WINDOWS_TERMINATING_CHARACTER, UNIX_TERMINATING_CHARACTER]; // Possible srec footers let srec_footers = vec![b"\nS9", b"\nS8", b"\nS7"]; // Need to grep for the srec footer to determine total size let grep = AhoCorasick::new(srec_footers.clone()).unwrap(); // Search for srec footer lines for srec_footer_match in grep.find_overlapping_iter(&file_data[offset..]) { // Assume origin OS is Unix unless proven otherwise let mut os_type: &str = "Unix"; // Start searching for terminating EOF characters after this footer match (all footer signatures are the same size) let mut srec_eof: usize = offset + srec_footer_match.start() + srec_footers[0].len(); let mut last_srec_eof = None; // Found the start of a possible srec footer line, loop over remianing bytes looking for the line termination while is_offset_safe(available_data, srec_eof, last_srec_eof) { // All srec lines should end in \n or \r\n if terminating_characters.contains(&file_data[srec_eof]) { // Windows systems use \r\n if file_data[srec_eof] == WINDOWS_TERMINATING_CHARACTER { // There should be one more character, a \n, which is common to both windows and linux implementations srec_eof += 1; os_type = "Windows"; } // Sanity check, don't want to index out of bounds if let Some(srec_last_byte) = file_data.get(srec_eof) { // Last byte should be a line feed (\n) if *srec_last_byte == UNIX_TERMINATING_CHARACTER { // Include the final line feed byte in the size of the s-record srec_eof += 1; // Report results result.size = srec_eof - offset; result.description = format!( "{}, origin OS: {}, total size: {} bytes", result.description, os_type, result.size ); return Ok(result); } } // Invalid srec termination, stop searching return Err(SignatureError); } // Not a terminating character, go to the next byte in the file last_srec_eof = Some(srec_eof); srec_eof += 1; } } // No valid srec footers found Err(SignatureError) } ================================================ FILE: src/signatures/svg.rs ================================================ use crate::extractors::svg::extract_svg_image; use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; /// Human readable description pub const DESCRIPTION: &str = "SVG image"; /// SVG magic bytes pub fn svg_magic() -> Vec> { vec![b" Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // Perform an extraction dry-run let dry_run = extract_svg_image(file_data, offset, None); // If the dry-run was a success, this is probably a valid JPEG file if dry_run.success { // Get the total size of the SVG if let Some(svg_size) = dry_run.size { // If this file starts with SVG data, there's no need to extract it if offset == 0 { result.extraction_declined = true; } // Report signature result result.size = svg_size; result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/tarball.rs ================================================ use crate::common::is_offset_safe; use crate::signatures::common::{ CONFIDENCE_HIGH, CONFIDENCE_MEDIUM, SignatureError, SignatureResult, }; /// Some tarball constants const TARBALL_BLOCK_SIZE: usize = 512; const TARBALL_MAGIC_OFFSET: usize = 257; const TARBALL_MAGIC_SIZE: usize = 5; const TARBALL_SIZE_OFFSET: usize = 124; const TARBALL_SIZE_LEN: usize = 11; const TARBALL_UNIVERSAL_MAGIC: &[u8; 5] = b"ustar"; const TARBALL_MIN_EXPECTED_HEADERS: usize = 10; /// Human readable description pub const DESCRIPTION: &str = "POSIX tar archive"; /// Magic bytes for tarball and GNU tarball file types pub fn tarball_magic() -> Vec> { vec![b"ustar\x00".to_vec(), b"ustar\x20\x20\x00".to_vec()] } /// Validate tarball signatures pub fn tarball_parser(file_data: &[u8], offset: usize) -> Result { // Stores the running total size of the tarball let mut tarball_total_size: usize = 0; // Keep a count of how many tar entry headers were validated let mut valid_header_count: usize = 0; // Calculate the actual start of the tarball (header magic does not start at the beginning of a tar entry) let tarball_start_offset = offset - TARBALL_MAGIC_OFFSET; // Tarball magic bytes do not start at the beginning of the tarball file let mut next_header_start = tarball_start_offset; let mut previous_header_start = None; let available_data = file_data.len(); // Loop through available data, processing tarball entry headers while is_offset_safe(available_data, next_header_start, previous_header_start) { // Calculate the end of the next tarball entry data let next_header_end = next_header_start + TARBALL_BLOCK_SIZE; // Get the next header's data; this will fail if not enough data is present, protecting // other functions (header_checksum_is_valid, tarball_entry_size) from out-of-bounds access match file_data.get(next_header_start..next_header_end) { None => { break; } Some(tarball_header_block) => { // Bad checksum? Quit processing headers. if !header_checksum_is_valid(tarball_header_block) { break; } // Increment the count of valid tarball headers found valid_header_count += 1; // Get the reported size of the next entry header match tarball_entry_size(tarball_header_block) { Err(_) => { break; } Ok(entry_size) => { // Update total size count, and next/previous header offsets tarball_total_size += entry_size; previous_header_start = Some(next_header_start); next_header_start += entry_size; } } } } } // We expect that a tarball should be, at a minimum, one block in size if tarball_total_size >= TARBALL_BLOCK_SIZE { // Default confidence is medium let mut confidence = CONFIDENCE_MEDIUM; // If more than just a few tarball headers were found and processed successfully, we have pretty high confidence that this isn't a false positive if valid_header_count >= TARBALL_MIN_EXPECTED_HEADERS { confidence = CONFIDENCE_HIGH; } return Ok(SignatureResult { description: format!("{DESCRIPTION}, file count: {valid_header_count}"), offset: tarball_start_offset, size: tarball_total_size, confidence, ..Default::default() }); } Err(SignatureError) } /// Validate a tarball entry checksum fn header_checksum_is_valid(header_block: &[u8]) -> bool { const TARBALL_CHECKSUM_START: usize = 148; const TARBALL_CHECKSUM_END: usize = 156; let checksum_value_string: &[u8] = &header_block[TARBALL_CHECKSUM_START..TARBALL_CHECKSUM_END]; let reported_checksum = tarball_octal(checksum_value_string); let mut sum: usize = 0; for (i, header_byte) in header_block.iter().enumerate() { if (TARBALL_CHECKSUM_START..TARBALL_CHECKSUM_END).contains(&i) { sum += 0x20; } else { sum += *header_byte as usize; } } sum == reported_checksum } /// Returns the size of a tarball entry, including header and data fn tarball_entry_size(tarball_entry_data: &[u8]) -> Result { // Get the tarball entry's magic bytes let entry_magic: &[u8] = &tarball_entry_data[TARBALL_MAGIC_OFFSET..TARBALL_MAGIC_OFFSET + TARBALL_MAGIC_SIZE]; // Make sure the magic bytes are valid if entry_magic == TARBALL_UNIVERSAL_MAGIC { // Pull this tarball entry's data size, stored as ASCII octal, out of the header let entry_size_string: &[u8] = &tarball_entry_data[TARBALL_SIZE_OFFSET..TARBALL_SIZE_OFFSET + TARBALL_SIZE_LEN]; // Convert the ASCII octal to a number let reported_entry_size: usize = tarball_octal(entry_size_string); // The actual size of this entry will be the data size, rounded up to the nearest block size, PLUS one block for the entry header let block_count: usize = 1 + (reported_entry_size as f32 / TARBALL_BLOCK_SIZE as f32).ceil() as usize; // Total size is the total number of blocks times the block size return Ok(block_count * TARBALL_BLOCK_SIZE); } Err(SignatureError) } /// Convert octal string to a number fn tarball_octal(octal_string: &[u8]) -> usize { let mut num: usize = 0; for octal_char in octal_string { // ASCII octal values should be ASCII if *octal_char < 0x30 || *octal_char > 0x39 { break; } else { num *= 8; num = num + (*octal_char as usize) - 0x30; } } num } ================================================ FILE: src/signatures/tplink.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::tplink::{parse_tplink_header, parse_tplink_rtos_header}; /// Human readable description pub const DESCRIPTION: &str = "TP-Link firmware header"; /// TP-Link firmware headers start with these bytes pub fn tplink_magic() -> Vec> { vec![b"\x01\x00\x00\x00TP-LINK Technologies\x00\x00\x00\x00ver. 1.0".to_vec()] } /// Validates the TP-Link header pub fn tplink_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // Parse the header if let Ok(tplink_header) = parse_tplink_header(&file_data[offset..]) { // Fill in size and description result.size = tplink_header.header_size; result.description = format!( "{}, kernel load address: {:#X}, kernel entry point: {:#X}, header size: {} bytes", result.description, tplink_header.kernel_load_address, tplink_header.kernel_entry_point, tplink_header.header_size ); return Ok(result); } Err(SignatureError) } /// Human readable description pub const RTOS_DESCRIPTION: &str = "TP-Link RTOS firmware"; /// TP-Link RTOS firmware start with these magic bytes pub fn tplink_rtos_magic() -> Vec> { vec![b"\x00\x14\x2F\xC0".to_vec()] } /// Parse and validate TP-Link RTOS firmware header pub fn tplink_rtos_parser( file_data: &[u8], offset: usize, ) -> Result { let mut result = SignatureResult { offset, description: RTOS_DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; if let Ok(fw_header) = parse_tplink_rtos_header(&file_data[offset..]) { result.description = format!( "{}, model number: {:X}, hardware version: {:X}.{:X}, header size: {} bytes, total size: {} bytes", result.description, fw_header.model_number, fw_header.hardware_rev_major, fw_header.hardware_rev_minor, fw_header.header_size, fw_header.total_size, ); return Ok(result); } Err(SignatureError) } ================================================ FILE: src/signatures/trx.rs ================================================ use crate::extractors::trx::extract_trx_partitions; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::trx::parse_trx_header; /// Human readable description pub const DESCRIPTION: &str = "TRX firmware image"; /// TRX magic bytes pub fn trx_magic() -> Vec> { vec![b"HDR0".to_vec()] } /// Validates a TRX signature pub fn trx_parser(file_data: &[u8], offset: usize) -> Result { // Success return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Do a dry run to validate the TRX data let dry_run = extract_trx_partitions(file_data, offset, None); if dry_run.success { if let Some(trx_total_size) = dry_run.size { // Dry run successful, parse the TRX header and return a useful description if let Ok(trx_header) = parse_trx_header(&file_data[offset..]) { result.size = trx_total_size; result.description = format!( "{}, version {}, partition count: {}, header size: {} bytes, total size: {} bytes", result.description, trx_header.version, trx_header.partitions.len(), trx_header.header_size, result.size ); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/ubi.rs ================================================ use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::ubi::{ parse_ubi_ec_header, parse_ubi_superblock_header, parse_ubi_volume_header, }; use aho_corasick::AhoCorasick; use std::collections::HashMap; /// Human readable desciptions pub const UBI_FS_DESCRIPTION: &str = "UBIFS image"; pub const UBI_IMAGE_DESCRIPTION: &str = "UBI image"; /// Erase block magic bytes; header version is assumed to be 1 pub fn ubi_magic() -> Vec> { vec![b"UBI#\x01".to_vec()] } /// UBI node magic; this matches *any* UBI node, but ubifs_parser ensures that only superblock nodes are reported pub fn ubifs_magic() -> Vec> { vec![b"\x31\x18\x10\x06".to_vec()] } /// Validates a UBIFS signature pub fn ubifs_parser(file_data: &[u8], offset: usize) -> Result { // Success return value let mut result = SignatureResult { offset, description: UBI_FS_DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Parse the UBIFS superblock header if let Ok(sb_header) = parse_ubi_superblock_header(&file_data[offset..]) { // Image size is the number of logical erase blocks times the size of each logical erase block result.size = sb_header.leb_count * sb_header.leb_size; result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } Err(SignatureError) } /// Validates a UBI signature pub fn ubi_parser(file_data: &[u8], offset: usize) -> Result { // Success return value let mut result = SignatureResult { offset, description: UBI_IMAGE_DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Parse the UBI header if let Ok(ubi_header) = parse_ubi_ec_header(&file_data[offset..]) { let data_offset: usize = offset + ubi_header.data_offset; let volume_offset: usize = offset + ubi_header.volume_id_offset; // Sanity check the reported volume and data offsets if file_data.len() > data_offset && file_data.len() > volume_offset { // Get the size of the UBI image if let Ok(image_size) = get_ubi_image_size(&file_data[offset..]) { result.size = image_size; result.description = format!( "{}, version: {}, image size: {} bytes", result.description, ubi_header.version, result.size ); return Ok(result); } } } Err(SignatureError) } /// Determines the LEB size and returns the size of the UBI image fn get_ubi_image_size(ubi_data: &[u8]) -> Result { let mut leb_size: usize = 0; let mut block_count: usize = 0; let mut best_leb_match_count: usize = 0; let mut previous_volume_offset: usize = 0; let mut possible_leb_sizes: HashMap = HashMap::new(); // Volume magic bytes, version is assumed to be 1 let ubi_vol_magic = vec![b"UBI!\x01"]; let grep = AhoCorasick::new(ubi_vol_magic).unwrap(); // grep for all volume header magic bytes for magic_match in grep.find_overlapping_iter(ubi_data) { // Offset in the UBI image where this magic match was found let this_volume_offset: usize = magic_match.start(); // Parse the volume header if parse_ubi_volume_header(&ubi_data[this_volume_offset..]).is_ok() { // Header looks valid, increment the block count block_count += 1; // If there was a previous UBI volume header identified, calculate the leb size as the distance between the two volume header if previous_volume_offset != 0 { let this_leb_size = this_volume_offset - previous_volume_offset; // Keep track of the calculated leb size, and how many times each possible leb size was found if possible_leb_sizes.contains_key(&this_leb_size) { possible_leb_sizes .insert(this_leb_size, possible_leb_sizes[&this_leb_size] + 1); } else { possible_leb_sizes.insert(this_leb_size, 1); } } previous_volume_offset = this_volume_offset; } } // Pick the most common leb size for (leb_candidate_size, leb_candidate_count) in possible_leb_sizes.iter() { if *leb_candidate_count > best_leb_match_count { leb_size = *leb_candidate_size; best_leb_match_count = *leb_candidate_count; } } // Image size is leb size times the number of blocks if leb_size != 0 && block_count != 0 { return Ok(block_count * leb_size); } Err(SignatureError) } ================================================ FILE: src/signatures/uboot.rs ================================================ use crate::common::{get_cstring, is_ascii_number}; use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; /// Human readable description pub const DESCRIPTION: &str = "U-Boot version string"; /// U-Boot version number magic bytes pub fn uboot_magic() -> Vec> { vec![b"U-Boot\x20".to_vec()] } /// Validates the U-Boot version number magic pub fn uboot_parser(file_data: &[u8], offset: usize) -> Result { const NUMBER_OFFSET: usize = 7; // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; if let Some(expected_number_byte) = file_data.get(offset + NUMBER_OFFSET) { if is_ascii_number(*expected_number_byte) { let uboot_version_string = get_cstring(&file_data[offset + NUMBER_OFFSET..]); if !uboot_version_string.is_empty() { result.size = uboot_version_string.len(); result.description = format!("{}: {:.100}", result.description, uboot_version_string); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/uefi.rs ================================================ use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::uefi::{parse_uefi_capsule_header, parse_uefi_volume_header}; /// Human readable descriptions pub const VOLUME_DESCRIPTION: &str = "UEFI PI firmware volume"; pub const CAPSULE_DESCRIPTION: &str = "UEFI capsule image"; /// UEFI volume magic bytes pub fn uefi_volume_magic() -> Vec> { vec![b"_FVH".to_vec()] } /// UEFI capsule GUIDs pub fn uefi_capsule_magic() -> Vec> { vec![ b"\xBD\x86\x66\x3B\x76\x0D\x30\x40\xB7\x0E\xB5\x51\x9E\x2F\xC5\xA0".to_vec(), // EFI capsule GUID b"\x8B\xA6\x3C\x4A\x23\x77\xFB\x48\x80\x3D\x57\x8C\xC1\xFE\xC4\x4D".to_vec(), // EFI2 capsule GUID b"\xB9\x82\x91\x53\xB5\xAB\x91\x43\xB6\x9A\xE3\xA9\x43\xF7\x2F\xCC".to_vec(), // UEFI capsule GUID ] } /// Validates UEFI volume signatures pub fn uefi_volume_parser( file_data: &[u8], offset: usize, ) -> Result { // The magic signature begins this many bytes from the start of the UEFI volume const UEFI_MAGIC_OFFSET: usize = 40; let mut result = SignatureResult { size: 0, offset: 0, description: VOLUME_DESCRIPTION.to_string(), confidence: CONFIDENCE_MEDIUM, ..Default::default() }; // Volume actually starts UEFI_MAGIC_OFFSET bytes before the magic bytes; make sure there are at least that many bytes preceeding the magic offset if offset >= UEFI_MAGIC_OFFSET { // Set the correct starting offset for this volume result.offset = offset - UEFI_MAGIC_OFFSET; // Parse the volume header if let Ok(uefi_volume_header) = parse_uefi_volume_header(&file_data[result.offset..]) { // Make sure the volume size is sane if file_data.len() >= (result.offset + uefi_volume_header.volume_size) { result.size = uefi_volume_header.volume_size; result.description = format!( "{}, header CRC: {:#X}, header size: {} bytes, total size: {} bytes", result.description, uefi_volume_header.header_crc as u32, uefi_volume_header.header_size, uefi_volume_header.volume_size ); return Ok(result); } } } Err(SignatureError) } /// Validates UEFI capsule signatures pub fn uefi_capsule_parser( file_data: &[u8], offset: usize, ) -> Result { // Success return value let mut result = SignatureResult { description: CAPSULE_DESCRIPTION.to_string(), offset, size: 0, confidence: CONFIDENCE_MEDIUM, ..Default::default() }; let available_data: usize = file_data.len() - offset; if let Ok(capsule_header) = parse_uefi_capsule_header(&file_data[offset..]) { // Sanity check on header total size field if capsule_header.total_size >= available_data { result.size = capsule_header.total_size; result.description = format!( "{}, header size: {} bytes, total size: {} bytes", result.description, capsule_header.header_size, capsule_header.total_size ); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/uimage.rs ================================================ use crate::common::epoch_to_string; use crate::extractors::uimage::extract_uimage; use crate::signatures::common::{ CONFIDENCE_HIGH, CONFIDENCE_LOW, CONFIDENCE_MEDIUM, SignatureError, SignatureResult, }; use crate::structures::uimage::parse_uimage_header; /// Human readable description pub const DESCRIPTION: &str = "uImage firmware image"; /// uImage magic bytes pub fn uimage_magic() -> Vec> { vec![ // Standard uImage magic b"\x27\x05\x19\x56".to_vec(), // Alternate uImage magic (https://git.openwrt.org/?p=openwrt/openwrt.git;a=commitdiff;h=01a1e21863aa30c7a2c252ff06b9aef0cf957970) b"OKLI".to_vec(), ] } /// Validates uImage signatures pub fn uimage_parser(file_data: &[u8], offset: usize) -> Result { // Success return value let mut result = SignatureResult { size: 0, offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Do an extraction dry-run let dry_run = extract_uimage(file_data, offset, None); if dry_run.success { if let Some(uimage_size) = dry_run.size { // Extraction dry-run ok, parse the header to display some useful info if let Ok(uimage_header) = parse_uimage_header(&file_data[offset..]) { result.size = uimage_size; // Decline extraction if the header CRC does not match, or if the reported data size is 0 result.extraction_declined = !uimage_header.header_crc_valid || uimage_header.data_size == 0; result.description = format!( "{}, header size: {} bytes, data size: {} bytes, compression: {}, CPU: {}, OS: {}, image type: {}, load address: {:#X}, entry point: {:#X}, creation time: {}, image name: \"{}\"", result.description, uimage_header.header_size, uimage_header.data_size, uimage_header.compression_type, uimage_header.cpu_type, uimage_header.os_type, uimage_header.image_type, uimage_header.load_address, uimage_header.entry_point_address, epoch_to_string(uimage_header.timestamp as u32), uimage_header.name ); // If the header CRC is invalid, adjust the reported confidence level and report the checksum mis-match if !uimage_header.header_crc_valid { // If the uImage header was otherwise valid and starts at file offset 0 then we're still fairly confident in the result if result.offset == 0 { result.confidence = CONFIDENCE_MEDIUM; } else { result.confidence = CONFIDENCE_LOW; } result.description = format!("{}, invalid checksum", result.description); } return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/vxworks.rs ================================================ use crate::common::get_cstring; use crate::extractors::vxworks::extract_symbol_table; use crate::signatures::common::{CONFIDENCE_HIGH, CONFIDENCE_LOW, SignatureError, SignatureResult}; /// Human readable descriptions pub const SYMTAB_DESCRIPTION: &str = "VxWorks symbol table"; pub const WIND_KERNEL_DESCRIPTION: &str = "VxWorks WIND kernel version"; /// WIND kernel version magic pub fn wind_kernel_magic() -> Vec> { // Magic version string for WIND kernels vec![b"WIND version ".to_vec()] } /// VxWorks symbol table magic bytes pub fn symbol_table_magic() -> Vec> { // These magic bytes match the type and group fields in the VxWorks symbol table, for both big and little endian targets vec![ b"\x00\x00\x05\x00\x00\x00\x00\x00".to_vec(), b"\x00\x00\x07\x00\x00\x00\x00\x00".to_vec(), b"\x00\x00\x09\x00\x00\x00\x00\x00".to_vec(), b"\x00\x05\x00\x00\x00\x00\x00\x00".to_vec(), b"\x00\x07\x00\x00\x00\x00\x00\x00".to_vec(), b"\x00\x09\x00\x00\x00\x00\x00\x00".to_vec(), ] } /// Validates WIND kernel version signatures pub fn wind_kernel_parser( file_data: &[u8], offset: usize, ) -> Result { // Length of the magic signatures bytes const MAGIC_SIZE: usize = 13; let mut result = SignatureResult { offset, description: WIND_KERNEL_DESCRIPTION.to_string(), confidence: CONFIDENCE_LOW, ..Default::default() }; // Want the string that proceeds the magic bytes let version_offset: usize = offset + MAGIC_SIZE; if let Some(version_bytes) = file_data.get(version_offset..) { // The wind kernel magic bytes should be followed by a string containing the wind kernel version let version_string = get_cstring(version_bytes); // Make sure we got a string if !version_string.is_empty() { result.size = MAGIC_SIZE + version_string.len(); result.description = format!("{} {}", result.description, version_string); return Ok(result); } } Err(SignatureError) } /// Validates VxWorks symbol table signatures pub fn symbol_table_parser( file_data: &[u8], offset: usize, ) -> Result { // The magic bytes start at this offset from the beginning of the symbol table const MAGIC_OFFSET: usize = 8; let mut result = SignatureResult { description: SYMTAB_DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // The magic bytes are not at the beginning of the VxWorks symbol table; sanity check the specified offset if offset >= MAGIC_OFFSET { // Actual start of the symbol table let symtab_start: usize = offset - MAGIC_OFFSET; // Do a dry-run extraction of the symbol table let dry_run = extract_symbol_table(file_data, symtab_start, None); // If dry run was a success, this is very likely a valid symbol table if dry_run.success { // Get the size of the symbol table from the dry-run if let Some(symtab_size) = dry_run.size { result.size = symtab_size; result.offset = symtab_start; result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/wince.rs ================================================ use crate::extractors::wince::wince_dump; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::wince::parse_wince_header; /// Human readable description pub const DESCRIPTION: &str = "Windows CE binary image"; /// Windows CE magic bytes pub fn wince_magic() -> Vec> { vec![b"B000FF\n".to_vec()] } /// Validates the Windows CE header pub fn wince_parser(file_data: &[u8], offset: usize) -> Result { // Successful return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Do an extraction dry-run let dry_run = wince_dump(file_data, offset, None); if dry_run.success { if let Some(total_size) = dry_run.size { result.size = total_size; // Parse the WinCE header to get some useful info to display if let Ok(wince_header) = parse_wince_header(&file_data[offset..]) { result.description = format!( "{}, base address: {:#X}, image size: {} bytes, file size: {} bytes", result.description, wince_header.base_address, wince_header.image_size, result.size ); return Ok(result); } } } Err(SignatureError) } ================================================ FILE: src/signatures/xz.rs ================================================ use crate::common::is_offset_safe; use crate::extractors::lzma::lzma_decompress; use crate::extractors::sevenzip::sevenzip_extractor; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::xz::parse_xz_header; /// Human readable description pub const DESCRIPTION: &str = "XZ compressed data"; /// XZ magic bytes pub fn xz_magic() -> Vec> { vec![b"\xFD\x37\x7a\x58\x5a\x00".to_vec()] } /// Validates XZ signatures pub fn xz_parser(file_data: &[u8], offset: usize) -> Result { // Success return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; let mut next_offset = offset; let mut previous_offset = None; let mut stream_header_count = 0; let available_data = file_data.len() - offset; // XZ streams can be concatenated together, need to process them all to determine the size of an XZ file while is_offset_safe(available_data, next_offset, previous_offset) { // Parse the next XZ header to validate the header CRC match parse_xz_header(&file_data[next_offset..]) { Err(_) => break, Ok(_) => { // Header is valid stream_header_count += 1; // Do an extraction dry-run to make sure the data decompresses correctly let dry_run = lzma_decompress(file_data, next_offset, None); // If dry run was a success, update the offset and size fields if dry_run.success && dry_run.size.is_some() { previous_offset = Some(next_offset); next_offset += dry_run.size.unwrap(); result.size += dry_run.size.unwrap(); // Else, report that the data is malformed and stop processing XZ streams } else { // 7z may be able to at least partially extract malformed data streams result.preferred_extractor = Some(sevenzip_extractor()); result.description = format!( "{}, valid header with malformed data stream", result.description ); break; } } } } // Return success if at least one valid XZ stream header was found if stream_header_count > 0 { // Only report the total size if we were able to determine the total size if result.size > 0 { result.description = format!("{}, total size: {} bytes", result.description, result.size); } return Ok(result); } Err(SignatureError) } ================================================ FILE: src/signatures/yaffs.rs ================================================ use crate::common::is_offset_safe; use crate::signatures::common::{CONFIDENCE_MEDIUM, SignatureError, SignatureResult}; use crate::structures::yaffs::{parse_yaffs_file_header, parse_yaffs_obj_header}; /// Minimum number of expected YAFFS objects in a YAFFS image const MIN_NUMBER_OF_OBJS: usize = 2; /// Human readable description pub const DESCRIPTION: &str = "YAFFSv2 filesystem"; /// Expect the first YAFFS entry to be either a directory (0x00000003) or file (0x00000001), big or little endian pub fn yaffs_magic() -> Vec> { vec![ b"\x03\x00\x00\x00\x01\x00\x00\x00\xFF\xFF".to_vec(), b"\x00\x00\x00\x03\x00\x00\x00\x01\xFF\xFF".to_vec(), b"\x01\x00\x00\x00\x01\x00\x00\x00\xFF\xFF".to_vec(), b"\x00\x00\x00\x01\x00\x00\x00\x01\xFF\xFF".to_vec(), ] } /// Validate a YAFFS signature pub fn yaffs_parser(file_data: &[u8], offset: usize) -> Result { // Max page size + max spare size const MAX_OBJ_SIZE: usize = 16896; const BIG_ENDIAN_FIRST_BYTE: u8 = 0; let mut result = SignatureResult { description: DESCRIPTION.to_string(), offset, size: 0, confidence: CONFIDENCE_MEDIUM, ..Default::default() }; let mut endianness = "little"; let available_data = file_data.len(); let required_min_offset = offset + (MAX_OBJ_SIZE * MIN_NUMBER_OF_OBJS); // Sanity check the amount of available data if is_offset_safe(available_data, required_min_offset, None) { // Detect endianness if file_data[offset] == BIG_ENDIAN_FIRST_BYTE { endianness = "big"; } // Determine the page if let Ok(page_size) = get_page_size(&file_data[offset..]) { // Deterine the chunk size if let Ok(spare_size) = get_spare_size(&file_data[offset..], page_size, endianness) { // Get the total image size if let Ok(image_size) = get_image_size(&file_data[offset..], page_size, spare_size, endianness) { result.size = image_size; result.description = format!( "{}, {} endian, page size: {}, spare size: {}, image size: {} bytes", result.description, endianness, page_size, spare_size, image_size ); return Ok(result); } } } } Err(SignatureError) } /// Returns the detected page size used by the YAFFS image fn get_page_size(file_data: &[u8]) -> Result { // Spare area is expected to start with these bytes, depending on endianess and ECC settings (YAFFS2 only) let spare_magics: Vec> = vec![ b"\x00\x00\x10\x00".to_vec(), b"\x00\x10\x00\x00".to_vec(), b"\xFF\xFF\x00\x00\x10\x00".to_vec(), b"\xFF\xFF\x00\x10\x00\x00".to_vec(), ]; // Valid YAFFS page sizes let page_sizes: Vec = vec![512, 1024, 2048, 4096, 8192, 16384]; // Loop through each page size looking for one that is immediately followed by a valid spare data entry. // This is only for YAFFS2! It will fail for YAFFS1 images. for page_size in &page_sizes { for spare_magic in &spare_magics { let start_spare_offset: usize = *page_size; let end_spare_offset: usize = start_spare_offset + spare_magic.len(); if let Some(spare_magic_candidate) = file_data.get(start_spare_offset..end_spare_offset) { // If this spare data starts with the expected bytes, then we've guessed the page size correctly if spare_magic_candidate == *spare_magic { return Ok(*page_size); } } } } // Nothing valid found Err(SignatureError) } /// Returns the detected spare size of the YAFFS image fn get_spare_size( file_data: &[u8], page_size: usize, endianness: &str, ) -> Result { // Valid spare sizes let spare_sizes: Vec = vec![16, 32, 64, 128, 256, 512]; // Loop through all spare sizes until a valid object header is found // This is only for YAFFS2! It will fail for YAFFS1 images. for spare_size in &spare_sizes { // If this spare size is correct, this should be the location of the next object header let next_obj_offset: usize = (page_size + *spare_size) * MIN_NUMBER_OF_OBJS; if let Some(obj_header_data) = file_data.get(next_obj_offset..) { // Attempt to parse this data as a YAFFS object header if parse_yaffs_obj_header(obj_header_data, endianness).is_ok() { return Ok(*spare_size); } } } // Nothing valid found Err(SignatureError) } /// Returns the total size of the image, in bytes fn get_image_size( file_data: &[u8], page_size: usize, spare_size: usize, endianness: &str, ) -> Result { // Object type for files const FILE_TYPE: usize = 1; let mut image_size: usize = 0; let mut next_obj_offset: usize = 0; let mut previous_obj_offset = None; let available_data = file_data.len(); let block_size: usize = page_size + spare_size; // Loop through all available data, parsing YAFFS object headers while is_offset_safe(available_data, next_obj_offset, previous_obj_offset) { match file_data.get(next_obj_offset..) { None => { return Err(SignatureError); } Some(obj_data) => { // Parse and validate the object header match parse_yaffs_obj_header(obj_data, endianness) { Err(_) => { // This is not necessarily an error; could just be that there is trailing data after the YAFFS image break; } Ok(header) => { // Each object header takes up at least one block of data let mut data_blocks: usize = 1; // If this is a file, the file data wil take up additional data blocks if header.obj_type == FILE_TYPE { match get_file_block_count(obj_data, page_size, endianness) { Err(e) => { return Err(e); } Ok(block_count) => { data_blocks += block_count; } } } // Update calculated image size and object header offsets previous_obj_offset = Some(next_obj_offset); image_size += data_blocks * block_size; next_obj_offset = image_size; } } } } } // Sanity check the calculated image size; should be large enough to fit MIN_NUMBER_OF_OBJS, but not extend past EOF if image_size > (block_size * MIN_NUMBER_OF_OBJS) && image_size <= available_data { return Ok(image_size); } Err(SignatureError) } /// Returns the number of data blocks used to store file data; this size is only valid for file type objects fn get_file_block_count( obj_data: &[u8], page_size: usize, endianness: &str, ) -> Result { // parse_yaffs_file_header only parses a portion of the header that we need; the partial structure starts this many bytes into the object data const INFO_STRUCT_START: usize = 268; if let Some(file_header_data) = obj_data.get(INFO_STRUCT_START..) { // Parse the partial object header. if let Ok(file_info) = parse_yaffs_file_header(file_header_data, endianness) { // File data is broken up into blocks of page_size bytes let file_block_count: usize = ((file_info.file_size as f64) / (page_size as f64)).ceil() as usize; return Ok(file_block_count); } } Err(SignatureError) } ================================================ FILE: src/signatures/zip.rs ================================================ use crate::common::is_offset_safe; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::zip::{parse_eocd_header, parse_zip_header}; use aho_corasick::AhoCorasick; /// Human readable description pub const DESCRIPTION: &str = "ZIP archive"; /// ZIP file entry magic bytes pub fn zip_magic() -> Vec> { vec![b"PK\x03\x04".to_vec()] } /// Validates a ZIP file entry signature pub fn zip_parser(file_data: &[u8], offset: usize) -> Result { // Success return value let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; // Parse the ZIP file header if let Ok(zip_file_header) = parse_zip_header(&file_data[offset..]) { // Locate the end-of-central-directory header, which must come after the zip local file entries match find_zip_eof(file_data, offset) { Ok(zip_info) => { result.size = zip_info.eof - offset; result.description = format!( "{}, version: {}.{}, file count: {}, total size: {} bytes", result.description, zip_file_header.version_major, zip_file_header.version_minor, zip_info.file_count, result.size ); } // If the ZIP file is corrupted and no EOCD header exists, attempt to parse all the individual ZIP file headers Err(_) => { let available_data = file_data.len() - offset; let mut previous_file_header_offset = None; let mut next_file_header_offset = offset + zip_file_header.total_size; while is_offset_safe( available_data, next_file_header_offset, previous_file_header_offset, ) { match parse_zip_header(&file_data[next_file_header_offset..]) { Ok(zip_header) => { previous_file_header_offset = Some(next_file_header_offset); next_file_header_offset += zip_header.total_size; } Err(_) => { result.size = next_file_header_offset - offset; result.description = format!( "{}, version: {}.{}, missing end-of-central-directory header, total size: {} bytes", result.description, zip_file_header.version_major, zip_file_header.version_minor, result.size ); break; } } } } } // Only return success if the ZIP file size was identified if result.size > 0 { return Ok(result); } } Err(SignatureError) } pub struct ZipEOCDInfo { pub eof: usize, pub file_count: usize, } /// Need to grep the rest of the file data to locate the end-of-central-directory header, which tells us where the ZIP file ends. pub fn find_zip_eof(file_data: &[u8], offset: usize) -> Result { // This magic string assumes that the disk_number and central_directory_disk_number are 0 const ZIP_EOCD_MAGIC: &[u8; 8] = b"PK\x05\x06\x00\x00\x00\x00"; // Instatiate AhoCorasick search with the ZIP EOCD magic bytes let grep = AhoCorasick::new(vec![ZIP_EOCD_MAGIC]).unwrap(); // Find all matching ZIP EOCD patterns for eocd_match in grep.find_overlapping_iter(&file_data[offset..]) { // Calculate the start and end of the fixed-size portion of the ZIP EOCD header in the file data let eocd_start: usize = eocd_match.start() + offset; // Parse the end-of-central-directory header if let Some(eocd_data) = file_data.get(eocd_start..) { if let Ok(eocd_header) = parse_eocd_header(eocd_data) { return Ok(ZipEOCDInfo { eof: eocd_start + eocd_header.size, file_count: eocd_header.file_count, }); } } } // No valid EOCD record found :( Err(SignatureError) } ================================================ FILE: src/signatures/zlib.rs ================================================ use crate::extractors::zlib::zlib_decompress; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; /// Human readable description pub const DESCRIPTION: &str = "Zlib compressed file"; /// Zlib magic bytes pub fn zlib_magic() -> Vec> { vec![ b"\x78\x9c".to_vec(), b"\x78\xDA".to_vec(), b"\x78\x5E".to_vec(), ] } /// Validate a zlib signature pub fn zlib_parser(file_data: &[u8], offset: usize) -> Result { let mut result = SignatureResult { offset, confidence: CONFIDENCE_HIGH, description: DESCRIPTION.to_string(), ..Default::default() }; // Decompress the zlib; no output directory specified, dry run only. let decompression_dry_run = zlib_decompress(file_data, offset, None); // If the decompression dry run was a success, this signature is almost certianly valid if decompression_dry_run.success { if let Some(zlib_file_size) = decompression_dry_run.size { result.size = zlib_file_size; result.description = format!("{}, total size: {} bytes", result.description, result.size); return Ok(result); } } Err(SignatureError) } ================================================ FILE: src/signatures/zstd.rs ================================================ use crate::common::is_offset_safe; use crate::signatures::common::{CONFIDENCE_HIGH, SignatureError, SignatureResult}; use crate::structures::zstd::{parse_block_header, parse_zstd_header}; /// Human readable description pub const DESCRIPTION: &str = "ZSTD compressed data"; /// ZSTD magic bytes pub fn zstd_magic() -> Vec> { vec![b"\x28\xb5\x2f\xfd".to_vec()] } /// Validate a ZSTD signature pub fn zstd_parser(file_data: &[u8], offset: usize) -> Result { // Size of checksum value at EOF const EOF_CHECKSUM_SIZE: usize = 4; // More or less arbitrarily chosen const MIN_BLOCK_COUNT: usize = 2; let mut result = SignatureResult { offset, description: DESCRIPTION.to_string(), confidence: CONFIDENCE_HIGH, ..Default::default() }; let available_data = file_data.len(); // Parse the ZSTD header; this should be safe as the ZSTD magic bytes wouldn't have matched at this offset if nothing was there... if let Ok(zstd_header) = parse_zstd_header(&file_data[offset..]) { /* * The first block header starts immediately after the ZSTD header, BUT there may be optional header fields present. * Must parse the frame header descriptor bit fields to determine total size of the header. */ let mut next_block_header_start = offset + zstd_header.fixed_header_size; let mut previous_block_header_start = None; // If single segment flag is not set, then window descriptor byte is present in the header if !zstd_header.single_segment_flag { next_block_header_start += 1; } // If the dictionary ID flag is non-zero, its value represents the size of the dictionary ID field; else, this field does not exist if zstd_header.dictionary_id_flag == 1 { next_block_header_start += 1; } else if zstd_header.dictionary_id_flag == 2 { next_block_header_start += 2; } else if zstd_header.dictionary_id_flag == 3 { next_block_header_start += 4; } /* * If the frame content flag is 0 and the single segment flag is set, then the frame content header field is 1 byte in length; * else, the frame content flag indicates the size of the grame content header field. */ if zstd_header.frame_content_flag == 0 && zstd_header.single_segment_flag { next_block_header_start += 1; } else if zstd_header.frame_content_flag == 1 { next_block_header_start += 2; } else if zstd_header.frame_content_flag == 2 { next_block_header_start += 4; } else if zstd_header.frame_content_flag == 3 { next_block_header_start += 8; } // Keep a count of how many blocks we've processed let mut block_count: usize = 0; // We now know where the first block header starts, loop through all the blocks to determine where the ZSTD data ends while is_offset_safe( available_data, next_block_header_start, previous_block_header_start, ) { // Parse the block header match parse_block_header(&file_data[next_block_header_start..]) { Err(_) => { break; } Ok(block_header) => { // Block header looks valid, increment block counter block_count += 1; // The next block header should start at the end of this block; note that the reported block size does not include the size of the block header previous_block_header_start = Some(next_block_header_start); next_block_header_start += block_header.header_size + block_header.block_size; // Was this the last block? if block_header.last_block { // Update the total size, which is the difference between the end of the last block and the start of the ZSTD header result.size = next_block_header_start - offset; // If a checksum is included at the end of the block stream, add the checksum size to the total size if zstd_header.content_checksum_present { result.size += EOF_CHECKSUM_SIZE; } // Make sure we've processed more than one block; if so, return Ok, else break and return Err below if block_count >= MIN_BLOCK_COUNT { result.description = format!( "{}, total size: {} bytes", result.description, result.size ); return Ok(result); } else { break; } } } } } } Err(SignatureError) } ================================================ FILE: src/signatures.rs ================================================ //! # File / Data Signatures //! //! Creating a signature to identify a particular file or data type is composed of two parts: //! //! 1. Defining the signature's attributes //! 2. Writing a parser to parse and validate potential signature matches //! //! ## Defining a Signature //! //! Signatures are defined using the `signatures::common::Signature` struct. This structure stores critical information //! about a signature, such as the signature name, the magic bytes that are associated with the signature, and which extractor //! to use (if any) to extract the data associated with the signature. //! //! ### Example //! //! ```ignore //! use binwalk::extractors::foobar::foobar_extractor; //! use binwalk::signatures::common::Signature; //! use binwalk::signatures::foobar::foobar_parser; //! //! // FooBar file signature //! let foobar_signature = Signature { //! // A unique name for the signature, no spaces; signatures can be included/excluded from analysis based on this attribute //! name: "foobar".to_string(), //! // Set to true for signatures with very short magic bytes; they will only be matched at file offset 0 //! short: false, //! // Offset from the start of the file to the "magic" bytes; only really relevant for short signatures //! magic_offset: 0, //! // Most signatures will want to set this to false and let the code in main.rs determine if/when to display //! always_display: false, //! // The magic bytes associated with this signature; there may be more than one set of magic bytes per signature //! magic: vec![b"\xF0\x00\xBA\xA2".to_vec()], //! // This is the parser function to call to validate magic byte matches //! parser: foobar_parser, //! // A short human-readable description of the signature //! description: "FooBar file".to_string(), //! // The extractor to use to extract this file/data type //! extractor: Some(foobar_extractor()), //! }; //! ``` //! //! Internally, Binwalk keeps a list of `Signature` definitions in `magic.rs`. //! //! ## Signature Parsers //! //! Signature parsers are at the heart of each defined signature. They parse and validate magic matches to ensure accuracy and //! determine the total size of the file data (if possible). //! //! Signature parsers must conform to the `signatures::common::SignatureParser` type definition. //! They are provided two arguments: the raw file data, and an offset into the file data where the signature's magic bytes were found. //! //! Signature parsers must parse and validate the expected signature data, and return either a `signatures::common::SignatureResult` //! structure on success, or a `signatures::common::SignatureError` on failure. //! //! ### Example //! //! ```ignore //! use binwalk::extractors::foobar::extract_foobar_file; //! use binwalk::signatures::common::{SignatureResult, SignatureError, CONFIDENCE_HIGH}; //! //! /// This function is responsible for parsing and validating the FooBar file system data whenever the "magic bytes" //! /// are found inside a file. It is provided access to the entire file data, and an offset into the file data where //! /// the magic bytes were found. On success, it will return a signatures::common::SignatureResult structure. //! /// //! pub fn foobar_parser(file_data: &Vec, offset: usize) -> Result { //! /* //! * This will be returned if the format of the suspected FooBar file system looks correct. //! * We will update it later with more information, but for now just define what is known //! * (the offset where the FooBar file starts, the human-readable description, and //! * a confidence level), and leave the remaining fields at their defaults. //! * //! * Note that confidence level is chosen somewhat arbitrarily, and should be one of: //! * //! * - CONFIDENCE_LOW (the default) //! * - CONFIDENCE_MEDIUM //! * - CONFIDENCE_HIGH //! * //! * In this case the extractor and header parser (defined elsewhere) validate CRC's, so if those pass, //! * the confidence that this is in fact a FooBar file type is high. //! */ //! let mut result = SignatureResult { //! offset: offset, //! description: "FooBar file".to_string(), //! confidence: CONFIDENCE_HIGH, //! ..Default::default() //! }; //! //! /* //! * The internal FooBar file extractor already parses the header and validates the data CRC. By passing it an output //! * directory of None, it will still parse and validate the data, but without performing an extraction. //! */ //! let dry_run = extact_foobar_file(file_data, offset, None); //! //! // The extractor should have reported success, as well as the total size of the file data //! if dry_run.success == true { //! if let Some(file_size) = dry_run.size { //! // Update the reported size and human-readable description and return the result //! result.size = file_size; //! result.description = format!("{}, total size: {} bytes", result.description, result.size); //! return Ok(result); //! } //! } //! //! // Something didn't look right about this file data, it is likely a false positive, so return an error //! return Err(SignatureError); //! } //! ``` pub mod aes; pub mod android_bootimg; pub mod androidsparse; pub mod apfs; pub mod arcadyan; pub mod arj; pub mod autel; pub mod binhdr; pub mod bmp; pub mod btrfs; pub mod bzip2; pub mod cab; pub mod cfe; pub mod chk; pub mod common; pub mod compressd; pub mod copyright; pub mod cpio; pub mod cramfs; pub mod csman; pub mod dahua_zip; pub mod deb; pub mod dkbs; pub mod dlink_tlv; pub mod dlke; pub mod dlob; pub mod dmg; pub mod dms; pub mod dpapi; pub mod dtb; pub mod dxbc; pub mod ecos; pub mod efigpt; pub mod elf; pub mod encfw; pub mod encrpted_img; pub mod ext; pub mod fat; pub mod gif; pub mod gpg; pub mod gzip; pub mod hashes; pub mod iso9660; pub mod jboot; pub mod jffs2; pub mod jpeg; pub mod linux; pub mod logfs; pub mod luks; pub mod lz4; pub mod lzfse; pub mod lzma; pub mod lzop; pub mod matter_ota; pub mod mbr; pub mod mh01; pub mod ntfs; pub mod openssl; pub mod packimg; pub mod pcap; pub mod pchrom; pub mod pdf; pub mod pe; pub mod pem; pub mod pjl; pub mod pkcs_der; pub mod png; pub mod qcow; pub mod qnx; pub mod rar; pub mod riff; pub mod romfs; pub mod rsa; pub mod rtk; pub mod seama; pub mod sevenzip; pub mod shrs; pub mod squashfs; pub mod srec; pub mod svg; pub mod tarball; pub mod tplink; pub mod trx; pub mod ubi; pub mod uboot; pub mod uefi; pub mod uimage; pub mod vxworks; pub mod wince; pub mod xz; pub mod yaffs; pub mod zip; pub mod zlib; pub mod zstd; ================================================ FILE: src/structures/android_bootimg.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store Android boot image header info #[derive(Debug, Default, Clone)] pub struct AndroidBootImageHeader { pub kernel_size: usize, pub ramdisk_size: usize, pub kernel_load_address: usize, pub ramdisk_load_address: usize, } /// Parses an Android boot image header pub fn parse_android_bootimg_header( bootimg_data: &[u8], ) -> Result { let bootimg_structure = vec![ ("magic", "u64"), ("kernel_size", "u32"), ("kernel_load_addr", "u32"), ("ramdisk_size", "u32"), ("ramdisk_load_addr", "u32"), ]; // Parse the header if let Ok(bootimg_header) = common::parse(bootimg_data, &bootimg_structure, "little") { return Ok(AndroidBootImageHeader { kernel_size: bootimg_header["kernel_size"], kernel_load_address: bootimg_header["kernel_load_addr"], ramdisk_size: bootimg_header["ramdisk_size"], ramdisk_load_address: bootimg_header["ramdisk_load_addr"], }); } Err(StructureError) } ================================================ FILE: src/structures/androidsparse.rs ================================================ use crate::structures::common::{self, StructureError}; /// Storage struct for AndroidSparse file header info #[derive(Debug, Default, Clone)] pub struct AndroidSparseHeader { pub major_version: usize, pub minor_version: usize, pub header_size: usize, pub block_size: usize, pub chunk_count: usize, } /// Parse Android Sparse header structures pub fn parse_android_sparse_header( sparse_data: &[u8], ) -> Result { // Version must be 1.0 const MAJOR_VERSION: usize = 1; const MINOR_VERSION: usize = 0; // Blocks must be aligned on a 4-byte boundary const BLOCK_ALIGNMENT: usize = 4; // Expected value for the reported chunk header size const CHUNK_HEADER_SIZE: usize = 12; // Header structure let android_sparse_structure = vec![ ("magic", "u32"), ("major_version", "u16"), ("minor_version", "u16"), ("header_size", "u16"), ("chunk_header_size", "u16"), ("block_size", "u32"), ("block_count", "u32"), ("total_chunks", "u32"), ("checksum", "u32"), ]; let expected_header_size = common::size(&android_sparse_structure); // Parse the header if let Ok(header) = common::parse(sparse_data, &android_sparse_structure, "little") { // Sanity check header values if header["major_version"] == MAJOR_VERSION && header["minor_version"] == MINOR_VERSION && header["header_size"] == expected_header_size && header["chunk_header_size"] == CHUNK_HEADER_SIZE && (header["block_size"] % BLOCK_ALIGNMENT) == 0 { return Ok(AndroidSparseHeader { major_version: header["major_version"], minor_version: header["minor_version"], header_size: header["header_size"], block_size: header["block_size"], chunk_count: header["total_chunks"], }); } } Err(StructureError) } /// Storage structure for Android Sparse chunk headers #[derive(Debug, Default, Clone)] pub struct AndroidSparseChunkHeader { pub header_size: usize, pub data_size: usize, pub block_count: usize, pub is_crc: bool, pub is_raw: bool, pub is_fill: bool, pub is_dont_care: bool, } /// Parse the header for an Android Sparse chunk pub fn parse_android_sparse_chunk_header( chunk_data: &[u8], ) -> Result { // Known chunk types const CHUNK_TYPE_RAW: usize = 0xCAC1; const CHUNK_TYPE_FILL: usize = 0xCAC2; const CHUNK_TYPE_DONT_CARE: usize = 0xCAC3; const CHUNK_TYPE_CRC: usize = 0xCAC4; let chunk_structure = vec![ ("chunk_type", "u16"), ("reserved", "u16"), ("output_block_count", "u32"), ("total_size", "u32"), ]; let mut chonker = AndroidSparseChunkHeader { header_size: common::size(&chunk_structure), ..Default::default() }; // Parse the header if let Ok(chunk_header) = common::parse(chunk_data, &chunk_structure, "little") { // Make sure the reserved field is zero if chunk_header["reserved"] == 0 { // Populate the structure values chonker.block_count = chunk_header["output_block_count"]; chonker.data_size = chunk_header["total_size"] - chonker.header_size; chonker.is_crc = chunk_header["chunk_type"] == CHUNK_TYPE_CRC; chonker.is_raw = chunk_header["chunk_type"] == CHUNK_TYPE_RAW; chonker.is_fill = chunk_header["chunk_type"] == CHUNK_TYPE_FILL; chonker.is_dont_care = chunk_header["chunk_type"] == CHUNK_TYPE_DONT_CARE; // The chunk type must be one of the known chunk types if chonker.is_crc || chonker.is_raw || chonker.is_fill || chonker.is_dont_care { return Ok(chonker); } } } Err(StructureError) } ================================================ FILE: src/structures/apfs.rs ================================================ use crate::structures::common::{self, StructureError}; /// Offset of the APFS magic bytes from the start of the APFS image pub const MAGIC_OFFSET: usize = 0x20; /// Struct to store APFS header info #[derive(Debug, Default, Clone)] pub struct APFSHeader { pub block_size: usize, pub block_count: usize, } /// Parses an APFS header pub fn parse_apfs_header(apfs_data: &[u8]) -> Result { const MAX_FS_COUNT: usize = 100; const FS_COUNT_BLOCK_SIZE: usize = 512; // Partial APFS header, just to figure out the size of the image and validate some fields // https://developer.apple.com/support/downloads/Apple-File-System-Reference.pdf let apfs_structure = vec![ ("magic", "u32"), ("block_size", "u32"), ("block_count", "u64"), ("nx_features", "u64"), ("nx_ro_compat_features", "u64"), ("nx_incompat_features", "u64"), ("nx_uuid_p1", "u64"), ("nx_uuid_p2", "u64"), ("nx_next_oid", "u64"), ("nx_next_xid", "u64"), ("nx_xp_desc_blocks", "u32"), ("nx_xp_data_blocks", "u32"), ("nx_xp_desc_base", "u64"), ("nx_xp_data_base", "u64"), ("nx_xp_desc_next", "u32"), ("nx_xp_data_next", "u32"), ("nx_xp_desc_index", "u32"), ("nx_xp_desc_len", "u32"), ("nx_xp_data_index", "u32"), ("nx_xp_data_len", "u32"), ("nx_spaceman_oid", "u64"), ("nx_omap_oid", "u64"), ("nx_reaper_oid", "u64"), ("nx_xp_test_type", "u32"), ("nx_xp_max_file_systems", "u32"), ]; // Expected values of superblock flag fields let allowed_feature_flags: Vec = vec![0, 1, 2, 3]; let allowed_incompat_flags: Vec = vec![0, 1, 2, 3, 0x100, 0x101, 0x102, 0x103]; let allowed_ro_compat_flags: Vec = vec![0]; let apfs_struct_start = MAGIC_OFFSET; let apfs_struct_end = apfs_struct_start + common::size(&apfs_structure); // Parse the header if let Some(apfs_structure_data) = apfs_data.get(apfs_struct_start..apfs_struct_end) { if let Ok(apfs_header) = common::parse(apfs_structure_data, &apfs_structure, "little") { // Simple sanity check on the reported block data if apfs_header["block_size"] != 0 && apfs_header["block_count"] != 0 { // Sanity check the feature flags if allowed_feature_flags.contains(&apfs_header["nx_features"]) && allowed_ro_compat_flags.contains(&apfs_header["nx_ro_compat_features"]) && allowed_incompat_flags.contains(&apfs_header["nx_incompat_features"]) { // The test_type field *must* be NULL if apfs_header["nx_xp_test_type"] == 0 { // Calculate the file system count; this is max_file_systems divided by 512, rounded up to nearest whole let fs_count = ((apfs_header["nx_xp_max_file_systems"] as f32) / (FS_COUNT_BLOCK_SIZE as f32)) .ceil() as usize; // Sanity check the file system count if fs_count > 0 && fs_count <= MAX_FS_COUNT { return Ok(APFSHeader { block_size: apfs_header["block_size"], block_count: apfs_header["block_count"], }); } } } } } } Err(StructureError) } ================================================ FILE: src/structures/arj.rs ================================================ use crate::common::{epoch_to_string, get_cstring}; use crate::structures::common; use crate::structures::common::StructureError; #[derive(Debug, Default, Clone)] pub struct ARJHeader { pub header_size: usize, pub version: u8, pub min_version: u8, pub flags: String, pub host_os: String, pub compression_method: String, pub file_type: String, pub original_name: String, pub original_file_date: String, pub compressed_file_size: i32, pub uncompressed_file_size: i32, } pub fn parse_arj_header(arj_data: &[u8]) -> Result { // ARJ header structure (https://www.fileformat.info/format/arj/corion.htm) let arj_header_structure = vec![ ("magic", "u16"), // offset 0x00 ("basic_header_size", "u16"), // offset 0x02 ("extra_header_size", "u8"), // offset 0x04 ("archiver_version", "u8"), // offset 0x05 ("min_version", "u8"), // offset 0x06 ("host_os", "u8"), // offset 0x07 ("internal_flags", "u8"), // offset 0x08 ("compression_method", "u8"), // offset 0x09 ("file_type", "u8"), // offset 0x0A ("reserved1", "u8"), // offset 0x0B ("datetime_file", "u32"), // offset 0x0C ("compressed_filesize", "u32"), // offset 0x10 ("original_filesize", "u32"), // offset 0x14 ]; if let Ok(arj_header) = common::parse(arj_data, &arj_header_structure, "little") { // check the version information in the header if !(1..=16).contains(&arj_header["archiver_version"]) || !(1..=16).contains(&arj_header["min_version"]) || arj_header["archiver_version"] < arj_header["min_version"] { return Err(StructureError); } let mut flags = if arj_header["internal_flags"] & 0x01 != 0 { "password".to_string() } else { "no password".to_string() }; if arj_header["internal_flags"] & 0x04 != 0 { flags = format!("{flags}|multi-volume"); } // let file_start_pos_is_available = arj_header["internal_flags"] & 0x08 != 0; if arj_header["internal_flags"] & 0x10 != 0 { flags = format!("{flags}|slash-switched"); } if arj_header["internal_flags"] & 0x20 != 0 { flags = format!("{flags}|backup"); } let host_os = match &arj_header["host_os"] { 0 => "MS-DOS".to_string(), 1 => "PRIMOS".to_string(), 2 => "UNIX".to_string(), 3 => "AMIGA".to_string(), 4 => "MAX-OS".to_string(), 5 => "OS/2".to_string(), 6 => "APPLE GS".to_string(), 7 => "ATARI ST".to_string(), 8 => "NeXT".to_string(), 9 => "VAX VMS".to_string(), _ => return Err(StructureError), }; let compression_method = match &arj_header["compression_method"] { 0 => "stored".to_string(), 1 => "compressed most".to_string(), 2 => "compressed".to_string(), 3 => "compressed faster".to_string(), 4 => "compressed fastest".to_string(), _ => return Err(StructureError), }; let file_type = match &arj_header["file_type"] { 0 => "binary".to_string(), 1 => "7-bit text".to_string(), 2 => "comment header".to_string(), 3 => "directory".to_string(), 4 => "volume label".to_string(), _ => return Err(StructureError), }; let compressed_file_size = arj_header["compressed_filesize"] as i32; if compressed_file_size < 0 { return Err(StructureError); } let uncompressed_file_size = arj_header["original_filesize"] as i32; if uncompressed_file_size < 0 { return Err(StructureError); } let header_size = arj_header["extra_header_size"]; let original_name = if let Some(data) = arj_data.get(header_size + 4..) { get_cstring(data) } else { "".to_string() }; return Ok(ARJHeader { header_size, version: arj_header["archiver_version"] as u8, min_version: arj_header["min_version"] as u8, flags, host_os, compression_method, file_type, original_name, original_file_date: epoch_to_string(arj_header["datetime_file"] as u32), compressed_file_size, uncompressed_file_size, }); } Err(StructureError) } ================================================ FILE: src/structures/autel.rs ================================================ use crate::common::get_cstring; use crate::structures::common::{self, StructureError}; /// Struct to store Autel ECC header info #[derive(Debug, Default, Clone)] pub struct AutelECCHeader { pub data_size: usize, pub header_size: usize, } /// Parses an Autel header pub fn parse_autel_header(autel_data: &[u8]) -> Result { const EXPECTED_HEADER_SIZE: usize = 0x20; const COPYRIGHT_SIZE: usize = 16; const EXPECTED_COPYRIGHT_STRING: &str = "Copyright Autel"; let autel_ecc_structure = vec![ ("magic", "u64"), ("data_size", "u32"), ("header_size", "u32"), // Followed by 16-byte copyright string ]; // Parse the header if let Ok(autel_header) = common::parse(autel_data, &autel_ecc_structure, "little") { // Sanity check the reported header size if autel_header["header_size"] == EXPECTED_HEADER_SIZE { let copyright_start = common::size(&autel_ecc_structure); let copyright_end = copyright_start + COPYRIGHT_SIZE; // Get the copyright string contained in the header if let Some(copyright_bytes) = autel_data.get(copyright_start..copyright_end) { let copyright_string = get_cstring(copyright_bytes); // Sanity check the copyright string value if copyright_string == EXPECTED_COPYRIGHT_STRING { return Ok(AutelECCHeader { data_size: autel_header["data_size"], header_size: autel_header["header_size"], }); } } } } Err(StructureError) } ================================================ FILE: src/structures/binhdr.rs ================================================ use crate::common::get_cstring; use crate::structures::common::{self, StructureError}; use std::collections::HashMap; /// Struct to store BIN header info pub struct BINHeader { pub board_id: String, pub hardware_revision: String, pub firmware_version_major: usize, pub firmware_version_minor: usize, } /// Parses a BIN header pub fn parse_bin_header(bin_hdr_data: &[u8]) -> Result { // The data strcuture is preceeded by a 4-byte board ID string const STRUCTURE_OFFSET: usize = 4; let bin_hdr_structure = vec![ ("reserved1", "u32"), ("build_date", "u32"), ("firmware_version_major", "u8"), ("firmware_version_minor", "u8"), ("magic", "u32"), ("hardware_id", "u8"), ("reserved2", "u24"), ("reserved3", "u64"), ]; let known_hardware_ids: HashMap = HashMap::from([(0, "4702"), (1, "4712"), (2, "4712L"), (3, "4704")]); // Parse the header if let Some(structure_data) = bin_hdr_data.get(STRUCTURE_OFFSET..) { if let Ok(header) = common::parse(structure_data, &bin_hdr_structure, "little") { // Make sure the reserved fields are NULL if header["reserved1"] == 0 && header["reserved2"] == 0 && header["reserved3"] == 0 { // Make sure the reported hardware ID is valid if known_hardware_ids.contains_key(&header["hardware_id"]) { // Get the board ID string, which immediately preceeds the data structure if let Some(board_id_bytes) = bin_hdr_data.get(0..STRUCTURE_OFFSET) { let board_id = get_cstring(board_id_bytes); // The board ID string should be 4 bytes in length if board_id.len() == STRUCTURE_OFFSET { return Ok(BINHeader { board_id, hardware_revision: known_hardware_ids[&header["hardware_id"]] .to_string(), firmware_version_major: header["firmware_version_major"], firmware_version_minor: header["firmware_version_minor"], }); } } } } } } Err(StructureError) } ================================================ FILE: src/structures/bmp.rs ================================================ use crate::structures::common::{self, StructureError}; #[derive(Debug, Default, Clone)] pub struct BMPFileHeader { pub size: usize, pub bitmap_bits_offset: usize, } pub fn parse_bmp_file_header(bmp_data: &[u8]) -> Result { // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapfileheader let bmp_header_structure = vec![ ("bfType", "u16"), ("bfSize", "u32"), ("bfReserved1", "u16"), ("bfReserved2", "u16"), ("bfOffBits", "u32"), ]; if let Ok(bmp_header) = common::parse(bmp_data, &bmp_header_structure, "little") { let bmp_data_size = bmp_data.len(); // The BMP file size cannot be bigger than bmp_data if bmp_data_size < bmp_header["bfSize"] { return Err(StructureError); } // The file size cannot be 0 if bmp_header["bfSize"] == 0 { return Err(StructureError); } // The offset cannot be 0 if bmp_header["bfOffBits"] == 0 { return Err(StructureError); } // The offset cannot be bigger than the file if bmp_header["bfOffBits"] > bmp_data_size { return Err(StructureError); } // If everything is Ok so far, return a BMPFileHeader return Ok(BMPFileHeader { size: bmp_header["bfSize"], bitmap_bits_offset: bmp_header["bfOffBits"], }); } Err(StructureError) } // https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapv5header // "The number of bytes required by the structure. Applications should use this member to determine which bitmap information header structure is being used." pub fn get_dib_header_size(bmp_data: &[u8]) -> Result { let valid_header_sizes = [ 12, // BITMAPCOREHEADER 40, // BITMAPINFOHEADER 108, // BITMAPV4HEADER 124, ]; let header_size = u32::from_le_bytes(bmp_data[..4].try_into().unwrap()); if !valid_header_sizes.contains(&header_size) { return Err(StructureError); } Ok(header_size as usize) } ================================================ FILE: src/structures/btrfs.rs ================================================ use crate::structures::common::{self, StructureError}; use crc32c::crc32c; /// Struct to store BTRFS super block info #[derive(Debug, Default, Clone)] pub struct BTRFSHeader { pub bytes_used: usize, pub total_size: usize, pub leaf_size: usize, pub node_size: usize, pub stripe_size: usize, pub sector_size: usize, } /// Parse and validate a BTRFS super block pub fn parse_btrfs_header(btrfs_data: &[u8]) -> Result { const SUPERBLOCK_OFFSET: usize = 0x10000; const SUPERBLOCK_END: usize = SUPERBLOCK_OFFSET + 0x1000; const CRC_START: usize = 0x20; // Partial BTRFS superblock structure for obtaining image size and CRC validation // https://archive.kernel.org/oldwiki/btrfs.wiki.kernel.org/index.php/On-disk_Format.html#Superblock let btrfs_structure = vec![ ("header_checksum", "u32"), ("unused1", "u32"), ("unused2", "u64"), ("unused3", "u64"), ("unused4", "u64"), ("uuid_p1", "u64"), ("uuid_p2", "u64"), ("block_phys_addr", "u64"), ("flags", "u64"), ("magic", "u64"), ("generation", "u64"), ("root_tree_address", "u64"), ("chunk_tree_address", "u64"), ("log_tree_address", "u64"), ("log_root_transid", "u64"), ("total_bytes", "u64"), ("bytes_used", "u64"), ("root_dir_objid", "u64"), ("num_devices", "u64"), ("sector_size", "u32"), ("node_size", "u32"), ("leaf_size", "u32"), ("stripe_size", "u32"), ]; // Parse the header if let Some(btrfs_header_data) = btrfs_data.get(SUPERBLOCK_OFFSET..SUPERBLOCK_END) { if let Ok(btrfs_header) = common::parse(btrfs_header_data, &btrfs_structure, "little") { // Validate the superblock CRC if btrfs_header["header_checksum"] == (crc32c(&btrfs_header_data[CRC_START..]) as usize) { return Ok(BTRFSHeader { sector_size: btrfs_header["sector_size"], node_size: btrfs_header["node_size"], leaf_size: btrfs_header["leaf_size"], stripe_size: btrfs_header["stripe_size"], bytes_used: btrfs_header["bytes_used"], total_size: btrfs_header["total_bytes"], }); } } } Err(StructureError) } ================================================ FILE: src/structures/cab.rs ================================================ use crate::structures::common::{self, StructureError}; /// Stores CAB header info #[derive(Debug, Default, Clone)] pub struct CabinetHeader { pub total_size: usize, pub header_size: usize, pub file_count: usize, pub folder_count: usize, } /// Parse a CAB file header pub fn parse_cab_header(header_data: &[u8]) -> Result { // CAB files must be version 1.3 const MAJOR_VERSION: usize = 1; const MINOR_VERSION: usize = 3; const CAB_STRUCT_SIZE: usize = 40; const CAB_EXTRA_STRUCT_SIZE: usize = 20; const FLAG_EXTRA_DATA_PRESENT: usize = 4; let cab_header_structure = vec![ ("magic", "u32"), ("reserved1", "u32"), ("size", "u32"), ("reserved2", "u32"), ("first_file_offset", "u32"), ("reserved3", "u32"), ("minor_version", "u8"), ("major_version", "u8"), ("folder_count", "u16"), ("file_count", "u16"), ("flags", "u16"), ("id", "u16"), ("set_number", "u16"), ("extra_header_size", "u16"), ("cbCFFolder", "u8"), ("cbCFData", "u8"), ]; let cab_extra_header_structure = vec![ ("unknown1", "u32"), ("data_offset", "u32"), ("data_size", "u32"), ("unknown2", "u32"), ("unknown3", "u32"), ]; let mut header_info = CabinetHeader { header_size: CAB_STRUCT_SIZE, ..Default::default() }; // Parse the CAB header if let Ok(cab_header) = common::parse(header_data, &cab_header_structure, "little") { // All reserved fields must be 0 if cab_header["reserved1"] == 0 && cab_header["reserved2"] == 0 && cab_header["reserved3"] == 0 { // Version must be 1.3 if cab_header["major_version"] == MAJOR_VERSION && cab_header["minor_version"] == MINOR_VERSION { // Update the CabinetHeader fields header_info.total_size = cab_header["size"]; header_info.file_count = cab_header["file_count"]; header_info.folder_count = cab_header["folder_count"]; // Assume everything is *not* ok, until proven otherwise let mut everything_ok: bool = false; // If the extra data flag was set, we need to parse the extra data header to get the size of the extra data if (cab_header["flags"] & FLAG_EXTRA_DATA_PRESENT) != 0 && cab_header["extra_header_size"] == CAB_EXTRA_STRUCT_SIZE { // Calclate the start and end of the extra header let extra_header_start: usize = CAB_STRUCT_SIZE; let extra_header_end: usize = extra_header_start + CAB_EXTRA_STRUCT_SIZE; // Get the extra header raw data if let Some(extra_header_data) = header_data.get(extra_header_start..extra_header_end) { // Parse the extra header if let Ok(extra_header) = common::parse(extra_header_data, &cab_extra_header_structure, "little") { // The extra data is expected to come immediately after the data specified in the main CAB header if extra_header["data_offset"] == cab_header["size"] { // Update the CAB file size to include the extra data header_info.total_size += extra_header["data_size"]; everything_ok = true; } } } } else { everything_ok = true; } // If everything checked out OK, return the result if everything_ok { return Ok(header_info); } } } } Err(StructureError) } ================================================ FILE: src/structures/chk.rs ================================================ use crate::common::get_cstring; use crate::structures::common::{self, StructureError}; /// Storage struct for CHK header info #[derive(Debug, Clone, Default)] pub struct CHKHeader { pub header_size: usize, pub kernel_size: usize, pub rootfs_size: usize, pub board_id: String, } /// Parse a CHK firmware header pub fn parse_chk_header(header_data: &[u8]) -> Result { // Somewhat arbitrarily chosen const MAX_EXPECTED_HEADER_SIZE: usize = 100; let chk_header_structure = vec![ ("magic", "u32"), ("header_size", "u32"), ("unknown", "u64"), ("kernel_checksum", "u32"), ("rootfs_checksum", "u32"), ("rootfs_size", "u32"), ("kernel_size", "u32"), ("image_checksum", "u32"), ("header_checksum", "u32"), // Board ID string follows ]; // Size of the fixed-length portion of the header structure let struct_size: usize = common::size(&chk_header_structure); // Parse the CHK header if let Ok(chk_header) = common::parse(header_data, &chk_header_structure, "big") { // Validate the reported header size if chk_header["header_size"] > struct_size && chk_header["header_size"] <= MAX_EXPECTED_HEADER_SIZE { // Read in the board ID string which immediately follows the fixed size structure and extends to the end of the header let board_id_start: usize = struct_size; let board_id_end: usize = chk_header["header_size"]; if let Some(board_id_raw_bytes) = header_data.get(board_id_start..board_id_end) { let board_id_string = get_cstring(board_id_raw_bytes); // We expect that there must be a valid board ID string if !board_id_string.is_empty() { return Ok(CHKHeader { board_id: board_id_string.clone(), header_size: chk_header["header_size"], kernel_size: chk_header["kernel_size"], rootfs_size: chk_header["rootfs_size"], }); } } } } Err(StructureError) } ================================================ FILE: src/structures/common.rs ================================================ use log::error; use std::collections::HashMap; /* * Note that all values returned by the parse() function are of type usize; this is a concious decision. * Returning usize types makes the calling code much cleaner, but that means that u64 fields won't fit into the return value on 32-bit systems. * Thus, only 64-bit systems are supported. This requirement is enforced here. */ #[cfg(not(target_pointer_width = "64"))] compile_error!("compilation is only allowed for 64-bit targets"); /// Error return value of structure parsers #[derive(Debug, Clone)] pub struct StructureError; /// Function to parse basic C-style data structures. /// /// ## Supported Data Types /// /// The following data types are supported: /// - u8 /// - u16 /// - u24 /// - u32 /// - u64 /// /// ## Arguments /// /// - `data`: The raw data to apply the structure over /// - `structure`: A vector of tuples describing the data structure /// - `endianness`: One of: "big", "little" /// /// ## Example: /// /// ``` /// # fn main() { #[allow(non_snake_case)] fn _doctest_main_src_structures_common_rs_34_0() -> Result { /// use binwalk::structures; /// /// let my_structure = vec![ /// ("magic", "u32"), /// ("size", "u64"), /// ("flags", "u8"), /// ("packed_bytes", "u24"), /// ("checksum", "u16"), /// ]; /// /// let some_data = b"AAAA\x01\x00\x00\x00\x00\x00\x00\x00\x08\x0A\x0B\x0C\x01\x02"; /// let header = structures::common::parse(some_data, &my_structure, "little")?; /// /// assert_eq!(header["magic"], 0x41414141); /// assert_eq!(header["checksum"], 0x0201); /// # Ok(true) /// # } _doctest_main_src_structures_common_rs_34_0(); } /// ``` pub fn parse( data: &[u8], structure: &Vec<(&str, &str)>, endianness: &str, ) -> Result, StructureError> { const U24_SIZE: usize = 3; let mut value: usize; let mut offset: usize = 0; let mut parsed_structure = HashMap::new(); // Get the size of the defined structure let structure_size = size(structure); if let Some(raw_data) = data.get(0..structure_size) { for (name, ctype) in structure { let data_type: String = ctype.to_string(); match type_to_size(ctype) { None => return Err(StructureError), Some(csize) => { if csize == std::mem::size_of::() { // u8, endianness doesn't matter value = u8::from_be_bytes(raw_data[offset..offset + csize].try_into().unwrap()) as usize; } else if csize == std::mem::size_of::() { if endianness == "big" { value = u16::from_be_bytes( raw_data[offset..offset + csize].try_into().unwrap(), ) as usize; } else { value = u16::from_le_bytes( raw_data[offset..offset + csize].try_into().unwrap(), ) as usize; } // Yes Virginia, u24's are real } else if csize == U24_SIZE { if endianness == "big" { value = ((raw_data[offset] as usize) << 16) + ((raw_data[offset + 1] as usize) << 8) + (raw_data[offset + 2] as usize); } else { value = ((raw_data[offset + 2] as usize) << 16) + ((raw_data[offset + 1] as usize) << 8) + (raw_data[offset] as usize); } } else if csize == std::mem::size_of::() { if endianness == "big" { value = u32::from_be_bytes( raw_data[offset..offset + csize].try_into().unwrap(), ) as usize; } else { value = u32::from_le_bytes( raw_data[offset..offset + csize].try_into().unwrap(), ) as usize; } } else if csize == std::mem::size_of::() { if endianness == "big" { value = u64::from_be_bytes( raw_data[offset..offset + csize].try_into().unwrap(), ) as usize; } else { value = u64::from_le_bytes( raw_data[offset..offset + csize].try_into().unwrap(), ) as usize; } } else { error!( "Cannot parse structure element with unknown data type '{data_type}'" ); return Err(StructureError); } offset += csize; parsed_structure.insert(name.to_string(), value); } } } return Ok(parsed_structure); } Err(StructureError) } /// Returns the size of a given structure definition. /// /// ## Example: /// /// ``` /// use binwalk::structures; /// /// let my_structure = vec![ /// ("magic", "u32"), /// ("size", "u64"), /// ("flags", "u8"), /// ("checksum", "u32"), /// ]; /// /// let struct_size = structures::common::size(&my_structure); /// /// assert_eq!(struct_size, 17); /// ``` pub fn size(structure: &Vec<(&str, &str)>) -> usize { let mut struct_size: usize = 0; for (_name, ctype) in structure { match type_to_size(ctype) { None => continue, Some(member_size) => { struct_size += member_size; } } } struct_size } /// Returns the size of a give type string fn type_to_size(ctype: &str) -> Option { // This table must be updated when new data types are added let size_lookup_table: HashMap<&str, usize> = HashMap::from([("u8", 1), ("u16", 2), ("u24", 3), ("u32", 4), ("u64", 8)]); if !size_lookup_table.contains_key(ctype) { error!("Unknown size for structure type '{ctype}'!"); return None; } Some(size_lookup_table[ctype]) } ================================================ FILE: src/structures/cpio.rs ================================================ use crate::structures::common::StructureError; /// Expected minimum size of a CPIO entry header pub const CPIO_HEADER_SIZE: usize = 110; /// Storage struct for CPIO entry header info #[derive(Debug, Clone, Default)] pub struct CPIOEntryHeader { pub magic: Vec, pub data_size: usize, pub file_name: String, pub header_size: usize, } /// Parses a CPIO entry header pub fn parse_cpio_entry_header(cpio_data: &[u8]) -> Result { // Some expected constants const NULL_BYTE_SIZE: usize = 1; const CPIO_MAGIC_START: usize = 0; const CPIO_MAGIC_END: usize = 6; const FILE_SIZE_START: usize = 54; const FILE_SIZE_END: usize = 62; const FILE_NAME_SIZE_START: usize = 94; const FILE_NAME_SIZE_END: usize = 102; let available_data: usize = cpio_data.len(); // TODO: If file mode parsing is added, internal extractor would be pretty easy to implement... if available_data > CPIO_HEADER_SIZE { // Grab the CPIO header magic bytes let header_magic = cpio_data[CPIO_MAGIC_START..CPIO_MAGIC_END].to_vec(); // Get the ASCII hex string representing the file's data size if let Ok(file_data_size_str) = String::from_utf8(cpio_data[FILE_SIZE_START..FILE_SIZE_END].to_vec()) { // Convert the file data size from ASCII hex to an integer if let Ok(file_data_size) = usize::from_str_radix(&file_data_size_str, 16) { // Get the ASCII hex string representing the file name's size if let Ok(file_name_size_str) = String::from_utf8(cpio_data[FILE_NAME_SIZE_START..FILE_NAME_SIZE_END].to_vec()) { // Convert the file name size from ASCII hex to an integer if let Ok(file_name_size) = usize::from_str_radix(&file_name_size_str, 16) { // The file name immediately follows the fixed-length header data. let file_name_start: usize = CPIO_HEADER_SIZE; let file_name_end: usize = file_name_start + file_name_size - NULL_BYTE_SIZE; // Get the file name if let Some(file_name_raw_bytes) = cpio_data.get(file_name_start..file_name_end) { if let Ok(file_name) = String::from_utf8(file_name_raw_bytes.to_vec()) { let header_total_size = CPIO_HEADER_SIZE + file_name_size; return Ok(CPIOEntryHeader { magic: header_magic.clone(), file_name: file_name.clone(), data_size: file_data_size + byte_padding(file_data_size), header_size: header_total_size + byte_padding(header_total_size), }); } } } } } } } Err(StructureError) } /// File data and CPIO headers are padded to 4-byte boundaries fn byte_padding(n: usize) -> usize { let modulus: usize = n % 4; if modulus == 0 { 0 } else { 4 - modulus } } ================================================ FILE: src/structures/cramfs.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store info about a CramFS header #[derive(Default, Debug, Clone)] pub struct CramFSHeader { pub size: usize, pub checksum: u32, pub file_count: usize, pub endianness: String, } /// Parses a CramFS header pub fn parse_cramfs_header(cramfs_data: &[u8]) -> Result { // Endian specific magic bytes const BIG_ENDIAN_MAGIC: usize = 0x453DCD28; const LITTLE_ENDIAN_MAGIC: usize = 0x28CD3D45; let allowed_magics: Vec = vec![BIG_ENDIAN_MAGIC, LITTLE_ENDIAN_MAGIC]; let cramfs_header_structure = vec![ ("magic", "u32"), ("size", "u32"), ("flags", "u32"), ("future", "u32"), ("signature_p1", "u64"), ("signature_p2", "u64"), ("checksum", "u32"), ("edition", "u32"), ("block_count", "u32"), ("file_count", "u32"), ]; let mut cramfs_info = CramFSHeader { ..Default::default() }; let cramfs_structure_size = common::size(&cramfs_header_structure); // Default to little endian cramfs_info.endianness = "little".to_string(); // Parse the CramFS header, try little endian first if let Ok(mut cramfs_header) = common::parse( cramfs_data, &cramfs_header_structure, &cramfs_info.endianness, ) { // Do the magic bytes match? if allowed_magics.contains(&cramfs_header["magic"]) { // If the magic bytes endianness don't match what's expected for little endian, switch to big endian if cramfs_header["magic"] == BIG_ENDIAN_MAGIC { cramfs_info.endianness = "big".to_string(); // Parse the header again, this time as big endian match common::parse( cramfs_data, &cramfs_header_structure, &cramfs_info.endianness, ) { Err(_) => { return Err(StructureError); } Ok(cramfs_be_header) => { cramfs_header = cramfs_be_header.clone(); } } } // Reported image size must be larger than the header structure if cramfs_header["size"] > cramfs_structure_size { // Populate info about the CramFS image cramfs_info.size = cramfs_header["size"]; cramfs_info.checksum = cramfs_header["checksum"] as u32; cramfs_info.file_count = cramfs_header["file_count"]; return Ok(cramfs_info); } } } Err(StructureError) } ================================================ FILE: src/structures/csman.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store CSMAN header info #[derive(Debug, Default, Clone)] pub struct CSManHeader { pub compressed: bool, pub data_size: usize, pub endianness: String, pub header_size: usize, } /// Parses a CSMAN header pub fn parse_csman_header(csman_data: &[u8]) -> Result { const COMPRESSED_MAGIC: &[u8] = b"\x78"; const LITTLE_ENDIAN_MAGIC: usize = 0x4353; let csman_header_structure = vec![ ("magic", "u16"), ("unknown1", "u16"), ("compressed_size", "u32"), ("unknown2", "u32"), ("decompressed_size", "u32"), ]; let mut result = CSManHeader { ..Default::default() }; // Parse the header if let Ok(mut csman_header) = common::parse(csman_data, &csman_header_structure, "big") { // Detect the endianness if csman_header["magic"] == LITTLE_ENDIAN_MAGIC { // If this is a little endian header, re-parse the data as little endian if let Ok(csman_header_le) = common::parse(csman_data, &csman_header_structure, "little") { csman_header = csman_header_le.clone(); result.endianness = "little".to_string(); } } else { result.endianness = "big".to_string(); } // Should have been able to determine the endianness if !result.endianness.is_empty() { result.data_size = csman_header["compressed_size"]; result.header_size = common::size(&csman_header_structure); result.compressed = csman_header["compressed_size"] != csman_header["decompressed_size"]; // If compressed, check the expected compressed magic bytes if result.compressed { if let Some(compressed_magic) = csman_data.get(result.header_size..result.header_size + COMPRESSED_MAGIC.len()) { if compressed_magic != COMPRESSED_MAGIC { return Err(StructureError); } } } return Ok(result); } } Err(StructureError) } /// Stores info about a single CSMan DAT file entry #[derive(Debug, Default, Clone)] pub struct CSManEntry { pub size: usize, pub eof: bool, pub key: usize, pub value: Vec, } /// Parses a single CSMan DAT file entry pub fn parse_csman_entry( entry_data: &[u8], endianness: &str, ) -> Result { const EOF_TAG: usize = 0; // The last entry is just a single 4-byte NULL value let csman_last_entry_structure = vec![("eof", "u32")]; // Entries consist of a 4-byte identifier, a 2-byte size, and a value let csman_entry_structure = vec![ ("key", "u32"), ("size", "u16"), // value of size bytes immediately follows ]; let mut entry = CSManEntry { ..Default::default() }; if let Ok(entry_header) = common::parse(entry_data, &csman_entry_structure, endianness) { let value_start: usize = common::size(&csman_entry_structure); let value_end: usize = value_start + entry_header["size"]; if let Some(entry_value) = entry_data.get(value_start..value_end) { entry.key = entry_header["key"]; entry.value = entry_value.to_vec(); entry.size = common::size(&csman_entry_structure) + entry_value.len(); return Ok(entry); } } else if let Ok(entry_header) = common::parse(entry_data, &csman_last_entry_structure, endianness) { if entry_header["eof"] == EOF_TAG { entry.eof = true; entry.size = common::size(&csman_last_entry_structure); return Ok(entry); } } Err(StructureError) } ================================================ FILE: src/structures/deb.rs ================================================ use crate::structures::common::StructureError; /// Struct to store DEB file info #[derive(Debug, Clone, Default)] pub struct DebHeader { pub file_size: usize, } /// Parse a DEB file pub fn parse_deb_header(deb_data: &[u8]) -> Result { const END_MARKER_SIZE: usize = 2; const DATA_FILE_SIZE_LEN: usize = 10; const DATA_FILE_SIZE_OFFSET: usize = 48; const CONTROL_FILE_SIZE_END: usize = 130; const CONTROL_FILE_SIZE_START: usize = 120; let mut deb_header = DebHeader { ..Default::default() }; // Index into the header to get the raw bytes of the decimal ASCII string that contains the control file size if let Some(control_file_size_data) = deb_data.get(CONTROL_FILE_SIZE_START..CONTROL_FILE_SIZE_END) { // Convert the raw bytes into an ASCII string if let Ok(control_file_size_str) = String::from_utf8(control_file_size_data.to_vec()) { // Trim white space from the string and convert to an integer value if let Ok(control_file_size) = control_file_size_str.trim().parse::() { // Calculate the offsets to the decimal ASCII string that contains the data file size let data_file_size_start: usize = CONTROL_FILE_SIZE_END + END_MARKER_SIZE + control_file_size + DATA_FILE_SIZE_OFFSET; let data_file_size_end: usize = data_file_size_start + DATA_FILE_SIZE_LEN; // Index into the header to get the raw bytes of the deciaml ASCII string that contains the data file size if let Some(data_file_size_data) = deb_data.get(data_file_size_start..data_file_size_end) { // Convert the raw bytes to an ASCII string if let Ok(data_file_size_str) = String::from_utf8(data_file_size_data.to_vec()) { // Trim whitespace from the string and convert to an integer value if let Ok(data_file_size) = data_file_size_str.trim().parse::() { // Total file size is the end of the file data size ASCII field, plus the 2-byte end marker, plus the length of the following data file // TODO: This size seems to be short by 2 bytes? Not a big deal for our purposes, but still... deb_header.file_size = data_file_size_end + END_MARKER_SIZE + data_file_size; return Ok(deb_header); } } } } } } Err(StructureError) } ================================================ FILE: src/structures/dkbs.rs ================================================ use crate::common::get_cstring; use crate::structures::common::{self, StructureError}; /// Struct to store DKBS header info #[derive(Debug, Default, Clone)] pub struct DKBSHeader { pub data_size: usize, pub header_size: usize, pub board_id: String, pub version: String, pub boot_device: String, pub endianness: String, } /// Parses a DKBS header pub fn parse_dkbs_header(dkbs_data: &[u8]) -> Result { // Header is a fixed size const HEADER_SIZE: usize = 0xA0; // Constant offsets for strings and known header fields const BOARD_ID_START: usize = 0; const BOARD_ID_END: usize = 0x20; const VERSION_START: usize = 0x28; const VERSION_END: usize = 0x48; const BOOT_DEVICE_START: usize = 0x70; const BOOT_DEVICE_END: usize = 0x90; const DATA_SIZE_START: usize = 0x68; const DATA_SIZE_END: usize = DATA_SIZE_START + 4; let data_size_field = vec![("size", "u32")]; let mut header = DKBSHeader { header_size: HEADER_SIZE, ..Default::default() }; // Available data should be at least big enough for the header to fit if dkbs_data.len() >= HEADER_SIZE { // Parse the version, board ID, and boot device strings header.version = get_cstring(&dkbs_data[VERSION_START..VERSION_END]); header.board_id = get_cstring(&dkbs_data[BOARD_ID_START..BOARD_ID_END]); header.boot_device = get_cstring(&dkbs_data[BOOT_DEVICE_START..BOOT_DEVICE_END]); // Sanity check to make sure the strings were retrieved if !header.version.is_empty() && !header.board_id.is_empty() && !header.boot_device.is_empty() { if let Some(data_size_bytes) = dkbs_data.get(DATA_SIZE_START..DATA_SIZE_END) { // Parse the payload size field if let Ok(data_size) = common::parse(data_size_bytes, &data_size_field, "big") { if data_size["size"] & 0xFF000000 == 0 { header.data_size = data_size["size"]; header.endianness = "big".to_string(); } else if let Ok(data_size) = common::parse(data_size_bytes, &data_size_field, "little") { header.data_size = data_size["size"]; header.endianness = "little".to_string(); } } if header.data_size != 0 { return Ok(header); } } } } Err(StructureError) } ================================================ FILE: src/structures/dlink_tlv.rs ================================================ use crate::common::get_cstring; use crate::structures::common::{self, StructureError}; /// Struct to store DLink TLV firmware header info #[derive(Debug, Default, Clone)] pub struct DlinkTLVHeader { pub model_name: String, pub board_id: String, pub header_size: usize, pub data_size: usize, pub data_checksum: String, } /// Parses a DLink TLV firmware header pub fn parse_dlink_tlv_header(tlv_data: &[u8]) -> Result { const MAX_STRING_LENGTH: usize = 0x20; const MODEL_NAME_OFFSET: usize = 4; const BOARD_ID_OFFSET: usize = 0x24; const MD5_HASH_OFFSET: usize = 0x4C; const DATA_TLV_OFFSET: usize = 0x6C; const HEADER_SIZE: usize = 0x74; const EXPECTED_DATA_TYPE: usize = 1; let tlv_structure = vec![ ("type", "u32"), ("length", "u32"), // value immediately follows ]; let mut header = DlinkTLVHeader { ..Default::default() }; // Get the header data if let Some(header_data) = tlv_data.get(0..HEADER_SIZE) { // Get the strings from the header header.board_id = get_cstring(&header_data[BOARD_ID_OFFSET..BOARD_ID_OFFSET + MAX_STRING_LENGTH]); header.model_name = get_cstring(&header_data[MODEL_NAME_OFFSET..MODEL_NAME_OFFSET + MAX_STRING_LENGTH]); header.data_checksum = get_cstring(&header_data[MD5_HASH_OFFSET..MD5_HASH_OFFSET + MAX_STRING_LENGTH]); // Make sure we got the expected strings OK (checksum is not always included) if !header.model_name.is_empty() && !header.board_id.is_empty() { // Parse the type and length values that describe the data the follows the header if let Ok(data_tlv) = common::parse(&header_data[DATA_TLV_OFFSET..], &tlv_structure, "little") { // Sanity check the reported type (should be 1) if data_tlv["type"] == EXPECTED_DATA_TYPE { header.data_size = data_tlv["length"]; header.header_size = HEADER_SIZE; return Ok(header); } } } } Err(StructureError) } ================================================ FILE: src/structures/dlob.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store DLOB header info #[derive(Debug, Default, Clone)] pub struct DlobHeader { pub data_size: usize, pub header_size: usize, } /// Parses a DLOB header pub fn parse_dlob_header(dlob_data: &[u8]) -> Result { let dlob_structure_p1 = vec![ ("magic", "u32"), ("metadata_size", "u32"), ("data_size", "u32"), ]; let dlob_structure_p2 = vec![ ("magic", "u32"), ("metadata_size", "u32"), ("data_size", "u32"), ("unknown", "u64"), ("unknown", "u64"), ]; // Parse the first half of the header if let Ok(dlob_header_p1) = common::parse(dlob_data, &dlob_structure_p1, "big") { // Calculate the offset to the second part of the header let dlob_header_p2_offset: usize = common::size(&dlob_structure_p1) + dlob_header_p1["metadata_size"]; // It is expected that the first header is metadata only if dlob_header_p1["data_size"] == 0 { // Parse the second part of the header if let Some(header_p2_data) = dlob_data.get(dlob_header_p2_offset..) { if let Ok(dlob_header_p2) = common::parse(header_p2_data, &dlob_structure_p2, "big") { // Both parts should have the same magic bytes if dlob_header_p1["magic"] == dlob_header_p2["magic"] { // Calculate total header size let header_total_size: usize = dlob_header_p2_offset + common::size(&dlob_structure_p2) + dlob_header_p2["metadata_size"]; // Basic sanity check on the reported data size vs header size if header_total_size < dlob_header_p2["data_size"] { return Ok(DlobHeader { header_size: header_total_size, data_size: dlob_header_p2["data_size"], }); } } } } } } Err(StructureError) } ================================================ FILE: src/structures/dmg.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store DMG footer info #[derive(Debug, Default, Clone)] pub struct DMGFooter { pub footer_size: usize, pub data_length: usize, pub xml_length: usize, } /// Parses a DMG footer structure pub fn parse_dmg_footer(dmg_data: &[u8]) -> Result { // https://newosxbook.com/DMG.html let dmg_footer_structure = vec![ ("magic", "u32"), ("version", "u32"), ("header_size", "u32"), ("flags", "u32"), ("running_data_fork_offset", "u64"), ("data_fork_offset", "u64"), ("data_fork_length", "u64"), ("rsrc_fork_offset", "u64"), ("rsrc_fork_length", "u64"), ("segment_number", "u32"), ("segment_count", "u32"), ("segment_id_p1", "u64"), ("segment_id_p2", "u64"), ("data_checksum_type", "u32"), ("data_checksum_size", "u32"), ("data_checksum_1", "u32"), ("data_checksum_2", "u32"), ("data_checksum_3", "u32"), ("data_checksum_4", "u32"), ("data_checksum_5", "u32"), ("data_checksum_6", "u32"), ("data_checksum_7", "u32"), ("data_checksum_8", "u32"), ("data_checksum_9", "u32"), ("data_checksum_10", "u32"), ("data_checksum_11", "u32"), ("data_checksum_12", "u32"), ("data_checksum_13", "u32"), ("data_checksum_14", "u32"), ("data_checksum_15", "u32"), ("data_checksum_16", "u32"), ("data_checksum_17", "u32"), ("data_checksum_18", "u32"), ("data_checksum_19", "u32"), ("data_checksum_20", "u32"), ("data_checksum_21", "u32"), ("data_checksum_22", "u32"), ("data_checksum_23", "u32"), ("data_checksum_24", "u32"), ("data_checksum_25", "u32"), ("data_checksum_26", "u32"), ("data_checksum_27", "u32"), ("data_checksum_28", "u32"), ("data_checksum_29", "u32"), ("data_checksum_30", "u32"), ("data_checksum_31", "u32"), ("data_checksum_32", "u32"), ("xml_offset", "u64"), ("xml_length", "u64"), ("reserved_1", "u64"), ("reserved_2", "u64"), ("reserved_3", "u64"), ("reserved_4", "u64"), ("reserved_5", "u64"), ("reserved_6", "u64"), ("reserved_7", "u64"), ("reserved_8", "u64"), ("reserved_9", "u64"), ("reserved_10", "u64"), ("reserved_11", "u64"), ("reserved_12", "u64"), ("reserved_13", "u64"), ("reserved_14", "u64"), ("reserved_15", "u64"), ("checksum_type", "u32"), ("checksum_size", "u32"), ("checksum_1", "u32"), ("checksum_2", "u32"), ("checksum_3", "u32"), ("checksum_4", "u32"), ("checksum_5", "u32"), ("checksum_6", "u32"), ("checksum_7", "u32"), ("checksum_8", "u32"), ("checksum_9", "u32"), ("checksum_10", "u32"), ("checksum_11", "u32"), ("checksum_12", "u32"), ("checksum_13", "u32"), ("checksum_14", "u32"), ("checksum_15", "u32"), ("checksum_16", "u32"), ("checksum_17", "u32"), ("checksum_18", "u32"), ("checksum_19", "u32"), ("checksum_20", "u32"), ("checksum_21", "u32"), ("checksum_22", "u32"), ("checksum_23", "u32"), ("checksum_24", "u32"), ("checksum_25", "u32"), ("checksum_26", "u32"), ("checksum_27", "u32"), ("checksum_28", "u32"), ("checksum_29", "u32"), ("checksum_30", "u32"), ("checksum_31", "u32"), ("checksum_32", "u32"), ("image_variant", "u32"), ("sector_count", "u64"), ("reserved_16", "u32"), ("reserved_17", "u32"), ("reserved_18", "u32"), ]; let structure_size: usize = common::size(&dmg_footer_structure); // Parse the DMG footer if let Ok(dmg_footer) = common::parse(dmg_data, &dmg_footer_structure, "big") { // Sanity check, make sure the reported header size is the size of this structure if dmg_footer["header_size"] == structure_size { return Ok(DMGFooter { data_length: dmg_footer["data_fork_length"], xml_length: dmg_footer["xml_length"], footer_size: structure_size, }); } } Err(StructureError) } ================================================ FILE: src/structures/dms.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store DMS header info #[derive(Debug, Default, Clone)] pub struct DMSHeader { pub image_size: usize, } /// Parses a DMS header pub fn parse_dms_header(dms_data: &[u8]) -> Result { const MAGIC_P1: usize = 0x4D47; const MAGIC_P2: usize = 0x3C31303E; let dms_structure = vec![ ("unknown1", "u16"), ("magic_p1", "u16"), ("magic_p2", "u32"), ("unknown2", "u32"), ("image_size", "u32"), ]; // Parse the first half of the header if let Ok(dms_header) = common::parse(dms_data, &dms_structure, "big") { if dms_header["magic_p1"] == MAGIC_P1 && dms_header["magic_p2"] == MAGIC_P2 { return Ok(DMSHeader { image_size: dms_header["image_size"], }); } } Err(StructureError) } ================================================ FILE: src/structures/dpapi.rs ================================================ use crate::structures::common::{self, StructureError}; /* Blob structure: from mimikatz repository. DWORD dwVersion; GUID guidProvider; DWORD dwMasterKeyVersion; GUID guidMasterKey; DWORD dwFlags; DWORD dwDescriptionLen; PWSTR szDescription; ALG_ID algCrypt; DWORD dwAlgCryptLen; DWORD dwSaltLen; PBYTE pbSalt; DWORD dwHmacKeyLen; PBYTE pbHmackKey; ALG_ID algHash; DWORD dwAlgHashLen; DWORD dwHmac2KeyLen; PBYTE pbHmack2Key; DWORD dwDataLen; PBYTE pbData; DWORD dwSignLen; PBYTE pbSign; */ /// Struct to store DPAPI blob structure #[derive(Debug, Default, Clone)] pub struct DPAPIBlobHeader { pub header_size: usize, pub blob_size: usize, pub version: usize, pub provider_id: usize, pub master_key_version: usize, pub master_key_id: usize, pub flags: usize, pub description_len: usize, pub crypto_algorithm: usize, pub crypti_alg_len: usize, pub salt_len: usize, pub hmac_key_len: usize, pub hash_algorithm: usize, pub hash_alg_len: usize, pub hmac2_key_len: usize, pub data_len: usize, pub sign_len: usize, } /// Parse a DPAPI BLOB pub fn parse_dpapi_blob_header(dpapi_blob_data: &[u8]) -> Result { let initial_dpapi_structure = vec![ ("version", "u32"), ("provider_id", "u128"), ("master_key_version", "u32"), ("master_key_id", "u128"), ("flags", "u32"), ("description_len", "u32"), ]; let mut offset: usize = (32 + 128 + 32 + 128 + 32 + 32) / 8; let mut dpapi_header = common::parse(dpapi_blob_data, &initial_dpapi_structure, "little")?; let description_len = dpapi_header["description_len"]; if description_len % 2 != 0 { return Err(StructureError); } let utf16_vec = utf8_to_utf16(&dpapi_blob_data[offset..=offset + description_len]).ok_or(StructureError)?; let desc = String::from_utf16(&utf16_vec).map_err(|_| StructureError)?; // NULL character becomes size 1 from size 2 if description_len != desc.len() - 1 { return Err(StructureError); } offset += description_len; let next_dpapi_structure = vec![ ("crypto_algorithm", "u32"), ("crypti_alg_len", "u32"), ("salt_len", "u32"), ]; dpapi_header.extend(common::parse( &dpapi_blob_data[offset..], &next_dpapi_structure, "little", )?); let salt_len = dpapi_header["salt_len"]; offset += (32 + 32 + 32) / 8 + salt_len; let next_dpapi_structure = vec![("hmac_key_len", "u32")]; dpapi_header.extend(common::parse( &dpapi_blob_data[offset..], &next_dpapi_structure, "little", )?); let hmac_key_len = dpapi_header["hmac_key_len"]; offset += 32 / 8 + hmac_key_len; let next_dpapi_structure = vec![ ("hash_algorithm", "u32"), ("hash_alg_len", "u32"), ("hmac2_key_len", "u32"), ]; dpapi_header.extend(common::parse( &dpapi_blob_data[offset..], &next_dpapi_structure, "little", )?); let hmac2_key_len = dpapi_header["hmac2_key_len"]; offset += (32 + 32 + 32) / 8 + hmac2_key_len; let next_dpapi_structure = vec![("data_len", "u32")]; dpapi_header.extend(common::parse( &dpapi_blob_data[offset..], &next_dpapi_structure, "little", )?); let data_len = dpapi_header["data_len"]; offset += 32 / 8 + data_len; let next_dpapi_structure = vec![("sign_len", "u32")]; dpapi_header.extend(common::parse( &dpapi_blob_data[offset..], &next_dpapi_structure, "little", )?); let sign_len = dpapi_header["sign_len"]; offset += 32 / 8 + sign_len; Ok(DPAPIBlobHeader { header_size: (32 * 13 + 128 * 2) / 8, blob_size: offset, version: dpapi_header["version"], provider_id: dpapi_header["provider_id"], master_key_version: dpapi_header["master_key_version"], master_key_id: dpapi_header["master_key_id"], flags: dpapi_header["flags"], description_len, crypto_algorithm: dpapi_header["crypto_algorithm"], crypti_alg_len: dpapi_header["crypti_alg_len"], salt_len, hmac_key_len, hash_algorithm: dpapi_header["hash_algorithm"], hash_alg_len: dpapi_header["hash_alg_len"], hmac2_key_len, data_len, sign_len, }) } /// Convert &[u8] into &[u16] as vec fn utf8_to_utf16(byte_array: &[u8]) -> Option> { let mut utf16_vec = Vec::with_capacity(byte_array.len() / 2); for i in 0..utf16_vec.len() { let buff = byte_array[2 * i..=2 * i + 1].try_into().ok()?; utf16_vec[i] = u16::from_be_bytes(buff); // Big endian as to keep bit order } Some(utf16_vec) } ================================================ FILE: src/structures/dtb.rs ================================================ use crate::common::get_cstring; use crate::structures::common::{self, StructureError}; /// Struct to store DTB info #[derive(Debug, Default, Clone)] pub struct DTBHeader { pub total_size: usize, pub version: usize, pub cpu_id: usize, pub struct_offset: usize, pub strings_offset: usize, pub struct_size: usize, pub strings_size: usize, } /// Parse DTB header pub fn parse_dtb_header(dtb_data: &[u8]) -> Result { // Expected version numbers const EXPECTED_VERSION: usize = 17; const EXPECTED_COMPAT_VERSION: usize = 16; const STRUCT_ALIGNMENT: usize = 4; const MEM_RESERVATION_ALIGNMENT: usize = 8; let dtb_structure = vec![ ("magic", "u32"), ("total_size", "u32"), ("dt_struct_offset", "u32"), ("dt_strings_offset", "u32"), ("mem_reservation_block_offset", "u32"), ("version", "u32"), ("min_compatible_version", "u32"), ("cpu_id", "u32"), ("dt_strings_size", "u32"), ("dt_struct_size", "u32"), ]; let dtb_structure_size = common::size(&dtb_structure); // Parse the header if let Ok(dtb_header) = common::parse(dtb_data, &dtb_structure, "big") { // Check the reported versioning if dtb_header["version"] == EXPECTED_VERSION && dtb_header["min_compatible_version"] == EXPECTED_COMPAT_VERSION { // Check required byte alignments for the specified offsets if (dtb_header["dt_struct_offset"] & STRUCT_ALIGNMENT) == 0 && (dtb_header["mem_reservation_block_offset"] % MEM_RESERVATION_ALIGNMENT) == 0 { // All offsets must start after the header structure if dtb_header["dt_struct_offset"] >= dtb_structure_size && dtb_header["dt_strings_offset"] >= dtb_structure_size && dtb_header["mem_reservation_block_offset"] >= dtb_structure_size { return Ok(DTBHeader { total_size: dtb_header["total_size"], version: dtb_header["version"], cpu_id: dtb_header["cpu_id"], struct_offset: dtb_header["dt_struct_offset"], strings_offset: dtb_header["dt_strings_offset"], struct_size: dtb_header["dt_struct_size"], strings_size: dtb_header["dt_strings_size"], }); } } } } Err(StructureError) } /// Describes a DTB node entry #[derive(Debug, Default, Clone)] pub struct DTBNode { pub begin: bool, pub end: bool, pub eof: bool, pub nop: bool, pub property: bool, pub name: String, pub data: Vec, pub total_size: usize, } /// Parse a DTB node from the DTB data structure pub fn parse_dtb_node(dtb_header: &DTBHeader, dtb_data: &[u8], node_offset: usize) -> DTBNode { const FDT_BEGIN_NODE: usize = 1; const FDT_END_NODE: usize = 2; const FDT_PROP: usize = 3; const FDT_NOP: usize = 4; const FDT_END: usize = 9; let node_token = vec![("id", "u32")]; let node_property = vec![("data_len", "u32"), ("name_offset", "u32")]; let mut node = DTBNode { ..Default::default() }; if let Some(node_data) = dtb_data.get(node_offset..) { if let Ok(token) = common::parse(node_data, &node_token, "big") { // Set total node size to the size of the token entry node.total_size = common::size(&node_token); if token["id"] == FDT_END_NODE { node.end = true; } else if token["id"] == FDT_NOP { node.nop = true; } else if token["id"] == FDT_END { node.eof = true; // All other node types must include additional data, so the available data must be greater than just the token entry size } else if node_data.len() > node.total_size { if token["id"] == FDT_BEGIN_NODE { // Begin nodes are immediately followed by a NULL-terminated name, padded to a 4-byte boundary if necessary node.begin = true; node.name = get_cstring(&node_data[node.total_size..]); node.total_size += dtb_aligned(node.name.len() + 1); } else if token["id"] == FDT_PROP { // Property tokens are followed by a property structure if let Ok(property) = common::parse(&node_data[node.total_size..], &node_property, "big") { // Update the total node size to include the property structure node.total_size += common::size(&node_property); // The property's data will immediately follow the property structure; property data may be NULL-padded for alignment if let Some(property_data) = node_data.get(node.total_size..node.total_size + property["data_len"]) { node.data = property_data.to_vec(); node.total_size += dtb_aligned(node.data.len()); // Get the property name from the DTB strings table if let Some(property_name_data) = dtb_data.get(dtb_header.strings_offset + property["name_offset"]..) { node.name = get_cstring(property_name_data); if !node.name.is_empty() { node.property = true; } } } } } } } } node } /// DTB entries must be aligned to 4-byte boundaries fn dtb_aligned(len: usize) -> usize { const ALIGNMENT: usize = 4; let rem = len % ALIGNMENT; if rem == 0 { len } else { len + (ALIGNMENT - rem) } } ================================================ FILE: src/structures/dxbc.rs ================================================ use crate::structures::common::{self, StructureError}; #[derive(Debug, Default, Clone)] pub struct DXBCHeader { pub size: usize, pub chunk_ids: Vec<[u8; 4]>, } // http://timjones.io/blog/archive/2015/09/02/parsing-direct3d-shader-bytecode pub fn parse_dxbc_header(data: &[u8]) -> Result { let dxbc_header_structure = vec![ ("magic", "u32"), ("signature_p1", "u64"), ("signature_p2", "u64"), ("one", "u32"), ("total_size", "u32"), ("chunk_count", "u32"), ]; // Parse the header if let Ok(header) = common::parse(data, &dxbc_header_structure, "little") { if header["one"] != 1 { return Err(StructureError); } // Sanity check: There are at least 14 known chunks, but most likely no more than 32. // Prevents the for loop from spiraling into an OOM on the offchance that both the magic and "one" check pass on garbage data if header["chunk_count"] > 32 { return Err(StructureError); } let header_end = common::size(&dxbc_header_structure); let mut chunk_ids = vec![]; for i in 0..header["chunk_count"] { let offset_data = data .get((header_end + i * 4)..(header_end + i * 4) + 4) .ok_or(StructureError)?; let offset = u32::from_le_bytes(offset_data.try_into().unwrap()) as usize; chunk_ids.push( data.get(offset..offset + 4) .ok_or(StructureError)? .try_into() .unwrap(), ); } return Ok(DXBCHeader { size: header["total_size"], chunk_ids, }); } Err(StructureError) } ================================================ FILE: src/structures/efigpt.rs ================================================ use crate::common::{crc32, is_offset_safe}; use crate::structures::common::{self, StructureError}; const BLOCK_SIZE: usize = 512; /// Struct to store EFI GPT header info #[derive(Debug, Default, Clone)] pub struct EFIGPTHeader { pub total_size: usize, } /// Parses an EFI GPT header pub fn parse_efigpt_header(efi_data: &[u8]) -> Result { const EXPTECTED_REVISION: usize = 0x00010000; // https://uefi.org/sites/default/files/resources/UEFI_Spec_2_10_Aug29.pdf, p.116 let efi_gpt_structure = vec![ ("magic", "u64"), ("revision", "u32"), ("header_size", "u32"), ("header_crc", "u32"), ("reserved", "u32"), ("my_lba", "u64"), ("alternate_lba", "u64"), ("first_usable_lba", "u64"), ("last_usable_lba", "u64"), ("disk_guid_p1", "u64"), ("disk_guid_p2", "u64"), ("partition_entry_lba", "u64"), ("partition_entry_count", "u32"), ("partition_entry_size", "u32"), ("partition_entries_crc", "u32"), ]; let mut result = EFIGPTHeader { ..Default::default() }; // EFI GPT structure starts at the second block (first block is MBR) if let Some(gpt_data) = efi_data.get(BLOCK_SIZE..) { // Parse the EFI GPT structure if let Ok(gpt_header) = common::parse(gpt_data, &efi_gpt_structure, "little") { // Make sure the reserved field is NULL if gpt_header["reserved"] == 0 { // Make sure the revision field is the expected valid if gpt_header["revision"] == EXPTECTED_REVISION { // Calculate the start and end offsets of the partition entries let partition_entries_start: usize = lba_to_offset(gpt_header["partition_entry_lba"]); let partition_entries_end: usize = partition_entries_start + (gpt_header["partition_entry_count"] * gpt_header["partition_entry_size"]); // Get the partition entires if let Some(partition_entries_data) = efi_data.get(partition_entries_start..partition_entries_end) { // Validate the partition entries' CRC if crc32(partition_entries_data) == (gpt_header["partition_entries_crc"] as u32) { let mut next_partition_offset = 0; let mut previous_partition_offset = None; let available_data = partition_entries_data.len(); // Loop through all partition entries while is_offset_safe( available_data, next_partition_offset, previous_partition_offset, ) { if let Some(partition) = parse_gpt_partition_entry( &partition_entries_data[next_partition_offset..], ) { // EOF is the end of the farthest away partition if partition.start_offset < partition.end_offset && partition.end_offset > result.total_size { result.total_size = partition.end_offset; } } previous_partition_offset = Some(next_partition_offset); next_partition_offset += gpt_header["partition_entry_size"]; } if result.total_size > 0 { return Ok(result); } } } } } } } Err(StructureError) } #[derive(Debug, Default, Clone)] struct GPTPartitionEntry { pub end_offset: usize, pub start_offset: usize, } /// Parse a GPT partition entry fn parse_gpt_partition_entry(entry_data: &[u8]) -> Option { let entry_structure = vec![ ("type_guid_p1", "u64"), ("type_guid_p2", "u64"), ("partition_guid_p1", "u64"), ("partition_guid_p2", "u64"), ("starting_lba", "u64"), ("ending_lba", "u64"), ("attributes", "u64"), ]; let mut result = GPTPartitionEntry { ..Default::default() }; if let Ok(entry_header) = common::parse(entry_data, &entry_structure, "little") { // GUID types of NULL can be ignored if entry_header["type_guid_p1"] != 0 && entry_header["type_guid_p2"] != 0 { result.start_offset = lba_to_offset(entry_header["starting_lba"]); result.end_offset = lba_to_offset(entry_header["ending_lba"]); return Some(result); } } None } // Convert LBA to offset fn lba_to_offset(lba: usize) -> usize { lba * BLOCK_SIZE } ================================================ FILE: src/structures/elf.rs ================================================ use crate::structures::common::{self, StructureError}; use std::collections::HashMap; /// Struct to store some useful ELF info #[derive(Debug, Default, Clone)] pub struct ELFHeader { pub class: String, pub osabi: String, pub machine: String, pub exe_type: String, pub endianness: String, } /// Partially parses an ELF header pub fn parse_elf_header(elf_data: &[u8]) -> Result { const ELF_INFO_STRUCT_SIZE: usize = 8; const ELF_IDENT_STRUCT_SIZE: usize = 16; const EXPECTED_VERSION: usize = 1; let elf_ident_structure = vec![ ("magic", "u32"), ("class", "u8"), ("endianness", "u8"), ("version", "u8"), ("osabi", "u8"), ("abiversion", "u8"), ("padding_1", "u32"), ("padding_2", "u24"), ]; // Just enough of the ELF structure to grab some useful info let elf_info_structure = vec![("type", "u16"), ("machine", "u16"), ("version", "u32")]; let elf_classes = HashMap::from([(1, 32), (2, 64)]); let elf_endianness = HashMap::from([(1, "little"), (2, "big")]); let elf_osabi = HashMap::from([ (0, "System-V (Unix)"), (1, "HP-UX"), (2, "NetBSD"), (3, "Linux"), (4, "GNU Hurd"), (5, "86Open"), (6, "Solaris"), (7, "AIX"), (8, "IRIX"), (9, "FreeBSD"), (10, "Tru64"), (11, "Novell Modesto"), (12, "OpenBSD"), (13, "OpenVMS"), (14, "NonStop Kernel"), (15, "AROS"), (16, "FenixOS"), (17, "Nuxi CloudABI"), (18, "OpenVOS"), (97, "ARM ABI"), (102, "Cell LV2"), (202, "Cafe OS"), (255, "embedded"), ]); let elf_types = HashMap::from([ (0, "no file type"), (1, "relocatable"), (2, "executable"), (3, "shared object"), (4, "core file"), ]); let elf_machines = HashMap::from([ (0, "no machine"), (1, "AT&T WE 32100"), (2, "SPARC"), (3, "x86"), (4, "Motorola 68k"), (5, "Motorola 88k"), (6, "Intel MCU"), (7, "Intel 80860"), (8, "MIPS"), (9, "IBM System/370"), (10, "MIPS RS3000"), (11, "RS6000"), (15, "HP PA-RISC"), (16, "nCUBE"), (17, "Fujitsu VPP500"), (18, "SPARC32PLUS"), (19, "Intel 80960"), (20, "PowerPC"), (21, "PowerPC 64-bit"), (22, "S390"), (23, "IBM SPU/SPC"), (24, "cisco SVIP"), (25, "cisco 7200"), (36, "NEC V800"), (37, "Fujitsu FR20"), (38, "TRW RH-32"), (39, "Motorola RCE"), (40, "ARM"), (41, "Digital Alpha"), (42, "SuperH"), (43, "SPARCv9"), (44, "Siemens TriCore embedded processor"), (45, "Argonaut RISC Core"), (46, "Hitachi H8/300"), (47, "Hitachi H8/300H"), (48, "Hitachi H8S"), (49, "Hitachi H8/500"), (50, "IA-64"), (51, "Stanford MIPS-X"), (52, "Motorola ColdFire"), (53, "Motorola M68HC12"), (54, "Fujitsu MMA Multimedia Accelerator"), (55, "Siemens PCP"), (56, "Sony nCPU embedded RISC processor"), (57, "Denso NDR1 microprocessor"), (58, "Motorola StarCore"), (59, "Toyota ME16"), (60, "STMicroelectronics ST100"), (61, "Advanced Logic TinyJ embedded processor"), (62, "AMD X86-64"), (63, "Sony DSP processor"), (64, "PDP-10"), (65, "PDP-11"), (66, "Siemens FX66"), (67, "STMicroelectronics ST9+"), (68, "STMicroelectronics ST7"), (69, "Motorola MC68HC16"), (70, "Motorola MC68HC11"), (71, "Motorola MC68HC08"), (72, "Motorola MC68HC05"), (73, "Silicon Graphics SVx"), (74, "STMicroelectonrics ST19"), (75, "Digital VAX"), (76, "Axis Communications 32-bit CPU"), (77, "Infineon Technologies 32-bit CPU"), (78, "Element 14 64-bit DSP"), (79, "LSI Logic 16-bit DSP"), (80, "MMIX"), (81, "Harvard machine-independent"), (82, "SiTera Prism"), (83, "Atmel AVR 8-bit"), (84, "Fujitsu FR30"), (85, "Mitsubishi D10V"), (86, "Mitsubishi D30V"), (87, "NEC v850"), (88, "Renesas M32R"), (89, "Matsushita MN10300"), (90, "Matsushita MN10200"), (91, "picoJava"), (92, "OpenRISC"), (93, "Synopsys ARCompact ARC700 cores"), (94, "Tensilica Xtensa"), (95, "Alphamosaic VideoCore"), (96, "Thompson Multimedia"), (97, "NatSemi 32k"), (98, "Tenor Network TPC"), (99, "Trebia SNP 1000"), (100, "STMicroelectronics ST200"), (101, "Ubicom IP2022"), (102, "MAX Processor"), (103, "NatSemi CompactRISC"), (104, "Fujitsu F2MC16"), (105, "TI msp430"), (106, "Analog Devices Blackfin"), (107, "S1C33 Family of Seiko Epson"), (108, "Sharp embedded"), (109, "Arca RISC"), (110, "PKU-Unity Ltd."), (111, "eXcess: 16/32/64-bit"), (112, "Icera Deep Execution Processor"), (113, "Altera Nios II"), (114, "NatSemi CRX"), (115, "Motorola XGATE"), (116, "Infineon C16x/XC16x"), (117, "Renesas M16C series"), (118, "Microchip dsPIC30F"), (119, "Freescale RISC core"), (120, "Renesas M32C series"), (131, "Altium TSK3000 core"), (132, "Freescale RS08"), (134, "Cyan Technology eCOG2"), (135, "Sunplus S+core7 RISC"), (136, "New Japan Radio (NJR) 24-bit DSP"), (137, "Broadcom VideoCore III processor"), (138, "LatticeMico32"), (139, "Seiko Epson C17 family"), (140, "TMS320C6000"), (141, "TMS320C2000"), (142, "TMS320C55x"), (144, "TI Programmable Realtime Unit"), (160, "STMicroelectronics 64bit VLIW DSP"), (161, "Cypress M8C"), (162, "Renesas R32C series"), (163, "NXP TriMedia family"), (164, "Qualcomm DSP6"), (165, "Intel 8051 and variants"), (166, "STMicroelectronics STxP7x family"), (167, "Andes embedded RISC"), (168, "Cyan eCOG1X family"), (169, "Dallas MAXQ30"), (170, "New Japan Radio (NJR) 16-bit DSP"), (171, "M2000 Reconfigurable RISC"), (172, "Cray NV2 vector architecture"), (173, "Renesas RX family"), (174, "META"), (175, "MCST Elbrus e2k"), (176, "Cyan Technology eCOG16 family"), (177, "NatSemi CompactRISC"), (178, "Freescale Extended Time Processing Unit"), (179, "Infineon SLE9X"), (180, "Intel L1OM"), (181, "Intel K1OM"), (183, "ARM 64-bit"), (185, "Atmel 32-bit family"), (186, "STMicroeletronics STM8 8-bit"), (187, "Tilera TILE64"), (188, "Tilera TILEPro"), (189, "Xilinx MicroBlaze 32-bit RISC"), (190, "NVIDIA CUDA architecture"), (191, "Tilera TILE-Gx"), (195, "Synopsys ARCv2/HS3x/HS4x cores"), (197, "Renesas RL78 family"), (199, "Renesas 78K0R"), (200, "Freescale 56800EX"), (201, "Beyond BA1"), (202, "Beyond BA2"), (203, "XMOS xCORE"), (204, "Microchip 8-bit PIC(r)"), (210, "KM211 KM32"), (211, "KM211 KMX32"), (212, "KM211 KMX16"), (213, "KM211 KMX8"), (214, "KM211 KVARC"), (215, "Paneve CDP"), (216, "Cognitive Smart Memory"), (217, "iCelero CoolEngine"), (218, "Nanoradio Optimized RISC"), (219, "CSR Kalimba architecture family"), (220, "Zilog Z80"), (221, "Controls and Data Services VISIUMcore processor"), ( 222, "FTDI Chip FT32 high performance 32-bit RISC architecture", ), (223, "Moxie processor family"), (224, "AMD GPU architecture"), (243, "RISC-V"), (244, "Lanai 32-bit processor"), (245, "CEVA Processor Architecture Family"), (246, "CEVA X2 Processor Family"), (247, "Berkeley Packet Filter"), (248, "Graphcore Intelligent Processing Unit"), (249, "Imagination Technologies"), (250, "Netronome Flow Processor"), (251, "NEC Vector Engine"), (252, "C-SKY processor family"), (253, "Synopsys ARCv3 64-bit ISA/HS6x cores"), (254, "MOS Technology MCS 6502 processor"), (255, "Synopsys ARCv3 32-bit"), (256, "Kalray VLIW core of the MPPA family"), (257, "WDC 65C816"), (258, "LoongArch"), (259, "ChipON KungFu32"), ]); let mut elf_hdr_info = ELFHeader { ..Default::default() }; // Endianness doesn't matter here, and we don't know what the ELF's endianness is yet if let Ok(e_ident) = common::parse(elf_data, &elf_ident_structure, "little") { // Sanity check the e_ident fields if e_ident["padding_1"] == 0 && e_ident["padding_2"] == 0 && e_ident["version"] == EXPECTED_VERSION && elf_classes.contains_key(&e_ident["class"]) && elf_osabi.contains_key(&e_ident["osabi"]) && elf_endianness.contains_key(&e_ident["endianness"]) { // Set the ident info elf_hdr_info.class = elf_classes[&e_ident["class"]].to_string(); elf_hdr_info.osabi = elf_osabi[&e_ident["osabi"]].to_string(); elf_hdr_info.endianness = elf_endianness[&e_ident["endianness"]].to_string(); // The rest of the ELF info comes immediately after the ident structure let elf_info_start: usize = ELF_IDENT_STRUCT_SIZE; let elf_info_end: usize = elf_info_start + ELF_INFO_STRUCT_SIZE; if let Some(elf_info_raw) = elf_data.get(elf_info_start..elf_info_end) { // Parse the remaining info from the ELF header if let Ok(elf_info) = common::parse( elf_info_raw, &elf_info_structure, elf_endianness[&e_ident["endianness"]], ) { // Sanity check the remaining ELF header fields if elf_info["version"] == EXPECTED_VERSION && elf_types.contains_key(&elf_info["type"]) { // Set the ELF info fields elf_hdr_info.exe_type = elf_types[&elf_info["type"]].to_string(); elf_hdr_info.machine = elf_machines .get(&elf_info["machine"]) // Use 'Unknown' as a fallback for the machine type .unwrap_or(&"Unknown") .to_string(); return Ok(elf_hdr_info); } } } } } Err(StructureError) } ================================================ FILE: src/structures/ext.rs ================================================ use crate::structures::common::{self, StructureError}; use std::collections::HashMap; /// Expected size of an EXT superblock pub const SUPERBLOCK_SIZE: usize = 1024; /// Expected file offset of an EXT superblock pub const SUPERBLOCK_OFFSET: usize = 1024; /// Struct to store some useful EXT info #[derive(Debug, Default, Clone)] pub struct EXTHeader { pub os: String, pub block_size: usize, pub image_size: usize, pub blocks_count: usize, pub inodes_count: usize, pub free_blocks_count: usize, pub reserved_blocks_count: usize, } /// Partially parses an EXT superblock structure pub fn parse_ext_header(ext_data: &[u8]) -> Result { // Max value of the EXT log block size const MAX_BLOCK_LOG: usize = 2; // Parital superblock structure, just enough for validation and size calculation let ext_superblock_structure = vec![ ("inodes_count", "u32"), ("blocks_count", "u32"), ("reserved_blocks_count", "u32"), ("free_blocks_count", "u32"), ("free_inodes_count", "u32"), ("first_data_block", "u32"), ("log_block_size", "u32"), ("log_frag_size", "u32"), ("blocks_per_group", "u32"), ("frags_per_group", "u32"), ("inodes_per_group", "u32"), ("modification_time", "u32"), ("write_time", "u32"), ("mount_count", "u16"), ("max_mount_count", "u16"), ("magic", "u16"), ("state", "u16"), ("errors", "u16"), ("s_minor_rev_level", "u16"), ("last_check", "u32"), ("check_interval", "u32"), ("creator_os", "u32"), ("s_rev_level", "u32"), ("resuid", "u16"), ("resgid", "u16"), ]; let allowed_rev_levels: Vec = vec![0, 1]; let allowed_first_data_blocks: Vec = vec![0, 1]; let supported_os: HashMap = HashMap::from([ (0, "Linux"), (1, "GNU HURD"), (2, "MASIX"), (3, "FreeBSD"), (4, "Lites"), ]); let mut ext_header = EXTHeader { ..Default::default() }; // Sanity check the available data if ext_data.len() >= (SUPERBLOCK_OFFSET + SUPERBLOCK_SIZE) { // Parse the EXT superblock structure if let Ok(ext_superblock) = common::parse( &ext_data[SUPERBLOCK_OFFSET..], &ext_superblock_structure, "little", ) { // Sanity check the reported OS this EXT image was created on if supported_os.contains_key(&ext_superblock["creator_os"]) { // Sanity check the s_rev_level field if allowed_rev_levels.contains(&ext_superblock["s_rev_level"]) { // Sanity check the first_data_block field, which must be either 0 or 1 if allowed_first_data_blocks.contains(&ext_superblock["first_data_block"]) { // Santiy check the log_block_size if ext_superblock["log_block_size"] <= MAX_BLOCK_LOG { // Update the reported image info ext_header.blocks_count = ext_superblock["blocks_count"]; ext_header.inodes_count = ext_superblock["inodes_count"]; ext_header.block_size = 1024 << ext_superblock["log_block_size"]; ext_header.free_blocks_count = ext_superblock["free_blocks_count"]; ext_header.os = supported_os[&ext_superblock["creator_os"]].to_string(); ext_header.reserved_blocks_count = ext_superblock["reserved_blocks_count"]; ext_header.image_size = ext_header.block_size * ext_superblock["blocks_count"]; return Ok(ext_header); } } } } } } Err(StructureError) } ================================================ FILE: src/structures/fat.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store FAT header info #[derive(Debug, Default, Clone)] pub struct FATHeader { pub is_fat32: bool, pub total_size: usize, } /// Parses a FAT header pub fn parse_fat_header(fat_data: &[u8]) -> Result { // Number of FATs could technically be 1 or greater, but *should* be 2 const EXPECTED_FAT_COUNT: usize = 2; // http://elm-chan.org/docs/fat_e.html let fat_boot_sector_structure = vec![ ("opcode1", "u8"), ("opcode2", "u8"), ("opcode3", "u8"), ("oem_name", "u64"), ("bytes_per_sector", "u16"), ("sectors_per_cluster", "u8"), ("reserved_sectors", "u16"), ("fat_count", "u8"), ("root_entries_count_16", "u16"), ("total_sectors_16", "u16"), ("media_type", "u8"), ("fat_size_16", "u16"), ("sectors_per_track", "u16"), ("number_of_heads", "u16"), ("hidden_sectors", "u32"), ("total_sectors_32", "u32"), ]; // First opcode should be jump instruction, either EB or E9 let valid_opcode1: Vec = vec![0xEB, 0xE9]; // bytes_per_sector must be one of these values let valid_sector_sizes: Vec = vec![512, 1024, 2048, 4096]; // sectors_per_cluster must be one of these values let valid_sectors_per_cluster: Vec = vec![1, 2, 4, 8, 16, 32, 64, 128]; // media_type must be one of these values let valid_media_types: Vec = vec![0xF0, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE]; // Return value let mut result = FATHeader { ..Default::default() }; // Parse the boot sector header if let Ok(bs_header) = common::parse(fat_data, &fat_boot_sector_structure, "little") { // Sanity check the first opcode if valid_opcode1.contains(&bs_header["opcode1"]) { // Sanity check the reported sector size if valid_sector_sizes.contains(&bs_header["bytes_per_sector"]) { // Sanity check the reported sectors per cluster if valid_sectors_per_cluster.contains(&bs_header["sectors_per_cluster"]) { // Reserved sectors must be at least 1 if bs_header["reserved_sectors"] > 0 { // Sanity check the reported number of FATs if bs_header["fat_count"] == EXPECTED_FAT_COUNT { // Sanity check the reported media type if valid_media_types.contains(&bs_header["media_type"]) { // This field is set to 0 for FAT32, but populated by FAT12/16 result.is_fat32 = bs_header["fat_size_16"] == 0; // total_sectors_16 is used for FAT12/16 that have less than 0x10000 sectors if bs_header["total_sectors_16"] != 0 { result.total_size = bs_header["total_sectors_16"] * bs_header["bytes_per_sector"]; // Else, total_sectors_32 is used to define the number of sectors } else { result.total_size = bs_header["total_sectors_32"] * bs_header["bytes_per_sector"]; } // If both total_sectors_32 and total_sectors_16 is 0, this is not a valid FAT if result.total_size > 0 { return Ok(result); } } } } } } } } Err(StructureError) } ================================================ FILE: src/structures/gif.rs ================================================ use crate::common::is_offset_safe; use crate::structures::common::{self, StructureError}; /// Struct to store GIF header info #[derive(Debug, Default, Clone)] pub struct GIFHeader { pub size: usize, pub image_width: usize, pub image_height: usize, } /// Parses a GIF header pub fn parse_gif_header(gif_data: &[u8]) -> Result { let gif_header_structure = vec![ ("magic_p1", "u24"), ("magic_p2", "u24"), ("image_width", "u16"), ("image_height", "u16"), ("flags", "u8"), ("bg_color_index", "u8"), ("aspect_ratio", "u8"), ]; // Parse the header if let Ok(gif_header) = common::parse(gif_data, &gif_header_structure, "little") { // Parse the flags to determine if a global color table is included in the header let flags = parse_gif_flags(gif_header["flags"]); return Ok(GIFHeader { size: common::size(&gif_header_structure) + flags.color_table_size, image_width: gif_header["image_width"], image_height: gif_header["image_height"], }); } Err(StructureError) } /// Struct to store GIF flags info #[derive(Debug, Default, Clone)] pub struct GIFFlags { /// Actual size of the color table, in bytes pub color_table_size: usize, } /// Parses a GIF flag byte to determine the size of a color table, if any fn parse_gif_flags(flags: usize) -> GIFFlags { const HAS_COLOR_TABLE: usize = 0x80; const COLOR_TABLE_SIZE_MASK: usize = 0b111; let mut retval = GIFFlags { ..Default::default() }; if (flags & HAS_COLOR_TABLE) != 0 { let encoded_table_size = ((flags & COLOR_TABLE_SIZE_MASK) + 1) as u32; retval.color_table_size = 3 * usize::pow(2, encoded_table_size); } retval } /// Parses an image descriptor; returns the total size of the descriptor and following image data pub fn parse_gif_image_descriptor(gif_data: &[u8]) -> Result { const LZW_CODE_SIZE: usize = 1; let img_desc_structure = vec![ ("magic", "u8"), ("image_left", "u16"), ("image_top", "u16"), ("image_width", "u16"), ("image_height", "u16"), ("flags", "u8"), ]; // Parse the image descriptor header if let Ok(desc_header) = common::parse(gif_data, &img_desc_structure, "little") { // Parse the flags field to determine if a local color table follows the header let flags = parse_gif_flags(desc_header["flags"]); let mut total_size: usize = common::size(&img_desc_structure) + flags.color_table_size; // After the header and optional color table will be a single-byte value representing the minimum LZW code size. total_size += LZW_CODE_SIZE; // An unspecified number of data sub-blocks follow. if let Some(image_sub_blocks) = gif_data.get(total_size..) { // Parse all sub-blocks to determine the total size of sub-blocks if let Ok(sub_blocks_size) = parse_gif_sub_blocks(image_sub_blocks) { total_size += sub_blocks_size; return Ok(total_size); } } } Err(StructureError) } /// Parses all data sub blocks until a sub-block terminator byte is found. /// Returns the size, in bytes, of all sub-block data. fn parse_gif_sub_blocks(sub_block_data: &[u8]) -> Result { const SUB_BLOCK_TERMINATOR: u8 = 0; let available_data = sub_block_data.len(); let mut next_offset = 0; let mut previous_offset = None; // Sub-blocks are just while is_offset_safe(available_data, next_offset, previous_offset) { match sub_block_data.get(next_offset) { None => break, Some(sub_block_size) => { if *sub_block_size == SUB_BLOCK_TERMINATOR { return Ok(next_offset + 1); } else { previous_offset = Some(next_offset); next_offset += (*sub_block_size as usize) + 1; } } } } Err(StructureError) } /// Parses a GIF extension block, returns the size of the extension block, in bytes. pub fn parse_gif_extension(extension_data: &[u8]) -> Result { const PLAIN_TEXT: usize = 1; const APPLICATION: usize = 0xFF; const HEADER_SIZE: usize = 2; // Some extensions do not include the sub_block_offset field; // this field is always parsed here, but only used if applicable. let extension_structure = vec![ ("magic", "u8"), ("extension_type", "u8"), ("sub_block_offset", "u8"), ]; // Parse the extension header to get the extension sub-type if let Ok(extension_header) = common::parse(extension_data, &extension_structure, "little") { let ext_type = extension_header["extension_type"]; let mut sub_blocks_offset: usize = HEADER_SIZE; // These extensions have some extra data before the sub-blocks; all other extensions are just a 2-byte header followed by sub-blocks if ext_type == APPLICATION || ext_type == PLAIN_TEXT { sub_blocks_offset += extension_header["sub_block_offset"] + 1; } // Parse all sub-block data to determine the total size of this extension block if let Some(sub_block_data) = extension_data.get(sub_blocks_offset..) { if let Ok(sub_blocks_size) = parse_gif_sub_blocks(sub_block_data) { return Ok(sub_blocks_offset + sub_blocks_size); } } } Err(StructureError) } ================================================ FILE: src/structures/gzip.rs ================================================ use crate::common::get_cstring; use crate::structures::common::{self, StructureError}; use std::collections::HashMap; /// Struct to store useful Gzip header info #[derive(Debug, Clone, Default)] pub struct GzipHeader { pub os: String, pub size: usize, pub comment: String, pub timestamp: u32, pub original_name: String, } /// Parses a Gzip file header pub fn parse_gzip_header(header_data: &[u8]) -> Result { // Some expected constant values const CRC_SIZE: usize = 2; const NULL_BYTE_SIZE: usize = 1; const DEFLATE_COMPRESSION: usize = 8; const FLAG_CRC: usize = 0b0000_0010; const FLAG_EXTRA: usize = 0b0000_0100; const FLAG_NAME: usize = 0b0000_1000; const FLAG_COMMENT: usize = 0b0001_0000; const FLAG_RESERVED: usize = 0b1110_0000; let gzip_header_structure = vec![ ("magic", "u16"), ("compression_method", "u8"), ("flags", "u8"), ("timestamp", "u32"), ("extra_flags", "u8"), ("osid", "u8"), ]; let gzip_extra_header_structure = vec![("id", "u16"), ("extra_data_len", "u16")]; let known_os_ids: HashMap = HashMap::from([ (0, "FAT filesystem (MS-DOS, OS/2, NT/Win32"), (1, "Amiga"), (2, "VMS (or OpenVMS)"), (3, "Unix"), (4, "VM/CMS"), (5, "Atari TOS"), (6, "HPFS filesystem (OS/2, NT)"), (7, "Macintosh"), (8, "Z-System"), (9, "CP/M"), (10, "TOPS-20"), (11, "NTFS filesystem (NT)"), (12, "QDOS"), (13, "Acorn RISCOS"), (255, "unknown"), ]); let mut header_info = GzipHeader { ..Default::default() }; // End of the fixed-size portion of the gzip header header_info.size = common::size(&gzip_header_structure); // Parse the gzip header if let Ok(gzip_header) = common::parse(header_data, &gzip_header_structure, "little") { // Report the timestamp header_info.timestamp = gzip_header["timestamp"] as u32; // Sanity check; compression type should be deflate, reserved flag bits should not be set, OS ID should be a known value if (gzip_header["flags"] & FLAG_RESERVED) == 0 && gzip_header["compression_method"] == DEFLATE_COMPRESSION && known_os_ids.contains_key(&gzip_header["osid"]) { // Set the operating system string header_info.os = known_os_ids[&gzip_header["osid"]].to_string(); // Check if the optional "extra" data follows the standard Gzip header if (gzip_header["flags"] & FLAG_EXTRA) != 0 { // File offsets and sizes for parsing the extra header let extra_header_size = common::size(&gzip_extra_header_structure); let extra_header_start: usize = header_info.size; let extra_header_end: usize = extra_header_start + extra_header_size; match header_data.get(extra_header_start..extra_header_end) { None => { return Err(StructureError); } Some(extra_header_data) => { // Parse the extra header and update the header_info.size to include this data match common::parse( extra_header_data, &gzip_extra_header_structure, "little", ) { Err(e) => { return Err(e); } Ok(extra_header) => { header_info.size += extra_header_size + extra_header["extra_data_len"]; } } } } } // If the NULL-terminated original file name is included, it will be next if (gzip_header["flags"] & FLAG_NAME) != 0 { match header_data.get(header_info.size..) { None => { return Err(StructureError); } Some(file_name_bytes) => { header_info.original_name = get_cstring(file_name_bytes); // The value returned by get_cstring does not include the terminating NULL byte header_info.size += header_info.original_name.len() + NULL_BYTE_SIZE; } } } // If a NULL-terminated comment is included, it will be next if (gzip_header["flags"] & FLAG_COMMENT) != 0 { match header_data.get(header_info.size..) { None => { return Err(StructureError); } Some(comment_bytes) => { header_info.comment = get_cstring(comment_bytes); // The value returned by get_cstring does not include the terminating NULL byte header_info.size += header_info.comment.len() + NULL_BYTE_SIZE; } } } // Finally, a checksum field may be included if (gzip_header["flags"] & FLAG_CRC) != 0 { header_info.size += CRC_SIZE; } // Deflate data should start at header_info.size; make sure this offset is sane if header_data.len() >= header_info.size { return Ok(header_info); } } } Err(StructureError) } ================================================ FILE: src/structures/iso9660.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store useful ISO info #[derive(Debug, Default, Clone)] pub struct ISOHeader { pub image_size: usize, } /// Partially parses an ISO header pub fn parse_iso_header(iso_data: &[u8]) -> Result { // Offset from the beginning of the ISO image to the start of iso_structure const ISO_STRUCT_START: usize = 32840; // Partial ISO header structure, enough to reasonably validate that this is not a false positive and to calculate the total ISO size let iso_structure = vec![ ("unused1", "u64"), ("volume_size_lsb", "u32"), ("volume_size_msb", "u32"), ("unused2", "u64"), ("unused3", "u64"), ("unused4", "u64"), ("unused5", "u64"), ("set_size_lsb", "u16"), ("set_size_msb", "u16"), ("sequence_number_lsb", "u16"), ("sequence_number_msb", "u16"), ("block_size_lsb", "u16"), ("block_size_msb", "u16"), ("path_table_size_lsb", "u32"), ("path_table_size_msb", "u32"), ]; let mut iso_info = ISOHeader { ..Default::default() }; if let Some(iso_header_data) = iso_data.get(ISO_STRUCT_START..) { // Parse the ISO header if let Ok(iso_header) = common::parse(iso_header_data, &iso_structure, "little") { // Make sure all the unused fields are, in fact, unused if iso_header["unused1"] == 0 && iso_header["unused2"] == 0 && iso_header["unused3"] == 0 && iso_header["unused4"] == 0 && iso_header["unused5"] == 0 { /* * Make sure all the identical, but byte-swapped, fields agree. * NOTE: The to_be() conversions probably won't work on big-endian hosts. */ if iso_header["set_size_lsb"] == (iso_header["set_size_msb"] as u16).to_be() as usize && iso_header["block_size_lsb"] == (iso_header["block_size_msb"] as u16).to_be() as usize && iso_header["volume_size_lsb"] == (iso_header["volume_size_msb"] as u32).to_be() as usize && iso_header["sequence_number_lsb"] == (iso_header["sequence_number_msb"] as u16).to_be() as usize && iso_header["path_table_size_lsb"] == (iso_header["path_table_size_msb"] as u32).to_be() as usize { iso_info.image_size = iso_header["volume_size_lsb"] * iso_header["block_size_lsb"]; return Ok(iso_info); } } } } Err(StructureError) } ================================================ FILE: src/structures/jboot.rs ================================================ use crate::common::{crc32, get_cstring}; use crate::structures::common::{self, StructureError}; use std::collections::HashMap; /// Struct to store JBOOT ARM firmware image info #[derive(Debug, Default, Clone)] pub struct JBOOTArmHeader { pub header_size: usize, pub data_size: usize, pub data_offset: usize, pub erase_offset: usize, pub erase_size: usize, pub rom_id: String, } /// Parses a JBOOT ARM image header pub fn parse_jboot_arm_header(jboot_data: &[u8]) -> Result { // Structure starts after 12-byte ROM ID const STRUCTURE_OFFSET: usize = 12; // Some expected header values const LPVS_VALUE: usize = 1; const MBZ_VALUE: usize = 0; const HEADER_ID_VALUE: usize = 0x4842; const HEADER_MAX_VERSION_VALUE: usize = 4; let arm_structure = vec![ ("drange", "u16"), ("image_checksum", "u16"), ("block_size", "u32"), ("reserved2", "u32"), ("reserved3", "u16"), ("lpvs", "u8"), ("mbz", "u8"), ("timestamp", "u32"), ("erase_start", "u32"), ("erase_size", "u32"), ("data_start", "u32"), ("data_size", "u32"), ("reserved4", "u32"), ("reserved5", "u32"), ("reserved6", "u32"), ("reserved7", "u32"), ("header_id", "u16"), ("header_version", "u16"), ("reserved8", "u16"), ("section_id", "u8"), ("image_info_type", "u8"), ("image_info_offset", "u32"), ("family", "u16"), ("header_checksum", "u16"), ]; let structure_size: usize = common::size(&arm_structure); let header_size: usize = structure_size + STRUCTURE_OFFSET; if let Some(header_data) = jboot_data.get(STRUCTURE_OFFSET..) { // Parse the header structure if let Ok(arm_header) = common::parse(header_data, &arm_structure, "little") { // Make sure the reserved fields are NULL if arm_header["reserved2"] == 0 && arm_header["reserved3"] == 0 && arm_header["reserved4"] == 0 && arm_header["reserved5"] == 0 && arm_header["reserved6"] == 0 && arm_header["reserved7"] == 0 && arm_header["reserved8"] == 0 { // Sanity check expected header values if arm_header["lpvs"] == LPVS_VALUE && arm_header["mbz"] == MBZ_VALUE && arm_header["header_id"] == HEADER_ID_VALUE && arm_header["header_version"] <= HEADER_MAX_VERSION_VALUE { return Ok(JBOOTArmHeader { header_size, rom_id: get_cstring(&jboot_data[0..STRUCTURE_OFFSET]), data_size: arm_header["data_size"], data_offset: arm_header["data_start"], erase_offset: arm_header["erase_start"], erase_size: arm_header["erase_size"], }); } } } } Err(StructureError) } /// Stores info about JBOOT STAG headers #[derive(Debug, Default, Clone)] pub struct JBOOTStagHeader { pub header_size: usize, pub image_size: usize, pub is_factory_image: bool, pub is_sysupgrade_image: bool, } /// Parses a JBOOT STAG header pub fn parse_jboot_stag_header(jboot_data: &[u8]) -> Result { // cmark value for factory images; for system upgrade images, cmark must equal id const FACTORY_IMAGE_TYPE: usize = 0xFF; let stag_structure = vec![ ("cmark", "u8"), ("id", "u8"), ("magic", "u16"), ("timestamp", "u32"), ("image_size", "u32"), ("image_checksum", "u16"), ("header_checksum", "u16"), ]; let mut result = JBOOTStagHeader { ..Default::default() }; // Parse the header structure if let Ok(stag_header) = common::parse(jboot_data, &stag_structure, "little") { result.header_size = common::size(&stag_structure); result.image_size = stag_header["image_size"]; if result.image_size > result.header_size { result.is_factory_image = stag_header["cmark"] == FACTORY_IMAGE_TYPE; result.is_sysupgrade_image = stag_header["cmark"] == stag_header["id"]; if result.is_factory_image || result.is_sysupgrade_image { return Ok(result); } } } Err(StructureError) } #[derive(Default, Debug, Clone)] pub struct JBOOTSchHeader { pub header_size: usize, pub compression: String, pub kernel_size: usize, pub kernel_entry_point: usize, pub kernel_checksum: usize, } /// Parses a JBOOT SCH2 header pub fn parse_jboot_sch2_header(jboot_data: &[u8]) -> Result { const VERSION_VALUE: usize = 2; let sch2_structure = vec![ ("magic", "u16"), ("compression_type", "u8"), ("version", "u8"), ("ram_entry_address", "u32"), ("kernel_image_size", "u32"), ("kernel_image_crc", "u32"), ("ram_start_address", "u32"), ("rootfs_flash_address", "u32"), ("rootfs_size", "u32"), ("rootfs_crc", "u32"), ("header_crc", "u32"), ("header_size", "u16"), ("cmd_line_size", "u16"), ]; let compression_types: HashMap = HashMap::from([(0, "none"), (1, "jz"), (2, "gzip"), (3, "lzma")]); let mut result = JBOOTSchHeader { header_size: common::size(&sch2_structure), ..Default::default() }; if let Ok(sch2_header) = common::parse(jboot_data, &sch2_structure, "little") { // Sanity check some header fields if sch2_header["version"] == VERSION_VALUE && sch2_header["header_size"] == result.header_size && compression_types.contains_key(&sch2_header["compression_type"]) { // Validate the header checksum if let Some(header_bytes) = jboot_data.get(0..sch2_header["header_size"]) { if sch2_header_crc(header_bytes) == sch2_header["header_crc"] { result.compression = compression_types[&sch2_header["compression_type"]].to_string(); result.kernel_checksum = sch2_header["kernel_image_crc"]; result.kernel_size = sch2_header["kernel_image_size"]; result.kernel_entry_point = sch2_header["ram_entry_address"]; return Ok(result); } } } } Err(StructureError) } /// Calculate a JBOOT SCH2 header CRC fn sch2_header_crc(sch2_header_bytes: &[u8]) -> usize { // Start and end offsets of the header CRC field const HEADER_CRC_START: usize = 32; const HEADER_CRC_END: usize = 36; let mut crc: usize = 0; if sch2_header_bytes.len() > HEADER_CRC_END { let mut crc_data: Vec = sch2_header_bytes.to_vec(); // Header CRC field has to be NULL'd out for crc_byte in crc_data .iter_mut() .take(HEADER_CRC_END) .skip(HEADER_CRC_START) { *crc_byte = 0; } crc = crc32(&crc_data) as usize; } crc } ================================================ FILE: src/structures/jffs2.rs ================================================ use crate::structures::common::{self, StructureError}; use crc32_v2; /// JFFS2 node header size pub const JFFS2_NODE_STRUCT_SIZE: usize = 12; /// Structure for storing useful JFFS node info #[derive(Debug, Default, Clone)] pub struct JFFS2Node { pub size: usize, pub node_type: u16, pub endianness: String, } /// Parse a JFFS2 node header pub fn parse_jffs2_node_header(node_data: &[u8]) -> Result { // Expected JFFS2 node magic const JFFS2_CORRECT_MAGIC: usize = 0x1985; // Number of header bytes over which the header CRC is calculated const JFFS2_HEADER_CRC_SIZE: usize = 8; let jffs2_node_structure = vec![ ("magic", "u16"), ("type", "u16"), ("size", "u32"), ("crc", "u32"), ]; let mut node = JFFS2Node { ..Default::default() }; // Try little endian first node.endianness = "little".to_string(); // Parse the node header if let Ok(mut node_header) = common::parse(node_data, &jffs2_node_structure, &node.endianness) { // If the node header magic isn't correct, try parsing the header as big endian if node_header["magic"] != JFFS2_CORRECT_MAGIC { node.endianness = "big".to_string(); match common::parse(node_data, &jffs2_node_structure, &node.endianness) { Err(_) => { return Err(StructureError); } Ok(node_header_be) => { node_header = node_header_be.clone(); } } } // Node magic must be correct at this point, else this node is invalid if node_header["magic"] == JFFS2_CORRECT_MAGIC { // Calculate the node header CRC let node_calculated_crc = jffs2_node_crc(&node_data[0..JFFS2_HEADER_CRC_SIZE]); // Validate the node header CRC if node_calculated_crc == node_header["crc"] { node.size = node_header["size"]; node.node_type = node_header["type"] as u16; return Ok(node); } } } Err(StructureError) } /// CRC calculation for JFFS fn jffs2_node_crc(file_data: &[u8]) -> usize { (crc32_v2::crc32(0xFFFFFFFF, file_data) ^ 0xFFFFFFFF) as usize } ================================================ FILE: src/structures/linux.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store linux ARM64 boot image header info #[derive(Debug, Default, Clone)] pub struct LinuxARM64BootHeader { pub header_size: usize, pub image_size: usize, pub endianness: String, } /// Struct to store Linux ARM zImage info #[derive(Debug, Default, Clone)] pub struct LinuxARMzImageHeader { pub endianness: String, } /// Parses a Linux ARM zImage header pub fn parse_linux_arm_zimage_header( zimage_data: &[u8], ) -> Result { const NOP_LE: usize = 0xE1A00000; const NOP_BE: usize = 0x0000A0E1; let zimage_structure = vec![ ("nop1", "u32"), ("nop2", "u32"), ("nop3", "u32"), ("nop4", "u32"), ("nop5", "u32"), ("nop6", "u32"), ("nop7", "u32"), ("nop8", "u32"), ]; if let Ok(zimage_nops) = common::parse(zimage_data, &zimage_structure, "little") { if zimage_nops["nop1"] == zimage_nops["nop2"] && zimage_nops["nop1"] == zimage_nops["nop3"] && zimage_nops["nop1"] == zimage_nops["nop4"] && zimage_nops["nop1"] == zimage_nops["nop5"] && zimage_nops["nop1"] == zimage_nops["nop6"] && zimage_nops["nop1"] == zimage_nops["nop7"] && zimage_nops["nop1"] == zimage_nops["nop8"] { if zimage_nops["nop1"] == NOP_LE { return Ok(LinuxARMzImageHeader { endianness: "little".to_string(), }); } if zimage_nops["nop1"] == NOP_BE { return Ok(LinuxARMzImageHeader { endianness: "big".to_string(), }); } } } Err(StructureError) } /// Parses a linux ARM64 boot header pub fn parse_linux_arm64_boot_image_header( img_data: &[u8], ) -> Result { const PE: &[u8] = b"PE"; const FLAGS_RESERVED_MASK: usize = 0b11111111_11111111_11111111_11111111_11111111_11111111_11111111_11110000; const FLAGS_ENDIAN_MASK: usize = 1; const BIG_ENDIAN: usize = 1; // https://www.kernel.org/doc/Documentation/arm64/booting.txt let boot_img_structure = vec![ ("code0", "u32"), ("code1", "u32"), ("image_load_offset", "u64"), ("image_size", "u64"), ("flags", "u64"), ("reserved1", "u64"), ("reserved2", "u64"), ("reserved3", "u64"), ("magic", "u32"), ("pe_offset", "u32"), ]; let mut result = LinuxARM64BootHeader { ..Default::default() }; // Parse the header if let Ok(img_header) = common::parse(img_data, &boot_img_structure, "little") { // Make sure the reserved fields are not set if img_header["reserved1"] == 0 && img_header["reserved2"] == 0 && img_header["reserved3"] == 0 { // Start and end of PE signature let pe_start = img_header["pe_offset"]; let pe_end = pe_start + PE.len(); // Get the data pointed to by the pe_offset header field if let Some(pe_data) = img_data.get(pe_start..pe_end) { // There should be a PE header here if pe_data == PE { // Make sure the reserved flag bits are not set if (img_header["flags"] & FLAGS_RESERVED_MASK) == 0 { // Determine the endianness from the flags field if (img_header["flags"] & FLAGS_ENDIAN_MASK) == BIG_ENDIAN { result.endianness = "big".to_string(); } else { result.endianness = "little".to_string(); } // Report the kernel image and header sizes result.image_size = img_header["image_size"]; result.header_size = common::size(&boot_img_structure); return Ok(result); } } } } } Err(StructureError) } ================================================ FILE: src/structures/logfs.rs ================================================ use crate::structures::common::{self, StructureError}; /// Offset of the LogFS magic bytes from the start of the file system pub const LOGFS_MAGIC_OFFSET: usize = 0x18; /// Struct to store LogFS info #[derive(Debug, Default, Clone)] pub struct LogFSSuperBlock { pub total_size: usize, } /// Parses a LogFS superblock pub fn parse_logfs_super_block(logfs_data: &[u8]) -> Result { //const LOGFS_CRC_START: usize = LOGFS_MAGIC_OFFSET + 12; //const LOGFS_CRC_END: usize = 256; let logfs_sb_structure = vec![ ("magic", "u64"), ("crc32", "u32"), ("ifile_levels", "u8"), ("iblock_levels", "u8"), ("data_levels", "u8"), ("segment_shift", "u8"), ("block_shift", "u8"), ("write_shift", "u8"), ("pad0", "u32"), ("pad1", "u16"), ("filesystem_size", "u64"), ("segment_size", "u32"), ("bad_seg_reserved", "u32"), ("feature_incompat", "u64"), ("feature_ro_compat", "u64"), ("feature_compat", "u64"), ("feature_flags", "u64"), ("root_reserve", "u64"), ("speed_reserve", "u64"), ]; if let Some(sb_struct_data) = logfs_data.get(LOGFS_MAGIC_OFFSET..) { if let Ok(super_block) = common::parse(sb_struct_data, &logfs_sb_structure, "big") { if super_block["pad0"] == 0 && super_block["pad1"] == 0 { return Ok(LogFSSuperBlock { total_size: super_block["filesystem_size"], }); } } } Err(StructureError) } ================================================ FILE: src/structures/luks.rs ================================================ use crate::common::get_cstring; use crate::structures::common::{self, StructureError}; /// Struct to store some useful LUKS info #[derive(Debug, Default, Clone)] pub struct LUKSHeader { pub version: usize, pub header_size: usize, pub hashfn: String, pub cipher_mode: String, pub cipher_algorithm: String, } /// Partially parses a LUKS header pub fn parse_luks_header(luks_data: &[u8]) -> Result { // Start and end offsets of the cipher algorithm string const CIPHER_ALGO_START: usize = 8; const CIPHER_ALGO_END: usize = 40; // Start and end offsets of the cipher mode string const CIPHER_MODE_START: usize = 40; const CIPHER_MODE_END: usize = 72; // Start and end offsets of the hash function string const HASHFN_START: usize = 72; const HASHFN_END: usize = 104; // Minimum LUKS2 header size (assuming no JSON data) const LUKS2_MIN_HEADER_SIZE: usize = 4032; // https://en.wikipedia.org/wiki/Linux_Unified_Key_Setup // https://vhs.codeberg.page/post/external-backup-drive-encryption/assets/luks2_doc_wip.pdf let luks_base_structure = vec![ ("magic_1", "u32"), ("magic_2", "u16"), ("version", "u16"), ("header_size", "u64"), // Only available in LUKS2 ]; let mut luks_hdr_info = LUKSHeader { ..Default::default() }; if let Ok(luks_base) = common::parse(luks_data, &luks_base_structure, "big") { luks_hdr_info.version = luks_base["version"]; // Both v1 and v2 include the hash function string at the same offset if let Some(hashfn_bytes) = luks_data.get(HASHFN_START..HASHFN_END) { luks_hdr_info.hashfn = get_cstring(hashfn_bytes); // Make sure there was actually a string at the expected hash function offset if !luks_hdr_info.hashfn.is_empty() { // Need to process v1 and v2 headers differently if luks_hdr_info.version == 1 { // Get the cipher algorithm string if let Some(cipher_algo_bytes) = luks_data.get(CIPHER_ALGO_START..CIPHER_ALGO_END) { luks_hdr_info.cipher_algorithm = get_cstring(cipher_algo_bytes); // Get the cipher mode string if let Some(cipher_mode_bytes) = luks_data.get(CIPHER_MODE_START..CIPHER_MODE_END) { luks_hdr_info.cipher_mode = get_cstring(cipher_mode_bytes); // Make sure there were valid strings specified for both cipher algo and cipher mode if !luks_hdr_info.cipher_mode.is_empty() && !luks_hdr_info.cipher_algorithm.is_empty() { return Ok(luks_hdr_info); } } } } else if luks_hdr_info.version == 2 { // v2 doesn't have the same string entries, but does include a header size luks_hdr_info.header_size = luks_base["header_size"]; // Sanity check the header size if luks_hdr_info.header_size > LUKS2_MIN_HEADER_SIZE && luks_hdr_info.header_size < luks_data.len() { return Ok(luks_hdr_info); } } } } } Err(StructureError) } ================================================ FILE: src/structures/lz4.rs ================================================ use crate::structures::common::{self, StructureError}; use xxhash_rust; /// Struct to store LZ4 file header info #[derive(Debug, Default, Clone)] pub struct LZ4FileHeader { pub header_size: usize, pub block_checksum_present: bool, pub content_checksum_present: bool, } /// Parse an LZ4 file header pub fn parse_lz4_file_header(lz4_data: &[u8]) -> Result { // Fixed size constants const MAGIC_SIZE: usize = 4; const LZ4_STRUCT_SIZE: usize = 6; const BD_RESERVED_MASK: usize = 0b10001111; const FLAGS_RESERVED_MASK: usize = 0b00000010; const FLAG_DICTIONARY_PRESENT: usize = 0b00000001; const FLAG_CONTENT_SIZE_PRESENT: usize = 0b00001000; const FLAG_BLOCK_CHECKSUM_PRESENT: usize = 0b00010000; const FLAG_CONTENT_CHECKSUM_PRESENT: usize = 0b00000100; const DICTIONARY_LEN: usize = 4; const CONTENT_SIZE_LEN: usize = 8; // Basic LZ4 header; optional fields and header CRC byte follow let lz4_structure = vec![("magic", "u32"), ("flags", "u8"), ("bd", "u8")]; let mut lz4_hdr_info = LZ4FileHeader { ..Default::default() }; // Parse the header if let Ok(lz4_header) = common::parse(lz4_data, &lz4_structure, "little") { // Make sure the reserved bits aren't set if (lz4_header["flags"] & FLAGS_RESERVED_MASK) == 0 && (lz4_header["bd"] & BD_RESERVED_MASK) == 0 { /* * Calculate the start and end of data used to calculate the header CRC. * CRC is calculated over the entire descriptor frame, including optional fields, * but does not include the magic bytes. */ let crc_data_start: usize = MAGIC_SIZE; let mut crc_data_end: usize = crc_data_start + (LZ4_STRUCT_SIZE - MAGIC_SIZE); // If the optional content size field is present, the CRC field is pushed back after the content size field if (lz4_header["flags"] & FLAG_CONTENT_SIZE_PRESENT) != 0 { crc_data_end += CONTENT_SIZE_LEN; } // If the optional dictionary ID field is present, the CRC field is pushed back after the dictionary ID field if (lz4_header["flags"] & FLAG_DICTIONARY_PRESENT) != 0 { crc_data_end += DICTIONARY_LEN; } // Get the data over which the CRC is calculated if let Some(crc_data) = lz4_data.get(crc_data_start..crc_data_end) { // Grab the header CRC value stored in the file header (one byte only) if let Some(actual_crc) = lz4_data.get(crc_data_end) { // Calculate the header CRC, which is the second byte of the xxh32 hash. It is calculated over the header, excluding the magic bytes. let calculated_crc: u8 = ((xxhash_rust::xxh32::xxh32(crc_data, 0) >> 8) & 0xFF) as u8; // Make sure the CRC's match if *actual_crc == calculated_crc { // Data blocks start immediately after the header checksum byte lz4_hdr_info.header_size = crc_data_end + 1; lz4_hdr_info.block_checksum_present = (lz4_header["flags"] & FLAG_BLOCK_CHECKSUM_PRESENT) != 0; lz4_hdr_info.content_checksum_present = (lz4_header["flags"] & FLAG_CONTENT_CHECKSUM_PRESENT) != 0; return Ok(lz4_hdr_info); } } } } } Err(StructureError) } /// Struct to store LZ4 block header info #[derive(Debug, Default, Clone)] pub struct LZ4BlockHeader { pub data_size: usize, pub header_size: usize, pub checksum_size: usize, pub last_block: bool, } /// Parse an LZ4 block header pub fn parse_lz4_block_header( lz4_block_data: &[u8], checksum_present: bool, ) -> Result { // Useful constants const SIZE_MASK: u32 = 0x7FFFFFFF; const END_MARKER: usize = 0; const CHECKSUM_SIZE: usize = 4; const BLOCK_STRUCT_SIZE: usize = 4; // Block headers are just a u32 size field let block_structure = vec![("block_size", "u32")]; let mut lz4_block = LZ4BlockHeader { ..Default::default() }; // Parse the block header if let Ok(block_header) = common::parse(lz4_block_data, &block_structure, "little") { // Header size is always 4 bytes lz4_block.header_size = BLOCK_STRUCT_SIZE; // If file size is 0, this is the end of the LZ4 data lz4_block.last_block = block_header["block_size"] == END_MARKER; // If a checksum is present, it will be an extra 4 bytes at the end of the block if checksum_present { lz4_block.checksum_size = CHECKSUM_SIZE; } // The high bit of the reported block size is not part of the actual block size lz4_block.data_size = ((block_header["block_size"] as u32) & SIZE_MASK) as usize; return Ok(lz4_block); } Err(StructureError) } ================================================ FILE: src/structures/lzfse.rs ================================================ use crate::structures::common::{StructureError, parse}; /// Struct to store LZFSE block info #[derive(Debug, Default, Clone)] pub struct LZFSEBlock { pub eof: bool, pub data_size: usize, pub header_size: usize, } /// Parse an LZFSE block header pub fn parse_lzfse_block_header(lzfse_data: &[u8]) -> Result { // LZFSE block types const ENDOFSTREAM: usize = 0x24787662; const UNCOMPRESSED: usize = 0x2d787662; const COMPRESSEDV1: usize = 0x31787662; const COMPRESSEDV2: usize = 0x32787662; const COMPRESSEDLZVN: usize = 0x6e787662; // Each block starts with a 4-byte magic identifier let block_type_structure = vec![("block_type", "u32")]; // Parse the block header if let Ok(block_type_header) = parse(lzfse_data, &block_type_structure, "little") { let block_type = block_type_header["block_type"]; // Block headers are different for different block types; process this block header accordingly if block_type == ENDOFSTREAM { return parse_endofstream_block_header(lzfse_data); } else if block_type == UNCOMPRESSED { return parse_uncompressed_block_header(lzfse_data); } else if block_type == COMPRESSEDV1 { return parse_compressedv1_block_header(lzfse_data); } else if block_type == COMPRESSEDV2 { return parse_compressedv2_block_header(lzfse_data); } else if block_type == COMPRESSEDLZVN { return parse_compressedlzvn_block_header(lzfse_data); } } Err(StructureError) } /// Parse an end-of-stream LZFSE block header fn parse_endofstream_block_header(_lzfse_data: &[u8]) -> Result { // This is easy; it's just the 4-byte magic bytes marking the end-of-stream Ok(LZFSEBlock { eof: true, data_size: 0, header_size: 4, }) } /// Parse an uncompressed LZFSE block header fn parse_uncompressed_block_header(lzfse_data: &[u8]) -> Result { const HEADER_SIZE: usize = 8; let block_structure = vec![("magic", "u32"), ("n_raw_bytes", "u32")]; if let Ok(header) = parse(lzfse_data, &block_structure, "little") { return Ok(LZFSEBlock { eof: false, data_size: header["n_raw_bytes"], header_size: HEADER_SIZE, }); } Err(StructureError) } /// Parse a compressed (version 1) LZFSE block header fn parse_compressedv1_block_header(lzfse_data: &[u8]) -> Result { const HEADER_SIZE: usize = 770; let block_structure = vec![ ("magic", "u32"), ("n_raw_bytes", "u32"), ("n_payload_bytes", "u32"), ("n_literals", "u32"), ("n_matches", "u32"), ("n_literal_payload_bytes", "u32"), ("n_lmd_payload_bytes", "u32"), ("literal_bits", "u32"), ("literal_state", "u64"), ("lmd_bits", "u32"), ("l_state", "u16"), ("m_state", "u16"), ("d_state", "u16"), // Frequency tables follow ]; if let Ok(header) = parse(lzfse_data, &block_structure, "little") { return Ok(LZFSEBlock { eof: false, data_size: header["n_literal_payload_bytes"] + header["n_lmd_payload_bytes"], header_size: HEADER_SIZE, }); } Err(StructureError) } /// Parse a compressed (version 2) LZFSE block header fn parse_compressedv2_block_header(lzfse_data: &[u8]) -> Result { const N_PAYLOAD_SHIFT: usize = 20; const LMD_PAYLOAD_SHIFT: usize = 40; const PAYLOAD_MASK: usize = 0b11111_11111_11111_11111; let block_structure = vec![ ("magic", "u32"), ("uncompressed_size", "u32"), ("packed_field_1", "u64"), ("packed_field_2", "u64"), ("header_size", "u32"), ("state_fields", "u32"), // Variable length header field follows ]; if let Ok(block_header) = parse(lzfse_data, &block_structure, "little") { let n_lmd_payload_bytes = (block_header["packed_field_2"] >> LMD_PAYLOAD_SHIFT) & PAYLOAD_MASK; let n_literal_payload_bytes = (block_header["packed_field_1"] >> N_PAYLOAD_SHIFT) & PAYLOAD_MASK; return Ok(LZFSEBlock { eof: false, data_size: n_lmd_payload_bytes + n_literal_payload_bytes, header_size: block_header["header_size"], }); } Err(StructureError) } /// Parse a LZVN compressed LZFSE block header fn parse_compressedlzvn_block_header(lzfse_data: &[u8]) -> Result { const HEADER_SIZE: usize = 12; let block_structure = vec![ ("magic", "u32"), ("n_raw_bytes", "u32"), ("n_payload_bytes", "u32"), ]; if let Ok(header) = parse(lzfse_data, &block_structure, "little") { return Ok(LZFSEBlock { eof: false, data_size: header["n_payload_bytes"], header_size: HEADER_SIZE, }); } Err(StructureError) } ================================================ FILE: src/structures/lzma.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store useful LZMA header data #[derive(Debug, Default, Clone)] pub struct LZMAHeader { pub properties: usize, pub dictionary_size: usize, pub decompressed_size: usize, } /// Parse an LZMA header pub fn parse_lzma_header(lzma_data: &[u8]) -> Result { // Streamed data has a reported size of -1 const LZMA_STREAM_SIZE: usize = 0xFFFFFFFFFFFFFFFF; // Some sane min and max values on the reported decompressed data size const MIN_SUPPORTED_DECOMPRESSED_SIZE: usize = 256; const MAX_SUPPORTED_DECOMPRESSED_SIZE: usize = 0xFFFFFFFF; let lzma_structure = vec![ ("properties", "u8"), ("dictionary_size", "u32"), ("decompressed_size", "u64"), ("null_byte", "u8"), ]; let mut lzma_hdr_info = LZMAHeader { ..Default::default() }; // Parse the lzma header if let Ok(lzma_header) = common::parse(lzma_data, &lzma_structure, "little") { // Make sure the expected NULL byte is NULL if lzma_header["null_byte"] == 0 { // Sanity check the reported decompressed size if lzma_header["decompressed_size"] >= MIN_SUPPORTED_DECOMPRESSED_SIZE && (lzma_header["decompressed_size"] == LZMA_STREAM_SIZE || lzma_header["decompressed_size"] <= MAX_SUPPORTED_DECOMPRESSED_SIZE) { lzma_hdr_info.properties = lzma_header["properties"]; lzma_hdr_info.dictionary_size = lzma_header["dictionary_size"]; lzma_hdr_info.decompressed_size = lzma_header["decompressed_size"]; return Ok(lzma_hdr_info); } } } Err(StructureError) } ================================================ FILE: src/structures/lzop.rs ================================================ use crate::structures::common::{self, StructureError}; /// LZO checksums are 4-bytes long const LZO_CHECKSUM_SIZE: usize = 4; /// Struct to store LZOP file header info #[derive(Debug, Default, Clone)] pub struct LZOPFileHeader { pub header_size: usize, pub block_checksum_present: bool, } /// Parse an LZOP file header pub fn parse_lzop_file_header(lzop_data: &[u8]) -> Result { // Max supported LZO version const LZO_MAX_VERSION: usize = 0x1040; const LZO_HEADER_SIZE_P1: usize = 21; const LZO_HEADER_SIZE_P2: usize = 13; const FILTER_SIZE: usize = 4; const FLAG_FILTER: usize = 0x000_00800; //const FLAG_CRC32_D: usize = 0x0000_0100; const FLAG_CRC32_C: usize = 0x0000_0200; //const FLAG_ADLER32_D: usize = 0x0000_0001; const FLAG_ADLER32_C: usize = 0x0000_0002; let lzo_structure_p1 = vec![ ("magic_p1", "u8"), ("magic_p2", "u64"), ("version", "u16"), ("lib_version", "u16"), ("version_needed", "u16"), ("method", "u8"), ("level", "u8"), ("flags", "u32"), ]; let lzo_structure_p2 = vec![ ("mode", "u32"), ("mtime", "u32"), ("gmt_diff", "u32"), ("file_name_length", "u8"), ]; let allowed_methods: Vec = vec![1, 2, 3]; let mut lzop_info = LZOPFileHeader { ..Default::default() }; // Parse the first part of the header if let Ok(lzo_header_p1) = common::parse(lzop_data, &lzo_structure_p1, "big") { // Sanity check the methods field if allowed_methods.contains(&lzo_header_p1["method"]) { // Sanity check the header version numbers if lzo_header_p1["version"] <= LZO_MAX_VERSION && lzo_header_p1["version"] >= lzo_header_p1["version_needed"] { // Unless the optional filter field is included, start of the second part of the header is at the end of the first let mut header_p2_start: usize = LZO_HEADER_SIZE_P1; // Next part of the header may or may not have an optional filter field if (lzo_header_p1["flags"] & FLAG_FILTER) != 0 { header_p2_start += FILTER_SIZE; } // Calculate the end of the second part of the header let header_p2_end: usize = header_p2_start + LZO_HEADER_SIZE_P2; if let Some(header_p2_data) = lzop_data.get(header_p2_start..header_p2_end) { // Parse the second part of the header if let Ok(lzo_header_p2) = common::parse(header_p2_data, &lzo_structure_p2, "big") { // Calculate the total header size; compressed data blocks will immediately follow lzop_info.header_size = header_p2_end + lzo_header_p2["file_name_length"] + LZO_CHECKSUM_SIZE; // Check if block headers include an optional compressed data checksum field lzop_info.block_checksum_present = (lzo_header_p1["flags"] & FLAG_ADLER32_C & FLAG_CRC32_C) != 0; // Sanity check on the calculated header size if lzop_info.header_size <= lzop_data.len() { return Ok(lzop_info); } } } } } } Err(StructureError) } /// Struct to store info on LZOP block headers #[derive(Debug, Default, Clone)] pub struct LZOPBlockHeader { pub header_size: usize, pub compressed_size: usize, pub uncompressed_size: usize, pub checksum_size: usize, } /// Parse an LZO block header pub fn parse_lzop_block_header( lzo_data: &[u8], compressed_checksum_present: bool, ) -> Result { // Size constants const BLOCK_HEADER_SIZE: usize = 12; const MAX_UNCOMPRESSED_BLOCK_SIZE: usize = 64 * 1024 * 1024; let block_structure = vec![ ("uncompressed_size", "u32"), ("compressed_size", "u32"), ("uncompressed_checksum", "u32"), ]; // Parse the block header if let Ok(block_header) = common::parse(lzo_data, &block_structure, "big") { // Basic sanity check on the block header values if block_header["compressed_size"] != 0 && block_header["uncompressed_size"] != 0 && block_header["uncompressed_checksum"] != 0 && block_header["uncompressed_size"] <= MAX_UNCOMPRESSED_BLOCK_SIZE { let mut block_hdr_info = LZOPBlockHeader { ..Default::default() }; block_hdr_info.header_size = BLOCK_HEADER_SIZE; block_hdr_info.compressed_size = block_header["compressed_size"]; block_hdr_info.uncompressed_size = block_header["uncompressed_size"]; // Checksum field is optional if compressed_checksum_present { block_hdr_info.checksum_size = LZO_CHECKSUM_SIZE; } return Ok(block_hdr_info); } } Err(StructureError) } /// Parse an LZOP EOF marker, returns the size of the EOF marker (always 4 bytes) pub fn parse_lzop_eof_marker(eof_data: &[u8]) -> Result { const EOF_MARKER: usize = 0; const EOF_MARKER_SIZE: usize = 4; let eof_structure = vec![("marker", "u32")]; /* * It is unclear, but observed, that LZOP files end with 0x00000000; this is assumed to be an EOF marker, * as other similar compression file formats use that. This assumption could be incorrect. */ if let Ok(eof_marker) = common::parse(eof_data, &eof_structure, "big") { // Sanity check the EOF marker if eof_marker["marker"] == EOF_MARKER { // Return the size of the EOF marker return Ok(EOF_MARKER_SIZE); } } Err(StructureError) } ================================================ FILE: src/structures/matter_ota.rs ================================================ use std::collections::HashMap; use crate::common::{get_cstring, is_offset_safe}; use crate::structures::common::{self, StructureError}; /// Struct to store Matter OTA header info #[derive(Debug, Default, Clone)] pub struct MatterOTAHeader { pub total_size: usize, pub header_size: usize, pub vendor_id: usize, pub product_id: usize, pub version: String, pub payload_size: usize, pub image_digest_type: usize, pub image_digest: String, } #[derive(Debug)] enum Value { Struct, EndOfContainer, Unsigned(usize), String(String), OctetString(Vec), } #[derive(Debug)] struct Element { tag: Option, value: Value, } /// Parse a Matter OTA firmware header pub fn parse_matter_ota_header(ota_data: &[u8]) -> Result { let ota_structure = vec![ ("magic", "u32"), ("total_size", "u64"), ("header_size", "u32"), ]; if let Ok(ota_header) = common::parse(ota_data, &ota_structure, "little") { let total_size: usize = ota_header["total_size"]; let header_size: usize = ota_header["header_size"]; // Header starts after the magic, total size and header size fields let header_start = common::size(&ota_structure); let header_end = header_start + header_size; let header_data = ota_data .get(header_start..header_end) .ok_or(StructureError)?; let header = parse_tlv_header(header_data)?; let mut result = MatterOTAHeader { total_size, header_size, ..Default::default() }; for (key, value) in header.into_iter() { match (key.as_ref(), value) { ("VendorID", Value::Unsigned(vendor_id)) => result.vendor_id = vendor_id, ("ProductID", Value::Unsigned(product_id)) => result.product_id = product_id, ("SoftwareVersionString", Value::String(version)) => result.version = version, ("PayloadSize", Value::Unsigned(payload_size)) => { result.payload_size = payload_size } ("ImageDigestType", Value::Unsigned(image_digest_type)) => { result.image_digest_type = image_digest_type } ("ImageDigest", Value::OctetString(image_digest)) => { let mut digest_string = String::new(); for b in image_digest { digest_string.push_str(&format!("{b:02x}")); } result.image_digest = digest_string; } // Ignore other fields _ => {} } } // Sanity check if (result.payload_size + header_start + header_size) == total_size { return Ok(result); } } Err(StructureError) } /// Parse tlv element, return result and new offset fn parse_tlv_element(data: &[u8]) -> Result<(Element, usize), StructureError> { let control_octet = data.first().ok_or(StructureError)?; let element_type = control_octet & 0x1f; let tag_control = control_octet >> 5; // Lower 2 bits of the control octet determine the field width of integer types // or the width of the length field for string types let field_width_type = match element_type & 0x3 { 0 => "u8", 1 => "u16", 2 => "u32", 3 => "u64", _ => return Err(StructureError), }; // Parse numerical tag. Only supports anonymous fields and fields with a one byte tag let (tag, field_offset) = match tag_control { 0 => (None, 1), // Anonymous field 1 => (Some(*data.get(1).ok_or(StructureError)? as usize), 2), _ => return Err(StructureError), }; let field_data = data.get(field_offset..).ok_or(StructureError)?; match element_type { 0b1_0101 => Ok(( // Struct container Element { tag, value: Value::Struct, }, field_offset, )), 0b1_1000 => Ok(( // End of container Element { tag, value: Value::EndOfContainer, }, field_offset, )), 0b0_0100..=0b0_0111 => { // Unsigned integer let structure = &vec![("field", field_width_type)]; let result = common::parse(field_data, structure, "little")?; Ok(( Element { tag, value: Value::Unsigned(result["field"]), }, field_offset + common::size(structure), )) } 0b0_1100..=0b0_1111 => { // UTF-8 String let structure = &vec![("string_length", field_width_type)]; let result = common::parse(field_data, structure, "little")?; let string_length = result["string_length"] as usize; let string_data = field_data .get(common::size(structure)..) .ok_or(StructureError)?; // The string buffer isn't null-terminated, so use the explicit length let string = string_data .get(..string_length) .map(get_cstring) .ok_or(StructureError)?; Ok(( Element { tag, value: Value::String(string), }, field_offset + common::size(structure) + string_length, )) } 0b1_0000..=0b1_0011 => { // Octet string let structure = &vec![("octet_string_length", field_width_type)]; let result = common::parse(field_data, structure, "little")?; let octet_string_length = result["octet_string_length"] as usize; let octet_string_data = field_data .get(common::size(structure)..) .ok_or(StructureError)?; Ok(( Element { tag, value: Value::OctetString( octet_string_data .get(..octet_string_length) .ok_or(StructureError)? .to_vec(), ), }, field_offset + common::size(structure) + octet_string_length, )) } _ => Err(StructureError), // Other types are not implemented, but not necessary for header parsing } } fn parse_tlv_header(data: &[u8]) -> Result, StructureError> { // Field names for the Matter OTA header indexed by the tag number let fields = [ "VendorID", "ProductID", "SoftwareVersion", "SoftwareVersionString", "PayloadSize", "MinApplicableSoftwareVersion", "MaxApplicableSoftwareVersion", "ReleaseNotesURL", "ImageDigestType", "ImageDigest", ]; let mut header = HashMap::new(); let available_data: usize = data.len(); let mut last_tlv_offset: Option = None; let mut next_tlv_offset: usize = 0; while is_offset_safe(available_data, next_tlv_offset, last_tlv_offset) { let (element, new_offset) = parse_tlv_element(&data[next_tlv_offset..])?; last_tlv_offset = Some(next_tlv_offset); next_tlv_offset += new_offset; if let Some(tag) = element.tag { let field_name = *fields.get(tag).ok_or(StructureError)?; header.insert(field_name.to_string(), element.value); } } Ok(header) } ================================================ FILE: src/structures/mbr.rs ================================================ use crate::structures::common::{self, StructureError}; use std::collections::HashMap; /// Struct to store MBR partition info #[derive(Debug, Default, Clone)] pub struct MBRPartition { pub start: usize, pub size: usize, pub name: String, } /// Struct to store MBR info #[derive(Debug, Default, Clone)] pub struct MBRHeader { pub image_size: usize, pub partitions: Vec, } /// Parse a Master Boot Record image pub fn parse_mbr_image(mbr_data: &[u8]) -> Result { const BLOCK_SIZE: usize = 512; const MIN_IMAGE_SIZE: usize = BLOCK_SIZE * 2; const PARTITION_COUNT: usize = 4; const PARTITION_TABLE_OFFSET: usize = 446; let partition_entry_structure = vec![ ("status", "u8"), ("chs_start", "u24"), ("os_type", "u8"), ("chs_end", "u24"), ("lba_start", "u32"), ("lba_size", "u32"), ]; let known_os_types = HashMap::from([ (0x07, "NTFS_IFS_HPFS_exFAT"), (0x0B, "FAT32"), (0x0C, "FAT32"), (0x43, "Linux"), (0x4D, "QNX Primary Volume"), (0x4E, "QNX Secondary Volume"), (0x81, "Minix"), (0x83, "Linux"), (0x8E, "Linux LVM"), (0x96, "ISO-9660"), (0xB1, "QNXv6 File System"), (0xB2, "QNXv6 File System"), (0xB3, "QNXv6 File System"), (0xEE, "EFI GPT Protective"), (0xEF, "EFI System Partition"), ]); let allowed_status_values: Vec = vec![0, 0x80]; let partition_structure_size = common::size(&partition_entry_structure); let partition_table_start: usize = PARTITION_TABLE_OFFSET; let partition_table_end: usize = partition_table_start + (partition_structure_size * PARTITION_COUNT); let mut mbr_header = MBRHeader { ..Default::default() }; // Get the partition table raw bytes if let Some(partition_table) = mbr_data.get(partition_table_start..partition_table_end) { // Parse each partition table entry for i in 0..PARTITION_COUNT { // Offset in the partition table for this entry let partition_entry_start: usize = i * partition_structure_size; // Parse this partition table entry match common::parse( &partition_table[partition_entry_start..], &partition_entry_structure, "little", ) { Err(_) => { return Err(StructureError); } Ok(partition_entry) => { // OS type of zero or LBA size of 0 can be ignored if partition_entry["os_type"] != 0 || partition_entry["lba_size"] != 0 { // Validate the reported MBR status value if allowed_status_values.contains(&partition_entry["status"]) { // Default to unknown partition type let mut this_partition_name: &str = "Unknown"; // If partition type is known, provide a descriptive name if known_os_types.contains_key(&partition_entry["os_type"]) { this_partition_name = known_os_types[&partition_entry["os_type"]]; } // Create an MBRPartition structure for this entry let this_partition = MBRPartition { start: partition_entry["lba_start"] * BLOCK_SIZE, size: partition_entry["lba_size"] * BLOCK_SIZE, name: this_partition_name.to_string(), }; // Calculate where this partition ends let this_partition_end_offset = this_partition.start + this_partition.size; // Some valid MBRs have partitions that start/end out of bounds WRT the disk image. // Not sure why? At any rate, don't include them in the reported partitions. if this_partition_end_offset <= mbr_data.len() { // Don't report the partition where the MBR header resides if this_partition.start != 0 { // Add it to the list of partitions mbr_header.partitions.push(this_partition.clone()); } // Image size is the end of the farthest away partition if this_partition_end_offset > mbr_header.image_size { mbr_header.image_size = this_partition_end_offset; } } } } } } } // There should be at least one valid partition if !mbr_header.partitions.is_empty() { // Total size should be greater than minimum size if mbr_header.image_size > MIN_IMAGE_SIZE { return Ok(mbr_header); } } } Err(StructureError) } ================================================ FILE: src/structures/mh01.rs ================================================ use crate::common::get_cstring; use crate::structures::common::{self, StructureError}; /// Struct to store MH01 header info #[derive(Debug, Default, Clone)] pub struct MH01Header { pub iv: String, pub iv_offset: usize, pub iv_size: usize, pub signature_offset: usize, pub signature_size: usize, pub encrypted_data_offset: usize, pub encrypted_data_size: usize, pub total_size: usize, } /// Parses an MH01 header pub fn parse_mh01_header(mh01_data: &[u8]) -> Result { const HEADER_SIZE: usize = 16; // This structure is actually two MH01 headers, each header is HEADER_SIZE bytes long. // The first header describes the offset and size of the firmware signature. // The second header describes the offset and size of the encrypted firmware image. // The OpenSSL IV is stored as an ASCII hex string between the second header and the encrypted firmware image. let mh01_structure = vec![ ("magic1", "u32"), ("signature_offset", "u32"), ("signature_size", "u32"), ("unknown1", "u32"), ("magic2", "u32"), ("iv_size", "u32"), ("encrypted_data_size", "u32"), ("unknown2", "u32"), // IV string of length iv_size immediately follows ]; let mut result = MH01Header { ..Default::default() }; // Parse the header if let Ok(header) = common::parse(mh01_data, &mh01_structure, "little") { // Make sure the expected magic bytes match if header["magic1"] == header["magic2"] { // IV size is specified in the header and immediately follows the header result.iv_size = header["iv_size"]; result.iv_offset = common::size(&mh01_structure); // The encrypted firmware image immediately follows the IV result.encrypted_data_size = header["encrypted_data_size"]; result.encrypted_data_offset = result.iv_offset + result.iv_size; // The signature should immediately follow the encrypted firmware image result.signature_size = header["signature_size"]; result.signature_offset = HEADER_SIZE + header["signature_offset"]; // Calculate the start and end bytes of the IV (ASCII hex) let iv_bytes_start = result.iv_offset; let iv_bytes_end = result.encrypted_data_offset; // Get the payload hash string if let Some(iv_bytes) = mh01_data.get(iv_bytes_start..iv_bytes_end) { let iv_string = get_cstring(iv_bytes); // Make sure we got a string of the expected length if iv_string.len() == result.iv_size { result.iv = iv_string.trim().to_string(); result.total_size = result.signature_offset + result.signature_size; return Ok(result); } } } } Err(StructureError) } ================================================ FILE: src/structures/ntfs.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store NTFS info #[derive(Debug, Default, Clone)] pub struct NTFSPartition { pub sector_size: usize, pub sector_count: usize, } /// Parses an NTFS partition header pub fn parse_ntfs_header(ntfs_data: &[u8]) -> Result { // https://en.wikipedia.org/wiki/NTFS let ntfs_structure = vec![ ("opcodes", "u24"), ("magic", "u64"), ("bytes_per_sector", "u16"), ("sectors_per_cluster", "u8"), ("unused1", "u16"), ("unused2", "u24"), ("unused3", "u16"), ("media_type", "u8"), ("unused4", "u16"), ("sectors_per_track", "u16"), ("head_count", "u16"), ("hidden_sector_count", "u32"), ("unused5", "u32"), ("unknown", "u32"), ("sector_count", "u64"), ]; // Parse the NTFS partition header if let Ok(ntfs_header) = common::parse(ntfs_data, &ntfs_structure, "little") { // Sanity check to make sure the unused fields are not used if ntfs_header["unused1"] == 0 && ntfs_header["unused2"] == 0 && ntfs_header["unused3"] == 0 && ntfs_header["unused4"] == 0 && ntfs_header["unused5"] == 0 { return Ok(NTFSPartition { sector_count: ntfs_header["sector_count"], sector_size: ntfs_header["bytes_per_sector"], }); } } Err(StructureError) } ================================================ FILE: src/structures/openssl.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store info on an OpenSSL crypto header pub struct OpenSSLCryptHeader { pub salt: usize, } /// Parse an OpenSSl crypto header pub fn parse_openssl_crypt_header(ssl_data: &[u8]) -> Result { let ssl_structure = vec![("magic", "u64"), ("salt", "u64")]; if let Ok(ssl_header) = common::parse(ssl_data, &ssl_structure, "big") { if ssl_header["salt"] != 0 { return Ok(OpenSSLCryptHeader { salt: ssl_header["salt"], }); } } Err(StructureError) } ================================================ FILE: src/structures/packimg.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store PackIMG header info pub struct PackIMGHeader { pub header_size: usize, pub data_size: usize, } /// Parse a PackIMG header pub fn parse_packimg_header(packimg_data: &[u8]) -> Result { // Fixed size header const PACKIMG_HEADER_SIZE: usize = 32; let packimg_structure = vec![ ("magic_p1", "u32"), ("magic_p2", "u32"), ("magic_p3", "u32"), ("padding1", "u32"), ("data_size", "u32"), ]; // Parse the packimg header if let Ok(packimg_header) = common::parse(packimg_data, &packimg_structure, "little") { return Ok(PackIMGHeader { header_size: PACKIMG_HEADER_SIZE, data_size: packimg_header["data_size"], }); } Err(StructureError) } ================================================ FILE: src/structures/pcap.rs ================================================ use crate::structures::common::{self, StructureError}; use std::collections::HashMap; /// Storage struct for Pcap block info #[derive(Debug, Clone, Default)] pub struct PcapBlock { pub block_type: usize, pub block_size: usize, } /// Parse a Pcap-ng block pub fn parse_pcapng_block( block_data: &[u8], endianness: &str, ) -> Result { // Reserved bit in block type field const BLOCK_TYPE_RESERVED_MASK: usize = 0x80000000; let block_header_structure = vec![("block_type", "u32"), ("block_size", "u32")]; let block_footer_structure = vec![("block_size", "u32")]; let mut result = PcapBlock { ..Default::default() }; let footer_size = common::size(&block_footer_structure); // Parse the block header if let Ok(block_header) = common::parse(block_data, &block_header_structure, endianness) { // Populate the block type and size values result.block_type = block_header["block_type"]; result.block_size = block_header["block_size"]; // Make sure the reserved bit of the block type is not set if (result.block_type & BLOCK_TYPE_RESERVED_MASK) == 0 { // Calculate the block footer offsets let block_footer_start = result.block_size - footer_size; let block_footer_end = block_footer_start + footer_size; // Validate that the block size in the block footer matches the block size in the block header if let Some(block_footer_data) = block_data.get(block_footer_start..block_footer_end) { if let Ok(block_footer) = common::parse(block_footer_data, &block_footer_structure, endianness) { if block_footer["block_size"] == result.block_size { return Ok(result); } } } } } Err(StructureError) } #[derive(Debug, Default, Clone)] pub struct PcapSectionBlock { pub block_size: usize, pub endianness: String, } /// Parse a Pcap-ng section block pub fn parse_pcapng_section_block(block_data: &[u8]) -> Result { // Section header block type (same value, regardless of endianness) const SECTION_HEADER_BLOCK_TYPE: usize = 0x0A0D0D0A; let section_header_structure = vec![ ("block_type", "u32"), ("block_size", "u32"), ("endian_magic", "u32"), ("major_version", "u16"), ("minor_version", "u16"), ("section_length", "u32"), ]; let endian_magics: HashMap = HashMap::from([(0x1A2B3C4D, "little"), (0x4D3C2B1A, "big")]); let mut result = PcapSectionBlock { ..Default::default() }; // Parse the section header structure; endianess doesn't matter (yet) if let Ok(section_header) = common::parse(block_data, §ion_header_structure, "little") { // Determine the endianness based on the endian magic bytes if endian_magics.contains_key(§ion_header["endian_magic"]) { result.endianness = endian_magics[§ion_header["endian_magic"]].to_string(); // Parse the section header block as a generic block to ensure it is valid if let Ok(block_header) = parse_pcapng_block(block_data, &result.endianness) { // Make sure the section header block type is the expected value if block_header.block_type == SECTION_HEADER_BLOCK_TYPE { result.block_size = block_header.block_size; return Ok(result); } } } } Err(StructureError) } ================================================ FILE: src/structures/pchrom.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store PCHROM image info #[derive(Debug, Default, Clone)] pub struct PCHRomHeader { pub data_size: usize, pub header_size: usize, } /// Parse a PCHROM header pub fn parse_pchrom_header(pch_data: &[u8]) -> Result { // Structure is a fixed size, at a fixed offset from the beginning of the PCHROM image const HEADER_STRUCTURE_SIZE: usize = 8; const HEADER_STRUCTURE_OFFSET: usize = 16; // All "expected" values are derived from the Intel PCH Programming Manual const EXPECTED_FCBA: usize = 3; const EXPECTED_FRBA: usize = 4; let expected_nc_values: Vec = vec![0, 1]; let pch_rom_header_structure = vec![ ("flmagic", "u32"), ("flmap0_fcba", "u8"), ("flmap0_nc", "u8"), ("flmap0_frba_nr", "u16"), ]; // Calculate the header structure start and end offsets let struct_start: usize = HEADER_STRUCTURE_OFFSET; let struct_end: usize = struct_start + HEADER_STRUCTURE_SIZE; if let Some(pch_structure_data) = pch_data.get(struct_start..struct_end) { // Parse the header structure if let Ok(pch_header) = common::parse(pch_structure_data, &pch_rom_header_structure, "little") { // Sanity check the expected header values if pch_header["flmap0_fcba"] == EXPECTED_FCBA && pch_header["flmap0_frba_nr"] == EXPECTED_FRBA && expected_nc_values.contains(&pch_header["flmap0_nc"]) { // Parse the flash rom region entries to determine the total image size if let Ok(pch_regions_size) = get_pch_regions_size(pch_data, 0, pch_header["flmap0_fcba"]) { return Ok(PCHRomHeader { header_size: HEADER_STRUCTURE_OFFSET, data_size: pch_regions_size, }); } } } } Err(StructureError) } /// Determine the total size of PCHROM regions fn get_pch_regions_size( pch_data: &[u8], offset: usize, fcba: usize, ) -> Result { // There are 5 defined flash regions: Descriptor, BIOS, ME, GBE, PDATA const FLASH_REGION_COUNT: usize = 5; // Each entry is a 32-bit value describing the region const FLASH_REGION_ENTRY_SIZE: usize = 4; // There is a 32-bit entry for each possible region in the PCH image let region_entry_structure = vec![("region_value", "u32")]; let mut image_size: usize = 0; // The base address of the flash regions is encoded into 8 bits of the FCBA header field, like so let flash_region_base_address: usize = ((fcba >> 16) & 0xFF) << 4; // Region entries are 32-bit values stored seqeuntially starting at the flash region base address for i in 0..FLASH_REGION_COUNT { // Get the offset of the next region's 32-bit entry let region_entry_start = offset + flash_region_base_address + (i * FLASH_REGION_ENTRY_SIZE); let region_entry_end = region_entry_start + FLASH_REGION_ENTRY_SIZE; // Get the next region's 32-bit value, in raw bytes match pch_data.get(region_entry_start..region_entry_end) { None => { return Err(StructureError); } Some(pch_region_data) => { // Parse the 32-bit entry value for this region if let Ok(region_entry) = common::parse(pch_region_data, ®ion_entry_structure, "little") { let region_value = region_entry["region_value"]; // The base (starting offset) and limit (ending offset) of the region is encoded into the 32-bit entry value let region_base = (region_value & 0x1FFF) << 12; let region_limit = (((region_value & 0x1FFF0000) >> 4) | 0xFFFF) + 1; // Size can be inferred from the base and limit values let region_size = region_limit - region_base; // If size is 0, this region is not used in this image if region_size > 0 && region_limit > image_size { image_size = region_limit; } } } } } if image_size > 0 { return Ok(image_size); } Err(StructureError) } ================================================ FILE: src/structures/pe.rs ================================================ use crate::structures::common::{self, StructureError}; use std::collections::HashMap; /// Stores info about the PE file pub struct PEHeader { pub machine: String, } /// Partially parse a PE header pub fn parse_pe_header(pe_data: &[u8]) -> Result { const PE_MAGIC: usize = 0x00004550; let dos_structure = vec![ ("e_magic", "u16"), // "MZ" ("e_cblp", "u16"), // Bytes on last page of file ("e_cp", "u16"), // Pages in file ("e_crlc", "u16"), // Relocations ("e_cparhdr", "u16"), // Header size, in paragraphs ("e_minalloc", "u16"), // Min extra paragraphs needed ("e_maxalloc", "u16"), // Max extra paragraphs needed ("e_ss", "u16"), // Initial relative SS value ("e_sp", "u16"), // Initial SP value ("e_csum", "u16"), // Checksum ("e_ip", "u16"), // Initial IP value ("e_cs", "u16"), // Initial relative CS value ("e_lfarlc", "u16"), // File address of relocation table ("e_ovno", "u16"), // Overlay number ("e_res_1", "u16"), ("e_res_2", "u16"), ("e_res_3", "u16"), ("e_res_4", "u16"), ("e_oemid", "u16"), // OEM identifier ("e_oeminfo", "u16"), // OEM specific information ("e_res_5", "u16"), ("e_res_6", "u16"), ("e_res_7", "u16"), ("e_res_8", "u16"), ("e_res_9", "u16"), ("e_res_10", "u16"), ("e_res_11", "u16"), ("e_res_12", "u16"), ("e_res_13", "u16"), ("e_res_14", "u16"), ("e_lfanew", "u32"), // Offset to the PE header ]; let pe_structure = vec![ ("magic", "u32"), ("machine", "u16"), ("number_of_sections", "u16"), ("timestamp", "u32"), ("symbol_table_ptr", "u32"), ("number_of_symbols", "u32"), ("optional_header_size", "u16"), ("characteristics", "u16"), ]; let known_machine_types: HashMap = HashMap::from([ (0, "Unknown"), (0x184, "Alpha32"), (0x284, "Alpha64"), (0x1D3, "Matsushita AM33"), (0x8664, "Intel x86-64"), (0x1C0, "ARM"), (0xAA64, "ARM-64"), (0x1C4, "ARM Thumb2"), (0xEBC, "EFI"), (0x14C, "Intel x86"), (0x200, "Intel Itanium"), (0x6232, "LoongArch 32-bit"), (0x6264, "LoongArch 64-bit"), (0x9041, "Mitsubishi M32R"), (0x266, "MIPS16"), (0x366, "MIPS with FPU"), (0x466, "MIPS16 with FPU"), (0x1F0, "PowerPC"), (0x1F1, "PowerPC with FPU"), (0x5032, "RISC-V 32-bit"), (0x5064, "RISC-V 64-bit"), (0x5128, "RISC-V 128-bit"), (0x1A2, "Hitachi SH3"), (0x1A3, "Hitachi SH3 DSP"), (0x1A6, "Hitachi SH4"), (0x1A8, "Hitachi SH5"), (0x1C2, "Thumb"), (0x169, "MIPS WCEv2"), ]); // Size of PE header structure let pe_header_size = common::size(&pe_structure); // Parse the DOS header if let Ok(dos_header) = common::parse(pe_data, &dos_structure, "little") { // Sanity check the reserved header fields; they should all be 0 if dos_header["e_res_1"] == 0 && dos_header["e_res_2"] == 0 && dos_header["e_res_3"] == 0 && dos_header["e_res_4"] == 0 && dos_header["e_res_5"] == 0 && dos_header["e_res_6"] == 0 && dos_header["e_res_7"] == 0 && dos_header["e_res_8"] == 0 && dos_header["e_res_9"] == 0 && dos_header["e_res_10"] == 0 && dos_header["e_res_11"] == 0 && dos_header["e_res_12"] == 0 && dos_header["e_res_13"] == 0 && dos_header["e_res_14"] == 0 { // Start and end offsets of the PE header let pe_header_start: usize = dos_header["e_lfanew"]; let pe_header_end: usize = pe_header_start + pe_header_size; // Sanity check the PE header offsets if let Some(pe_header_data) = pe_data.get(pe_header_start..pe_header_end) { // Parse the PE header if let Ok(pe_header) = common::parse(pe_header_data, &pe_structure, "little") { // Check the PE magic bytes if pe_header["magic"] == PE_MAGIC { // Check the reported machine type if known_machine_types.contains_key(&pe_header["machine"]) { return Ok(PEHeader { machine: known_machine_types[&pe_header["machine"]].to_string(), }); } } } } } } Err(StructureError) } ================================================ FILE: src/structures/png.rs ================================================ use crate::structures::common::{self, StructureError}; /// Stores info on a PNG chunk header pub struct PNGChunkHeader { pub total_size: usize, pub is_last_chunk: bool, } /// Parse a PNG chunk header pub fn parse_png_chunk_header(chunk_data: &[u8]) -> Result { // All PNG chunks are followed by a 4-byte CRC const CRC_SIZE: usize = 4; // The "IEND" chunk is the last chunk in the PNG const IEND_CHUNK_TYPE: usize = 0x49454E44; let png_chunk_structure = vec![("length", "u32"), ("type", "u32")]; let chunk_structure_size: usize = common::size(&png_chunk_structure); // Parse the chunk header if let Ok(chunk_header) = common::parse(chunk_data, &png_chunk_structure, "big") { return Ok(PNGChunkHeader { is_last_chunk: chunk_header["type"] == IEND_CHUNK_TYPE, total_size: chunk_structure_size + chunk_header["length"] + CRC_SIZE, }); } Err(StructureError) } ================================================ FILE: src/structures/qcow.rs ================================================ use crate::structures::common; use crate::structures::common::StructureError; use std::collections::HashMap; #[derive(Debug, Default, Clone)] pub struct QcowHeader { pub version: u8, pub storage_media_size: u64, pub cluster_block_bits: u8, pub encryption_method: String, } pub fn parse_qcow_header(qcow_data: &[u8]) -> Result { let qcow_basehdr_structure = vec![("magic", "u32"), ("version", "u32")]; let qcow_header_v1_structure = vec![ ("backing_filename_offset", "u64"), ("backing_filename_size", "u32"), ("modification_timestamp", "u32"), ("storage_media_size", "u64"), ("cluster_block_bits", "u8"), ("level2_table_bits", "u8"), ("reserved1", "u16"), ("encryption_method", "u32"), ("level1_table_offset", "u64"), ]; let qcow_header_v2_structure = vec![ ("backing_filename_offset", "u64"), ("backing_filename_size", "u32"), ("cluster_block_bits", "u32"), ("storage_media_size", "u64"), ("encryption_method", "u32"), ("level1_table_refs", "u32"), ("level1_table_offset", "u64"), ("refcount_table_offset", "u64"), ("refcount_table_clusters", "u32"), ("snapshot_count", "u32"), ("snapshot_offset", "u64"), ]; let qcow_header_v3_structure = vec![ ("backing_filename_offset", "u64"), ("backing_filename_size", "u32"), ("cluster_block_bits", "u32"), ("storage_media_size", "u64"), ("encryption_method", "u32"), ("level1_table_refs", "u32"), ("level1_table_offset", "u64"), ("refcount_table_offset", "u64"), ("refcount_table_clusters", "u32"), ("snapshot_count", "u32"), ("snapshot_offset", "u64"), ("incompatible_feature_flags", "u64"), ("compatible_feature_flags", "u64"), ("autoclear_feature_flags", "u64"), ("refcount_order", "u32"), ("file_hdr_size", "u32"), // 104 or 112 ]; let encryption_methods = HashMap::from([(0, "None"), (1, "AES128-CBC"), (2, "LUKS")]); if let Ok(qcow_base_header) = common::parse(qcow_data, &qcow_basehdr_structure, "big") { let qcow_version = qcow_base_header["version"]; let qcow_data = qcow_data.get(8..).ok_or(StructureError)?; let qcow_header = match qcow_version { 1 => common::parse(qcow_data, &qcow_header_v1_structure, "big"), 2 => common::parse(qcow_data, &qcow_header_v2_structure, "big"), 3 => common::parse(qcow_data, &qcow_header_v3_structure, "big"), _ => Err(StructureError), }?; let encryption_method = encryption_methods .get(&qcow_header["encryption_method"]) .ok_or(StructureError)? .to_string(); let cluster_block_bits = *qcow_header .get("cluster_block_bits") .filter(|&&bits| (9..=21).contains(&bits)) .ok_or(StructureError)?; // sanity check: existing offsets need to be aligned to cluster boundary if let Some(offset) = qcow_header.get("level1_table_offset") { if offset % (1 << cluster_block_bits) != 0 { return Err(StructureError); } } if let Some(offset) = qcow_header.get("refcount_table_offset") { if offset % (1 << cluster_block_bits) != 0 { return Err(StructureError); } } if let Some(offset) = qcow_header.get("snapshot_offset") { if offset % (1 << cluster_block_bits) != 0 { return Err(StructureError); } } return Ok(QcowHeader { version: qcow_version as u8, storage_media_size: qcow_header["storage_media_size"] as u64, cluster_block_bits: cluster_block_bits as u8, encryption_method, }); } Err(StructureError) } ================================================ FILE: src/structures/qnx.rs ================================================ use crate::structures::common::{self, StructureError}; /// Stores info on a QNX IFS header pub struct IFSHeader { pub total_size: usize, } /// Parse a QNX IFS header pub fn parse_ifs_header(ifs_data: &[u8]) -> Result { // https://github.com/askac/dumpifs/blob/master/sys/startup.h let ifs_structure = vec![ ("magic", "u32"), ("version", "u16"), ("flags1", "u8"), ("flags2", "u8"), ("header_size", "u16"), ("machine", "u16"), ("startup_vaddr", "u32"), ("paddr_bias", "u32"), ("image_paddr", "u32"), ("ram_paddr", "u32"), ("ram_size", "u32"), ("startup_size", "u32"), ("stored_size", "u32"), ("imagefs_paddr", "u32"), ("imagefs_size", "u32"), ("preboot_size", "u16"), ("zero_0", "u16"), ("zero_1", "u32"), ("zero_2", "u32"), ("zero_3", "u32"), ]; // Parse the IFS header if let Ok(ifs_header) = common::parse(ifs_data, &ifs_structure, "little") { // The flags2 field is unused and should be 0 if ifs_header["flags2"] == 0 { // Verify that all the zero fields are, in fact, zero if ifs_header["zero_0"] == 0 && ifs_header["zero_1"] == 0 && ifs_header["zero_2"] == 0 && ifs_header["zero_3"] == 0 { return Ok(IFSHeader { total_size: ifs_header["stored_size"], }); } } } Err(StructureError) } ================================================ FILE: src/structures/rar.rs ================================================ use crate::structures::common::{self, StructureError}; use std::collections::HashMap; /// Stores info on a RAR archive #[derive(Debug, Default, Clone)] pub struct RarArchiveHeader { pub version: usize, } /// Parse a RAR archive header pub fn parse_rar_archive_header(rar_data: &[u8]) -> Result { let archive_header_structure = vec![("magic_p1", "u32"), ("magic_p2", "u16"), ("version", "u8")]; // Version field of 0 indicates RARv4; version field of 1 indicates RARv5 let version_map: HashMap = HashMap::from([(0, 4), (1, 5)]); // Parse the header if let Ok(archive_header) = common::parse(rar_data, &archive_header_structure, "little") { // Make sure the version number is one of the known versions if version_map.contains_key(&archive_header["version"]) { return Ok(RarArchiveHeader { version: version_map[&archive_header["version"]], }); } } Err(StructureError) } ================================================ FILE: src/structures/riff.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store info from a RIFF header pub struct RIFFHeader { pub size: usize, pub chunk_type: String, } /// Parse a RIFF image header pub fn parse_riff_header(riff_data: &[u8]) -> Result { const MAGIC: usize = 0x46464952; const CHUNK_TYPE_START: usize = 8; const CHUNK_TYPE_END: usize = 12; const FILE_SIZE_OFFSET: usize = 8; let riff_structure = vec![ ("magic", "u32"), ("file_size", "u32"), ("chunk_type", "u32"), ]; // Parse the riff header if let Ok(riff_header) = common::parse(riff_data, &riff_structure, "little") { // Sanity check expected magic bytes if riff_header["magic"] == MAGIC { // Get the RIFF type string (e.g., "WAVE") if let Ok(type_string) = String::from_utf8(riff_data[CHUNK_TYPE_START..CHUNK_TYPE_END].to_vec()) { return Ok(RIFFHeader { size: riff_header["file_size"] + FILE_SIZE_OFFSET, chunk_type: type_string.trim().to_string(), }); } } } Err(StructureError) } ================================================ FILE: src/structures/romfs.rs ================================================ use crate::common::get_cstring; use crate::structures::common::{self, StructureError}; /// Stores RomFS header info #[derive(Default, Debug, Clone)] pub struct RomFSHeader { pub image_size: usize, pub header_size: usize, pub volume_name: String, } /// Parse a RomFS header pub fn parse_romfs_header(romfs_data: &[u8]) -> Result { // Maximum amount of data that the RomFS CRC is calculated over const MAX_HEADER_CRC_DATA_LEN: usize = 512; let header_structure = vec![("magic", "u64"), ("image_size", "u32"), ("checksum", "u32")]; // Get the size of the defined header structure let header_size = common::size(&header_structure); // Parse the header structure if let Ok(header) = common::parse(romfs_data, &header_structure, "big") { // Sanity check the reported image size if header["image_size"] > header_size { // The volume name is a NULL-terminated string that immediately follows the RomFS header if let Some(volume_name_bytes) = romfs_data.get(header_size..) { let volume_name = get_cstring(volume_name_bytes); let mut crc_data_len: usize = MAX_HEADER_CRC_DATA_LEN; if header["image_size"] < crc_data_len { crc_data_len = header["image_size"]; } // Validate the header CRC if let Some(crc_data) = romfs_data.get(0..crc_data_len) { if romfs_crc_valid(crc_data) { return Ok(RomFSHeader { image_size: header["image_size"], volume_name: volume_name.clone(), // Volume name has a NULL terminator and is padded to a 16 byte boundary alignment header_size: header_size + romfs_align(volume_name.len() + 1), }); } } } } } Err(StructureError) } /// Struct to store info on a RomFS file entry #[derive(Debug, Default, Clone)] pub struct RomFSFileHeader { pub info: usize, pub size: usize, pub name: String, pub checksum: usize, /// Offset to the start of the file data, *relative to the beginning of this header* pub data_offset: usize, pub file_type: usize, pub executable: bool, pub symlink: bool, pub directory: bool, pub regular: bool, pub block_device: bool, pub character_device: bool, pub fifo: bool, pub socket: bool, /// Offset to the next file header, *relative to the beginning of the RomFS image* pub next_header_offset: usize, } /// Parse a RomFS file entry pub fn parse_romfs_file_entry(romfs_data: &[u8]) -> Result { // Bit masks const FILE_TYPE_MASK: usize = 0b0111; const FILE_EXEC_MASK: usize = 0b1000; const NEXT_OFFSET_MASK: usize = 0b11111111_11111111_11111111_11110000; // We only support extraction of these file types const ROMFS_DIRECTORY: usize = 1; const ROMFS_REGULAR_FILE: usize = 2; const ROMFS_SYMLINK: usize = 3; const ROMFS_BLOCK_DEVICE: usize = 4; const ROMFS_CHAR_DEVICE: usize = 5; const ROMFS_SOCKET: usize = 6; const ROMFS_FIFO: usize = 7; let file_header_structure = vec![ ("next_header_offset", "u32"), ("info", "u32"), ("size", "u32"), ("checksum", "u32"), ]; // Size of the defined file header structure let file_header_size = common::size(&file_header_structure); // Parse the file header if let Ok(file_entry_header) = common::parse(romfs_data, &file_header_structure, "big") { // Null terminated file name immediately follows the header if let Some(file_name_bytes) = romfs_data.get(file_header_size..) { let file_name = get_cstring(file_name_bytes); // A file should have a name if !file_name.is_empty() { // Instantiate a new RomFSEntry structure let mut file_header = RomFSFileHeader { ..Default::default() }; // Populate basic info file_header.size = file_entry_header["size"]; file_header.info = file_entry_header["info"]; file_header.checksum = file_entry_header["checksum"]; file_header.name = file_name.clone(); // File data begins immediately after the file header, including the NULL-terminated, 16-byte alignment padded file name file_header.data_offset = file_header_size + romfs_align(file_name.len() + 1); // These values are encoded into the next header offset field file_header.file_type = file_entry_header["next_header_offset"] & FILE_TYPE_MASK; file_header.executable = (file_entry_header["next_header_offset"] & FILE_EXEC_MASK) != 0; // Set the type of entry that this is file_header.fifo = file_header.file_type == ROMFS_FIFO; file_header.socket = file_header.file_type == ROMFS_SOCKET; file_header.symlink = file_header.file_type == ROMFS_SYMLINK; file_header.regular = file_header.file_type == ROMFS_REGULAR_FILE; file_header.directory = file_header.file_type == ROMFS_DIRECTORY; file_header.block_device = file_header.file_type == ROMFS_BLOCK_DEVICE; file_header.character_device = file_header.file_type == ROMFS_CHAR_DEVICE; // The next file header offset is an offset from the beginning of the RomFS image file_header.next_header_offset = file_entry_header["next_header_offset"] & NEXT_OFFSET_MASK; return Ok(file_header); } } } Err(StructureError) } /// RomFS aligns things to a 16-byte boundary fn romfs_align(x: usize) -> usize { const ALIGNMENT: usize = 16; let mut padding: usize = 0; let remainder = x % ALIGNMENT; if remainder > 0 { padding = ALIGNMENT - remainder; } x + padding } /// Pretty simple checksum used by RomFS fn romfs_crc_valid(crc_data: &[u8]) -> bool { let word_size: usize = std::mem::size_of::(); // Checksum size must be 4-byte aligned if (crc_data.len() % word_size) == 0 { let mut i: usize = 0; let mut sum: u32 = 0; // Sum each word while i < crc_data.len() { sum = sum.wrapping_add(u32::from_be_bytes( crc_data[i..i + word_size].try_into().unwrap(), )); i += word_size; } /* * The header checksum is set such that summing the bytes should result in a sum of 0. */ return sum == 0; } false } ================================================ FILE: src/structures/rtk.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store RTK firmware header info #[derive(Debug, Default, Clone)] pub struct RTKHeader { pub image_size: usize, pub header_size: usize, } /// Parses a RTK header pub fn parse_rtk_header(rtk_data: &[u8]) -> Result { const MAGIC_SIZE: usize = 4; let rtk_structure = vec![ ("magic", "u32"), ("image_size", "u32"), ("checksum", "u32"), ("unknown1", "u32"), ("header_size", "u32"), ("unknown2", "u32"), ("unknown3", "u32"), ("identifier", "u32"), ]; let mut result = RTKHeader { ..Default::default() }; // Parse the header if let Ok(rtk_header) = common::parse(rtk_data, &rtk_structure, "little") { result.image_size = rtk_header["image_size"]; result.header_size = rtk_header["header_size"] + MAGIC_SIZE; return Ok(result); } Err(StructureError) } ================================================ FILE: src/structures/seama.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store SEAMA firmware header data pub struct SeamaHeader { pub data_size: usize, pub header_size: usize, } /// Parse a SEAMA firmware header pub fn parse_seama_header(seama_data: &[u8]) -> Result { // SEAMA magic const MAGIC: usize = 0x5EA3A417; let seama_structure = vec![ ("magic", "u32"), ("description_size", "u32"), ("data_size", "u32"), ("unknown1", "u64"), ("unknown2", "u64"), ]; let mut endianness: &str = "little"; let available_data = seama_data.len(); let header_size: usize = common::size(&seama_structure); // Parse the header; try little endian first if let Ok(mut seama_header) = common::parse(seama_data, &seama_structure, endianness) { // If the magic bytes don't match, switch to big endian if seama_header["magic"] != MAGIC { endianness = "big"; match common::parse(seama_data, &seama_structure, endianness) { Err(_) => { return Err(StructureError); } Ok(seama_header_be) => { seama_header = seama_header_be.clone(); } } } // Sanity check on magic bytes if seama_header["magic"] == MAGIC { let total_header_size = header_size + seama_header["description_size"]; // Sanity check on total header size if total_header_size >= header_size && available_data >= total_header_size { return Ok(SeamaHeader { data_size: seama_header["data_size"], header_size: total_header_size, }); } } } Err(StructureError) } ================================================ FILE: src/structures/sevenzip.rs ================================================ use crate::common::crc32; use crate::structures::common::{self, StructureError}; /// Struct to store 7zip header info #[derive(Debug, Default, Clone)] pub struct SevenZipHeader { pub header_size: usize, pub major_version: usize, pub minor_version: usize, pub next_header_crc: usize, pub next_header_size: usize, pub next_header_offset: usize, } /// Parse a 7zip header pub fn parse_7z_header(sevenzip_data: &[u8]) -> Result { // Offset & size constants const SEVENZIP_CRC_START: usize = 12; const SEVENZIP_HEADER_SIZE: usize = 32; let sevenzip_structure = vec![ ("magic_p1", "u16"), ("magic_p2", "u32"), ("major_version", "u8"), ("minor_version", "u8"), ("header_crc", "u32"), ("next_header_offset", "u64"), ("next_header_size", "u64"), ("next_header_crc", "u32"), ]; // Parse the 7zip header if let Ok(sevenzip_header) = common::parse(sevenzip_data, &sevenzip_structure, "little") { // Validate header CRC, which is calculated over the 'next_header_offset', 'next_header_size', and 'next_header_crc' values if let Some(crc_data) = sevenzip_data.get(SEVENZIP_CRC_START..SEVENZIP_HEADER_SIZE) { if crc32(crc_data) == (sevenzip_header["header_crc"] as u32) { return Ok(SevenZipHeader { header_size: SEVENZIP_HEADER_SIZE, major_version: sevenzip_header["major_version"], minor_version: sevenzip_header["minor_version"], next_header_crc: sevenzip_header["next_header_crc"], next_header_size: sevenzip_header["next_header_size"], next_header_offset: sevenzip_header["next_header_offset"], }); } } } Err(StructureError) } ================================================ FILE: src/structures/shrs.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store SHRS firmware header info #[derive(Debug, Default, Clone)] pub struct SHRSHeader { pub iv: Vec, pub data_size: usize, pub header_size: usize, } /// Parses an SHRS header pub fn parse_shrs_header(shrs_data: &[u8]) -> Result { const IV_START: usize = 12; const IV_END: usize = IV_START + 16; const HEADER_SIZE: usize = 0x6DC; let shrs_structure = vec![ ("magic", "u32"), ("unknown1", "u32"), ("encrypted_data_size", "u32"), // 16-byte IV immediately follows ]; // Parse the header if let Ok(shrs_header) = common::parse(shrs_data, &shrs_structure, "big") { if let Some(iv_bytes) = shrs_data.get(IV_START..IV_END) { return Ok(SHRSHeader { iv: iv_bytes.to_vec(), data_size: shrs_header["encrypted_data_size"], header_size: HEADER_SIZE, }); } } Err(StructureError) } ================================================ FILE: src/structures/squashfs.rs ================================================ use crate::structures::common::{self, StructureError}; use std::collections::HashMap; /// Stores SquashFS header info #[derive(Debug, Default, Clone)] pub struct SquashFSHeader { pub timestamp: usize, pub block_size: usize, pub image_size: usize, pub header_size: usize, pub inode_count: usize, pub endianness: String, pub compression: usize, pub major_version: usize, pub minor_version: usize, pub uid_table_start: usize, } /// Parse a SquashFS superblock header pub fn parse_squashfs_header(sqsh_data: &[u8]) -> Result { // Size & offset constants const MAX_SQUASHFS_VERSION: u16 = 4; const SQUASHFS_VERSION_END: usize = 30; const SQUASHFS_VERSION_START: usize = 28; const MIN_SQUASHFS_HEADER_SIZE: usize = 120; let squashfs_v4_structure = vec![ ("magic", "u32"), ("inode_count", "u32"), ("modification_time", "u32"), ("block_size", "u32"), ("fragment_count", "u32"), ("compression_id", "u16"), ("block_log", "u16"), ("flags", "u16"), ("id_count", "u16"), ("major_version", "u16"), ("minor_version", "u16"), ("root_inode_ref", "u64"), ("image_size", "u64"), ("uid_start", "u64"), ]; let squashfs_v3_structure = vec![ ("magic", "u32"), ("inode_count", "u32"), ("bytes_used_2", "u32"), ("uid_start_2", "u32"), ("guid_start_2", "u32"), ("inode_table_start_2", "u32"), ("directory_table_start_2", "u32"), ("major_version", "u16"), ("minor_version", "u16"), ("block_size_1", "u16"), ("block_log", "u16"), ("flags", "u8"), ("uid_count", "u8"), ("guid_count", "u8"), ("modification_time", "u32"), ("root_inode_ref", "u64"), ("block_size", "u32"), ("fragment_entry_count", "u32"), ("fragment_table_start_2", "u32"), ("image_size", "u64"), ("uid_start", "u64"), ("guid_start", "u64"), ("inode_table_start", "u64"), ("directory_table_start", "u64"), ("fragment_table_start", "u64"), ("lookup_table_start", "u64"), ]; // Default to little endian let mut sqsh_header = SquashFSHeader { endianness: "little".to_string(), ..Default::default() }; // Make sure there is at least enough data to read in a SquashFS header if sqsh_data.len() > MIN_SQUASHFS_HEADER_SIZE { /* * Regardless of the SquashFS version, the version number is always at the same location in the SquashFS suprblock header. * This can then be reliably used to determine both the SquashFS superblock header version, as well as the endianess used. * Interpret the squashfs major version, assuming little endian. */ let mut squashfs_version: u16 = u16::from_le_bytes( sqsh_data[SQUASHFS_VERSION_START..SQUASHFS_VERSION_END] .try_into() .unwrap(), ); // If the version number doesn't look sane, switch to big endian if squashfs_version == 0 || squashfs_version > MAX_SQUASHFS_VERSION { sqsh_header.endianness = "big".to_string(); squashfs_version = u16::from_be_bytes( sqsh_data[SQUASHFS_VERSION_START..SQUASHFS_VERSION_END] .try_into() .unwrap(), ); } // Sanity check the version number if squashfs_version <= MAX_SQUASHFS_VERSION && squashfs_version > 0 { let squashfs_header_size: usize; let mut squashfs_header: HashMap; // Parse the SquashFS header, using the appropriate version header. if squashfs_version == 4 { squashfs_header_size = common::size(&squashfs_v4_structure); match common::parse(sqsh_data, &squashfs_v4_structure, &sqsh_header.endianness) { Err(e) => { return Err(e); } Ok(squash4_header) => { squashfs_header = squash4_header.clone(); } } } else { squashfs_header_size = common::size(&squashfs_v3_structure); match common::parse(sqsh_data, &squashfs_v3_structure, &sqsh_header.endianness) { Err(e) => { return Err(e); } Ok(squash3_header) => { squashfs_header = squash3_header.clone(); // Adjust the reported header values for v1 and v2 images if squashfs_version < 3 { squashfs_header .insert("uid_start".to_string(), squashfs_header["uid_start_2"]); squashfs_header .insert("guid_start".to_string(), squashfs_header["guid_start_2"]); squashfs_header .insert("image_size".to_string(), squashfs_header["bytes_used_2"]); squashfs_header.insert( "inode_table_start".to_string(), squashfs_header["inode_table_start_2"], ); squashfs_header.insert( "directory_table_start".to_string(), squashfs_header["directory_table_start_2"], ); } } } } // Report the total size of this SquashFS image sqsh_header.image_size = squashfs_header["image_size"]; // Make sure the reported image size is at least bigger than the SquashFS header if sqsh_header.image_size > MIN_SQUASHFS_HEADER_SIZE { // Make sure the block size and block log fields agree if squashfs_header["block_size"] > 0 && squashfs_header["block_log"] == (squashfs_header["block_size"].ilog2() as usize) { // Report relevant squashfs fields sqsh_header.timestamp = squashfs_header["modification_time"]; sqsh_header.block_size = squashfs_header["block_size"]; sqsh_header.header_size = squashfs_header_size; sqsh_header.inode_count = squashfs_header["inode_count"]; sqsh_header.major_version = squashfs_header["major_version"]; sqsh_header.minor_version = squashfs_header["minor_version"]; sqsh_header.uid_table_start = squashfs_header["uid_start"]; // v3 headers don't have a compression ID if squashfs_header.contains_key("compression_id") { sqsh_header.compression = squashfs_header["compression_id"]; } return Ok(sqsh_header); } } } } Err(StructureError) } /// Parse a UID entry for either SquashFSv4 or SquashFSv3 pub fn parse_squashfs_uid_entry( uid_data: &[u8], version: usize, endianness: &str, ) -> Result { let squashfs_v4_uid_table_structure = vec![("uid_block_ptr", "u64")]; let squashfs_v3_uid_table_structure = vec![("uid_block_ptr", "u32")]; // Parse one entry from the UID table if version == 4 { match common::parse(uid_data, &squashfs_v4_uid_table_structure, endianness) { Err(e) => Err(e), Ok(uidv4) => Ok(uidv4["uid_block_ptr"]), } } else { match common::parse(uid_data, &squashfs_v3_uid_table_structure, endianness) { Err(e) => Err(e), Ok(uidv3) => Ok(uidv3["uid_block_ptr"]), } } } ================================================ FILE: src/structures/svg.rs ================================================ use crate::structures::common::StructureError; use aho_corasick::AhoCorasick; const SVG_OPEN_TAG: &[u8] = b""; const SVG_HEAD_MAGIC: &str = "xmlns=\"http://www.w3.org/2000/svg\""; /// Stores info about an SVG image #[derive(Debug, Default, Clone)] pub struct SVGImage { pub total_size: usize, } /// Parse an SVG image to determine its total size pub fn parse_svg_image(svg_data: &[u8]) -> Result { let mut head_tag_count: usize = 0; let mut unclosed_svg_tags: usize = 0; let svg_tags = vec![SVG_OPEN_TAG, SVG_CLOSE_TAG]; let grep = AhoCorasick::new(svg_tags).unwrap(); // Need to search through the data to find all and tags. // There may be multiple of these tags in any given SVG image. for tag_match in grep.find_overlapping_iter(svg_data) { let tag_start: usize = tag_match.start(); match parse_svg_tag(&svg_data[tag_start..]) { Err(_) => { break; } Ok(svg_tag) => { if svg_tag.is_head { head_tag_count += 1; } if svg_tag.is_open { unclosed_svg_tags += 1; } if svg_tag.is_close { unclosed_svg_tags -= 1; } // There should be only one head tag if head_tag_count > 1 { break; } // If one head tag was found and all svg tags are closed, that's EOF if head_tag_count == 1 && unclosed_svg_tags == 0 { return Ok(SVGImage { total_size: tag_start + SVG_CLOSE_TAG.len(), }); } } } } Err(StructureError) } /// Stores info about a parsed SVG tag #[derive(Debug, Default, Clone)] struct SVGTag { pub is_head: bool, pub is_open: bool, pub is_close: bool, } /// Parse an individual SVG tag fn parse_svg_tag(tag_data: &[u8]) -> Result { const END_TAG: u8 = 0x3E; let mut result = SVGTag { ..Default::default() }; let svg_open_tag = String::from_utf8(SVG_OPEN_TAG.to_vec()).unwrap(); let svg_close_tag = String::from_utf8(SVG_CLOSE_TAG.to_vec()).unwrap(); let svg_head_string = SVG_HEAD_MAGIC.to_string(); // Tags are expected to start with '', and end with '>' for i in 0..tag_data.len() { if tag_data[i] == END_TAG { if let Some(tag_bytes) = tag_data.get(0..i + 1) { if let Ok(tag_string) = String::from_utf8(tag_bytes.to_vec()) { result.is_open = tag_string.starts_with(&svg_open_tag); result.is_close = tag_string.starts_with(&svg_close_tag); result.is_head = tag_string.contains(&svg_head_string); return Ok(result); } } } } Err(StructureError) } ================================================ FILE: src/structures/tplink.rs ================================================ use crate::structures::common::{self, StructureError}; /// Stores info about a TP-Link firmware header #[derive(Debug, Default, Clone)] pub struct TPLinkFirmwareHeader { pub header_size: usize, pub kernel_load_address: usize, pub kernel_entry_point: usize, } /// Pase a TP-Link firmware header pub fn parse_tplink_header(tplink_data: &[u8]) -> Result { // Offset of data structure, after firmware signature const STRUCTURE_OFFSET: usize = 0x40; // Total size of the firmware header const HEADER_SIZE: usize = 0x200; // https://github.com/jtreml/firmware-mod-kit/blob/master/src/tpl-tool/doc/Image_layout let tplink_structure = vec![ ("product_id", "u32"), ("product_version", "u32"), ("reserved1", "u32"), ("image_checksum_p1", "u64"), ("image_checksum_p2", "u64"), ("reserved2", "u32"), ("kernel_checksum_p1", "u64"), ("kernel_checksum_p2", "u64"), ("reserved3", "u32"), ("kernel_load_address", "u32"), ("kernel_entry_point", "u32"), ("image_length", "u32"), ("kernel_offset", "u32"), ("kernel_length", "u32"), ("rootfs_offset", "u32"), ("rootfs_length", "u32"), ("bootloader_offset", "u32"), ("bootloader_length", "u32"), ("fw_version_major", "u16"), ("fw_version_minor", "u16"), ("fw_version_patch", "u16"), ("reserved4", "u32"), ]; let mut result = TPLinkFirmwareHeader { header_size: HEADER_SIZE, ..Default::default() }; // Sanity check available data if tplink_data.len() >= HEADER_SIZE { if let Some(structure_data) = tplink_data.get(STRUCTURE_OFFSET..) { // Parse the header if let Ok(tplink_header) = common::parse(structure_data, &tplink_structure, "little") { // Make sure the reserved fields are NULL if tplink_header["reserved1"] == 0 && tplink_header["reserved2"] == 0 && tplink_header["reserved3"] == 0 && tplink_header["reserved4"] == 0 { // Unfortunately, most header fields aren't reliably used; these seem to be, so report them result.kernel_entry_point = tplink_header["kernel_entry_point"]; result.kernel_load_address = tplink_header["kernel_load_address"]; return Ok(result); } } } } Err(StructureError) } /// Stores info about a TP-Link RTOS firmware header #[derive(Debug, Default, Clone)] pub struct TPLinkRTOSFirmwareHeader { pub header_size: usize, pub total_size: usize, pub model_number: usize, pub hardware_rev_major: usize, pub hardware_rev_minor: usize, } /// Parse a TP-Link RTOS firmware header pub fn parse_tplink_rtos_header( tplink_data: &[u8], ) -> Result { const HEADER_SIZE: usize = 0x94; const MAGIC2_VALUE: usize = 0x494D4730; const TOTAL_SIZE_OFFSET: usize = 20; let tplink_rtos_structure = vec![ ("magic1", "u32"), ("unknown1", "u64"), ("unknown2", "u64"), ("magic2", "u32"), ("data_size", "u32"), ("model_number", "u16"), ("hardware_revision_major", "u8"), ("hardware_revision_minor", "u8"), ]; if let Ok(header) = common::parse(tplink_data, &tplink_rtos_structure, "big") { if header["magic2"] == MAGIC2_VALUE { return Ok(TPLinkRTOSFirmwareHeader { header_size: HEADER_SIZE, total_size: header["data_size"] + TOTAL_SIZE_OFFSET, model_number: header["model_number"], hardware_rev_major: header["hardware_revision_major"], hardware_rev_minor: header["hardware_revision_minor"], }); } } Err(StructureError) } ================================================ FILE: src/structures/trx.rs ================================================ use crate::structures::common::{self, StructureError}; /// Stores TRX firmware header info #[derive(Debug, Clone, Default)] pub struct TRXHeader { pub version: usize, pub checksum: usize, pub total_size: usize, pub header_size: usize, pub partitions: Vec, } /// Parse a TRX firmware header pub fn parse_trx_header(header_data: &[u8]) -> Result { // TRX comes in two flavors: v1 and v2 const TRX_VERSION_2: usize = 2; let trx_header_structure = vec![ ("magic", "u32"), ("total_size", "u32"), ("crc32", "u32"), ("flags", "u16"), ("version", "u16"), ("partition1_offset", "u32"), ("partition2_offset", "u32"), ("partition3_offset", "u32"), ("partition4_offset", "u32"), ]; let allowed_versions: Vec = vec![1, 2]; // Size of the fixed-length portion of the header structure let mut struct_size: usize = common::size(&trx_header_structure); // Parse the header if let Ok(trx_header) = common::parse(header_data, &trx_header_structure, "little") { // Sanity check partition offsets. Partition offsets may be 0. if trx_header["partition1_offset"] <= trx_header["total_size"] && trx_header["partition2_offset"] <= trx_header["total_size"] && trx_header["partition3_offset"] <= trx_header["total_size"] { // Sanity check the reported total size if trx_header["total_size"] > struct_size { // Sanity check the reported version number if allowed_versions.contains(&trx_header["version"]) { let mut partitions: Vec = vec![]; if trx_header["partition1_offset"] != 0 { partitions.push(trx_header["partition1_offset"]); } if trx_header["partition2_offset"] != 0 { partitions.push(trx_header["partition2_offset"]); } if trx_header["partition3_offset"] != 0 { partitions.push(trx_header["partition3_offset"]); } // Only TRXv2 has a fourth partition entry if trx_header["version"] == TRX_VERSION_2 { if trx_header["partition4_offset"] != 0 { partitions.push(trx_header["partition4_offset"]); } } else { // For TRXv1, this means the real structure size is 4 bytes shorter struct_size -= std::mem::size_of::(); } return Ok(TRXHeader { version: trx_header["version"], checksum: trx_header["crc32"], total_size: trx_header["total_size"], header_size: struct_size, partitions: partitions.clone(), }); } } } } Err(StructureError) } ================================================ FILE: src/structures/ubi.rs ================================================ use crate::common::crc32; use crate::structures::common::{self, StructureError}; /// Stores UBI superblock header info #[derive(Debug, Default, Clone)] pub struct UbiSuperBlockHeader { pub leb_size: usize, pub leb_count: usize, } /// Partially parse a UBI superblock header pub fn parse_ubi_superblock_header(ubi_data: &[u8]) -> Result { // Type & offset constants const MAX_GROUP_TYPE: usize = 2; const CRC_START_OFFSET: usize = 8; const SUPERBLOCK_NODE_TYPE: usize = 6; // There are some other fields in the superblock header that we don't parse because we don't really care about them... const SUPERBLOCK_STRUCTURE_EXTRA_SIZE: usize = 3968; let ubi_sb_structure = vec![ ("magic", "u32"), ("header_crc", "u32"), ("sequence_number", "u64"), ("node_len", "u32"), ("node_type", "u8"), ("group_type", "u8"), ("padding1", "u32"), ("key_hash", "u8"), ("key_format", "u8"), ("flags", "u32"), ("min_io_size", "u32"), ("leb_size", "u32"), ("leb_count", "u32"), ("max_leb_count", "u32"), ("max_bud_bytes", "u64"), ("log_lebs", "u32"), ("lpt_lebs", "u32"), ("orph_lebs", "u32"), ("jhead_count", "u32"), ("fanout", "u32"), ("lsave_count", "u32"), ("fmt_version", "u32"), ("default_compression", "u16"), ("padding2", "u16"), ("rp_uid", "u32"), ("rp_gid", "u32"), ("rp_size", "u64"), ("time_gran", "u32"), ("uuid_p1", "u64"), ("uuid_p2", "u64"), ("ro_compat_version", "u32"), ]; let sb_struct_size: usize = common::size(&ubi_sb_structure) + SUPERBLOCK_STRUCTURE_EXTRA_SIZE; // Parse the UBI superblock header if let Ok(sb_header) = common::parse(ubi_data, &ubi_sb_structure, "little") { // Make sure the padding fields are NULL if sb_header["padding1"] == 0 && sb_header["padding2"] == 0 { // Make sure the node type is SUPERBLOCK if sb_header["node_type"] == SUPERBLOCK_NODE_TYPE { // Make sure the group type is valid if sb_header["group_type"] <= MAX_GROUP_TYPE { // Validate the header CRC, which is calculated over the entire header except for the magic bytes and CRC field if let Some(crc_data) = ubi_data.get(CRC_START_OFFSET..sb_struct_size) { if ubi_crc(crc_data) == sb_header["header_crc"] { return Ok(UbiSuperBlockHeader { leb_size: sb_header["leb_size"], leb_count: sb_header["leb_count"], }); } } } } } } Err(StructureError) } /// Stores info about a UBI erase count header #[derive(Debug, Default, Clone)] pub struct UbiECHeader { pub version: usize, pub data_offset: usize, pub volume_id_offset: usize, } /// Parse a UBI erase count header pub fn parse_ubi_ec_header(ubi_data: &[u8]) -> Result { let ubi_ec_structure = vec![ ("magic", "u32"), ("version", "u8"), ("padding1", "u24"), ("ec", "u64"), ("volume_id_header_offset", "u32"), ("data_offset", "u32"), ("image_sequence_number", "u32"), ("padding2", "u64"), ("padding3", "u64"), ("padding4", "u64"), ("padding5", "u64"), ("header_crc", "u32"), ]; let ec_header_size: usize = common::size(&ubi_ec_structure); let crc_data_size: usize = ec_header_size - std::mem::size_of::(); // Parse the first half of the header if let Ok(ubi_ec_header) = common::parse(ubi_data, &ubi_ec_structure, "big") { // Offsets should be beyond the EC header if ubi_ec_header["data_offset"] >= ec_header_size && ubi_ec_header["volume_id_header_offset"] >= ec_header_size { // Validate the header CRC if let Some(crc_data) = ubi_data.get(0..crc_data_size) { if ubi_crc(crc_data) == ubi_ec_header["header_crc"] { return Ok(UbiECHeader { version: ubi_ec_header["version"], data_offset: ubi_ec_header["data_offset"], volume_id_offset: ubi_ec_header["volume_id_header_offset"], }); } } } } Err(StructureError) } /// Dummy structure indicating a UBI volume header was parsed successfully #[derive(Debug, Default, Clone)] pub struct UbiVolumeHeader; /// Parse a UBI volume header pub fn parse_ubi_volume_header(ubi_data: &[u8]) -> Result { let ubi_vol_structure = vec![ ("magic", "u32"), ("version", "u8"), ("volume_type", "u8"), ("copy_flag", "u8"), ("compat_type", "u8"), ("volume_id", "u32"), ("logical_erase_block_number", "u32"), ("padding1", "u32"), ("data_size", "u32"), ("used_erase_block_count", "u32"), ("data_padding_size", "u32"), ("data_crc", "u32"), ("padding2", "u32"), ("sequence_number", "u64"), ("padding3", "u64"), ("padding4", "u32"), ("header_crc", "u32"), ]; let vol_header_size: usize = common::size(&ubi_vol_structure); let crc_data_size: usize = vol_header_size - std::mem::size_of::(); // Parse the volume header if let Ok(ubi_vol_header) = common::parse(ubi_data, &ubi_vol_structure, "big") { // Sanity check padding fields, they should all be null if ubi_vol_header["padding1"] == 0 && ubi_vol_header["padding2"] == 0 && ubi_vol_header["padding3"] == 0 && ubi_vol_header["padding4"] == 0 { // Validate the header CRC if let Some(crc_data) = ubi_data.get(0..crc_data_size) { if ubi_crc(crc_data) == ubi_vol_header["header_crc"] { return Ok(UbiVolumeHeader); } } } } Err(StructureError) } /// Calculate a UBI checksum fn ubi_crc(data: &[u8]) -> usize { const UBI_CRC_INIT: u32 = 0xFFFFFFFF; ((!crc32(data)) & UBI_CRC_INIT) as usize } ================================================ FILE: src/structures/uefi.rs ================================================ use crate::structures::common::{self, StructureError}; /// Stores info about a UEFI volume header #[derive(Debug, Default, Clone)] pub struct UEFIVolumeHeader { pub header_crc: usize, pub header_size: usize, pub volume_size: usize, } /// Parse a UEFI volume header pub fn parse_uefi_volume_header(uefi_data: &[u8]) -> Result { // The revision field must be 1 or 2 let valid_revisions: Vec = vec![1, 2]; let uefi_pi_header_structure = vec![ ("volume_size", "u64"), ("magic", "u32"), ("attributes", "u32"), ("header_size", "u16"), ("header_crc", "u16"), ("extended_header_offset", "u16"), ("reserved", "u8"), ("revision", "u8"), ]; // Parse the volume header if let Ok(uefi_volume_header) = common::parse(uefi_data, &uefi_pi_header_structure, "little") { // Make sure the header size is sane (must be smaller than the total volume size) if uefi_volume_header["header_size"] < uefi_volume_header["volume_size"] { // The reserved field *must* be 0 if uefi_volume_header["reserved"] == 0 { // The revision number must be 1 or 2 if valid_revisions.contains(&uefi_volume_header["revision"]) { return Ok(UEFIVolumeHeader { // TODO: Validate UEFI header CRC header_crc: uefi_volume_header["header_crc"], header_size: uefi_volume_header["header_size"], volume_size: uefi_volume_header["volume_size"], }); } } } } Err(StructureError) } /// Stores info about a UEFI capsule header #[derive(Debug, Default, Clone)] pub struct UEFICapsuleHeader { pub total_size: usize, pub header_size: usize, } /// Parse UEFI capsule header pub fn parse_uefi_capsule_header(uefi_data: &[u8]) -> Result { let uefi_capsule_structure = vec![ ("guid_p1", "u64"), ("guid_p2", "u64"), ("header_size", "u32"), ("flags", "u32"), ("total_size", "u32"), ]; // Parse the capsule header if let Ok(capsule_header) = common::parse(uefi_data, &uefi_capsule_structure, "little") { // Sanity check on header and total size fields if capsule_header["header_size"] < capsule_header["total_size"] { return Ok(UEFICapsuleHeader { total_size: capsule_header["total_size"], header_size: capsule_header["header_size"], }); } } Err(StructureError) } ================================================ FILE: src/structures/uimage.rs ================================================ use crate::common::{crc32, get_cstring}; use crate::structures::common::{self, StructureError}; use std::collections::HashMap; /// Stores info about a uImage header #[derive(Debug, Default, Clone)] pub struct UImageHeader { pub header_size: usize, pub name: String, pub data_size: usize, pub data_checksum: usize, pub load_address: usize, pub entry_point_address: usize, pub timestamp: usize, pub compression_type: String, pub cpu_type: String, pub os_type: String, pub image_type: String, pub header_crc_valid: bool, } /// Pase a uImage header pub fn parse_uimage_header(uimage_data: &[u8]) -> Result { const UIMAGE_HEADER_SIZE: usize = 64; const UIMAGE_NAME_OFFSET: usize = 32; let uimage_structure = vec![ ("magic", "u32"), ("header_crc", "u32"), ("creation_timestamp", "u32"), ("data_size", "u32"), ("load_address", "u32"), ("entry_point_address", "u32"), ("data_crc", "u32"), ("os_type", "u8"), ("cpu_type", "u8"), ("image_type", "u8"), ("compression_type", "u8"), ]; let valid_os_types = HashMap::from([ (1, "OpenBSD"), (2, "NetBSD"), (3, "FreeBSD"), (4, "4.4BSD"), (5, "Linux"), (6, "SVR4"), (7, "Esix"), (8, "Solaris"), (9, "Irix"), (10, "SCO"), (11, "Dell"), (12, "NCR"), (13, "LynxOS"), (14, "VxWorks"), (15, "pSOS"), (16, "QNX"), (17, "Firmware"), (18, "RTEMS"), (19, "ARTOS"), (20, "Unity OS"), (21, "INTEGRITY"), (22, "OSE"), (23, "Plan 9"), (24, "OpenRTOS"), (25, "ARM Trusted Firmware"), (26, "Trusted Execution Environment"), (27, "OpenSBI"), (28, "EFI Firmware"), (29, "ELF Image"), ]); let valid_cpu_types = HashMap::from([ (1, "Alpha"), (2, "ARM"), (3, "Intel x86"), (4, "IA64"), (5, "MIPS32"), (6, "MIPS64"), (7, "PowerPC"), (8, "IBM S390"), (10, "SuperH"), (11, "Sparc"), (12, "Sparc64"), (13, "M68K"), (14, "Nios-32"), (15, "MicroBlaze"), (16, "Nios-II"), (17, "Blackfin"), (18, "AVR32"), (19, "ST200"), (20, "Sandbox"), (21, "NDS32"), (22, "OpenRISC"), (23, "ARM64"), (24, "ARC"), (25, "x86-64"), (26, "Xtensa"), (27, "RISC-V"), ]); let valid_compression_types = HashMap::from([ (0, "none"), (1, "gzip"), (2, "bzip2"), (3, "lzma"), (4, "lzo"), (5, "lz4"), (6, "zstd"), ]); let valid_image_types = HashMap::from([ (1, "Standalone Program"), (2, "OS Kernel Image"), (3, "RAMDisk Image"), (4, "Multi-File Image"), (5, "Firmware Image"), (6, "Script file"), (7, "Filesystem Image"), (8, "Binary Flat Device Tree Blob"), (9, "Kirkwood Boot Image"), (10, "Freescale IMXBoot Image"), (11, "Davinci UBL Image"), (12, "TI OMAP Config Header Image"), (13, "TI Davinci AIS Image"), (14, "OS Kernel Image"), (15, "Freescale PBL Boot Image"), (16, "Freescale MXSBoot Image"), (17, "TI Keystone GPHeader Image"), (18, "ATMEL ROM bootable Image"), (19, "Altera SOCFPGA CV/AV Preloader"), (20, "x86 setup.bin Image"), (21, "x86 setup.bin Image"), (22, "A list of typeless images"), (23, "Rockchip Boot Image"), (24, "Rockchip SD card"), (25, "Rockchip SPI image"), (26, "Xilinx Zynq Boot Image"), (27, "Xilinx ZynqMP Boot Image"), (28, "Xilinx ZynqMP Boot Image (bif)"), (29, "FPGA Image"), (30, "VYBRID .vyb Image"), (31, "Trusted Execution Environment OS Image"), (32, "Firmware Image with HABv4 IVT"), (33, "TI Power Management Micro-Controller Firmware"), (34, "STMicroelectronics STM32 Image"), (35, "Altera SOCFPGA A10 Preloader"), (36, "MediaTek BootROM loadable Image"), (37, "Freescale IMX8MBoot Image"), (38, "Freescale IMX8Boot Image"), (39, "Coprocessor Image for remoteproc"), (40, "Allwinner eGON Boot Image"), (41, "Allwinner TOC0 Boot Image"), (42, "Binary Flat Device Tree Blob in a Legacy Image"), (43, "Renesas SPKG image"), (44, "StarFive SPL image"), ]); // Parse the first half of the header if let Ok(uimage_header) = common::parse(uimage_data, &uimage_structure, "big") { // Sanity check header fields if valid_os_types.contains_key(&uimage_header["os_type"]) && valid_cpu_types.contains_key(&uimage_header["cpu_type"]) && valid_image_types.contains_key(&uimage_header["image_type"]) && valid_compression_types.contains_key(&uimage_header["compression_type"]) { // Get the header bytes to validate the CRC if let Some(crc_data) = uimage_data.get(0..UIMAGE_HEADER_SIZE) { return Ok(UImageHeader { header_size: UIMAGE_HEADER_SIZE, name: get_cstring(&uimage_data[UIMAGE_NAME_OFFSET..]), data_size: uimage_header["data_size"], data_checksum: uimage_header["data_crc"], timestamp: uimage_header["creation_timestamp"], load_address: uimage_header["load_address"], entry_point_address: uimage_header["entry_point_address"], compression_type: valid_compression_types[&uimage_header["compression_type"]] .to_string(), cpu_type: valid_cpu_types[&uimage_header["cpu_type"]].to_string(), os_type: valid_os_types[&uimage_header["os_type"]].to_string(), image_type: valid_image_types[&uimage_header["image_type"]].to_string(), header_crc_valid: calculate_uimage_header_checksum(crc_data) == uimage_header["header_crc"], }); } } } Err(StructureError) } /// uImage checksum calculator fn calculate_uimage_header_checksum(hdr: &[u8]) -> usize { const HEADER_CRC_START: usize = 4; const HEADER_CRC_END: usize = 8; // Header checksum has to be nulled out to calculate the CRC let mut uimage_header: Vec = hdr.to_vec(); for crc_byte in uimage_header .iter_mut() .take(HEADER_CRC_END) .skip(HEADER_CRC_START) { *crc_byte = 0; } crc32(&uimage_header) as usize } ================================================ FILE: src/structures/vxworks.rs ================================================ use crate::structures::common::{self, StructureError}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Stores info about a single VxWorks symbol table entry #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct VxWorksSymbolTableEntry { pub size: usize, pub name: usize, pub value: usize, pub symtype: String, } /// Parse a single VxWorks symbol table entry pub fn parse_symtab_entry( symbol_data: &[u8], endianness: &str, ) -> Result { // This *seems* to be the correct structure for a symbol table entry, it may be different for different VxWorks versions... let symtab_structure = vec![ ("name_ptr", "u32"), ("value_ptr", "u32"), ("type", "u32"), ("group", "u32"), ]; // There may be more types; these are the only ones I've found in the wild let allowed_symbol_types: HashMap = HashMap::from([ (0x500, "function".to_string()), (0x700, "initialized data".to_string()), (0x900, "uninitialized data".to_string()), ]); let symtab_structure_size: usize = common::size(&symtab_structure); // Parse the symbol table entry if let Ok(symbol_entry) = common::parse(symbol_data, &symtab_structure, endianness) { // Sanity check expected values in the symbol table entry if allowed_symbol_types.contains_key(&symbol_entry["type"]) && symbol_entry["name_ptr"] != 0 && symbol_entry["value_ptr"] != 0 { return Ok(VxWorksSymbolTableEntry { size: symtab_structure_size, name: symbol_entry["name_ptr"], value: symbol_entry["value_ptr"], symtype: allowed_symbol_types[&symbol_entry["type"]].clone(), }); } } Err(StructureError) } /// Detect a symbol table entry's endianness pub fn get_symtab_endianness(symbol_data: &[u8]) -> Result { const TYPE_FIELD_OFFSET: usize = 9; let mut endianness = "little"; // The type field starts at offset 8 and is 0x00_00_05_00, so for big endian targets the 9th byte will be NULL if let Some(offset_field) = symbol_data.get(TYPE_FIELD_OFFSET) { if *offset_field == 0 { endianness = "big"; } return Ok(endianness.to_string()); } Err(StructureError) } ================================================ FILE: src/structures/wince.rs ================================================ use crate::structures::common::{self, StructureError}; /// Struct to store WindowsCE header info #[derive(Debug, Default, Clone)] pub struct WinCEHeader { pub base_address: usize, pub image_size: usize, pub header_size: usize, } /// Parses a Windows CE header pub fn parse_wince_header(wince_data: &[u8]) -> Result { let wince_header_structure = vec![ ("magic_p1", "u32"), ("magic_p2", "u24"), ("image_start", "u32"), ("image_size", "u32"), ]; // Parse the WinCE header if let Ok(wince_header) = common::parse(wince_data, &wince_header_structure, "little") { return Ok(WinCEHeader { base_address: wince_header["image_start"], image_size: wince_header["image_size"], header_size: common::size(&wince_header_structure), }); } Err(StructureError) } /// Struct to store WindowsCE block info #[derive(Debug, Default, Clone)] pub struct WinCEBlock { pub address: usize, pub data_size: usize, pub header_size: usize, } /// Parse a WindowsCE block header pub fn parse_wince_block_header(block_data: &[u8]) -> Result { let wince_block_structure = vec![("address", "u32"), ("size", "u32"), ("checksum", "u32")]; if let Ok(block_header) = common::parse(block_data, &wince_block_structure, "little") { return Ok(WinCEBlock { address: block_header["address"], data_size: block_header["size"], header_size: common::size(&wince_block_structure), }); } Err(StructureError) } ================================================ FILE: src/structures/xz.rs ================================================ use crate::common::crc32; use crate::structures::common::{self, StructureError}; /// Parse and validate an XZ header, returns the header size pub fn parse_xz_header(xz_data: &[u8]) -> Result { const XZ_CRC_END: usize = 8; const XZ_CRC_START: usize = 6; const XZ_HEADER_SIZE: usize = 12; let xz_structure = vec![ ("magic_p1", "u32"), ("magic_p2", "u16"), ("flags", "u16"), ("header_crc", "u32"), ]; if let Ok(xz_header) = common::parse(xz_data, &xz_structure, "little") { if let Some(crc_data) = xz_data.get(XZ_CRC_START..XZ_CRC_END) { if crc32(crc_data) == (xz_header["header_crc"] as u32) { return Ok(XZ_HEADER_SIZE); } } } Err(StructureError) } ================================================ FILE: src/structures/yaffs.rs ================================================ use crate::structures::common::{self, StructureError}; /// Stores info about a YAFFS object #[derive(Debug, Default, Clone)] pub struct YAFFSObject { // All that is needed for now is the object type; this may be updated in the future as necessary pub obj_type: usize, } /// Partially parse a YAFFS object header pub fn parse_yaffs_obj_header( header_data: &[u8], endianness: &str, ) -> Result { // The name checksum field is unused and should be 0xFFFF const UNUSED: usize = 0xFFFF; // First part of an object header let yaffs_object_structure = vec![ ("type", "u32"), ("parent_id", "u32"), ("name_checksum", "u16"), ]; // Allowed object types let allowed_types: Vec = vec![0, 1, 2, 3, 4, 5]; // Parse the object header if let Ok(obj_header) = common::parse(header_data, &yaffs_object_structure, endianness) { // Validate that the header looks sane if allowed_types.contains(&obj_header["type"]) && (obj_header["parent_id"] > 0) && (obj_header["name_checksum"] == UNUSED) { return Ok(YAFFSObject { obj_type: obj_header["type"], }); } } Err(StructureError) } /// Stores info about a YAFFS file header #[derive(Debug, Default, Clone)] pub struct YAFFSFileHeader { // Only this field is needed, for now. Struct may be updated in the future if necessary. pub file_size: usize, } /// Partially parse a YAFFS file header pub fn parse_yaffs_file_header( header_data: &[u8], endianness: &str, ) -> Result { // Second part of an object header (after the name field) let yaffs_file_info = vec![ ("mode", "u32"), ("uid", "u32"), ("gid", "u32"), ("atime", "u32"), ("mtime", "u32"), ("ctime", "u32"), ("file_size", "u32"), ]; if let Ok(file_info) = common::parse(header_data, &yaffs_file_info, endianness) { return Ok(YAFFSFileHeader { file_size: file_info["file_size"], }); } Err(StructureError) } ================================================ FILE: src/structures/zip.rs ================================================ use crate::structures::common::{self, StructureError}; #[derive(Debug, Default, Clone)] pub struct ZipFileHeader { pub data_size: usize, pub header_size: usize, pub total_size: usize, pub version_major: usize, pub version_minor: usize, } /// Validate a ZIP file header pub fn parse_zip_header(zip_data: &[u8]) -> Result { // Unused flag bits const UNUSED_FLAGS_MASK: usize = 0b11010111_10000000; // Encrypted compression type const COMPRESSION_ENCRYPTED: usize = 99; let zip_local_file_structure = vec![ ("magic", "u32"), ("version", "u16"), ("flags", "u16"), ("compression", "u16"), ("modification_time", "u16"), ("modification_date", "u16"), ("crc", "u32"), ("compressed_size", "u32"), ("uncompressed_size", "u32"), ("file_name_len", "u16"), ("extra_field_len", "u16"), ]; let allowed_compression_methods: Vec = vec![ 0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 14, 18, 19, 20, 93, 94, 95, 96, 97, 98, COMPRESSION_ENCRYPTED, ]; let mut result = ZipFileHeader { ..Default::default() }; // Parse the ZIP local file structure if let Ok(zip_local_file_header) = common::parse(zip_data, &zip_local_file_structure, "little") { // Unused/reserved flag bits should be 0 if (zip_local_file_header["flags"] & UNUSED_FLAGS_MASK) == 0 { // Specified compression method should be one of the defined ZIP compression methods if allowed_compression_methods.contains(&zip_local_file_header["compression"]) { result.version_major = zip_local_file_header["version"] / 10; result.version_minor = zip_local_file_header["version"] % 10; result.header_size = common::size(&zip_local_file_structure) + zip_local_file_header["file_name_len"] + zip_local_file_header["extra_field_len"]; result.data_size = if zip_local_file_header["compressed_size"] > 0 { zip_local_file_header["compressed_size"] } else { zip_local_file_header["uncompressed_size"] }; result.total_size = result.header_size + result.data_size; return Ok(result); } } } Err(StructureError) } /// Stores info about a ZIP end-of-central-directory header #[derive(Debug, Default, Clone)] pub struct ZipEOCDHeader { pub size: usize, pub file_count: usize, } /// Parse a ZIP end-of-central-directory header pub fn parse_eocd_header(eocd_data: &[u8]) -> Result { let zip_eocd_structure = vec![ ("magic", "u32"), ("disk_number", "u16"), ("central_directory_disk_number", "u16"), ("central_directory_disk_entries", "u16"), ("central_directory_total_entries", "u16"), ("central_directory_size", "u32"), ("central_directory_offset", "u32"), ("comment_length", "u16"), ]; // Parse the EOCD header if let Ok(zip_eocd_header) = common::parse(eocd_data, &zip_eocd_structure, "little") { // Assume there is only one "disk", so disk entries and total entries should be the same, and the ZIP archive should contain at least one file if zip_eocd_header["central_directory_disk_entries"] == zip_eocd_header["central_directory_total_entries"] && zip_eocd_header["central_directory_total_entries"] > 0 { // An optional comment may follow the EOCD header; include the comment length in the offset of the ZIP EOF offset let zip_eof: usize = common::size(&zip_eocd_structure) + zip_eocd_header["comment_length"]; return Ok(ZipEOCDHeader { size: zip_eof, file_count: zip_eocd_header["central_directory_total_entries"], }); } } Err(StructureError) } ================================================ FILE: src/structures/zstd.rs ================================================ use crate::structures::common::{self, StructureError}; /// Stores info about a ZSTD file header #[derive(Debug, Default, Clone)] pub struct ZSTDHeader { pub fixed_header_size: usize, pub dictionary_id_flag: usize, pub content_checksum_present: bool, pub single_segment_flag: bool, pub frame_content_flag: usize, } /// Parse a ZSTD file header pub fn parse_zstd_header(zstd_data: &[u8]) -> Result { // Mask and shift bits const FRAME_UNUSED_BITS_MASK: usize = 0b00011000; const DICTIONARY_ID_MASK: usize = 0b11; const CONTENT_CHECKSUM_MASK: usize = 0b100; const SINGLE_SEGMENT_MASK: usize = 0b100000; const FRAME_CONTENT_MASK: usize = 0b11000000; const FRAME_CONTENT_SHIFT: usize = 6; let zstd_header_structure = vec![("magic", "u32"), ("frame_header_descriptor", "u8")]; let mut zstd_info = ZSTDHeader { fixed_header_size: common::size(&zstd_header_structure), ..Default::default() }; // Parse the ZSTD header if let Ok(zstd_header) = common::parse(zstd_data, &zstd_header_structure, "little") { // Unused bits should be unused if (zstd_header["frame_header_descriptor"] & FRAME_UNUSED_BITS_MASK) == 0 { // Indicates if a dictionary ID field is present, and if so, how big it is zstd_info.dictionary_id_flag = zstd_header["frame_header_descriptor"] & DICTIONARY_ID_MASK; // Indicates if there is a 4-byte checksum present at the end of the compressed block stream zstd_info.content_checksum_present = (zstd_header["frame_header_descriptor"] & CONTENT_CHECKSUM_MASK) != 0; // If this flag is set, then the window descriptor byte is not present zstd_info.single_segment_flag = (zstd_header["frame_header_descriptor"] & SINGLE_SEGMENT_MASK) != 0; // Indicates if the frame content field is present, and if so, how big it is zstd_info.frame_content_flag = (zstd_header["frame_header_descriptor"] & FRAME_CONTENT_MASK) >> FRAME_CONTENT_SHIFT; return Ok(zstd_info); } } Err(StructureError) } /// Stores info about a ZSTD block header #[derive(Debug, Default, Clone)] pub struct ZSTDBlockHeader { pub header_size: usize, pub block_type: usize, pub block_size: usize, pub last_block: bool, } /// Parse a ZSTD block header pub fn parse_block_header(block_data: &[u8]) -> Result { // Bit mask constants const ZSTD_BLOCK_TYPE_MASK: usize = 0b110; const ZSTD_BLOCK_TYPE_SHIFT: usize = 1; const ZSTD_RLE_BLOCK_TYPE: usize = 1; const ZSTD_RESERVED_BLOCK_TYPE: usize = 3; const ZSTD_LAST_BLOCK_MASK: usize = 0b1; const ZSTD_BLOCK_SIZE_MASK: usize = 0b1111_1111_1111_1111_1111_1000; const ZSTD_BLOCK_SIZE_SHIFT: usize = 3; let zstd_block_header_structure = vec![("info_bits", "u24")]; let mut block_info = ZSTDBlockHeader { header_size: common::size(&zstd_block_header_structure), ..Default::default() }; // Parse the block header if let Ok(block_header) = common::parse(block_data, &zstd_block_header_structure, "little") { // Interpret the bit fields of the block header, which indicate the type of block, the size of the block, and if this is the last block block_info.last_block = (block_header["info_bits"] & ZSTD_LAST_BLOCK_MASK) != 0; block_info.block_type = (block_header["info_bits"] & ZSTD_BLOCK_TYPE_MASK) >> ZSTD_BLOCK_TYPE_SHIFT; block_info.block_size = (block_header["info_bits"] & ZSTD_BLOCK_SIZE_MASK) >> ZSTD_BLOCK_SIZE_SHIFT; /* * An RLE block consists of a single byte of raw block data, which when decompressed must be repeased block_size times. * We're not decompressing, just want to know the size of the raw data so we can check the next block header. * * Reserved block types should not be encountered, and are considered an error during decompression. */ if block_info.block_type == ZSTD_RLE_BLOCK_TYPE { block_info.block_size = 1; } // Block type is invalid if set to the reserved block type if block_info.block_type != ZSTD_RESERVED_BLOCK_TYPE { return Ok(block_info); } } Err(StructureError) } ================================================ FILE: src/structures.rs ================================================ //! # Data Structure Parsing //! //! Both signatures and internal extractors may need to parse data structures used by various file formats. //! Structure parsing code is placed in the `structures` module. //! //! ## Helper Functions //! //! There are some definitions and helper functions in `structures::common` that are generally helpful for processing data structures. //! //! The `structures::common::parse` function provides a way to parse basic data structures by defining the data structure format, //! the endianness to use, and the data to cast the structure over. It is heavily used by most structure parsers. //! It supports the following data types: //! //! - u8 //! - u16 //! - u24 //! - u32 //! - u64 //! //! Regardless of the data type specified, all values are returned as `usize` types. //! If an error occurs (typically due to not enough data available to process the specified data structure), `Err(structures::common::StructureError)` is returned. //! //! The `structures::common::size` function is a convenience function that returns the number of bytes required to parse a defined data structure. //! //! The `structures::common::StructureError` struct is typically used by structure parsers to return an error. //! //! ## Writing a Structure Parser //! //! Structure parsers may be defined however they need to be; there are no strict rules here. //! Generally, however, they should: //! //! - Accept some data to parse //! - Parse the data structure //! - Validate the structure fields for correctness //! - Return an error or success status //! //! ### Example //! //! To write a structure parser for a simple, fictional, `FooBar` file header: //! //! ```no_run //! use binwalk::common::{crc32, get_cstring}; //! use binwalk::structures::common::{self, StructureError}; //! //! #[derive(Debug, Default, Clone)] //! pub struct FooBarHeader { //! pub data_crc: usize, //! pub data_size: usize, //! pub header_size: usize, //! pub original_file_name: String, //! } //! //! /// This function parses and validates the FooBar file header. //! /// It returns a FooBarHeader struct on success, StructureError on failure. //! fn parse_foobar_header(foobar_data: &[u8]) -> Result { //! // The header CRC is calculated over the first 13 bytes of the header (everything except the header_crc field) //! const HEADER_CRC_LEN: usize = 13; //! //! // Define a data structure; structure members must be in the order in which they appear in the data //! let foobar_struct = vec![ //! ("magic", "u32"), //! ("flags", "u8"), //! ("data_size", "u32"), //! ("data_crc", "u32"), //! ("header_crc", "u32"), //! // NULL-terminated original file name immediately follows the header structure //! ]; //! //! let struct_size: usize = common::size(&foobar_struct); //! //! // Parse the provided data in accordance with the layout defined in foobar_struct, interpret fields as little endian //! if let Ok(foobar_header) = common::parse(foobar_data, &foobar_struct, "little") { //! //! if let Some(crc_data) = foobar_data.get(0..HEADER_CRC_LEN) { //! // Validate the header CRC //! if foobar_header["header_crc"] == (crc32(crc_data) as usize) { //! // Get the NULL-terminated file name that immediately follows the header structure //! if let Some(file_name_bytes) = foobar_data.get(struct_size..) { //! let file_name = get_cstring(file_name_bytes); //! //! // The file name should be non-zero in length //! if file_name.len() > 0 { //! return Ok(FooBarHeader{ //! data_crc: foobar_header["data_crc"], //! data_size: foobar_header["data_size"], //! header_size: struct_size + file_name.len() + 1, // Total header size is structure size + name length + NULL byte //! original_file_name: file_name.clone(), //! }); //! } //! } //! } //! } //! } //! //! return Err(StructureError); //! } //! ``` pub mod android_bootimg; pub mod androidsparse; pub mod apfs; pub mod arj; pub mod autel; pub mod binhdr; pub mod bmp; pub mod btrfs; pub mod cab; pub mod chk; pub mod common; pub mod cpio; pub mod cramfs; pub mod csman; pub mod deb; pub mod dkbs; pub mod dlink_tlv; pub mod dlob; pub mod dmg; pub mod dms; pub mod dpapi; pub mod dtb; pub mod dxbc; pub mod efigpt; pub mod elf; pub mod ext; pub mod fat; pub mod gif; pub mod gzip; pub mod iso9660; pub mod jboot; pub mod jffs2; pub mod linux; pub mod logfs; pub mod luks; pub mod lz4; pub mod lzfse; pub mod lzma; pub mod lzop; pub mod matter_ota; pub mod mbr; pub mod mh01; pub mod ntfs; pub mod openssl; pub mod packimg; pub mod pcap; pub mod pchrom; pub mod pe; pub mod png; pub mod qcow; pub mod qnx; pub mod rar; pub mod riff; pub mod romfs; pub mod rtk; pub mod seama; pub mod sevenzip; pub mod shrs; pub mod squashfs; pub mod svg; pub mod tplink; pub mod trx; pub mod ubi; pub mod uefi; pub mod uimage; pub mod vxworks; pub mod wince; pub mod xz; pub mod yaffs; pub mod zip; pub mod zstd; ================================================ FILE: tests/arcadyan.rs ================================================ mod common; #[test] fn integration_test() { const SIGNATURE_TYPE: &str = "arcadyan"; const INPUT_FILE_NAME: &str = "arcadyan.bin"; common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME); } ================================================ FILE: tests/arj.rs ================================================ use crate::common::assert_results_ok; mod common; #[test] fn integration_test_valid_arj() { const SIGNATURE_TYPE: &str = "arj"; const INPUT_FILE_NAME: &str = "arj.bin"; let expected_signature_offsets: Vec = vec![0xD, 0x46]; let expected_extraction_offsets: Vec = vec![0xD]; let results = common::run_binwalk(SIGNATURE_TYPE, INPUT_FILE_NAME); assert_results_ok( results, expected_signature_offsets, expected_extraction_offsets, ) } ================================================ FILE: tests/bmp.rs ================================================ mod common; #[test] fn integration_test() { const SIGNATURE_TYPE: &str = "bmp"; const INPUT_FILE_NAME: &str = "bmp.bin"; let expected_signature_offsets: Vec = vec![0xB7F94, 0x10AFEC]; let expected_extraction_offsets: Vec = vec![0xB7F94, 0x10AFEC]; let results = common::run_binwalk(SIGNATURE_TYPE, INPUT_FILE_NAME); common::assert_results_ok( results, expected_signature_offsets, expected_extraction_offsets, ); } ================================================ FILE: tests/bzip2.rs ================================================ mod common; #[test] fn integration_test() { const SIGNATURE_TYPE: &str = "bzip2"; const INPUT_FILE_NAME: &str = "bzip2.bin"; common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME); } ================================================ FILE: tests/common/mod.rs ================================================ use binwalk::{AnalysisResults, Binwalk}; /// Convenience function for running an integration test against the specified file, with the provided signature filter. /// Assumes that there will be one signature result and one extraction result at file offset 0. #[allow(dead_code)] pub fn integration_test(signature_filter: &str, file_name: &str) { let expected_signature_offsets: Vec = vec![0]; let expected_extraction_offsets: Vec = vec![0]; // Run binwalk, get analysis/extraction results let results = run_binwalk(signature_filter, file_name); // Assert that there was a valid signature and successful result at, and only at, file offset 0 assert_results_ok( results, expected_signature_offsets, expected_extraction_offsets, ); } /// Assert that there was a valid signature match and corresponding extraction at, and only at, the specified file offsets pub fn assert_results_ok( results: AnalysisResults, signature_offsets: Vec, extraction_offsets: Vec, ) { // Assert that the number of signature results and extractions match the expected results assert!(results.file_map.len() == signature_offsets.len()); assert!(results.extractions.len() == extraction_offsets.len()); // Assert that each signature match was at an expected offset and that extraction, if expected, was successful for signature_result in results.file_map { assert!(signature_offsets.contains(&signature_result.offset)); if extraction_offsets.contains(&signature_result.offset) { assert!(results.extractions[&signature_result.id].success); } } } /// Run Binwalk, with extraction, against the specified file, with the provided signature filter pub fn run_binwalk(signature_filter: &str, file_name: &str) -> AnalysisResults { // Build the path to the input file let file_path = std::path::Path::new("tests") .join("inputs") .join(file_name) .display() .to_string(); // Build the path to the output directory let output_directory = std::path::Path::new("tests") .join("binwalk_integration_test_extractions") .display() .to_string(); // Delete the output directory, if it exists let _ = std::fs::remove_dir_all(&output_directory); // Configure binwalk let binwalker = Binwalk::configure( Some(file_path), Some(output_directory.clone()), Some(vec![signature_filter.to_string()]), None, None, false, ) .expect("Binwalk initialization failed"); // Run analysis let results = binwalker.analyze(&binwalker.base_target_file, true); // Clean up the output directory let _ = std::fs::remove_dir_all(output_directory); results } ================================================ FILE: tests/cramfs.rs ================================================ mod common; #[test] fn integration_test() { const SIGNATURE_TYPE: &str = "cramfs"; const INPUT_FILE_NAME: &str = "cramfs.bin"; common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME); } ================================================ FILE: tests/gzip.rs ================================================ mod common; #[test] fn integration_test() { const SIGNATURE_TYPE: &str = "gzip"; const INPUT_FILE_NAME: &str = "gzip.bin"; common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME); } ================================================ FILE: tests/jpeg.rs ================================================ mod common; #[test] fn integration_test() { const SIGNATURE_TYPE: &str = "jpeg"; const INPUT_FILE_NAME: &str = "jpeg.bin"; let expected_signature_offsets: Vec = vec![0, 0x15BBE]; let expected_extraction_offsets: Vec = vec![0, 0x15BBE]; let results = common::run_binwalk(SIGNATURE_TYPE, INPUT_FILE_NAME); common::assert_results_ok( results, expected_signature_offsets, expected_extraction_offsets, ); } ================================================ FILE: tests/matter_ota.rs ================================================ mod common; #[test] fn integration_test() { const SIGNATURE_TYPE: &str = "matter_ota"; const INPUT_FILE_NAME: &str = "matter_ota.bin"; common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME); } ================================================ FILE: tests/mbr.rs ================================================ mod common; #[test] fn integration_test() { const SIGNATURE_TYPE: &str = "mbr"; const INPUT_FILE_NAME: &str = "mbr.bin"; common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME); } ================================================ FILE: tests/pdf.rs ================================================ mod common; #[test] fn integration_test() { const SIGNATURE_TYPE: &str = "pdf"; const INPUT_FILE_NAME: &str = "pdf.bin"; let expected_signature_offsets: Vec = vec![0]; let expected_extraction_offsets: Vec = vec![]; let results = common::run_binwalk(SIGNATURE_TYPE, INPUT_FILE_NAME); common::assert_results_ok( results, expected_signature_offsets, expected_extraction_offsets, ); } ================================================ FILE: tests/png.rs ================================================ mod common; #[test] fn integration_test() { const SIGNATURE_TYPE: &str = "png"; const INPUT_FILE_NAME: &str = "png_malformed.bin"; let expected_signature_offsets: Vec = vec![]; let expected_extraction_offsets: Vec = vec![]; let results = common::run_binwalk(SIGNATURE_TYPE, INPUT_FILE_NAME); common::assert_results_ok( results, expected_signature_offsets, expected_extraction_offsets, ); } ================================================ FILE: tests/qcow.rs ================================================ mod common; #[test] fn integration_test() { const SIGNATURE_TYPE: &str = "qcow"; const INPUT_FILE_NAME: &str = "qcow.bin"; let expected_signature_offsets: Vec = vec![0]; let expected_extraction_offsets: Vec = vec![]; let results = common::run_binwalk(SIGNATURE_TYPE, INPUT_FILE_NAME); common::assert_results_ok( results, expected_signature_offsets, expected_extraction_offsets, ); } ================================================ FILE: tests/riff.rs ================================================ mod common; #[test] fn integration_test() { const SIGNATURE_TYPE: &str = "riff"; const INPUT_FILE_NAME: &str = "riff.bin"; common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME); } ================================================ FILE: tests/romfs.rs ================================================ mod common; #[test] fn integration_test() { const SIGNATURE_TYPE: &str = "romfs"; const INPUT_FILE_NAME: &str = "romfs.bin"; common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME); } ================================================ FILE: tests/sevenzip.rs ================================================ mod common; #[test] fn integration_test() { const SIGNATURE_TYPE: &str = "7zip"; const INPUT_FILE_NAME: &str = "7z.bin"; common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME); } ================================================ FILE: tests/squashfs.rs ================================================ mod common; #[test] fn integration_test() { const SIGNATURE_TYPE: &str = "squashfs"; const INPUT_FILE_NAME: &str = "squashfs.bin"; common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME); } ================================================ FILE: tests/squashfs_v2.rs ================================================ mod common; #[test] fn integration_test() { const SIGNATURE_TYPE: &str = "squashfs"; const INPUT_FILE_NAME: &str = "squashfs_v2.bin"; common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME); } ================================================ FILE: tests/yaffs2.rs ================================================ mod common; #[test] fn integration_test() { const SIGNATURE_TYPE: &str = "yaffs"; const INPUT_FILE_NAME: &str = "yaffs2.bin"; common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME); } ================================================ FILE: tests/zip.rs ================================================ mod common; #[test] fn integration_test_valid_zip() { const SIGNATURE_TYPE: &str = "zip"; const INPUT_FILE_NAME: &str = "zip.bin"; common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME); } ================================================ FILE: tests/zip_truncated.rs ================================================ mod common; #[test] fn integration_test_truncated_zip() { const SIGNATURE_TYPE: &str = "zip"; const INPUT_FILE_NAME: &str = "zip_truncated.bin"; common::integration_test(SIGNATURE_TYPE, INPUT_FILE_NAME); }