本文主要讲解如何使用 Lsposed
搭建一个 RPC
服务,并且 Hook
快手极速版提供加密参数,本文不做加密定位分析,仅讲解 Lsposed
插件开发以及服务搭建。
在阅读本文前,你应该已经了解的地方有:
本文提供生成 sig
, sig3
, tokensig
的服务
本文使用的 app
为快手极速版,版本号为:10.6.30.3979
,包名为:com.kuaishou.nebula
本文调用的实例路径为:com.kwai.framework.network.RetrofitInitModule$a$a
初始化
首先新建一个名为 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
服务,所以可以按照以下步骤来搭建一下:
然后因为我们需要在用于访问 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
方法,用户请求后我做了以下几件事情:
通过 session
获取用户请求的 url
参数,我们拿到的是一个 hashMap
如果 action
为空,我们直接调用 encrypt
方法,然后将返回值复制给 ret
如果 action
有值,则有以下几种情况:
action
为 sig3
:表示用户只需要拿到 sig3
,我们直接调用 getSig3
action
为 sig
:表示用户只需要拿到 sig
,我们直接调用 getSig
action
为 tokensig
:表示用户只需要拿到 tokensig
,我们直接调用 getTokensig
看到这里,你应该清楚,我们的路由分发已经完成,且我们预留了四个路由方法,他们分别为:
读到这里,不了解快手的可能会有些疑惑,为什么会有一个 encrypt
方法,其实这是由快手的加密逻辑决定的,简单介绍一下加密逻辑:
我们会传入三个参数,他们分别是 str、salt、path
使用 sig
和 salt
生成 tokensig
因此我们这边写一个方法来一步到位,避免用户多次调用接口!
好的,话说回来我们已经预留了四个方法了,那么目前我们就需要去实现这几个方法了
sig
从上文中我们已经得知,sig
方法对应的代码为:com.kwai.framework.network.RetrofitInitModule$a$a.d
,我们先看看 jadx
里面的代码:
从上述代码我们可以看到,这个方法是一个实例方法,所以我们调用这个方法的时候我们需要一个实例对象,而这个实例就是 com.kwai.framework.network.RetrofitInitModule$a$a
关于如何获取这个实例,我们一般有以下几种思路:
获取类之后自己实例化,但是需要分析出实例所需要的参数等等
直接去 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)
发现就是直接实例化,并没有传入任何参数(当然你也可以点到父类里面去看看他的构造方法),根据上面的逻辑我们应该知道了怎么去构造我们需要的实例对象了,步骤如下:
加载 class1
:com.kwai.framework.network.RetrofitInitModule$a$a
加载 class2
:com.kwai.framework.network.RetrofitInitModule$a
加载 class3
:com.kwai.framework.network.RetrofitInitModule
传入 ins1
,通过 class2
生成实例 ins2
传入 ins2
, 通过 class3
生成实例 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);
}
}
编译调试
运行 MainActivity
, 会自动安装 app
到手机上