在博主的暑假作业中,要用到一个神奇的 App,沪江开心词场。既然要背单词,自然会有词库,本例中使用的是“沪江法语B2班词场”,涵盖了 1000 个 DELF B2 考试高频词汇。不过,博主在一年前就已经通过了这项考试。

该软件在 Google Play 上的版本是 6.3.0,纯净,却过时。根据暑假作业要求,需要升级到有奖励系统的新版本 6.5.1。这个版本可就流氓多了……

既然已经背完所有单词,博主的想法是,将词库做成一个 Excel 文档,附带音频,随时复习。既然手上有 Android 手机,一不做二不休,先将数据库导出为 csv 再另存为 xlsx。不过呢,Android 的安全机制不允许在非 root 权限下访问用户数据,这里利用 Android 自带的备份功能绕过这一限制。

从 Google 开发者官网下载 Android SDK Platform-Tools,并执行

adb backup -f backup.ab com.hjwordgames

由于 Windows 平台没有 dd 工具,博主利用 Python 自带的 zlib 库去除文件头,解压出词库文件 iWord_book,以下为代码。

import base64
import io
import shutil
import sqlite3
import tarfile
import zlib

f = open('backup.ab', 'rb')
f.seek(24)
ab = f.read()
f.close()
stream = zlib.decompress(ab)
tar = tarfile.open(fileobj = io.BytesIO(stream))
tar.extract('apps/com.hjwordgames/db/iWord_book')
tar.close()

之后,使用 SQL 语句读取数据库,提取出加密的文本。

db = sqlite3.connect('apps/com.hjwordgames/db/iWord_book')
cs = db.cursor()
cs.execute('SELECT word, word_def, sentence, sentence_def FROM book_word ORDER BY unit_id')
data = cs.fetchall()
db.close()
shutil.rmtree('apps')

经过多次对照,不难发现,提取出的文本使用自定义 Base64 加密,码表倒置。

f = open('output.csv', 'w', encoding = 'utf8')
for col in data:
    f.write(col[0])
    for i in range(1, 3):
        f.write('\t' + str(base64.b64decode(col[i].translate(str.maketrans(\
'/+9876543210zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA', \
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'))).\
decode('utf8')))
    f.write('\n')
f.close()

如此操作,便得到了一份完整的词库文本,但在手机中找到的音频,均为加了密的特殊文件。博主猜想是沪江开心词场 App 中有对应解密的实现,便尝试监听下载资源包的过程,通过重放攻击来下载原始数据。

由于沪江开心词场启用了 HTTPS 协议,博主使用 Fiddler 4,在 Options - HTTPS 处勾选 Decrypt HTTPS traffic 以注入自签名证书,在传输过程中截得 3 个 zip 格式的加密压缩包。博主还顺便制作了一份通关脚本:

import requests
for i in range(1, 101):
    r = requests.put('https://cichang.hjapi.com/v2/user/me/books/finished_units', headers = headers, json = [{'bookId': 11852, 'finishedDate': '2017-08-31T12:34:56.789', 'isFinished': i == 100, 'source': i, 'studyStars': 3, 'studyWordCount':10, 'unitId': i, 'unitIndex': i}])

其中 headers 按照实际情况填写即可。

对于压缩包解密的实现,博主使用 jadx 对原 apk 进行反汇编,通过跟踪 bookResource.zipNewVersion 变量,得到了生成密码的算法。以下是密码生成程序的 Java 源码。

import java.util.Base64;

public class HelloWorld
{
    public static String decode(String str)
    {
        byte[] bArr = str.getBytes();
        for (int i = 0; i < bArr.length; ++i) bArr[i] = (byte) (bArr[i] ^ -1);
        return new String(Base64.getEncoder().encode(bArr));
    }
    
    public static void main(String []args)
    {
        String zipNewVersion = "<Filename>";
        System.out.println(decode(zipNewVersion));
    }
}

其中 <Filename> 为文件名。或者,使用 Python:

unzipPwd = base64.b64encode(bytes([ord(s) ^ 0xff for s in zipNewVersion])).decode()

如此则更为方便。通过以上的方法,博主获得了 16 位长的密码,并成功解压了之前截获的压缩包,得到纯文本单词表和所有的 mp3 格式音频。