From 1cf33e434312f845e52b5c4177519b345c5c2db9 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Fri, 11 Aug 2023 08:38:06 +0000 Subject: [PATCH 1/4] tests: create workspaces only during tests, not import closes #1707 --- tests/fixtures.py | 49 --------------------------------- tests/test_extractor_hashing.py | 48 ++++++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 55 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index af6e5f8f1..6d35485ee 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -42,7 +42,6 @@ BBHandle, CallHandle, InsnHandle, - SampleHashes, ThreadHandle, ProcessHandle, FunctionHandle, @@ -653,54 +652,6 @@ def parametrize(params, values, **kwargs): return pytest.mark.parametrize(params, values, ids=ids, **kwargs) -EXTRACTOR_HASHING_TESTS = [ - # viv extractor - ( - get_viv_extractor(get_data_path_by_name("mimikatz")), - SampleHashes( - md5="5f66b82558ca92e54e77f216ef4c066c", - sha1="e4f82e4d7f22938dc0a0ff8a4a7ad2a763643d38", - sha256="131314a6f6d1d263c75b9909586b3e1bd837036329ace5e69241749e861ac01d", - ), - ), - # PE extractor - ( - get_pefile_extractor(get_data_path_by_name("mimikatz")), - SampleHashes( - md5="5f66b82558ca92e54e77f216ef4c066c", - sha1="e4f82e4d7f22938dc0a0ff8a4a7ad2a763643d38", - sha256="131314a6f6d1d263c75b9909586b3e1bd837036329ace5e69241749e861ac01d", - ), - ), - # dnFile extractor - ( - get_dnfile_extractor(get_data_path_by_name("b9f5b")), - SampleHashes( - md5="b9f5bd514485fb06da39beff051b9fdc", - sha1="c72a2e50410475a51d897d29ffbbaf2103754d53", - sha256="34acc4c0b61b5ce0b37c3589f97d1f23e6d84011a241e6f85683ee517ce786f1", - ), - ), - # dotnet File - ( - get_dotnetfile_extractor(get_data_path_by_name("b9f5b")), - SampleHashes( - md5="b9f5bd514485fb06da39beff051b9fdc", - sha1="c72a2e50410475a51d897d29ffbbaf2103754d53", - sha256="34acc4c0b61b5ce0b37c3589f97d1f23e6d84011a241e6f85683ee517ce786f1", - ), - ), - # cape extractor - ( - get_cape_extractor(get_data_path_by_name("0000a657")), - SampleHashes( - md5="e2147b5333879f98d515cd9aa905d489", - sha1="ad4d520fb7792b4a5701df973d6bd8a6cbfbb57f", - sha256="0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82", - ), - ), -] - DYNAMIC_FEATURE_PRESENCE_TESTS = sorted( [ # file/string diff --git a/tests/test_extractor_hashing.py b/tests/test_extractor_hashing.py index 9bb2fe5e1..4fa10a202 100644 --- a/tests/test_extractor_hashing.py +++ b/tests/test_extractor_hashing.py @@ -16,12 +16,48 @@ logger = logging.getLogger(__name__) -@fixtures.parametrize( - "extractor,hashes", - fixtures.EXTRACTOR_HASHING_TESTS, -) -def test_hash_extraction(extractor, hashes): - assert extractor.get_sample_hashes() == hashes +def test_viv_hash_extraction(): + assert fixtures.get_viv_extractor(fixtures.get_data_path_by_name("mimikatz")).get_sample_hashes() == SampleHashes( + md5="5f66b82558ca92e54e77f216ef4c066c", + sha1="e4f82e4d7f22938dc0a0ff8a4a7ad2a763643d38", + sha256="131314a6f6d1d263c75b9909586b3e1bd837036329ace5e69241749e861ac01d", + ) + + +def test_pefile_hash_extraction(): + assert fixtures.get_pefile_extractor( + fixtures.get_data_path_by_name("mimikatz") + ).get_sample_hashes() == SampleHashes( + md5="5f66b82558ca92e54e77f216ef4c066c", + sha1="e4f82e4d7f22938dc0a0ff8a4a7ad2a763643d38", + sha256="131314a6f6d1d263c75b9909586b3e1bd837036329ace5e69241749e861ac01d", + ) + + +def test_dnfile_hash_extraction(): + assert fixtures.get_dnfile_extractor(fixtures.get_data_path_by_name("b9f5b")).get_sample_hashes() == SampleHashes( + md5="b9f5bd514485fb06da39beff051b9fdc", + sha1="c72a2e50410475a51d897d29ffbbaf2103754d53", + sha256="34acc4c0b61b5ce0b37c3589f97d1f23e6d84011a241e6f85683ee517ce786f1", + ) + + +def test_dotnetfile_hash_extraction(): + assert fixtures.get_dotnetfile_extractor( + fixtures.get_data_path_by_name("b9f5b") + ).get_sample_hashes() == SampleHashes( + md5="b9f5bd514485fb06da39beff051b9fdc", + sha1="c72a2e50410475a51d897d29ffbbaf2103754d53", + sha256="34acc4c0b61b5ce0b37c3589f97d1f23e6d84011a241e6f85683ee517ce786f1", + ) + + +def test_cape_hash_extraction(): + assert fixtures.get_cape_extractor(fixtures.get_data_path_by_name("0000a657")).get_sample_hashes() == SampleHashes( + md5="e2147b5333879f98d515cd9aa905d489", + sha1="ad4d520fb7792b4a5701df973d6bd8a6cbfbb57f", + sha256="0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82", + ) # We need to skip the binja test if we cannot import binaryninja, e.g., in GitHub CI. From 6de23a97487bf9789300a9d193b869bfe31a9981 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Fri, 11 Aug 2023 08:56:06 +0000 Subject: [PATCH 2/4] tests: main: demonstrate CAPE analysis (and bug #1702) --- tests/fixtures.py | 10 ++++++++ tests/test_main.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/tests/fixtures.py b/tests/fixtures.py index 6d35485ee..2bf81e67d 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -363,8 +363,18 @@ def get_data_path_by_name(name) -> Path: / "data" / "dynamic" / "cape" + / "v2.2" / "0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json.gz" ) + elif name.startswith("d46900"): + return ( + CD + / "data" + / "dynamic" + / "cape" + / "v2.2" + / "d46900384c78863420fb3e297d0a2f743cd2b6b3f7f82bf64059a168e07aceb7.json.gz" + ) elif name.startswith("ea2876"): return CD / "data" / "ea2876e9175410b6f6719f80ee44b9553960758c7d0f7bed73c0fe9a78d8e669.dll_" else: diff --git a/tests/test_main.py b/tests/test_main.py index da592dc45..d09f33975 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -6,8 +6,10 @@ # 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 gzip import json import textwrap +from pathlib import Path import pytest import fixtures @@ -582,3 +584,59 @@ def test_main_rd(): assert capa.main.main([path, "-j"]) == 0 assert capa.main.main([path, "-q"]) == 0 assert capa.main.main([path]) == 0 + + +def extract_cape_report(tmp_path: Path, gz: Path) -> Path: + report = tmp_path / "report.json" + report.write_bytes(gzip.decompress(gz.read_bytes())) + return report + + +def test_main_cape1(tmp_path): + path = extract_cape_report(tmp_path, fixtures.get_data_path_by_name("0000a657")) + + # TODO(williballenthin): use default rules set + # https://github.com/mandiant/capa/pull/1696 + rules = tmp_path / "rules" + rules.mkdir() + (rules / "create-or-open-registry-key.yml").write_text( + textwrap.dedent( + """ + rule: + meta: + name: create or open registry key + authors: + - testing + scopes: + static: instruction + dynamic: call + features: + - or: + - api: advapi32.RegOpenKey + - api: advapi32.RegOpenKeyEx + - api: advapi32.RegCreateKey + - api: advapi32.RegCreateKeyEx + - api: advapi32.RegOpenCurrentUser + - api: advapi32.RegOpenKeyTransacted + - api: advapi32.RegOpenUserClassesRoot + - api: advapi32.RegCreateKeyTransacted + - api: ZwOpenKey + - api: ZwOpenKeyEx + - api: ZwCreateKey + - api: ZwOpenKeyTransacted + - api: ZwOpenKeyTransactedEx + - api: ZwCreateKeyTransacted + - api: NtOpenKey + - api: NtCreateKey + - api: SHRegOpenUSKey + - api: SHRegCreateUSKey + - api: RtlCreateRegistryKey + """ + ) + ) + + assert capa.main.main([str(path), "-r", str(rules)]) == 0 + assert capa.main.main([str(path), "-q", "-r", str(rules)]) == 0 + assert capa.main.main([str(path), "-j", "-r", str(rules)]) == 0 + assert capa.main.main([str(path), "-v", "-r", str(rules)]) == 0 + assert capa.main.main([str(path), "-vv", "-r", str(rules)]) == 0 From dafbefb325dc6f106a7c9381df1a40bc0d9df2d4 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Fri, 11 Aug 2023 09:02:29 +0000 Subject: [PATCH 3/4] render: verbose: render call address closes #1702 --- capa/render/verbose.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/capa/render/verbose.py b/capa/render/verbose.py index a5787f920..87f9cd2ac 100644 --- a/capa/render/verbose.py +++ b/capa/render/verbose.py @@ -71,6 +71,10 @@ def format_address(address: frz.Address) -> str: tid = address.value assert isinstance(tid, int) return f"thread id: {tid}" + elif address.type == frz.AddressType.CALL: + assert isinstance(address.value, tuple) + ppid, pid, tid, id_ = address.value + return f"process ppid: {ppid}, process pid: {pid}, thread id: {tid}, call: {id_}" elif address.type == frz.AddressType.NO_ADDRESS: return "global" else: From f48e4a8ad8b28355e95ca9dc2a799dd1fc95282d Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Fri, 11 Aug 2023 09:07:11 +0000 Subject: [PATCH 4/4] render: verbose: render dynamic call return address --- capa/render/verbose.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/capa/render/verbose.py b/capa/render/verbose.py index 87f9cd2ac..77392cf92 100644 --- a/capa/render/verbose.py +++ b/capa/render/verbose.py @@ -56,10 +56,8 @@ def format_address(address: frz.Address) -> str: return f"token({capa.helpers.hex(token)})+{capa.helpers.hex(offset)}" elif address.type == frz.AddressType.DYNAMIC: assert isinstance(address.value, tuple) - id_, return_address = address.value - assert isinstance(id_, int) - assert isinstance(return_address, int) - return f"event: {id_}, retaddr: 0x{return_address:x}" + ppid, pid, tid, id_, return_address = address.value + return f"process ppid: {ppid}, process pid: {pid}, thread id: {tid}, call: {id_}, return address: {capa.helpers.hex(return_address)}" elif address.type == frz.AddressType.PROCESS: assert isinstance(address.value, tuple) ppid, pid = address.value