NSA, Ghidra und Einhörner

NSA, Ghidra und Einhörner

Dieses Mal wurde das PVS-Studio-Team von Ghidra angezogen, einem großen und bösen Framework für das Reverse Engineering, mit dem Sie verschiedene Binärdateien analysieren und alle möglichen beängstigenden Dinge damit tun können. Das Interessanteste daran ist nicht, dass es kostenlos verwendet werden kann oder gut mit Plugins erweitert werden kann, sondern dass es in der NSA geschrieben und für alle auf GitHub veröffentlicht wurde. Einerseits scheint die NSA über genügend Ressourcen zu verfügen, um die Codebasis sauber zu halten. Andererseits könnten neue Mitwirkende, die nicht sehr vertraut damit waren, in letzter Zeit versehentlich unentdeckte Fehler hinzugefügt haben. Ausgehend von einer statischen Analyse haben wir uns daher entschlossen, in diesem Projekt nach Schwachstellen zu suchen.

Auftakt


Insgesamt gab der statische Analysator PVS-Studio im Java-Teil des Ghidra-Projekts 651 Warnungen für hohe, 904 mittlere und 909 niedrige Warnungen aus ( Release 9.1.2, Commit 687ce7f ). Unter diesen wurde etwa die Hälfte der hohen und mittleren Reaktionen durch die V6022- Diagnose ausgelöst.Parameter wird nicht im Hauptteil der Methode verwendet ", was normalerweise nach dem Refactoring angezeigt wird, wenn ein Parameter nicht mehr benötigt wurde oder eine Funktionalität vorübergehend durch Kommentare deaktiviert wurde. Ein kurzer Blick auf diese Warnungen (es gibt zu viele davon, um sie jeweils als externen Beobachter anzuzeigen ) In diesem Projekt wurde nichts offensichtlich Verdächtiges festgestellt. Es ist wahrscheinlich zulässig, dass dieses Projekt diese Diagnose in den Einstellungen des Analysators vorübergehend deaktiviert, um nicht davon abgelenkt zu werden. In der Praxis werden häufig Tippfehler im Namen des Setter- oder Konstruktorparameters angezeigt, und dies sollte im Allgemeinen der Fall sein Ich bin sicher, dass die meisten Leser mindestens einmal auf ein ähnliches unangenehmes Muster stoßen:

public class A {
  private String value;
  public A(String val) { // V6022
    this.value = value;
  }
  public int hashCode() {
    return value.hashCode(); // NullPointerException
  }
}

Mehr als die Hälfte der niedrigen Warnungen wurde durch die Diagnose " V6008 Potentielle Null-Dereferenzierung von 'Variablen'" ausgegeben. Beispielsweise wird der Wert File.getParentFile () häufig ohne Überprüfung auf Null verwendet . Wenn das Dateiobjekt, für das diese Methode aufgerufen wurde, ohne absoluten Pfad erstellt wurde, wird null zurückgegeben und das Fehlen einer Überprüfung kann die Anwendung löschen.

Traditionell werden wir nur Warnungen auf hohem und mittlerem Niveau analysieren, da der Großteil der tatsächlichen Fehler darin enthalten ist. Bei der Arbeit mit Analysatorberichten empfehlen wir immer, Warnungen in absteigender Reihenfolge ihrer Zuverlässigkeit zu analysieren.

Als nächstes betrachten wir einige vom Analysator angezeigte Fragmente, die mir verdächtig oder interessant erschienen. Die Codebasis des Projekts erwies sich als beeindruckend groß, und es ist fast unmöglich, solche Orte manuell zu finden.

Fragment 1: fehlerhafte Validierung


private boolean parseDataTypeTextEntry()
throws InvalidDataTypeException {
  ...
  try {
    newDataType = parser.parse(selectionField.getText(),
                               getDataTypeRootForCurrentText());
  }
  catch (CancelledException e) {
    return false;
  }
  if (newDataType != null) {
    if (maxSize >= 0
        && newDataType.getLength() > newDataType.getLength()) { // <=
      throw new InvalidDataTypeException("data-type larger than "
                                         + maxSize + " bytes");
    }
    selectionField.setSelectedValue(newDataType);
    return true;
  }
  return false;
}

PVS-Studio Warnung: V6001 Links und rechts vom Operator '>' befinden sich identische Unterausdrücke 'newDataType.getLength ()'. DataTypeSelectionEditor.java data66

Diese Klasse bietet eine grafische Komponente zur Auswahl eines Datentyps, der die automatische Vervollständigung unterstützt. Der Entwickler, der diese Komponente verwendet, kann die maximal zulässige Größe des ausgewählten Datentyps (über das Feld maxSize ) festlegen oder durch Festlegen eines negativen Werts unbegrenzt festlegen . Es wurde angenommen, dass beim Validieren der eingegebenen Daten beim Überschreiten des Grenzwerts eine Ausnahme ausgelöst wird, die dann im Aufrufstapel aufgefangen wird und dem Benutzer eine Nachricht angezeigt wird.

Es scheint, dass der Autor der Komponente zum Zeitpunkt des Schreibens dieses Tests abgelenkt war oder vielleicht über den Sinn des Lebens nachgedacht hat, aber am Ende wird die Validierung einfach nicht durchgeführt, da die Anzahl niemals größer sein kann als sie selbst, und dementsprechend ignorieren wir diesen Zustand. Dies bedeutet, dass diese Komponente ungültige Daten bereitstellen kann.

Ein weiterer ähnlicher Fehler wurde in zwei weiteren Klassen gefunden: GuidUtil und NewGuid .

public class GuidUtil {
  ...
  public static GuidInfo parseLine(...) {
    ...
    long[] data = new long[4];
    ...
    if (isOK(data)) {
      if (!hasVersion) {
        return new GuidInfo(guidString, name, guidType);
      }
      return new VersionedGuidInfo(guidString, version, name, guidType);
    }
    return null;
  }
  ...
  private static boolean isOK(long[] data) {
    for (int i = 0; i < data.length; i++) {
      if ((data[i] != 0) || (data[i] != 0xFFFFFFFFL)) { // <=
        return true;
      }
    }
    return false;
  }
  ...
}

PVS-Studio- Warnung : V6007 Ausdruck 'data [i]! = 0xFFFFFFFFL' ist immer wahr. GuidUtil.java:200

Die for- Schleife der isOK- Methode überprüft, ob derselbe Wert nicht zwei verschiedenen Zahlen gleichzeitig entspricht. Wenn ja, wird die GUID sofort als gültig erkannt. Das heißt, wird die GUID ungültig nur, wenn die Daten - Array ist leer, und das geschieht nie, da der Wert der entsprechenden Variablen nur einmal vergeben wird - zu Beginn der parseLine Methode . IsOK

Methodenkörperin beiden Klassen stimmt es vollständig überein, was auf die Idee eines weiteren Kopierens und Einfügens von falschem Code hindeutet. Was genau der Autor überprüfen wollte, weiß ich nicht genau, aber ich kann davon ausgehen, dass diese Methode wie folgt behoben werden sollte:

private static boolean isOK(long[] data) {
  for (int i = 0; i < data.length; i++) {
    if ((data[i] == 0) || (data[i] == 0xFFFFFFFFL)) {
      return false;
    }
  }
  return true;
}

Fragment 2: Ausnahmen ausblenden


public void putByte(long offsetInMemBlock, byte b)
throws MemoryAccessException, IOException {
  long offsetInSubBlock = offsetInMemBlock - subBlockOffset;
  try {
    if (ioPending) {
      new MemoryAccessException("Cyclic Access"); // <=
    }
    ioPending = true;
    doPutByte(mappedAddress.addNoWrap(offsetInSubBlock / 8),
              (int) (offsetInSubBlock % 8), b);
  }
  catch (AddressOverflowException e) {
    new MemoryAccessException("No memory at address"); // <=
  }
  finally {
    ioPending = false;
  }
}

PVS-Studio Warnung: V6006 Das Objekt wurde erstellt, wird jedoch nicht verwendet. Das Schlüsselwort 'throw' könnte fehlen: 'new MemoryAccessException ("Cyclic Access")'. BitMappedSubMemoryBlock.java:99

Die Ausnahmeobjekte selbst tun, wie Sie wissen, nichts (oder sollten zumindest nichts tun). Fast immer werden ihre neuen Instanzen durch den Wurf geworfen , in einigen seltenen Fällen - irgendwo übertragen oder vielleicht in einer Sammlung abgelegt.

Die Klasse, die diese Methode enthält, ist ein Wrapper über einem Speicherblock, der das Lesen und Schreiben von Daten ermöglicht. Hier kann aufgrund der Tatsache, dass keine Ausnahmen ausgelöst werden, die auferlegte Einschränkung des Zugriffs auf den aktuellen Speicherblock mit dem ioPending- Flag verletzt werdenAußerdem wird eine AddressOverflowException ignoriert . Somit können die Daten stillschweigend beschädigt werden, und anstatt explizit einen Fehler an einer bestimmten Stelle anzuzeigen, erhält der Entwickler seltsame Artefakte, die vom Debugger analysiert werden müssen.

Es gab acht dieser verlorenen Ausnahmen:

  • BitMappedSubMemoryBlock.java: Zeilen 77, 99, 106, 122
  • ByteMappedSubMemoryBlock.java: Zeilen 52, 73, 92, 114

Es ist charakteristisch, dass es in denselben Dateien äußerst ähnliche Methoden gibt, bei denen throw vorhanden ist. Höchstwahrscheinlich wurde eine Methode ursprünglich ähnlich wie das obige Fragment geschrieben, danach wurde sie mehrmals kopiert, fand irgendwie einen Fehler und korrigierte ihn an den Stellen, an die sie sich erinnern konnten.

Fragment 3: Minenfeld


private void processSelection(OptionsTreeNode selectedNode) {
  if (selectedNode == null) {
    setViewPanel(defaultPanel, selectedNode); // <=
    return;
  }
  ...
}
private void setViewPanel(JComponent component, OptionsTreeNode selectedNode) {
  ...
  setHelpLocation(component, selectedNode);
  ...
}
private void setHelpLocation(JComponent component, OptionsTreeNode node) {
  Options options = node.getOptions();
  ...
}

PVS-Studio- Warnung : V6008 Null-Dereferenzierung von 'selectedNode' in der Funktion 'setViewPanel'. OptionsPanel.java:266

Der Analysator hat ein bisschen gelogen - im Moment führt der Aufruf der processSelection- Methode nicht zu einer NullPointerException , da diese Methode nur zweimal aufgerufen wird und selectedNode vor dem Aufruf explizit auf null überprüft wird . Dies sollte nicht durchgeführt werden, da ein anderer Entwickler möglicherweise erkennt, dass die Methode den Fall selectedNode == null explizit behandelt , und entscheidet, dass dies ein gültiger Wert ist, der dann zum Absturz der Anwendung führt. Solche Überraschungen sind gerade in offenen Projekten besonders gefährlich, da Leute, die die Codebasis nicht kennen, gründlich daran teilnehmen.

Im Allgemeinen muss ich sagen, dass die gesamte processSelection- Methode ziemlich seltsam aussieht. Es ist wahrscheinlich, dass dies ein Fehler beim Kopieren und Einfügen ist, da bei derselben Methode ein if-Block mit demselben Text zweimal häufiger auftritt, wenn auch unter unterschiedlichen Bedingungen. Zu diesem Zeitpunkt ist der selectedNode jedoch nicht mehr null , und die Aufrufkette setViewPanel-setHelpLocation führt nicht zu einer NullPointerException .

Fragment 4: Automatische Vervollständigung für das Böse


public static final int[] UNSUPPORTED_OPCODES_LIST = { ... };
public static final Set<Integer> UNSUPPORTED_OPCODES = new HashSet<>();

static {
  for (int opcode : UNSUPPORTED_OPCODES) {
    UNSUPPORTED_OPCODES.add(opcode);
  }
}

PVS-Studio Warnung: V6053 Die Sammlung wird während der Iteration geändert. ConcurrentModificationException kann auftreten. DWARFExpressionOpCodes.java:205

In diesem Fall hat der Analysator erneut ein wenig gelogen - die Ausnahme wird nicht ausgelöst, da die Auflistung UNSUPPORTED_OPCODES immer leer ist und die Schleife einfach nicht ausgeführt wird. Außerdem ist die Sammlung eine Vielzahl, und das Hinzufügen eines bereits vorhandenen Elements ändert nichts daran. Höchstwahrscheinlich hat der Autor für jeden eingetragenden Namen der Sammlung durch automatische Vervollständigung und bemerkte nicht, dass das falsche Feld vorgeschlagen wurde. Eine Änderung der Sammlung während der Iteration ist nicht möglich, aber unter guten Umständen, wie in diesem Fall, kann die Anwendung nicht fallen. Hier hat dieser Tippfehler einen indirekten Effekt: Der Computer, der DWARF-Dateien analysiert, verwendet diese Sammlung, um die Analyse zu stoppen, wenn nicht unterstützte Opcodes gefunden werden.

Ab Java 9 lohnt es sich, die Factory-Methoden der Standardbibliothek für konstante Sammlungen zu verwenden: Beispielsweise ist Set.of (T ... elements) nicht nur wesentlich praktischer, sondern macht die erstellte Sammlung auch sofort unveränderlich, was die Zuverlässigkeit des Codes erhöht.

Fragment 5: da ist alles


public void setValueAt(Object aValue, int row, int column) {
  ...
  int index = indexOf(newName);
  if (index >= 0) {                  // <=
    Window window = tool.getActiveWindow();
    Msg.showInfo(getClass(), window, "Duplicate Name",
                 "Name already exists: " + newName);
    return;
  }

  ExternalPath path = paths.get(row); // <=
  ...
}
private int indexOf(String name) {
  for (int i = 0; i < paths.size(); i++) {
    ExternalPath path = paths.get(i);
    if (path.getName().equals(name)) {
      return i;
    }
  }
  return 0;
}

PVS-Studio-Warnungen:

  • V6007 Der Ausdruck 'index> = 0' ist immer wahr. ExternalNamesTableModel.java:105
  • V6019 Unreachable code detected. It is possible that an error is present. ExternalNamesTableModel.java:109

Der Autor darüber nachdachte, und in der indexOf Methode , anstelle von „Index“ -1 für einen unbekannten Wert 0 zurück - der Index des ersten Elements der Pfad Sammlung . Auch wenn die Sammlung leer ist. Oder vielleicht wurde die Methode generiert, aber vergessen, den Standardrückgabewert zu ändern. Infolgedessen verwirft die setValueAt- Methode alle an sie übergebenen Werte und zeigt dem Benutzer den Fehler "Name existiert bereits" an, auch wenn kein Name vorhanden ist.

By the way, indexOf ist nirgendwo anders, und sein Wert wird nur benötigt , um festzustellen , ob das Element Sie besteht suchen verwendet. Vielleicht schreiben Sie anstelle einer separaten Methode für jede direkt in setValueAt und geben Sie return zurückauf einem passenden Gegenstand anstelle von Spielen mit Indizes.

Hinweis: Ich konnte den angeblichen Fehler nicht reproduzieren. Die setValueAt- Methode wird wahrscheinlich nicht mehr verwendet oder nur unter bestimmten Bedingungen aufgerufen.

Fragment 6: Schweigen


final static Map<Character, String> DELIMITER_NAME_MAP = new HashMap<>(20);
// Any non-alphanumeric char can be used as a delimiter.
static {
  DELIMITER_NAME_MAP.put(' ', "Space");
  DELIMITER_NAME_MAP.put('~', "Tilde");
  DELIMITER_NAME_MAP.put('`', "Back quote");
  DELIMITER_NAME_MAP.put('@', "Exclamation point");
  DELIMITER_NAME_MAP.put('@', "At sign");
  DELIMITER_NAME_MAP.put('#', "Pound sign");
  DELIMITER_NAME_MAP.put('$', "Dollar sign");
  DELIMITER_NAME_MAP.put('%', "Percent sign");
  ...
}

PVS-Studio Warnung: V6033 Ein Element mit demselben Schlüssel '@' wurde bereits hinzugefügt. FilterOptions.java:45

Ghidra unterstützt das Filtern von Daten in verschiedenen Kontexten: Sie können beispielsweise die Liste der Projektdateien nach Namen filtern. Außerdem wird das Filtern nach mehreren Schlüsselwörtern gleichzeitig implementiert: '.java, .c' zeigt im aktivierten 'ODER'-Modus alle Dateien an, deren Name entweder' .java 'oder' .c 'enthält. Es versteht sich, dass jedes Sonderzeichen als Worttrennzeichen verwendet werden kann (in den Filtereinstellungen ist ein bestimmtes Trennzeichen ausgewählt), aber in Wirklichkeit war das Ausrufezeichen nicht verfügbar.

In solchen Initialisierungsblättern ist das Versiegeln sehr einfach, da sie häufig durch Kopieren und Einfügen geschrieben werden. Wenn Sie sich einen solchen Code ansehen, verschwimmen Ihre Augen schnell. Und wenn der Tippfehler nicht in zwei benachbarten Zeilen war, dann wird er von Hand mit ziemlicher Sicherheit niemand sehen.

Fragment 7: Der Rest der Teilung ist immer 0


void setFactorys(FieldFactory[] fieldFactorys,
                 DataFormatModel dataModel, int margin) {
  factorys = new FieldFactory[fieldFactorys.length];

  int x = margin;
  int defaultGroupSizeSpace = 1;
  for (int i = 0; i < factorys.length; i++) {
    factorys[i] = fieldFactorys[i];
    factorys[i].setStartX(x);
    x += factorys[i].getWidth();
    // add in space between groups
    if (((i + 1) % defaultGroupSizeSpace) == 0) { // <=
      x += margin * dataModel.getUnitDelimiterSize();
    }
  }
  width = x - margin * dataModel.getUnitDelimiterSize() + margin;
  layoutChanged();
}

PVS-Studio-Warnungen:

  • V6007 Ausdruck '((i + 1)% defaultGroupSizeSpace) == 0' ist immer wahr. ByteViewerLayoutModel.java:66
  • V6048 Dieser Ausdruck kann vereinfacht werden. Der Operand 'defaultGroupSizeSpace' in der Operation entspricht 1. ByteViewerLayoutModel.java:66

Der Hex-Byte-Viewer unterstützt die Auswahl der Größe der angezeigten Gruppen: Sie können beispielsweise die Ausgabe im Format 'ffff ffff' oder 'ff ff ff ff' konfigurieren. Die setFactorys- Methode ist für den Speicherort dieser Gruppen in der Benutzeroberfläche verantwortlich . Trotz der Tatsache, dass Anpassung und Anzeige korrekt funktionieren, sieht der Zyklus bei dieser Methode äußerst verdächtig aus: Der Rest der Division durch Eins ist immer Null, was bedeutet, dass die x- Koordinate bei jeder Iteration zunimmt. Suspicion fügt Eigenschaften und Verfügbarkeit GRUPPEN in Einstellung Datamodel .

Verbleibender Müll nach dem Refactoring? Oder möglicherweise gingen die Berechnungen der defaultGroupSizeSpace- Variablen verloren? In jedem Fall hat ein Versuch, seinen Wert durch dataModel.getGroupSize () zu ersetzen, das Layout beschädigt , und möglicherweise kann nur der Autor dieses Codes eine eindeutige Antwort geben.

Fragment 8: gebrochene Validierung, Teil 2


private String parseArrayDimensions(String datatype,
                                    List<Integer> arrayDimensions) {
  String dataTypeName = datatype;
  boolean zeroLengthArray = false;
  while (dataTypeName.endsWith("]")) {
    if (zeroLengthArray) {                   // <=
      return null; // only last dimension may be 0
    }
    int rBracketPos = dataTypeName.lastIndexOf(']');
    int lBracketPos = dataTypeName.lastIndexOf('[');
    if (lBracketPos < 0) {
      return null;
    }
    int dimension;
    try {
      dimension = Integer.parseInt(dataTypeName.substring(lBracketPos + 1,
                                                          rBracketPos));
      if (dimension < 0) {
        return null; // invalid dimension
      }
    }
    catch (NumberFormatException e) {
      return null;
    }
    dataTypeName = dataTypeName.substring(0, lBracketPos).trim();
    arrayDimensions.add(dimension);
  }
  return dataTypeName;
}

PVS-Studio Warnung: V6007 Der Ausdruck 'zeroLengthArray' ist immer falsch. PdbDataTypeParser.java:278

Diese Methode analysiert die Dimension eines mehrdimensionalen Arrays und gibt entweder den nach dem Parsen verbleibenden Text oder null für ungültige Daten zurück. Ein Kommentar neben einer der Validierungsprüfungen besagt, dass nur die letzte Lesegröße Null sein kann. Die Analyse geht von rechts nach links, daher versteht es sich, dass '[0] [1] [2]' ein gültiger Eingabetext ist und '[2] [1] [0]' nicht.

Aber Problem: Niemand hat die Überprüfung hinzugefügt, dass die nächste Größe Null ist, und der Parser isst ungültige Daten ohne unnötige Fragen. Sie sollten den try-Block wahrscheinlich wie folgt reparieren:

try {
  dimension = Integer.parseInt(dataTypeName.substring(lBracketPos + 1,
                                                      rBracketPos));
  if (dimension < 0) {
    return null; // invalid dimension
  } else if (dimension == 0) {
    zeroLengthArray = true;
  }
}
catch (NumberFormatException e) {
  return null;
}

Natürlich besteht die Möglichkeit, dass sich dieses Kriterium der Gültigkeit im Laufe der Zeit als unnötig herausstellte oder der Kommentar des Autors eine andere Bedeutung hat und die erste gelesene Dimension überprüft werden muss. In jedem Fall ist die Datenvalidierung ein kritischer Bestandteil jeder Anwendung, die mit voller Verantwortung übernommen werden muss. Fehler darin können zu harmlosen Abstürzen der Anwendung sowie zu Sicherheitslücken, Datenlecks, Datenbeschädigung oder -verlust führen (z. B. wenn Sie die SQL-Injection während der Abfrageüberprüfung überspringen).

Ein bisschen über den Rest der Warnungen


Der Leser kann feststellen, dass viele Warnungen ausgegeben wurden, aber nur wenige berücksichtigt wurden. Nicht sehr ordentlich abgestimmtes Cloc im Projekt zählte ungefähr 1,25 Millionen Zeilen Java-Code (nicht leer und keine Kommentare). Tatsache ist, dass fast alle Warnungen sehr ähnlich sind: Hier haben sie vergessen, auf Null zu prüfen , sie haben den nicht verwendeten Legacy-Code dort nicht gelöscht. Ich möchte den Leser nicht wirklich langweilen, indem ich dasselbe aufführe, und ich habe einen Teil solcher Fälle am Anfang des Artikels erwähnt.

Ein weiteres Beispiel sind die fünfzig Warnungen " V6009- Funktion erhält ein ungerades Argument" im Zusammenhang mit der ungenauen Verwendung der Teilzeichenfolgenmethode(CParserUtils.java:280, ComplexName.java:48 und andere), um den Rest der Zeichenfolge nach einem Trennzeichen abzurufen. Entwickler hoffen oft, dass dieses Trennzeichen in der Zeichenfolge vorhanden ist, und vergessen, dass indexOf andernfalls -1 zurückgibt , was ein falscher Wert für die Teilzeichenfolge ist . Wenn die Daten validiert wurden oder nicht von außen empfangen wurden, verringert sich natürlich die Wahrscheinlichkeit eines Absturzes der Anwendung erheblich. Im Allgemeinen sind dies jedoch potenziell gefährliche Orte, die wir beseitigen möchten.

Fazit


Im Allgemeinen war Ghidra mit der Qualität des Codes zufrieden - es sind keine besonderen Albträume erkennbar. Der Code ist gut formatiert und hat einen sehr konsistenten Stil: In den meisten Fällen erhalten Variablen, Methoden und alles andere eindeutige Namen, es werden erklärende Kommentare gefunden, es gibt eine Vielzahl von Tests.

Natürlich gab es keine Probleme, darunter:

  • Toter Code, der höchstwahrscheinlich nach zahlreichen Umgestaltungen erhalten blieb;
  • Viele Javadocs sind hoffnungslos veraltet und weisen beispielsweise auf nicht vorhandene Parameter hin.
  • Bei Verwendung von IntelliJ IDEA besteht keine Möglichkeit einer bequemen Entwicklung .
  • Ein modulares System, das auf Reflexion basiert, erschwert das Navigieren in einem Projekt und das Auffinden von Abhängigkeiten zwischen Komponenten erheblich.

Bitte vernachlässigen Sie die Entwicklertools nicht. Statische Analysen sind wie Sicherheitsgurte kein Allheilmittel, tragen jedoch dazu bei, einige Katastrophen vor der Freigabe zu verhindern. Und niemand benutzt gerne die angemeldete Software.

Weitere bewährte Projekte finden Sie in unserem Blog . Außerdem verfügen wir über eine Testlizenz und verschiedene Optionen für die Verwendung des Analysegeräts, ohne dafür bezahlen zu müssen.



Wenn Sie diesen Artikel einem englischsprachigen Publikum zugänglich machen möchten, verwenden Sie bitte den Link zur Übersetzung: Nikita Lazeba. NSA, Ghidra und Einhörner .

All Articles