5.2 快手极速版

本文主要讲解如何使用 Lsposed 搭建一个 RPC 服务,并且 Hook 快手极速版提供加密参数,本文不做加密定位分析,仅讲解 Lsposed 插件开发以及服务搭建。

在阅读本文前,你应该已经了解的地方有:

  1. 本文提供生成 sigsig3tokensig 的服务

  2. 本文使用的 app 为快手极速版,版本号为:10.6.30.3979 ,包名为:com.kuaishou.nebula

  3. 本文调用的实例路径为:com.kwai.framework.network.RetrofitInitModule$a$a

  4. 方法对应规则如下:

    • sig3a

    • sigd

    • tokensigc

  5. 确保你的设备已经安装好 lsposed 框架

初始化

首先新建一个名为 KsTools 的项目

进行 Lsposed 相关的初始化:

模块声明

app/src/main/AndroidManifest.xml 输入以下代码

<!--        声明这是一个 Xposed 模块-->
<meta-data
        android:name="xposedmodule"
        android:value="true"/>

<!--        这个是 Xposed 模块的显示信息-->
<meta-data
        android:name="xposeddescription"
        android:value="快手极速版 RPC 模块"/>

<!--        这个是 Xposed 小版本-->
<meta-data
        android:name="xposedminversion"
        android:value="53"/>

权限声明

因为我们到时候是需要搭建 RPC 服务的,所以需要声明一下权限,在 app/src/main/AndroidManifest.xml 输入以下代码

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

依赖声明

自己在网上下载好 jar 包后移入 app/libs 下面

app/build.gradle 下进行依赖声明,因为我们要搭建 RPC, 所以我们还需要依赖一个 Web 框架,我这边选用的是 nanohttpd

    implementation 'org.nanohttpd:nanohttpd:2.2.0'
//    compileOnly files('libs/XposedBridgeAPI-89.jar')
    compileOnly files('libs/LSPosed-api-1.0-SNAPSHOT.jar')

然后点击 IDEA 上的 Sync Now 进行同步

app/src/main/assets 下新建一个文件 xposed_init (如果目录不存在,你需要自己创建一下),然后在里面写上你的 Hook 类的地址, 如:com.example.kstools.Hooker

代码编写

经过上述初始化之后,我们就可以开始编写 Lsposed 插件了

首先我们需要自己实现 IXposedHookLoadPackage 的接口,然后重载 handleLoadPackage 这个方法,该方法接受一个参数 loadPackageParam,参数类型为:XC_LoadPackage.LoadPackageParam

当我们接收到这个参数后,可以使用 loadPackageParam.packageName 获取当前应用的包名,所以我们需要判断,当应用包名为快手极速版的包名时,再进行我们的 hook 逻辑!模板如下:

package com.example.kstools; 

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

import java.util.Objects;

public class Hooker implements IXposedHookLoadPackage {


    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) {

        //  先判断包名,当包名为快手极速版的包名的时候,编写 Hook 逻辑
        if (Objects.equals(loadPackageParam.packageName, "com.kuaishou.nebula")) {
            // 在这里写你的 Hook 逻辑

        }
    }


}
}

然后因为我们需要启动一个 web 服务,所以可以按照以下步骤来搭建一下:

  1. 自己写一个类继承 NanoHTTPD

  2. 编写构造方法,指定端口等信息

  3. 重写 serve 方法,用于路由分发

  4. 使用 start 方法启动服务器

然后因为我们需要在用于访问 api 的时候调用 Xposed 去主动调用快手极速版内的方法,所以我这边封装了一个 run_server 方法,该方法接收一个 ClassLoader, 便于后面去加载快手极速版的方法来调用,当我检测到当前应用为快手极速版的时候,我就调用 run_server 方法启动我的 web 服务,并且传入当前的 ClassLoader,当前 ClassLoader 可以通过 loadPackageParam.classLoader 的方式拿到,代码如下:

package com.example.kstools;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
import fi.iki.elonen.NanoHTTPD;

import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;

public class Hooker implements IXposedHookLoadPackage {

    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) {

        //  先判断包名,当包名为快手极速版的包名的时候,编写 Hook 逻辑
        if (Objects.equals(loadPackageParam.packageName, "com.kuaishou.nebula")) {
            // 在这里写你的 Hook 逻辑
            ClassLoader classLoader = loadPackageParam.classLoader;
            run_server(classLoader);

        }
    }

    private void run_server(ClassLoader classLoader) throws IOException {

        class myHttpServer extends NanoHTTPD {

            public myHttpServer() throws IOException {
                // 端口是8899,也就是说要通过http://127.0.0.1:8899来访当问
                // 电脑使用adb forward tcp:8899 tcp:8899 转发端口
                super(8899);
                start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
                XposedBridge.log("--- Server start success---");
            }

            @Override
            public Response serve(IHTTPSession session) {
                Map<String, String> params = session.getParms();

                try {
                    // 这里用于 根据请求的参数来进行路由分发
                    XposedBridge.log("params: " + params.toString());
                    String action = params.get("action");
                    String ret = "";

                    return newFixedLengthResponse(ret);
                } catch (Exception e) {
                    return newFixedLengthResponse("调用失败: " + Arrays.toString(e.getStackTrace()));
                }
            }
        }
        new myHttpServer();

    }
}

上面的 serve 方法我进行了重载,每次用户请求的时候,我就判断用户请求的 url 参数里面的 action 的值,然后准备通过 action 来进行路由分发,返回结果。理解完这段逻辑之后,我们就可以将代码完善一下,如下:

package com.example.kstools;

import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
import fi.iki.elonen.NanoHTTPD;
import org.json.JSONException;

import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;

public class Hooker implements IXposedHookLoadPackage {
    Object ins = null;

    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws IOException {

        //  先判断包名
        if (Objects.equals(loadPackageParam.packageName, "com.kuaishou.nebula")) {
            XposedBridge.log("Hook com.kuaishou.nebula");
            ClassLoader classLoader = loadPackageParam.classLoader;
            run_server(classLoader);

        }
    }

    private void run_server(ClassLoader classLoader) throws IOException {

        class myHttpServer extends NanoHTTPD {

            public myHttpServer() throws IOException {
                // 端口是8899,也就是说要通过http://127.0.0.1:8899来访当问
                // 电脑使用adb forward tcp:8899 tcp:8899 转发端口
                super(8899);
                start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
                XposedBridge.log("--- Server start success---");
            }

            @Override
            public Response serve(IHTTPSession session) {
                Map<String, String> params = session.getParms();

                try {
                    XposedBridge.log("params: " + params.toString());
                    String action = params.get("action");
                    String ret = "";
                    if (action == null) {
                        ret = encrypt(params);
                    } else {
                        switch (action) {
                            case "sig3": {
                                ret = getSig3(params);
                                break;
                            }
                            case "sig": {
                                ret = getSig(params);
                                break;
                            }

                            case "tokensig": {
                                ret = getTokensig(params);
                                break;
                            }

                        }

                    }

                    return newFixedLengthResponse(ret);
                } catch (Exception e) {
                    return newFixedLengthResponse("调用失败: " + Arrays.toString(e.getStackTrace()));
                }
            }

            public String encrypt(Map<String, String> params) throws ClassNotFoundException, IllegalAccessException, InstantiationException, JSONException {
                return "";
            }

            public String getSig3(Map<String, String> params) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
                return "";
            }

            public String getSig(Map<String, String> params) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
                return "";
            }

            public String getTokensig(Map<String, String> params) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
                return "";
            }


        }
        new myHttpServer();

    }
}

继续看到我们的 serve 方法,用户请求后我做了以下几件事情:

  1. 通过 session 获取用户请求的 url 参数,我们拿到的是一个 hashMap

  2. 获取用户请求的 action

  3. 如果 action 为空,我们直接调用 encrypt 方法,然后将返回值复制给 ret

  4. 如果 action 有值,则有以下几种情况:

    • actionsig3:表示用户只需要拿到 sig3,我们直接调用 getSig3

    • actionsig:表示用户只需要拿到 sig,我们直接调用 getSig

    • actiontokensig:表示用户只需要拿到 tokensig,我们直接调用 getTokensig

看到这里,你应该清楚,我们的路由分发已经完成,且我们预留了四个路由方法,他们分别为:

  • getSig3: 获取 sig3

  • getSig: 获取 sig

  • getTokensig:获取 tokensig

  • encrypt: 一步到位,直接生成 sig3

读到这里,不了解快手的可能会有些疑惑,为什么会有一个 encrypt 方法,其实这是由快手的加密逻辑决定的,简单介绍一下加密逻辑:

  1. 我们会传入三个参数,他们分别是 str、salt、path

  2. 使用 str 生成 sig

  3. 使用 sigsalt 生成 tokensig

  4. 使用 sigpath 生成 sig3

因此我们这边写一个方法来一步到位,避免用户多次调用接口!

好的,话说回来我们已经预留了四个方法了,那么目前我们就需要去实现这几个方法了

sig

从上文中我们已经得知,sig 方法对应的代码为:com.kwai.framework.network.RetrofitInitModule$a$a.d,我们先看看 jadx 里面的代码:

从上述代码我们可以看到,这个方法是一个实例方法,所以我们调用这个方法的时候我们需要一个实例对象,而这个实例就是 com.kwai.framework.network.RetrofitInitModule$a$a

关于如何获取这个实例,我们一般有以下几种思路:

  1. 获取类之后自己实例化,但是需要分析出实例所需要的参数等等

  2. 直接去 hook 这个类的构造方法,在这个类实例化的时候拿到实例对象,赋值到一个全局对象,供其他人调用

这边我们选取的是第一种方法,因为这个类构造起来很简单

首先我们看到 com.kwai.framework.network.RetrofitInitModule$a$a 的构造方法:

发现他是不需要任何参数的,但是因为他是匿名类,所以其实应该接受了他外层按个类的实例,也就是 com.kwai.framework.network.RetrofitInitModule$a 的实例,所以我们接着往上看 com.kwai.framework.network.RetrofitInitModule$a 的构造方法:

![image-20230704141446218](../../../Library/Application Support/typora-user-images/image-20230704141446218.png)

发现他也是不需要任何参数的,但也是一个匿名类,所以我们需要一个 com.kwai.framework.network.RetrofitInitModule 的实例,我们直接对 RetrofitInitModule 查找用例,看看在快手极速版里面是怎么实例化的

![image-20230704141919900](../../../Library/Application Support/typora-user-images/image-20230704141919900.png)

![image-20230704141938435](../../../Library/Application Support/typora-user-images/image-20230704141938435.png)

发现就是直接实例化,并没有传入任何参数(当然你也可以点到父类里面去看看他的构造方法),根据上面的逻辑我们应该知道了怎么去构造我们需要的实例对象了,步骤如下:

  1. 加载 class1com.kwai.framework.network.RetrofitInitModule$a$a

  2. 加载 class2com.kwai.framework.network.RetrofitInitModule$a

  3. 加载 class3com.kwai.framework.network.RetrofitInitModule

  4. 通过 class3 生成实例 ins1

  5. 传入 ins1,通过 class2生成实例 ins2

  6. 传入 ins2, 通过 class3 生成实例 ins

  7. ins 就是我们需要的实例了

代码如下:

public Object getIns() throws ClassNotFoundException, IllegalAccessException, InstantiationException {

    Class<?> claszz = classLoader.loadClass("com.kwai.framework.network.RetrofitInitModule$a");
    Class<?> claszz2 = classLoader.loadClass("com.kwai.framework.network.RetrofitInitModule$a$a");
    Class<?> RetrofitInitModuleClass = classLoader.loadClass("com.kwai.framework.network.RetrofitInitModule");
    Object RetrofitInitModuleIns = RetrofitInitModuleClass.newInstance();
    Object ins2 = XposedHelpers.newInstance(claszz, RetrofitInitModuleIns);
    ins = XposedHelpers.newInstance(claszz2, ins2);

    return ins;
}

有了实例之后我们继续来实现我们的 sig 方法,该方法接收一个 str, 代码如下:

public String getSig(Map<String, String> params) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
    String str = params.get("str");
    ins = getIns();
    Object ret = XposedHelpers.callMethod(ins, "d", str);
    XposedBridge.log("调用 sig 方法, str: " + str);
    XposedBridge.log("调用 sig 方法, 返回值为: " + ret.toString());
    return ret.toString();
}

tokensig

从上文中我们已经得知,tokensig 方法对应的代码为:com.kwai.framework.network.RetrofitInitModule$a$a.c,我们先看看 jadx 里面的代码:

该方法接收两个字符串,代码如下:

public String getTokensig(Map<String, String> params) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
    String sig = params.get("sig");
    String salt = params.get("salt");
    ins = getIns();
    Object ret = XposedHelpers.callMethod(ins, "c", sig, salt);
    XposedBridge.log("调用 tokensig 方法, sig: " + sig);
    XposedBridge.log("调用 tokensig 方法, salt: " + salt);
    XposedBridge.log("调用 tokensig 方法, 返回值为: " + ret.toString());
    return ret.toString();
}

sig3

从上文中我们已经得知,sig3 方法对应的代码为:com.kwai.framework.network.RetrofitInitModule$a$a.b,我们先看看 jadx 里面的代码:

该方法也接收两个字符串,代码如下:

public String getSig3(Map<String, String> params) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
    String sig = params.get("sig");
    String path = params.get("path");
    ins = getIns();
    Object ret = XposedHelpers.callMethod(ins, "b", sig, path);
    XposedBridge.log("调用 sig3 方法, sig: " + sig);
    XposedBridge.log("调用 sig3 方法, path: " + path);
    XposedBridge.log("调用 sig3 方法, 返回值为: " + ret.toString());
    return ret.toString();
}

encrypt

在根据加密的逻辑,我们可以完善一下这个 encrypt 方法,代码如下:

public String encrypt(Map<String, String> params) throws ClassNotFoundException, IllegalAccessException, InstantiationException, JSONException {
    String str = params.get("str");
    String salt = params.get("salt");
    String path = params.get("path");

    HashMap<String, String> hashMap1 = new HashMap<>();
    hashMap1.put("str", str);
    String sig = getSig(hashMap1);


    HashMap<String, String> hashMap2 = new HashMap<>();
    hashMap2.put("sig", sig);
    hashMap2.put("salt", salt);
    String tokenSig = getTokensig(hashMap2);

    HashMap<String, String> hashMap3 = new HashMap<>();
    hashMap3.put("sig", sig);
    hashMap3.put("path", path);

    String sig3 = getSig3(hashMap3);

    JSONObject ret = new JSONObject();
    ret.put("sig", sig);
    ret.put("tokensig", tokenSig);
    ret.put("sig3", sig3);
    return ret.toString();
}

服务优化

服务可能会被多次启动,所以我们可以自己 try 一下

try {
    run_server(classLoader);
} catch (Exception e) {
    XposedBridge.log("启动服务失败: " + e.toString());
}

为了避免实例被多次创建,可以使用外部变量进行缓存

public Object getIns() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
    if (ins == null) {
        Class<?> claszz = classLoader.loadClass("com.kwai.framework.network.RetrofitInitModule$a");
        Class<?> claszz2 = classLoader.loadClass("com.kwai.framework.network.RetrofitInitModule$a$a");
        Class<?> RetrofitInitModuleClass = classLoader.loadClass("com.kwai.framework.network.RetrofitInitModule");
        Object RetrofitInitModuleIns = RetrofitInitModuleClass.newInstance();
        Object ins2 = XposedHelpers.newInstance(claszz, RetrofitInitModuleIns);
        ins = XposedHelpers.newInstance(claszz2, ins2);
    }
    return ins;
}

打开 Lsposed 插件的时候同步打开快手极速版,简化流程,我们可以直接编写 MainActivity 方法,在 onCreate 的时候直接打开

package com.example.kstools;

import android.content.ComponentName;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        openApp();
    }

    public void openApp() {
        Intent intent = new Intent();
        intent.setComponent(new ComponentName("com.kuaishou.nebula", "com.yxcorp.gifshow.HomeActivity"));
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
        startActivity(intent);
    }
}

编译调试

  1. 手机与电脑连接同一个 Wifi

  2. 连接手机后, 开启 usb 调试

  3. 运行 MainActivity , 会自动安装 app到手机上

  4. Lsposed 里面启用模块

  5. 关闭我们的模块重新打开

  6. 使用 PostMan 进行调试

最后更新于

这有帮助吗?