dissect.target 3.18.dev15__py3-none-any.whl → 3.19__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. dissect/target/filesystem.py +44 -25
  2. dissect/target/filesystems/config.py +32 -21
  3. dissect/target/filesystems/extfs.py +4 -0
  4. dissect/target/filesystems/itunes.py +1 -1
  5. dissect/target/filesystems/tar.py +1 -1
  6. dissect/target/filesystems/zip.py +81 -46
  7. dissect/target/helpers/config.py +22 -7
  8. dissect/target/helpers/configutil.py +69 -5
  9. dissect/target/helpers/cyber.py +4 -2
  10. dissect/target/helpers/fsutil.py +32 -4
  11. dissect/target/helpers/loaderutil.py +26 -7
  12. dissect/target/helpers/network_managers.py +22 -7
  13. dissect/target/helpers/record.py +37 -0
  14. dissect/target/helpers/record_modifier.py +23 -4
  15. dissect/target/helpers/shell_application_ids.py +732 -0
  16. dissect/target/helpers/utils.py +11 -0
  17. dissect/target/loader.py +1 -0
  18. dissect/target/loaders/ab.py +285 -0
  19. dissect/target/loaders/libvirt.py +40 -0
  20. dissect/target/loaders/mqtt.py +14 -1
  21. dissect/target/loaders/tar.py +8 -4
  22. dissect/target/loaders/utm.py +3 -0
  23. dissect/target/loaders/velociraptor.py +6 -6
  24. dissect/target/plugin.py +60 -3
  25. dissect/target/plugins/apps/browser/chrome.py +1 -0
  26. dissect/target/plugins/apps/browser/chromium.py +7 -5
  27. dissect/target/plugins/apps/browser/edge.py +1 -0
  28. dissect/target/plugins/apps/browser/firefox.py +82 -36
  29. dissect/target/plugins/apps/remoteaccess/anydesk.py +70 -50
  30. dissect/target/plugins/apps/remoteaccess/remoteaccess.py +8 -8
  31. dissect/target/plugins/apps/remoteaccess/teamviewer.py +46 -31
  32. dissect/target/plugins/apps/ssh/openssh.py +1 -1
  33. dissect/target/plugins/apps/ssh/ssh.py +177 -0
  34. dissect/target/plugins/apps/texteditor/__init__.py +0 -0
  35. dissect/target/plugins/apps/texteditor/texteditor.py +13 -0
  36. dissect/target/plugins/apps/texteditor/windowsnotepad.py +340 -0
  37. dissect/target/plugins/child/qemu.py +21 -0
  38. dissect/target/plugins/filesystem/ntfs/mft.py +132 -45
  39. dissect/target/plugins/filesystem/unix/capability.py +102 -87
  40. dissect/target/plugins/filesystem/walkfs.py +32 -21
  41. dissect/target/plugins/filesystem/yara.py +144 -23
  42. dissect/target/plugins/general/network.py +82 -0
  43. dissect/target/plugins/general/users.py +14 -10
  44. dissect/target/plugins/os/unix/_os.py +19 -5
  45. dissect/target/plugins/os/unix/bsd/freebsd/_os.py +3 -5
  46. dissect/target/plugins/os/unix/esxi/_os.py +29 -23
  47. dissect/target/plugins/os/unix/etc/etc.py +5 -8
  48. dissect/target/plugins/os/unix/history.py +3 -7
  49. dissect/target/plugins/os/unix/linux/_os.py +15 -14
  50. dissect/target/plugins/os/unix/linux/android/_os.py +15 -24
  51. dissect/target/plugins/os/unix/linux/redhat/_os.py +1 -1
  52. dissect/target/plugins/os/unix/locale.py +17 -6
  53. dissect/target/plugins/os/unix/shadow.py +47 -31
  54. dissect/target/plugins/os/windows/_os.py +4 -4
  55. dissect/target/plugins/os/windows/adpolicy.py +4 -1
  56. dissect/target/plugins/os/windows/catroot.py +1 -11
  57. dissect/target/plugins/os/windows/credential/__init__.py +0 -0
  58. dissect/target/plugins/os/windows/credential/lsa.py +174 -0
  59. dissect/target/plugins/os/windows/{sam.py → credential/sam.py} +5 -2
  60. dissect/target/plugins/os/windows/defender.py +6 -3
  61. dissect/target/plugins/os/windows/dpapi/blob.py +3 -0
  62. dissect/target/plugins/os/windows/dpapi/crypto.py +61 -23
  63. dissect/target/plugins/os/windows/dpapi/dpapi.py +127 -133
  64. dissect/target/plugins/os/windows/dpapi/keyprovider/__init__.py +0 -0
  65. dissect/target/plugins/os/windows/dpapi/keyprovider/credhist.py +21 -0
  66. dissect/target/plugins/os/windows/dpapi/keyprovider/empty.py +17 -0
  67. dissect/target/plugins/os/windows/dpapi/keyprovider/keychain.py +20 -0
  68. dissect/target/plugins/os/windows/dpapi/keyprovider/keyprovider.py +8 -0
  69. dissect/target/plugins/os/windows/dpapi/keyprovider/lsa.py +38 -0
  70. dissect/target/plugins/os/windows/dpapi/master_key.py +3 -0
  71. dissect/target/plugins/os/windows/jumplist.py +292 -0
  72. dissect/target/plugins/os/windows/lnk.py +96 -93
  73. dissect/target/plugins/os/windows/regf/shellbags.py +8 -5
  74. dissect/target/plugins/os/windows/regf/shimcache.py +2 -2
  75. dissect/target/plugins/os/windows/regf/usb.py +179 -114
  76. dissect/target/plugins/os/windows/task_helpers/tasks_xml.py +1 -1
  77. dissect/target/plugins/os/windows/wua_history.py +1073 -0
  78. dissect/target/target.py +4 -3
  79. dissect/target/tools/fs.py +53 -15
  80. dissect/target/tools/fsutils.py +243 -0
  81. dissect/target/tools/info.py +11 -4
  82. dissect/target/tools/query.py +2 -2
  83. dissect/target/tools/shell.py +505 -333
  84. dissect/target/tools/utils.py +23 -2
  85. dissect/target/tools/yara.py +65 -0
  86. dissect/target/volumes/md.py +2 -2
  87. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/METADATA +11 -7
  88. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/RECORD +94 -75
  89. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/WHEEL +1 -1
  90. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/entry_points.txt +1 -0
  91. dissect/target/helpers/ssh.py +0 -177
  92. /dissect/target/plugins/os/windows/{credhist.py → credential/credhist.py} +0 -0
  93. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/COPYRIGHT +0 -0
  94. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/LICENSE +0 -0
  95. {dissect.target-3.18.dev15.dist-info → dissect.target-3.19.dist-info}/top_level.txt +0 -0
@@ -3,6 +3,7 @@ import json
3
3
  import logging
4
4
  from base64 import b64decode
5
5
  from hashlib import pbkdf2_hmac, sha1
6
+ from itertools import chain
6
7
  from typing import Iterator, Optional
7
8
 
8
9
  from dissect.sql import sqlite3
@@ -14,7 +15,7 @@ from dissect.target.exceptions import FileNotFoundError, UnsupportedPluginError
14
15
  from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
15
16
  from dissect.target.helpers.fsutil import TargetPath
16
17
  from dissect.target.helpers.record import create_extended_descriptor
17
- from dissect.target.plugin import export
18
+ from dissect.target.plugin import OperatingSystem, export
18
19
  from dissect.target.plugins.apps.browser.browser import (
19
20
  GENERIC_COOKIE_FIELDS,
20
21
  GENERIC_DOWNLOAD_RECORD_FIELDS,
@@ -24,7 +25,7 @@ from dissect.target.plugins.apps.browser.browser import (
24
25
  BrowserPlugin,
25
26
  try_idna,
26
27
  )
27
- from dissect.target.plugins.general.users import UserDetails
28
+ from dissect.target.plugins.general.users import UserRecord
28
29
 
29
30
  try:
30
31
  from asn1crypto import algos, core
@@ -44,7 +45,10 @@ try:
44
45
  except ImportError:
45
46
  HAS_CRYPTO = False
46
47
 
47
- FIREFOX_EXTENSION_RECORD_FIELDS = [("uri", "source_uri"), ("string[]", "optional_permissions")]
48
+ FIREFOX_EXTENSION_RECORD_FIELDS = [
49
+ ("uri", "source_uri"),
50
+ ("string[]", "optional_permissions"),
51
+ ]
48
52
 
49
53
  log = logging.getLogger(__name__)
50
54
 
@@ -54,7 +58,7 @@ class FirefoxPlugin(BrowserPlugin):
54
58
 
55
59
  __namespace__ = "firefox"
56
60
 
57
- DIRS = [
61
+ USER_DIRS = [
58
62
  # Windows
59
63
  "AppData/Roaming/Mozilla/Firefox/Profiles",
60
64
  "AppData/local/Mozilla/Firefox/Profiles",
@@ -66,6 +70,10 @@ class FirefoxPlugin(BrowserPlugin):
66
70
  "Library/Application Support/Firefox",
67
71
  ]
68
72
 
73
+ SYSTEM_DIRS = [
74
+ "/data/data/org.mozilla.vrbrowser/files/mozilla",
75
+ ]
76
+
69
77
  BrowserHistoryRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
70
78
  "browser/firefox/history", GENERIC_HISTORY_RECORD_FIELDS
71
79
  )
@@ -79,7 +87,8 @@ class FirefoxPlugin(BrowserPlugin):
79
87
  )
80
88
 
81
89
  BrowserExtensionRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
82
- "browser/firefox/extension", GENERIC_EXTENSION_RECORD_FIELDS + FIREFOX_EXTENSION_RECORD_FIELDS
90
+ "browser/firefox/extension",
91
+ GENERIC_EXTENSION_RECORD_FIELDS + FIREFOX_EXTENSION_RECORD_FIELDS,
83
92
  )
84
93
 
85
94
  BrowserPasswordRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
@@ -88,27 +97,32 @@ class FirefoxPlugin(BrowserPlugin):
88
97
 
89
98
  def __init__(self, target):
90
99
  super().__init__(target)
91
- self.users_dirs: list[tuple[UserDetails, TargetPath]] = []
100
+ self.dirs: list[tuple[UserRecord, TargetPath]] = []
101
+
92
102
  for user_details in self.target.user_details.all_with_home():
93
- for directory in self.DIRS:
103
+ for directory in self.USER_DIRS:
94
104
  cur_dir = user_details.home_path.joinpath(directory)
95
105
  if not cur_dir.exists():
96
106
  continue
97
- self.users_dirs.append((user_details, cur_dir))
107
+ self.dirs.append((user_details.user, cur_dir))
108
+
109
+ for directory in self.SYSTEM_DIRS:
110
+ if (cur_dir := target.fs.path(directory)).exists():
111
+ self.dirs.append((None, cur_dir))
98
112
 
99
113
  def check_compatible(self) -> None:
100
- if not len(self.users_dirs):
114
+ if not len(self.dirs):
101
115
  raise UnsupportedPluginError("No Firefox directories found")
102
116
 
103
- def _iter_profiles(self) -> Iterator[tuple[UserDetails, TargetPath, TargetPath]]:
117
+ def _iter_profiles(self) -> Iterator[tuple[UserRecord, TargetPath, TargetPath]]:
104
118
  """Yield user directories."""
105
- for user, cur_dir in self.users_dirs:
119
+ for user, cur_dir in self.dirs:
106
120
  for profile_dir in cur_dir.iterdir():
107
121
  if not profile_dir.is_dir():
108
122
  continue
109
123
  yield user, cur_dir, profile_dir
110
124
 
111
- def _iter_db(self, filename: str) -> Iterator[tuple[UserDetails, SQLite3]]:
125
+ def _iter_db(self, filename: str) -> Iterator[tuple[UserRecord, SQLite3]]:
112
126
  """Yield opened history database files of all users.
113
127
 
114
128
  Args:
@@ -117,12 +131,24 @@ class FirefoxPlugin(BrowserPlugin):
117
131
  Yields:
118
132
  Opened SQLite3 databases.
119
133
  """
120
- for user, cur_dir, profile_dir in self._iter_profiles():
121
- db_file = profile_dir.joinpath(filename)
134
+ iter_system = ((None, system_dir, None) for user, system_dir in self.dirs if user is None)
135
+
136
+ for user, cur_dir, profile_dir in chain(iter_system, self._iter_profiles()):
137
+ if user is None and profile_dir is None:
138
+ db_file = cur_dir.parent.joinpath(filename)
139
+ # On some Android variants, some files may exist in the base directory (places.sqlite) but others
140
+ # in a nested profile directory (cookies.sqlite)
141
+ # /data/data/org.mozilla.vrbrowser/files/places.sqlite
142
+ # /data/data/org.mozilla.vrbrowser/files/mozilla/xxxxxx.default/cookies.sqlite
143
+ if not db_file.exists():
144
+ continue
145
+ else:
146
+ db_file = profile_dir.joinpath(filename)
147
+
122
148
  try:
123
149
  yield user, db_file, sqlite3.SQLite3(db_file.open())
124
150
  except FileNotFoundError:
125
- self.target.log.warning("Could not find %s file: %s", filename, db_file)
151
+ self.target.log.info("Could not find %s file: %s", filename, db_file)
126
152
  except SQLError as e:
127
153
  self.target.log.warning("Could not open %s file: %s", filename, db_file)
128
154
  self.target.log.debug("", exc_info=e)
@@ -151,6 +177,11 @@ class FirefoxPlugin(BrowserPlugin):
151
177
  from_url (uri): URL of the "from" visit.
152
178
  source: (path): The source file of the history record.
153
179
  """
180
+ if self.target.os == OperatingSystem.ANDROID:
181
+ from_timestamp = from_unix_ms
182
+ else:
183
+ from_timestamp = from_unix_us
184
+
154
185
  for user, db_file, db in self._iter_db("places.sqlite"):
155
186
  try:
156
187
  places = {row.id: row for row in db.table("moz_places").rows()}
@@ -167,7 +198,7 @@ class FirefoxPlugin(BrowserPlugin):
167
198
  from_visit, from_place = None, None
168
199
 
169
200
  yield self.BrowserHistoryRecord(
170
- ts=from_unix_us(row.visit_date),
201
+ ts=from_timestamp(row.visit_date),
171
202
  browser="firefox",
172
203
  id=row.id,
173
204
  url=try_idna(place.url),
@@ -183,7 +214,7 @@ class FirefoxPlugin(BrowserPlugin):
183
214
  from_url=try_idna(from_place.url) if from_place else None,
184
215
  source=db_file,
185
216
  _target=self.target,
186
- _user=user.user,
217
+ _user=user,
187
218
  )
188
219
  except SQLError as e:
189
220
  self.target.log.warning("Error processing history file: %s", db_file, exc_info=e)
@@ -228,7 +259,8 @@ class FirefoxPlugin(BrowserPlugin):
228
259
  is_http_only=bool(cookie.isHttpOnly),
229
260
  same_site=bool(cookie.sameSite),
230
261
  source=db_file,
231
- _user=user.user,
262
+ _target=self.target,
263
+ _user=user,
232
264
  )
233
265
  except SQLError as e:
234
266
  self.target.log.warning("Error processing cookie file: %s", db_file, exc_info=e)
@@ -254,7 +286,10 @@ class FirefoxPlugin(BrowserPlugin):
254
286
  for user, db_file, db in self._iter_db("places.sqlite"):
255
287
  try:
256
288
  places = {row.id: row for row in db.table("moz_places").rows()}
257
- attributes = {row.id: row.name for row in db.table("moz_anno_attributes").rows()}
289
+ if not (moz_anno_attributes := db.table("moz_anno_attributes")):
290
+ continue
291
+
292
+ attributes = {row.id: row.name for row in moz_anno_attributes.rows()}
258
293
  annotations = {}
259
294
 
260
295
  for row in db.table("moz_annos"):
@@ -315,7 +350,7 @@ class FirefoxPlugin(BrowserPlugin):
315
350
  state=state,
316
351
  source=db_file,
317
352
  _target=self.target,
318
- _user=user.user,
353
+ _user=user,
319
354
  )
320
355
  except SQLError as e:
321
356
  self.target.log.warning("Error processing history file: %s", db_file, exc_info=e)
@@ -350,7 +385,9 @@ class FirefoxPlugin(BrowserPlugin):
350
385
 
351
386
  if not extension_file.exists():
352
387
  self.target.log.warning(
353
- "No 'extensions.json' addon file found for user %s in directory %s", user, profile_dir
388
+ "No 'extensions.json' addon file found for user %s in directory %s",
389
+ user.name,
390
+ profile_dir,
354
391
  )
355
392
  continue
356
393
 
@@ -359,29 +396,31 @@ class FirefoxPlugin(BrowserPlugin):
359
396
 
360
397
  for extension in extensions.get("addons", []):
361
398
  yield self.BrowserExtensionRecord(
362
- ts_install=extension.get("installDate", 0) // 1000,
363
- ts_update=extension.get("updateDate", 0) // 1000,
399
+ ts_install=from_unix_ms(extension.get("installDate", 0)),
400
+ ts_update=from_unix_ms(extension.get("updateDate", 0)),
364
401
  browser="firefox",
365
402
  id=extension.get("id"),
366
- name=extension.get("defaultLocale", {}).get("name"),
403
+ name=(extension.get("defaultLocale", {}) or {}).get("name"),
367
404
  short_name=None,
368
405
  default_title=None,
369
- description=extension.get("defaultLocale", {}).get("description"),
406
+ description=(extension.get("defaultLocale", {}) or {}).get("description"),
370
407
  version=extension.get("version"),
371
408
  ext_path=extension.get("path"),
372
409
  from_webstore=None,
373
- permissions=extension.get("userPermissions", {}).get("permissions"),
410
+ permissions=(extension.get("userPermissions", {}) or {}).get("permissions"),
374
411
  manifest_version=extension.get("manifestVersion"),
375
412
  source_uri=extension.get("sourceURI"),
376
- optional_permissions=extension.get("optionalPermissions", {}).get("permissions"),
413
+ optional_permissions=(extension.get("optionalPermissions", {}) or {}).get("permissions"),
377
414
  source=extension_file,
378
415
  _target=self.target,
379
- _user=user.user,
416
+ _user=user,
380
417
  )
381
418
 
382
419
  except FileNotFoundError:
383
420
  self.target.log.info(
384
- "No 'extensions.json' addon file found for user %s in directory %s", user, profile_dir
421
+ "No 'extensions.json' addon file found for user %s in directory %s",
422
+ user.name,
423
+ profile_dir,
385
424
  )
386
425
  except json.JSONDecodeError:
387
426
  self.target.log.warning(
@@ -409,7 +448,9 @@ class FirefoxPlugin(BrowserPlugin):
409
448
 
410
449
  if not login_file.exists():
411
450
  self.target.log.warning(
412
- "No 'logins.json' password file found for user %s in directory %s", user, profile_dir
451
+ "No 'logins.json' password file found for user %s in directory %s",
452
+ user.name,
453
+ profile_dir,
413
454
  )
414
455
  continue
415
456
 
@@ -444,9 +485,9 @@ class FirefoxPlugin(BrowserPlugin):
444
485
  break
445
486
 
446
487
  yield self.BrowserPasswordRecord(
447
- ts_created=login.get("timeCreated", 0) // 1000,
448
- ts_last_used=login.get("timeLastUsed", 0) // 1000,
449
- ts_last_changed=login.get("timePasswordChanged", 0) // 1000,
488
+ ts_created=from_unix_ms(login.get("timeCreated", 0)),
489
+ ts_last_used=from_unix_ms(login.get("timeLastUsed", 0)),
490
+ ts_last_changed=from_unix_ms(login.get("timePasswordChanged", 0)),
450
491
  browser="firefox",
451
492
  id=login.get("id"),
452
493
  url=login.get("hostname"),
@@ -456,14 +497,19 @@ class FirefoxPlugin(BrowserPlugin):
456
497
  decrypted_password=decrypted_password,
457
498
  source=login_file,
458
499
  _target=self.target,
459
- _user=user.user,
500
+ _user=user,
460
501
  )
461
502
 
462
503
  except FileNotFoundError:
463
- self.target.log.info("No password file found for user %s in directory %s", user, profile_dir)
504
+ self.target.log.info(
505
+ "No password file found for user %s in directory %s",
506
+ user.name,
507
+ profile_dir,
508
+ )
464
509
  except json.JSONDecodeError:
465
510
  self.target.log.warning(
466
- "logins.json file in directory %s is malformed, consider inspecting the file manually", profile_dir
511
+ "logins.json file in directory %s is malformed, consider inspecting the file manually",
512
+ profile_dir,
467
513
  )
468
514
 
469
515
 
@@ -1,91 +1,111 @@
1
- from datetime import datetime
1
+ import re
2
+ from datetime import datetime, timezone
3
+ from typing import Iterator
2
4
 
3
5
  from dissect.target.exceptions import UnsupportedPluginError
6
+ from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
7
+ from dissect.target.helpers.fsutil import TargetPath
8
+ from dissect.target.helpers.record import create_extended_descriptor
4
9
  from dissect.target.plugin import export
5
10
  from dissect.target.plugins.apps.remoteaccess.remoteaccess import (
11
+ GENERIC_LOG_RECORD_FIELDS,
6
12
  RemoteAccessPlugin,
7
- RemoteAccessRecord,
8
13
  )
14
+ from dissect.target.plugins.general.users import UserDetails
9
15
 
10
16
 
11
17
  class AnydeskPlugin(RemoteAccessPlugin):
12
- """
13
- Anydesk plugin.
14
- """
18
+ """Anydesk plugin."""
15
19
 
16
20
  __namespace__ = "anydesk"
17
21
 
18
22
  # Anydesk logs when installed as a service
19
23
  SERVICE_GLOBS = [
20
- "/sysvol/ProgramData/AnyDesk/*.trace", # Standard client >= Windows 7
21
- "/sysvol/ProgramData/AnyDesk/ad_*/*.trace", # Custom client >= Windows 7
22
- "/var/log/anydesk*.trace", # Standard/Custom client Linux/MacOS
24
+ # Standard client >= Windows 7
25
+ "sysvol/ProgramData/AnyDesk/*.trace",
26
+ # Custom client >= Windows 7
27
+ "sysvol/ProgramData/AnyDesk/ad_*/*.trace",
28
+ # Windows XP / 2003
29
+ "sysvol/Documents and Settings/Public/AnyDesk/*.trace",
30
+ "sysvol/Documents and Settings/Public/AnyDesk/ad_*/*.trace",
31
+ # Standard/Custom client Linux/MacOS
32
+ "var/log/anydesk*/*.trace",
23
33
  ]
24
34
 
25
35
  # User specific Anydesk logs
26
36
  USER_GLOBS = [
27
- "appdata/roaming/AnyDesk/*.trace", # Standard client Windows
28
- "appdata/roaming/AnyDesk/ad_*/*.trace", # Custom client Windows
29
- ".anydesk/*.trace", # Standard client Linux/MacOS
30
- ".anydesk_ad_*/*.trace", # Custom client Linux/MacOS
37
+ # Standard client Windows
38
+ "AppData/Roaming/AnyDesk/*.trace",
39
+ # Custom client Windows
40
+ "AppData/Roaming/AnyDesk/ad_*/*.trace",
41
+ # Windows XP / 2003
42
+ "AppData/AnyDesk/*.trace",
43
+ # Standard client Linux/MacOS
44
+ ".anydesk/*.trace",
45
+ # Custom client Linux/MacOS
46
+ ".anydesk_ad_*/*.trace",
31
47
  ]
32
48
 
49
+ RemoteAccessLogRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
50
+ "remoteaccess/anydesk/log", GENERIC_LOG_RECORD_FIELDS
51
+ )
52
+
33
53
  def __init__(self, target):
34
54
  super().__init__(target)
35
55
 
36
- self.logfiles = []
56
+ self.trace_files: set[tuple[TargetPath, UserDetails]] = set()
37
57
 
38
- # Check service globs
58
+ # Service globs
39
59
  user = None
40
- for log_glob in self.SERVICE_GLOBS:
41
- for logfile in self.target.fs.glob(log_glob):
42
- self.logfiles.append([logfile, user])
60
+ for trace_glob in self.SERVICE_GLOBS:
61
+ for trace_file in self.target.fs.path().glob(trace_glob):
62
+ self.trace_files.add((trace_file, user))
43
63
 
44
- # Anydesk logs when as user
64
+ # User globs
45
65
  for user_details in self.target.user_details.all_with_home():
46
- for log_glob in self.USER_GLOBS:
47
- for logfile in user_details.home_path.glob(log_glob):
48
- self.logfiles.append([logfile, user_details.user])
66
+ for trace_glob in self.USER_GLOBS:
67
+ for trace_file in user_details.home_path.glob(trace_glob):
68
+ self.trace_files.add((trace_file, user_details.user))
49
69
 
50
70
  def check_compatible(self) -> None:
51
- if not (len(self.logfiles)):
52
- raise UnsupportedPluginError("No Anydesk logs found")
71
+ if not self.trace_files:
72
+ raise UnsupportedPluginError("No Anydesk trace files found on target")
53
73
 
54
- @export(record=RemoteAccessRecord)
55
- def logs(self):
56
- """Return the content of the AnyDesk logs.
74
+ @export(record=RemoteAccessLogRecord)
75
+ def logs(self) -> Iterator[RemoteAccessLogRecord]:
76
+ """Parse AnyDesk trace files.
57
77
 
58
78
  AnyDesk is a remote desktop application and can be used by adversaries to get (persistent) access to a machine.
59
- Log files (.trace files) are retrieved from various location based on OS and client type.
79
+ Log files (.trace files) can be stored on various locations, based on target OS and client type.
80
+ Timestamps in trace files do not carry a time zone designator (TZD) but are in fact UTC.
60
81
 
61
82
  References:
62
83
  - https://www.inversecos.com/2021/02/forensic-analysis-of-anydesk-logs.html
63
84
  - https://support.anydesk.com/knowledge/trace-files#trace-file-locations
64
85
  """
65
- for logfile, user in self.logfiles:
66
- logfile = self.target.fs.path(logfile)
67
-
68
- for line in logfile.open("rt"):
86
+ for trace_file, user in self.trace_files:
87
+ for line in trace_file.open("rt", errors="backslashreplace"):
69
88
  line = line.strip()
70
89
 
71
- # Skip empty lines
72
- if not line:
73
- continue
74
-
75
- if "* * * * * * * * * * * * * *" in line:
90
+ if not line or "* * * * * * * * * * * * * *" in line:
76
91
  continue
77
92
 
78
- level, ts_day, ts_time, description = line.split(" ", 3)
79
- description = f"{level} {description}"
80
- ts_time = ts_time.split(".")[0]
81
-
82
- timestamp = datetime.strptime(f"{ts_day} {ts_time}", "%Y-%m-%d %H:%M:%S")
83
-
84
- yield RemoteAccessRecord(
85
- ts=timestamp,
86
- tool="anydesk",
87
- logfile=str(logfile),
88
- description=description,
89
- _target=self.target,
90
- _user=user,
91
- )
93
+ try:
94
+ level, ts_date, ts_time, message = line.split(" ", 3)
95
+
96
+ timestamp = datetime.strptime(f"{ts_date} {ts_time}", "%Y-%m-%d %H:%M:%S.%f").replace(
97
+ tzinfo=timezone.utc
98
+ )
99
+ message = re.sub(r"\s\s+", " ", f"{level} {message}")
100
+
101
+ yield self.RemoteAccessLogRecord(
102
+ ts=timestamp,
103
+ message=message,
104
+ source=trace_file,
105
+ _target=self.target,
106
+ _user=user,
107
+ )
108
+
109
+ except ValueError as e:
110
+ self.target.log.warning("Could not parse log line in file %s: '%s'", trace_file, line)
111
+ self.target.log.debug("", exc_info=e)
@@ -2,14 +2,14 @@ from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExt
2
2
  from dissect.target.helpers.record import create_extended_descriptor
3
3
  from dissect.target.plugin import NamespacePlugin
4
4
 
5
- RemoteAccessRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
6
- "application/log/remoteaccess",
7
- [
8
- ("datetime", "ts"),
9
- ("string", "tool"),
10
- ("path", "logfile"),
11
- ("string", "description"),
12
- ],
5
+ GENERIC_LOG_RECORD_FIELDS = [
6
+ ("datetime", "ts"),
7
+ ("string", "message"),
8
+ ("path", "source"),
9
+ ]
10
+
11
+ RemoteAccessLogRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
12
+ "remoteaccess/log", GENERIC_LOG_RECORD_FIELDS
13
13
  )
14
14
 
15
15
 
@@ -1,60 +1,74 @@
1
1
  import re
2
- from datetime import datetime
2
+ from datetime import datetime, timezone
3
+ from typing import Iterator
3
4
 
4
5
  from dissect.target.exceptions import UnsupportedPluginError
6
+ from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
7
+ from dissect.target.helpers.fsutil import TargetPath
8
+ from dissect.target.helpers.record import create_extended_descriptor
5
9
  from dissect.target.plugin import export
6
10
  from dissect.target.plugins.apps.remoteaccess.remoteaccess import (
11
+ GENERIC_LOG_RECORD_FIELDS,
7
12
  RemoteAccessPlugin,
8
- RemoteAccessRecord,
9
13
  )
14
+ from dissect.target.plugins.general.users import UserDetails
10
15
 
11
16
  START_PATTERN = re.compile(r"^(\d{2}|\d{4})/")
12
17
 
13
18
 
14
- class TeamviewerPlugin(RemoteAccessPlugin):
15
- """
16
- Teamviewer plugin.
19
+ class TeamViewerPlugin(RemoteAccessPlugin):
20
+ """TeamViewer client plugin.
21
+
22
+ Resources:
23
+ - https://teamviewer.com/en/global/support/knowledge-base/teamviewer-classic/contact-support/find-your-log-files
24
+ - https://www.systoolsgroup.com/forensics/teamviewer/
25
+ - https://benleeyr.wordpress.com/2020/05/19/teamviewer-forensics-tested-on-v15/
17
26
  """
18
27
 
19
28
  __namespace__ = "teamviewer"
20
29
 
21
- # Teamviewer log when service (Windows)
22
- GLOBS = [
30
+ SYSTEM_GLOBS = [
23
31
  "sysvol/Program Files/TeamViewer/*.log",
24
32
  "sysvol/Program Files (x86)/TeamViewer/*.log",
33
+ "/var/log/teamviewer*/*.log",
34
+ ]
35
+
36
+ USER_GLOBS = [
37
+ "AppData/Roaming/TeamViewer/teamviewer*_logfile.log",
38
+ "Library/Logs/TeamViewer/teamviewer*_logfile*.log",
25
39
  ]
26
40
 
41
+ RemoteAccessLogRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
42
+ "remoteaccess/teamviewer/log", GENERIC_LOG_RECORD_FIELDS
43
+ )
44
+
27
45
  def __init__(self, target):
28
46
  super().__init__(target)
29
47
 
30
- self.logfiles = []
48
+ self.logfiles: list[list[TargetPath, UserDetails]] = []
31
49
 
32
- # Check service globs
33
- user = None
34
- for log_glob in self.GLOBS:
50
+ # Find system service log files.
51
+ for log_glob in self.SYSTEM_GLOBS:
35
52
  for logfile in self.target.fs.glob(log_glob):
36
- self.logfiles.append([logfile, user])
53
+ self.logfiles.append([logfile, None])
37
54
 
38
- # Teamviewer logs when as user (Windows)
55
+ # Find user log files.
39
56
  for user_details in self.target.user_details.all_with_home():
40
- for logfile in user_details.home_path.glob("appdata/roaming/teamviewer/teamviewer*_logfile.log"):
41
- self.logfiles.append([logfile, user_details.user])
57
+ for log_glob in self.USER_GLOBS:
58
+ for logfile in user_details.home_path.glob(log_glob):
59
+ self.logfiles.append([logfile, user_details])
42
60
 
43
61
  def check_compatible(self) -> None:
44
62
  if not len(self.logfiles):
45
63
  raise UnsupportedPluginError("No Teamviewer logs found")
46
64
 
47
- @export(record=RemoteAccessRecord)
48
- def logs(self):
49
- """Return the content of the TeamViewer logs.
50
-
51
- TeamViewer is a commercial remote desktop application. An adversary may use it to gain persistence on a
52
- system.
65
+ @export(record=RemoteAccessLogRecord)
66
+ def logs(self) -> Iterator[RemoteAccessLogRecord]:
67
+ """Yield TeamViewer client logs.
53
68
 
54
- References:
55
- - https://www.teamviewer.com/nl/
69
+ TeamViewer is a commercial remote desktop application. An adversary may use it to gain persistence on a system.
56
70
  """
57
- for logfile, user in self.logfiles:
71
+ for logfile, user_details in self.logfiles:
58
72
  logfile = self.target.fs.path(logfile)
59
73
 
60
74
  start_date = None
@@ -83,7 +97,7 @@ class TeamviewerPlugin(RemoteAccessPlugin):
83
97
  if not re.match(START_PATTERN, line):
84
98
  continue
85
99
 
86
- ts_day, ts_time, description = line.split(" ", 2)
100
+ ts_day, ts_time, message = line.split(" ", 2)
87
101
  ts_time = ts_time.split(".")[0]
88
102
 
89
103
  # Correct for use of : as millisecond separator
@@ -99,13 +113,14 @@ class TeamviewerPlugin(RemoteAccessPlugin):
99
113
  if ts_day.count("/") == 2 and len(ts_day.split("/")[0]) == 2:
100
114
  ts_day = "20" + ts_day
101
115
 
102
- timestamp = datetime.strptime(f"{ts_day} {ts_time}", "%Y/%m/%d %H:%M:%S")
116
+ timestamp = datetime.strptime(f"{ts_day} {ts_time}", "%Y/%m/%d %H:%M:%S").replace(
117
+ tzinfo=timezone.utc
118
+ )
103
119
 
104
- yield RemoteAccessRecord(
105
- tool="teamviewer",
120
+ yield self.RemoteAccessLogRecord(
106
121
  ts=timestamp,
107
- logfile=str(logfile),
108
- description=description,
122
+ message=message,
123
+ source=logfile,
109
124
  _target=self.target,
110
- _user=user,
125
+ _user=user_details.user if user_details else None,
111
126
  )
@@ -7,7 +7,6 @@ from typing import Iterator
7
7
  from dissect.target import Target
8
8
  from dissect.target.exceptions import UnsupportedPluginError
9
9
  from dissect.target.helpers.fsutil import TargetPath
10
- from dissect.target.helpers.ssh import SSHPrivateKey
11
10
  from dissect.target.plugin import export
12
11
  from dissect.target.plugins.apps.ssh.ssh import (
13
12
  AuthorizedKeysRecord,
@@ -15,6 +14,7 @@ from dissect.target.plugins.apps.ssh.ssh import (
15
14
  PrivateKeyRecord,
16
15
  PublicKeyRecord,
17
16
  SSHPlugin,
17
+ SSHPrivateKey,
18
18
  calculate_fingerprints,
19
19
  )
20
20