Jak napisać zasady dla Checkmarx i nie zwariować

Cześć, Habr!

W swojej pracy nasza firma często korzysta z różnych narzędzi do statycznej analizy kodu (SAST). Z pudełka większość z nich działa średnio. Oczywiście, wszystko zależy od projektu i używanych w nim technologii, a także od tego, jak dobrze technologie te są pokryte regułami analizy. Moim zdaniem, jednym z najważniejszych kryteriów przy wyborze narzędzia SAST jest możliwość dostosowania go do specyfiki swoich aplikacji, a mianowicie pisania i zmieniania reguł analizy, znanych częściej jako Niestandardowe Zapytania.

Jak napisać zasady dla Checkmarx i nie zwariować

Najczęściej używamy Checkmarx — bardzo interesującego i potężnego analizatora kodu. W tym artykule przedstawię swoje doświadczenia związane z pisaniem reguł analizy dla tego narzędzia.

Spis treści

Wprowadzenie

Na początek chciałbym polecić jeden z niewielu artykułów w języku rosyjskim dotyczących specyfiki pisania zapytań dla Checkmarx. Został opublikowany na Habrze pod koniec 2019 roku pod tytułem: „Witaj, Checkmarx! Jak napisać zapytanie dla Checkmarx SAST i znaleźć fajne podatności.

W artykule szczegółowo opisano, jak napisać pierwsze zapytania w języku CxQL (Checkmarx Query Language) dla przykładowej aplikacji oraz przedstawiono podstawowe zasady działania reguł analizy.

Nie zamierzam powtarzać tego, co tam opisano, chociaż niektóre przytoczone elementy będą się pokrywać. W swoim artykule postaram się stworzyć pewien „zbiór przepisów”, listę rozwiązań konkretnych problemów, z jakimi się spotkałem podczas mojej pracy z Checkmarx. Nad wieloma z tych zadań musiałem się nieco nagłowić. Czasami brakowało mi danych w dokumentacji, a czasem trudno było zrozumieć, jak zrealizować to, co było potrzebne. Mam nadzieję, że moje doświadczenie i nieprzespane noce nie będą stracone na marne, a ten „zbiór przepisów Niestandardowych Zapytania” zaoszczędzi wam kilka godzin lub parę nerwowych komórek. Zaczynajmy!

Informacje ogólne na temat reguł

Na początek omówmy kilka podstawowych pojęć i proces współpracy z zasadami, aby lepiej zrozumieć, co wydarzy się dalej. I jeszcze dlatego, że w dokumentacji nie jest to jasno opisane lub jest mocno rozproszone po strukturze, co jest dość niewygodne.

  1. Zasady są stosowane podczas skanowania w zależności od wybranego na początku presetu (zbioru aktywnych zasad). Można stworzyć nieograniczoną liczbę presetów, a to, jak je uporządkujesz, zależy od szczególności twojego procesu. Można je pogrupować według języków lub wyodrębnić presety dla każdego projektu. Liczba aktywnych zasad wpływa na szybkość i dokładność skanowania.

    Jak napisać zasady dla Checkmarx i nie zwariowaćUstawienia Preset w interfejsie Checkmarx

  2. Zasady są edytowane w specjalnym narzędziu o nazwie CxAuditor. To aplikacja desktopowa, która łączy się z serwerem Checkmarx. Narzędzie to ma dwa tryby pracy: edytowanie zasad i analizowanie wyników już przeprowadzonego skanowania.

    Jak napisać zasady dla Checkmarx i nie zwariowaćInterfejs CxAudit

  3. Zasady w Checkmarx są podzielone według języków, co oznacza, że dla każdego języka istnieje własny zestaw zapytań. Istnieją również pewne ogólne zasady, które stosują się niezależnie od języka, tzw. zasady podstawowe. W większości przypadków zasady podstawowe zawierają wyszukiwanie informacji, które są używane przez inne zasady.

    Jak napisać zasady dla Checkmarx i nie zwariowaćPodział zasad według języków

  4. Zasady dzielą się na „Wykonywalne” i „Niewykonywalne”. To nie do końca trafna nazwa, moim zdaniem, ale taka jest rzeczywistość. Istota polega na tym, że wyniki wykonania zasad „Wykonywalnych” będą wyświetlane w wynikach skanowania w UI, podczas gdy zasady „Niewykonywalne” są potrzebne tylko do wykorzystania ich wyników w innych zapytaniach (w zasadzie to po prostu funkcja).

    Jak napisać zasady dla Checkmarx i nie zwariowaćOkreślenie typu zasady przy tworzeniu

  5. Można tworzyć nowe zasady lub uzupełniać/przepisować istniejące. Aby przepisać zasadę, trzeba ją znaleźć w drzewie, kliknąć prawym przyciskiem i w menu kontekstowym wybrać opcję „Override“. Ważne jest, aby pamiętać, że nowe zasady początkowo nie są włączone w presetach i nie są aktywne. Aby rozpocząć ich używanie, należy je aktywować w menu „Preset Manager” w narzędziu. Przepisane zasady zachowują swoje ustawienia, co oznacza, że jeśli zasada była aktywna, takim pozostanie i będzie stosowana od razu.

    Jak napisać zasady dla Checkmarx i nie zwariowaćPrzykład nowej zasady w interfejsie Menedżera presetów

  6. W trakcie wykonywania budowane jest „drzewo” zapytań, co od czego zależy. Najpierw wykonywane są zasady zbierające informacje, a następnie te, które je wykorzystują. Wyniki wykonania są buforowane, więc jeśli istnieje możliwość wykorzystania wyników istniejącej zasady, lepiej to zrobić, ponieważ pozwala to skrócić czas skanowania.

  7. Zasady można stosować na różnych poziomach:

  • Na poziomie systemu — będzie używana do skanowania każdego projektu.

  • Na poziomie zespołu (Team) — będzie stosowana tylko do skanowania projektów w wybranym zespole.

  • Na poziomie projektu — będzie stosowana w konkretnym projekcie

    Jak napisać zasady dla Checkmarx i nie zwariowaćOkreślenie poziomu, na którym będzie stosowana zasada

„Słownik“ dla początkującego

I zacznę od kilku rzeczy, które budziły moje wątpliwości, a także pokażę szereg trików, które znacznie ułatwią życie.

Operacje na listach

- Odejmowanie jednego od drugiego (list2 - list1)
* Przecięcie list (list1 * list2)
+ Dodawanie list (list1 + list2)

& (logiczne AND) - łączy listy przez zgodność (list1 & list2), podobnie jak przecięcie (list1 * list2)
| (logiczne OR) - łączy listy przez szerokie wyszukiwanie (list1 | list2)

Z listami nie działa: ^  &&  ||  %  / 

Wszystkie znalezione elementy

W ramach skanowanego języka można uzyskać listę wszystkich elementów określonych przez Checkmarx (linie, funkcje, klasy, metody itp.). To pewna przestrzeń obiektów, do których można się odwołać przez Wszystko. To znaczy, aby znaleźć obiekt o konkretnej nazwie searchMe, można przeprowadzić wyszukiwanie, na przykład po nazwie wśród wszystkich znalezionych obiektów:

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

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

Jednak jeśli trzeba przeprowadzić wyszukiwanie w innym języku, który z jakichś powodów nie został uwzględniony w skanowaniu (na przykład groovy w projekcie dla Androida), można rozszerzyć naszą przestrzeń obiektów przez zmienną:

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

Funkcje do analizy Flow

Funkcje te są używane w wielu zasadach i oto mała ściągawka, co one oznaczają:

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

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

Uzyskiwanie nazwy/ścieżki pliku

Istnieje kilka atrybutów, które można uzyskać z wyników wykonania zapytania (nazwa pliku, w którym znaleziono wystąpienie, linia itp.), ale jak je uzyskać i wykorzystać w dokumentacji nie zostało podane. Aby to zrobić, należy odwołać się do właściwości LinePragma, a wewnątrz niej będą znajdować się potrzebne obiekty:

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

Warto pamiętać, że NazwaPliku ten faktycznie zawiera ścieżkę do pliku, ponieważ użyliśmy metody GetFirstGraph.

Wynik wykonania

Wewnątrz CxQL znajduje się specjalna zmienna result, która zwraca wynik wykonania napisanego przez Ciebie reguły. Jest inicjalizowana od razu i możesz zapisywać w niej tymczasowe wyniki, zmieniając i doprecyzowując je w trakcie pracy. Jeśli jednak wewnątrz reguły nie będzie przypisania tej zmiennej lub funkcji return— wynik wykonania zawsze będzie zerowy.

Następujące zapytanie nie zwróci nam nic w wyniku wykonania i zawsze będzie puste:

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

Jednak przypisując wynik wykonania do magicznej zmiennej result — zobaczymy, co zwraca to wywołanie:

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

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

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

Wykorzystanie wyników wykonania innych reguł

Reguły w Checkmarx można porównać do funkcji w zwykłym języku programowania. Pisząc regułę, możesz z powodzeniem korzystać z wyników innych zapytań. Na przykład, nie ma potrzeby za każdym razem wyszukiwać wszystkich wywołań metod w kodzie, wystarczy wywołać odpowiednią regułę:

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

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

Takie podejście pozwala skrócić kod i znacznie zmniejszyć czas wykonania reguły.

Rozwiązywanie problemów

Rejestrowanie

Podczas pracy z narzędziem, czasami nie udaje się od razu napisać odpowiedniego zapytania i trzeba eksperymentować, próbując różnych opcji. Dla takiego przypadku w narzędziu przewidziano logowanie, które wywołuje się w następujący sposób:

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

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

Ale warto pamiętać, że ten metod przyjmuje tylko ciąg, więc nie uda się uzyskać pełnej listy znalezionych elementów w wyniku wykonania pierwszej operacji. Drugą opcją, która jest używana do debugowania — jest od czasu do czasu przypisywać magicznej zmiennej result wynik wykonania zapytania i zobaczyć, co z tego wyjdzie. Takie podejście nie jest zbyt wygodne, trzeba być pewnym, że w kodzie później nie ma nadpisania lub operacji z tym result lub po prostu komentować poniższy kod. Albo można, jak ja, zapomnieć usunąć kilka takich wywołań z gotowej reguły i zastanawiać się, dlaczego nic nie działa.

Bardziej wygodny sposób — to wywołać metod return z odpowiednim parametrem. W takim przypadku wykonanie reguły zakończy się i zobaczymy, co udało się osiągnąć w wyniku naszego pisania:

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

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

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

Problem z logowaniem

Są sytuacje, gdy nie można uzyskać dostępu do narzędzia CxAudit (które służy do pisania reguł). Przyczyn może być wiele: nagłe zakończenie pracy, niespodziewana aktualizacja Windows, BSOD oraz inne nieprzewidziane sytuacje, na które nie mamy wpływu. W takim przypadku czasami pozostaje niezakończona sesja w bazie danych, która uniemożliwia ponowne zalogowanie się. Aby to naprawić, należy wykonać kilka zapytań:

Dla Checkmarx do 8.6:

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

Dla Checkmarx po 8.6:

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

Pisanie reguł

Teraz przechodzimy do najciekawszego. Gdy zaczynasz pisać reguły w CxQL, często brakuje nie tylko dokumentacji, ale także żywych przykładów rozwiązania konkretnych zadań oraz opisu procesu działania zapytań w ogóle.

Postaram się nieco uprościć życie tym, którzy zaczynają zgłębiać język zapytań, i przedstawię kilka przykładów użycia Custom Queries do rozwiązania określonych zadań. Niektóre z nich są dość ogólne i można je zastosować w Twojej firmie praktycznie bez zmian, inne są bardziej specyficzne, ale również można je wykorzystać, modyfikując kod zgodnie ze specyfiką Twoich aplikacji.

Oto zadania, z którymi najczęściej się spotykaliśmy:

Zadanie: W wynikach wykonania reguły znajduje się kilka Flow, z których jeden jest podwieszony w stosunku do drugiego; należy pozostawić jeden z nich.

Rozwiązanie: Rzeczywiście, czasami Checkmarx pokazuje kilka Flow przepływu danych, które mogą się nakładać i być skróconą wersją innych. Dla takich przypadków istnieje specjalna metoda ReduceFlow. W zależności od parametru wybierze najkrótszy lub najdłuższy Flow:

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

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

Zadanie: Rozszerzyć listę wrażliwych danych, na które reaguje narzędzie

Rozwiązanie: W Checkmarx istnieją podstawowe reguły, których wyniki są wykorzystywane przez wiele innych zapytań. Uzupełniając niektóre z takich reguł danymi specyficznymi dla Twojej aplikacji, można od razu poprawić wyniki skanowania. Poniżej przykład reguły, od której można zacząć:

General_privacy_violation_list

Dodamy kilka zmiennych, które są używane w naszej aplikacji do przechowywania wrażliwych informacji:

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

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

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

Zadanie: Rozszerzyć listę zmiennych z hasłami

Rozwiązanie: Zalecałbym zwrócenie uwagi na podstawową zasadę określania haseł w kodzie oraz dodanie do niej listy nazw zmiennych, które są powszechnie używane w Twojej firmie.

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

// Uzupełniamy domyślną listę zmiennych
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);
	}
}

Zadanie: Dodaj używane frameworki, które nie są wspierane przez Checkmarx

Rozwiązanie: Wszystkie zapytania w Checkmarx są podzielone według języków, dlatego zasady należy uzupełniać dla każdego języka. Poniżej kilka przykładów takich zasad.

Jeśli używane są biblioteki, które rozszerzają lub zastępują standardową funkcjonalność — łatwo dodać je do podstawowej zasady. Wtedy wszyscy, którzy z niej korzystają, od razu dowiedzą się o nowych wymaganiach. Na przykład, biblioteki do logowania w Androidzie — Timber i Loggi. W podstawowym zestawie zasad nie ma definicji niesystemowych wywołań, więc jeśli hasło lub identyfikator sesji trafi do logu, nie dowiemy się o tym. Spróbujmy dodać do zasad Checkmarx definicje takich metod.

Przykład testowego kodu, który używa biblioteki Timber do logowania:

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

Oto przykład zapytania dla Checkmarx, które pozwoli dodać definicję wywołania metod Timber jako punktu wyjścia danych z aplikacji:

FindAndroidOutputs

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

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

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

Można również uzupełnić sąsiednie zasady, które dotyczą bezpośrednio logowania w Androidzie:

FindAndroidLog_Outputs

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

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

Jeśli w aplikacjach Android używany jest WorkManager do asynchronicznej pracy, warto również poinformować o tym Checkmarx, dodając metodę pobierania danych z zadania getInputData:

FindAndroidRead

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

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

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

Zadanie: Wyszukiwanie wrażliwych danych w plist dla projektów iOS

Rozwiązanie: Często do przechowywania różnych zmiennych i wartości w iOS używa się specjalnych plików z rozszerzeniem .plist. Przechowywanie haseł, tokenów, kluczy i innych wrażliwych danych w tych plikach nie jest zalecane, ponieważ mogą być one łatwo wydobyte z urządzenia.

Pliki plist mają cechy, które nie są oczywiste na pierwszy rzut oka, ale są ważne dla Checkmarx. Napiszemy regułę, która będzie wyszukiwać potrzebne nam dane i informować nas, jeśli gdziekolwiek wspomniane są hasła lub tokeny.

Przykład takiego pliku, w którym zaszyty jest token do komunikacji z usługą backend:

DeviceDictionary
	
		phone
		iPhone 6s
		
	privatekey
	MIICXAIBAAKBgQCqGKukO1De7zhZj6+

A zasada dla Checkmarx, w której należy uwzględnić kilka niuansów przy pisaniu:

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

Zadanie: Wyszukiwanie informacji w XML

Rozwiązanie: W Checkmarx są bardzo wygodne funkcje do pracy z XML i wyszukiwania wartości, tagów, atrybutów i innych. Niestety w dokumentacji jest błąd, przez który żaden z przykładów nie działa. Mimo, że w ostatniej wersji dokumentacji ten błąd został naprawiony — bądź ostrożny, jeśli używasz wcześniejszych wersji dokumentów.

Oto niepoprawny przykład z dokumentacji:

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

W wyniku próby wykonania otrzymamy błąd, że Wszystko nie ma takiej metody... I to prawda, ponieważ do korzystania z funkcji do pracy z XML istnieje specjalna, oddzielna przestrzeń obiektów — cxXPath. Oto, jak wygląda poprawne zapytanie w celu znalezienia ustawienia w Androidzie, pozwalającego na użycie ruchu HTTP:

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

Przeanalizujmy to nieco dokładniej, ponieważ składnia wszystkich funkcji jest podobna, po zrozumieniu jednej, wystarczy tylko wybrać odpowiednią. Zatem, kolejno według parametrów:

  • "*.xml"— maska plików, według których należy prowadzić wyszukiwanie

  • 8 — id języka, dla którego stosowane jest zasady

  • "cleartextTrafficPermitted"— nazwa atrybutu w xml

  • "true" — wartość tego atrybutu

  • false — użycie wyrażenia regularnego przy wyszukiwaniu

  • true — oznacza, że wyszukiwanie będzie wykonane z ignorowaniem wielkości liter, czyli case-insensitive

Dla przykładu zastosowano zasadę, która definiuje niepoprawne, z punktu widzenia bezpieczeństwa, ustawienia połączenia sieciowego w Androidzie, które pozwalają na komunikację z serwerem za pomocą protokołu HTTP. Przykład ustawienia, zawierającego atrybut cleartextTrafficPermitted o wartości true:

example.com
        
            
        
        
            secure.example.com

Zadanie: Ogranicz wyniki według nazwy/ścieżki pliku

Rozwiązanie: W jednym z dużych projektów związanych z tworzeniem aplikacji mobilnej na Androida, spotkaliśmy się z fałszywymi alarmami zasady, która definiuje ustawienie obfuskacji. Rzecz w tym, że zasada z pudełka szuka w pliku build.gradle ustawienia, odpowiedzialnego za zastosowanie zasad obfuskacji dla wersji produkcyjnej aplikacji.

Jednak w dużych projektach czasami pojawiają się pliki podrzędne build.gradle, które odnoszą się do bibliotek włączonych do projektu. Cechą charakterystyczną jest to, że nawet jeśli w tych plikach nie wskazano potrzeby obfuskacji, podczas kompilacji stosowane będą ustawienia pliku macierzystego.

Tak więc, zadanie polega na odcięciu alarmów w plikach podrzędnych, które odnoszą się do bibliotek. Można je zidentyfikować po obecności ciągu apply 'com.android.library'.

Przykład kodu z pliku build.gradle, definiującego potrzebę obfuskacji:

apply plugin: 'com.android.application'

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

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

dependencies {
  ...
}

Przykład pliku build.gradle dla biblioteki dołączonej do projektu, która nie ma takiej konfiguracji:

apply plugin: 'android-library'

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

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

I reguła dla 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);
		}
	}
}

Takie podejście może być wystarczająco uniwersalne i przyda się nie tylko dla aplikacji Android, ale także w innych przypadkach, gdy trzeba określić przynależność wyniku do konkretnego pliku.

Zadanie: Dodać wsparcie dla zewnętrznej biblioteki, jeśli składnia nie jest w pełni obsługiwana

Rozwiązanie: Liczba różnych frameworków używanych w procesie pisania kodu jest po prostu ogromna. Oczywiście Checkmarx nie zawsze wie o ich istnieniu, a naszym zadaniem jest nauczyć go rozumienia, że niektóre metody są związane właśnie z tym frameworkiem. Czasami komplikuje to fakt, że frameworki używają nazw funkcji, które są powszechnie spotykane i nie można jednoznacznie określić, czy dane wywołanie należy do konkretnej biblioteki.

Trudność polega na tym, że składnia takich bibliotek nie zawsze jest poprawnie rozpoznawana i trzeba eksperymentować, aby uniknąć dużej liczby fałszywych trafień. Istnieje kilka opcji, aby poprawić dokładność skanowania i rozwiązać postawione zadanie:

  • Pierwsza opcja, wiemy z całą pewnością, że biblioteka jest używana w danym projekcie i możemy zastosować regułę na poziomie zespołu. Ale w przypadku, gdy zespół zdecyduje się na inny sposób lub używa kilku bibliotek, w których pokrywają się nazwy funkcji, możemy otrzymać nieprzyjemny obraz wielu fałszywych trafień.

  • Druga opcja, zastosować wyszukiwanie w plikach, w których wyraźnie następuje import biblioteki. Przy takim podejściu będziemy pewni, że w danym pliku na pewno stosowana jest potrzebna nam biblioteka.

  • I trzecia opcja, to wykorzystanie obu powyższych podejść wspólnie.

Jako przykład omówimy znaną w wąskich kręgach bibliotekę slick dla języka programowania Scala, a dokładniej, funkcjonalność Splicing Literal Values. W ogólnym przypadku, aby przekazać parametry do zapytania SQL, należy użyć operatora $, który wstawia dane do wstępnie sformułowanego zapytania SQL. Oznacza to, że w rzeczywistości jest bezpośrednim odpowiednikiem Prepared Statement w Javie. Jednak w przypadku, gdy konieczne jest dynamiczne konstruowanie zapytania SQL, na przykład gdy trzeba przekazać nazwy tabel, można użyć operatora #$, który bezpośrednio wstawi dane do zapytania (praktycznie jak konkatenacja ciągów).

Przykład kodu:

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

Checkmarx na razie nie potrafi rozpoznać użycia Splicing Literal Values i pomija operatory #$, więc spróbujemy nauczyć go identyfikacji potencjalnych ataków SQL Injection i podświetlania odpowiednich miejsc w kodzie:

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

Zadanie: Wyszukiwanie używanych podatnych funkcji w bibliotekach open-source

Rozwiązanie: W wielu firmach stosuje się narzędzia do kontroli open-source (praktyka OSA), które umożliwiają wykrycie użycia podatnych wersji bibliotek w rozwijanych aplikacjach. Czasami aktualizacja takiej biblioteki do bezpiecznej wersji nie jest możliwa. W niektórych przypadkach istnieją ograniczenia funkcjonalne, w innych nie ma bezpiecznej wersji. W takim przypadku pomoże kombinacja praktyk SAST i OSA, pozwalająca określić, że funkcje prowadzące do wykorzystania podatności nie są używane w kodzie.

Jednak czasami, zwłaszcza w przypadku JavaScript, może to być zadanie nieco bardziej skomplikowane. Poniżej przedstawione jest rozwiązanie, które może nie być idealne, ale mimo wszystko działa, na przykładzie podatności w komponencie (42 tysiące linii kodu, 28 autorów, 2 komiterów, 30 otwartych problemów); w metodach template i *set.

Przykłady kodu potencjalnie podatnego w pliku 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 przy bezpośrednim połączeniu w HTML:

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

Szukamy wszystkich naszych podatnych metod, które są wymienione w podatnościach:

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

Zadanie: Wyszukiwanie wbudowanych w aplikację certyfikatów

Rozwiązanie: Często aplikacje, zwłaszcza mobilne, używają certyfikatów lub kluczy do dostępu do różnych serwerów lub weryfikacji SSL-Pinning. Z punktu widzenia bezpieczeństwa, przechowywanie takich rzeczy w kodzie nie jest najlepszą praktyką. Spróbujemy napisać zasady, które będą szukać podobnych plików w repozytorium:

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

Zadanie: Wyszukiwanie skompromitowanych tokenów w aplikacji

Rozwiązanie: Często zdarza się, że trzeba wycofać skompromitowane tokeny lub inne ważne informacje, które znajdują się w kodzie. Oczywiście, przechowywanie ich w źródłach nie jest najlepszym pomysłem, ale sytuacje bywają różne. Dzięki zapytaniom CxQL łatwo jest znaleźć takie elementy:

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

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

Podsumowanie

Mam nadzieję, że dla tych, którzy zaczynają swoje zapoznanie z narzędziem Checkmarx, ten artykuł będzie pomocny. Może również ci, którzy już od dawna piszą swoje zasady, znajdą coś przydatnego w tym przewodniku.

Niestety, obecnie brakuje źródła, z którego można czerpać nowe pomysły podczas opracowywania reguł dla Checkmarx. Dlatego stworzyliśmy repozytorium na Githubie, gdzie będziemy publikować nasze osiągnięcia, aby każdy, kto używa CxQL, mógł znaleźć w nim coś użytecznego, a także miał możliwość podzielenia się ze społecznością swoimi pracami. Repozytorium jest w trakcie zapełniania i strukturyzacji treści, więc zapraszamy do współpracy!

Dziękuję za uwagę!

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster