Add unsquashfs and stop SquashFS tools failing on xattrs

Install Ubuntu's squashfs-tools so the stock unsquashfs/mksquashfs exist
next to sasquatch. As the non-root container user, unsquashfs and
sasquatch can't restore security.* xattrs and exit 2 although every file
was extracted; wrap them to pass -no-xattrs unless an xattr option is
given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 19:51:45 +01:00
co-authored by Claude Opus 5.5
parent 0315d5816e
commit c198c36ea4
3 changed files with 29 additions and 4 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ Early stage. What exists: a single image (`Dockerfile`, `launch-apps.sh`) with C
- **ESP32 Flash Loader** (https://github.com/dynacylabs/ghidra-esp32-flash-loader): a Ghidra loader ("ESP32 Flash Image" in *File → Import File*) for ESP32 app images and flash dumps. It reads the partition table at `0x8000`, maps segments to their addresses, adds peripherals from Espressif's SVD files and loads the chip's ROM ELF for symbols. A flash dump is only recognized when it **starts with the bootloader** (ESP32-S/C layouts). Original-ESP32 dumps (bootloader at `0x1000`) are rejected, and trimming the first `0x1000` bytes doesn't help: the loader then finds no partition table at `0x8000`, loads only the bootloader and throws a NullPointerException while still reporting success. Workaround: cut the app partition out of the dump (offsets from the partition table) and import it as an app image. Its tags ship no binaries and the older forks (tslater2006, Ebiroll, saibotk v1.1.0) don't compile against Ghidra 12, so the `esp32-loader-build` stage builds dynacylabs' master at a pinned commit, with its `data/svd` and `ghidra_scripts` submodules fetched over HTTPS at pinned commits and Gradle checked against gradle.org's SHA-256. It is unpacked into `/opt/ghidra/Ghidra/Extensions/esp32`; loaders need no tool config.
- **REA ("Reverse Engineer Anything")**: https://github.com/morluto/rea (MIT, TypeScript), the `rea-agents` npm package, pinned to **6.8.0** with the tarball checked against the registry's integrity hash; its dependencies are locked by the `npm-shrinkwrap.json` it ships. It needs Node.js ≥24.22.1 (official tarball, SHA-256 checked, in `/opt/node`) and procps' `ps`. It uses Ghidra 12.1.x headless through `GHIDRA_INSTALL_DIR` (its own temporary projects, separate from the GUI's) binwalk 3.1.x through `REA_BINWALK_COMMAND` (`inspect_firmware_regions`) and unblob 26.6.x through `REA_UNBLOB_COMMAND` (`extract_firmware`); Hopper, IDA, Kuna, JADX, tshark, pwntools and a browser are not installed, and frida (optional, runtime instrumentation) stays unbuilt because npm blocks its install script. Check with `rea doctor --provider ghidra --json`. REA's bundled skill (`reverse-engineer-anything`) is copied from the package into `~/.claude/skills` in the image, for Claude Code sessions in Claude Desktop's Code tab (they load user skills from there). Claude Desktop's chat doesn't read that folder: its skills come from the account (Settings → Capabilities → Skills) and are cached by the app.
Some REA tools run the target (process capture, runtime/Electron observation), so it does **not** run in the Claude Desktop container. `run.sh` starts a second container from the same image (`claude-re-mcp`): `rea mcp --http --host 0.0.0.0 --port 8765`, only the workspace mounted, no display, `--cap-drop ALL`, `no-new-privileges`, and only on the `--internal` network `claude-re-setup-internal` (no route out) under the alias `rea`. The Claude Desktop container is created on the default network plus that internal one, so it can sign in and reach `http://rea:8765/mcp`. REA's HTTP endpoint has no authentication and serves one client session at a time; the internal network is what limits who can reach it. A detached `docker wait` started by `run.sh` stops REA when Claude Desktop's container ends.
- **binwalk, unblob and esptool** are CLI tools in the same image (no MCP server of their own; REA wraps binwalk and unblob). The `Dockerfile` has a `binwalk-build` stage adapted from binwalk's own Dockerfile (https://github.com/ReFirmLabs/binwalk) to Ubuntu 24.04, with every source pinned: binwalk at a master commit (3.1.1; the last tag, v3.1.0, is from 2024), Rust toolchain, `dumpifs`/`lzfse`/`dmg2img` commits, and the Python extractors (`jefferson`, `ubi-reader`, `uefi_firmware`, `vmlinux-to-elf`, ...) in `/opt/binwalk-py`. esptool is in `/opt/esptool`. unblob (https://github.com/onekey-sec/unblob) 26.6.4 is installed from its PyPI wheel (SHA-256 checked) into `/opt/unblob`, with dependencies pinned to its `uv.lock` at the release commit via `scripts/uvlock-to-constraints.py`; `/usr/local/bin/unblob` is a wrapper that puts that venv first on `PATH`, so unblob uses its own locked `jefferson`/`ubireader` rather than binwalk's. Its extractors follow upstream's `install-deps.sh`: Ubuntu packages, sasquatch v4.5.1-6 (shared with binwalk) and onekey-sec's e2fsprogs fork (v1.47.0-3.ok2, replacing Ubuntu's 1.47.0), whose `debugfs rdump` options unblob's ext2/3/4 handler needs. The build fails unless `unblob --show-external-dependencies` finds every extractor. unblob's Landlock sandbox works in the locked-down REA container (`status=FullyEnforced`). The venvs' entry points are symlinked into `/usr/local/bin` (not their `python`/`pip`). The final stage adds upstream's runtime extractors from apt, sasquatch from onekey-sec's `.deb`, and Kaleido for `binwalk -E -p` (PNG entropy graphs), found via `KALEIDO_PATH`. Upstream publishes no checksums for sasquatch, onekey's e2fsprogs or Kaleido; their SHA-256s were recorded when pinned, amd64 only. `BINWALK_RM_EXTRACTION_SYMLINK=1` avoids dangling symlinks to container paths in extraction output. esptool needs the serial device passed into the container (`--device`) to talk to hardware; that isn't set up.
- **binwalk, unblob and esptool** are CLI tools in the same image (no MCP server of their own; REA wraps binwalk and unblob). The `Dockerfile` has a `binwalk-build` stage adapted from binwalk's own Dockerfile (https://github.com/ReFirmLabs/binwalk) to Ubuntu 24.04, with every source pinned: binwalk at a master commit (3.1.1; the last tag, v3.1.0, is from 2024), Rust toolchain, `dumpifs`/`lzfse`/`dmg2img` commits, and the Python extractors (`jefferson`, `ubi-reader`, `uefi_firmware`, `vmlinux-to-elf`, ...) in `/opt/binwalk-py`. esptool is in `/opt/esptool`. unblob (https://github.com/onekey-sec/unblob) 26.6.4 is installed from its PyPI wheel (SHA-256 checked) into `/opt/unblob`, with dependencies pinned to its `uv.lock` at the release commit via `scripts/uvlock-to-constraints.py`; `/usr/local/bin/unblob` is a wrapper that puts that venv first on `PATH`, so unblob uses its own locked `jefferson`/`ubireader` rather than binwalk's. Its extractors follow upstream's `install-deps.sh`: Ubuntu packages, sasquatch v4.5.1-6 (shared with binwalk) and onekey-sec's e2fsprogs fork (v1.47.0-3.ok2, replacing Ubuntu's 1.47.0), whose `debugfs rdump` options unblob's ext2/3/4 handler needs. The build fails unless `unblob --show-external-dependencies` finds every extractor. unblob's Landlock sandbox works in the locked-down REA container (`status=FullyEnforced`). The venvs' entry points are symlinked into `/usr/local/bin` (not their `python`/`pip`). The final stage adds upstream's runtime extractors from apt, sasquatch from onekey-sec's `.deb`, Ubuntu's `squashfs-tools` (plain `unsquashfs`/`mksquashfs`, the commands Claude tries before `sasquatch`). `unsquashfs`, `sasquatch` and `sasquatch-v4be` in `/usr/local/bin` are wrappers (`scripts/squashfs-no-xattrs.sh`) that add `-no-xattrs` unless an xattr option is given: as non-root they can't restore `security.*` xattrs and exit 2 although every file was extracted, which looked like a failed extraction, and Kaleido for `binwalk -E -p` (PNG entropy graphs), found via `KALEIDO_PATH`. Upstream publishes no checksums for sasquatch, onekey's e2fsprogs or Kaleido; their SHA-256s were recorded when pinned, amd64 only. `BINWALK_RM_EXTRACTION_SYMLINK=1` avoids dangling symlinks to container paths in extraction output. esptool needs the serial device passed into the container (`--device`) to talk to hardware; that isn't set up.
- **MCP transport.** Claude Desktop starts local MCP servers as stdio subprocesses. GhidraMCP runs in the same container, so `ghidra-mcp` is a plain stdio entry in `claude_desktop_config.json` that reaches Ghidra on `127.0.0.1`. Servers in other containers (so far REA) serve streamable HTTP and are bridged into the stdio config with `mcp-remote` (`--allow-http --transport http-only`), installed in `/opt/mcp-bridge` from the committed `claude-desktop/mcp-bridge/package-lock.json` and started with `/opt/node/bin/node` by absolute path. Use the same pattern for future servers. Avoid mounting the Docker socket into the Claude Desktop container just to `docker exec` MCP servers.
## Conventions
+12 -3
View File
@@ -256,8 +256,11 @@ RUN set -e \
# 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
# release unblob 26.6.4 uses. squashfs-tools adds the stock unsquashfs/
# mksquashfs next to it (sasquatch only installs sasquatch*), since that's
# the command people and Claude reach for first.
# 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'
@@ -309,7 +312,7 @@ RUN set -e \
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 \
upx-ucl libmagic1t64 xz-utils squashfs-tools \
&& 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; \
@@ -321,6 +324,12 @@ RUN set -e \
&& binwalk --version && esptool version && unblob --version \
&& debugfs -V 2>&1 | grep -q 1.47.0 \
&& unblob --show-external-dependencies
# unsquashfs/sasquatch exit 2 as non-root on images with security.* xattrs
# although every file was extracted; the wrapper adds -no-xattrs by default.
COPY --chmod=755 scripts/squashfs-no-xattrs.sh /usr/local/libexec/squashfs-no-xattrs
RUN for t in unsquashfs sasquatch sasquatch-v4be; do \
ln -s /usr/local/libexec/squashfs-no-xattrs "/usr/local/bin/$t"; \
done
# 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 \
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
# Installed in the image as /usr/local/bin/{unsquashfs,sasquatch,sasquatch-v4be}
# (symlinks to this script), in front of the real tools in /usr/bin.
#
# As the unprivileged container user these tools can't restore security.*
# xattrs (SELinux labels, file capabilities) that firmware images often
# carry: they extract every file but print "not superuser" errors and exit 2,
# which reads as a failed extraction. Pass -no-xattrs unless the caller
# already chose an xattr option (-xattrs, -user-xattrs, -xattrs-include, ...).
tool="/usr/bin/$(basename "$0")"
for arg in "$@"; do
case "$arg" in
-*xattrs*) exec "$tool" "$@" ;;
esac
done
exec "$tool" -no-xattrs "$@"