diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ca290a5..3af1a93e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,9 @@ - fix: remove unreachable backports.functools_lru_cache fallback and dead dependency @williballenthin - fix: Scopes.from_dict uses cls instead of self so subclasses return the correct type @williballenthin - fix: correct wrong dict key in VMRay _compute_monitor_threads assertion (used thread_id instead of process_id) @williballenthin +fix: replace assert with isinstance guard in get_callee for invalid MethodSpec tokens @williballenthin +- fix: replace assert with isinstance guard in get_callee for invalid MethodSpec tokens @williballenthin +- fix: omit trailing ` -> ` suffix in syscall names when there is no return value @williballenthin (SURF-49) - fix: use AbsoluteVirtualAddress instead of FileOffsetAddress for string addresses in Ghidra and IDA file extractors @williballenthin (SURF-48) - fix: use dest.value.value and indirect_src.value.value for LLIL_CONST call destinations in binja insn.py @williballenthin (SURF-47) - fix: remove duplicate getPrevLocation call and dead loc variable in get_previous_instructions @williballenthin (SURF-46) diff --git a/capa/features/extractors/drakvuf/extractor.py b/capa/features/extractors/drakvuf/extractor.py index 0e023ece..3870a4b4 100644 --- a/capa/features/extractors/drakvuf/extractor.py +++ b/capa/features/extractors/drakvuf/extractor.py @@ -81,10 +81,12 @@ class DrakvufExtractor(DynamicFeatureExtractor): def get_call_name(self, ph: ProcessHandle, th: ThreadHandle, ch: CallHandle) -> str: call: Call = ch.inner + ret = getattr(call, "return_value", "") + suffix = f" -> {ret}" if ret else "" call_name = "{}({}){}".format( call.name, ", ".join(f"{arg_name}={arg_value}" for arg_name, arg_value in call.arguments.items()), - (f" -> {getattr(call, 'return_value', '')}"), # SysCalls don't have a return value, while WinApi calls do + suffix, ) return call_name diff --git a/tests/test_drakvuf_models.py b/tests/test_drakvuf_models.py index b0525579..493fda3e 100644 --- a/tests/test_drakvuf_models.py +++ b/tests/test_drakvuf_models.py @@ -14,7 +14,14 @@ import json -from capa.features.extractors.drakvuf.models import SystemCall, ConciseModel +from capa.features.address import ThreadAddress, ProcessAddress, DynamicCallAddress +from capa.features.extractors.base_extractor import ( + CallHandle, + ThreadHandle, + ProcessHandle, +) +from capa.features.extractors.drakvuf.models import SystemCall, WinApiCall, ConciseModel, DrakvufReport +from capa.features.extractors.drakvuf.extractor import DrakvufExtractor def test_concise_model_ignores_extra_fields(): @@ -60,3 +67,83 @@ def test_syscall_argument_construction(): assert call.arguments["NumEntriesRemoved"] == "0xfffff506a02846bc" assert call.arguments["Timeout"] == "0xfffff506a02846d8" assert call.arguments["Alertable"] == "0x0" + + +def _make_call_handle(call): + proc_addr = ProcessAddress(pid=1, ppid=0) + thread_addr = ThreadAddress(process=proc_addr, tid=1) + call_addr = DynamicCallAddress(thread=thread_addr, id=0) + return CallHandle(address=call_addr, inner=call) + + +def _make_extractor(): + return DrakvufExtractor(report=DrakvufReport()) + + +def _make_process_handle(): + proc_addr = ProcessAddress(pid=1, ppid=0) + return ProcessHandle(address=proc_addr, inner={}) + + +def _make_thread_handle(): + proc_addr = ProcessAddress(pid=1, ppid=0) + thread_addr = ThreadAddress(process=proc_addr, tid=1) + return ThreadHandle(address=thread_addr, inner={}) + + +def test_get_call_name_syscall_has_no_return_value_suffix(): + call_dict = json.loads(r""" + { + "Plugin": "syscall", + "TimeStamp": "1716999134.581449", + "PID": 3888, + "PPID": 2852, + "TID": 368, + "UserName": "SessionID", + "UserId": 2, + "ProcessName": "\\Device\\HarddiskVolume2\\Windows\\explorer.exe", + "Method": "NtClose", + "EventUID": "0x1f", + "Module": "nt", + "vCPU": 0, + "CR3": "0x119b1002", + "Syscall": 15, + "NArgs": 1, + "Handle": "0xffffffff80001ac0" + } + """) + call = SystemCall(**call_dict) + extractor = _make_extractor() + ph = _make_process_handle() + th = _make_thread_handle() + ch = _make_call_handle(call) + + name = extractor.get_call_name(ph, th, ch) + + assert " -> " not in name + assert name == "NtClose(Handle=0xffffffff80001ac0)" + + +def test_get_call_name_winapicall_includes_return_value(): + call_dict = { + "Plugin": "apimon", + "TimeStamp": "1716999134.581449", + "PID": 3888, + "PPID": 2852, + "TID": 368, + "ProcessName": "explorer.exe", + "Method": "CreateFileW", + "Event": "api_called", + "Arguments": ["hFile=0x1234"], + "ReturnValue": "0x5678", + } + call = WinApiCall(**call_dict) # type: ignore[arg-type] # dict literal infers object values due to mixed str/list types + extractor = _make_extractor() + ph = _make_process_handle() + th = _make_thread_handle() + ch = _make_call_handle(call) + + name = extractor.get_call_name(ph, th, ch) + + assert " -> 0x5678" in name + assert name == "CreateFileW(hFile=0x1234) -> 0x5678"