笔趣阁
去签
首先尝试一下签名然后安装看看有无异常
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(); boolean isVip = CacheUtils.isVip(); TextView textView = this.tvUsername; if (isLogin) { StringBuilder stringBuilder2 = new StringBuilder(); stringBuilder2.append("ID:"); stringBuilder2.append(CacheUtils.getLoginData().getUserName()); stringBuilder = stringBuilder2.toString(); } else { stringBuilder = "注册/登录"; } textView.setText(stringBuilder); this.tvStatus.setText(isVip ? "VIP会员" : "普通会员"); this.tvStatus.setTextColor(Color.parseColor(isVip ? "#FEDB1F" : "#FFFFFF")); int i = 0; 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
|
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 { 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
匹配的功能。
数据来源与依赖
从 SharedPreferences
中读取缓存的登录数据(loginData
键)。反序列化为 LoginVO
对象,包含用户的功能列表 userFeatures
。
表示用户功能的实体类,可能包含功能的有效期、状态等字段。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 应该就可以破解了
回到isVip
的smali
代码,在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
目录下的0OO00l111l1l
、o0oooOO0ooOo.dat
、t86
、tosversion

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) { ArrayList arrayList = new ArrayList(); arrayList.add(new UserDefault(UDKeys.S1.name(), String.valueOf(false))); UserDefault.c.O(getContext(), arrayList); } put(isPremiumKey, z); this.mIsPremiumFlow.setValue(Boolean.valueOf(z)); } public boolean isPremium() { boolean z = false; if (getBoolean(isPremiumKey, false) || getBoolean(isASUnlockedKey, false) || getBoolean(isCTUnlockedKey, false)) { z = true; } Boolean valueOf = Boolean.valueOf(z); MutableStateFlow mutableStateFlow = this.mIsPremiumFlow; if (mutableStateFlow != null) { mutableStateFlow.setValue(valueOf); } return valueOf.booleanValue(); }
|
分析完,和上面思路一样,同样是在return
前加const/4 v0,0x1
把返回值改为1

