Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
51 changes: 51 additions & 0 deletions dissect/target/plugins/apps/chat/chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from typing import Union

from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
from dissect.target.helpers.record import create_extended_descriptor
from dissect.target.plugin import NamespacePlugin

COMMON_FIELDS = [
("datetime", "ts"),
("string", "client"),
("string", "account"),
("string", "sender"),
("string", "recipient"),
]

GENERIC_USER_FIELDS = [
("datetime", "ts_mtime"),
("string", "client"),
("string", "account"),
]

GENERIC_ATTACHMENT_FIELDS = [
*COMMON_FIELDS,
("path", "attachment"),
("string", "description"),
]

GENERIC_MESSAGE_FIELDS = [
*COMMON_FIELDS,
("string", "message"),
]

ChatUserRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
"chat/user",
GENERIC_USER_FIELDS,
)

ChatMessageRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
"chat/message",
GENERIC_MESSAGE_FIELDS,
)

ChatAttachmentRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
"chat/attachment",
GENERIC_ATTACHMENT_FIELDS,
)

ChatRecord = Union[ChatUserRecord, ChatMessageRecord, ChatAttachmentRecord]


class ChatPlugin(NamespacePlugin):
__namespace__ = "chat"
116 changes: 116 additions & 0 deletions dissect/target/plugins/apps/chat/msn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from __future__ import annotations

from typing import Iterator

from defusedxml import ElementTree as ET

from dissect.target import Target
from dissect.target.exceptions import UnsupportedPluginError
from dissect.target.helpers.fsutil import TargetPath
from dissect.target.plugin import export
from dissect.target.plugins.apps.chat.chat import (
ChatAttachmentRecord,
ChatMessageRecord,
ChatPlugin,
)
from dissect.target.plugins.general.users import UserDetails


class MSNPlugin(ChatPlugin):
"""Microsoft MSN Messenger plugin.

Supports the following versions on Windows XP and Windows 7:
- Windows Live Messenger (WLM) 2009
- MSN 7.5

Other versions might work but have not been tested. Does not support ``Messenger Plus! Live`` artifacts.
Tested using Escargot (https://escargot.chat).

Resources:
- https://en.wikipedia.org/wiki/Microsoft_Messenger_service
- https://en.wikipedia.org/wiki/MSN_Messenger
- http://computerforensics.parsonage.co.uk/downloads/MSNandLiveMessengerArtefactsOfConversations.pdf
"""

__namespace__ = "msn"

DATA_PATH = "Application Data\\Microsoft\\MSN Messenger"
HIST_PATH = "My Documents\\My Received Files"

def __init__(self, target: Target):
super().__init__(target)
self.installs = list(self.find_installs())

def find_installs(self) -> Iterator[tuple[UserDetails, TargetPath]]:
for user_details in self.target.user_details.all_with_home():
if (path := self.target.fs.path(user_details.user.home).joinpath(self.DATA_PATH)).exists():
for profile in path.iterdir():
if profile.is_dir():
yield user_details, profile

def check_compatible(self) -> None:
if not self.installs:
raise UnsupportedPluginError("No Microsoft MSN installs found on target")

@export(record=[ChatMessageRecord, ChatAttachmentRecord])
def history(self) -> Iterator[ChatMessageRecord | ChatAttachmentRecord]:
"""Yield MSN chat history messages.

Chat history artifacts can be found in:
- ``$HOME/My Documents/My Received Files/MsnMsgr.txt``
- ``$HOME/My Documents/My Received Files/$username$PassportID/History/*.xml``
"""

for user_details, profile in self.installs:
if not (hist_root := self.target.fs.path(user_details.user.home).joinpath(self.HIST_PATH)).exists():
self.target.log.warning(
"User %s does not have saved MSN chat history: directory %s does not exist",
user_details.user.name,
hist_root,
)
continue

hist_dir = None
for item in hist_root.iterdir():
if item.is_dir() and (hist_dir := item.name).endswith(profile.name):
for hist_file in hist_root.joinpath(hist_dir).joinpath("History").glob("*.xml"):
try:
xml = ET.fromstring(hist_file.read_text())
except Exception as e:
self.target.log.warning("XML file %s is malformed: %s", hist_file, e)
continue

for entry in xml:
common = {
"ts": entry.attrib.get("DateTime", 0),
"client": self.__namespace__,
"account": profile.name,
"sender": entry.find(".//From/User").get("FriendlyName"),
"_user": user_details.user,
"_target": self.target,
}

if entry.tag == "Message":
yield ChatMessageRecord(
**common,
recipient=entry.find(".//To/User").get("FriendlyName"),
message=entry.find(".//Text").text,
)

elif entry.tag in ["Invitation", "InvitationResponse"]:
if (file := entry.find(".//File")) is not None:
yield ChatAttachmentRecord(
**common,
recipient=None, # unknown with Invitations
attachment=file.text,
description=entry.find(".//Text").text,
)


def convert_email(string: str) -> int:
"""Convert MSN email address to 10 digit Passport ID."""
num = 0
for char in string.lower():
num = num * 101 + ord(char)
num -= (num // 4294967296) * 4294967296
return num
4 changes: 4 additions & 0 deletions dissect/target/plugins/os/windows/_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,10 @@ def users(self) -> Iterator[WindowsUserRecord]:
home = profile_image_path.value
name = home.split("\\")[-1]

# Windows XP uses %variables% in home paths
Comment thread
JSCU-CNI marked this conversation as resolved.
if "%" in home:
home = self.target.resolve(home)

yield WindowsUserRecord(
sid=subkey.name,
name=name,
Expand Down
3 changes: 3 additions & 0 deletions tests/_data/plugins/apps/chat/msn/history.xml
Git LFS file not shown
Empty file.
185 changes: 185 additions & 0 deletions tests/plugins/apps/chat/test_msn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
from datetime import datetime, timezone

from dissect.target import Target
from dissect.target.filesystem import VirtualFilesystem
from dissect.target.plugins.apps.chat.msn import MSNPlugin, convert_email
from tests._utils import absolute_path


def test_msn(target_win_users: Target, fs_win: VirtualFilesystem) -> None:
"""test if we parse MSN Chat messages on Windows XP correctly."""

morpheus_id = convert_email("morpheus@matrix.internal")
neo_id = convert_email("neo@matrix.internal")

assert morpheus_id == 2450688751
assert neo_id == 4092013818

fs_win.makedirs(f"Users/John/Application Data/Microsoft/MSN Messenger/{morpheus_id}")
fs_win.map_file(
f"Users/John/My Documents/My Received Files/morpheus{morpheus_id}/History/neo{neo_id}.xml",
absolute_path("_data/plugins/apps/chat/msn/history.xml"),
)

target_win_users.add_plugin(MSNPlugin)
assert len(target_win_users.msn.installs) == 1

results = list(target_win_users.msn.history())
assert len(results) == 35

assert results[0].username == "John"
assert results[0].hostname is None

assert results[0].ts == datetime(2025, 4, 1, 13, 37, 0, tzinfo=timezone.utc)
assert results[0].client == "msn"
assert results[0].account == str(morpheus_id)
assert results[0].sender == "morpheus@matrix.internal"
assert results[0].recipient == "neo@matrix.internal"

assert [(r.sender.replace("@matrix.internal", ""), r.message) for r in results] == [
(
"morpheus",
"At last.",
),
(
"morpheus",
"Welcome, Neo. As you no doubt have guessed, I am Morpheus.",
),
(
"neo",
"It's an honor.",
),
(
"morpheus",
"No, the honor is mine. Please. Come. Sit.",
),
(
"morpheus",
"I imagine, right now, you must be feeling a bit like Alice, tumbling down the rabbit hole?",
),
(
"neo",
"You could say that.",
),
(
"morpheus",
"I can see it in your eyes. You have the look of a man who accepts "
"what he sees because he is expecting to wake up.",
),
(
"morpheus",
"Ironically, this is not far from the truth. But I'm getting ahead of "
"myself. Can you tell me, Neo, why are you here?",
),
(
"neo",
"You're Morpheus. You're a legend. Most hackers would die to meet you.",
),
(
"morpheus",
"Yes. Thank you. But I think we both know there's more to it than that. Do you believe in fate, Neo?",
),
(
"neo",
"No.",
),
(
"morpheus",
"Why not?",
),
(
"neo",
"Because I don't like the idea that I'm not in control of my life.",
),
(
"morpheus",
"I know exactly what you mean.",
),
(
"morpheus",
"Let me tell you why you are here. You have come because you know something.",
),
(
"morpheus",
"What you know you can't explain but you feel it.",
),
(
"morpheus",
"You've felt it your whole life, felt that something is wrong with the world.",
),
(
"morpheus",
"You don't know what, but it's there like a splinter in your mind, "
"driving you mad. It is this feeling that brought you to me.",
),
(
"morpheus",
"Do you know what I'm talking about?",
),
(
"neo",
"The Matrix?",
),
(
"morpheus",
"Do you want to know what it is?",
),
(
"morpheus",
"The Matrix is everywhere, it's all around us, here even in this room.",
),
(
"morpheus",
"You can see it out your window or on your television. You feel it "
"when you go to work, or go to church or pay your taxes.",
),
(
"morpheus",
"It is the world that has been pulled over your eyes to blind you from the truth.",
),
(
"neo",
"What truth?",
),
(
"morpheus",
"That you are a slave, Neo. Like everyone else, you were born into "
"bondage, kept inside a prison that you cannot smell, taste, or touch.",
),
(
"morpheus",
"A prison for your mind.",
),
(
"morpheus",
"Unfortunately, no one can be told what the Matrix is.",
),
(
"morpheus",
"You have to see it for yourself.",
),
(
"morpheus",
"This is your last chance. After this, there is no going back.",
),
(
"morpheus",
"You take the blue pill and the story ends.",
),
(
"morpheus",
"You wake in your bed and you believe whatever you want to believe.",
),
(
"morpheus",
"You take the red pill and you stay in Wonderland and I show you how deep the rabbit-hole goes.",
),
(
"morpheus",
"Remember that all I am offering is the truth. Nothing more.",
),
(
"morpheus",
"Follow me.",
),
]