本文的贡献在于:第一个给出了当encrypted_value以v10或v11开头时Windows平台下的解密方法(Java实现)及分析过程,测试版本为最新的Chrome 80.0.3987.106。代码位于:https://github.com/mlkui/chrome-cookie-password-decryption
解密Chrome浏览器Cookie可以有如下几个用途:
1)弥补WebDriver的不足,主要由于WebDriver相关Cookie操作的API大多仅能针对当前domain,处理极为不便。实际上笔者正是受制于此,而决定直接在Cookie的物理存储层面而非WebDriver层面处理Cookie。当然,如果能够容忍使用headless模式时的不便,也可以使用《解决Chrome浏览器无法通过–disable-cookie-encryption禁用Cookie加密的问题》一文中的方法禁用Cookie加密。
2)非法窃取Cookie拿到别人的登陆状态;
3)非法窃取Chrome浏览器中保存的密码;
Chrome的Cookie默认是加密的,根据不同的操作系统位于https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md所述的路径中,例如:
1 |
C:\Users\Alice\AppData\Local\Google\Chrome\User Data\Default\Cookies |
Cookies文件是一个SQLite3文件,被加密的Cookie位于encrypted_value中:
上述encrypted_value是被对称加密的,根据操作系统的不同,加密的Key分别保存在:
1)Windows,当前用户的ProtectedData中;
2)Linux,固定的密钥或钥匙链;
3)Mac,钥匙链;
参考资料5中给出了很多重要的讨论,例如使用secret-tool查找密钥等,但没有针对Windows的讨论。
实际上,Chrome的源码中(https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_unittest.cc?q=CryptProtectData&dr=C)我们可以看出Windows下有两种加密方式:
// This test verifies that the header of the data returned from CryptProtectData
// never collides with the kEncryptionVersionPrefix (“v10”) used in
// os_crypt_win.cc. If this ever happened, we would not be able to distinguish
// between data encrypted using the legacy DPAPI interface, and data that’s been
// encrypted with the new session key.
我们可以在Cookies文件对应的SQLite文件中验证:
当encrypted_value不以v10或v11开头时,在Windows下是使用Data Protection API(DPAPI)进行加密的,我们可以很容易地进行解密(下面为C#):
1 2 3 4 5 6 7 |
private void button1_Click(object sender, EventArgs e) { byte[] encryptedData = Convert.FromBase64String("djEwc12zLGDumKnXWs2YWYCp+ZUWYFGY+VhhJ6jtYAg6/vGR5qzPyBDMPm8VF0Yo+BRbikc3lTJkXjydr7GAsUPXMgsuwwL3O5wun3hik9p1uHrtbJTy1n7QBQGm4CdML9Cz0GsA3nOo6eIvMoQq5YuFK8Ka6QZ08LBO8WTUrscqyYhkeZZdCpAf8Ooje6D9W0G3PK20L5t3t1TJoCvnCaJjZH3TFIKVRf3MwTt0lfsf63d7j2gMkANs5vWPi1q8/vv4zDBj7oYCWHgl9CM="); var decodedData = System.Security.Cryptography.ProtectedData.Unprotect(encryptedData, null, System.Security.Cryptography.DataProtectionScope.CurrentUser); var plainText = Encoding.ASCII.GetString(decodedData); MessageBox.Show(plainText); } |
如果是Java的话,GitHub上有一个工程https://github.com/benjholla/CookieMonster有一些基本的逻辑可以直接拿来用。不过,该工程中用于操作JNI调用DPAPI并不能正常工作,可以使用另外的一个库windpapi4j:
1 2 3 4 |
WinDPAPI winDPAPI = WinDPAPI.newInstance(CryptProtectFlag.CRYPTPROTECT_UI_FORBIDDEN); decryptedBytes = winDPAPI.unprotectData(encryptedValue); String decryptedMessage = new String(decryptedBytes, "UTF-8"); System.out.println(decryptedMessage); |
接下来继续研究当encrypted_value以v10或v11开头时的处理逻辑,对于Linux和Mac而言可以直接参考参考资料4和参考资料5,然而对于Windows而言则是搜遍全网无任何资料。无奈只能看源码,在Chrome源码中,我们可以看到从Local State中读取了加密用Key:
Local State是一个JSON格式的文件:
结合源码,我们可以看到该encrypted_key为BASE64编码:
1 2 |
// Contains base64 random key encrypted with DPAPI. const char kOsCryptEncryptedKeyPrefName[] = "os_crypt.encrypted_key"; |
并以DPAPI开头:
1 2 |
// Key prefix for a key encrypted with DPAPI. const char kDPAPIKeyPrefix[] = "DPAPI"; |
密钥和NONCE/IV的长度分别为:
1 2 3 4 5 |
// AEAD key length in bytes. const size_t kKeyLength = 256 / 8; // AEAD nonce length in bytes. const size_t kNonceLength = 96 / 8; |
解密的方法为也可以在源码中看到(https://cs.chromium.org/chromium/src/components/os_crypt/os_crypt_win.cc?q=OSCrypt&dr=CSs):
可见,encrypted_value的前缀v10后为12字节的NONCE(IV),然后再是真正的密文。Chrome使用的是AES-256-GCM的AEAD对称加密,使用BoringsSSL实现:
其中的GCM_TAG_LENGTH=16需要重点关注。
对于Java而言,AES-256-GCM的实现方式有两种:
1、使用Java原生代码,但需要配置JCE。在J2EE中(JDK和JRE的7、8版本均包括),受JCE providers的进出口许可问题(The Export/Import Issues)影响,KeyGenerator生成的默认AES密钥为128bit=16Byte,配置JCE的方法可以参见笔者数年前的《Android与J2EE之间AES对称加密算法默认密钥长度不一致导致互操作失败的坑》一文,代码可以参考https://javainterviewpoint.com/java-aes-256-gcm-encryption-and-decryption;
2、使用Bouncy Castle;
我们直接基于Bouncy Castle,实现与Chrome源码中C++代码一致的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.security.Security; import java.util.Arrays; import javax.crypto.Cipher; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Base64; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.Test; import com.github.windpapi4j.WinDPAPI; import com.github.windpapi4j.WinDPAPI.CryptProtectFlag; import com.mlkui.common.helper.HexDumpHelper; public class ChromeCookieTest { @Test public void test() { Security.addProvider(new BouncyCastleProvider()); int keyLength = 256 / 8; int nonceLength = 96 / 8; String kEncryptionVersionPrefix = "v10"; int GCM_TAG_LENGTH = 16; try { byte[] encryptedKeyBytes = Base64.decodeBase64( "RFBBUEkBAAAA0Iyd3wEV0RGMegDAT8KX6wEAAACtP/XjPc54RaDgxj8Eef6QAAAAAAIAAAAAABBmAAAAAQAAIAAAAHICJrBWI0qPkRwEH6iO4zyo4cupd1kX23HTvlGjbf3rAAAAAA6AAAAAAgAAIAAAANV1OHmguiAyFxk6vAFOb+K1bwNCjUshXByRmwbXYd8mMAAAAKzGG5QNq4NliAapY5N3rKaS+kqJNmYJJrla5tZ7LS/9Z39jogumIA0zjkypIiG7EkAAAACrrL+zxA4OSuc8onLmqfimVj/lhh21n8ERnTNMk+67dFnoy2KzcUgk8mJfKforKbgNRH5RcVcNOh4Lz/LcUgMu"); System.out.println(new String(encryptedKeyBytes)); assertTrue(new String(encryptedKeyBytes).startsWith("DPAPI")); encryptedKeyBytes = Arrays.copyOfRange(encryptedKeyBytes, "DPAPI".length(), encryptedKeyBytes.length); WinDPAPI winDPAPI = WinDPAPI.newInstance(CryptProtectFlag.CRYPTPROTECT_UI_FORBIDDEN); byte[] keyBytes = winDPAPI.unprotectData(encryptedKeyBytes); System.out.println(Base64.encodeBase64String(keyBytes)); System.out.println(HexDumpHelper.dumpHexString(keyBytes)); assertEquals(keyLength, keyBytes.length); byte[] encryptedValue = new byte[] { (byte) 0x76, (byte) 0x31, (byte) 0x30, (byte) 0x74, (byte) 0x90, (byte) 0x31, (byte) 0x14, (byte) 0xed, (byte) 0x50, (byte) 0xac, (byte) 0x4e, (byte) 0xf9, (byte) 0x7e, (byte) 0x6b, (byte) 0x62, (byte) 0x17, (byte) 0x01, (byte) 0x52, (byte) 0x5f, (byte) 0x52, (byte) 0x78, (byte) 0xcd, (byte) 0x24, (byte) 0x1a, (byte) 0x34, (byte) 0x0f, (byte) 0xc2, (byte) 0xbd, (byte) 0x2a, (byte) 0xc3, (byte) 0x2f, (byte) 0x00, (byte) 0x5d, (byte) 0x0d, (byte) 0x56, (byte) 0xf5, (byte) 0xa6, (byte) 0x84, (byte) 0x59 }; System.out.println(HexDumpHelper.dumpHexString(encryptedValue)); // Obtain the nonce. byte[] nonce = Arrays.copyOfRange(encryptedValue, kEncryptionVersionPrefix.length(), kEncryptionVersionPrefix.length() + nonceLength); System.out.println(HexDumpHelper.dumpHexString(nonce)); // Strip off the versioning prefix before decrypting. encryptedValue = Arrays.copyOfRange(encryptedValue, kEncryptionVersionPrefix.length() + nonceLength, encryptedValue.length); System.out.println(HexDumpHelper.dumpHexString(encryptedValue)); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES"); GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce); cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec); String encryptedValueString = new String(cipher.doFinal(encryptedValue)); System.err.println(encryptedValueString); } catch (Exception ex) { System.out.println(ex.toString()); } } } |
可见v10开头的encrypted_value被正确解密了:
对于保存在Chrome中的密码而言,在Chome中显示时同样受到DPAPI的保护,正常情况下需要提供相应的凭证:
但我们亦可直接从Login Data中读取(路径形如:C:\Users\Alice\AppData\Local\Google\Chrome\User Data\Default\Login Data)。如果Chrome正在运行的话该文件无法直接打开,可将该文件复制一份后再打开:
使用同样的方法解密即可。
参考资料:
1、https://stackoverflow.com/questions/22532870/encrypted-cookies-in-chrome
2、https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.protecteddata.unprotect?view=netframework-4.8
3、https://github.com/peter-gergely-horvath/windpapi4j
4、https://github.com/n8henrie/pycookiecheat
5、https://github.com/n8henrie/pycookiecheat/issues/12
6、http://www.nirsoft.net/utils/chromepass.html,一个可以解密不同user-data-dir下密码数据的工具(正是跟本文一样利用了Local State中的encrypted_key)
7、https://stackoverflow.com/questions/35558249/aes-gcm-with-bouncycastle-throws-mac-check-in-gcm-failed-when-used-with-iv
8、http://dy.163.com/v2/article/detail/D9OSVK640511CJ6O.html
转载时请保留出处,违法转载追究到底:进城务工人员小梅 » Chrome浏览器Cookie及密码解密的分析过程及Java实现(Windows平台下v10及以上Cookie文件encrypted_value及Login Data文件password_value的解密)
兄弟,你并不是第一个https://github.com/CCob/gookies/commit/3eab185fd701a9aa1dc7fae1885874c7910c979c
我定语修饰的是第一个用Java实现及分析过程啊,丝毫没有抢这个第一的意思,我觉得我写的很明白了:第一个给出了当encrypted_value以v10或v11开头时Windows平台下的解密方法(Java实现)及分析过程
不知道用易语言 怎么实现,密钥 先base64解码,再dpapi 解密 32字节,初始向量12节 ,后面的密文转明文不知道用什么函数实现
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce)
这个是什么意思,12字节 转换为128bit吗
报错Illegal key size
仔细看文档。
不太可能,代码我都测过
纯java实现:https://github.com/hnuuhc/often-utils
使用:Map cookies = LoginData.home().getLoginDatasForDomain(“pixiv.net”);
不知道Local storage本地存储数据应该如何解密?