// Package ransomware erkennt statistische Auffaelligkeiten in Sicherungslaeufen. // // Die Vorgabe des Implementierungsplans (§18) steht in vier Worten: // // Do not make destructive decisions automatically. Alert first. // // Dieses Paket loescht nichts, sperrt nichts und haelt nichts an. Es beschreibt, // was auffaellig ist, und ueberlaesst jede Entscheidung einem Menschen. Der // Grund ist nicht Vorsicht, sondern Erfahrung: Eine Heuristik, die // selbsttaetig handelt, macht aus jedem Fehlalarm einen Schaden — und // Fehlalarme wird es geben, weil ein Betriebssystem-Update von aussen genauso // aussieht wie ein Verschluesselungsangriff. package ransomware import ( "fmt" "math" "sort" ) // minimumBaselineSamples ist die Zahl der Laeufe fuer einen belastbaren Basiswert. // // Fuenf, nicht drei: Mit drei Messwerten ist ein Median eine Formalitaet und die // Streuung Zufall. Darunter gibt es **keine Aussage** — und keine geratene // Schwelle, die so tut als gaebe es eine. const minimumBaselineSamples = 5 // maximumBaselineSamples begrenzt die Zahl betrachteter Laeufe. // // Zwanzig: Genug fuer einen stabilen Wert, kurz genug, dass sich der Basiswert // an einen veraenderten Betrieb anpasst. Ein Basiswert aus einem Jahr // beschriebe eine Anlage, die es so nicht mehr gibt. const maximumBaselineSamples = 20 // Baseline ist der statistische Normalwert einer Groesse. // // Median und mittlere absolute Abweichung statt Mittelwert und // Standardabweichung: Bei wenigen Laeufen macht ein einziger Ausreisser den // Mittelwert unbrauchbar — und genau ein Ausreisser ist der Fall, den wir // erkennen wollen. Wuerde er den Basiswert mitbestimmen, hobe er die Schwelle // an, gegen die er gemessen wird. type Baseline struct { // SampleCount ist die Zahl eingeflossener Laeufe. SampleCount int `json:"sample_count"` // Median ist der mittlere Wert. Median float64 `json:"median"` // AbsoluteDeviation ist die mittlere absolute Abweichung vom Median. AbsoluteDeviation float64 `json:"absolute_deviation"` // Minimum ist der kleinste beobachtete Wert. Minimum float64 `json:"minimum"` // Maximum ist der groesste beobachtete Wert. Maximum float64 `json:"maximum"` } // IsUsable meldet einen belastbaren Basiswert. func (baseline Baseline) IsUsable() bool { return baseline.SampleCount >= minimumBaselineSamples } // BuildBaseline bildet den Basiswert aus beobachteten Werten. // // Die juengsten Werte stehen vorn; ueberzaehlige aeltere fallen weg. func BuildBaseline(observedValues []float64) Baseline { if len(observedValues) == 0 { return Baseline{} } consideredValues := observedValues if len(consideredValues) > maximumBaselineSamples { consideredValues = consideredValues[:maximumBaselineSamples] } sortedValues := make([]float64, len(consideredValues)) copy(sortedValues, consideredValues) sort.Float64s(sortedValues) medianValue := medianOf(sortedValues) deviations := make([]float64, 0, len(sortedValues)) for _, sortedValue := range sortedValues { deviations = append(deviations, math.Abs(sortedValue-medianValue)) } sort.Float64s(deviations) return Baseline{ SampleCount: len(sortedValues), Median: medianValue, AbsoluteDeviation: medianOf(deviations), Minimum: sortedValues[0], Maximum: sortedValues[len(sortedValues)-1], } } // medianOf liefert den Median einer sortierten Reihe. func medianOf(sortedValues []float64) float64 { valueCount := len(sortedValues) if valueCount == 0 { return 0 } middleIndex := valueCount / 2 if valueCount%2 == 1 { return sortedValues[middleIndex] } return (sortedValues[middleIndex-1] + sortedValues[middleIndex]) / 2 } // deviationMultiplier ist die Zahl der Abweichungen bis zur Auffaelligkeit. // // Drei: Bei normalverteilten Werten liegen darunter rund 99 Prozent. Ein // niedrigerer Wert erzeugte staendig Fehlalarme, ein hoeherer liesse den // eigentlichen Fall durchgehen. const deviationMultiplier = 3.0 // minimumRelativeChange ist die kleinste beachtete relative Veraenderung. // // Ohne diese Untergrenze schluege die Erkennung bei jeder winzigen Schwankung // an, sobald die Streuung des Basiswerts nahe null liegt — etwa bei einer // Quelle, die sich nie aendert. const minimumRelativeChange = 0.5 // DeviationResult beschreibt die Abweichung eines Werts vom Basiswert. type DeviationResult struct { // ObservedValue ist der beobachtete Wert. ObservedValue float64 `json:"observed_value"` // Baseline ist der zugrunde liegende Basiswert. Baseline Baseline `json:"baseline"` // IsUnusual meldet eine Abweichung ueber der Schwelle. IsUnusual bool `json:"is_unusual"` // DeviationFactor ist die Abweichung in Vielfachen der mittleren Abweichung. // // Null, wenn sich kein Faktor bilden laesst — etwa bei einer Streuung von // null. Das Feld traegt dann keine Aussage, und IsUnusual entscheidet. DeviationFactor float64 `json:"deviation_factor"` // Explanation erklaert die Einordnung in einem Satz. Explanation string `json:"explanation"` } // EvaluateDeviation misst einen Wert gegen seinen Basiswert. // // Betrachtet wird ausschliesslich die Abweichung **nach oben**: Weniger // geaenderte Dateien als sonst sind kein Angriffszeichen, und eine // zweiseitige Pruefung erzeugte bei jedem ruhigen Wochenende einen Befund. // // minimumAbsoluteChange ist die Untergrenze, unterhalb derer nichts auffaellt. // Sie ist je Signal verschieden und deshalb ein Parameter: Bei einer Quelle mit // zwei geaenderten Dateien je Lauf ist „drei statt zwei" eine Steigerung um // fuenfzig Prozent — und vollkommen belanglos. Bei einer Datenmenge waeren // dieselben fuenfzig Prozent dagegen aussagekraeftig, sobald es um Gigabyte // geht. Eine gemeinsame Grenze fuer beide gibt es nicht. func EvaluateDeviation(observedValue float64, baseline Baseline, minimumAbsoluteChange float64) DeviationResult { result := DeviationResult{ObservedValue: observedValue, Baseline: baseline} if !baseline.IsUsable() { result.Explanation = fmt.Sprintf("Es liegen erst %d von %d noetigen Laeufen vor. "+ "Ohne Basiswert gibt es keine Aussage — eine geratene Schwelle waere schlechter "+ "als keine.", baseline.SampleCount, minimumBaselineSamples) return result } if observedValue <= baseline.Median { result.Explanation = fmt.Sprintf("Der Wert liegt bei %.1f und damit nicht ueber dem "+ "ueblichen Stand von %.1f.", observedValue, baseline.Median) return result } absoluteChange := observedValue - baseline.Median // Unterhalb der absoluten Untergrenze faellt nichts auf, egal wie gross der // Faktor rechnerisch waere. if absoluteChange < minimumAbsoluteChange { // Die Untergrenze wird ausdruecklich genannt. Ohne sie liest sich der Satz // bei einem tausendfachen Anstieg wie ein Fehler des Werkzeugs — und wer // dem Werkzeug einmal misstraut, liest auch den echten Befund nicht mehr. result.Explanation = fmt.Sprintf("Der Wert stieg von %.1f auf %.1f. Der Zuwachs von %.1f "+ "bleibt unter der Untergrenze von %.1f, ab der diese Groesse etwas aussagt.", baseline.Median, observedValue, absoluteChange, minimumAbsoluteChange) return result } // Eine Streuung von null bedeutet: Die Groesse war bisher immer gleich. // Dann zaehlt die relative Veraenderung, nicht das Vielfache einer // Abweichung, die es nicht gibt. if baseline.AbsoluteDeviation <= 0 { relativeChange := absoluteChange / math.Max(baseline.Median, 1) result.IsUnusual = relativeChange >= minimumRelativeChange result.Explanation = fmt.Sprintf("Bisher lag der Wert stets bei %.1f, jetzt bei %.1f "+ "(%.0f Prozent mehr).", baseline.Median, observedValue, relativeChange*100) return result } result.DeviationFactor = absoluteChange / baseline.AbsoluteDeviation relativeChange := absoluteChange / math.Max(baseline.Median, 1) // Beide Bedingungen muessen zutreffen. Der Faktor allein schluege bei sehr // gleichmaessigen Werten schon bei geringfuegigen Aenderungen an; die // relative Aenderung allein uebersaehe eine Quelle, die ohnehin stark // schwankt. result.IsUnusual = result.DeviationFactor >= deviationMultiplier && relativeChange >= minimumRelativeChange result.Explanation = fmt.Sprintf("Der Wert liegt bei %.1f, ueblich sind %.1f "+ "(%.1f-fache der ueblichen Schwankung, %.0f Prozent mehr).", observedValue, baseline.Median, result.DeviationFactor, relativeChange*100) return result }