mirror of
https://github.com/mandiant/capa.git
synced 2025-12-05 20:40:05 -08:00
1656 lines
50 KiB
Python
1656 lines
50 KiB
Python
# Copyright 2020 Google LLC
|
|
#
|
|
# 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
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# 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
|
|
import capa.engine
|
|
import capa.features.common
|
|
import capa.features.address
|
|
from capa.engine import Or
|
|
from capa.features.file import FunctionName
|
|
from capa.features.insn import API, Number, Offset, Property
|
|
from capa.features.common import (
|
|
OS,
|
|
OS_LINUX,
|
|
ARCH_I386,
|
|
FORMAT_PE,
|
|
ARCH_AMD64,
|
|
FORMAT_ELF,
|
|
OS_WINDOWS,
|
|
Arch,
|
|
Format,
|
|
String,
|
|
Substring,
|
|
FeatureAccess,
|
|
)
|
|
|
|
ADDR1 = capa.features.address.AbsoluteVirtualAddress(0x401001)
|
|
ADDR2 = capa.features.address.AbsoluteVirtualAddress(0x401002)
|
|
ADDR3 = capa.features.address.AbsoluteVirtualAddress(0x401003)
|
|
ADDR4 = capa.features.address.AbsoluteVirtualAddress(0x401004)
|
|
|
|
|
|
def test_rule_ctor():
|
|
r = capa.rules.Rule(
|
|
"test rule", capa.rules.Scopes(capa.rules.Scope.FUNCTION, capa.rules.Scope.FILE), Or([Number(1)]), {}
|
|
)
|
|
assert bool(r.evaluate({Number(0): {ADDR1}})) is False
|
|
assert bool(r.evaluate({Number(1): {ADDR2}})) is True
|
|
|
|
|
|
def test_rule_yaml():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
authors:
|
|
- user@domain.com
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
examples:
|
|
- foo1234
|
|
- bar5678
|
|
features:
|
|
- and:
|
|
- number: 1
|
|
- number: 2
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
assert bool(r.evaluate({Number(0): {ADDR1}})) is False
|
|
assert bool(r.evaluate({Number(0): {ADDR1}, Number(1): {ADDR1}})) is False
|
|
assert bool(r.evaluate({Number(0): {ADDR1}, Number(1): {ADDR1}, Number(2): {ADDR1}})) is True
|
|
assert bool(r.evaluate({Number(0): {ADDR1}, Number(1): {ADDR1}, Number(2): {ADDR1}, Number(3): {ADDR1}})) is True
|
|
|
|
|
|
def test_rule_yaml_complex():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
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 bool(r.evaluate({Number(5): {ADDR1}, Number(6): {ADDR1}, Number(7): {ADDR1}, Number(8): {ADDR1}})) is True
|
|
assert bool(r.evaluate({Number(6): {ADDR1}, Number(7): {ADDR1}, Number(8): {ADDR1}})) is False
|
|
|
|
|
|
def test_rule_descriptions():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- and:
|
|
- description: and description
|
|
- number: 1 = number description
|
|
- string: mystring
|
|
description: string description
|
|
- string: '/myregex/'
|
|
description: regex description
|
|
- mnemonic: inc = mnemonic description
|
|
# TODO - count(number(2 = number description)): 2
|
|
- or:
|
|
- description: or description
|
|
- and:
|
|
- offset: 0x50 = offset description
|
|
- offset: 0x34 = offset description
|
|
- description: and description
|
|
- and:
|
|
- description: and description
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
|
|
def rec(statement):
|
|
if isinstance(statement, capa.engine.Statement):
|
|
assert statement.description == statement.name.lower() + " description"
|
|
for child in statement.get_children():
|
|
rec(child)
|
|
else:
|
|
if isinstance(statement.value, str):
|
|
assert "description" not in statement.value
|
|
assert statement.description == statement.name + " description"
|
|
|
|
rec(r.statement)
|
|
|
|
|
|
def test_invalid_rule_statement_descriptions():
|
|
# statements can only have one description
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- or:
|
|
- number: 1 = This is the number 1
|
|
- description: description
|
|
- description: another description (invalid)
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def test_rule_yaml_not():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- and:
|
|
- number: 1
|
|
- not:
|
|
- number: 2
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
assert bool(r.evaluate({Number(1): {ADDR1}})) is True
|
|
assert bool(r.evaluate({Number(1): {ADDR1}, Number(2): {ADDR1}})) is False
|
|
|
|
|
|
def test_rule_yaml_count():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- count(number(100)): 1
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
assert bool(r.evaluate({Number(100): set()})) is False
|
|
assert bool(r.evaluate({Number(100): {ADDR1}})) is True
|
|
assert bool(r.evaluate({Number(100): {ADDR1, ADDR2}})) is False
|
|
|
|
|
|
def test_rule_yaml_count_range():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- count(number(100)): (1, 2)
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
assert bool(r.evaluate({Number(100): set()})) is False
|
|
assert bool(r.evaluate({Number(100): {ADDR1}})) is True
|
|
assert bool(r.evaluate({Number(100): {ADDR1, ADDR2}})) is True
|
|
assert bool(r.evaluate({Number(100): {ADDR1, ADDR2, ADDR3}})) is False
|
|
|
|
|
|
def test_rule_yaml_count_string():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- count(string(foo)): 2
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
assert bool(r.evaluate({String("foo"): set()})) is False
|
|
assert bool(r.evaluate({String("foo"): {ADDR1}})) is False
|
|
assert bool(r.evaluate({String("foo"): {ADDR1, ADDR2}})) is True
|
|
assert bool(r.evaluate({String("foo"): {ADDR1, ADDR2, ADDR3}})) is False
|
|
|
|
|
|
def test_invalid_rule_feature():
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- foo: true
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: file
|
|
dynamic: process
|
|
features:
|
|
- characteristic: nzxor
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: thread
|
|
features:
|
|
- characteristic: embedded pe
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: basic block
|
|
dynamic: thread
|
|
features:
|
|
- characteristic: embedded pe
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def test_multi_scope_rules_features():
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- or:
|
|
- api: write
|
|
- and:
|
|
- os: linux
|
|
- mnemonic: syscall
|
|
- number: 1 = write
|
|
"""
|
|
)
|
|
)
|
|
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- or:
|
|
- api: read
|
|
- and:
|
|
- os: linux
|
|
- mnemonic: syscall
|
|
- number: 0 = read
|
|
"""
|
|
)
|
|
)
|
|
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: instruction
|
|
dynamic: call
|
|
features:
|
|
- and:
|
|
- or:
|
|
- api: socket
|
|
- and:
|
|
- os: linux
|
|
- mnemonic: syscall
|
|
- number: 41 = socket()
|
|
- number: 6 = IPPROTO_TCP
|
|
- number: 1 = SOCK_STREAM
|
|
- number: 2 = AF_INET
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def test_rules_flavor_filtering():
|
|
rules = [
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: static rule
|
|
scopes:
|
|
static: function
|
|
dynamic: unsupported
|
|
features:
|
|
- api: CreateFileA
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: dynamic rule
|
|
scopes:
|
|
static: unsupported
|
|
dynamic: thread
|
|
features:
|
|
- api: CreateFileA
|
|
"""
|
|
)
|
|
),
|
|
]
|
|
|
|
static_rules = capa.rules.RuleSet([r for r in rules if r.scopes.static is not None])
|
|
dynamic_rules = capa.rules.RuleSet([r for r in rules if r.scopes.dynamic is not None])
|
|
|
|
# only static rule
|
|
assert len(static_rules) == 1
|
|
# only dynamic rule
|
|
assert len(dynamic_rules) == 1
|
|
|
|
|
|
def test_meta_scope_keywords():
|
|
static_scopes = sorted([e.value for e in capa.rules.STATIC_SCOPES])
|
|
dynamic_scopes = sorted([e.value for e in capa.rules.DYNAMIC_SCOPES])
|
|
|
|
for static_scope in static_scopes:
|
|
for dynamic_scope in dynamic_scopes:
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
f"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: {static_scope}
|
|
dynamic: {dynamic_scope}
|
|
features:
|
|
- or:
|
|
- format: pe
|
|
"""
|
|
)
|
|
)
|
|
|
|
# its also ok to specify "unsupported"
|
|
for static_scope in static_scopes:
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
f"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: {static_scope}
|
|
dynamic: unsupported
|
|
features:
|
|
- or:
|
|
- format: pe
|
|
"""
|
|
)
|
|
)
|
|
for dynamic_scope in dynamic_scopes:
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
f"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: unsupported
|
|
dynamic: {dynamic_scope}
|
|
features:
|
|
- or:
|
|
- format: pe
|
|
"""
|
|
)
|
|
)
|
|
|
|
# but at least one scope must be specified
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes: {}
|
|
features:
|
|
- or:
|
|
- format: pe
|
|
"""
|
|
)
|
|
)
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: unsupported
|
|
dynamic: unsupported
|
|
features:
|
|
- or:
|
|
- format: pe
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def test_subscope_same_as_scope():
|
|
static_scopes = sorted(
|
|
[e.value for e in capa.rules.STATIC_SCOPES if e not in (capa.rules.Scope.FILE, capa.rules.Scope.GLOBAL)]
|
|
)
|
|
dynamic_scopes = sorted(
|
|
[e.value for e in capa.rules.DYNAMIC_SCOPES if e not in (capa.rules.Scope.FILE, capa.rules.Scope.GLOBAL)]
|
|
)
|
|
|
|
for static_scope in static_scopes:
|
|
for dynamic_scope in dynamic_scopes:
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
f"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: {static_scope}
|
|
dynamic: {dynamic_scope}
|
|
features:
|
|
- or:
|
|
- {static_scope}:
|
|
- format: pe
|
|
- {dynamic_scope}:
|
|
- format: pe
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def test_lib_rules():
|
|
rules = capa.rules.RuleSet(
|
|
[
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: a lib rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
lib: true
|
|
features:
|
|
- api: CreateFileA
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: a standard rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
lib: false
|
|
features:
|
|
- api: CreateFileW
|
|
"""
|
|
)
|
|
),
|
|
]
|
|
)
|
|
# lib rules are added to the rule set
|
|
assert len(rules.function_rules) == 2
|
|
|
|
|
|
def test_subscope_rules():
|
|
rules = capa.rules.RuleSet(
|
|
[
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test function subscope
|
|
scopes:
|
|
static: file
|
|
dynamic: process
|
|
features:
|
|
- and:
|
|
- characteristic: embedded pe
|
|
- function:
|
|
- and:
|
|
- characteristic: nzxor
|
|
- characteristic: loop
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test process subscope
|
|
scopes:
|
|
static: file
|
|
dynamic: file
|
|
features:
|
|
- and:
|
|
- import: WININET.dll.HttpOpenRequestW
|
|
- process:
|
|
- and:
|
|
- substring: "http://"
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test thread subscope
|
|
scopes:
|
|
static: file
|
|
dynamic: process
|
|
features:
|
|
- and:
|
|
- string: "explorer.exe"
|
|
- thread:
|
|
- api: HttpOpenRequestW
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test call subscope
|
|
scopes:
|
|
static: basic block
|
|
dynamic: thread
|
|
features:
|
|
- and:
|
|
- string: "explorer.exe"
|
|
- call:
|
|
- api: HttpOpenRequestW
|
|
"""
|
|
)
|
|
),
|
|
]
|
|
)
|
|
# the file rule scope will have four rules:
|
|
# - `test function subscope`, `test process subscope` and
|
|
# `test thread subscope` for the static scope
|
|
# - and `test process subscope` for both scopes
|
|
assert len(rules.file_rules) == 3
|
|
|
|
# the function rule scope have two rule:
|
|
# - the rule on which `test function subscope` depends
|
|
assert len(rules.function_rules) == 1
|
|
|
|
# the process rule scope has three rules:
|
|
# - the rule on which `test process subscope` depends,
|
|
assert len(rules.process_rules) == 3
|
|
|
|
# the thread rule scope has two rule:
|
|
# - the rule on which `test thread subscope` depends
|
|
# - the `test call subscope` rule
|
|
assert len(rules.thread_rules) == 2
|
|
|
|
# the call rule scope has one rule:
|
|
# - the rule on which `test call subcsope` depends
|
|
assert len(rules.call_rules) == 1
|
|
|
|
|
|
def test_duplicate_rules():
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.RuleSet(
|
|
[
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule-name
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- api: CreateFileA
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule-name
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- api: CreateFileW
|
|
"""
|
|
)
|
|
),
|
|
]
|
|
)
|
|
|
|
|
|
def test_missing_dependency():
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.RuleSet(
|
|
[
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: dependent rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- match: missing rule
|
|
"""
|
|
)
|
|
),
|
|
]
|
|
)
|
|
|
|
|
|
def test_invalid_rules():
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- characteristic: number(1)
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- characteristic: count(number(100))
|
|
"""
|
|
)
|
|
)
|
|
|
|
# att&ck and mbc must be lists
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
att&ck: Tactic::Technique::Subtechnique [Identifier]
|
|
features:
|
|
- number: 1
|
|
"""
|
|
)
|
|
)
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
mbc: Objective::Behavior::Method [Identifier]
|
|
features:
|
|
- number: 1
|
|
"""
|
|
)
|
|
)
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: basic block
|
|
behavior: process
|
|
features:
|
|
- number: 1
|
|
"""
|
|
)
|
|
)
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
legacy: basic block
|
|
dynamic: process
|
|
features:
|
|
- number: 1
|
|
"""
|
|
)
|
|
)
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: process
|
|
dynamic: process
|
|
features:
|
|
- number: 1
|
|
"""
|
|
)
|
|
)
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: basic block
|
|
dynamic: function
|
|
features:
|
|
- number: 1
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def test_number_symbol():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
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) is True
|
|
assert (Number(0xFFFFFFFF) in children) is True
|
|
assert (Number(2, description="symbol name") in children) is True
|
|
assert (Number(3, description="symbol name") in children) is True
|
|
assert (Number(4, description="symbol name = another name") in children) is True
|
|
assert (Number(0x100, description="symbol name") in children) is True
|
|
|
|
|
|
def test_count_number_symbol():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
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 bool(r.evaluate({Number(2): set()})) is False
|
|
assert bool(r.evaluate({Number(2): {ADDR1}})) is True
|
|
assert bool(r.evaluate({Number(2): {ADDR1, ADDR2}})) is False
|
|
assert bool(r.evaluate({Number(0x100, description="symbol name"): {ADDR1}})) is False
|
|
assert bool(r.evaluate({Number(0x100, description="symbol name"): {ADDR1, ADDR2, ADDR3}})) is True
|
|
|
|
|
|
def test_count_api():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: thread
|
|
features:
|
|
- or:
|
|
- count(api(kernel32.CreateFileA)): 1
|
|
- count(api(System.Convert::FromBase64String)): 1
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
# apis including their DLL names are not extracted anymore
|
|
assert bool(r.evaluate({API("kernel32.CreateFileA"): set()})) is False
|
|
assert bool(r.evaluate({API("kernel32.CreateFile"): set()})) is False
|
|
assert bool(r.evaluate({API("CreateFile"): {ADDR1}})) is False
|
|
assert bool(r.evaluate({API("CreateFileA"): {ADDR1}})) is True
|
|
assert bool(r.evaluate({API("System.Convert::FromBase64String"): {ADDR1}})) is True
|
|
|
|
|
|
def test_invalid_number():
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- number: "this is a string"
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- number: 2=
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- number: symbol name = 2
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def test_offset_symbol():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
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) is True
|
|
assert (Offset(2, description="symbol name") in children) is True
|
|
assert (Offset(3, description="symbol name") in children) is True
|
|
assert (Offset(4, description="symbol name = another name") in children) is True
|
|
assert (Offset(0x100, description="symbol name") in children) is True
|
|
|
|
|
|
def test_count_offset_symbol():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
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 bool(r.evaluate({Offset(2): set()})) is False
|
|
assert bool(r.evaluate({Offset(2): {ADDR1}})) is True
|
|
assert bool(r.evaluate({Offset(2): {ADDR1, ADDR2}})) is False
|
|
assert bool(r.evaluate({Offset(0x100, description="symbol name"): {ADDR1}})) is False
|
|
assert bool(r.evaluate({Offset(0x100, description="symbol name"): {ADDR1, ADDR2, ADDR3}})) is True
|
|
|
|
|
|
def test_invalid_offset():
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- offset: "this is a string"
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- offset: 2=
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- offset: symbol name = 2
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def test_invalid_string_values_int():
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- string: 123
|
|
"""
|
|
)
|
|
)
|
|
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- string: 0x123
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def test_explicit_string_values_int():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- or:
|
|
- string: "123"
|
|
- string: "0x123"
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
children = list(r.statement.get_children())
|
|
assert (String("123") in children) is True
|
|
assert (String("0x123") in children) is True
|
|
|
|
|
|
def test_string_values_special_characters():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- or:
|
|
- string: "hello\\r\\nworld"
|
|
- string: "bye\\nbye"
|
|
description: "test description"
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
children = list(r.statement.get_children())
|
|
assert (String("hello\r\nworld") in children) is True
|
|
assert (String("bye\nbye") in children) is True
|
|
|
|
|
|
def test_substring_feature():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- or:
|
|
- substring: abc
|
|
- substring: "def"
|
|
- substring: "gh\\ni"
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
children = list(r.statement.get_children())
|
|
assert (Substring("abc") in children) is True
|
|
assert (Substring("def") in children) is True
|
|
assert (Substring("gh\ni") in children) is True
|
|
|
|
|
|
def test_substring_description():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- or:
|
|
- substring: abc
|
|
description: the start of the alphabet
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
children = list(r.statement.get_children())
|
|
assert (Substring("abc") in children) is True
|
|
|
|
|
|
def test_filter_rules():
|
|
rules = capa.rules.RuleSet(
|
|
[
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 1
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
authors:
|
|
- joe
|
|
features:
|
|
- api: CreateFile
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 2
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
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
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- match: rule 2
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 2
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- match: rule 3
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 3
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
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
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
authors:
|
|
- joe
|
|
features:
|
|
- match: rule 2
|
|
"""
|
|
)
|
|
),
|
|
]
|
|
)
|
|
|
|
|
|
def test_rules_namespace_dependencies():
|
|
rules = [
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 1
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
namespace: ns1/nsA
|
|
features:
|
|
- api: CreateFile
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 2
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
namespace: ns1/nsB
|
|
features:
|
|
- api: CreateFile
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 3
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- match: ns1/nsA
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: rule 4
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- match: ns1
|
|
"""
|
|
)
|
|
),
|
|
]
|
|
|
|
r3 = {r.name for r in 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 = {r.name for r in 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
|
|
|
|
|
|
def test_function_name_features():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: file
|
|
dynamic: process
|
|
features:
|
|
- and:
|
|
- function-name: strcpy
|
|
- function-name: strcmp = copy from here to there
|
|
- function-name: strdup
|
|
description: duplicate a string
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
children = list(r.statement.get_children())
|
|
assert (FunctionName("strcpy") in children) is True
|
|
assert (FunctionName("strcmp", description="copy from here to there") in children) is True
|
|
assert (FunctionName("strdup", description="duplicate a string") in children) is True
|
|
|
|
|
|
def test_os_features():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: file
|
|
dynamic: process
|
|
features:
|
|
- and:
|
|
- os: windows
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
children = list(r.statement.get_children())
|
|
assert (OS(OS_WINDOWS) in children) is True
|
|
assert (OS(OS_LINUX) not in children) is True
|
|
|
|
|
|
def test_format_features():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: file
|
|
dynamic: process
|
|
features:
|
|
- and:
|
|
- format: pe
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
children = list(r.statement.get_children())
|
|
assert (Format(FORMAT_PE) in children) is True
|
|
assert (Format(FORMAT_ELF) not in children) is True
|
|
|
|
|
|
def test_arch_features():
|
|
rule = textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: file
|
|
dynamic: process
|
|
features:
|
|
- and:
|
|
- arch: amd64
|
|
"""
|
|
)
|
|
r = capa.rules.Rule.from_yaml(rule)
|
|
children = list(r.statement.get_children())
|
|
assert (Arch(ARCH_AMD64) in children) is True
|
|
assert (Arch(ARCH_I386) not in children) is True
|
|
|
|
|
|
def test_property_access():
|
|
r = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- property/read: System.IO.FileInfo::Length
|
|
"""
|
|
)
|
|
)
|
|
assert bool(r.evaluate({Property("System.IO.FileInfo::Length", access=FeatureAccess.READ): {ADDR1}})) is True
|
|
|
|
assert bool(r.evaluate({Property("System.IO.FileInfo::Length"): {ADDR1}})) is False
|
|
assert bool(r.evaluate({Property("System.IO.FileInfo::Length", access=FeatureAccess.WRITE): {ADDR1}})) is False
|
|
|
|
|
|
def test_property_access_symbol():
|
|
r = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
features:
|
|
- property/read: System.IO.FileInfo::Length = some property
|
|
"""
|
|
)
|
|
)
|
|
assert (
|
|
bool(
|
|
r.evaluate(
|
|
{
|
|
Property("System.IO.FileInfo::Length", access=FeatureAccess.READ, description="some property"): {
|
|
ADDR1
|
|
}
|
|
}
|
|
)
|
|
)
|
|
is True
|
|
)
|
|
|
|
|
|
def test_translate_com_features():
|
|
r = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
scopes:
|
|
static: basic block
|
|
dynamic: call
|
|
features:
|
|
- com/class: WICPngDecoder
|
|
# 389ea17b-5078-4cde-b6ef-25c15175c751 WICPngDecoder
|
|
# e018945b-aa86-4008-9bd4-6777a1e40c11 WICPngDecoder
|
|
"""
|
|
)
|
|
)
|
|
com_name = "WICPngDecoder"
|
|
com_features = [
|
|
capa.features.common.Bytes(b"{\xa1\x9e8xP\xdeL\xb6\xef%\xc1Qu\xc7Q", f"CLSID_{com_name} as bytes"),
|
|
capa.features.common.StringFactory("389ea17b-5078-4cde-b6ef-25c15175c751", f"CLSID_{com_name} as GUID string"),
|
|
capa.features.common.Bytes(b"[\x94\x18\xe0\x86\xaa\x08@\x9b\xd4gw\xa1\xe4\x0c\x11", f"IID_{com_name} as bytes"),
|
|
capa.features.common.StringFactory("e018945b-aa86-4008-9bd4-6777a1e40c11", f"IID_{com_name} as GUID string"),
|
|
]
|
|
assert set(com_features) == set(r.statement.get_children())
|
|
|
|
|
|
def test_invalid_com_features():
|
|
# test for unknown COM class
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- com/class: invalid_com
|
|
"""
|
|
)
|
|
)
|
|
|
|
# test for unknown COM interface
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- com/interface: invalid_com
|
|
"""
|
|
)
|
|
)
|
|
|
|
# test for invalid COM type
|
|
# valid_com_types = "class", "interface"
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
_ = capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule
|
|
features:
|
|
- com/invalid_COM_type: WICPngDecoder
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def test_circular_dependency():
|
|
rules = [
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule 1
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
lib: true
|
|
features:
|
|
- or:
|
|
- match: test rule 2
|
|
- api: kernel32.VirtualAlloc
|
|
"""
|
|
)
|
|
),
|
|
capa.rules.Rule.from_yaml(
|
|
textwrap.dedent(
|
|
"""
|
|
rule:
|
|
meta:
|
|
name: test rule 2
|
|
scopes:
|
|
static: function
|
|
dynamic: process
|
|
lib: true
|
|
features:
|
|
- match: test rule 1
|
|
"""
|
|
)
|
|
),
|
|
]
|
|
with pytest.raises(capa.rules.InvalidRule):
|
|
list(capa.rules.get_rules_and_dependencies(rules, rules[0].name))
|