1
2
3
4
5
6 import os
7 import logging
8 import random
9 import subprocess
10 import tempfile
11
12 from ctypes import byref, c_ulong, create_string_buffer, c_int, sizeof
13 from ctypes import c_uint, c_wchar_p, create_unicode_buffer
14
15 from lib.common.constants import SHUTDOWN_MUTEX
16 from lib.common.defines import KERNEL32, NTDLL, SYSTEM_INFO, STILL_ACTIVE
17 from lib.common.defines import THREAD_ALL_ACCESS, PROCESS_ALL_ACCESS
18 from lib.common.errors import get_error_string
19 from lib.common.exceptions import CuckooError
20 from lib.common.results import upload_to_host
21
22 log = logging.getLogger(__name__)
25 return subprocess.check_call(
26 args, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
27 stderr=subprocess.PIPE, env=env,
28 )
29
31 return subprocess.check_output(
32 args, stdin=subprocess.PIPE, stderr=subprocess.PIPE, env=env,
33 )
34
36 """Windows process."""
37 first_process = True
38 config = None
39
40
41
42
43 dumpmem = {}
44
45 - def __init__(self, pid=None, tid=None, process_name=None):
46 """
47 @param pid: process identifier.
48 @param tid: thread identifier.
49 @param process_name: process name.
50 """
51 self.pid = pid
52 self.tid = tid
53 self.process_name = process_name
54
55 @staticmethod
59
61 """Get system information."""
62 self.system_info = SYSTEM_INFO()
63 KERNEL32.GetSystemInfo(byref(self.system_info))
64
68
72
84
86 """Get process image file path.
87 @return: decoded file path.
88 """
89 process_handle = self.open_process()
90
91 NT_SUCCESS = lambda val: val >= 0
92
93 pbi = create_string_buffer(200)
94 size = c_int()
95
96
97 NTDLL.NtQueryInformationProcess.restype = c_int
98
99 ret = NTDLL.NtQueryInformationProcess(process_handle,
100 27,
101 byref(pbi),
102 sizeof(pbi),
103 byref(size))
104
105 KERNEL32.CloseHandle(process_handle)
106
107 if NT_SUCCESS(ret) and size.value > 8:
108 try:
109 fbuf = pbi.raw[8:]
110 fbuf = fbuf[:fbuf.find("\x00\x00")+1]
111 return fbuf.decode("utf16", errors="ignore")
112 except:
113 return ""
114
115 return ""
116
118 """Process is alive?
119 @return: process status.
120 """
121 return self.exit_code() == STILL_ACTIVE
122
124 """Get the Parent Process ID."""
125 process_handle = self.open_process()
126
127 NT_SUCCESS = lambda val: val >= 0
128
129 pbi = (c_int * 6)()
130 size = c_int()
131
132
133 NTDLL.NtQueryInformationProcess.restype = c_int
134
135 ret = NTDLL.NtQueryInformationProcess(process_handle,
136 0,
137 byref(pbi),
138 sizeof(pbi),
139 byref(size))
140
141 KERNEL32.CloseHandle(process_handle)
142
143 if NT_SUCCESS(ret) and size.value == sizeof(pbi):
144 return pbi[5]
145
146 return None
147
149 """Returns the shortpath for a file.
150
151 As Python 2.7 does not support passing along unicode strings in
152 subprocess.Popen() and alike this will have to do. See also:
153 http://stackoverflow.com/questions/2595448/unicode-filename-to-python-subprocess-call
154 """
155 KERNEL32.GetShortPathNameW.restype = c_uint
156 KERNEL32.GetShortPathNameW.argtypes = c_wchar_p, c_wchar_p, c_uint
157
158 buf = create_unicode_buffer(0x8000)
159 KERNEL32.GetShortPathNameW(path, buf, len(buf))
160 return buf.value
161
163 """Convert a list of arguments to a string that can be passed along
164 on the command-line.
165 @param args: list of arguments
166 @return: the command-line equivalent
167 """
168 ret = []
169 for line in args:
170 if " " in line:
171 ret.append('"%s"' % line)
172 else:
173 ret.append(line)
174 return " ".join(ret)
175
176 - def is32bit(self, pid=None, process_name=None, path=None):
177 """Is a PE file 32-bit or does a process identifier belong to a
178 32-bit process.
179 @param pid: process identifier.
180 @param process_name: process name.
181 @param path: path to a PE file.
182 @return: boolean or exception.
183 """
184 count = (pid is None) + (process_name is None) + (path is None)
185 if count != 2:
186 raise CuckooError("Invalid usage of is32bit, only one identifier "
187 "should be specified")
188
189 is32bit_exe = os.path.join("bin", "is32bit.exe")
190
191 if pid:
192 args = [is32bit_exe, "-p", "%s" % pid]
193 elif process_name:
194 args = [is32bit_exe, "-n", process_name]
195
196
197
198
199
200 elif os.path.isdir("C:\\Windows\\Sysnative") and \
201 path.lower().startswith("c:\\windows\\system32"):
202 return False
203 else:
204 args = [is32bit_exe, "-f", self.shortpath(path)]
205
206 try:
207 bitsize = int(subprocess_checkoutput(args))
208 except subprocess.CalledProcessError as e:
209 raise CuckooError("Error returned by is32bit: %s" % e)
210
211 return bitsize == 32
212
213 - def execute(self, path, args=None, dll=None, free=False, curdir=None,
214 source=None, mode=None, maximize=False, env=None,
215 trigger=None):
216 """Execute sample process.
217 @param path: sample path.
218 @param args: process args.
219 @param dll: dll path.
220 @param free: do not inject our monitor.
221 @param curdir: current working directory.
222 @param source: process identifier or process name which will
223 become the parent process for the new process.
224 @param mode: monitor mode - which functions to instrument.
225 @param maximize: whether the GUI should be maximized.
226 @param env: environment variables.
227 @param trigger: trigger to indicate analysis start
228 @return: operation status.
229 """
230 if not os.access(path, os.X_OK):
231 log.error("Unable to access file at path \"%s\", "
232 "execution aborted", path)
233 return False
234
235 is32bit = self.is32bit(path=path)
236
237 if not dll:
238 if is32bit:
239 dll = "monitor-x86.dll"
240 else:
241 dll = "monitor-x64.dll"
242
243 dllpath = os.path.abspath(os.path.join("bin", dll))
244
245 if not os.path.exists(dllpath):
246 log.warning("No valid DLL specified to be injected, "
247 "injection aborted.")
248 return False
249
250 if source:
251 if isinstance(source, (int, long)) or source.isdigit():
252 inject_is32bit = self.is32bit(pid=int(source))
253 else:
254 inject_is32bit = self.is32bit(process_name=source)
255 else:
256 inject_is32bit = is32bit
257
258 if inject_is32bit:
259 inject_exe = os.path.join("bin", "inject-x86.exe")
260 else:
261 inject_exe = os.path.join("bin", "inject-x64.exe")
262
263 argv = [
264 inject_exe,
265 "--app", self.shortpath(path),
266 "--only-start",
267 ]
268
269 if args:
270 argv += ["--args", self._encode_args(args)]
271
272 if curdir:
273 argv += ["--curdir", self.shortpath(curdir)]
274
275 if source:
276 if isinstance(source, (int, long)) or source.isdigit():
277 argv += ["--from", "%s" % source]
278 else:
279 argv += ["--from-process", source]
280
281 if maximize:
282 argv += ["--maximize"]
283
284 try:
285 output = subprocess_checkoutput(argv, env)
286 self.pid, self.tid = map(int, output.split())
287 except Exception:
288 log.error("Failed to execute process from path %r with "
289 "arguments %r (Error: %s)", path, argv,
290 get_error_string(KERNEL32.GetLastError()))
291 return False
292
293 if is32bit:
294 inject_exe = os.path.join("bin", "inject-x86.exe")
295 else:
296 inject_exe = os.path.join("bin", "inject-x64.exe")
297
298 argv = [
299 inject_exe,
300 "--resume-thread",
301 "--pid", "%s" % self.pid,
302 "--tid", "%s" % self.tid,
303 ]
304
305 if free:
306 argv.append("--free")
307 else:
308 argv += [
309 "--apc",
310 "--dll", dllpath,
311 "--config", self.drop_config(mode=mode, trigger=trigger),
312 ]
313
314 try:
315 subprocess_checkoutput(argv, env)
316 except Exception:
317 log.error("Failed to execute process from path %r with "
318 "arguments %r (Error: %s)", path, argv,
319 get_error_string(KERNEL32.GetLastError()))
320 return False
321
322 log.info("Successfully executed process from path %r with "
323 "arguments %r and pid %d", path, args or "", self.pid)
324 return True
325
327 """Terminate process.
328 @return: operation status.
329 """
330 process_handle = self.open_process()
331
332 ret = KERNEL32.TerminateProcess(process_handle, 1)
333 KERNEL32.CloseHandle(process_handle)
334
335 if ret:
336 log.info("Successfully terminated process with pid %d.", self.pid)
337 return True
338 else:
339 log.error("Failed to terminate process with pid %d.", self.pid)
340 return False
341
342 - def inject(self, dll=None, apc=False, track=True, mode=None):
343 """Inject our monitor into the specified process.
344 @param dll: Cuckoo DLL path.
345 @param apc: Use APC injection.
346 @param track: Track this process in the analyzer.
347 @param mode: Monitor mode - which functions to instrument.
348 """
349 if not self.pid and not self.process_name:
350 log.warning("No valid pid or process name specified, "
351 "injection aborted.")
352 return False
353
354
355
356 if not self.process_name and not self.is_alive():
357 log.warning("The process with pid %s is not alive, "
358 "injection aborted", self.pid)
359 return False
360
361 if self.process_name:
362 is32bit = self.is32bit(process_name=self.process_name)
363 elif self.pid:
364 is32bit = self.is32bit(pid=self.pid)
365
366 if not dll:
367 if is32bit:
368 dll = "monitor-x86.dll"
369 else:
370 dll = "monitor-x64.dll"
371
372 dllpath = os.path.abspath(os.path.join("bin", dll))
373 if not os.path.exists(dllpath):
374 log.warning("No valid DLL specified to be injected in process "
375 "with pid %s / process name %s, injection aborted.",
376 self.pid, self.process_name)
377 return False
378
379 if is32bit:
380 inject_exe = os.path.join("bin", "inject-x86.exe")
381 else:
382 inject_exe = os.path.join("bin", "inject-x64.exe")
383
384 args = [
385 inject_exe,
386 "--dll", dllpath,
387 "--config", self.drop_config(track=track, mode=mode),
388 ]
389
390 if self.pid:
391 args += ["--pid", "%s" % self.pid]
392 elif self.process_name:
393 args += ["--process-name", self.process_name]
394
395 if apc:
396 args += ["--apc", "--tid", "%s" % self.tid]
397 else:
398 args += ["--crt"]
399
400 try:
401 subprocess_checkcall(args)
402 except Exception:
403 log.error("Failed to inject %s-bit process with pid %s and "
404 "process name %s", 32 if is32bit else 64, self.pid,
405 self.process_name)
406 return False
407
408 return True
409
410 - def drop_config(self, track=True, mode=None, trigger=None):
411 """Helper function to drop the configuration for a new process."""
412 fd, config_path = tempfile.mkstemp()
413
414
415 if Process.first_process:
416
417
418
419 Process.startup_time = random.randint(1, 30) * 20 * 60 * 1000
420
421 lines = {
422 "pipe": self.config.pipe,
423 "logpipe": self.config.logpipe,
424 "analyzer": os.getcwd(),
425 "first-process": "1" if Process.first_process else "0",
426 "startup-time": Process.startup_time,
427 "shutdown-mutex": SHUTDOWN_MUTEX,
428 "force-sleepskip": self.config.options.get("force-sleepskip", "0"),
429 "track": "1" if track else "0",
430 "mode": mode or "",
431 "disguise": self.config.options.get("disguise", "0"),
432 "pipe-pid": "1",
433 "trigger": trigger or "",
434 }
435
436 for key, value in lines.items():
437 os.write(fd, "%s=%s\n" % (key, value))
438
439 os.close(fd)
440 Process.first_process = False
441 return config_path
442
444 """Dump process memory, optionally target only a certain memory range.
445 @return: operation status.
446 """
447 if not self.pid:
448 log.warning("No valid pid specified, memory dump aborted")
449 return False
450
451 if not self.is_alive():
452 log.warning("The process with pid %d is not alive, memory "
453 "dump aborted", self.pid)
454 return False
455
456 if self.is32bit(pid=self.pid):
457 inject_exe = os.path.join("bin", "inject-x86.exe")
458 else:
459 inject_exe = os.path.join("bin", "inject-x64.exe")
460
461
462 dump_path = tempfile.mktemp()
463
464 try:
465 args = [
466 inject_exe,
467 "--pid", "%s" % self.pid,
468 "--dump", dump_path,
469 ]
470
471
472 if addr and length:
473 args += [
474 "--dump-block",
475 "0x%x" % addr,
476 "%s" % length,
477 ]
478
479 subprocess_checkcall(args)
480 except subprocess.CalledProcessError:
481 log.error("Failed to dump memory of %d-bit process with pid %d.",
482 32 if self.is32bit(pid=self.pid) else 64, self.pid)
483 return
484
485
486
487
488 idx = self.dumpmem[self.pid] = self.dumpmem.get(self.pid, 0) + 1
489
490 if addr and length:
491 file_name = os.path.join(
492 "memory", "block-%s-0x%x-%s.dmp" % (self.pid, addr, idx)
493 )
494 else:
495 file_name = os.path.join("memory", "%s-%s.dmp" % (self.pid, idx))
496
497 upload_to_host(dump_path, file_name)
498 os.unlink(dump_path)
499
500 log.info("Memory dump of process with pid %d completed", self.pid)
501 return True
502
503
504
505 dump_memory_block = dump_memory
506