Bilder aus dem Internet können eine erhebliche Gefahr darstellen und bösartige Links oder Schadcode
enthalten. Während eine reine Bilddatei (wie ein normales JPEG oder PNG) den Computer beim reinen
Betrachten meist nicht direkt infizieren kann, nutzen Cyberkriminelle raffinierte Methoden, um Bilder
als Tarnung für Angriffe zu missbrauchen. (Quelle Microsoft)
Ein Beispiel: Bei der Bild-Steganographie (abgeleitet vom griechischen steganos für „verdeckt“
und graphein für „schreiben“) geht es im Kern darum, Informationen so in einem Medium zu verstecken,
dass die Existenz der Information selbst geheim bleibt. Im Bereich der Cyberkriminalität wird diese
Technik missbraucht, um Schadcode an Firewalls und Antivirenprogrammen vorbeizuschmuggeln.
Wie wird der Schadcode im Bild versteckt? Digitale Bilder bestehen aus Millionen von Pixeln, und jedes
Pixel wird durch Farbwerte (Rot, Grün, Blau-RGB) definiert. Diese Werte werden in Binärcode
(Nullen und Einsen) gespeichert. Angreifer nutzen meist zwei Methoden, um Code einzubauen: Die LSB-Methode
(Least Significant Bit): Dies ist das häufigste Verfahren. Der Angreifer verändert das allerletzte Bit
(das niederwertigste Bit) eines Farbwerts. Wenn man beispielsweise den Rotwert eines Pixels von der
Binärzahl 1010110**0** in 1010110**1** ändert, ändert sich die Farbe für das menschliche Auge so minimal,
dass es absolut unsichtbar bleibt. In diesen unzähligen "letzten Bits" des gesamten Bildes wird der
Schadcode bitweise versteckt. Verstecken in den Metadaten: Bilder enthalten Header-Informationen und
Metadaten (wie EXIF-Daten mit Kameratyp, Datum oder GPS-Koordinaten). Angreifer können Schadcode in diese
Textfelder einbetten oder den Code einfach an das eigentliche Ende der Bilddatei anhängen (nach dem
sogenannten "End of Image"-Marker).
Warum schlagen Antivirenscanner keinen Alarm? Klassische Antivirensoftware scannt Dateien nach bekannten
Mustern (Signaturen) von Schadsoftware. Wenn ein Schadcode jedoch in die Pixelstruktur eines Bildes zerlegt
und mit den echten Bilddaten vermischt ist, existiert diese Signatur im Ganzen nicht mehr. Für die
Sicherheitssoftware sieht die Datei wie eine völlig legitime, unschädliche .png- oder .jpg-Datei aus.
Da Bilder in der Regel keine ausführbaren Programme sind, werden sie von Scannern oft
weniger streng geprüft.
Achtung! Das nachfolgende Skript kann das Problem nicht lösen! Aber zumindest
werden versteckte Muster wie http:// oder https:// geortet! Struktur & Funktionsweise siehe weiter unten in
dieser Beschreibung!
Diese Webseite stellt für den Quellcode Download- und Kopierfunktionen zur Verfügung. Das geht nur mit JavaScript.
Wenn du mich für einen unsicheren Kantonisten hältst, verlasse diese Webseite. Alle Informationen und Angebote
sind ohne Gewähr!
#!/usr/bin/env python3
# *****************************************************************************
# Grafische Oberfläche (PyQt6) für eine Bild-Analyse
# Installation der Abhängigkeit: pip install PyQt6 ; pip install pillow
# pip install reportlab ; Start: python Bild_Analyse.py
# (c) Hans Busche Datum: 26.07.2026
# *****************************************************************************
import sys
import re
import os
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QPushButton, QFileDialog, QLabel,
QTextEdit, QSplitter, QFrame, QMessageBox)
from PyQt6.QtGui import QPixmap, QFont, QIcon
from PyQt6.QtCore import Qt
from PIL import Image
from PIL.ExifTags import TAGS
from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors
# Verzeichnis dieser Datei, damit das Icon unabhängig vom Startort gefunden wird
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
ICON_PATH = os.path.join(BASE_DIR, "Schaefer.ico")
class ImageAnalyzerApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Bild Analyse")
# Icon
if os.path.exists(ICON_PATH):
self.setWindowIcon(QIcon(ICON_PATH))
self.resize(950, 700)
# Holt die aktuellen Flags des Fensters
flags = self.windowFlags()
# Deaktiviert den Maximieren-Button
flags &= ~Qt.WindowType.WindowMaximizeButtonHint
# Deaktiviert den Minimieren-Button
#flags &= ~Qt.WindowType.WindowMinimizeButtonHint
self.setMinimumSize(600, 600)
self.setMaximumSize(1000, 800)
# Wichtig: Die geänderten Flags wieder an das Fenster übergeben
self.setWindowFlags(flags)
# Aktuelle Analysedaten für den Export erfassen
self.current_file_path = ""
self.extracted_links = []
self.extracted_text = []
self.extracted_exif = {}
# Globales Stylesheet für eine dunkles Aussehen
self.setStyleSheet("""
QMainWindow {
background-color: #121214;
}
QWidget {
color: #e0e0e6;
font-family: 'Segoe UI', sans-serif;
font-size: 13px;
}
QPushButton {
background-color: #00adb5;
color: #121214;
border: none;
border-radius: 4px;
padding: 10px 18px;
font-weight: bold;
font-size: 14px;
}
QPushButton:hover {
background-color: #00cfd8;
}
QPushButton:pressed {
background-color: #00888f;
}
QPushButton:disabled {
background-color: #2a2a30;
color: #666670;
}
QPushButton#btnExport {
background-color: #e23e57;
color: #ffffff;
}
QPushButton#btnExport:hover {
background-color: #ff4d6d;
}
QPushButton#btnExport:pressed {
background-color: #b8233c;
}
QFrame#imageBorderFrame {
border: 2px dashed #3a3a42;
border-radius: 6px;
background-color: #1a1a1e;
}
QTextEdit {
background-color: #1a1a1e;
border: 1px solid #2a2a30;
border-radius: 4px;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 12px;
color: #00ffcc;
padding: 6px;
}
QLabel {
font-weight: bold;
color: #a0a0aa;
}
QLabel#lblHeadline {
color: #00adb5;
font-size: 16px;
font-weight: bold;
}
QScrollBar:vertical {
border: none;
background: #121214;
width: 10px;
margin: 0px;
}
QScrollBar::handle:vertical {
background: #3a3a42;
min-height: 20px;
border-radius: 5px;
}
QScrollBar::handle:vertical:hover {
background: #00adb5;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
height: 0px;
}
""")
self.init_ui()
def init_ui(self):
main_widget = QWidget()
self.setCentralWidget(main_widget)
main_layout = QVBoxLayout(main_widget)
main_layout.setContentsMargins(15, 15, 15, 15)
main_layout.setSpacing(15)
# Button Layout
top_layout = QHBoxLayout()
self.btn_open = QPushButton(" Bild auswahlen (Windows Explorer)")
self.btn_open.setCursor(Qt.CursorShape.PointingHandCursor)
self.btn_open.clicked.connect(self.open_image)
self.btn_export = QPushButton(" PDF-Bericht exportieren")
self.btn_export.setObjectName("btnExport")
self.btn_export.setCursor(Qt.CursorShape.PointingHandCursor)
self.btn_export.setEnabled(False)
self.btn_export.clicked.connect(self.export_to_pdf)
top_layout.addWidget(self.btn_open)
top_layout.addWidget(self.btn_export)
main_layout.addLayout(top_layout)
# Splitter für flexible Anordnung nebeneinander
splitter = QSplitter(Qt.Orientation.Horizontal)
main_layout.addWidget(splitter)
# Linker Bereich: Bildcontainer
self.img_frame = QFrame()
self.img_frame.setObjectName("imageBorderFrame")
img_frame_layout = QVBoxLayout(self.img_frame)
img_frame_layout.setContentsMargins(10, 10, 10, 10)
self.lbl_image = QLabel("Kein Bild geladen")
self.lbl_image.setAlignment(Qt.AlignmentFlag.AlignCenter)
img_frame_layout.addWidget(self.lbl_image)
splitter.addWidget(self.img_frame)
# Rechter Bereich: Analyse Text
right_widget = QWidget()
right_layout = QVBoxLayout(right_widget)
right_layout.setContentsMargins(0, 0, 0, 0)
right_layout.setSpacing(10)
# Gruppe 1: Versteckte Links
right_layout.addWidget(QLabel("Gefundene Links (im Bild & Dateiende):"))
self.txt_links = QTextEdit()
self.txt_links.setReadOnly(True)
right_layout.addWidget(self.txt_links)
# Gruppe 2: Text Fragmente
right_layout.addWidget(QLabel("Extrahiert lesbare Textfragmente (Strings):"))
self.txt_strings = QTextEdit()
self.txt_strings.setReadOnly(True)
right_layout.addWidget(self.txt_strings)
# Gruppe 3: EXIF Meta-Data
right_layout.addWidget(QLabel("EXIF Metadaten-Analyse:"))
self.txt_exif = QTextEdit()
self.txt_exif.setReadOnly(True)
right_layout.addWidget(self.txt_exif)
splitter.addWidget(right_widget)
# Die Standardgrößen der Splitter (50 % / 50 %) ausgleichen
splitter.setSizes([600, 600])
def open_image(self):
file_path, _ = QFileDialog.getOpenFileName(
self, "Bilddatei auswaehlen", "", "Bilder (*.png *.jpg *.jpeg *.bmp *.gif)"
)
if file_path:
self.current_file_path = file_path
# Scaled image display
pixmap = QPixmap(file_path)
self.lbl_image.setPixmap(pixmap.scaled(
self.lbl_image.size(),
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation
))
self.analyze_image_file(file_path)
self.btn_export.setEnabled(True)
def analyze_image_file(self, file_path):
# Byte-Daten für Regex-Suchvorgänge lesen
with open(file_path, "rb") as f:
binary_data = f.read()
# 1. URL-Suche (eingebettete und angehängte Zeichenfolgen)
url_pattern = re.compile(b'https?://[a-zA-Z0-9./?=&_-]+')
found_raw = url_pattern.findall(binary_data)
self.extracted_links = sorted(list(set([link.decode('utf-8', errors='ignore') for link in found_raw])))
if self.extracted_links:
self.txt_links.setText("\n".join(self.extracted_links))
else:
self.txt_links.setText("Keine versteckten Links gefunden.")
# 2. Suche nach zusammenhängenden Textzeichenfolgen (Länge 4+)
text_pattern = re.compile(b'[a-zA-Z0-9 .,!?;:_\-()]{4,}')
strings_raw = text_pattern.findall(binary_data)
self.extracted_text = [s.decode('utf-8', errors='ignore').strip() for s in strings_raw if s.strip()]
if self.extracted_text:
self.txt_strings.setText("\n".join(self.extracted_text[:200])) # display top 200 lines
else:
self.txt_strings.setText("Keine klaren Textfragmente im Binärcode extrahiert.")
# 3. Parsen von EXIF-Metadaten mit Pillow
self.extracted_exif = {}
try:
with Image.open(file_path) as img:
exif_data = img._getexif()
if exif_data:
for tag_id, value in exif_data.items():
tag_name = TAGS.get(tag_id, tag_id)
# Bereinigen Sie Binärwerte, um sie druckbar zu machen.
if isinstance(value, bytes):
value = value.decode('utf-8', errors='ignore')
self.extracted_exif[str(tag_name)] = str(value)
except Exception as e:
self.extracted_exif = {"Fehler": f"Metadaten konnten nicht gelesen werden: {str(e)}"}
if self.extracted_exif:
exif_lines = [f"{k}: {v}" for k, v in self.extracted_exif.items()]
self.txt_exif.setText("\n".join(exif_lines))
else:
self.txt_exif.setText("Keine EXIF-Metadaten in dieser Datei vorhanden.")
def export_to_pdf(self):
if not self.current_file_path:
return
save_path, _ = QFileDialog.getSaveFileName(
self, "Bild-Analyse PDF-Bericht speichern", "Bild_Bericht.pdf", "PDF-Dateien (*.pdf)"
)
if not save_path:
return
try:
doc = SimpleDocTemplate(save_path, pagesize=letter, title="Bildanalyse-Bericht")
styles = getSampleStyleSheet()
# Individuelle, klare Stile für das PDF
title_style = ParagraphStyle(
'ReportTitle', parent=styles['Heading1'], fontSize=22, spaceAfter=12,
textColor=colors.HexColor('#121214')
)
h2_style = ParagraphStyle(
'ReportH2', parent=styles['Heading2'], fontSize=14, spaceBefore=14,
spaceAfter=6, textColor=colors.HexColor('#00adb5')
)
body_style = ParagraphStyle(
'ReportBody', parent=styles['BodyText'], fontSize=10, spaceAfter=4,
textColor=colors.HexColor('#2c3e50')
)
code_style = ParagraphStyle(
'ReportCode', parent=styles['Code'], fontSize=9, leading=11,
textColor=colors.HexColor('#1a1a24'), backColor=colors.HexColor('#f4f4f6'), borderPadding=5
)
story = []
# Title Elemente
story.append(Paragraph("Bildanalyse-Bericht", title_style))
story.append(Paragraph(f"Analysierte Datei: {os.path.basename(self.current_file_path)}", body_style))
story.append(Paragraph(f"Vollstandiger Pfad: {self.current_file_path}", body_style))
story.append(Spacer(1, 15))
# 1. Links
story.append(Paragraph("1. Versteckte Links / URLs", h2_style))
if self.extracted_links:
links_text = "
".join(self.extracted_links)
story.append(Paragraph(links_text, code_style))
else:
story.append(Paragraph("Keine versteckten Links oder URLs detektiert.", body_style))
# 2. EXIF Meta
story.append(Paragraph("2. Extrahierte EXIF-Metadaten", h2_style))
if self.extracted_exif and "Fehler" not in self.extracted_exif:
exif_table_data = [[Paragraph("Tag", body_style), Paragraph("Wert", body_style)]]
for k, v in self.extracted_exif.items():
exif_table_data.append([Paragraph(str(k), body_style), Paragraph(str(v), body_style)])
t = Table(exif_table_data, colWidths=[150, 350])
t.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), colors.HexColor('#eeeeee')),
('GRID', (0,0), (-1,-1), 0.5, colors.HexColor('#cccccc')),
('VALIGN', (0,0), (-1,-1), 'TOP'),
('PADDING', (0,0), (-1,-1), 5),
]))
story.append(t)
else:
msg = self.extracted_exif.get("Fehler", "Keine EXIF-Metadaten verfugbar.")
story.append(Paragraph(msg, body_style))
# 3. Text Strings
story.append(Paragraph("3. Extrahierte Text-Fragmente (Auszug)", h2_style))
if self.extracted_text:
# Begrenzen Sie die Textzeichenfolgen im Bericht auf die Top 50, um riesige Dateien zu vermeiden.
strings_text = "
".join(self.extracted_text[:50])
story.append(Paragraph(strings_text, code_style))
else:
story.append(Paragraph("Keine lesbaren Textfragmente im Binärcode gefunden.", body_style))
doc.build(story)
QMessageBox.information(self, "Erfolg", "Der PDF-Bericht wurde erfolgreich generiert!")
except Exception as e:
QMessageBox.critical(self, "Fehler", f"PDF-Export fehlgeschlagen: {str(e)}")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = ImageAnalyzerApp()
window.show()
sys.exit(app.exec())
Dieses Python-Skript führt eine präzise Tiefenanalyse einer Bilddatei durch. Es kombiniert die Suche nach
versteckten URLs im gesamten Datei-Quellcode (mit speziellem Fokus auf das Dateiende) sowie das Auslesen
von URLs aus den unsichtbaren EXIF-Metadaten.
Über QFileDialog.getOpenFileName öffnet sich der Windows-Dateiexplorer, um gängige Bildformate
(PNG, JPEG, BMP, GIF) auszuwählen. Visuelle Bildanzeige: Das geladene Bild wird skaliert und formgetreu
auf der linken Seite des Anwendungsfensters dargestellt.
Versteckte Links: Liest die Datei auf Binärebene (rb-Modus). Ein regulärer
Ausdruck sucht nach Mustern wie http:// oder https://. Dies erfasst URLs, die mittels Steganografie in
die Pixeldaten eingebettet wurden, sowie Daten, die an das Dateiende (EOF - End of File) angehängt wurden.
Text-Visualisierung: Scannt die Binärdaten nach fortlaufenden, menschenlesbaren
ASCII/UTF-8-Zeichenketten (Strings-Extraktion). Gefundene Textfragmente, Metadaten oder versteckte Nachrichten
werden extrahiert und formatiert ausgegeben.
EXIF-Metadaten-Analyse: Integriert die automatisierte Auslesung von Kamera-Tags,
Zeitstempeln und Geräte-Informationen mittels Python-Pillow-Bibliothek.
Drei Detektive! Eine kurze Beschreibung mit einfachen Worten: Sobald das Bild geladen
ist, läuft im Hintergrund sofort die dreifache Überprüfung:
Detektiv 1: Die Link-Suche (Versteckte URLs) Bilder bestehen aus Millionen von
Farbpunkten (Pixeln). Man kann Text oder Links so in diesen Punkten verstecken, dass das Auge sie nicht sieht.
Das Skript liest das Bild als reinen Computercode (Binärdaten) und sucht nach typischen Mustern wie http:// oder
https://. Es findet Links, die irgendwo mittendrin oder ganz frech an das eigentliche Dateiende (EOF)
angehängt wurden.
Detektiv 2: Die Text-Suche (Strings-Extraktion). Manchmal verstecken Leute geheime
Nachrichten oder Kommentare im Code eines Bildes. Das Skript scannt die Datei nach lesbaren Buchstabenketten.
Sobald mindestens 4 normale Buchstaben oder Zahlen am Stück auftauchen (z. B. "Geheimnis123"), filtert das Skript sie
heraus und zeigt sie dir an.
Detektiv 3: Der digitale Fingerabdruck (EXIF-Metadaten). Wenn du ein Foto mit dem
Smartphone oder einer Kamera machst, speichert das Gerät unsichtbare Zusatzdaten direkt im Bild ab. Das Skript nutzt
die Bibliothek Pillow, um dieses digitale Tagebuch auszulesen. Es zeigt dir (falls vorhanden) das Kameramodell,
das genaue Datum und die Uhrzeit der Aufnahme an.
Jetzt aber zurück zum Thema PyQt6-Festertechnik! "Aber flott, Donnerwetter"!
Der fensterübergreifende Aufbau der PyQt6-Oberfläche basiert auf einer verschachtelten Box-Struktur (Layouts), die
wie unsichtbare Schienen funktionieren. Sie sorgen dafür, dass sich alle Elemente beim Vergrößern des Fensters
automatisch anpassen. Hier ist die PyQt6-Struktur im Detail von außen nach innen aufgeschlüsselt:
Das Fundament (QMainWindow & QWidget)QMainWindow: Das Hauptfenster-Objekt.
Es liefert das Standard-Rahmengerüst für Windows-Anwendungen (inklusive der Titelleiste, Minimieren-/Maximieren-
Knöpfen und der Fenstergröße).central_widget (QWidget): Ein leeres Basis-Element. PyQt6 verlangt zwingend, dass
der gesamte Inhalt des Fensters in ein einziges zentrales Element gepackt wird (self.setCentralWidget).
Die Hauptaufteilung (QHBoxLayout & QSplitter): Innerhalb des zentralen Elements liegt
ein horizontales Layout (QHBoxLayout). Dieses teilt das Fenster in eine linke und eine rechte Hälfte: Linke Seite
(Bildbereich): Ein vertikales Layout (QVBoxLayout). Es stapelt die beiden oberen Aktions-Buttons (Laden & PDF-Export)
über der großen Bildanzeige (QLabel).Mitte (QSplitter): Der unsichtbare Trenner. Ein QSplitter sorgt dafür, dass
du die Trennlinie zwischen der Bildvorschau (links) und den Textboxen (rechts) mit der Maus verschieben kannst. Rechte
Seite (Datenbereich): Ein vertikales Layout (QVBoxLayout), das die drei Ergebnis-Fenster sauber untereinander stapelt.
Die Ergebnis-Fenster (QTextEdit): Die drei Analyse-Bereiche (Links, Strings, EXIF)
bestehen jeweils aus einem QTextEdit. Diese Widgets sind darauf ausgelegt, große Mengen Text anzuzeigen. PyQt6 spendiert
diesen Boxen automatisch Scrollbalken, sobald der Text länger als das Fenster ist.Sie sind über .setReadOnly(True)
gesperrt, damit du die Ergebnisse zwar kopieren, aber nicht versehentlich überschreiben kannst.
Das Design-Kleid (setStyleSheet): Das gesamte Aussehen wird über QSS (Qt Style Sheets)
gesteuert. Das ist das PyQt6-Äquivalent zu CSS im Webdesign. Jedes Element bekommt hierbei Farbwerte für den normalen
Zustand (z. B. dunkles Grau für den Hintergrund). Über spezielle Befehle wie :hover (Maus zieht darüber)
oder :pressed (Knopf wird geklickt) weiß PyQt6, wann es die Button-Farbe dynamisch in Cyan ändern muss.
Die Signal-Slot-Logik ist das zentrale Nervensystem von PyQt6. Sie steuert, wie die grafische Oberfläche (die Buttons)
mit der Logik deines Python-Codes (den Analyse-Funktionen) kommuniziert. In einfachen Worten: Ein Signal ist das
Ereignis, und der Slot ist die Reaktion darauf.
Das Grundprinzip: Wer funkt wen an? Das Signal (Der Auslöser): Jedes Mal, wenn ein Benutzer
eine Aktion ausführt (z. B. auf einen Button klickt, ein Textfeld ändert oder das Fenster schließt), „funkt“ das
PyQt6-Element ein Signal in die Anwendung. Der Slot (Der Empfänger): Ein Slot ist eine ganz normale Python-Funktion in
unserem Code, die darauf wartet, ausgeführt zu werden, sobald das Signal ertönt. Die Verbindung wird im Code
über den Befehl .connect() hergestellt.

Bild analysieren. Ohne Gewähr © 2026 Hans Busche