1
2
3
4
5 import os
6 import sys
7 import socket
8 import struct
9 import random
10 import pkgutil
11 import logging
12 import hashlib
13 import xmlrpclib
14 import traceback
15 from ctypes import create_unicode_buffer, create_string_buffer
16 from ctypes import c_wchar_p, byref, c_int, sizeof
17 from threading import Lock, Thread
18 from datetime import datetime
19
20 from lib.api.process import Process
21 from lib.common.abstracts import Package, Auxiliary
22 from lib.common.constants import PATHS, PIPE
23 from lib.common.defines import KERNEL32
24 from lib.common.defines import ERROR_MORE_DATA, ERROR_PIPE_CONNECTED
25 from lib.common.defines import PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE
26 from lib.common.defines import PIPE_READMODE_MESSAGE, PIPE_WAIT
27 from lib.common.defines import PIPE_UNLIMITED_INSTANCES, INVALID_HANDLE_VALUE
28 from lib.common.exceptions import CuckooError, CuckooPackageError
29 from lib.common.results import upload_to_host
30 from lib.core.config import Config
31 from lib.core.packages import choose_package
32 from lib.core.privileges import grant_debug_privilege
33 from lib.core.startup import create_folders, init_logging
34 from modules import auxiliary
35
36 log = logging.getLogger()
37
38 BUFSIZE = 512
39 FILES_LIST = []
40 DUMPED_LIST = []
41 PROCESS_LIST = []
42 PROCESS_LOCK = Lock()
43 DEFAULT_DLL = None
44
45 PID = os.getpid()
46 PPID = Process(pid=PID).get_parent_pid()
47
48
50 """Checks file name against some protected names."""
51 if not fname:
52 return False
53
54 protected_names = []
55 for name in protected_names:
56 if name in fname:
57 return True
58
59 return False
60
62 """Add a process to process list."""
63 if type(pid) == long or type(pid) == int or type(pid) == str:
64 log.info("Added new process to list with pid: %s", pid)
65 PROCESS_LIST.append(pid)
66
68 """Add PID."""
69 if type(pids) == list:
70 for pid in pids:
71 add_pid(pid)
72 else:
73 add_pid(pids)
74
76 """Add a file to file list."""
77 if file_path not in FILES_LIST:
78 log.info("Added new file to list with path: %s",
79 unicode(file_path).encode("utf-8", "replace"))
80 FILES_LIST.append(file_path)
81
83 """Create a copy of the give file path."""
84 try:
85 if os.path.exists(file_path):
86 sha256 = hashlib.sha256(open(file_path, "rb").read()).hexdigest()
87 if sha256 in DUMPED_LIST:
88
89 return
90 else:
91 log.warning("File at path \"%s\" does not exist, skip", file_path)
92 return
93 except IOError as e:
94 log.warning("Unable to access file at path \"%s\": %s", file_path, e)
95 return
96
97
98 path = create_unicode_buffer(32 * 1024)
99 name = c_wchar_p()
100 KERNEL32.GetFullPathNameW(file_path, 32 * 1024, path, byref(name))
101 file_path = path.value
102
103
104
105 if name.value:
106
107 file_name = name.value[name.value.find(":")+1:]
108 else:
109 return
110
111 upload_path = os.path.join("files",
112 str(random.randint(100000000, 9999999999)),
113 file_name)
114 try:
115 upload_to_host(file_path, upload_path)
116 DUMPED_LIST.append(sha256)
117 except (IOError, socket.error) as e:
118 log.error("Unable to upload dropped file at path \"%s\": %s",
119 file_path, e)
120
121
123 dump_file(fname)
124
125
126 fnames = [x.lower() for x in FILES_LIST]
127
128
129
130 if fname.lower() in fnames:
131 FILES_LIST.pop(fnames.index(fname.lower()))
132
134
135 fnames = [x.lower() for x in FILES_LIST]
136
137
138 if old_fname.lower() in fnames:
139
140
141 idx = fnames.index(old_fname.lower())
142
143
144 FILES_LIST[idx] = new_fname
145
147 """Dump all the dropped files."""
148 for file_path in FILES_LIST:
149 dump_file(file_path)
150
152 """Pipe Handler.
153
154 This class handles the notifications received through the Pipe Server and
155 decides what to do with them.
156 """
157
159 """@param h_pipe: PIPE to read."""
160 Thread.__init__(self)
161 self.h_pipe = h_pipe
162
164 """Run handler.
165 @return: operation status.
166 """
167 data = ""
168 response = "OK"
169 wait = False
170 proc = None
171
172
173 while True:
174 bytes_read = c_int(0)
175
176 buf = create_string_buffer(BUFSIZE)
177 success = KERNEL32.ReadFile(self.h_pipe,
178 buf,
179 sizeof(buf),
180 byref(bytes_read),
181 None)
182
183 data += buf.value
184
185 if not success and KERNEL32.GetLastError() == ERROR_MORE_DATA:
186 continue
187
188
189
190
191 break
192
193 if data:
194 command = data.strip()
195
196
197
198
199 if command == "GETPIDS":
200 response = struct.pack("II", PID, PPID)
201
202
203
204 elif command == "HOOKDLLS":
205 is_url = Config(cfg="analysis.conf").category != "file"
206
207 url_dlls = "ntdll", "kernel32"
208
209 def hookdll_encode(names):
210
211
212 names = [name + "\x00" * (16-len(name)) for name in names]
213 f = lambda s: "".join(ch + "\x00" for ch in s)
214 return "".join(f(name) for name in names)
215
216
217
218
219 if not is_url:
220 response = "\x00"
221 else:
222 response = hookdll_encode(url_dlls)
223
224
225
226 elif command.startswith("PROCESS:"):
227
228
229
230 PROCESS_LOCK.acquire()
231
232
233
234 dll = DEFAULT_DLL
235
236
237 data = command[8:]
238 process_id = thread_id = None
239 if not "," in data:
240 if data.isdigit():
241 process_id = int(data)
242 elif len(data.split(",")) == 2:
243 process_id, param = data.split(",")
244 thread_id = None
245 if process_id.isdigit():
246 process_id = int(process_id)
247 else:
248 process_id = None
249
250 if param.isdigit():
251 thread_id = int(param)
252 else:
253
254 if isinstance(param, str):
255 dll = param
256
257 if process_id:
258 if process_id not in (PID, PPID):
259
260
261
262 if process_id not in PROCESS_LIST:
263
264
265 proc = Process(pid=process_id,
266 thread_id=thread_id)
267
268 filepath = proc.get_filepath()
269 filename = os.path.basename(filepath)
270
271 log.info("Announced process name: %s", filename)
272
273 if not protected_filename(filename):
274
275
276 add_pids(process_id)
277
278
279
280 if process_id and thread_id:
281 proc.inject(dll, apc=True)
282 else:
283
284
285
286 proc.inject(dll)
287 wait = True
288
289 log.info("Successfully injected process with "
290 "pid %s", proc.pid)
291 else:
292 log.warning("Received request to inject Cuckoo "
293 "processes, skip")
294
295
296
297 PROCESS_LOCK.release()
298
299
300 elif command.startswith("FILE_NEW:"):
301
302 file_path = command[9:].decode("utf-8")
303
304 add_file(file_path)
305
306
307
308 elif command.startswith("FILE_DEL:"):
309
310 file_path = command[9:].decode("utf-8")
311
312 del_file(file_path)
313 elif command.startswith("FILE_MOVE:"):
314
315 if "::" in command[10:]:
316 old_fname, new_fname = command[10:].split("::", 1)
317 move_file(old_fname.decode("utf-8"),
318 new_fname.decode("utf-8"))
319
320 KERNEL32.WriteFile(self.h_pipe,
321 create_string_buffer(response),
322 len(response),
323 byref(bytes_read),
324 None)
325
326 KERNEL32.CloseHandle(self.h_pipe)
327
328
329 if wait:
330 proc.wait()
331
332 if proc:
333 proc.close()
334
335 return True
336
338 """Cuckoo PIPE server.
339
340 This Pipe Server receives notifications from the injected processes for
341 new processes being spawned and for files being created or deleted.
342 """
343
345 """@param pipe_name: Cuckoo PIPE server name."""
346 Thread.__init__(self)
347 self.pipe_name = pipe_name
348 self.do_run = True
349
351 """Stop PIPE server."""
352 self.do_run = False
353
383
385 """Cuckoo Windows Analyzer.
386
387 This class handles the initialization and execution of the analysis
388 procedure, including handling of the pipe server, the auxiliary modules and
389 the analysis packages.
390 """
391 PIPE_SERVER_COUNT = 4
392
397
399 """Prepare env for analysis."""
400 global DEFAULT_DLL
401
402
403
404 grant_debug_privilege()
405
406
407 create_folders()
408
409
410 init_logging()
411
412
413 self.config = Config(cfg="analysis.conf")
414
415
416 clock = datetime.strptime(self.config.clock, "%Y%m%dT%H:%M:%S")
417
418
419
420
421
422
423
424 os.system("echo:|date {0}".format(clock.strftime("%m-%d-%y")))
425 os.system("echo:|time {0}".format(clock.strftime("%H:%M:%S")))
426
427
428 DEFAULT_DLL = self.get_options().get("dll", None)
429
430
431
432 for x in xrange(self.PIPE_SERVER_COUNT):
433 self.pipes[x] = PipeServer()
434 self.pipes[x].daemon = True
435 self.pipes[x].start()
436
437
438
439 if self.config.category == "file":
440 self.target = os.path.join(os.environ["TEMP"] + os.sep,
441 str(self.config.file_name))
442
443 else:
444 self.target = self.config.target
445
447 """Get analysis options.
448 @return: options dict.
449 """
450
451
452
453
454
455
456 options = {}
457 if self.config.options:
458 try:
459
460 fields = self.config.options.strip().split(",")
461 except ValueError as e:
462 log.warning("Failed parsing the options: %s", e)
463 else:
464 for field in fields:
465
466 try:
467 key, value = field.strip().split("=")
468 except ValueError as e:
469 log.warning("Failed parsing option (%s): %s", field, e)
470 else:
471
472
473 options[key.strip()] = value.strip()
474
475 return options
476
486
488 """Run analysis.
489 @return: operation status.
490 """
491 self.prepare()
492
493 log.info("Starting analyzer from: %s", os.getcwd())
494 log.info("Storing results at: %s", PATHS["root"])
495 log.info("Pipe server name: %s", PIPE)
496
497
498
499 if not self.config.package:
500 log.info("No analysis package specified, trying to detect "
501 "it automagically")
502
503
504 if self.config.category == "file":
505 package = choose_package(self.config.file_type, self.config.file_name)
506
507
508 else:
509 package = "ie"
510
511
512
513 if not package:
514 raise CuckooError("No valid package available for file "
515 "type: {0}".format(self.config.file_type))
516
517 log.info("Automatically selected analysis package \"%s\"", package)
518
519 else:
520 package = self.config.package
521
522
523 package_name = "modules.packages.%s" % package
524
525
526 try:
527 __import__(package_name, globals(), locals(), ["dummy"], -1)
528
529 except ImportError:
530 raise CuckooError("Unable to import package \"{0}\", does "
531 "not exist.".format(package_name))
532
533
534 Package()
535
536
537 try:
538 package_class = Package.__subclasses__()[0]
539 except IndexError as e:
540 raise CuckooError("Unable to select package class "
541 "(package={0}): {1}".format(package_name, e))
542
543
544 pack = package_class(self.get_options())
545
546
547 Auxiliary()
548 prefix = auxiliary.__name__ + "."
549 for loader, name, ispkg in pkgutil.iter_modules(auxiliary.__path__, prefix):
550 if ispkg:
551 continue
552
553
554 try:
555 __import__(name, globals(), locals(), ["dummy"], -1)
556 except ImportError as e:
557 log.warning("Unable to import the auxiliary module "
558 "\"%s\": %s", name, e)
559
560
561 aux_enabled = []
562 for module in Auxiliary.__subclasses__():
563
564 try:
565 aux = module()
566 aux.start()
567 except (NotImplementedError, AttributeError):
568 log.warning("Auxiliary module %s was not implemented",
569 aux.__class__.__name__)
570 continue
571 except Exception as e:
572 log.warning("Cannot execute auxiliary module %s: %s",
573 aux.__class__.__name__, e)
574 continue
575 finally:
576 log.info("Started auxiliary module %s",
577 aux.__class__.__name__)
578 aux_enabled.append(aux)
579
580
581
582 try:
583 pids = pack.start(self.target)
584 except NotImplementedError:
585 raise CuckooError("The package \"{0}\" doesn't contain a run "
586 "function.".format(package_name))
587 except CuckooPackageError as e:
588 raise CuckooError("The package \"{0}\" start function raised an "
589 "error: {1}".format(package_name, e))
590 except Exception as e:
591 raise CuckooError("The package \"{0}\" start function encountered "
592 "an unhandled exception: "
593 "{1}".format(package_name, e))
594
595
596
597 if pids:
598 add_pids(pids)
599 pid_check = True
600
601
602
603 else:
604 log.info("No process IDs returned by the package, running "
605 "for the full timeout")
606 pid_check = False
607
608
609
610 if self.config.enforce_timeout:
611 log.info("Enabled timeout enforce, running for the full timeout")
612 pid_check = False
613
614 time_counter = 0
615
616 while True:
617 time_counter += 1
618 if time_counter == int(self.config.timeout):
619 log.info("Analysis timeout hit, terminating analysis")
620 break
621
622
623
624
625 if PROCESS_LOCK.locked():
626 KERNEL32.Sleep(1000)
627 continue
628
629 try:
630
631
632 if pid_check:
633 for pid in PROCESS_LIST:
634 if not Process(pid=pid).is_alive():
635 log.info("Process with pid %s has terminated", pid)
636 PROCESS_LIST.remove(pid)
637
638
639
640 if len(PROCESS_LIST) == 0:
641 log.info("Process list is empty, "
642 "terminating analysis...")
643 break
644
645
646
647
648 pack.set_pids(PROCESS_LIST)
649
650 try:
651
652
653
654
655 if not pack.check():
656 log.info("The analysis package requested the "
657 "termination of the analysis...")
658 break
659
660
661
662
663 except Exception as e:
664 log.warning("The package \"%s\" check function raised "
665 "an exception: %s", package_name, e)
666 finally:
667
668 KERNEL32.Sleep(1000)
669
670 try:
671
672
673 pack.finish()
674 except Exception as e:
675 log.warning("The package \"%s\" finish function raised an "
676 "exception: %s", package_name, e)
677
678
679 for aux in aux_enabled:
680 try:
681 aux.stop()
682 except (NotImplementedError, AttributeError):
683 continue
684 except Exception as e:
685 log.warning("Cannot terminate auxiliary module %s: %s",
686 aux.__class__.__name__, e)
687
688
689
690 log.info("Terminating remaining processes before shutdown...")
691
692 for pid in PROCESS_LIST:
693 proc = Process(pid=pid)
694 if proc.is_alive():
695 try:
696 proc.terminate()
697 except:
698 continue
699
700
701 self.complete()
702
703 return True
704
705 if __name__ == "__main__":
706 success = False
707 error = ""
708
709 try:
710
711 analyzer = Analyzer()
712
713 success = analyzer.run()
714
715 except KeyboardInterrupt:
716 error = "Keyboard Interrupt"
717
718
719
720
721 except Exception as e:
722
723 error_exc = traceback.format_exc()
724 error = str(e)
725
726
727 if len(log.handlers) > 0:
728 log.exception(error_exc)
729 else:
730 sys.stderr.write("{0}\n".format(error_exc))
731
732
733 finally:
734
735 server = xmlrpclib.Server("http://127.0.0.1:8000")
736 server.complete(success, error, PATHS["root"])
737