mirror of
https://github.com/mandiant/capa.git
synced 2025-12-22 23:26:21 -08:00
Get rid of the `characteristic(switch)` feature as any of our rules use it and its analysis is not very easy. Analysis results most likely differ across backends, leading to inconsistency.
803 lines
23 KiB
Python
803 lines
23 KiB
Python
# Copyright (C) 2020 FireEye, Inc. All Rights Reserved.
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at: [package root]/LICENSE.txt
|
|
# Unless required by applicable law or agreed to in writing, software distributed under the License
|
|
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and limitations under the License.
|
|
|
|
import textwrap
|
|
|
|
import pytest
|
|
|
|
import capa.rules
|
|
from capa.features import ARCH_X32, ARCH_X64, String
|
|
from capa.features.insn import Number, Offset
|
|
|
|
|
|
def test_rule_ctor():
|
|
r = capa.rules.Rule("test rule", capa.rules.FUNCTION_SCOPE, Number(1), {})
|
|
assert r.evaluate({Number(0): {1}}) == False
|
|
assert r.evaluate({Number(1): {1}}) == True
|
|
|
|
|
|
def test_rule_yaml():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
author: user@domain.com
|
|
scope: function
|
|
examples:
|
|
- foo1234
|
|
- bar5678
|
|
features:
|
|
- and:
|
|
- number: 1
|
|
- number: 2
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
assert r.evaluate({Number(0): {1}}) == False
|
|
assert r.evaluate({Number(0): {1}, Number(1): {1}}) == False
|
|
assert r.evaluate({Number(0): {1}, Number(1): {1}, Number(2): {1}}) == True
|
|
assert r.evaluate({Number(0): {1}, Number(1): {1}, Number(2): {1}, Number(3): {1}}) == True
|
|
|
|
|
|
def test_rule_yaml_complex():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- or:
|
|
- and:
|
|
- number: 1
|
|
- number: 2
|
|
- or:
|
|
- number: 3
|
|
- 2 or more:
|
|
- number: 4
|
|
- number: 5
|
|
- number: 6
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
assert r.evaluate({Number(5): {1}, Number(6): {1}, Number(7): {1}, Number(8): {1}}) == True
|
|
assert r.evaluate({Number(6): {1}, Number(7): {1}, Number(8): {1}}) == False
|
|
|
|
|
|
def test_rule_yaml_descriptions():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- and:
|
|
- number: 1 = This is the number 1
|
|
- string: This program cannot be run in DOS mode.
|
|
description: MS-DOS stub message
|
|
- string: '/SELECT.*FROM.*WHERE/i'
|
|
description: SQL WHERE Clause
|
|
- count(number(2 = AF_INET/SOCK_DGRAM)): 2
|
|
- or:
|
|
- and:
|
|
- offset: 0x50 = IMAGE_NT_HEADERS.OptionalHeader.SizeOfImage
|
|
- offset: 0x34 = IMAGE_NT_HEADERS.OptionalHeader.ImageBase
|
|
description: 32-bits
|
|
- and:
|
|
- offset: 0x50 = IMAGE_NT_HEADERS64.OptionalHeader.SizeOfImage
|
|
- offset: 0x30 = IMAGE_NT_HEADERS64.OptionalHeader.ImageBase
|
|
description: 64-bits
|
|
description: PE headers offsets
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
assert (
|
|
r.evaluate(
|
|
{
|
|
Number(1): {1},
|
|
Number(2): {2, 3},
|
|
String("This program cannot be run in DOS mode."): {4},
|
|
String("SELECT password FROM hidden_table WHERE user == admin"): {5},
|
|
Offset(0x50): {6},
|
|
Offset(0x30): {7},
|
|
}
|
|
)
|
|
== True
|
|
)
|
|
|
|
|
|
def test_rule_yaml_not():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- and:
|
|
- number: 1
|
|
- not:
|
|
- number: 2
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
assert r.evaluate({Number(1): {1}}) == True
|
|
assert r.evaluate({Number(1): {1}, Number(2): {1}}) == False
|
|
|
|
|
|
def test_rule_yaml_count():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- count(number(100)): 1
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
assert r.evaluate({Number(100): {}}) == False
|
|
assert r.evaluate({Number(100): {1}}) == True
|
|
assert r.evaluate({Number(100): {1, 2}}) == False
|
|
|
|
|
|
def test_rule_yaml_count_range():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- count(number(100)): (1, 2)
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
assert r.evaluate({Number(100): {}}) == False
|
|
assert r.evaluate({Number(100): {1}}) == True
|
|
assert r.evaluate({Number(100): {1, 2}}) == True
|
|
assert r.evaluate({Number(100): {1, 2, 3}}) == False
|
|
|
|
|
|
def test_invalid_rule_feature():
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- foo: true
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scope: file
|
|
features:
|
|
- characteristic: nzxor
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scope: function
|
|
features:
|
|
- characteristic: embedded pe
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scope: basic block
|
|
features:
|
|
- characteristic: embedded pe
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def test_lib_rules():
|
|
rules = capa.rules.RuleSet(
|
|
[
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: a lib rule
|
|
lib: true
|
|
features:
|
|
- api: CreateFileA
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: a standard rule
|
|
lib: false
|
|
features:
|
|
- api: CreateFileW
|
|
"""
|
|
)
|
|
),
|
|
]
|
|
)
|
|
assert len(rules.function_rules) == 1
|
|
|
|
|
|
def test_subscope_rules():
|
|
rules = capa.rules.RuleSet(
|
|
[
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scope: file
|
|
features:
|
|
- and:
|
|
- characteristic: embedded pe
|
|
- function:
|
|
- and:
|
|
- characteristic: nzxor
|
|
- characteristic: loop
|
|
"""
|
|
)
|
|
)
|
|
]
|
|
)
|
|
# the file rule scope will have one rules:
|
|
# - `test rule`
|
|
assert len(rules.file_rules) == 1
|
|
|
|
# the function rule scope have one rule:
|
|
# - the rule on which `test rule` depends
|
|
assert len(rules.function_rules) == 1
|
|
|
|
|
|
def test_duplicate_rules():
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
rules = capa.rules.RuleSet(
|
|
[
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule-name
|
|
features:
|
|
- api: CreateFileA
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule-name
|
|
features:
|
|
- api: CreateFileW
|
|
"""
|
|
)
|
|
),
|
|
]
|
|
)
|
|
|
|
|
|
def test_missing_dependency():
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
rules = capa.rules.RuleSet(
|
|
[
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: dependent rule
|
|
features:
|
|
- match: missing rule
|
|
"""
|
|
)
|
|
),
|
|
]
|
|
)
|
|
|
|
|
|
def test_invalid_rules():
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
r = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- characteristic: number(1)
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
r = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- characteristic: count(number(100))
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def test_number_symbol():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- and:
|
|
- number: 1
|
|
- number: 0xFFFFFFFF
|
|
- number: 2 = symbol name
|
|
- number: 3 = symbol name
|
|
- number: 4 = symbol name = another name
|
|
- number: 0x100 = symbol name
|
|
- number: 0x11 = (FLAG_A | FLAG_B)
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
children = list(r.statement.get_children())
|
|
assert (Number(1) in children) == True
|
|
assert (Number(0xFFFFFFFF) in children) == True
|
|
assert (Number(2, description="symbol name") in children) == True
|
|
assert (Number(3, description="symbol name") in children) == True
|
|
assert (Number(4, description="symbol name = another name") in children) == True
|
|
assert (Number(0x100, description="symbol name") in children) == True
|
|
|
|
|
|
def test_count_number_symbol():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- or:
|
|
- count(number(2 = symbol name)): 1
|
|
- count(number(0x100 = symbol name)): 2 or more
|
|
- count(number(0x11 = (FLAG_A | FLAG_B))): 2 or more
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
assert r.evaluate({Number(2): {}}) == False
|
|
assert r.evaluate({Number(2): {1}}) == True
|
|
assert r.evaluate({Number(2): {1, 2}}) == False
|
|
assert r.evaluate({Number(0x100, description="symbol name"): {1}}) == False
|
|
assert r.evaluate({Number(0x100, description="symbol name"): {1, 2, 3}}) == True
|
|
|
|
|
|
def test_invalid_number():
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
r = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- number: "this is a string"
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
r = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- number: 2=
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
r = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- number: symbol name = 2
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def test_number_arch():
|
|
r = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- number/x32: 2
|
|
"""
|
|
)
|
|
)
|
|
assert r.evaluate({Number(2, arch=ARCH_X32): {1}}) == True
|
|
|
|
assert r.evaluate({Number(2): {1}}) == False
|
|
assert r.evaluate({Number(2, arch=ARCH_X64): {1}}) == False
|
|
|
|
|
|
def test_offset_symbol():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- and:
|
|
- offset: 1
|
|
- offset: 2 = symbol name
|
|
- offset: 3 = symbol name
|
|
- offset: 4 = symbol name = another name
|
|
- offset: 0x100 = symbol name
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
children = list(r.statement.get_children())
|
|
assert (Offset(1) in children) == True
|
|
assert (Offset(2, description="symbol name") in children) == True
|
|
assert (Offset(3, description="symbol name") in children) == True
|
|
assert (Offset(4, description="symbol name = another name") in children) == True
|
|
assert (Offset(0x100, description="symbol name") in children) == True
|
|
|
|
|
|
def test_count_offset_symbol():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- or:
|
|
- count(offset(2 = symbol name)): 1
|
|
- count(offset(0x100 = symbol name)): 2 or more
|
|
- count(offset(0x11 = (FLAG_A | FLAG_B))): 2 or more
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
assert r.evaluate({Offset(2): {}}) == False
|
|
assert r.evaluate({Offset(2): {1}}) == True
|
|
assert r.evaluate({Offset(2): {1, 2}}) == False
|
|
assert r.evaluate({Offset(0x100, description="symbol name"): {1}}) == False
|
|
assert r.evaluate({Offset(0x100, description="symbol name"): {1, 2, 3}}) == True
|
|
|
|
|
|
def test_offset_arch():
|
|
r = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- offset/x32: 2
|
|
"""
|
|
)
|
|
)
|
|
assert r.evaluate({Offset(2, arch=ARCH_X32): {1}}) == True
|
|
|
|
assert r.evaluate({Offset(2): {1}}) == False
|
|
assert r.evaluate({Offset(2, arch=ARCH_X64): {1}}) == False
|
|
|
|
|
|
def test_invalid_offset():
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
r = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- offset: "this is a string"
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
r = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- offset: 2=
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
r = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- offset: symbol name = 2
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def test_invalid_string_values_int():
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
r = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- string: 123
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
r = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- string: 0x123
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def test_explicit_string_values_int():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- or:
|
|
- string: "123"
|
|
- string: "0x123"
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
children = list(r.statement.get_children())
|
|
assert (String("123") in children) == True
|
|
assert (String("0x123") in children) == True
|
|
|
|
|
|
def test_regex_values_always_string():
|
|
rules = [
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- or:
|
|
- string: /123/
|
|
- string: /0x123/
|
|
"""
|
|
)
|
|
),
|
|
]
|
|
features, matches = capa.engine.match(
|
|
capa.engine.topologically_order_rules(rules), {capa.features.String("123"): {1}}, 0x0,
|
|
)
|
|
assert capa.features.MatchedRule("test rule") in features
|
|
|
|
features, matches = capa.engine.match(
|
|
capa.engine.topologically_order_rules(rules), {capa.features.String("0x123"): {1}}, 0x0,
|
|
)
|
|
assert capa.features.MatchedRule("test rule") in features
|
|
|
|
|
|
def test_filter_rules():
|
|
rules = capa.rules.RuleSet(
|
|
[
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 1
|
|
author: joe
|
|
features:
|
|
- api: CreateFile
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 2
|
|
features:
|
|
- string: joe
|
|
"""
|
|
)
|
|
),
|
|
]
|
|
)
|
|
rules = rules.filter_rules_by_meta("joe")
|
|
assert len(rules) == 1
|
|
assert "rule 1" in rules.rules
|
|
|
|
|
|
def test_filter_rules_dependencies():
|
|
rules = capa.rules.RuleSet(
|
|
[
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 1
|
|
features:
|
|
- match: rule 2
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 2
|
|
features:
|
|
- match: rule 3
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 3
|
|
features:
|
|
- api: CreateFile
|
|
"""
|
|
)
|
|
),
|
|
]
|
|
)
|
|
rules = rules.filter_rules_by_meta("rule 1")
|
|
assert len(rules.rules) == 3
|
|
assert "rule 1" in rules.rules
|
|
assert "rule 2" in rules.rules
|
|
assert "rule 3" in rules.rules
|
|
|
|
|
|
def test_filter_rules_missing_dependency():
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
capa.rules.RuleSet(
|
|
[
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 1
|
|
author: joe
|
|
features:
|
|
- match: rule 2
|
|
"""
|
|
)
|
|
),
|
|
]
|
|
)
|
|
|
|
|
|
def test_rules_namespace_dependencies():
|
|
rules = [
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 1
|
|
namespace: ns1/nsA
|
|
features:
|
|
- api: CreateFile
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 2
|
|
namespace: ns1/nsB
|
|
features:
|
|
- api: CreateFile
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 3
|
|
features:
|
|
- match: ns1/nsA
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 4
|
|
features:
|
|
- match: ns1
|
|
"""
|
|
)
|
|
),
|
|
]
|
|
|
|
r3 = set(map(lambda r: r.name, capa.rules.get_rules_and_dependencies(rules, "rule 3")))
|
|
assert "rule 1" in r3
|
|
assert "rule 2" not in r3
|
|
assert "rule 4" not in r3
|
|
|
|
r4 = set(map(lambda r: r.name, capa.rules.get_rules_and_dependencies(rules, "rule 4")))
|
|
assert "rule 1" in r4
|
|
assert "rule 2" in r4
|
|
assert "rule 3" not in r4
|