0. 前言
这个需求是要求持续监听土豆app的群聊消息。
开始的话已经有frida的相关python脚本了,可以hook到消息和一些数据,但是不全,需要再连接到土豆app自己的sqlite3数据库去获取跟id对应的用户名和群组名。
做完后发现稳定性实在是堪忧,adb链接很容易就断开了,而且还要配台电脑在旁边跑,很麻烦。于是聪明的我想到了能不能用lsposed模块来完成这个任务。
(当然,frida我也没摸过多少次,稳定性这方面雀食不太懂)
1. Android Studio
既然是要编写一个APK,当然是要用对应的编辑器,这里用的是Android Studio,下载完就感觉像回到家了一样,还得是JetBrain的编辑器对味。
🦑是第一次写apk,啥也不会,网上看了一下教程,新建了一个 Empty Activity ,给了一个项目名potatohook,Minimum SDK选择了我的手机的google版本邻近的API29(我的手机是安卓10,为了支持安卓版本更低的机型也可以开到API27),Build configuration language选择Kotlin后,来到 `app/java/com.example.potatohook` 目录下创建了一个MainHook.java 文件,在这里编写lsposed的核心代码。
2. Kotlin
当然,作为第一次写的我,完全不知道这些长得奇奇怪怪的文件是什么玩意。他有就算了,还有两个!名字还一样!这谁搞得清啊。

百度一下,再gpt一下,可得:
app/build.gradle.kts
Gradle 构建系统的配置文件,用于定义项目的依赖、任务和插件等。
// 声明使用的插件
plugins {
// 添加 Android 应用程序插件,这是开发 Android 应用必需的
id("com.android.application")
}
// Android 项目的具体配置
android {
// 应用的包名,用于唯一标识应用
namespace = "com.example.potatohook"
// 编译用的 Android SDK 版本,这里使用 API 34 (Android 14)
compileSdk = 34
// 默认配置块,包含应用的基本信息
defaultConfig {
// 应用的唯一标识符
applicationId = "com.example.potatohook"
// 支持的最低 Android 版本 (Android 7.0)
minSdk = 24
// 目标 Android 版本 (Android 14)
targetSdk = 34
// 应用的版本号,用于区分不同版本
versionCode = 1
// 应用的版本名称,用户可见的版本号
versionName = "1.0"
}
// 构建类型配置
buildTypes {
// release 版本的特定配置
release {
// 是否开启代码混淆,这里设置为不开启
isMinifyEnabled = false
// 指定混淆规则文件
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
// Java 编译选项配置
compileOptions {
// 源代码的 Java 版本
sourceCompatibility = JavaVersion.VERSION_1_8
// 目标 Java 字节码版本
targetCompatibility = JavaVersion.VERSION_1_8
}
}
// 项目依赖配置
dependencies {
// 添加 Xposed 框架的 API 依赖
// compileOnly 表示仅在编译时使用,不会打包到最终的 APK 中
// 82 是 Xposed API 的版本号
compileOnly("de.robv.android.xposed:api:82")
}
settings.gradle.kts
配置构建仓库,依赖管理,项目结构定义
// 插件管理配置块,用于配置 Gradle 插件的仓库源
pluginManagement {
repositories {
// Google 的 Maven 仓库,包含 Android 相关的库和插件
google()
// Maven 中央仓库,包含大多数开源 Java 库
mavenCentral()
// Gradle 插件门户,默认的 Gradle 插件仓库
gradlePluginPortal()
// Xposed 框架的 Maven 仓库,用于获取 Xposed API
maven(url = "https://api.xposed.info/")
// JitPack 仓库,用于获取 GitHub 上的开源项目
maven(url = "https://jitpack.io")
}
}
// 依赖解析管理配置块,用于配置项目依赖的仓库源
dependencyResolutionManagement {
// 设置仓库模式为 FAIL_ON_PROJECT_REPOS
// 这意味着如果在个别模块中定义了额外的仓库,构建会失败
// 这样可以确保所有依赖都从这里定义的仓库中获取
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
// 配置项目依赖的仓库源
repositories {
// 同上,配置各种 Maven 仓库
google()
mavenCentral()
maven(url = "https://api.xposed.info/")
maven(url = "https://jitpack.io")
}
}
// 设置项目根目录的名称
rootProject.name = "PotatoHook"
// 包含 app 模块到项目中
// 这表示项目包含一个名为 "app" 的子模块
include(":app")
比较重要的就这两个配置文件
3. 主要代码
最主要的当然就是我们hook的代码了,我们先重新看一下当前的目标和问题:
- 目标:
- 监听群组消息
- 数据要有作者id和名称,群组id和名称
- 上传到数据库
- 问题:
- 只有frida的hook脚本,要根据frida脚本编写xposed hook脚本
- hook到的数据只有id,没有名称,名称要到数据库自己搜
- 注:部分源码及核心功能已删改,如有需要可自行编写
app\src\main\java\com\example\potatohook\MainHook.java
package com.example.potatohook;
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;
import org.json.JSONObject;
import org.json.JSONArray;
import java.io.File;
import java.net.URL;
import java.io.OutputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.net.HttpURLConnection;
import java.util.concurrent.TimeUnit;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentHashMap;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
public class MainHook implements IXposedHookLoadPackage {
private static final String TARGET_PACKAGE = "org.potato.messenger.web"; // 目标包名
private static final String API_URL = "https://api.toyourdatabase.com/potato"; // 上传数据到自己数据库的接口
private static final String DB_PATH = "/data/data/org.potato.messenger.web/files/cache4.db"; // 土豆app的数据库路径
private SQLiteDatabase db = null; // 初始化数据库对象
private static final long CACHE_EXPIRE_TIME = TimeUnit.DAYS.toMillis(1); // 1天的毫秒数,缓存过期时间
// 缓存项类,包含数据和时间戳
private static class CacheItem {
final JSONObject data;
final long timestamp;
CacheItem(JSONObject data) {
this.data = data;
this.timestamp = System.currentTimeMillis();
}
boolean isExpired() {
return System.currentTimeMillis() - timestamp > CACHE_EXPIRE_TIME;
}
}
// 使用CacheItem包装缓存数据
private final ConcurrentHashMap<String, CacheItem> chatCache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, CacheItem> userCache = new ConcurrentHashMap<>();
private void initDatabase() {
// 与土豆数据库进行连接
try {
File dbFile = new File(DB_PATH);
db = SQLiteDatabase.openDatabase(DB_PATH, null, SQLiteDatabase.OPEN_READONLY);
XposedBridge.log("PotatoHook: Database connected successfully");
}
private void warmUpCache() {
// 由于量可能比较大,在开始前先将用户和群组数据全部缓存一遍可有效减少对数据库的查询次数
try {
if (db == null) {
XposedBridge.log("PotatoHook: Database not connected, cannot warm up cache");
return;
}
// 预热群组缓存
...
XposedBridge.log("PotatoHook: Warmed up " + chatCache.size() + " chat entries");
// 预热用户缓存
...
XposedBridge.log("PotatoHook: Warmed up " + userCache.size() + " user entries");
} catch (Exception e) {
XposedBridge.log("PotatoHook: Cache warm-up failed: " + e.getMessage());
e.printStackTrace();
}
}
private JSONObject getChatInfo(long chatId) {
// 根据群组id查询群组名称
String chatIdStr = String.valueOf(chatId);
// 先查缓存
CacheItem cachedItem = chatCache.get(chatIdStr);
if (cachedItem != null && !cachedItem.isExpired()) {
XposedBridge.log("PotatoHook: Chat cache hit for " + chatIdStr);
return cachedItem.data;
}
// 缓存未命中或已过期,查询数据库
try {
...
// 更新缓存
chatCache.put(chatIdStr, new CacheItem(chatInfo));
return chatInfo;
}
} catch (Exception e) {
XposedBridge.log("PotatoHook: Error getting chat info: " + e.getMessage());
}
// 出错时返回默认值
try {
JSONObject defaultInfo = new JSONObject();
defaultInfo.put("id", Integer.valueOf(chatIdStr)); // 直接使用 long 类型
defaultInfo.put("title", "未知群组");
return defaultInfo;
} catch (Exception e) {
XposedBridge.log("PotatoHook: Error creating default chat info: " + e.getMessage());
return null;
}
}
private JSONObject getUserInfo(long userId) {
// 根据用户id查询用户名称
String userIdStr = String.valueOf(userId);
// 先查缓存
CacheItem cachedItem = userCache.get(userIdStr);
if (cachedItem != null && !cachedItem.isExpired()) {
XposedBridge.log("PotatoHook: User cache hit for " + userIdStr);
return cachedItem.data;
}
// 缓存未命中或已过期,查询数据库
try {
...
// 更新缓存
userCache.put(userIdStr, new CacheItem(userInfo));
return userInfo;
}
} catch (Exception e) {
XposedBridge.log("PotatoHook: Error getting user info: " + e.getMessage());
}
// 出错时返回默认值
try {
JSONObject defaultInfo = new JSONObject();
defaultInfo.put("id", Integer.valueOf(userIdStr)); // 直接使用 long 类型
defaultInfo.put("username", "未知用户");
defaultInfo.put("last_name", "");
return defaultInfo;
} catch (Exception e) {
XposedBridge.log("PotatoHook: Error creating default user info: " + e.getMessage());
return null;
}
}
// 定期清理过期缓存
private void startCacheCleanup() {
// 单独开一个线程
// 防止内存溢出,每隔一小时清理一次缓存,如果有数据一天都没被调用,则会被移出缓存
new Thread(() -> {
while (true) {
try {
Thread.sleep(TimeUnit.HOURS.toMillis(1)); // 每小时清理一次
// 清理群组缓存
chatCache.entrySet().removeIf(entry -> entry.getValue().isExpired());
// 清理用户缓存
userCache.entrySet().removeIf(entry -> entry.getValue().isExpired());
XposedBridge.log("PotatoHook: Cache cleanup completed. " +
"Remaining entries - Chats: " + chatCache.size() +
", Users: " + userCache.size());
} catch (Exception e) {
XposedBridge.log("PotatoHook: Cache cleanup error: " + e.getMessage());
}
}
}).start();
}
private void sendToServer(JSONObject messageData) { // 改为接收 JSONObject
// 也是单独开一个线程,异步去跑,不要阻塞hook逻辑
new Thread(() -> {
try {
// 由于后端是go,接口强制要求id的类型是数字,但是我怎么转都是字符串,所以在这里强行转换了一次
// 确保 chat.id 和 from.id 是整数类型
JSONObject chat = messageData.getJSONObject("chat");
JSONObject from = messageData.getJSONObject("from");
// 重新设置 ID,强制使用整数类型
if (chat.has("id")) {
int chatId = chat.getInt("id"); // 先获取为 int
chat.put("id", chatId); // 重新放入确保类型
}
if (from.has("id")) {
int fromId = from.getInt("id"); // 先获取为 int
from.put("id", fromId); // 重新放入确保类型
}
String urlStr = API_URL.replace("'", "").trim();
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// 创建包含数组的数据结构
JSONObject wrapper = new JSONObject();
wrapper.put("data", messageData);
String wrappedData = wrapper.toString();
// 发送数据
try (OutputStream os = conn.getOutputStream()) {
byte[] input = wrappedData.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
// 读取响应内容
StringBuilder response = new StringBuilder();
try (java.io.BufferedReader br = new java.io.BufferedReader(
new java.io.InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
response.append(line);
}
}
XposedBridge.log("PotatoHook: Server Response Body: " + response.toString());
} catch (Exception e) {
XposedBridge.log("PotatoHook: HTTP Error: " + e.getMessage());
XposedBridge.log("PotatoHook: Stack trace: ");
e.printStackTrace();
}
}).start();
}
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
// 主流程
if (!lpparam.packageName.equals(TARGET_PACKAGE)) {
return;
}
// 初始化数据库连接
initDatabase();
// 预热缓存
warmUpCache();
// 启动缓存清理线程
startCacheCleanup();
try {
// 获取目标类
Class<?> targetClass = XposedHelpers.findClass(
"org.potato.tgnet.b0$h1",
lpparam.classLoader
);
// hook目标类的目标方法
XposedBridge.hookAllMethods(targetClass, "h", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
// 在方法运行后进行操作
Object result = param.getResult();
if (result == null) return;
try {
JSONObject messageData = new JSONObject();
...
// 直接传递 JSONObject
sendToServer(messageData);
XposedBridge.log("PotatoHook: " + messageData.toString());
} catch (Throwable t) {
XposedBridge.log("PotatoHook: Error processing message: " + t.getMessage());
t.printStackTrace();
}
}
});
} catch (Throwable t) {
XposedBridge.log("PotatoHook error: " + t.getMessage());
t.printStackTrace();
}
}
}
assets/xposed_init
xposed的入口,告诉xposed该运行哪个文件
com.example.potatohook.MainHook
4. 同步和编译
本来在修改配置的时候就应该进行同步,不过还是放在一起说了
同步(Sync Project with Gradle Files):为项目下载依赖(相当于python里面的pip?)但是这个还是比较容易报错的,大部分应该是版本问题,可以问一下gpt如何解决
编译(Make Project):构建整个项目,并且新版本会顺带编译一个app-debug.apk
在完成修改后,需要同步并编译得到新的apk,该apk的路径是 \PotatoLSP\app\build\outputs\apk\debug\app-debug.apk
5. 安装并启用LSPosed
打开cmd或者直接用Android Studio的终端,利用adb直接安装apk
adb install app-debug.apk
打开lsposed,模块里面已经可以看到我们编写的插件了,启用模块,勾选土豆app,再打开土豆app即可

肥肠成功!

发表回复