Jak pisać reguły dla Checkmarxa, nie zwariując

Hej Habra!

W naszej pracy nasza firma bardzo często zajmuje się różnymi narzędziami do analizy kodu statycznego (SAST). Po wyjęciu z pudełka wszystkie działają przeciętnie. Oczywiście wszystko zależy od projektu i zastosowanych w nim technologii, a także od tego, jak dobrze te technologie są objęte zasadami analizy. Moim zdaniem jednym z najważniejszych kryteriów przy wyborze narzędzia SAST jest możliwość dostosowania go do specyfiki swoich aplikacji, czyli pisania i zmiany reguł analizy lub, jak się je częściej nazywa, Zapytań Niestandardowych.

Jak pisać reguły dla Checkmarxa, nie zwariując

Najczęściej używamy Checkmarx - bardzo ciekawego i potężnego analizatora kodu. W tym artykule opowiem o swoim doświadczeniu w pisaniu dla niego reguł analizy.

Spis treści

Wejście

Na początek chciałbym polecić jeden z niewielu artykułów w języku rosyjskim na temat funkcji pisania zapytań dla Checkmarx. Ukazał się na Habré pod koniec 2019 roku pod tytułem: „Witam, Checkmarx!” Jak napisać zapytanie Checkmarx SAST i znaleźć fajne luki.

Szczegółowo analizuje sposób pisania pierwszych zapytań w języku CxQL (Checkmarx Query Language) dla niektórych aplikacji testowych i pokazuje podstawowe zasady działania reguł analizy.

Nie będę powtarzał tego, co jest w nim opisane, chociaż niektóre skrzyżowania nadal będą obecne. W moim artykule postaram się stworzyć swego rodzaju „zbiór przepisów”, listę rozwiązań konkretnych problemów, które napotkałem podczas pracy z Checkmarxem. Musiałem się mocno zastanowić nad wieloma z tych problemów. Czasami dokumentacja nie zawierała wystarczających informacji, a czasami nawet trudno było zrozumieć, jak zrobić to, co jest wymagane. Mam nadzieję, że moje doświadczenie i nieprzespane noce nie pójdą na marne, a ten „zbiór przepisów na zapytania niestandardowe” pozwoli Ci zaoszczędzić kilka godzin lub kilka komórek nerwowych. A więc zaczynajmy!

Ogólne informacje o zasadach

Najpierw przyjrzyjmy się kilku podstawowym pojęciom i procesowi pracy z regułami, aby lepiej zrozumieć, co stanie się dalej. A także dlatego, że dokumentacja nic na ten temat nie mówi lub jest bardzo rozproszona w strukturze, co nie jest zbyt wygodne.

  1. Reguły są stosowane podczas skanowania w zależności od ustawienia wybranego na początku (zestawu aktywnych reguł). Możesz utworzyć nieograniczoną liczbę presetów, a ich struktura zależy od specyfiki Twojego procesu. Możesz pogrupować je według języka lub wybrać ustawienia wstępne dla każdego projektu. Liczba aktywnych reguł wpływa na szybkość i dokładność skanowania.

    Jak pisać reguły dla Checkmarxa, nie zwariującKonfigurowanie ustawień wstępnych w interfejsie Checkmarx

  2. Edycję reguł przeprowadza się w specjalnym narzędziu o nazwie CxAuditor. Jest to aplikacja komputerowa, która łączy się z serwerem, na którym działa Checkmarx. Narzędzie to posiada dwa tryby działania: edycję reguł oraz analizę wyników już wykonanego skanowania.

    Jak pisać reguły dla Checkmarxa, nie zwariującInterfejs CxAudit

  3. Reguły w Checkmarx są podzielone według języka, co oznacza, że ​​każdy język ma swój własny zestaw zapytań. Istnieją również pewne ogólne zasady, które obowiązują niezależnie od języka, są to tzw. zapytania podstawowe. W większości przypadków podstawowe zapytania polegają na wyszukiwaniu informacji używanych przez inne reguły.

    Jak pisać reguły dla Checkmarxa, nie zwariującPodział reguł według języka

  4. Reguły to „wykonywalne” i „niewykonywalne” (wykonane i niewykonane). Moim zdaniem nie do końca trafna nazwa, ale tak właśnie jest. Najważniejsze jest to, że wynik wykonania reguł „wykonywalnych” zostanie wyświetlony w wynikach skanowania w interfejsie użytkownika, a reguły „niewykonywalne” są potrzebne tylko do wykorzystania ich wyników w innych żądaniach (w istocie jest to tylko funkcja).

    Jak pisać reguły dla Checkmarxa, nie zwariującOkreślanie typu reguły podczas tworzenia

  5. Możesz tworzyć nowe reguły lub uzupełniać/przepisywać istniejące. Aby przepisać regułę należy odnaleźć ją w drzewie, kliknąć prawym przyciskiem myszy i z rozwijanego menu wybrać opcję „Zastąp”. Należy tutaj pamiętać, że nowe reguły nie są początkowo uwzględnione w ustawieniach wstępnych i nie są aktywne. Aby zacząć z nich korzystać należy je aktywować w menu „Preset Manager” w instrumencie. Przepisane reguły zachowują swoje ustawienia, czyli jeśli reguła była aktywna, tak pozostanie i zostanie natychmiast zastosowana.

    Jak pisać reguły dla Checkmarxa, nie zwariującPrzykład nowej reguły w interfejsie Preset Managera

  6. Podczas wykonywania budowane jest „drzewo” żądań, które zależy od czego. W pierwszej kolejności wykonywane są reguły zbierające informacje, w drugiej kolejności ci, którzy z nich korzystają. Wynik wykonania jest buforowany, więc jeśli istnieje możliwość wykorzystania wyników istniejącej reguły, to lepiej to zrobić, skróci to czas skanowania.

  7. Reguły można stosować na różnych poziomach:

  • Dla całego systemu - będzie używany do dowolnego skanowania dowolnego projektu

  • Na poziomie zespołu (Team) – będzie używany wyłącznie do skanowania projektów w wybranym zespole.

  • Na poziomie projektu — zostanie zastosowany w konkretnym projekcie

    Jak pisać reguły dla Checkmarxa, nie zwariującOkreślenie poziomu, na którym reguła będzie stosowana

„Słownik” dla początkujących

A zacznę od kilku rzeczy, które wzbudziły we mnie pytania, a także pokażę szereg technik, które znacząco ułatwią życie.

Operacje na listach

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

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

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

Wszystkie znalezione przedmioty

W ramach zeskanowanego języka możesz uzyskać listę absolutnie wszystkich elementów, które Checkmarx zidentyfikował (łańcuchy, funkcje, klasy, metody itp.). To pewna przestrzeń obiektów, przez którą można uzyskać dostęp All. To znaczy, aby wyszukać obiekt o określonej nazwie searchMe, możesz wyszukiwać na przykład według nazwy wszystkie znalezione obiekty:

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

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

Jeśli jednak potrzebujesz wyszukać w innym języku, który z jakiegoś powodu nie został uwzględniony w skanie (na przykład groovy w projekcie na Androida), możesz rozszerzyć naszą przestrzeń obiektową za pomocą zmiennej:

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

Funkcje analizy przepływu

Funkcje te są używane w wielu regułach, a oto mała ściągawka wyjaśniająca ich znaczenie:

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

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

Pobieranie nazwy/ścieżki pliku

Z wyników zapytania można uzyskać kilka atrybutów (nazwa pliku, w którym znaleziono wpis, ciąg znaków itp.), ale dokumentacja nie mówi, jak je uzyskać i wykorzystać. Aby to zrobić, musisz uzyskać dostęp do właściwości LinePragma, a potrzebne nam obiekty będą się w niej znajdować:

// Для примера найдем все методы
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 o tym pamiętać FileName zawiera właściwie ścieżkę do pliku, ponieważ zastosowaliśmy tę metodę GetFirstGraph.

Wynik wykonania

Wewnątrz CxQL znajduje się specjalna zmienna result, która zwraca wynik wykonania zapisanej reguły. Jest inicjowany natychmiastowo i można w nim zapisywać wyniki pośrednie, zmieniając je i udoskonalając w trakcie pracy. Ale jeśli w regule nie ma przypisania do tej zmiennej lub funkcji return— wynik wykonania będzie zawsze wynosić zero.

Poniższe zapytanie w wyniku wykonania nic nam nie zwróci i zawsze będzie puste:

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

Ale po przypisaniu wyniku wykonania do wyniku magicznej zmiennej zobaczymy, co zwróci nam to wywołanie:

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

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

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

Korzystanie z wyników innych reguł

Reguły w Checkmarxie można wywołać analogicznie do funkcji w zwykłym języku programowania. Pisząc regułę, możesz równie dobrze wykorzystać wyniki innych zapytań. Na przykład nie ma potrzeby każdorazowego wyszukiwania w kodzie wszystkich wywołań metod, wystarczy wywołać żądaną regułę:

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

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

Takie podejście pozwala skrócić kod i znacząco skrócić czas wykonania reguły.

Rozwiązywanie problemów

Logowanie

Podczas pracy z narzędziem czasami nie da się od razu napisać żądanego zapytania i trzeba poeksperymentować, wypróbowując różne opcje. W takim przypadku narzędzie zapewnia logowanie, które nazywa się następująco:

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

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

Warto jednak pamiętać, że ta metoda akceptuje tylko dane wejściowe strunowy, więc w wyniku pierwszej operacji nie będzie możliwe wyświetlenie pełnej listy znalezionych elementów. Drugą opcją używaną do debugowania jest od czasu do czasu przypisanie do magicznej zmiennej result wynik zapytania i zobacz, co się stanie. To podejście nie jest zbyt wygodne; musisz mieć pewność, że w kodzie nie ma żadnych zastąpień ani operacji z tym związanych result lub po prostu skomentuj poniższy kod. Możesz też, tak jak ja, zapomnieć o usunięciu kilku takich wywołań z gotowej reguły i zastanawiać się, dlaczego nic nie działa.

Wygodniejszym sposobem jest wywołanie metody return z wymaganym parametrem. W takim przypadku wykonanie reguły zakończy się i będziemy mogli zobaczyć, co się stało w wyniku tego, co napisaliśmy:

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

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

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

Problem z logowaniem

Zdarzają się sytuacje, gdy nie można uzyskać dostępu do narzędzia CxAudit (które służy do pisania reguł). Przyczyn tego może być wiele, w tym awarie, nagłe aktualizacje systemu Windows, BSOD i inne nieprzewidziane sytuacje, na które nie mamy wpływu. Czasami w bazie danych znajduje się niezakończona sesja, która uniemożliwia ponowne zalogowanie. Aby to naprawić, musisz uruchomić kilka zapytań:

Dla Checkmarx przed wersją 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');

Zasady pisania

Teraz dochodzimy do najciekawszej części. Kiedy zaczynasz pisać reguły w CxQL, często brakuje Ci nie tyle dokumentacji, ile żywych przykładów rozwiązania określonych problemów i opisania procesu działania zapytań w ogóle.

Postaram się ułatwić życie tym, którzy zaczynają zagłębiać się w język zapytań i podam kilka przykładów wykorzystania zapytań niestandardowych do rozwiązania pewnych problemów. Niektóre z nich są dość ogólne i można je zastosować w Twojej firmie praktycznie bez zmian, inne są bardziej szczegółowe, ale można je również zastosować dostosowując kod do specyfiki Twoich aplikacji.

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

Zadanie: W wynikach wykonania reguły znajduje się kilka Przepływów i jeden z nich jest zagnieżdżeniem drugiego, jeden z nich należy opuścić.

rozwiązanie: Rzeczywiście, czasami Checkmarx pokazuje kilka przepływów danych, które mogą się nakładać i być skróconą wersją innych. Na takie przypadki istnieje specjalna metoda Zmniejsz przepływ. W zależności od parametru wybierze najkrótszy lub najdłuższy przepływ:

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

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

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

rozwiązanie: Checkmarx ma podstawowe reguły, których wyniki są wykorzystywane przez wiele innych zapytań. Uzupełniając niektóre z tych reguł o dane specyficzne dla Twojej aplikacji, możesz od razu poprawić wyniki skanowania. Poniżej znajduje się przykładowa reguła na początek:

Lista_naruszeń_ogólnej_prywatności

Dodajmy kilka zmiennych, które są wykorzystywane 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: Rozwiń listę zmiennych z hasłami

rozwiązanie: Radziłbym od razu zwrócić uwagę na podstawową zasadę definiowania haseł w kodzie i dodać do tego listę nazw zmiennych, które są powszechnie używane w Twojej firmie.

Lista_naruszeń_ochrony_prywatności

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

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

rozwiązanie: Wszystkie zapytania w Checkmarx są podzielone według języka, dlatego musisz dodać reguły dla każdego języka. Poniżej kilka przykładów takich zasad.

Jeżeli stosowane są biblioteki uzupełniające lub zastępujące standardową funkcjonalność, można je łatwo dodać do podstawowej reguły. Wtedy każdy, kto z niego skorzysta, od razu dowie się o nowościach. Przykładowo biblioteki do logowania w systemie Android to Timber i Loggi. W pakiecie podstawowym nie ma żadnych zasad identyfikacji wywołań niesystemowych, więc jeśli do logu trafi hasło lub identyfikator sesji, nie będziemy o tym wiedzieć. Spróbujmy dodać definicje takich metod do reguł Checkmarxa.

Przykład kodu testowego wykorzystującego bibliotekę Timber do logowania:

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 oto przykład żądania dla Checkmarxa, które pozwoli Ci dodać definicję wywoływania metod Timbera jako punktu wyjścia danych z aplikacji:

Znajdź wyniki Androida

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

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

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

Możesz też dodać do sąsiedniej reguły, ale ta dotyczy bezpośrednio logowania do Androida:

ZnajdźAndroidLog_Outputs

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

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

Ponadto, jeśli aplikacje na Androida korzystają Menedżer pracy w przypadku pracy asynchronicznej warto dodatkowo poinformować o tym Checkmarxa dodając metodę pobierania danych z zadania getInputData:

ZnajdźAndroidPrzeczytaj

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

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

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

Zadanie: Wyszukiwanie wrażliwych danych w pliście dla projektów iOS

rozwiązanie: iOS często używa specjalnych plików z rozszerzeniem .plist do przechowywania różnych zmiennych i wartości. Nie zaleca się przechowywania w tych plikach haseł, tokenów, kluczy i innych wrażliwych danych, gdyż można je bez problemu wydobyć z urządzenia.

Pliki Plist mają funkcje, które nie są widoczne gołym okiem, ale są ważne dla Checkmarx. Napiszmy regułę, która wyszuka potrzebne nam dane i powie nam, czy gdzieś są wspomniane hasła lub tokeny.

Przykład takiego pliku, który zawiera token do komunikacji z usługą backendową:

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

I zasada dla Checkmarxa, która ma kilka niuansów, które należy wziąć pod uwagę podczas pisania:

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

rozwiązanie: Checkmarx posiada bardzo wygodne funkcje do pracy z XML i wyszukiwania wartości, znaczników, atrybutów i nie tylko. Ale niestety wystąpił błąd w dokumentacji, przez który nie działa ani jeden przykład. Pomimo tego, że w najnowszej wersji dokumentacji usunięto tę wadę, należy zachować ostrożność w przypadku korzystania z 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 All nie ma takiej metody... I to prawda, gdyż istnieje specjalna, wydzielona przestrzeń obiektowa do wykorzystania funkcji do pracy z XML-em - cxXPath. Tak wygląda prawidłowe zapytanie, aby znaleźć ustawienie w Androidzie, które pozwala na wykorzystanie ruchu HTTP:

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

Przyjrzyjmy się temu bardziej szczegółowo, ponieważ składnia wszystkich funkcji jest podobna, po znalezieniu jednej wystarczy wybrać tę, której potrzebujesz. Zatem po kolei według parametrów:

  • "*.xml"— maska ​​plików do przeszukania

  • 8 — identyfikator języka, do którego stosowana jest reguła

  • "cleartextTrafficPermitted"— nazwa atrybutu w formacie XML

  • "true" — wartość tego atrybutu

  • false — użycie wyrażeń regularnych podczas wyszukiwania

  • true — oznacza, że ​​wyszukiwanie zostanie przeprowadzone z pominięciem wielkości liter, czyli bez uwzględniania wielkości liter

Jako przykład wykorzystaliśmy regułę identyfikującą nieprawidłowe, z punktu widzenia bezpieczeństwa, ustawienia połączeń sieciowych w systemie Android, które umożliwiają komunikację z serwerem poprzez protokół HTTP. Przykład ustawienia zawierającego atrybut cleartextTrafficPermitted ze znaczeniem 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>

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

rozwiązanie: W jednym z dużych projektów związanych z rozwojem aplikacji mobilnej na Androida natknęliśmy się na fałszywe alarmy reguły określającej ustawienie zaciemniania. Faktem jest, że reguła nieszablonowa przeszukuje plik build.gradle ustawienie odpowiedzialne za stosowanie reguł zaciemniania dla wydanej wersji aplikacji.

Ale w dużych projektach czasami istnieją pliki podrzędne build.gradle, które odnoszą się do bibliotek zawartych w projekcie. Osobliwością jest to, że nawet jeśli te pliki nie wskazują na potrzebę zaciemniania, podczas kompilacji zostaną zastosowane ustawienia nadrzędnego pliku zestawu.

Zatem zadaniem jest odcięcie wyzwalaczy w plikach podrzędnych należących do bibliotek. Można je rozpoznać po obecności linii apply 'com.android.library'.

Przykładowy kod z pliku build.gradle, co określa potrzebę zaciemniania:

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 zawartej w projekcie, która nie ma tego ustawienia:

apply plugin: 'android-library'

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

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

I zasada Checkmarxa:

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ć dość uniwersalne i przydatne nie tylko w przypadku aplikacji na Androida, ale także w innych przypadkach, gdy trzeba ustalić, czy wynik należy do konkretnego pliku.

Zadanie: Dodaj obsługę biblioteki strony trzeciej, 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 nieprawdopodobna. Oczywiście Checkmarx nie zawsze wie o ich istnieniu, a naszym zadaniem jest nauczyć go rozumieć, że pewne metody należą specyficznie do tego frameworka. Czasami komplikuje to fakt, że frameworki używają nazw funkcji, które są bardzo powszechne i nie da się jednoznacznie określić powiązania konkretnego wywołania z konkretną biblioteką.

Trudność polega na tym, że składnia takich bibliotek nie zawsze jest poprawnie rozpoznawana i trzeba eksperymentować, aby uniknąć dużej liczby fałszywych alarmów. Istnieje kilka opcji poprawy dokładności skanowania i rozwiązania problemu:

  • W przypadku pierwszej opcji wiemy na pewno, że biblioteka jest wykorzystywana w konkretnym projekcie i możemy zastosować regułę na poziomie zespołu. Jeśli jednak zespół zdecyduje się na inne podejście lub skorzysta z kilku bibliotek, w których nazwy funkcji nakładają się na siebie, możemy uzyskać niezbyt przyjemny obraz licznych fałszywych alarmów

  • Drugą opcją jest wyszukanie plików, w których biblioteka jest wyraźnie zaimportowana. Dzięki takiemu podejściu możemy być pewni, że potrzebna nam biblioteka jest dokładnie wykorzystana w tym pliku.

  • Trzecią opcją jest jednoczesne użycie dwóch powyższych podejść.

Jako przykład spójrzmy na bibliotekę dobrze znaną w wąskich kręgach przysiek dla języka programowania Scala, a mianowicie funkcjonalności Łączenie wartości literału. Ogólnie rzecz biorąc, aby przekazać parametry do zapytania SQL, należy użyć operatora $, który podstawia dane do wcześniej utworzonego zapytania SQL. Oznacza to, że jest to bezpośredni odpowiednik przygotowanej instrukcji w Javie. Jeśli jednak chcesz dynamicznie konstruować zapytanie SQL, na przykład jeśli chcesz przekazać nazwy tabel, możesz użyć operatora #$, co bezpośrednio podstawi dane do zapytania (prawie jak łączenie ciągów).

Przykładowy kod:

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

Checkmarx nie wie jeszcze, jak wykryć użycie wartości literału splicingu i pomija operatory #$, spróbujmy więc nauczyć go rozpoznawania potencjalnych zastrzyków SQL i zaznaczania 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: Szukaj używanych, wrażliwych funkcji w bibliotekach Open-Source

rozwiązanie: Wiele firm korzysta z narzędzi monitorujących Open Source (praktyka OSA) w celu wykrywania wykorzystania podatnych na ataki wersji bibliotek w tworzonych aplikacjach. Czasami nie jest możliwa aktualizacja takiej biblioteki do wersji bezpiecznej. W niektórych przypadkach występują ograniczenia funkcjonalne, w innych w ogóle nie ma bezpiecznej wersji. W takim przypadku połączenie praktyk SAST i OSA pomoże ustalić, czy w kodzie nie zostały użyte funkcje, które prowadzą do wykorzystania luki.

Czasami jednak, zwłaszcza biorąc pod uwagę JavaScript, może to nie być całkowicie trywialne zadanie. Poniżej rozwiązanie, może nie idealne, ale jednak działające, na przykładzie luk w komponencie lodash w metodach template и *set.

Przykłady testowania potencjalnie podatnego na ataki kodu 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 podczas łączenia bezpośrednio w 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>

Poszukujemy 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 certyfikatów osadzonych w aplikacji

rozwiązanie: Nierzadko zdarza się, że aplikacje, zwłaszcza mobilne, korzystają z certyfikatów lub kluczy w celu uzyskania dostępu do różnych serwerów lub weryfikacji przypinania SSL. Z punktu widzenia bezpieczeństwa przechowywanie takich rzeczy w kodzie nie jest najlepszą praktyką. Spróbujmy napisać regułę, która będzie wyszukiwała podobne pliki 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: Znalezienie skompromitowanych tokenów w aplikacji

rozwiązanie: Często konieczne jest unieważnienie skompromitowanych tokenów lub innych ważnych informacji zawartych w kodzie. Oczywiście przechowywanie ich w kodzie źródłowym nie jest dobrym pomysłem, ale sytuacje są różne. Dzięki zapytaniom CxQL znalezienie takich rzeczy jest dość łatwe:

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

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

wniosek

Mam nadzieję, że ten artykuł będzie przydatny dla tych, którzy rozpoczynają swoją przygodę z narzędziem Checkmarx. Być może ci, którzy od dłuższego czasu piszą własne zasady, również znajdą w tym poradniku coś przydatnego.

Niestety, obecnie brakuje zasobów, w których można by zebrać nowe pomysły podczas opracowywania zasad dla Checkmarx. Dlatego stworzyliśmy repozytorium na Githubie, gdzie będziemy publikować naszą twórczość, aby każdy korzystający z CxQL mógł znaleźć w niej coś przydatnego, a także miał możliwość podzielenia się swoją pracą ze społecznością. Repozytorium jest w trakcie wypełniania i porządkowania treści, więc współautorzy są mile widziani!

Dziękuję za uwagę!

Źródło: www.habr.com

Dodaj komentarz