Sticky Notes and OneNote: A new artifact for Velociraptor

Sticky Notes is one of those small Windows applications that sometimes turns out to be surprisingly valuable during forensic work. Over time, I’ve noticed that people tend to store all sorts of sensitive or useful information in there. Everything from passwords and quick reminders to various other notes. Because of that, it’s always one of the artifacts I make sure to check during an investigation.

Recently, this came up again when I needed to collect Sticky Notes from a machine as part of an investigation.

As usual, I started by going straight to the standard file locations I'd relied on in the past. These paths are well documented on Forensafe and are also referenced in the KapeFiles targets repository. Normally, this approach works without any issues.

This time, however, I immediately ran into a problem: there was simply no data in any of the expected locations. No StickyNotes.snt, no plum.sqlite. The directories were completely empty, which was a bit unexpected.

After digging a little deeper, I discovered that Microsoft has rolled out a newer version of Sticky Notes that is integrated with OneNote. With this updated version, the traditional storage paths no longer apply, which explains why the usual artifacts were missing.

So, with that in mind, this post explains where the new version stores its data and introduces a Velociraptor artifact I wrote to collect it.

Where Sticky Notes used to live

For context, the legacy old locations are as follows:

  • Microsoft Sticky Notes (Windows 7, 8, and 10 version 1511 and earlier)
    Path: C:\Users\%user%\AppData\Roaming\Microsoft\StickyNotes\
    File: StickyNotes.snt
  • Microsoft Sticky Notes (Windows 10 version 1607 and later, before the OneNote integration)
    Path: C:\Users\%user%\AppData\Local\Packages\Microsoft.MicrosoftStickyNotes*\LocalState\
    File: plum.sqlite*

If you are still seeing those paths and files, the system is running the old Sticky Notes application. If the folders are empty, the user is likely running the newer OneNote-based version.

What changed?

Microsoft introduced a new Sticky Notes app that runs on top of OneNote. From the Microsoft 365 Insider blog:

"The new Sticky Notes app is available to Current Channel (Preview) users running OneNote on Windows Version 2402 (Build 17328.20000) or later."

In practice, this means the notes are no longer stored in the old Roaming or Packages directories. Instead, they are written by the OneNote process to a different location. If your system is running a recent Current Channel (Preview) build with OneNote installed, you should expect the notes to reside in the location described below.

Finding the new location

I spun up a Windows 11 lab VM with the latest version of OneNote installed, to do some testing. I ran Procmon, and filtered on write operations from the onenote.exe process. My reasoning was that the notes have to land on disk somewhere, so that filter should reveal the new storage path.

Procmon monitoring onenote.exe write operations

The new base path is:

%UserProfile%\AppData\Local\Microsoft\OneNote\16.0\Memory

OneNote Sticky Notes database path in Procmon

Inside that folder you will find a SQLite3 database named notes.sdk_*.db. I opened it up in SQLite3 and checked the schema. The raw note content itself resides in a table called notes, in the value column. That column contains a nested and escaped JSON object, which includes the creation time, last modified time, and the text itself, among other things. Each row therefore represents a single note.

The Velociraptor artifact

I use Velociraptor for most of my DFIR work, both with their live agents aswell as for deadbox forensics. I therefore wrote an artifact for this new Sticky Notes update. The artifact discovers these these databases per user profile, reads the notes table, parses the nested escaped JSON from the value column, and outputs one row per note with both the creation and last-modified timestamps, and the full note content. This is way easier than having to pull the SQLite3 database yourself and parse the ugly nested json data with a manual SQL query.

Here is an example output:

Velociraptor artifact result

As you can see, the output includes the note ID, creation and last-modified times, and the full note content in a structured format. This makes it easy to filter in the notebook or export results for later analysis.

Final thoughts

If you are only collecting the old paths, you may miss data on systems running the newer OneNote-backed version of the application. The artifact below targets the new location only, so for full coverage you should continue collecting the old paths as well.

If you have any questions or feedback, feel free to reach out.

Velociraptor artifact

artifact.yaml
1name: Windows.Forensics.StickyNotes.Onenote 2author: "Victor Buch | victorbuch.dk" 3 4description: | 5 Reads the new OneNote-based Sticky Notes app SQLite databases and outputs each notes content along with its creation and last-modified timestamps 6 7reference: 8 - Sticky Notes and OneNote A new artifact for Velociraptor (https://victorbuch.dk/articles/sticky-notes-artifact) 9 - Microsoft 365 Insider blog Introducing the new Sticky Notes app on Windows (https://techcommunity.microsoft.com/blog/microsoft365insiderblog/introducing-the-new-sticky-notes-app-on-windows/4223819) 10 11 12parameters: 13 - name: StickyNotesGlob 14 default: "\AppData\Local\Microsoft\OneNote\16.0\Memory\notes.sdk*.db" 15 - name: userRegex 16 default: . 17 type: regex 18 19type: CLIENT 20 21sources: 22 - precondition: 23 SELECT OS FROM info() WHERE OS = 'windows' 24 25 query: | 26 LET sticky_notes_db = SELECT * 27 FROM foreach(row={ 28 SELECT Uid, 29 Name AS User, 30 expand(path=Directory) AS HomeDirectory 31 FROM Artifact.Windows.Sys.Users() 32 WHERE Name =~ userRegex 33 }, 34 query={ 35 SELECT User, 36 OSPath, 37 Mtime 38 FROM glob(globs=StickyNotesGlob, root=HomeDirectory) 39 }) 40 41 LET notes_rows = SELECT * 42 FROM foreach(row=sticky_notes_db, 43 query={ 44 SELECT key, 45 regex_replace(source=value, re='""', replace='"') AS clean_value, 46 OSPath, 47 Mtime, 48 User 49 FROM sqlite(file=OSPath, query="SELECT key, value FROM notes") 50 }) 51 52 LET notes_with_time = SELECT 53 key, 54 clean_value, 55 OSPath, 56 timestamp(epoch=parse_json(data=clean_value).createdAt) AS CreatedAt, 57 timestamp(epoch=parse_json(data=clean_value).documentModifiedAt) AS DocumentModifiedAt, 58 OSPath AS SourceFilePath, 59 Mtime, 60 User 61 FROM notes_rows 62 63 LET paragraphs = SELECT key, 64 CreatedAt, 65 DocumentModifiedAt, 66 _value.content.text AS ParagraphText, 67 OSPath AS SourceFilePath, 68 Mtime, 69 User 70 FROM foreach(row=notes_with_time, 71 query={ 72 SELECT key, 73 CreatedAt, 74 DocumentModifiedAt, 75 OSPath, 76 Mtime, 77 User, 78 _value 79 FROM items(item=parse_json(data=clean_value).document.content) 80 }) 81 82 SELECT key, 83 CreatedAt, 84 DocumentModifiedAt, 85 join(array=ParagraphText, sep=" ") AS FullText, 86 SourceFilePath, 87 Mtime, 88 User 89 FROM paragraphs 90 GROUP BY key, CreatedAt, DocumentModifiedAt, SourceFilePath, 91 Mtime, User 92