Skip to content

Commit 7b76faf

Browse files
authored
Add ESE (#4)
1 parent 064de01 commit 7b76faf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+12661
-3
lines changed

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
A Dissect module implementing parsers for various database formats, including:
44

5-
- Berkeley DB
6-
- SQLite3
5+
- Berkeley DB, used for example in older RPM databases
6+
- Microsofts Extensible Storage Engine (ESE), used for example in Active Directory, Exchange and Windows Update
7+
- SQLite3, commonly used by applications to store configuration data
78

89
For more information, please see [the documentation](https://docs.dissect.tools/en/latest/projects/dissect.database/index.html).
910

@@ -17,6 +18,20 @@ pip install dissect.database
1718

1819
This module is also automatically installed if you install the `dissect` package.
1920

21+
## Tools
22+
23+
### Impacket compatibility shim for secretsdump.py
24+
25+
Impacket does not ([yet](https://github.com/fortra/impacket/pull/1452)) have native support for `dissect.database`,
26+
so in the meantime a compatibility shim is provided. To use this shim, simply install `dissect.database` using the
27+
instructions above, and execute `secretsdump.py` like so:
28+
29+
```bash
30+
python -m dissect.database.ese.tools.impacket /path/to/impacket/examples/secretsdump.py -h
31+
```
32+
33+
Impacket `secretsdump.py` will now use `dissect.database` for parsing the `NTDS.dit` file, resulting in a significant performance improvement!
34+
2035
## Build and test instructions
2136

2237
This project uses `tox` to build source and wheel distributions. Run the following command from the root folder to build

dissect/database/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from __future__ import annotations
22

33
from dissect.database.bsd.db import DB
4+
from dissect.database.ese.ese import ESE
45
from dissect.database.exception import Error
56
from dissect.database.sqlite3.sqlite3 import SQLite3
67

78
__all__ = [
89
"DB",
10+
"ESE",
911
"Error",
1012
"SQLite3",
1113
]

dissect/database/ese/__init__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from __future__ import annotations
2+
3+
from dissect.database.ese.ese import ESE
4+
from dissect.database.ese.exception import (
5+
InvalidDatabase,
6+
KeyNotFoundError,
7+
NoNeighbourPageError,
8+
)
9+
from dissect.database.ese.index import Index
10+
from dissect.database.ese.page import Page
11+
from dissect.database.ese.record import Record
12+
from dissect.database.ese.table import Table
13+
14+
__all__ = [
15+
"ESE",
16+
"CompressedTaggedDataError",
17+
"Index",
18+
"InvalidDatabase",
19+
"KeyNotFoundError",
20+
"NoNeighbourPageError",
21+
"Page",
22+
"Record",
23+
"Table",
24+
]

dissect/database/ese/btree.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
from dissect.database.ese.exception import KeyNotFoundError, NoNeighbourPageError
6+
7+
if TYPE_CHECKING:
8+
from dissect.database.ese.ese import ESE
9+
from dissect.database.ese.page import Node, Page
10+
11+
12+
class BTree:
13+
"""A simple implementation for searching the ESE B+Trees.
14+
15+
This is a stateful interactive class that moves an internal cursor to a position within the BTree.
16+
17+
Args:
18+
db: An instance of :class:`~dissect.database.ese.ese.ESE`.
19+
page: The page to open the :class:`BTree` on.
20+
"""
21+
22+
def __init__(self, db: ESE, root: int | Page):
23+
self.db = db
24+
25+
if isinstance(root, int):
26+
page_num = root
27+
root = db.page(page_num)
28+
else:
29+
page_num = root.num
30+
31+
self.root = root
32+
33+
self._page = root
34+
self._page_num = page_num
35+
self._node_num = 0
36+
37+
def reset(self) -> None:
38+
"""Reset the internal state to the root of the BTree."""
39+
self._page = self.root
40+
self._page_num = self._page.num
41+
self._node_num = 0
42+
43+
def node(self) -> Node:
44+
"""Return the node the BTree is currently on.
45+
46+
Returns:
47+
A :class:`~dissect.database.ese.page.Node` object of the current node.
48+
"""
49+
return self._page.node(self._node_num)
50+
51+
def next(self) -> Node:
52+
"""Move the BTree to the next node and return it.
53+
54+
Can move the BTree to the next page as a side effect.
55+
56+
Returns:
57+
A :class:`~dissect.database.ese.page.Node` object of the next node.
58+
"""
59+
if self._node_num + 1 > self._page.node_count - 1:
60+
self.next_page()
61+
else:
62+
self._node_num += 1
63+
64+
return self.node()
65+
66+
def next_page(self) -> None:
67+
"""Move the BTree to the next page in the tree.
68+
69+
Raises:
70+
NoNeighbourPageError: If the current page has no next page.
71+
"""
72+
if self._page.next_page:
73+
self._page = self.db.page(self._page.next_page)
74+
self._node_num = 0
75+
else:
76+
raise NoNeighbourPageError(f"{self._page} has no next page")
77+
78+
def prev(self) -> Node:
79+
"""Move the BTree to the previous node and return it.
80+
81+
Can move the BTree to the previous page as a side effect.
82+
83+
Returns:
84+
A :class:`~dissect.database.ese.page.Node` object of the previous node.
85+
"""
86+
if self._node_num - 1 < 0:
87+
self.prev_page()
88+
else:
89+
self._node_num -= 1
90+
91+
return self.node()
92+
93+
def prev_page(self) -> None:
94+
"""Move the BTree to the previous page in the tree.
95+
96+
Raises:
97+
NoNeighbourPageError: If the current page has no previous page.
98+
"""
99+
if self._page.previous_page:
100+
self._page = self.db.page(self._page.previous_page)
101+
self._node_num = self._page.node_count - 1
102+
else:
103+
raise NoNeighbourPageError(f"{self._page} has no previous page")
104+
105+
def search(self, key: bytes, exact: bool = True) -> Node:
106+
"""Search the tree for the given ``key``.
107+
108+
Moves the BTree to the matching node, or on the last node that is less than the requested key.
109+
110+
Args:
111+
key: The key to search for.
112+
exact: Whether to only return successfully on an exact match.
113+
114+
Raises:
115+
KeyNotFoundError: If an ``exact`` match was requested but not found.
116+
"""
117+
page = self._page
118+
while True:
119+
node = find_node(page, key)
120+
121+
if page.is_branch:
122+
page = self.db.page(node.child)
123+
else:
124+
self._page = page
125+
self._page_num = page.num
126+
self._node_num = node.num
127+
break
128+
129+
if exact and key != node.key:
130+
raise KeyNotFoundError(f"Can't find key: {key}")
131+
132+
return self.node()
133+
134+
135+
def find_node(page: Page, key: bytes) -> Node:
136+
"""Search a page for a node matching ``key``.
137+
138+
Args:
139+
page: The page to search.
140+
key: The key to search.
141+
"""
142+
first_node_idx = 0
143+
last_node_idx = page.node_count - 1
144+
145+
node = None
146+
while first_node_idx < last_node_idx:
147+
node_idx = (first_node_idx + last_node_idx) // 2
148+
node = page.node(node_idx)
149+
150+
# It turns out that the way BTree keys are compared matches 1:1 with how Python compares bytes
151+
# First compare data, then length
152+
if key < node.key:
153+
last_node_idx = node_idx
154+
elif key == node.key:
155+
if page.is_branch:
156+
# If there's an exact match on a key on a branch page, the actual leaf nodes are in the next branch
157+
# Page keys for branch pages appear to be non-inclusive upper bounds
158+
node_idx = min(node_idx + 1, page.node_count - 1)
159+
node = page.node(node_idx)
160+
161+
return node
162+
else:
163+
first_node_idx = node_idx + 1
164+
165+
# We're at the last node
166+
return page.node(first_node_idx)

0 commit comments

Comments
 (0)