Skip to content

A negated pcre is read as a positive one, so the condition means its opposite #697

Description

@NRGLine4Sec

Engine: Sagan, built from quadrantsec/sagan@3b9b0fa
Affects: any rule writing pcre:!"/pattern/"
Impact: the rule loads without a warning and asserts what it meant to forbid


Summary

content supports negation: content:!"foo" is parsed by Check_Content_Not and stored in content_not[]. pcre has no equivalent. The parser hands the quoted pattern straight to Between_Quotes without ever looking for a leading !, no pcre_not field exists anywhere in the rule structure, and PcreS() counts matches with no notion of negation.

The ! is therefore dropped silently, and pcre:!"/x/" behaves exactly like pcre:"/x/". A rule written to exclude something requires it instead.

What was measured

Sagan built from 3b9b0fa, one reduced rule, one event user bob failed login from program sshd:

Rule Result Expected
program:sshd; alert alert
program:sshd; pcre:!"/ZZZ_absent/"; no alert alert
program:sshd; pcre:!"/failed/"; alert no alert
program:sshd; pcre:"/failed/"; alert alert

Rows two and three are the defect, and they are each other's mirror: the rule stays silent exactly when the forbidden pattern is absent, and fires exactly when it is present.

The same holds with a content beside it, and with two negated patterns, so it is not an interaction with anything else in the rule.

Where it comes from

src/rules.c, the pcre block at line 3013, takes the argument and calls

Between_Quotes(arg, tmp2, sizeof(tmp2), ruleset_fullname, linecount, false);

with no Check_Content_Not(arg) beside it, unlike the content block near line2803 which does exactly that. Nothing else in the block inspects the !.

src/pcre-s.c, PcreS() at line 39, is the whole evaluation:

for(z=0; z<rulestruct[rule_position].pcre_count; z++)
    {
        rc = pcre_exec( ... );
        if ( rc > 0 )
            {
                match++;
            }
    }

if ( match == rulestruct[rule_position].pcre_count )
    {
        return(true);
    }
return(false);

Every pattern must match for the rule to pass. There is no per-pattern flag to invert, and grep -rn pcre_not src/ returns nothing.

json_pcre is worth checking at the same time: it has its own evaluation path and this issue does not cover it.

Suggested directions

Either add the negation, mirroring content: a pcre_not[] flag set by Check_Content_Not in the parser, and PcreS() counting a non-match as a match for those entries. Or reject the syntax at load time, so a rule that cannot mean what it says is refused rather than quietly inverted.

The second is a smaller change and arguably the safer one: silently inverting a security rule's exclusion is worse than refusing to start.

Corpus impact

One rule uses it: windows-powershell.rules sid 5007143, "Suspicious FromBase64String Encoded Commands". It carries

pcre:!"/C:\\Packages\\Plugins\\Microsoft\.Powershell\.DSC\\[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\\bin\\Azure\.psm1/"

meaning to exclude a legitimate Azure DSC module path. As the engine reads it, the rule fires only on that path: it alerts on precisely the activity it was written to ignore, and on nothing else.

No rules change is proposed here. The rule is correct as written; the engine is not.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions