數位鑑識是現代資訊安全領域中不可或缺的專業技能,而瀏覽器作為使用者與網際網路互動的主要介面,儲存了大量具有調查價值的數位足跡。Firefox 和 Chrome 這兩款主流瀏覽器都採用 SQLite 資料庫來儲存使用者的瀏覽資料,包括瀏覽歷史、Cookie、下載記錄、書籤和表單資料等。對於數位鑑識分析師、資安事件調查人員或企業內部稽核團隊而言,理解這些資料庫的結構並掌握提取與分析技術,是還原使用者行為軌跡和調查資安事件的關鍵能力。本文將深入探討瀏覽器資料庫的技術架構,並透過 Python 程式碼示範如何系統性地進行數位鑑識分析,同時也會討論帳戶安全防護的最佳實踐。

瀏覽器數位鑑識的應用場景與法律考量

在進行瀏覽器數位鑑識之前,必須先了解這項技術的合法應用場景以及相關的法律與倫理考量。數位鑑識技術應該用於正當目的,並在適當的授權下執行。

合法應用場景

企業資安事件調查是瀏覽器鑑識最常見的應用場景之一。當組織遭遇資料外洩、惡意軟體感染或內部威脅時,資安團隊需要分析員工的瀏覽記錄來追蹤攻擊來源、識別被入侵的系統,以及了解資料外洩的範圍。透過分析瀏覽歷史和下載記錄,可以判斷使用者是否造訪了惡意網站或下載了可疑檔案。

法律訴訟和刑事調查也經常需要瀏覽器鑑識支援。在涉及網路犯罪、智慧財產權侵害或員工不當行為的案件中,瀏覽器資料可以作為重要的數位證據。鑑識分析師需要以符合法庭要求的方式收集和保存這些證據,確保其完整性和可信度。

個人資料恢復是另一個常見的應用。使用者可能因為誤刪書籤、清除瀏覽記錄或系統故障而遺失重要的瀏覽資料。透過直接存取瀏覽器資料庫,有時可以恢復這些被刪除的資訊。

法律與倫理考量

進行瀏覽器鑑識時必須遵守相關法律法規。在台灣,個人資料保護法對於個人資料的蒐集、處理和利用有嚴格的規範。未經授權存取他人的瀏覽器資料可能構成刑法上的妨害電腦使用罪。因此,鑑識人員必須確保已取得適當的授權,無論是法院命令、公司政策授權還是資料所有人的同意。

在企業環境中,應該在員工入職時就明確告知公司對於工作設備的監控政策,並取得員工的書面同意。這不僅保護了公司的合法權益,也尊重了員工的隱私期待。

Firefox 瀏覽器資料庫架構解析

Firefox 瀏覽器使用多個 SQLite 資料庫檔案來儲存不同類型的使用者資料。這些檔案位於使用者設定檔目錄中,在不同作業系統上的預設路徑有所不同。在 Windows 系統上,路徑通常是 C:\Users<username>\AppData\Roaming\Mozilla\Firefox\Profiles<profile>;在 macOS 上是 ~/Library/Application Support/Firefox/Profiles/;在 Linux 上則是 ~/.mozilla/firefox/

主要資料庫檔案

places.sqlite 是 Firefox 中最重要的資料庫檔案,它儲存了瀏覽歷史、書籤和標籤等資訊。這個資料庫包含多個相互關聯的表格,其中 moz_places 表格儲存了所有造訪過的 URL 及其相關中繼資料,moz_historyvisits 表格則記錄了每次造訪的詳細資訊,包括造訪時間和造訪類型。

cookies.sqlite 儲存了網站設定的 Cookie 資訊。moz_cookies 表格包含了 Cookie 的名稱、值、所屬網域、過期時間等欄位。分析 Cookie 可以了解使用者登入過哪些網站,以及網站追蹤使用者的方式。

formhistory.sqlite 儲存了使用者在網頁表單中輸入過的資料,例如搜尋關鍵字、使用者名稱等。這些資料對於了解使用者的搜尋行為和興趣特別有價值。

webappsstore.sqlite 儲存了網站使用 Web Storage API 存放的本機資料,包括 localStorage 和 sessionStorage 的內容。

Firefox 資料庫結構圖

以下圖表展示了 Firefox 主要資料庫檔案及其包含的關鍵表格結構。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

package "Firefox Profile 目錄" {
  package "places.sqlite" {
    [moz_places] as places
    [moz_historyvisits] as visits
    [moz_bookmarks] as bookmarks
    [moz_annos] as annos

    places <-- visits : place_id
    places <-- bookmarks : fk
  }

  package "cookies.sqlite" {
    [moz_cookies] as cookies
  }

  package "formhistory.sqlite" {
    [moz_formhistory] as formhist
  }

  package "webappsstore.sqlite" {
    [webappsstore2] as webapps
  }
}

note bottom of places
  儲存 URL、標題
  造訪次數、favicon
end note

note bottom of visits
  造訪時間戳記
  造訪類型
  來源頁面
end note

@enduml

這個架構圖清楚地展示了 Firefox 資料儲存的組織方式。places.sqlite 是資訊最豐富的資料庫,包含了使用者的完整瀏覽足跡。moz_places 和 moz_historyvisits 之間透過 place_id 建立關聯,讓我們能夠查詢特定 URL 的所有造訪記錄。

Chrome 瀏覽器資料庫架構解析

Chrome 瀏覽器同樣使用 SQLite 資料庫來儲存使用者資料,但其檔案組織方式與 Firefox 略有不同。Chrome 的使用者資料目錄在 Windows 上通常位於 C:\Users<username>\AppData\Local\Google\Chrome\User Data\Default;在 macOS 上是 ~/Library/Application Support/Google/Chrome/Default;在 Linux 上則是 ~/.config/google-chrome/Default。

主要資料庫檔案

History 是 Chrome 最重要的資料庫檔案,它整合了瀏覽歷史和下載記錄。urls 表格儲存了所有造訪過的 URL,visits 表格記錄了每次造訪的詳細資訊,downloads 表格則包含了所有的下載記錄。

Cookies 儲存了網站的 Cookie 資訊。與 Firefox 不同,Chrome 會對某些敏感的 Cookie 值進行加密保護。

Web Data 儲存了自動填入表單的資料,包括地址、信用卡資訊等。這些敏感資料同樣會被加密。

Login Data 儲存了使用者儲存的網站登入憑證。密碼會使用作業系統的加密機制進行保護,在 Windows 上使用 DPAPI,在 macOS 上使用 Keychain。

Chrome 時間戳記格式

Chrome 使用一種特殊的時間戳記格式,稱為 WebKit/Chrome timestamp。這個格式以西元 1601 年 1 月 1 日為起點,記錄從那時到現在經過的微秒數。在進行鑑識分析時,需要將這個數值轉換為可讀的日期時間格式。

以下函式示範如何將 Chrome 時間戳記轉換為 Python 的 datetime 物件。

# 匯入必要的模組
import datetime

def convert_chrome_timestamp(timestamp):
    """
    將 Chrome/WebKit 時間戳記轉換為可讀的日期時間格式

    Chrome 使用的時間戳記以 1601 年 1 月 1 日為起點
    記錄從那時起經過的微秒數

    Args:
        timestamp: Chrome 格式的時間戳記(微秒)

    Returns:
        datetime: 轉換後的 datetime 物件
        如果時間戳記無效則返回 None
    """
    try:
        # 定義 Chrome 時間戳記的起點
        # 西元 1601 年 1 月 1 日
        epoch_start = datetime.datetime(1601, 1, 1)

        # 將微秒數轉換為 timedelta 物件
        delta = datetime.timedelta(microseconds=int(timestamp))

        # 計算實際的日期時間
        result = epoch_start + delta

        # 驗證結果是否合理
        # 避免異常的時間戳記導致錯誤
        if result.year < 1970 or result.year > 2100:
            return None

        return result

    except (ValueError, OverflowError) as e:
        # 處理無效的時間戳記
        print(f"時間戳記轉換錯誤: {e}")
        return None

# 使用範例
# 這個時間戳記對應到某個特定的日期時間
sample_timestamp = 13308000000000000
converted_time = convert_chrome_timestamp(sample_timestamp)
if converted_time:
    print(f"轉換結果: {converted_time.strftime('%Y-%m-%d %H:%M:%S')}")

這個函式是進行 Chrome 鑑識分析的基礎工具。由於 Chrome 資料庫中幾乎所有的時間欄位都使用這種格式,因此在撰寫任何分析程式之前都需要先實作這個轉換邏輯。

Python 數位鑑識工具實作

使用 Python 進行瀏覽器鑑識分析的優勢在於其豐富的標準函式庫和第三方套件支援。Python 內建的 sqlite3 模組可以直接操作 SQLite 資料庫,而 pandas 等資料分析套件則可以方便地處理和視覺化提取出的資料。

Firefox 資料提取工具

以下程式碼展示了一個完整的 Firefox 鑑識分析工具,能夠提取瀏覽歷史、Cookie 和下載記錄。

# 匯入必要的模組
import sqlite3
import os
import datetime
import shutil
from pathlib import Path

class FirefoxForensicsAnalyzer:
    """
    Firefox 瀏覽器數位鑑識分析器

    此類別提供了從 Firefox 資料庫提取和分析
    瀏覽歷史、Cookie 和下載記錄的功能
    """

    def __init__(self, profile_path):
        """
        初始化分析器

        Args:
            profile_path: Firefox 設定檔目錄的路徑
        """
        self.profile_path = Path(profile_path)

        # 驗證路徑是否存在
        if not self.profile_path.exists():
            raise ValueError(f"設定檔目錄不存在: {profile_path}")

        # 定義資料庫檔案路徑
        self.places_db = self.profile_path / "places.sqlite"
        self.cookies_db = self.profile_path / "cookies.sqlite"

    def _create_db_copy(self, db_path):
        """
        建立資料庫檔案的副本以避免鎖定問題

        當 Firefox 正在運行時,資料庫可能被鎖定
        透過複製檔案可以安全地進行分析

        Args:
            db_path: 原始資料庫檔案路徑

        Returns:
            str: 副本檔案的路徑
        """
        if not db_path.exists():
            return None

        # 建立暫存副本
        copy_path = str(db_path) + ".forensics_copy"
        shutil.copy2(db_path, copy_path)
        return copy_path

    def extract_browsing_history(self, limit=1000):
        """
        提取瀏覽歷史記錄

        從 places.sqlite 提取使用者的瀏覽歷史
        包括 URL、標題、造訪時間和造訪次數

        Args:
            limit: 返回的最大記錄數

        Returns:
            list: 包含瀏覽歷史記錄的字典列表
        """
        db_copy = self._create_db_copy(self.places_db)
        if not db_copy:
            print("無法存取 places.sqlite")
            return []

        history_records = []

        try:
            # 連線到資料庫副本
            conn = sqlite3.connect(db_copy)
            cursor = conn.cursor()

            # 執行 SQL 查詢
            # 關聯 moz_places 和 moz_historyvisits 表格
            # 取得完整的瀏覽歷史資訊
            query = """
                SELECT
                    p.url,
                    p.title,
                    p.visit_count,
                    h.visit_date,
                    h.visit_type
                FROM moz_places p
                INNER JOIN moz_historyvisits h
                    ON p.id = h.place_id
                ORDER BY h.visit_date DESC
                LIMIT ?
            """

            cursor.execute(query, (limit,))
            rows = cursor.fetchall()

            # 處理查詢結果
            for row in rows:
                # Firefox 時間戳記是以微秒為單位的 Unix 時間
                visit_time = datetime.datetime.fromtimestamp(
                    row[3] / 1000000
                ) if row[3] else None

                record = {
                    "url": row[0],
                    "title": row[1] or "(無標題)",
                    "visit_count": row[2],
                    "visit_time": visit_time,
                    "visit_type": self._get_visit_type_name(row[4])
                }
                history_records.append(record)

            conn.close()

        except sqlite3.Error as e:
            print(f"資料庫錯誤: {e}")

        finally:
            # 清理暫存檔案
            if os.path.exists(db_copy):
                os.remove(db_copy)

        return history_records

    def _get_visit_type_name(self, visit_type):
        """
        將造訪類型代碼轉換為可讀名稱

        Args:
            visit_type: 造訪類型代碼

        Returns:
            str: 造訪類型名稱
        """
        visit_types = {
            1: "連結點擊",
            2: "輸入網址",
            3: "書籤",
            4: "嵌入內容",
            5: "重新導向(永久)",
            6: "重新導向(暫時)",
            7: "下載",
            8: "框架連結"
        }
        return visit_types.get(visit_type, f"未知類型 ({visit_type})")

    def extract_cookies(self, domain_filter=None):
        """
        提取 Cookie 資訊

        從 cookies.sqlite 提取網站 Cookie
        可選擇性地過濾特定網域

        Args:
            domain_filter: 網域過濾條件(可選)

        Returns:
            list: 包含 Cookie 資訊的字典列表
        """
        db_copy = self._create_db_copy(self.cookies_db)
        if not db_copy:
            print("無法存取 cookies.sqlite")
            return []

        cookie_records = []

        try:
            conn = sqlite3.connect(db_copy)
            cursor = conn.cursor()

            # 建立查詢
            if domain_filter:
                query = """
                    SELECT
                        host,
                        name,
                        value,
                        path,
                        expiry,
                        isSecure,
                        isHttpOnly,
                        sameSite
                    FROM moz_cookies
                    WHERE host LIKE ?
                    ORDER BY host, name
                """
                cursor.execute(query, (f"%{domain_filter}%",))
            else:
                query = """
                    SELECT
                        host,
                        name,
                        value,
                        path,
                        expiry,
                        isSecure,
                        isHttpOnly,
                        sameSite
                    FROM moz_cookies
                    ORDER BY host, name
                """
                cursor.execute(query)

            rows = cursor.fetchall()

            for row in rows:
                # 轉換過期時間
                expiry_time = datetime.datetime.fromtimestamp(
                    row[4]
                ) if row[4] else None

                record = {
                    "host": row[0],
                    "name": row[1],
                    "value": row[2][:50] + "..." if len(row[2]) > 50 else row[2],
                    "path": row[3],
                    "expiry": expiry_time,
                    "is_secure": bool(row[5]),
                    "is_http_only": bool(row[6]),
                    "same_site": row[7]
                }
                cookie_records.append(record)

            conn.close()

        except sqlite3.Error as e:
            print(f"資料庫錯誤: {e}")

        finally:
            if os.path.exists(db_copy):
                os.remove(db_copy)

        return cookie_records

    def generate_report(self, output_file="firefox_forensics_report.txt"):
        """
        生成完整的鑑識分析報告

        Args:
            output_file: 輸出檔案路徑
        """
        with open(output_file, "w", encoding="utf-8") as f:
            f.write("=" * 60 + "\n")
            f.write("Firefox 數位鑑識分析報告\n")
            f.write("=" * 60 + "\n\n")

            # 報告生成時間
            f.write(f"報告生成時間: {datetime.datetime.now()}\n")
            f.write(f"分析目標: {self.profile_path}\n\n")

            # 瀏覽歷史摘要
            f.write("-" * 40 + "\n")
            f.write("瀏覽歷史摘要(最近 100 筆)\n")
            f.write("-" * 40 + "\n\n")

            history = self.extract_browsing_history(limit=100)
            for record in history:
                f.write(f"時間: {record['visit_time']}\n")
                f.write(f"標題: {record['title']}\n")
                f.write(f"網址: {record['url']}\n")
                f.write(f"類型: {record['visit_type']}\n")
                f.write("\n")

            f.write(f"\n{len(history)} 筆記錄\n\n")

        print(f"報告已儲存至: {output_file}")

# 使用範例
if __name__ == "__main__":
    # 設定 Firefox 設定檔路徑
    # 需要根據實際環境修改此路徑
    profile_path = "/path/to/firefox/profile"

    try:
        analyzer = FirefoxForensicsAnalyzer(profile_path)

        # 提取瀏覽歷史
        print("正在提取瀏覽歷史...")
        history = analyzer.extract_browsing_history(limit=50)
        print(f"共提取 {len(history)} 筆瀏覽記錄\n")

        # 顯示最近的瀏覽記錄
        for i, record in enumerate(history[:5], 1):
            print(f"{i}. {record['title']}")
            print(f"   網址: {record['url']}")
            print(f"   時間: {record['visit_time']}")
            print()

    except ValueError as e:
        print(f"錯誤: {e}")

這個程式碼實作了一個完整的 Firefox 鑑識分析工具類別。它採用了物件導向的設計方式,將不同的分析功能封裝在獨立的方法中,提高了程式碼的可維護性和重用性。值得注意的是,程式會先複製資料庫檔案再進行分析,這樣可以避免因為瀏覽器正在運行而導致的資料庫鎖定問題。

Chrome 資料提取工具

以下程式碼展示了針對 Chrome 瀏覽器的鑑識分析工具。

# 匯入必要的模組
import sqlite3
import os
import datetime
import shutil
from pathlib import Path

class ChromeForensicsAnalyzer:
    """
    Chrome 瀏覽器數位鑑識分析器

    此類別提供了從 Chrome 資料庫提取和分析
    瀏覽歷史、下載記錄和搜尋關鍵字的功能
    """

    def __init__(self, user_data_path):
        """
        初始化分析器

        Args:
            user_data_path: Chrome 使用者資料目錄的路徑
        """
        self.user_data_path = Path(user_data_path)

        if not self.user_data_path.exists():
            raise ValueError(f"使用者資料目錄不存在: {user_data_path}")

        # 定義資料庫檔案路徑
        self.history_db = self.user_data_path / "History"
        self.cookies_db = self.user_data_path / "Cookies"

    def _chrome_timestamp_to_datetime(self, timestamp):
        """
        將 Chrome 時間戳記轉換為 datetime 物件

        Args:
            timestamp: Chrome 格式的時間戳記

        Returns:
            datetime: 轉換後的日期時間,或 None
        """
        if not timestamp:
            return None

        try:
            # Chrome 時間戳記起點:1601 年 1 月 1 日
            epoch = datetime.datetime(1601, 1, 1)
            delta = datetime.timedelta(microseconds=int(timestamp))
            return epoch + delta
        except (ValueError, OverflowError):
            return None

    def _create_db_copy(self, db_path):
        """
        建立資料庫副本

        Args:
            db_path: 原始資料庫路徑

        Returns:
            str: 副本路徑
        """
        if not db_path.exists():
            return None

        copy_path = str(db_path) + ".forensics_copy"
        shutil.copy2(db_path, copy_path)
        return copy_path

    def extract_browsing_history(self, limit=1000):
        """
        提取瀏覽歷史記錄

        Args:
            limit: 最大記錄數

        Returns:
            list: 瀏覽歷史記錄列表
        """
        db_copy = self._create_db_copy(self.history_db)
        if not db_copy:
            print("無法存取 History 資料庫")
            return []

        history_records = []

        try:
            conn = sqlite3.connect(db_copy)
            cursor = conn.cursor()

            # 查詢瀏覽歷史
            # 關聯 urls 和 visits 表格
            query = """
                SELECT
                    u.url,
                    u.title,
                    u.visit_count,
                    v.visit_time,
                    v.visit_duration,
                    u.typed_count
                FROM urls u
                INNER JOIN visits v
                    ON u.id = v.url
                ORDER BY v.visit_time DESC
                LIMIT ?
            """

            cursor.execute(query, (limit,))
            rows = cursor.fetchall()

            for row in rows:
                visit_time = self._chrome_timestamp_to_datetime(row[3])

                # 計算造訪持續時間(微秒轉秒)
                duration_seconds = row[4] / 1000000 if row[4] else 0

                record = {
                    "url": row[0],
                    "title": row[1] or "(無標題)",
                    "visit_count": row[2],
                    "visit_time": visit_time,
                    "duration_seconds": round(duration_seconds, 2),
                    "typed_count": row[5]
                }
                history_records.append(record)

            conn.close()

        except sqlite3.Error as e:
            print(f"資料庫錯誤: {e}")

        finally:
            if os.path.exists(db_copy):
                os.remove(db_copy)

        return history_records

    def extract_downloads(self, limit=500):
        """
        提取下載記錄

        Args:
            limit: 最大記錄數

        Returns:
            list: 下載記錄列表
        """
        db_copy = self._create_db_copy(self.history_db)
        if not db_copy:
            print("無法存取 History 資料庫")
            return []

        download_records = []

        try:
            conn = sqlite3.connect(db_copy)
            cursor = conn.cursor()

            # 查詢下載記錄
            query = """
                SELECT
                    target_path,
                    tab_url,
                    referrer,
                    start_time,
                    end_time,
                    received_bytes,
                    total_bytes,
                    state,
                    danger_type,
                    mime_type
                FROM downloads
                ORDER BY start_time DESC
                LIMIT ?
            """

            cursor.execute(query, (limit,))
            rows = cursor.fetchall()

            for row in rows:
                start_time = self._chrome_timestamp_to_datetime(row[3])
                end_time = self._chrome_timestamp_to_datetime(row[4])

                record = {
                    "target_path": row[0],
                    "source_url": row[1],
                    "referrer": row[2],
                    "start_time": start_time,
                    "end_time": end_time,
                    "received_bytes": row[5],
                    "total_bytes": row[6],
                    "state": self._get_download_state(row[7]),
                    "danger_type": self._get_danger_type(row[8]),
                    "mime_type": row[9]
                }
                download_records.append(record)

            conn.close()

        except sqlite3.Error as e:
            print(f"資料庫錯誤: {e}")

        finally:
            if os.path.exists(db_copy):
                os.remove(db_copy)

        return download_records

    def _get_download_state(self, state):
        """
        將下載狀態代碼轉換為可讀名稱

        Args:
            state: 狀態代碼

        Returns:
            str: 狀態名稱
        """
        states = {
            0: "進行中",
            1: "完成",
            2: "已取消",
            3: "已中斷",
            4: "已暫停"
        }
        return states.get(state, f"未知 ({state})")

    def _get_danger_type(self, danger_type):
        """
        將危險類型代碼轉換為可讀名稱

        Args:
            danger_type: 危險類型代碼

        Returns:
            str: 危險類型名稱
        """
        danger_types = {
            0: "安全",
            1: "危險檔案",
            2: "危險網址",
            3: "危險內容",
            4: "可能不需要",
            5: "不常見內容",
            6: "使用者已驗證",
            7: "危險主機",
            8: "可能危險"
        }
        return danger_types.get(danger_type, f"未知 ({danger_type})")

    def extract_search_terms(self, limit=200):
        """
        提取搜尋關鍵字

        從 keyword_search_terms 表格提取使用者的搜尋記錄

        Args:
            limit: 最大記錄數

        Returns:
            list: 搜尋關鍵字列表
        """
        db_copy = self._create_db_copy(self.history_db)
        if not db_copy:
            return []

        search_terms = []

        try:
            conn = sqlite3.connect(db_copy)
            cursor = conn.cursor()

            # 查詢搜尋關鍵字
            # 關聯 keyword_search_terms 和 urls 表格
            query = """
                SELECT
                    k.term,
                    u.url,
                    u.title
                FROM keyword_search_terms k
                INNER JOIN urls u
                    ON k.url_id = u.id
                ORDER BY u.last_visit_time DESC
                LIMIT ?
            """

            cursor.execute(query, (limit,))
            rows = cursor.fetchall()

            for row in rows:
                record = {
                    "term": row[0],
                    "url": row[1],
                    "title": row[2]
                }
                search_terms.append(record)

            conn.close()

        except sqlite3.Error as e:
            print(f"資料庫錯誤: {e}")

        finally:
            if os.path.exists(db_copy):
                os.remove(db_copy)

        return search_terms

    def analyze_top_sites(self, top_n=20):
        """
        分析最常造訪的網站

        Args:
            top_n: 返回的網站數量

        Returns:
            list: 最常造訪網站列表
        """
        db_copy = self._create_db_copy(self.history_db)
        if not db_copy:
            return []

        top_sites = []

        try:
            conn = sqlite3.connect(db_copy)
            cursor = conn.cursor()

            # 查詢造訪次數最多的網站
            query = """
                SELECT
                    url,
                    title,
                    visit_count,
                    typed_count,
                    last_visit_time
                FROM urls
                ORDER BY visit_count DESC
                LIMIT ?
            """

            cursor.execute(query, (top_n,))
            rows = cursor.fetchall()

            for row in rows:
                last_visit = self._chrome_timestamp_to_datetime(row[4])

                record = {
                    "url": row[0],
                    "title": row[1] or "(無標題)",
                    "visit_count": row[2],
                    "typed_count": row[3],
                    "last_visit": last_visit
                }
                top_sites.append(record)

            conn.close()

        except sqlite3.Error as e:
            print(f"資料庫錯誤: {e}")

        finally:
            if os.path.exists(db_copy):
                os.remove(db_copy)

        return top_sites

# 使用範例
if __name__ == "__main__":
    # 設定 Chrome 使用者資料路徑
    # 需要根據實際環境修改此路徑
    user_data_path = "/path/to/chrome/user/data/Default"

    try:
        analyzer = ChromeForensicsAnalyzer(user_data_path)

        # 分析最常造訪的網站
        print("最常造訪的網站:\n")
        top_sites = analyzer.analyze_top_sites(top_n=10)

        for i, site in enumerate(top_sites, 1):
            print(f"{i}. {site['title']}")
            print(f"   網址: {site['url']}")
            print(f"   造訪次數: {site['visit_count']}")
            print(f"   最後造訪: {site['last_visit']}")
            print()

        # 提取下載記錄
        print("\n最近的下載記錄:\n")
        downloads = analyzer.extract_downloads(limit=5)

        for download in downloads:
            print(f"檔案: {download['target_path']}")
            print(f"來源: {download['source_url']}")
            print(f"狀態: {download['state']}")
            print(f"大小: {download['total_bytes']} bytes")
            print()

    except ValueError as e:
        print(f"錯誤: {e}")

Chrome 鑑識分析工具的實作與 Firefox 版本類似,但需要處理不同的時間戳記格式和資料庫結構。這個工具特別加入了下載記錄分析和搜尋關鍵字提取功能,這些資訊對於了解使用者的網路活動行為特別有價值。

鑑識分析流程圖

以下圖表展示了完整的瀏覽器數位鑑識分析流程。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

start

:確認授權與合法性;
note right
  取得適當授權
  遵守法律規範
end note

:識別目標系統;

:定位瀏覽器資料目錄;

if (瀏覽器類型?) then (Firefox)
  :取得 places.sqlite;
  :取得 cookies.sqlite;
  :取得 formhistory.sqlite;
else (Chrome)
  :取得 History;
  :取得 Cookies;
  :取得 Web Data;
endif

:建立資料庫副本;
note right
  保護原始證據
  避免資料污染
end note

:連線至資料庫副本;

:執行 SQL 查詢;

:提取目標資料;

:轉換時間戳記格式;

:資料正規化處理;

:生成分析報告;

:保存證據鏈記錄;

stop

@enduml

這個流程圖說明了進行瀏覽器鑑識分析的標準步驟。每個步驟都有其重要性,特別是授權確認和證據保護環節,這些是確保分析結果具有法律效力的關鍵。

帳戶安全與密碼防護最佳實踐

瀏覽器資料庫中儲存的資訊可能被惡意軟體或攻擊者濫用,因此了解如何保護自己的帳戶安全至關重要。以下討論一些關鍵的安全防護措施。

強密碼策略

密碼是帳戶安全的第一道防線。一個強密碼應該具備足夠的長度(建議至少 12 個字元)、複雜度(混合大小寫字母、數字和特殊符號)以及獨特性(每個帳戶使用不同的密碼)。避免使用個人資訊(如生日、姓名)或常見詞彙作為密碼,因為這些很容易被猜測或透過字典攻擊破解。

密碼管理器是管理多個強密碼的最佳工具。它可以生成隨機的強密碼,並安全地儲存這些密碼,讓使用者只需要記住一個主密碼。主流的密碼管理器如 1Password、Bitwarden 或 KeePass 都提供了跨平台支援和瀏覽器整合功能。

多因素認證

多因素認證(Multi-Factor Authentication,MFA)透過要求額外的驗證因素來大幅提升帳戶安全性。即使密碼被洩露,攻擊者仍然需要取得第二個因素才能登入帳戶。常見的第二因素包括簡訊驗證碼、認證應用程式(如 Google Authenticator)生成的一次性密碼,或是硬體安全金鑰(如 YubiKey)。

建議在所有支援 MFA 的服務上啟用此功能,特別是電子郵件、銀行和社群媒體帳戶。認證應用程式比簡訊驗證碼更安全,因為簡訊可能被 SIM 卡劫持攻擊截取。

瀏覽器安全設定

瀏覽器本身也提供了多種安全功能來保護使用者。定期清除瀏覽資料可以減少敏感資訊的暴露風險。使用瀏覽器的隱私瀏覽模式可以避免留下本機瀏覽記錄。檢視並管理已儲存的密碼,刪除不再需要的登入資訊。

Chrome 和 Firefox 都內建了密碼外洩檢查功能,可以警告使用者其儲存的密碼是否出現在已知的資料外洩事件中。定期執行這個檢查並更新被洩露的密碼是維護帳戶安全的重要步驟。

帳戶安全防護架構圖

以下圖表說明了帳戶安全防護的多層次架構。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

package "帳戶安全防護層次" {
  package "第一層:密碼安全" {
    [強密碼策略] as StrongPwd
    [密碼管理器] as PwdMgr
    [定期更換密碼] as RotatePwd
  }

  package "第二層:多因素認證" {
    [認證應用程式] as AuthApp
    [硬體安全金鑰] as SecurityKey
    [生物辨識] as Biometric
  }

  package "第三層:監控與回應" {
    [登入活動監控] as Monitor
    [異常行為警報] as Alert
    [事件回應計畫] as Response
  }
}

StrongPwd --> AuthApp
PwdMgr --> AuthApp
AuthApp --> Monitor
SecurityKey --> Monitor
Monitor --> Alert
Alert --> Response

note bottom of StrongPwd
  長度 >= 12 字元
  混合字元類型
  避免個人資訊
end note

note bottom of AuthApp
  TOTP 演算法
  30 秒更新週期
  離線可用
end note

@enduml

總結

瀏覽器數位鑑識是資訊安全專業人員必備的技能之一。透過理解 Firefox 和 Chrome 的資料庫結構,並運用 Python 等工具進行系統性的資料提取和分析,鑑識人員能夠有效地還原使用者的網路活動軌跡,為資安事件調查和法律訴訟提供重要的數位證據。

本文介紹的技術工具和方法應該用於合法且經過授權的目的。在進行任何鑑識分析之前,必須確認已取得適當的授權,並遵守相關的法律法規和倫理規範。同時,個人使用者也應該了解這些技術的存在,並採取適當的安全措施來保護自己的隱私和帳戶安全。

隨著瀏覽器技術的持續演進,資料儲存方式和安全機制也在不斷更新。鑑識人員需要持續學習和更新知識,以跟上這些變化。同時,各種自動化鑑識工具如 Hindsight 也可以作為手動分析的補充,提高分析效率。無論使用何種方法,維持證據的完整性和建立完善的證據鏈記錄始終是數位鑑識工作的核心原則。