Forked from IntendedConsequence/read-chrome-sessions.py
Last active
November 7, 2025 00:41
-
-
Save sfoster/01951fb61d8cc6fcf9cae450882c1320 to your computer and use it in GitHub Desktop.
Parse urls and titles from chrome browser sessions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import sys | |
| import os | |
| import csv | |
| import json | |
| import struct | |
| import argparse | |
| from io import BytesIO | |
| from enum import Enum, EnumMeta | |
| from urllib.parse import unquote | |
| from pathlib import Path | |
| """ | |
| SNSS Session File Parser for PyBrinf | |
| This module parses SNSS files and returns a list of current tabs. | |
| Script made by Manuel Cabral | |
| Modified by YourMom for proper string padding alignment and String16 support | |
| Modified by sfoster to output a list of SessionCommand types | |
| """ | |
| snss_type_to_command_name = { | |
| 0: "CommandSetTabWindow", | |
| 1: "CommandSetWindowBoundsObsolete", | |
| 2: "CommandSetTabIndexInWindow", | |
| 3: "CommandTabClosed", | |
| 4: "CommandWindowClosed", | |
| 5: "CommandTabNavigationPathPrunedFromBack", | |
| 6: "CommandUpdateTabNavigation", | |
| 7: "CommandSetSelectedNavigationIndex", | |
| 8: "CommandSetSelectedTabInIndex", | |
| 9: "CommandSetWindowType", | |
| 11: "CommandTabNavigationPathPrunedFromFront", | |
| 12: "CommandSetPinnedState", | |
| 13: "CommandSetExtensionAppID", | |
| 14: "CommandSetWindowBounds3", | |
| 15: "CommandSetWindowAppName", | |
| 16: "CommandTabClosed", | |
| 17: "CommandWindowClosed", | |
| 18: "CommandSetTabUserAgentOverride", | |
| 19: "CommandSessionStorageAssociated", | |
| 20: "CommandSetActiveWindow", | |
| 21: "CommandLastActiveTime", | |
| 22: "CommandSetWindowWorkspaceObsolete", | |
| 23: "CommandSetWindowWorkspace2", | |
| 24: "CommandTabNavigationPathPruned", | |
| 25: "CommandSetTabGroup", | |
| 26: "CommandSetTabGroupMetadataObsolete", | |
| 27: "CommandSetTabGroupMetadata2", | |
| 28: "CommandSetTabGuid", | |
| 29: "CommandSetTabUserAgentOverride2", | |
| 30: "CommandSetTabData", | |
| 31: "CommandSetWindowUserTitle", | |
| 32: "CommandSetWindowVisibleOnAllWorkspaces", | |
| 33: "CommandAddTabExtraData", | |
| 34: "CommandAddWindowExtraData", | |
| 35: "CommandSetPlatformSessionId", | |
| 36: "CommandSetSplitTab", | |
| 37: "CommandSetSplitTabData", | |
| 255: "InitialStateMarkerCommandId", | |
| } | |
| class MetaEnum(EnumMeta): | |
| """This class is used for creating an enum with a custom __contains__ method""" | |
| def __contains__(cls, item: int) -> bool: | |
| try: | |
| cls(item) | |
| except ValueError: | |
| return False | |
| return True | |
| class SNSSTypeCommand(Enum, metaclass=MetaEnum): | |
| """Enum for SNSS command types""" | |
| CommandSetTabWindow = 0 | |
| CommandSetWindowBoundsObsolete = 1 | |
| CommandSetTabIndexInWindow = 2 | |
| CommandTabNavigationPathPrunedFromBack = 5 | |
| CommandUpdateTabNavigation = 6 | |
| CommandSetSelectedNavigationIndex = 7 | |
| CommandSetSelectedTabInIndex = 8 | |
| CommandSetWindowType = 9 | |
| CommandTabNavigationPathPrunedFromFront = 11 | |
| CommandSetPinnedState = 12 | |
| CommandSetExtensionAppID = 13 | |
| CommandSetWindowBounds3= 14 | |
| CommandSetWindowAppName = 15 | |
| CommandTabClosed = 16 | |
| CommandWindowClosed = 17 | |
| CommandSetTabUserAgentOverride = 18 | |
| CommandSessionStorageAssociated = 19 | |
| CommandSetActiveWindow = 20 | |
| CommandLastActiveTime = 21 | |
| CommandSetWindowWorkspaceObsolete = 22 | |
| CommandSetWindowWorkspace2 = 23 | |
| CommandTabNavigationPathPruned = 24 | |
| CommandSetTabGroup = 25 | |
| CommandSetTabGroupMetadataObsolete = 26 | |
| CommandSetTabGroupMetadata2 = 27 | |
| CommandSetTabGuid = 28 | |
| CommandSetTabUserAgentOverride2 = 29 | |
| CommandSetTabData = 30 | |
| CommandSetWindowUserTitle = 31 | |
| CommandSetWindowVisibleOnAllWorkspaces = 32 | |
| CommandAddTabExtraData = 33 | |
| CommandAddWindowExtraData = 34 | |
| CommandSetPlatformSessionId = 35 | |
| CommandSetSplitTab = 36 | |
| CommandSetSplitTabData = 37 | |
| InitialStateMarkerCommandId = 255 | |
| class SNSSCommand: | |
| """Command class core""" | |
| def __init__(self, id: int, content: bytes): | |
| self.id = id | |
| self.content = content | |
| def parse_commands(commands): | |
| output = [] | |
| for command in commands: | |
| if command.id in SNSSTypeCommand: | |
| output.append(command.id) | |
| return output | |
| def parse_navigation_commands(commands): | |
| output = [] | |
| for command in commands: | |
| if command.id in SNSSTypeCommand: | |
| if command.id == SNSSTypeCommand.CommandUpdateTabNavigation.value: | |
| # Extracting the pickle payload | |
| content = BytesIO(command.content) | |
| # Get the size of the pickle | |
| content.seek(0, os.SEEK_END) | |
| pickle_size = content.tell() | |
| content.seek(0, os.SEEK_SET) | |
| # Get the size of the payload, unpacking uint32 | |
| (payload_size,) = struct.unpack("I", content.read(4)) | |
| payloard_start = pickle_size - payload_size | |
| # Get the payload, unpacking uint32 | |
| (tab_id,) = struct.unpack("I", content.read(4)) | |
| (index,) = struct.unpack("I", content.read(4)) | |
| def read_str8(): | |
| # Get the url size, unpacking uint32 | |
| (str_length,) = struct.unpack("I", content.read(4)) | |
| padding = 4 - (str_length % 4) if str_length % 4 else 0 | |
| # If the url size is more than the difference between | |
| # the pickle size and the current position, it's invalid | |
| if str_length > pickle_size - content.tell(): | |
| raise Exception("Invalid string length") | |
| return content.read(str_length + padding)[:str_length].decode( | |
| "utf-8", "ignore" | |
| ) | |
| def read_str16(): | |
| # Get the url size, unpacking uint32 | |
| (str_length,) = struct.unpack("I", content.read(4)) | |
| str_length = str_length * 2 | |
| padding = 4 - (str_length % 4) if str_length % 4 else 0 | |
| # if str_length % 4 != 0: | |
| # str_length += 4 - (str_length % 4) | |
| # If the url size is more than the difference between | |
| # the pickle size and the current position, it's invalid | |
| if str_length > pickle_size - content.tell(): | |
| # raise Exception("Invalid string length") | |
| return "undefined" | |
| return content.read(str_length + padding)[:str_length].decode( | |
| "utf-16", "ignore" | |
| ) | |
| # else: | |
| url = read_str8() | |
| title = read_str16() | |
| output.append((tab_id, index, url, title)) | |
| return output | |
| class InvalidSNSSFileException(Exception): | |
| pass | |
| def parse(path): | |
| # Parse a SNSS Session File to get all commands | |
| commands = [] | |
| file = open(path, "rb") | |
| # Getting file size | |
| file.seek(0, os.SEEK_END) | |
| end = file.tell() | |
| file.seek(0, os.SEEK_SET) | |
| # Reading signature, unpacking int32 | |
| (signature,) = struct.unpack("i", file.read(4)) | |
| if signature != 0x53534E53: # SNSS | |
| raise InvalidSNSSFileException("Invalid SNSS file") | |
| # Reading version version, unpacking int32 | |
| (version,) = struct.unpack("i", file.read(4)) | |
| while end - file.tell() > 0: | |
| # Read command size, unpacking uint16 | |
| (command_size,) = struct.unpack("H", file.read(2)) | |
| if command_size == 0: | |
| raise Exception("Invalid command size, maybe corrupted file") | |
| # Read command id, unpacking uint8 | |
| (id,) = struct.unpack("B", file.read(1)) | |
| # Read content, NOTE: command id is included in command_size | |
| content = file.read(command_size - 1) | |
| command = SNSSCommand(id, content) | |
| commands.append(command) | |
| file.close() | |
| return commands | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("path", type=Path) | |
| args = parser.parse_args() | |
| path = args.path | |
| if path.exists(): | |
| paths = [path] if not path.is_dir() else [x for x in path.glob("*.*") if x.is_file() and x.stat().st_size > 4] | |
| data = {} | |
| for path in paths: | |
| try: | |
| commands = parse(path) | |
| except InvalidSNSSFileException: | |
| print(f"File is not a valid SNSS format, skipping: {path}", file=sys.stderr) | |
| continue | |
| pathkey = str(path) | |
| data[pathkey] = [] | |
| for snss_command in commands: | |
| command_name = snss_type_to_command_name[snss_command.id] if snss_command.id in snss_type_to_command_name else "unknown type" | |
| data[pathkey].append(f"{snss_command.id}\t{command_name}") | |
| print(pathkey) | |
| print("\n".join(data[pathkey])) | |
| else: | |
| print(f"File not found: {path}", file=sys.stderr) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment