Initial commit

This commit is contained in:
dev
2025-06-12 22:55:04 +02:00
commit ecb089ea79
53 changed files with 3265 additions and 0 deletions
View File
View File
+68
View File
@@ -0,0 +1,68 @@
"""Binwalk Analyzer for Image Submissions."""
import shutil
import subprocess
from pathlib import Path
from .utils import update_data
def analyze_binwalk(input_img: Path, output_dir: Path) -> None:
"""Analyze an image submission using binwalk."""
image_name = input_img.name
extracted_dir = output_dir / f"_{image_name}.extracted"
try:
stderr = ""
# Run binwalk
data = subprocess.run(
["binwalk", "-e", "../" + str(image_name), "--run-as=root"],
cwd=output_dir,
capture_output=True,
text=True,
check=False,
)
stderr += data.stderr
zip_exist = False
if extracted_dir.exists():
# Zip extracted files
zip_data = subprocess.run(
["7z", "a", "../binwalk.7z", "*"],
cwd=extracted_dir,
capture_output=True,
text=True,
check=False,
)
zip_exist = True
stderr += zip_data.stderr
if len(stderr) > 0:
err = {
"binwalk": {
"status": "error",
"error": stderr,
}
}
update_data(output_dir, err)
return None
# Remove the extracted directory
if extracted_dir.exists():
shutil.rmtree(extracted_dir)
output_data = {
"binwalk": {
"status": "ok",
"output": data.stdout.split("\n") if data else [],
}
}
if zip_exist:
output_data["binwalk"]["download"] = f"/download/{output_dir.name}/binwalk"
update_data(output_dir, output_data)
except Exception as e:
update_data(output_dir, {"binwalk": {"status": "error", "error": str(e)}})
return None
+69
View File
@@ -0,0 +1,69 @@
"""Bits Decomposer Analyzer for Image Submissions."""
from pathlib import Path
import numpy as np
from PIL import Image
from .utils import update_data
def analyze_decomposer(input_img: Path, output_dir: Path) -> None:
"""Analyze an image submission using bits decomposition."""
img = Image.open(input_img)
img_np = np.array(img)
# Handle grayscale, RGB, RGBA, etc.
if len(img_np.shape) == 2:
channels = 1
else:
channels = img_np.shape[2]
if channels > 1:
channel_names = ["Red", "Green", "Blue", "Alpha"]
else:
channel_names = ["Grayscale"]
image_json = {}
# Add Superimposed RGB bit planes
if channels >= 3:
superimposed_json = []
for bit in range(8):
bit_mask = 1 << bit
rgb_planes = [((img_np[..., c] & bit_mask) >> bit) * 255 for c in range(3)]
rgb_image = np.stack(rgb_planes, axis=-1).astype(np.uint8)
rgb_img = Image.fromarray(rgb_image, mode="RGB")
img_name = f"superimposed_bit_{bit}.png"
out_path = output_dir / img_name
dl_path = Path(output_dir.name) / img_name
superimposed_json.append("/image/" + str(dl_path))
rgb_img.save(out_path)
image_json["Superimposed"] = superimposed_json
# Process individual channels
for c in range(channels):
channel_data = img_np[..., c] if channels > 1 else img_np
channel_label = channel_names[c]
channel_json = []
for bit in range(8):
bit_mask = 1 << bit
bit_plane = ((channel_data & bit_mask) >> bit) * 255
bit_img = Image.fromarray(bit_plane.astype(np.uint8), mode="L")
img_name = f"{channel_label}_bit_{bit}.png"
out_path = output_dir / img_name
dl_path = Path(output_dir.name) / img_name
channel_json.append("/image/" + str(dl_path))
bit_img.save(out_path)
image_json[channel_label] = channel_json
update_data(
output_dir,
{
"decomposer": {
"status": "ok",
"images": image_json,
}
},
)
+38
View File
@@ -0,0 +1,38 @@
"""Exiftool Analyzer for Image Submissions."""
import subprocess
from pathlib import Path
from .utils import update_data
def analyze_exiftool(input_img: Path, output_dir: Path) -> None:
"""Analyze an image submission using exiftool."""
try:
data = subprocess.run(
["exiftool", input_img], capture_output=True, text=True, check=True
)
if data.stderr:
update_data(
output_dir, {"strings": {"status": "error", "error": data.stderr}}
)
return
metadata: dict[str, str] = {}
for line in data.stdout.split("\n"):
if ":" in line:
key, value = line.split(":", 1)
metadata[key.strip()] = value.strip()
# exiftool_dir = output_dir / "exiftool"
# exiftool_dir.mkdir(parents=True, exist_ok=True)
# update_data(exiftool_dir, metadata, "data.json")
update_data(
output_dir,
{"exiftool": {"status": "ok", "output": metadata}},
)
except Exception as e:
update_data(output_dir, {"exiftool": {"status": "error", "error": str(e)}})
+57
View File
@@ -0,0 +1,57 @@
"""Foremost Analyzer for Image Submissions."""
import shutil
import subprocess
from pathlib import Path
from .utils import update_data
def analyze_foremost(input_img: Path, output_dir: Path) -> None:
"""Analyze an image submission using foremost."""
try:
foremost_dir = output_dir / "foremost"
foremost_dir.mkdir(parents=True, exist_ok=True)
stderr = ""
# Run foremost
data = subprocess.run(
["foremost", "-o", foremost_dir, "-i", input_img],
capture_output=True,
text=True,
check=False,
)
# Note: foremost use stderr as standard output for results :o)
# So we can't use it as error dectection :
# stderr += data.stderr
# Zip extracted files
zip_data = subprocess.run(
["7z", "a", "../foremost.7z", "*"],
cwd=foremost_dir,
capture_output=True,
text=True,
check=False,
)
stderr += zip_data.stderr
# Remove the extracted directory
shutil.rmtree(foremost_dir)
data_strings = data.stdout.split("\n") if data else []
data_strings = [s for s in data_strings if s]
update_data(
output_dir,
{
"foremost": {
"status": "ok",
"output": data_strings,
"download": f"/download/{output_dir.name}/foremost",
}
},
)
except Exception as e:
update_data(output_dir, {"foremost": {"status": "error", "error": str(e)}})
+94
View File
@@ -0,0 +1,94 @@
"""Outguess Analyzer for Image Submissions."""
import shutil
import subprocess
from pathlib import Path
from typing import Optional
from .utils import update_data
def analyze_outguess(
input_img: Path, output_dir: Path, password: Optional[str] = None
) -> None:
"""Analyze an image submission using stehide."""
image_name = input_img.name
try:
outguess_dir = output_dir / "outguess"
outguess_dir.mkdir(parents=True, exist_ok=True)
outguess_file = "outguess.data"
stderr = ""
cmd: list[str]
if password:
cmd = [
"outguess",
"-k",
password,
"-r",
"../../" + str(image_name),
outguess_file,
]
else:
cmd = ["outguess", "-r", "../../" + str(image_name), outguess_file]
data = subprocess.run(
cmd,
cwd=outguess_dir,
capture_output=True,
text=True,
check=False,
)
if data.returncode != 0:
stderr += data.stderr
err = {
"outguess": {
"status": "error",
"error": stderr,
}
}
update_data(output_dir, err)
return None
# Zip extracted files
zip_data = subprocess.run(
["7z", "a", "../outguess.7z", "*"],
cwd=outguess_dir,
capture_output=True,
text=True,
check=False,
)
stderr += zip_data.stderr
if len(stderr) > 0:
err = {
"outguess": {
"status": "error",
"error": stderr,
}
}
update_data(output_dir, err)
return None
# Remove the extracted directory
shutil.rmtree(outguess_dir)
data_strings = data.stderr.split("\n") if data else []
data_strings = [s for s in data_strings if s]
update_data(
output_dir,
{
"outguess": {
"status": "ok",
"output": data_strings,
"download": f"/download/{output_dir.name}/outguess",
}
},
)
except Exception as e:
update_data(output_dir, {"outguess": {"status": "error", "error": str(e)}})
return None
+118
View File
@@ -0,0 +1,118 @@
"""Steghide Analyzer for Image Submissions."""
import re
import shutil
import subprocess
from pathlib import Path
from typing import Optional
from .utils import update_data
def analyze_steghide(
input_img: Path, output_dir: Path, password: Optional[str] = None
) -> None:
"""Analyze an image submission using stehide."""
image_name = input_img.name
try:
stderr = ""
# Get embedded file information first
cmd: list[str] = ["steghide", "info", "../" + str(image_name)]
if password:
cmd += ["-p", password]
else:
cmd += ["-p", ""]
data = subprocess.run(
cmd,
cwd=output_dir,
capture_output=True,
text=True,
check=False,
)
match = re.search(r'embedded file "([^"]+)"', data.stdout)
embedded_filename = match.group(1) if match else None
# if bad file format or wrong password, raise an error
if data.returncode != 0 or embedded_filename is None:
stderr += data.stderr.replace(f'"../{image_name}" ', "") # hide file name
# the file format of the file \"../foobar.png\" is not supported.
# become
# the file format of the file is not supported.
err = {
"steghide": {
"status": "error",
"error": stderr,
}
}
update_data(output_dir, err)
return None
# The passphrase is correct, we can extract the file
# prevent path traversal
embedded_file = output_dir / "steghide" / str(Path(embedded_filename).name)
extracted_dir = embedded_file.parent
extracted_dir.mkdir(parents=True, exist_ok=True)
cmd_ext: list[str] = [
"steghide",
"extract",
"-sf",
"../" + str(image_name),
"-xf",
str(embedded_filename),
]
if password:
cmd_ext += ["-p", password]
else:
cmd_ext += ["-p", ""]
data = subprocess.run(
cmd_ext,
cwd=output_dir,
capture_output=True,
text=True,
check=False,
)
stderr += data.stderr
# Zip extracted files
zip_data = subprocess.run(
["7z", "a", "../steghide.7z", "*"],
cwd=extracted_dir,
capture_output=True,
text=True,
check=False,
)
stderr += zip_data.stderr
if len(stderr) > 0:
err = {
"binwalk": {
"status": "error",
"error": stderr,
}
}
update_data(output_dir, err)
return None
# Remove the extracted directory
shutil.rmtree(extracted_dir)
update_data(
output_dir,
{
"steghide": {
"status": "ok",
"output": data.stdout.split("\n") if data else [],
"download": f"/download/{output_dir.name}/steghide",
}
},
)
except Exception as e:
update_data(output_dir, {"steghide": {"status": "error", "error": str(e)}})
return None
+40
View File
@@ -0,0 +1,40 @@
"""Strings Analyzer for Image Submissions."""
import subprocess
from pathlib import Path
from .utils import update_data
def analyze_strings(input_img: Path, output_dir: Path) -> None:
"""Analyze an image submission using strings."""
try:
data = subprocess.run(
["strings", input_img], capture_output=True, text=True, check=False
)
if data.stderr:
update_data(
output_dir, {"strings": {"status": "error", "error": data.stderr}}
)
return
# strings_dir = output_dir / "strings"
# strings_dir.mkdir(parents=True, exist_ok=True)
# with open(strings_dir / "strings.txt", "w", encoding="utf-8") as f:
# subprocess.run(["strings", input_img], stdout=f, check=True)
data_strings = data.stdout.split("\n") if data else []
data_strings = [s for s in data_strings if s]
update_data(
output_dir,
{
"strings": {
"status": "ok",
"output": data_strings,
}
},
)
except Exception as e:
update_data(output_dir, {"strings": {"status": "error", "error": str(e)}})
+74
View File
@@ -0,0 +1,74 @@
"""Custom Analyzer for Image Submissions."""
import shutil
import subprocess
from pathlib import Path
from .utils import update_data
def analyze_myanalyzer(input_img: Path, output_dir: Path) -> None:
"""Analyze an image submission using myanalyzer."""
image_name = input_img.name
# Dir generated by the tool
extracted_dir = output_dir / "myanalyzer_output_dir"
try:
stderr, stdout = "", ""
# Run myanalyzer
data = subprocess.run(
["myanalyzer", "-e", "../" + str(image_name)],
cwd=output_dir,
capture_output=True,
text=True,
check=False,
)
stderr += data.stderr
stdout += data.stdout
if len(stderr) > 0: # Return error if error has been raised
err = {
"myanalyzer": {
"status": "error",
"error": stderr,
}
}
update_data(output_dir, err)
return None
zip_exist = False
if extracted_dir.exists(): # If result file(s) exist, zip them
subprocess.run(
["7z", "a", "../myanalyzer.7z", "*"],
cwd=extracted_dir,
capture_output=True,
text=True,
check=False,
)
zip_exist = True
if extracted_dir.exists(): # Remove the extracted directory
shutil.rmtree(extracted_dir)
output = data.stdout.split("\n") if data else []
output_data = {
"myanalyzer": {
"status": "ok",
"output": output,
}
}
if zip_exist:
output_data["myanalyzer"][
"download"
] = f"/download/{output_dir.name}/myanalyzer"
update_data(output_dir, output_data)
except Exception as e:
update_data(output_dir, {"myanalyzer": {"status": "error", "error": str(e)}})
return None
+46
View File
@@ -0,0 +1,46 @@
"""Utility functions for analyzers modules."""
import fcntl
import json
import os
import threading
from pathlib import Path
from typing import Any
_thread_lock = threading.Lock()
def update_data(
output_dir: Path, new_data: dict[Any, Any], json_filename: str = "results.json"
) -> None:
"""Thread-safe and process-safe JSON update using lock file and atomic replace."""
json_file = Path(output_dir) / json_filename
lock_file = json_file.with_suffix(json_file.suffix + ".lock")
tmp_file = json_file.with_suffix(json_file.suffix + ".tmp")
json_file.parent.mkdir(parents=True, exist_ok=True)
with _thread_lock: # synchronizes across threads
with open(lock_file, "w", encoding="utf-8") as lock:
fcntl.flock(lock, fcntl.LOCK_EX) # synchronizes across processes
try:
# Read existing JSON
data: dict[Any, Any] = {}
if json_file.exists():
try:
with open(json_file, "r", encoding="utf-8") as f:
data = json.load(f)
except json.JSONDecodeError:
data = {}
# Update with new data
data.update(new_data)
# Write safely to a temp file
with open(tmp_file, "w", encoding="utf-8") as f:
json.dump(data, f, sort_keys=False)
os.replace(tmp_file, json_file) # ensures file write is atomic
finally:
fcntl.flock(lock, fcntl.LOCK_UN)
+36
View File
@@ -0,0 +1,36 @@
"""Zsteg Analyzer for Image Submissions."""
import subprocess
from pathlib import Path
from .utils import update_data
def analyze_zsteg(input_img: Path, output_dir: Path) -> None:
"""Analyze an image submission using zsteg."""
try:
data = subprocess.run(
["zsteg", input_img], capture_output=True, text=True, check=False
)
if data.stderr or "PNG::NotSupported" in data.stdout[:100]:
error_msg = data.stderr or data.stdout
update_data(output_dir, {"zsteg": {"status": "error", "error": error_msg}})
return
# zsteg_dir = output_dir / "zsteg"
# zsteg_dir.mkdir(parents=True, exist_ok=True)
# with open(zsteg_dir / "zsteg.txt", "w", encoding="utf-8") as f:
# subprocess.run(["zsteg", input_img], stdout=f, check=True)
update_data(
output_dir,
{
"zsteg": {
"status": "ok",
"output": data.stdout.split("\n") if data else [],
}
},
)
except Exception as e:
update_data(output_dir, {"zsteg": {"status": "error", "error": str(e)}})
+245
View File
@@ -0,0 +1,245 @@
"""AperiSolve Flask application."""
import hashlib
import json
import os
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
from flask import Flask, Response, abort, jsonify, render_template, request, send_file
from redis import Redis
from rq import Queue
from .cleanup import cleanup_old_entries
from .config import IMAGE_EXTENSIONS, RESULT_FOLDER, WORKER_FILES
from .models import Image, Submission, db, init_db
app: Flask = Flask(__name__)
app.json.sort_keys = False # type: ignore
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("DB_URI")
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
RESULT_FOLDER.mkdir(parents=True, exist_ok=True)
db.init_app(app)
init_db(app)
redis_conn = Redis(host="redis", port=6379)
queue = Queue(connection=redis_conn)
@app.route("/")
def index() -> str:
"""Render the main index page."""
return render_template("index.html")
@app.route("/faq")
def faq() -> str:
"""Render the FAQ page."""
return render_template("faq.html")
@app.route("/show")
def show() -> str:
"""Show all active submissions."""
db_images = Image.query.all()
images = []
for img in db_images:
last_sub = img.submissions[-1]
images.append({"file": f"/image/{img.hash}", "link": f"/{last_sub.hash}"})
return render_template("show.html", images=images)
@app.route("/<hash_val>", methods=["GET"])
def result_page(hash_val: str) -> str:
"""Render the result page for a given submission hash."""
submission = Submission.query.get_or_404(hash_val) # type: ignore
Image.query.get_or_404(submission.image_hash) # type: ignore
return render_template("result.html", hash_val=hash_val)
@app.route("/upload", methods=["POST"])
def upload_image() -> tuple[Response, int]:
"""Handle image upload and initiate analysis."""
cleanup_old_entries()
if "image" not in request.files:
return jsonify({"error": "No image provided"}), 400
image = request.files["image"]
password = request.form.get("password")
deep_analysis = request.form.get("deep") == "true"
if image.filename is None or image.filename == "":
return jsonify({"error": "No image provided"}), 400
if (
"." not in image.filename
or Path(image.filename).suffix.lower() not in IMAGE_EXTENSIONS
):
return jsonify({"error": "Unsupported file type"}), 400
# Compute hashes and prepare submission
image_data = image.read()
img_hash = hashlib.md5(image_data).hexdigest()
image_name = img_hash + "." + image.filename.rsplit(".", maxsplit=1)[-1].lower()
submission_data = (
image_data
+ image.filename.encode()
+ (password.encode() if password else b"")
+ (b"deep_analysis" if deep_analysis else b"")
)
submission_hash = hashlib.md5(submission_data).hexdigest()
submission_path = RESULT_FOLDER / img_hash / submission_hash
# If submission already exists, return submission hash
if submission_path.exists():
submission: Submission = Submission.query.filter_by(
hash=submission_hash
).first()
return jsonify({"submission_hash": submission.hash}), 200
# If image is new, create a new Image entry
new_img_path = RESULT_FOLDER / img_hash / image_name
if not new_img_path.parent.exists():
new_img_path.parent.mkdir(parents=True, exist_ok=True)
with open(new_img_path, "wb") as f: # Write file to disk
f.write(image_data)
new_img = Image(
file=str(new_img_path),
hash=img_hash,
size=len(image_data),
upload_count=0,
first_submission_date=datetime.now(timezone.utc),
last_submission_date=datetime.now(timezone.utc),
)
db.session.add(new_img)
db.session.commit()
sub_img = Image.query.filter_by(hash=img_hash).first() # type: ignore
sub_img.upload_count += 1
db.session.commit()
# Create new Submission entry
submission_path.mkdir(parents=True, exist_ok=True)
submission = Submission(
filename=image.filename,
password=password,
deep_analysis=deep_analysis,
hash=submission_hash,
status="pending",
date=time.time(),
image_hash=sub_img.hash, # type: ignore
)
db.session.add(submission)
db.session.commit() # Commit to save the new Image and Submission
# Start the analysis jobs
queue.enqueue("aperisolve.workers.analyze_image", submission.hash, job_timeout=300)
return jsonify({"submission_hash": submission.hash}), 200
@app.route("/status/<hash_val>", methods=["GET"])
def get_status(hash_val: str) -> Response:
"""Get the processing status of a submission."""
submission = Submission.query.get_or_404(hash_val) # type: ignore
return jsonify({"status": submission.status}) # type: ignore
@app.route("/infos/<hash_val>", methods=["GET"])
def get_infos(hash_val: str) -> Response:
"""Get the metadata and information of a submission."""
submission = Submission.query.get_or_404(hash_val)
image = Image.query.get_or_404(submission.image_hash) # type: ignore
names = [name for name in set(sub.filename for sub in image.submissions) if name]
passwords = [pwd for pwd in set(sub.password for sub in image.submissions) if pwd]
return jsonify(
{
"image_path": "image/" + str(Path(image.file).name),
"names": names,
"size": image.size,
"first_submission_date": image.first_submission_date,
"last_submission_date": image.last_submission_date,
"upload_count": image.upload_count,
"passwords": passwords,
}
) # type: ignore
@app.route("/result/<hash_val>", methods=["GET"])
def get_result(hash_val: str) -> tuple[Response, int]:
"""Get the analysis results of a submission."""
submission = Submission.query.get_or_404(hash_val)
image = Image.query.get_or_404(submission.image_hash) # type: ignore
result_path = (
RESULT_FOLDER / str(image.hash) / str(submission.hash) / "results.json"
)
if not result_path.exists():
return jsonify({"error": "Results not ready yet..."}), 425
with open(result_path, "r", encoding="utf-8") as f:
results = json.load(f)
return jsonify({"results": results}), 200
@app.route("/download/<hash_val>/<tool>")
def download_output(hash_val: str, tool: str) -> Response:
"""Download the output of a specific analyzer for a given submission hash."""
submission = Submission.query.filter_by(hash=hash_val).first_or_404()
image = Image.query.get_or_404(submission.image_hash) # type: ignore
output_dir = RESULT_FOLDER / str(image.hash) / str(submission.hash)
output_file = output_dir / f"{tool}.7z"
if tool not in WORKER_FILES or not output_file.exists():
abort(404, description="Tool output not found.")
return send_file(output_file, as_attachment=True)
@app.route("/image/<img_name>")
@app.route("/image/<hash_val>/<img_name>")
def get_image(
hash_val: Optional[str] = None, img_name: Optional[str] = None
) -> Response:
"""
Download the image with a specific name (usually ones generated with
decomposer) for a given submission hash.
"""
if img_name is None:
return abort(404, description="Image not found or unsupported format")
if hash_val is not None:
submission = Submission.query.filter_by(hash=hash_val).first_or_404()
image = Image.query.get_or_404(submission.image_hash) # type: ignore
output_dir = RESULT_FOLDER / str(image.hash) / str(submission.hash)
output_file = output_dir / Path(img_name).name
else:
hash_val = img_name.split(".")[0] # Extract hash from the image name
image = Image.query.filter_by(hash=hash_val).first_or_404()
output_file = Path(image.file) # type: ignore
if (not output_file.exists()) or (
output_file.suffix.lower() not in IMAGE_EXTENSIONS
):
return abort(404, description="Image not found or unsupported format")
return send_file(output_file, as_attachment=True)
if __name__ == "__main__":
debug_mode = os.environ.get("FLASK_DEBUG", "false").lower() == "true"
app.run(host="0.0.0.0", port=5000, debug=debug_mode)
+50
View File
@@ -0,0 +1,50 @@
"""Cleanup old entries in the database and file system."""
import shutil
import time
from datetime import datetime, timezone
from os import getenv
from pathlib import Path
from .models import Image, Submission, db
RESULT_FOLDER = Path("./results")
MAX_PENDING_TIME = int(getenv("MAX_PENDING_TIME", "600")) # 10 minutes by default
MAX_STORE_TIME = int(getenv("MAX_STORE_TIME", "259200")) # 3 days by default
def cleanup_old_entries() -> None:
"""Clean up old entries in the database and file system."""
now = time.time()
for submission in Submission.query.all(): # type: ignore
if (
submission.status in ("pending", "running") # type: ignore
and now - submission.date > MAX_PENDING_TIME # type: ignore
):
# Processing took too long, delete
db.session.delete(submission) # type: ignore
elif submission.status == "done": # type: ignore
# Search for buggy results, delete
result_path = (
RESULT_FOLDER / str(submission.image_hash) / str(submission.hash)
)
result_file = result_path / "results.json"
if not result_file.exists():
db.session.delete(submission) # type: ignore
shutil.rmtree(result_path)
# Delete "old" images
for img in Image.query.all(): # type: ignore
if img.last_submission_date.tzinfo is None:
img_date = img.last_submission_date.replace(tzinfo=timezone.utc)
else:
img_date = img.last_submission_date
delay = datetime.now(timezone.utc) - img_date
if delay.total_seconds() > MAX_STORE_TIME:
for s in img.submissions:
db.session.delete(s) # type: ignore
shutil.rmtree(RESULT_FOLDER / img.hash) # type: ignore
db.session.delete(img)
db.session.commit()
+8
View File
@@ -0,0 +1,8 @@
"""AperiSolve configuration variables."""
from pathlib import Path
IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff"]
WORKER_FILES = ["binwalk", "foremost", "steghide", "zsteg"]
RESULT_FOLDER = Path(__file__).parent.resolve() / "results"
+61
View File
@@ -0,0 +1,61 @@
"""This module defines the database models for the Aperisolve application."""
from datetime import datetime, timezone
from decimal import Decimal
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String
db: SQLAlchemy = SQLAlchemy()
class Image(db.Model): # type: ignore
"""Model representing an image file in the database."""
hash: Column[str] = Column(
String(64), primary_key=True, unique=True, nullable=False
)
file: Column[str] = Column(String(128), unique=True, nullable=False)
size: Column[int] = Column(Integer, nullable=False)
first_submission_date: Column[datetime] = Column(
DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)
)
last_submission_date: Column[datetime] = Column(
DateTime,
nullable=False,
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
)
upload_count: Column[int] = Column(Integer)
submissions = db.relationship("Submission", backref="image", lazy=True)
class Submission(db.Model): # type: ignore
"""Model representing a file submission for analysis.
Submissions are defined with a filename, password, image content, and analysis
option.
"""
hash: Column[str] = Column(
String(128), primary_key=True, unique=True, nullable=False
)
filename: Column[str] = Column(String(128), nullable=False)
password: Column[str] = Column(String(128))
deep_analysis: Column[bool] = Column(Boolean, default=False)
status: Column[str] = Column(String(20), default="pending")
date: Column[Decimal] = Column(
Float, nullable=False, default=lambda: datetime.now(timezone.utc)
)
# Foreign key to Image
image_hash = Column(String, db.ForeignKey("image.hash"), nullable=False)
def init_db(app: Flask) -> None:
"""Initialize the database with the given Flask app."""
with app.app_context():
if not db.engine.dialect.has_table(db.engine.connect(), "image"):
print("Creating database...")
db.create_all()
print("Database created successfully.")
+10
View File
@@ -0,0 +1,10 @@
Flask
Flask-SQLAlchemy
numpy
redis
rq
psycopg2-binary
types-Flask-SQLAlchemy
sqlalchemy-stubs
Pillow
gunicorn
+410
View File
@@ -0,0 +1,410 @@
/* ======== Base Layout ======== */
::selection {
background: #c9fbff;
color: black;
}
:root {
--theme-dark-blue: #0b121f;
--theme-dark-blue-hover: #0d1931;
--theme-green: #9fef00;
--theme-green-shadow: #9fef0055;
--theme-green-shadow2: #9fef00aa;
}
html {
--dark-bg: var(--theme-dark-blue);
--dark-bg-hover: var(--theme-dark-blue-hover);
--bs-progress-bar-bg: #f00 !important;
}
body {
background-color: var(--dark-bg);
color: #e0e0e0;
font-family: 'Fira Code', monospace;
}
/* ======== Typography ======== */
h1,
h2,
h3,
h4,
h5,
h6,
strong,
.highlight {
color: var(--theme-green);
text-shadow: 0 0 5px var(--theme-green);
}
a,
.text-accent {
color: var(--theme-green);
text-decoration: none;
}
a:hover {
color: #ccff66;
text-shadow: 0 0 5px #ccff66;
}
.title_link {
text-decoration: none;
color: inherit;
}
/* ======== Background ======== */
#three-bg {
position: fixed;
top: 0;
left: 0;
z-index: 0;
width: 100%;
height: 100%;
pointer-events: none;
background-color: transparent;
}
.container,
#result,
form {
position: relative;
z-index: 1;
}
/* ======== Components ======== */
.card,
.form-control,
.table,
.border,
.card-hack {
background-color: var(--dark-bg);
border: 1px solid var(--theme-green) !important;
color: #e0e0e0;
box-shadow: 0 0 10px var(--theme-green-shadow);
border-radius: 0.5rem;
}
.material-icons {
vertical-align: middle;
}
#main-img-right span {
font-weight: bold;
color: var(--theme-green);
}
#water-logo {
width: 70px;
display: inline;
}
/* Buttons */
.btn,
.btn-primary,
.btn-hack {
background-color: var(--theme-green);
color: #000;
font-weight: bold;
border: none;
text-shadow: none;
}
.btn:hover,
.btn-primary:hover,
.btn-hack:hover {
background-color: var(--theme-green);
box-shadow: 0 0 10px var(--theme-green-shadow2);
}
/* Bootstrap contextual colors override */
.text-success,
.bg-success {
color: var(--theme-green) !important;
background-color: #1e2b00 !important;
}
.text-danger,
.bg-danger {
color: #ff4d4d !important;
background-color: #330000 !important;
}
.text-warning,
.bg-warning {
color: #ffcc00 !important;
background-color: #332b00 !important;
}
.text-info,
.bg-info {
color: #00e6e6 !important;
background-color: #002e2e !important;
}
.analyzer {
margin-top: 25px;
}
.analyzer h2+div {
margin-top: 15px;
}
.analyzer h3 {
margin-top: 25px;
}
/* ======== Forms & Inputs ======== */
input,
textarea,
select,
pre {
border: 1px solid var(--theme-green);
color: #ffffff;
}
input:focus,
textarea:focus,
select:focus,
pre {
outline: none;
box-shadow: 0 0 10px var(--theme-green);
background-color: var(--dark-bg-hover) !important;
}
textarea.form-control,
pre {
padding: 25px;
}
.textarea-container {
display: block;
}
.copy-icon {
position: absolute;
right: 10px;
margin-top: -40px;
cursor: pointer;
display: none;
color: #555;
background: rgba(255, 255, 255, 0.8);
border-radius: 4px;
padding: 4px;
transition: color 0.2s;
}
.textarea-container:hover .copy-icon {
display: block;
}
.copy-icon:hover {
color: #000;
}
/* File drop zone */
#drop-zone {
background-color: var(--dark-bg);
border: 2px dashed var(--theme-green);
color: var(--theme-green);
transition: all 0.3s ease;
}
#drop-zone:hover,
#drop-zone.dragover {
background-color: var(--dark-bg-hover);
box-shadow: 0 0 10px var(--theme-green);
}
#upload-form {
transition: height 0.4s ease, padding 0.4s ease, margin 0.4s ease;
overflow: hidden;
}
/* ======== Tables ======== */
table {
width: 100%;
background-color: var(--theme-dark-blue);
border-collapse: collapse;
color: white;
}
th,
td {
border: 1px solid var(--theme-dark-blue);
padding: 0.75rem;
}
th,
td:first-child {
color: var(--theme-green);
}
tr:nth-child(even) {
background-color: rgba(0, 0, 0, 0.25);
}
tr:first-child td {
border-top: 1px solid #ddd;
}
td {
text-align: left;
border-bottom: 1px solid #ddd;
}
/* ======== Images ======== */
#result img {
width: 100%;
height: auto;
cursor: pointer;
}
.results_img {
width: 25%;
display: inline-block;
padding: 10px;
box-sizing: border-box;
}
/* ======== Modal ======== */
#image-modal-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
z-index: 1000;
}
#image-modal-container img {
width: 60%;
border: 2px solid var(--theme-green);
box-shadow: 0 0 20px var(--theme-green);
cursor: pointer;
}
#image-modal-container.modal-visible {
opacity: 1;
visibility: visible;
}
.browse-images {
display: inline-block;
padding: 10px;
width: 25%;
}
/* ======== FAQ ======== */
#cheatsheet>div {
background-color: rgba(0, 0, 0, 0.5);
padding: 10px;
margin-top: 10px;
margin-bottom: 50px;
}
.command {
background-color: var(--theme-dark-blue);
border: 3px solid var(--theme-green);
padding: 5px 10px;
margin-bottom: 25px;
}
#cheatsheet ul {
list-style: none;
padding-left: 5px;
margin-left: 0;
}
#cheatsheet li::before {
content: "\f054";
font-family: "Font Awesome 6 Free";
font-weight: 900;
position: absolute;
left: 0;
top: 0;
color: var(--theme-green);
}
#cheatsheet li {
padding-left: 25px;
}
#cheatsheet img,
#cheatsheet table {
margin-bottom: 15px;
}
/* ======== Analyzer Blocks ======== */
.analyzer {
margin-bottom: 10px;
}
#result-infos {
display: flex;
}
#result-infos>div {
height: 100%;
width: 50%;
height: auto;
flex: 1 1 0;
width: 50%;
display: inline-block;
padding: 10px;
box-sizing: border-box;
vertical-align: top;
}
.a-exiftool .table-container {
max-height: 400px;
overflow-y: auto;
}
.a-strings textarea {
height: 400px;
}
/* ======== Footer ======== */
footer {
position: relative;
z-index: 1;
margin: 0 20px 20px 0;
justify-content: flex-end !important;
}
@media screen and (min-width: 1800px) {
footer {
position: fixed;
bottom: 0;
right: 0;
left: auto;
margin: 0 20px 20px 0;
}
}
footer a i {
font-size: 32px;
}
/* ======== Utility ======== */
.glow-border {
border: 1px solid var(--theme-green);
box-shadow: 0 0 10px var(--theme-green-shadow);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

+38
View File
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
<path style="stroke: rgb(0, 0, 0); fill: rgb(248, 248, 248);" d="M 31.958 348.979 L 46.064 320.766 C 46.047 320.759 47.854 319.528 49.992 320.984 C 235.683 447.434 428.251 360.852 447.82 321.895 C 455.205 307.194 469.197 361.358 470.39 359.7 L 464.183 365.343 C 312.335 459.691 89.515 409.047 39.857 354.622 L 31.958 348.979 Z"/>
<g transform="matrix(2.383155, 0, 0, 2.383155, -289.696167, -169.945801)" style="">
<path style="stroke: rgb(0, 0, 0); fill: rgb(239, 0, 64);" d="M 140.55 205.12 C 173.246 149.661 211.285 96.449 226.104 85.795 C 227.402 84.861 228.115 84.366 228.115 84.366 C 239.087 83.147 310.831 208.184 310.831 208.184 C 310.831 208.184 309.411 207.705 308.189 209.024 C 277.042 242.645 200.771 242.626 141.917 207.581 C 141.117 207.105 140.069 205.936 140.55 205.12 Z"/>
<path style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0);" d="M 256.995 160.248 C 248.803 157.518 250.924 171.394 257.679 169.143 C 260.181 168.308 260.764 166.876 259.503 164.353 C 258.837 163.021 259.381 159.336 257.451 159.336"/>
<path style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0);" d="M 215.943 161.388 C 214.039 161.388 213.999 163.571 213.435 165.266 C 213.326 165.593 212.492 167.06 212.979 167.546 C 213.538 168.106 216.797 170.114 217.768 169.143 C 220.406 166.504 220.814 163.71 217.084 161.845 C 216.557 161.581 216.095 160.476 215.487 160.476"/>
<path style="stroke: rgb(0, 0, 0); fill: rgb(159, 239, 0);" d="M 124.977 235.493 C 127.445 230.983 129.889 226.459 132.38 221.962 C 132.953 220.928 133.002 218.705 134.168 218.899 C 138.489 219.619 142.108 222.611 146.166 224.26 C 158.45 229.25 168.269 234.918 183.184 238.812 C 211.525 246.212 242.407 248.212 271.005 241.875 C 291.758 237.276 302.132 226.415 318.49 220.686 C 319.92 220.185 320.587 222.922 321.298 224.26 C 323.495 228.396 330.058 233.337 327.17 237.025 C 295.801 277.087 172.547 281.531 124.977 233.195"/>
<path style="fill: rgb(216, 216, 216); stroke: rgb(0, 0, 0);" d="M 221.873 166.178 C 220.414 166.907 219.129 167.561 218.452 168.915 C 217.611 170.597 220.359 174.077 221.645 172.792 C 223.341 171.096 224.223 169.916 225.294 167.774 C 226.197 165.967 223.469 164.01 223.469 163.213"/>
<g transform="matrix(0.1, 0, 0, -0.1, 79.254257, 330.714203)" fill="#000000" stroke="none">
<path d="M 1380 2413 C 1212 2240 859 1714 557 1187 C 453 1004 438 974 446 951 C 464 900 665 784 845 720 C 1058 645 1282 610 1547 610 C 1807 610 2015 646 2198 724 C 2341 784 2500 896 2500 936 C 2500 963 1966 1886 1812 2125 C 1724 2261 1619 2399 1571 2443 C 1537 2473 1522 2480 1488 2480 C 1449 2480 1441 2475 1380 2413 Z M 1562 2383 C 1681 2259 1833 2018 2249 1293 C 2426 984 2466 903 2429 925 C 2424 929 2420 926 2420 920 C 2420 914 2249 791 2107 741 C 1799 633 1388 625 1013 720 C 885 752 780 790 780 804 C 780 809 809 878 844 959 C 880 1039 970 1251 1045 1429 C 1226 1860 1338 2116 1416 2279 C 1451 2353 1480 2415 1480 2419 C 1480 2422 1468 2407 1453 2386 C 1404 2312 1263 2004 993 1375 C 908 1177 835 1011 831 1007 C 821 997 729 1037 650 1085 C 618 1104 590 1120 586 1120 C 573 1120 581 1097 598 1083 C 632 1056 821 984 815 963 C 811 947 753 817 749 814 C 733 805 513 930 495 959 C 492 965 537 1055 596 1158 C 889 1669 1232 2184 1400 2363 C 1478 2447 1497 2449 1562 2383 Z"/>
<path d="M 1505 2392 C 1518 2383 1580 2304 1580 2297 C 1580 2294 1565 2294 1548 2296 C 1520 2299 1519 2298 1537 2290 C 1570 2276 1634 2190 1697 2072 C 1728 2014 1755 1971 1757 1977 C 1760 1984 1754 2002 1745 2019 C 1737 2036 1725 2062 1720 2077 C 1711 2104 1711 2105 1726 2083 C 1735 2071 1753 2049 1766 2033 C 1779 2018 1797 1989 1807 1970 C 1816 1951 1837 1914 1854 1889 C 1871 1863 1892 1830 1901 1814 C 1955 1721 2052 1553 2060 1540 C 2065 1532 2086 1496 2107 1460 C 2127 1424 2167 1353 2197 1303 C 2226 1252 2247 1204 2244 1196 C 2236 1176 2138 1118 2050 1082 C 1859 1004 1678 974 1461 983 C 1299 990 1275 994 1145 1027 C 1090 1041 910 1094 910 1082 C 910 1071 962 1049 1051 1020 C 1248 958 1528 935 1728 964 C 1887 988 1997 1022 2136 1090 C 2269 1156 2290 1172 2273 1198 C 2268 1207 2187 1346 2095 1505 C 1888 1862 1829 1961 1725 2120 C 1635 2257 1523 2400 1506 2400 C 1499 2400 1498 2397 1505 2392 Z"/>
<path d="M1501 2247 c-1 -23 8 -52 22 -75 12 -20 28 -48 35 -62 20 -40 113 -163 118 -157 3 3 -17 36 -44 74 -66 91 -114 178 -123 222 l-7 36 -1 -38z"/>
<path d="M1474 2146 c-6 -17 2 -36 17 -36 12 0 12 37 -1 45 -6 3 -13 -1 -16 -9z"/>
<path d="M1602 2139 c1 -12 9 -24 16 -27 9 -3 13 3 10 19 -1 12 -9 24 -16 27 -9 3 -13 -3 -10 -19z"/>
<path d="M1166 1985 c-28 -42 -17 -78 16 -48 18 16 25 73 9 73 -5 0 -16 -11 -25 -25z"/>
<path d="M1706 1962 c-3 -6 2 -16 11 -22 17 -11 17 -11 0 -6 -12 4 -17 2 -15 -6 6 -18 38 -22 38 -6 0 22 -26 53 -34 40z"/>
<path d="M1404 1936 c-8 -22 3 -49 19 -43 6 2 12 14 12 27 0 27 -22 39 -31 16z"/>
<path d="M1562 1923 c2 -14 8 -28 14 -30 17 -6 26 27 13 43 -18 22 -31 16 -27 -13z"/>
<path d="M1778 1910 c12 -5 27 -21 33 -34 29 -62 -33 -77 -63 -16 -15 29 -18 31 -18 13 0 -46 59 -80 88 -51 29 29 -4 99 -45 97 -14 0 -13 -2 5 -9z"/>
<path d="M1365 1728 c-33 -18 -45 -40 -45 -83 0 -30 6 -47 23 -63 52 -49 127 -12 127 64 -1 42 -13 64 -47 82 -26 14 -32 14 -58 0z m30 -68 c0 -18 -6 -26 -22 -28 -20 -3 -23 1 -23 28 0 27 3 31 23 28 16 -2 22 -10 22 -28z m45 -38 c0 -4 -5 -14 -10 -22 -9 -13 -11 -13 -19 0 -6 9 -6 19 1 27 10 12 28 9 28 -5z"/>
<path d="M1755 1728 c-54 -31 -61 -93 -16 -139 16 -16 37 -29 46 -29 29 0 75 55 75 90 0 22 -9 41 -29 61 -32 31 -45 35 -76 17z m30 -77 c-9 -29 -39 -23 -43 8 -2 13 2 27 10 32 19 12 41 -14 33 -40z"/>
<path d="M997 1672 c-18 -20 -23 -62 -8 -62 14 0 41 41 41 62 0 23 -13 23 -33 0z"/>
<path d="M1524 1556 c-17 -43 31 -116 75 -116 42 0 62 23 74 85 5 30 5 30 -41 30 -26 0 -59 4 -74 9 -22 6 -29 4 -34 -8z m94 -58 c19 -16 14 -38 -10 -38 -21 0 -58 26 -58 40 0 11 53 9 68 -2z"/>
<path d="M1822 1514 c-27 -18 -28 -36 -4 -57 23 -21 51 -22 80 -1 26 18 28 37 6 58 -20 20 -53 20 -82 0z m72 -10 c24 -9 19 -33 -9 -40 -33 -8 -75 3 -75 20 0 22 48 34 84 20z"/>
<path d="M1292 1488 c-31 -31 -1 -68 55 -68 33 0 53 16 53 42 0 33 -81 53 -108 26z m72 -8 c14 -5 26 -14 26 -20 0 -16 -42 -29 -73 -23 -43 8 -34 34 20 52 1 1 13 -3 27 -9z"/>
<path d="M835 1351 c-14 -28 -12 -61 4 -61 17 0 31 26 31 57 0 41 -15 42 -35 4z"/>
<path d="M2034 1306 c-10 -25 13 -68 34 -64 25 5 22 48 -5 65 -20 14 -23 14 -29 -1z"/>
<path d="M1131 1261 c-15 -27 -14 -46 5 -61 12 -11 18 -10 31 3 13 13 15 23 7 44 -12 34 -30 40 -43 14z"/>
<path d="M1401 1231 c-16 -29 -13 -58 6 -65 36 -14 50 47 18 74 -10 9 -16 6 -24 -9z"/>
<path d="M1714 1226 c-3 -8 -4 -25 -2 -38 4 -32 42 -32 46 0 6 38 -32 70 -44 38z"/>
<path d="M654 1201 c22 -24 196 -114 205 -106 8 9 -19 28 -116 80 -88 49 -117 57 -89 26z"/>
<path d="M 2260 1044 C 1912 837 1410 803 947 956 C 904 970 866 979 862 975 C 846 959 1325 845 1325 845 C 1397 831 1617 828 1718 840 C 1832 853 2081 930 2090 930 C 2098 930 2332 1045 2332 1045 L 2349 1022 C 2358 1009 2378 984 2394 967 L 2422 935 L 2394 985 C 2379 1013 2360 1047 2352 1063 C 2345 1078 2338 1090 2337 1089 C 2336 1089 2301 1069 2260 1044"/>
<path d=""/>
<path d=""/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

+478
View File
@@ -0,0 +1,478 @@
const TOOL_ORDER = [
"decomposer",
"exiftool",
"binwalk",
"foremost",
"outguess",
"steghide",
"zsteg",
"strings",
];
function escapeHtml(text) {
if (typeof text !== "string") return text;
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function capitalize(str) {
if (typeof str !== "string" || str.length === 0) return str;
return str.charAt(0).toUpperCase() + str.slice(1);
}
function slideUp(element, duration = 400) {
element.style.height = element.offsetHeight + "px";
element.offsetHeight; // force reflow
element.style.transitionProperty = "height, margin, padding";
element.style.transitionDuration = duration + "ms";
element.style.boxSizing = "border-box";
element.style.overflow = "hidden";
element.style.height = 0;
element.style.paddingTop = 0;
element.style.paddingBottom = 0;
element.style.marginTop = 0;
element.style.marginBottom = 0;
window.setTimeout(() => {
element.style.display = "none";
// Optional: reset styles
element.style.removeProperty("height");
element.style.removeProperty("padding-top");
element.style.removeProperty("padding-bottom");
element.style.removeProperty("margin-top");
element.style.removeProperty("margin-bottom");
element.style.removeProperty("overflow");
element.style.removeProperty("transition-duration");
element.style.removeProperty("transition-property");
}, duration);
}
function formatBytes(bytes) {
if (typeof bytes !== "number" || isNaN(bytes)) return bytes;
const units = ["o", "Ko", "Mo", "Go", "To", "Po"];
let i = 0;
while (bytes >= 1024 && i < units.length - 1) {
bytes /= 1024;
i++;
}
return `${bytes % 1 === 0 ? bytes : bytes.toFixed(2)} ${units[i]}`;
}
/**
* Alert functions
*/
function showAlert(message, alertType, reset) {
const resultDiv = document.getElementById("result-analyzers");
if (reset) {
resultDiv.innerHTML = "";
}
resultDiv.innerHTML += `<div class="alert alert-${escapeHtml(
alertType
)}" role="alert">${escapeHtml(message)}</div>`;
}
function showSuccess(message, reset) {
showAlert(message, "success", reset);
}
function showDanger(message, reset) {
showAlert(message, "danger", reset);
}
function showWarning(message, reset) {
showAlert(message, "warning", reset);
}
function showInfo(message, reset) {
showAlert(message, "info", reset);
}
/**
* Copy to clipboard
*/
document.addEventListener('click', function (event) {
// Check if the clicked element is a copy icon
if (event.target.classList.contains('copy-icon')) {
const icon = event.target;
const textarea = icon.closest('.textarea-container').querySelector('textarea');
if (textarea) {
textarea.select();
navigator.clipboard.writeText(textarea.value).then(() => {
icon.classList.remove('fa-copy');
icon.classList.add('fa-check');
setTimeout(() => {
icon.classList.remove('fa-check');
icon.classList.add('fa-copy');
}, 1500);
});
}
}
});
/**
* Image preview functions
*/
// Reference the modal elements
const modal = document.getElementById("image-modal-container");
const modalImage = document.getElementById("modal-image");
let currentImages = [];
let currentIndex = -1;
function updateCurrentImages() {
currentImages = Array.from(document.querySelectorAll(".results_img img"));
}
// Function to handle image click (open modal)
function openImageModal(src) {
updateCurrentImages();
currentIndex = currentImages.findIndex((img) => img.src === src);
if (currentIndex !== -1) {
modalImage.src = currentImages[currentIndex].src;
modal.classList.add("modal-visible");
modal.classList.remove("modal-hidden");
}
}
function closeImageModal() {
modal.classList.remove("modal-visible");
modal.classList.add("modal-hidden");
currentIndex = -1;
}
// Handle dynamic images added with innerHTML
document.addEventListener("click", function (e) {
const img = e.target.closest(".results_img img");
if (img) {
openImageModal(img.src);
}
});
// Click on modal image to download and close
modalImage.addEventListener("click", function () {
const link = document.createElement("a");
link.href = modalImage.src;
link.download = modalImage.src.split("/").pop();
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
closeImageModal();
});
// Optional: Close modal if user clicks outside the image
modal.addEventListener("click", function (e) {
if (e.target === modal) {
closeImageModal();
}
});
// Keyboard support
document.addEventListener("keydown", function (e) {
if (!modal.classList.contains("modal-visible")) return;
if (e.key === "Escape") {
closeImageModal();
} else if (e.key === "ArrowLeft") {
// Show previous image
if (currentIndex > 0) {
currentIndex--;
modalImage.src = currentImages[currentIndex].src;
}
} else if (e.key === "ArrowRight") {
// Show next image
if (currentIndex < currentImages.length - 1) {
currentIndex++;
modalImage.src = currentImages[currentIndex].src;
}
}
});
async function fetchImageInfo(submission_hash) {
const infoResp = await fetch(`/infos/${submission_hash}`);
const infoData = await infoResp.json();
const resultInfosDiv = document.getElementById("result-infos");
resultInfosDiv.innerHTML = ""; // Clear previous info
const mainImgLeft = document.createElement("div");
const mainImgRight = document.createElement("div");
const tableInfos = document.createElement("table");
mainImgLeft.id = "main-img-left";
mainImgRight.id = "main-img-right";
resultInfosDiv.appendChild(mainImgLeft);
resultInfosDiv.appendChild(mainImgRight);
mainImgRight.appendChild(tableInfos);
mainImgLeft.innerHTML += `<div id="main_image"><img src="${infoData.image_path}"/></div>`;
tableInfos.innerHTML += `<tr><td><i class="fa fa-backward-step"></i> First upload:</td><td>${infoData.first_submission_date}</td></tr>`;
tableInfos.innerHTML += `<tr><td><i class="fa fa-history"></i> Last upload:</td><td>${infoData.last_submission_date}</td></tr>`;
if (Array.isArray(infoData.names)) {
const nameList = infoData.names
.map((name) => `<code>${escapeHtml(name)}</code>`)
.join(", ");
tableInfos.innerHTML += `<tr><td><i class="fa fa-list"></i> Name(s):</td><td>${nameList}</td></tr>`;
}
tableInfos.innerHTML += `<tr><td><i class="fa fa-balance-scale"></i> Size:</td><td>${formatBytes(
infoData.size
)}</td></tr>`;
tableInfos.innerHTML += `<tr><td><i class="fa fa-upload"></i> Upload count:</td><td>${infoData.upload_count}</td></tr>`;
if (Array.isArray(infoData.passwords)) {
const passwordList = infoData.passwords
.map((pwd) => `<code>${escapeHtml(pwd)}</code>`)
.join(", ");
tableInfos.innerHTML += `<tr><td><i class="fa fa-key"></i> Common password(s):</td><td>${passwordList}</td></tr>`;
}
}
/**
* Parsing results from the server
*/
function parseResult(result) {
const resultDiv = document.getElementById("result-analyzers");
resultDiv.innerHTML = "";
for (const tool of TOOL_ORDER) {
if (!Object.keys(result).includes(tool)) {
// Skip if the tool is not in the result
continue;
}
const analyzer = document.createElement("div");
analyzer.className = `analyzer a-${escapeHtml(tool)}`;
resultDiv.appendChild(analyzer);
analyzer.innerHTML += `<h2>${capitalize(tool)}</h2>`;
if (result[tool]["status"] === "ok") {
if ("images" in result[tool]) {
// Parse image output
var channels = ["Superimposed", "Red", "Green", "Blue", "Alpha"];
if (Object.keys(result[tool]["images"]).length == 1) {
channels = ["Greyscale"];
} else if (Object.keys(result[tool]["images"]).length == 4) {
channels = ["Superimposed", "Red", "Green", "Blue"];
}
for (const channel of channels) {
const images = result[tool]["images"][channel];
analyzer.innerHTML += `<h3>${capitalize(escapeHtml(channel))}</h3>`;
for (const image of images) {
analyzer.innerHTML += `<div class='results_img'><img src='${escapeHtml(
image
)}'/></div>`;
}
}
}
// Parse text output
if (typeof result[tool]["output"] === "string") {
analyzer.innerHTML += `<pre>${escapeHtml(
result[tool]["output"]
)}</pre>`;
} else if (Array.isArray(result[tool]["output"])) {
if (result[tool]["output"].length > 0) {
var texarea_content = `<div class="textarea-container">`;
texarea_content += `<textarea class="form-control w-100 mb-2" rows="8" readonly>`;
for (const line of result[tool]["output"]) {
texarea_content += escapeHtml(`${line}\n`);
}
texarea_content += `</textarea>`;
texarea_content += `<i class="fas fa-copy copy-icon"></i>`;
texarea_content += `</div>`;
analyzer.innerHTML += texarea_content;
}
} else if (typeof result[tool]["output"] === "object") {
var table_content = `<div class="table-container">`;
table_content += `<table>`;
for (const key in result[tool]["output"]) {
table_content += `<tr><td>${escapeHtml(key)}</td>`;
table_content += `<td>${escapeHtml(
result[tool]["output"][key]
)}</td></tr>`;
}
table_content += `</table>`;
table_content += `</table>`;
analyzer.innerHTML += table_content;
} else {
// analyzer.innerHTML += `<pre>${result[tool]["output"]}</pre>`;
}
if ("download" in result[tool]) {
// Parse download link
analyzer.innerHTML += `<br/><a href="${escapeHtml(
result[tool]["download"]
)}" target="_blank" class="btn btn-primary mt-2"><i class="fa fa-download"></i> Download file</a>`;
}
} else if (result[tool]["status"] === "error") {
showDanger(result[tool]["error"]);
}
}
}
// Poll status of a submission
async function pollStatus(submission_hash) {
if (!window.location.pathname.endsWith(`/${submission_hash}`)) {
window.history.pushState({}, "", `/${submission_hash}`);
}
const statusResp = await fetch(`/status/${submission_hash}`);
const statusData = await statusResp.json();
if (statusData.status === "completed") {
const resultResp = await fetch(`/result/${submission_hash}`);
const resultData = await resultResp.json();
fetchImageInfo(submission_hash);
if ("error" in resultData){
showWarning(`❌ ${resultData.error}`, true);
}else if ("results" in resultData) {
parseResult(resultData.results);
}
} else if (statusData.status === "error") {
//resultDiv.innerHTML = "";
showDanger("❌ Error during the analysis.", true);
} else {
setTimeout(() => pollStatus(submission_hash), 1000);
try {
const resultResp = await fetch(`/result/${submission_hash}`);
const resultData = await resultResp.json();
if ("error" in resultData){
// showWarning(`❌ ${resultData.error}`, true);
}else if ("results" in resultData) {
fetchImageInfo(submission_hash);
parseResult(resultData.results);
}
} catch (error) {
console.dir(error);
}
}
}
/**
* Drag and drop functionality
*/
const dropZone = document.getElementById("drop-zone");
const fileInput = document.getElementById("image");
const browseBtn = document.getElementById("browse-btn");
const dragMsg = document.getElementById("drag-msg");
function showFilename(filename) {
dragMsg.innerHTML = "🖼️ " + escapeHtml(filename);
}
if (browseBtn) {
// Clicking the button triggers file input
browseBtn.addEventListener("click", () => fileInput.click());
dropZone.addEventListener("click", () => fileInput.click());
dropZone.addEventListener("dragover", (e) => {
e.preventDefault();
dropZone.style.boxShadow = "0 0 20px #9fef00";
});
dropZone.addEventListener("dragleave", () => {
dropZone.style.boxShadow = "";
});
// Drag events
["dragenter", "dragover"].forEach((eventName) => {
dropZone.addEventListener(eventName, (e) => {
e.preventDefault();
dropZone.classList.add("dragover");
});
});
["dragleave", "drop"].forEach((eventName) => {
dropZone.addEventListener(eventName, (e) => {
e.preventDefault();
dropZone.classList.remove("dragover");
});
});
// Drop file
dropZone.addEventListener("drop", (e) => {
const files = e.dataTransfer.files;
if (files.length > 0) {
fileInput.files = files; // Attach files to the hidden input
showFilename(fileInput.files[0].name);
}
});
fileInput.addEventListener("change", () => {
if (fileInput.files.length > 0) {
showFilename(fileInput.files[0].name);
}
});
// File upload Form submission
document
.getElementById("upload-form")
.addEventListener("submit", function (e) {
e.preventDefault();
const fileInput = document.getElementById("image");
const passwordInput = document.getElementById("password");
const deepCheckbox = document.getElementById("deep");
const progressContainer = document.getElementById("progress-container");
const progressBar = document.getElementById("progress-bar");
if (!fileInput.files.length) {
showDanger("Please select an image.", true);
}
const formData = new FormData();
formData.append("image", fileInput.files[0]);
formData.append("password", passwordInput.value);
formData.append("deep", deepCheckbox.checked);
const xhr = new XMLHttpRequest();
xhr.open("POST", "/upload", true);
xhr.upload.addEventListener("loadstart", () => {
progressContainer.classList.remove("d-none");
progressBar.style.width = "0%";
progressBar.textContent = "0%";
});
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
progressBar.style.width = percent + "%";
progressBar.textContent = percent + "%";
}
});
xhr.onload = () => {
if (xhr.status === 200) {
try {
const response = JSON.parse(xhr.responseText);
if (response.submission_hash) {
pollStatus(response.submission_hash);
} else {
showDanger(
"❌ Invalid server response: missing submission_hash.",
true
);
}
} catch (e) {
showDanger("❌ Invalid server response.", true);
}
} else {
showDanger(`❌ HTTP error ${xhr.status}`, true);
}
progressBar.style.width = "100%";
progressBar.textContent = "Upload complete";
const uploadForm = document.getElementById("upload-form");
slideUp(uploadForm);
};
xhr.onerror = () => {
showDanger("❌ An error occurred during the transfer", true);
};
xhr.send(formData);
});
}
+167
View File
@@ -0,0 +1,167 @@
import * as THREE from "three";
import Stats from "three/addons/stats.module.js";
let container, stats;
let camera, scene, renderer, mesh;
const instances = 5000;
let lastTime = 0;
const moveQ = new THREE.Quaternion(0.5, 0.5, 0.5, 0.0).normalize();
const tmpQ = new THREE.Quaternion();
const tmpM = new THREE.Matrix4();
const currentM = new THREE.Matrix4();
init();
function init() {
container = document.getElementById("three-bg");
camera = new THREE.PerspectiveCamera(
50,
window.innerWidth / window.innerHeight,
1,
1000
);
scene = new THREE.Scene();
// Get the computed background color of the body
const bodyBg = getComputedStyle(document.body).backgroundColor;
scene.background = new THREE.Color(bodyBg);
// geometry
const geometry = new THREE.InstancedBufferGeometry();
// per mesh data x,y,z,w,u,v,s,t for 4-element alignment
// only use x,y,z and u,v; but x, y, z, nx, ny, nz, u, v would be a good layout
const vertexBuffer = new THREE.InterleavedBuffer(
new Float32Array([
// Front
-1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, -1, -1, 1, 0, 0, 1, 0, 0,
1, -1, 1, 0, 1, 1, 0, 0,
// Back
1, 1, -1, 0, 1, 0, 0, 0, -1, 1, -1, 0, 0, 0, 0, 0, 1, -1, -1, 0, 1, 1, 0,
0, -1, -1, -1, 0, 0, 1, 0, 0,
// Left
-1, 1, -1, 0, 1, 1, 0, 0, -1, 1, 1, 0, 1, 0, 0, 0, -1, -1, -1, 0, 0, 1, 0,
0, -1, -1, 1, 0, 0, 0, 0, 0,
// Right
1, 1, 1, 0, 1, 0, 0, 0, 1, 1, -1, 0, 1, 1, 0, 0, 1, -1, 1, 0, 0, 0, 0, 0,
1, -1, -1, 0, 0, 1, 0, 0,
// Top
-1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, -1, 1, -1, 0, 0, 1, 0, 0,
1, 1, -1, 0, 1, 1, 0, 0,
// Bottom
1, -1, 1, 0, 1, 0, 0, 0, -1, -1, 1, 0, 0, 0, 0, 0, 1, -1, -1, 0, 1, 1, 0,
0, -1, -1, -1, 0, 0, 1, 0, 0,
]),
8
);
// Use vertexBuffer, starting at offset 0, 3 items in position attribute
const positions = new THREE.InterleavedBufferAttribute(vertexBuffer, 3, 0);
geometry.setAttribute("position", positions);
// Use vertexBuffer, starting at offset 4, 2 items in uv attribute
const uvs = new THREE.InterleavedBufferAttribute(vertexBuffer, 2, 4);
geometry.setAttribute("uv", uvs);
const indices = new Uint16Array([
0, 2, 1, 2, 3, 1, 4, 6, 5, 6, 7, 5, 8, 10, 9, 10, 11, 9, 12, 14, 13, 14, 15,
13, 16, 17, 18, 18, 17, 19, 20, 21, 22, 22, 21, 23,
]);
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
// material
const material = new THREE.MeshBasicMaterial();
material.map = new THREE.TextureLoader().load("static/img/octo2.png");
material.map.colorSpace = THREE.SRGBColorSpace;
material.map.flipY = false;
material.transparent = true;
material.opacity = 0.3;
// per instance data
const matrix = new THREE.Matrix4();
const offset = new THREE.Vector3();
const orientation = new THREE.Quaternion();
const scale = new THREE.Vector3(1, 1, 1);
let x, y, z, w;
mesh = new THREE.InstancedMesh(geometry, material, instances);
for (let i = 0; i < instances; i++) {
// offsets
x = Math.random() * 100 - 50;
y = Math.random() * 100 - 50;
z = Math.random() * 100 - 50;
offset.set(x, y, z).normalize();
offset.multiplyScalar(5); // move out at least 5 units from center in current direction
offset.set(x + offset.x, y + offset.y, z + offset.z);
// orientations
x = Math.random() * 2 - 1;
y = Math.random() * 2 - 1;
z = Math.random() * 2 - 1;
w = Math.random() * 2 - 1;
orientation.set(x, y, z, w).normalize();
matrix.compose(offset, orientation, scale);
mesh.setMatrixAt(i, matrix);
}
scene.add(mesh);
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setAnimationLoop(animate);
container.appendChild(renderer.domElement);
stats = new Stats();
container.appendChild(stats.dom);
window.addEventListener("resize", onWindowResize);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
//
function animate() {
const time = performance.now();
mesh.rotation.y = time * 0.00005;
const delta = (time - lastTime) / 5000;
tmpQ.set(moveQ.x * delta, moveQ.y * delta, moveQ.z * delta, 1).normalize();
tmpM.makeRotationFromQuaternion(tmpQ);
for (let i = 0, il = instances; i < il; i++) {
mesh.getMatrixAt(i, currentM);
currentM.multiply(tmpM);
mesh.setMatrixAt(i, currentM);
}
mesh.instanceMatrix.needsUpdate = true;
mesh.computeBoundingSphere();
lastTime = time;
renderer.render(scene, camera);
stats.update();
}
+162
View File
@@ -0,0 +1,162 @@
var Stats = function () {
var mode = 0;
var container = document.createElement("div");
container.style.cssText =
"position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000";
//
function addPanel(panel) {
container.appendChild(panel.dom);
return panel;
}
function showPanel(id) {
for (var i = 0; i < container.children.length; i++) {
container.children[i].style.display = i === id ? "block" : "none";
}
mode = id;
}
var beginTime = (performance || Date).now(),
prevTime = beginTime,
frames = 0;
var fpsPanel = addPanel(new Stats.Panel("FPS", "#0ff", "#f00"));
var msPanel = addPanel(new Stats.Panel("MS", "#0f0", "#f00"));
if (self.performance && self.performance.memory) {
var memPanel = addPanel(new Stats.Panel("MB", "#f08", "#f00"));
}
showPanel(-1);
return {
REVISION: 16,
dom: container,
addPanel: addPanel,
showPanel: showPanel,
begin: function () {
beginTime = (performance || Date).now();
},
end: function () {
frames++;
var time = (performance || Date).now();
msPanel.update(time - beginTime, 200);
if (time >= prevTime + 1000) {
fpsPanel.update((frames * 1000) / (time - prevTime), 100);
prevTime = time;
frames = 0;
if (memPanel) {
var memory = performance.memory;
memPanel.update(
memory.usedJSHeapSize / 1048576,
memory.jsHeapSizeLimit / 1048576
);
}
}
return time;
},
update: function () {
beginTime = this.end();
},
// Backwards Compatibility
domElement: container,
setMode: showPanel,
};
};
Stats.Panel = function (name, fg, bg) {
var min = Infinity,
max = 0,
round = Math.round;
var PR = round(window.devicePixelRatio || 1);
var WIDTH = 80 * PR,
HEIGHT = 48 * PR,
TEXT_X = 3 * PR,
TEXT_Y = 2 * PR,
GRAPH_X = 3 * PR,
GRAPH_Y = 15 * PR,
GRAPH_WIDTH = 74 * PR,
GRAPH_HEIGHT = 30 * PR;
var canvas = document.createElement("canvas");
canvas.width = WIDTH;
canvas.height = HEIGHT;
canvas.style.cssText = "width:80px;height:48px";
var context = canvas.getContext("2d");
context.font = "bold " + 9 * PR + "px Helvetica,Arial,sans-serif";
context.textBaseline = "top";
context.fillStyle = bg;
context.fillRect(0, 0, WIDTH, HEIGHT);
context.fillStyle = fg;
context.fillText(name, TEXT_X, TEXT_Y);
context.fillRect(GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT);
context.fillStyle = bg;
context.globalAlpha = 0.9;
context.fillRect(GRAPH_X, GRAPH_Y, GRAPH_WIDTH, GRAPH_HEIGHT);
return {
dom: canvas,
update: function (value, maxValue) {
min = Math.min(min, value);
max = Math.max(max, value);
context.fillStyle = bg;
context.globalAlpha = 1;
context.fillRect(0, 0, WIDTH, GRAPH_Y);
context.fillStyle = fg;
context.fillText(
round(value) + " " + name + " (" + round(min) + "-" + round(max) + ")",
TEXT_X,
TEXT_Y
);
context.drawImage(
canvas,
GRAPH_X + PR,
GRAPH_Y,
GRAPH_WIDTH - PR,
GRAPH_HEIGHT,
GRAPH_X,
GRAPH_Y,
GRAPH_WIDTH - PR,
GRAPH_HEIGHT
);
context.fillRect(GRAPH_X + GRAPH_WIDTH - PR, GRAPH_Y, PR, GRAPH_HEIGHT);
context.fillStyle = bg;
context.globalAlpha = 0.9;
context.fillRect(
GRAPH_X + GRAPH_WIDTH - PR,
GRAPH_Y,
PR,
round((1 - value / maxValue) * GRAPH_HEIGHT)
);
},
};
};
export default Stats;
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+41
View File
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8" />
<title>Aperi'Solve</title>
<link rel="icon" type="image/svg+xml" href="/static/img/logo.svg" />
<link href="https://fonts.googleapis.com/css2?family=Fira+Code&display=swap" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="/static/css/aperisolve.css" rel="stylesheet" />
</head>
<body class="text-light">
<div id="three-bg"></div>
<div class="container mt-5">
<a class="title_link" href="/">
<h1 class="mb-4"><img id="water-logo" src="/static/img/logo.svg" /> Aperi'Solve</h1>
</a>
{% block body_content %} {% endblock %}
</div>
<!-- Modal Structure -->
<div id="image-modal-container">
<img id="modal-image" src="" />
</div>
<script src="/static/js/aperisolve.js"></script>
<!-- Background -->
<script type="importmap">
{
"imports": {
"three": "./static/js/three.module.min.js",
"three/addons/": "./static/js/jsm/"
}
}
</script>
<script type="module" src="/static/js/background.js"></script>
{% block footer_content %} {% endblock %}
{% include "footer.html" %}
</body>
</html>
+409
View File
@@ -0,0 +1,409 @@
{% extends 'base.html' %}
{% block body_content %}
<div id="cheatsheet">
<h1>Cheatsheet</h1>
<h2>Disclaimer</h2>
<div>
<div>
This cheatsheet is intended to guide CTF players in their research. This cheatsheet is not representative of
modern steganography/seganalysis techniques, and its content does not match with the creation of an interesting
challenges 😉.
</div>
</div>
<h2>Check file type / metadata</h2>
<div>
<div>
<ul class="fa-ul">
<li>Check the file extension on the internet if you don't know it. Also check the file format.</li>
<li>Use the <span class='highlight'>file</span> command to check that the extension matches the file type. This
command is based on the <a href='https://en.wikipedia.org/wiki/List_of_file_signatures' target='_blank'
class='highlight'>Magic Bytes</a> at the beginning of the file, so it may return false positives.</li>
</ul>
<div class="command"><span class="highlight">$</span> file data.raw
<div class="clipboard" data-value='file data.raw'></div>
</div>
<ul class="fa-ul">
<li>In case of unknown extension and file, inspect the hexadecimal structure of the file with an editor like <a
href='https://hexed.it/' target='_blank' class='highlight'>hexed.it</a> to identify the file structure.</li>
<li>Display the file's metadata with the <span class='highlight'>exiftool</span> command.</li>
</ul>
<div class="command">
<span class="highlight">$</span> exiftool data.raw
<div class="clipboard" data-value='file data.raw'></div>
</div>
</div>
</div>
<h2 id="filestruct">Modify the file structure</h2>
<div>
<div>
<ul class="fa-ul">
<li>Modify the file structure with a hexadecimal editor like <a href='https://hexed.it/' target='_blank'
class='highlight'>hexed.it</a>. PNG chunks can be easily edited with <a
href='http://entropymine.com/jason/tweakpng/' target='_blank' class='highlight'>TweakPNG</a> (Windows or
Wine).</li>
</ul>
<div style="text-align: center;">
<img src="/static/img/cheatsheet/tweakpng.png" alt="TweakPNG" />
</div>
<ul class="fa-ul">
<li>Verify the checksums of the file. These are usually CRC32, some tools like <a
href='http://www.libpng.org/pub/png/apps/pngcheck.html' target='_blank' class='highlight'>PngCheck</a> or <a
href='https://github.com/sherlly/PCRT' target='_blank' class='highlight'>PCRT</a> are usefull to verify and
correct these checksums.</li>
</ul>
<div class="command">
<span class="highlight">$</span> pngcheck -c file.png<br />
<span class="highlight">$</span> PCRT.py -v -i file.png
</div>
<ul class="fa-ul">
<li>For many types of files (PNG, WAV, ...) the media size is contained in the headers. This size can then be
reduced to display only a part of the media (beginning of an image, beginning of a sound, ...).</li>
</ul>
</div>
</div>
<h2 id="rawdata">Raw data</h2>
<div>
<div>
<ul class="fa-ul">
<li>It is possible to import a raw file into <a href='https://www.audacityteam.org/download/' target='_blank'
class='highlight'>Audacity</a> to listen to it as a soundtrack (File > Import > Raw Data).</li>
</ul>
<div style="text-align: center;">
<img src="/static/img/cheatsheet/audacity_raw.png" alt="Audacity" />
</div>
<ul class="fa-ul">
<li>Similarly, the <a href='https://www.gimp.org/downloads/' target='_blank' class='highlight'>GIMP</a> tool
allows image import from raw data (File > Open).</li>
<li>The <a href='http://binvis.io/' target='_blank' class='highlight'>BinVis.io</a> tool allows you to
graphically view binary files and can give hints as to the file type.</li>
<li>The <span class='highlight'>strings</span> command allows you to display the strings present in a file.</li>
</ul>
<div class="command">
<span class="highlight">$</span> strings -s file.raw<br />
<span class="highlight">$</span> strings -S file.raw<br />
<span class="highlight">$</span> strings -b file.raw<br />
<span class="highlight">$</span> strings -l file.raw<br />
<span class="highlight">$</span> strings -B file.raw<br />
<span class="highlight">$</span> strings -L file.raw
</div>
</div>
</div>
<h2 id="image">Image</h2>
<div>
<div>
<table class="cstable center">
<tr>
<th class="table_tl_none"></th>
<th>PNG</th>
<th>JPG/JPEG</th>
<th>BMP</th>
</tr>
<tr>
<td>Aperi'Solve</td>
<td class="hgreen">OK</td>
<td class="hgreen">OK</td>
<td class="hgreen">OK</td>
</tr>
<tr>
<td>Zsteg</td>
<td class="hgreen">OK</td>
<td class="hred">KO</td>
<td class="hgreen">OK</td>
</tr>
<tr>
<td>Steghide</td>
<td class="hred">KO</td>
<td class="hgreen">OK</td>
<td class="hgreen">OK</td>
</tr>
<tr>
<td>OutGuess</td>
<td class="hred">KO</td>
<td class="hgreen">OK</td>
<td class="hred">KO</td>
</tr>
</table>
</div>
<div>
<ul class="fa-ul">
<li>The robustness of steganography relies on the algorithm used and the knowledge of the cover medium. A search
of the original media allows us to make a comparison and identify the alterations made. A reverse search on <a
href='https://images.google.com/' target='_blank' class='highlight'>Google Image</a> or <a
href='https://yandex.com/images/' target='_blank' class='highlight'>Yandex Images</a> can find the original
media (be sure to check that the file size and type match).</li>
<li>The <a href='http://www.caesum.com/handbook/Stegsolve.jar' target='_blank' class='highlight'>Stegsolve</a>
tool allows you to perform operations on 2 images, and thus identify the differences between a cover medium
and a stegano medium using the XOR operation.</li>
</ul>
<div style="text-align: center;">
<img src="/static/img/cheatsheet/stegsolve_xor.png" alt="Stegsolve Xor" />
<img src="/static/img/cheatsheet/stegsolve_xor2.png" alt="Stegsolve Xor 2" />
</div>
<ul class="fa-ul">
<li>The <a href='https://github.com/zed-0xff/zsteg' target='_blank' class='highlight'>Zsteg</a> tool allows you
to extract messages and binaries encoded on different layers, such as the 2 green LSB.</li>
</ul>
<div class="command">
<span class="highlight">$</span> zsteg file.png 2b,g,lsb,xy<br />
<span class="highlight">$</span> zsteg file.png -E '1b,rgb,lsb'
<div class="clipboard" data-value='zsteg file.png'></div>
</div>
<ul class="fa-ul">
<li>Sometimes, bit-layer analysis on the <a href='http://www.caesum.com/handbook/Stegsolve.jar' target='_blank'
class='highlight'>Stegsolve</a> tool (or Aperi'Solve) can highlight specific areas that have been altered.
Scripting may then be required to extract specific areas. The following python code retrieves an image as a
list of pixels: [(100,120,43), (230, 124, 110), ...]</li>
</ul>
<div class="command">
# pip install Pillow<br />
from PIL import Image<br />
stegano_image = Image.open('file.png')<br />
width, height = stegano_image.size<br />
pxs = list(stegano_image.getdata())<br />
print(pxs[:10])
<div class="clipboard" data-value='# pip install Pillow
from PIL import Image
stegano_image = Image.open("file.png")
width, height = stegano_image.size
pxs = list(stegano_image.getdata())
print(pxs[:10])'></div>
</div>
<ul class="fa-ul">
<li>Stegano mediums can rely on an algorithm using an encryption key. This is the case for <a
href='http://steghide.sourceforge.net/' target='_blank' class='highlight'>Steghide</a> and <a
href='https://github.com/resurrecting-open-source-projects/outguess' target='_blank'
class='highlight'>OutGuess</a>. The password can be the name of the file, a string of characters contained
in the file (<span class='highlight'>strings</span> & <span class='highlight'>exiftool</span>), or the object
represented by the image. In some cases, the password cannot be retrieved and a bruteforce must be performed.
The tool <a href='https://github.com/Paradoxis/StegCracker' target='_blank' class='highlight'>StegCracker</a>
and <a href='https://github.com/ R4yGM/stegbrute' target='_blank' class='highlight'>Stegbrute</a> allows
bruteforce of hidden secrets with <a href='http://steghide.sourceforge.net/' target='_blank'
class='highlight'>Steghide</a>.</li>
</ul>
<div class="command">
<span class="highlight">$</span> steghide extract -p "secret" -sf file.jpg<br />
<span class="highlight">$</span> stegcracker file.jpg /usr/share/wordlists/rockyou.txt
</div>
</div>
</div>
<h2 id="png">PNG (Portable Network Graphics)</h2>
<div>
<div>
<ul class="fa-ul">
<li>Tools like <a href='http://steghide.sourceforge.net/' target='_blank' class='highlight'>Steghide</a> and <a
href='https://github.com/resurrecting-open-source-projects/outguess' target='_blank'
class='highlight'>OutGuess</a> does not work for PNG files. On the other hand, this type of file allows a
backup without compression, and therefore to hide messages on the LSBs. The tool <a
href='https://github.com/zed-0xff/zsteg' target='_blank' class='highlight'>Zsteg</a> presented above and
available on Aperi'Solve allows the extracting secrets from PNG files.</li>
</ul>
<div class="command">
<span class="highlight">$</span> zsteg file.png 2b,g,lsb,xy<br />
<span class="highlight">$</span> zsteg file.png -E '1b,rgb,lsb'
<div class="clipboard" data-value='zsteg file.png'></div>
</div>
<ul class="fa-ul">
<li>PNG offers an <a href='https://en.wikipedia.org/wiki/APNG' target='_blank' class='highlight'>"APNG"</a>
extension allowing to have an animation of images like GIF files. It is possible that the visualization of the
PNG does not reflect all of its content (for example, a frame with a zero duration, or a very long first
frame). The tool <a href='https://ezgif.com/apng-maker' target='_blank' class='highlight'>APNG Maker</a>
allows you to view the different frames of an APNG file. A message can also be encoded on the durations of
each frame.</li>
</ul>
<div style="text-align: center;">
<img src="https://upload.wikimedia.org/wikipedia/commons/1/14/Animated_PNG_example_bouncing_beach_ball.png"
alt="APNG" />
</div>
<ul class="fa-ul">
<li>The tool <a href='http://entropymine.com/jason/tweakpng/' target='_blank' class='highlight'>TweakPNG</a>
presented earlier allows to perform several types of operations like recalculate checksums, reorder PNG
chunks, increase the visible size of the PNG or modify a header.</li>
</ul>
<div style="text-align: center;">
<img src="/static/img/cheatsheet/tweakpng.png" alt="TweakPNG" />
</div>
</div>
</div>
<h2 id="jpg">JPEG (Joint Photographic Experts Group)</h2>
<div>
<div>
<ul class="fa-ul">
<li>The tool <a href='https://github.com/zed-0xff/zsteg' target='_blank' class='highlight'>Zsteg</a> does not
work on JPEG files (jpg, jpeg, ...) because the latter necessarily include a compression algorithm reducing
the quality of the image and altering the LSBs. In contrast, the tools <a
href='http://steghide.sourceforge.net/' target='_blank' class='highlight'>Steghide</a> and <a
href='https://github.com/resurrecting-open-source-projects/outguess' target='_blank'
class='highlight'>OutGuess</a> shown above are compatible with this file format.</li>
</ul>
<div class="command">
<span class="highlight">$</span> outguess -k secret file.jpg output.raw<br />
<span class="highlight">$</span> steghide extract -p "secret" -sf file.jpg
</div>
</div>
</div>
<h2 id="gif">GIF (Graphics Interchange Format)</h2>
<div>
<div>
<ul class="fa-ul">
<li>GIFs are generally animated images whose format is generally poorly supported by steganalysis tools. In
addition, the image format generally offers a color palette not exceeding 256 colors, against 16 million for
PNG (excluding transparency). However, the tool <a href='https://ezgif.com/maker' target='_blank'
class='highlight'>GIF Maker</a> makes it easy to manipulate GIF files.</li>
<li>GIFs can embed images (frames) with zero duration. These will then not be viewable without an analysis of
each frame.</li>
</ul>
<div style="text-align: center;">
<img src="/static/img/cheatsheet/gif_maker.png" alt="GIF Maker" />
</div>
<ul class="fa-ul">
<li>A hidden message can be embedded over the duration of the frames (morse, ascii, binary, ...).</li>
<li><a href='https://ffmpeg.org/' target='_blank' class='highlight'>Ffmpeg</a> can be used to extract frames
from a GIF. Completeness of extracted frames cannot be guaranteed.</li>
</ul>
<div class="command">
<span class="highlight">$</span> ffmpeg -i file.gif -vsync 0 output/file%d.png
<div class="clipboard" data-value='ffmpeg -i file.gif -vsync 0 output/file%d.png'></div>
</div>
</div>
</div>
<h2 id="audio">Audio</h2>
<div>
<div>
<ul class="fa-ul">
<li>The most common technique for sound files is based on the audio spectrum. Indeed, it is possible to draw a
visible message on the audio spectrum with tools like <a href='https://www.abc.se/~re/Coagula/Coagula.html'
target='_blank' class='highlight'>Coagula</a>. Then this spectrum can be analyzed using spectral analysis
tools like <a href='https://www.sonicvisualiser.org/' target='_blank' class='highlight'>Sonic Visualiser</a>,
<a href='https://www.audacityteam.org/' target='_blank' class='highlight'>Audacity</a> or the online tool <a
href='https://www.dcode.fr/spectral-analysis' target='_blank' class='highlight'>dcode</a>.</li>
</ul>
<div style="text-align: center;">
<img src="/static/img/cheatsheet/audacity_1.png" alt="Audacity spectrum" />
</div>
<br />Right-clicking the scale to zoom out and view the entire spectrum, or switch to logarithm mode, may be
required.
<div style="text-align: center;"><br />
<img src="/static/img/cheatsheet/audacity_2.png" alt="Audacity spectrum" />
</div>
<ul class="fa-ul">
<li>Inaudible frequencies can be used to hide messages. A spectral analysis beyond 20KHz and below 20Hz is
recommended, a message encoded in morse or binary can be hidden there.</li>
<li>Sounds are simple telecommunications signals that can be used for other purposes. Special encoding methods
can be used, such as <a href='https://en.wikipedia.org/wiki/Dual-tone_multi-frequency_signaling'
target='_blank' class='highlight'>DTMF Code</a> formerly used in telephony or <a
href='https://en.wikipedia.org/wiki/Slow-scan_television' target='_blank' class='highlight'>SSTV</a> used in
television for image transfer. These signals are encoded on audible frequencies but their interpretations
require special decoders. We will then opt for a <a href='http://dialabc.com/sound/detect/' target='_blank'
class='highlight'>DTMF decoder</a> for the first technique, and an SSTV decoder like <a
href='https://doc.ubuntu-fr.org/qsstv' target='_blank' class='highlight'>QSSTV</a> for the second one.</li>
<li>Just like PNG files, some sound files embed their theoretical sizes in their headers. This is particularly
the case for <a href='https://fr.wikipedia.org/wiki/Waveform_Audio_File_Format#En-t%C3%AAte_de_fichier_WAV'
target='_blank' class='highlight'>WAV files</a> whose DataSize block can be decreased or increased using a
hex editor.</li>
<li>The LSB technique can be used on certain audio files. It is then necessary to extract the LSBs from the
audio data; the <a href='https://github.com/ragibson/Steganography#WavSteg' target='_blank'
class='highlight'>WavSteg</a> tool allows you to perform this manipulation on WAV files.</li>
</ul>
<div class="command">
<span class="highlight">$</span> stegolsb wavsteg -r -i file.wav -o output.txt -n 1 -b 1000
<div class="clipboard" data-value='stegolsb wavsteg -r -i file.wav -o output.txt -n 1 -b 1000'></div>
</div>
<ul class="fa-ul">
<li>The tools <a href='http://steghide.sourceforge.net/' target='_blank' class='highlight'>Steghide</a> and <a
href='http://jpinsoft.net/deepsound/overview.aspx' target='_blank' class='highlight'>DeepSound</a> are
commonly used in CTF to hide messages. These can take a key as a parameter, so you will have to guess this key
from the available information (media, file name, challenge name, etc.) or by using a wordlist.</li>
</ul>
<div style="text-align: center;"><br />
<img src="/static/img/cheatsheet/DeepSound.png" alt="DeepSound" />
</div>
</div>
</div>
<h2 id="polyglot">Polyglot Files</h2>
<div>
<div>
This is a file type that is valid for different file formats. For example an image file which can therefore be
viewed and which is also a jar file (which can be executed). There are several types of polyglot file types:
<ul class="fa-ul">
<li>"Simple" polyglot: This is a simple concatenation of files;</li>
<li>"Parasitic" polyglot: This is a file that contains another type of file.</li>
<li>"Mille-feuilles" polyglot: The layers are alternated by controlling the internal structure of the file.</li>
<li>"Chimera" polyglot: The file has a body (data) and several heads. Since several formats use the same
algorithm to store data, such as Zlib's Deflate, the same block of data is used by different headers. (for
example, the pixels of an image). Several headers are present so that this image is visible via several
formats (jpg, png, ..), within the same file.</li>
<li>"Schizophrenic" files: This is a single type of file, but its contents are interpreted differently depending
on the tool that is running or accessing this file. These are usually PDF files (interpretation or not of
javascript) or Images (like the technique of <a href='https://carlmastrangelo.com/blog/gamma-steganography'
target='_blank' class='highlight '>Gamma</a>).</li>
<li>Angecryption : The result of an encryption or decryption of a file gives another valid file of the same type
or of a different type:</li>
</ul>
<div style="text-align: center;"><br />
<img src="/static/img/cheatsheet/Angecryption.png" alt="Angecryption" />
</div>
<ul class="fa-ul">
<li>Docx, jar, apk, pptx, odf... files are valid archives and can be decompressed. You must then check whether
the file in question can be opened with a tool other than the one associated with its extension. The <span
class='highlight'>file</span> command does not identify polyglot files.</li>
</ul>
</div>
</div>
<h2 id="text">Encoding and exotic languages</h2>
<div>
<div>
<ul class="fa-ul">
<li>ASCII text can be encoded in binary in blocks of <span class='highlight'>8 bits</span>, it is then easily
identifiable because each block corresponding to a letter begins with a 0. It is possible to remove this 0 ,
the blocks are then only <span class='highlight'>7 bits</span>. The text can also be coded on an integer of
type <span class='highlight'>long</span> (sometimes in a base other than base 10), for example the character
string “Hello world! encoded on a long type in base 10 can be decoded with the following python 2 code:</li>
</ul>
<div class="command">
<span class="highlight">python2></span> hex(22405534230753963835153736737L)[2:].strip('L').decode("hex")<br />
<span class="highlight">python2></span> "48656c6c6f20776f726c6421".decode("hex")
<div class="clipboard" data-value='hex(long_var)[2:].strip(&quot;L(&quot;).decode("hex")'></div>
</div>
<ul class="fa-ul">
<li>Tools like <a href='https://gchq.github.io/CyberChef/' target='_blank' class='highlight'>CyberChef</a> (and
the "magic" option) can help detect the encoding used and decode the hidden message. Sometimes a charset
change may be necessary (monoalphabetic substitution, or base change).</li>
<li>Exotic or esoteric languages are programming languages designed to be unique, difficult to program, or just
plain weird. Their design and their charsets can be particular, it is the case of the most found languages in
CTF like <a href='https://esolangs.org/wiki/Brainfuck' target='_blank' class='highlight'>BrainFuck</a>, <a
href='https://esolangs.org/wiki/Whitespace' target='_blank' class='highlight'>WhiteSpace</a>, <a
href='https://esolangs.org/wiki/Piet' target='_blank' class='highlight'>PIET</a> or <a
href='https://esolangs.org/wiki/Malbolge' target='_blank' class='highlight'>Malbolge</a>. The website <a
href='https://esolangs.org/wiki/Language_list' target='_blank' class='highlight'>EsoLang</a> references
these different languages and can be useful in your research.</li>
</ul>
<div class="command">
<span
class="highlight">brainfuck></span><br />++++++++++[&#x3E;+&#x3E;+++&#x3E;+++++++&#x3E;++++++++++&#x3C;&#x3C;&#x3C;&#x3C;-]&#x3E;&#x3E;&#x3E;++.&#x3E;+.+++++++..+++.&#x3C;&#x3C;++.&#x3E;&#x3E;++++++++.--------.+++.------.--------.&#x3C;&#x3C;+.
<br /><br /><span
class="highlight">malbolge></span><br />(&#x27;&#x26;%:9]!~}|z2Vxwv-,POqponl$Hjig%eB@@&#x3E;}=&#x3C;M:9wv6WsU2T|nm-,jcL(I&#x26;%$#&#x22;
&#x60;CB]V?Tx&#x3C;uVtT&#x60;Rpo3NlF.Jh++FdbCBA@?]!~|4XzyTT43Qsqq(Lnmkj&#x22;Fhg${z@&#x3E;
</div>
<div style="text-align: center;">
<a href='https://esolangs.org/wiki/Piet' target="_blank"><img src="/static/img/cheatsheet/piet.png"
alt="PIET" /></a>
</div><br />
</div>
</div>
<h2 id="more">To go further</h2>
<div>
<div>
<ul class="fa-ul">
<li><a href='https://www.aperikube.fr/cat/steg/' target='_blank' class='highlight'> Aperi'Kube's Blog -
Stéganography category</a></li>
<li><a href='https://ctftime.org/writeups' target='_blank' class='highlight'>CTF Time - Writeups (Stegano)</a>
</li>
<li><a href='http://corkami.github.io/' target='_blank' class='highlight'>Corkami</a></li>
<li><a href='https://www.root-me.org/en/Challenges/Steganography/' target='_blank' class='highlight'>Root-Me -
Steganography</a></li>
<li><a href='https://en.wikipedia.org/wiki/Autostereogram' target='_blank' class='highlight'>Stereogram</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}
+18
View File
@@ -0,0 +1,18 @@
<footer class="d-flex justify-content-center gap-4 my-4">
<div>
<a href="/faq" title="FAQ" class="text-light text-decoration-none">
<i class="fa-regular fa-circle-question"></i>
</a>
</div>
<div>
<a href="https://github.com/Zeecka/AperiSolve" target="_blank" title="GitHub"
class="text-light text-decoration-none">
<i class="fa-brands fa-github"></i>
</a>
</div>
<div>
<a href="https://x.com/Zeecka_" target="_blank" title="Twitter" class="text-light text-decoration-none">
<i class="fa-brands fa-x-twitter"></i>
</a>
</div>
</footer>
+38
View File
@@ -0,0 +1,38 @@
{% extends 'base.html' %}
{% block body_content %}
<form id="upload-form" class="card p-4" method="POST" enctype="multipart/form-data">
<div class="mb-3">
<div>
<h2>What is this ?</h2>
<p>Aperi'Solve is an online platform which performs layer analysis on image. The platform also uses zsteg,
steghide, outguess, exiftool, binwalk, foremost and strings for deeper steganography analysis. The platform
supports the following images format: .png, .jpg, .gif, .bmp, .jpeg, .jfif, .jpe, .tiff...</p>
</div>
<div id="drop-zone" class="border border-secondary rounded p-4 text-center mb-3">
<p id="drag-msg">📂 Drag and drop an image here or click to select</p>
<input type="file" id="image" name="image" accept="image/*" hidden required />
<button type="button" class="btn btn-secondary mt-2" id="browse-btn">
Choose an image
</button>
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password (optional)</label>
<input class="form-control" type="text" name="password" id="password" />
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="deep" />
<label class="form-check-label" for="deep">Deep analysis</label>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
<div class="progress mt-3 d-none" id="progress-container">
<div class="progress-bar" id="progress-bar" role="progress-bar" style="width: 0%">
0%
</div>
</div>
</form>
<div id="result" class="mt-5">
<div id="result-infos" class="mb-3"></div>
<div id="result-analyzers" class="mb-3"></div>
</div>
{% endblock %}
+12
View File
@@ -0,0 +1,12 @@
{% extends 'base.html' %}
{% block body_content %}
<div id="result" class="mt-5">
<div id="result-infos" class="mb-3"></div>
<div id="result-analyzers" class="mb-3"></div>
</div>
{% endblock %}
{% block footer_content %}
<script>
pollStatus("{{ hash_val }}");
</script>
{% endblock %}
+10
View File
@@ -0,0 +1,10 @@
{% extends 'base.html' %}
{% block body_content %}
<div id="result" class="mt-5">
{% for img in images %}<!--
--><div class="browse-images">
<a href="{{ img.link }}" target="_blank"><img src="{{ img.file }}"/></a>
</div><!--
-->{% endfor %}
</div>
{% endblock %}
+76
View File
@@ -0,0 +1,76 @@
"""Asynchronous worker for analyzing image submissions."""
import threading
from pathlib import Path
from typing import Any
from .analyzers.binwalk import analyze_binwalk
from .analyzers.decomposer import analyze_decomposer
from .analyzers.exiftool import analyze_exiftool
from .analyzers.foremost import analyze_foremost
from .analyzers.outguess import analyze_outguess
from .analyzers.steghide import analyze_steghide
from .analyzers.strings import analyze_strings
from .analyzers.zsteg import analyze_zsteg
from .app import app, db
from .config import RESULT_FOLDER
from .models import Image, Submission
def analyze_image(submission_hash: str) -> None:
"""Analyze an image submission in separate threads."""
with app.app_context():
submission: Submission = Submission.query.get(submission_hash) # type: ignore
image = Image.query.get_or_404(submission.image_hash) # type: ignore
if not submission or not image: # No submission found
return
submission.status = "running" # type: ignore
db.session.commit()
try:
img_path: Path = Path(image.file)
result_path: Path = RESULT_FOLDER / str(image.hash) / str(submission.hash)
result_path.mkdir(parents=True, exist_ok=True)
threads: list[threading.Thread] = []
def run_analyzer(analyzer_func: Any, *args: Any) -> None:
"""Run an analyzer function in a separate thread."""
try:
analyzer_func(*args)
except Exception as e:
print(f"Error in {analyzer_func.__name__}: {e}")
analyzers = [
(analyze_binwalk, img_path, result_path),
(analyze_decomposer, img_path, result_path),
(analyze_exiftool, img_path, result_path),
(analyze_foremost, img_path, result_path),
(analyze_strings, img_path, result_path),
(analyze_steghide, img_path, result_path, submission.password),
(analyze_zsteg, img_path, result_path),
]
# Deep analysis only
if submission.deep_analysis:
analyzers.extend(
[
(analyze_outguess, img_path, result_path),
]
)
for analyzer in analyzers:
thread = threading.Thread(target=run_analyzer, args=analyzer)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
submission.status = "completed"
except Exception:
submission.status = "error"
finally:
db.session.commit()
+6
View File
@@ -0,0 +1,6 @@
"""Aperi'Solve WSGI runner."""
from .app import app as application
if __name__ == "__main__":
application.run()