Detecting Impossible Travel, with Google SecOps: Part 1

cmmartin_google

Introduction

Imagine a user logging in from New York, then minutes later, accessing files from a server in Tokyo.  Physically impossible? Absolutely. But in the realm of cybersecurity, this "impossible travel" could signal a compromised account or a sophisticated attack.

In part 1 of this series, we'll delve into the Impossible Travel use case and demonstrate how to implement detection within Google SecOps. We'll create a custom YARA-L Detection rule, leveraging GeoSpatial functions like math.geo_distance, and make use of SecOps SIEM's native GeoIP enrichment. Then, in part 2, we'll showcase how to present the findings in a clear and actionable Case within SecOps SOAR.

cmmartin_google_0-1723209732845.png

Did the user really reach Mach 4, four times the speed of sound?  Read on to find out.

What is Impossible Travel?

Impossible travel occurs when a user's activity appears to originate from geographically distant locations within an unrealistically short time frame. This often signals compromised credentials, enabling unauthorized access from various parts of the world. For instance, logins from New York and Tokyo minutes apart would be flagged as impossible, given the physical limitations of travel.

By analyzing IP addresses and geolocation data, impossible travel detection serves as an early warning system. Organizations can proactively identify and address account compromises, preventing data breaches, system disruptions, and reputational damage. Additionally, it helps uncover unauthorized account sharing, enforcing security policies.

Prevention & Mitigation

Proactive security measures are crucial for safeguarding against compromised credentials and account takeovers. Before implementing detection controls in Google SecOps, consider these preventative steps:

Google Workspace

  • Suspicious Login Detection: Leverages machine learning to identify and block login attempts from unusual locations or devices, serving as an initial line of defense.

Google Cloud Platform

Building the Impossible Travel Detection Rule 

cmmartin_google_1-1723209826722.png

The Impossible Travel YARA-L Rule as viewed in SecOps SOAR

Capturing the First Login Event

  • Event Variable: We start by creating a placeholder called $e1 to store information about the first login event.
  • Event Filtering: The rule then filters for specific authentication events. In this example, we're using Google Workspace as the Identity Provider (IdP). You'll need to adjust these values to match your environment's IdP.
  • Match Variables:  We create variables to store the user's email ($user) and the latitude and longitude coordinates of the first login ($e1_lat and $e1_long).

 

    events:   
     	$e1.metadata.log_type = "WORKSPACE_ACTIVITY"
       $e1.metadata.event_type = "USER_LOGIN"
       $e1.metadata.product_event_type = "login_success"
       // match variables
       $user = $e1.extracted.fields["actor.email"]
       $e1_lat = $e1.principal.location.region_coordinates.latitude
       $e1_long = $e1.principal.location.region_coordinates.longitude

 

Capturing the Second Login Event

The second part of our YARA-L rule, represented by the event variable $e2, mirrors the logic applied to the first event ($e1), and filters for the same type of authentication events.

 

       $e2.metadata.log_type = "WORKSPACE_ACTIVITY"
       $e2.metadata.event_type = "USER_LOGIN"    
       $e2.metadata.product_event_type = "login_success"
       // match variables
       $user = $e2.extracted.fields["actor.email"]
       $e2_lat = $e2.principal.location.region_coordinates.latitude
       $e2_long = $e2.principal.location.region_coordinates.longitude

 

With both login events captured and filtered, we're now ready to calculate the time and distance between them to assess the possibility of impossible travel.

Ensure Distinct Locations

To detect impossible travel, it's crucial to ensure we're comparing two distinct logon events from different geographical locations. To achieve this, we incorporate two key checks in our YARA-L rule:

 

        // ensure consistent event sequencing, i.e., $e1 is before $e2
        $e1.metadata.event_timestamp.seconds < $e2.metadata.event_timestamp.seconds

        // check the $e1 and $e2 coordinates represent different locations
        $e1_lat != $e2_lat
        $e1_long != $e2_long

 

  1. Event Sequencing: The first line checks that the timestamp of the first event ($e1) is earlier than the second event ($e2). This establishes a clear timeline and prevents the rule from accidentally matching the same event twice (a self-join).
  2. Distinct Locations: The next two lines compare the latitude and longitude coordinates of the two events ($e1_lat,$e1_long,$e2_lat, and $e2_long). By ensuring they are not equal, we guarantee that the logins originated from different geographical points.

Grouping Events

The match criteria in a YARA-L rule serves a similar purpose to a GROUP BY clause in SQL, allowing us to group related events together for further analysis.  

In our impossible travel detection scenario, we group successful logon events ($e1 and $e2) that belong to the same user ($user) within a 1-day window. This time frame allows for legitimate travel while still flagging suspicious activity from vastly different locations occurring within a short period.

Crucially, we include the coordinates from both events ($e1_lat, $e1_long, $e2_lat, $e2_long) in the match statement. This ensures we analyze distinct logins from different locations, preventing false negatives.  For instance, if a user logs in from Alaska (e1), then Antarctica (e2), and later again from Alaska (e3), grouping solely on the $user could lead us to compare the two Alaskan logins and miss the suspicious activity from Antarctica.

 

    match:
        $user, 
        $e1_lat, 
        $e1_long, 
        $e2_lat, 
        $e2_long 
        over 1d

 

By carefully crafting the match criteria, we ensure that our rule effectively groups relevant events, enabling us to focus on identifying genuine impossible travel patterns indicative of potential security threats.

Calculating Impossible with Outcomes

Now that we've captured two geographically distinct login events from the same user, it's time to analyze the time and distance between them to assess the likelihood of impossible travel. The outcome section of our YARA-L rule handles this analysis, and helps prepare the results for analysts' investigation in SecOps SOAR.

By subtracting the timestamps of the two events we can create the interval in seconds, and convert to hours by dividing by 3600.

 

        // calculate the interval between first and last event, in seconds
        $duration_hours = cast.as_int(
            min(
                ($e2.metadata.event_timestamp.seconds - $e1.metadata.event_timestamp.seconds) 
                / 3600
            )
        )

 

This time interval will play a crucial role in our subsequent calculations, as we'll combine it with the distance between the two locations to determine if the travel was realistically possible within that time frame.

Calculating Distance with math.geo_distance

To determine if the travel between the two login events is feasible, we need to first calculate the distance separating them. The math.geo_distance YARA-L function in Google SIEM is ideal for this purpose, taking two coordinates parameters and returning the difference in meters: 

 

        // calculate distance between login events, and convert results into kilometers
        // - math.ceil rounds up a float up to the nearest int
        $distance_kilometers = math.ceil(
            max(
                math.geo_distance(
                    $e1_long,
                    $e1_lat,
                    $e2_long,
                    $e2_lat
                )
            ) 
            // convert the math.geo_distance result from meters to kilometers
            / 1000 
        )

 

The math.geo_distance function returns the distance as a float value, which can include many decimal places (e.g., 15546.629971089747).  However, we don’t need that level of precision, and so the math.ceil function is used to round up the float value up to the nearest integer (e.g., 15547).

cmmartin_google_3-1723210023310.png

_______________________________________________________________________________________________________________________

Tip: If you are familiar with GeoSpatial Analytics then the math.geo_distance is analogous to the SQL ST_DISTANCE function with the Spheroid parameter set to False. 

 

/* Verify the results of the YARA-L Impossible Travel rule using SQL in GCP BigQuery */

SELECT 
  ST_DISTANCE(
    -- ST_GEOGPOINT(longitude, latitude)
    ST_GEOGPOINT(-99.9018131, 31.968598800000002), 
    ST_GEOGPOINT(103.819836, 1.352083), 
    false  -- If use_spheroid is FALSE, the function measures distance on the surface of a perfect spher
  )

-- https://cloud.google.com/bigquery/docs/reference/standard-sql/geography_functions#st_geogpoint
-- https://cloud.google.com/bigquery/docs/reference/standard-sql/geography_functions#st_distance

 

_______________________________________________________________________________________________________________________

Now that we have the distance in kilometers ($distance_kilometers) and the time interval in hours ($duration_hours), we can calculate the speed at which the user would have had to travel to physically login in both locations. 

 

        // calculate the speed in KPH
        $kph = math.ceil($distance_kilometers / $duration_hours)

 

To complete the Outcome section of the impossible travel detection rule we'll use a series of conditional statements to assign a $risk_score based on how fast the travel appears to be.

 

        // // generate risk_score based on KPH, i.e., speed over distance travelled
        $risk_score = (
            if($kph >= 100 and $kph <= 249, 35) +
            if($kph > 250 and $kph <= 449, 50) +
            if($kph > 500 and $kph <= 999, 75) +
            if($kph >= 1000, 90)
        ) 

 

This code assigns progressively higher risk scores to faster speeds. For instance, speeds between 100 and 249 KPH (typical for high-speed trains or short flights) receive a moderate risk score of 35. In contrast, speeds exceeding 1000 KPH, which are far beyond the capabilities of conventional travel, warrant a high risk score of 90.

Each if statement in the risk score calculation includes both lower and upper boundaries to ensure that the final $risk_score remains within the 0-100 range.  Without these upper boundaries, the risk score could become cumulative, potentially exceeding 100 and making it difficult to interpret the severity of the alert.

The specific risk score values used in the rule are somewhat subjective and can be adjusted based on your organization's risk tolerance and typical travel patterns.

Setting the Alert Threshold and Finalizing the Rule

The $risk_score we've calculated plays a pivotal role in determining whether our rule triggers an alert. We introduce a configurable $risk_score_threshold to define the level of risk that warrants further investigation.

 

        // change this according 
        $risk_score_threshold = 90

 

In this case, we've set the threshold to 90, meaning any pair of login events with a calculated speed of 1000 KPH or higher will trigger an alert.

Finally, we define the condition statement that ties everything together:

 

    condition:
        $e1 and $e2 and $risk_score >= $risk_score_threshold

 

This condition states that an alert will be generated only if:

  • Both events ($e1 and $e2) are present, meaning we have two successful logins from the same user
  • The calculated $risk_score is equal to or greater than the defined $risk_score_threshold

When these conditions are met, the rule will not only trigger an alert but also create a Case in SecOps SOAR, providing security analysts with the necessary context to investigate the potential impossible travel incident further, which we shall explore further in Part 2 of this series.

cmmartin_google_4-1723210175339.png

Example results of a YARA-L Impossible Travel Detection Alert in Chronicle SIEM

The Impossible Travel YARA-L Rule

For reference, here is the Impossible Travel YARA-L rule in its entirety.  Please note, this is an example, and should be updated to match your environment accordingly.

 

rule suspicious_auth_unusual_interval_time {

    meta:
        author = "@Google SecOps Community"
        description = "Generates a detection for authentication activity occuring between two locations in an unusual interval of time."
        severity = "LOW"
        priority = "LOW"

    events:
        $e1.metadata.log_type = "WORKSPACE_ACTIVITY"
        $e1.metadata.event_type = "USER_LOGIN"
        $e1.metadata.product_event_type = "login_success"
        // match variables
        $user = $e1.extracted.fields["actor.email"]
        $e1_lat = $e1.principal.location.region_coordinates.latitude
        $e1_long = $e1.principal.location.region_coordinates.longitude

        // ensure consistent event sequencing, i.e., $e1 is before $e2
        $e1.metadata.event_timestamp.seconds < $e2.metadata.event_timestamp.seconds
        // check the $e1 and $e2 coordinates represent different locations
        $e1_lat != $e2_lat
        $e1_long != $e2_long

        $e2.metadata.log_type = "WORKSPACE_ACTIVITY"
        $e2.metadata.event_type = "USER_LOGIN"    
        $e2.metadata.product_event_type = "login_success"
        // match variables
        $user = $e2.extracted.fields["actor.email"]
        $e2_lat = $e2.principal.location.region_coordinates.latitude
        $e2_long = $e2.principal.location.region_coordinates.longitude

    match:
        $user, 
        $e1_lat, 
        $e1_long, 
        $e2_lat, 
        $e2_long 
        over 1d

    outcome:
        // calculate the interval between first and last event, in seconds
        $duration_hours = cast.as_int(
            min(
                ($e2.metadata.event_timestamp.seconds - $e1.metadata.event_timestamp.seconds) 
                / 3600
            )
        )

        // calculate distance between login events, and convert results into kilometers
        // - math.ceil rounds up a float up to the nearest int
        $distance_kilometers = math.ceil(
            max(
                math.geo_distance(
                    $e1_long,
                    $e1_lat,
                    $e2_long,
                    $e2_lat
                )
            ) 
            // convert the math.geo_distance result from meters to kilometers
            / 1000 
        )

        // calculate the speed in KPH
        $kph = math.ceil($distance_kilometers / $duration_hours)

        // // generate risk_score based on KPH, i.e., speed over distance travelled
        $risk_score = (
            if($kph >= 100 and $kph <= 249, 35) +
            if($kph > 250 and $kph <= 449, 50) +
            if($kph > 500 and $kph <= 999, 75) +
            if($kph >= 1000, 90)
        ) 

        // change this according to your requirements
        $risk_score_threshold = 90

    condition:
        $e1 and $e2 and $risk_score >= $risk_score_threshold
}

 

11 12 5,251