逆向米■游N0vaDesktop
首先众所周知米■游N0vaDesktop的实现是一个cef player,而且N0vaDesktop太大了,而且一旦开启就要吃掉一堆内存&CPU,而且还会拉低续航。我们想要下载N0vaDesktop中的壁纸,用Windows原生壁纸工具设置,然后直接卸载N0vaDesktop,但是一个一个点击下载按钮太难受了,所以希望自动化该流程。
声明:本文仅供学习交流使用,禁止用于任何商业用途及非法用途,作者不对因使用本文内容而产生的任何后果负责。
渐入
先分析N0vaDesktop.exe,直接查看strings,找到:
1 | .rdata:0000000140B98140 00000032 C https://api-takumi-static.■■■■■■.com/cls/backyard |
可以猜到这些就是API接口。
直接访问提示{"data":null,"message":"参数错误","retcode":-1000}
跟进到函数体内部:
1 | __int64 __fastcall sub_1400AB7E0(__int64 a1) |
可以看到请求的URL是https://api-takumi-static.■■■■■■.com/cls/backyard/api/listVideoGroups,请求头有content-type: application/json,请求体是一个JSON对象,包含app_id和group_video_version字段。app_id的值是固定的5f17af398d20e6ddc9767e5a,group_video_version的值是0。
发送这个请求可以得到响应:
1 | {"retcode":0,"message":"OK","data":{"cipher_text":"BKz4IZOvkyzwYAXKDPi2BKEQqTLF29QyLsWt52Zt+iIx2Cs2k9wbmu5oWHOkYL7Nb45QXPdIS7LXt9rhEBR7QQwhhlrME463UPKrsK/E20n/Voni9IBmZLGxK9mBWrIVge7v06RH5dOg1VOBR/lIb0fs9J90Ely00AxaFTj8pg2QK9fosQP9CITav7pFaHriHxM6FfquMtNIyHs+Yv7sR3VYhEMAgqm7hgq6Y2gYnf1grHe2VIwb2qeLB6RKnHfD5HO19fRJmWprWy3pTQE/GRBHyIqJCmtuvKNzsvJ4FmqENTOqUiGxDZzKV9E09FSWveDEBDAk67CRskoh2yghEL45scSojVrBTZZvrvT+pHOcJU2DG95mLiVpuwdtOLleuBUSQzuabgIhwidqXPo0r6DaMJGI4ou/70LXy8pmvI1SmHT1kaNgNT0FFikDG9uBaVAIxOEXT8pw5iV1PDhDnCNkJy9GhvmJaHxFCupTkDyR9mayTSDcoo0oDj7SidY1IU3VsoMzKEFwbJB3amFzicLl9ldSAqJPf4waf0lBVsucJGT6BVl44egazxlSpA6yGr6gLI5Lx1NfbabvRe28GdskNX5QGxkkhIbAg8Sn2Po="}} |
注意到data字段中的cipher_text,这是一个加密后的字符串。(首先肯定不是纯base64)
解密
在刚才那一系列strings周围,找到了一个:
1 | .rdata:0000000140B98000 00000010 C GetFsm success. |
推测与响应处理有关,跟进到函数:
注意到这么一段:
1 | if ( *(_DWORD *)(*(_QWORD *)a2 + 4i64) ) |
先sub_140087A20((QByteArray *)v18, a2);如果*(_DWORD *)(v18[0] + 4)那么调用sub_1400B3100(a1, v18);,否则就会记录日志“get text from response failed.”,这难道不引人注意吗?
前面一个函数:
1 | QByteArray *__fastcall sub_140087A20(QByteArray *a1, __int64 a2) |
注意到里面提到了data还有cipher_text字段,正好跟响应JSON对应。问了AI说它就是从响应中提取cipher_text字段。
继续跟进到sub_1400B3100(a1, v18);:
1 | void __fastcall sub_1400B3100(__int64 a1, __int64 a2) |
上来就是
1 | v17[1] = -2i64; |
你看它fromJson,那么sub_1400879C0((__int64)v13, a2);肯定是要从cipher_text里面搞出一个json。
1 | __int64 __fastcall sub_1400879C0(__int64 a1, __int64 a2) |
这不就看到曙光了,它调用了CryptoOperator::DecodeAes。
1 | void __fastcall sub_1401A1CE0(__int64 a1) |
很明显这个函数是用来生成AES密钥的,密钥是把字符串”aomKlo+qx6Djr”的后8位跟”rHcTGz1pPLGH”的前8位拼接起来得到的,也就是o+qx6DjrrHcTGz1p。
问题是还差一个iv向量,没有看到iv相关的东西,猜测可能是全0向量(解一下发现第一个block乱码)。
问题是CryptoOperator::DecodeAes(a1, a2, &v5);这个函数不是在N0vaDesktop.exe中实现的,直接去看导入表:
1 | 00000001403CD5E0 CryptoOperator::DecodeAes(QByteArray const &,QString const &) MHYDPInterFace |
所以接下来逆向MHYDPInterFace.dll,目标是获得iv。
1 | __int64 __fastcall CryptoOperator::DecodeAes(__int64 a1, __int64 a2, __int64 a3) |
直接询问AI:
在DecodeAes函数中,我们注意到调用sub_180004080时,传递的第三个参数是v7,即从Base64解码的QByteArray,这应该是密文。第四个参数是a3(即v12,是a3字符串的UTF-8表示),第五个参数是v6(即v10,也是a3字符串的UTF-8表示)。所以,这里a3字符串被重复使用了。实际上,在解密过程中,a3字符串可能作为密钥(key)和初始化向量(IV)?这不太符合常规。
我们不等输出了,直接去cyberchef测试:
发现解密大成功。
稍微优化一下进下一步
在先前的步骤中,我们得知了对于任意API request,我们都可以通过Base64解码后AES解密得到原始数据,AES的key和IV均为字符串”a+qx6DjrrHcTGz1p”的UTF-8表示。
那么我们写一个node.js脚本来自动化这个过程:他会读取URL,然后进行解密,最后输出结果。
1 | const crypto = require('crypto'); |
接下来是web的部分
要研究清楚这些API request的作用和参数。
咋搞呢?懒得一个一个试了,目前想到的方法是把几个核心函数喂给AI让它整理。整理结果如下:
URL:
https://api-takumi-static.■■■■■■.com/cls/backyard/api/getFSM参数:
- app_id:
5f17af398d20e6ddc9767e5a - version:
2.2.1.3 - is_preview:
false
- app_id:
URL:
https://api-takumi-static.■■■■■■.com/cls/backyard/api/listWebConf参数:
- app_id:
5f17af398d20e6ddc9767e5a - is_preview:
false
- app_id:
URL:
https://api-takumi-static.■■■■■■.com/cls/backyard/api/listStyle参数:
- app_id:
5f17af398d20e6ddc9767e5a - version:
2.2.1.3 - is_preview:
false
- app_id:
URL:
https://api-takumi-static.■■■■■■.com/cls/backyard/api/listVideoGroups参数:
- app_id:
5f17af398d20e6ddc9767e5a - group_video_version:
0
- app_id:
URL:
https://api-takumi-static.■■■■■■.com/cls/backyard/api/listGroupVideos参数:
- app_id:
5f17af398d20e6ddc9767e5a - key: string,来自传入的QString参数
- group_video_version: integer,默认为
0 - format: integer,来自传入的QString参数的第二个字段
- current_page: integer,来自传入的QString参数的第六个字段
- page_size: integer,来自传入的QString参数的第七个字段
- is_preview:
false - tag: string,可选,来自传入的QString参数的偏移字段
- app_id:
URL:
https://api-takumi-static.■■■■■■.com/cls/backyard/api/getVideoGroupStats参数:
- app_id:
5f17af398d20e6ddc9767e5a - key: string,来自传入的QString参数(a3)
- group_video_version: integer,默认为
0 - is_preview:
false
- app_id:
别的没试
先listVideoGroups:
1 | { |
用key=game传getVideoGroupStats
1 | { |
然后listGroupVideos:结果略,btw,page_size最大500。
问题来了,我们看到输出的JSON里全是电脑端壁纸,那么手机端壁纸哪去了?
试了一下group_video_version,发现只有0和1有数据,但都不是手机端壁纸。
好吧我们猜测手机端应该和电脑端不是一个API接口,手机端的等有空了再逆向。
将ndf转为png或mp4
这个网上教程多了是了,不再赘述。