Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## master (unreleased)

- Adds user-options for adding comments, bookmarks, and namespaces via a dialog box in `capa_explorer.py`.
- Implemented granular control over annotations:
- `create_capa_namespace`: Handles namespace creation and labels.
- `create_capa_comments`: Manages plate and pre-comments.

### New Features

### Breaking Changes
Expand Down
142 changes: 93 additions & 49 deletions capa/ghidra/capa_explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,19 @@ def __init__(
matches,
attack: list[dict[Any, Any]],
mbc: list[dict[Any, Any]],
do_labels: bool,
do_comments: bool,
do_bookmarks: bool,
):
self.namespace = namespace
self.scope = scope
self.capability = capability
self.matches = matches
self.attack = attack
self.mbc = mbc
self.do_labels = do_labels
self.do_comments = do_comments
self.do_bookmarks = do_bookmarks

def bookmark_functions(self):
"""create bookmarks for MITRE ATT&CK & MBC mappings"""
Expand Down Expand Up @@ -144,8 +150,11 @@ def set_pre_comment(self, ghidra_addr, sub_type, description):
else:
return

def label_matches(self):
"""label findings at function scopes and comment on subscope matches"""
def create_capa_namespace(self):
"""create namespace and labels for matched rules"""
if not self.do_labels:
return

capa_namespace = create_namespace(self.namespace)
symbol_table = currentProgram().getSymbolTable() # type: ignore [name-defined] # noqa: F821

Expand All @@ -160,21 +169,7 @@ def label_matches(self):
if sym is not None:
if sym.getSymbolType() == SymbolType.FUNCTION:
create_label(ghidra_addr, sym.getName(), capa_namespace)
self.set_plate_comment(ghidra_addr)

# parse the corresponding nodes, and pre-comment subscope matched features
# under the encompassing function(s)
for sub_match in self.matches.get(addr):
for loc, node in sub_match.items():
sub_ghidra_addr = toAddr(hex(loc)) # type: ignore [name-defined] # noqa: F821
if sub_ghidra_addr == ghidra_addr:
# skip duplicates
continue

# precomment subscope matches under the function
if node != {}:
for sub_type, description in parse_node(node):
self.set_pre_comment(sub_ghidra_addr, sub_type, description)

else:
# resolve the encompassing function for the capa namespace
# of non-function scoped main matches
Expand All @@ -187,41 +182,75 @@ def label_matches(self):
if func is not None:
func_addr = func.getEntryPoint()
create_label(func_addr, func.getName(), capa_namespace)
self.set_plate_comment(func_addr)

# create subscope match precomments
for sub_match in self.matches.get(addr):
for loc, node in sub_match.items():
sub_ghidra_addr = toAddr(hex(loc)) # type: ignore [name-defined] # noqa: F821
if func is not None:
# basic block/ insn scope under resolved function
# this would be a global/file scoped main match
# try to resolve the encompassing function via the subscope match, instead
# Ex. "run as service" rule
sub_func = getFunctionContaining(sub_ghidra_addr) # type: ignore [name-defined] # noqa: F821
if sub_func is not None:
sub_func_addr = sub_func.getEntryPoint()
# place function in capa namespace & create the subscope match label in Ghidra's global namespace
create_label(sub_func_addr, sub_func.getName(), capa_namespace)
else:
# addr is in some other file section like .data
# represent this location with a label symbol under the capa namespace
# Ex. See "Reference Base64 String" rule
# in many cases, these will be ghidra-labeled data, so just add the existing
# label symbol to the capa namespace
for sym in symbol_table.getSymbols(sub_ghidra_addr):
if sym.getSymbolType() == SymbolType.LABEL:
sym.setNamespace(capa_namespace)
Comment on lines +189 to +207

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The nested if func is not None and if sub_func is not None can be simplified by using guard clauses or combining the conditions. This would improve readability.


def create_capa_comments(self):
"""create comments for matched rules"""
if not self.do_comments:
return

if node != {}:
if func is not None:
# basic block/ insn scope under resolved function
# create plate comments for the main rule match
for addr in self.matches.keys():
ghidra_addr = toAddr(hex(addr)) # type: ignore [name-defined] # noqa: F821
if self.scope == "function":
self.set_plate_comment(ghidra_addr)
else:
func = getFunctionContaining(ghidra_addr) # type: ignore [name-defined] # noqa: F821
if func is not None:
self.set_plate_comment(func.getEntryPoint())

# create pre comments for subscoped matches of main rules
for addr in self.matches.keys():
for sub_match in self.matches.get(addr):
for loc, node in sub_match.items():
sub_ghidra_addr = toAddr(hex(loc)) # type: ignore [name-defined] # noqa: F821
func = getFunctionContaining(sub_ghidra_addr) # type: ignore [name-defined] # noqa: F821

if node != {}:
if func is not None:
# basic block / insn scope under resolved function
for sub_type, description in parse_node(node):
self.set_pre_comment(sub_ghidra_addr, sub_type, description)
else:
# this would be a global/file scoped main match
# try to resolve the encompassing function via the subscope match, instead
# Ex. "run as service" rule
sub_func = getFunctionContaining(sub_ghidra_addr) # type: ignore [name-defined] # noqa: F821
if sub_func is not None:
sub_func_addr = sub_func.getEntryPoint()
self.set_plate_comment(sub_func_addr)
for sub_type, description in parse_node(node):
self.set_pre_comment(sub_ghidra_addr, sub_type, description)
else:
# this would be a global/file scoped main match
# try to resolve the encompassing function via the subscope match, instead
# Ex. "run as service" rule
sub_func = getFunctionContaining(sub_ghidra_addr) # type: ignore [name-defined] # noqa: F821
if sub_func is not None:
sub_func_addr = sub_func.getEntryPoint()
# place function in capa namespace & create the subscope match label in Ghidra's global namespace
create_label(sub_func_addr, sub_func.getName(), capa_namespace)
self.set_plate_comment(sub_func_addr)
for sub_type, description in parse_node(node):
self.set_pre_comment(sub_ghidra_addr, sub_type, description)
else:
# addr is in some other file section like .data
# represent this location with a label symbol under the capa namespace
# Ex. See "Reference Base64 String" rule
for sub_type, description in parse_node(node):
# in many cases, these will be ghidra-labeled data, so just add the existing
# label symbol to the capa namespace
for sym in symbol_table.getSymbols(sub_ghidra_addr):
if sym.getSymbolType() == SymbolType.LABEL:
sym.setNamespace(capa_namespace)
self.set_pre_comment(sub_ghidra_addr, sub_type, description)
# addr is in some other file section like .data
# represent this location with a label symbol under the capa namespace
# Ex. See "Reference Base64 String" rule
# in many cases, these will be ghidra-labeled data, so just add the existing
# label symbol to the capa namespace
for sub_type, description in parse_node(node):
self.set_pre_comment(sub_ghidra_addr, sub_type, description)
Comment on lines +231 to +253

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block of code is similar to the one in label_matches. Consider refactoring this into a helper function to reduce duplication.

Comment on lines +232 to +253

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The nested if func is not None and if sub_func is not None can be simplified by using guard clauses or combining the conditions. This would improve readability.



def get_capabilities():
Expand Down Expand Up @@ -283,7 +312,7 @@ def parse_node(node_data):
yield feat_type, data


def parse_json(capa_data):
def parse_json(capa_data, do_labels, do_comments, do_bookmarks):
"""Parse json produced by capa"""

for rule, capability in capa_data.get("rules", {}).items():
Expand Down Expand Up @@ -335,7 +364,7 @@ def parse_json(capa_data):
# Ex. 'contain loop' -> capa::lib::contain-loop
namespace = "capa" + Namespace.DELIMITER + "lib" + fmt_rule

yield CapaMatchData(namespace, scope, rule, rule_matches, attack, mbc)
yield CapaMatchData(namespace, scope, rule, rule_matches, attack, mbc, do_labels, do_comments, do_bookmarks)


def main():
Expand Down Expand Up @@ -366,9 +395,24 @@ def main():
popup("capa explorer found no matches.") # type: ignore [name-defined] # noqa: F821
return capa.main.E_EMPTY_REPORT

for item in parse_json(capa_data):
item.bookmark_functions()
item.label_matches()
options = ["Add Labels/Namespace", "Add Comments", "Add Bookmarks"]
selected_options = askChoices("Capa Explorer Options", "Select options for capa analysis", options, options) # type: ignore [name-defined] # noqa: F821

do_labels = "Add Labels/Namespace" in selected_options
do_comments = "Add Comments" in selected_options
do_bookmarks = "Add Bookmarks" in selected_options

if not any([do_bookmarks, do_comments, do_labels]):
logger.info("No annotations selected")
return 0

for item in parse_json(capa_data, do_labels, do_comments, do_bookmarks):
if do_labels:
item.create_capa_namespace()
if do_comments:
item.create_capa_comments()
if do_bookmarks:
item.bookmark_functions()
logger.info("capa explorer analysis complete")
popup("capa explorer analysis complete.\nPlease see results in the Bookmarks Window and Namespaces section of the Symbol Tree Window.") # type: ignore [name-defined] # noqa: F821
return 0
Expand Down
2 changes: 1 addition & 1 deletion rules
Submodule rules updated 50 files
+2 −2 anti-analysis/anti-av/check-for-sandbox-and-av-modules.yml
+2 −2 anti-analysis/anti-vm/vm-detection/reference-anti-vm-strings-targeting-parallels.yml
+1 −1 anti-analysis/anti-vm/vm-detection/reference-anti-vm-strings-targeting-qemu.yml
+11 −10 anti-analysis/anti-vm/vm-detection/reference-anti-vm-strings-targeting-virtualbox.yml
+2 −2 anti-analysis/anti-vm/vm-detection/reference-anti-vm-strings-targeting-virtualpc.yml
+2 −2 anti-analysis/anti-vm/vm-detection/reference-anti-vm-strings-targeting-vmware.yml
+1 −1 anti-analysis/anti-vm/vm-detection/reference-anti-vm-strings-targeting-xen.yml
+19 −19 anti-analysis/anti-vm/vm-detection/reference-anti-vm-strings.yml
+63 −63 communication/dns/reference-dns-over-https-endpoints.yml
+1 −4 data-manipulation/compression/decompress-data-using-aplib.yml
+0 −42 data-manipulation/encryption/chaskey/encrypt-data-using-chaskey.yml
+0 −37 data-manipulation/encryption/speck/encrypt-data-using-speck.yml
+2 −2 host-interaction/bootloader/manipulate-boot-configuration.yml
+1 −1 host-interaction/driver/disable-driver-code-integrity.yml
+2 −2 host-interaction/gui/taskbar/find/find-taskbar.yml
+1 −1 host-interaction/gui/window/find/find-graphical-window.yml
+11 −13 host-interaction/mutex/check-mutex-on-windows.yml
+0 −2 host-interaction/mutex/create-or-open-mutex-on-windows.yml
+12 −21 host-interaction/process/inject/allocate-or-change-rwx-memory.yml
+1 −4 linking/runtime-linking/link-function-at-runtime-on-windows.yml
+0 −39 load-code/dotnet/load-assembly-via-iassembly.yml
+0 −64 malware-family/donut-loader/load-shellcode-via-donut.yml
+0 −19 nursery/decrypt-data-using-tripledes-in-dotnet.yml
+0 −27 nursery/disable-device-guard-features-via-registry-on-windows.yml
+0 −27 nursery/disable-firewall-features-via-registry-on-windows.yml
+0 −76 nursery/disable-system-features-via-registry-on-windows.yml
+0 −20 nursery/disable-system-restore-features-via-registry-on-windows.yml
+0 −49 nursery/disable-windows-defender-features-via-registry-on-windows.yml
+0 −19 nursery/encrypt-data-using-tripledes-in-dotnet.yml
+5 −5 nursery/enumerate-device-drivers-on-windows.yml
+1 −1 nursery/persist-via-appcertdlls-registry-key.yml
+1 −1 nursery/persist-via-autodialdll-registry-key.yml
+1 −1 nursery/persist-via-bootverificationprogram-registry-key.yml
+1 −1 nursery/persist-via-dotnet-dbgmanageddebugger-registry-key.yml
+1 −1 nursery/persist-via-errorhandler-script.yml
+1 −1 nursery/persist-via-get-variable-hijack.yml
+2 −2 nursery/persist-via-lsa-registry-key.yml
+1 −1 nursery/persist-via-natural-language-registry-key.yml
+1 −1 nursery/persist-via-network-provider-registry-key.yml
+2 −2 nursery/persist-via-powershell-profile.yml
+1 −1 nursery/persist-via-print-monitors-registry-key.yml
+1 −1 nursery/persist-via-print-processors-registry-key.yml
+1 −1 nursery/persist-via-rdp-startup-programs-registry-key.yml
+1 −1 nursery/persist-via-timeproviders-registry-key.yml
+1 −1 nursery/persist-via-ts-initialprogram-registry-key.yml
+7 −7 nursery/persist-via-windows-accessibility-tools.yml
+1 −1 nursery/persist-via-windows-terminal-profile.yml
+1 −1 persistence/registry/run/persist-via-run-registry-key.yml
+1 −1 persistence/service/persist-via-rc-script.yml
+1 −1 persistence/service/persist-via-windows-service.yml
Loading