Skip to content

Commit bb9a488

Browse files
JSCU-CNISchamper
andauthored
Add Windows MSN plugin (#1084)
Co-authored-by: Erik Schamper <1254028+Schamper@users.noreply.github.com>
1 parent e65a678 commit bb9a488

File tree

7 files changed

+359
-0
lines changed

7 files changed

+359
-0
lines changed

dissect/target/plugins/apps/chat/__init__.py

Whitespace-only changes.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from typing import Union
2+
3+
from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
4+
from dissect.target.helpers.record import create_extended_descriptor
5+
from dissect.target.plugin import NamespacePlugin
6+
7+
COMMON_FIELDS = [
8+
("datetime", "ts"),
9+
("string", "client"),
10+
("string", "account"),
11+
("string", "sender"),
12+
("string", "recipient"),
13+
]
14+
15+
GENERIC_USER_FIELDS = [
16+
("datetime", "ts_mtime"),
17+
("string", "client"),
18+
("string", "account"),
19+
]
20+
21+
GENERIC_ATTACHMENT_FIELDS = [
22+
*COMMON_FIELDS,
23+
("path", "attachment"),
24+
("string", "description"),
25+
]
26+
27+
GENERIC_MESSAGE_FIELDS = [
28+
*COMMON_FIELDS,
29+
("string", "message"),
30+
]
31+
32+
ChatUserRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
33+
"chat/user",
34+
GENERIC_USER_FIELDS,
35+
)
36+
37+
ChatMessageRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
38+
"chat/message",
39+
GENERIC_MESSAGE_FIELDS,
40+
)
41+
42+
ChatAttachmentRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
43+
"chat/attachment",
44+
GENERIC_ATTACHMENT_FIELDS,
45+
)
46+
47+
ChatRecord = Union[ChatUserRecord, ChatMessageRecord, ChatAttachmentRecord]
48+
49+
50+
class ChatPlugin(NamespacePlugin):
51+
__namespace__ = "chat"
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from __future__ import annotations
2+
3+
from typing import Iterator
4+
5+
from defusedxml import ElementTree as ET
6+
7+
from dissect.target import Target
8+
from dissect.target.exceptions import UnsupportedPluginError
9+
from dissect.target.helpers.fsutil import TargetPath
10+
from dissect.target.plugin import export
11+
from dissect.target.plugins.apps.chat.chat import (
12+
ChatAttachmentRecord,
13+
ChatMessageRecord,
14+
ChatPlugin,
15+
)
16+
from dissect.target.plugins.general.users import UserDetails
17+
18+
19+
class MSNPlugin(ChatPlugin):
20+
"""Microsoft MSN Messenger plugin.
21+
22+
Supports the following versions on Windows XP and Windows 7:
23+
- Windows Live Messenger (WLM) 2009
24+
- MSN 7.5
25+
26+
Other versions might work but have not been tested. Does not support ``Messenger Plus! Live`` artifacts.
27+
Tested using Escargot (https://escargot.chat).
28+
29+
Resources:
30+
- https://en.wikipedia.org/wiki/Microsoft_Messenger_service
31+
- https://en.wikipedia.org/wiki/MSN_Messenger
32+
- http://computerforensics.parsonage.co.uk/downloads/MSNandLiveMessengerArtefactsOfConversations.pdf
33+
"""
34+
35+
__namespace__ = "msn"
36+
37+
DATA_PATH = "Application Data\\Microsoft\\MSN Messenger"
38+
HIST_PATH = "My Documents\\My Received Files"
39+
40+
def __init__(self, target: Target):
41+
super().__init__(target)
42+
self.installs = list(self.find_installs())
43+
44+
def find_installs(self) -> Iterator[tuple[UserDetails, TargetPath]]:
45+
for user_details in self.target.user_details.all_with_home():
46+
if (path := self.target.fs.path(user_details.user.home).joinpath(self.DATA_PATH)).exists():
47+
for profile in path.iterdir():
48+
if profile.is_dir():
49+
yield user_details, profile
50+
51+
def check_compatible(self) -> None:
52+
if not self.installs:
53+
raise UnsupportedPluginError("No Microsoft MSN installs found on target")
54+
55+
@export(record=[ChatMessageRecord, ChatAttachmentRecord])
56+
def history(self) -> Iterator[ChatMessageRecord | ChatAttachmentRecord]:
57+
"""Yield MSN chat history messages.
58+
59+
Chat history artifacts can be found in:
60+
- ``$HOME/My Documents/My Received Files/MsnMsgr.txt``
61+
- ``$HOME/My Documents/My Received Files/$username$PassportID/History/*.xml``
62+
"""
63+
64+
for user_details, profile in self.installs:
65+
if not (hist_root := self.target.fs.path(user_details.user.home).joinpath(self.HIST_PATH)).exists():
66+
self.target.log.warning(
67+
"User %s does not have saved MSN chat history: directory %s does not exist",
68+
user_details.user.name,
69+
hist_root,
70+
)
71+
continue
72+
73+
hist_dir = None
74+
for item in hist_root.iterdir():
75+
if item.is_dir() and (hist_dir := item.name).endswith(profile.name):
76+
for hist_file in hist_root.joinpath(hist_dir).joinpath("History").glob("*.xml"):
77+
try:
78+
xml = ET.fromstring(hist_file.read_text())
79+
except Exception as e:
80+
self.target.log.warning("XML file %s is malformed: %s", hist_file, e)
81+
continue
82+
83+
for entry in xml:
84+
common = {
85+
"ts": entry.attrib.get("DateTime", 0),
86+
"client": self.__namespace__,
87+
"account": profile.name,
88+
"sender": entry.find(".//From/User").get("FriendlyName"),
89+
"_user": user_details.user,
90+
"_target": self.target,
91+
}
92+
93+
if entry.tag == "Message":
94+
yield ChatMessageRecord(
95+
**common,
96+
recipient=entry.find(".//To/User").get("FriendlyName"),
97+
message=entry.find(".//Text").text,
98+
)
99+
100+
elif entry.tag in ["Invitation", "InvitationResponse"]:
101+
if (file := entry.find(".//File")) is not None:
102+
yield ChatAttachmentRecord(
103+
**common,
104+
recipient=None, # unknown with Invitations
105+
attachment=file.text,
106+
description=entry.find(".//Text").text,
107+
)
108+
109+
110+
def convert_email(string: str) -> int:
111+
"""Convert MSN email address to 10 digit Passport ID."""
112+
num = 0
113+
for char in string.lower():
114+
num = num * 101 + ord(char)
115+
num -= (num // 4294967296) * 4294967296
116+
return num

dissect/target/plugins/os/windows/_os.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,10 @@ def users(self) -> Iterator[WindowsUserRecord]:
311311
home = profile_image_path.value
312312
name = home.split("\\")[-1]
313313

314+
# Windows XP uses %variables% in home paths
315+
if "%" in home:
316+
home = self.target.resolve(home)
317+
314318
yield WindowsUserRecord(
315319
sid=subkey.name,
316320
name=name,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:3ed4ca21729fcb8ed80ef647d8773e3bd01a682949a29d4beaf400c5a93e676a
3+
size 14543

tests/plugins/apps/chat/__init__.py

Whitespace-only changes.
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
from datetime import datetime, timezone
2+
3+
from dissect.target import Target
4+
from dissect.target.filesystem import VirtualFilesystem
5+
from dissect.target.plugins.apps.chat.msn import MSNPlugin, convert_email
6+
from tests._utils import absolute_path
7+
8+
9+
def test_msn(target_win_users: Target, fs_win: VirtualFilesystem) -> None:
10+
"""test if we parse MSN Chat messages on Windows XP correctly."""
11+
12+
morpheus_id = convert_email("morpheus@matrix.internal")
13+
neo_id = convert_email("neo@matrix.internal")
14+
15+
assert morpheus_id == 2450688751
16+
assert neo_id == 4092013818
17+
18+
fs_win.makedirs(f"Users/John/Application Data/Microsoft/MSN Messenger/{morpheus_id}")
19+
fs_win.map_file(
20+
f"Users/John/My Documents/My Received Files/morpheus{morpheus_id}/History/neo{neo_id}.xml",
21+
absolute_path("_data/plugins/apps/chat/msn/history.xml"),
22+
)
23+
24+
target_win_users.add_plugin(MSNPlugin)
25+
assert len(target_win_users.msn.installs) == 1
26+
27+
results = list(target_win_users.msn.history())
28+
assert len(results) == 35
29+
30+
assert results[0].username == "John"
31+
assert results[0].hostname is None
32+
33+
assert results[0].ts == datetime(2025, 4, 1, 13, 37, 0, tzinfo=timezone.utc)
34+
assert results[0].client == "msn"
35+
assert results[0].account == str(morpheus_id)
36+
assert results[0].sender == "morpheus@matrix.internal"
37+
assert results[0].recipient == "neo@matrix.internal"
38+
39+
assert [(r.sender.replace("@matrix.internal", ""), r.message) for r in results] == [
40+
(
41+
"morpheus",
42+
"At last.",
43+
),
44+
(
45+
"morpheus",
46+
"Welcome, Neo. As you no doubt have guessed, I am Morpheus.",
47+
),
48+
(
49+
"neo",
50+
"It's an honor.",
51+
),
52+
(
53+
"morpheus",
54+
"No, the honor is mine. Please. Come. Sit.",
55+
),
56+
(
57+
"morpheus",
58+
"I imagine, right now, you must be feeling a bit like Alice, tumbling down the rabbit hole?",
59+
),
60+
(
61+
"neo",
62+
"You could say that.",
63+
),
64+
(
65+
"morpheus",
66+
"I can see it in your eyes. You have the look of a man who accepts "
67+
"what he sees because he is expecting to wake up.",
68+
),
69+
(
70+
"morpheus",
71+
"Ironically, this is not far from the truth. But I'm getting ahead of "
72+
"myself. Can you tell me, Neo, why are you here?",
73+
),
74+
(
75+
"neo",
76+
"You're Morpheus. You're a legend. Most hackers would die to meet you.",
77+
),
78+
(
79+
"morpheus",
80+
"Yes. Thank you. But I think we both know there's more to it than that. Do you believe in fate, Neo?",
81+
),
82+
(
83+
"neo",
84+
"No.",
85+
),
86+
(
87+
"morpheus",
88+
"Why not?",
89+
),
90+
(
91+
"neo",
92+
"Because I don't like the idea that I'm not in control of my life.",
93+
),
94+
(
95+
"morpheus",
96+
"I know exactly what you mean.",
97+
),
98+
(
99+
"morpheus",
100+
"Let me tell you why you are here. You have come because you know something.",
101+
),
102+
(
103+
"morpheus",
104+
"What you know you can't explain but you feel it.",
105+
),
106+
(
107+
"morpheus",
108+
"You've felt it your whole life, felt that something is wrong with the world.",
109+
),
110+
(
111+
"morpheus",
112+
"You don't know what, but it's there like a splinter in your mind, "
113+
"driving you mad. It is this feeling that brought you to me.",
114+
),
115+
(
116+
"morpheus",
117+
"Do you know what I'm talking about?",
118+
),
119+
(
120+
"neo",
121+
"The Matrix?",
122+
),
123+
(
124+
"morpheus",
125+
"Do you want to know what it is?",
126+
),
127+
(
128+
"morpheus",
129+
"The Matrix is everywhere, it's all around us, here even in this room.",
130+
),
131+
(
132+
"morpheus",
133+
"You can see it out your window or on your television. You feel it "
134+
"when you go to work, or go to church or pay your taxes.",
135+
),
136+
(
137+
"morpheus",
138+
"It is the world that has been pulled over your eyes to blind you from the truth.",
139+
),
140+
(
141+
"neo",
142+
"What truth?",
143+
),
144+
(
145+
"morpheus",
146+
"That you are a slave, Neo. Like everyone else, you were born into "
147+
"bondage, kept inside a prison that you cannot smell, taste, or touch.",
148+
),
149+
(
150+
"morpheus",
151+
"A prison for your mind.",
152+
),
153+
(
154+
"morpheus",
155+
"Unfortunately, no one can be told what the Matrix is.",
156+
),
157+
(
158+
"morpheus",
159+
"You have to see it for yourself.",
160+
),
161+
(
162+
"morpheus",
163+
"This is your last chance. After this, there is no going back.",
164+
),
165+
(
166+
"morpheus",
167+
"You take the blue pill and the story ends.",
168+
),
169+
(
170+
"morpheus",
171+
"You wake in your bed and you believe whatever you want to believe.",
172+
),
173+
(
174+
"morpheus",
175+
"You take the red pill and you stay in Wonderland and I show you how deep the rabbit-hole goes.",
176+
),
177+
(
178+
"morpheus",
179+
"Remember that all I am offering is the truth. Nothing more.",
180+
),
181+
(
182+
"morpheus",
183+
"Follow me.",
184+
),
185+
]

0 commit comments

Comments
 (0)