APP逆向

Android逆向入门:某点中文app网络请求参数分析

2020-08-05  本文已影响0人  好奇害死猫咪阿

前言

很久没有输出点有价值的内容了……今天终于有时间来水一贴,给喜欢逆向的小伙伴提供一点参考资料,权当抛砖引玉了。

为什么会想到这个题材呢,因为我之前的毕设是在线小说阅读器,而我又实在不想搭后台,爬数据,就想着能不能用某中文网的服务器数据。

阅读须知

本文所有内容仅供个人学习交流,严禁用于其它用途。

本文所含知识点:

Fiddler 抓包

既然想拿到服务器的数据,第一件事肯定就是抓包分析一下它的网络请求参数,然后仿照它的格式构造我们自己的请求。抓包的工具有很多,我这里使用的是 Fiddler。下面简单介绍一下怎么用 Fiddler 抓移动端的包。

  1. 打开 Fiddler,在上面的工具条那里点击 Tools-Options,然后切换到 https 选项卡,选中下面几项。
Fiddler https 设置

如果手机是第一次抓包,需要点击上面的 Actions,选择 Export Root Certificate to Desktop 将 Fiddler 自己的证书导出到桌面,然后推到手机里安装这个证书,否则手机不认可这个证书,抓 https 会失败。

之后选中Connections选项卡,如下图勾选。

Fiddler Connections 设置
  1. 将电脑和手机置于同一个局域网下。如果是同一个 WIFI,只要路由器没有开 AP 隔离就可以。如果不行,就打开电脑的移动热点,然后把手机连接到这个热点。我这里选择第二种。

  2. 查看电脑 IP。这个很简单啊,直接打开 cmd 执行 ipconfig,然后找到 ipv4 地址(通常有多个网络适配器,要选哪个要具体情况具体分析,实在不行就一个个试嘛)。

    ipconfig
  3. 手机连接电脑的热点。 代理一项选择手动服务器主机名一项输入上面的 ip 地址,端口号输入8888。连接成功之后,不出意外的话,手机端的所有请求都能被 Fiddler 捕获到了。

手机端代理设置
  1. 清空 Fiddler 面板的所有连接,打开某中文应用,此时会有一大串连接,具体是哪个就要有点耐心找找了。我这里以广场内容示例,抓到的网络响应如图所示。


    广场内容网络请求

请求参数分析

看起来比较奇怪的东西就是:QDSignQDInfoAegisSign这三个参数。怎么看出来的呢,如果是一大串不明意义的字符,有很大概率是我们需要逆向的。但注意是“很大概率”,不一定全部都是。有的字符串是写死的,只要是同一设备,这个值就一样,那我们就没必要研究它到底是怎么得来的,直接“拿来主义”拿来用就好了,比如上面的 appIdqimei 等。这个参数每次都不一样,就必须打开 apk 看看是怎么得到的。

jadx 逆向 apk

这个 apk 内含有 4 个 dex 文件,如果直接用 jadx 反编译,很大概率会卡死,所以先试试把第一个 classes.dex 拖进去。文本搜索 "QDSign",运气很好,只有一个结果,直接点进去。


jadx直接文本搜索
相关代码

可以看到,我们需要的全部三个参数都在这里定义。接下来就分别就这三个参数详细分析。

QDInfo

jadx 支持直接跳转,即按住 Ctrl + 鼠标左键跳转。如果像上图一样出现了类的全名或跳转不进,说明这个函数在另一个 dex 里。一步步跟进去看看。


c.t().A()

可以看到随后又调用了另一个函数,其中一个参数是 B()。先不管这个 B(),跟进去看看。

image.png

看到这里,有经验的小伙伴应该能一眼看出来了,没错,就是一个非常典型的 DES 加密。jadx 反编译效果很好,直接拷贝到我们的项目中就可以了。接下来再看上面那个 B()

B()

似乎是将一些变量拼接成字符串。我们直接文本搜索 this.l =,发现这玩意似乎和 uuid 有关系。

this.l =

到此差不多就可以明白了,这个类应该是记录 uuid,imei 这类信息的。既然如此,有必要弄明白这些变量到底是什么意思吗?答案是否定的,还是那句话,我们不需要知道它是怎么来的,只要知道怎么用就行。这里我们直接上万能的 Xposed 大法,hook 掉这个函数,看看到底返回了什么信息。

package com.ablist97.xqdreader.core;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class MainHook implements IXposedHookLoadPackage {
    private static final String TAG = "MainHook";

    private static final String PACKAGE_NAME = "com.qidian.QDReader";

    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam param) throws Throwable {
        XposedBridge.log("handleLoadPackage: " + param.packageName);

        if (! PACKAGE_NAME.equals(param.packageName)) {
            return;
        }

        XposedHelpers.findAndHookMethod("com.qidian.QDReader.core.config.c",
                param.classLoader,
                "B",
                new XC_MethodHook() {
                    @Override
                    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                        XposedBridge.log("afterHookedMethod: " + param.getResult());
                    }
                });
    }
}

对于 Xposed 不太熟悉的小伙伴可以百度一下,相关内容非常多。我这里使用的是 VirtualXposed,更新模块后不用重启,非常方便。看一下打印出的 log:


afterHookedMethod

结合这份 log,应该可以猜得出上面那一串是什么东西,无非就是版本名,版本号,imei,uuid,屏幕分辨率一类的。

QDSign

老规矩,直接跳进去看看。

image.png

这里的逻辑比较清楚,根据网络请求的不同跳转到了不同的逻辑,我们这里进 GET 分支看看。

image.png

大致逻辑就是将传进来的 url 分割得到请求参数,然后再进一步分割,塞进 TreeMap 里排序。值得注意的是图中的第 85 行,调用 c.signParams 函数,返回了一个字节数组,看起来像是加密操作。我们跟进去看看。

signParams()

果不其然,是一个 native 函数。重要的逻辑放到 native 层,这里要表扬一下。从上面的 static 块可以看到它加载了 c-lib 这个库,我们直接用 IDA 打开。

libc-lib.so

这里又有一个小技巧,对于 JNI 函数,我们一般使用 JNIEXPORT 将符号导出,此时它肯定在 Exports 窗口。javah 自动生成的函数名是 Java_类名_函数名_参数 这种格式的,比如 Java_com_ablist97_xqdreader_core_main_hook,非常好辨认。如果没有,说明可能使用了动态绑定,我们去 JNI_OnLoad 函数里看看,直接 F5 转伪代码。

JNI_OnLoad

第一眼看上去非常复杂,这都什么跟什么呀。实际上是 IDA 参数自动推导的问题。鼠标移动到 a1 处,我们都知道实际上是个 JavaVM* 类型,按下 Y 键修正数据类型。

image.png

跳转到 off_5004 查看。

image.png

直接双击,进入 j_s()函数,F5转伪代码。此时代码可读性仍然比较差,就需要按照 java 层函数的声明类型,手动修正参数。对于任意一个 JNI 函数,它的前两个参数都是 JNIEnv *envjobject _this (或 jclass _class)。

image.png

非常明显的一个签名校验。如果转为 java 代码大概是这样的:

PackageManager pm = context.getPackageManager();
PackageInfo info = pm.getPackageInfo(context.getPackageName(), 
        PackageManager.GET_SIGNATURES);
String signature = info.signatures[0].toCharsString();

下面的代码主要是根据上面的签名进行 DES 加密,就不另外分析了。所以问题在于如何让它返回正确的签名。我们可以在 Application 初始化时用动态代理替换掉 IPackage。

public static class IPackageManagerSpy implements InvocationHandler {
        private static final String TAG = "IPackageManagerSpy";
        
        public static void install(Application app) {
            try {
                // 获取 ActivityThread 里的 sPackageManager 原始对象
                Field sPackageManager = ActivityThread.class
                        .getDeclaredField("sPackageManager");
                sPackageManager.setAccessible(true);

                // 创建代

理对象
                Object chief = sPackageManager.get(null);
                Class<?> cls = chief.getClass();
                Object spy = Proxy.newProxyInstance(
                        cls.getClassLoader(),
                        cls.getInterfaces(),
                        new IPackageManagerSpy(chief)
                );

                // 替换掉 ActivityThread 里的 sPackageManager
                sPackageManager.set(null, spy);

                // 替换掉 ApplicationPackageManager 里的 mPm
                PackageManager pm = app.getPackageManager();
                Field mPm = ApplicationPackageManager.class.getDeclaredField("mPM");
                mPm.setAccessible(true);
                mPm.set(pm, spy);
            } catch (Throwable t) {
                Log.e(TAG, "install: failed", t);
            }
        }

        private static final String MY_PACKAGE_NAME = "com.ablist97.qdreader";
        private static final String FAKE_SIGNATURE = "308202253082018ea00302010202044e239460300d06092a864886f70d0101050500305731173015060355040a0c0ec386c3b0c2b5c3a3c396c390c38e311d301b060355040b0c14c386c3b0c2b5c3a3c396c390c38ec384c38dc3b8311d301b06035504030c14c386c3b0c2b5c3a3c396c390c38ec384c38dc3b8301e170d3131303731383032303331325a170d3431303731303032303331325a305731173015060355040a0c0ec386c3b0c2b5c3a3c396c390c38e311d301b060355040b0c14c386c3b0c2b5c3a3c396c390c38ec384c38dc3b8311d301b06035504030c14c386c3b0c2b5c3a3c396c390c38ec384c38dc3b830819f300d06092a864886f70d010101050003818d0030818902818100a3d47f8bfd8d54de1dfbc40a9caa88a43845e287e8f40da2056be126b17233669806bfa60799b3d1364e79a78f355fd4f72278650b377e5acc317ff4b2b3821351bcc735543dab0796c716f769c3a28fedc3bca7780e5fff6c87779f3f3cdec6e888b4d21de27df9e7c21fc8a8d9164bfafac6df7d843e59b88ec740fc52a3c50203010001300d06092a864886f70d0101050500038181001f7946581b8812961a383b2d860b89c3f79002d46feb96f2a505bdae57097a070f3533c42fc3e329846886281a2fbd5c87685f59ab6dd71cc98af24256d2fbf980ded749e2c35eb0151ffde993193eace0b4681be4bcee5f663dd71dd06ab64958e02a60d6a69f21290cb496dd8784a4c31ebadb1b3cc5cb0feebdaa2f686ee2";
        
        
        private IPackageManagerSpy(Object pm) {
            mRemote = pm;
        }

        private final Object mRemote;

        private PackageInfo getPackageInfo(Method method, Object[] args) throws Throwable {
            PackageInfo info = (PackageInfo) method.invoke(mRemote, args);

            if (!MY_PACKAGE_NAME.equals(args[0]) || info == null) {
                Log.i(TAG, "getPackageInfo: ignore param: " + args[0]);
                return info;
            }

            if (info.signatures == null || info.signatures.length == 0) {
                Log.w(TAG, "getPackageInfo: signature is null");
                return info;
            }
            info.signatures[0] = new Signature(FAKE_SIGNATURE);
            return info;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String name = method.getName();
            Log.i(TAG, "invoke: " + name);

            if ("getPackageInfo".equals(name)) {
                return getPackageInfo(method, args);
            }
            // 不要忘记调用原函数 !!!
            return method.invoke(mRemote, args);
        }
    }

下一个问题又来了,signParams() 函数有这么多参数,怎么确定每个参数呢,答案很简单:Xposed 大法,直接打印出来就可以了。

AegisSign

这个参数的分析方法和前面的 QDSign 相同,就不多说了。有点奇怪的是,我计算出的 AegisSign 和官方的不一样,不知道为什么。本来想用 IDA 动态调试,但服务器似乎并没有校验这个参数,也就作罢。

总结

本文从抓包开始,使用 jadx 和 IDA 等工具,实现了简单的静态分析。我一再强调的一点是:逆向不同于普通 app 开发,不需要关心每个细节,只要服务器能正常下发数据,就一切 ok。比如上面的 libc-lib.socom.qidian.QDReader.core.e.c 里的加密函数,不需要重新写,重新拷贝过来,能用就可以。

上一篇 下一篇

猜你喜欢

热点阅读