某节拍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字样吗

image-20251108172808792

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");

// Hook isPro()
UserCache.isPro.implementation = function () {
console.log("[*] Hooked isPro() called");
return true; // 强制返回true,表示用户是VIP
};

console.log("[+] isPro() hook installed");
});
}
setImmediate(main)
//frida -U -f "com.eumlab.android.prometronome" -l
1
frida -U -f "com.eumlab.android.prometronome" -l prometronome.js

执行后正常vip功能均可使用

image-20251108194756218

但是进入我的账号后会闪退

根据 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.useruser.getVip() 依然可能是 null
  • 当别的地方访问 user.getVip().getExpireTime() 或类似字段时,就会 NullPointerException,导致闪退。

也就是说:

“判断逻辑”绕过了,但没让“数据结构”一致。

image-20251108200746445

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

image-20251108201317460

User 里确实有 vip 字段(类型是 CustomerVip),而不是 Vip;

User 里还有一个布尔字段 isPro

有 expire、loginTime、token、refreshToken 等过期验证相关字段;

所以 “我的账号页面” 崩溃的原因,大概率就是 vip == null或者 token == null。

我们可以构造一个假的 User对象,填充它的关键字段;返回它,让所有 getUser() 调用都拿到稳定的对象,同时强制 isPro()、checkLogin()返回true。

但是要注意的是,我们这里不能直接new一个User,我们自己new一个容易出现

创建/返回的 fakeUsergetUser() 期望的 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 {
// 检查 vip
var rVip = null;
rVip = real.getVip.call(real);

if (rVip === null || rVip === undefined) {
// 创建并注入一个 CustomerVip(来自相同类加载器)
var fakeVip = CustomerVip.$new();
fakeVip.setStatus(1);
real.setVip(fakeVip);
console.log("[*] patched real.user.vip -> injected fakeVip");
}

// 确保 token/refreshToken/expire/loginTime 等非空
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);

// 标记已登录 / pro 字段
real.setLogin.call(real, true);
real.setPro.call(real, true);

} catch (err) {
console.log("[-] patch error:", err);
}
return real;
}
// 如果 real 为 null,降级回我们创建的 fakeUser(见下方 fallback)
console.log("[*] real user is null -> will try fallback creation");
return null;
};

// 保险:也 Hook isPro/checkLogin
try {
UserCache.isPro.implementation = function () { return true; };
UserCache.checkLogin.implementation = function () { return true; };
} catch (e) { console.log("[-] hook isPro/checkLogin failed:", e); }
});


}
setImmediate(main)
//frida -U -f "com.eumlab.android.prometronome" -l

还是太复杂了,看了记某节拍器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)

image-20251108210657919

如果希望这个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")

image-20251109112557724

image-20251109142709359

这一步主要是指定模块的作用域包名,效果就是在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>

image-20251109112845424

最后,我们在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" />
<!-- 模块描述,显示在xposed模块列表那里第二行 -->
<meta-data
android:name="xposeddescription"
android:value="不可以涩涩" />
<!-- 最低xposed版本号(lib文件名可知,一般填54即可) -->
<meta-data
android:name="xposedminversion"
android:value="54" />
<!-- 模块作用域 -->
<meta-data
android:name="xposedscope"
android:resource="@array/xposedscope"/>

image-20251109153600493

image-20251109140749380

把那些杂七杂八的Test和androidTest都给删了

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

image-20251109145438206

最后 build buildapks

image-20251109145606465

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);

// Hook User.getVip(),在返回前注入/修改 CustomerVip
XposedHelpers.findAndHookMethod(UserClass, "getVip", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
Object vip = param.getResult();
if (vip == null) {
// 创建 CustomerVip 实例(同一 classloader)
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){}
// 返回我们构造的 vip
param.setResult(myVip);
XposedBridge.log("[promhook] injected new CustomerVip");
} else {
// 若已存在,确保标记为 pro 并把到期改长
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");
}
}
});

// 可选:额外hook UserCache.isPro/checkLogin 保证不会被二次判断
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());
}
}
}

image-20251109153806048

image-20251109153706718