IE/Edgeに保存されているログインデータを出力してみる


はじめに

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

IE/Edgeのログインデータの保存方法

調べてみたところ、ログインデータは「Windows Vaults」と呼ばれる特殊なフォルダに保存されていることがわかりました。
具体的には、C:\Windows\System32\vaultcli.dllに含まれているWindows VaultsのAPIを使用してアクセスする仕組みになっているようです。

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

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


using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;

namespace FetchVaultCredential
{
    public static class Program
    {
        public enum VAULT_ELEMENT_TYPE : int
        {
            Undefined = -1,
            Boolean = 0,
            Short = 1,
            UnsignedShort = 2,
            Int = 3,
            UnsignedInt = 4,
            Double = 5,
            Guid = 6,
            String = 7,
            ByteArray = 8,
            TimeStamp = 9,
            ProtectedArray = 10,
            Attribute = 11,
            Sid = 12,
            Last = 13
        }

        public enum VAULT_SCHEMA_ELEMENT_ID : int
        {
            Illegal = 0,
            Resource = 1,
            Identity = 2,
            Authenticator = 3,
            Tag = 4,
            PackageSid = 5,
            AppStart = 100,
            AppEnd = 10000
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
        public struct VAULT_ITEM_WIN8
        {
            public Guid SchemaId;
            public IntPtr pszCredentialFriendlyName;
            public IntPtr pResourceElement;
            public IntPtr pIdentityElement;
            public IntPtr pAuthenticatorElement;
            public IntPtr pPackageSid;
            public ulong LastModified;
            public int dwFlags;
            public int dwPropertiesCount;
            public IntPtr pPropertyElements;
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
        public struct VAULT_ITEM_WIN7
        {
            public Guid SchemaId;
            public IntPtr pszCredentialFriendlyName;
            public IntPtr pResourceElement;
            public IntPtr pIdentityElement;
            public IntPtr pAuthenticatorElement;
            public ulong LastModified;
            public int dwFlags;
            public int dwPropertiesCount;
            public IntPtr pPropertyElements;
        }

        [StructLayout(LayoutKind.Explicit, CharSet = CharSet.Ansi)]
        public struct VAULT_ITEM_ELEMENT
        {
            [FieldOffset(0)] public VAULT_SCHEMA_ELEMENT_ID SchemaElementId;
            [FieldOffset(8)] public VAULT_ELEMENT_TYPE Type;
        }

        [DllImport("vaultcli.dll")]
        public extern static int VaultOpenVault(ref Guid vaultGuid, int offset, ref IntPtr vaultHandle);

        [DllImport("vaultcli.dll")]
        public extern static int VaultCloseVault(ref IntPtr vaultHandle);

        [DllImport("vaultcli.dll")]
        public extern static int VaultFree(ref IntPtr vaultHandle);

        [DllImport("vaultcli.dll")]
        public extern static int VaultEnumerateVaults(int offset, ref int vaultCount, ref IntPtr vaultGuid);

        [DllImport("vaultcli.dll")]
        public extern static int VaultEnumerateItems(IntPtr vaultHandle, int chunkSize, ref int vaultItemCount, ref IntPtr vaultItem);

        [DllImport("vaultcli.dll", EntryPoint = "VaultGetItem")]
        public extern static int VaultGetItem_WIN8(IntPtr vaultHandle, ref Guid schemaId, IntPtr pResourceElement, IntPtr pIdentityElement, IntPtr pPackageSid, IntPtr zero, int arg6, ref IntPtr passwordVaultPtr);

        [DllImport("vaultcli.dll", EntryPoint = "VaultGetItem")]
        public extern static int VaultGetItem_WIN7(IntPtr vaultHandle, ref Guid schemaId, IntPtr pResourceElement, IntPtr pIdentityElement, IntPtr zero, int arg5, ref IntPtr passwordVaultPtr);

        public static void GetLogins()
        {
            // Windows Vaultsをチェック
            var OSVersion = Environment.OSVersion.Version;
            var OSMajor = OSVersion.Major;
            var OSMinor = OSVersion.Minor;

            Type VAULT_ITEM;

            if (OSMajor >= 6 && OSMinor >= 2)
            {
                // Windows 8以降
                VAULT_ITEM = typeof(VAULT_ITEM_WIN8);
            }
            else
            {
                // Windows 7以前
                VAULT_ITEM = typeof(VAULT_ITEM_WIN7);
            }

            // VAULT_ITEM_ELEMENTからItemValueを取り出す
            object GetVaultElementValue(IntPtr vaultElementPtr)
            {
                object results;
                object partialElement = Marshal.PtrToStructure(vaultElementPtr, typeof(VAULT_ITEM_ELEMENT));
                FieldInfo partialElementInfo = partialElement.GetType().GetField("Type");
                var partialElementType = partialElementInfo.GetValue(partialElement);

                IntPtr elementPtr = (IntPtr)(vaultElementPtr.ToInt64() + 16);
                switch ((int)partialElementType)
                {
                    case 7: // String(パスワード)
                        IntPtr StringPtr = Marshal.ReadIntPtr(elementPtr);
                        results = Marshal.PtrToStringUni(StringPtr);
                        break;
                    case 0: // bool
                        results = Marshal.ReadByte(elementPtr);
                        results = (bool)results;
                        break;
                    case 1: // Short
                        results = Marshal.ReadInt16(elementPtr);
                        break;
                    case 2: // Unsigned Short
                        results = Marshal.ReadInt16(elementPtr);
                        break;
                    case 3: // Int
                        results = Marshal.ReadInt32(elementPtr);
                        break;
                    case 4: // Unsigned Int
                        results = Marshal.ReadInt32(elementPtr);
                        break;
                    case 5: // Double
                        results = Marshal.PtrToStructure(elementPtr, typeof(Double));
                        break;
                    case 6: // GUID
                        results = Marshal.PtrToStructure(elementPtr, typeof(Guid));
                        break;
                    case 12: // セキュリティ識別子
                        IntPtr sidPtr = Marshal.ReadIntPtr(elementPtr);
                        var sidObject = new System.Security.Principal.SecurityIdentifier(sidPtr);
                        results = sidObject.Value;
                        break;
                    default:
                        // VAULT_ELEMENT_TYPEが実装されていないので無視
                        results = null;
                        break;
                }
                return results;
            }

            int vaultCount = 0;
            // VaultのGUIDポインタ
            IntPtr vaultGuidPtr = IntPtr.Zero;
            // 全てのVaultを取得
            var result = VaultEnumerateVaults(0, ref vaultCount, ref vaultGuidPtr);

            if (result != 0)
            {
                throw new Exception("Vaultを取得できません。(0x" + result.ToString() + ")");
            }

            // GUID対応表
            IntPtr guidAddress = vaultGuidPtr;
            Dictionary<Guid, string> vaultSchema = new Dictionary<Guid, string>();
            vaultSchema.Add(new Guid("2F1A6504-0641-44CF-8BB5-3612D865F2E5"), "Windows Secure Note");
            vaultSchema.Add(new Guid("3CCD5499-87A8-4B10-A215-608888DD3B55"), "Windows Web Password Credential");
            vaultSchema.Add(new Guid("154E23D0-C644-4E6F-8CE6-5069272F999F"), "Windows Credential Picker Protector");
            vaultSchema.Add(new Guid("4BF4C442-9B8A-41A0-B380-DD4A704DDB28"), "Web Credentials");
            vaultSchema.Add(new Guid("77BC582B-F0A6-4E15-4E80-61736B6F3B29"), "Windows Credentials");
            vaultSchema.Add(new Guid("E69D7838-91B5-4FC9-89D5-230D4D4CC2BC"), "Windows Domain Certificate Credential");
            vaultSchema.Add(new Guid("3E0E35BE-1B77-43E7-B873-AED901B6275B"), "Windows Domain Password Credential");
            vaultSchema.Add(new Guid("3C886FF3-2669-4AA2-A8FB-3F6759A77548"), "Windows Extended Credential");
            vaultSchema.Add(new Guid("00000000-0000-0000-0000-000000000000"), null);

            for (int i = 0; i < vaultCount; i++)
            {
                // Vaultを開く
                object vaultGuidString = Marshal.PtrToStructure(guidAddress, typeof(Guid));
                Guid vaultGuid = new Guid(vaultGuidString.ToString());
                guidAddress = (IntPtr)(guidAddress.ToInt64() + Marshal.SizeOf(typeof(Guid)));
                IntPtr vaultHandle = IntPtr.Zero;
                string vaultType;
                if (vaultSchema.ContainsKey(vaultGuid))
                {
                    vaultType = vaultSchema[vaultGuid];
                }
                else
                {
                    vaultType = vaultGuid.ToString();
                }
                result = VaultOpenVault(ref vaultGuid, 0, ref vaultHandle);
                if (result != 0)
                {
                    throw new Exception(vaultType + "からVaultを開けませんでした。" + "(0x" + result.ToString() + ")");
                }

                // Vault内のアイテムを列挙
                int vaultItemCount = 0;
                IntPtr vaultItemPtr = IntPtr.Zero;
                result = VaultEnumerateItems(vaultHandle, 512, ref vaultItemCount, ref vaultItemPtr);
                if (result != 0)
                {
                    throw new Exception(vaultType + "からのVaultの列挙に失敗しました。" + "(0x" + result.ToString() + ")");
                }
                var structAddress = vaultItemPtr;
                if (vaultItemCount > 0)
                {
                    for (int j = 1; j <= vaultItemCount; j++)
                    {
                        // Vaultの列挙を開始
                        var currentItem = Marshal.PtrToStructure(structAddress, VAULT_ITEM);
                        structAddress = (IntPtr)(structAddress.ToInt64() + Marshal.SizeOf(VAULT_ITEM));

                        IntPtr passwordVaultItem = IntPtr.Zero;
                        // フィールド情報の検索
                        FieldInfo schemaIdInfo = currentItem.GetType().GetField("SchemaId");
                        Guid schemaId = new Guid(schemaIdInfo.GetValue(currentItem).ToString());
                        FieldInfo pResourceElementInfo = currentItem.GetType().GetField("pResourceElement");
                        IntPtr pResourceElement = (IntPtr)pResourceElementInfo.GetValue(currentItem);
                        FieldInfo pIdentityElementInfo = currentItem.GetType().GetField("pIdentityElement");
                        IntPtr pIdentityElement = (IntPtr)pIdentityElementInfo.GetValue(currentItem);
                        FieldInfo dateTimeInfo = currentItem.GetType().GetField("LastModified");
                        ulong lastModified = (ulong)dateTimeInfo.GetValue(currentItem);

                        IntPtr pPackageSid = IntPtr.Zero;
                        if (OSMajor >= 6 && OSMinor >= 2)
                        {
                            // 新しいバージョンにはパッケージSIDがある
                            FieldInfo pPackageSidInfo = currentItem.GetType().GetField("pPackageSid");
                            pPackageSid = (IntPtr)pPackageSidInfo.GetValue(currentItem);
                            result = VaultGetItem_WIN8(vaultHandle, ref schemaId, pResourceElement, pIdentityElement, pPackageSid, IntPtr.Zero, 0, ref passwordVaultItem);
                        }
                        else
                        {
                            result = VaultGetItem_WIN7(vaultHandle, ref schemaId, pResourceElement, pIdentityElement, IntPtr.Zero, 0, ref passwordVaultItem);
                        }

                        if (result != 0)
                        {
                            throw new Exception("Error occured while retrieving vault item. Error: 0x" + result.ToString());
                        }
                        object passwordItem = Marshal.PtrToStructure(passwordVaultItem, VAULT_ITEM);
                        FieldInfo pAuthenticatorElementInfo = passwordItem.GetType().GetField("pAuthenticatorElement");
                        IntPtr pAuthenticatorElement = (IntPtr)pAuthenticatorElementInfo.GetValue(passwordItem);
                        // 認証情報を取得
                        object cred = GetVaultElementValue(pAuthenticatorElement);
                        object packageSid = null;
                        if (pPackageSid != IntPtr.Zero && pPackageSid != null)
                        {
                            packageSid = GetVaultElementValue(pPackageSid);
                        }
                        if (cred != null) // 取得に成功したデータを表示
                        {
                            Console.WriteLine("Vault Type   : {0}", vaultType);
                            object resource = GetVaultElementValue(pResourceElement);
                            if (resource != null)
                            {
                                Console.WriteLine("Resource     : {0}", resource);
                            }
                            object identity = GetVaultElementValue(pIdentityElement);
                            if (identity != null)
                            {
                                Console.WriteLine("Identity     : {0}", identity);
                            }
                            if (packageSid != null)
                            {
                                Console.WriteLine("PacakgeSid  : {0}", packageSid);
                            }
                            Console.WriteLine("Credential   : {0}", cred);
                            Console.WriteLine("LastModified : {0}", System.DateTime.FromFileTimeUtc((long)lastModified));
                            Console.WriteLine();
                        }
                    }
                }
            }
        }

        public static void Main(string[] args)
        {
            GetLogins();
            Console.ReadKey(true);
        }
    }
}

実行する際は、ビルド設定の「32 ビットを選ぶ」のチェックを外してください。

仕組みとしては、Windows VaultのAPIをDllImportで読み込み、それを使ってVaultの構造体を取得してデータを取り出しています。
取得したデータ自体に暗号化などはされていないようです。

これを実行すると、以下のようにログインデータが出力されます。

Vault Type   : Web Credentials
Resource     : https://login.microsoftonline.com/
Identity     : [email protected]
Credential   : TekitounaPassword
LastModified : 2020/05/23 15:50:49

Vault Type   : Web Credentials
Resource     : https://accounts.google.com/
Identity     : [email protected]
Credential   : SecurePass123456
LastModified : 2020/05/26 11:52:57

おわりに

という訳で、IE/Edgeに保存されているログインデータも取得できてしまいました。
実は、Windows Vaultsに関するAPIの仕様はMicrosoft公式からは公開されていません。
だから、Microsoftは特にログインデータに暗号化処理をしなかったのだと思います。
しかし、海外のハッカー達にとってWindows Vaultsの仕様を一から自力で解析することなど朝飯前だったようです。恐ろしいですね。

参考文献