Microsoft Sentinel on SIEM/SOAR järjestelmä, jonka tehtävänä on tunnistaa kyberpoikkeamia normaalin liikenteen seasta ja ratkaista niistä aiheutuneet hälytykset. Datalähteinä voi toimia yrityksen pilviresurssit (Entra ID, palvelimet, virtuaalikoneet yms.), päätelaitteet, sovellukset ja on-premises järjestelmät. Kuinka näitä uhkia tunnistetaan analyysisäännöillä (analytic rules), miten analyysisäänötjä luodaan ja miten organisaatio voi pysyä kehittyvien uhkatoimijoiden edellä? Näitä asioita käymme tässä artikkelissa läpi.
Analyysisäännöt
Uhkien tunnistaminen Sentinelissä tapahtuu analyysisääntöjen avulla. Yksi analyysisääntö luodaan aina tietyn tapahtuman tunnistamista varten. Seurattavia tapahtumia on useita, joten luonnollisesti yhdessä Sentinel ympäristössä on useita analyysisääntöjä aktiivisena samanaikaisesti.
Teknisesti analyysisäännöt sisältävät KQL-kielellä (Kusto Query Language) kirjoitetun haun, joka määrittelee mitä lokidataa haetaan ja millä perusteilla tästä datasta luodaan hälytyksiä. Tämä haku suoritetaan Sentinelin alla olevaan Log Analytic Workspaceen, joka siis sisältää kaiken Sentinelissä käsitellyn lokidatan. Yksinkertaistettuna KQL-haku on lista kriteerejä tapahtumille ja kun nämä kriteerit täyttyvät, niin sääntö luo hälytyksen valvontanäkymään.
Esimerkki yksinkertaisesta säännöstä:
Tavoitteena halutaan tietää onko organisaation Entra ID tenanttiin kirjauduttu Suomen ulkopuolelta viimeisen 14 päivän aikana. Tavoitteen pohjalta luodaan KQL-haku, jossa määritellään seuraavat askeleet tapahtuman tunnistamiseksi.
- Haetaan kirjautumislokit
- Määritellään ajanjakso viimeiselle 14 päivälle
- Filtteröidään pois onnistuneet kirjautumiset
- Filtteröidään pois Suomesta tehdyt kirjautumiset
Jos tämä haku tuottaa tuloksia, niin voimme luoda hälytyksen valvontanäkymään.
Esimerkki koodina:
SigninLogs // Määritellään kirjautumispöytä
| where TimeGenerated > ago(14d) // Viimeiset 14 päivää
| where Location != "FI" // Sijainti ei saa olla Suomi
| where ResultType != 0 // Kirjautuminen ei ole epäonnistunut (yksinkertaistettu)
Sääntö näyttää melko yksinkertaiselta, koska siinä ei olla otettu huomioon muita mahdollisia “ResultType” arvoja, jotka viittaavat onnistuneeseen kirjautumiseen. Siinä ei myöskään muokata lokien muotoa lisäämällä ja poistamalla tiettyjä lokikenttiä tuloksesta.
KQL-haun lisäksi analyysisäännöt sisältävät seuraavat ominaisuudet:
- Kuvauksen säännöstä
- Syntyvän hälytyksen kriittisyyden (informational, low, medium, high)
- Logiikan eli KQL-haut
- Hälytyksen tutkintaohjeet
- Mahdolliset automatisaatiot
Analyysisääntöjä on kolmea tyyppiä (englanniksi):
- Scheduled Analytic Rule: Ajetaan tietyin aikavälein, kuten kerran päivässä. Tämä sääntötyyppi on yleisin.
- NRT Query Rule(Near Real Time): Ajaa kerran minuutissa, täten tunnistaa uhkat melkein reaaliajassa.
- Microsoft Incident Creation Rule: Mahdollistaa incidenttien synkronisoinnin muiden Microsoft tietoturvatuotteiden välillä. Tämä sääntötyyppi on poistunut jos organisaatiosi on yhdistänyt Sentinelin uuteen Microsoft Defender XDR portaaliin.
Yllä mainituista sääntötyypeistä Scheduled Analytic Rule on ylivoimaisesti yleisin johtuen siitä, että NRT-sääntöjä voi olla maksimissaan 50 samanaikaisesti aktiivisena ja Microsoft Incident Creation säännöt voivat synkronisoida vain tiettyjen tietoturvatuotteiden hälytykset Sentineliin.
Seuraavaksi näytän hieman erikoisemman ja monimutkaisemman analyysisäännön Microsoftin yhteisöltä.
Esimerkki edistyneestä analyysisäännöstä:
Tässä etsitään Azure Key Vault operaatioiden määrän epätavallista piikkiä, jonka on suorittanut yksi IP-osoite. Kysely käyttää KQL-kieleen sisäänrakennettua poikkeamantunnistus algoritmia, jolla tunnistetaan normaalista poikkeavia tilanteita (tässä tapauksessa operaatioiden määrää). Äkillinen kasvu Azure Key Vaultin käyttökerroissa voi viitata hyökkääjän automatisoituun toimintoon, jonka avulla yritetään varastaa tunnistetietoja.
let starttime = 14d;
let timeframe = 1d;
let scorethreshold = 3;
let baselinethreshold = 25;
// Tunnettu sovellus (Azure Resource Graph), jonka toimintaan kuuluu suuri määrä Key Vault operaatioita
// Kyseinen sovellus filtteröidään pois
let Allowedappid = dynamic(["509e4652-da8d-478d-a730-e9d4a1996ca4"]);
// Seurattavat operaatiot
let OperationList = dynamic(
["SecretGet", "KeyGet", "VaultGet"]);
// Luodaan data, josta anomaliaa etsitään
let TimeSeriesData = AzureDiagnostics
| where TimeGenerated between (startofday(ago(starttime))..startofday(now()))
| where not((identity_claim_appid_g in (Allowedappid)) and OperationName == 'VaultGet')
| where ResourceType =~ "VAULTS" and ResultType =~ "Success"
| where OperationName in (OperationList)
| extend ResultType = column_ifexists("ResultType", "None"), CallerIPAddress = column_ifexists("CallerIPAddress", "None")
| where ResultType !~ "None" and isnotempty(ResultType)
| where CallerIPAddress !~ "None" and isnotempty(CallerIPAddress)
| project TimeGenerated, OperationName, Resource, CallerIPAddress
// KQL:n sisäänrakennettu ominaisuus, joka valmistelee datan algoritmia varten
| make-series HourlyCount=count() on TimeGenerated from startofday(ago(starttime)) to startofday(now()) step timeframe by CallerIPAddress;
// Filtteröi anomalioiden pohjalta
let TimeSeriesAlerts = TimeSeriesData
// "series_decompose_anomalies" on KQL-kieleen sisäänrakennettu anomaliaa
// tunnistava funktio
| extend (anomalies, score, baseline) = series_decompose_anomalies(HourlyCount, scorethreshold, -1, 'linefit')
| mv-expand HourlyCount to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double),score to typeof(double), baseline to typeof(long)
| where anomalies > 0 | extend AnomalyHour = TimeGenerated
| where baseline > baselinethreshold // Filtteröidään vain useat määrät mukaan per baselinethreshold
| project CallerIPAddress, AnomalyHour, TimeGenerated, HourlyCount, baseline, anomalies, score;
let AnomalyHours = TimeSeriesAlerts | where TimeGenerated > ago(2d) | project TimeGenerated;
// Valitaan hälytykset tietyltä ajanjaksolta
TimeSeriesAlerts
| where TimeGenerated > ago(2d)
// Yhdistetään "normaaleihin" lokeihin, joka näyttää anomaliaa ympäröivät lokit
| join kind = innerunique (
AzureDiagnostics
| where TimeGenerated > ago(2d)
| where not((identity_claim_appid_g in (Allowedappid)) and OperationName == 'VaultGet')
| where ResourceType =~ "VAULTS" and ResultType =~ "Success"
| where OperationName in (OperationList)
| extend DateHour = bin(TimeGenerated, 1h)
| where DateHour in ((AnomalyHours))
| extend ResultType = column_ifexists("ResultType", "NoResultType")
| extend requestUri_s = column_ifexists("requestUri_s", "None"), identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g = column_ifexists("identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g", "None"),identity_claim_oid_g = column_ifexists("identity_claim_oid_g", ""),
identity_claim_upn_s = column_ifexists("identity_claim_upn_s", "")
| extend
CallerObjectId = iff(isempty(identity_claim_oid_g), identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g, identity_claim_oid_g),
CallerObjectUPN = iff(isempty(identity_claim_upn_s), identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s, identity_claim_upn_s)
| extend id_s = column_ifexists("id_s", "None"), CallerIPAddress = column_ifexists("CallerIPAddress", "None"), clientInfo_s = column_ifexists("clientInfo_s", "None")
| summarize PerOperationCount=count(), LatestAnomalyTime = arg_max(TimeGenerated,*) by bin(TimeGenerated,1h), Resource, OperationName, id_s, CallerIPAddress, identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g, identity_claim_oid_g, requestUri_s, clientInfo_s
) on CallerIPAddress
| extend
CallerObjectId = iff(isempty(identity_claim_oid_g), identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g, identity_claim_oid_g),
CallerObjectUPN = iff(isempty(identity_claim_upn_s), identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s, identity_claim_upn_s)
| summarize EventCount=count(), OperationNameList = make_set(OperationName,1000), RequestURLList = make_set(requestUri_s, 100), AccountList = make_set(CallerObjectId, 100), AccountMax = arg_max(CallerObjectId,*) by Resource, id_s, clientInfo_s, LatestAnomalyTime
| extend timestamp = LatestAnomalyTim
Huh! Siinäpä vasta on koodia. KQL-kielellä kirjoittaminen vaatii tiettyjä prosesseja, joiden avulla hakutuloksista saadaan kaivettua tärkeimmät tapahtumat esille.
Yllä oleva koodipätkä on lähtöisin Microsoftin yhteisöltä, jossa on useita valmiiksi rakennettuja analyysisääntöjä julkisesti saatavilla. Tässä linkki: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Azure%20Key%20Vault/Analytic%20Rules/TimeSeriesKeyvaultAccessAnomaly.yaml .
Yhteisön luomat säännöt vaativat yleensä muovailua ja kiillotusta kun niitä integroidaan olemassa olevaan järjestelmään.
KQL-hakujen tärkein tehtävä on saada selväksi, että
- Onko uhka olemassa?
- Missä uhka on olemassa?
- Miksi uhka on olemassa?
Nämä tiedot voidaan kaivaa yksinkertaisillakin hauilla esille.
Analyysisäätöjen luominen uhkalähtöisesti
Olemme käyneet läpi analyysisääntöjen anatomiaa, mutta on kuitenkin tärkeä tietää millä tavalla tehokkaita ja toimivia sääntöjä luodaan. Analyysisääntöjen luomisprosesseja on rajaton määrä, mutta yleisesti ne kaikki noudattavat seuraavanlaista prosessia:
- Määritellään skenaario tai uhkamalli
- Määritellään mitä halutaan tunnistaa ja mistä datalähteistä data voidaan hakea
- Luodaan KQL-haku ja analyysisääntö
- Testataan analyysisäännön toimivuus
- Hienosäädetään sääntöä hälytysten pohjalta (esimerkiksi sallitut käyttäjät voidaan suodattaa hälytyksistä pois)
Parhaisiin käytänteisiin kuuluu sääntöjen uhkalähtöinen kehittäminen, joka voi perustua liiketoiminnalliseen vaatimukseen, kyberhyökkäyksien estämiseen tai uhkatietojen hyödyntämiseen.
Liiketoiminnalliset vaatimukset
Liiketoiminnallisiin vaatimuksiin voi liittyä tapahtumia, joiden tunnistaminen on tärkeää liiketoiminnan kannalta. Näiden tapahtumien pohjalta voidaan luoda yritysjohdolle automatisoituja raportteja ja kehittää liiketoimintaa. Tässä muutama esimerkki:
- Käyttäjien seuranta (ulkoiset käyttäjät, palvelutunnukset)
- Vaatimustenmukaisuus ja sääntely (ulkomaan kirjautumiset, MFA:n käyttäminen, ylläpitäjien toiminta)
- Sovellusten käyttäminen
Kyberhyökkäyksien estäminen
Kyberhyökkäyksistä luodaan uhkamalleja, joiden pohjalta tunnistetaan niihin yhdistettyjä tapahtumia kuten uusien prosessien luontia, haittaohjelman lataamista internetistä ja oikeuksien muutoksia.
Muutama esimerkki yleisistä kyberhyökkäyksiin linkitetyistä tapahtumista:
- Kirjautumisyritysten äkillinen kasvu (Väsytyshyökkäys)
- Epäilyttävien prosessien luominen
- Käyttöoikeuksien korottaminen
- Uusien käyttäjien luominen ja poistaminen
- Kirjautumisyritykset samaan käyttäjään useista IP-osoitteista
- Suoritettavien tiedostojen lataaminen internetistä (esimerkki alla)
Tässä KQL-haussa tunnistetaan HTTP-pyynnöllä ladattu suoritettava tiedosto, joka voi viitata haittaohjelman automatisoituun toimintoon.
let ExecutableFileExtentions = dynamic(['bat', 'cmd', 'com', 'cpl', 'ex', 'exe', 'jse', 'lnk','msc', 'ps1', 'reg', 'vb', 'vbe', 'ws', 'wsf']);
DeviceNetworkEvents
| where ActionType == "NetworkSignatureInspected"
| extend
SignatureName = tostring(parse_json(AdditionalFields).SignatureName),
SignatureMatchedContent = tostring(parse_json(AdditionalFields).SignatureMatchedContent),
SamplePacketContent = tostring(parse_json(AdditionalFields).SamplePacketContent)
| where SignatureName == "HTTP_Client"
| extend HTTP_Request_Method = tostring(split(SignatureMatchedContent, " /", 0)[0])
| where HTTP_Request_Method == "GET"
| extend DownloadedContent = extract(@'.*/(.*)HTTP', 1, SignatureMatchedContent)
| extend DownloadContentFileExtention = extract(@'.*\.(.*)$', 1, DownloadedContent)
| where isnotempty(DownloadContentFileExtention) and string_size(DownloadContentFileExtention) < 8
| where DownloadContentFileExtention has_any (ExecutableFileExtentions)
| project-reorder TimeGenerated, DeviceName, DownloadedContent, HTTP_Request_Method, RemoteIP
Uhkatietojen hyödyntäminen
Uhkatietojen kerääminen avoimista lähteistä on paras tapa pysyä uusien kyberuhkien perässä. Sentineliin voidaan luoda listoja (watchlist) uhkatiedoista, jotka saadaan kätevästi KQL-haussa käyttöön. Uhkatietojen ylläpitämistä voi ostaa Microsoftilta (Microsoft Threat Intelligence) ja kolmansilta osapuolilta (vink vink Tekve Oy).
Esimerkki:
Tämä KQL-haku käyttää DeviceFileEvents datapöytää eli päätelaitteiden tiedostotapahtumia ja etsii sieltä uhkatietoon liittyviä tiedoston tunnistetietoja. Käytämme tässä esimerkissä viime viikolla uutisoitua “FormBook” -haittaohjelman tiedosto tunnisteita uhkatietona.
let dt_lookBack = 1h;
// Watchlist, joka sisältää csv muodossa FormBook haittaohjelman tiedosto tunnisteita
let FormBookHashes = (_GetWatchlist("FormBookFileHashes")| project fileHash);
let DeviceFileEvents_ = (union
(DeviceFileEvents | where TimeGenerated > ago(dt_lookBack) | where isnotempty(SHA1) | extend FileHashValue = SHA1),
(DeviceFileEvents | where TimeGenerated > ago(dt_lookBack) | where isnotempty(SHA256) | extend FileHashValue = SHA256));
DeviceFileEvents_
| where FileHashValue in (FormBookHashes)
Jatkuva kehitys
Useat organisaatiot asentavat Microsoft Sentinelin ja ajattelevat, että nyt on suurin työ tehty eikä sen kehittämistä ajatella enempää. Se on suuri virhe, sillä kyberuhkat kehittyvät joka päivä ja löytävät uusia keinoja päästä organisaatioon käsiksi. Tästä syystä on tärkeä jatkuvasti ylläpitää säännöstöjä ja IOC (Indicator of Compromise) tietokantoja. Ilman näitä Sentinelin toiminta menettää tarkoituksen.
On myös tärkeää luoda prosesseja organisaatioissa, jotta Sentinelin valvonta pysyy aktiivisena eikä hälytyksiä mene ohi ilman vaadittavia jatkotoimia. Tekve Oy tarjoaa järkevällä kuukausihinnalla palvelua, jossa yrityksesi Sentinelin tehokkuus pidetään ajan tasalla. Palveluun voi liittyä myös analyysisääntöjen ja uhkatietojen pitämistä ajan tasalla uhkatoimijoiden kanssa.
Lue lisää palveluistamme ja ota rohkeasti yhteyttä jos heräsi mielenkiinto!
Kirjoittaja: Petter Kauppi (petter@tekve.fi)
Contact: toimisto@tekve.fi, +358 41 311 9277