Merge pull request #100 from Philip-Zimmermann/main
- Fix image resize analyzer - Fixed broken stegbrute URL - Fix minor typos
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.11-slim AS build
|
FROM python:3.14-slim AS build
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user