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