diff --git a/Dockerfile b/Dockerfile index 5e4eb5c..fe69aa9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,20 +2,34 @@ FROM python:3.14-slim AS build WORKDIR / -# Install dependencies +# Install build tools and dependencies RUN apt-get update && apt-get install -y \ + wget \ + default-jre \ + ruby \ zip \ - 7zip \ + 7zip + +# Install steganography and forensics tools +RUN apt-get update && apt-get install -y \ binwalk \ foremost \ exiftool \ steghide \ - ruby \ binutils \ outguess \ pngcheck \ - && gem install zsteg \ - && apt-get clean + && gem install zsteg + +# Install 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 && \ + dpkg -i /tmp/openstego.deb && \ + rm /tmp/openstego.deb + +# Clean up apt cache and remove useless packages +RUN apt-get remove -y wget && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* COPY aperisolve/ /aperisolve/ @@ -25,4 +39,4 @@ RUN pip install --no-cache-dir -r /aperisolve/requirements.txt ENV PYTHONUNBUFFERED=1 -CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "--access-logfile", "-", "--error-logfile", "-", "--log-level", "info", "--capture-output", "aperisolve.wsgi:application"] \ No newline at end of file +CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "--access-logfile", "-", "--error-logfile", "-", "--log-level", "info", "--capture-output", "aperisolve.wsgi:application"] diff --git a/README.md b/README.md index 6f7f2eb..403b06d 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ Table of contents - zsteg (LSB text/data extraction) - steghide (extraction with password) - outguess (extraction with password) + - openstego (extraction with password) - exiftool (metadata and geolocation) - binwalk (embedded archives) - foremost (carved files) @@ -197,4 +198,4 @@ CI will run these checks on each PR. ## Credits Acknowledgements: -- Thanks to contributors and the open-source community for the tools integrated (zsteg, steghide, binwalk, foremost, exiftool). +- Thanks to contributors and the open-source community for the tools integrated (zsteg, steghide, openstego, binwalk, foremost, exiftool, ...). diff --git a/aperisolve/analyzers/openstego.py b/aperisolve/analyzers/openstego.py new file mode 100644 index 0000000..f9fde97 --- /dev/null +++ b/aperisolve/analyzers/openstego.py @@ -0,0 +1,115 @@ +"""OpenStego Analyzer for Image Submissions.""" + +import shutil +import subprocess +from pathlib import Path +from typing import Optional + +from .utils import MAX_PENDING_TIME, update_data + + +def analyze_openstego(input_img: Path, output_dir: Path, password: str = "") -> None: + """Analyze an image submission using openstego.""" + + image_name = input_img.name + try: + stderr = "" + # Try to extract the embedded file + extracted_dir = output_dir / "openstego" + extracted_dir.mkdir(parents=True, exist_ok=True) + + for algo in ["AES128", "AES256"]: + cmd: list[str] = [ + "openstego", + "extract", + "-a", + "randomlsb", + "--cryptalgo", + algo, + "-sf", + "../" + str(image_name), + "-xd", + str(extracted_dir), + "-p", + password, + ] + + data = subprocess.run( + cmd, + cwd=output_dir, + capture_output=True, + text=True, + check=False, + timeout=MAX_PENDING_TIME, + ) + + # Check if extraction was successful + if data.returncode != 0: + stderr += data.stderr.replace( + f'"../{image_name}" ', "" + ) # hide file name + err = { + "openstego": { + "status": "error", + "error": stderr, + } + } + update_data(output_dir, err) + return None + + # Find the extracted file + extracted_files = list(extracted_dir.glob("*")) + if not extracted_files: + err = { + "openstego": { + "status": "error", + "error": "No file extracted, password may be incorrect.", + } + } + update_data(output_dir, err) + return None + + # Filter stdout for success messages + stdout = [] + for f in extracted_files: + stdout.append(f"Recovered file: {f.name}") + + # Zip extracted files + zip_data = subprocess.run( + ["7z", "a", "../openstego.7z", "*"], + cwd=extracted_dir, + capture_output=True, + text=True, + check=False, + timeout=MAX_PENDING_TIME, + ) + + stderr += zip_data.stderr + + if len(stderr) > 0: + err = { + "openstego": { + "status": "error", + "error": stderr, + } + } + update_data(output_dir, err) + return None + + # Remove the extracted directory + shutil.rmtree(extracted_dir) + + update_data( + output_dir, + { + "openstego": { + "status": "ok", + "output": stdout, + "download": f"/download/{output_dir.name}/openstego", + } + }, + ) + + except Exception as e: + update_data(output_dir, {"openstego": {"status": "error", "error": str(e)}}) + return None diff --git a/aperisolve/config.py b/aperisolve/config.py index 117e852..96016c3 100644 --- a/aperisolve/config.py +++ b/aperisolve/config.py @@ -3,6 +3,6 @@ from pathlib import Path IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff"] -WORKER_FILES = ["binwalk", "foremost", "steghide", "zsteg", "image_resize"] +WORKER_FILES = ["binwalk", "foremost", "steghide", "zsteg", "image_resize", "openstego"] RESULT_FOLDER = Path(__file__).parent.resolve() / "results" diff --git a/aperisolve/static/js/aperisolve.js b/aperisolve/static/js/aperisolve.js index 2867666..a093ce7 100644 --- a/aperisolve/static/js/aperisolve.js +++ b/aperisolve/static/js/aperisolve.js @@ -6,6 +6,7 @@ const TOOL_ORDER = [ "outguess", "pngcheck", "steghide", + "openstego", "zsteg", "strings", "image_resize", @@ -286,7 +287,7 @@ function parseResult(result) { } // Parse text output - + if (typeof result[tool]["output"] === "string") { analyzer.innerHTML += `
${escapeHtml(
diff --git a/aperisolve/workers.py b/aperisolve/workers.py
index acb6b8e..c5be8c7 100644
--- a/aperisolve/workers.py
+++ b/aperisolve/workers.py
@@ -9,6 +9,7 @@ from .analyzers.decomposer import analyze_decomposer
from .analyzers.exiftool import analyze_exiftool
from .analyzers.foremost import analyze_foremost
from .analyzers.image_resize import analyze_image_resize
+from .analyzers.openstego import analyze_openstego
from .analyzers.outguess import analyze_outguess
from .analyzers.pngcheck import analyze_pngcheck
from .analyzers.steghide import analyze_steghide
@@ -77,6 +78,7 @@ def analyze_image(submission_hash: str) -> None:
(analyze_strings, img_path, result_path),
(analyze_pngcheck, img_path, result_path),
(analyze_steghide, img_path, result_path, submission.password),
+ (analyze_openstego, img_path, result_path, submission.password),
(analyze_zsteg, img_path, result_path),
(analyze_image_resize, img_path, result_path),
]
diff --git a/examples/example3.png b/examples/example3.png
new file mode 100644
index 0000000..843ad8c
Binary files /dev/null and b/examples/example3.png differ
diff --git a/examples/size_change_png.png b/examples/size_change_png.png
deleted file mode 100644
index 064c96a..0000000
Binary files a/examples/size_change_png.png and /dev/null differ