-
Notifications
You must be signed in to change notification settings - Fork 567
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
dynamic: add sequence scope #2532
base: master
Are you sure you want to change the base?
Conversation
addresses discussion in mandiant/capa-rules#951
CHANGELOG updated or no update needed, thanks! 😄
we also may want to update the vverbose render to only show each call event once, leaving the match details to a separate section, maybe like:
|
@jorik-utwente FYI |
I realize I dropped this PR without much warning 😇 I went from "I wonder how this would work" to "huh, it seems to work OK" pretty quickly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
awesome, this looks very promising already!!
major things to discuss include the naming and potentially handling of loops
@@ -4,6 +4,8 @@ | |||
|
|||
### New Features | |||
|
|||
- add dynamic sequence scope for matching nearby calls within a thread #2532 @williballenthin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
naming alternatives to sequence (matching occurs in any order): span, ngram, group/cluster
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 cluster
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"window", "slice", "range"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
math: multiset (or bag, or mset) - https://en.wikipedia.org/wiki/Multiset
- multiple instances of same object
- order doesn't matter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
optionally prefix with "call", e.g., callbag
, callcluster
?
Good point. I think we'd want to see how this works in practice against a large number of samples and the rules we can translate to use this construct. In particular, loops (like you say) such as you'd see in ransomware. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work, I'm excited about where this is going for an initial implementation. I echo a few of @mr-tz 's comments/concerns. Additionally, the value 5
comes close to being too small for some of our existing rules, e.g. https://github.com/mandiant/capa-rules/blob/e033410c8910f8b46718a5eefd9f0c7768be1b99/communication/c2/shell/create-reverse-shell.yml#L19-L23 so we'll need to do some additional work to find the sweet spot.
d6106ea
to
6d05d3c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I spent a few moments focusing on the core extension here and added some places for additional documentation.
c0af878
to
a8abb16
Compare
a8abb16
to
37f6ccb
Compare
also, for repeating behavior, match only the first instance.
ea9daed
to
b10d591
Compare
computing the features for the sequence, which involves merging features from many calls, seems to take quite a bit of time: i'll have to think on whether there's a creative way to optimize this profile informationbefore: sequence length: 20before: sequence length: 0optimized, sequence length 1 and 20:conclusion:So, there's a bit of overhead to use this new algorithm, but it's independent of SEQUENCE_LENGTH, which is desirable. |
TODO?!
|
269a2e0
to
a31cfd9
Compare
4683882
to
69f4728
Compare
I've run into some bugs where sequence scoped rules matching sequence scoped rules that are hammered can't be tracked well. Currently thinking this through and figuring out a fix. Details: Sequence scope matches logic found within a sliding window of calls, currently of length 20. To avoid showing too many results when a program "hammers" a behavior, such as calling sleep in a tight loop, the sequence engine only "publishes" a match if it wasn't seen in the prior sequence. As long as the length of the sequence is larger than the behavior within a tight loop, capa shows the match at the first loop iteration and does not "publish" the subsequent run of duplicate behavior. By "publish" we mean that capa reports on a match, recording it within the result document and rendering it to the user. The sequence engine still recognizes all the matches within the tight loop, and other rules can match against those matches, they just aren't propagated into the final results. We think this behavior is generally agreeable and intuitive; however, there's a subtle bug. When we build the result document to show vverbose output, which prints the precise data used to make a match, a rule that depends on another rule that has been hammered can't be easily resolved. If we haven't published the hammered rule, then we can't easily show how the dependent rule matched, because the match details aren't available. For example, in 0000a... for PID 1852 and thread 2596, there's a tight loop of My current theory for a fix is: when there's a newly recognized rule (that is, not hammered), walk it's logic, and if it depends on any other rules, ensure they're published (even if they were hammered). This will take a bit more code, and will require some inline documentation like I've added above, but should be enough to ensure we can always prove why capa matched some logic. |
I think that's expected. If it's an issue that will take longer to fix we should handle it separately. The benefits of the sequence scope (way less FPs vs. thread scope) outweigh the shortcomings here. |
contains the call ids for all the calls within the sequence, so we know where to look for related matched.
ded0e27
to
6dde963
Compare
value: Union[ | ||
# for absolute, relative, file | ||
int, | ||
# for DNToken, Process, Thread, Call | ||
tuple[int, ...], | ||
# for sequence | ||
tuple[int, int, int, int, tuple[int, ...]], | ||
# for NO_ADDRESS, | ||
None, | ||
] = None # None default value to support deserialization of NO_ADDRESS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks, the documentation helps a lot here
we could also add documentation what the values are: ppid, pid, tid, id_, calls
or use a dataclass?
@@ -181,6 +182,55 @@ def test_dynamic_sequence_scope_length(): | |||
assert r.name not in capabilities.matches | |||
|
|||
|
|||
# show that the DynamicSequenceAddress has the correct structure. | |||
# temporarily uses a sequence of length 2, for simplicity. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great!
late night idea: to handle the link between dependent rules in the presence of hammering, I think we can just pick the most recent earlier match (in the same thread). We don't need to track this precisely, such as with specialized sequence addresses, so we can delete some of the recent code. |
Nice, this is a good solution for API hammering across/between dependent rule matches. I think we still have to come up with a solution for API hammering within a rule?
features:
- and:
- api: WantedApi1
- api: WantedApi2 |
This PR implements the dynamic "sequence scope" introduced here: mandiant/capa-rules#951
In summary, we want a way to match across calls (in dynamic mode) without resorting to the entire thread (which may be very long, like thousands of events). So, we add a new scope "sequence" that represents the sliding 20-tuples of calls across each thread. Rules can match against any set of logic within each of these 20-tuples.
For example, consider the initial behavior of thread 3064 in our test CAPE file 0000a657:
This is a long thread with many calls, so yesterday it was tough to write a rule for any behavior that spans multiple calls without introducing false positives. Consider matching on the dynamic resolution and invocation of
AddVectoredExceptionHandler
. Now we can write a rule like:So, within a region of 20 calls, match all this logic.
Here's what the output looks like:
The implementation is pretty easy: maintain a deque of the trailing 5 call events, merging and matching those features.
I picked 20 fairly randomly. I think we can tweak this number as necessary. Smaller and its harder to match logic. Larger and the performance might decrease a bit, and then there's more FP possibility. But I don't think this is too risky.
I think this will affect runtime a bit, since we're matching features twice for each call event (one for the precise call event, one for the sliding window).
There's probably some edge cases to work out around overlapping windows. Consider a rule that matches a single call event within a sequence: that call event is contained by 20 sequences (some covering the events before, some covering the events after). So, we may have to do a little more work (TODO) to not emit those matches twice. I'm not precisely sure of the behavior at this moment. I'll write a test for it.Checklist