某节拍APP逆向+XPosed模块编写
看文章无意看到了https://twogoat.github.io/2025/04/09/%E8%AE%B0%E6%9F%90%E8%8A%82%E6%8B%8D%E5%99%A8app%E7%9A%84%E4%B8%80%E6%AC%A1%E7%AE%80%E5%8D%95%E7%A0%B4%E8%A7%A3/,这个想着android还没怎么深入实战下,因此写了个这玩意儿
想知道是哪个软件可以联系我
只含java层逆向,软件中含pro字样吗

ctrl+shift+f搜一下搜到isPro,判断是否是pro会员
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function main(){ Java.perform(function () { var UserCache = Java.use("com.eumlab.prometronome.baselib.util.UserCache");
UserCache.isPro.implementation = function () { console.log("[*] Hooked isPro() called"); return true; };
console.log("[+] isPro() hook installed"); }); } setImmediate(main)
|
1
| frida -U -f "com.eumlab.android.prometronome" -l prometronome.js
|
执行后正常vip功能均可使用

但是进入我的账号后会闪退
根据 UserCache 源码:
1 2 3 4
| public boolean isPro() { User user = this.user; return (user == null || user.getVip() == null || this.user.getVip().getStatus() != 1) ? false : true; }
|
可以看出:
- 如果只 Hook
isPro(),它返回 true;
- 但
this.user、user.getVip() 依然可能是 null;
- 当别的地方访问
user.getVip().getExpireTime() 或类似字段时,就会 NullPointerException,导致闪退。
也就是说:
“判断逻辑”绕过了,但没让“数据结构”一致。

发现大部分是UserCache.getInstance().getUser()

User 里确实有 vip 字段(类型是 CustomerVip),而不是 Vip;
User 里还有一个布尔字段 isPro
有 expire、loginTime、token、refreshToken 等过期验证相关字段;
所以 “我的账号页面” 崩溃的原因,大概率就是 vip == null或者 token == null。
我们可以构造一个假的 User对象,填充它的关键字段;返回它,让所有 getUser() 调用都拿到稳定的对象,同时强制 isPro()、checkLogin()返回true。
但是要注意的是,我们这里不能直接new一个User,我们自己new一个容易出现
创建/返回的 fakeUser 跟 getUser() 期望的 User 实例并非来自同一个类定义,从而出现类加载器的错误
修改并返回真实的 User 实例是最优方案,hook 里取到原始对象(real),直接修改它的字段(补 vip/token/expire 等)
1
| rVip = real.getVip.call(real);
|
在 Frida 中,Java 方法是个“包装对象”,不能直接 real.getVip()(会被 Frida 拦截成 JS 函数),
所以要用 .call(real) 去在那个 Java 实例上执行真正的 Java 方法。
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 55 56 57
| function main(){ Java.perform(function () { console.log("[*] patch-real-user hook installing...");
var UserCache = Java.use("com.eumlab.prometronome.baselib.util.UserCache"); var CustomerVip = Java.use("com.eumlab.prometronome.baselib.data.db.entity.CustomerVip"); var System = Java.use("java.lang.System"); var JString = Java.use("java.lang.String");
UserCache.getUser.implementation = function () { var real = this.getUser.call(this); if (real !== null && real !== undefined) { try { var rVip = null; rVip = real.getVip.call(real);
if (rVip === null || rVip === undefined) { var fakeVip = CustomerVip.$new(); fakeVip.setStatus(1); real.setVip(fakeVip); console.log("[*] patched real.user.vip -> injected fakeVip"); }
if (!real.getToken.call(real)) real.setToken.call(real, JString.$new("fake_token_123")); var now = System.currentTimeMillis(); real.setLoginTime.call(real, now); real.setExpire.call(real, Math.floor(now/1000) + 3600);
real.setLogin.call(real, true); real.setPro.call(real, true);
} catch (err) { console.log("[-] patch error:", err); } return real; } console.log("[*] real user is null -> will try fallback creation"); return null; };
try { UserCache.isPro.implementation = function () { return true; }; UserCache.checkLogin.implementation = function () { return true; }; } catch (e) { console.log("[-] hook isPro/checkLogin failed:", e); } });
} setImmediate(main)
|
还是太复杂了,看了记某节拍器app的一次简单破解 | twogoat/showmakerの小站
这样就很好
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
| function main(){ Java.perform(function(){
var String = Java.use("java.lang.String"); var CustomerVip = Java.use("com.eumlab.prometronome.baselib.data.db.entity.CustomerVip")
var MyCustomerVip = CustomerVip.$new(); var MyCustomerVip_level = String.$new("114514"); var MyCustomer_customerId = String.$new("114514"); var MyCustomer_startDate = String.$new("114514-14-19"); var MyCustomer_endDate = String.$new("114513-14-19"); var MyCustomerVip_level = String.$new("114514");
MyCustomerVip.setStatus(1) MyCustomerVip.setVipLevel(MyCustomerVip_level); MyCustomerVip.setStartDate(MyCustomer_startDate) MyCustomerVip.setEndDate(MyCustomer_endDate) MyCustomerVip.setCustomerId(MyCustomer_customerId)
var User = Java.use("com.eumlab.prometronome.baselib.data.db.entity.User") var getVip = User.getVip getVip.implementation = function(){ this.setVip(MyCustomerVip) var result = this.getVip() return result } }) } setImmediate(main)
|

如果希望这个hook常驻,而不用每次frida启动调试,可以用算法助手中使用frida
第二个就是用xposed创建一个模块
这里用第二种试试
在此之前又去了解了下LSPosed XPosed模块和Magisk模块的区别
| 项目 |
LSPosed 模块 |
Magisk 模块 |
| Hook 位置 |
Java 层(Zygote 注入,作用于 App 进程) |
系统层(root 权限下修改文件或系统属性) |
| 原理 |
依托 Xposed/LSPosed 框架在 Java 层动态修改方法(ClassLoader Hook) |
利用 Magisk 的 mount / init 模块机制修改系统分区或注入脚本 |
| 典型用途 |
Hook APP 内部类、方法(绕登录、修改逻辑、注入功能) |
修改系统文件、替换 system/lib、隐藏 root、改 hosts 等 |
| 是否需要 root |
✅ 需要(LSPosed 运行在 root 环境) |
✅ 需要(Magisk 本身是 root 管理框架) |
| 是否作用于 App |
✅ 是(在目标 App 的 Java 层运行) |
❌ 一般不是直接作用于 Java 层 App(但可修改系统环境) |
| 安装方式 |
安装为普通 APK → 在 LSPosed 中勾选目标 App |
.zip 模块刷入 Magisk(开机执行 shell 脚本) |
| Hook 粒度 |
代码级别(方法、类、字段) |
文件级别(系统属性、库、配置) |
| 典型示例 |
改掉 UserCache.isPro() / getVip() |
改 build.prop 隐藏 root、换 hosts 文件 |
Magisk 模块:系统底层 Hook(Native 层)
Magisk 在 boot.img/init 阶段注入自身逻辑;
它允许模块在 /data/adb/modules/ 下放入脚本、库、替换文件;
启动时,通过 overlay mount (magic mount) 把修改的文件“伪装”成系统原文件;
修改 /system/build.prop
替换 /system/lib/libxyz.so
添加 /system/etc/hosts
隐藏 root(MagiskHide 模块)
注入 native 层 Hook 库(配合 Riru、Zygisk)
Magisk 模块更适合 Native / 系统级修改。
Xposed,作为一款可以在不修改APK的情况下影响程序运行的框架,可以基于它可以制作出许多功能强大的模块,且在功能不冲突的情况下同时运作。微信防撤回、步数修改、去广告、美化…….他能实现许许多多的功能,可以说没有它玩机的世界将会少很多的乐趣。
它的原理就是通过替换系统原本的app_process,加载一个额外的jar包,从而实现对zygote进程及其创建的Dalvik/ART虚拟机的劫持。
如何新建XPosed项目可以看吾爱破解安卓逆向入门教程《安卓逆向这档事》七、Sorry,会Hook真的可以为所欲为-xposed快速上手(上)模块编写,Api详解_哔哩哔哩_bilibili(老版本)
从零开始编写Xposed模块
Android Xposed 模块入门 - 淮城一只猫
1 2
| maven { url=uri("https://api.xposed.info") } compileOnly("de.robv.android.xposed:api:82")
|


这一步主要是指定模块的作用域包名,效果就是在Lsposed中勾选作用域时会在应用下提示推荐应用。
新建arrays.xml
1 2 3 4 5 6 7
| <?xml version="1.0" encoding="utf-8"?> <resources> <string-array name="xposedscope" > <item>com.eumlab.android.prometronome</item> </string-array> </resources>
|

最后,我们在Run那里编辑一下启动配置,勾选Always install with package manager并且将Launch Options改成Nothing
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <meta-data android:name="xposedmodule" android:value="true" />
<meta-data android:name="xposeddescription" android:value="不可以涩涩" />
<meta-data android:name="xposedminversion" android:value="54" />
<meta-data android:name="xposedscope" android:resource="@array/xposedscope"/>
|


把那些杂七杂八的Test和androidTest都给删了
注意,这里还要填一个material(其实没用),不然编译apk会报错,因为res里面有引用了这个模板,也可以把res里面的东西删一些

最后 build buildapks

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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| package com.example.prometronome;
import de.robv.android.xposed.IXposedHookLoadPackage; import de.robv.android.xposed.callbacks.XC_LoadPackage; import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XposedHelpers; import de.robv.android.xposed.XposedBridge;
public class MainHook implements IXposedHookLoadPackage { @Override public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { if (!lpparam.packageName.equals("com.eumlab.android.prometronome")) return;
XposedBridge.log("[promhook] loaded for: " + lpparam.packageName); final ClassLoader cl = lpparam.classLoader;
try { final Class<?> UserClass = XposedHelpers.findClass("com.eumlab.prometronome.baselib.data.db.entity.User", cl); final Class<?> CustomerVipClass = XposedHelpers.findClass("com.eumlab.prometronome.baselib.data.db.entity.CustomerVip", cl);
XposedHelpers.findAndHookMethod(UserClass, "getVip", new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { Object vip = param.getResult(); if (vip == null) { Object myVip = XposedHelpers.newInstance(CustomerVipClass); try { XposedHelpers.callMethod(myVip, "setStatus", 1); } catch(Throwable t){} try { XposedHelpers.callMethod(myVip, "setEndDate", "9999-12-31"); } catch(Throwable t){} try { XposedHelpers.callMethod(myVip, "setStartDate", "2020-01-01"); } catch(Throwable t){} try { XposedHelpers.callMethod(myVip, "setVipLevel", "114514"); } catch(Throwable t){} param.setResult(myVip); XposedBridge.log("[promhook] injected new CustomerVip"); } else { try { XposedHelpers.callMethod(vip, "setStatus", 1); } catch(Throwable t){} try { XposedHelpers.callMethod(vip, "setEndDate", "9999-12-31"); } catch(Throwable t){} param.setResult(vip); XposedBridge.log("[promhook] patched existing CustomerVip"); } } });
XposedHelpers.findAndHookMethod( "com.eumlab.prometronome.baselib.util.UserCache", cl, "isPro", new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { param.setResult(true); } });
XposedHelpers.findAndHookMethod( "com.eumlab.prometronome.baselib.util.UserCache", cl, "checkLogin", new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { param.setResult(true); } });
XposedBridge.log("[promhook] hooks installed"); } catch (Throwable t) { XposedBridge.log("[promhook] hook error: " + t.toString()); } } }
|

