声明

仅用于学习目的.

仅使用自己的微信聊天记录.


写在前面的废话

最近由于工作需要我又复习学习了一遍数据库,是在极客时间上跟着陈旸博士学的,其中一节讲微信聊天记录的存储方式,然后评论区有朋友就提到生成词云的点子。很开心,学以致用才是最好的方式嘛。但是呢,这课是2019年的,当我开始做的时候,坑早就埋好了。

朋友们,我折腾了快两周,终于完整的完成了这个效果,先上图!


circle_pan.jpg

circle


butterfly_shang.jpg

butterfly


效果还行吧?下面记录下步骤和遇到的坑。 PS:我使用的环境是 win10+wsl2+Python3.9,使用 mac/linux 的朋友我觉得不需要看(狗头


跟大象放进冰箱一样,我们把聊天记录生成词云也只需要三步对吧。

  1. 获取聊天记录
  2. 解密聊天记录
  3. 解析文本生成词云效果

emmmm…每一步都有很多坑,我们一个个看


获取聊天记录

无非是从手机端或者电脑端获取存放聊天记录的数据库文件对吧,当然我也见过在聊天窗口不断选中聊天内容然后复制的,这种方式还挺开心的,我们不讨论。 说回正途。目前的微信版本,无论从电脑端(3.5.0.39)还是手机端(8.0.15),拿到数据库文件还算简单,但都是加密过的,解密才是最难的。 对我来说电脑端解密似乎更难,我们就从手机端入手吧。

考虑到系统安全和保修问题,现在大家用的手机,无论 Android 还是 iPhone 很少去人root或者越狱了对吧,所以我们可以用电脑端的安卓模拟器获取 root 权限然后仅对微信做一些操作。 对于安卓,我知道特定品牌的手机都有各自的获取备份文件(包括各个APP的数据)的方式,以及iPhone也可以通过iTunes获取备份,但是后面解密数据库依然要靠安卓模拟器,而且考虑到通用性,使用安卓模拟器就是最佳选择了。

PS: 写作本文时发现iPhone通过iTunes似乎是可以直接获取没有加密的版本的 WX Backup,由于我没有用 iPhone, 我们还是继续说 Android.

步骤如下:

  1. 备份聊天记录至电脑
  2. 恢复聊天记录至安卓模拟器
  3. 从安卓模拟器获取数据库文件

备份聊天记录至电脑

大家对电脑端的这个界面应该不陌生。需要注意的是要保证手机和电脑在同一局域网,然后电脑端设置项中发起备份。

备份.png


恢复聊天记录至安卓模拟器

  • 安装模拟器并开启root权限(一般默认都开启了,可以在模拟器的设置中查看)。模拟器有很多种,常见的有网易 MuMu夜游神,我用的是夜游神,注意目前的大多数模拟器都要进 BIOS 开启虚拟技术的功能,具体参看各模拟器的官方帮助。 MuMu 在后面安装微信时会提示网络问题导致失败,大概是公司网络限制,我没有深究,所以换了夜游神,当然夜游神也有坑,后面再说。

  • 通过模拟器自带的应用商店安装微信并登录。注意这时候真正的手机及电脑端的微信都会被迫退出,电脑端重新登录即可,手机端不要登录。

  • 电脑端再选择「恢复聊天记录」至手机(当然实际是恢复到模拟器中)。对于我们这次的需求,为加快速度,我们可以仅恢复文字消息。

更多选项.png


从安卓模拟器获取数据库文件

  • 使用模拟器自带的文件浏览器或者ES文件浏览器(若应用商店中没有可在酷安之类的平台上下载APK直接安装),在类似 /data/data/com.tencent.mm/ 这样的路径下寻找 数据库文件EnMicroMsg.db (仅针对目前8.0.15版本微信,不保证后续微信不会修改路径、文件名甚至存储方式,不过差别应该不大)。

  • 通过模拟器文件共享的方式将 EnMicroMsg.db 放到电脑的文件系统中。 微信终端的这个数据库使用的是SQLite,我们可以用 DB Browser/Navicat 等软件可视化查看这个 EnMicroMsg.db ,显然现在还打不开,因为需要解密。


解密聊天记录

首先说下,之前很多人提到的通过 IMEI 和 UIN 进行 MD5 计算并拿到密码的方式已经没有作用,微信在某个版本之后不再使用这种方式生成的密码,所以我们还是要用其他办法。

emmmm 那么…

  • 使用 frida 获取密码
  • sqlcipher 解密并生成未加密的数据库

使用 frida 获取密码

这是花费我时间和精力最多的一步(狗头微笑

frida 的官网 https://frida.re/ 是这么介绍的:

Dynamic instrumentation toolkit for developers, reverse-engineers, and security researchers.

  • Scriptable
  • Portable
  • Free
  • Battle-tested

简单的说就是它简单、多平台、免费而且久经考验。可用于逆向,可用于安全研究。

好的,我们还是看看它怎么用吧。对于我们的应用场景,它的原理大致是这样的:

手机端模拟器上打开微信,然后运行 frida-server ,再把模拟器的端口转发到电脑端,电脑端运行 Python 脚本与之通信,通过 Python 代码向模拟器中注入一段 Javascript 脚本,最后模拟器上的微信执行登录步骤时会调用一些接口并传递打开数据库用到的密码,这个时候脚本就能获取到密码。

我们具体来看:


电脑端安装 frida

可通过 npm 安装,也可以通过 Python 安装,我选择了通过 Python 来安装。

首先,Python 尽量3.5以上的版本吧,Python 的包管理工具 pip 也要装上。

然后继续,终端输入:

1
2
pip install frida
pip install frida-tools

这里有些坑:

  • 在 python3.10 上安装 frida 后无法 import,会报 DLL load failed while importing _frida , 换 Python3.9 可以,GitHub 上有人提到这个问题,应该是 frida 的兼容性没做好。当然看到此文的时候也许最新版已经修复,你们可以试试。
  • 目前都是使用 pip 进行安装,由于众所周知的网络问题,需要设置国内镜像,使用自己的代理会出现 SSLError,简单 google 后没有找到有用信息,还是设置国内镜像比较快。
  • 即使设置了国内镜像,由于其中会从其他地方下载一些预编译的包,终端还是会卡在那,耐心等会。
  • 即使耐心等待了几分甚至十几分钟,安装完成后你依然要在终端输入 frida --version 以及 frida-ps 看看有没有输出,有的话才安装成功。否则卸载掉再多试几次。
  • 安装 frida 折腾了我很久,祝你好运(狗头微笑

模拟器安装并运行 frida-server

这里说是安装,其实仅仅是把 frida-server 放到模拟器中。从https://github.com/frida/frida/releases 下载 frida-server,有很多版本,应该下哪个呢?

终端输入 adb shell getprop ro.product.cpu.abi 查看模拟器的 cpu 架构,我的是 x86。

此处还要注意一下,如果你安装了 AndroidStudio 又或者其他什么渠道仅仅装了 adb ,你需要保证模拟器中的 adb 版本与你本来装好的 adb 版本一致,否则后续使用 adb push 等命令时会失败。若你没有安装,那就使用模拟器中的 adb 好了。特别的,夜游神的 adb 是 %InstallPath%/Nox/bin/nox_adb.exe

上一步中我安装的 frida 的版本是15.1.14,所以我这里下载的是 frida-server-15.1.14-android-x86.xz

这是一个压缩包,如果你有安装 Git Bash 或者 Cygwin 之类的,你在下载路径中打开终端输入 xz -d -k ./frida-server-15.1.14-android-x86.xz 即可解压, 没有的话,安装一下吧…

打开模拟器中「开发者选项」,再打开「USB调试」的功能。这样电脑端输入 frida-ps -U 就可以查看连接的USB设备的进程。

电脑终端依次输入以下命令:

1
2
3
4
5
6
> adb push ./frida-server-15.1.14-android-x86 /data/local/tmp/
> adb shell
# su
# cd /data/local/tmp/
# chmod 777 ./frida-server-15.1.14-android-x86
# ./frida-server-15.1.14-android-x86

这样模拟器端的 frida-server 就运行起来了。

电脑端另开一个终端,输入:

1
2
adb forward tcp:27042 tcp:27042
adb forward tcp:27043 tcp:27043

这样进行端口转发,然后输入 frida-ps -R 就可以查看服务器端(模拟器)进程的运行情况。

这里我又遇到了一个坑:每次输入命令查看进程,模拟器就会重启,开始我以为是微信导致又或者是我不小心安装了谷歌三件套导致的,最后重装模拟器然后不装任何APP还是不行。 我恍惚了很久,快要放弃时突然想起来看看夜游神的更新记录,我用的V7.0.2.0版本的

支持安卓7(32位)使用Xposed框架

Xposed! 跟 frida 一样的 Hook 框架,可能有点冲突或者其他的Bug吧,回退到V7.0.1.9版本就好了…


电脑端运行Python脚本获取密码

最关键的时候来了,我们保证模拟器上的微信处于登录界面(之前登录并同步了聊天记录,需要退出登录先),模拟器的 frida-server 处于运行状态.

然后电脑端运行如下Python脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import frida
import sys

jscode = """
Java.perform(function () {
    var utils = Java.use("com.tencent.wcdb.database.SQLiteDatabase"); // 类的加载路径

    utils.openDatabase.overload('java.lang.String', '[B', 'com.tencent.wcdb.database.SQLiteCipherSpec', 'com.tencent.wcdb.database.SQLiteDatabase$CursorFactory', 'int', 'com.tencent.wcdb.DatabaseErrorHandler', 'int').implementation = function (a, b, c, d, e, f, g) {
        console.log("Hook start......");
        var JavaString = Java.use("java.lang.String");
        var database = this.openDatabase(a, b, c, d, e, f, g);
        send(a);
        console.log(JavaString.$new(b));
        send("Hook ending......");
        return database;
    };

});
"""


def on_message(message, data):  # js中执行send函数后要回调的函数

    if message["type"] == "send":
        print("[*] {0}".format(message["payload"]))
    else:
        print(message)


process = frida.get_remote_device()
pid = process.spawn(['com.tencent.mm'])  # spawn函数:进程启动的瞬间就会调用该函数

session = process.attach(pid)  # 加载进程号

# session = process.attach('com.android.phone')  # 加载进程号

script = session.create_script(jscode)  # 创建js脚本

script.on('message', on_message)  # 加载回调函数,也就是js中执行send函数规定要执行的python函数

script.load()  # 加载脚本

process.resume(pid)  # 重启app

sys.stdin.read()

然后模拟器上点击登录。很快 Python 脚本就打印出密码来。

密码拿到!!!(开心


Sqlcipher 解密并生成未加密的数据库

接下来相对来说就简单了,由于 sqlcipher 没有免费版了,需要自己根据sqlcipher源码编译,在wsl2下编译时可能遇到的问题:

  • 除了需要安装编译 c/c++ 用到的 build-essential 之外,还需要安装 tcl tk openssl
  • configure 后提示 error, 在 config.log 中查看发现是 ld cannot find -lcrypto, 注意说的是 libcrypto.so,不是libcrypt.so. 查找该库,可能找到类似 libcrypto.so.1.1.1 这样的,建一个软连接就好了
  • make 后报错提示 sqlite_int64 之类未定义的话先查看上一步生成的 sqlite3.h 是否为空,是的话重新 configure 一下。

OK, sqlcipher 就编好了,运行 ./sqlcipher ./EnMicroMsg.db 再输入以下指令,注意 yourkey 替换成上面获取到的密码

1
2
3
4
5
6
7
8
PRAGMA key = "yourkey";
PRAGMA cipher_use_hmac = OFF;
PRAGMA cipher_page_size = 1024;
PRAGMA kdf_iter = 4000;
PRAGMA cipher_compatibility = 1;
ATTACH 'decrypt.db' AS d KEY '';
SELECT sqlcipher_export('d');
DETACH DATABASE d;

这样就拿到了解密的数据库文件 decrypt.db,从 wls2 的文件系统转存到 win10 中,然后就可以用 DB Browser/navicat 之类的软件以可视化方式查看。

当然,当我费劲巴拉编好 sqlcipher 才发现 SQLiteStudio 这款免费软件包含了 sqlcipher, 打开 SQLiteStudio 并选择打开数据库,数据类型选则 SQLCipher,然后密码和参数如上就好啦。Hmmm…SQLiteStudio 效果不如 DB Browser ,我编 sqlcipher 没有错(狗头微笑

还有一个坑就是 wsl2 和大部分安卓模拟器不能同时使用,解决方案是,win10 底部搜索栏直接搜索「启用或关闭 Windows 功能」并打开,使用安卓模拟器时就去掉「Hyper-V」和「虚拟机平台」,使用 wsl2 时则相反。


解析文本生成词云效果

花了这么多时间和精力,终于到了我原本想做的事情(滑稽

我们直接上代码吧,这是一段 Python 脚本,使用 SQL 语句获取特定群或者联系人的聊天记录,然后用 jieba 库对中文分词,wordcloud 库分析词频, matplotlib.pyplot 显示效果。

如果你不能运行,注意查看里面用到的库,使用 pip 安装即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
"""
根据微信数据库文件创建聊天词云
"""

import sqlite3
import re
from wordcloud import WordCloud
from wordcloud import ImageColorGenerator
import matplotlib.pyplot as plt
import jieba
from PIL import Image
import numpy as np

# 这个 stop_words 可以根据你的实际情况再添加

stop_words = [
    '什么',
    '呢',
    '嗯',
    '啊',
    '的',
    '了',
    '吧',
    '吗',
    '么',
    '哦',
    '噢',
]


# 生成词云

def create_word_cloud(f):
    print('根据微信聊天记录,生成词云!')
    mask = np.array(Image.open('./circle.jpg'))
    maskImg = np.array(Image.open('./pikachu-character.jpg'))
    maskColor = ImageColorGenerator(maskImg)
    cut_text = " ".join(jieba.cut(f, cut_all=False, HMM=True))
    wc = WordCloud(
        font_path='simhei.ttf',
        max_words=100,
        # width=2000,

        # height=2000,

        background_color='white',
        mask=mask,
        collocations=False,
        stopwords=stop_words,
        # color_func=maskColor, # 可根据解析的图片颜色生成相应颜色的文字效果

    )
    wordcloud = wc.generate(cut_text)
    # 写词云图片

    wordcloud.to_file("./data_view/wordcloud.jpg")
    # 显示词云文件

    plt.imshow(wordcloud)
    plt.axis("off")
    plt.show()


def extract_content(f):
    f = f[2 : len(f) - 3]  # 去掉开头的 (' 以及结尾的 ',)

    sub_pattern = re.compile(r'^.+:\\n')
    f = sub_pattern.sub('', f)
    return f


def get_content_from_weixin():
    # 创建数据库连接

    conn = sqlite3.connect(r"D:\src_code_release\test_code\python_\data_view\weixin.db")
    # 获取游标

    cur = conn.cursor()
    # 创建数据表

    # 查询当前数据库中的所有数据表

    sql = "SELECT content FROM message;"  # 这样是解析全部聊天内容

    # sql = "SELECT content FROM message WHERE talker = '666666@chatroom' ORDER BY msgId;"  # 解析特定的群

    # sql = "SELECT content FROM message WHERE talker = 'wxid_888888';"  # 解析你与某个朋友的聊天内容


    cur.execute(sql)
    contents = ''
    rets = cur.fetchall()
    for temp in rets:
        contents = contents + extract_content(str(temp))

    # 提交事务

    conn.commit()
    # 关闭游标

    cur.close()
    # 关闭数据库连接

    conn.close()
    return contents


content = get_content_from_weixin()
# 去掉HTML标签里的内容

pattern = re.compile(r'<[^>]+>', re.S)
content = pattern.sub('', content)
# 将聊天记录生成词云

create_word_cloud(content)

如果你的聊天内容非常多,这个脚本可能会运行很久,耐心等待…

再看两张效果图

pika_food4.jpg

Pikachu


heart.jpg

heart

所以我哪里是在学数据库,这是在搞逆向安全研究?

我要继续学习了,祝大家开心~