笔趣阁

去签

首先尝试一下签名然后安装看看有无异常

APK去签Pro 密码d86s

签名后安装会闪退,用算法助手看看日志

应该就是有签名校验,懒得手撕了,直接 APK去签Pro 去签

任意选择一个去签方式即可

VIP破解

用 MT 的 Dex编辑器++ 打开 dex 文件

搜索一下常量会员

VIP会员点击搜索定位到方法

导航到方法开头

转成 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
private void initLoginInfo() {
CharSequence stringBuilder;
boolean isLogin = CacheUtils.isLogin(); //CacheUtils.isLogin()检查用户是否登录
boolean isVip = CacheUtils.isVip(); //CacheUtils.isVip()判断是否为VIP会员
TextView textView = this.tvUsername;
if (isLogin) { //已登录显示ID否则显示注册/登录
StringBuilder stringBuilder2 = new StringBuilder();
stringBuilder2.append("ID:");
stringBuilder2.append(CacheUtils.getLoginData().getUserName());
stringBuilder = stringBuilder2.toString();
} else {
stringBuilder = "注册/登录";
}
textView.setText(stringBuilder); //tvStatus控件根据VIP状态显示"VIP会员"(黄色)或"普通会员"(白色)
this.tvStatus.setText(isVip ? "VIP会员" : "普通会员");
this.tvStatus.setTextColor(Color.parseColor(isVip ? "#FEDB1F" : "#FFFFFF"));
int i = 0; //llDeleteAccount和llExit两个布局的可见性由登录状态控制:登录可见,否则隐藏。
this.llDeleteAccount.setVisibility(isLogin ? 0 : 8);
LinearLayout linearLayout = this.llExit;
if (!isLogin) {
i = 8;
}
linearLayout.setVisibility(i);
}

回到 smali 代码,我们直接把isLogin()修改成isVip()这样就不会验证是否登录了

接下来跳转到com.xxoo.net.net.CacheUtils类的isVip方法

isVip逻辑分析

CacheUtils类转为 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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
//
// Decompiled by Jadx (from NP Manager)
//
package com.xxoo.net.net;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import com.xxoo.net.net.-$.Lambda.CacheUtils.RxV7noXQYex-FH75g0IDX27hExY;
import com.xxoo.net.net.common.vo.LoginVO;
import com.xxoo.net.net.common.vo.UserFeatureVO;
import com.xxoo.net.net.common.vo.UserPassword;
import com.xxoo.net.net.constants.FeatureEnum;
import com.xxoo.net.net.constants.SysConfigEnum;
import com.xxoo.net.net.util.GsonUtil;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Map;
import org.json.JSONException;
import org.json.JSONObject;

public class CacheUtils {
private static String SHARED_PREFERENCE_KEY;
private static Context context;

public static boolean isLogin() {
boolean z = false;
try {
if (TextUtils.isEmpty(getUserPassword().getUserName())) {
return false;
}
String token = getToken();
if (TextUtils.isEmpty(token)) {
return false;
}
String[] split = token.split("\\.");
if (split.length != 3) {
return false;
}
token = Charset.forName("utf-8").decode(ByteBuffer.wrap(Base64.decode(split[1], 8))).toString();
try {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("token payload: ");
stringBuilder.append(token);
Log.d("lhp", stringBuilder.toString());
if (new JSONObject(token).optLong("exp", TimeUtils.getTimeAfterNow(1, TimeUnitEnum.DAY).getTime()) * 1000 > System.currentTimeMillis()) {
z = true;
}
return z;
} catch (JSONException e) {
e.printStackTrace();
return false;
}
} catch (Exception e2) {
e2.printStackTrace();
return false;
}
}

public static LoginVO getLoginData() {
SharedPreferences publicPreferences = getPublicPreferences();
String str = "loginData";
if (!publicPreferences.contains(str)) {
return new LoginVO();
}
String string = publicPreferences.getString(str, "");
if (TextUtils.isEmpty(string)) {
return new LoginVO();
}
try {
return (LoginVO) GsonUtil.fromJson(string, new 1().getType());
} catch (Exception e) {
e.printStackTrace();
return new LoginVO();
}
}

private static boolean isTokenValid() {
String token = getToken();
boolean z = false;
if (TextUtils.isEmpty(token)) {
return false;
}
String[] split = token.split("\\.");
if (split.length != 3) {
return false;
}
token = Charset.forName("utf-8").decode(ByteBuffer.wrap(Base64.decode(split[1], 8))).toString();
try {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("token payload: ");
stringBuilder.append(token);
Log.d("lhp", stringBuilder.toString());
if (new JSONObject(token).optLong("exp", TimeUtils.getTimeAfterNow(1, TimeUnitEnum.DAY).getTime()) * 1000 > System.currentTimeMillis()) {
z = true;
}
return z;
} catch (JSONException e) {
e.printStackTrace();
return false;
}
}

public static void setConfigs(Map<String, String> map) {
getConfigPreferences().edit().clear().apply();
Editor edit = getConfigPreferences().edit();
for (String str : map.keySet()) {
edit.putString(str, (String) map.get(str));
}
edit.apply();
}

public static UserPassword getUserPassword() {
SharedPreferences publicPreferences = getPublicPreferences();
String str = "login.userName";
if (!publicPreferences.contains(str)) {
return new UserPassword();
}
String str2 = "";
return new UserPassword(publicPreferences.getString(str, str2), publicPreferences.getString("login.password", str2));
}

public static void setLoginData(LoginVO loginVO) {
String str = "loginData.token";
getPublicPreferences().edit().putString("loginData", GsonUtil.toJson(loginVO)).putString(str, loginVO.getToken()).apply();
}

public static boolean canUse(FeatureEnum featureEnum) {
try {
UserFeatureVO userFeatureVO = (UserFeatureVO) Linq.of(getLoginData().getUserFeatures()).first(new RxV7noXQYex-FH75g0IDX27hExY(featureEnum));
if (userFeatureVO != null && userFeatureVO.isValid()) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}

public static void save(String str, String str2) {
getPublicPreferences().edit().putString(str, str2).apply();
}

public static void setUserNamePassword(String str, String str2) {
getPublicPreferences().edit().putString("login.userName", str).putString("login.password", str2).apply();
}

public static void exitLogin() {
getPublicPreferences().edit().clear().apply();
}

public static boolean getConfigBoolean(String str, boolean z) {
try {
return Boolean.valueOf(getConfig(str, String.valueOf(z))).booleanValue();
} catch (Exception unused) {
return z;
}
}

public static int getConfigInt(String str, int i) {
try {
return Integer.valueOf(getConfig(str, String.valueOf(i))).intValue();
} catch (Exception unused) {
return i;
}
}

public static void init(Context context) {
context = context;
SHARED_PREFERENCE_KEY = context.getPackageName();
}

public static String getADConfig(SysConfigEnum sysConfigEnum) {
return getPublicPreferences().getString(sysConfigEnum.getKeyName(), sysConfigEnum.getValue());
}

public static String getADConfig(String str, String str2) {
return getPublicPreferences().getString(str, str2);
}

public static String getConfig(SysConfigEnum sysConfigEnum) {
return getConfigPreferences().getString(sysConfigEnum.getKeyName(), sysConfigEnum.getValue());
}

public static String getConfig(String str, String str2) {
return getConfigPreferences().getString(str, str2);
}

public static boolean getConfigBoolean(SysConfigEnum sysConfigEnum) {
return getConfigBoolean(sysConfigEnum.getKeyName(), sysConfigEnum.getValueBoolean());
}

public static int getConfigInt(SysConfigEnum sysConfigEnum) {
return getConfigInt(sysConfigEnum.getKeyName(), sysConfigEnum.getValueInt());
}

private static SharedPreferences getConfigPreferences() {
return getSharedPreferences("CONFIG");
}

private static SharedPreferences getPublicPreferences() {
return getSharedPreferences(SHARED_PREFERENCE_KEY);
}

private static SharedPreferences getSharedPreferences(String str) {
return context.getSharedPreferences(str, 0);
}

public static String getToken() {
return getPublicPreferences().getString("loginData.token", "");
}

public static boolean isNeedPay() {
return getConfigBoolean("ischarge", false);
}

public static boolean isPay() {
return isNeedPay() && !isVip();
}

public static boolean isVip() {
return canUse(FeatureEnum.YUMAO_BOOK);
}
}

我们只看对我们有用的,

方法定义

isVip()方法定义直接调用 canUse(FeatureEnum.YUMAO_BOOK),通过检查用户是否具备 YUMAO_BOOK 功能来判断是否是VIP。

1
2
3
public static boolean isVip() {
return canUse(FeatureEnum.YUMAO_BOOK);
}

依赖的 canUse 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static boolean canUse(FeatureEnum featureEnum) {
try {
// 从登录数据中获取用户功能列表,并筛选匹配的 FeatureEnum
UserFeatureVO userFeatureVO = (UserFeatureVO) Linq.of(getLoginData().getUserFeatures())
.first(new RxV7noXQYex-FH75g0IDX27hExY(featureEnum));

// 检查功能是否有效
if (userFeatureVO != null && userFeatureVO.isValid()) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}

通过 Linq 工具类筛选出与 FeatureEnum.YUMAO_BOOK 匹配的功能。

数据来源与依赖
  • getLoginData():

SharedPreferences 中读取缓存的登录数据(loginData 键)。反序列化为 LoginVO 对象,包含用户的功能列表 userFeatures

  • UserFeatureVO:

表示用户功能的实体类,可能包含功能的有效期、状态等字段。isValid()方法可能检查时间有效性(例如会员是否过期)。

UserFeatureVO

我们到com.xxoo.net.net.constants.FeatureEnum类看看isValid()方法,就是一个是否在有效期的检查

1
2
3
4
5
6
7
8
9
10
11
public boolean isValid() {
if (!this.limitAmount || this.amount > 0) {
if (this.limitExpireTime) {
Timestamp timestamp = this.expireTime;
if (timestamp != null && timestamp.getTime() > System.currentTimeMillis()) {
}
}
return true;
}
return false;
}

其实再看下去也不需要了,我们基本可以确定,将isVip强制返回 true 应该就可以破解了

回到isVipsmali代码,在return v0前加上const/4 v0,0x1即强制把return返回值赋值为1

同样isLogin返回值前也可以加上

保存后安装

forest 专注森林

脱壳

有腾讯御安全的壳

frida_dump

frida_dump/dump_dex.js at master · lasting-yang/frida_dump

利用 frida dump dex

DEX修复

全选修复一下 DEX

重命名一下

入口点修改

MT管理修改 xml 会闪退,这里使用 NP管理器

用DEX编辑Plus 打开 dex

找到com.wrapper.proxyapplication.WrapperProxyApplication方法

找到入口方法名cc.forestapp.applications.ForestApp

编辑AndroidManifest.xml,找到入口方法MyWrapperProxyApplication

替换成cc.forestapp.applications.ForestApp

保存即可,记得不要自动签名

dex替换

回到 MT管理器 将 dump 的 dex 替换回安装包里

特征删除

主要有几个特征需要删除

主目录下的tencent_stub
assets目录下的0OO00l111l1lo0oooOO0ooOo.datt86tosversion

lib目录下个架构的so文件里的
libshella-x.so(x为版本号)、libshell-super.2019.so(这个可能没有)

去签

修改完成后可以看到已经没有加固了。

我们需要过一下签名校验,点击功能-去除签名校验

完成,安装即可

可正常打开运行

破解

搜索方法名ispremium

定位到 smali 代码

转为 Java 阅读一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void setIsPremium(boolean z) {
if (!isPremium() && z) { //仅在用户升级为Premium时触发
ArrayList arrayList = new ArrayList(); //创建一个包含 UserDefault 配置的列表,并将其持久化
arrayList.add(new UserDefault(UDKeys.S1.name(), String.valueOf(false)));
UserDefault.c.O(getContext(), arrayList); //存储配置
}
put(isPremiumKey, z); //更新本地存储的Premium状态
this.mIsPremiumFlow.setValue(Boolean.valueOf(z));
}
public boolean isPremium() {
boolean z = false;
if (getBoolean(isPremiumKey, false) || getBoolean(isASUnlockedKey, false) || getBoolean(isCTUnlockedKey, false)) {
z = true;
} //用户满足以下任一条件即为Premium:
//直接标记为Premium(isPremiumKey)。
//通过其他途径解锁(isASUnlockedKey 或 isCTUnlockedKey)
Boolean valueOf = Boolean.valueOf(z); //将当前状态同步到 mIsPremiumFlow
MutableStateFlow mutableStateFlow = this.mIsPremiumFlow;
if (mutableStateFlow != null) {
mutableStateFlow.setValue(valueOf);
}
return valueOf.booleanValue(); //返回最终的Premium状态。
}

分析完,和上面思路一样,同样是在return前加const/4 v0,0x1把返回值改为1