mirror of
https://github.com/mandiant/capa.git
synced 2025-12-05 20:40:05 -08:00
Replace the header from source code files using the following script:
```Python
for dir_path, dir_names, file_names in os.walk("capa"):
for file_name in file_names:
# header are only in `.py` and `.toml` files
if file_name[-3:] not in (".py", "oml"):
continue
file_path = f"{dir_path}/{file_name}"
f = open(file_path, "rb+")
content = f.read()
m = re.search(OLD_HEADER, content)
if not m:
continue
print(f"{file_path}: {m.group('year')}")
content = content.replace(m.group(0), NEW_HEADER % m.group("year"))
f.seek(0)
f.write(content)
```
Some files had the copyright headers inside a `"""` comment and needed
manual changes before applying the script. `hook-vivisect.py` and
`pyinstaller.spec` didn't include the license in the header and also
needed manual changes.
The old header had the confusing sentence `All rights reserved`, which
does not make sense for an open source license. Replace the header by
the default Google header that corrects this issue and keep capa
consistent with other Google projects.
Adapt the linter to work with the new header.
Replace also the copyright text in the `web/public/index.html` file for
consistency.
1626 lines
49 KiB
Python
1626 lines
49 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_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))
|