Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Microsoft Office add-in detection #966

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open

Microsoft Office add-in detection #966

wants to merge 19 commits into from

Conversation

twiggler
Copy link
Contributor

@twiggler twiggler commented Dec 5, 2024

Detects:

  • Persistence through startup folders (templates, executables)
  • COM addins
  • VSTO addins by parsing ClickOnce Deployment manifests
  • Web addins by inspecting WEF cache and parsing manifests

Limitations:

  • Office for Mac is not supported
  • Supports Microsoft Office 2016 and later
  • Manifests which are wof compressed are unreadable

Bonus:

  • Fix issue in resolver where user environment was not taken into account when expanding paths
  • Fix issue where partial path resolution resolved to directories.
  • Added convenience functions for querying registry

Copy link

codecov bot commented Dec 5, 2024

Codecov Report

Attention: Patch coverage is 89.51965% with 24 lines in your changes missing coverage. Please review.

Project coverage is 77.85%. Comparing base (6770095) to head (97a57f4).
Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
...ssect/target/plugins/apps/productivity/msoffice.py 87.36% 24 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #966      +/-   ##
==========================================
+ Coverage   77.72%   77.85%   +0.13%     
==========================================
  Files         326      328       +2     
  Lines       28571    28863     +292     
==========================================
+ Hits        22206    22472     +266     
- Misses       6365     6391      +26     
Flag Coverage Δ
unittests 77.85% <89.51%> (+0.13%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@twiggler twiggler force-pushed the office-addins branch 3 times, most recently from 95ab63d to d55a0c4 Compare December 5, 2024 13:29
@@ -172,6 +172,18 @@ def value(self, value: str) -> RegistryValue:
"""
raise NotImplementedError()

def value_or_default(self, value: str, default: ValueType = None) -> RegistryValue:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you roll this into the value method so it's more in line with standard library Python code?

dissect/target/helpers/utils.py Outdated Show resolved Hide resolved
@internal
def value(self, key: str, value: str) -> ValueCollection:
"""Convenience method for accessing a specific value."""
return self.key(key).value(value)

@internal
def value_or_empty(self, key: str, value: str) -> ValueCollection:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

@@ -282,11 +286,31 @@ def key(self, key: Optional[str] = None) -> KeyCollection:

return res

@internal
def key_or_empty(self, key: Optional[str] = None) -> KeyCollection:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with this one.

@twiggler twiggler force-pushed the office-addins branch 4 times, most recently from 6338cec to 755f6cf Compare December 5, 2024 15:26
@@ -29,17 +29,20 @@ def findall(buf: bytes, needle: bytes) -> Iterator[int]:
T = TypeVar("T")


def to_list(value: T | list[T]) -> list[T]:
"""Convert a single value or a list of values to a list.
def to_list(value: T | list[T] | None) -> list[T]:
Copy link
Contributor Author

@twiggler twiggler Dec 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To Miauwkeru: For our usecases, your implementation won in the end.

@twiggler twiggler marked this pull request as ready for review December 5, 2024 15:39
@twiggler twiggler requested review from Miauwkeru and Horofic December 6, 2024 07:14
dissect/target/plugins/apps/productivity/msoffice.py Outdated Show resolved Hide resolved
dissect/target/plugins/apps/productivity/msoffice.py Outdated Show resolved Hide resolved
dissect/target/plugins/apps/productivity/msoffice.py Outdated Show resolved Hide resolved
dissect/target/plugins/apps/productivity/msoffice.py Outdated Show resolved Hide resolved
dissect/target/plugins/apps/productivity/msoffice.py Outdated Show resolved Hide resolved
dissect/target/plugins/apps/productivity/msoffice.py Outdated Show resolved Hide resolved
dissect/target/plugins/apps/productivity/msoffice.py Outdated Show resolved Hide resolved
dissect/target/plugins/apps/productivity/msoffice.py Outdated Show resolved Hide resolved
dissect/target/plugins/apps/productivity/msoffice.py Outdated Show resolved Hide resolved
@EinatFox EinatFox linked an issue Dec 16, 2024 that may be closed by this pull request
@twiggler twiggler force-pushed the office-addins branch 4 times, most recently from 375faea to 951e4e3 Compare January 7, 2025 13:34
@twiggler twiggler requested a review from Horofic January 8, 2025 08:39
@@ -299,10 +303,10 @@ def iterkeys(self, keys: Union[str, list[str]]) -> Iterator[KeyCollection]:
yield key

@internal
def keys(self, keys: Union[str, list[str]]) -> Iterator[KeyCollection]:
def keys(self, keys: Union[str, list[str]]) -> Iterator[RegistryKey]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def keys(self, keys: Union[str, list[str]]) -> Iterator[RegistryKey]:
def keys(self, keys: str | list[str]) -> Iterator[RegistryKey]:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bump

@@ -316,6 +320,19 @@ def keys(self, keys: Union[str, list[str]]) -> Iterator[KeyCollection]:
except HiveUnavailableError:
pass

@internal
def values(self, keys: Union[str, list[str]], value: str) -> Iterator[RegistryValue]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def values(self, keys: Union[str, list[str]], value: str) -> Iterator[RegistryValue]:
def values(self, keys: str | list[str], value: str) -> Iterator[RegistryValue]:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bump

tests/plugins/filesystem/test_resolver.py Show resolved Hide resolved
dissect/target/plugins/apps/productivity/msoffice.py Outdated Show resolved Hide resolved
- https://learn.microsoft.com/en-us/office/dev/add-ins/overview/office-add-ins
- https://learn.microsoft.com/en-us/visualstudio/vsto/registry-entries-for-vsto-add-ins

Yields a OfficeNativeAddinRecord with fields:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Yields a OfficeNativeAddinRecord with fields:
Yields a ``OfficeNativeAddinRecord`` with fields:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bump

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this one was already processed?

dissect/target/plugins/apps/productivity/msoffice.py Outdated Show resolved Hide resolved
except RegistryValueNotFoundError:
if default is self.__marker:
raise
return VirtualValue(VirtualHive(), value, default)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't we still want the actual hive to be linked?

@@ -161,7 +161,7 @@ def subkey(self, subkey: str) -> CbRegistryKey:
def subkeys(self) -> list[CbRegistryKey]:
return list(map(self.subkey, self.data["sub_keys"]))

def value(self, value: str) -> str:
def _value(self, value: str) -> str:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe update these return types while we're at it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love types, so happy to do it.

But is there light at the end of the tunnel?

Unfortunately, a lot of the type annotations are incorrect. As I am used to inferring the behavior of a function by its type signature, this has put me on the wrong foot on multiple occasions. The type annotations are incorrect because the code is not type checked, either in CI or locally. I think one of the problems with enforcing type checking is that core dependencies such as flow record and cstruct use dynamic typing.
I believe @Miauwkeru mentioned a solution for somehow adding type information to cstruct. Or we could use code generation at "compile-time" instead of during run-time.

Happy to help out with this (also in my own time). However, I also think a principled decision needs to be made to stop relying on techniques which use dynamic types.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .pyi generation for cstruct was indeed my idea, of which Miauwkeru made a POC. Happy to hear other suggestions of yours.

However, 95% of code in dissect.target does not interact with flow.record or dissect.cstruct, and we still want accurate type information there. I agree that it's best if we can check that in CI or locally. So I think the best way forward for the time being is to figure out if there's some way to do that and ignore all flow.record and dissect.cstruct types while we work separately on a solution for those dynamic types.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I take it the return type can be covariant?)

@@ -308,7 +308,7 @@ def subkeys(self) -> list[SmbRegistryKey]:

return subkeys

def value(self, value: str) -> str:
def _value(self, value: str) -> str:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe update these return types while we're at it?

dissect/target/plugins/apps/productivity/msoffice.py Outdated Show resolved Hide resolved
if not isinstance(value, list):
if value is None:
return []
elif not isinstance(value, list):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
elif not isinstance(value, list):
if not isinstance(value, list):

Comment on lines 251 to 260
nativePluginStatus = self._parse_plugin_status(addin)
yield OfficeNativeAddinRecord(
name=addin.value("FriendlyName", None).value,
modification_time=addin.timestamp,
loaded=nativePluginStatus.loaded if nativePluginStatus else None,
load_behavior=nativePluginStatus.load_behavior.name if nativePluginStatus else None,
type=addin_type,
manifest=windows_path(manifest_path_str) if manifest_path_str else None,
codebases=executables,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason this is not snake cased?

Suggested change
nativePluginStatus = self._parse_plugin_status(addin)
yield OfficeNativeAddinRecord(
name=addin.value("FriendlyName", None).value,
modification_time=addin.timestamp,
loaded=nativePluginStatus.loaded if nativePluginStatus else None,
load_behavior=nativePluginStatus.load_behavior.name if nativePluginStatus else None,
type=addin_type,
manifest=windows_path(manifest_path_str) if manifest_path_str else None,
codebases=executables,
)
native_plugin_status = self._parse_plugin_status(addin)
yield OfficeNativeAddinRecord(
name=addin.value("FriendlyName", None).value,
modification_time=addin.timestamp,
loaded=native_plugin_status.loaded if nativePluginStatus else None,
load_behavior=native_plugin_status.load_behavior.name if native_plugin_status else None,
type=addin_type,
manifest=windows_path(manifest_path_str) if manifest_path_str else None,
codebases=executables,
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add plugin for Office addins
3 participants