Package modules :: Package reporting :: Module maec40
[hide private]
[frames] | no frames]

Source Code for Module modules.reporting.maec40

  1  # Copyright (c) 2013, The MITRE Corporation 
  2  # Copyright (c) 2010-2015, Cuckoo Developers 
  3  # All rights reserved. 
  4   
  5  # This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org 
  6  # See the file "docs/LICENSE" for copying permission. 
  7   
  8  import os 
  9  import hashlib 
 10  import re 
 11  import traceback 
 12  from collections import defaultdict 
 13   
 14  from lib.maec.maec40 import api_call_mappings, hiveHexToString,\ 
 15      socketTypeToString, socketProtoToString, socketAFToString,\ 
 16      regDatatypeToString, intToHex, regStringToKey, regStringToHive 
 17   
 18  from lib.cuckoo.common.abstracts import Report 
 19  from lib.cuckoo.common.exceptions import CuckooDependencyError, CuckooReportError 
 20  from lib.cuckoo.common.utils import datetime_to_iso 
 21   
 22  try: 
 23      import cybox 
 24      import cybox.utils.nsparser 
 25      from cybox.core import Object 
 26      from cybox.common import ToolInformation 
 27      from cybox.common import StructuredText 
 28      from maec.bundle.bundle import Bundle 
 29      from maec.bundle.malware_action import MalwareAction 
 30      from maec.bundle.bundle_reference import BundleReference 
 31      from maec.bundle.process_tree import ProcessTree 
 32      from maec.bundle.av_classification import AVClassification 
 33      from maec.id_generator import Generator 
 34      from maec.package.malware_subject import MalwareSubject 
 35      from maec.package.package import Package 
 36      from maec.package.analysis import Analysis 
 37      from maec.utils import MAECNamespaceParser 
 38      HAVE_MAEC = True 
 39  except ImportError: 
 40      HAVE_MAEC = False 
 41   
42 -class MAEC40Report(Report):
43 """Generates a MAEC 4.0.1 report. 44 --Output modes (set in reporting.conf): 45 mode = "full": Output fully mapped Actions (see maec40_mappings), including Windows Handle mapped/substituted objects, 46 along with API call/parameter capture via Action Implementations. 47 mode = "overview": Output only fully mapped Actions, without any Action Implementations. Default mode. 48 mode = "api": Output only Actions with Action Implementations, but no mapped components. 49 --Other configuration parameters: 50 processtree = "true" | "false". Output captured ProcessTree as part of dynamic analysis MAEC Bundle. Default = "true". 51 output_handles = "true" | "false". Output the Windows Handles used to construct the Object-Handle mappings as a 52 separate Object Collection in the dynamic analysis MAEC Bundle. Only applicable 53 for mode = "full" or mode = "overview". Default = "false". 54 static = "true" | "false". Output Cuckoo static analysis (PEfile) output as a separate MAEC Bundle in the document. 55 Default = "true". 56 strings = "true" | "false". Output Cuckoo strings output as a separate MAEC Bundle in the document. Default = "true". 57 virustotal = "true" | "false". Output VirusTotal output as a separate MAEC Bundle in the document. Default = "true". 58 """ 59
60 - def run(self, results):
61 """Writes report. 62 @param results: Cuckoo results dict. 63 @raise CuckooReportError: if fails to write report. 64 """ 65 # We put the raise here and not at the import because it would 66 # otherwise trigger even if the module is not enabled in the config. 67 if not HAVE_MAEC: 68 raise CuckooDependencyError("Unable to import cybox and maec (install with `pip install cybox==2.0.1.4` then `pip install maec==4.0.1.0`)") 69 70 self._illegal_xml_chars_RE = re.compile(u"[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]") 71 # Map of PIDs to the Actions that they spawned. 72 self.pidActionMap = {} 73 # Windows Handle map. 74 self.handleMap = {} 75 # Save results. 76 self.results = results 77 # Setup MAEC document structure. 78 self.setupMAEC() 79 # Build MAEC doc. 80 self.addSubjectAttributes() 81 self.addDroppedFiles() 82 self.addAnalyses() 83 self.addActions() 84 self.addProcessTree() 85 # Write XML report. 86 self.output()
87
88 - def setupMAEC(self):
89 """Generates MAEC Package, Malware Subject, and Bundle structure""" 90 if "target" in self.results and self.results["target"]["category"] == "file": 91 self.id_generator = Generator(self.results["target"]["file"]["md5"]) 92 elif "target" in self.results and self.results["target"]["category"] == "url": 93 self.id_generator = Generator(hashlib.md5(self.results["target"]["url"]).hexdigest()) 94 else: 95 raise CuckooReportError("Unknown target type or targetinfo module disabled") 96 97 # Generate Package. 98 self.package = Package(self.id_generator.generate_package_id()) 99 # Generate Malware Subject. 100 self.subject = MalwareSubject(self.id_generator.generate_malware_subject_id()) 101 # Add the Subject to the Package. 102 self.package.add_malware_subject(self.subject) 103 # Generate dynamic analysis bundle. 104 self.dynamic_bundle = Bundle(self.id_generator.generate_bundle_id(), False, "4.0.1", "dynamic analysis tool output") 105 # Add the Bundle to the Subject. 106 self.subject.add_findings_bundle(self.dynamic_bundle) 107 # Generate Static Analysis Bundles, if static results exist. 108 if self.options["static"] and "static" in self.results and self.results["static"]: 109 self.static_bundle = Bundle(self.id_generator.generate_bundle_id(), False, "4.0.1", "static analysis tool output") 110 self.subject.add_findings_bundle(self.static_bundle) 111 if self.options["strings"] and "strings" in self.results and self.results["strings"]: 112 self.strings_bundle = Bundle(self.id_generator.generate_bundle_id(), False, "4.0.1", "static analysis tool output") 113 self.subject.add_findings_bundle(self.strings_bundle) 114 if self.options["virustotal"] and "virustotal" in self.results and self.results["virustotal"]: 115 self.virustotal_bundle = Bundle(self.id_generator.generate_bundle_id(), False, "4.0.1", "static analysis tool output") 116 self.subject.add_findings_bundle(self.virustotal_bundle)
117
118 - def addActions(self):
119 """Add Actions section.""" 120 # Process-initiated Actions. 121 if "behavior" in self.results and "processes" in self.results["behavior"]: 122 for process in self.results["behavior"]["processes"]: 123 self.createProcessActions(process) 124 # Network actions. 125 if "network" in self.results and isinstance(self.results["network"], dict) and len(self.results["network"]) > 0: 126 if "udp" in self.results["network"] and isinstance(self.results["network"]["udp"], list) and len(self.results["network"]["udp"]) > 0: 127 if not self.dynamic_bundle.collections.action_collections.has_collection("Network Actions"): 128 self.dynamic_bundle.add_named_action_collection("Network Actions", self.id_generator.generate_action_collection_id()) 129 for network_data in self.results["network"]["udp"]: 130 self.createActionNet(network_data, {"value": "connect to socket address", "xsi:type": "maecVocabs:NetworkActionNameVocab-1.0"}, "UDP") 131 if "dns" in self.results["network"] and isinstance(self.results["network"]["dns"], list) and len(self.results["network"]["dns"]) > 0: 132 if not self.dynamic_bundle.collections.action_collections.has_collection("Network Actions"): 133 self.dynamic_bundle.add_named_action_collection("Network Actions", self.id_generator.generate_action_collection_id()) 134 for network_data in self.results["network"]["dns"]: 135 self.createActionNet(network_data, {"value": "send dns query", "xsi:type": "maecVocabs:DNSActionNameVocab-1.0"}, "UDP", "DNS") 136 if "tcp" in self.results["network"] and isinstance(self.results["network"]["tcp"], list) and len(self.results["network"]["tcp"]) > 0: 137 if not self.dynamic_bundle.collections.action_collections.has_collection("Network Actions"): 138 self.dynamic_bundle.add_named_action_collection("Network Actions", self.id_generator.generate_action_collection_id()) 139 for network_data in self.results["network"]["tcp"]: 140 self.createActionNet(network_data, {"value": "connect to socket address", "xsi:type": "maecVocabs:NetworkActionNameVocab-1.0"}, "TCP") 141 if "http" in self.results["network"] and isinstance(self.results["network"]["http"], list) and len(self.results["network"]["http"]) > 0: 142 if not self.dynamic_bundle.collections.action_collections.has_collection("Network Actions"): 143 self.dynamic_bundle.add_named_action_collection("Network Actions", self.id_generator.generate_action_collection_id()) 144 for network_data in self.results["network"]["http"]: 145 self.createActionNet(network_data, {"value": "send http " + str(network_data["method"]).lower() + " request", "xsi:type": "maecVocabs:HTTPActionNameVocab-1.0"}, "TCP", "HTTP")
146
147 - def createActionNet(self, network_data, action_name, layer4_protocol=None, layer7_protocol=None):
148 """Create a network Action. 149 @return: action. 150 """ 151 src_category = "ipv4-addr" 152 dst_category = "ipv4-addr" 153 if ":" in network_data.get("src", ""): src_category = "ipv6-addr" 154 if ":" in network_data.get("dst", ""): dst_category = "ipv6-addr" 155 # Construct the various dictionaries. 156 if layer7_protocol is not None: 157 object_properties = {"xsi:type": "NetworkConnectionObjectType", 158 "layer4_protocol": {"value": layer4_protocol, "force_datatype": True}, 159 "layer7_protocol": {"value": layer7_protocol, "force_datatype": True}} 160 else: 161 object_properties = {"xsi:type": "NetworkConnectionObjectType", 162 "layer4_protocol": {"value": layer4_protocol, "force_datatype": True}} 163 associated_object = {"id": self.id_generator.generate_object_id(), "properties": object_properties} 164 # General network connection properties. 165 if layer7_protocol is None: 166 object_properties["source_socket_address"] = {"ip_address": {"category": src_category, "address_value": network_data["src"]}, 167 "port": {"port_value": network_data["sport"]}} 168 object_properties["destination_socket_address"] = {"ip_address": {"category": dst_category, "address_value": network_data["dst"]}, 169 "port": {"port_value": network_data["dport"]}} 170 # Layer 7-specific object properties. 171 if layer7_protocol == "DNS": 172 answer_resource_records = [] 173 for answer_record in network_data["answers"]: 174 answer_resource_records.append({"entity_type": answer_record["type"], 175 "record_data": answer_record["data"]}) 176 object_properties["layer7_connections"] = {"dns_queries": [{"question": {"qname": {"value": network_data["request"]}, 177 "qtype": network_data["type"]}, 178 "answer_resource_records": answer_resource_records}]} 179 elif layer7_protocol == "HTTP": 180 object_properties["layer7_connections"] = {"http_session": 181 {"http_request_response": [{"http_client_request": {"http_request_line": {"http_method": {"value" : network_data["method"], "force_datatype": True}, 182 "value": network_data["path"], 183 "version": network_data["version"]}, 184 "http_request_header": {"parsed_header": {"user_agent": network_data["user-agent"], 185 "host": {"domain_name": {"value": network_data["host"]}, 186 "port": {"port_value": network_data["port"]}}}}, 187 "http_message_body": {"message_body": network_data["body"]}} 188 } 189 ]} 190 } 191 action_dict = {"id": self.id_generator.generate_malware_action_id(), 192 "name": action_name, 193 "associated_objects": [associated_object]} 194 # Add the Action to the dynamic analysis bundle. 195 self.dynamic_bundle.add_action(MalwareAction.from_dict(action_dict), "Network Actions")
196
197 - def addProcessTree(self):
198 """Creates the ProcessTree corresponding to that observed by Cuckoo.""" 199 if self.options["processtree"] and "behavior" in self.results and "processtree" in self.results["behavior"] and self.results["behavior"]["processtree"]: 200 # Process Tree TypedField Fix. 201 NS_LIST = cybox.utils.nsparser.NS_LIST + [ 202 ("http://maec.mitre.org/XMLSchema/maec-bundle-4", "maecBundle", "http://maec.mitre.org/language/version4.0.1/maec_bundle_schema.xsd"), 203 ] 204 OBJ_LIST = cybox.utils.nsparser.OBJ_LIST + [ 205 ("ProcessTreeNodeType", "maec.bundle.process_tree.ProcessTreeNode", "", "http://cybox.mitre.org/objects#ProcessObject-2", ["ProcessObjectType"]), 206 ] 207 cybox.META = cybox.utils.nsparser.Metadata(NS_LIST, OBJ_LIST) 208 209 root_node = self.results["behavior"]["processtree"][0] 210 211 if root_node: 212 root_node_dict = {"id": self.id_generator.generate_process_tree_node_id(), 213 "pid": root_node["pid"], 214 "name": root_node["name"], 215 "initiated_actions": self.pidActionMap[root_node["pid"]], 216 "spawned_processes": [self.createProcessTreeNode(child_process) for child_process in root_node["children"]]} 217 218 self.dynamic_bundle.set_process_tree(ProcessTree.from_dict({"root_process": root_node_dict}))
219
220 - def createProcessTreeNode(self, process):
221 """Creates a single ProcessTreeNode corresponding to a single node in the tree observed cuckoo. 222 @param process: process from cuckoo dict. 223 """ 224 process_node_dict = {"id": self.id_generator.generate_process_tree_node_id(), 225 "pid": process["pid"], 226 "name": process["name"], 227 "initiated_actions": self.pidActionMap[process["pid"]], 228 "spawned_processes": [self.createProcessTreeNode(child_process) for child_process in process["children"]]} 229 return process_node_dict
230
231 - def apiCallToAction(self, call, pos):
232 """Create and return a dictionary representing a MAEC Malware Action. 233 @param call: the input API call. 234 @param pos: position of the Action with respect to the execution of the malware. 235 """ 236 # Setup the action/action implementation dictionaries and lists. 237 action_dict = {} 238 parameter_list = [] 239 # Add the action parameter arguments. 240 apos = 1 241 for arg in call["arguments"]: 242 parameter_list.append({"ordinal_position": apos, 243 "name": arg["name"], 244 "value": self._illegal_xml_chars_RE.sub("?", arg["value"]) 245 }) 246 apos = apos + 1 247 # Try to add the mapped Action Name. 248 if call["api"] in api_call_mappings: 249 mapping_dict = api_call_mappings[call["api"]] 250 # Handle the Action Name. 251 if "action_vocab" in mapping_dict: 252 action_dict["name"] = {"value": mapping_dict["action_name"], "xsi:type": mapping_dict["action_vocab"]} 253 else: 254 action_dict["name"] = {"value": mapping_dict["action_name"]} 255 # Try to add the mapped Action Arguments and Associated Objects. 256 # Only output in "overview" or "full" modes. 257 if self.options["mode"].lower() == "overview" or self.options["mode"].lower() == "full": 258 # Check to make sure we have a mapping for this API call. 259 if call["api"] in api_call_mappings: 260 mapping_dict = api_call_mappings[call["api"]] 261 # Handle the Action Name. 262 if "action_vocab" in mapping_dict: 263 action_dict["name"] = {"value": mapping_dict["action_name"], "xsi:type": mapping_dict["action_vocab"]} 264 else: 265 action_dict["name"] = {"value": mapping_dict["action_name"]} 266 # Handle any Parameters. 267 if "parameter_associated_arguments" in mapping_dict: 268 actions_args = self.processActionArguments(mapping_dict["parameter_associated_arguments"], parameter_list) 269 if actions_args: 270 action_dict["action_arguments"] = actions_args 271 else: 272 action_dict["action_arguments"] = [] 273 # Handle any Associated Objects. 274 if "parameter_associated_objects" in mapping_dict: 275 action_dict["associated_objects"] = self.processActionAssociatedObjects(mapping_dict["parameter_associated_objects"], parameter_list) 276 277 # Only output Implementation in "api" or "full" modes. 278 if self.options["mode"].lower() == "api" or self.options["mode"].lower() == "full": 279 action_dict["implementation"] = self.processActionImplementation(call, parameter_list) 280 281 # Add the common Action properties. 282 action_dict["id"] = self.id_generator.generate_malware_action_id() 283 action_dict["ordinal_position"] = pos 284 action_dict["action_status"] = self.mapActionStatus(call["status"]) 285 action_dict["timestamp"] = str(call["timestamp"]).replace(" ", "T").replace(",", ".") 286 287 return action_dict
288
289 - def processActionImplementation(self, call, parameter_list):
290 """Creates a MAEC Action Implementation based on API call input. 291 @param parameter_list: the input parameter list (from the API call). 292 """ 293 # Generate the API Call dictionary. 294 if len(parameter_list) > 0: 295 api_call_dict = {"function_name": call["api"], 296 "return_value": call["return"], 297 "parameters": parameter_list} 298 else: 299 api_call_dict = {"function_name": call["api"], 300 "return_value": call["return"]} 301 # Generate the action implementation dictionary. 302 action_implementation_dict = {"id": self.id_generator.generate_action_implementation_id(), 303 "type": "api call", 304 "api_call": api_call_dict} 305 return action_implementation_dict
306
307 - def processActionArguments(self, parameter_mappings_dict, parameter_list):
308 """Processes a dictionary of parameters that should be mapped to Action Arguments in the Malware Action. 309 @param parameter_mappings_dict: the input parameter to Arguments mappings. 310 @param parameter_list: the input parameter list (from the API call). 311 """ 312 arguments_list = [] 313 for call_parameter in parameter_list: 314 parameter_name = call_parameter["name"] 315 argument_value = call_parameter["value"] 316 # Make sure the argument value is set, otherwise skip this parameter. 317 if not argument_value: 318 continue 319 if parameter_name in parameter_mappings_dict and "associated_argument_vocab" in parameter_mappings_dict[parameter_name]: 320 arguments_list.append({"argument_value": argument_value, 321 "argument_name": {"value": parameter_mappings_dict[parameter_name]["associated_argument_name"], 322 "xsi:type": parameter_mappings_dict[parameter_name]["associated_argument_vocab"]}}) 323 elif parameter_name in parameter_mappings_dict and "associated_argument_vocab" not in parameter_mappings_dict[parameter_name]: 324 arguments_list.append({"argument_value": argument_value, 325 "argument_name": {"value": parameter_mappings_dict[parameter_name]["associated_argument_name"]}}) 326 return arguments_list
327 328
329 - def processActionAssociatedObjects(self, associated_objects_dict, parameter_list):
330 """Processes a dictionary of parameters that should be mapped to Associated Objects in the Action 331 @param associated_objects_dict: the input parameter to Associated_Objects mappings. 332 @param parameter_list: the input parameter list (from the API call). 333 """ 334 associated_objects_list = [] 335 processed_parameters = [] 336 # First, handle any parameters that need to be grouped together into a single Object. 337 if "group_together" in associated_objects_dict: 338 grouped_list = associated_objects_dict["group_together"] 339 associated_object_dict = {} 340 associated_object_dict["id"] = self.id_generator.generate_object_id() 341 associated_object_dict["properties"] = {} 342 for parameter_name in grouped_list: 343 parameter_value = self.getParameterValue(parameter_list, parameter_name) 344 # Make sure the parameter value is set. 345 if parameter_value: 346 self.processAssociatedObject(associated_objects_dict[parameter_name], parameter_value, associated_object_dict) 347 # Add the parameter to the list of those that have already been processed. 348 processed_parameters.append(parameter_name) 349 associated_objects_list.append(associated_object_dict) 350 # Handle grouped nested parameters (corner case). 351 if "group_together_nested" in associated_objects_dict: 352 nested_group_dict = associated_objects_dict["group_together_nested"] 353 # Construct the values dictionary. 354 values_dict = {} 355 for parameter_mapping in nested_group_dict["parameter_mappings"]: 356 parameter_value = self.getParameterValue(parameter_list, parameter_mapping["parameter_name"]) 357 # Handle any values that require post-processing (via external functions). 358 if "post_processing" in parameter_mapping: 359 parameter_value = globals()[parameter_mapping["post_processing"]](parameter_value) 360 # Make sure the parameter value is set. 361 if parameter_value and "/" not in parameter_mapping["element_name"]: 362 values_dict[parameter_mapping["element_name"].lower()] = parameter_value 363 elif parameter_value and "/" in parameter_mapping["element_name"]: 364 split_element_name = parameter_mapping["element_name"].split("/") 365 values_dict[split_element_name[0].lower()] = self.createNestedDict(split_element_name[1:], parameter_value) 366 # Make sure we have data in the values dictionary. 367 if values_dict: 368 associated_objects_list.append(self.processAssociatedObject(nested_group_dict, values_dict)) 369 # Handle non-grouped, normal parameters. 370 for call_parameter in parameter_list: 371 if call_parameter["name"] not in processed_parameters and call_parameter["name"] in associated_objects_dict: 372 parameter_value = self.getParameterValue(parameter_list, call_parameter["name"]) 373 # Make sure the parameter value is set. 374 if parameter_value: 375 associated_objects_list.append(self.processAssociatedObject(associated_objects_dict[call_parameter["name"]], parameter_value)) 376 if associated_objects_list: 377 # Process any RegKeys to account for the Hive == Handle corner case. 378 self.processRegKeys(associated_objects_list) 379 # Perform Windows Handle Update/Replacement Processing. 380 return self.processWinHandles(associated_objects_list) 381 else: 382 return []
383
384 - def processWinHandles(self, associated_objects_list):
385 """Process any Windows Handles that may be associated with an Action. Replace Handle references with 386 actual Object, if possible. 387 @param associated_objects_list: the list of associated_objects processed for the Action. 388 """ 389 input_handles = [] 390 output_handles = [] 391 input_objects = [] 392 output_objects = [] 393 394 # Add the named object collections if they do not exist. 395 if not self.dynamic_bundle.collections.object_collections.has_collection("Handle-mapped Objects"): 396 self.dynamic_bundle.add_named_object_collection("Handle-mapped Objects", self.id_generator.generate_object_collection_id()) 397 if self.options["output_handles"] and not self.dynamic_bundle.collections.object_collections.has_collection("Windows Handles"): 398 self.dynamic_bundle.add_named_object_collection("Windows Handles", self.id_generator.generate_object_collection_id()) 399 # Determine the types of objects we're dealing with. 400 for associated_object_dict in associated_objects_list: 401 object_type = associated_object_dict["properties"]["xsi:type"] 402 object_association_type = associated_object_dict["association_type"]["value"] 403 # Check for handle objects. 404 if object_type is "WindowsHandleObjectType": 405 if object_association_type is "output": 406 output_handles.append(associated_object_dict) 407 elif object_association_type is "input": 408 input_handles.append(associated_object_dict) 409 # Check for non-handle objects. 410 elif object_type is not "WindowsHandleObjectType": 411 if object_association_type is "output": 412 output_objects.append(associated_object_dict) 413 elif object_association_type is "input": 414 input_objects.append(associated_object_dict) 415 # Handle the different cases. 416 # If no input/output handle, then just return the list unchanged. 417 if not input_handles and not output_handles: 418 return associated_objects_list 419 # Handle the case where there is an input object and output handle. 420 # Also handle the case where there is an output handle and output object. 421 if len(output_handles) == 1: 422 mapped_object = None 423 output_handle = output_handles[0] 424 if len(input_objects) == 1: 425 mapped_object = input_objects[0] 426 elif len(output_objects) == 1: 427 mapped_object = output_objects[0] 428 # Add the handle to the mapping and get the substituted object. 429 if mapped_object: 430 substituted_object = self.addHandleToMap(output_handle, mapped_object) 431 if substituted_object: 432 associated_objects_list.remove(mapped_object) 433 associated_objects_list.remove(output_handle) 434 associated_objects_list.append(substituted_object) 435 # Handle the corner case for certain calls with two output handles and input objects or output objects. 436 elif len(output_handles) == 2: 437 object_list = [] 438 if len(input_objects) == 2: 439 object_list = input_objects 440 elif len(output_objects) == 2: 441 object_list = output_objects 442 443 for object in object_list: 444 if "properties" in object and object["properties"]["xsi:type"] is "WindowsThreadObjectType": 445 for output_handle in output_handles: 446 if "type" in output_handle["properties"] and output_handle["properties"]["type"] is "Thread": 447 substituted_object = self.addHandleToMap(output_handle, object) 448 if substituted_object: 449 associated_objects_list.remove(object) 450 associated_objects_list.remove(output_handle) 451 associated_objects_list.append(substituted_object) 452 elif "properties" in object and object["properties"]["xsi:type"] is "ProcessObjectType": 453 for output_handle in output_handles: 454 if "type" in output_handle["properties"] and output_handle["properties"]["type"] is "Process": 455 substituted_object = self.addHandleToMap(output_handle, object) 456 if substituted_object: 457 associated_objects_list.remove(object) 458 associated_objects_list.remove(output_handle) 459 associated_objects_list.append(substituted_object) 460 461 # Handle the case where there is an . 462 # Lookup the handle and replace it with the appropriate object if we've seen it before. 463 for input_handle in input_handles: 464 if "type" in input_handle["properties"]: 465 handle_type = input_handle["properties"]["type"] 466 handle_id = input_handle["properties"]["id"] 467 if handle_type in self.handleMap and handle_id in self.handleMap[handle_type]: 468 merged_objects = False 469 mapped_object = self.handleMap[handle_type][handle_id] 470 # If the input object is of the same type, then "merge" them into a new object. 471 for input_object in input_objects: 472 if input_object["properties"]["xsi:type"] == mapped_object["properties"]["xsi:type"]: 473 merged_dict = defaultdict(dict) 474 for k, v in input_object.iteritems(): 475 if isinstance(v, dict): 476 merged_dict[k].update(v) 477 else: 478 merged_dict[k] = v 479 for k, v in mapped_object.iteritems(): 480 if isinstance(v, dict): 481 merged_dict[k].update(v) 482 else: 483 merged_dict[k] = v 484 # Assign the merged object a new ID. 485 merged_dict["id"] = self.id_generator.generate_object_id() 486 # Set the association type to that of the input object. 487 merged_dict["association_type"] = input_object["association_type"] 488 # Add the new object to the list of associated objects. 489 associated_objects_list.remove(input_handle) 490 associated_objects_list.remove(input_object) 491 associated_objects_list.append(merged_dict) 492 merged_objects = True 493 # Otherwise, add the existing object via a reference. 494 if not merged_objects: 495 substituted_object = {"idref": mapped_object["id"], 496 "association_type": {"value": "input", "xsi:type": "maecVocabs:ActionObjectAssociationTypeVocab-1.0"}} 497 associated_objects_list.remove(input_handle) 498 associated_objects_list.append(substituted_object) 499 return associated_objects_list
500
501 - def addHandleToMap(self, handle_dict, object_dict):
502 """Add a new Handle/Object pairing to the Handle mappings dictionary. 503 @param handle_dict: the dictionary of the Handle to which the object is mapped. 504 @param object_dict: the dictionary of the object mapped to the Handle. 505 return: the substituted object dictionary 506 """ 507 if "type" in handle_dict["properties"]: 508 handle_type = handle_dict["properties"]["type"] 509 handle_id = handle_dict["properties"]["id"] 510 substituted_object = {"idref": object_dict["id"], 511 "association_type": object_dict["association_type"]} 512 if handle_type not in self.handleMap: 513 self.handleMap[handle_type] = {} 514 self.handleMap[handle_type][handle_id] = object_dict 515 # Add the Handle to the Mapped Object as a related object. 516 # This is optional, as the handles themselves may not be very useful. 517 if self.options["output_handles"]: 518 handle_reference_dict = {} 519 handle_reference_dict["relationship"] = {"value": "Related_To", "xsi:type": "cyboxVocabs:ObjectRelationshipVocab-1.0"} 520 handle_reference_dict["idref"] = handle_dict["id"] 521 object_dict["related_objects"] = [handle_reference_dict] 522 # Add the Objects to their corresponding Collections. 523 self.dynamic_bundle.add_object(Object.from_dict(handle_dict), "Windows Handles") 524 self.dynamic_bundle.add_object(Object.from_dict(object_dict), "Handle-mapped Objects") 525 return substituted_object 526 return None
527
528 - def processRegKeys(self, associated_objects_list):
529 """Process any Registry Key associated with an action. Special case to handle registry Hives that may refer to Handles. 530 @param associated_objects_list: the list of associated_objects processed for the Action. 531 """ 532 for associated_object in associated_objects_list: 533 if associated_object["properties"]["xsi:type"] is "WindowsRegistryKeyObjectType": 534 if "hive" in associated_object["properties"] and "HKEY_" not in associated_object["properties"]["hive"]: 535 associated_object = self.processRegKeyHandle(associated_object["properties"]["hive"], associated_object)
536
537 - def processRegKeyHandle(self, handle_id, current_dict):
538 """Process a Registry Key Handle and return the full key, recursing as necessary. 539 @param handle_id: the id of the root-level handle 540 @param current_dict: the dictionary containing the properties of the current key 541 """ 542 if "RegistryKey" in self.handleMap and handle_id in self.handleMap["RegistryKey"]: 543 handle_mapped_key = self.handleMap["RegistryKey"][handle_id] 544 if "key" in handle_mapped_key["properties"]: 545 if "key" not in current_dict["properties"]: 546 current_dict["properties"]["key"] = "" 547 current_dict["properties"]["key"] = (handle_mapped_key["properties"]["key"] + "\\" + current_dict["properties"]["key"]) 548 if "hive" in handle_mapped_key["properties"]: 549 # If we find the "HKEY_" then we assume we're done. 550 if "HKEY_" in handle_mapped_key["properties"]["hive"]: 551 current_dict["properties"]["hive"] = handle_mapped_key["properties"]["hive"] 552 return current_dict 553 # If not, then we assume the hive refers to a Handle so we recurse. 554 else: 555 self.processRegKeyHandle(handle_mapped_key["properties"]["hive"], current_dict) 556 else: 557 return current_dict
558
559 - def processAssociatedObject(self, parameter_mapping_dict, parameter_value, associated_object_dict = None):
560 """Process a single Associated Object mapping. 561 @param parameter_mapping_dict: input parameter to Associated Object mapping dictionary. 562 @param parameter_value: the input parameter value (from the API call). 563 @param associated_object_dict: optional associated object dict, for special cases. 564 """ 565 if not associated_object_dict: 566 associated_object_dict = {} 567 associated_object_dict["id"] = self.id_generator.generate_object_id() 568 associated_object_dict["properties"] = {} 569 # Set the Association Type if it has not been set already. 570 if "association_type" not in associated_object_dict: 571 associated_object_dict["association_type"] = {"value": parameter_mapping_dict["association_type"], "xsi:type": "maecVocabs:ActionObjectAssociationTypeVocab-1.0"} 572 # Handle any values that require post-processing (via external functions). 573 if "post_processing" in parameter_mapping_dict: 574 parameter_value = globals()[parameter_mapping_dict["post_processing"]](parameter_value) 575 576 # Handle the actual element value 577 if "associated_object_element" in parameter_mapping_dict and parameter_mapping_dict["associated_object_element"]: 578 # Handle simple (non-nested) elements 579 if "/" not in parameter_mapping_dict["associated_object_element"]: 580 associated_object_dict["properties"][parameter_mapping_dict["associated_object_element"].lower()] = parameter_value 581 # Handle complex (nested) elements. 582 elif "/" in parameter_mapping_dict["associated_object_element"]: 583 split_elements = parameter_mapping_dict["associated_object_element"].split("/") 584 if "list__" in split_elements[0]: 585 associated_object_dict["properties"][split_elements[0].lstrip("list__").lower()] = [self.createNestedDict(split_elements[1:], parameter_value)] 586 else: 587 associated_object_dict["properties"][split_elements[0].lower()] = self.createNestedDict(split_elements[1:], parameter_value) 588 # Corner case for some Registry Keys 589 else: 590 associated_object_dict["properties"] = parameter_value 591 # Set any "forced" properties that should be set alongside the current 592 if "forced" in parameter_mapping_dict: 593 self.processAssociatedObject(parameter_mapping_dict["forced"], parameter_mapping_dict["forced"]["value"], associated_object_dict) 594 # Finally, set the XSI type if it has not been set already. 595 if "associated_object_type" in parameter_mapping_dict and "xsi:type" not in associated_object_dict["properties"]: 596 associated_object_dict["properties"]["xsi:type"] = parameter_mapping_dict["associated_object_type"] 597 598 return associated_object_dict
599
600 - def createNestedDict(self, list, value):
601 """Helper function: returns a nested dictionary for an input list. 602 @param list: input list. 603 @param value: value to set the last embedded dictionary item to. 604 """ 605 nested_dict = {} 606 607 if len(list) == 1: 608 if "list__" in list[0]: 609 if isinstance(value, dict): 610 list_element = [value] 611 else: 612 list_element = [{list[0].lstrip("list__").lower(): value}] 613 return list_element 614 else: 615 nested_dict[list[0].lower()] = value 616 return nested_dict 617 618 for list_item in list: 619 next_index = list.index(list_item) + 1 620 if "list__" in list_item: 621 nested_dict[list_item.lower().lstrip("list__")] = [self.createNestedDict(list[next_index:], value)] 622 else: 623 nested_dict[list_item.lower()] = self.createNestedDict(list[next_index:], value) 624 break 625 626 return nested_dict
627
628 - def getParameterValue(self, parameter_list, parameter_name):
629 """Finds and returns an API call parameter value from a list. 630 @param parameter_list: list of API call parameters. 631 @param parameter_name: name of parameter to return value for. 632 """ 633 for parameter_dict in parameter_list: 634 if parameter_dict["name"] == parameter_name: 635 return parameter_dict["value"]
636
637 - def createProcessActions(self, process):
638 """Creates the Actions corresponding to the API calls initiated by a process. 639 @param process: process from cuckoo dict. 640 """ 641 pos = 1 642 pid = process["process_id"] 643 644 for call in process["calls"]: 645 # Generate the action collection name and create a new named action collection if one does not exist. 646 action_collection_name = str(call["category"]).capitalize() + " Actions" 647 if not self.dynamic_bundle.collections.action_collections.has_collection(action_collection_name): 648 self.dynamic_bundle.add_named_action_collection(action_collection_name, self.id_generator.generate_action_collection_id()) 649 650 # Generate the Action dictionary. 651 action_dict = self.apiCallToAction(call, pos) 652 653 # Add the action ID to the list of Actions spawned by the process. 654 if pid in self.pidActionMap: 655 action_list = self.pidActionMap[pid].append({"action_id": action_dict["id"]}) 656 else: 657 self.pidActionMap[pid] = [{"action_id": action_dict["id"]}] 658 659 # Add the action to the dynamic analysis Bundle. 660 self.dynamic_bundle.add_action(MalwareAction.from_dict(action_dict), action_collection_name) 661 # Update the action position 662 pos = pos + 1
663 664 # Map the Cuckoo status to that used in the MAEC/CybOX action_status field.
665 - def mapActionStatus(self, status):
666 if status is True or status == 1: 667 return "Success" 668 elif status is False or status == 0: 669 return "Fail" 670 else: 671 return None
672
673 - def createWinExecFileObj(self):
674 """Creates a Windows Executable File (PE) object for capturing static analysis output. 675 """ 676 677 # A mapping of Cuckoo resource type names to their name in MAEC 678 resource_type_mappings = {"GIF": "Bitmap", 679 "RT_ACCELERATOR": "Accelerators", 680 "RT_ANICURSOR": "AniCursor", 681 "RT_ANIICON": "AniIcon", 682 "RT_BITMAP": "Bitmap", 683 "RT_CURSOR": "Cursor", 684 "RT_DIALOG": "Dialog", 685 "RT_DLGINCLUDE": "DLGInclude", 686 "RT_FONT": "Font", 687 "RT_FONTDIR": "Fontdir", 688 "RT_GROUP_CURSOR": "GroupCursor", 689 "RT_GROUP_ICON": "GroupIcon", 690 "RT_HTML": "HTML", 691 "RT_ICON": "Icon", 692 "RT_MANIFEST": "Manifest", 693 "RT_MENU": "Menu", 694 "RT_PLUGPLAY": "PlugPlay", 695 "RT_RCDATA": "RCData", 696 "RT_STRING": "String", 697 "RT_VERSION": "VersionInfo", 698 "RT_VXD": "Vxd"} 699 700 if len(self.results["static"]) > 0: 701 exports = None 702 imports = None 703 sections = None 704 resources = None 705 706 # PE exports. 707 if "pe_exports" in self.results["static"] and len(self.results["static"]["pe_exports"]) > 0: 708 exports = {} 709 exported_function_list = [] 710 for x in self.results["static"]["pe_exports"]: 711 exported_function_dict = { 712 "function_name": x["name"], 713 "ordinal": x["ordinal"], 714 "entry_point": x["address"] 715 } 716 exported_function_list.append(exported_function_dict) 717 exports["exported_functions"] = exported_function_list 718 # PE Imports. 719 if "pe_imports" in self.results["static"] and len(self.results["static"]["pe_imports"]) > 0: 720 imports = [] 721 for x in self.results["static"]["pe_imports"]: 722 imported_functions = [] 723 import_dict = { "file_name": x["dll"], 724 "imported_functions": imported_functions} 725 726 # Imported functions. 727 for i in x["imports"]: 728 imported_function_dict = {"function_name": i["name"], 729 "virtual_address": i["address"]} 730 imported_functions.append(imported_function_dict) 731 imports.append(import_dict) 732 # Resources. 733 if "pe_resources" in self.results["static"] and len(self.results["static"]["pe_resources"]) > 0: 734 resources = [] 735 for r in self.results["static"]["pe_resources"]: 736 if r["name"] in resource_type_mappings: 737 resource_dict = {"type": resource_type_mappings[r["name"]]} 738 resources.append(resource_dict) 739 # Sections. 740 if "pe_sections" in self.results["static"] and len(self.results["static"]["pe_sections"]) > 0: 741 sections = [] 742 for s in self.results["static"]["pe_sections"]: 743 section_dict = {"section_header": 744 {"virtual_size": int(s["virtual_size"], 16), 745 "virtual_address": s["virtual_address"], 746 "name": s["name"], 747 "size_of_raw_data": s["size_of_data"] 748 }, 749 "entropy": {"value": s["entropy"]} 750 } 751 sections.append(section_dict) 752 # Version info. 753 if "pe_versioninfo" in self.results["static"] and len(self.results["static"]["pe_versioninfo"]) > 0: 754 if not resources: 755 resources = [] 756 version_info = {} 757 for k in self.results["static"]["pe_versioninfo"]: 758 if not k["value"]: 759 continue 760 if k["name"].lower() == "comments": 761 version_info["comments"] = k["value"] 762 if k["name"].lower() == "companyname": 763 version_info["companyname"] = k["value"] 764 if k["name"].lower() == "productversion": 765 version_info["productversion"] = k["value"] 766 if k["name"].lower() == "productname": 767 version_info["product_name"] = k["value"] 768 if k["name"].lower() == "filedescription": 769 version_info["filedescription"] = k["value"] 770 if k["name"].lower() == "fileversion": 771 version_info["fileversion"] = k["value"] 772 if k["name"].lower() == "internalname": 773 version_info["internalname"] = k["value"] 774 if k["name"].lower() == "langid": 775 version_info["langid"] = k["value"] 776 if k["name"].lower() == "legalcopyright": 777 version_info["legalcopyright"] = k["value"] 778 if k["name"].lower() == "legaltrademarks": 779 version_info["legaltrademarks"] = k["value"] 780 if k["name"].lower() == "originalfilename": 781 version_info["originalfilename"] = k["value"] 782 if k["name"].lower() == "privatebuild": 783 version_info["privatebuild"] = k["value"] 784 if k["name"].lower() == "productname": 785 version_info["productname"] = k["value"] 786 if k["name"].lower() == "productversion": 787 version_info["productversion"] = k["value"] 788 if k["name"].lower() == "specialbuild": 789 version_info["specialbuild"] = k["value"] 790 resources.append(version_info) 791 object_dict = {"id": self.id_generator.generate_object_id(), 792 "properties": {"xsi:type":"WindowsExecutableFileObjectType", 793 "imports": imports, 794 "exports": exports, 795 "sections": sections, 796 "resources": resources 797 } 798 } 799 win_exec_file_obj = Object.from_dict(object_dict) 800 return win_exec_file_obj
801
802 - def createFileStringsObj(self):
803 """Creates a File object for capturing strings output.""" 804 extracted_string_list = [] 805 for extracted_string in self.results["strings"]: 806 extracted_string_list.append({"string_value": self._illegal_xml_chars_RE.sub("?", extracted_string)}) 807 extracted_features = {"strings": extracted_string_list} 808 object_dict = {"id": self.id_generator.generate_object_id(), 809 "properties": {"xsi:type":"FileObjectType", 810 "extracted_features": extracted_features 811 } 812 } 813 strings_file_obj = Object.from_dict(object_dict) 814 return strings_file_obj
815
816 - def createFileObj(self, file):
817 """Creates a File object. 818 @param file: file dict from Cuckoo dict. 819 @requires: file object. 820 """ 821 if "ssdeep" in file and file["ssdeep"] is not None: 822 hashes_list = [{"type": "MD5", "simple_hash_value": file["md5"]}, 823 {"type": "SHA1", "simple_hash_value": file["sha1"]}, 824 {"type": "SHA256", "simple_hash_value": file["sha256"]}, 825 {"type": "SHA512", "simple_hash_value": file["sha512"]}, 826 {"type": "SSDEEP", "fuzzy_hash_value": file["ssdeep"]}] 827 else: 828 hashes_list = [{"type": "MD5", "simple_hash_value": file["md5"]}, 829 {"type": "SHA1", "simple_hash_value": file["sha1"]}, 830 {"type": "SHA256", "simple_hash_value": file["sha256"]}, 831 {"type": "SHA512", "simple_hash_value": file["sha512"]}] 832 object_dict = {"id": self.id_generator.generate_object_id(), 833 "properties": {"xsi:type":"FileObjectType", 834 "file_name": file["name"], 835 "file_path": {"value": file["path"]}, 836 "file_format": file["type"], 837 "size_in_bytes": file["size"], 838 "hashes": hashes_list} 839 } 840 file_obj = Object.from_dict(object_dict) 841 return file_obj
842
843 - def addSubjectAttributes(self):
844 """Add Malware Instance Object Attributes to the Malware Subject.""" 845 # File Object. 846 if self.results["target"]["category"] == "file": 847 self.subject.set_malware_instance_object_attributes(self.createFileObj(self.results["target"]["file"])) 848 # URL Object. 849 elif self.results["target"]["category"] == "url": 850 url_object_dict = {"id": self.id_generator.generate_object_id(), "properties": {"xsi:type": "URIObjectType", "value": self.results["target"]["url"]}} 851 self.subject.set_malware_instance_object_attributes(Object.from_dict(url_object_dict))
852
853 - def addAnalyses(self):
854 """Adds analysis header.""" 855 # Add the dynamic analysis. 856 dynamic_analysis = Analysis(self.id_generator.generate_analysis_id(), "dynamic", "triage", BundleReference.from_dict({'bundle_idref': self.dynamic_bundle.id})) 857 dynamic_analysis.start_datetime = datetime_to_iso(self.results["info"]["started"]) 858 dynamic_analysis.complete_datetime = datetime_to_iso(self.results["info"]["ended"]) 859 dynamic_analysis.summary = StructuredText("Cuckoo Sandbox dynamic analysis of the malware instance object.") 860 dynamic_analysis.add_tool(ToolInformation.from_dict({"id": self.id_generator.generate_tool_id(), 861 "name": "Cuckoo Sandbox", 862 "version": self.results["info"]["version"], 863 "vendor": "http://www.cuckoosandbox.org"})) 864 self.subject.add_analysis(dynamic_analysis) 865 866 # Add the static analysis. 867 if self.options["static"] and "static" in self.results and self.results["static"]: 868 static_analysis = Analysis(self.id_generator.generate_analysis_id(), "static", "triage", BundleReference.from_dict({"bundle_idref": self.static_bundle.id})) 869 static_analysis.start_datetime = datetime_to_iso(self.results["info"]["started"]) 870 static_analysis.complete_datetime = datetime_to_iso(self.results["info"]["ended"]) 871 static_analysis.summary = StructuredText("Cuckoo Sandbox static (PE) analysis of the malware instance object.") 872 static_analysis.add_tool(ToolInformation.from_dict({"id": self.id_generator.generate_tool_id(), 873 "name": "Cuckoo Sandbox Static Analysis", 874 "version": self.results["info"]["version"], 875 "vendor": "http://www.cuckoosandbox.org"})) 876 self.subject.add_analysis(static_analysis) 877 # Add the static file results. 878 self.static_bundle.add_object(self.createWinExecFileObj()) 879 # Add the strings analysis. 880 if self.options["strings"] and self.results["strings"]: 881 strings_analysis = Analysis(self.id_generator.generate_analysis_id(), "static", "triage", BundleReference.from_dict({"bundle_idref": self.strings_bundle.id})) 882 strings_analysis.start_datetime = datetime_to_iso(self.results["info"]["started"]) 883 strings_analysis.complete_datetime = datetime_to_iso(self.results["info"]["ended"]) 884 strings_analysis.summary = StructuredText("Cuckoo Sandbox strings analysis of the malware instance object.") 885 strings_analysis.add_tool(ToolInformation.from_dict({"id": self.id_generator.generate_tool_id(), 886 "name": "Cuckoo Sandbox Strings", 887 "version": self.results["info"]["version"], 888 "vendor": "http://www.cuckoosandbox.org"})) 889 self.subject.add_analysis(strings_analysis) 890 # Add the strings results. 891 self.strings_bundle.add_object(self.createFileStringsObj()) 892 # Add the VirusTotal analysis. 893 if self.options["virustotal"] and "virustotal" in self.results and self.results["virustotal"]: 894 virustotal_analysis = Analysis(self.id_generator.generate_analysis_id(), "static", "triage", BundleReference.from_dict({"bundle_idref": self.virustotal_bundle.id})) 895 virustotal_analysis.start_datetime = datetime_to_iso(self.results["info"]["started"]) 896 virustotal_analysis.complete_datetime = datetime_to_iso(self.results["info"]["ended"]) 897 virustotal_analysis.summary = StructuredText("Virustotal results for the malware instance object.") 898 virustotal_analysis.add_tool(ToolInformation.from_dict({"id": self.id_generator.generate_tool_id(), 899 "name": "VirusTotal", 900 "vendor": "https://www.virustotal.com/"})) 901 self.subject.add_analysis(virustotal_analysis) 902 # Add the VirusTotal results. 903 if "scans" in self.results["virustotal"]: 904 for engine, signature in self.results["virustotal"]["scans"].items(): 905 if signature["detected"]: 906 self.virustotal_bundle.add_av_classification(AVClassification.from_dict({"vendor": engine, 907 "engine_version": signature["version"], 908 "definition_version": signature["update"], 909 "classification_name": signature["result"]}))
910
911 - def addDroppedFiles(self):
912 """Adds Dropped files as Objects.""" 913 if "dropped" in self.results: 914 objs = self.results["dropped"] 915 # Add the named object collection. 916 self.dynamic_bundle.add_named_object_collection("Dropped Files", self.id_generator.generate_object_collection_id()) 917 for file in objs: 918 self.dynamic_bundle.add_object(self.createFileObj(file), "Dropped Files")
919
920 - def output(self):
921 """Writes report to disk.""" 922 try: 923 report = open(os.path.join(self.reports_path, "report.maec-4.0.1.xml"), "w") 924 report.write("<?xml version='1.0' encoding='UTF-8'?>\n") 925 report.write("<!DOCTYPE doc [<!ENTITY comma '&#44;'>]>\n") 926 report.write("<!--\n") 927 report.write("Cuckoo Sandbox MAEC 4.0.1 malware analysis report\n") 928 report.write("http://www.cuckoosandbox.org\n") 929 report.write("-->\n") 930 self.package.to_obj().export(report, 0, name_="MAEC_Package", namespacedef_=MAECNamespaceParser(self.package.to_obj()).get_namespace_schemalocation_str()) 931 report.flush() 932 report.close() 933 except (TypeError, IOError) as e: 934 traceback.print_exc() 935 raise CuckooReportError("Failed to generate MAEC 4.0.1 report: %s" % e)
936