Merge pull request #100 from Philip-Zimmermann/main

- Fix image resize analyzer
- Fixed broken stegbrute URL
- Fix minor typos
This commit is contained in:
Zeecka
2025-12-09 22:18:03 +01:00
committed by GitHub
9 changed files with 212 additions and 11 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.11-slim AS build FROM python:3.14-slim AS build
WORKDIR / WORKDIR /
+2 -3
View File
@@ -73,13 +73,13 @@ Here is a batch of usefull commands.
```bash ```bash
docker compose down -v # Remove volume containings results docker compose down -v # Remove volume containings results
docker compose -f docker-compose-dev.yml up # Start application in developpement mode docker compose -f compose.dev.yml up # Start application in developpement mode
rm -rf aperisolve/results # Remove results from developpement mode rm -rf aperisolve/results # Remove results from developpement mode
docker exec -it postgres psql -U aperiuser -d aperisolve # Get postgresql shell docker exec -it postgres psql -U aperiuser -d aperisolve # Get postgresql shell
docker exec -it aperisolve-web bash # Get shell in running web app docker exec -it aperisolve-web bash # Get shell in running web app
``` ```
> Note, if you used [docker-compose-dev.yml](docker-compose-dev.yml), you must remove "results" when switching to [production docker](docker-compose.yml). > Note, if you used [compose.dev.yml](compose.dev.yml), you must remove "results" when switching to [production docker](compose.yml).
### Code quality ### Code quality
@@ -94,7 +94,6 @@ GitHub CI will alert in case of failure.
## Roadmap ## Roadmap
- [x] PngCheck
- [ ] Zsteg extract / --all --> **Download `zsteg` files** such as mp3 encoded on LSB - [ ] Zsteg extract / --all --> **Download `zsteg` files** such as mp3 encoded on LSB
- [ ] Test on mobile - [ ] Test on mobile
- [ ] i18n - [ ] i18n
+190
View File
@@ -0,0 +1,190 @@
"""Image size bruteforcer for common PNG sizes."""
import zlib
import shutil
from pathlib import Path
from .utils import update_data
# Full list of expected sizes
EXPECTED_SIZES = [
# -------------------------
# ICONS (Square)
# -------------------------
(16, 16), (20, 20), (24, 24), (28, 28),
(32, 32), (48, 48), (64, 64), (72, 72),
(96, 96), (128, 128), (192, 192), (256, 256),
(384, 384), (512, 512), (768, 768), (1024, 1024),
(1536, 1536), (2048, 2048), (4096, 4096),
# -------------------------
# 1:1 Square (General)
# -------------------------
(300, 300), (500, 500), (800, 800), (1000, 1000),
(1500, 1500), (2000, 2000), (2500, 2500), (3000, 3000),
# -------------------------
# 4:3 Aspect (Classic Screens)
# -------------------------
(320, 240), (640, 480), (800, 600), (960, 720),
(1024, 768), (1280, 960), (1600, 1200), (2048, 1536),
(2560, 1920), (3200, 2400), (4096, 3072),
# -------------------------
# 3:2 Aspect (Photography)
# -------------------------
(600, 400), (900, 600), (1200, 800), (1500, 1000),
(1800, 1200), (2400, 1600), (3000, 2000), (3600, 2400),
(4500, 3000), (6000, 4000),
# -------------------------
# 16:9 Aspect (Video / Web)
# -------------------------
(426, 240), (640, 360), (854, 480), (960, 540),
(1280, 720), (1366, 768), (1600, 900), (1920, 1080),
(2560, 1440), (3200, 1800), (3440, 1935), (3840, 2160),
(5120, 2880), (7680, 4320), (10240, 5760),
# -------------------------
# 16:10 Aspect (Laptops)
# -------------------------
(1280, 800), (1440, 900), (1680, 1050),
(1920, 1200), (2560, 1600), (2880, 1800),
(3840, 2400), (5120, 3200),
# -------------------------
# Mobile Portrait (9:16)
# -------------------------
(720, 1280), (1080, 1920), (1440, 2560),
(2160, 3840), (1440, 2960), (1080, 2400),
(1170, 2532), (1284, 2778), (1290, 2796),
# -------------------------
# Social Media Sizes
# -------------------------
# Instagram
(1080, 1080), (1080, 1350), (1080, 1920),
# Facebook
(1200, 628), (1200, 1200),
# Twitter/X
(1200, 675), (1500, 500),
# YouTube Thumbnails
(1280, 720), (1920, 1080),
# -------------------------
# Banners / Headers
# -------------------------
(1600, 500), (1920, 500), (2560, 700),
(3000, 1000), (3840, 1200),
# -------------------------
# Print-like Digital Sizes
# -------------------------
(2550, 3300), # Letter @ 300 DPI
(2480, 3508), # A4 @ 300 DPI
(3508, 4961), # A3 @ 300 DPI
(4961, 7016), # A2 @ 300 DPI
(7016, 9933), # A1 @ 300 DPI
]
def calc_checksum(header_chunk, width_bytes, height_bytes):
"""
Calculates the CRC32 of the IHDR chunk with new dimensions.
header_chunk: The full IHDR chunk (Type + Data).
"""
# Reconstruct IHDR: [Type(4)] + [New Width(4)] + [New Height(4)] + [Rest of Data(5)]
new_header = header_chunk[:4] + width_bytes + height_bytes + header_chunk[12:]
# Calculate CRC and return as 4 bytes (Big Endian)
return bytearray((zlib.crc32(new_header) & 0xffffffff).to_bytes(4, byteorder='big'))
def analyze_image_resize(input_img: Path, output_dir: Path) -> None:
"""
Analyze an image submission using python-based resize bruteforce.
Attempts to recover PNGs with modified resolutions but valid CRCs.
"""
# 1. Setup Paths
input_img = Path(input_img)
output_dir = Path(output_dir)
# 3. Initialize Logs
logs = []
recovered_image = None
try:
with input_img.open("rb") as image:
f = image.read()
b = bytearray(f)
# Find IHDR start
IHDR = b.find(b'\x49\x48\x44\x52')
if IHDR == -1:
logs.append("Failure: PNG header (IHDR) not found.")
update_data(output_dir, {"image_resize": {"status": "error", "error": "PNG header not found."}})
return
# Calculate Chunk Length (The 4 bytes BEFORE IHDR tag)
chunk_length = int.from_bytes((b[IHDR- 4: IHDR]), byteorder="big") + 4
# Extract Target CRC (The 4 bytes AFTER the chunk data)
target_crc = b[IHDR + chunk_length:IHDR + chunk_length + 4]
# Isolate the header chunk (Type + Data)
header_chunk = b[IHDR:IHDR + chunk_length]
logs.append(f"Target CRC found: 0x{target_crc.hex()}")
match_found = False
# Brute-force Loop
for size in EXPECTED_SIZES:
width, height = size
# Convert dimensions to 4-byte Big Endian arrays
width_bytes = bytearray(width.to_bytes(4, byteorder='big'))
height_bytes = bytearray(height.to_bytes(4, byteorder='big'))
# Check if this size matches the file's CRC
if target_crc == calc_checksum(header_chunk, width_bytes, height_bytes):
match_found = True
logs.append(f"Success: Match found! Dimensions: {width}x{height}")
# Create Output Filename
img_name = f"recovered_{width}x{height}.png"
# Create parent directories if they don't exist
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / img_name
# Splicing: [Start...IHDR+4] + [W] + [H] + [IHDR+12...End]
full_png_data = b[:IHDR+4] + width_bytes + height_bytes + b[IHDR+12:]
# Write the recovered file to disk
with output_path.open("wb") as out_f:
out_f.write(full_png_data)
recovered_image = Path(output_dir.name) / img_name
break # Stop looking after finding the match
if not match_found:
logs.append("Failure: No matching dimensions found.")
# 4. Final Data Update
output_data = {
"image_resize": { # Key name must match your module name in AperiSolve
"status": "ok",
"output": logs
}
}
if recovered_image:
output_data["image_resize"]["image"] = "/image/" + str(recovered_image)
update_data(output_dir, output_data)
except Exception as e:
update_data(output_dir, {"image_resize": {"status": "error", "error": str(e)}})
return None
+1 -1
View File
@@ -3,6 +3,6 @@
from pathlib import Path from pathlib import Path
IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff"] IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff"]
WORKER_FILES = ["binwalk", "foremost", "steghide", "zsteg"] WORKER_FILES = ["binwalk", "foremost", "steghide", "zsteg", "image_resize"]
RESULT_FOLDER = Path(__file__).parent.resolve() / "results" RESULT_FOLDER = Path(__file__).parent.resolve() / "results"
+11 -1
View File
@@ -8,6 +8,7 @@ const TOOL_ORDER = [
"steghide", "steghide",
"zsteg", "zsteg",
"strings", "strings",
"image_resize",
]; ];
function escapeHtml(text) { function escapeHtml(text) {
@@ -248,7 +249,7 @@ function parseResult(result) {
analyzer.className = `analyzer a-${escapeHtml(tool)}`; analyzer.className = `analyzer a-${escapeHtml(tool)}`;
resultDiv.appendChild(analyzer); resultDiv.appendChild(analyzer);
analyzer.innerHTML += `<h2>${capitalize(tool)}</h2>`; analyzer.innerHTML += `<h2>${capitalize(tool.replace("_"," "))}</h2>`;
if (result[tool]["status"] === "ok") { if (result[tool]["status"] === "ok") {
if ("images" in result[tool]) { if ("images" in result[tool]) {
@@ -270,8 +271,17 @@ function parseResult(result) {
} }
} }
} }
if ("image" in result[tool]) {
// Parse image output
analyzer.innerHTML += `<div class='results_img'><img src='${escapeHtml(
result[tool]["image"]
)}'/></div>`;
}
// Parse text output // Parse text output
if (typeof result[tool]["output"] === "string") { if (typeof result[tool]["output"] === "string") {
analyzer.innerHTML += `<pre>${escapeHtml( analyzer.innerHTML += `<pre>${escapeHtml(
result[tool]["output"] result[tool]["output"]
+3 -3
View File
@@ -6,8 +6,8 @@
<div> <div>
<div> <div>
This cheatsheet is intended to guide CTF players in their research. This cheatsheet is not representative of 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 modern steganography/steganalysis techniques, and its content does not match with the creation of an interesting
challenges 😉. challenge 😉.
</div> </div>
</div> </div>
<h2>Check file type / metadata</h2> <h2>Check file type / metadata</h2>
@@ -48,7 +48,7 @@
<ul class="fa-ul"> <ul class="fa-ul">
<li>Verify the checksums of the file. These are usually CRC32, some tools like <a <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='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 href='https://github.com/sherlly/PCRT' target='_blank' class='highlight'>PCRT</a> are useful to verify and
correct these checksums.</li> correct these checksums.</li>
</ul> </ul>
<div class="command"> <div class="command">
+2
View File
@@ -13,6 +13,7 @@ from .analyzers.steghide import analyze_steghide
from .analyzers.strings import analyze_strings from .analyzers.strings import analyze_strings
from .analyzers.zsteg import analyze_zsteg from .analyzers.zsteg import analyze_zsteg
from .analyzers.pngcheck import analyze_pngcheck from .analyzers.pngcheck import analyze_pngcheck
from .analyzers.image_resize import analyze_image_resize
from .app import app, db from .app import app, db
from .config import RESULT_FOLDER from .config import RESULT_FOLDER
from .models import Image, Submission from .models import Image, Submission
@@ -53,6 +54,7 @@ def analyze_image(submission_hash: str) -> None:
(analyze_pngcheck, img_path, result_path), (analyze_pngcheck, img_path, result_path),
(analyze_steghide, img_path, result_path, submission.password), (analyze_steghide, img_path, result_path, submission.password),
(analyze_zsteg, img_path, result_path), (analyze_zsteg, img_path, result_path),
(analyze_image_resize, img_path, result_path),
] ]
# Deep analysis only # Deep analysis only
View File