0%

对妹子图APP的逆向分析

据我所知,妹子图https://www.mzitu.com)是很多人学习爬虫的必爬网站之一,而我在进入站点后发现他还提供安卓客户端,因此决定分析一下APP。本文涉及frida工具的使用、360加固应用的简单脱壳、APICloud本地资源的解密和xposed应用的编写等,干货满满!

前言

作为Python的初学者,若想要锻炼自己的代码能力,爬虫肯定是入门的不二选择,通过爬虫既能熟悉语法,又能提高自己的逻辑能力和一些网络编程的技术

但本文不会重点介绍爬虫的知识,而是将重点放在对APP的逆向分析上,因为APP请求的是接口,已经不太属于爬虫的范围了

如果你已经打开了这个网站,或是已经安装好了这个APP,求求你们,请一定要把持住自己

下面开始发车

抓包分析

逆向分析的时候,个人习惯第一步先抓包看看数据的内容。这里使用HttpCanary进行抓包:

请求响应
请求响应

通过抓包可以知道:

  • 请求头必须设置Referer:https://app.mmzztt.com(实际上是后面分析得到,这里顺带一提)
  • 响应体是json类型,且list字段被某种方式加密过了,后面需要重点分析

想要知道如何解密服务器返回的数据,看来只能逆向APP分析了

初步分析APP

首先拖入jadx工具中看下代码结构:

jadx

  • 看到com.qihoo.utilcom.stub出现就知道应用被360加固过了,想要继续分析其代码部分则需要进行脱壳

  • 同时还注意到其assets中疑似有网页资源文件,且文件内容被加密了:

assets

脱壳

对360加固的应用进行脱壳网上已经有比较成熟的方案了,我这里使用天鉴这款App对其进行脱壳,由于篇幅原因,关于360的脱壳有时间我将另写一篇文章分析

脱壳后的文件

脱出来的dex文件重命名为classes.dex拖进jadx中分析其代码:

脱壳后的dex

现在就可以看到其完整的代码了

重新查看代码结构:

apicloud

uzmap

发现该app使用了apicloud开发

APICLOUD简单来说就是:APICloud实现安卓框架,自己开发web就好了

所以app的主要逻辑基本都在js文件中,java代码只是个外壳,所以我们必须要对app的本地资源进行解密

解密本地资源

要知道,如果APP的文件被加密了是不能直接运行的,必须在代码开始运行前进行解密,这也是我们能够逆向解密它的一个关键

这里参考了看雪帖子:APICloud解密本地资源到逆向APP算法到通用资源解密

我们现在有两个方法获得解密后的本地资源:

  1. 分析源码,编写js脚本,使用frida去hook关键方法得到解密后的文件内容
  2. 使用看雪帖子中的通用解密代码,编写xposed hook应用来dump文件(实际上也是分析源码)

方法一 使用Frida直接Hook关键方法

分析源代码

首先找到拦截本地文件关键方法WebViewClient.shouldInterceptRequest(WebView, String),在jadx中搜索shouldInterceptRequest

search

点进去:

shouldInterceptRequest

分析这段代码:

if

跟进b.e():

b.d

继续跟进v.d():

v.d

可知这是判断是否是需要进行管理的文件类型

回到shouldInterceptRequest,this.b和this.a分别处理这两种情况:

需要处理文件不需要处理文件
需要管理不需要处理

到了这里 SDK 接管资源的痕迹就很明显了,再往下应该还可以找到怎么加载&解密文件,但是我们的目的只是dump出原来的明文资源就好,到这里就可以停止跟进分析了

注意到这两个方法的返回都类似于:return new j(c, new com.uzmap.pkg.uzcore.e.d(a2, a));

所以我们跟进j():

j()

这是WebResourceResponse(String mimeType, String encoding,InputStream data)的实现

再往下就是WebView内部了,SDK也没法做什么改变了,相信InputStream就是标准的数据了,所以从new com.uzmap.pkg.uzcore.e.d(a2, a)入手用Frida来Hook出数据看一下

Frida脚本

Frida的安装和配置就不多说了,百度都有的,这里不再赘述

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
import sys
import frida


# 自定义回调函数
def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)


# hook代码,采用js编写
file = open("meizitu.js", encoding="utf-8")
jscode = file.read()
# 配置设备
device = frida.get_remote_device()
pid = device.spawn(["com.kmw.chemeizu"])
device.resume(pid)
session = device.attach(pid)
script = session.create_script(jscode)
script.on('message', on_message)
script.load()
# 暂停脚本,不让程序运行完成退出
sys.stdin.read()

JavaScript(重点):

1
2
3
4
5
6
7
8
9
10
11
12
// 解密本地资源
Java.perform(function () {
var javaString = Java.use("java.lang.String");
// 混淆后需要hook的类名
var resource = Java.use("com.uzmap.pkg.uzcore.e.d");
resource.$init.overload("[B", "java.lang.String").implementation = function (x, y) {
var bytes = javaString.$new(x);
send("文件名:" + y);
send("文件内容:" + bytes);
this.$init(x, y);
};
});

运行结果

运行结果

这样就看到解密后的资源了

方法二 编写Xposed 通用解密程序

上面的代码分析提到,这类程序到最后都会交给Android系统底层的android.webkit.WebResourceResponse类进行网页文件的处理,所以制作通用资源解密程序就可直接Hook这个类来实现

当然也可以用Frida工具实现(Frida工具太强大了),这里介绍另一个Hook方式:自己编写Xposed程序来实现

  1. 首先用Android Studio新建一个工程,这个应用不需要用户界面,所以我选择了Add No Activity

addnoactivity

project_name

  1. 添加依赖de.robv.android.xposed:api:53(根据实际需要选择api版本,这里选择53,它最低支持在Android4.x系统上运行):

20

  1. 新建Hook类,实现IXposedHookLoadPackage这个接口,并实现handleLoadPackage方法:
1
2
3
4
5
6
public class Hook implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
···
}
}
  1. 下面编写hook方法和保存文件的方法:
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
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
// 首先应过滤包名,只Hook某个个应用。这里用于通用解密就不用过滤了,解密后将模块停止就不会造成内存浪费了
// if (lpparam.packageName.equals("com.kmw.chemeizu")){
XposedBridge.log("开始Hook...");
XposedHelpers.findAndHookConstructor(WebResourceResponse.class, String.class, String.class, InputStream.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
XposedBridge.log("开始dump数据");
String mime = (String) param.args[0];
Object obj = param.args[2];
Field[] fields = obj.getClass().getDeclaredFields();

String fileName = "";
byte[] buffer = "没有解密数据".getBytes(StandardCharsets.UTF_8);
for (Field field : fields) {
String name = field.getName();
String type = field.getGenericType().toString();
switch (type) {
case "class [B":
buffer = (byte[]) XposedHelpers.getObjectField(obj, name);
break;
case "class java.lang.String":
fileName = (String) XposedHelpers.getObjectField(obj, name);
break;
}
}
if (!fileName.equals("")||mime.equals("null")) {
saveFile(buffer, fileName);
}
}
});
// }
}

// 将会保存解密后的文件到/data/data/应用包名/files/decrypt/目录下
private void saveFile(byte[] buffer, String fileName) {
String baseDir = AndroidAppHelper.currentApplication().getFilesDir().getPath();
String filePath = baseDir + "/decrypt" + fileName.replace("file://", "");
XposedBridge.log("save file: " + filePath);
try {
File file = new File(filePath);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
FileOutputStream out = new FileOutputStream(file);
out.write(buffer);
out.close();
XposedBridge.log("保存成功");
} catch (IOException e) {
XposedBridge.log("保存文件出错");
}
}
  1. 修改AndroidManifest.xml文件,在application标签下新增:
1
2
3
4
5
6
7
8
9
<meta-data
android:name="xposedmodule"
android:value="true"/>
<meta-data
android:name="xposeddescription"
android:value="一个用于解密apicloud本地资源的工具 -by 苏乞儿"/>
<meta-data
android:name="xposedminversion"
android:value="53"/>
  1. 新建Assets Floder
  2. Assets文件夹下新建文本文件xposed_init,文件内容为你的Hook类路径:
1
com.suqir.android.fuckapicloud.Hook
  1. 由于程序没有Activity,所以不能直接通过点击运行按钮进行安装,我们使用Build->Build bundle(s)/APK(s)->Build APK(s)

21

  1. Build完成后会在右下角出现提示,我们点击location打开文件所在的位置:

apk文件

  1. 将apk文件安装到手机上:

22

我这里使用adb命令:

1
adb install app-debug.apk
  1. 最后一步,在xposed管理器中激活模块,重启手机后打开妹子图app稍等一会就能在/data/data/com.kmw.chemeizu/files/decrypt/目录下找到解密后的文件了:

解密后的文件内容

经过与未加密的文件列表对比,发现并不是所有的加密文件都被dump出来,不过还好不影响我们后面的分析。但是从脱壳后的dex代码中分析,只要找到加密/解密的代码,我们自己写解密脚本也是可以的,那样就可以解密所有加密的文件,这里就不作分析了

数据解密

有了上一步的解密过的资源后,我们就能分析它的逻辑了

打开list.js文件发现以下代码:

ajax

这说明app每次请求数据都会在请求头中设置Referer: 'https://app.mmzztt.com',所以我们后面在写爬虫脚本的时候需要设置这个字段

我们现在的目的是想知道抓包抓到的list字段是如何解密的

同样在list.js文件中,发现可疑代码:

26

这是一段DES解密的代码,通过apicloud请求到的密钥key和偏移量iv来解密传入的data,这里并没有直接解密,而是将这些参数传递给java部分的方法aesDecodeCBCSync,让这个方法进行处理。所以我们打开jadx搜索这个方法:

aesDecodeCBCSync

点进去

jsmethod

发现解密方法AESUtils.decrypt(),跟进

29

在这里我们使用frida来Hook一下这个方法,看看处理结果是什么:

1
2
3
4
5
6
7
8
9
10
11
Java.perform(function () {
var info = Java.use("com.apicloud.signature.AESUtils");
info.decrypt.overload("java.lang.String", "[B", "[B").implementation = function (x, y,z) {
send("待解密:" + x);
send("key:" + String.fromCharCode.apply(String, y));
send("iv:" + String.fromCharCode.apply(String, z));
var res = this.decrypt(x,y,z);
send("结果:" + res);
return res;
};
});

运行结果:

运行结果

太棒了!通过hook这个方法,我们知道了服务器返回的数据确实是经过AES加密的,而且我们得到了密钥key和偏移量iv,这样我们就可以自己写数据的解密脚本来进行爬取服务器数据了,再一次感叹Frida工具的简单和强大!

结语

通过这一次的逆向分析,我学到了很多东西,比如Frida这个工具的简单上手、Xposed应用的开发、还有后面爬虫的一些细节处理。我始终相信,实战是对提升自己技术和能力的最好方法

您的支持将鼓励我的创作!

欢迎关注我的其它发布渠道