Detecting GraphStrike’s Achilles heel with KQL

Some time ago, I stumbled upon an interesting blog post by Alex Reid, writing about the development process of an offensive tool called “GraphStrike”. The concept of this offensive tool is quite simple – this tool is a C2 beacon that routes its traffic over Microsoft’s Graph API. This kind of traffic is quite stealthy as in your network logging, you expect to see a lot of Graph API connections from different devices.

Usually for a defender, a beacon that is going through domains like Microsoft’s own domains are real headscratchers in terms of how to detect them. Obviously, there are statistical analyses possible of network traffic per endpoint to find recurring domains that are intermittently being contacted. However, these detections are oftentimes very noisy and can lead to alert fatigue with the analysts.

Finding the weakness

Alex does a great job of identifying what will be potential obstacles in his path of creating this tool. In his blog, he mentions that access tokens to access the Graph API will be a great issue for this project, as these expire in 60-90 minutes. This excerpt from the blog explains it best:

“Updating Beacon’s request headers to include a new access token as time goes on is one piece of the puzzle, but actually retrieving the new access tokens is another entirely. With the issues discussed above in mind, getting Beacon to fetch access tokens itself really seemed like the smart direction to go. This would involve Beacon making HTTPS requests to login.microsoft.com in order to retrieve new access tokens, as well as HTTPS requests to graph.microsoft.com for actual C2 communications.”

This paragraph made me realize that a beacon getting a new access token and then polling the Graph API for new instructions would be something that is actually pretty unique behavior when looking at it from EDR-data.

Creating the detection

Before determining the viability of this rule, I had to verify there were not very many processes that connected to the login.microsoft.com endpoint AND the graph.microsoft.com endpoint within 2 hours of each other (the 2 hours timespan here is very generous, you could also narrow this down to 1.5 hours). This revealed to me that it was mainly browser processes, some Outlook behavior and the Microsoft AAD Broker Plugin doing this legitimately. This is great news!

Now, the only thing that’s left for this detection is to filter out these legitimate processes showing this behavior while making sure the detection does not become more brittle and easier to bypass.

Fortunately for us, KQL has a handy functionality called FileProfile, which will give more information on a file when providing a hash string of the file to the function argument. This allows us to determine the signer of a file, how prevalent it is globally, if the root signer is Microsoft and other information. For simplicity’s sake in this detection, I will require that each whitelisted process displaying this behavior is signed and is seen more than 500 times globally.

Doing this will make sure that this is not a custom C2 implant trying to masquerade as, for example, a browser like Chrome. The file should not have the required global prevalence for this.

The result

Before I show the detection query for GraphStrike, I would highly recommend reading the two blog posts by on RedSiege’s website (redsiege.com/blog/2024/01/graphstrike-release/ and redsiege.com/blog/2024/01/graphstrike-developer/) into this super interesting offensive tool. Especially the developer blog helps us blue teamers understand a lot about what red teams (and even real adversaries) think about when developing their tooling. It gives us a path to creating better detections for emerging threats which the red team has already put time into mastering.

You’ll need to verify which processes in your environment make these same connections and whitelist them in a similar manner as I did here for the browsers and the AAD Broker plugin.

let SuspiciousProcesses = (DeviceNetworkEvents
| where Timestamp > ago(2h)
| where RemoteUrl contains "graph.microsoft.com" or RemoteUrl contains "login.microsoft.com"
| summarize make_set(RemoteUrl) by InitiatingProcessId, InitiatingProcessFileName, DeviceName, DeviceId, bin(Timestamp, 2h)
| where set_RemoteUrl contains "graph.microsoft.com" and set_RemoteUrl contains "login.microsoft.com");
DeviceNetworkEvents
| where Timestamp > ago(2h)
| where RemoteUrl contains "graph.microsoft.com" or RemoteUrl contains "login.microsoft.com"
| join kind=innerunique SuspiciousProcesses on InitiatingProcessId, DeviceId, InitiatingProcessFileName
| invoke FileProfile(InitiatingProcessSHA256, 1000)
| where not(InitiatingProcessFileName in~ ("chrome.exe", "msedge.exe", "firefox.exe", "Microsoft.AAD.BrokerPlugin.exe") and SignatureState == "SignedValid" and GlobalPrevalence > 500)

About the Author:

Jani Vleurinck

Cybersecurity Consultant @ The Collective Consulting

Reference:

Vleurinck, J (2024). Detecting GraphStrike’s Achilles heel with KQL. Available at: Detecting GraphStrike’s Achilles heel with KQL | LinkedIn [Accessed: 26th September 2024].

Share this on...

Rate this Post:

Share: