LoginSignup
3
2

More than 3 years have passed since last update.

Firefoxに保存されているログインデータを解読してみる

Last updated at Posted at 2020-06-06

はじめに

Chromeに保存されているパスワードを解読してみる
IE/Edgeに保存されているログインデータを出力してみる
に引き続き、今回はFirefoxがどのようにWebサイトのログインデータを保存しているのか調べてみました。

Firefoxのログインデータの保存方法

調べてみたところ、FirefoxはNSS(Network Security Services)と呼ばれるライブラリを使用してユーザー名とパスワードを暗号化し、Base64エンコードしてプロファイルフォルダのlogins.jsonに保存していることがわかりました。

プロファイルは
ユーザーフォルダ\AppData\Roaming\Mozilla\Firefox\Profiles
でフォルダごとに管理されています。
また、NSSライブラリは、Firefoxをインストールしたフォルダ\nss3.dllに格納されています。

ログインデータを読み込んでみる

ログインデータを読み込むプログラムを実装してみます。
前まではC#を使っていましたが、今回はどうもうまくいかなかったのでPythonを使います。

まずは復号化に必要なNSSライブラリを読み込みます。
PK11SlotInfo構造体は不透明(Opaque)であるため、メンバを定義していません。


# DLL読み込み
dllpath = os.path.join(os.environ["programfiles"], "Mozilla Firefox\\nss3.dll")
nss3 = ct.CDLL(dllpath)

# 関数を読み込む
def getfunc(restype, name, *argtypes):
    res = getattr(nss3, name)
    res.restype = restype
    res.argtypes = argtypes
    return res

class SECItem(ct.Structure):
    _fields_ = [
        ('type', ct.c_uint),
        ('data', ct.c_char_p),
        ('len', ct.c_uint),
    ]

class PK11SlotInfo(ct.Structure):
    # 不透明な構造体
    pass

SlotInfoPtr = ct.POINTER(PK11SlotInfo)
SECItemPtr = ct.POINTER(SECItem)

NSS_Init = getfunc(ct.c_int, "NSS_Init", ct.c_char_p)
NSS_Shutdown = getfunc(ct.c_int, "NSS_Shutdown")
PK11_GetInternalKeySlot = getfunc(SlotInfoPtr, "PK11_GetInternalKeySlot")
PK11_FreeSlot = getfunc(None, "PK11_FreeSlot", SlotInfoPtr)
PK11_CheckUserPassword = getfunc(ct.c_int, "PK11_CheckUserPassword", SlotInfoPtr, ct.c_char_p)
PK11SDR_Decrypt = getfunc(ct.c_int, "PK11SDR_Decrypt", SECItemPtr, SECItemPtr, ct.c_void_p)
SECITEM_ZfreeItem = getfunc(None, "SECITEM_ZfreeItem", SECItemPtr, ct.c_int)

次に、コンピュータに保存されているプロファイルを列挙し、ログインデータが保存されている(logins.jsonがある)プロファイルのみを抽出します。


# プロファイルを列挙
def getprofiles():
    profdir = os.path.join(os.environ["appdata"], "Mozilla\\Firefox\\Profiles")
    files = os.listdir(profdir)
    profiles = [os.path.join(profdir, f) for f in files if os.path.isfile(os.path.join(profdir, f, "logins.json"))]

    return profiles

プロファイルを一つ選び、そのプロファイルでNSSを初期化します。


profiles = getprofiles()
print("プロファイルの番号を入力してください。")
for i in range(len(profiles)):
    print("%d: %s" % (i, profiles[i]))
number = int(input("番号: "))

encprof = profiles[number].encode("utf8")
# NSS初期化
e = NSS_Init(b"sql:" + encprof)
if e != 0:
    raise Exception("NSSの初期化に失敗しました。")

Firefoxでは個人情報を保護するため、プロファイルにマスターパスワードを設定できます。
マスターパスワードが設定されている場合、PK11_CheckUserPassword関数で認証する必要があります。


keyslot = PK11_GetInternalKeySlot()
if not keyslot:
    raise Exception("Keyslotを取得できませんでした。")
askpass = input("マスターパスワードを入力してください: ")
if askpass:
    e = PK11_CheckUserPassword(keyslot, askpass.encode("utf8"))
    if e != 0:
        raise Exception("マスターパスワードが正しくありません。")
else:
    print("パスワードなし")
PK11_FreeSlot(keyslot)

次に、logins.jsonを読み込みます。
かなり省略していますが、logins.jsonの構造は以下のようになっています。

{
    "logins": [
        {
            "hostname": "https://id.unity.com",
            "encryptedUsername": "暗号化されたユーザー名1",
            "encryptedPassword": "暗号化されたパスワード1",
            "encType": 1
        },
        {
            "hostname": "https://accounts.google.com",
            "encryptedUsername": "暗号化されたユーザー名2",
            "encryptedPassword": "暗号化されたパスワード2",
            "encType": 1
        },
    ]
}

logins.jsonを読み込み、

def getcreds(profile):
    db = os.path.join(profile, "logins.json")
    with open(db) as fh:
        data = json.load(fh)
        try:
            logins = data["logins"]
        except Exception:
            raise Exception("{0}を読み込めません。".format(db))

        for i in logins:
            yield (i["hostname"], i["encryptedUsername"],
                   i["encryptedPassword"], i["encType"])

最後に復号化して表示し、NSSを閉じておきます。

for url, user, passw, enctype in getcreds(profiles[number]):
    if enctype:
        user = decode(user)
        passw = decode(passw)
        print("Url: " + url)
        print("Username: " + user)
        print("Password: " + passw)

NSS_Shutdown()

肝心の復号化関数decodeの中身は以下のようになっています。

def decode(data64):
    data = b64decode(data64)
    inp = SECItem(0, data, len(data))
    out = SECItem(0, None, 0)

    e = PK11SDR_Decrypt(inp, out, None)
    try:
        if e == -1:
            print("復号化に失敗しました。")
            exit()

        res = ct.string_at(out.data, out.len).decode("utf8")
    finally:
        # SECItemを開放
        SECITEM_ZfreeItem(out, 0)

    return res

SECItem構造体にBase64デコードしたデータを入れ、PK11SDR_Decrypt関数に渡すと、復号化されたデータが返ってきます。

ログインデータを読み込むプログラム

Firefoxのログインデータを出力するプログラムは以下のようになります。

import ctypes as ct
import os
import json
from base64 import b64decode

# DLL読み込み
dllpath = os.path.join(os.environ["programfiles"], "Mozilla Firefox\\nss3.dll")
nss3 = ct.CDLL(dllpath)

# 関数読み込み
def getfunc(restype, name, *argtypes):
    res = getattr(nss3, name)
    res.restype = restype
    res.argtypes = argtypes
    return res

class SECItem(ct.Structure):
    _fields_ = [
        ('type', ct.c_uint),
        ('data', ct.c_char_p),
        ('len', ct.c_uint),
    ]

class PK11SlotInfo(ct.Structure):
    # 不透明な構造体
    pass

SlotInfoPtr = ct.POINTER(PK11SlotInfo)
SECItemPtr = ct.POINTER(SECItem)

NSS_Init = getfunc(ct.c_int, "NSS_Init", ct.c_char_p)
NSS_Shutdown = getfunc(ct.c_int, "NSS_Shutdown")
PK11_GetInternalKeySlot = getfunc(SlotInfoPtr, "PK11_GetInternalKeySlot")
PK11_FreeSlot = getfunc(None, "PK11_FreeSlot", SlotInfoPtr)
PK11_CheckUserPassword = getfunc(ct.c_int, "PK11_CheckUserPassword", SlotInfoPtr, ct.c_char_p)
PK11SDR_Decrypt = getfunc(ct.c_int, "PK11SDR_Decrypt", SECItemPtr, SECItemPtr, ct.c_void_p)
SECITEM_ZfreeItem = getfunc(None, "SECITEM_ZfreeItem", SECItemPtr, ct.c_int)

# 復号化処理
def decode(data64):
    data = b64decode(data64)
    inp = SECItem(0, data, len(data))
    out = SECItem(0, None, 0)

    e = PK11SDR_Decrypt(inp, out, None)
    try:
        if e == -1:
            print("復号化に失敗しました。")
            exit()

        res = ct.string_at(out.data, out.len).decode("utf8")
    finally:
        # SECItemを開放
        SECITEM_ZfreeItem(out, 0)

    return res

# プロファイルを列挙
def getprofiles():
    profdir = os.path.join(os.environ["appdata"], "Mozilla\\Firefox\\Profiles")
    files = os.listdir(profdir)
    profiles = [os.path.join(profdir, f) for f in files if os.path.isfile(os.path.join(profdir, f, "logins.json"))]

    return profiles

# Json読み込み
def getcreds(profile):
    db = os.path.join(profile, "logins.json")
    with open(db) as fh:
        data = json.load(fh)
        try:
            logins = data["logins"]
        except Exception:
            raise Exception("{0}を読み込めません。".format(db))

        for i in logins:
            yield (i["hostname"], i["encryptedUsername"],
                   i["encryptedPassword"], i["encType"])

def main():
    profiles = getprofiles()
    print("プロファイルの番号を入力してください。")
    for i in range(len(profiles)):
        print("%d: %s" % (i, profiles[i]))
    number = int(input("番号: "))

    # NSS初期化
    encprof = profiles[number].encode("utf8")
    e = NSS_Init(b"sql:" + encprof)
    if e != 0:
        raise Exception("NSSの初期化に失敗しました。")

    # パスワード認証
    keyslot = PK11_GetInternalKeySlot()
    if not keyslot:
        raise Exception("Keyslotを取得できませんでした。")
    askpass = input("マスターパスワードを入力してください: ")
    if askpass:
        e = PK11_CheckUserPassword(keyslot, askpass.encode("utf8"))
        if e != 0:
            raise Exception("マスターパスワードが正しくありません。")
    else:
        print("パスワードなし")
    PK11_FreeSlot(keyslot)

    # 復号化して出力
    for url, user, passw, enctype in getcreds(profiles[number]):
        if enctype:
            user = decode(user)
            passw = decode(passw)
            print("Url: " + url)
            print("Username: " + user)
            print("Password: " + passw)

    NSS_Shutdown()

main()

それでは、プログラムを動かしてみます。

実行すると読み込むプロファイルを聞かれるので、その番号を入力してください。

プロファイルの番号を入力してください。
0: C:\Users\admin\AppData\Roaming\Mozilla\Firefox\Profiles\aaaaaaaa.TestProfile
1: C:\Users\admin\AppData\Roaming\Mozilla\Firefox\Profiles\bbbbbbbb.default-release
番号: 

次に、マスターパスワードを入力します。設定してない場合はそのままEnterキーを押します。

マスターパスワードを入力してください:

するとログインデータが出力されます。

Url: https://id.unity.com
Username: admin
Password: SecurePass9999

Url: https://accounts.google.com
Username: TekitounaAddress@gmail.com
Password: passwd314159

おわりに

という訳で、今回はFirefoxのログインデータを解析してみました。
ChromeやEdge/IEよりも簡単な仕組みだったように感じますが、マスターパスワードを設定すればどのブラウザよりも強固なセキュリティになると思います。

現在、マスターパスワードを直接解読する術はないようなので、結論としては、Firefoxにマスターパスワードを設定して使うのが一番安全だと思われます。

余談: C#で実装できなかった件について

以下のコードがC#で実装しようとしたコードです。


using System;
using System.Text;
using System.Runtime.InteropServices;
using System.IO;
using Newtonsoft.Json;

namespace FirefoxDecrypt
{
    struct SECItem
    {
        public uint type;
        public byte[] data;
        public uint len;
    }

    public class Logins
    {
        public Credential[] logins { get; set; }
    }

    public class Credential
    {
        public string hostname { get; set; }
        public string encryptedUsername { get; set; }
        public string encryptedPassword { get; set; }
        public int encType { get; set; }
    }

    class Program
    {
        const string nss = "C:\\Program Files\\Mozilla Firefox\\nss3.dll";
        const string profile = "C:\\Users\\user01\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\xxxxxxxx.default-release";

        [DllImport(nss)]
        static extern int NSS_Init(string s);
        [DllImport(nss)]
        static extern int NSS_Shutdown();
        [DllImport(nss)]
        static extern int PK11SDR_Decrypt(ref SECItem s1, ref SECItem s2, IntPtr ptr);
        [DllImport(nss)]
        static extern void SECITEM_ZfreeItem(ref SECItem s, int i);

        static void Main(string[] args)
        {
            int e = NSS_Init("sql:" + profile);
            Console.WriteLine("NSS_Init: " + e);

            string db = Path.Combine(profile, "logins.json");
            string v = File.ReadAllText(db);
            var data = JsonConvert.DeserializeObject<Logins>(v);
            foreach(var login in data.logins)
            {
                if(login.encType == 1)
                {
                    var user = Decode(login.encryptedUsername);
                    var pass = Decode(login.encryptedPassword);
                    Console.WriteLine("URL: " + login.hostname);
                    Console.WriteLine("Username: " + user);
                    Console.WriteLine("Password: " + pass);
                }
            }

            NSS_Shutdown();
            Console.ReadKey(true);

        }
        static string Decode(string b64)
        {
            var bin = Convert.FromBase64String(b64);

            // 多分この辺りに問題がある
            SECItem inp = new SECItem { data = bin, len = (uint)bin.Length, type = 0 };
            SECItem outs = new SECItem { data = null, len = 0, type = 0 };

            var e = PK11SDR_Decrypt(ref inp, ref outs, IntPtr.Zero);
            Console.WriteLine("PK11SDR_Decrypt: " + e);

            var res = Encoding.UTF8.GetString(outs.data);
            SECITEM_ZfreeItem(ref outs, 0);
            return res;
        }
    }
}

Python版とほぼ同じ処理をしているはずなのですが、DecodeメソッドのPK11SDR_Decryptを呼び出した際に、どうしてもエラーコード-1が返ってきてしまいます。
Base64デコードしたデータに問題はなかったので、SECItem構造体に関して問題があるのだと思いますが、結局原因がわからず諦めました。
誰か原因がわかる方がいましたら、是非コメント欄で教えてほしいです。流石にいないか...

追記(2020/6/12)

コメント欄にてSECItem構造体のメモリアライメントが原因とのご指摘を受け、再度調べてみたところ、無事修正できたので追記しておきます。

dataは8bytesである必要があるそうで、これをbyte[]型にしていたのが原因だったようです。
以下のように、byte[]をIntPtrに変えました。

struct SECItem
{
    public uint type;
    public IntPtr data;
    public uint len;
}

また、Decodeメソッドを以下のように修正しました。

static string Decode(string b64)
{
    var bin = Convert.FromBase64String(b64);
    // binをIntPtrに変換
    IntPtr binptr = Marshal.AllocHGlobal(bin.Length);
    Marshal.Copy(bin, 0, binptr, bin.Length);

    SECItem inp = new SECItem { data = binptr, len = (uint)bin.Length, type = 0 };
    SECItem outs = new SECItem { data = IntPtr.Zero, len = 0, type = 0 };

    var e = PK11SDR_Decrypt(ref inp, ref outs, IntPtr.Zero);
    Console.WriteLine("PK11SDR_Decrypt: " + e);

    // IntPtrからstringを取得
    var data = new byte[outs.len];
    Marshal.Copy(outs.data, data, 0, data.Length);
    var res = Encoding.UTF8.GetString(data);

    SECITEM_ZfreeItem(ref outs, 0);
    return res;
}

これで動くはずです。
また、ビルド設定の「32ビットを選ぶ」のチェックを外さないと動かないので注意してください。
マスターパスワードの認証については割愛します。

参考文献

3
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2