unblob 26.6.4 (the version REA is verified with) is installed from its PyPI wheel into /opt/unblob, with dependencies pinned to its uv.lock at the release commit. A wrapper puts that venv first on PATH, and REA_UNBLOB_COMMAND points REA's extract_firmware at it. Its extractors follow upstream's install-deps.sh: Ubuntu packages, sasquatch v4.5.1-6 (also used by binwalk) and onekey-sec's e2fsprogs fork, whose debugfs rdump options the ext2/3/4 handler needs. The build checks that unblob finds every extractor. The uv.lock converter moves to scripts/ and skips the locked project itself generically, since ghidra-mcp and unblob both use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
426 lines
24 KiB
Docker
426 lines
24 KiB
Docker
# Ghidra from the official GitHub release, checked against the SHA-256 in the
|
|
# release notes: https://github.com/NationalSecurityAgency/ghidra/releases
|
|
# 12.1.4 because that is the version the GhidraMCP release below is built
|
|
# for; Ghidra only loads extensions built for its exact version.
|
|
ARG GHIDRA_VERSION=12.1.4
|
|
ARG GHIDRA_BUILD_DATE=20260921
|
|
ARG GHIDRA_SHA256=ddac49f903da9d5bac833e5cc79395098b9c33cfd3279be5f31bd00387d2d4db
|
|
|
|
|
|
# --- Stage: Ghidra distribution ----------------------------------------------
|
|
# Downloaded once, used by the final image and to build extensions against.
|
|
FROM ubuntu:24.04 AS ghidra-dist
|
|
ARG GHIDRA_VERSION
|
|
ARG GHIDRA_BUILD_DATE
|
|
ARG GHIDRA_SHA256
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends ca-certificates curl unzip \
|
|
&& rm -rf /var/lib/apt/lists/* \
|
|
&& curl -fsSLo /tmp/ghidra.zip \
|
|
"https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${GHIDRA_VERSION}_build/ghidra_${GHIDRA_VERSION}_PUBLIC_${GHIDRA_BUILD_DATE}.zip" \
|
|
&& echo "${GHIDRA_SHA256} /tmp/ghidra.zip" | sha256sum -c - \
|
|
&& unzip -q /tmp/ghidra.zip -d /opt \
|
|
&& rm /tmp/ghidra.zip
|
|
|
|
|
|
# --- Build stage: ESP32 Flash Loader for Ghidra --------------------------------
|
|
# https://github.com/dynacylabs/ghidra-esp32-flash-loader: loads ESP32 flash
|
|
# dumps / app images (partition table, segments at their addresses, chip
|
|
# ROM, peripherals from Espressif's SVD files). Its tags ship no binaries,
|
|
# so it's built against this exact Ghidra. Pinned to a master commit (Ghidra
|
|
# 12 API, newer than the v12.0 tag) and its submodules' commits. Gradle is
|
|
# downloaded directly, checked against the SHA-256 gradle.org publishes,
|
|
# because Ghidra's own wrapper doesn't verify its download.
|
|
FROM ubuntu:24.04 AS esp32-loader-build
|
|
ARG DEBIAN_FRONTEND=noninteractive
|
|
ARG GHIDRA_VERSION
|
|
ARG ESP32_LOADER_COMMIT=59d88034d07010fb3a3601053554baf02e7be2e4
|
|
ARG ESP32_SVD_COMMIT=be20aa12560889d6125d144cdb48cf615ac17628
|
|
ARG GHIDRA_SVD_LOADER_COMMIT=8fb5601c3444111159cf1e056cbe9d8457985237
|
|
ARG GRADLE_VERSION=9.7.1
|
|
ARG GRADLE_SHA256=acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends ca-certificates curl git unzip openjdk-21-jdk-headless \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
COPY --from=ghidra-dist /opt/ghidra_${GHIDRA_VERSION}_PUBLIC /opt/ghidra
|
|
RUN set -e \
|
|
&& fetch() { git init -q "$2"; git -C "$2" fetch -q --depth 1 "$1" "$3"; git -C "$2" checkout -q FETCH_HEAD; } \
|
|
&& fetch https://github.com/dynacylabs/ghidra-esp32-flash-loader.git /src/esp32 "${ESP32_LOADER_COMMIT}" \
|
|
&& rmdir /src/esp32/data/svd /src/esp32/ghidra_scripts \
|
|
&& fetch https://github.com/espressif/svd.git /src/esp32/data/svd "${ESP32_SVD_COMMIT}" \
|
|
&& fetch https://github.com/dynacylabs/ghidra-svd-loader.git /src/esp32/ghidra_scripts "${GHIDRA_SVD_LOADER_COMMIT}" \
|
|
&& curl -fsSLo /tmp/gradle.zip "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" \
|
|
&& echo "${GRADLE_SHA256} /tmp/gradle.zip" | sha256sum -c - \
|
|
&& unzip -q /tmp/gradle.zip -d /opt \
|
|
&& cd /src/esp32 \
|
|
&& GHIDRA_INSTALL_DIR=/opt/ghidra "/opt/gradle-${GRADLE_VERSION}/bin/gradle" --no-daemon -q buildExtension \
|
|
&& unzip -q dist/ghidra_${GHIDRA_VERSION}_PUBLIC_*_esp32.zip -d /out \
|
|
&& grep -qx "version=${GHIDRA_VERSION}" /out/esp32/extension.properties
|
|
|
|
|
|
# --- Build stage: binwalk and its helpers -------------------------------------
|
|
# Based on binwalk's own Dockerfile (https://github.com/ReFirmLabs/binwalk,
|
|
# Dockerfile at the pinned commit), adapted to Ubuntu 24.04 and with every
|
|
# source pinned. Only the results are copied into the final image, so
|
|
# compilers and the Rust toolchain stay out of it.
|
|
FROM ubuntu:24.04 AS binwalk-build
|
|
|
|
ARG DEBIAN_FRONTEND=noninteractive
|
|
# binwalk: last tag (v3.1.0) is from 2024; master is 3.1.1 with later fixes.
|
|
ARG BINWALK_COMMIT=26713972e3f9f52dc37d4a421c4554b0bd9e82ef
|
|
ARG RUST_VERSION=1.99.1
|
|
ARG DUMPIFS_COMMIT=b7bac90e8312eca2796f2003a52791899eb8dcd9
|
|
ARG LZFSE_COMMIT=e634ca58b4821d9f3d560cdc6df5dec02ffc93fd
|
|
ARG DMG2IMG_COMMIT=a3e413489ccdd05431401357bf21690536425012
|
|
ARG VMLINUX_TO_ELF_COMMIT=36da74ded9cd486f294c8852e0dcab9ef6a48c6b
|
|
ARG ESPTOOL_VERSION=5.6.0
|
|
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends \
|
|
ca-certificates curl git build-essential clang pkg-config \
|
|
zlib1g-dev liblzma-dev liblzo2-dev libucl-dev liblz4-dev libbz2-dev \
|
|
libssl-dev libsrecord-dev libfontconfig1-dev \
|
|
python3 python3-venv python3-dev \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Shallow-fetch one exact commit of a git repository.
|
|
RUN printf '%s\n' '#!/bin/sh' 'set -e' \
|
|
'git init -q "$2"' \
|
|
'git -C "$2" fetch -q --depth 1 "$1" "$3"' \
|
|
'git -C "$2" checkout -q FETCH_HEAD' > /usr/local/bin/fetch-commit \
|
|
&& chmod +x /usr/local/bin/fetch-commit
|
|
|
|
# binwalk itself, with a pinned Rust toolchain and the locked crate versions.
|
|
RUN fetch-commit https://github.com/ReFirmLabs/binwalk.git /src/binwalk "${BINWALK_COMMIT}" \
|
|
&& curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain "${RUST_VERSION}" \
|
|
&& . /root/.cargo/env \
|
|
&& cargo build --release --locked --manifest-path /src/binwalk/Cargo.toml \
|
|
&& install -D -m 755 /src/binwalk/target/release/binwalk /out/bin/binwalk
|
|
|
|
# Extractors binwalk calls that Ubuntu doesn't package (upstream src.sh).
|
|
# dumpifs ships a prebuilt binary in its repo; it's removed and rebuilt.
|
|
RUN fetch-commit https://github.com/askac/dumpifs.git /src/dumpifs "${DUMPIFS_COMMIT}" \
|
|
&& rm /src/dumpifs/dumpifs \
|
|
&& make -C /src/dumpifs dumpifs \
|
|
&& fetch-commit https://github.com/lzfse/lzfse.git /src/lzfse "${LZFSE_COMMIT}" \
|
|
&& make -C /src/lzfse install \
|
|
&& fetch-commit https://github.com/Lekensteyn/dmg2img.git /src/dmg2img "${DMG2IMG_COMMIT}" \
|
|
&& make -C /src/dmg2img dmg2img vfdecrypt HAVE_LZFSE=1 \
|
|
&& install -m 755 /src/dumpifs/dumpifs /usr/local/bin/lzfse \
|
|
/src/dmg2img/dmg2img /src/dmg2img/vfdecrypt /out/bin/
|
|
|
|
# Python extractors binwalk calls (upstream requirements.txt, versions
|
|
# pinned), and esptool. Each gets its own venv at its final path; the final
|
|
# image uses the same base and Python, so the venvs can be copied as-is.
|
|
RUN python3 -m venv /opt/binwalk-py \
|
|
&& CC=clang /opt/binwalk-py/bin/pip install --no-cache-dir --disable-pip-version-check \
|
|
uefi_firmware==1.16 jefferson==0.4.8 ubi-reader==0.8.16 \
|
|
lz4==4.4.5 zstandard==0.25.0 \
|
|
"vmlinux-to-elf @ git+https://github.com/marin-m/vmlinux-to-elf@${VMLINUX_TO_ELF_COMMIT}" \
|
|
&& python3 -m venv /opt/esptool \
|
|
&& /opt/esptool/bin/pip install --no-cache-dir --disable-pip-version-check \
|
|
"esptool==${ESPTOOL_VERSION}"
|
|
|
|
# unblob (https://github.com/onekey-sec/unblob), the firmware extractor REA
|
|
# uses for extract_firmware; REA is verified with 26.6.4. Installed from the
|
|
# PyPI wheel (SHA-256 from PyPI) into its own venv, with every dependency
|
|
# pinned to unblob's uv.lock at the release tag's commit, like ghidra-mcp.
|
|
ARG UNBLOB_VERSION=26.6.4
|
|
ARG UNBLOB_COMMIT=1fcc7a0a584a70a96c31f5a276c20944d199a089
|
|
ARG UNBLOB_WHEEL_SHA256=0fc16e9c7dc92fcf86b8b61475700c9dd16e1dddacf34467574ea965a5d3584e
|
|
ARG UNBLOB_UVLOCK_SHA256=0a88ed397d60e0624bdef34c7a0b7329a6d278dfdedb86f940c6ee25401c0317
|
|
COPY scripts/uvlock-to-constraints.py /tmp/uvlock-to-constraints.py
|
|
RUN set -e \
|
|
&& whl="unblob-${UNBLOB_VERSION}-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" \
|
|
&& curl -fsSLo "/tmp/${whl}" "https://files.pythonhosted.org/packages/cp39/u/unblob/${whl}" \
|
|
&& echo "${UNBLOB_WHEEL_SHA256} /tmp/${whl}" | sha256sum -c - \
|
|
&& curl -fsSLo /tmp/uv.lock "https://raw.githubusercontent.com/onekey-sec/unblob/${UNBLOB_COMMIT}/uv.lock" \
|
|
&& echo "${UNBLOB_UVLOCK_SHA256} /tmp/uv.lock" | sha256sum -c - \
|
|
&& python3 -I /tmp/uvlock-to-constraints.py /tmp/uv.lock > /tmp/constraints.txt \
|
|
&& python3 -m venv /opt/unblob \
|
|
&& /opt/unblob/bin/pip install --no-cache-dir --disable-pip-version-check \
|
|
--constraint /tmp/constraints.txt "/tmp/${whl}"
|
|
# (unblob needs libmagic to start, which this stage lacks; the final stage
|
|
# runs it.)
|
|
|
|
|
|
# --- Final image -------------------------------------------------------------
|
|
FROM ubuntu:24.04
|
|
|
|
ARG DEBIAN_FRONTEND=noninteractive
|
|
# Match the host user so files written to mounted volumes keep the right owner.
|
|
ARG USER_NAME=re
|
|
ARG USER_UID=1000
|
|
ARG USER_GID=1000
|
|
|
|
# Fingerprint of Anthropic's apt signing key, from the official install docs:
|
|
# https://code.claude.com/docs/en/desktop-linux
|
|
ARG ANTHROPIC_APT_FPR=31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE
|
|
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends ca-certificates curl gnupg \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Add Anthropic's apt repository and refuse to continue if the key is wrong.
|
|
RUN curl -fsSLo /usr/share/keyrings/claude-desktop-archive-keyring.asc \
|
|
https://downloads.claude.ai/claude-desktop/key.asc \
|
|
&& gpg --show-keys --with-colons /usr/share/keyrings/claude-desktop-archive-keyring.asc \
|
|
| grep -q "^fpr:::::::::${ANTHROPIC_APT_FPR}:" \
|
|
&& echo "deb [arch=amd64,arm64 signed-by=/usr/share/keyrings/claude-desktop-archive-keyring.asc] https://downloads.claude.ai/claude-desktop/apt/stable stable main" \
|
|
> /etc/apt/sources.list.d/claude-desktop.list
|
|
|
|
# Recommends are skipped: they pull in QEMU/OVMF for Cowork, which needs KVM
|
|
# and isn't useful in this container. No keyring is installed: sign-in is not
|
|
# kept between runs. x11-utils provides the display check tools.
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends \
|
|
claude-desktop \
|
|
x11-utils \
|
|
xauth \
|
|
fonts-dejavu-core \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Ghidra (downloaded and verified in the ghidra-dist stage). Ghidra 12.1.4
|
|
# needs JDK 21 (application.java.min=21). The full JDK, not the headless one,
|
|
# because Ghidra's GUI needs the AWT/X11 libraries.
|
|
ARG GHIDRA_VERSION
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends openjdk-21-jdk unzip \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
COPY --from=ghidra-dist /opt/ghidra_${GHIDRA_VERSION}_PUBLIC /opt/ghidra_${GHIDRA_VERSION}_PUBLIC
|
|
RUN ln -s "/opt/ghidra_${GHIDRA_VERSION}_PUBLIC" /opt/ghidra \
|
|
&& ln -s /opt/ghidra/ghidraRun /usr/local/bin/ghidra
|
|
ENV GHIDRA_INSTALL_DIR=/opt/ghidra
|
|
|
|
# Enable the "Developer" plugin package (File > Configure > Developer) in the
|
|
# CodeBrowser. Ghidra copies its default tools into the user's settings on
|
|
# first start, and settings are reset on every container start, so the
|
|
# default CodeBrowser.tool inside the jar is patched instead of user config.
|
|
# A bare <PACKAGE NAME="Developer" /> only adds the package's RELEASED
|
|
# plugins; the snippet also lists the STABLE ones. The build fails if the
|
|
# snippet could not be inserted.
|
|
COPY ghidra/codebrowser-developer.xml /tmp/codebrowser-developer.xml
|
|
RUN set -e; cd /tmp \
|
|
&& jar_file=/opt/ghidra/Ghidra/Configurations/Public_Release/lib/Public_Release.jar \
|
|
&& unzip -q "$jar_file" defaultTools/CodeBrowser.tool \
|
|
&& awk 'FNR==NR { snip = snip $0 "\n"; next } !done && /<ROOT_NODE/ { printf "%s", snip; done = 1 } { print }' \
|
|
/tmp/codebrowser-developer.xml defaultTools/CodeBrowser.tool > /tmp/CodeBrowser.tool \
|
|
&& mv /tmp/CodeBrowser.tool defaultTools/CodeBrowser.tool \
|
|
&& grep -q 'FidDebugPlugin' defaultTools/CodeBrowser.tool \
|
|
&& jar uf "$jar_file" defaultTools/CodeBrowser.tool \
|
|
&& rm -r /tmp/defaultTools /tmp/codebrowser-developer.xml
|
|
|
|
# ESP32 Flash Loader (built above), installed like GhidraMCP into Ghidra's
|
|
# own Extensions directory. It's a loader, not a plugin, so no tool config is
|
|
# needed: it shows up as a format in File > Import File.
|
|
COPY --from=esp32-loader-build /out/esp32 /opt/ghidra/Ghidra/Extensions/esp32
|
|
|
|
# GhidraMCP (https://github.com/LaurieWired/GhidraMCP), official release zip
|
|
# checked against the SHA-256 GitHub lists for it. It holds:
|
|
# - the Ghidra extension, unpacked into Ghidra's own Extensions directory so
|
|
# it is installed for every user. Its GhidraMCPPlugin is a RELEASED plugin
|
|
# in the Developer package, so the CodeBrowser config above loads it. It
|
|
# serves an HTTP backend on 127.0.0.1:8080 while the CodeBrowser is open.
|
|
# - the ghidra-mcp Python MCP server (wheel), installed into its own venv.
|
|
# Claude Desktop starts it over stdio; it forwards calls to that backend.
|
|
# The wheel only gives version ranges, so every dependency is pinned to
|
|
# the version in GhidraMCP's uv.lock at the release commit.
|
|
ARG GHIDRAMCP_VERSION=2.0
|
|
ARG GHIDRAMCP_SHA256=ebf904fd6782e429e95a60a24fe0724b20c0acb2dedb304135a5281f6b5f2e72
|
|
ARG GHIDRAMCP_COMMIT=1e980252087b2479a21b9ca6d82d5829538651a7
|
|
ARG GHIDRAMCP_UVLOCK_SHA256=f80ffe8df8167535f253976a1dfb702a9a7f8836d884084017dd7c04e1444837
|
|
COPY scripts/uvlock-to-constraints.py /tmp/uvlock-to-constraints.py
|
|
RUN set -e \
|
|
&& apt-get update \
|
|
&& apt-get install -y --no-install-recommends python3 python3-venv \
|
|
&& rm -rf /var/lib/apt/lists/* \
|
|
&& rel="GhidraMCP-release-$(echo "${GHIDRAMCP_VERSION}" | tr . -)" \
|
|
&& curl -fsSLo /tmp/ghidramcp.zip \
|
|
"https://github.com/LaurieWired/GhidraMCP/releases/download/${GHIDRAMCP_VERSION}/${rel}.zip" \
|
|
&& echo "${GHIDRAMCP_SHA256} /tmp/ghidramcp.zip" | sha256sum -c - \
|
|
&& unzip -q /tmp/ghidramcp.zip -d /tmp/ghidramcp \
|
|
&& unzip -q /tmp/ghidramcp/${rel}/ghidra_${GHIDRA_VERSION}_PUBLIC_*_GhidraMCP.zip \
|
|
-d /opt/ghidra/Ghidra/Extensions \
|
|
&& grep -qx "version=${GHIDRA_VERSION}" /opt/ghidra/Ghidra/Extensions/GhidraMCP/extension.properties \
|
|
&& curl -fsSLo /tmp/uv.lock \
|
|
"https://raw.githubusercontent.com/LaurieWired/GhidraMCP/${GHIDRAMCP_COMMIT}/uv.lock" \
|
|
&& echo "${GHIDRAMCP_UVLOCK_SHA256} /tmp/uv.lock" | sha256sum -c - \
|
|
&& python3 -I /tmp/uvlock-to-constraints.py /tmp/uv.lock > /tmp/constraints.txt \
|
|
&& python3 -m venv /opt/ghidra-mcp \
|
|
&& /opt/ghidra-mcp/bin/pip install --no-cache-dir --disable-pip-version-check \
|
|
--constraint /tmp/constraints.txt /tmp/ghidramcp/${rel}/ghidra_mcp-*-py3-none-any.whl \
|
|
&& /opt/ghidra-mcp/bin/ghidra-mcp --help >/dev/null \
|
|
&& rm -rf /tmp/ghidramcp /tmp/ghidramcp.zip /tmp/uv.lock /tmp/constraints.txt /tmp/uvlock-to-constraints.py
|
|
|
|
# binwalk and unblob (built above) with the extraction tools their install
|
|
# scripts (binwalk's Dockerfile, unblob's install-deps.sh) put in place.
|
|
# sasquatch (SquashFS variants) comes as a .deb from onekey-sec, at the
|
|
# release unblob 26.6.4 uses. unblob's ext2/3/4 extraction needs the rdump
|
|
# options from onekey-sec's e2fsprogs fork, so its .debs replace Ubuntu's
|
|
# e2fsprogs (same upstream 1.47.0). These releases publish no checksums;
|
|
# the SHA-256s were taken when pinned.
|
|
# The Python tools' entry points are linked into /usr/local/bin; the venvs'
|
|
# python/pip are not, so the system python3 isn't shadowed. unblob gets a
|
|
# wrapper that puts its own venv first on PATH, so it calls its own locked
|
|
# jefferson/ubireader rather than binwalk's.
|
|
ARG SASQUATCH_RELEASE=sasquatch-v4.5.1-6
|
|
ARG SASQUATCH_SHA256_AMD64=efbc92bd27594e01211e1b582f105a463a0017360a0746a93de3f283906ade95
|
|
ARG E2FSPROGS_OK_RELEASE=v1.47.0-3.ok2
|
|
ARG LIBEXT2FS2_OK_SHA256_AMD64=4eaf27e79a570ec89e15d8851f7df693b8b3ae78ccbb5b2a4a7d3eee845f1beb
|
|
ARG LIBSS2_OK_SHA256_AMD64=6dfc4612915d885607f36fcea2b851a947f9c73943494208ec48a0cbe9887427
|
|
ARG E2FSPROGS_OK_SHA256_AMD64=360adf4aa996cc80bf39002775d39900ae207b591c105d12a8ad2a833eb536f4
|
|
# binwalk -E -p (entropy graph as PNG) renders through Kaleido. binwalk's
|
|
# build downloads it into the build stage's /root, which the image doesn't
|
|
# have, so -p would crash; it's fetched here instead (same release the crate
|
|
# uses, no upstream checksum, SHA-256 taken when pinned) and found through
|
|
# KALEIDO_PATH at runtime.
|
|
ARG KALEIDO_VERSION=0.2.1
|
|
ARG KALEIDO_SHA256_AMD64=3eaa3efd41a00db05dd71add7f54ad4e9d4259552e3f7470ac4c1a8594cea214
|
|
COPY --from=binwalk-build /out/bin/ /usr/local/bin/
|
|
COPY --from=binwalk-build /opt/binwalk-py /opt/binwalk-py
|
|
COPY --from=binwalk-build /opt/esptool /opt/esptool
|
|
COPY --from=binwalk-build /opt/unblob /opt/unblob
|
|
RUN set -e \
|
|
&& arch="$(dpkg --print-architecture)" \
|
|
&& [ "$arch" = amd64 ] || { echo "sasquatch checksum only pinned for amd64" >&2; exit 1; } \
|
|
&& curl -fsSLo /tmp/sasquatch.deb \
|
|
"https://github.com/onekey-sec/sasquatch/releases/download/${SASQUATCH_RELEASE}/sasquatch_1.0_${arch}.deb" \
|
|
&& echo "${SASQUATCH_SHA256_AMD64} /tmp/sasquatch.deb" | sha256sum -c - \
|
|
&& mkdir /tmp/e2fsprogs-ok && cd /tmp/e2fsprogs-ok \
|
|
&& v="${E2FSPROGS_OK_RELEASE#v}" \
|
|
&& printf '%s %s\n' \
|
|
"${LIBEXT2FS2_OK_SHA256_AMD64}" "libext2fs2_${v}_${arch}.deb" \
|
|
"${LIBSS2_OK_SHA256_AMD64}" "libss2_${v}_${arch}.deb" \
|
|
"${E2FSPROGS_OK_SHA256_AMD64}" "e2fsprogs_${v}_${arch}.deb" > SHA256SUMS \
|
|
&& for f in $(awk '{print $2}' SHA256SUMS); do \
|
|
curl -fsSLO "https://github.com/onekey-sec/e2fsprogs/releases/download/${E2FSPROGS_OK_RELEASE}/$f"; \
|
|
done \
|
|
&& sha256sum -c SHA256SUMS && cd / \
|
|
&& curl -fsSLo /tmp/kaleido.zip \
|
|
"https://github.com/plotly/Kaleido/releases/download/v${KALEIDO_VERSION}/kaleido_linux_x64.zip" \
|
|
&& echo "${KALEIDO_SHA256_AMD64} /tmp/kaleido.zip" | sha256sum -c - \
|
|
&& unzip -q /tmp/kaleido.zip -d /opt/kaleido \
|
|
&& rm /tmp/kaleido.zip \
|
|
&& apt-get update \
|
|
&& apt-get install -y --no-install-recommends \
|
|
/tmp/sasquatch.deb /tmp/e2fsprogs-ok/*.deb \
|
|
7zip 7zip-standalone zstd srecord tar unzip sleuthkit cabextract \
|
|
lz4 lzop unrar unyaffs cpio device-tree-compiler \
|
|
liblzo2-2 libucl1 libfontconfig1 \
|
|
android-sdk-libsparse-utils erofs-utils lziprecover partclone unar \
|
|
upx-ucl libmagic1t64 xz-utils \
|
|
&& rm -rf /var/lib/apt/lists/* /tmp/sasquatch.deb /tmp/e2fsprogs-ok \
|
|
&& for f in /opt/binwalk-py/bin/* /opt/esptool/bin/*; do \
|
|
case "$(basename "$f")" in python*|pip*|activate*|Activate*) continue ;; esac; \
|
|
ln -s "$f" /usr/local/bin/; \
|
|
done \
|
|
&& printf '%s\n' '#!/bin/sh' 'PATH="/opt/unblob/bin:$PATH" exec /opt/unblob/bin/unblob "$@"' \
|
|
> /usr/local/bin/unblob \
|
|
&& chmod 755 /usr/local/bin/unblob \
|
|
&& binwalk --version && esptool version && unblob --version \
|
|
&& debugfs -V 2>&1 | grep -q 1.47.0 \
|
|
&& unblob --show-external-dependencies
|
|
# Binwalk leaves a symlink to the input file in its extraction directory. The
|
|
# target is a container path, which is dangling when viewed from the host.
|
|
ENV BINWALK_RM_EXTRACTION_SYMLINK=1 \
|
|
KALEIDO_PATH=/opt/kaleido
|
|
|
|
# Node.js, from the official tarball checked against nodejs.org's
|
|
# SHASUMS256.txt. Ubuntu's nodejs (18) is too old for REA; 24.22.1 is the
|
|
# version REA's own .nvmrc names.
|
|
ARG NODE_VERSION=24.22.1
|
|
ARG NODE_SHA256_AMD64=6c172a9ab6718f7b801fb5a7bd486af2ee5e32758086a04b84fcce2aa77095e6
|
|
RUN set -e \
|
|
&& arch="$(dpkg --print-architecture)" \
|
|
&& [ "$arch" = amd64 ] || { echo "Node.js checksum only pinned for amd64" >&2; exit 1; } \
|
|
&& apt-get update \
|
|
&& apt-get install -y --no-install-recommends xz-utils procps \
|
|
&& rm -rf /var/lib/apt/lists/* \
|
|
&& curl -fsSLo /tmp/node.tar.xz \
|
|
"https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" \
|
|
&& echo "${NODE_SHA256_AMD64} /tmp/node.tar.xz" | sha256sum -c - \
|
|
&& mkdir /opt/node \
|
|
&& tar -xJf /tmp/node.tar.xz -C /opt/node --strip-components=1 --no-same-owner \
|
|
&& rm /tmp/node.tar.xz
|
|
ENV PATH=/opt/node/bin:${PATH}
|
|
|
|
# REA, "Reverse Engineer Anything" (https://github.com/morluto/rea), from the
|
|
# rea-agents npm package. The tarball is checked against the integrity hash
|
|
# the npm registry lists for it; its dependencies are locked by the
|
|
# npm-shrinkwrap.json it ships. npm blocks dependency install scripts by
|
|
# default, so frida (optional, runtime instrumentation) stays unbuilt.
|
|
# REA runs in its own container (see run.sh), not next to Claude Desktop,
|
|
# because some of its tools run the target. It finds Ghidra through
|
|
# GHIDRA_INSTALL_DIR and needs procps' ps (installed above).
|
|
ARG REA_VERSION=6.8.0
|
|
ARG REA_INTEGRITY=sha512-9QDeRnGqEDzABZyr04eLnBXBv/c6I9/TWu/6M0MD4pEfkySI/BIgwTODIbAReX3C1x6hjw9+Jlx4A2yUSPZ9ww==
|
|
RUN set -e; cd /tmp \
|
|
&& npm pack --silent "rea-agents@${REA_VERSION}" \
|
|
&& actual="sha512-$(sha512sum "rea-agents-${REA_VERSION}.tgz" | cut -d' ' -f1 \
|
|
| python3 -I -c 'import base64,sys; print(base64.b64encode(bytes.fromhex(sys.stdin.read().strip())).decode())')" \
|
|
&& [ "$actual" = "${REA_INTEGRITY}" ] || { echo "rea-agents integrity mismatch: $actual" >&2; exit 1; } \
|
|
&& npm install --global --omit=optional --no-audit --no-fund "./rea-agents-${REA_VERSION}.tgz" \
|
|
&& rm -rf "rea-agents-${REA_VERSION}.tgz" /root/.npm \
|
|
&& [ "$(rea --version)" = "${REA_VERSION}" ]
|
|
ENV REA_BINWALK_COMMAND=/usr/local/bin/binwalk \
|
|
REA_UNBLOB_COMMAND=/usr/local/bin/unblob
|
|
|
|
# stdio-to-HTTP bridge (mcp-remote) that Claude Desktop starts to reach REA's
|
|
# HTTP endpoint in the other container. Installed from a committed lockfile.
|
|
COPY claude-desktop/mcp-bridge/package.json claude-desktop/mcp-bridge/package-lock.json /opt/mcp-bridge/
|
|
RUN cd /opt/mcp-bridge \
|
|
&& npm ci --omit=dev --ignore-scripts --no-audit --no-fund \
|
|
&& rm -rf /root/.npm \
|
|
&& test -f node_modules/mcp-remote/dist/proxy.js
|
|
|
|
# Ubuntu 24.04 ships a default "ubuntu" user on UID 1000; replace it.
|
|
RUN if id ubuntu >/dev/null 2>&1; then userdel -r ubuntu; fi \
|
|
&& groupadd -g "${USER_GID}" "${USER_NAME}" \
|
|
&& useradd -m -u "${USER_UID}" -g "${USER_GID}" -s /bin/bash "${USER_NAME}" \
|
|
&& install -d -o "${USER_UID}" -g "${USER_GID}" /workspace \
|
|
&& ln -s /workspace "/home/${USER_NAME}/workspace" \
|
|
&& install -d -o "${USER_UID}" -g "${USER_GID}" \
|
|
"/home/${USER_NAME}/.config" \
|
|
"/home/${USER_NAME}/.config/ghidra" \
|
|
"/home/${USER_NAME}/.config/ghidra/ghidra_${GHIDRA_VERSION}_PUBLIC" \
|
|
"/home/${USER_NAME}/.config/Claude"
|
|
|
|
# Ghidra's project window has no default config to patch; it builds one in
|
|
# code and saves it on exit. So a starting FrontEndTool.xml with the Developer
|
|
# plugins is put in the user settings. Pre-creating the settings dir makes
|
|
# Ghidra ask for its user agreement, which it otherwise accepts by itself on
|
|
# a fresh start, so preferences carries the same USER_AGREEMENT=ACCEPT.
|
|
COPY --chown=${USER_UID}:${USER_GID} ghidra/FrontEndTool.xml ghidra/preferences \
|
|
/home/${USER_NAME}/.config/ghidra/ghidra_${GHIDRA_VERSION}_PUBLIC/
|
|
|
|
# Claude Desktop's MCP servers. Only this file is put in ~/.config/Claude;
|
|
# sign-in data is still created at runtime and discarded with the container.
|
|
COPY --chown=${USER_UID}:${USER_GID} claude-desktop/claude_desktop_config.json \
|
|
/home/${USER_NAME}/.config/Claude/claude_desktop_config.json
|
|
|
|
# REA's skill (workflow instructions) for Claude Code sessions in Claude
|
|
# Desktop's Code tab, which load skills from ~/.claude/skills. Copied from
|
|
# the installed rea-agents package, so it matches the REA version. Chat in
|
|
# Claude Desktop does not read this folder.
|
|
RUN install -d -o "${USER_UID}" -g "${USER_GID}" \
|
|
"/home/${USER_NAME}/.claude" "/home/${USER_NAME}/.claude/skills" \
|
|
&& cp -r /opt/node/lib/node_modules/rea-agents/skills/reverse-engineer-anything \
|
|
"/home/${USER_NAME}/.claude/skills/" \
|
|
&& chown -R "${USER_UID}:${USER_GID}" "/home/${USER_NAME}/.claude/skills/reverse-engineer-anything" \
|
|
&& test -f "/home/${USER_NAME}/.claude/skills/reverse-engineer-anything/SKILL.md"
|
|
|
|
COPY --chmod=755 launch-apps.sh /usr/local/bin/launch-apps
|
|
|
|
USER ${USER_NAME}
|
|
WORKDIR /home/${USER_NAME}
|
|
|
|
# launch-apps starts Claude Desktop and Ghidra and stops both (and so the
|
|
# container) when either one is closed.
|
|
# No keyring: tell Electron not to look for one. Session data lives only in
|
|
# the container's filesystem, which is discarded on exit (run with --rm), so
|
|
# the user signs in on every start.
|
|
# --no-sandbox: Docker's default seccomp profile blocks the namespaces
|
|
# Chromium's sandbox needs, so the zygote dies on start. The container's own
|
|
# seccomp/namespaces stay in place as the boundary to the host instead.
|
|
CMD ["launch-apps", "--password-store=basic", "--no-sandbox"]
|