沪江开心词场APP词书接口和数据破解

查看 80|回复 6
作者:uusama   
说明
接上一篇沪江小D(现APP已不能使用)的破解,主要思路也是Fiddler抓接口,然后jadx反编译apk包,根据接口定位反编译代码,分析代码得到数据解密方式,最后使用python实现解密算法得到解密秘钥和文本。
准备工具
  • Android模拟器:逍遥模拟器
  • Anroid抓包代.理软件: Drony 1.3.154
  • PC抓包软件: Fiddler
  • apk反编译工具: jadx 1.3.0
  • Android so库反编译工具:IDA Pro,站内有,这儿就不放链接了

    相关工具以及破解的apk包可以通过百度云获取: https://pan.baidu.com/s/1CjdDkXgVGJpMToXuc_8SRQ,提取码uurr。
    实现的整个源码可以参考github,其中实现了沪江词书的解密和爬虫。: https://github.com/youyouzh/PythonPractice/blob/master/spider/word/crawler/hujiang_crawler.py
    词书查询下载接口
    打开沪江开心词场,注册登录后,添加词书,这儿选择日语词书 -> 查看更多,可以筛选,然后点击其中一本词书添加。抓包接口如下:


    ci-query.png (227.06 KB, 下载次数: 0)
    下载附件
    2024-11-6 10:19 上传

    其中第一个接口GET /v3/book/search_by_tag根据tag来搜索,尝试在postman中构造这个请求,连登录态都不需要,没有任何校验,直接可以查寻词书,返回值也没有做任何加密,其中就有词书id。
    看第二个接口GET /v3/user/me/book/13216/resource用于获取词书文件下载地址,同样在postman中构造这个请求,这个请求需要token登录态才可以,没有其他限制。
    注意到返回值json中显然有词书文件压缩包的地址,而且看接下来的请求是下载2109011636.xml.zip后缀的词书,其他的词书估计没用到先不管。
    访问词书压缩包下载地址https://c2g.hjfile.cn/tools_book_package/13216/2109011636.xml.zip,可以直接下载zip文件,没有登录态和其他安全检查。
    压缩包下载以后,解压,可以看到其中的文件列表,但是提取文件的时候显示需要密码。


    ci-unzip-password.png (37.31 KB, 下载次数: 0)
    下载附件
    2024-11-6 10:20 上传

    词书压缩文件密码算法破解
    压缩包加密码正常操作,要不然别人也太容易爬取它的词书了。别想着暴力破解压缩密码啥的,那太慢了,直接反编译apk,看代码里面怎么解压缩的。
    打开jadx,然后反编译沪江开心词场的apk包。
    这儿的搜索就很有技巧了,我们的目的是要找出压缩包的解压密码,可以通过压缩包下载路径搜索,但是压缩包下载路径是从接口GET /v3/user/me/book/13216/resource动态获取的,然而搜索/v3/user/me/book/没有结果,试着搜索/user/me/book/也不行,搜索user/me/book/终于有结果了。搜索的时候要多试一试,找一些区分度大的字符串搜索,这样找到目标代码的概率会比较大。


    ci-jadx-search-book.png (157.82 KB, 下载次数: 0)
    下载附件
    2024-11-6 10:20 上传

    看搜索结果,使用都是在UserBookAPI的类中,猜测这个类就是词书下载处理类。
    在UserBookAPI类中搜索resource很容易定位到接口处理的代码:


    ci-jadx-request-book.png (205.19 KB, 下载次数: 0)
    下载附件
    2024-11-6 10:20 上传

    其中BookResourceResultList结构很明显了,就是词书资源接口的返回结果,这个方法就是一个http request的简单实现,返回值的处理在requestCallback回调中,但是此处的类型RequestCallback是一个抽象处理类,其中没有解压缩包的具体实现,为了弄清楚requestCallback具体是什么,我们搜索这个方法的调用,看这个参数是怎么传进来的。


    ci-jadx-call.png (126.21 KB, 下载次数: 0)
    下载附件
    2024-11-6 10:21 上传

    直接搜索方法名m30808c,可以看到结果有3个,点击第一个进入可以看到创建了一个RequestCallback的匿名实现类,分析其中的实现并没有找到解压的处理。
    这就是我为什么说搜索很有技巧了,上面的步骤其实是在搜索查询词书资源并处理的代码,一般认为下载完之后应该会立即解压缩,所以分析下载后的代码应该很容找到解压缩才对,然而实际上却很费劲,因为程序的解压缩可以放在异步线程里面,或者加一个观察者来实现,解压缩的代码有可能和下载的代码不在一个地方。
    那么换一个思路,一般解压缩我们想到的英文单词是unzip,而且在实际开发过程中,对于解压操作容易出错,我们一般会在解压缩前后打印日志,而日志字符串是没有混淆的。那么我们不妨直接搜索unzip,很少人打印日志用中文,应为写代码是英文,如果打印日志用中文要频繁切换输入法。


    ci-jadx-unzip.png (304.19 KB, 下载次数: 0)
    下载附件
    2024-11-6 10:21 上传

    搜索结果很多,可以大概浏览一下,很多类名都没有混淆,其中词书相关的类比如BookResManager,点进去一眼就可以发现是调用UnzipProcessor的静态方法实现解压缩。看类名就知道是专门处理解压缩的。


    ci-jadx-password-param.png (194.02 KB, 下载次数: 0)
    下载附件
    2024-11-6 10:21 上传

    UnzipProcessor这个类其中解压的实现逻辑,很容易看到密码的赋值unzipModel.unzipPwd = bookRes.m40584j();语句,接下来研究bookRes.m40584j();的实现即可知道密码是怎么得到的,点进这个方法,可以看到密码的构造过程。


    ci-jadx-password-build.png (147.4 KB, 下载次数: 0)
    下载附件
    2024-11-6 10:21 上传

    其中逻辑很简单,首先判断this.f33179i是否为空,为空直接返回空字符串也就是没有密码,否则调用一个加密工具类中的方法EncodeUtils.m39475b(valueOf),在进入这个加密工具类之前,先弄清楚输入参数是什么,也就是this.f33179i的值。


    ci-jadx-zip-md5.png (147.68 KB, 下载次数: 0)
    下载附件
    2024-11-6 10:22 上传

    可以看到this.f33179i是在父类BookResource中定义,注意在其上面的注解@DatabaseField(m23752a = "zip_new_version"),这个字段应该是版本号,千万不要看错看成下面的注解@DatabaseField(m23752a = "zip_md5")!!!查询词书资源那个接口GET /v3/user/me/book/13216/resource返回值中有一个version字段,这儿还不能直接确定(虽然就是),可以继续看代码。
    注意this.f33179i所在类型BookRes只有一个构造函数public BookRes(BookResource bookResource),并且this.f33179i的值是直接从输入参数参数父类成员赋值过来。


    ci-jadx-book-res.png (171.39 KB, 下载次数: 0)
    下载附件
    2024-11-6 10:22 上传

    可以搜索构造函数的调用BookRes(,也可以直接搜索这个成员变量f33179i的赋值,得到这个值是怎么取的。此处搜索这个成员变量赋值的地方。


    ci-jadx-f33179i.png (146.16 KB, 下载次数: 0)
    下载附件
    2024-11-6 10:22 上传

    搜索结果主要注意左值表达式,前面几个是BookRes的构造函数赋值,只是简单的类型转换不用理会,而其中有一个比较this.version != a.f33179i,居然和version版本号比较,点进去。


    ci-jadx-version.png (192.08 KB, 下载次数: 0)
    下载附件
    2024-11-6 10:22 上传

    可以看到a.f33179i的值就是this.version,而当前类为BookResourceResult看其中的字段就是词书资源接口返回的结果,而这个f33179i显然就是version字段的内容了,这个函数名称checkVersion更加确认了这一点。
    弄清楚参数的名称以后,我们就可以看那个解密工具类的方法了。


    ci-jadx-encode.png (141.08 KB, 下载次数: 0)
    下载附件
    2024-11-6 10:23 上传

    其实现逻辑很简单,就是把输入参数str转成字节码,然后每个字节取反,再作为参数进行Base64编码。
    到此,终于确定了解压密码的生成方式了,接下来写一个python快速实现这个算法:
    import base64
    def generate_zip_file_password(version: int) -> str:
        version = str(version).encode('UTF-8')
        not_md5 = []
        # 按位取反,注意不能直接使用 ~ ,python中的byte不能为负,此处和 0xFF 取异或,最后得到的结果是一致的
        for byte in version:
            not_md5.append(byte ^ 0xFF)
        not_md5 = bytes(not_md5)
        password = base64.standard_b64encode(not_md5)
        return password.decode("UTF-8")
    generate_zip_file_password(2110131156)
    注意python中byte类型不能为负,所以不能直接取反,而是和0xFF取异或,为了验证最后的结果一致,可以输入相同的参数和Java版本比对。
    // java版本的密码生成函数
    public String generateZipPassword(String version) {
        byte[] bArr = version.getBytes(StandardCharsets.UTF_8);
        int length = bArr.length;
        byte[] bArr2 = new byte[length];
        for (int i = 0; i
    两边运行结果是一致的。而且将生成的密码填入,能够成功解压。


    ci-zip-decode.png (97.2 KB, 下载次数: 0)
    下载附件
    2024-11-6 10:23 上传

    词书内容解密
    打开其中的word.txt显然就是词书的具体内容了,而且是xml格式,python中可以用xmltodict库把xml转成dict格式,然而事情并没有结束,仔细看里面的字段,很多都是加密过的。
    好家伙,压缩包有密码,里面的内容还是加密的!双重加密!很安全。
    不过并不用慌,回到jadx,看看能不能找到解密方式的线索。用刚才解压缩的word.txt里面的几个字段名称搜索可以看到这些字段怎么读取和操作的。


    ci-field-decode.png (291.29 KB, 下载次数: 0)
    下载附件
    2024-11-6 10:23 上传

    很容易发现其中调用了EncodeUtils.m39477a方法,就是之前分析的那个加密工具类,其实现也很简单,刚好和压缩密码生成反过来,先把加密字符串Base64.decode,然后对每个byte取反。
    保险起见,搜索一下m39477a这个方法的调用。


    ci-field-decode-use.png (288.81 KB, 下载次数: 0)
    下载附件
    2024-11-6 10:23 上传

    这些调用,不就是解密字段吗,而且这些字段不就是刚才word.txt里面的字段名称吗,基本100%肯定这个方法就是解密方法了。
    用python快速实现,然后解密试一下:
    import base64
    def decode_book_field(encode_content: str) -> str:
        encode_content = encode_content.encode('UTF-8')
        decode_content = base64.standard_b64decode(encode_content)
        result = []
        for byte in decode_content:
            result.append(byte ^ 0xFF)
        result = bytes(result)
        print(result.decode('UTF-8'))
        return result.decode('UTF-8')
    decode_book_field('HHxTHHxiHHxDHHx3HH1tGWRHHH5wHH5gHH1+HH5UpBx8eBx8Qxx9QKIcfW0WZHkcfX4cfXQcf30=')
    可以顺利得到解密文本。
    至此,完成了对沪江开心词场词书的破解,并且用python实现了整个加密解密算法。

    下载次数, 下载附件

  • 小小面团   

    支持分享,非常厉害
    ameiz   

    非常厉害,学习了。
    cherrytop   

    谢谢楼主分享
    comotemira   

    看完了,楼主厉害~
    铺路的code泥水   

    学习了,思路很好
    woeine   

    学习了,谢谢楼主分享
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部