Update analysers (WIP)

This commit is contained in:
Zeecka
2026-01-01 23:31:42 +01:00
parent e400f65786
commit 78f8d867ea
9 changed files with 189 additions and 526 deletions
+18 -66
View File
@@ -3,75 +3,27 @@
# mypy: disable-error-code=unused-awaitable # mypy: disable-error-code=unused-awaitable
"""Binwalk Analyzer for Image Submissions.""" """Binwalk Analyzer for Image Submissions."""
import shutil
import subprocess
from pathlib import Path from pathlib import Path
from typing import Any
from .utils import MAX_PENDING_TIME, update_data from .base_analyzer import SubprocessAnalyzer
class BinwalkAnalyzer(SubprocessAnalyzer):
"""Analyzer for binwalk."""
def __init__(self, *args):
super().__init__("binwalk", *args, has_archive=True)
self.cmd = ["binwalk", "--matryoshka", "-e", self.img, "--run-as=root"]
self.make_folder = False # Binwalk already create its own folder
def get_extracted_dir(self) -> Path:
return self.output_dir / f"_{self.input_img.name}.extracted"
def is_error(self, returncode: int, stdout: str, stderr: str, zip_exist: bool) -> bool:
return len(stderr) > 0 and not zip_exist and False
def analyze_binwalk(input_img: Path, output_dir: Path) -> None: def analyze_binwalk(input_img: Path, output_dir: Path) -> None:
"""Analyze an image submission using binwalk.""" """Analyze an image submission using binwalk."""
analyzer = BinwalkAnalyzer(input_img, output_dir)
image_name = input_img.name analyzer.analyze()
extracted_dir = output_dir / f"_{image_name}.extracted"
try:
stderr = ""
# Run binwalk
data = subprocess.run(
["binwalk", "--matryoshka", "-e", "../" + str(image_name), "--run-as=root"],
cwd=output_dir,
capture_output=True,
text=True,
check=False,
timeout=MAX_PENDING_TIME,
)
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,
timeout=MAX_PENDING_TIME,
)
zip_exist = True
stderr += zip_data.stderr
# Remove the extracted directory
if extracted_dir.exists():
shutil.rmtree(extracted_dir)
# Only report error if stderr exists AND extraction failed.
# If zip_exist is True, binwalk worked (despite warnings), so we proceed.
if len(stderr) > 0 and not zip_exist:
err: dict[str, dict[str, Any]] = {
"binwalk": {
"status": "error",
"output": data.stdout.split("\n") if data else [],
"error": stderr,
}
}
update_data(output_dir, err)
return None
output_data: dict[str, dict[str, Any]] = {
"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
+20 -37
View File
@@ -3,46 +3,29 @@
# mypy: disable-error-code=unused-awaitable # mypy: disable-error-code=unused-awaitable
"""Exiftool Analyzer for Image Submissions.""" """Exiftool Analyzer for Image Submissions."""
import subprocess
from pathlib import Path from pathlib import Path
from .utils import MAX_PENDING_TIME, update_data from .base_analyzer import SubprocessAnalyzer
class ExiftoolAnalyzer(SubprocessAnalyzer):
"""Analyzer for exiftool."""
def __init__(self, *args):
super().__init__("exiftool", *args)
self.cmd = ["exiftool", "-a", "-u", "-g1", self.img]
def process_output(self, stdout: str, stderr: str) -> dict[str, str]:
"""Process the stdout into a list of lines."""
metadata: dict[str, str] = {}
for line in stdout.split("\n"):
if ":" in line:
key, value = line.split(":", 1)
metadata[key.strip()] = value.strip()
return metadata
def analyze_exiftool(input_img: Path, output_dir: Path) -> None: def analyze_exiftool(input_img: Path, output_dir: Path) -> None:
"""Analyze an image submission using exiftool.""" """Analyze an image submission using exiftool."""
analyzer = ExiftoolAnalyzer(input_img, output_dir)
try: analyzer.analyze()
data = subprocess.run(
["exiftool", input_img],
capture_output=True,
text=True,
check=True,
timeout=MAX_PENDING_TIME,
)
metadata: dict[str, str] = {}
for line in data.stdout.split("\n"):
if ":" in line:
key, value = line.split(":", 1)
metadata[key.strip()] = value.strip()
if data.stderr:
update_data(
output_dir,
{
"exiftool": {
"status": "error",
"output": metadata,
"error": data.stderr,
}
},
)
return
update_data(
output_dir,
{"exiftool": {"status": "ok", "output": metadata}},
)
except Exception as e:
update_data(output_dir, {"exiftool": {"status": "error", "error": str(e)}})
+21 -51
View File
@@ -3,60 +3,30 @@
# mypy: disable-error-code=unused-awaitable # mypy: disable-error-code=unused-awaitable
"""Foremost Analyzer for Image Submissions.""" """Foremost Analyzer for Image Submissions."""
import shutil
import subprocess
from pathlib import Path from pathlib import Path
from .utils import MAX_PENDING_TIME, update_data from .base_analyzer import SubprocessAnalyzer
class ForemostAnalyzer(SubprocessAnalyzer):
"""Analyzer for foremost."""
def __init__(self, *args):
super().__init__("foremost", *args, has_archive=True)
self.cmd = ["foremost", "-o", str(self.get_extracted_dir()), "-i", self.img]
def is_error(self, returncode: int, stdout: str, stderr: str, zip_exist: bool) -> bool:
"""Check if the result is an error."""
return len(stderr) > 60
def process_output(self, stdout: str, stderr: str) -> str | list[str] | dict[str, str]:
"""Process the stdout/stderr."""
if "Processing" in stderr and "|*|" in stderr:
return stderr.strip().split("\n")
return []
def analyze_foremost(input_img: Path, output_dir: Path) -> None: def analyze_foremost(input_img: Path, output_dir: Path) -> None:
"""Analyze an image submission using foremost.""" """Analyze an image submission using foremost."""
analyzer = ForemostAnalyzer(input_img, output_dir)
try: analyzer.analyze()
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,
timeout=MAX_PENDING_TIME,
)
# Note: foremost use stderr as standard output for results :o)
# So we can't use it as error detection :
# stderr += data.stderr
# Zip extracted files
zip_data = subprocess.run(
["7z", "a", "../foremost.7z", "*"],
cwd=foremost_dir,
capture_output=True,
text=True,
check=False,
timeout=MAX_PENDING_TIME,
)
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)}})
+19 -84
View File
@@ -3,97 +3,32 @@
# mypy: disable-error-code=unused-awaitable # mypy: disable-error-code=unused-awaitable
"""Outguess Analyzer for Image Submissions.""" """Outguess Analyzer for Image Submissions."""
import shutil
import subprocess
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from .utils import MAX_PENDING_TIME, update_data from .base_analyzer import SubprocessAnalyzer
def analyze_outguess( class OutguessAnalyzer(SubprocessAnalyzer):
input_img: Path, output_dir: Path, password: Optional[str] = None """Analyzer for outguess."""
) -> None:
"""Analyze an image submission using outguess."""
image_name = input_img.name def __init__(self, *args):
try: super().__init__("outguess", *args, has_archive=True)
outguess_dir = output_dir / "outguess"
outguess_dir.mkdir(parents=True, exist_ok=True) def build_cmd(self, password: Optional[str] = None) -> list[str]:
outguess_file = "outguess.data" extracted_dir = self.get_extracted_dir()
stderr = "" assert extracted_dir is not None # since has_archive is True
cmd: list[str] out = str(extracted_dir / "outguess.data")
if password: if password:
cmd = [ return ["outguess", "-k", password, "-r", self.img, out]
"outguess",
"-k",
password,
"-r",
"../../" + str(image_name),
outguess_file,
]
else: else:
cmd = ["outguess", "-r", "../../" + str(image_name), outguess_file] return ["outguess", "-r", self.img, out]
data = subprocess.run(
cmd,
cwd=outguess_dir,
capture_output=True,
text=True,
check=False,
timeout=MAX_PENDING_TIME,
)
if data.returncode != 0: def analyze_outguess(input_img: Path, output_dir: Path, password: Optional[str] = None) -> None:
stderr += data.stderr """Analyze an image submission using outguess."""
err = { analyzer = OutguessAnalyzer(input_img, output_dir)
"outguess": { if password:
"status": "error", analyzer.analyze(password)
"error": stderr, else:
} analyzer.analyze()
}
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,
timeout=MAX_PENDING_TIME,
)
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
+14 -45
View File
@@ -3,54 +3,23 @@
# mypy: disable-error-code=unused-awaitable # mypy: disable-error-code=unused-awaitable
"""Pngcheck Analyzer for Image Submissions.""" """Pngcheck Analyzer for Image Submissions."""
import subprocess
from pathlib import Path from pathlib import Path
from .utils import MAX_PENDING_TIME, update_data from .base_analyzer import SubprocessAnalyzer
class PngcheckAnalyzer(SubprocessAnalyzer):
"""Analyzer for pngcheck."""
def __init__(self, *args):
super().__init__("pngcheck", *args)
self.cmd = ["pngcheck", "-v", self.img]
def is_error(self, returncode: int, stdout: str, stderr: str, zip_exist: bool) -> bool:
return "this is neither a PNG or JNG image nor a MNG stream" in stdout
def analyze_pngcheck(input_img: Path, output_dir: Path) -> None: def analyze_pngcheck(input_img: Path, output_dir: Path) -> None:
"""Analyze an image submission using pngcheck.""" """Analyze an image submission using pngcheck."""
analyzer = PngcheckAnalyzer(input_img, output_dir)
try: analyzer.analyze()
data = subprocess.run(
["pngcheck", "-v", input_img],
capture_output=True,
text=True,
check=False,
timeout=MAX_PENDING_TIME,
)
# Error based on https://github.com/pnggroup/pngcheck/issues/59
# if data.stderr:
# update_data(
# output_dir, {"pngcheck": {"status": "error", "error": data.stderr}}
# )
# return
if "this is neither a PNG or JNG image nor a MNG stream" in data.stdout:
update_data(
output_dir,
{
"pngcheck": {
"status": "error",
"error": "ERRORS DETECTED: this is neither a PNG or JNG image nor a "
"MNG stream.",
}
},
)
return
data_pngcheck = data.stdout.split("\n") if data else []
data_pngcheck = [s for s in data_pngcheck if s]
update_data(
output_dir,
{
"pngcheck": {
"status": "ok",
"output": data_pngcheck,
}
},
)
except Exception as e:
update_data(output_dir, {"pngcheck": {"status": "error", "error": str(e)}})
+47 -107
View File
@@ -4,127 +4,67 @@
"""Steghide Analyzer for Image Submissions.""" """Steghide Analyzer for Image Submissions."""
import re import re
import shutil
import subprocess
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from .utils import MAX_PENDING_TIME, update_data from .base_analyzer import SubprocessAnalyzer
def analyze_steghide( class SteghideAnalyzer(SubprocessAnalyzer):
input_img: Path, output_dir: Path, password: Optional[str] = None """Analyzer for steghide."""
) -> None:
"""Analyze an image submission using steghide."""
image_name = input_img.name def __init__(self, *args):
try: super().__init__("steghide", *args, has_archive=True)
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( def build_cmd(self, password: Optional[str] = None) -> list[str]:
cmd, if password is None:
cwd=output_dir, password = ""
capture_output=True,
text=True, # First, get the embedded file name, and return the test cmd if failed
check=False, cmd_test = ["steghide", "info", self.img, "-p", password]
timeout=MAX_PENDING_TIME, data = self.run_command(cmd_test, cwd=self.output_dir)
) err = self.is_error(data.returncode, data.stdout, data.stderr, False)
if err:
return cmd_test
match = re.search(r'embedded file "([^"]+)"', data.stdout) match = re.search(r'embedded file "([^"]+)"', data.stdout)
assert match is not None
hidden = match.group(1)
cmd = ["steghide", "extract", "-sf", self.img, "-xf", hidden, "-p", password]
return cmd
def is_error(self, returncode: int, stdout: str, stderr: str, zip_exist: bool) -> bool:
"""Check if the result is an error."""
match = re.search(r'embedded file "([^"]+)"', stdout)
embedded_filename = match.group(1) if match else None embedded_filename = match.group(1) if match else None
# if bad file format or wrong password, raise an error # if bad file format or wrong password, raise an error
if data.returncode != 0 or embedded_filename is None: return 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 def process_output(self, stdout: str, stderr: str) -> str | list[str] | dict[str, str]:
# prevent path traversal """Process the stdout into a list of lines."""
embedded_file = output_dir / "steghide" / str(Path(embedded_filename).name) out = []
extracted_dir = embedded_file.parent for line in stderr.split("\n"):
extracted_dir.mkdir(parents=True, exist_ok=True)
cmd_ext: list[str] = [
"steghide",
"extract",
"-sf",
"../../" + str(image_name),
"-xf",
str(embedded_file.name),
]
if password:
cmd_ext += ["-p", password]
else:
cmd_ext += ["-p", ""]
data = subprocess.run(
cmd_ext,
cwd=extracted_dir,
capture_output=True,
text=True,
check=False,
timeout=MAX_PENDING_TIME,
)
# Filter out lines that indicate success
stdout = []
for line in data.stderr.split("\n"):
if line.startswith('wrote extracted data to "'): if line.startswith('wrote extracted data to "'):
stdout.append(line) out.append(line)
elif line: return out
stderr += line + "\n"
# Zip extracted files def process_error(self, stdout: str, stderr: str) -> str:
zip_data = subprocess.run( """Process stderr."""
["7z", "a", "../steghide.7z", "*"], if "the file format of the file" in stderr and "not supported" in stderr:
cwd=extracted_dir, return "The file format of the file is not supported (JPEG or BMP only)."
capture_output=True, out = ""
text=True, for line in stderr.split("\n"):
check=False, if line and not line.startswith('wrote extracted data to "'):
timeout=MAX_PENDING_TIME, out += line + "\n"
) return out
stderr += zip_data.stderr
if len(stderr) > 0: def analyze_steghide(input_img: Path, output_dir: Path, password: Optional[str] = None) -> None:
err = { """Analyze an image submission using steghide."""
"steghide": { analyzer = SteghideAnalyzer(input_img, output_dir)
"status": "error", if password:
"error": stderr, analyzer.analyze(password)
} else:
} analyzer.analyze()
update_data(output_dir, err)
return None
# Remove the extracted directory
shutil.rmtree(extracted_dir)
update_data(
output_dir,
{
"steghide": {
"status": "ok",
"output": stdout,
"download": f"/download/{output_dir.name}/steghide",
}
},
)
except Exception as e:
update_data(output_dir, {"steghide": {"status": "error", "error": str(e)}})
return None
+11 -36
View File
@@ -3,45 +3,20 @@
# mypy: disable-error-code=unused-awaitable # mypy: disable-error-code=unused-awaitable
"""Strings Analyzer for Image Submissions.""" """Strings Analyzer for Image Submissions."""
import subprocess
from pathlib import Path from pathlib import Path
from .utils import MAX_PENDING_TIME, update_data from .base_analyzer import SubprocessAnalyzer
class StringsAnalyzer(SubprocessAnalyzer):
"""Analyzer for strings."""
def __init__(self, *args):
super().__init__("strings", *args)
self.cmd = ["strings", self.img]
def analyze_strings(input_img: Path, output_dir: Path) -> None: def analyze_strings(input_img: Path, output_dir: Path) -> None:
"""Analyze an image submission using strings.""" """Analyze an image submission using strings."""
analyzer = StringsAnalyzer(input_img, output_dir)
try: analyzer.analyze()
data = subprocess.run(
["strings", input_img],
capture_output=True,
text=True,
check=False,
timeout=MAX_PENDING_TIME,
)
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)}})
+18 -67
View File
@@ -1,79 +1,30 @@
# flake8: noqa: E203,E501,W503 # flake8: noqa: E203,E501,W503
# pylint: disable=C0413,W0718,R0903,R0801 # pylint: disable=C0413,W0718,R0903,R0801
# mypy: disable-error-code=unused-awaitable # mypy: disable-error-code=unused-awaitable
"""Custom Analyzer for Image Submissions.""" """<toolname> Analyzer for Image Submissions."""
import shutil
import subprocess
from pathlib import Path from pathlib import Path
from typing import Optional
from .utils import MAX_PENDING_TIME, update_data from .base_analyzer import SubprocessAnalyzer
def analyze_myanalyzer(input_img: Path, output_dir: Path) -> None: class TemplateAnalyze(SubprocessAnalyzer):
"""Analyze an image submission using myanalyzer.""" """Analyzer for <toolname>."""
image_name = input_img.name def __init__(self, *args):
super().__init__("<toolname>", *args, has_archive=True)
# Dir generated by the tool def build_cmd(self, password: Optional[str] = None) -> list[str]:
extracted_dir = output_dir / "myanalyzer_output_dir" if password:
return ["<toolname>", "-p", password, "-i", self.img]
return ["<toolname>", "-i", self.img]
try:
stderr, stdout = "", ""
# Run myanalyzer def analyze_template(input_img: Path, output_dir: Path, password: Optional[str] = None) -> None:
data = subprocess.run( """Analyze an image submission using <toolname>."""
["myanalyzer", "-e", "../" + str(image_name)], analyzer = TemplateAnalyze(input_img, output_dir)
cwd=output_dir, if password:
capture_output=True, analyzer.analyze(password)
text=True, else:
check=False, analyzer.analyze()
timeout=MAX_PENDING_TIME,
)
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,
timeout=MAX_PENDING_TIME,
)
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
+21 -33
View File
@@ -1,43 +1,31 @@
# flake8: noqa: E203,E501,W503 # flake8: noqa: E203,E501,W503
# pylint: disable=C0413,W0718,R0903,R0801 # pylint: disable=C0413,W0718,R0903,R0801
# mypy: disable-error-code=unused-awaitable # mypy: disable-error-code=unused-awaitable
"""Zsteg Analyzer for Image Submissions.""" """Strings Analyzer for Image Submissions."""
import subprocess
from pathlib import Path from pathlib import Path
from .utils import MAX_PENDING_TIME, update_data from .base_analyzer import SubprocessAnalyzer
class ZstegAnalyzer(SubprocessAnalyzer):
"""Analyzer for zsteg."""
def __init__(self, *args, **kwargs):
super().__init__("zsteg", *args, **kwargs)
self.cmd = ["zsteg", self.img]
def is_error(self, returncode: int, stdout: str, stderr: str, zip_exist: bool) -> bool:
return bool(stderr) or "PNG::NotSupported" in stdout[:100]
def process_error(self, stdout: str, stderr: str) -> str:
"""Process the stderr."""
if "PNG::NotSupported" in stdout[:100]:
return "The file format of the file is not supported (PNG only)."
return stderr
def analyze_zsteg(input_img: Path, output_dir: Path) -> None: def analyze_zsteg(input_img: Path, output_dir: Path) -> None:
"""Analyze an image submission using zsteg.""" """Analyze an image submission using zsteg."""
analyzer = ZstegAnalyzer(input_img, output_dir)
try: analyzer.analyze()
data = subprocess.run(
["zsteg", input_img],
capture_output=True,
text=True,
check=False,
timeout=MAX_PENDING_TIME,
)
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)}})