Repository: mandiant/capa
Issue: unexpected address type in verbose mode during dynamic analysis

```
@williballenthin ➜ /workspaces/capa (dynamic-feature-extraction) $ python -m capa.main -r rules1 tests/data/dynamic/cape/v2.2/d46900384c78863420fb3e297d0a2f743cd2b6b3f7f82bf64059a168e07aceb7.json -v
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/usr/local/lib/python3.10/runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "/workspaces/capa/capa/main.py", line 1655, in <module>
    sys.exit(main())
  File "/workspaces/capa/capa/main.py", line 1601, in main
    print(capa.render.verbose.render(meta, rules, capabilities))
  File "/workspaces/capa/capa/render/verbose.py", line 236, in render
    return render_verbose(rd.ResultDocument.from_capa(meta, rules, capabilities))
  File "/workspaces/capa/capa/render/verbose.py", line 229, in render_verbose
    render_rules(ostream, doc)
  File "/workspaces/capa/capa/render/verbose.py", line 214, in render_rules
    rows.append(("matches", "\\n".join(map(format_address, locations))))
  File "/workspaces/capa/capa/render/verbose.py", line 77, in format_address
    raise ValueError("unexpected address type")
ValueError: unexpected address type
```

d46900384c78863420fb3e297d0a2f743cd2b6b3f7f82bf64059a168e07aceb7 from v2.2 archive in https://github.com/mandiant/capa/issues/1698

  - [x] add test demonstrating rendering of the currently broken address type
  - [ ] fix the issue


Changed files:
capa/render/verbose.py, tests/test_extractor_hashing.py, tests/test_main.py, tests/fixtures.py

diff --git a/capa/render/verbose.py b/capa/render/verbose.py
index a5787f92..77392cf9 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
@@ -71,6 +69,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:
diff --git a/tests/fixtures.py b/tests/fixtures.py
index af6e5f8f..2bf81e67 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -42,7 +42,6 @@ from capa.features.extractors.base_extractor import (
     BBHandle,
     CallHandle,
     InsnHandle,
-    SampleHashes,
     ThreadHandle,
     ProcessHandle,
     FunctionHandle,
@@ -364,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:
@@ -653,54 +662,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 9bb2fe5e..4fa10a20 100644
--- a/tests/test_extractor_hashing.py
+++ b/tests/test_extractor_hashing.py
@@ -16,12 +16,48 @@ from capa.features.extractors.base_extractor import SampleHashes
 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.
diff --git a/tests/test_main.py b/tests/test_main.py
index da592dc4..d09f3397 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