Jak napsat pravidla pro Checkmarx, aniž byste se zbláznili

Čau Habr!

Při své práci se naše společnost velmi často zabývá různými nástroji pro analýzu statického kódu (SAST). Po vybalení všechny fungují průměrně. Vše samozřejmě závisí na projektu a technologiích v něm použitých a také na tom, jak dobře jsou tyto technologie pokryty pravidly analýzy. Podle mého názoru je jedním z nejdůležitějších kritérií při výběru nástroje SAST schopnost přizpůsobit jej specifikům vašich aplikací, konkrétně psát a měnit pravidla analýzy nebo, jak se jim častěji říká, Custom Queries.

Jak napsat pravidla pro Checkmarx, aniž byste se zbláznili

Nejčastěji používáme Checkmarx - velmi zajímavý a výkonný analyzátor kódu. V tomto článku budu hovořit o svých zkušenostech s psaním pravidel pro analýzu.

obsah

Vstup

Pro začátek bych rád doporučil jeden z mála článků v ruštině o funkcích psaní dotazů pro Checkmarx. Vyšlo na Habré na konci roku 2019 pod názvem: "Ahoj, Checkmarxi!" Jak napsat dotaz Checkmarx SAST a najít skvělé zranitelnosti.

Podrobně zkoumá, jak napsat první dotazy v CxQL (Checkmarx Query Language) pro některé testovací aplikace, a ukazuje základní principy fungování pravidel analýzy.

Nebudu opakovat to, co je v něm popsáno, i když některé křižovatky budou stále přítomny. Ve svém článku se pokusím sestavit jakousi „sbírku receptů“, seznam řešení konkrétních problémů, se kterými jsem se při práci s Checkmarxem setkal. Nad mnoha z těchto problémů jsem si musel lámat hlavu. Někdy v dokumentaci nebylo dostatek informací a někdy bylo dokonce obtížné porozumět tomu, jak udělat, co bylo požadováno. Doufám, že moje zkušenosti a bezesné noci nepřijdou nazmar a tato „sbírka receptů Custom Queries“ vám ušetří pár hodin nebo pár nervových buněk. Takže, začněme!

Obecné informace o pravidlech

Nejprve se podívejme na pár základních pojmů a na proces práce s pravidly, abychom lépe pochopili, co se bude dít dál. A také proto, že dokumentace o tom nic neříká nebo je ve struktuře velmi rozprostřena, což není příliš pohodlné.

  1. Pravidla se použijí během kontroly v závislosti na předvolbě vybrané při spuštění (souboru aktivních pravidel). Můžete vytvořit neomezený počet předvoleb a přesně to, jak je strukturovat, závisí na specifikách vašeho procesu. Můžete je seskupit podle jazyka nebo vybrat předvolby pro každý projekt. Počet aktivních pravidel ovlivňuje rychlost a přesnost skenování.

    Jak napsat pravidla pro Checkmarx, aniž byste se zblázniliNastavení předvolby v rozhraní Checkmarx

  2. Pravidla se upravují ve speciálním nástroji CxAuditor. Toto je desktopová aplikace, která se připojuje k serveru, na kterém běží Checkmarx. Tento nástroj má dva provozní režimy: úprava pravidel a analýza výsledků již provedené kontroly.

    Jak napsat pravidla pro Checkmarx, aniž byste se zblázniliRozhraní CxAudit

  3. Pravidla v Checkmarx jsou rozdělena podle jazyka, to znamená, že každý jazyk má svou vlastní sadu dotazů. Existují také některá obecná pravidla, která platí bez ohledu na jazyk, jedná se o tzv. základní dotazy. Základní dotazy většinou zahrnují hledání informací, které používají jiná pravidla.

    Jak napsat pravidla pro Checkmarx, aniž byste se zblázniliDělení pravidel podle jazyka

  4. Pravidla jsou „Spustitelná“ a „Nespustitelná“ (spuštěna a neprovedena). Podle mě to není úplně správný název, ale je to tak. Pointa je, že výsledek spouštění „Spustitelných“ pravidel se zobrazí ve výsledcích kontroly v uživatelském rozhraní a „Nespustitelná“ pravidla jsou potřebná pouze k použití jejich výsledků v jiných požadavcích (ve skutečnosti jsou to jen funkce ).

    Jak napsat pravidla pro Checkmarx, aniž byste se zblázniliUrčení typu pravidla při vytváření

  5. Můžete vytvářet nová pravidla nebo doplňovat/přepisovat stávající. Chcete-li pravidlo přepsat, musíte ho najít ve stromu, kliknout pravým tlačítkem myši a z rozbalovací nabídky vybrat možnost „Přepsat“. Zde je důležité si uvědomit, že nová pravidla nejsou zpočátku zahrnuta v předvolbách a nejsou aktivní. Chcete-li je začít používat, musíte je aktivovat v nabídce „Preset Manager“ v přístroji. Přepsaná pravidla si zachovají svá nastavení, to znamená, že pokud bylo pravidlo aktivní, tak zůstane a bude okamžitě aplikováno.

    Jak napsat pravidla pro Checkmarx, aniž byste se zblázniliPříklad nového pravidla v rozhraní Správce předvoleb

  6. Během provádění se vytváří „strom“ požadavků, který závisí na tom, co. Pravidla, která shromažďují informace, jsou prováděna jako první a ti, kteří je používají, až jako druhá. Výsledek provádění je uložen do mezipaměti, takže pokud je možné použít výsledky existujícího pravidla, je lepší tak učinit, zkrátí se tím doba kontroly.

  7. Pravidla lze použít na různých úrovních:

  • Pro celý systém - bude použit pro jakékoli skenování libovolného projektu

  • Na úrovni týmu (Tým) – použije se pouze ke skenování projektů ve vybraném týmu.

  • Na úrovni projektu - Bude aplikováno v konkrétním projektu

    Jak napsat pravidla pro Checkmarx, aniž byste se zblázniliUrčení úrovně, na které bude pravidlo aplikováno

"Slovník" pro začátečníky

A začnu pár věcmi, které mi způsobily otázky, a ukážu také řadu technik, které výrazně zjednoduší život.

Operace se seznamy

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

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

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

Všechny nalezené položky

V rámci skenovaného jazyka můžete získat seznam absolutně všech prvků, které Checkmarx identifikoval (řetězce, funkce, třídy, metody atd.). Toto je určitý prostor objektů, přes který lze přistupovat All. Tedy hledat objekt s konkrétním názvem searchMe, můžete vyhledávat například podle názvu ve všech nalezených objektech:

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

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

Pokud však potřebujete hledat v jiném jazyce, který z nějakého důvodu nebyl zahrnut do skenování (například groovy v projektu Android), můžete rozšířit náš prostor objektů pomocí proměnné:

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

Funkce pro analýzu toku

Tyto funkce se používají v mnoha pravidlech a zde je malý cheat, co znamenají:

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

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

Získání názvu/cesty souboru

Existuje několik atributů, které lze získat z výsledků dotazu (název souboru, ve kterém byl záznam nalezen, řetězec atd.), ale dokumentace neuvádí, jak je získat a použít. Chcete-li to provést, musíte získat přístup k vlastnosti LinePragma a objekty, které potřebujeme, budou umístěny uvnitř:

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

To stojí za to mít na paměti FileName obsahuje ve skutečnosti cestu k souboru, protože jsme použili metodu GetFirstGraph.

Výsledek provedení

Uvnitř CxQL je speciální proměnná result, který vrátí výsledek provedení vašeho psaného pravidla. Okamžitě se inicializuje a můžete do něj zapisovat mezivýsledky, měnit je a upřesňovat při práci. Pokud však v pravidle není žádné přiřazení k této proměnné nebo funkci return— výsledek provedení bude vždy nulový.

Následující dotaz nám v důsledku provedení nic nevrátí a bude vždy prázdný:

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

Ale po přiřazení výsledku provedení magické proměnné result uvidíme, co nám toto volání vrátí:

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

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

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

Použití výsledků jiných pravidel

Pravidla v Checkmarx lze nazvat analogicky k funkcím v běžném programovacím jazyce. Při psaní pravidla můžete dobře využít výsledky jiných dotazů. Například není nutné pokaždé hledat všechna volání metod v kódu, stačí zavolat požadované pravidlo:

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

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

Tento přístup umožňuje zkrátit kód a výrazně zkrátit dobu provádění pravidla.

Řešení problémů

Protokolování

Při práci s nástrojem někdy není možné okamžitě napsat požadovaný dotaz a musíte experimentovat a zkoušet různé možnosti. V takovém případě nástroj poskytuje protokolování, které se nazývá takto:

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

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

Je však třeba si uvědomit, že tato metoda přijímá pouze jako vstup tětiva, takže v důsledku první operace nebude možné zobrazit úplný seznam nalezených prvků. Druhá možnost, která se používá pro ladění, je čas od času přiřadit magické proměnné result výsledek dotazu a uvidíte, co se stane. Tento přístup není příliš pohodlný; musíte si být jisti, že v kódu po něm nejsou žádné přepisy nebo operace result nebo jednoduše okomentujte kód níže. Nebo můžete, jako já, zapomenout odstranit několik takových volání z hotového pravidla a divit se, proč nic nefunguje.

Pohodlnějším způsobem je volání metody return s požadovaným parametrem. V tomto případě bude provádění pravidla ukončeno a my budeme moci vidět, co se stalo v důsledku toho, co jsme napsali:

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

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

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

Problém s přihlášením

Existují situace, kdy nemáte přístup k nástroji CxAudit (který se používá k psaní pravidel). Může to mít mnoho důvodů, včetně selhání, náhlých aktualizací Windows, BSOD a dalších nepředvídaných situací, které jsou mimo naši kontrolu. V takovém případě se někdy v databázi vyskytne nedokončená relace, která vám brání v opětovném přihlášení. Chcete-li to opravit, musíte spustit několik dotazů:

Pro Checkmarx před 8.6:

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

Pro Checkmarx po 8.6:

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

Pravidla psaní

Nyní se dostáváme k nejzajímavější části. Když začnete psát pravidla v CxQL, často vám chybí ani tak dokumentace, jako nějaké živé příklady řešení určitých problémů a obecně popisující proces fungování dotazů.

Pokusím se trochu usnadnit život těm, kteří se začínají ponořit do dotazovacího jazyka, a uvedu několik příkladů použití vlastních dotazů k řešení určitých problémů. Některé z nich jsou poměrně obecné a lze je ve vaší firmě používat prakticky beze změn, jiné jsou konkrétnější, ale lze je využít i změnou kódu tak, aby vyhovoval specifikům vašich aplikací.

Zde jsou tedy problémy, se kterými jsme se nejčastěji setkávali:

Úkol: Ve výsledcích provádění pravidla je několik toků a jeden z nich je vnořením jiného, ​​jeden z nich musíte opustit.

řešení: Checkmarx skutečně někdy zobrazuje několik datových toků, které se mohou překrývat a být zkrácenou verzí jiných. Pro takové případy existuje speciální metoda ReduceFlow. V závislosti na parametru vybere nejkratší nebo nejdelší průtok:

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

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

Úkol: Rozbalte seznam citlivých dat, na které nástroj reaguje

řešení: Checkmarx má základní pravidla, jejichž výsledky využívá mnoho dalších dotazů. Doplněním některých z těchto pravidel o data specifická pro vaši aplikaci můžete okamžitě zlepšit výsledky skenování. Níže je příklad pravidla, jak začít:

Seznam obecných_porušení_soukromí

Přidejme několik proměnných, které se v naší aplikaci používají k ukládání citlivých informací:

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

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

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

Úkol: Rozšiřte seznam proměnných pomocí hesel

řešení: Doporučil bych okamžitě věnovat pozornost základnímu pravidlu pro definování hesel v kódu a přidat k němu seznam názvů proměnných, které se ve vaší firmě běžně používají.

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

Úkol: Přidejte použité rámce, které Checkmarx nepodporuje

řešení: Všechny dotazy v Checkmarx jsou rozděleny podle jazyka, takže je potřeba přidat pravidla pro každý jazyk. Níže uvádíme několik příkladů takových pravidel.

Pokud se používají knihovny, které doplňují nebo nahrazují standardní funkce, lze je snadno přidat k základnímu pravidlu. Pak se každý, kdo jej používá, okamžitě dozví o nových úvodech. Například knihovny pro přihlášení v Androidu jsou Timber a Loggi. V základním balíčku nejsou žádná pravidla pro identifikaci nesystémových volání, takže pokud se do logu dostane heslo nebo identifikátor relace, nebudeme o tom vědět. Pokusme se přidat definice takových metod do pravidel Checkmarx.

Příklad testovacího kódu, který používá knihovnu Timber pro protokolování:

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

A zde je příklad požadavku na Checkmarx, který vám umožní přidat definici volání metod Timber jako výstupního bodu pro data z aplikace:

Najděte AndroidOutputs

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

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

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

A můžete také přidat k sousednímu pravidlu, ale toto se přímo vztahuje k přihlášení do Androidu:

Najděte AndroidLog_Outputs

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

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

Také pokud používají aplikace pro Android WorkManager pro asynchronní práci je dobré o tom dodatečně informovat Checkmarx přidáním metody pro získávání dat z úlohy getInputData:

Najít AndroidRead

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

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

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

Úkol: Vyhledávání citlivých dat v plist pro projekty iOS

řešení: iOS často používá speciální soubory s příponou .plist k ukládání různých proměnných a hodnot. Ukládání hesel, tokenů, klíčů a dalších citlivých dat v těchto souborech se nedoporučuje, protože je lze bez problémů extrahovat ze zařízení.

Soubory Plist mají vlastnosti, které nejsou viditelné pouhým okem, ale jsou důležité pro Checkmarx. Pojďme napsat pravidlo, které vyhledá potřebná data a řekne nám, jestli jsou někde zmíněna hesla nebo tokeny.

Příklad takového souboru, který obsahuje token pro komunikaci s backendovou službou:

<?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>

A pravidlo pro Checkmarx, které má několik nuancí, které je třeba vzít v úvahu při psaní:

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

Úkol: Vyhledání informací v XML

řešení: Checkmarx má velmi pohodlné funkce pro práci s XML a vyhledávání hodnot, značek, atributů a dalších. Ale bohužel došlo k chybě v dokumentaci, kvůli které nefunguje ani jeden příklad. Navzdory skutečnosti, že tato vada byla v nejnovější verzi dokumentace odstraněna, buďte opatrní, pokud používáte starší verze dokumentů.

Zde je nesprávný příklad z dokumentace:

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

V důsledku pokusu o provedení obdržíme chybu, která All žádná taková metoda neexistuje... A to je pravda, protože existuje speciální, oddělený objektový prostor pro použití funkcí pro práci s XML - cxXPath. Takto vypadá správný dotaz k nalezení nastavení v Androidu, které umožňuje použití HTTP provozu:

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

Podívejme se na to trochu podrobněji, protože syntaxe všech funkcí je podobná, poté, co jste na jednu přišli, stačí vybrat tu, kterou potřebujete. Takže postupně podle parametrů:

  • "*.xml"— maska ​​souborů, které mají být prohledávány

  • 8 — id jazyka, pro který platí pravidlo

  • "cleartextTrafficPermitted"— název atributu v xml

  • "true" — hodnota tohoto atributu

  • false — použití regulárního výrazu při vyhledávání

  • true — znamená, že vyhledávání bude provedeno bez ohledu na velká a malá písmena, tj. bez ohledu na velikost písmen

Jako příklad jsme použili pravidlo, které identifikuje z bezpečnostního hlediska nesprávné nastavení síťového připojení v Androidu, které umožňuje komunikaci se serverem přes protokol HTTP. Příklad nastavení obsahujícího atribut cleartextTrafficPermitted s hodnotou 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>

Úkol: Omezte výsledky podle názvu souboru/cesty

řešení: V jednom z velkých projektů souvisejících s vývojem mobilní aplikace pro Android jsme narazili na falešná pozitiva pravidla, které určuje nastavení mlžení. Faktem je, že pravidlo out of the box hledá v souboru build.gradle nastavení zodpovědné za použití pravidel zmatku pro vydanou verzi aplikace.

Ale ve velkých projektech někdy existují podřízené soubory build.gradle, které odkazují na knihovny zahrnuté v projektu. Zvláštností je, že i když tyto soubory neindikují potřebu zmatku, při kompilaci se uplatní nastavení souboru nadřazené sestavy.

Úkolem je tedy odříznout spouštěče v podřízených souborech, které patří knihovnám. Lze je identifikovat podle přítomnosti čáry apply 'com.android.library'.

Příklad kódu ze souboru build.gradle, který určuje potřebu zatemnění:

apply plugin: 'com.android.application'

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

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

dependencies {
  ...
}

Příklad souboru build.gradle pro knihovnu zahrnutou v projektu, která nemá toto nastavení:

apply plugin: 'android-library'

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

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

A pravidlo pro Checkmarxe:

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

Tento přístup může být poměrně univerzální a užitečný nejen pro Android aplikace, ale i pro jiné případy, kdy potřebujete určit, zda výsledek patří ke konkrétnímu souboru.

Úkol: Pokud syntaxe není plně podporována, přidejte podporu pro knihovnu třetí strany

řešení: Počet různých rámců, které se používají v procesu psaní kódu, je prostě mimo tabulky. Checkmarx samozřejmě o jejich existenci vždy neví a naším úkolem je naučit ho chápat, že určité metody patří specificky do tohoto rámce. Někdy je to komplikováno tím, že frameworky používají názvy funkcí, které jsou velmi běžné a nelze jednoznačně určit vztah konkrétního volání ke konkrétní knihovně.

Potíž je v tom, že syntaxe takových knihoven není vždy správně rozpoznána a vy musíte experimentovat, abyste se vyhnuli velkému počtu falešně pozitivních výsledků. Existuje několik možností, jak zlepšit přesnost skenování a vyřešit problém:

  • První možnost, s jistotou víme, že knihovna se používá v konkrétním projektu a může pravidlo aplikovat na týmové úrovni. Pokud se však tým rozhodne pro jiný přístup nebo použije několik knihoven, ve kterých se názvy funkcí překrývají, můžeme získat nepříliš příjemný obrázek o četných falešných pozitivech.

  • Druhou možností je vyhledání souborů, ve kterých je knihovna přehledně naimportována. S tímto přístupem si můžeme být jisti, že knihovna, kterou potřebujeme, je v tomto souboru přesně použita.

  • A třetí možností je použít oba výše uvedené přístupy společně.

Jako příklad se podívejme na knihovnu známou v úzkých kruzích úhledný pro programovací jazyk Scala, jmenovitě funkčnost Spojování doslovných hodnot. Obecně platí, že pro předání parametrů do SQL dotazu musíte použít operátor $, který nahrazuje data do předem vytvořeného SQL dotazu. To je ve skutečnosti přímá analogie připraveného prohlášení v Javě. Pokud však potřebujete dynamicky sestavit dotaz SQL, například pokud potřebujete předat názvy tabulek, můžete použít operátor #$, který přímo dosadí data do dotazu (téměř jako zřetězení řetězců).

Ukázkový kód:

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

Checkmarx zatím neví, jak detekovat použití sestřihových doslovných hodnot a přeskakuje operátory #$, tak se ho pokusme naučit identifikovat potenciální SQL injekce a zvýraznit správná místa v kódu:

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

Úkol: Vyhledejte použité zranitelné funkce v knihovnách Open-Source

řešení: Mnoho společností používá nástroje pro monitorování Open-Source (praxe OSA) k detekci používání zranitelných verzí knihoven ve vyvíjených aplikacích. Někdy není možné takovou knihovnu aktualizovat na zabezpečenou verzi. V některých případech existují funkční omezení, v jiných není bezpečná verze vůbec. V tomto případě kombinace postupů SAST a OSA pomůže určit, že funkce, které vedou ke zneužití zranitelnosti, nejsou v kódu použity.

Někdy to ale nemusí být, zvláště při zvažování JavaScriptu, úplně triviální úkol. Níže je řešení, možná ne ideální, ale přesto fungující, na příkladu zranitelností v komponentě lodash v metodách template и *set.

Příklady testování potenciálně zranitelného kódu v souboru 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!'

A při přímém připojení v 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>

Hledáme všechny naše zranitelné metody, které jsou uvedeny v zranitelnostech:

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

Úkol: Vyhledávání certifikátů vložených do aplikace

řešení: Není neobvyklé, že aplikace, zejména mobilní, používají certifikáty nebo klíče k přístupu na různé servery nebo k ověření SSL-Pinning. Z hlediska zabezpečení není ukládání takových věcí v kódu nejlepším postupem. Zkusme napsat pravidlo, které bude hledat podobné soubory v úložišti:

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

Úkol: Hledání kompromitovaných tokenů v aplikaci

řešení: Často je nutné zrušit kompromitované tokeny nebo jiné důležité informace, které jsou přítomny v kódu. Ukládat je do zdrojového kódu samozřejmě není dobrý nápad, ale situace se liší. Díky CxQL dotazům je hledání takových věcí docela snadné:

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

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

Závěr

Doufám, že tento článek bude užitečný pro ty, kteří se začínají seznamovat s nástrojem Checkmarx. Snad v tomto návodu najdou něco užitečného i ti, kteří si už delší dobu píší vlastní pravidla.

Bohužel v současné době chybí zdroj, kde by bylo možné získat nové nápady během vývoje pravidel pro Checkmarx. Proto jsme tvořili úložiště na Github, kde budeme umisťovat naši práci, aby v ní každý, kdo používá CxQL, našel něco užitečného a měl také možnost sdílet svou práci s komunitou. Úložiště je v procesu plnění a strukturování obsahu, takže přispěvatelé jsou vítáni!

Спасибо за внимание!

Zdroj: www.habr.com

Přidat komentář