Security Operations Suite arrow_forward expand_more
Solutions arrow_forward expand_more
Why Chronicle arrow_forward expand_more
Resources arrow_forward expand_more
Security Operations Suite arrow_forward expand_more
Solutions arrow_forward expand_more
Why Chronicle arrow_forward expand_more
Resources arrow_forward expand_more
New to Chronicle: The replacements

This is the seventh post from Google Cloud Principal Security Strategist John Stoner as part of his deep-dive "New to Chronicle" series, which helps propel forward security teams either new to SIEM or replacing their SIEM with Chronicle. You can view the entire series here.

As promised in our previous blog post, we will now introduce the regular expression function re.replace. We’ve already discussed the functions for regular expression matching, re.regex, and capture, re.capture, as well as base64 decoding with strings.base64_decode.

The re.replace function is aptly named because its job is to take a string, match some or all of the string using a regular expression, and then replace it with some other value. That value could also include a null value, which we will see in action a bit later. Like other functions, it can be nested, and much like our example with re.capture, re.replace will be part of a comparison against another value or list or written to a placeholder to be used in an additional operation.

Here is the basic syntax: re.replace(,,). A basic example of its use would be to take the principal.hostname value of google.org and make it google.com. To accomplish this, we would use the replace function like this: re.replace($event.principal.hostname,`org`, "com"). Of course we would still need to do something with it, but you get the idea.

With that brief introduction out of the way, let’s pick up where we left off in our last post. For those just joining us, we had built a rule that looked for PowerShell processes that included a few encoded command switches, captured these encoded strings, and decoded them, all within our rule. We used the outcome section of the rule to display both the encoded and decoded PowerShell commands.


Now, I would argue that this is pretty cool, but let’s think about how we might take this a step further. While providing a decoded base64 string is great, it would be even better if we could compare it to some value of interest and if there was a match, a rule could trigger and an alert could be created. That way, the analyst does not need to look at every base64 string at the initial point of triage. Sounds good so far?

Let’s take a value from our detection above to illustrate this point. Sharpnopsexec is a fileless command used for lateral movement. Perhaps we want to be alerted if we see this string being referenced in encoded PowerShell. Building on our PowerShell rule, we could add another piece of criteria to the events section, in bold, and we can look for references to sharpnopsexec, with case insensitivity, within the decoded PowerShell. Cool!


rule suspicious_encoded_powershell_command {

meta:
 author = "John Stoner"
 description = "Detects the string downloadstring in encoded powershell commands."   
   severity = "Low"

events:
 $event.metadata.event_type = "PROCESS_LAUNCH"
 re.regex($event.target.process.file.full_path,
`(system32|syswow64)\\WindowsPowerShell\\v1\.0\\powershell(|\_ise)\.exe`)
nocase
 re.regex($event.target.process.command_line,
`(?i)(?:-enc|-ec|-en)\s*\S*`)
 $encoded_value = re.capture($event.target.process.command_line,
`(?i)(?:-enc|-ec|-en)\s*(\S*)`)
 $decoded_value =
strings.base64_decode(re.capture($event.target.process.command_line,
`(?i)(?:-enc|-ec|-en)\s*(\S*)`))
  re.regex($decoded_value, `sharpnopsexec`) nocase

outcome:
$encoded_powershell = $encoded_value
  $decoded_powershell = $decoded_value

condition:
 $event
}




John? It doesn’t look good…No it doesn’t.

The problem that you can often run into when decoding PowerShell is that it will output in UTF-16, which has two bytes per character and the second byte becomes a null byte. To illustrate, we have the encoded PowerShell loaded into CyberChef and converted to base64 in our first example below.

Because the output doesn’t have anything to describe the second byte, we see a period to denote that extra byte.

And in this example, we have added to our recipe the removal of null bytes.

Notice how those extra periods fall away and we are left with something a bit more legible?

In our rule, we are trying to perform a regular expression match against something that looks like the first example and the regular expression can’t find a match. So, we are left with a few options.

The first option is to modify our regular expression match. There are a few options, including the ones below. Again, for the regular expression aficionados out there, you may have something better...and that’s great.

re.regex($decoded_value, `sharpnopsexec`) nocase or 
re.regex($decoded_value, `s\0h\0a\0r\0p\0n\0o\0p\0s\0e\0x\0e\0c\0`)
nocase

re.regex($decoded_value, `sharpnopsexec`) nocase or
re.regex($decoded_value, `s.h.a.r.p.n.o.p.s.e.x.e.c.`) nocase

The \0 represents a null character and the period denotes any character, so both of them could be used. However, who wants to have to default to this kind of regular expression match?

There’s got to be a better way. There is.

This is my really long lead up to the re.replace function. We can take our re.replace function and add it to our previously built nested function. To highlight the difference, we changed the name of the placeholder function from $decoded_value to $no_null_string. The re.replace encloses our strings.base64_decode and re.capture functions and treats the pair of nested functions as the string value, that is, the first argument in the re.replace function.

Then, in bold, we have the second and third arguments. The second is the regular expression that we are looking for, which in this case are null characters and the third argument is what we are going to replace the null characters with. We aren’t going to be replacing them with a specific character, so the third argument is a null value.

$no_null_string = 
re.replace(strings.base64_decode(re.capture($event.target.process.command_line,
`(?i)(?:-enc|-ec|-en)\s*(\S*)`)),`\0`, "")

With our new function added, we can set our re.regex function back to just the string value, without needing to account for the null characters since they have been effectively removed with our replace function.

re.regex($no_null_string, `sharpnopsexec`) nocase

And now when we test our rule, we get our two detections as expected that match sharpnopsexec in the decoded PowerShell!





It occurred to me that we have covered this rule evolving over the past three blog posts and we have been swapping out some values as we have added functions. To ensure that everyone is tracking these changes, I wanted to provide the complete rule below based on what we have covered over the past three blogs. I also added a match condition to group similar events like Sysmon/1 and Windows/4688 process executions together, which, for readers of this series will recognize as something we talked about previously.

rule suspicious_encoded_powershell_command {
meta:
author = "John Stoner"
description = "Detects the string downloadstring in encoded powershell commands."  
  severity = "Low"

events:
$event.metadata.event_type = "PROCESS_LAUNCH"
$event.metadata.event_type = $event_type
re.regex($event.target.process.file.full_path,
`(system32|syswow64)\\WindowsPowerShell\\v1\.0\\powershell(|\_ise)\.exe`) nocase
re.regex($event.target.process.command_line, `(?i)(?:-enc|-ec|-en)\s*\S*`)

$encoded_value = re.capture($event.target.process.command_line,
`(?i)(?:-enc|-ec|-en)\s*(\S*)`)
$decoded_value = strings.base64_decode(re.capture($event.target.process.command_line,
`(?i)(?:-enc|-ec|-en)\s*(\S*)`))
$no_null_string = re.replace(strings.base64_decode(re.capture($event.target.process.command_line,
`(?i)(?:-enc|-ec|-en)\s*(\S*)`)),`\0`, "")
  re.regex($no_null_string, `sharpnopsexec`) nocase

match:
  $event_type over 1m

outcome:
 $null_decoded_powershell = array_distinct($no_null_string)

condition:
  $event

}

Whew, that was a lot to describe just this one function, but hopefully it pays dividends to you. That said, I can already hear the rumblings. Does this mean I need to build a large OR statement to accommodate all of the possible encoded strings that I want to look for? That is one approach. We could monitor for both sharpnopsexec and rubeus like this:

re.regex($no_null_string, `sharpnopsexec`) nocase or 
re.regex($no_null_string, `rubeus`) nocase

I’m going to end it there. But we will circle back to this rule to continue to expand and build on it with additional functions and capabilities that Chronicle provides.

New to Chronicle Series
Let’s work together
Ready for Google-speed threat detection and response?
Contact us