Switch Ghidra to 12.1.4 (with openjdk-21-jdk), the version the GhidraMCP 2.0 release is built for; Ghidra only loads extensions built for its exact version. From LaurieWired's GhidraMCP 2.0 release zip (SHA-256 checked): - unpack the Ghidra extension into /opt/ghidra/Ghidra/Extensions; the build fails if it doesn't target GHIDRA_VERSION. GhidraMCPPlugin is a RELEASED Developer plugin, so the CodeBrowser config loads it, and it serves its HTTP backend on 127.0.0.1:8080. - install the ghidra-mcp wheel into /opt/ghidra-mcp, with every dependency pinned to GhidraMCP's uv.lock at the release commit (ghidra/uvlock-to-constraints.py, honouring per-Python markers). claude-desktop/claude_desktop_config.json adds a stdio "ghidra" server running ghidra-mcp against http://127.0.0.1:8080/ and is copied into ~/.config/Claude. Verified with a test project: ghidra-mcp started from that config connected to Ghidra 12.1.4 in GUI mode and returned program info, segments and imports for the open program. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
151 lines
8.2 KiB
Docker
151 lines
8.2 KiB
Docker
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 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.
|
|
# 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=12.1.4
|
|
ARG GHIDRA_BUILD_DATE=20260921
|
|
ARG GHIDRA_SHA256=ddac49f903da9d5bac833e5cc79395098b9c33cfd3279be5f31bd00387d2d4db
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends openjdk-21-jdk 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 \
|
|
&& 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
|
|
|
|
# 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 ghidra/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
|
|
|
|
# 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
|
|
|
|
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"]
|