Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better handling of overrides after all fr.Behaviors are added #487

Merged
merged 12 commits into from
Oct 4, 2024

Conversation

egibs
Copy link
Member

@egibs egibs commented Oct 4, 2024

Closes #486

We hit the edge case where an override rule was being evaluated before the original rule, so we never found it in the slice of behaviors.

This PR handles overrides as a post-processing step, rather than during the mrs loop.

This PR also introduces our first Critical -> High override for a third-party (!) rule:

HIGH  override/plugx                  detect PlugX in memory, [malware_PlugX_config], by   $v2b                                                                                                                     
                                      JPCERT/CC Incident Response Group                    $v2f                                                                                                                     
                                                                                           $v2g                                                                                                                     
                                                                                           $v2h                                                                                                                     
                                                                                           �DDDD                                                                                                                    
                                                                                           �DDDD                                                                                                                    
                                                                                           �3333                                                                                                                    
                                                                                           �DDDD  

Because of this rule, I was able to see that the original override logic was insufficient. The new approach is more resilient and we should successfully apply all overrides now.

@egibs egibs requested a review from tstromberg October 4, 2024 03:56
if originalExists && overrideExists {
for _, b := range fr.Behaviors {
if b.RuleName == override.RuleName {
b.RuleAuthor = original.RuleAuthor
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was getting incorrect results if I did b = original, so I was more prescriptive with the fields here. I think it adds helpful context anyway.

Copy link
Collaborator

@tstromberg tstromberg Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be reversed: preserve the original rule, but apply the necessary override (priority) to it: otherwise we're going to just keep on adding fields to this code as new ones come up.

One thing I just realized is that we probably shouldn't modify the rules description directly: otherwise it gets messy if multiple overrides apply to a rule. Instead we can add the rule name to a list of overrides that apply to the behavior (for example: Behavior.Overrides), and modify the description at the presentation layer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flipped the logic in a4c70f4 (#487).

rules/override/plugx.yara Outdated Show resolved Hide resolved
Signed-off-by: egibs <[email protected]>
@egibs
Copy link
Member Author

egibs commented Oct 4, 2024

Currently working through a discrepancy between analyze and scan. The former works, but the latter seems to ignore the override completely (i.e., validOverrides isn't even populated).

Copy link
Collaborator

@tstromberg tstromberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you make it so that overridden rules use the original name rather than the override name?

meta:
malware_shellcode_hash = "high"

strings:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tstromberg -- can you think of a less redundant way to handle this?

This is a new override rule for another third-party false positive we saw with Pulumi, but it feels gross to duplicate the strings/conditions. Using something less restrictive will allow other override rules to match, though (e.g., condition: true).

Copy link
Collaborator

@tstromberg tstromberg Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should invert how this override functions: it should simply detect the appropriate Pulumi binary rather than repeat the logic of the offending rule.

Here's what I would do in your shoes:

  • Rename this file to "pulumi.yara"
  • Rename the rule to the binary you are overriding, like pulumictl (if it's not obviously pulumi, maybe "pulumi_".
  • Add 1-2 long strings you expect to show up in the binary, such as "PULUMI_HOME" or "How did Pulumi come up with such a weird name?"
  • Ditch the rest of the rules: we're only trying to detect that this binary is probably pulumi, not the rules it is overriding.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Your Bash example comes to mind and that's a good way to go about it.

@egibs
Copy link
Member Author

egibs commented Oct 4, 2024

Could you make it so that overridden rules use the original name rather than the override name?

Updated in 01ddf7d (#487).

meta:
malware_shellcode_hash = "high"

strings:
Copy link
Collaborator

@tstromberg tstromberg Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should invert how this override functions: it should simply detect the appropriate Pulumi binary rather than repeat the logic of the offending rule.

Here's what I would do in your shoes:

  • Rename this file to "pulumi.yara"
  • Rename the rule to the binary you are overriding, like pulumictl (if it's not obviously pulumi, maybe "pulumi_".
  • Add 1-2 long strings you expect to show up in the binary, such as "PULUMI_HOME" or "How did Pulumi come up with such a weird name?"
  • Ditch the rest of the rules: we're only trying to detect that this binary is probably pulumi, not the rules it is overriding.

rules/override/shellcode.yara Outdated Show resolved Hide resolved
rules/override/shellcode.yara Outdated Show resolved Hide resolved
@tstromberg
Copy link
Collaborator

Ideal: instead of a generic overrides/ folder within rules, maybe a known_good/ folder specifically geared toward addressing false positives?

I figure that overrides that add severity should probably live elsewhere?

@egibs egibs requested a review from tstromberg October 4, 2024 14:13
Signed-off-by: egibs <[email protected]>
@@ -521,6 +517,72 @@ func Generate(ctx context.Context, path string, mrs yara.MatchRules, c malconten
// TODO: If we match multiple rules within a single namespace, merge matchstrings
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can some of the override handling code move to a new function? This one is getting overly large and complicated.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to a new function in 660024a (#487).

// If this is the override rule, delete it from the behavior slice
// Otherwise, treat the rule as a possible original (vo.Original makes this an O(1) lookup)
if isOverride {
fr.Behaviors = slices.Delete(fr.Behaviors, i, i+1)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of deleting overrides from Behaviors, would the logic be simpler if overrides were stored elsewhere instead? For example, fr.Overrides or a different variable altogether.

By the time fr.Behaviors is populated, we already know if the rule has an override tag.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added two new fields for overrides in 660024a (#487).

switch c.Scan {
case true:
switch {
case newRisk != overallRiskScore && newRisk >= 3:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this use a const of some sort? I don't remember if "3" is "HIGH" or "CRITICAL". I'm not sure why either needs to be hardcoded here to be honest, but I think it'd improve readability.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 660024a (#487).

// Regardless of running an `analyze` or `scan`, adjust the overall risk if we deviated from overallRiskScore
// Scans will still need to drop medium or lower results
newRisk := highestBehaviorRisk(fr)
switch c.Scan {
Copy link
Collaborator

@tstromberg tstromberg Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the nested switches are helping readability. Maybe something like:

if newRisk != overallRiskScore {
			overallRiskScore = newRisk
}

if c.Scan && newRisk < HIGH:
  return malcontent.FileReport{}, nil
}

Is this just checking if the command is "scan" vs "analyze"? If so, I don't think this function should need to know: all scan should need to do in main is set a default minRisk of "HIGH" and everything else should just work out.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaned this up in 660024a (#487).

Signed-off-by: egibs <[email protected]>
}

for _, o := range fr.Overrides {
if b, exists := behaviorMap[o.Override]; exists {
Copy link
Member Author

@egibs egibs Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this out with a critical false positive and it works.

Test override rule:

rule pulumi_shellcode : override {
  meta:
    malware_shellcode_hash = "low"
    description = "Ignore shellcode findings in the pulumi binary"
  strings:
    $pulumi = { 50 75 6C 75 6D 69 }
    $pulumi2 = { 70 75 6C 75 6D 69 }
  condition:
    any of ($pulumi*)
}

Result:
CleanShot 2024-10-04 at 13 16 39@2x

@@ -1,4 +1,4 @@
rule go_dns_refs {
rule go_dns_refs_local {
Copy link
Member Author

@egibs egibs Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were two go_dns_refs rules and there was a chance one would clobber the other and result in different report results:

  • rule go_dns_refs {
    meta:
    description = "Uses DNS (Domain Name Service)"
    strings:
    $dnsmessage = "dnsmessage"
    $edns = "SetEDNS0"
    $cname = "CNAMEResource"
    $nodejs = /require\(['"]dns['"]\)/
    condition:
    any of them
    }
  • rule go_dns_refs {
    meta:
    description = "Examines local DNS servers"
    strings:
    $resolv = "resolv.conf" fullword
    $dns_getservers = "dns.getServers"
    $cname = "CNAMEResource"
    condition:
    any of them
    }

@egibs egibs merged commit ab8a15b into chainguard-dev:main Oct 4, 2024
6 checks passed
@egibs egibs deleted the fix-overrides branch October 6, 2024 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Rule overrides can fail if the override is iterated over before the original
2 participants