Ici, nous allons discuter de la manière dont les solutions SAST détectent les failles de sécurité. Je vais vous parler d’approches différentes et complémentaires pour détecter les vulnérabilités potentielles, vous expliquer pourquoi chacune d’entre elles est nécessaire et comment transformer la théorie en pratique.
SAST (Static Application Security Testing) est utilisé pour trouver des défauts de sécurité sans exécuter une application. Alors que l’analyse statique « traditionnelle » est le moyen de détecter les erreurs, SAST se concentre sur la détection des vulnérabilités potentielles.
À quoi ressemble le SAST pour nous ? Nous prenons des sources, les donnons à l’analyseur et obtenons un rapport avec une liste des défauts de sécurité possibles.
Ainsi, le but principal de cet article est de répondre à la question de savoir comment exactement les outils SAST recherchent les vulnérabilités potentielles.
Types d’informations utilisées
Les solutions SAST n’analysent pas le code source dans une simple représentation textuelle : c’est peu pratique, inefficace et souvent insuffisant. Par conséquent, les analyseurs travaillent avec des représentations de code intermédiaires et plusieurs types d’informations. S’ils sont combinés, ils fournissent la représentation la plus complète d’une application.
Informations sur la syntaxe
Les analyseurs fonctionnent avec une représentation de code intermédiaire. Les plus courants sont les arbres de syntaxe (arbre de syntaxe abstraite ou arbre d’analyse).
Jetons un coup d’œil au modèle d’erreur :
operand#1 <operator> operand#1
Le fait est que le même opérande est utilisé à gauche et à droite de l’opérateur. Un tel code peut contenir une erreur lorsqu’une opération de comparaison est utilisée, par exemple.
Cependant, le cas ci-dessus est particulier et il existe de nombreuses variantes :
- Un ou les deux opérandes peuvent être placés entre crochets ;
- L’opérateur peut être non seulement ‘==’, mais aussi ‘!=’, ‘||’, etc.
- Les opérandes peuvent être des accès à des éléments, des appels de fonction, etc., plutôt que des identificateurs.
Dans ce cas, il n’est tout simplement pas pratique d’analyser le code comme un texte. C’est là que les arbres de syntaxe peuvent aider.
Voyons l’expression : a == (a)
. Son arbre de syntaxe peut ressembler à ceci :
De tels arbres sont plus faciles à utiliser : il y a des informations sur la structure et il est facile d’extraire des opérandes et des opérateurs à partir d’expressions. Avez-vous besoin d’omettre les crochets? Aucun problème. Descendez simplement de l’arbre.
De cette façon, les arbres sont utilisés comme une représentation pratique et structurée du code. Cependant, les arbres de syntaxe seuls ne suffisent pas.
Informations sémantiques
Voici un exemple :
if (lhsVar == rhsVar)
{ .... }
Si lhsVar
et rhsVar
sont les variables de la double
type, le code peut avoir quelques problèmes. Par exemple, si les deux lhsVar
et rhsVar
sont précisément égaux à 0.5
cette comparaison est true
. Cependant, si une valeur est 0.5
et l’autre est 0.4999999999999
la vérification donne alors false
. La question se pose alors : à quel type de comportement le développeur s’attend-il ? S’il s’attend à ce que la différence se situe dans la marge d’erreur, la comparaison doit être réécrite.
Supposons que nous aimerions attraper de tels cas. Mais voici le problème : la même comparaison sera tout à fait correcte si les types de lhsVar
et rhsVar
sont entiers.
Imaginons : l’analyseur rencontre l’expression suivante lors de la vérification du code :
if (lhsVar == rhsVar)
{ .... }
La question est : devons-nous ou non émettre un avertissement dans ce cas ? Vous pouvez regarder l’arborescence et voir que les opérandes sont des identifiants et que l’opération d’infixe est une comparaison. Cependant, nous ne pouvons pas décider si ce cas est dangereux ou non car nous ne connaissons pas les types de lhsVar
et rhsVar
variables.
L’information sémantique vient à la rescousse dans ce cas. En utilisant la sémantique, vous pouvez obtenir les données du nœud de l’arbre :
- Quel type (en termes de langage de programmation) a l’expression de nœud correspondante ;
- Quelle entité est représentée par le nœud : variable locale, paramètre, champ, etc. ;
Dans l’exemple ci-dessus, nous avons besoin d’informations sur les types de lhsVar
et rhsVar
variables. Tout ce que vous avez à faire est d’obtenir ces informations à l’aide d’un modèle sémantique. Si le type de variable est réel, émettez un avertissement.
Annotations de fonction
La syntaxe et la sémantique ne suffisent parfois pas. Regardez l’exemple:
IEnumerable<int> seq = null;
var list = Enumerable.ToList(seq);
....
Les ToList
la méthode est déclarée dans une bibliothèque externe ; l’analyseur n’a pas accès au code source. Il y a un seq
variables avec un nul valeur, qui est transmise au ToList
méthode. Est-ce une opération sûre ou non ?
Utilisons les informations de syntaxe. Vous pouvez déterminer où est le littéral, où est l’identifiant et où est l’appel de méthode. Mais est-ce que l’appel de la méthode est sûr ? Ce n’est pas clair.
Essayons la sémantique. Vous pouvez comprendre que seq
est une variable locale et même trouver sa valeur. Que pouvons-nous apprendre sur Enumerable.ToList
? Par exemple, le type de la valeur de retour et le type du paramètre. Est-il sécuritaire de passer null
à ça? Ce n’est pas clair.
Les annotations sont une solution possible. Les annotations sont un moyen de guider l’analyseur sur ce que fait la méthode, les contraintes qu’elle impose sur les valeurs d’entrée et de retour, etc.
Annotation pour le ToList
méthode dans le code de l’analyseur peut être la suivante :
Annotation("System.Collections.Generic",
nameof(Enumerable),
nameof(Enumerable.ToList),
AddReturn(ReturnFlags.NotNull),
AddArg(ArgFlags.NotNull));
Les principales informations que contient cette annotation :
- le nom complet de la méthode (y compris le nom du type et l’espace de noms). En cas de surcharges, des informations supplémentaires sur les paramètres peuvent être nécessaires ;
- restrictions sur la valeur de retour.
ReturnFlags.NotNull
signale que la valeur renvoyée ne sera pasnull
; - restrictions sur les valeurs d’entrée.
ArgFlags.NotNull
spécifie à l’analyseur que le seul argument de la méthode ne doit pas avoir denull
évaluer.
Reprenons l’exemple initial :
IEnumerable<int> seq = null;
var list = Enumerable.ToList(seq);
....
A l’aide du mécanisme d’annotation, l’analyseur reconnaît les limites du ToList
méthode. Si l’analyseur suit la valeur de la seq
variable, il pourra émettre un avertissement sur une exception de la NullReferenceException
taper.
Types d’analyse
Nous avons maintenant un aperçu des informations utilisées pour l’analyse. Parlons donc des types d’analyse.
Analyse basée sur des modèles
Parfois, ces erreurs « régulières » sont en fait des failles de sécurité. Regardez cet exemple de vulnérabilité.
iOS : CVE-2014-1266
Informations sur la vulnérabilité :
- ID CVE : CVE-2014-1266
- CWE-ID : CWE-20 : validation d’entrée incorrecte
- L’entrée NVD
- La description: La fonction SSLVerifySignedServerKeyExchange dans libsecurity_ssl/lib/sslKeyExchange.c dans la fonction Secure Transport du composant Data Security dans Apple iOS 6.x avant 6.1.6 et 7.x avant 7.0.6, Apple TV 6.x avant 6.0.2, et Apple OS X 10.9.x antérieur à 10.9.2 ne vérifie pas la signature dans un message d’échange de clé de serveur TLS, ce qui permet aux attaquants de l’intermédiaire d’usurper les serveurs SSL en (1) utilisant une clé privée arbitraire pour la signature étape ou (2) en omettant l’étape de signature.
Code:
....
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail;
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
goto fail;
....
A première vue, il peut sembler que tout va bien. En fait, la deuxième goto
est inconditionnel. C’est pourquoi le chèque auprès du SSLHashSHA1.final
l’appel de méthode n’a jamais été effectué.
Le code doit être formaté de cette façon :
....
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail;
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
goto fail;
....
Comment un analyseur statique peut-il détecter ce genre de défaut ?
La première façon est de voir que goto
est inconditionnel et est suivi d’expressions sans aucune étiquette.
Prenons le code simplifié ayant le même sens :
{
if (condition)
goto fail;
goto fail;
....
}
Son arbre de syntaxe peut ressembler à ceci :
Block
est un ensemble d’énoncés. Il ressort également de l’arbre de syntaxe que :
- La première
goto
déclaration concerne laif
déclaration, tandis que la seconde se rapporte directement au bloc ; - Les
ExpressionStatement
la déclaration est entreGotoStatement
etLabeledStatement
; goto
relatif au bloc est exécuté sans condition, mais il n’y a pas d’étiquette devant leExpressionStatement
. Cela signifie queExpressionStatement
est inaccessible dans ce cas.
Bien sûr, il s’agit d’un cas particulier d’heuristique. En pratique, ce genre de problème est mieux résolu par des mécanismes plus généraux de calcul de l’accessibilité du code.
Une autre façon d’attraper le défaut est de vérifier si le formatage du code correspond à la logique d’exécution.
Un algorithme simplifié serait le suivant :
- Examinez l’indentation avant le
if
déclarationthen
bifurquer. - Prenez la déclaration suivante après
if
. - Si une déclaration est sur la ligne suivante après
then
branche et ils ont la même indentation – émettre un avertissement.
Les algorithmes sont simplifiés pour plus de clarté et n’incluent pas les cas extrêmes. Les règles de diagnostic sont généralement plus compliquées et contiennent plus d’exceptions pour les cas où aucun avertissement n’est nécessaire.
Analyse des flux de données
Voici un exemple :
if (ptr || ptr->foo())
{ .... }
Les développeurs ont chamboulé la logique du code en mélangeant les opérateurs ‘&&’ et ‘||’. Donc si ptr
est un pointeur nul ; c’est déréférencé.
Dans ce cas, le contexte est local et il est possible de trouver une erreur par analyse basée sur des modèles. Les problèmes surgissent lorsque le contexte se répand. Par example:
if (ptr)
{ .... }
// 50 lines of code
....
auto test = ptr->foo();
Ici le ptr
le pointeur est vérifié NULL
puis déréférencé sans vérification ; ça a l’air suspect.
Note: j’utilise NULL
dans le texte pour indiquer la valeur du pointeur null, pas comme une macro C.
Il serait difficile d’attraper un cas similaire dans les modèles. Il est nécessaire d’émettre un avertissement pour l’exemple de code ci-dessus mais pas pour le fragment de code ci-dessous car ptr
n’est pas nul…