Comment écrire des règles pour Checkmarx sans devenir fou

Salut, Habr !

Dans notre travail, notre entreprise est souvent confrontée à divers outils d'analyse statique de code (SAST). Par défaut, ils fonctionnent tous de manière moyenne. Bien sûr, tout dépend du projet et des technologies utilisées, ainsi que de la manière dont ces technologies sont couvertes par les règles d'analyse. À mon avis, l'un des critères les plus importants lors du choix d'un outil SAST est la possibilité de le configurer en fonction des spécificités de vos applications, c'est-à-dire de rédiger et de modifier les règles d'analyse ou, comme on les appelle souvent, des requêtes personnalisées.

Comment écrire des règles pour Checkmarx sans devenir fou

Nous utilisons le plus souvent Checkmarx, un analyseur de code très intéressant et puissant. Dans cet article, je vais partager mon expérience de rédaction de règles d'analyse pour cet outil.

Table des matières

Introduction

Pour commencer, je voudrais recommander l'un des rares articles en langue russe sur les particularités de la rédaction de requêtes pour Checkmarx. Il a été publié sur Habr à la fin de 2019 sous le titre : « Bonjour, Checkmarx ! ». Comment écrire une requête pour Checkmarx SAST et trouver des vulnérabilités intéressantes.

L'article examine en détail comment rédiger les premières requêtes en CxQL (Checkmarx Query Language) pour une application de test donnée et montre les principes fondamentaux de fonctionnement des règles d'analyse.

Je ne vais pas répéter ce qui y est décrit, bien que certaines recoupements soient inévitables. Dans mon article, j'essaierai de composer un "recueil de recettes", une liste de solutions à des problèmes concrets auxquels j'ai été confronté au cours de mon travail avec Checkmarx. Pour beaucoup de ces problèmes, j'ai dû me creuser la tête. Parfois, il manquait des informations dans la documentation, et parfois il était difficile de comprendre comment faire ce qui était requis. J'espère que mon expérience et mes nuits blanches ne seront pas vaines, et que ce "recueil de recettes de requêtes personnalisées" vous fera économiser quelques heures ou quelques neurones. Alors, commençons !

Informations générales sur les règles

Commençons par examiner quelques concepts de base et le processus de travail suivant les règles, afin de mieux comprendre ce qui va suivre. Et aussi parce que la documentation à ce sujet n'est pas claire ou est trop diluée dans la structure, ce qui n'est pas très pratique.

  1. Les règles s'appliquent lors du scan en fonction du preset choisi au démarrage (un ensemble de règles actives). Il est possible de créer un nombre illimité de presets, et la façon dont vous les structurez dépend des caractéristiques de votre processus. Vous pouvez les regrouper par langue ou créer des presets pour chaque projet. Le nombre de règles actives influence la vitesse et la précision du scan.

    Comment écrire des règles pour Checkmarx sans devenir fouConfiguration du Preset dans l'interface Checkmarx

  2. Les règles sont modifiées dans un outil spécial appelé CxAuditor. C'est une application de bureau qui se connecte au serveur Checkmarx. Cet outil a deux modes de fonctionnement : modification des règles et analyse des résultats des scans déjà réalisés.

    Comment écrire des règles pour Checkmarx sans devenir fouInterface CxAudit

  3. Les règles dans Checkmarx sont classées par langue, c'est-à-dire qu'il existe un ensemble spécifique de requêtes pour chaque langue. Il y a aussi certaines règles communes qui s'appliquent indépendamment de la langue, ce qu'on appelle les requêtes de base. Dans la plupart des cas, les requêtes de base contiennent des recherches d'informations utilisées par d'autres règles.

    Comment écrire des règles pour Checkmarx sans devenir fouSéparation des règles par langues

  4. Les règles peuvent être “Executable” et “Non-Executable” (Exécutables et Non-exécutables). Ce n'est pas tout à fait un terme correct, à mon avis, mais c'est ce qu'il y a. L'idée est que le résultat de l'exécution des règles “Executable” apparaîtra dans les résultats du scan dans l'interface utilisateur, tandis que les règles “Non-Executable” ne sont nécessaires que pour utiliser leurs résultats dans d'autres requêtes (essentiellement — juste une fonction).

    Comment écrire des règles pour Checkmarx sans devenir fouDétermination du type de règle lors de la création

  5. Il est possible de créer de nouvelles règles ou de modifier/réécrire les existantes. Pour réécrire une règle, il faut la trouver dans l'arborescence, cliquer avec le bouton droit de la souris et sélectionner “Override” dans le menu déroulant. Il est important de garder à l'esprit que les nouvelles règles ne sont pas activées dans les presets par défaut et ne sont pas actives. Pour commencer à les utiliser, il faut les activer dans le menu “Preset Manager” dans l'outil. Les règles réécrites conservent leurs paramètres, c'est-à-dire que si une règle était active, elle le restera et sera appliquée immédiatement.

    Comment écrire des règles pour Checkmarx sans devenir fouExemple d'une nouvelle règle dans l'interface du Gestionnaire de Préréglages

  6. Lors de l'exécution, un "arbre" de requêtes est construit, dépendant les unes des autres. Les règles qui collectent des informations sont exécutées en premier, suivies de celles qui les utilisent. Le résultat de l'exécution est mis en cache, donc si il est possible d'utiliser les résultats d'une règle existante, il vaut mieux le faire, ce qui réduira le temps de scan.

  7. Les règles peuvent être appliquées à différents niveaux :

  • Au niveau du système — utilisé pour tout scan de tout projet

  • Au niveau de l'équipe (Team) — utilisé uniquement pour le scan des projets de l'équipe sélectionnée.

  • Au niveau du projet — utilisé dans un projet spécifique

    Comment écrire des règles pour Checkmarx sans devenir fouDéfinition du niveau auquel la règle sera appliquée

Un "dictionnaire" pour les débutants

Je vais commencer par quelques éléments qui m'ont posé des questions, ainsi que montrer une série d'astuces qui simplifieront considérablement la vie.

Opérations sur les listes

- soustraction d'un à l'autre (list2 - list1)
* intersection des listes (list1 * list2)
+ addition des listes (list1 + list2)

& (ET logique) - combine les listes par correspondance (list1 & list2), de manière similaire à l'intersection (list1 * list2)
| (OU logique) - combine les listes par recherche large (list1 | list2)

Ne fonctionne pas avec les listes :  ^  &&  ||  %  / 

Tous les éléments trouvés

Dans le langage scanné, il est possible d'obtenir la liste de tous les éléments définis par Checkmarx (lignes, fonctions, classes, méthodes, etc.). C'est un certain espace d'objets auquel on peut accéder via Tous. Donc, pour rechercher un objet avec un nom spécifique searchMe, vous pouvez effectuer une recherche par nom dans tous les objets trouvés :

// Такой запрос выдаст все элементы
result = All;

// Такой запрос выдаст все элементы, в имени которых присутствует “searchMe“
result = All.FindByName("searchMe");

Cependant, s'il est nécessaire de rechercher dans un autre langage qui, pour une raison quelconque, n'a pas été inclus dans le scan (par exemple groovy dans un projet pour Android), vous pouvez étendre notre espace d'objets via une variable :

result = AllMembers.All.FindByName("searchMe");

Fonctions pour l'analyse Flow

Ces fonctions sont utilisées dans de nombreuses règles et voici un petit rappel de leur signification :

// Какие данные second влияют на first.
// Другими словами - ТО (second) что влияет на  МЕНЯ (first).
result = first.DataInfluencedBy(second);

// Какие данные first влияют на second.
// Другими словами - Я (first) влияю на ТО (second).
result = first.DataInfluencingOn(second);

Obtention du nom/chemin du fichier

Il y a plusieurs attributs que l'on peut obtenir des résultats d'exécution de la requête (nom du fichier dans lequel l'occurrence a été trouvée, ligne, etc.), mais comment les obtenir et les utiliser n'est pas mentionné dans la documentation. Pour ce faire, vous devez accéder à la propriété LinePragma et à l'intérieur se trouveront les objets requis :

// Для примера найдем все методы
CxList methods = Find_Methods();

// В методах найдем по имени метод scope
CxList scope = methods.FindByName("scope");

// Таким образом можо получить путь к файлу
string current_filename = scope.GetFirstGraph().LinePragma.FileName;

// А вот таким - строку, где нашлось срабатывание
int current_line = scope.GetFirstGraph().LinePragma.Line;

// Эти параметры можно использовать по разному
// Например получить все объекты в файле
CxList inFile = All.FindByFileName(current_filename);

// Или найти что происходит в конкретной строке
CxList inLine = inFile.FindByPosition(current_line);

À noter que FileName contient en réalité le chemin vers le fichier, car nous avons utilisé la méthode GetFirstGraph.

Résultat de l'exécution

À l'intérieur de CxQL, il existe une variable spéciale result, qui retourne le résultat de l'exécution de votre règle écrite. Elle est initialisée immédiatement et peut recevoir des résultats intermédiaires, en les modifiant et les précisant au cours du travail. Mais, si à l'intérieur de la règle il n'y a pas d'attribution à cette variable ou à cette fonction retourner— le résultat de l'exécution sera toujours nul.

La requête suivante ne nous renverra rien lors de son exécution et sera toujours vide :

// Находим элементы foo
CxList libraries = All.FindByName("foo");

Mais, en attribuant le résultat de l'exécution à la variable magique result — nous verrons ce que cet appel renvoie :

// Находим элементы foo
CxList libraries = All.FindByName("foo");

// Выводим, как результат выполнения правила
result = libraries

// Или еще короче
result = All.FindByName("foo");

Utilisation des résultats de l'exécution d'autres règles

Les règles dans Checkmarx peuvent être considérées comme l'équivalent des fonctions dans un langage de programmation ordinaire. Lors de l'écriture d'une règle, vous pouvez tout à fait utiliser les résultats d'autres requêtes. Par exemple, il n'est pas nécessaire de rechercher chaque fois tous les appels de méthodes dans le code, il suffit d'appeler la règle nécessaire :

// Получаем результат выполнения другого правила
CxList methods = Find_Methods();

// Ищем внутри метод foo. 
// Второй параметр false означает, что ищем без чувствительности к регистру
result = methods.FindByShortName("foo", false);

Cette approche permet de réduire le code et de diminuer considérablement le temps d'exécution de la règle.

Résolution des problèmes

Journalisation

Lors de l'utilisation de l'outil, il arrive parfois qu'il soit impossible d'écrire immédiatement la requête nécessaire et il faut expérimenter, en essayant différentes options. Pour ce cas, l'outil dispose d'une journalisation, qui s'appelle comme suit :

// Находим что-то
CxList toLog = All.FindByShortName("log");

// Формируем строку и отправляем в лог
cxLog.WriteDebugMessage (“number of DOM elements =” + All.Count);

Mais il faut garder à l'esprit que cette méthode n'accepte que une chaîne, donc il ne sera pas possible d'afficher la liste complète des éléments trouvés lors de l'exécution de la première opération. La deuxième option, qui est utilisée pour le débogage, consiste à attribuer de temps en temps à la variable magique result le résultat de l'exécution de la requête et à voir ce que cela donne. Cette approche n'est pas très pratique, il faut être sûr qu'il n'y ait pas de redéfinition ou d'opérations avec cela dans le code suivant result ou tout simplement commenter le code situé plus bas. Ou on peut comme moi, oublier d'enlever plusieurs de ces appels de la règle prête et se demander pourquoi rien ne fonctionne.

Une méthode plus pratique consiste à appeler la méthode retourner avec le bon paramètre. Dans ce cas, l'exécution de la règle se terminera et nous pourrons voir ce que nous avons obtenu en résultat de ce que nous avons écrit :

// Находим что-то
CxList toLog = All.FindByShortName("log");

// Выводим результат выполнения
return toLog

//Все, что написано дальше не будет выполнено
result = All.DataInfluencedBy(toLog)

Problème de connexion

Il y a des situations où il est impossible d'accéder à l'outil CxAudit (utilisé pour écrire des règles). Les raisons peuvent être nombreuses, comme un arrêt d'urgence, une mise à jour inattendue de Windows, un BSOD et d'autres situations imprévues échappant à notre contrôle. Dans ce cas, il arrive parfois qu'une session inachevée demeure dans la base de données, ce qui empêche d'accéder à nouveau. Pour corriger cela, il est nécessaire d'exécuter quelques requêtes :

Pour Checkmarx avant 8.6 :

// Проверяем, что есть залогиненые пользователи, выполнив запрос в БД
SELECT COUNT(*) FROM [CxDB].[dbo].LoggedinUser WHERE [ClientType] = 6;
 
// Если что-то есть, а на самом деле даже если и нет, попробовать выполнить запрос
DELETE FROM [CxDB].[dbo].LoggedinUser WHERE [ClientType] = 6;

Pour Checkmarx après 8.6 :

// Проверяем, что есть залогиненые пользователи, выполнив запрос в БД
SELECT COUNT(*) FROM LoggedinUser WHERE (ClientType = 'Audit');
 
// Если что-то есть, а на самом деле даже если и нет, попробовать выполнить запрос
DELETE FROM [CxDB].[dbo].LoggedinUser WHERE (ClientType = 'Audit');

Rédaction de règles

Nous voilà arrivés à la partie la plus intéressante. Lorsque vous commencez à écrire des règles en CxQL, il manque souvent non seulement de la documentation, mais aussi des exemples concrets de solutions à certaines tâches et une explication du processus de fonctionnement des requêtes dans leur ensemble.

Je vais essayer de simplifier un peu la vie de ceux qui commencent à s'immerger dans le langage des requêtes et fournir quelques exemples d'utilisation des Custom Queries pour résoudre certaines tâches. Certains d'entre eux sont assez généraux et peuvent être appliqués dans votre entreprise presque sans modifications, d'autres sont plus spécifiques, mais peuvent également être utilisés en adaptant le code aux spécificités de vos applications.

Voici donc les problèmes que nous avons le plus souvent rencontrés :

Problème : Dans les résultats de l'exécution de la règle, plusieurs Flow et l'un d'eux est l'imbrication d'un autre, il faut en garder un.

Solution : En effet, il arrive parfois que Checkmarx montre plusieurs Flow de mouvement de données, qui peuvent se chevaucher et être une version abrégée des autres. Pour de tels cas, il existe une méthode spéciale. ReduceFlow. Selon le paramètre, il choisira le Flow le plus court ou le plus long :

// Оставить только длинные Flow
result = result.ReduceFlow(CxList.ReduceFlowType.ReduceSmallFlow);

// Оставить только короткие Flow
result = result.ReduceFlow(CxList.ReduceFlowType.ReduceBigFlow);

Problème : Étendre la liste des données sensibles auxquelles l'outil réagit.

Solution : Dans Checkmarx, il existe des règles de base dont les résultats sont utilisés par de nombreuses autres requêtes. En complétant certaines de ces règles avec des données spécifiques à votre application, vous pouvez immédiatement améliorer les résultats de la numérisation. Voici un exemple de règle pour commencer :

General_privacy_violation_list

Ajoutons quelques variables utilisées dans notre application pour stocker des informations sensibles :

// Получаем результат выполнения базового правила
result = base.General_privacy_violation_list();

// Ищем элементы, которые попадают под простые регулярные выражения. Можно дополнить характерными для вас паттернами.
CxList personalList = All.FindByShortNames(new List<string> {
	"*securityToken*", "*sessionId*"}, false);

// Добавляем к конечному результату
result.Add(personalList);

Problème : Étendre la liste des variables contenant des mots de passe.

Solution : Je vous recommande de prêter attention à la règle de base concernant la définition des mots de passe dans le code et d'y ajouter une liste des noms de variables qui sont couramment utilisés dans votre entreprise.

Liste_de_violation_de_la_vérité_du_mot_de_passe

CxList allStrings = All.FindByType("String"); 
allStrings.Add(All.FindByType(typeof(StringLiteral))); 
allStrings.Add(Find_UnknownReference());
allStrings.Add(All.FindByType(typeof (Declarator)));
allStrings.Add(All.FindByType(typeof (MemberAccess)));
allStrings.Add(All.FindByType(typeof(EnumMemberDecl))); 
allStrings.Add(Find_Methods().FindByShortName("get*"));

// Compléter la liste par défaut des variables
List  pswdIncludeList = new List{"*password*", "*psw", "psw*", "pwd*", "*pwd", "*authKey*", "pass*", "cipher*", "*cipher", "pass", "adgangskode", "benutzerkennwort", "chiffre", "clave", "codewort", "contrasena", "contrasenya", "geheimcode", "geslo", "heslo", "jelszo", "kennwort", "losenord", "losung", "losungswort", "lozinka", "modpas", "motdepasse", "parol", "parola", "parole", "pasahitza", "pasfhocal", "passe", "passord", "passwort", "pasvorto", "paswoord", "salasana", "schluessel", "schluesselwort", "senha", "sifre", "wachtwoord", "wagwoord", "watchword", "zugangswort", "PAROLACHIAVE", "PAROLA CHIAVE", "PAROLECHIAVI", "PAROLE CHIAVI", "paroladordine", "verschluesselt", "sisma",
                "pincode",
								"pin"};
								
List  pswdExcludeList = new List{"*pass", "*passable*", "*passage*", "*passenger*", "*passer*", "*passing*", "*passion*", "*passive*", "*passover*", "*passport*", "*passed*", "*compass*", "*bypass*", "pass-through", "passthru", "passthrough", "passbytes", "passcount", "passratio"};

CxList tempResult = allStrings.FindByShortNames(pswdIncludeList, false);
CxList toRemove = tempResult.FindByShortNames(pswdExcludeList, false);
tempResult -= toRemove;
tempResult.Add(allStrings.FindByShortName("pass", false));

foreach (CxList r in tempResult)
{
	CSharpGraph g = r.data.GetByIndex(0) as CSharpGraph;
	if(g != null && g.ShortName != null && g.ShortName.Length < 50)
	{
		result.Add(r);
	}
}

Problème : Ajoutez les frameworks utilisés qui ne sont pas pris en charge par Checkmarx.

Solution : Toutes les requêtes dans Checkmarx sont classées par langues, il est donc nécessaire de compléter les règles pour chaque langue. Voici quelques exemples de telles règles.

Si des bibliothèques sont utilisées pour compléter ou remplacer la fonctionnalité standard, il est facile de les ajouter à la règle de base. Ainsi, tous ceux qui l'utilisent seront immédiatement informés des nouvelles informations. Par exemple, les bibliothèques de logging pour Android — Timber et Loggi. Dans l'ensemble des règles de détermination des appels non systémiques, il n'y a pas de vérification, donc si un mot de passe ou un identifiant de session est enregistré dans le journal, nous ne le saurons pas. Essayons d'ajouter à la règle Checkmarx la détection de tels méthodes.

Exemple de code testant qui utilise la bibliothèque Timber pour le logging :

package com.death.timberdemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

import timber.log.Timber;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Timber.e("Error Message");
        Timber.d("Debug Message");

        Timber.tag("Some Different tag").e("And error message");
    }
}

Voici un exemple de requête pour Checkmarx qui permettra d'ajouter la définition des appels de méthodes Timber comme point de sortie des données de l'application :

FindAndroidOutputs

// Получаем результат выполнения базового правила
result = base.Find_Android_Outputs();

// Дополняем вызовами, которые приходят из библиотеки Timber
CxList timber = All.FindByExactMemberAccess("Timber.*") +
    All.FindByShortName("Timber").GetMembersOfTarget();

// Добавляем к конечному результату
result.Add(timber);

On peut également compléter la règle adjacente, mais celle-ci sera directement liée à la journalisation dans Android :

FindAndroidLog_Outputs

// Получаем результат выполнения базового правила
result = base.Find_Android_Log_Outputs();

// Дополняем вызовами, которые приходят из библиотеки Timber
result.Add(
  All.FindByExactMemberAccess("Timber.*") +
  All.FindByShortName("Timber").GetMembersOfTarget()
);

De plus, si des applications Android utilisent WorkManager pour le travail asynchrone, il est avantageux d'en informer Checkmarx en ajoutant la méthode pour obtenir des données de la tâche getInputData:

FindAndroidRead

// Получаем результат выполнения базового правила
result = base.Find_Android_Read();

// Дополняем вызовом функции getInputData, которая используется в WorkManager
CxList getInputData = All.FindByShortName("getInputData");

// Добавляем к конечному результату
result.Add(getInputData.GetMembersOfTarget());

Problème : Recherche de données sensibles dans plist pour des projets iOS

Solution : Souvent, des fichiers avec l'extension .plist sont utilisés pour stocker différentes variables et valeurs dans iOS. Il est déconseillé de stocker des mots de passe, des jetons, des clés et d'autres données sensibles dans ces fichiers, car ils peuvent être extraits facilement de l'appareil.

Les fichiers plist ont des particularités qui ne sont pas évidentes à première vue, mais qui sont importantes pour Checkmarx. Rédigeons une règle qui recherchera les données nécessaires et nous informera si jamais des mots de passe ou des jetons sont mentionnés.

Exemple d'un tel fichier dans lequel un jeton pour interagir avec le service backend est intégré :

DeviceDictionary
	
		phone
		iPhone 6s
	
	privatekey
	MIICXAIBAAKBgQCqGKukO1De7zhZj6+

Et une règle pour Checkmarx, dans laquelle quelques nuances doivent être prises en compte lors de la rédaction :

// Используем результат выполнения правила по поиску файлов plist, чтобы уменьшить время работы правила и 
CxList plist = Find_Plist_Elements();

// Инициализируем новую переменную
CxList dictionarySettings = All.NewCxList();

// Теперь добавим поиск всех интересующих нас значений. В дальнейшем можно расширять этот список.
// Для поиска значений, как ни странно, используется FindByMemberAccess - поиск обращений к методам. Второй параметр внутри функции, false, означает, что поиск нечувствителен к регистру
dictionarySettings.Add(plist.FindByMemberAccess("privatekey", false));
dictionarySettings.Add(plist.FindByMemberAccess("privatetoken", false));

// Для корректного поиска из-за особенностей структуры plist - нужно искать по типу "If statement"
CxList ifStatements = plist.FindByType(typeof(IfStmt));

// Добавляем в результат, перед этим получив родительский узел - для правильного отображения
result = dictionarySettings.FindByFathers(ifStatements);

Problème : Recherche d'informations dans XML

Solution : Checkmarx offre des fonctionnalités très pratiques pour travailler avec XML et rechercher des valeurs, des balises, des attributs, etc. Malheureusement, une erreur s'est glissée dans la documentation, rendant aucun exemple fonctionnel. Bien que ce problème ait été corrigé dans la dernière version de la documentation, soyez prudent si vous utilisez des versions antérieures des documents.

Voici un exemple incorrect tiré de la documentation :

// Код работать не будет
result = All.FindXmlAttributesByNameAndValue("*.app", 8, “id”, "error- section", false, true);

Suite à une tentative d'exécution, nous obtiendrons une erreur indiquant que Tous il n'y a pas de telle méthode… Et c'est vrai, car pour utiliser des fonctions pour travailler avec XML, il existe un espace d'objets spécial et séparé — cxXPath. Voici à quoi ressemble une requête correcte pour trouver le paramètre dans Android qui autorise l'utilisation du trafic HTTP :

// Правильный вариант с использованием cxXPath
result = cxXPath.FindXmlAttributesByNameAndValue("*.xml", 8, "cleartextTrafficPermitted", "true", false, true);

Examinons cela de plus près, car la syntaxe de toutes les fonctions est similaire, une fois que l'on a compris l'une, il suffit de choisir celle qu'il faut. Ainsi, procédons par ordre des paramètres :

  • "*.xml"— le masque de fichiers sur lequel il faut effectuer la recherche

  • 8 — l'ID de la langue pour laquelle la règle est appliquée

  • "cleartextTrafficPermitted"— le nom de l'attribut dans xml

  • "true" — la valeur de cet attribut

  • faux — utilisation d'une expression régulière lors de la recherche

  • true — signifie que la recherche sera effectuée sans tenir compte de la casse, c'est-à-dire de manière insensible à la casse

À titre d'exemple, une règle a été utilisée qui définit des paramètres de connexion réseau non sécurisés selon Android, permettant la communication avec le serveur via le protocole HTTP. Exemple de paramètre contenant l'attribut cleartextTrafficPermitted avec la valeur true:

example.com
        
            
        
        
            secure.example.com

Problème : Limiter les résultats par nom/chemin de fichier

Solution : Dans l'un des grands projets liés au développement d'une application mobile pour Android, nous avons été confrontés à des déclenchements erronés de la règle qui définit le paramètre d'obfuscation. Le fait est que la règle par défaut recherche dans le fichier build.gradle le paramètre qui applique les règles d'obfuscation pour la version de production de l'application.

Mais dans de grands projets, il existe parfois des fichiers enfants build.gradle, qui se rapportent à des bibliothèques incluses dans le projet. La spécificité est que même si ces fichiers ne précisent pas la nécessité d'obfuscation, lors de la compilation, les paramètres du fichier parent de construction seront appliqués.

Ainsi, l'objectif est de filtrer les déclenchements dans les fichiers enfants qui se rapportent aux bibliothèques. Il est possible de les identifier par la présence de la ligne apply 'com.android.library'.

Exemple de code extrait du fichier build.gradle, déterminant la nécessité d'obfuscation :

appliquer le plugin : 'com.android.application'

android {
    compileSdkVersion 24
    buildToolsVersion "24.0.2"
    defaultConfig {
        ...
    }

    buildTypes {
        release {
            minifyEnabled true
            ...
        }
    }
}

dependencies {
  ...
}

Exemple de fichier build.gradle pour une bibliothèque incluse dans le projet et n'ayant pas ce paramètre :

appliquer le plugin : 'android-library'

dependencies {
  compile 'com.android.support:support-v4:18.0.+'
}

android {
  compileSdkVersion 14
  buildToolsVersion '17.0.0'
  ...
}

Et la règle pour Checkmarx :

ProGuardObfuscationNotInUse

// Поиск метода release среди всех методов в Gradle файлах
CxList releaseMethod = Find_Gradle_Method("release");

// Все объекты из файлов build.gradle
CxList gradleBuildObjects = Find_Gradle_Build_Objects();

// Поиск того, что находится внутри метода "release" среди всех объектов из файлов build.gradle
CxList methodInvokesUnderRelease = gradleBuildObjects.FindByType(typeof(MethodInvokeExpr)).GetByAncs(releaseMethod);

// Ищем внутри gradle-файлов строку "com.android.library" - это значит, что данный файл относится к библиотеке и его необходимо исключить из правила
CxList android_library = gradleBuildObjects.FindByName("com.android.library");

// Инициализация пустого массива
List<string> libraries_path = new List<string> {};

// Проходим через все найденные "дочерние" файлы
foreach(CxList library in android_library)
{
    // Получаем путь к каждому файлу
	string file_name_library = library.GetFirstGraph().LinePragma.FileName;
    
    // Добавляем его в наш массив
	libraries_path.Add(file_name_library);
}

// Ищем все вызовы включения обфускации в релизных настройках
CxList minifyEnabled = methodInvokesUnderRelease.FindByShortName("minifyEnabled");

// Получаем параметры этих вызовов
CxList minifyValue = gradleBuildObjects.GetParameters(minifyEnabled, 0);

// Ищем среди них включенные
CxList minifyValueTrue = minifyValue.FindByShortName("true");

// Немного магии, если не нашли стандартным способом :D
if (minifyValueTrue.Count == 0) {
	minifyValue = minifyValue.FindByAbstractValue(abstractValue => abstractValue is TrueAbstractValue);
} else {
    // А если всё-таки нашли, то предыдущий результат и оставляем
	minifyValue = minifyValueTrue;	
}

// Если не нашлось таких методов
if (minifyValue.Count == 0)
{
    // Для более корректного отображения места срабатывания в файле ищем или buildTypes или android
	CxList tempResult = All.NewCxList();
	CxList buildTypes = Find_Gradle_Method("buildTypes");
	if (buildTypes.Count > 0) {
		tempResult = buildTypes;
	} else {
		tempResult = Find_Gradle_Method("android");
	}
	
	// Для каждого из найденных мест срабатывания проходим и определяем, дочерний или основной файлы сборки
	foreach(CxList res in tempResult)
	{
        // Определяем, в каком файле был найден buildType или android методы
		string file_name_result = res.GetFirstGraph().LinePragma.FileName;
        
        // Если такого файла нет в нашем списке "дочерних" файлов - значит это основной файл и его можно добавить в результат
		if (libraries_path.Contains(file_name_result) == false){
			result.Add(res);
		}
	}
}

Cette approche peut être suffisamment universelle et utile non seulement pour les applications Android, mais aussi pour d'autres cas où il est nécessaire de déterminer l'appartenance d'un résultat à un fichier spécifique.

Problème : Ajouter le support d'une bibliothèque tierce si la syntaxe n'est pas complètement prise en charge

Solution : Le nombre de frameworks utilisés dans le processus de rédaction de code est simplement énorme. Bien sûr, Checkmarx ne connaît pas toujours leur existence et notre tâche est de l'apprendre à comprendre que certaines méthodes appartiennent vraiment à ce framework. Parfois, cela est compliqué par le fait que les frameworks utilisent des noms de fonctions qui sont très répandus, rendant impossible de déterminer de manière claire la relation d'un appel avec une bibliothèque spécifique.

La complexité réside dans le fait que la syntaxe de ces bibliothèques n'est pas toujours correctement reconnue, il faut donc expérimenter pour éviter une grande quantité de faux positifs. Plusieurs options existent pour améliorer la précision du scan et résoudre le problème :

  • La première option, nous savons avec certitude qu'une bibliothèque est utilisée dans un projet spécifique et nous pouvons appliquer la règle au niveau de l'équipe. Mais si l'équipe décide d'utiliser une autre approche ou utilise plusieurs bibliothèques avec des noms de fonctions qui se chevauchent, nous pouvons obtenir une image peu agréable avec de nombreux faux positifs.

  • La deuxième option consiste à rechercher dans les fichiers où la bibliothèque est explicitement importée. Avec cette approche, nous pouvons être certains que dans ce fichier, la bibliothèque dont nous avons besoin est effectivement utilisée.

  • Et la troisième option, c'est d'utiliser les deux approches précédentes conjointement.

Prenons comme exemple une bibliothèque connue dans certains cercles slick pour le langage de programmation Scala, à savoir, la fonctionnalité Splicing Literal Values. En général, pour passer des paramètres dans une requête SQL, il est nécessaire d'utiliser l'opérateur $, qui insère les données dans une requête SQL préalablement formulée. En fait, il s'agit d'un équivalent direct du Prepared Statement en Java. Cependant, lorsqu'il est nécessaire de construire dynamiquement une requête SQL, par exemple, si des noms de tables doivent être transmis, l'opérateur peut être utilisé #$, qui insérera directement les données dans la requête (pratiquement, comme la concaténation de chaînes).

Exemple de code :

// В общем случае - значения, контролируемые пользователем
val table = "coffees"
sql"select * from #$table where name = $name".as[Coffee].headOption

Checkmarx ne sait pas encore détecter l'utilisation de Splicing Literal Values et ignore les opérateurs #$, donc essayons de lui apprendre à identifier les potentielles injections SQL et à mettre en lumière les endroits nécessaires dans le code :

// Находим все импорты
CxList imports = All.FindByType(typeof(Import));

// Ищем по имени, есть ли в импортах slick
CxList slick = imports.FindByShortName("slick");

// Некоторый флаг, определяющий, что импорт библиотеки в коде присутствует
// Для более точного определения - можно применить подход с именем файла
bool not_empty_list = false;
foreach (CxList r in slick)
{
    // Если встретили импорт, считаем, что slick используется
	not_empty_list = true;
}

if (not_empty_list) {
    // Ищем вызовы, в которые передается SQL-строка
	CxList sql = All.FindByShortName("sql");
	sql.Add(All.FindByShortName("sqlu"));
	
	// Определяем данные, которые попадают в эти вызовы
	CxList data_sql = All.DataInfluencingOn(sql);
	
	// Так как синтакис не поддерживается, можно применить подход с регулярными выражениями
	// RegExp стоит использовать крайне осторожно и не применять его на большом количестве данных, так как это может сильно повлиять на производительность
	CxList find_possible_inj = data_sql.FindByRegex(@"#$", true, true, true);

    // Избавляемся от лишних срабатываний, если они есть и выводим в результат
	result = find_possible_inj.FindByType(typeof(BinaryExpr));
}

Problème : Recherche des fonctions vulnérables utilisées dans les bibliothèques Open-Source

Solution : De nombreuses entreprises utilisent des outils pour contrôler l'Open-Source (pratique OSA), permettant de détecter l'utilisation de versions vulnérables de bibliothèques dans les applications en développement. Parfois, il n'est pas possible de mettre à jour une telle bibliothèque vers une version sécurisée. Dans certains cas, il existe des limitations fonctionnelles, dans d'autres, il n'y a tout simplement pas de version sécurisée. Dans ce cas, une combinaison des pratiques SAST et OSA peut aider à déterminer que les fonctions exploitant la vulnérabilité ne sont pas utilisées dans le code.

Mais parfois, surtout si l'on considère JavaScript, cela peut ne pas être une tâche triviale. Voici une solution, peut-être pas idéale, mais fonctionnelle, en prenant comme exemple des vulnérabilités dans le composant lodash dans les méthodes template et *set.

Exemples de code potentiellement vulnérable dans un fichier JS :

/**
 * Template example
 */

'use strict';
var _ = require("./node_modules/lodash.js");


// Use the "interpolate" delimiter to create a compiled template.
var compiled = _.template('hello <%= js %>!');
console.log(compiled({ 'js': 'lodash' }));
// => 'hello lodash!'

// Use the internal `print` function in "evaluate" delimiters.

var compiled = _.template('<% print("hello " + js); %>!');
console.log(compiled({ 'js': 'lodash' }));
// => 'hello lodash!'

Et lors de l'inclusion directe dans le html :

<!DOCTYPE html>
<html>
<head>
    <title>Tutoriel Lodash</title>
    <script src="./node_modules/lodash.js"></script>
    <script type="text/javascript">
  // Lodash chunking array
        nums = [1, 2, 3, 4, 5, 6, 7, 8, 9];

        let c1 = _.template('<% print("hello " + js); %>!');
        console.log(c1);

        let c2 = _.template('<% print("hello " + js); %>!');
        console.log(c2);
    </script>
</head>
<body></body>
</html>

Nous recherchons toutes nos méthodes vulnérables, qui sont énumérées dans les vulnérabilités :

// Ищем все строки: в которых встречается строка lodash (предполагаем, что это объявление импорта библиотеки
CxList lodash_strings = Find_String_Literal().FindByShortName("*lodash*");

// Ищем все данные: которые взаимодействуют с этими строками
CxList data_on_lodash = All.InfluencedBy(lodash_strings);


// Задаем список уязвимых методов
List<string> vulnerable_methods = new List<string> {"template", "*set"};

// Ищем все наши уязвимые методы, которые перечисленны в уязвимостях и отфильтровываем их только там, где они вызывались
CxList vulnerableMethods = All.FindByShortNames(vulnerable_methods).FindByType(typeof(MethodInvokeExpr));

//Находим все данные: которые взаимодействуют с данными методами
CxList vulnFlow = All.InfluencedBy(vulnerableMethods);

// Если есть пересечение по этим данным - кладем в результат
result = vulnFlow * data_on_lodash;

// Формируем список путей по которым мы уже прошли, чтобы фильтровать в дальнейшем дубли
List<string> lodash_result_path = new List<string> {};

foreach(CxList lodash_result in result)
{
    // Очередной раз получаем пути к файлам
	string file_name = lodash_result.GetFirstGraph().LinePragma.FileName;
	lodash_result_path.Add(file_name);
}

// Дальше идет часть относящаяся к html файлам, так как в них мы не можем проследить откуда именно идет вызов
// Формируем массив путей файлов, чтобы быть уверенными, что срабатывания уязвимых методов были именно в тех файлах, в которых объявлен lodash
List<string> lodash_path = new List<string> {};
foreach(CxList string_lodash in lodash_strings)
{
	string file_name = string_lodash.GetFirstGraph().LinePragma.FileName;
	lodash_path.Add(file_name);
}

// Перебираем все уязвимые методы и убеждаемся, что они вызваны в тех же файлах, что и объявление/включение lodash
foreach(CxList method in vulnerableMethods)
{
	string file_name_method = method.GetFirstGraph().LinePragma.FileName;
	if (lodash_path.Contains(file_name_method) == true && lodash_result_path.Contains(file_name_method) == false){
		result.Add(method);
	}
}

// Убираем все UknownReferences и оставляем самый "длинный" из путей, если такие встречаются
result = result.ReduceFlow(CxList.ReduceFlowType.ReduceSmallFlow) - result.FindByType(typeof(UnknownReference));

Problème : Recherche de certificats intégrés dans l'application

Solution : Il n'est pas rare que les applications, notamment mobiles, utilisent des certificats ou des clés pour accéder à divers serveurs ou pour vérifier le SSL-Pinning. Du point de vue de la sécurité, stocker de telles choses dans le code n'est pas la meilleure pratique. Essayons d'écrire une règle qui cherchera de tels fichiers dans le référentiel :

// Найдем все сертификаты по маске файла
CxList find_certs = All.FindByShortNames(new List<string> {"*.der", "*.cer", "*.pem", "*.key"}, false);

// Проверим, где в приложении они используются
CxList data_used_certs = All.DataInfluencedBy(find_certs);

// И для мобильных приложений - можем поискать методы, где вызывается чтение сертификатов
// Для других платформ и приложений могут быть различные методы
CxList methods = All.FindByMemberAccess("*.getAssets");

// Пересечение множеств даст нам результат по использованию локальных сертификатов в приложении
result = methods * data_used_certs;

Problème : Recherche de tokens compromis dans l'application

Solution : Il arrive souvent de devoir révoquer des jetons compromis ou d'autres informations sensibles présentes dans le code. Bien sûr, les conserver dans le code source n'est pas la meilleure idée, mais les situations varient. Grâce aux requêtes CxQL, il est assez facile de trouver de telles choses :

// Получаем все строки, которые содержатся в коде
CxList strings = base.Find_Strings();

// Ищем среди всех строк нужное нам значение. В примере токен в виде строки "qwerty12345"
result = strings.FindByShortName("qwerty12345");

Conclusion

J'espère que cet article sera utile à ceux qui découvrent l'outil Checkmarx. Peut-être que ceux qui écrivent des règles depuis longtemps trouveront également quelque chose d'utile dans ce guide.

Malheureusement, il manque actuellement une ressource où l'on pourrait s'inspirer pour développer de nouvelles règles pour Checkmarx. C'est pourquoi nous avons créé un dépôt sur Github, où nous allons partager nos travaux, afin que chacun utilisant CxQL puisse y trouver quelque chose d'utile, tout en ayant la possibilité de partager ses propres contributions avec la communauté. Le dépôt est en cours de remplissage et de structuration du contenu, donc les contributeurs sont les bienvenus !

Merci de votre attention !

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster