Summary
There is a silent information-loss correctness bug in capa's default summary renderer (capa/render/default.py).
To reduce clutter in the default summary table, capa suppresses low-level "sub-rules" (rules that are matched as dependencies by higher-level parent rules). However, the current suppression logic is global and strictly name-based.
If a sub-rule Rule B is matched by a parent rule Rule A in Function 1, "Rule B" is added to the global suppression set. If "Rule B" also matches completely independently in Function 2 (where Rule A does not match), Rule B is still completely suppressed. As a result, the independent match at Function 2 is silently hidden from the default CLI summary output, leading to incomplete capability reports.
1. Technical Analysis & The Defect
In capa/render/default.py, the function find_subrule_matches traverses the match trees of all capability rules and adds the names of all successfully matched sub-rules to a global set:
# From capa/render/default.py
elif isinstance(match.node, rd.FeatureNode) and isinstance(match.node.feature, frzf.MatchFeature):
# Adds the sub-rule name to a global set
matches.add(match.node.feature.match)
Then, in render_capabilities, capa completely skips rendering any rule whose name exists in that global set:
# From capa/render/default.py
for rule in rutils.capability_rules(doc):
if rule.meta.name in subrule_matches:
# Sub-rule name matches the global set; skip rendering entirely!
continue
Because the check is global and based only on the name, all independent matches of the sub-rule elsewhere in the binary are silently discarded in the default CLI output view.
2. Proof of Concept (PoC)
This bug can be demonstrated using a simple ruleset with two mock rules:
rule_b (Low-level, e.g. encrypt data using RC4)
rule_a (High-level, e.g. Ransomware behavior, which depends on match: rule_b)
Consider a mock binary with two functions:
Function 1 (0x401000): Contains features triggering both rule_a and rule_b (so rule_b acts as a sub-rule).
Function 2 (0x402000): Contains features triggering only rule_b (so rule_b matches independently).
Expected Behavior (CLI Summary)
rule_a should show 1 match (at 0x401000).
rule_b should show 1 match (at 0x402000, representing the independent match that is not covered by rule_a).
Actual Behavior (Current CLI Summary)
rule_a shows 1 match (at 0x401000).
rule_b is completely hidden from the summary table! The independent match at 0x402000 is completely lost in the default view.
Verification in Verbose Mode (-v)
In verbose mode (which does not use suppression), both matches are correctly shown:
rule_a
scope function
matches 0x401000
rule_b (2 matches)
scope function
matches 0x401000
0x402000
This confirms that rule_b did indeed match at 0x402000 independently, yet was silently suppressed from the default user summary.
Is this how we want capa to work?
Summary
There is a silent information-loss correctness bug in
capa's default summary renderer (capa/render/default.py).To reduce clutter in the default summary table,
capasuppresses low-level "sub-rules" (rules that are matched as dependencies by higher-level parent rules). However, the current suppression logic is global and strictly name-based.If a sub-rule
Rule Bis matched by a parent ruleRule Ain Function 1,"Rule B"is added to the global suppression set. If"Rule B"also matches completely independently in Function 2 (whereRule Adoes not match),Rule Bis still completely suppressed. As a result, the independent match atFunction 2is silently hidden from the default CLI summary output, leading to incomplete capability reports.1. Technical Analysis & The Defect
In
capa/render/default.py, the functionfind_subrule_matchestraverses the match trees of all capability rules and adds the names of all successfully matched sub-rules to a global set:Then, in
render_capabilities,capacompletely skips rendering any rule whose name exists in that global set:Because the check is global and based only on the name, all independent matches of the sub-rule elsewhere in the binary are silently discarded in the default CLI output view.
2. Proof of Concept (PoC)
This bug can be demonstrated using a simple ruleset with two mock rules:
rule_b(Low-level, e.g.encrypt data using RC4)rule_a(High-level, e.g.Ransomware behavior, which depends onmatch: rule_b)Consider a mock binary with two functions:
Function 1 (0x401000): Contains features triggering bothrule_aandrule_b(sorule_bacts as a sub-rule).Function 2 (0x402000): Contains features triggering onlyrule_b(sorule_bmatches independently).Expected Behavior (CLI Summary)
rule_ashould show 1 match (at0x401000).rule_bshould show 1 match (at0x402000, representing the independent match that is not covered byrule_a).Actual Behavior (Current CLI Summary)
rule_ashows 1 match (at0x401000).rule_bis completely hidden from the summary table! The independent match at0x402000is completely lost in the default view.Verification in Verbose Mode (
-v)In verbose mode (which does not use suppression), both matches are correctly shown:
This confirms that
rule_bdid indeed match at0x402000independently, yet was silently suppressed from the default user summary.Is this how we want capa to work?