Come scrivere regole per Checkmarx senza impazzire

Ehi Habr!

Nel nostro lavoro, la nostra azienda si occupa molto spesso di vari strumenti di analisi statica del codice (SAST). Fuori dagli schemi funzionano tutti nella media. Naturalmente, tutto dipende dal progetto e dalle tecnologie utilizzate in esso, nonché da quanto bene queste tecnologie sono coperte dalle regole di analisi. A mio avviso, uno dei criteri più importanti nella scelta di uno strumento SAST è la capacità di personalizzarlo in base alle specifiche delle proprie applicazioni, ovvero scrivere e modificare regole di analisi o, come vengono più spesso chiamate, query personalizzate.

Come scrivere regole per Checkmarx senza impazzire

Utilizziamo molto spesso Checkmarx, un analizzatore di codice molto interessante e potente. In questo articolo parlerò della mia esperienza nella scrittura di regole di analisi per questo.

Sommario

Iscrizione

Per cominciare, vorrei consigliare uno dei pochi articoli in russo sulle caratteristiche della scrittura di query per Checkmarx. È stato pubblicato su Habré alla fine del 2019 con il titolo: "Ciao, Checkmarx!" Come scrivere una query SAST di Checkmarx e trovare interessanti vulnerabilità.

Esamina in dettaglio come scrivere le prime query in CxQL (Checkmarx Query Language) per alcune applicazioni di test e mostra i principi base di funzionamento delle regole di analisi.

Non ripeterò quanto in esso descritto, anche se alcuni incroci saranno comunque presenti. Nel mio articolo cercherò di compilare una sorta di “raccolta di ricette”, un elenco di soluzioni a problemi specifici che ho riscontrato durante il mio lavoro con Checkmarx. Ho dovuto scervellarmi su molti di questi problemi. A volte nella documentazione non c'erano abbastanza informazioni, a volte era addirittura difficile capire come fare ciò che veniva richiesto. Spero che la mia esperienza e le notti insonni non siano vane, e questa “raccolta di ricette per query personalizzate” ti farà risparmiare qualche ora o un paio di cellule nervose. Allora, cominciamo!

Informazioni generali sulle regole

Innanzitutto, diamo un'occhiata ad alcuni concetti di base e al processo di lavoro con le regole, per una migliore comprensione di ciò che accadrà dopo. E anche perché la documentazione non dice nulla a riguardo o è molto sparsa nella struttura, il che non è molto comodo.

  1. Le regole vengono applicate durante la scansione in base alla preimpostazione selezionata all'avvio (un insieme di regole attive). Puoi creare un numero illimitato di preimpostazioni e il modo esatto in cui strutturarle dipende dalle specifiche del tuo processo. Puoi raggrupparli per lingua o selezionare preimpostazioni per ciascun progetto. Il numero di regole attive influisce sulla velocità e sulla precisione della scansione.

    Come scrivere regole per Checkmarx senza impazzireImpostazione della preimpostazione nell'interfaccia di Checkmarx

  2. Le regole vengono modificate in uno strumento speciale chiamato CxAuditor. Questa è un'applicazione desktop che si connette a un server che esegue Checkmarx. Questo strumento ha due modalità di funzionamento: modifica delle regole e analisi dei risultati di una scansione già eseguita.

    Come scrivere regole per Checkmarx senza impazzireInterfaccia CxAudit

  3. Le regole in Checkmarx sono divise per lingua, ovvero ogni lingua ha il proprio set di query. Esistono poi alcune regole generali che valgono indipendentemente dalla lingua, queste sono le cosiddette query di base. Nella maggior parte dei casi, le query di base implicano la ricerca di informazioni utilizzate da altre regole.

    Come scrivere regole per Checkmarx senza impazzireDividere le regole per lingua

  4. Le regole sono "eseguibili" e "non eseguibili" (eseguite e non eseguite). Non è proprio il nome corretto, secondo me, ma è quello che è. La conclusione è che il risultato dell'esecuzione delle regole "eseguibili" verrà visualizzato nei risultati della scansione nell'interfaccia utente e le regole "non eseguibili" sono necessarie solo per utilizzare i risultati in altre richieste (in sostanza, solo una funzione).

    Come scrivere regole per Checkmarx senza impazzireDeterminazione del tipo di regola durante la creazione

  5. Puoi creare nuove regole o integrare/riscrivere quelle esistenti. Per riscrivere una regola, è necessario trovarla nell'albero, fare clic con il tasto destro e selezionare "Sostituisci" dal menu a discesa. È importante ricordare qui che le nuove regole inizialmente non sono incluse nelle preimpostazioni e non sono attive. Per iniziare ad utilizzarli è necessario attivarli nel menu “Preset Manager” dello strumento. Le regole riscritte mantengono le loro impostazioni, ovvero, se la regola era attiva, rimarrà tale e verrà applicata immediatamente.

    Come scrivere regole per Checkmarx senza impazzireEsempio di una nuova regola nell'interfaccia Preset Manager

  6. Durante l'esecuzione viene costruito un “albero” di richieste, che dipende da cosa. Le regole che raccolgono le informazioni vengono eseguite per prime e quelle che le utilizzano per seconde. Il risultato dell'esecuzione viene memorizzato nella cache, quindi se è possibile utilizzare i risultati di una regola esistente, è meglio farlo, poiché ciò ridurrà il tempo di scansione.

  7. Le regole possono essere applicate a diversi livelli:

  • Per l'intero sistema: verrà utilizzato per qualsiasi scansione di qualsiasi progetto

  • A livello di team (Team): verrà utilizzato solo per scansionare progetti nel team selezionato.

  • A livello di progetto: verrà applicato in un progetto specifico

    Come scrivere regole per Checkmarx senza impazzireDeterminare il livello al quale verrà applicata la regola

“Dizionario” per principianti

E inizierò con alcune cose che mi hanno causato domande e mostrerò anche una serie di tecniche che semplificheranno notevolmente la vita.

Operazioni con liste

- вычитание одного из другого (list2 - list1)
* пересечение списков (list1 * list2)
+ сложение списков (list1 + list2)

& (логическое И) - объединяет списки по совпадению (list1 & list2), аналогично пересечению (list1 * list2)
| (логическое ИЛИ) - объединяет списки по широкому поиску (list1 | list2)

Со списками не работает:  ^  &&  ||  %  / 

Tutti gli elementi trovati

All'interno della lingua scansionata è possibile ottenere un elenco di tutti gli elementi identificati da Checkmarx (stringhe, funzioni, classi, metodi, ecc.). Questo è uno spazio di oggetti a cui è possibile accedere All. Cioè cercare un oggetto con un nome specifico searchMe, puoi cercare, ad esempio, per nome tra tutti gli oggetti trovati:

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

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

Ma, se hai bisogno di cercare in un'altra lingua che per qualche motivo non è stata inclusa nella scansione (ad esempio, groovy in un progetto Android), puoi espandere il nostro spazio oggetti attraverso una variabile:

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

Funzioni per l'analisi del flusso

Queste funzioni sono utilizzate in molte regole ed ecco un piccolo suggerimento su cosa significano:

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

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

Ottenere il nome/percorso del file

Esistono diversi attributi che possono essere ottenuti dai risultati di una query (nome del file in cui è stata trovata la voce, stringa, ecc.), ma la documentazione non dice come ottenerli e utilizzarli. Quindi, per fare ciò, dobbiamo accedere alla proprietà LinePragma e al suo interno si troveranno gli oggetti di cui abbiamo bisogno:

// Для примера найдем все методы
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);

Vale la pena tenerlo presente FileName contiene in realtà il percorso del file, poiché abbiamo utilizzato il metodo GetFirstGraph.

Risultato dell'esecuzione

C'è una variabile speciale all'interno di CxQL result, che restituisce il risultato dell'esecuzione della regola scritta. Viene inizializzato immediatamente e puoi scriverci risultati intermedi, modificandoli e perfezionandoli mentre lavori. Ma se non c'è alcuna assegnazione a questa variabile o funzione all'interno della regola return— il risultato dell'esecuzione sarà sempre zero.

La seguente query non ci restituirà nulla a seguito dell'esecuzione e sarà sempre vuota:

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

Ma, dopo aver assegnato il risultato dell'esecuzione alla variabile magica result, vedremo cosa ci restituisce questa chiamata:

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

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

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

Utilizzando i risultati di altre regole

Le regole in Checkmarx possono essere chiamate analoghe alle funzioni in un normale linguaggio di programmazione. Quando scrivi una regola, potresti utilizzare i risultati di altre query. Ad esempio, non è necessario cercare ogni volta tutte le chiamate ai metodi nel codice, basta chiamare la regola desiderata:

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

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

Questo approccio consente di abbreviare il codice e ridurre significativamente il tempo di esecuzione delle regole.

Risoluzione dei problemi

Registrazione

Quando si lavora con lo strumento, a volte non è possibile scrivere immediatamente la query desiderata e bisogna sperimentare, provando diverse opzioni. In tal caso, lo strumento fornisce la registrazione, denominata come segue:

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

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

Ma vale la pena ricordare che questo metodo accetta solo come input la stringa, quindi non sarà possibile visualizzare l'elenco completo degli elementi trovati a seguito della prima operazione. La seconda opzione, utilizzata per il debug, consiste nell'assegnare di tanto in tanto una variabile magica result il risultato della query e vedere cosa succede. Questo approccio non è molto conveniente; devi essere sicuro che non ci siano sostituzioni o operazioni con questo nel codice successivo result o semplicemente commenta il codice qui sotto. Oppure puoi, come me, dimenticare di rimuovere molte di queste chiamate da una regola già pronta e chiederti perché non funziona nulla.

Un modo più conveniente è chiamare il metodo return con il parametro richiesto. In questo caso l’esecuzione della regola terminerà e potremo vedere cosa è successo in conseguenza di quanto scritto:

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

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

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

Problema di accesso

Ci sono situazioni in cui non è possibile accedere allo strumento CxAudit (utilizzato per scrivere regole). Le ragioni possono essere molte, inclusi arresti anomali, aggiornamenti improvvisi di Windows, BSOD e altre situazioni impreviste che sfuggono al nostro controllo. In questo caso, a volte nel database è presente una sessione incompiuta che impedisce di accedere nuovamente. Per risolverlo, è necessario eseguire diverse query:

Per Checkmarx prima della 8.6:

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

Per Checkmarx dopo la versione 8.6:

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

Regole di scrittura

Ora arriviamo alla parte più interessante. Quando inizi a scrivere regole in CxQL, ciò che spesso ti manca non è tanto la documentazione quanto alcuni esempi viventi di risoluzione di determinati problemi e descrizione del processo di funzionamento delle query in generale.

Cercherò di rendere la vita un po' più semplice a coloro che stanno iniziando ad immergersi nel linguaggio delle query e fornirò diversi esempi di utilizzo delle query personalizzate per risolvere determinati problemi. Alcuni di essi sono abbastanza generali e possono essere utilizzati nella vostra azienda praticamente senza modifiche, altri sono più specifici, ma possono anche essere utilizzati modificando il codice per adattarlo alle specificità delle vostre applicazioni.

Quindi, ecco i problemi che abbiamo riscontrato più spesso:

obiettivo: Sono presenti diversi flussi nei risultati dell'esecuzione della regola e uno di essi è un annidamento di un altro, è necessario lasciarne uno.

soluzione: In effetti, a volte Checkmarx mostra diversi flussi di dati che possono sovrapporsi ed essere una versione abbreviata di altri. Esiste un metodo speciale per questi casi Ridurre il flusso. A seconda del parametro, selezionerà il flusso più breve o più lungo:

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

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

obiettivo: Espandi l'elenco dei dati sensibili a cui reagisce lo strumento

soluzione: Checkmarx ha regole di base, i cui risultati vengono utilizzati da molte altre query. Integrando alcune di queste regole con dati specifici per la tua applicazione, puoi migliorare immediatamente i risultati della scansione. Di seguito è riportato un esempio di regola per iniziare:

General_privacy_violation_list

Aggiungiamo diverse variabili utilizzate nella nostra applicazione per archiviare informazioni sensibili:

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

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

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

obiettivo: Espandi l'elenco delle variabili con le password

soluzione: Consiglierei subito di prestare attenzione alla regola di base per definire le password nel codice e di aggiungervi un elenco di nomi di variabili comunemente utilizzati nella vostra azienda.

Password_privacy_violation_list

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*"));

// Дополняем дефолтный список переменных
List < string > pswdIncludeList = new List<string>{"*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 < string > pswdExcludeList = new List<string>{"*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);
	}
}

obiettivo: Aggiungi framework usati che non sono supportati da Checkmarx

soluzione: Tutte le query in Checkmarx sono divise per lingua, quindi è necessario aggiungere regole per ciascuna lingua. Di seguito sono riportati alcuni esempi di tali regole.

Se vengono utilizzate librerie che completano o sostituiscono le funzionalità standard, possono essere facilmente aggiunte alla regola di base. Quindi tutti coloro che lo utilizzano verranno immediatamente a conoscenza delle nuove introduzioni. Ad esempio, le librerie per l'accesso in Android sono Timber e Loggi. Nel pacchetto base non ci sono regole per identificare le chiamate non di sistema, quindi se una password o un identificatore di sessione entra nel registro, non lo sapremo. Proviamo ad aggiungere le definizioni di tali metodi alle regole di Checkmarx.

Esempio di codice di test che utilizza la libreria Timber per la registrazione:

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");
    }
}

Ed ecco un esempio di richiesta per Checkmarx, che ti consentirà di aggiungere una definizione di chiamata ai metodi Timber come punto di uscita per i dati dall'applicazione:

TrovaAndroidOutputs

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

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

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

E puoi anche aggiungere alla regola vicina, ma questa si riferisce direttamente all'accesso in Android:

TrovaAndroidLog_Outputs

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

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

Inoltre, se si utilizzano applicazioni Android Direttore del lavoro per il lavoro asincrono, è una buona idea informare ulteriormente Checkmarx aggiungendo un metodo per ottenere dati dall'attività getInputData:

TrovaAndroidLeggi

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

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

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

obiettivo: Ricerca di dati sensibili in plist per progetti iOS

soluzione: iOS utilizza spesso file speciali con estensione .plist per memorizzare varie variabili e valori. Non è consigliabile archiviare password, token, chiavi e altri dati sensibili in questi file poiché possono essere estratti dal dispositivo senza problemi.

I file Plist hanno caratteristiche che non sono evidenti ad occhio nudo, ma sono importanti per Checkmarx. Scriviamo una regola che cercherà i dati di cui abbiamo bisogno e ci dirà se password o token sono menzionati da qualche parte.

Un esempio di tale file, che contiene un token per la comunicazione con il servizio backend:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>DeviceDictionary</key>
	<dict>
		<key>phone</key>
		<string>iPhone 6s</string>
	</dict>
	<key>privatekey</key>
	<string>MIICXAIBAAKBgQCqGKukO1De7zhZj6+</string>
</dict>
</plist>

E una regola per Checkmarx, che ha diverse sfumature che dovrebbero essere prese in considerazione quando si scrive:

// Используем результат выполнения правила по поиску файлов 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);

obiettivo: Trovare informazioni in XML

soluzione: Checkmarx dispone di funzioni molto comode per lavorare con XML e cercare valori, tag, attributi e altro. Ma sfortunatamente si è verificato un errore nella documentazione a causa del quale non funziona un solo esempio. Nonostante questo difetto sia stato eliminato nell'ultima versione della documentazione, fai attenzione se utilizzi versioni precedenti dei documenti.

Ecco un esempio errato dalla documentazione:

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

Come risultato del tentativo di esecuzione, riceveremo un errore that All non esiste un metodo del genere... E questo è vero, poiché esiste uno spazio oggetti speciale e separato per utilizzare le funzioni per lavorare con XML - cxXPath. Ecco come appare la query corretta per trovare un'impostazione in Android che consenta l'utilizzo del traffico HTTP:

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

Diamo un'occhiata più in dettaglio, poiché la sintassi per tutte le funzioni è simile, dopo averne individuata una, devi solo selezionare quella che ti serve. Quindi, in sequenza secondo i parametri:

  • "*.xml"— maschera dei file da cercare

  • 8 — ID della lingua per la quale si applica la regola

  • "cleartextTrafficPermitted"— nome dell'attributo in xml

  • "true" — il valore di questo attributo

  • false — uso delle espressioni regolari durante la ricerca

  • true — significa che la ricerca verrà eseguita ignorando le maiuscole, ovvero senza distinzione tra maiuscole e minuscole

Ad esempio, abbiamo utilizzato una regola che identifica impostazioni errate, dal punto di vista della sicurezza, della connessione di rete in Android che consentono la comunicazione con il server tramite il protocollo HTTP. Esempio di un'impostazione contenente un attributo cleartextTrafficPermitted con significato true:

<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">example.com</domain>
        <trust-anchors>
            <certificates src="@raw/my_ca"/>
        </trust-anchors>
        <domain-config cleartextTrafficPermitted="true">
            <domain includeSubdomains="true">secure.example.com</domain>
        </domain-config>
    </domain-config>
</network-security-config>

obiettivo: Limita i risultati in base al nome/percorso del file

soluzione: In uno dei grandi progetti legati allo sviluppo di un'applicazione mobile per Android, abbiamo riscontrato falsi positivi della regola che determina l'impostazione dell'offuscamento. Il fatto è che la regola cerca immediatamente nel file build.gradle un'impostazione responsabile dell'applicazione delle regole di offuscamento per la versione di rilascio dell'applicazione.

Ma nei progetti di grandi dimensioni a volte ci sono file secondari build.gradle, che fanno riferimento alle librerie incluse nel progetto. La particolarità è che anche se questi file non indicano la necessità di offuscamento, durante la compilazione verranno applicate le impostazioni del file assembly genitore.

Pertanto, il compito è eliminare i trigger nei file secondari che appartengono alle librerie. Possono essere identificati dalla presenza della linea apply 'com.android.library'.

Codice di esempio dal file build.gradle, che determina la necessità di offuscamento:

apply plugin: 'com.android.application'

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

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

dependencies {
  ...
}

Esempio di file build.gradle per una libreria inclusa nel progetto che non ha questa impostazione:

apply plugin: 'android-library'

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

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

E la regola per 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);
		}
	}
}

Questo approccio può essere abbastanza universale e utile non solo per le applicazioni Android, ma anche per altri casi in cui è necessario determinare se un risultato appartiene a un file specifico.

obiettivo: Aggiungi il supporto per una libreria di terze parti se la sintassi non è completamente supportata

soluzione: Il numero di vari framework utilizzati nel processo di scrittura del codice è semplicemente fuori scala. Naturalmente Checkmarx non sempre è a conoscenza della loro esistenza e il nostro compito è insegnargli a capire che determinati metodi appartengono specificamente a questo quadro. A volte questo è complicato dal fatto che i framework utilizzano nomi di funzioni molto comuni ed è impossibile determinare in modo inequivocabile la relazione di una particolare chiamata con una libreria specifica.

La difficoltà è che la sintassi di tali librerie non viene sempre riconosciuta correttamente e bisogna sperimentare per evitare di ottenere un gran numero di falsi positivi. Esistono diverse opzioni per migliorare la precisione della scansione e risolvere il problema:

  • Sappiamo per certo che la prima opzione è che la libreria viene utilizzata in un progetto specifico e possiamo applicare la regola a livello di team. Ma se il team decide di adottare un approccio diverso o utilizza più librerie in cui i nomi delle funzioni si sovrappongono, possiamo ottenere un quadro poco piacevole di numerosi falsi positivi

  • La seconda opzione è cercare file in cui la libreria è chiaramente importata. Con questo approccio possiamo essere sicuri che la libreria di cui abbiamo bisogno sia utilizzata esattamente in questo file.

  • E la terza opzione è utilizzare insieme i due approcci sopra indicati.

Ad esempio, consideriamo una biblioteca ben nota in ambienti ristretti chiazza di petrolio per il linguaggio di programmazione Scala, vale a dire la funzionalità Splicing di valori letterali. In generale, per passare parametri a una query SQL è necessario utilizzare l'operatore $, che sostituisce i dati in una query SQL preimpostata. Cioè, in effetti, è un analogo diretto di Prepared Statement in Java. Ma, se devi costruire dinamicamente una query SQL, ad esempio, se devi passare i nomi delle tabelle, puoi utilizzare l'operatore #$, che sostituirà direttamente i dati nella query (quasi come la concatenazione di stringhe).

Esempio di codice:

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

Checkmarx non sa ancora come rilevare l'uso di valori letterali di splicing e salta gli operatori #$, quindi proviamo a insegnargli a identificare potenziali SQL injection ed evidenziare i posti giusti nel codice:

// Находим все импорты
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));
}

obiettivo: Cerca le funzioni vulnerabili utilizzate nelle librerie Open Source

soluzione: Molte aziende utilizzano strumenti di monitoraggio Open Source (pratica OSA) per rilevare l'uso di versioni vulnerabili delle librerie nelle applicazioni sviluppate. A volte non è possibile aggiornare tale libreria a una versione sicura. In alcuni casi esistono limitazioni funzionali, in altri non esiste alcuna versione sicura. In questo caso, una combinazione di pratiche SAST e OSA aiuterà a determinare che le funzioni che portano allo sfruttamento della vulnerabilità non vengono utilizzate nel codice.

Ma a volte, soprattutto se si considera JavaScript, questo potrebbe non essere un compito del tutto banale. Di seguito è riportata una soluzione, forse non ideale, ma comunque funzionante, utilizzando l'esempio delle vulnerabilità nel componente lodash nei metodi template и *set.

Esempi di test di codice potenzialmente vulnerabile in un file 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!'

E quando ci si connette direttamente in html:

<!DOCTYPE html>
<html>
<head>
    <title>Lodash Tutorial</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>

Stiamo cercando tutti i nostri metodi vulnerabili, che sono elencati in vulnerabilità:

// Ищем все строки: в которых встречается строка 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));

obiettivo: Ricerca dei certificati incorporati nell'applicazione

soluzione: Non è raro che le applicazioni, soprattutto quelle mobili, utilizzino certificati o chiavi per accedere a vari server o verificare SSL-Pinning. Dal punto di vista della sicurezza, archiviare tali elementi nel codice non è la pratica migliore. Proviamo a scrivere una regola che cercherà file simili nel repository:

// Найдем все сертификаты по маске файла
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;

obiettivo: Ricerca di token compromessi nell'applicazione

soluzione: Spesso è necessario revocare token compromessi o altre informazioni importanti presenti nel codice. Naturalmente, memorizzarli nel codice sorgente non è una buona idea, ma le situazioni variano. Grazie alle query CxQL, trovare cose come queste è abbastanza semplice:

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

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

conclusione

Spero che questo articolo sia utile a coloro che stanno iniziando a conoscere lo strumento Checkmarx. Forse anche chi scrive le proprie regole da tempo troverà qualcosa di utile in questa guida.

Sfortunatamente, attualmente mancano risorse da cui raccogliere nuove idee durante lo sviluppo delle regole per Checkmarx. Ecco perché abbiamo creato deposito su Github, dove pubblicheremo il nostro lavoro in modo che tutti coloro che utilizzano CxQL possano trovarvi qualcosa di utile e abbiano anche l'opportunità di condividere il proprio lavoro con la comunità. Il repository è in fase di riempimento e strutturazione dei contenuti, quindi i contributori sono i benvenuti!

Grazie!

Fonte: habr.com

Aggiungi un commento