MSDN Magazin > Home > Ausgaben > 2008 > November >  Sicherheitsquiz: Testen Sie Ihren Sicherheits-I...
Sicherheitsquiz
Testen Sie Ihren Sicherheits-IQ
Michael Howard und Bryan Sullivan

Uns beiden macht es Spaß, Code auf Sicherheitsfehler zu überprüfen. Man könnte sogar behaupten, dass wir ziemlich gut darin sind. Die besten sind wir vielleicht nicht unbedingt, aber die Fehlersuche geht uns relativ schnell von der Hand. Wie steht es mit Ihnen?
Würden Sie einen Sicherheitsfehler erkennen? Finden Sie es mit diesem Quiz heraus. Jedes Codebeispiel enthält mindestens ein Sicherheitsrisiko. Versuchen Sie, möglichst alle Fehler zu finden. Dem Code folgt eine Zusammenfassung aller Sicherheitsrisiken, ein Kommentar und ggf. eine Beschreibung, wie der Sicherheitsentwicklungszyklus (Security Development Lifecycle, SDL) dabei behilflich sein kann, diese Fehler zu finden. Unser Dank gilt Peter Torr und Eric Lippert für ihre Beiträge und Codebeispiele.
Fehler Nr. 1 (C oder C++)
void func(char *s1, char *s2) {
  char d[32];
  strncpy(d,s1,sizeof d - 1);
  strncat(d,s2,sizeof d - 1);
  ...
}
Antwort Wir wollten es Ihnen zu Anfang mit einem gewöhnlichen Pufferüberlauf leicht machen. Vielen denken, dass der Code sicher ist, weil die begrenzten strncpy- und strncat-Funktionen verwendet werden. Diese Funktionen sind aber nur sicher, wenn die Puffergrößen korrekt sind, und in diesem Beispiel sind sie leider falsch. Sogar völlig falsch.
Technisch gesehen ist der erste Aufruf sicher, der zweite dagegen falsch. Das letzte Argument der strncpy- und strncat-Funktionen ist die noch verbleibende Speichermenge im Puffer, und diese wurde mit dem Aufruf von strncpy gerade teilweise oder vollständig ausgeschöpft Das Ergebnis: Ein Pufferüberlauf. Michael hat bereits 2004 in seinem Blog über diesen Fehlertyp geschrieben.
In Visual C++ 2005 und höheren Versionen werden Sie durch die Warnung C4996 aufgefordert, den ungültigen Funktionsaufruf durch einen sicheren Aufruf zu ersetzen, und die Option „/analyze“ gibt eine C6053-Warnung aus, dass die Zeichenfolge durch den Aufruf von strncat möglicherweise nicht mit 0 (null) beendet wird.
Um ganz ehrlich zu sein, sind strncpy und strncat (und ihre „n“-Verwandten) aus verschiedenen Gründen schlimmer als strcpy und strcat (und Funktionen ihrer Art). Erstens ist der Rückgabewert einfach lächerlich, da auf einen gültigen oder vielleicht auch auf einen ungültigen Puffer verwiesen wird. Es gibt keine Möglichkeit, dies festzustellen. Zweitens ist es wirklich schwer, die Zielpuffergröße korrekt einzustellen. Wenn Sie den Fehler entdeckt haben, geben Sie sich einen Punkt.

Fehler Nr. 2 (C oder C++)
int main(int argc, char* argv[]) {
  char d[32];
  if (argc==2) 
    strcpy(d,argv[1]);

  ...
  return 0;
}
Antwort Dies ist ein hinterhältiger Fehler. Dieser Fehler wird oft als Beispiel für einen Pufferüberlauf verwendet, und meistens ist es unmöglich zu bestimmen, ob der Code einen Sicherheitsfehler enthält oder nicht. Es hängt alles davon ab, wie der Code verwendet wird.
Wenn es sich um eine standardmäßige Win32-EXE handelt, liegt kein Sicherheitsfehler vor, denn schlimmstenfalls kommt es zu einem Selbstangriff und einer Selbstausführung des Codes, und dies stellt keinen Sicherheitsfehler dar.
Falls sich dieser Code allerdings im ServiceMain eines Windows-Diensts befindet, der zum Beispiel als System ausgeführt wird, oder in der Hauptfunktion „setuid root“ einer Linux-Anwendung, dann wird der Code zu einem ernsten Sicherheitsfehler.
Angenommen, der Code befindet sich in einer Linux-Anwendung, die als „setuid root“ gekennzeichnet ist. Wenn die Anwendung von einem Standardbenutzer gestartet wird, wird sie tatsächlich als Stamm ausgeführt, das heißt, es liegt eine Sicherheitsanfälligkeit bezüglich der lokalen Berechtigungserhöhung vor.
Wie im Codebeispiel für Fehler Nr. 1 angegeben, werden C4996-Warnungen für den Aufruf von strcpy ausgegeben, und „/analyze“ gibt eine C6204-Warnung aus, die einen potenziellen Pufferüberlauf anzeigt. Wenn Sie mit „Dazu brauche ich viel mehr Kontext“ geantwortet haben, geben Sie sich zwei Punkte. Andernfalls gehen Sie leer aus.

Fehler Nr. 3 (beliebige Sprache, Beispiel in C#)
byte[] GetKey(UInt32 keySize) {
  byte[] key = null;

  try {
    key = new byte[keySize];
    RNGCryptoServiceProvider.Create().GetBytes(key);
  }
  catch (Exception e) {
    Random r = new Random();
    r.NextBytes(key);
  }

  return key;
}
Antwort Dieser miserable Schlüsselgenerierungscode enthält zwei Fehler. Der erste ist ziemlich offensichtlich: Wenn der Aufruf des kryptografisch soliden Zufallszahlengenerators fehlschlägt, erfasst der Code die Ausnahme und ruft dann einen wirklich miserablen und vorhersagbaren Zufallsgenerator auf. Wenn Sie dies bemerkt haben, geben Sie sich einen Punkt. Es handelt sich um eine SDL-Anforderung, beim Generieren von Schlüsseln kryptografische Zufallszahlen zu verwenden.
Aber es gibt einen weiteren Fehler: Der Code erfasst alle Ausnahmen. Abgesehen von seltenen Fällen bedeutet die Erfassung aller Ausnahmen, dass wirkliche Fehler wie C++- oder Microsoft .NET Framework-Ausnahmen oder die strukturierte Ausnahmebehandlung unter Windows nicht aufgedeckt werden. Lassen Sie dies also sein.
Ein strukturierter Ausnahmehandler in C oder C++, der alle Ausnahmen (einschließlich Zugriffsschutzfehler wie Pufferüberläufe) erfasst, gibt bei der Kompilierung mit „/analyze“ eine C6320-Warnung heraus. Diese Art von Entwurf hat Angreifer dazu verleitet, ihre Angriffe gegen den animierten Cursor (Fehler MS07-017) erneut zu versuchen. Wenn Sie den Ausnahmebehandlungsfehler entdeckt haben, erhalten Sie einen weiteren Punkt.

Fehler Nr. 4
void func(const char *s) {
  if (!s) return;
  char t[3];
  memcpy(t,s,3);
  t[3] = 0;
  ...
}
Antwort Einen Fehler wie diesen fanden wir vor einigen Jahren in Windows Vista, als sich das System noch im Entwicklungsstadium befand. Handelt es sich hierbei aber um einen Sicherheitsfehler? Offensichtlich ist es ein Codefehler, denn der Code schreibt an das vierte Arrayelement, aber das Array enthält nur drei Elemente. Denken Sie daran, dass Arrays bei null anfangen, nicht bei eins. Ich würde behaupten, dass dies kein Sicherheitsfehler ist, weil der Angreifer keinerlei Kontrolle erhält.
Wenn dieser Fehler aber vorhanden ist, wenn der Angreifer i kontrolliert, könnte der Angreifer an einem beliebigen Ort im Speicher eine null schreiben. Das wiederum ist ein unumstrittener Sicherheitsfehler:
void func(const char *s, int i) {
  if (!s) return;
  char t[3];
  memcpy(t,s,3);
  t[i] = 0;
  ...
}
Dieser Code führt bei der Kompilierung mit „/analyze“ zu einer C6201-Warnung „außerhalb des gültigen Indexbereichs“. Wenn Ihre Antwort „kein Sicherheitsfehler“ lautete, geben Sie sich einen Punkt.

Fehler Nr. 5
public class Barrel {
  // By default, a barrel contains one rhesus monkey.  
  private static Monkey[] defaultMonkeys = 
    new[] { new RhesusMonkey() };
  // backing store for property.
  private IEnumerable<Monkey> monkeys = null; 

  public IEnumerable<Monkey> Monkeys {
    get {
      if (monkeys == null) {
        if (MonkeysReady())
          monkeys = PopulateMonkeys();
        else
          monkeys = defaultMonkeys;
      }
      return monkeys;
    }
  }
}
Antwort Dieser Fehler ist nicht einfach zu erkennen. Der Autor dieser Klasse denkt, dass er sowohl sicher als auch effizient vorgeht. Der Sicherungsspeicher ist privat, die Eigenschaft ist schreibgeschützt, und der Eigenschaftstyp ist „IEnumerable<T>“. Deshalb kann der Aufrufer nichts anderes vornehmen, als den Status des Barrels abzulesen.
Der Autor hat vergessen, dass durch einen schädlichen Aufruf versucht werden kann, den Rückgabewert der Eigenschaft in „Monkey[]“umzuwandeln. Wenn zwei Barrels vorliegen und jedes die Standardliste „Monkey“ enthält, kann durch einen schädlichen Aufruf, der über eine davon verfügt, der Eintrag RhesusMonkey in der statischen Standardliste durch einen anderen Monkey oder null ersetzt und dadurch effektiv der Zustand des anderen Barrels geändert werden.
Die Lösung besteht darin, ein ReadOnlyCollection<T> oder einen anderen wirklich schreibgeschützten Speicher zwischenzuspeichern, um das zugrunde liegende Array vor einer Mutation durch einen schädlichen oder fehlerhaften Aufruf zu schützen. Wenn Sie dies erkannt haben, geben Sie sich zwei Punkte.

Fehler Nr. 6 (C#)
protected void Page_Load(object sender, EventArgs e) {
  string lastLogin = Request["LastLogin"];
  if (String.IsNullOrEmpty(lastLogin)) {
    HttpCookie lastLoginCookie = new HttpCookie("LastLogin",
      DateTime.Now.ToShortDateString());
    lastLoginCookie.Expires = DateTime.Now.AddYears(1);
    Response.Cookies.Add(lastLoginCookie);
  }
  else {
    Response.Write("Welcome back! You last logged in on " + lastLogin);
    Response.Cookies["LastLogin"].Value = 
      DateTime.Now.ToShortDateString();
  }
}
Antwort Hierbei handelt es sich um eine einfache Sicherheitsanfälligkeit bei siteübergreifender Skripterstellung, dem häufigsten Sicherheitsrisiko im Web. Obwohl der Code zu implizieren scheint, dass der lastLogin-Wert immer von einem Cookie stammt, bevorzugt die HttpRequest.Item-Eigenschaft eigentlich einen Wert von der Abfragezeichenfolge als einen Wert von einem Cookie.
Unabhängig davon, auf welchen Wert das lastLogin-Cookie gesetzt ist, wenn ein Angreifer der Abfragezeichenfolge das Namens-/Wertpaar „lastLogin=<script>alert('0wned!')</script>“ hinzufügt, wählt die Anwendung die schädliche Skripteingabe als den Wert der lastLogin-Variable aus. Wenn Sie XSS geantwortet haben, geben Sie sich einen Punkt.

Fehler Nr. 7 (C#)
private decimal? lookupPrice(XmlDocument doc) {
  XmlNode node = doc.SelectSingleNode(
    @"//products/product[id/text()='" + 
    Request["itemId"] + "']/price");
  if (node == null)
    return null;
  else
    return (Convert.ToDecimal(node.InnerText));
}
Antwort Wenn Sie XPath-Injektion geantwortet haben, erhalten Sie einen Punkt. Die XPath-Injektion arbeitet genau nach dem gleichen Prinzip wie die weit berühmtere (und berüchtigtere) SQL-Injektion. Durch Erstellen einer Abfrage, in der der XPath-Code mit nicht validierter Benutzereingabe ohne Escapefrequenz kombiniert wird, ist dieser Code anfällig für Injektionsangriffe. Jede Anwendung, die einen Text ändert, der dann für die Ausführung eines Vorgangs verwendet wird, unterliegt Injektionssicherheitsrisiken.

Fehler Nr. 8 (C#)
public class CustomSessionIDManager : System.Web.Session    State.SessionIDManager
{
    private static object lockObject = new object();

    public override string CreateSessionID(HttpContext context)
    {
        lock (lockObject)
        {
            Int32? lastSessionId = (int?)context.Application                ["LastSessionId"];
            if (lastSessionId == null)
                lastSessionId = 1;
            else
                lastSessionId++;
            context.Application["LastSessionId"] = lastSessionId;
            return lastSessionId.ToString();
        }
    }
}
Antwort Hier gibt es zwei Hauptprobleme. Obwohl der Code ordnungsgemäß eine Sperre um die Anwendungslogik anwendet, um sicherzustellen, dass zwei Threads nicht gleichzeitig die gleiche Sitzungs-ID erstellen, kann er trotzdem nicht sicher in einer Serverfarm bereitgestellt werden. Der durch das HttpContext.Application-Objekt referenzierte Anwendungszustand ist nicht serverübergreifend freigegeben. Bei Bereitstellung dieser Anwendung in einer Serverfarm könnte dies zu Sitzungs-ID-Konflikten führen. Wenn Sie diesen Fehler entdeckt haben, geben Sie sich einen Punkt.
Ein anderes ernstes Problem ist, dass die von dieser Klasse generierten Sitzungs-IDs problemlos zu erratende sequenzielle Ganzzahlen sind. Wenn ein Benutzer auf seinem Sitzungstoken die Sitzungs-ID 100 erkennt, könnte er ein einfaches Browserdienstprogramm verwenden, um die Sitzungs-ID auf 99 oder 98 oder einen anderen niedrigeren Wert zu ändern, um die Sitzungen der entsprechenden Benutzer zu manipulieren.
In diesem Fall ist es für den Entwickler eine weitaus bessere Option, eine GUID oder andere lange Zeichenfolgen aus zufälligen Buchstaben und Zahlen zu verwenden. Wenn Sie erkannt haben, dass sequenzielle Ganzzahlen eine schlechte Wahl für Sitzungs-ID-Token sind, erzielen Sie einen Punkt.

Fehler Nr. 9 (C#)
bool login(string username, 
           string password, 
           SqlConnection connection, 
           out string errorMessage) {
  SqlCommand selectUserAndPassword = new SqlCommand(
    "SELECT Password FROM UserAccount WHERE Username = @username",
    connection);
  selectUserAndPassword.Parameters.Add(
    new SqlParameter("@username",  username));
  string validPassword = 
    (string)selectUserAndPassword.ExecuteScalar();

  if (validPassword == null) {
    // the user doesn't exist in the database
    errorMessage = "Invalid user name";
    return false;
  }
  else if (validPassword != password) {
    // the given password doesn't match 
    errorMessage = "Incorrect password";
    return false;
  }
  else {
    // success
    errorMessage = String.Empty;
    return true;
  }
}
Antwort Das größte Problem hier besteht darin, dass die Anwendung im Fall einer fehlgeschlagenen Anmeldung zu viele Informationen an den Benutzer zurückgibt. Es ist zwar für einen Benutzer sicher hilfreich zu wissen, ob er sich lediglich bei seinem Kennwort vertippt hat oder ob er seinen Benutzernamen vergessen hat, doch diese Informationen kommen auch einem Angreifer zugute, der versucht, einen Brute-Force-Angriff auf die Anwendung durchzuführen. Es mag auf den ersten Blick nicht einleuchtend erscheinen, aber in dieser Situation ist es besser, keine hilfreichen Informationen bereitzustellen. Bei einem fehlgeschlagenen Anmeldungsversuch sollte eine Nachricht wie „Ungültiger Benutzername oder ungültiges Kennwort“ und nicht „Ungültiger Benutzername“ und „Ungültiges Kennwort“ angezeigt werden.
Wenn Sie dies erkannt haben, erhalten Sie einen Punkt. Geben Sie sich außerdem einen Bonuspunkt, wenn Sie daran gedacht haben, dass Kennwörter in der Anwendung nicht in Nur-Text in der Datenbank gespeichert werden sollten. Stattdessen sollten frisierte Hashwerte der Kennwörter gespeichert und verglichen werden.
Ihre Ergebnisse
Punkte Kommentar
15+ Sie kriegen den Job.
11-14
Gar nicht so schlecht. Wenden Sie Ihr Talent jetzt auf den Code an, den Ihre Kollegen geschrieben haben.
7-10
Hmmm. OK. Sie haben eindeutig noch viel zu lernen, aber Sie sind wahrscheinlich besser als die meisten Anwendungsentwickler von heute.
4-6
Sie haben noch VIEL zu lernen. Gehen Sie in Ihren Lieblingsbuchladen, und besorgen Sie sich alle Bücher, die von den beiden Autoren dieses Artikels verfasst wurden.

0-3 Lassen Sie die Finger von Editor und Compiler, und keiner kommt zu Schaden.

Fehler Nr. 10 (Silverlight-CLR C#)
bool verifyCode(string discountCode) {
  // We store the hash of the secret code instead of 
  // the plaintext of the secret code.
  // Hash the incoming value and compare it against 
  // the stored hash.
  SHA1Managed hashFunction = new SHA1Managed();
  byte[] codeHash = 
    hashFunction.ComputeHash(
      System.Text.Encoding.Unicode.GetBytes(discountCode));
  byte[] secretCode = new byte[] { 
    116, 46, 130, 122, 36, 234, 158, 125, 163, 122,
    157, 186, 64, 142, 51, 153, 113, 79, 1, 42 };

  if (codeHash.Length != secretCode.Length) {
    // The hash lengths don't match, so the strings don't
    // match this should never happen, but we check anyway
    return false;
  }

  // perform an element-by-element comparison of the arrays
  for (int i = 0; i < codeHash.Length; i++) {
    if (codeHash[i] != secretCode[i])
      return false; // the hashes don't match
  }

  // all the elements match, so the strings match
  // the discount code is valid, inform the server
  WebServiceSoapClient client = new WebServiceSoapClient();
  client.ApplyDiscountCode();

  return true;
}
Antwort Der Entwickler hat die weise Entscheidung getroffen, den Geheimcode nicht in Nur-Text im Code einzubetten. Wenn Sie einfach nur testen müssen, ob ein Benutzer ein Geheimnis (z. B. einen Rabattcode oder ein Kennwort) kennt, ist es immer besser, einen Hashwert dieses Geheimnisses zu speichern und die Hashes zu vergleichen, anstatt Nur-Text zu speichern und Zeichenfolgen direkt zu vergleichen. Leider hat sich der Entwickler für den SHA-1-Hashalgorithmus entschieden, der ernste Schwachstellen aufweist und nachträglich vom SDL verboten wurde. Eine viel bessere Auswahl wäre die SHA256Managed-Klasse gewesen, die den vom SDL zugelassenen und empfohlenen SHA-256-Hashalgorithmus implementiert. Wenn Sie dies erkannt haben, erhalten Sie einen Punkt.
Schlimmer noch als die Auswahl von SHA-1 gegenüber SHA-256 ist die Tatsache, dass der Entwickler vernachlässigt hat, den Hashwert zu frisieren. Unfrisierte Hashes sind viel anfälliger für Angriffe über Hashtabellen (oft auch Regenbogentabellen genannt). Es würde einen Angreifer wahrscheinlich nur wenig Zeit kosten, aus dem unfrisierten Hashwert den ursprünglichen Nur-Text-Geheimcode zu bestimmen. (Die Autoren warten darauf, im SDL-Blog die erste Person beglückwünschen zu können, die uns den Nur-Text-Geheimcode mitteilt.) Geben Sie sich einen Punkt, wenn Sie den unfrisierten Hashwert erkannt haben.
Das größte Problem mit diesem Code ist jedoch die Tatsache, dass er überhaupt auf dem Clientcomputer ausgeführt wird! Denken Sie an die Angabe oben, dass es sich um einen Silverlight-CLR-Code handelt, der im Browser des Benutzers ausgeführt wird. Jeder auf dem Client ausgeführte Code kann von einem Angreifer geändert werden. Nichts hält einen entschlossenen Benutzer davon ab, einen Debugger an die Browserinstanz anzuhängen, die den Silverlight-Code ausführt, und den ausgeführten Code zu durchlaufen.
Ein Angreifer könnte beispielsweise die codeHash-Variable so festlegen, dass sie dem secretCode-Hash entspricht, und damit würde die Vergleichslogik immer gelingen. Weiterhin könnte die Überprüfungslogik vollständig ausgelassen und mit der aktuellen Anweisung einfach zum Webdienstaufruf gesprungen werden, der den Rabattcode anwendet. Die einfachste Möglichkeit wäre, den Debugger vollständig zu vermeiden und direkt die Webdienstmethode „ApplyDiscountCode“ aufzurufen.
Halten Sie sich Folgendes immer vor Augen: Auch wenn Sie, genau wie bei ASP.NET-Web Forms, vielleicht C# oder Visual Basic zum Erstellen von Silverlight-Anwendungen verwenden, wird der Silverlight-Code auf dem Computer des Clients ausgeführt, und der Web Forms-Code auf dem Server. Auf dem Client ausgeführter Code kann angezeigt und durch einen Angreifer geändert werden. Betten Sie Geheimnisse niemals in clientseitigen Code ein, oder ermöglichen Sie clientseitigem Code niemals, privilegierte Entscheidungen zu treffen (z. B. ob ein Rabattcode gültig ist oder ob ein Benutzer berechtigt ist, eine bestimmte Aktion durchzuführen). Wenn Sie diesen Fehler entdeckt haben, geben Sie sich einen Punkt.

Communityinhalt   Was ist Community Content?
Neuen Inhalt hinzufügen RSS  Anmerkungen
Processing
Page view tracker