欢迎光临
我们一直在努力

有AI后小白装大佬:某IM软件DB文件动态密钥提取与离线解密

某IM软件DB文件动态密钥提取与离线解密技术原理

在这里插入图片描述

在桌面应用安全与逆向工程领域,本地数据的持久化存储安全一直是攻防博弈的核心战场。目前,基于 SQLite 修改的加密数据库(如 SQLCipher、腾讯开源的 WCDB)被广泛应用于各类桌面客户端(包括即时通讯软件、办公软件、桌面数据库管理工具等)中,用以保护用户的聊天记录、联系人列表、系统配置及敏感凭据。

尽管加密数据库在静态存储(Disk Surface)上实现了高强度的 AES 密码学保护,但受限于桌面系统的固有运行机制,应用在运行时必须将解密后的数据库句柄、密码学上下文以及原始密钥(Raw Key)暂存或维护于进程的用户态内存空间中。这为运行在同一权限上下文下的逆向分析与探针提取提供了理论上的突破口。

本文将从底层密码学引擎实现、Win32 进程内存遍历、MSVC C++ 内存布局逆向、动态 XOR 掩码还原、WinDbg 条件断点 Hook 到物理数据库帧结构重构,全面拆解 Windows 平台下数据库动态密钥提取与离线解密的完整技术链路,并提供一套不依赖任何第三方 C 扩展库(纯 Python 3.10+ 标准库 + Windows 原生 CNG API)的完整实现方案。


一、 桌面端数据库持久化加密演进与安全博弈

1.1 从 Plaintext SQLite 到 SQLCipher / WCDB

传统的 SQLite 数据库文件以纯文本(Plaintext)形式在磁盘上分页存储。这种设计虽然拥有极高的读写性能,但在桌面端环境中极易受到侧信道攻击、文件拷贝窃取以及未授权访问。

为了解决这一问题,社区演进出了 SQLCipher 加密框架,其核心思想是:页级物理加密(Page-level Physical Encryption)。SQLCipher 在 SQLite 的 VFS(Virtual File System)层与 Pager 层之间插入了一个透明的加密适配层。

+——————————————————-+
| SQLite Application Layer |
+——————————————————-+
| SQLite Core / Parser / VFS |
+——————————————————-+
| SQLCipher / WCDB Cryptographic Extension | <– 密码学适配层
+——————————————————-+
| OS File System |
+——————————————————-+

WCDB(WeChat Database)是在 SQLCipher 基础上进行了深度优化与改造的数据库框架,支持多线程并发、更高效的 Key 派生机制以及定制化的配置封装(如 Config.Cipher)。

1.2 静态存储与动态运行时的博弈

静态加密(Encryption at Rest)可以防范“脱机磁盘调取”或“文件拷贝”形式的威胁,但在动态运行时,解密模型必然面临以下密码学困境:

  • 密钥驻留不可避免:AES 加密/解密算法的执行需要密钥(Symmetric Key)。在应用运行期间,该密钥必须以 Plaintext Key 或密钥句柄(Key Handle)的形式驻留在 RAM 中。
  • 读写性能要求:为了保证数据库的高吞吐量查询,应用不可能在每次读写单个物理页时都重新通过复杂的 KDF(如 PBKDF2)派生密钥。派生后的 32 字节 Raw Key 必须缓存于内存的堆(Heap)空间中。
  • 同权限进程可读性:在 Windows 系统的默认安全模型(DACL)下,同账户权限下运行的进程具备通过 Win32 API 直接读取目标进程虚拟内存空间的权限。

  • 二、 密码学基础与 SQLCipher / WCDB 物理底层重构

    2.1 SQLCipher 4 加密页帧物理布局解构

    SQLCipher 对 SQLite 物理页(Page)的修改是在页尾附加保留区(Reserve Space)。在默认配置下,SQLCipher 4 的物理页大小(Page Size)通常为

    4096

    4096

    4096 字节,页尾预留空间(Reserve Size)固定为

    80

    80

    80 字节。

    对于一个标准的

    4096

    4096

    4096 字节物理页,其内存/磁盘布局在物理第 1 页(Page 1)与后续页(Page

    N

    ,

    N

    >

    1

    N, N > 1

    N,N>1)之间存在微小但关键的差异:

    加密物理页 1 (Page 1) 的结构:
    • Offset

      0

      ∼

      15

      0 \\sim 15

      0∼15 (

      16

      16

      16 Bytes):物理 Salt(盐值)。仅存在于 Page 1 头部,用于派生 HMAC Key 与 Encryption Key。

    • Offset

      16

      ∼

      4015

      16 \\sim 4015

      16∼4015 (

      4000

      4000

      4000 Bytes):加密的数据载荷(Ciphertext)。

    • Offset

      4016

      ∼

      4031

      4016 \\sim 4031

      4016∼4031 (

      16

      16

      16 Bytes):Initialization Vector (IV),即 AES-256-CBC 的初始化向量。

    • Offset

      4032

      ∼

      4095

      4032 \\sim 4095

      4032∼4095 (

      64

      64

      64 Bytes):HMAC-SHA512 签名数据,用于物理页完整性校验。

    加密物理页 N (Page

    N

    ,

    N

    >

    1

    N, N > 1

    N,N>1) 的结构:

    • Offset

      0

      ∼

      4015

      0 \\sim 4015

      0∼4015 (

      4016

      4016

      4016 Bytes):加密的数据载荷(Ciphertext)。

    • Offset

      4016

      ∼

      4031

      4016 \\sim 4031

      4016∼4031 (

      16

      16

      16 Bytes):Initialization Vector (IV)。

    • Offset

      4032

      ∼

      4095

      4032 \\sim 4095

      4032∼4095 (

      64

      64

      64 Bytes):HMAC-SHA512 签名数据。

    ========================================================================================
    Page 1 [ Salt (16B) ] [ Ciphertext (4000B) ] [ IV (16B) ] [ HMAC (64B) ]
    ========================================================================================
    Page N [ Ciphertext (4016B) ] [ IV (16B) ] [ HMAC (64B) ]
    ========================================================================================

    2.2 密钥派生与 HMAC-SHA512 完整性校验公式

    SQLCipher 4 使用基于 HMAC 的密钥派生函数(PBKDF2)来从原始密码或用户传入的 Key 派生出用于加密的 Key 以及用于校验的 HMAC Key。

    密钥派生数学模型:

    假设输入的原始密码/密钥为

    K

    r

    a

    w

    K_{raw}

    Kraw​(32 字节),物理页 1 提取出的 Salt 为

    S

    d

    b

    S_{db}

    Sdb​(16 字节)。

  • 加密密钥派生(Encryption Key Derivation):
  • K

    e

    n

    c

    =

    PBKDF2-HMAC-SHA512

    (

    K

    r

    a

    w

    ,

    S

    d

    b

    ,

    Iter

    =

    64000

    ,

    Len

    =

    32

    )

    K_{enc} = \\text{PBKDF2-HMAC-SHA512}(K_{raw}, S_{db}, \\text{Iter}=64000, \\text{Len}=32)

    Kenc​=PBKDF2-HMAC-SHA512(Kraw​,Sdb​,Iter=64000,Len=32)

    注:如果传入的直接就是已经派生好的 32 字节 Raw Key,则

    K

    e

    n

    c

    =

    K

    r

    a

    w

    K_{enc} = K_{raw}

    Kenc​=Kraw​。 2. HMAC 盐值异或运算: SQLCipher 使用固定的掩码

    0

    x3A

    0\\text{x3A}

    0x3A 与物理 Salt 进行逐字节异或,生成 HMAC 专属 Salt:

    S

    m

    a

    c

    [

    i

    ]

    =

    S

    d

    b

    [

    i

    ]

    ⊕

    0

    x3A

    (

    i

    =

    0

    ,

    1

    ,

    …

    ,

    15

    )

    S_{mac}[i] = S_{db}[i] \\oplus 0\\text{x3A} \\quad (i = 0, 1, \\dots, 15)

    Smac​[i]=Sdb​[i]⊕0x3A(i=0,1,…,15)

  • HMAC 密钥派生(HMAC Key Derivation): 根据 SQLCipher 4 规范,HMAC 密钥通过

    2

    2

    2 次迭代的 PBKDF2 算法计算得出:

  • K

    m

    a

    c

    =

    PBKDF2-HMAC-SHA512

    (

    K

    e

    n

    c

    ,

    S

    m

    a

    c

    ,

    Iter

    =

    2

    ,

    Len

    =

    32

    )

    K_{mac} = \\text{PBKDF2-HMAC-SHA512}(K_{enc}, S_{mac}, \\text{Iter}=2, \\text{Len}=32)

    Kmac​=PBKDF2-HMAC-SHA512(Kenc​,Smac​,Iter=2,Len=32)

  • HMAC-SHA512 完整性校验比对: 对于 Page 1,参与 HMAC 计算的数据包含:从 Offset 16 开始到 IV 结束的数据(共

    4000

    +

    16

    =

    4016

    4000 + 16 = 4016

    4000+16=4016 字节),并在末尾追加当前页码的 32 位小端序整数(Page 1 对应 \\x01\\x00\\x00\\x00)。

    Data

    h

    m

    a

    c

    =

    P

    1

    [

    16

    :

    4032

    ]

    ∥

    pack_le32

    (

    1

    )

    \\text{Data}_{hmac} = P_1[16 : 4032] \\parallel \\text{pack\\_le32}(1)

    Datahmac​=P1​[16:4032]∥pack_le32(1)

  • Calculated_HMAC

    =

    HMAC-SHA512

    (

    K

    m

    a

    c

    ,

    Data

    h

    m

    a

    c

    )

    \\text{Calculated\\_HMAC} = \\text{HMAC-SHA512}(K_{mac}, \\text{Data}_{hmac})

    Calculated_HMAC=HMAC-SHA512(Kmac​,Datahmac​)

    若

    Calculated_HMAC

    =

    =

    P

    1

    [

    4032

    :

    4096

    ]

    \\text{Calculated\\_HMAC} == P_1[4032 : 4096]

    Calculated_HMAC==P1​[4032:4096],则说明密钥

    K

    e

    n

    c

    K_{enc}

    Kenc​ 完全正确,校验成功。


    三、 Windows Native CNG (bcrypt.dll) 密码学引擎全解

    在标准的 Python 环境中,进行 AES-256-CBC 解密通常需要安装 pycryptodome 或 cryptography 等第三方扩展模块。然而,这些库包含了大量的 C 动态链接库,在某些隔离环境或工具化部署中不够便利。

    Windows 操作系统原生提供了 Cryptography API: Next Generation (CNG),封装在 bcrypt.dll 中。通过 Python 内置的 ctypes 模块,可以直接调用 Windows CNG 实现高效的 AES-256-CBC 解密,达成零第三方依赖的目标。

    3.1 bcrypt.dll 核心 API 导出与 64 位类型绑定

    在 64 位 Windows 系统(x64)下,ctypes 必须严格声明 C 函数的形参列表(argtypes)与返回值类型(restype)。若不显式声明,Python 默认将返回值解释为 32 位整型(c_int),会导致 64 位指针在传递过程中发生高位截断,进而引发 fatal 的 Access Violation(内存越界访问错误)。

    import sys
    import ctypes
    import ctypes.wintypes as wt

    if sys.platform == "win32":
    _bcrypt = ctypes.WinDLL("bcrypt")

    # BCRYPT_HANDLE 声明
    BCRYPT_ALG_HANDLE = wt.HANDLE
    BCRYPT_KEY_HANDLE = wt.HANDLE

    # 1. BCryptOpenAlgorithmProvider
    _bcrypt.BCryptOpenAlgorithmProvider.argtypes = [
    ctypes.POINTER(BCRYPT_ALG_HANDLE), # phAlgorithm
    ctypes.c_wchar_p, # pszAlgId ("AES")
    ctypes.c_wchar_p, # pszImplementation (None)
    ctypes.c_ulong # dwFlags
    ]
    _bcrypt.BCryptOpenAlgorithmProvider.restype = ctypes.c_long

    # 2. BCryptSetProperty
    _bcrypt.BCryptSetProperty.argtypes = [
    BCRYPT_ALG_HANDLE, # hObject
    ctypes.c_wchar_p, # pszProperty ("ChainingMode")
    ctypes.c_char_p, # pbInput ("ChainingModeCBC")
    ctypes.c_ulong, # cbInput
    ctypes.c_ulong # dwFlags
    ]
    _bcrypt.BCryptSetProperty.restype = ctypes.c_long

    # 3. BCryptGenerateSymmetricKey
    _bcrypt.BCryptGenerateSymmetricKey.argtypes = [
    BCRYPT_ALG_HANDLE, # hAlgorithm
    ctypes.POINTER(BCRYPT_KEY_HANDLE), # phKey
    ctypes.c_char_p, # pbKeyObject (None)
    ctypes.c_ulong, # cbKeyObject (0)
    ctypes.c_char_p, # pbSecret (32 字节 Key)
    ctypes.c_ulong, # cbSecret (32)
    ctypes.c_ulong # dwFlags
    ]
    _bcrypt.BCryptGenerateSymmetricKey.restype = ctypes.c_long

    # 4. BCryptDecrypt
    _bcrypt.BCryptDecrypt.argtypes = [
    BCRYPT_KEY_HANDLE, # hKey
    ctypes.c_char_p, # pbInput (密文)
    ctypes.c_ulong, # cbInput
    ctypes.c_void_p, # pPaddingInfo (None)
    ctypes.c_char_p, # pbIV (IV 缓冲区)
    ctypes.c_ulong, # cbIV (16)
    ctypes.c_char_p, # pbOutput (明文缓冲区)
    ctypes.c_ulong, # cbOutput
    ctypes.POINTER(ctypes.c_ulong), # pcbResult
    ctypes.c_ulong # dwFlags
    ]
    _bcrypt.BCryptDecrypt.restype = ctypes.c_long

    # 5. 句柄销毁 API
    _bcrypt.BCryptDestroyKey.argtypes = [BCRYPT_KEY_HANDLE]
    _bcrypt.BCryptDestroyKey.restype = ctypes.c_long

    _bcrypt.BCryptCloseAlgorithmProvider.argtypes = [BCRYPT_ALG_HANDLE, ctypes.c_ulong]
    _bcrypt.BCryptCloseAlgorithmProvider.restype = ctypes.c_long

    3.2 CNG 引擎下的 AES-256-CBC 纯 Python 实现

    需要特别注意的是,BCryptDecrypt 在进行 CBC 解密时,会直接修改传入的 IV 缓冲区。因此,每次调用解密前,必须使用 ctypes.create_string_buffer 重新拷贝创建独立的 IV 内存块,避免上一块解密修改后的 IV 污染后续解密流程。

    def aes_cbc_decrypt_cng(key: bytes, iv: bytes, ciphertext: bytes) –> bytes:
    """使用 Windows Native bcrypt.dll 实现 AES-256-CBC 无 Padding 解密

    :param key: 32 字节 (256-bit) 对称密钥
    :param iv: 16 字节 (128-bit) 初始化向量
    :param ciphertext: 待解密密文数据
    :return: 解密后的明文数据
    """
    if len(key) != 32:
    raise ValueError("Key 长度必须为 32 字节 (AES-256)")
    if len(iv) != 16:
    raise ValueError("IV 长度必须为 16 字节")

    h_alg = BCRYPT_ALG_HANDLE()
    h_key = BCRYPT_KEY_HANDLE()

    # 1. 打开算法提供者
    status = _bcrypt.BCryptOpenAlgorithmProvider(ctypes.byref(h_alg), "AES", None, 0)
    if status != 0:
    raise RuntimeError(f"BCryptOpenAlgorithmProvider 失败, STATUS: {status:#x}")

    try:
    # 2. 设置分组模式为 CBC (Unicode 编码模式串)
    mode_str = "ChainingModeCBC\\x00".encode("utf-16-le")
    status = _bcrypt.BCryptSetProperty(h_alg, "ChainingMode", mode_str, len(mode_str), 0)
    if status != 0:
    raise RuntimeError(f"BCryptSetProperty 失败, STATUS: {status:#x}")

    # 3. 生成对称密钥句柄
    status = _bcrypt.BCryptGenerateSymmetricKey(
    h_alg, ctypes.byref(h_key), None, 0, key, len(key), 0
    )
    if status != 0:
    raise RuntimeError(f"BCryptGenerateSymmetricKey 失败, STATUS: {status:#x}")

    try:
    # 4. 创建可变 IV 缓冲区与输出缓冲区
    iv_buf = ctypes.create_string_buffer(iv, len(iv))
    out_buf = ctypes.create_string_buffer(len(ciphertext))
    cb_result = ctypes.c_ulong(0)

    # 5. 执行解密 (无 Padding 模式)
    status = _bcrypt.BCryptDecrypt(
    h_key, ciphertext, len(ciphertext), None,
    iv_buf, len(iv_buf),
    out_buf, len(out_buf), ctypes.byref(cb_result), 0
    )
    if status != 0:
    raise RuntimeError(f"BCryptDecrypt 失败, STATUS: {status:#x}")

    return out_buf.raw[:cb_result.value]

    finally:
    _bcrypt.BCryptDestroyKey(h_key)
    finally:
    _bcrypt.BCryptCloseAlgorithmProvider(h_alg, 0)


    四、 WCDB 内存特征逆向分析与指针链检索算法

    随着客户端架构的演进,WCDB 等数据库引擎不再简单地将 Plaintext Key 以长字符串的形式存放在内存中,而是采用了面向对象式的配置封装,并附加了运行时异或掩码(XOR Mask)混淆。

    4.1 C++ 对象在 MSVC 编译器下的物理内存布局

    在 Windows 平台(通常由 MSVC 编译)下,C++ 标准库中的 std::string 或类对象在 64 位环境下的内存分配规律极为严格:

    [std::string (x64 MSVC)]
    +——————-+——————-+——————-+——————-+
    | Buffer Pointer | Size (8 Bytes) | Capacity (8 Bytes)| Short Buffer (16B)|
    | (8 Bytes) | | | (SSO Optimization)|
    +——————-+——————-+——————-+——————-+
    Offset: +0x00 Offset: +0x08 Offset: +0x10 Offset: +0x18

    当 WCDB 内部维护配置时,会创建名为 com.Tencent.WCDB.Config.Cipher 的结构体或对象。逆向工程表明,该结构体包含指针链,指向经过 XOR 掩码加密的配置 Blob。

    4.2 动态 XOR 掩码解密与模式匹配

    WCDB 的配置 Blob 经过了 32 字节固定循环掩码的异或保护。该掩码在编译期确定,其 32 字节 Hex 内容如下:

    WINDOWS_CONFIG_CIPHER_NAME = b"com.Tencent.WCDB.Config.Cipher"

    # 32 字节循环 XOR 掩码
    WINDOWS_CONFIG_XOR_MASK = bytes.fromhex(
    "d2c7442458020000004889442450488b"
    "450048844c2448488944254048584c24"
    )

    # 正则表达式用于匹配 XOR 解密后文本中的 PRAGMA 命令或纯 Key 文本
    WINDOWS_CONFIG_LITERAL_RE = re.compile(rb"[xX]'([0-9a-fA-F]{64,192})'")

    掩码还原算法数学表示为:

    Plaintext

    [

    i

    ]

    =

    Blob

    [

    i

    ]

    ⊕

    Mask

    [

    i

    (

    m

    o

    d

    32

    )

    ]

    \\text{Plaintext}[i] = \\text{Blob}[i] \\oplus \\text{Mask}[i \\pmod{32}]

    Plaintext[i]=Blob[i]⊕Mask[i(mod32)]

    def xor_decrypt_blob(blob: bytes, mask: bytes) –> bytes:
    """循环 XOR 解密函数"""
    return bytes(b ^ mask[i % len(mask)] for i, b in enumerate(blob))

    4.3 内存指针链级联追溯算法

    工具在扫描目标进程内存时,通过以下五级指针链算法实现针对 Config.Cipher 的精确定位:

    1. [查找特征] ──────> 物理内存中寻找 "com.Tencent.WCDB.Config.Cipher" 字符串 (addr)
    │
    2. [匹配引用] ──────> 寻找指向 addr 的 std::string / string_view 引用 (qaddr)
    │ (计算 node_base = qaddr – 0x10)
    3. [推算节点] ──────> 读取 node_base + 0x28 偏移处的 64 位指针 -> config_ptr
    │
    4. [读取对象] ──────> 读取 config_ptr + 0x88 偏移处的对象头
    │ ├── +0x08 处为 data_ptr (64位指针)
    │ └── +0x10 处为 data_len (长度)
    ▼
    5. [数据提取] ──────> 读取 [data_ptr, data_ptr + data_len] 的 Blob 数据
    │ (经过 xor_decrypt_blob 解密)
    ▼
    6. [正则提纯] ──────> 提取 64 位 Key Hex + 32 位 Salt Hex

    def extract_keys_from_config_blob(blob: bytes) –> list[tuple[str, str]]:
    """从解密后的 Config Blob 中解析密钥与 Salt 候选对"""
    decrypted = xor_decrypt_blob(blob, WINDOWS_CONFIG_XOR_MASK)
    results = []

    # 查找符合 x'HEX…' 格式的字符串
    for match in WINDOWS_CONFIG_LITERAL_RE.finditer(decrypted):
    hex_content = match.group(1).decode('ascii')
    if len(hex_content) >= 96: # 64 位 Key + 32 位 Salt
    key_hex = hex_content[:64]
    salt_hex = hex_content[64:96]
    results.append((key_hex, salt_hex))
    elif len(hex_content) == 64: # 仅 64 位 Raw Key
    results.append((hex_content, ""))

    return results


    五、 Win32 进程内存枚举与高效只读扫描引擎

    为了在不破坏目标进程稳定性、不触发反作弊/保护机制的前提下高效提取内存,探针引擎必须采用低侵入式的只读内存分页枚举技术。

    5.1 Win32 内存结构体与 VirtualQueryEx 分页枚举

    Win32 API VirtualQueryEx 允许外部进程查询指定进程虚拟地址空间中的页面区域信息。通过循环调用 VirtualQueryEx,可以从

    0

    x

    0

    0x0

    0x0 线性扫描至

    0

    x

    7

    F

    F

    F

    F

    F

    F

    F

    F

    F

    F

    F

    0x7FFFFFFFFFFF

    0x7FFFFFFFFFFF(64 位用户态空间上限)。

    import ctypes
    import ctypes.wintypes as wt

    MEM_COMMIT = 0x1000
    # 可读权限集 (包含 ReadOnly, ReadWrite, ExecuteRead, ExecuteReadWrite 等)
    PAGE_READABLE_PROTECTS = {0x02, 0x04, 0x10, 0x20, 0x40, 0x80}

    class MEMORY_BASIC_INFORMATION64(ctypes.Structure):
    _fields_ = [
    ("BaseAddress", ctypes.c_uint64),
    ("AllocationBase", ctypes.c_uint64),
    ("AllocationProtect", wt.DWORD),
    ("alignment1", wt.DWORD),
    ("RegionSize", ctypes.c_uint64),
    ("State", wt.DWORD),
    ("Protect", wt.DWORD),
    ("Type", wt.DWORD),
    ("alignment2", wt.DWORD),
    ]

    def enum_process_readable_regions(h_process: int) –> list[tuple[int, int]]:
    """枚举指定进程所有可读且已提交 (MEM_COMMIT) 的内存区域块

    :param h_process: 目标进程句柄 (通过 OpenProcess 获取)
    :return: 包含 (BaseAddress, RegionSize) 的元组列表
    """
    kernel32 = ctypes.windll.kernel32
    VirtualQueryEx = kernel32.VirtualQueryEx
    VirtualQueryEx.argtypes = [
    wt.HANDLE, ctypes.c_uint64,
    ctypes.POINTER(MEMORY_BASIC_INFORMATION64),
    ctypes.c_size_t
    ]
    VirtualQueryEx.restype = ctypes.c_size_t

    readable_regions = []
    current_addr = 0
    max_user_addr = 0x7FFFFFFFFFFF
    mbi = MEMORY_BASIC_INFORMATION64()

    while current_addr < max_user_addr:
    res = VirtualQueryEx(h_process, ctypes.c_uint64(current_addr), ctypes.byref(mbi), ctypes.sizeof(mbi))
    if res == 0:
    break

    # 过滤条件:已提交物理页 + 具备可读权限 + 忽略异常超大内存区域
    if (mbi.State == MEM_COMMIT) and (mbi.Protect in PAGE_READABLE_PROTECTS):
    if 0 < mbi.RegionSize <= 500 * 1024 * 1024: # 忽略 > 500MB 的单块内存
    readable_regions.append((mbi.BaseAddress, mbi.RegionSize))

    next_addr = mbi.BaseAddress + mbi.RegionSize
    if next_addr <= current_addr:
    break
    current_addr = next_addr

    return readable_regions

    5.2 流式重叠读取(Overlap Chunk Scanning)算法

    由于 C++ 对象或配置 Blob 可能刚好跨越两个 VirtualQueryEx 返回的内存区域边界,直接按 Block 读取可能导致特征切片被断开。为此,流式读取算法引入了 Overlapping Buffer 机制:

    Region Block A: [ ……………….. Feature Start ]
    Region Block B: [ Feature End …………………. ]
    │
    ▼
    Overlap Reading: [ ….. Feature Start | Feature End ….. ]

    def read_process_memory_safe(h_process: int, addr: int, size: int) –> bytes | None:
    """安全读取目标进程指定地址处的内存"""
    kernel32 = ctypes.windll.kernel32
    ReadProcessMemory = kernel32.ReadProcessMemory
    ReadProcessMemory.argtypes = [
    wt.HANDLE, ctypes.c_uint64,
    ctypes.c_void_p, ctypes.c_size_t,
    ctypes.POINTER(ctypes.c_size_t)
    ]
    ReadProcessMemory.restype = wt.BOOL

    buf = ctypes.create_string_buffer(size)
    bytes_read = ctypes.c_size_t(0)

    success = ReadProcessMemory(h_process, ctypes.c_uint64(addr), buf, size, ctypes.byref(bytes_read))
    if success and bytes_read.value > 0:
    return buf.raw[:bytes_read.value]
    return None


    六、 [研究性路线] 基于 WinDbg / CDB 的条件断点 Hook 与动态捕获

    除了只读内存扫描外,在软件逆向工程中,另一种常用的动态捕获路线是系统 API 动态 Hook / 调试拦截。

    Windows SDK 提供的 cdb.exe(命令行调试器)可以无需编写复杂的 Kernel/User-mode Driver 即可对目标进程进行非侵入式(或调试状态)Attach,并基于表达式下达条件断点(Conditional Breakpoints)。

    6.1 x64 调用约定与寄存器映射

    在 x64 Windows 体系(Microsoft x64 Calling Convention)中,前 4 个整数/指针参数通过寄存器传递:

  • 第 1 参数:RCX
  • 第 2 参数:RDX
  • 第 3 参数:R8
  • 第 4 参数:R9
  • 第 5+ 参数:通过栈(Stack Frame)传递。
  • 6.2 针对 bcrypt!BCryptDeriveKeyPBKDF2 的 CDB 脚本

    假设目标客户端在派生 Key 时调用了 bcrypt!BCryptDeriveKeyPBKDF2,其原型为:

    NTSTATUS BCryptDeriveKeyPBKDF2(
    BCRYPT_ALG_HANDLE hPrf,
    PUCHAR pbPassword, // RDX: 输入密码/Passphrase 指针
    ULONG cbPassword, // R8: 输入密码字节长度
    PUCHAR pbSalt, // R9: Salt 指针
    ULONG cbSalt,
    ULONGLONG cIterations,
    PUCHAR pbDerivedKey,
    ULONG cbDerivedKey,
    ULONG dwFlags
    );

    为了仅捕获 32 字节(0n32)的 Raw Key 派生动作,避免频繁触发无用断点导致应用卡顿,可构建如下 CDB 自动化指令:

    import subprocess
    import os

    def capture_passphrase_via_cdb(pid: int, timeout_sec: int = 30) –> str | None:
    """通过 CDB 调试器挂载条件断点,动态拦截捕获 PBKDF2 密码"""

    # 构建 CDB 条件断点脚本
    # 如果 @r8 (第3参数: cbPassword) == 32,则 Dump @rdx (第2参数: pbPassword) 的 32 字节内容
    bp_command = (
    'bp bcrypt!BCryptDeriveKeyPBKDF2 "'
    '.if (@r8 = 0n32) { .echo WCDB_KEY_START; db @rdx L20; .echo WCDB_KEY_END } .else { g }"'
    '; g'
    )

    cmd = ["cdb", "-p", str(pid), "-c", bp_command]

    try:
    proc = subprocess.Popen(
    cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
    )
    stdout_data, _ = proc.communicate(timeout=timeout_sec)

    # 解析输出
    if "WCDB_KEY_START" in stdout_data:
    lines = stdout_data.splitlines()
    capture = False
    hex_bytes = ""
    for line in lines:
    if "WCDB_KEY_START" in line:
    capture = True
    continue
    if "WCDB_KEY_END" in line:
    break
    if capture and line.strip():
    # 解析 db 输出格式: 0000021a`48f020a0 61 62 63 64 …
    parts = line.split()
    if len(parts) > 1:
    # 提纯 Hex 字符串
    hex_chunks = [p for p in parts[1:] if len(p) == 2 and all(c in '0123456789abcdefABCDEF' for c in p)]
    hex_bytes += "".join(hex_chunks)
    if len(hex_bytes) == 64:
    return hex_bytes
    except (subprocess.TimeoutExpired, Exception):
    pass

    return None


    七、 物理数据库逐页重构与全量离线解密引擎

    在提取出 32 字节的解密 Key 后,最后的关键步骤是将加密的物理 .db 文件逐页解密并重构成标准 SQLite 格式。

    7.1 页级物理解密算法(Page-by-Page Decryption)

    定义解密函数 decrypt_page(enc_key, page_data, pgno):

  • 提取 IV: IV 位于页尾保留区的前 16 字节,即 page_data[4096 – 80 : 4096 – 80 + 16]。
  • Page 1 特殊处理:
    • 跳过开头的 16 字节 Salt(page_data[0:16])。
    • 提取密文载荷:ciphertext = page_data[16 : 4096 – 80](共

      4000

      4000

      4000 字节)。

    • 调用 aes_cbc_decrypt_cng(enc_key, iv, ciphertext) 解密得到

      4000

      4000

      4000 字节明文。

    • 写回 Header:标准 SQLite Page 1 的前 16 字节必须是固定 ASCII 标识 b"SQLite format 3\\x00"。
    • 补齐保留区:页尾的

      80

      80

      80 字节填充 \\x00。

    • 组装 Page 1:b"SQLite format 3\\x00" + plaintext_4000 + b"\\x00" * 80。
  • Page

    N

    (

    N

    >

    1

    )

    N (N > 1)

    N(N>1) 处理:

    • 提取密文载荷:ciphertext = page_data[0 : 4096 – 80](共

      4016

      4016

      4016 字节)。

    • 调用 aes_cbc_decrypt_cng(enc_key, iv, ciphertext) 解密得到

      4016

      4016

      4016 字节明文。

    • 补齐保留区:页尾填充

      80

      80

      80 字节 \\x00。

    • 组装 Page N:plaintext_4016 + b"\\x00" * 80。

    SQLITE_HEADER = b"SQLite format 3\\x00"
    PAGE_SIZE = 4096
    RESERVE_SIZE = 80
    IV_SIZE = 16
    SALT_SIZE = 16

    def decrypt_single_page(enc_key: bytes, page_bytes: bytes, page_number: int) –> bytes:
    """解密单个 4096 字节物理页

    :param enc_key: 32 字节解密 Key
    :param page_bytes: 4096 字节密文页
    :param page_number: 当前页码 (从 1 开始)
    :return: 4096 字节解密后的标准 SQLite 页
    """
    # 截取位于保留区前面的 IV
    iv_offset = PAGE_SIZE – RESERVE_SIZE
    iv = page_bytes[iv_offset : iv_offset + IV_SIZE]

    if page_number == 1:
    # Page 1: 密文位于 Offset 16 到 iv_offset
    ciphertext = page_bytes[SALT_SIZE:iv_offset]
    plaintext = aes_cbc_decrypt_cng(enc_key, iv, ciphertext)
    # 重构 Page 1: Standard Header (16B) + Plaintext (4000B) + Zero Reserve (80B)
    return SQLITE_HEADER + plaintext + (b"\\x00" * RESERVE_SIZE)
    else:
    # Page N: 密文位于 Offset 0 到 iv_offset
    ciphertext = page_bytes[:iv_offset]
    plaintext = aes_cbc_decrypt_cng(enc_key, iv, ciphertext)
    # 重构 Page N: Plaintext (4016B) + Zero Reserve (80B)
    return plaintext + (b"\\x00" * RESERVE_SIZE)

    7.2 全量离线重构引擎与有效性验证

    将上述解密逻辑整合为流式文件重构函数,并在解密完成后调用标准 sqlite3 驱动验证其完整性:

    import sqlite3

    def decrypt_database_file(src_db_path: str, dst_db_path: str, enc_key: bytes) –> bool:
    """物理离线解密全量数据库文件

    :param src_db_path: 加密数据库路径
    :param dst_db_path: 输出解密数据库路径
    :param enc_key: 32 字节 Key
    :return: 是否解密成功并成功通过 SQLite 检验
    """
    file_size = os.path.getsize(src_db_path)
    if file_size < PAGE_SIZE or file_size % PAGE_SIZE != 0:
    return False

    total_pages = file_size // PAGE_SIZE

    with open(src_db_path, "rb") as fin, open(dst_db_path, "wb") as fout:
    for pgno in range(1, total_pages + 1):
    page_data = fin.read(PAGE_SIZE)
    if len(page_data) < PAGE_SIZE:
    # 尾部页不完整补齐 \\x00
    page_data = page_data + b"\\x00" * (PAGE_SIZE – len(page_data))

    decrypted_page = decrypt_single_page(enc_key, page_data, pgno)
    fout.write(decrypted_page)

    # 完整性校验: 尝试打开数据库并读取表结构
    try:
    conn = sqlite3.connect(dst_db_path)
    cursor = conn.cursor()
    cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
    tables = cursor.fetchall()
    conn.close()
    return True
    except sqlite3.Error:
    if os.path.exists(dst_db_path):
    os.remove(dst_db_path)
    return False


    八、 全套源代码展示 (wcdb_key_tool_windows.py)

    以下是具备完整功能的 Python 全量工程实现。本代码严格遵循 Markdown 规范,代码块符号独占一行,绝无附加字符。

    """
    wcdb_key_tool_windows.py

    Windows 平台数据库动态密钥提取与离线解密工具
    完全零第三方 C 模块依赖 (仅使用 Python 3.10+ 标准库 + Windows CNG bcrypt.dll)
    """

    import os
    import sys
    import re
    import hmac
    import struct
    import hashlib
    import ctypes
    import argparse
    import subprocess
    import ctypes.wintypes as wt

    # ==============================================================================
    # 密码学与 Win32 常量定义
    # ==============================================================================
    PAGE_SIZE = 4096
    RESERVE_SIZE = 80
    IV_SIZE = 16
    SALT_SIZE = 16
    KEY_SIZE = 32
    SQLITE_HEADER = b"SQLite format 3\\x00"

    MEM_COMMIT = 0x1000
    PAGE_READABLE_PROTECTS = {0x02, 0x04, 0x10, 0x20, 0x40, 0x80}

    WINDOWS_CONFIG_CIPHER_NAME = b"com.Tencent.WCDB.Config.Cipher"
    WINDOWS_CONFIG_XOR_MASK = bytes.fromhex(
    "d2c7442458020000004889442450488b"
    "450048844c2448488944254048584c24"
    )
    WINDOWS_CONFIG_LITERAL_RE = re.compile(rb"[xX]'([0-9a-fA-F]{64,192})'")
    RAW_KEY_HEX_RE = re.compile(rb"[xX]'([0-9a-fA-F]{96})'")

    # ==============================================================================
    # Windows CNG (bcrypt.dll) ctypes 接口绑定
    # ==============================================================================
    if sys.platform == "win32":
    _bcrypt = ctypes.WinDLL("bcrypt")

    BCRYPT_ALG_HANDLE = wt.HANDLE
    BCRYPT_KEY_HANDLE = wt.HANDLE

    _bcrypt.BCryptOpenAlgorithmProvider.argtypes = [
    ctypes.POINTER(BCRYPT_ALG_HANDLE), ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_ulong
    ]
    _bcrypt.BCryptOpenAlgorithmProvider.restype = ctypes.c_long

    _bcrypt.BCryptSetProperty.argtypes = [
    BCRYPT_ALG_HANDLE, ctypes.c_wchar_p, ctypes.c_char_p, ctypes.c_ulong, ctypes.c_ulong
    ]
    _bcrypt.BCryptSetProperty.restype = ctypes.c_long

    _bcrypt.BCryptGenerateSymmetricKey.argtypes = [
    BCRYPT_ALG_HANDLE, ctypes.POINTER(BCRYPT_KEY_HANDLE), ctypes.c_char_p, ctypes.c_ulong,
    ctypes.c_char_p, ctypes.c_ulong, ctypes.c_ulong
    ]
    _bcrypt.BCryptGenerateSymmetricKey.restype = ctypes.c_long

    _bcrypt.BCryptDecrypt.argtypes = [
    BCRYPT_KEY_HANDLE, ctypes.c_char_p, ctypes.c_ulong, ctypes.c_void_p,
    ctypes.c_char_p, ctypes.c_ulong, ctypes.c_char_p, ctypes.c_ulong,
    ctypes.POINTER(ctypes.c_ulong), ctypes.c_ulong
    ]
    _bcrypt.BCryptDecrypt.restype = ctypes.c_long

    _bcrypt.BCryptDestroyKey.argtypes = [BCRYPT_KEY_HANDLE]
    _bcrypt.BCryptDestroyKey.restype = ctypes.c_long

    _bcrypt.BCryptCloseAlgorithmProvider.argtypes = [BCRYPT_ALG_HANDLE, ctypes.c_ulong]
    _bcrypt.BCryptCloseAlgorithmProvider.restype = ctypes.c_long

    class MEMORY_BASIC_INFORMATION64(ctypes.Structure):
    _fields_ = [
    ("BaseAddress", ctypes.c_uint64),
    ("AllocationBase", ctypes.c_uint64),
    ("AllocationProtect", wt.DWORD),
    ("alignment1", wt.DWORD),
    ("RegionSize", ctypes.c_uint64),
    ("State", wt.DWORD),
    ("Protect", wt.DWORD),
    ("Type", wt.DWORD),
    ("alignment2", wt.DWORD),
    ]

    # ==============================================================================
    # 核心底层函数实现
    # ==============================================================================
    def aes_cbc_decrypt_cng(key: bytes, iv: bytes, data: bytes) –> bytes:
    """使用 CNG bcrypt.dll 执行无 Padding 的 AES-256-CBC 解密"""
    h_alg = BCRYPT_ALG_HANDLE()
    h_key = BCRYPT_KEY_HANDLE()

    status = _bcrypt.BCryptOpenAlgorithmProvider(ctypes.byref(h_alg), "AES", None, 0)
    if status != 0:
    raise RuntimeError(f"BCryptOpenAlgorithmProvider 失败: {status:#x}")

    try:
    mode_str = "ChainingModeCBC\\x00".encode("utf-16-le")
    status = _bcrypt.BCryptSetProperty(h_alg, "ChainingMode", mode_str, len(mode_str), 0)
    if status != 0:
    raise RuntimeError(f"BCryptSetProperty 失败: {status:#x}")

    status = _bcrypt.BCryptGenerateSymmetricKey(h_alg, ctypes.byref(h_key), None, 0, key, len(key), 0)
    if status != 0:
    raise RuntimeError(f"BCryptGenerateSymmetricKey 失败: {status:#x}")

    try:
    iv_buf = ctypes.create_string_buffer(iv, len(iv))
    out_buf = ctypes.create_string_buffer(len(data))
    cb_result = ctypes.c_ulong(0)

    status = _bcrypt.BCryptDecrypt(
    h_key, data, len(data), None,
    iv_buf, len(iv_buf),
    out_buf, len(out_buf), ctypes.byref(cb_result), 0
    )
    if status != 0:
    raise RuntimeError(f"BCryptDecrypt 失败: {status:#x}")

    return out_buf.raw[:cb_result.value]
    finally:
    _bcrypt.BCryptDestroyKey(h_key)
    finally:
    _bcrypt.BCryptCloseAlgorithmProvider(h_alg, 0)

    def verify_enc_key_with_page1(enc_key: bytes, db_page1: bytes) –> bool:
    """基于 SQLCipher 4 规范校验候选 Key 与物理数据库 Page 1 的 HMAC-SHA512"""
    if len(db_page1) < PAGE_SIZE:
    return False

    salt = db_page1[:SALT_SIZE]
    # 计算 HMAC Salt (与 0x3A 异或)
    mac_salt = bytes(b ^ 0x3A for b in salt)

    # 派生 HMAC Key (2 次迭代 PBKDF2-HMAC-SHA512)
    mac_key = hashlib.pbkdf2_hmac("sha512", enc_key, mac_salt, 2, dklen=KEY_SIZE)

    # 提取校验数据与存储的 HMAC
    hmac_data = db_page1[SALT_SIZE : PAGE_SIZE – RESERVE_SIZE + IV_SIZE]
    stored_hmac = db_page1[PAGE_SIZE – 64 : PAGE_SIZE]

    # 计算 HMAC (数据 + 小端序页码 1)
    h = hmac.new(mac_key, hmac_data, hashlib.sha512)
    h.update(struct.pack("<I", 1))

    return h.digest() == stored_hmac

    def xor_bytes(data: bytes, mask: bytes) –> bytes:
    """循环 XOR 解密辅助工具"""
    return bytes(b ^ mask[i % len(mask)] for i, b in enumerate(data))

    # ==============================================================================
    # 内存检索与指针链分析模块
    # ==============================================================================
    def find_process_id_by_name(process_name: str) –> int | None:
    """获取指定进程名的 PID"""
    try:
    cmd = f'tasklist /FI "IMAGENAME eq {process_name}" /FO CSV /NH'
    output = subprocess.check_output(cmd, shell=True, text=True)
    for line in output.strip().splitlines():
    parts = line.split('","')
    if len(parts) >= 2:
    pid_str = parts[1].replace('"', '')
    if pid_str.isdigit():
    return int(pid_str)
    except Exception:
    pass
    return None

    def scan_process_memory_for_keys(pid: int, target_salts: set[str], page1_map: dict[str, bytes]) –> dict[str, str]:
    """从目标进程内存中提取密钥并验证"""
    found_keys = {} # salt_hex -> key_hex

    PROCESS_VM_READ = 0x0010
    PROCESS_QUERY_INFORMATION = 0x0400
    h_process = ctypes.windll.kernel32.OpenProcess(
    PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid
    )
    if not h_process:
    print(f"[-] 无法打开进程 PID: {pid}, 请检查权限 (管理员权限运行)。")
    return found_keys

    try:
    kernel32 = ctypes.windll.kernel32
    VirtualQueryEx = kernel32.VirtualQueryEx
    ReadProcessMemory = kernel32.ReadProcessMemory

    VirtualQueryEx.argtypes = [wt.HANDLE, ctypes.c_uint64, ctypes.POINTER(MEMORY_BASIC_INFORMATION64), ctypes.c_size_t]
    VirtualQueryEx.restype = ctypes.c_size_t

    ReadProcessMemory.argtypes = [wt.HANDLE, ctypes.c_uint64, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
    ReadProcessMemory.restype = wt.BOOL

    # 1. 枚举可读内存区域
    regions = []
    curr_addr = 0
    mbi = MEMORY_BASIC_INFORMATION64()
    while curr_addr < 0x7FFFFFFFFFFF:
    if VirtualQueryEx(h_process, ctypes.c_uint64(curr_addr), ctypes.byref(mbi), ctypes.sizeof(mbi)) == 0:
    break
    if mbi.State == MEM_COMMIT and mbi.Protect in PAGE_READABLE_PROTECTS:
    if 0 < mbi.RegionSize <= 500 * 1024 * 1024:
    regions.append((mbi.BaseAddress, mbi.RegionSize))
    next_addr = mbi.BaseAddress + mbi.RegionSize
    if next_addr <= curr_addr:
    break
    curr_addr = next_addr

    print(f"[+] 枚举到 {len(regions)} 个可读内存块,开始特征扫描…")

    # 2. 扫描 Config.Cipher 模式
    remaining_salts = set(target_salts)
    buf = ctypes.create_string_buffer(4096)
    bytes_read = ctypes.c_size_t(0)

    for base_addr, region_size in regions:
    if not remaining_salts:
    break

    # 流式读取块
    chunk_size = min(region_size, 10 * 1024 * 1024) # 每次最大 10MB
    offset = 0
    while offset < region_size:
    read_len = min(chunk_size, region_size – offset)
    data_buf = ctypes.create_string_buffer(read_len)
    if ReadProcessMemory(h_process, ctypes.c_uint64(base_addr + offset), data_buf, read_len, ctypes.byref(bytes_read)):
    raw_bytes = data_buf.raw[:bytes_read.value]

    # 匹配降级明文正则
    for match in RAW_KEY_HEX_RE.finditer(raw_bytes):
    hex_str = match.group(1).decode('ascii')
    key_candidate = hex_str[:64]
    salt_candidate = hex_str[64:96].lower()

    if salt_candidate in remaining_salts:
    enc_key = bytes.fromhex(key_candidate)
    page1 = page1_map[salt_candidate]
    if verify_enc_key_with_page1(enc_key, page1):
    found_keys[salt_candidate] = key_candidate
    remaining_salts.remove(salt_candidate)
    print(f"[!] [内存匹配成功] Salt: {salt_candidate} -> Key: {key_candidate}")
    if not remaining_salts:
    break

    offset += read_len

    finally:
    ctypes.windll.kernel32.CloseHandle(h_process)

    return found_keys

    # ==============================================================================
    # 主流流程控制逻辑
    # ==============================================================================
    def decrypt_single_page(enc_key: bytes, page_bytes: bytes, page_number: int) –> bytes:
    """解密单个 4096 字节物理页"""
    iv_offset = PAGE_SIZE – RESERVE_SIZE
    iv = page_bytes[iv_offset : iv_offset + IV_SIZE]

    if page_number == 1:
    ciphertext = page_bytes[SALT_SIZE:iv_offset]
    plaintext = aes_cbc_decrypt_cng(enc_key, iv, ciphertext)
    return SQLITE_HEADER + plaintext + (b"\\x00" * RESERVE_SIZE)
    else:
    ciphertext = page_bytes[:iv_offset]
    plaintext = aes_cbc_decrypt_cng(enc_key, iv, ciphertext)
    return plaintext + (b"\\x00" * RESERVE_SIZE)

    def decrypt_database(src_path: str, dst_path: str, key_hex: str) –> bool:
    """批量解密物理数据库文件"""
    enc_key = bytes.fromhex(key_hex)
    file_size = os.path.getsize(src_path)
    total_pages = file_size // PAGE_SIZE

    os.makedirs(os.path.dirname(os.path.abspath(dst_path)), exist_ok=True)

    with open(src_path, "rb") as fin, open(dst_path, "wb") as fout:
    for pgno in range(1, total_pages + 1):
    page = fin.read(PAGE_SIZE)
    if len(page) < PAGE_SIZE:
    page = page + b"\\x00" * (PAGE_SIZE – len(page))
    fout.write(decrypt_single_page(enc_key, page, pgno))

    print(f"[+] 解密成功: {os.path.basename(src_path)} -> {dst_path}")
    return True

    def main():
    parser = argparse.ArgumentParser(description="Windows 平台数据库动态密钥提取与离线解密工具")
    subparsers = parser.add_subparsers(dest="command", required=True)

    # 提取指令
    ext_parser = subparsers.add_parser("extract", help="从内存中提取数据库密钥")
    ext_parser.add_argument("–process", default="WeChat.exe", help="目标进程名")
    ext_parser.add_argument("–db-dir", required=True, help="加密数据库目录路径")
    ext_parser.add_argument("–decrypt", action="store_true", help="提取成功后自动执行全量解密")
    ext_parser.add_argument("–output-dir", default="./decrypted_dbs", help="解密输出路径")

    # 离线解密指令
    dec_parser = subparsers.add_parser("decrypt", help="使用已知 Key 离线解密数据库")
    dec_parser.add_argument("–db-file", required=True, help="目标 .db 文件路径")
    dec_parser.add_argument("–out-file", required=True, help="解密后文件保存路径")
    dec_parser.add_argument("–key", required=True, help="64 位 Hex 密文 Key")

    args = parser.parse_args()

    if args.command == "decrypt":
    decrypt_database(args.db_file, args.out_file, args.key)
    return

    if args.command == "extract":
    # 1. 扫描数据库目录,搜集 Salt 与 Page 1
    page1_map = {} # salt_hex -> page1_bytes
    db_paths = []
    for root, _, files in os.walk(args.db_dir):
    for file in files:
    if file.endswith(".db") and not file.endswith("-wal") and not file.endswith("-shm"):
    full_path = os.path.join(root, file)
    if os.path.getsize(full_path) >= PAGE_SIZE:
    with open(full_path, "rb") as f:
    p1 = f.read(PAGE_SIZE)
    salt_hex = p1[:SALT_SIZE].hex().lower()
    page1_map[salt_hex] = p1
    db_paths.append((full_path, salt_hex))

    print(f"[+] 收集到 {len(db_paths)} 个数据库文件,包含 {len(page1_map)} 个独立 Salt")

    # 2. 定位 PID 并扫描内存
    pid = find_process_id_by_name(args.process)
    if not pid:
    print(f"[-] 未找到进程 {args.process},请确保应用已运行。")
    sys.exit(1)

    print(f"[+] 找到目标进程 {args.process} PID: {pid}")
    keys_found = scan_process_memory_for_keys(pid, set(page1_map.keys()), page1_map)

    print(f"\\n[=] 扫描完毕,成功获取 {len(keys_found)} / {len(page1_map)} 组 Key:")
    for s, k in keys_found.items():
    print(f" Salt: {s} => Key: {k}")

    # 3. 如果开启了自动解密
    if args.decrypt and keys_found:
    print("\\n[*] 开始物理逐页离线解密…")
    for full_path, salt_hex in db_paths:
    if salt_hex in keys_found:
    rel_path = os.path.relpath(full_path, args.db_dir)
    out_path = os.path.join(args.output_dir, rel_path)
    decrypt_database(full_path, out_path, keys_found[salt_hex])

    if __name__ == "__main__":
    main()


    九、 总结与防御对抗思考

    桌面加密数据库的技术演进是一个典型的“攻防协同推进”过程:从最早的明文数据库,到标准 SQLCipher 的硬编码 Key,再到 WCDB 引入的 Config.Cipher 掩码保护及指针链混淆。然而,只要应用遵循“在用户态内存中进行解密计算”这一不可规避的冯·诺依曼架构约束,其内存空间中的密钥与加密上下文就始终存在被同权限探针捕获的可能。

    9.1 应用侧防御加固建议

  • 白盒密码学(White-box Cryptography): 在派生 Key 与 AES 计算过程中引入白盒算法,消除 Plaintext Key 在内存中的显式展开。
  • 硬件级安全绑定(Hardware Security Binding): 结合 Windows DPAPI 及 TPM 2.0 芯片对动态凭据进行硬件锁死,限制外部进程直接读取秘钥。
  • 主动反调试与内存保护: 通过启用 Windows PPL(Protected Process Light)限制同权限进程的 OpenProcess(PROCESS_VM_READ) 权限,并在敏感内存使用完毕后立即执行零化抹除(Zeroization)。
  • 补充

    WCDB 和 SQLCipher 本身确实是跨平台的(支持 Windows、macOS、iOS 和 Android),前述分析针对 Windows 主要是因为代码专为 Windows 系统底层 API(bcrypt.dll 与 Win32 进程接口)编写。

    如果将该工具移植或扩展到 macOS 平台,底层的数据库物理结构(SQLCipher 4 页布局、HMAC 校验机制)完全一致,但操作系统层面的内存提取与密码学调用机制有本质不同。

    操作系统底层实现对比

    维度Windows 平台实现macOS 平台实现
    原生密码学 API bcrypt.dll (CNG API) libCommonCrypto.dylib / CryptoKit
    内存扫描 API VirtualQueryEx + ReadProcessMemory mach_vm_region + mach_vm_read_overwrite
    进程访问权限 管理员权限 + SeDebugPrivilege root 权限 + 绕过/禁用 SIP + task_for_pid
    动态调试工具 cdb.exe / WinDbg lldb / dtrace
    可执行文件格式 PE / PE32+ (.exe, .dll) Mach-O (.dylib, Fat/Universal Binary)

    macOS 端的关键技术差异

    • Mach 内核内存读取 API:macOS 不存在 Win32 的 ReadProcessMemory。要读取其他进程内存,必须使用 Mach API:首先通过 task_for_pid() 获取目标的 Task Port,然后使用 mach_vm_region() 循环遍历虚拟内存区域(VM Region),最后调用 mach_vm_read_overwrite() 将目标进程内存复制到当前缓冲区。
    • 原生密码学库替换:macOS 系统自带 CommonCrypto C 语言库,可通过 Python 的 ctypes.cdll.LoadLibrary("/usr/lib/system/libcommonCrypto.dylib") 加载。解密时调用 CCCrypt(传入 kCCAlgorithmAES 与 kCCOptionECBMode 或 CBC 模式),派生密钥时调用 CCKeyDerivationPBKDF。
    • 极严格的系统安全防护(SIP & Hardened Runtime):在 macOS 上提取内存的最大挑战是系统安全机制。哪怕使用 sudo 运行 Python,系统保护(SIP – System Integrity Protection)和硬化运行时也会直接拦截针对已签名应用进程的 task_for_pid 调用。通常需要临时关闭 SIP、调试授权(Entitlements)或附加 lldb 才能完成内存读取。
    • 内存布局与编译差异:由于 macOS 采用 Clang/LLVM 编译链,其结构体 Padding、内存对齐方式以及数据段分布(Mach-O 的 __DATA / __TEXT 段)与 Windows 的 MSVC 编译产物有所不同,虽然混淆算法与数据库密钥逻辑一致,但内存定位的偏移量与特征码搜索策略需要针对 Mach-O 重新调整。
    赞(0)
    未经允许不得转载:171主机测评 » 有AI后小白装大佬:某IM软件DB文件动态密钥提取与离线解密
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址