A Forensic Gold Mine III: Forensic Analysis of the Microsoft Teams Desktop Client

As part of my master’s thesis at Abertay University, I’d spent most of the past three months digging through the artefacts generated by Microsoft Teams Desktop Client throughout the application usage and analysing how these could be used in a forensic investigation. My research showed that Microsoft Teams stores an abundance of information, both metadata and user-generated artefacts, that can prove extremely valuable. As my thesis turned out quite technical and is still in the publication process, this post should provide a first overview of my findings. I will also introduce you to my brand-new Autopsy parser for Microsoft Teams that allows extracting communication artefacts, such as messages, contacts and call logs programmatically.

Microsoft Teams’ Directory Structure

Like most instant messaging and communication applications these days, Microsoft Teams is based on the Electron Framework. For those who are not familiar with Electron, it is basically a framework that combines the Chromium Content Framework (a very simplified Chromium Browser) and Node.js to allow developers to build cross-platform applications using web languages like JavaScript, HTML and CSS. I, therefore, wasn’t surprised to see that the directory structures were similar to those Chromium-Esque browsers, such as Google Chrome, and other Electron-based communication and collaboration tools, like Slack, Twist, Discord or Riot.im.

AppData Local Installation Directory

The most common installation method 1 for Microsoft Teams is probably the one-click Squirrel-Framework based installer retrievable from https://www.microsoft.com/en-ww/microsoft-teams/download-app that installs all the application files to the user’s C:\Users\%USERNAME%\AppData\Local\Microsoft\Teams directory. The benefit of installing to the user profile rather than the global program folder is that this does not require the user to have administrative privileges to perform the installation.

If the client is installed to the user profile, the Teams directory looks similar to this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
C:\Users\%USERNAME%\AppData\Local\Microsoft\Teams
β”‚   πŸ“ app.ico
β”‚   πŸ“ Resources.pri
β”‚   πŸ“ setup.json
β”‚   πŸ“ SquirrelSetup.log
β”‚   πŸ“ Update.exe
β”‚   πŸ“ Update.VisualElementsManifest.xml
β”œβ”€β”€β”€πŸ“ current
β”œβ”€β”€β”€πŸ“ packages
β””β”€β”€β”€πŸ“ previous

As the names of the files and folders might not be totally obvious, here is my brief description of the functionality and usage of each of the files:

  • The current directory contains the extracted program files of the currently installed copy of Teams
  • The packages directory contains the nugget package (basically a compressed copy of the program files with some metadata) that gets installed by the Squirrel Framework
  • The previous directory contains the program files of the previously installed copy of Teams. The Squirrel framework keeps these as a backup to rollback in case the update process fails. This directory should be empty if Teams is installed for the first time.
  • The app.ico is just an icon for Microsoft Teams.
  • The Ressources.pri appears to be a Package Resource Index File generated by Visual Studio.
  • The setup.json contains a single parameter referencing a Microsoft Teams executable.
  • The SquirrelSetup.log is an append log that keeps track of Squirrel’s check for newer versions, successful installations and updates. This file could be used for timeline analysis to track and correlate starts of the application with usage activities.
  • The Update.exe is the executable that the Microsoft Teams shortcut in the start menu and on the desktop link to. This executable is part of the Squirrel framework and is responsible for installing and updating the application components at launch. Ideally, this happens transparently to the user. Once the Update.exe has performed its checks, it launches the actual Teams.exe located within the current subdirectory.
  • The Update.VisualElementsManifest.xml is an XML file that contains a few references to logos of the application.

AppData Roaming Usage Artefacts

The actual juicy artefacts, the user-generated content, is distributed across various files within the C:\Users\%USERNAME%\AppData\Roaming\Microsoft\Teams directory. The following figure shows the different directories and files that can commonly be found within the directory. Based on my research, however, it is worth noting that the number and type of the artefacts may vary depending on the version, the tenant type and application usage.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
C:\Users\%USERNAME%\AppData\Roaming\Microsoft\Teams
β”‚   πŸ“ Cookies
β”‚   πŸ“ desktop-config.json
β”‚   πŸ“ installTime.txt
β”‚   πŸ“ logs.txt
β”‚   πŸ“ Network Persistent State
β”‚   πŸ“ preauth.json
β”‚   πŸ“ Preferences
β”‚   πŸ“ QuotaManager
β”‚   πŸ“ settings.json
β”‚   πŸ“ SquirrelTelemetry.log
β”‚   πŸ“ storage.json
β”‚   πŸ“ teams_install_session.json
β”‚   πŸ“ TransportSecurity
β”‚   πŸ“ update-telemetry-config.json
β”œβ”€β”€β”€πŸ“ ai_models
β”œβ”€β”€β”€πŸ“ blob_storage
β”œβ”€β”€β”€πŸ“ Cache
β”œβ”€β”€β”€πŸ“ Code Cache
β”œβ”€β”€β”€πŸ“ CS_skylib
β”œβ”€β”€β”€πŸ“ databases
β”œβ”€β”€β”€πŸ“ dictionaries
β”œβ”€β”€β”€πŸ“ GPUCache
β”œβ”€β”€β”€πŸ“ images
β”œβ”€β”€β”€πŸ“ imagesMSA
β”œβ”€β”€β”€πŸ“ IndexedDB
β”œβ”€β”€β”€πŸ“ Local Storage
β”œβ”€β”€β”€πŸ“ logs
β”œβ”€β”€β”€πŸ“ media-stack
β”œβ”€β”€β”€πŸ“ Service Worker
β”œβ”€β”€β”€πŸ“ Session Storage
β”œβ”€β”€β”€πŸ“ SkypeRT
β””β”€β”€β”€πŸ“ tmp

Most notable are the following:

  • The Cookies file is an SQLite database, which stores session cookies. In the current version, the cookies are still unencrypted.
  • The desktop-config.json is a JSON file that contains configuration settings, such as the accountholders name, the public IP address and the account’s email addresses.
  • The installTime.txt is a plain-text file that contains the date when Microsoft Teams has been first installed on the client.
  • The logs.txt is continiously written append log which stores the communication with the middleware. This file typically contains a large number of timestamps including their timezone information. Based on this file it is possible to restore the application launch, shutdown and even the resising of the application window.
  • The storage.json is a JSON file that had an auth_tenant_users_map field with an entry for each user that had logged into teams. This included their full name, email address, and the public IP address from which they logged in last. Additionally, it has a property called auth_time, which is an EPOCH timestamp that indicates when a user is first successfully authenticated within Microsoft Teams.
  • The IndexedDB directory contains a LevelDB database that, among other entries, stores the messages, posts, comments, contacts, appointments and call logs.
  • The Session Storage directory contains another LevelDB database which stores the JWT tokens that are used for authenticating the client against the server.
  • The Local Storage directory contains the third LevelDB database utilised by Microsoft Teams. Among other entries, the local storage database keeps track of the file transfers and also contains message drafts.
  • The Cache directory is home to a Google DiskCache that contains (mostly) cached copies of user-generated artefacts, such as cached copies of the profile pictures but also thumbnails of files that had been exchanged.
  • The Network Persistent State file contains various URLs and network quality indicators. If the client was connected over wifi, it might also contain the access point’s SSID as a base64 encoded string.

Processing the Forensic Artefacts

There is quite a considerable number of files and directories to sift through - actually way more than I could cover in this post. Therefore, I will be focusing on the most relevant ones, namely the IndexedDB LevelDB database stored in the IndexeDB folder and the Chromium DiskCache located within the Cache folder. A full-blown discussion of all the files of forensic interest can be found within my thesis that is hopefully soon to be published.

IndexedDB Database

The https_teams.microsoft.com_0.indexeddb.leveldb LevelDB database within the IndexedDB folder is undoubtly the single most important data structure to investigate. Among others, the records of the IndexedDB contained all the sent messages, comments, posts, call logs, contacts and file transfers. IndexedDB is a standardised, low-level browser API for storing data in a JavaScript-based, object-oriented transactional database system. The actual database implementation of the IndexedDB API are realised using LevelDB databases for Electron applications. LevelDB itself is a fast key-value database written by Google that allows storing keys and values as byte-arrays. It follows the NoSQL paradigm and, therefore, does not support SQL queries nor relational models, which are commonly found in relational databases, such as SQLite. If you are curious about the inner workings of the LevelDB databases, I can highly recommend a blog post by Alex Caithness 2, which does a fantastic job explaining the high-level concepts behind this relatively novel database type.

Even if you are not interested in the nitty-gritty details of the LevelDB databases, you should know that LevelDB databases use an append log that contains data for storing the most recent transactions that can grow up to a size of 4 MB. Once the .log file has reached its maximum size, the records get deduplicated and compressed into one or more higher level ldb files. This detail is crucial as this step increases the entropy makes string searches highly ineffective for the higher level files.

Dumping the IndexedDB LevelDB Database

A major hurdle when trying to investigate IndexedDB LevelDB databases is the lack of robust, versatile and publicly available tools that allow navigating the databases and extracting its content. As part of my thesis, I’d tried various different tools, such as FastNoSQL, which only yielded a jumbled mess. Therefore, I’d developed a few Python scripts based on the excellent ccl_chrome_indexeddb Python library that allowed me to easily process the Microsoft Teams IndexedDB database and access records that were located in both the log and ldb files.

The first script is simply called dump_leveldb.py script that allows dumping a LevelDB’s records to a JSON file. The usage is extremely simple. All that has to be specified is the path to the LevelDB database and the path where the JSON file should be written.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 _____                        _            _
|  ___|__  _ __ ___ _ __  ___(_) ___ ___  (_)_ __ ___
| |_ / _ \| '__/ _ \ '_ \/ __| |/ __/ __| | | '_ ` _ \
|  _| (_) | | |  __/ | | \__ \ | (__\__ \_| | | | | | |
|_|  \___/|_|  \___|_| |_|___/_|\___|___(_)_|_| |_| |_|

 ____                          _____           _
|  _ \ _   _ _ __ ___  _ __   |_   _|__   ___ | |
| | | | | | | '_ ` _ \| '_ \    | |/ _ \ / _ \| |
| |_| | |_| | | | | | | |_) |   | | (_) | (_) | |
|____/ \__,_|_| |_| |_| .__/    |_|\___/ \___/|_|
                      |_|

usage: dump_leveldb.py [-h] -f FILEPATH -o OUTPUTPATH

The dumped records all follow the same setup. They have:

  • A key which identifies the record.
  • A origin_file with the path to the file where the record was found. This could be the .log or one of the .ldb files.
  • A store that refers to the object store in which the record was located. Object stores can be compared to database tables in relational database.
  • A value that contains the actual data of a specific record.

Database Structure and Object Stores

Based on my analysis Microsoft Teams utilises at least the following databases and object stores. The relevant flag is my own estimation of whether or not an object store is of forensic interest. Please note that the number of databases and object stores may vary depending on the platform that is investigated. Furthermore, it might be possible that the names of the object stores might change during upcoming releases.

DatabaseObject StoreRelevant
Teams:TypingUsers:{uid}TypingUsersStoreFALSE
Teams:activity-manager:{uid}feed-itemsTRUE
Teams:activity-manager:{uid}internal-dataFALSE
Teams:app-definitions-manager:{uid}app-definition-mapsFALSE
Teams:app-definitions-manager:{uid}app-definitionsFALSE
Teams:app-policy-manager:{uid}app-policyFALSE
Teams:buddy-manager:{uid}buddylistTRUE
Teams:buddy-manager:{uid}buddy-internal-dataFALSE
TeamsπŸ“†{uid}calendarFALSE
TeamsπŸ“†{uid}calendar-internal-dataFALSE
TeamsπŸ“†{uid}connected-calendar-settingsFALSE
Teams:chat-installed-apps-manager:{uid}app-entitlementsFALSE
Teams:chat-installed-apps-manager:{uid}app-definitionsFALSE
Teams:contactssync-manager:{uid}rawContactsFALSE
Teams:contactssync-manager:{uid}contactssync-internal-dataTRUE
Teams:conversation-manager:{uid}conversationsTRUE
Teams:conversation-manager:{uid}conversations-internal-dataFALSE
Teams:files:{uid}p2p_filesFALSE
Teams:policies-manager:{uid}policiesFALSE
Teams:profiles:{uid}profilesTRUE
Teams:replychain-manager:{uid}replychainsTRUE
Teams:rosterparticipantmanager:{uid}rosterparticipantFALSE
Teams:search-history-manager:{uid}searchhistoryFALSE
Teams:settings:{uid}settingsFALSE
Teams:syncstate-manager:{uid}syncstatesFALSE
user-installed-apps-manager:{uid}user-app-entitlementsFALSE
user-installed-apps-manager:{uid}user-app-definitionsFALSE
user-preferences-manager:{uid}user-preferencesTRUE
teams-service-worker-telemetry-v2LogEventsFALSE
teams-service-worker-v2SwStateFALSE

Database Records of Interest

Let’s have a look at a couple of dumped database records to understand what data could be recovered. If you want to follow along, you can find a copy of the IndexedDB database on my GitHub and simply use the previously discussed dump_leveldb.py script for dumping the database.

A major concern during a forensic investigation will always be to retrace communication a suspect was involved in. Luckily for us, Microsoft Teams keeps a copy of the exchanged text messages, comments and posts within replychains object store. A record of a message that has previously been exchanged might look like something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
{
    "key": "b'\\x00\\x05\\x02\\x01\\x04\\x02\\x01[\\x001\\x009\\x00:\\x005\\x004\\x00d\\x00d\\x002\\x007\\x00a\\x007\\x00-\\x00f\\x00b\\x00b\\x000\\x00-\\x004\\x00b\\x00f\\x000\\x00-\\x008\\x002\\x000\\x008\\x00-\\x00a\\x004\\x00b\\x003\\x001\\x00a\\x005\\x007\\x008\\x00a\\x003\\x00f\\x00_\\x00e\\x006\\x002\\x00b\\x007\\x00c\\x00e\\x00c\\x00-\\x007\\x003\\x007\\x009\\x00-\\x004\\x00d\\x006\\x00f\\x00-\\x00a\\x00e\\x00d\\x007\\x00-\\x002\\x004\\x00b\\x004\\x008\\x00b\\x00e\\x006\\x008\\x00a\\x007\\x004\\x00@\\x00u\\x00n\\x00q\\x00.\\x00g\\x00b\\x00l\\x00.\\x00s\\x00p\\x00a\\x00c\\x00e\\x00s\\x01\\r\\x001\\x006\\x002\\x002\\x003\\x006\\x008\\x000\\x009\\x002\\x009\\x001\\x006\\x01\\xd6D\\x00\\x00\\x00\\x00\\x00'",
    "origin_file": "C:\\dev\\forensicsim\\testdata\\John Doe\\IndexedDB\\https_teams.microsoft.com_0.indexeddb.leveldb\\000116.ldb",
    "store": "replychains",
    "value": {
        "conversationId": "19:54dd27a7-fbb0-4bf0-8208-a4b31a578a3f_e62b7cec-7379-4d6f-aed7-24b48be68a74@unq.gbl.spaces",
        "isInsideChat": "true",
        "latestDeliveryTime": "0001622368092916",
        "messages": {
            "4235357803446472000,8:orgid:e62b7cec-7379-4d6f-aed7-24b48be68a74": {
                "_callRecording": null,
                "_callTranscript": null,
                "_conversationIdMessageIdUnion": "19:54dd27a7-fbb0-4bf0-8208-a4b31a578a3f_e62b7cec-7379-4d6f-aed7-24b48be68a74@unq.gbl.spaces_1622368092916",
                "_meetingObjects": null,
                "_pinState": {
                    "isPinned": false
                },
                "attachments": [],
                "cachedDeduplicationKey": "8:orgid:e62b7cec-7379-4d6f-aed7-24b48be68a744235357803446472000",
                "cachedOriginalArrivalTime": "2021-05-30T09:48:12.916Z",
                "cachedOriginalArrivalTimeUtc": 1622368092916.0,
                "callDuration": 0,
                "callParticipantsCount": 0,
                "callParticipantsMris": [],
                "clientArrivalTime": "2021-05-30T09:48:13.048Z",
                "clientmessageid": "4235357803446472000",
                "composetime": "2021-05-30T09:48:12.916Z",
                "content": "<div>To Sherlock Holmes she is always the woman.</div>",
                "contenttype": "text",
                "conversationId": "19:54dd27a7-fbb0-4bf0-8208-a4b31a578a3f_e62b7cec-7379-4d6f-aed7-24b48be68a74@unq.gbl.spaces",
                "conversationLink": "https://notifications.skype.net/v1/users/ME/conversations/19:54dd27a7-fbb0-4bf0-8208-a4b31a578a3f_e62b7cec-7379-4d6f-aed7-24b48be68a74@unq.gbl.spaces",
                "createdTime": 1622368092916.0,
                "creator": "8:orgid:e62b7cec-7379-4d6f-aed7-24b48be68a74",
                "creatorProfile": {
                    "displayName": "Jane Doe",
                    "givenName": "Jane Doe",
                    "mri": "8:orgid:e62b7cec-7379-4d6f-aed7-24b48be68a74",
                    "objectId": "e62b7cec-7379-4d6f-aed7-24b48be68a74",
                    "type": "person",
                    "userPrincipalName": "JaneDoe_forensics.im#EXT#@Forensicsim.onmicrosoft.com"
                },
                "from": "https://notifications.skype.net/v1/users/ME/contacts/8:orgid:e62b7cec-7379-4d6f-aed7-24b48be68a74",
                "hyperLinks": [],
                "id": "1622368092916",
                "idUnion": "4235357803446472000",
                "imdisplayname": "Jane Doe",
                "inputExtensionAttachments": [],
                "isActionExecuteUpdate": false,
                "isForceDelete": false,
                "isFromMe": false,
                "isPlainTextConvertedToHtml": true,
                "isRenderContentWithGiphyDisplayEnabled": true,
                "isRichContentProcessed": true,
                "isRichMessagePropertiesProcessed": true,
                "isSanitized": true,
                "isSfBGroupConversation": false,
                "mentions": [],
                "messageContentContainsImage": null,
                "messageContentContainsVideo": false,
                "messageKind": "skypeMessageLocal",
                "messageLayoutType": 0,
                "messageStorageState": 1,
                "messagetype": "RichText/Html",
                "notificationLevel": 1,
                "originalarrivaltime": "2021-05-30T09:48:12.916Z",
                "parentMessageId": "1622368092916",
                "properties": {
                    "importance": 0,
                    "subject": ""
                },
                "renderContent": "<div>To Sherlock Holmes she is always the woman.</div>",
                "replyChainLatestDeliveryTime": 1622368092916.0,
                "sequenceId": 2,
                "source": 2,
                "state": 2,
                "trimmedMessageContent": "To Sherlock Holmes she is always the woman.",
                "type": "Message",
                "userHasStarred": false,
                "version": "1622368092916",
                "versionNumber": 1622368092916.0
            }
        },
        "parentMessageId": "1622368092916"
    }
},

As you can see, even a simple text message has quite a few properties. From a forensic perspective, the most interesting properties are the following:

  • The renderContent contains the message body of the text message.
  • The creator holds the user ID of the author of the message.
  • The creatorProfile contains a dictionary with details on the author including first name, last name and UPN.
  • The conversationId identifies the thread on which a message has been sent.
  • The composetime stores the timestamp when the message was originally authored.
  • The isFromMe flag indicates the direction of a message, whether it was outgoing or incoming.

Files that were sent between users can be tracked based on the dictionary under attachments. The following figure, for example, shows the record of a file called bagpipes.mp4, which had been exchanged between the two users.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
"attachments": [
    {
        "fileServerRelativeUrl": "/personal/janedoe_forensics_im_ext__forensicsim_onmicrosoft_com/Documents/Microsoft Teams Chat Files/bagpipes.mp4",
        "objectId": "de543145-9339-43c1-ba16-ed715a1cd21b",
        "objectUrl": "https://forensicsim-my.sharepoint.com/personal/janedoe_forensics_im_ext__forensicsim_onmicrosoft_com/Documents/Microsoft Teams Chat Files/bagpipes.mp4",
        "providerData": "{\"code\":null,\"type\":0}",
        "serverRelativeUrl": "",
        "serviceName": "p2p",
        "siteUrl": "https://forensicsim-my.sharepoint.com/personal/janedoe_forensics_im_ext__forensicsim_onmicrosoft_com",
        "sourceOfFile": 3,
        "state": "active",
        "thumbnail": {},
        "title": "bagpipes.mp4",
        "type": "mp4"
    }
],

Again, a couple of properties are especially important.

  • The objectUrl contains the remote server address of the resource. Though, it’s worth noting that files are typically not publicly accessible and valid user credentials with appropriate permissions are required to access the file stored on SharePoint.
  • The title stores the filename of the file that has been transfered.
  • The type refers to the filetype of the exchange file.

Similarly to messages, can contacts also be found within the LevelDB database. Unlike messages, these, are stored in a different object store called people. Their structure look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
}
    "key": "b'\\x00\\x05\\x07\\x01\\x01,\\x008\\x00:\\x00o\\x00r\\x00g\\x00i\\x00d\\x00:\\x00e\\x006\\x002\\x00b\\x007\\x00c\\x00e\\x00c\\x00-\\x007\\x003\\x007\\x009\\x00-\\x004\\x00d\\x006\\x00f\\x00-\\x00a\\x00e\\x00d\\x007\\x00-\\x002\\x004\\x00b\\x004\\x008\\x00b\\x00e\\x006\\x008\\x00a\\x007\\x004\\x012\\xc7\\x02\\x00\\x00\\x00\\x00'",
    "origin_file": "C:\\dev\\forensicsim\\testdata\\John Doe\\IndexedDB\\https_teams.microsoft.com_0.indexeddb.leveldb\\000116.ldb",
    "store": "people",
    "value": {
        "$$accessed": 1622467311066.0,
        "$$atMentions_LastAccessedDT": 1622467311065.0,
        "$$audioVideo_LastAccessedDT": 1622467311065.0,
        "$$firstname_lowercase": "jane doe",
        "$$fullname_lowercase": "jane doe",
        "$$lastname_lowercase": null,
        "$$p2p_LastAccessedDT": 1622467311065.0,
        "$$requestCount": 1,
        "$$subType": "ADUser",
        "$$updated": 1622367784683.0,
        "$$userSettingsUpdated": 1622467311064.0,
        "accountEnabled": true,
        "alias": "JaneDoe_forensics.im#EXT#",
        "description": "JaneDoe@forensics.im",
        "displayName": "Jane Doe",
        "email": "JaneDoe@forensics.im",
        "featureSettings": {
            "coExistenceMode": "Islands",
            "enableScheduleOwnerPermissions": false,
            "enableShiftPresence": false,
            "isPrivateChatEnabled": true
        },
        "givenName": "Jane Doe",
        "guestlessDisplayName": "Jane Doe",
        "isShortProfile": false,
        "isSipDisabled": true,
        "mail": "JaneDoe@forensics.im",
        "mri": "8:orgid:e62b7cec-7379-4d6f-aed7-24b48be68a74",
        "objectId": "e62b7cec-7379-4d6f-aed7-24b48be68a74",
        "objectType": "User",
        "phones": [],
        "responseSourceInformation": "AAD",
        "showInAddressList": false,
        "skypeTeamsInfo": {
            "isSkypeTeamsUser": true
        },
        "smtpAddresses": [
            "JaneDoe@forensics.im"
        ],
        "type": "person",
        "userPrincipalName": "JaneDoe_forensics.im#EXT#@Forensicsim.onmicrosoft.com",
        "userType": "Member"
    }
},

Of interest might be the following properties:

  • mri is the user ID of a specific contact. Can be used to map a contact to a text message.
  • userPrincipalName contains the UPN of the contact.
  • email unsurprisingly holds the email address associated with an account.
  • givenName full name of the contact. Includes both first and last name.

Textual messages are good, but how about call logs you may ask? Luckily for us, Microsoft Teams also keeps a record of these and precisely logs these within the replychains object store. A single record of an accepted, outgoing call looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
{
        "key": "b'\\x00\\x05\\x02\\x01\\x04\\x02\\x01\\x0b\\x004\\x008\\x00:\\x00c\\x00a\\x00l\\x00l\\x00l\\x00o\\x00g\\x00s\\x01\\r\\x001\\x006\\x002\\x002\\x003\\x007\\x002\\x004\\x008\\x007\\x006\\x007\\x007\\x018x\\x00\\x00\\x00\\x00\\x00'",
        "origin_file": "C:\\dev\\forensicsim\\testdata\\John Doe\\IndexedDB\\https_teams.microsoft.com_0.indexeddb.leveldb\\000116.ldb",
        "store": "replychains",
        "value": {
            "conversationId": "48:calllogs",
            "isInsideChat": "false",
            "latestDeliveryTime": "0001622372487677",
            "messages": {
                "1622372487292,8:orgid:54dd27a7-fbb0-4bf0-8208-a4b31a578a3f": {
                    "_callRecording": null,
                    "_callTranscript": null,
                    "_meetingObjects": null,
                    "_pinState": {
                        "isPinned": false
                    },
                    "cachedOriginalArrivalTime": "2021-05-30T11:01:27.6770000Z",
                    "cachedOriginalArrivalTimeUtc": 1622372487677.0,
                    "callDuration": 0,
                    "callParticipantsCount": 0,
                    "callParticipantsMris": [],
                    "clientArrivalTime": "2021-05-30T11:02:27.470Z",
                    "clientmessageid": "1622372487292",
                    "composetime": "2021-05-30T11:01:27.6770000Z",
                    "content": "<div>Call Log for Call e4dc8590-eca0-4979-92ec-9384afcd3f79</div>",
                    "contenttype": "text",
                    "conversationId": "48:calllogs",
                    "conversationLink": "https://uk.ng.msg.teams.microsoft.com/v1/users/ME/conversations/48:calllogs",
                    "createdTime": 1622372487677.0,
                    "creator": "8:orgid:54dd27a7-fbb0-4bf0-8208-a4b31a578a3f",
                    "from": "https://uk.ng.msg.teams.microsoft.com/v1/users/ME/contacts/8:orgid:54dd27a7-fbb0-4bf0-8208-a4b31a578a3f",
                    "id": "1622372487677",
                    "idUnion": "1622372487292",
                    "imdisplayname": "",
                    "isActionExecuteUpdate": false,
                    "isForceDelete": false,
                    "isFromMe": true,
                    "isPlainTextConvertedToHtml": true,
                    "isRichContentProcessed": false,
                    "isRichMessagePropertiesProcessed": false,
                    "isSanitized": true,
                    "isSfBGroupConversation": false,
                    "messageKind": "skypeMessageLocal",
                    "messageStorageState": 1,
                    "messagetype": "RichText/Html",
                    "notificationLevel": 1,
                    "originalarrivaltime": "2021-05-30T11:01:27.6770000Z",
                    "parentMessageId": "1622372487677",
                    "properties": {
                        "call-log": "{\"startTime\":\"2021-05-30T11:00:40.4001688Z\",\"connectTime\":\"2021-05-30T11:00:48.3233481Z\",\"endTime\":\"2021-05-30T11:01:27.2667135Z\",\"callDirection\":\"outgoing\",\"callType\":\"twoParty\",\"callState\":\"accepted\",\"userParticipantId\":\"a176b448-0c7c-4b67-ad1a-9b21414ec889\",\"originator\":\"8:orgid:54dd27a7-fbb0-4bf0-8208-a4b31a578a3f\",\"target\":\"8:orgid:e62b7cec-7379-4d6f-aed7-24b48be68a74\",\"originatorParticipant\":{\"id\":\"8:orgid:54dd27a7-fbb0-4bf0-8208-a4b31a578a3f\",\"type\":\"default\",\"displayName\":\"John Doe\"},\"targetParticipant\":{\"id\":\"8:orgid:e62b7cec-7379-4d6f-aed7-24b48be68a74\",\"type\":\"default\",\"displayName\":null},\"callId\":\"e4dc8590-eca0-4979-92ec-9384afcd3f79\",\"callAttributes\":null,\"forwardingInfo\":{\"acceptedBy\":{\"id\":\"8:orgid:e62b7cec-7379-4d6f-aed7-24b48be68a74\",\"type\":\"voicemail\",\"displayName\":null}},\"transferInfo\":null,\"participants\":null,\"participantList\":null,\"threadId\":null,\"sessionType\":\"default\",\"sharedCorrelationId\":\"abb9c356-f01c-4779-b187-50f798d764ac\",\"messageId\":null}",
                        "s2spartnername": "concore_gvc"
                    },
                    "replyChainLatestDeliveryTime": 1622372487677.0,
                    "sequenceId": 1,
                    "source": 1,
                    "state": 2,
                    "trimmedMessageContent": "Call Log for Call e4dc8590-eca0-4979-92ec-9384afcd3f79",
                    "type": "Message",
                    "userHasStarred": false,
                    "version": "1622372487677",
                    "versionNumber": 1622372487677.0
                }
            },
            "parentMessageId": "1622372487677"
        }
    },

All the interesting call log data, such as the start end end time of the call, the call direction, call type are conveniently stored as a JSON array accessible through the dictionary key call-log under properties.

Automated Parsing of the IndexedDB LevelDB Database within Autopsy Forensic Suite

If you don’t like fiddling with command-line tools and manually browsing through thousands of lines of JSON files, then I’ve got good news for you. As part of my thesis I’ve also been working on an Autopsy ingest module that automtically extracts all the relevant records and turns these into Blackboard artefacts. More information on the Autopsy parser can be found on my projects website under forensics.im or within the demo video on YouTube.

image

Microsoft Teams Autopsy Parser Module

Chromium DiskCache

Microsoft Teams uses DiskCache data structures for caching content various contents, such as application files, icons but also user generated content on the client side. The user-generated ones were all located within the Cache folder and could be inspected using ChromeCacheView which allowed convenient access to the records distributed across the cache files.

Among the files that got persisted in the cache are the profile pictures of the account holders, thumbnails for links with Twitter cards functionality and the previews of images that were exchanged. One aspect that stood out is that the thumbnails of the exchanged images all had the same file name called 1.jfif, which makes them fairly easy to identify. The following figure shows the thumbnail of an image that had been exchanged as part of a conversation that has occured as a direct message.

image

Microsoft Teams DiskCache viewed in ChromeCacheView

Conclusion

I thoroughly enjoyed my forensic investigation of Microsoft Teams and was quite astonished at just how much potential evidence could be recovered from Microsoft Teams’ Desktop Client. Even though this post could only scratch the surface of what I’d covered in my thesis, I still hope it provides enough information to get your own investigation started. In the future, I am also planning to inspect the files of the other Microsoft Teams desktop clients, such as the one for macOS and Linux. Once I come around to do that, I will update the post accordingly.


  1. Microsoft Teams can also be installed machine-wide if installed through the MSI file. In this case, the install locations would be C:\Program Files (x86)\Teams Installer or in C:\Program Files\Teams Installer depending on the architecture. More information on this deployment method can be found here https://docs.microsoft.com/en-us/microsoftteams/msi-deployment ↩︎

  2. Caithness, A. (2020, September 23). Hang on! That’s not SQLite! Chrome, Electron and LevelDB. https://www.cclsolutionsgroup.com//post/hang-on-thats-not-sqlite-chrome-electron-and-leveldb ↩︎

Posts in this series