Android M中的密码存储

Android M中的密码存储

Android已获得许多安全增强功能在最近的几个版本中,自2.x天以来,锁屏(也称为键盘保护)和密码存储几乎保持不变,除了增加了多用户支持外。Android M终于通过官方支持指纹认证的方式改变了这一点。尽管目前尚不提供与生物识别支持有关的代码,但是一些负责密码存储和用户身份验证的新代码在AOSP的master分支中部分可用。检查当前的Android M预览所使用的运行时行为和文件,可以发现已经部署了一些密码存储更改。这篇文章将简要回顾一下M版本之前的Android版本中如何实现密码存储,然后介绍Android M带来的变化。

键盘锁解锁方法

Stock Android提供了三种键盘锁解锁方法:图案,PIN和密码(Face Unlock已重命名为“ Trusted face”,并移至Google Play服务的专有Smart Lock扩展程序中)。模式解锁是原始的Android解锁方法,而PIN和密码(本质上是等效的)在2.2版本中添加。以下各节将讨论如何为模式和PIN /密码解锁方法注册,存储和验证凭据。

图案解锁

通过在3×3矩阵上连接至少四个点来输入Android的模式解锁(某些自定义ROM允许更大的矩阵)。每个点只能使用一次(忽略交叉点),最大点数为9。模式在内部转换为字节序列,每个点由其索引表示,其中0是左上角,8是右下角。因此,该模式类似于PIN码(最少四个,最多九个数字),仅使用九个不同的数字(0至8)。但是,由于不能重复点,因此与九位PIN的解锁图案相比,解锁图案的更改数量要少得多。作为图案解锁是由Android支持的原始和最初唯一解锁方法,一个公平的研究已经完成了它的安全性。已经表明,使用所谓的污迹攻击可以相当可靠地猜测模式,并且可能的组合总数少于40万,而4点(默认)模式只有1624个组合。

pattern lock

Android将解锁模式的未加盐SHA-1哈希存储在多用户设备之中/data/system/gesture.key/data/system/users/<user ID>/gesture.key之上。上方屏幕快照中显示的“ Z”模式看起来可能像这样。

$ od -tx1 pose.key
0000000 6a 06 2b 9b 34 52 e3 66 40 71 81 a1 bf 92 ea 73
0000020 e9 ed 4c 48

由于哈希值未加盐,因此很容易预先计算所有可能组合的哈希值并立即恢复原始模式。由于组合的数量非常少,因此哈希表不需要特殊的索引编制或文件格式优化,并且拥有文件后,只需使用grepxxd命令即可恢复模式gesture.key

$ grep`xxd -p pose.key` pattern_hashes.txt
00010204060708,6a062b9b3452e366407181a1bf92ea73e9ed4c48

PIN /密码解锁

PIN /密码解锁方法还依赖于存储的用户凭据散列,但是它也使用64位随机的,按用户显示的盐。盐locksettings.db与其他与锁屏有关的设置一起存储在SQLite数据库中。密码哈希保存在  /data/system/password.key 文件中,该文件包含密码的SHA-1和MD5哈希值的串联。该文件的内容可能如下所示:

$ cat password.key && echo
2E704465DB8C3CBFF085D8A5135A6F3CA32D5A2CA4A628AE48E22443250C30A3E1449BD0

请注意,哈希不是嵌套的,但是它们的值只是连接在一起的,因此,如果您要蛮力破解密码,则只需要攻击较弱的哈希MD5。另一个有用的事实是,为了启用密码审核,Android将有关当前PIN /密码格式的详细信息存储在device_policies.xml 文件中,如下所示:

<policies setup-complete="true">
...
<active-password length="6" letters="0" lowercase="0" nonletter="6" 
                 numeric="6" quality="196608" symbols="0" uppercase="0">
</active-password>
</policies>
pin lock

如果您能够获取该password.key文件,则很可能您也将拥有该device_policies.xml文件。通过指定掩码或密码规则,此文件为您提供了足够的信息,可以在恢复密码时大大缩小搜索空间。例如,我们可以使用 John the Ripper (JtR)通过指定?d?d?d?d?d?d掩码并使用“动态” MD5哈希格式(hashcat具有专用的  Android PIN哈希模式)轻松地在大约一秒钟内恢复以下6位数的密码下面。?l?l?l?l?l?l?l?l在同一硬件上,一个8个字符(),仅小写的密码需要花费几个小时。

$ cat lockscreen.txt
user:$dynamic_1$A4A628AE48E22443250C30A3E1449BD0$327d5ce3f570d2eb

$ ./john --mask=?d?d?d?d?d?d lockscreen.txt
Loaded 1 password hash (dynamic_1 [md5($p.$s) (joomla) 128/128 AVX 480x4x3])
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
456987           (user)
1g 0:00:00:00 DONE  6.250g/s 4953Kp/s 4953Kc/s 4953KC/s 234687..575297

只需删除gesture.key和  password.key文件,即可轻松重置Android的锁屏密码,因此您可能想知道强行使用密码的目的是什么。如前几篇文章所述,锁屏密码用于派生用于保护密钥库(如果不是由硬件支持)的密钥,VPN配置文件密码,备份以及磁盘加密密钥,因此如果尝试提取数据,则可能很有价值。这些服务中的任何一项。当然,特定用户在所有设备上使用相同模式,PIN或密码的机会非常高。

网闸密码存储

我们gatekeeper密钥库重新设计中简要介绍了Android M的守护程序,该守护程序与每个密钥授权令牌有关。事实证明,网守所要做的不只是此事,还负责注册(称为“注册”)和验证用户密码。注册会将纯文本密码转换为所谓的“密码句柄”,该密码句柄是不透明的,与实现相关的字节字符串。然后可以将密码句柄存储在磁盘上,并用于检查用户提供的密码是否与当前注册的句柄匹配。虽然网守HAL未指定密码句柄的格式,但是默认软件实现使用以下格式:

typedef uint64_t secure_id_t;
typedef uint64_t salt_t;

static const uint8_t HANDLE_VERSION = 2;
struct __attribute__ ((__packed__)) password_handle_t {
    // fields included in signature
    uint8_t version;
    secure_id_t user_id;
    uint64_t flags;

    // fields not included in signature
    salt_t salt;
    uint8_t signature[32];

    bool hardware_backed;
};

secure_id_t是随机生成的64位安全用户ID,它/data/misc/gatekeeper以用户的Android用户ID(* not * Linux UID;主要用户为0)保存在目录中的文件中。签名格式留给实现,但是AOSP的提交日志显示,对于当前的默认实现,它很可能是scrypt。其他网守实现可能选择使用受硬件保护的对称或非对称密钥来生成“真实”签名(或HMAC)。

HAL和当前可用的AOSP源代码都没有指定密码句柄的存储位置,但是通过浏览/data/system目录可以发现以下文件,其中一个文件的大小恰好与文件大小相同。password_handle_t结构体。这意味着它可能包含序列化的password_handle_t实例。

# ls -l /data/system/*key
-rw------- system   system         57 2015-06-24 10:24 gatekeeper.gesture.key
-rw------- system   system          0 2015-06-24 10:24 gatekeeper.password.key

不过,这是相当多的假设,因此现在是时候通过解析gatekeeper.gesture.key文件并检查signature字段是否与我们的锁屏模式的scrypt值(00010204060708以二进制表示)相匹配来进行验证。我们可以使用以下Python代码执行此操作:

$ cat m-pass-hash.py
...
N = 16384;
r = 8;
p = 1;

f = open('gatekeeper.gesture.key', 'rb')
blob = f.read()

s = struct.Struct('<'+'17s 8s 32s')
(meta, salt, signature) = s.unpack_from(blob)
password = binascii.unhexlify('00010204060708');
to_hash = meta
to_hash += password
hash = scrypt.hash(to_hash, salt, N, r, p)

print 'signature  %s' % signature.encode('hex')
print 'Hash:      %s' % hash[0:32].encode('hex')
print 'Equal:     %s' % (hash[0:32] == signature)

$./m-pass-hash.py
signature: 3d1a20985dec4bd937e5040aadb465fc75542c71f617ad090ca1c0f96950a4b8
Hash:      3d1a20985dec4bd937e5040aadb465fc75542c71f617ad090ca1c0f96950a4b8
Equal: True

上面的程序输出使我们相信,密码句柄文件中存储的“签名”确实是blob版本的scrypt值,64位安全用户ID和blob flags字段(与纯文本模式值连接)。使用存储的64位盐和scrypt参数N = 16384,r = 8,p = 1来计算scrypt哈希值。使用PIN /密码字符串值作为输入,以相同的方式计算PIN或密码的密码句柄。

使用这种新的散列方案,可以以相同的方式处理模式和密码,因此不再容易对模式进行暴力破解。就是说,借助device_policies.xml该文件提供了模式的长度和一个预先计算的模式表,可以大大减少要尝试的模式数量,因为大多数用户可能会使用4-6步模式(大约35,000个总组合)。

由于Androd M的密码哈希算法在计算scrypt值时不会直接使用纯文本密码,因此无法直接使用经过优化的密码恢复工具(例如hashcatJtR)来评估暴力成本。但是,假设同时使用device_policies.xml和,构建我们自己的工具来检查简单的PIN抵御暴力攻击的方式也相当容易。gatekeeper.password.key已获取文件。如下所示,当一个简单的Python脚本在与我们之前的JtR示例相同的硬件上运行时,按顺序尝试从0000到9999的所有PIN大约需要10分钟(使用相同的6位PIN大约需要17个小时程序)。相比之下,对于Android 5.1(或更早版本)的6位PIN进行暴力破解,不到一秒钟,并且很明显,Android M引入的新哈希方案即使在使用简单PIN时也大大提高了密码存储的安全性。当然,正如我们前面提到的,网守守护程序是Android HAL的一部分,因此供应商可以自由地使用更多(或更少…)安全的网守实现。

$ time ./m-pass-hash.py gatekeeper.password.key 4
Trying 0000...
Trying 0001...
Trying 0002...

...
Trying 9997...
Trying 9998...
Trying 9999...
Found PIN: 9999

real    9m46.118s
user    9m6.804s
sys     0m39.107s

框架API

Android M仍处于预览阶段,因此框架API几乎不稳定,但是我们将展示网守的AIDL接口以确保完整性。在当前的预览版本中,它被称为IGateKeeperService,如下所示:

interface android.service.gatekeeper.IGateKeeperService {

    void clearSecureUserId(int uid);

    byte[] enroll(int uid, byte[] currentPasswordHandle, 
                  byte[] currentPassword, byte[] desiredPassword);

    long getSecureUserId(int uid);

    boolean verify(int uid, byte[] enrolledPasswordHandle, byte[] providedPassword);

    byte[] verifyChallenge(int uid, long challenge, 
                           byte[] enrolledPasswordHandle, byte[] providedPassword);
}

如您所见,该界面提供了用于生成/获取和清除特定用户的安全用户ID的方法,以及提供了  enroll()verify()verifyChallenge()其参数与较低级别的HAL接口紧密匹配的方法。为了验证是否存在一个实现此接口的实时服务,我们可以尝试getSecureUserId()使用service命令行实用工具调用该方法,如下所示:

$ service call android.service.gatekeeper.IGateKeeperService 4 i32 0
Result: Parcel(00000000 ee555c25 ea679e08  '....%\U...g.')

这将返回具有主要用户(用户ID 0)安全用户ID的Binder包裹,该用户ID与/data/misc/gatekeeper/0以下所示存储的值(以网络字节顺序存储)匹配。

#od -tx1 / data / misc / gatekeeper / 0
37777776644 25 5c 55 ee 08 9e 67 ea
37777776644

与以前的版本一样 ,密码散列(句柄)的实际存储由LockSettingsService(接口ILockSettings)执行。该服务已扩展为支持新的网闸密码句柄格式,以及将旧式哈希迁移到新格式。通过调用checkPassword(String password, int userId)如果密码匹配则返回true 的方法很容易验证这一点:

# service call lock_settings 11 s16 1234 i32 0
Result: Parcel(00000000 00000000   '........')
# service call lock_settings 11 s16 9999 i32 0
Result: Parcel(00000000 00000001   '........')

摘要

Android M引入了一项新的系统服务-Gatekeeper,该服务负责将纯文本密码转换为可以安全存储在磁盘上的不透明二进制Blob(称为密码句柄)。该网守是Android HAL的一部分,因此可以对其进行修改以利用设备的本机安全功能,例如安全存储或TEE,而无需修改核心平台。当前Android M预览版本随附的默认实现使用scrypt来哈希解锁图案,PIN或密码,并且比以前使用的单轮MD5和SHA-1哈希提供更好的防止暴力破解保护。 

0