diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e348e91..7e52a0a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -251,10 +251,16 @@ RUN apt-get update && apt-get install -y \ Or for tools requiring compilation: ```dockerfile -RUN git clone https://github.com/tool/repo.git /tmp/tool && \ - cd /tmp/tool && \ - make && make install && \ - rm -rf /tmp/tool +# Builder stage example (jphide) +RUN git clone https://github.com/h3xx/jphs.git /tmp/jphs && \ + cd /tmp/jphs && \ + make && \ + cp jphide jpseek /usr/local/bin/ && \ + rm -rf /tmp/jphs + +# Runtime stage - copy compiled binaries +COPY --from=builder /usr/local/bin/jphide /usr/local/bin/jphide +COPY --from=builder /usr/local/bin/jpseek /usr/local/bin/jpseek ``` > [!NOTE] diff --git a/Dockerfile b/Dockerfile index 522d13a..2d8fc39 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,13 +5,26 @@ FROM python:3.14-slim AS builder # Install wget only to download OpenStego RUN apt-get update && apt-get install -y --no-install-recommends \ - wget \ + wget git build-essential \ && rm -rf /var/lib/apt/lists/* # Download OpenStego RUN wget https://github.com/syvaidya/openstego/releases/download/openstego-0.8.6/openstego_0.8.6-1_all.deb \ -O /tmp/openstego.deb +# Builder stage - add after other tool compilations +RUN git clone https://github.com/h3xx/jphs.git /tmp/jphs +RUN cd /tmp/jphs/jpeg-8a && ./configure && make +RUN cd /tmp/jphs && sed -i 's/open(seekfilename,O_WRONLY|O_TRUNC|O_CREAT)/open(seekfilename,O_WRONLY|O_TRUNC|O_CREAT, 0644)/' jpseek.c +RUN cd /tmp/jphs \ + && sed -i 's/^LIBS = .*/JPEG_OBJS = $(filter-out jpeg-8a\/cjpeg.o jpeg-8a\/djpeg.o jpeg-8a\/jpegtran.o jpeg-8a\/rdjpgcom.o jpeg-8a\/wrjpgcom.o,$(wildcard jpeg-8a\/\*.o))/' Makefile \ + && sed -i 's/^LDFLAGS = .*/LDFLAGS =/' Makefile \ + && sed -i 's/^jphide: \(.*\)$/jphide: \1 $(JPEG_OBJS)/' Makefile \ + && sed -i 's/^jpseek: \(.*\)$/jpseek: \1 $(JPEG_OBJS)/' Makefile +RUN cd /tmp/jphs && make all +RUN cd /tmp/jphs && cp jphide jpseek /usr/local/bin/ +RUN rm -rf /tmp/jphs + # ========================== # Stage 2 : Image runtime minimale # ========================== @@ -21,6 +34,7 @@ WORKDIR /app # Install runtime + stego tools RUN apt-get update && apt-get install -y --no-install-recommends \ + expect \ default-jre \ file \ ruby \ @@ -48,5 +62,9 @@ COPY aperisolve/ /app/aperisolve/ # Install Python dependencies RUN pip install --no-cache-dir -r /app/aperisolve/requirements.txt +# Copy jphide binaries +COPY --from=builder /usr/local/bin/jphide /usr/local/bin/jphide +COPY --from=builder /usr/local/bin/jpseek /usr/local/bin/jpseek + # Commande de lancement CMD ["gunicorn", "-w", "8", "-b", "0.0.0.0:5000", "--access-logfile", "-", "--error-logfile", "-", "--log-level", "info", "--capture-output", "aperisolve.utils.wsgi:application"] diff --git a/README.md b/README.md index 8db4425..2a4e757 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ Aperi'Solve is an open-source steganalysis web platform that performs automated - [steghide](https://steghide.sourceforge.net/) (extraction with password) - [strings](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/strings.html) - [zsteg](https://github.com/zed-0xff/zsteg) (LSB text/data extraction) - - [file](https://www.darwinsys.com/file/) (MIME type and format detection) + - [file](https://manned.org/file.1) (MIME type and format detection) + - [jpseek](https://github.com/h3xx/jphs) (JPEG steganography detection and extraction) - Worker queue architecture for offloading heavy/slow analyzers (Redis + background workers). - Results stored for later browsing and download. diff --git a/aperisolve/analyzers/jpseek.py b/aperisolve/analyzers/jpseek.py new file mode 100644 index 0000000..fb4ec96 --- /dev/null +++ b/aperisolve/analyzers/jpseek.py @@ -0,0 +1,74 @@ +# flake8: noqa: E203,E501,W503 +# pylint: disable=C0413,W0718,R0903,R0801 +# mypy: disable-error-code=unused-awaitable +"""JPSeek Analyzer for Image Submissions.""" + +from pathlib import Path +from typing import Optional + +from .base_analyzer import SubprocessAnalyzer + + +class JpseekAnalyzer(SubprocessAnalyzer): + """Analyzer for jpseek.""" + + def __init__(self, input_img: Path, output_dir: Path) -> None: + super().__init__("jpseek", input_img, output_dir, has_archive=True) + + def build_cmd(self, password: Optional[str] = None) -> list[str]: + extracted_dir = self.get_extracted_dir() + assert extracted_dir is not None # since has_archive is True + out = str(extracted_dir / "jpseek.out") + jpseek_cmd = f"jpseek {self.img} {out}" + if password is None: + password = "" + expect_cmd = ( + f"expect -c 'spawn {jpseek_cmd}; " + f'expect "Passphrase:"; ' + f'send "{password}\\r"; ' + f"expect eof'" + ) + command = ["bash", "-c", expect_cmd] + print("Built command:", command) + return command + + def _remove_output_artifacts(self, output: str) -> str: + extracted_dir = self.get_extracted_dir() + out_file = str(extracted_dir / "jpseek.out") + o = output + o = o.replace("jpseek, version 0.3 (c) 1998 Allan Latham ", "") + o = o.replace("This is licenced software but no charge is made for its use.", "") + o = o.replace("NO WARRANTY whatsoever is offered with this product.", "") + o = o.replace("NO LIABILITY whatsoever is accepted for its use.", "") + o = o.replace("You are using this entirely at your OWN RISK.", "") + o = o.replace("See the GNU Public Licence for full details.", "") + o = o.replace("Passphrase:", "") + o = o.replace(f"spawn jpseek {self.img} {out_file}", "") + return o.strip() + + def is_error(self, returncode: int, stdout: str, stderr: str, zip_exist: bool) -> bool: + """Check if the result is an error.""" + return bool(returncode) and "File not completely recovered" not in "".join([stdout, stderr]) + + def process_error(self, stdout: str, stderr: str) -> str: + """Process stderr.""" + output = stdout + stderr + output = self._remove_output_artifacts(output) + return output + + def process_output(self, stdout: str, stderr: str) -> str: + """Process stdout.""" + output = stdout + stderr + output = self._remove_output_artifacts(output) + if output == "": + output = "File completely recovered." + return output + + +def analyze_jpseek(input_img: Path, output_dir: Path, password: Optional[str] = None) -> None: + """Analyze an image submission using jpseek.""" + analyzer = JpseekAnalyzer(input_img, output_dir) + if password: + analyzer.analyze(password) + else: + analyzer.analyze() diff --git a/aperisolve/config.py b/aperisolve/config.py index 11c8ce1..8ecf6e6 100644 --- a/aperisolve/config.py +++ b/aperisolve/config.py @@ -7,7 +7,7 @@ from os import getenv from pathlib import Path IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff"] -WORKER_FILES = ["binwalk", "foremost", "steghide", "zsteg", "openstego", "pcrt"] +WORKER_FILES = ["binwalk", "foremost", "steghide", "zsteg", "openstego", "pcrt", "jpseek"] RESULT_FOLDER = Path(__file__).parent.resolve() / "results" REMOVED_IMAGES_FOLDER = Path(__file__).parent.resolve() / "removed_images" diff --git a/aperisolve/static/js/aperisolve.js b/aperisolve/static/js/aperisolve.js index ac22bed..895738a 100644 --- a/aperisolve/static/js/aperisolve.js +++ b/aperisolve/static/js/aperisolve.js @@ -8,6 +8,7 @@ const TOOL_ORDER = [ "pngcheck", "pcrt", "steghide", + "jpseek", "openstego", "zsteg", "strings" diff --git a/aperisolve/workers.py b/aperisolve/workers.py index 8da3463..01ada4e 100644 --- a/aperisolve/workers.py +++ b/aperisolve/workers.py @@ -18,6 +18,7 @@ from .analyzers.decomposer import analyze_decomposer from .analyzers.exiftool import analyze_exiftool from .analyzers.file import analyze_file from .analyzers.foremost import analyze_foremost +from .analyzers.jpseek import analyze_jpseek from .analyzers.openstego import analyze_openstego from .analyzers.outguess import analyze_outguess from .analyzers.pcrt import analyze_pcrt @@ -106,6 +107,7 @@ def analyze_image(submission_hash: str) -> None: (analyze_exiftool, img_path, result_path), (analyze_file, img_path, result_path), (analyze_foremost, img_path, result_path), + (analyze_jpseek, img_path, result_path, submission.password), (analyze_openstego, img_path, result_path, submission.password), (analyze_pngcheck, img_path, result_path), (analyze_pcrt, img_path, result_path), diff --git a/docs/adding-analyzer.md b/docs/adding-analyzer.md index 4be6466..0d22415 100644 --- a/docs/adding-analyzer.md +++ b/docs/adding-analyzer.md @@ -5,7 +5,7 @@ This guide shows you how to add a new image analysis tool to the system. ## Quick Start 1. Create a new file in `analyzers/` from template file `template_analyzer.py`. -2. Register it in `workers.py`, `config.py` and `aperisolve.js`. +2. Register it in `workers.py`, `config.py` (if it extracts files) and `aperisolve.js`. ## Basic Template @@ -24,7 +24,7 @@ def analyze_mytool(input_img: Path, output_dir: Path) -> None: analyzer.analyze() ``` -You can also see `template_analyzer.py`. +You can also see `template_analyzer.py` or check real examples like `jpseek.py`, `steghide.py`, or `strings.py`. ## Common Scenarios