1
2
3
4
5 import os
6 import re
7 import logging
8 import time
9
10 import xml.etree.ElementTree as ET
11
12 from lib.cuckoo.common.config import Config
13 from lib.cuckoo.common.exceptions import CuckooCriticalError
14 from lib.cuckoo.common.exceptions import CuckooMachineError
15 from lib.cuckoo.common.exceptions import CuckooOperationalError
16 from lib.cuckoo.common.exceptions import CuckooReportError
17 from lib.cuckoo.common.exceptions import CuckooDependencyError
18 from lib.cuckoo.common.objects import Dictionary
19 from lib.cuckoo.common.utils import create_folder
20 from lib.cuckoo.core.database import Database
21 from lib.cuckoo.core.resultserver import ResultServer
22
23 try:
24 import libvirt
25 HAVE_LIBVIRT = True
26 except ImportError:
27 HAVE_LIBVIRT = False
28
29 log = logging.getLogger(__name__)
30
32 """Base abstract class for auxiliary modules."""
33
38
41
44
47
49 raise NotImplementedError
50
52 raise NotImplementedError
53
54
56 """Base abstract class for machinery modules."""
57
58
59
60
61 LABEL = "label"
62
73
75 """Set machine manager options.
76 @param options: machine manager options dict.
77 """
78 self.options = options
79
81 """Read, load, and verify machines configuration.
82 @param module_name: module name.
83 """
84
85 self._initialize(module_name)
86
87
88 self._initialize_check()
89
91 """Read configuration.
92 @param module_name: module name.
93 """
94 self.module_name = module_name
95 mmanager_opts = self.options.get(module_name)
96
97 for machine_id in mmanager_opts["machines"].strip().split(","):
98 try:
99 machine_opts = self.options.get(machine_id.strip())
100 machine = Dictionary()
101 machine.id = machine_id.strip()
102 machine.label = machine_opts[self.LABEL]
103 machine.platform = machine_opts["platform"]
104 machine.tags = machine_opts.get("tags")
105 machine.ip = machine_opts["ip"]
106
107
108
109 machine.interface = machine_opts.get("interface")
110
111
112
113 machine.snapshot = machine_opts.get("snapshot")
114
115
116
117 opt_resultserver = self.options_globals.resultserver
118
119
120
121 opt_resultserver.port = ResultServer().port
122
123 ip = machine_opts.get("resultserver_ip", opt_resultserver.ip)
124 port = machine_opts.get("resultserver_port", opt_resultserver.port)
125
126 machine.resultserver_ip = ip
127 machine.resultserver_port = port
128
129
130 for key, value in machine.items():
131 if value and isinstance(value, basestring):
132 machine[key] = value.strip()
133
134 self.db.add_machine(name=machine.id,
135 label=machine.label,
136 ip=machine.ip,
137 platform=machine.platform,
138 tags=machine.tags,
139 interface=machine.interface,
140 snapshot=machine.snapshot,
141 resultserver_ip=ip,
142 resultserver_port=port)
143 except (AttributeError, CuckooOperationalError) as e:
144 log.warning("Configuration details about machine %s "
145 "are missing: %s", machine_id, e)
146 continue
147
149 """Runs checks against virtualization software when a machine manager
150 is initialized.
151 @note: in machine manager modules you may override or superclass
152 his method.
153 @raise CuckooMachineError: if a misconfiguration or a unkown vm state
154 is found.
155 """
156 try:
157 configured_vms = self._list()
158 except NotImplementedError:
159 return
160
161 for machine in self.machines():
162
163
164 if machine.label in configured_vms and \
165 self._status(machine.label) in [self.POWEROFF, self.ABORTED]:
166 continue
167
168
169
170 try:
171 self.stop(machine.label)
172 except CuckooMachineError as e:
173 msg = "Please update your configuration. Unable to shut " \
174 "'{0}' down or find the machine in its proper state:" \
175 " {1}".format(machine.label, e)
176 raise CuckooCriticalError(msg)
177
178 if not self.options_globals.timeouts.vm_state:
179 raise CuckooCriticalError("Virtual machine state change timeout "
180 "setting not found, please add it to "
181 "the config file.")
182
184 """List virtual machines.
185 @return: virtual machines list
186 """
187 return self.db.list_machines()
188
190 """How many machines are free.
191 @return: free machines count.
192 """
193 return self.db.count_machines_available()
194
195 - def acquire(self, machine_id=None, platform=None, tags=None):
208
210 """Release a machine.
211 @param label: machine name.
212 """
213 self.db.unlock_machine(label)
214
216 """Returns running virtual machines.
217 @return: running virtual machines list.
218 """
219 return self.db.list_machines(locked=True)
220
222 """Shutdown the machine manager. Kills all alive machines.
223 @raise CuckooMachineError: if unable to stop machine.
224 """
225 if len(self.running()) > 0:
226 log.info("Still %s guests alive. Shutting down...",
227 len(self.running()))
228 for machine in self.running():
229 try:
230 self.stop(machine.label)
231 except CuckooMachineError as e:
232 log.warning("Unable to shutdown machine %s, please check "
233 "manually. Error: %s", machine.label, e)
234
236 """Set status for a virtual machine.
237 @param label: virtual machine label
238 @param status: new virtual machine status
239 """
240 self.db.set_machine_status(label, status)
241
242 - def start(self, label=None):
243 """Start a machine.
244 @param label: machine name.
245 @raise NotImplementedError: this method is abstract.
246 """
247 raise NotImplementedError
248
249 - def stop(self, label=None):
250 """Stop a machine.
251 @param label: machine name.
252 @raise NotImplementedError: this method is abstract.
253 """
254 raise NotImplementedError
255
257 """Lists virtual machines configured.
258 @raise NotImplementedError: this method is abstract.
259 """
260 raise NotImplementedError
261
263 """Takes a memory dump of a machine.
264 @param path: path to where to store the memory dump.
265 """
266 raise NotImplementedError
267
269 """Waits for a vm status.
270 @param label: virtual machine name.
271 @param state: virtual machine status, accepts multiple states as list.
272 @raise CuckooMachineError: if default waiting timeout expire.
273 """
274
275 waitme = 0
276 try:
277 current = self._status(label)
278 except NameError:
279 return
280
281 if isinstance(state, str):
282 state = [state]
283 while current not in state:
284 log.debug("Waiting %i cuckooseconds for machine %s to switch "
285 "to status %s", waitme, label, state)
286 if waitme > int(self.options_globals.timeouts.vm_state):
287 raise CuckooMachineError("Timeout hit while for machine {0} "
288 "to change status".format(label))
289 time.sleep(1)
290 waitme += 1
291 current = self._status(label)
292
293
295 """Libvirt based machine manager.
296
297 If you want to write a custom module for a virtualization software
298 supported by libvirt you have just to inherit this machine manager and
299 change the connection string.
300 """
301
302
303 RUNNING = "running"
304 PAUSED = "paused"
305 POWEROFF = "poweroff"
306 ERROR = "machete"
307 ABORTED = "abort"
308
314
316 """Initialize machine manager module. Override default to set proper
317 connection string.
318 @param module: machine manager module
319 """
320 super(LibVirtMachinery, self).initialize(module)
321
337
389
390 - def stop(self, label):
391 """Stops a virtual machine. Kill them all.
392 @param label: virtual machine name.
393 @raise CuckooMachineError: if unable to stop virtual machine.
394 """
395 log.debug("Stopping machine %s", label)
396
397 if self._status(label) == self.POWEROFF:
398 raise CuckooMachineError("Trying to stop an already stopped "
399 "machine {0}".format(label))
400
401
402 conn = self._connect()
403 try:
404 if not self.vms[label].isActive():
405 log.debug("Trying to stop an already stopped machine %s. "
406 "Skip", label)
407 else:
408 self.vms[label].destroy()
409 except libvirt.libvirtError as e:
410 raise CuckooMachineError("Error stopping virtual machine "
411 "{0}: {1}".format(label, e))
412 finally:
413 self._disconnect(conn)
414
415 self._wait_status(label, self.POWEROFF)
416
418 """Override shutdown to free libvirt handlers - they print errors."""
419 super(LibVirtMachinery, self).shutdown()
420
421
422 self.vms = None
423
425 """Takes a memory dump.
426 @param path: path to where to store the memory dump.
427 """
428 log.debug("Dumping memory for machine %s", label)
429
430 conn = self._connect()
431 try:
432 self.vms[label].coreDump(path, flags=libvirt.VIR_DUMP_MEMORY_ONLY)
433 except libvirt.libvirtError as e:
434 raise CuckooMachineError("Error dumping memory virtual machine "
435 "{0}: {1}".format(label, e))
436 finally:
437 self._disconnect(conn)
438
440 """Gets current status of a vm.
441 @param label: virtual machine name.
442 @return: status string.
443 """
444 log.debug("Getting status for %s", label)
445
446
447
448
449
450
451
452
453
454
455
456
457 conn = self._connect()
458 try:
459 state = self.vms[label].state(flags=0)
460 except libvirt.libvirtError as e:
461 raise CuckooMachineError("Error getting status for virtual "
462 "machine {0}: {1}".format(label, e))
463 finally:
464 self._disconnect(conn)
465
466 if state:
467 if state[0] == 1:
468 status = self.RUNNING
469 elif state[0] == 3:
470 status = self.PAUSED
471 elif state[0] == 4 or state[0] == 5:
472 status = self.POWEROFF
473 else:
474 status = self.ERROR
475
476
477 if status:
478 self.set_status(label, status)
479 return status
480 else:
481 raise CuckooMachineError("Unable to get status for "
482 "{0}".format(label))
483
485 """Connects to libvirt subsystem.
486 @raise CuckooMachineError: when unable to connect to libvirt.
487 """
488
489 if not self.dsn:
490 raise CuckooMachineError("You must provide a proper "
491 "connection string")
492
493 try:
494 return libvirt.open(self.dsn)
495 except libvirt.libvirtError:
496 raise CuckooMachineError("Cannot connect to libvirt")
497
499 """Disconnects to libvirt subsystem.
500 @raise CuckooMachineError: if cannot disconnect from libvirt.
501 """
502 try:
503 conn.close()
504 except libvirt.libvirtError:
505 raise CuckooMachineError("Cannot disconnect from libvirt")
506
508 """Fetch machines handlers.
509 @return: dict with machine label as key and handle as value.
510 """
511 vms = {}
512 for vm in self.machines():
513 vms[vm.label] = self._lookup(vm.label)
514 return vms
515
517 """Search for a virtual machine.
518 @param conn: libvirt connection handle.
519 @param label: virtual machine name.
520 @raise CuckooMachineError: if virtual machine is not found.
521 """
522 conn = self._connect()
523 try:
524 vm = conn.lookupByName(label)
525 except libvirt.libvirtError:
526 raise CuckooMachineError("Cannot find machine "
527 "{0}".format(label))
528 finally:
529 self._disconnect(conn)
530 return vm
531
533 """List available virtual machines.
534 @raise CuckooMachineError: if unable to list virtual machines.
535 """
536 conn = self._connect()
537 try:
538 names = conn.listDefinedDomains()
539 except libvirt.libvirtError:
540 raise CuckooMachineError("Cannot list domains")
541 finally:
542 self._disconnect(conn)
543 return names
544
546 """Check if libvirt release supports snapshots.
547 @return: True or false.
548 """
549 if libvirt.getVersion() >= 8000:
550 return True
551 else:
552 return False
553
555 """Get current snapshot for virtual machine
556 @param label: virtual machine name
557 @return None or current snapshot
558 @raise CuckooMachineError: if cannot find current snapshot or
559 when there are too many snapshots available
560 """
561 def _extract_creation_time(node):
562 """Extracts creation time from a KVM vm config file.
563 @param node: config file node
564 @return: extracted creation time
565 """
566 xml = ET.fromstring(node.getXMLDesc(flags=0))
567 return xml.findtext("./creationTime")
568
569 snapshot = None
570 conn = self._connect()
571 try:
572 vm = self.vms[label]
573
574
575
576 if vm.hasCurrentSnapshot(flags=0):
577 snapshot = vm.snapshotCurrent(flags=0)
578 else:
579 log.debug("No current snapshot, using latest snapshot")
580
581
582 snapshot = sorted(vm.listAllSnapshots(flags=0),
583 key=_extract_creation_time,
584 reverse=True)[0]
585 except libvirt.libvirtError:
586 raise CuckooMachineError("Unable to get snapshot for "
587 "virtual machine {0}".format(label))
588 finally:
589 self._disconnect(conn)
590
591 return snapshot
592
594 """Base abstract class for processing module."""
595 order = 1
596 enabled = True
597
599 self.analysis_path = ""
600 self.logs_path = ""
601 self.task = None
602 self.options = None
603
605 """Set report options.
606 @param options: report options dict.
607 """
608 self.options = options
609
611 """Add task information.
612 @param task: task dictionary.
613 """
614 self.task = task
615
617 """Set paths.
618 @param analysis_path: analysis folder path.
619 """
620 self.analysis_path = analysis_path
621 self.log_path = os.path.join(self.analysis_path, "analysis.log")
622 self.file_path = os.path.realpath(os.path.join(self.analysis_path,
623 "binary"))
624 self.dropped_path = os.path.join(self.analysis_path, "files")
625 self.logs_path = os.path.join(self.analysis_path, "logs")
626 self.shots_path = os.path.join(self.analysis_path, "shots")
627 self.pcap_path = os.path.join(self.analysis_path, "dump.pcap")
628 self.pmemory_path = os.path.join(self.analysis_path, "memory")
629 self.memory_path = os.path.join(self.analysis_path, "memory.dmp")
630
632 """Start processing.
633 @raise NotImplementedError: this method is abstract.
634 """
635 raise NotImplementedError
636
638 """Base class for Cuckoo signatures."""
639
640 name = ""
641 description = ""
642 severity = 1
643 categories = []
644 families = []
645 authors = []
646 references = []
647 alert = False
648 enabled = True
649 minimum = None
650 maximum = None
651
652
653
654
655 order = 0
656
657 evented = False
658 filter_processnames = set()
659 filter_apinames = set()
660 filter_categories = set()
661
663 self.data = []
664 self.results = results
665 self._current_call_cache = None
666 self._current_call_dict = None
667
669 """Checks a pattern against a given subject.
670 @param pattern: string or expression to check for.
671 @param subject: target of the check.
672 @param regex: boolean representing if the pattern is a regular
673 expression or not and therefore should be compiled.
674 @return: boolean with the result of the check.
675 """
676 if regex:
677 exp = re.compile(pattern, re.IGNORECASE)
678 if isinstance(subject, list):
679 for item in subject:
680 if exp.match(item):
681 return item
682 else:
683 if exp.match(subject):
684 return subject
685 else:
686 if isinstance(subject, list):
687 for item in subject:
688 if item == pattern:
689 return item
690 else:
691 if subject == pattern:
692 return subject
693
694 return None
695
697 """Checks for a file being opened.
698 @param pattern: string or expression to check for.
699 @param regex: boolean representing if the pattern is a regular
700 expression or not and therefore should be compiled.
701 @return: boolean with the result of the check.
702 """
703 subject = self.results["behavior"]["summary"]["files"]
704 return self._check_value(pattern=pattern,
705 subject=subject,
706 regex=regex)
707
709 """Checks for a registry key being opened.
710 @param pattern: string or expression to check for.
711 @param regex: boolean representing if the pattern is a regular
712 expression or not and therefore should be compiled.
713 @return: boolean with the result of the check.
714 """
715 subject = self.results["behavior"]["summary"]["keys"]
716 return self._check_value(pattern=pattern,
717 subject=subject,
718 regex=regex)
719
721 """Checks for a mutex being opened.
722 @param pattern: string or expression to check for.
723 @param regex: boolean representing if the pattern is a regular
724 expression or not and therefore should be compiled.
725 @return: boolean with the result of the check.
726 """
727 subject = self.results["behavior"]["summary"]["mutexes"]
728 return self._check_value(pattern=pattern,
729 subject=subject,
730 regex=regex)
731
732 - def check_api(self, pattern, process=None, regex=False):
733 """Checks for an API being called.
734 @param pattern: string or expression to check for.
735 @param process: optional filter for a specific process name.
736 @param regex: boolean representing if the pattern is a regular
737 expression or not and therefore should be compiled.
738 @return: boolean with the result of the check.
739 """
740
741 for item in self.results["behavior"]["processes"]:
742
743 if process:
744 if item["process_name"] != process:
745 continue
746
747
748 for call in item["calls"]:
749
750 if self._check_value(pattern=pattern,
751 subject=call["api"],
752 regex=regex):
753 return call["api"]
754
755 return None
756
757 - def check_argument_call(self,
758 call,
759 pattern,
760 name=None,
761 api=None,
762 category=None,
763 regex=False):
764 """Checks for a specific argument of an invoked API.
765 @param call: API call information.
766 @param pattern: string or expression to check for.
767 @param name: optional filter for the argument name.
768 @param api: optional filter for the API function name.
769 @param category: optional filter for a category name.
770 @param regex: boolean representing if the pattern is a regular
771 expression or not and therefore should be compiled.
772 @return: boolean with the result of the check.
773 """
774
775 if api:
776 if call["api"] != api:
777 return False
778
779
780 if category:
781 if call["category"] != category:
782 return False
783
784
785 for argument in call["arguments"]:
786
787 if name:
788 if argument["name"] != name:
789 continue
790
791
792 if self._check_value(pattern=pattern,
793 subject=argument["value"],
794 regex=regex):
795 return argument["value"]
796
797 return False
798
799 - def check_argument(self,
800 pattern,
801 name=None,
802 api=None,
803 category=None,
804 process=None,
805 regex=False):
806 """Checks for a specific argument of an invoked API.
807 @param pattern: string or expression to check for.
808 @param name: optional filter for the argument name.
809 @param api: optional filter for the API function name.
810 @param category: optional filter for a category name.
811 @param process: optional filter for a specific process name.
812 @param regex: boolean representing if the pattern is a regular
813 expression or not and therefore should be compiled.
814 @return: boolean with the result of the check.
815 """
816
817 for item in self.results["behavior"]["processes"]:
818
819 if process:
820 if item["process_name"] != process:
821 continue
822
823
824 for call in item["calls"]:
825 r = self.check_argument_call(call, pattern, name,
826 api, category, regex)
827 if r:
828 return r
829
830 return None
831
832 - def check_ip(self, pattern, regex=False):
833 """Checks for an IP address being contacted.
834 @param pattern: string or expression to check for.
835 @param regex: boolean representing if the pattern is a regular
836 expression or not and therefore should be compiled.
837 @return: boolean with the result of the check.
838 """
839 return self._check_value(pattern=pattern,
840 subject=self.results["network"]["hosts"],
841 regex=regex)
842
843 - def check_domain(self, pattern, regex=False):
844 """Checks for a domain being contacted.
845 @param pattern: string or expression to check for.
846 @param regex: boolean representing if the pattern is a regular
847 expression or not and therefore should be compiled.
848 @return: boolean with the result of the check.
849 """
850 if "domains" not in self.results["network"]:
851 return None
852
853 for item in self.results["network"]["domains"]:
854 if self._check_value(pattern=pattern,
855 subject=item["domain"],
856 regex=regex):
857 return item
858
859 return None
860
862 """Checks for a URL being contacted.
863 @param pattern: string or expression to check for.
864 @param regex: boolean representing if the pattern is a regular
865 expression or not and therefore should be compiled.
866 @return: boolean with the result of the check.
867 """
868 for item in self.results["network"]["http"]:
869 if self._check_value(pattern=pattern,
870 subject=item["uri"],
871 regex=regex):
872 return item
873
874 return None
875
877 """Retrieves the value of a specific argument from an API call.
878 @param call: API call object.
879 @param name: name of the argument to retrieve.
880 @return: value of the required argument.
881 """
882
883
884 if call is not self._current_call_cache:
885 self._current_call_cache = call
886 self._current_call_dict = dict()
887
888 for argument in call["arguments"]:
889 self._current_call_dict[argument["name"]] = argument["value"]
890
891
892 if name in self._current_call_dict:
893 return self._current_call_dict[name]
894
895 return None
896
898 """Adds a match to the signature data.
899 @param process: The process triggering the match.
900 @param type: The type of matching data (ex: 'api', 'mutex', 'file', etc.)
901 @param match: Value or array of values triggering the match.
902 """
903 signs = []
904 if isinstance(match, list):
905 for item in match:
906 signs.append({ 'type': type, 'value': item })
907 else:
908 signs.append({ 'type': type, 'value': match })
909
910 process_summary = None
911 if process:
912 process_summary = {}
913 process_summary['process_name'] = process['process_name']
914 process_summary['process_id'] = process['process_id']
915
916 self.data.append({ 'process': process_summary, 'signs': signs })
917
919 """Returns true if there is matches (data is not empty)
920 @return: boolean indicating if there is any match registered
921 """
922 return len(self.data) > 0
923
925 """Notify signature about API call. Return value determines
926 if this signature is done or could still match.
927 @param call: logged API call.
928 @param process: process doing API call.
929 @raise NotImplementedError: this method is abstract.
930 """
931 raise NotImplementedError
932
934 """Evented signature is notified when all API calls are done.
935 @return: Match state.
936 @raise NotImplementedError: this method is abstract.
937 """
938 raise NotImplementedError
939
941 """Start signature processing.
942 @param results: analysis results.
943 @raise NotImplementedError: this method is abstract.
944 """
945 raise NotImplementedError
946
960
962 """Base abstract class for reporting module."""
963 order = 1
964
966 self.analysis_path = ""
967 self.reports_path = ""
968 self.task = None
969 self.options = None
970
972 """Set analysis folder path.
973 @param analysis_path: analysis folder path.
974 """
975 self.analysis_path = analysis_path
976 self.conf_path = os.path.join(self.analysis_path, "analysis.conf")
977 self.file_path = os.path.realpath(os.path.join(self.analysis_path,
978 "binary"))
979 self.reports_path = os.path.join(self.analysis_path, "reports")
980 self.shots_path = os.path.join(self.analysis_path, "shots")
981 self.pcap_path = os.path.join(self.analysis_path, "dump.pcap")
982
983 try:
984 create_folder(folder=self.reports_path)
985 except CuckooOperationalError as e:
986 CuckooReportError(e)
987
989 """Set report options.
990 @param options: report options dict.
991 """
992 self.options = options
993
995 """Add task information.
996 @param task: task dictionary.
997 """
998 self.task = task
999
1001 """Start report processing.
1002 @raise NotImplementedError: this method is abstract.
1003 """
1004 raise NotImplementedError
1005