65.9K
CodeProject 正在变化。 阅读更多。
Home

Firefox 凭据的秘密

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2017年1月30日

CPOL

5分钟阅读

viewsIcon

26962

获取近期 Firefox 版本中存储凭据的方法

引言

本文是关于获取浏览器(以及其他应用程序,例如:MS Outlook)存储的(已加密)凭据的秘密系列文章中的第四篇。第一篇文章介绍了 Wi-Fi 凭据。第二篇文章介绍了 Chrome 的凭据。第三篇文章介绍了 Internet Explorer 的凭据。本文是最具挑战性的一篇。它介绍了 Firefox,并解释了它是如何存储凭据以及如何从中提取凭据。

是什么让 Firefox 更安全

与早期版本相比,以及与许多其他浏览器相比,Firefox 的最新版本在安全性方面有了很大的提高。过去,只需获取位于 Firefox 配置文件目录下的 signons.txt 文件,就可以在那里找到所有存储的凭据。从 3.5 版本开始,文本格式已被 SQLite 数据库和 JSON 文件(名为 ‘logins.json’)的组合所取代。此外,如果设置了“主密码”,则在不知道该密码的情况下,无法解密存储的凭据。当未设置主密码时,您可以在 logins.json 中找到数据并解密凭据。每个凭据的用户和密码都使用 PK#11 加密。

定位 Firefox 配置文件路径

首先,我们需要找到要获取的数据的位置,即 Firefox 配置文件路径。通常位于 c:\users\<your user account>\appdata\local\Mozilla\Firefox\\profiles.ini

std::wstring GetFirefoxProfilePath()
{
    wchar_t strAppData[MAX_PATH];
    SHGetSpecialFolderPath(NULL, strAppData, CSIDL_APPDATA, TRUE);

    wchar_t strIniPath[MAX_PATH];
    swprintf_s(strIniPath, MAX_PATH, L"%s\\Mozilla\\Firefox\\profiles.ini", strAppData);

    wchar_t strRelativePath[MAX_PATH];
    GetPrivateProfileString(L"Profile0", L"Path", 
                            L"", strRelativePath, MAX_PATH, strIniPath);

    wchar_t strFullPath[MAX_PATH];
    swprintf_s(strFullPath, MAX_PATH, 
               L"%s\\Mozilla\\Firefox\\%s", strAppData, strRelativePath);

    wchar_t strShortPath[MAX_PATH];
    GetShortPathName(strFullPath, strShortPath, MAX_PATH);

    if (!PathFileExists(strShortPath))
    {
        return L"";
    }
    return ((std::wstring)strShortPath);
}

映射 nss3 DLL

nss3.dll 由一组名为 Network Security Services (NSS) 的库使用。这些库旨在支持支持 SSL、S/MIME 和其他 Internet 安全标准的通信应用程序的跨平台开发。有关 NSS 及其支持的标准概述,请参阅页面。

由于本文是关于 Windows 实现的,我将只提及 Windows 使用的库

  • nss3.dll - Windows 共享库
  • nss3.lib - 绑定到 nss3.dll 的 Windows 导入库
  • nss.lib - Windows 静态库

因此,我们首先需要映射 nss3.dll 中我们打算使用的函数。

fpNSS_Init = (NSS_Init_p)GetProcAddress(moduleNSS, "NSS_Init"); 
fpNSS_Shutdown = (NSS_Shutdown_p)GetProcAddress(moduleNSS, "NSS_Shutdown"); 
fpPL_ArenaFinish = (PL_ArenaFinish_p)GetProcAddress(moduleNSS, "PL_ArenaFinish"); 
fpPR_Cleanup = (PR_Cleanup_p)GetProcAddress(moduleNSS, "PR_Cleanup");
fpPK11_GetInternalKeySlot = 
  (PK11_GetInternalKeySlot_p)GetProcAddress(moduleNSS, "PK11_GetInternalKeySlot");
fpPK11_FreeSlot = (PK11_FreeSlot_p)GetProcAddress(moduleNSS, "PK11_FreeSlot");  
fpPK11SDR_Decrypt = (PK11SDR_Decrypt_p)GetProcAddress(moduleNSS, "PK11SDR_Decrypt"); 
PK11_CheckUserPassword = 
  (PK11CheckUserPassword)GetProcAddress(moduleNSS, "PK11_CheckUserPassword");

主密码

Firefox 引入了一项新的安全措施,称为主密码。主密码是一个集中的加密密钥,用于加密所有存储的凭据。此密钥已被哈希处理,但未存储在任何地方,因此无法找到它。

知道主密码时以编程方式解密凭据

我进行了一些测试,试图找出在设置了主密码的情况下解密 Firefox 存储的凭据需要做什么。首先,应该解释如何在知道主密码的情况下以编程方式解密凭据。

猜测主密码

或者,黑客可能会使用暴力破解技术尝试猜测主密码。这可以通过遍历所有可能的长度为 1-7 个字符的字符串(或字母数字字符串)的组合来完成(使用此方法几乎不可能破解更长的密码)。另一种方法是遍历常用密码列表。这种方法称为“字典”。

检查给定字符串所需的代码

以下代码检查“MyGuess”是否确实是主密码。如果是,运行我们工具的其余代码将解密所有存储的密码。

bool GuessMasterPassword(char *MyGuess)
{
    bool result = FALSE;

    PK11SlotInfo *pK11Slot = fpPK11_GetInternalKeySlot();
    if (PK11_CheckUserPassword(pK11Slot, MyGuess) == SECSuccess)
    {
        result = true;
    }
    (*PK11FreeSlot) (pK11Slot);
    return result;
}

如果 GuessMasterPassword 的返回结果为 true,那么我们可以继续。

解密凭据

我们代码的下一部分将解密凭据条目中的任何加密部分,并且无论主密码如何都应使用它。如果没有设置主密码,此代码仍需要执行,如果设置了主密码,我们假设 GuessMasterPassword 出于任何原因(您知道它、您猜到了它或您暴力破解了它)返回了“true”。

LPSTR DecryptString(LPSTR strCryptData)
{
    if (strCryptData[0] == 0x0)
        return FALSE;

    DWORD dwOut;
    LPSTR strClearData = "";
    LPBYTE lpBuffer = base64_decode((char*)strCryptData, strlen(strCryptData), (int *)&dwOut);
    PK11SlotInfo *pK11Slot = fpPK11_GetInternalKeySlot();

    if (pK11Slot)
    {
        SECItem pInSecItem, pOutSecItem;;
        pInSecItem.data = lpBuffer;
        pInSecItem.len = dwOut;

        pOutSecItem.data = 0;
        pOutSecItem.len = 0;

        if (fpPK11SDR_Decrypt(&pInSecItem, &pOutSecItem, NULL) == 0)
        {
            strClearData = (LPSTR)malloc(pOutSecItem.len + 1);
            memcpy(strClearData, pOutSecItem.data, pOutSecItem.len);
            *(strClearData+pOutSecItem.len) = '\0';
        }

        fpPK11_FreeSlot(pK11Slot);
    }
    return strClearData;
}

处理 Firefox 日期/时间

为了能够处理来自各种浏览器的记录,每种浏览器都有自己的日期和时间存储方法。我们需要将每个日期/时间记录转换为一种单一的格式,以便以后进行操作,例如查找给定日期或自上次检查以来的所有凭据。对于 Firefox,它将其日期存储为大整数。这个数字称为Epoch Time(纪元时间)。您可以将 Epoch Time 输入此网站,查看它代表的日期,反之亦然。

我们工具使用的代码旨在生成一个 CTime,以便我们基于 Windows MFC 的工具在将它们转换为这种 Windows 日期/时间格式后处理各种日期。通过将任何给定的日期/时间(或长整数)转换为两个中间格式:FILETIMESYSTEMTIME,可以做到这一点。我们函数的输入将是 std::string,因为将 XML 或 JSON 项先读取到 string 中更容易。

SYSTEMTIME FirefoxTimeToSysTime(string FirefoxTime)
{
    __int64 unixtime = atol(FirefoxTime.c_str());
    __int64 longLongVar = unixtime + EPOCH_DIFFERENCE;

    longLongVar = longLongVar*TICKS_PER_SECOND;

    FILETIME ftTime;

    ftTime.dwLowDateTime = (DWORD)longLongVar;
    ftTime.dwHighDateTime = longLongVar >> 32;

    SYSTEMTIME stTime;
    FileTimeToSystemTime(&ftTime, &stTime);
    return stTime;
}

Parson 库

Parson 是 Krzysztof Gabis 创建的一个 JSON 解析器。我发现它适合我们的项目,因为它轻巧且易于集成。

将 Firefox 日期/时间从 JSON 转换为 CTime

首先,我们从 JSON 元素中读取日期/时间,但仅使用前 10 个(共 13 个)字符。

 double tempnum = json_object_get_number(commit, strDateTime);
 string datetime;
 datetime = std::to_string(tempnum);
 datetime = datetime.substr(0, 10);

注释

  1. Parson 库提供了一个将数字读取为“double”的函数,这就是为什么我们首先将日期/时间读取到一个 double 变量中。
  2. 接下来,我们将 double 转换为 std::string
  3. 然后我们截取最后 3 位数字,使 string 的长度为 10 个字符。
  4. 我们调用 FirefoxTimeToSysTime() 并将我们的 string 传递给它。

SYSTEMTIME 转换为 CTime 是直接的。

SYSTEMTIME newtime = utils::FirefoxTimeToSysTime(datetime);
CTime ct(newtime);

之后,我们可以将数据显示在报告或屏幕上,将 CTime 变量转换为用户友好的日期格式。在 Secured Globe, Inc.,我们使用以下方法。

#define SG_FRIEDLY_DATEFORMAT L"%d-%m-%Y, %H:%M:%S"

因此,当我们想要使用存储的凭据并显示其某个元素时,我们将使用

credentials[i].DateCreated.FormatGmt(SG_FRIEDLY_DATEFORMAT).GetBuffer();

如果您希望处理来自不同国家/地区用户的时区数据,FormatGmt 很重要。对于本地使用,代码如下:

credentials[i].DateCreated.Format(SG_FRIEDLY_DATEFORMAT).GetBuffer();

Firefox 凭据查看器工具

当您启动 Firefox 凭据查看器时,一眨眼的功夫,您就会看到一个类似的屏幕。所有存储的凭据都将被显示。

您可以从 Source Forge 下载此工具。

历史

  • 2017 年 1 月 30 日:初始版本
© . All rights reserved.