Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions BitsParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@

class BitsParser:

def __init__(self, queue_dir, carve_db, carve_all, out_file):
def __init__(self, queue_dir, carve_db, carve_all, out_file, sid_lookup=True):

self.queue_dir = queue_dir
self.carve_db_files = carve_db
self.carve_all_files = carve_all
self.out_file = out_file
self.sid_lookup = sid_lookup

self.sid_user_cache = {}
self.visited_jobs = set()
Expand All @@ -66,6 +67,10 @@ def __init__(self, queue_dir, carve_db, carve_all, out_file):
def get_username_from_sid(self, sid):
""" Returns the username associated with the given SID by calling LookupAccountSid """

# The lookup runs against the accounts of this machine, not the one the database came from
if not self.sid_lookup:
return None

# Cache usernames to improve efficiency with repeated lookups
if sid in self.sid_user_cache:
return self.sid_user_cache[sid]
Expand Down Expand Up @@ -350,6 +355,35 @@ def output_jobs(self, file_path, jobs):
sys.stdout = orig_stdout


def output_jobs_properjson(self, file_path, jobs):
"""Cleans up and outputs the parsed jobs from the qmgr database files"""

# If an output file is specified, open it and use it instead of stdout
if self.out_file:
orig_stdout = sys.stdout
sys.stdout = open(self.out_file, "w")

try:
uniquejobs = []
for job in jobs:
# Skip incomplete carved jobs as they do not contain useful info
if job.is_carved() and not job.is_useful_for_analysis():
continue

# Output unique jobs
if job.hash not in self.visited_jobs:
uniquejobs.append(job)
self.visited_jobs.add(job.hash)

results = [j.job_dict for j in uniquejobs]
jobsJson = json.dumps({"jobs" : results}, indent=4)
print(jobsJson)
finally:
if self.out_file:
sys.stdout.close()
sys.stdout = orig_stdout


def process_file(self, file_path):
""" Processes the given BITS file. Attempts to find/parse jobs. """

Expand All @@ -374,7 +408,7 @@ def process_file(self, file_path):
else:
jobs = self.load_non_qmgr_jobs(file_data)

self.output_jobs(file_path, jobs)
self.output_jobs_properjson(file_path, jobs)

except Exception:
print(f'Exception occurred processing file {file_path}: ' + traceback.format_exc(), file=sys.stderr)
Expand Down Expand Up @@ -564,8 +598,10 @@ def parse(self):
parser.add_argument('--output', '-o', help='Optionally specify a file for JSON output. If not specified the output will be printed to stdout.')
parser.add_argument('--carvedb', action='store_true', help='Carve deleted records from database files')
parser.add_argument('--carveall', action='store_true', help='Carve deleted records from all other files')
parser.add_argument('--no-sid-lookup', action='store_true', help='Do not resolve owner SIDs to account names on this machine (no "Owner" field)')
parsed_args = parser.parse_args()

queue_dir = os.path.expandvars(parsed_args.input)
bits_parser = BitsParser(queue_dir, parsed_args.carvedb, parsed_args.carveall, parsed_args.output)
bits_parser = BitsParser(queue_dir, parsed_args.carvedb, parsed_args.carveall, parsed_args.output,
sid_lookup=not parsed_args.no_sid_lookup)
bits_parser.run()
11 changes: 9 additions & 2 deletions ese/ese.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,9 @@ def __init__(self, db, data=None):
self.record = None
if data is not None:
self.record = ESENT_PAGE_HEADER(self.__DBHeader['Version'], self.__DBHeader['FileFormatRevision'], self.__DBHeader['PageSize'], data)
# From format 0x620 revision 0x122 the upper 4 bits of the tag count are reserved
if self.__DBHeader['Version'] == 0x620 and self.__DBHeader['FileFormatRevision'] >= 0x122:
self.record['FirstAvailablePageTag'] &= 0x0fff

def getTag(self, tagNum):
"""Gets the next tag from this page"""
Expand Down Expand Up @@ -640,8 +643,12 @@ def openTable(self, tableName):
cursor['CurrentPageData'] = page
cursor['CurrentTag'] = 0

# Create a mapping of the long values tree
cursor['LongValues'] = self.__getLongValues(cursor['TableData']['LongValues']['FatherDataPageNumber'])
# Create a mapping of the long values tree (a table that never stored a long value has none)
longValues = cursor['TableData']['LongValues']
if isinstance(longValues, OrderedDict):
cursor['LongValues'] = {}
else:
cursor['LongValues'] = self.__getLongValues(longValues['FatherDataPageNumber'])

return cursor

Expand Down
8 changes: 8 additions & 0 deletions requirements-build.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Tools to build the one-file exe with PyInstaller (Python 3.11.9), on top of the runtime requirements.
-r requirements.txt
pyinstaller==6.22.3
altgraph==0.17.5
packaging==26.3
pefile==2024.8.26
pyinstaller-hooks-contrib==2026.8
pywin32-ctypes==0.2.3
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
bits_parser
bits_parser @ git+https://github.com/ANSSI-FR/bits_parser@5755dff9d69fafa2defbf0dd3f81ae43826bad2d