查看原文
其他

电子取证中的Chrome各版本解密总结

CTF组 山海之关 2023-12-16

简介

本文主要分析总结了Chrome各版本对于Cookies、LoginData登录信息以及历史记录的加解密过程,使用masterkey解密80.X以前的版本以及使用 LocalState文件对于80.X以后的版本进行解密。

本来想以Chrome的内存取证为基础出Misc题的,但是出题过程中对于各个数据的解密碰到诸多问题,发现网上也没有一个很好的总结性文章,索性简单研究总结记录一下。

这里实验所使用的浏览器都为Google Chrome,理论上Chrome内核的浏览器应该都可以,主要针对windows操作系统

80.X版本以前的chrome可以在:

https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Win_x64

下载到

本文所使用的是:

https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Win_x64/704086

对应版本是79.0.3938.0

前置知识点

Chrome的重要存储文件有如下:

  • Cookies文件:记录了用户的cookie信息,本质为sqlite数据库

    • 80.X版本以前位于%localappdata%\Chromium\User Data\Default\Cookies

    • 80.X版本以后位于%localappdata%\Google\Chrome\User Data\Default\Cookies

    • 至少是110.X版本以后%localappdata%\Google\Chrome\User Data\Default\Network\Cookies

  • History文件:记录了访问历史记录,本质为sqlite数据库

    • 80.X版本以前位于%localappdata%\Chromium\User Data\Default\History

    • 80.X版本以后位于%localappdata%\Google\Chrome\User Data\Default\History

  • Login Data文件:记录了用户保存的用户名密码,本质为sqlite数据库

    • 80.X版本以前位于%localappdata%\Chromium\User Data\Default\Login Data

    • 80.X版本以后位于%localappdata%\Google\Chrome\User Data\Default\Login Data

  • Local State文件:密钥文件,本质为json

    • 80.X版本以前位于%localappdata%\Chromium\User Data\Local State

    • 80.X版本以后位于%localappdata%\Google\Chrome\User Data\Local State

以上文件路径不一定完全正确并未测过所有版本,但基本上大差不差。

在chrome中对于cookies和login data的文件加密在windows中根据版本不同主要分为两种加密方式DPAPI与AES-256-GCM:

  • 1、没有以v10v11为前缀的加密值,主要是在80.X版本以前,为DPAPI加密

  • 2、以v10v11为前缀的加密值,主要是在80.X版本以后,为AES-256-GCM加密

可以使用DB Browser for SQLite或者Navicat查看数据库内容

80版本以后的v10前缀加密:

80版本以前的无v10前缀(也可能有,但加密算法没变,v10后文会提到只是一个iv值),前缀似乎是固定的0x010000(不确定)

对于80.X以前版本的解密

对于80以前版本的,采用了DPAPI加密,简单抄抄一下DPAPI:

DPAPI是Windows系统级对数据进行加解密的一种接口无需自实现加解密代码微软已经提供了经过验证的高质量加解密算法提供了用户态的接口对密钥的推导存储数据加解密实现透明并提供较高的安全保证

DPAPI提供了两个用户态接口CryptProtectData加密数据CryptUnprotectData解密数据加密后的数据由应用程序负责安全存储应用无需解析加密后的数据格式。但是加密后的数据存储需要一定的机制因为该数据可以被其他任何进程用来解密当然CryptProtectData也提供了用户输入额外数据来参与对用户数据进行加密的参数但依然无法放于暴力破解。

总体来说程序可以使用DPAPI来对自己敏感的数据进行加解密也可持久化存储程序或系统重启后可解密密文获取原文。如果应用程序对此敏感数据只是暂存于内存为了防止被黑客dump内存后进行破解也对此数据无需进行持久化存储微软还提供了加解密内存的接口CryptProtectMemoryCryptUnprotectMemory。加解密内存的接口并可指定Flag对此内存加解密的声明周期做控制详细见Memory加密及优缺点章节

对于其解密相对来说比较简单,这里主要采用mimikatz进行解密

如果有目标用户的桌面权限,先`privilege::debug`提权

然后直接使用命令

dpapi::chrome /in:"%localappdata%\Chromium\User Data\Default\Cookies" /unprotect

如果脱机的时候解密(这里使用我win7虚拟机的cookies),会提示报错,需要对应guid的Masterkey

Master Key:

64字节,用于解密DPAPI blob,使用用户登录密码、SID和16字节随机数加密后保存在Master Key file中

Master Key file:

二进制文件,可使用用户登录密码对其解密,获得Master Key

分为两种:

·用户Master Key file,位于%APPDATA%\Microsoft\Protect\%SID%

·系统Master Key file,位于%WINDIR%\System32\Microsoft\Protect\{sid}\User

Preferred文件:

位于Master Key file的同级目录,显示当前系统正在使用的MasterKey及其过期时间,默认90天有效期


拿masterkey的几种方法

方法一 直接在目标机器运行Mimikatz提取

privilege::debugsekurlsa::dpapi

(需目标用户已登录)

方法二 转储lsass.exe 进程从内存提取masterkey

如果目标用户已经登陆 lsass进程的内存中会存在masterkey 转储之 使用Mimikatz提取

procdump.exe -accepteula -ma lsass.exe 666.dmpsekurlsa::minidump lsass.dmpsekurlsa::dpapi

这里dump下来win7转到物理机中获取

方法三 导出SAM注册表 提取user hash 解密masterkey文件(有点麻烦不太推荐)

需SYSTEM权限

reg save HKLM\SYSTEM SS.hivreg save HKLM\SECURITY SE.hiv

m/u 值解密Masterkey文件

mimikatz log "lsadump::secrets /system:SS.hiv /security:SE.hiv"

拿到DPAPI_SYSTEM m/u 后半部分的值 (HASH)

这种方法对应MASTERKEY位置在C:\Windows\System32\Microsoft\Protect\S-1-5-18\{GUID}

dpapi::masterkey /in:C:\Windows\System32\Microsoft\Protect\S-1-5-18\{GUID} /system:HASH

即可拿到masterkey

方法四 已知用户密码(或hash) 用户SID(masterkey路径) 拿到加密后的masterkey文件

这是当时 volatility内存取证题遇到的问题,做内存题用这个方法很好用,原理其实跟方法二没差

先mimikatz获取获取用户密码

接着找到masterkey文件位置

这种方法的文件位置在C:\Users{username}\AppData\Roaming\Microsoft\Protect\{SID}\{GUID}

dpapi::masterkey /in:{masterkeyfile} /sid:{sid} /password:{password} /protected

/password可以用/hash:密码hash代替(NTLM or SHA1)(存疑,没成功过)即可拿到masterkey

方法五 通过域管理员导出backup key 恢复Master key

利用条件:目标机器加入域 要拿的是域用户的key 拿到域管理员权限

lsadump::backupkeys /system:123.com /export

(需要域管理员权限) 导出domain backup key

dpapi::masterkey /in:"C:\Users\spotless.OFFENSE\AppData\Roaming\Microsoft\Protect\{sid}\{guid}" /pvk:ntds_capi_0_d2685b31-402d-493b-8d12-5fe48ee26f5a.pvk

即可拿到masterkey

使用masterkey离线解密cookies\logindata

dpapi::chrome /in:{Cookies} /masterkey:{masterkey} /unprotect

mimikatz会自动储存masterkey和guid对应值,其实可以直接自动解密

对80.X以后版本的加密过程分析

80版本以后的cookies是没法再用masterkey来解密的,提前挖个坑,80版本后基本上失去脱机解密的可能性了,不知道以后有没有机会填坑

直接解会提示报错没有正确的aes的key,80版本后加密变成了aes256gcm

对于cookies和logindata的加密过程详细解析参考

http://www.meilongkui.com/archives/1904

查看chrome的加密源码
https://source.chromium.org/chromium/chromium/src/+/main:components/os_crypt/sync/os_crypt_unittest.cc?q=os_crypt_unittest.cc&ss=chromium%2Fchromium%2Fsrc:components%2Fos_crypt%2F

os_crypt_win.cc文件里记录了具体加密过程,可以看到是用localstate文件内读取了key

结合源码可以看到这个encrypted_key是base64加密,且以DPAPI为开头的结果

密钥和NONCE/IV的长度也被记录在开头

解密方法也写在同文件里了

可以看出,encrypted_value的前缀v10后为12字节的NONCE(IV),然后再是真正的密文。Chrome使用的是AES-256-GCM的AEAD对称加密,使用BoringsSSL实现:

GCM_TAG_LENGTH=16

解密脚本(支持所有版本)

# -*- coding=utf-8 -*-import osimport sqlite3import jsonimport base64from cryptography.hazmat.backends import default_backendfrom cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modesimport ctypesfrom ctypes import wintypes
# 定义cookie、localstate、logindata三个文件的位置cookie_path = os.path.expanduser(os.path.join( os.environ['LOCALAPPDATA'], r'Google\Chrome\User Data\Default\Network\Cookies'))
local_state_path = os.path.join( os.environ['LOCALAPPDATA'], r"Google\Chrome\User Data\Local State")
login_data_path =os.path.expanduser(os.path.join( os.environ['LOCALAPPDATA'], r'Google\Chrome\User Data\Default\Login Data'))
# cookie_path = os.path.expanduser(os.path.join(# os.environ['LOCALAPPDATA'], r'Chromium\User Data\Default\Cookies'))# local_state_path = os.path.join(# os.environ['LOCALAPPDATA'], r"Chromium\User Data\Local State")# login_data_path =os.path.expanduser(os.path.join(# os.environ['LOCALAPPDATA'], r'Chromium\User Data\Default\Login Data'))
class AES_GCM: @staticmethod def encrypt(cipher, plaintext, nonce): cipher.mode = modes.GCM(nonce) encryptor = cipher.encryptor() ciphertext = encryptor.update(plaintext) return cipher, ciphertext, nonce
@staticmethod def decrypt(cipher, ciphertext, nonce): cipher.mode = modes.GCM(nonce) decryptor = cipher.decryptor() return decryptor.update(ciphertext)
@staticmethod def get_cipher(key): cipher = Cipher(algorithms.AES(key), None, backend=default_backend()) return cipher

def dpapi_decrypt(encrypted): class DATA_BLOB(ctypes.Structure): _fields_ = [('cbData', wintypes.DWORD), ('pbData', ctypes.POINTER(ctypes.c_char))]
try: p = ctypes.create_string_buffer(encrypted, len(encrypted)) blobin = DATA_BLOB(ctypes.sizeof(p), p) blobout = DATA_BLOB() retval = ctypes.windll.crypt32.CryptUnprotectData( ctypes.byref(blobin), None, None, None, None, 0, ctypes.byref(blobout)) if not retval: raise ctypes.WinError() result = ctypes.string_at(blobout.pbData, blobout.cbData) return result except Exception as e: print(f"Error in dpapi_decrypt: {e}") return None

def get_key_from_local_state(): with open(local_state_path, encoding='utf-8', mode="r") as f: jsn = json.loads(str(f.readline())) return jsn["os_crypt"]["encrypted_key"]

def aes_decrypt(encrypted_txt): encoded_key = get_key_from_local_state() encrypted_key = base64.b64decode(encoded_key.encode()) encrypted_key = encrypted_key[5:] key = dpapi_decrypt(encrypted_key) nonce = encrypted_txt[3:15] cipher = AES_GCM.get_cipher(key) return AES_GCM.decrypt(cipher, encrypted_txt[15:], nonce)

def chrome_decrypt(encrypted_txt): if encrypted_txt[:4] == b'x01x00x00x00': decrypted_txt = dpapi_decrypt(encrypted_txt) return decrypted_txt.decode() elif encrypted_txt[:3] == b'v10': decrypted_txt = aes_decrypt(encrypted_txt) return decrypted_txt[:-16].decode()

def query_cookie(host): if host: sql = f"select host_key, name, encrypted_value from cookies where host_key = '{host}'" else: sql = "select host_key, name, encrypted_value from cookies" with sqlite3.connect(cookie_path) as conn: result = conn.execute(sql).fetchall()
return result
def query_logindata(url): if url: sql = f"select origin_url, username_value, password_value from logins where origin_url = '{url}'" else: sql = "select origin_url, username_value, password_value from logins" with sqlite3.connect(login_data_path) as conn: result = conn.execute(sql).fetchall()
return result


if __name__ == '__main__': print("Decrypt Cookies:") cookies = query_cookie("chat.openai.com") # 可以传入参数筛选指定host_key for data in cookies: cok = data[0], data[1], chrome_decrypt(data[2]) print(cok) print() print("Decrypt Login Data:") logindata = query_logindata("") # 可以传入参数筛选指定url for data in logindata: login = data[0], data[1], chrome_decrypt(data[2]) print(login)

解析结果还不错

由于其中对Locat State的encrypt_key的DPAPI解密似乎是绑定操作系统本身环境的,尝试了各种方法暂未能够在更换主机的情况下解密encrypt_key

History解析

history里面还是记录了很多东西的,包括访问历史记录、下载记录等等

但是都是以明文记录的,这里也就不做过多解释了

在内存取证中推荐一个volatility2的插件

https://github.com/superponible/volatility-plugins

其中的chromehistory对chrome历史记录的解析还可以

继续滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存