吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 153|回复: 3
上一主题 下一主题
收起左侧

[原创] 微信4.x 图片离线导出实战:从 WCDB 密钥到 dat 成图

  [复制链接]
跳转到指定楼层
楼主
ACL 发表于 2026-10-5 21:18 回帖奖励
本帖最后由 ACL 于 2026-10-5 22:17 编辑

声明:本文仅供学习交流;所有分析均在本人设备、本人账号上进行,请勿用于任何非法用途。

此文章由AI整理生成,不理解的地方可以在下方留言,看到会一一解答

一、起因:实现查看指定联系人图库后导出备份

需求很具体:不联网,拿名称换 wxid,得到wxid 的 md5 值定位图片路径、解密目录下的图片。信息都在主数据库:

xwechat_files\<wxid>\db_storage\contact\contact.db

SQLite 格式,但文件头不是 SQLite format 3,前 16 字节是随机 salt——整库被 SQLCipher 加密,一个库一把 32 字节 key。key 拿不到,库就是一块密文。所以第一件事:从正在运行的微信进程里把 key 拿出来。

二、 论坛内xuxinhang的方法

xuxinhang 的思路(帖子里有完整过程,建议先读)(https://www.52pojie.cn/forum.php?mod=viewthread&tid=2105908 ):

  1. 幸亏微信开源了数据库框架 WCDB,从 Database::setCipherKey 切入——配置名 "com.Tencent.WCDB.Config.Cipher" 和优先级常量 0x8000000 是现成的定位特征;
  2. 源码往下追 CipherConfig::invoke → CipherHandle::setCipherKey,拿 buffer[66] = '\''(对应汇编 mov byte ptr, 0x27)当断点;
  3. x64dbg 断点命中后,RBX 指向 x'37ba…2ef' 形态的字符串——密钥以明文串常驻内存;
  4. 写脚本全内存扫 x' 前缀 + 后继 hex 校验,把所有密钥列出来,逐个尝试。

(他文中也提到,更早的 0xlane 方法在他那代已经失效——这类方法可能因为微信更新都要重新找一次。)

在 4.1.15.11 上复现:零命中

照做,扫 x' 前缀——零命中。那就把能想到的密钥形态全部试一遍:

扫描特征 结果
x' + 48/96hex 完整密钥串(老方法本体) 0 命中
64hex 裸串正则(无包装的密钥形态) 0 命中
32B 高熵二进制段(密钥的裸形态) 候选全验失败

零命中。密钥在内存里的形态变了。变成什么样了?

三、密钥现在长什么样:从 WCDB 源码到 IDA

3.1 WCDB 源码:密钥的持有者

本地把 WCDB 源码拉下来翻,经过查找分析,最终定位到四处:

src/common/core/CoreConst.h 第 80 行,配置的名字是个明文字符串常量:

// CoreConst.h:80
WCDBLiteralStringDefine(CipherConfigName, "com.Tencent.WCDB.Config.Cipher");

src/cpp/core/Database.cpp 第 455 行,setCipherKey 拿这个名字注册配置:

// Database.cpp:455
void Database::setCipherKey(const UnsafeData& cipherKey, int cipherPageSize, CipherVersion cipherVersion)
{
    if (cipherKey.size() > 0) {
        m_innerDatabase->setConfig(
        CipherConfigName,                              // ← 名字即键
        std::static_pointer_cast<Config>(std::make_shared<CipherConfig>(
        cipherKey, cipherPageSize, cipherVersion)),
        Configs::Priority::Highest);
    } else {
        m_innerDatabase->removeConfig(CipherConfigName);
    }
}

src/common/core/config/CipherConfig.cpp 第 38 行,配置对象持有密钥本体,invoke 时喂给每个新 handle:

// CipherConfig.cpp:38
bool CipherConfig::invoke(InnerHandle* handle)
{
    ...
        ret = handle->setCipherKey(m_key);             // ← m_key 即密钥本体
    ...
}

src/common/core/config/Configs.hpp 第 34 行,这张表的本体:

// Configs.hpp:34
class Configs final : public UniqueList<StringView, std::shared_ptr<Config>> {};

四处连起来读:Database 持有一张名字→配置对象的表,"Cipher" 这一格挂着 CipherConfig 对象,密钥就在它的 m_key 里。

源码能给的到这就到头了:密钥明文进 m_key,invoke 原样转手,看不出任何花样。

3.2 IDA:微信在骨架外加了 XOR

那编译进 Weixin.dll 之后呢?IDA Strings 搜 com.Tencent.WCDB.Config.Cipher,.rdata 仅一处——配置机制在。持有者的函数簇挂在这片常量区里,逐个 F5,管密钥注入的那个(sub_181482920)反编译出来是这样:

// sub_181482920(a1=持有者this, a2=框架句柄)
if ( sub_18086A940(a1 + 136) )            // 取串:+136 
    v4 = sub_1802A2130(a1 + 8);
else
    v4 = sub_1802A2130(a1 + 136);
v5 = sub_1874A9DF0(v4);                   // 开一块等长缓冲
...
if ( v8 >= 0x20 )                          // XOR,长度够 32B 走 SIMD
{
    v28 = _mm_xor_ps(*(__m128 *)(v5 + 16), (__m128)xmmword_18901B0D0);
    *(__m128 *)v5 = _mm_xor_ps(*(__m128 *)v5, (__m128)xmmword_18901B0C0);
}
...
do
    *(_BYTE *)(v5 + v9) ^= byte_18901B160[v9 & 0x1F];   // XOR:data[i] ^= MASK[i % 32]
while ( v8 > v9 );

sub_18145B7D0(v32, v5, v12);               // 注入:还原后的明文喂给 setCipher
v13 = (*(...)(a2->vtable + 104))(a2, v32);
sub_18145BD80(v32);

LOBYTE(v15) = -69;                          // 抹除 0xBB
sub_1875426F0(v5, v15, v14);
...
LOBYTE(v24) = -86;                          // 抹除 0xAA
sub_1875426F0(v5, v24, v23);

这段伪代码说白了就是四句话:

  1. 密钥放在这个对象的两个成员里(+8 和 +136,新成员优先用);
  2. 放进去之前,先和一段 32 字节的常量挨个字节做异或,存的是"打乱过的";
  3. 要用的时候,临时拷一份、异或回来、喂给 sqlite,用完立刻把临时那份抹掉——所以内存里从头到尾找不到明文密钥,老方法就死在这;
  4. 但原来存的那份不动,一直躺在那——微信自己随用随取,我们也就随时能来读。

那 32 字节长这样,双击伪代码里的 xmmword_18901B0C0(或 byte_18901B160)就能看到:

D2 C7 44 24 58 02 00 00 00 48 89 44 24 50 48 8B
45 00 48 84 4C 24 48 48 89 44 25 40 48 58 4C 24

有个细节值得记一笔:这 32 字节本身是一串合法的 x86-64 指令(48 89 44 24 50 就是 mov [rsp+50h], rax),在常量区里毫不起眼——想在二进制里直接把它搜出来,不容易。

3.3 老方法失效的原因

  1. 明文是瞬态的——还原只发生在"用"的那一刻,用完缓冲立即擦除;4.0.5.18 那代明文串常驻,4.1.15.11 这代明文只闪一下。
  2. 驻留态是异或的——常驻内存的那份是异或过的,x' 正则对异或字节完全失效。

四、方法:搜容器名,不搜密钥

密钥本体搜不到,但装密钥的对象跑不掉:它必须能被框架按名字找到(前面源码那套机制),所以指向它的配置名是明文字符串。在内存里搜这个字符串,就等于在搜"密钥持有者在这"的路标——

① 映射区(DLL 常量区)搜 "com.Tencent.WCDB.Config.Cipher"——静态串,全进程就 2 处
② 私有区(堆)搜 {ptr, 31} 的 qword 对定位 std::string 节点,节点验证:+0x10 回指 needle、+0x18 == 31
③ node+0x28 → config 对象
④ config+0x88 → {+0x8 data_ptr, +0x10 data_len},读出 blob(≤1KB),和 32B 掩码逐字节异或
⑤ 正则 [xX]'([0-9a-fA-F]{64,192})' 抓字面量:前 64hex=密钥,后 32hex=内嵌 salt
⑥ 拿库文件第一页做 HMAC-SHA512 离线验证,开得动就是对的

大白话解释就是:

  1. 搜路标。 打个比方:微信把所有房间的钥匙串成一串,锁进一个带锁的盒子。明文钥匙只在使用的一瞬间闪现,平时盒子里锁着。撬不开盒子,但盒子主人登记名字的那块名牌是明文的——名牌钉在 DLL 常量区的公告栏上,全进程就两块。先到公告栏把名牌的位置记下来;
  2. 认名牌。 再到堆里搜"谁登记了这个名字":挨家看有没有 {指向名牌的指针, 31} 这样一对值,对上了就是一个持有者的门牌——喊一声应答 178 个;
  3. 顺线走。 字符串对象往后 0x28 处藏着一个指针,顺着它就走到装密钥的对象;
  4. 开盒子。 密钥对象内部 +0x88 处是一对"指针+长度",指向真正存密钥的那坨数据,读出来和 32 字节掩码挨个异或——盒子开了,开出来是一张清单,每行一把钥匙加一段盐;
  5. 读清单。 清单是 x'....' 格式的文本,前 64 个 hex 是密钥,后 32 个是盐;
  6. 试钥匙。 每把钥匙去开对应的库文件,开得动就是对的。

五、验证:

5.1 完整脚本(可直接运行)

# scan_keys_simple.py — 精简版 key 提取
# 用法: python scan_keys_simple.py <db目录>     例: python scan_keys_simple.py xwechat_files
# 原理: 全内存扫 "com.Tencent.WCDB.Config.Cipher" -> 搜 {串地址,31} 对 -> node+0x28=config
#       -> config+0x88={+8 ptr,+0x10 len} -> blob XOR 32B 掩码解码 -> 抓 x'<key><salt>' -> salt 对库名

import ctypes, os, re, struct
from ctypes import wintypes as wt

NEEDLE = b'com.Tencent.WCDB.Config.Cipher'
XOR = bytes.fromhex("d2c7442458020000004889442450488b450048844c2448488944254048584c24")

k32 = ctypes.windll.kernel32

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

def rpm(h, addr, sz):
    buf = ctypes.create_string_buffer(sz); n = ctypes.c_size_t()
    if k32.ReadProcessMemory(h, ctypes.c_uint64(addr), buf, sz, ctypes.byref(n)):
        return buf.raw[:n.value]
    return None

def weixin_pid():
    class PE(ctypes.Structure):
        _fields_ = [("dwSize", wt.DWORD), ("cnt", wt.DWORD), ("pid", wt.DWORD),
                    ("heap", ctypes.c_void_p), ("mod", wt.DWORD), ("thr", wt.DWORD),
                    ("ppid", wt.DWORD), ("pri", wt.LONG), ("flags", wt.DWORD),
                    ("exe", ctypes.c_char * 260)]
    snap = k32.CreateToolhelp32Snapshot(2, 0); pe = PE(); pe.dwSize = ctypes.sizeof(PE)
    pid = 0
    if k32.Process32First(snap, ctypes.byref(pe)):
        while True:
            if pe.exe.decode(errors='replace').lower() == 'weixin.exe':
                pid = pe.pid; break
            if not k32.Process32Next(snap, ctypes.byref(pe)): break
    k32.CloseHandle(snap)
    return pid

def regions(h):
    out, addr, m = [], 0, MBI()
    while addr < 0x7FFFFFFFFFFF:
        if not k32.VirtualQueryEx(h, ctypes.c_uint64(addr), ctypes.byref(m), ctypes.sizeof(m)):
            break
        if m.State == 0x1000 and m.Protect in (2, 4, 8, 0x10, 0x20, 0x40, 0x80) and m.RegionSize < 0x20000000:
            out.append((m.BaseAddress, m.RegionSize))
        addr = m.BaseAddress + m.RegionSize if m.RegionSize else addr + 0x1000
    return out

def u64(b, o): return struct.unpack_from("<Q", b, o)[0]

def scan_blobs(h):
    regs = regions(h)
    addrs = set()
    for base, size in regs:                              # pass1: 找 needle 地址
        d = rpm(h, base, size)
        if d:
            p = d.find(NEEDLE)
            while p >= 0:
                addrs.add(base + p); p = d.find(NEEDLE, p + 1)
    if not addrs:
        return []
    pat = re.compile(b'|'.join(re.escape(struct.pack('<Q', a)) for a in addrs))
    len31 = struct.pack('<Q', len(NEEDLE))
    blobs = []
    for base, size in regs:                              # pass2: {ptr,31} 对 -> 顺指针取 blob
        d = rpm(h, base, size)
        if not d: continue
        for m in pat.finditer(d):                        # 所有地址一条正则一次扫完
            if d[m.end():m.end() + 8] != len31: continue
            cfg = u64(d, m.start() + 0x18)               # pair 即 node+0x10,cfg=node+0x28 就在手里
            if cfg <= 0x10000: continue
            obj = rpm(h, cfg + 0x88, 0x18)
            if not obj: continue
            dp, dl = u64(obj, 8), u64(obj, 0x10)
            if 0 < dl <= 4096:
                blob = rpm(h, dp, dl)
                if blob: blobs.append(blob)
    return blobs

def keys_from_blob(blob):
    dec = bytes(v ^ XOR[i % 32] for i, v in enumerate(blob))
    out = []
    for m in re.finditer(rb"[xX]'([0-9a-fA-F]{64,192})'", dec):
        run = m.group(1).decode().lower()
        # 长串可能是多把 key 拼接,按 32 hex 滑窗切
        starts = [0] if len(run) <= 96 else list(range(0, len(run) - 63, 32)) + [len(run) - 64]
        for s in dict.fromkeys(starts):
            if 0 <= s and s + 64 <= len(run):
                k = run[s:s + 64]
                salt = run[s + 64:s + 96] if s + 96 <= len(run) else ''
                if (k, salt) not in out: out.append((k, salt))
    return out

def main():
    import sys
    root = sys.argv[1] if len(sys.argv) > 1 else r'xwechat_files'
    salt2name = {}
    for dp, _, fns in os.walk(root):
        for fn in fns:
            if not fn.endswith('.db'): continue
            p = os.path.join(dp, fn)
            try: page1 = open(p, 'rb').read(4096)
            except OSError: continue
            if len(page1) == 4096 and page1[:15] != b'SQLite format 3':
                salt2name.setdefault(page1[:16].hex(), os.path.relpath(p, root))
    # print('加密库 %d 个' % len(salt2name))
    pid = weixin_pid()
    h = k32.OpenProcess(0x410, False, pid)
    if not h:
        print('OpenProcess 失败 pid=%d err=%d' % (pid, k32.GetLastError())); return
    n, seen = 0, set()
    for blob in scan_blobs(h):
        for key, salt in keys_from_blob(blob):
            if key in seen: continue
            seen.add(key)
            name = salt2name.get(salt)
            if not name:                                 # 无内嵌 salt,跳过不输出
                continue
            print('%s  %s' % (name, key)); n += 1
    print('共 %d 个 key' % n)

main()

5.2 闭环:打开 contact.db,wxid 到手

拿 contact.db 的 key 打开库:

# query_db.py — 微信4x 加密库直查(key 已在手,纯离线版)
# 用法:
#   python query_db.py <db路径> <64hex key>              列全部表名
#   python query_db.py <db路径> <64hex key> contact      表结构 + 前 10 行
#   python query_db.py <db路径> <64hex key> "SELECT ..." 任意 SQL

# "SELECT username, nick_name, remark FROM contact WHERE nick_name = '名称' OR remark = '名称'"

import hashlib, hmac as hm, os, re, struct, sys, tempfile
from Crypto.Cipher import AES

PS, KS, SS, IV, HS = 4096, 32, 16, 16, 64
RS = (IV + HS + 15) // 16 * 16

def hmac_ok(raw, key):
    salt = raw[:SS]
    mk = hashlib.pbkdf2_hmac('sha512', key, bytes(b ^ 0x3A for b in salt), 2, dklen=32)
    h = hm.new(mk, raw[SS:PS-RS+IV], hashlib.sha512); h.update(struct.pack('<I', 1))
    return h.digest() == raw[PS-RS+IV:PS-RS+IV+HS]

def decrypt(raw, key):
    out = [b'SQLite format 3\x00']
    for i in range(0, len(raw), PS):
        pg = raw[i:i+PS]
        if len(pg) < PS: break
        if i == 0: pg = pg[SS:]                           # page1 去盐
        out.append(AES.new(key, AES.MODE_CBC, pg[-RS:][:IV]).decrypt(pg[:-RS]) + pg[-RS:])
    return b''.join(out)

def show(cur):
    print('\t'.join(d[0] for d in cur.description))
    for row in cur.fetchall()[:100]:
        print('\t'.join('' if v is None else str(v)[:120] for v in row))

def main():
    if len(sys.argv) < 3:
        print('用法: python query_db.py <db路径> <64hex key|-> [表名|SQL]'); return
    path, key_hex = sys.argv[1], sys.argv[2]
    arg = sys.argv[3] if len(sys.argv) > 3 else None
    raw = open(path, 'rb').read()
    if raw[:15] == b'SQLite format 3':
        tmp = path                                        # 明文库直接查
    else:
        key = bytes.fromhex(key_hex)
        if not hmac_ok(raw[:PS], key):
            print('key 配对失败(HMAC 不匹配)'); return
        tmp = os.path.join(tempfile.gettempdir(), 'wxq_' + os.path.basename(path))
        open(tmp, 'wb').write(decrypt(raw, key))
    import sqlite3
    cur = sqlite3.connect(tmp).cursor()
    if arg is None:
        for (t,) in cur.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"):
            print(t)
    elif re.match(r'(?i)^(select|with|pragma)', arg.strip()):
        show(cur.execute(arg))
    else:
        show(cur.execute("SELECT * FROM [%s] LIMIT 10" % arg))

main()

其实拿联系人列表还有一种更简单的实现链路(读联系人管理器 → 全局链表)读取后保存到本地文件

1. GetMgr 拿服务定位器 → 2. 探测:遍历候选 val,走 val+208 链表数 (type|4)==5 命中 → 唯一定位 X
3. head = *(X+208)(同构全局链表头)
4. 沿 node+0x00 走到哨兵终止,每节点 +0x30 = Contact*

六、拿图片解密的两把 key(aes和xor),

图片格式是 .dat 文件,版头 6 字节 07 08 56 32 08 07,前段 AES-128-ECB,尾段一段单字节 XOR。key 是账号级的两样:aeskey(16 字符)和 xorkey(1 字节)。这两样不用像库密钥那样翻内存找持有者——微信内部就有两个现成函数,调用即返回。

xorkey 的 getter(sub_180A72660):

__int64 sub_180A72660()
{
 ...
  return result;
}

aeskey 的 getter(sub_180A72710,出参 std::string):

__int64 __fastcall sub_180A72710(__int64 a1, unsigned int a2)
{

 ...
  return a1;
}

怎么调不展开,路子很多:进程内直调这两个函数、直接读上面那两处缓存,离线推——%APPDATA%\Tencent\xwechat 的 kvcomm 目录里,统计上报文件的文件名中就埋着 key 的种子,配上 wxid 一个 MD5 就能把两把 key 推出来(实测 10 张图全中)。按自己顺手的路子取就行。

七、图片 dat 到成图:MMEncryptOrDecryptFile 的复刻

拿到两把 key 只是入场券,.dat 文件本身的壳是 MMEncryptOrDecryptFile 加密的。通过解密的字段串表,顺藤摸瓜两步就到:

  1. 符号串定位:字符串表里有 kernel::securefileutil::MMEncryptOrDecryptFile。字符串表本身的获取方法不在这里展开——上一篇文章《从零开始解开微信 xlog 加密日志》里已经写过完整流程 https://www.52pojie.cn/thread-2130855-1-1.html
  2. xref 锚到本体:对这条符号串做交叉引用,直接锁到函数本体,sub_180A75F80(_QWORD *a1, _QWORD *a2, unsigned int a3, unsigned int a4) 这样的现成形态,后两个参数是模式位(加密取 0, 1)。
    整个函数就是三段:一个 15 字节的头部解析,一段 AES-128-ECB,一段单字节 XOR。头部解析里最关键的两个魔数 0x07085632、0x0807,加上"加密时 AES 段取前 1024 字节、XOR 尾段不足 1MB 全量"这两个边界条件,都能从反编译的分支里直接读出来。照着写,十几行就能复刻加密解密:
import struct
from Crypto.Cipher import AES

HDR_SZ, AES_MAX, XOR_CAP = 15, 1024, 1 << 20

def decrypt_v2(dat, key16, xorkey):
    assert dat[:3] == b"\x07\x08\x56" and dat[4:6] == b"\x08\x07"  # dat[3] 是版本号 '1'/'2'
    aes_len, xor_len = struct.unpack_from("<II", dat, 6)   # 小端: AES 明文长 / XOR 尾段长
    aes_pad = (aes_len & ~0xF) + 16                        # PKCS7 恒多出一整块
    head = AES.new(key16, AES.MODE_ECB).decrypt(dat[HDR_SZ:HDR_SZ + aes_pad])[:aes_len]
    rest = dat[HDR_SZ + aes_pad:]
    mid = rest[:len(rest) - xor_len]
    return head + mid + bytes(b ^ xorkey for b in rest[len(rest) - xor_len:])

文件里 AES 段的实际长度是补齐后的 aes_pad,不是头里记的 aes_len——这是复刻时最容易踩的坑,解出来取前 aes_len 字节正好把 padding 丢掉。加密是它的镜像:明文前 min(尺寸, 1024) 字节按 PKCS7 补到 16 的倍数做 AES-ECB,尾段按 xorkey 逐字节异或,套上 15 字节头。解密再加密回去,与原 dat 逐字节一致——复刻正确性的判据就这么定。

剥开壳,载荷分两种:普通图片(JPEG/PNG/GIF),存下来就能看;还有微信自家的 wxgf。后者看着神秘,其实 F5 它的解码函数族(wxam_dec_*)会发现整个流程就是:校验头、拿元数据、喂 H.265 解码器、取位图。把一张真实 wxgf 的字节摊开看,38 字节头之后紧跟的就是标准 HEVC 裸流的 NAL 起始码——wxgf 不是私有编码,就是"38 字节头 + 标准 H.265 单帧",所以任何 HEVC 解码器都行,PyAV 几行搞定:

import io
import av

def wxgf_to_image(payload):
    """wxgf(38B头 + 标准HEVC单帧) -> PIL Image;非 wxgf 返回 None"""
    if not payload.startswith(b"wxgf"):
        return None
    i = payload.find(b"\x00\x00\x00\x01")     # 找第一个 HEVC NAL 起始码, 剥掉头
    if i < 0:
        return None
    container = av.open(io.BytesIO(payload[i:]), format="hevc")  # 当 HEVC 裸流解
    try:
        for frame in container.decode(video=0):
            return frame.to_image()            # 第一帧即整图
    finally:
        container.close()

def save_image(img_bytes, path):
    """载荷按实际格式落盘: wxgf 转成 JPEG, 其他格式原样保存"""
    pic = wxgf_to_image(img_bytes)
    if pic is not None:
        out = path.rsplit(".", 1)[0] + ".jpg"
        pic.save(out, quality=88)
        return out, "wxgf->JPEG (PyAV)"
    open(path, "wb").write(img_bytes)
    return path, "raw"

# 一张 dat 从解壳到成图, 两行:
img = decrypt_v2(open(dat_path, "rb").read(), key16, xorkey)
save_image(img, out_path)

从 .dat 到能在看图软件里打开的图片,整条链纯 Python离线闭环(不依赖VoipEngine 导出函数):dat 壳(AES+XOR 复刻)→ wxgf 头(38 字节剥除)→ HEVC 帧(PyAV 解码)→ JPEG/PNG 落盘。

发帖前要善用【论坛搜索】功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
51895991 发表于 2026-10-6 22:13
有成品吗,期待成品
3#
 楼主| ACL 发表于 2026-10-6 22:31 |楼主
4#
tian6888 发表于 2026-10-6 23:39
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-10-7 01:46

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表