本文首先讲解了 .NET 如何读取 COM 结构化存储的文件,其次在查找 Windows 7-10 便笺数据存储的位置,然后对其进行相对应的解析。文章中所涉及的代码均可在公网中找到。由于程序的满意程度并未达到个人预期,因此程序也就不提供了,但在文末有相对应的 Github。
0x00 前言
从 Windows 7 开始,便笺已经内置在 Windows 系统中。个人平时也喜欢使用系统自带的便笺进行一些计划及命令信息等记录,因此觉得有必要对此进行读取。并且,如果存在使用便笺的情况,那么所记录的东西通常是一些较为有趣的内容。因此在本文中,将研究如何读取存储文件中的内容。其实我在查找资料的时候,发现这部分内容(也就是本文内容)已经有人搞出来了。
本文所涉及的环境:
Windows 10 Version 1803
Windows 7
Microsoft Visual Studio Professional 2019
0x01 关于 .NET 的 COM 结构化存储
关于 COM结构化存储 ,可以直接查询维基百科进行了解。
该章节以读取 Thumbs.db
文件举例。Windows 生成的 Thumbs.db 文件是用来存储所有的缩略图,用于显示目录中的图形文件。它很可能以 "db "结尾,但它不是一个标准的数据库文件 -- 它是结构化存储。
1.1、PInvoke the easy way
我们可以通过 StgIsStorageFile
来判断文件是否为结构化存储。
[DllImport("ole32.dll")]
static extern int StgIsStorageFile([MarshalAs(UnmanagedType.LPWStr)] string pwcsName);
static void Main(string[] args)
{
string file = @"Thumbs.db";
int result = StgIsStorageFile(file);
if (result == 0)
{
Console.WriteLine("[*] {0} Is Storage File", file);
}
}
1.2、Open the file
在”使用”结构化存储文件时,需要做的第一件事就是打开它。我们可以使用 StgOpenStorage
完成这一部分内容。该函数原型如下:
HRESULT StgOpenStorage(
const WCHAR *pwcsName, // 指向以NULL结尾的 Unicode字符串文件的路径的指针,该文件包含要打开的存储对象。
IStorage *pstgPriority, // 指向IStorage接口的指针 ,应该为NULL。
DWORD grfMode, // 指定用于打开存储对象的访问模式。
SNB snbExclude, // 如果不为NULL,则在打开存储对象时,指向存储中要排除的元素块的指针。
DWORD reserved, // 表示保留供将来使用;必须为零。
IStorage **ppstgOpen // 指向 IStorage *指针变量的指针,该变量接收指向打开的存储器的接口指针。
);
其定义可在 PINVOKE.NET
找到。创建一个 ole32.class
,内容如下:
internal class ole32
{
[DllImport("ole32.dll")]
public static extern int StgIsStorageFile([MarshalAs(UnmanagedType.LPWStr)] string pwcsName);
[DllImport("ole32.dll")]
public static extern int StgOpenStorage([MarshalAs(UnmanagedType.LPWStr)] string pwcsName,
IStorage pstgPriority, STGM grfMode, IntPtr snbExclude, uint reserved, out IStorage ppstgOpen);
[Flags]
public enum STGM : int
{
/// <summary>
/// http://maul-esel.github.io/COM-Classes/master/STGM
/// </summary>
// Access
READ = 0x00000000, // 指示该对象是只读的,这意味着无法进行修改。
WRITE = 0x00000001, // 使您能够保存对对象的更改,但不允许访问其数据。
READWRITE = 0x00000002, // 允许访问和修改对象数据。
// Sharing
SHARE_DENY_NONE = 0x00000040, // 指定不拒绝随后打开对象的读取或写入访问。
SHARE_DENY_READ = 0x00000030, // 防止其他人随后以READ模式打开对象。
SHARE_DENY_WRITE = 0x00000020, // 防止其他人随后打开该对象以进行WRITE或READWRITE访问。
SHARE_EXCLUSIVE = 0x00000010, // 防止其他人随后以任何模式打开对象。
PRIORITY = 0x00040000, // 打开对最近提交的版本具有独占访问权的存储对象。
// Creation
CREATE = 0x00001000, // 指示在新对象替换之前,应删除现有存储对象或流。
CONVERT = 0x00020000, // 在保留名为“ Contents”的流中的现有数据的同时创建新对象。
FAILIFTHERE = 0x00000000, // 如果存在具有指定名称的现有对象,则使创建操作失败。
// Transactioning
DIRECT = 0x00000000, // 指示在直接模式下,对存储或流元素的每次更改都会在发生更改时写入。
TRANSACTED = 0x00010000, // 指示在事务处理模式下,仅当调用显式提交操作时,更改才会被缓冲并写入。
// Transactioning performance
NOSCRATCH = 0x00100000, // 指示在事务处理模式下,通常使用临时暂存文件来保存修改,直到调用 Commit方法为止。
NOSNAPSHOT = 0x00200000, // 当打开具有TRANSACTED且没有SHARE_EXCLUSIVE或SHARE_DENY_WRITE的存储对象时,将使用此标志。
// Direct SWMR and Simple
SIMPLE = 0x08000000, // 在有限但经常使用的情况下提供复合文件的更快实现。
DIRECT_SWMR = 0x00400000, // 支持单写,多读文件操作的直接模式。
// Delete On Release
DELETEONRELEASE = 0x04000000, // 指示在释放根存储对象时将自动销毁基础文件。
}
[ComImport]
[Guid("0000000b-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IStorage
{
void CreateStream(
/* [string][in] */ string pwcsName,
/* [in] */ uint grfMode,
/* [in] */ uint reserved1,
/* [in] */ uint reserved2,
/* [out] */ out IStream ppstm);
void OpenStream(
/* [string][in] */ string pwcsName,
/* [unique][in] */ IntPtr reserved1,
/* [in] */ uint grfMode,
/* [in] */ uint reserved2,
/* [out] */ out IStream ppstm);
void CreateStorage(
/* [string][in] */ string pwcsName,
/* [in] */ uint grfMode,
/* [in] */ uint reserved1,
/* [in] */ uint reserved2,
/* [out] */ out IStorage ppstg);
void OpenStorage(
/* [string][unique][in] */ string pwcsName,
/* [unique][in] */ IStorage pstgPriority,
/* [in] */ uint grfMode,
/* [unique][in] */ IntPtr snbExclude,
/* [in] */ uint reserved,
/* [out] */ out IStorage ppstg);
void CopyTo(
/* [in] */ uint ciidExclude,
/* [size_is][unique][in] */ [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] Guid[] rgiidExclude,
/* [unique][in] */ IntPtr snbExclude,
/* [unique][in] */ IStorage pstgDest);
void MoveElementTo(
/* [string][in] */ string pwcsName,
/* [unique][in] */ IStorage pstgDest,
/* [string][in] */ string pwcsNewName,
/* [in] */ uint grfFlags);
void Commit(
/* [in] */ uint grfCommitFlags);
void Revert();
void EnumElements(
/* [in] */ uint reserved1,
/* [size_is][unique][in] */ IntPtr reserved2,
/* [in] */ uint reserved3,
/* [out] */ out IEnumSTATSTG ppenum);
void DestroyElement(
/* [string][in] */ string pwcsName);
void RenameElement(
/* [string][in] */ string pwcsOldName,
/* [string][in] */ string pwcsNewName);
void SetElementTimes(
/* [string][unique][in] */ string pwcsName,
/* [unique][in] */
System.Runtime.InteropServices.ComTypes.FILETIME pctime,
/* [unique][in] */
System.Runtime.InteropServices.ComTypes.FILETIME patime,
/* [unique][in] */
System.Runtime.InteropServices.ComTypes.FILETIME pmtime);
void SetClass(
/* [in] */ Guid clsid);
void SetStateBits(
/* [in] */ uint grfStateBits,
/* [in] */ uint grfMask);
void Stat(
/* [out] */ out
System.Runtime.InteropServices.
ComTypes.STATSTG pstatstg,
/* [in] */ uint grfStatFlag);
}
[ComImport]
[Guid("0000000d-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IEnumSTATSTG
{
// The user needs to allocate an STATSTG array whose size is celt.
[PreserveSig]
uint Next(
uint celt,
[MarshalAs(UnmanagedType.LPArray),
Out]
System.Runtime.InteropServices.
ComTypes.STATSTG[] rgelt,
out uint pceltFetched
);
void Skip(uint celt);
void Reset();
[return: MarshalAs(UnmanagedType.Interface)]
IEnumSTATSTG Clone();
}
}
1.3、Using IStorage
StgOpenStorage
函数返回一个 IStorage
接口,接下来定义一些功能来操作结构化存储文件。在大多数情况下,打开结构化存储文件后,我们要做的第一件事就是找出其中存储的内容。该过程分为两个步骤:
-
首先获取一个枚举器;
-
然后使用该枚举器中的
Next
函数检索每个存储元素的STATSTG
结构。
ole32.IEnumSTATSTG SSenum;
Is.EnumElements(0, IntPtr.Zero, 0, out SSenum);
接下来,我们可以使用循环逐步执行 STATSTG
结构。唯一有点麻烦的是:我们必须传递一个 STATSTG
结构数组,并说明每次使用 Next
时要检索的结构数。
var SSstruct = new System.Runtime.InteropServices.ComTypes.STATSTG[1];
Next 函数尝试返回所请求的数字,如果返回值小于或等于 0,则停止迭代。
uint NumReturned;
do
{
SSenum.Next(1, SSstruct, out NumReturned);
if (NumReturned != 0)
{
Console.WriteLine(SSstruct[0].pwcsName + " - " + SSstruct[0].type.ToString());
}
} while (NumReturned > 0);
此示例中,每个结构化存储元素的名称都与元素的类型一起输出。如果使用 7z 打开 Thumbs.db
文件,则会看到与输出一致的内容,说明代码没有问题,如图:
Thumbs.db
所有类型都为 2,我们可以根据官方文档中提供的 C++ 定义为此多添加一个类型的定义:
enum STGTY:int
{
STGTY_STORAGE = 1, // 指示该存储元素是一个存储对象。
STGTY_STREAM = 2, // 指示存储元素是流对象。
STGTY_LOCKBYTES = 3, // 指示该存储元素是一个字节数组对象。
STGTY_PROPERTY = 4 // 指示存储元素是属性存储对象
};
可以看到结构化存储文件中存储了四种类型。
-
类型 1 的元素是存储对象。就像我们打开的根存储对象一样,它们可以包含完整的归档系统。可以将其视为根目录中子目录的类推。也就是说,我们要找出存储在结构化存储中的所有内容,必须要从顶级存储对象开始,遍历每个存储对象,并枚举它们包含的内容。
-
Thumbs.db
中的所有元素都是流对象,即可以以通常方式写入和读取的数据文件; -
类型 3是字节数组;
-
类型 4 是属性名称值对的属性集。
其实前置知识已经差不多了。了解结构化存储文件的文件类型的细化即可,流可直接读写;如果是存储对象,则需要再进一步枚举。
0x02 Windows 7
此时我们要做的第一件事就是找到保存便笺数据的文件。可借助 ProcessMonitor
达到此目的:启动 ProcessMonitor
,在设置好相应的过滤条件(主要是 ReadFile||WriteFile
条件),我们可以运行便笺,并保存新的笔记。
下图是 Windows 7 计算机上的便笺:
StickyNotes.snt
是 COM 结构化存储对象。结合上一章节的内容,我们对其进行解析。对其初步读取的结果为:
[*] StickyNotes.snt Is Storage File
[>] Version - 2
[>] Metafile - 2
[>] 5a997beb-e50b-11ea-9 - 1
[>] fc14ad33-e942-11ea-9 - 1
[>] b86489ac-ea11-11ea-9 - 1
我们使用 7z 打开 StickyNotes.snt
,可以发现 5a997beb-e50b-11ea-9
和其他两个文件夹。文件夹内容如下:
-
0 包含 RTF 格式的数据(这部分内容我们不用理它);
-
而 3 包含原始文本,也就是我们需要的数据。
手动打开 3 ,验证是否属实。
因此根据 STGTY
的不同,可以逐步枚举我们所需要的数据。接下来进行 套娃 操作:
使用 StgOpenStorage
打开 StickyNotes.snt
;
选择 STGTY
为 STGTY_STORAGE(1)
的对象;
再使用 OpenStorage
打开该对象,获取新的对象列表;
此时的对象类型全为 STGTY_STREAM(2)
,也就是流对象,可直接读取;
选择 pwcsName=3
的对象,该流对象存储着我们所需要的数据;
直接读取流进行输出即可。
修改程序,跑一跑,验证思路是否存在问题。也就是这样:
可以看到,程序正常运行及正确输出。最后读取流文件就行了。但这里出现了一个新的问题,就是内容不全的情况。
将该程序实际应用读取,最后呈现的结果为:
可以明显的看出,读取的结果是不完全的,这个原因我并没有找到,因为 StickyNotes.snt
文件中也没有更多的信息。
0x03 Windows 10
讲完 Windows 7,我们来看看 Windows 10 的内容。一样的方法,找到相应的文件。
可以发现,我们所记录的东西是保存在 plum.sqlite
中。
使用 DBBrowserforSQLite
打开该文件,查找我们所查找的数据(下面的数据库是更改过后的,因此与上图多了两个便笺),在 Note
表的 Text
字段中找到相应内容。
对于 C# 来说,读取 SQLite
很简单,只需要引入 System.Data.SQLite
库即可(在实现时,扣取了 SharpWeb
关于 SQLite 的库进行查询,但在对 plum.sqlite
查询时不起作用,原因未知)。这部分代码如下:
private static List<string> RunQuery(string dbPath)
{
var list = new List<string>();
using (var connection = new SQLiteConnection("Data Source=" + dbPath))
{
connection.Open();
var command = connection.CreateCommand();
command.CommandText = @" SELECT text FROM note; ";
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
var text = reader.GetString(0);
list.Add(text);
}
}
}
return list;
}
static void Main(string[] args)
{
string path = "plum.sqlite";
if (!File.Exists(path))
{
Console.WriteLine("[*] StickNotes SQLite DB not found!");
}
try
{
var results = RunQuery(path);
foreach (var result in results)
{
Console.WriteLine(result);
}
}
catch (Exception ex)
{
Console.WriteLine(" [!] Exception occured reading StickyNotes DB!");
Console.WriteLine(" [!] Exception: {0}", ex.Message);
}
}
将该程序实际应用读取,最后呈现的结果为:
Windows 10 未出现读取不全的情况,因为所有内容都存放在数据库中。但是程序怎么减少依赖,还是个问题。
0x04 参考
https://www.developerfusion.com/article/84406/com-structured-storage-from-net/
https://blog.csdn.net/jh_zzz/article/details/1515657
在 C# 中读取复合文档
https://windows10.pro/sticky-notes-using-tutorials/
https://blog.two06.info/Reading-Windows-Sticky-Notes/
https://github.com/two06/SharpStick
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论