本教程仅用作分享,不提供成品,且不应以技术分享谋利,谢谢!
目标app是某讯,前阵子一直有在使用,里面很多内容很实用,存在开屏广告,开通会员以后可以看到更多有趣的版块内容,感觉会受益匪浅,但是奈何本人钱包空空,于是想利用现成的知识改写一下app内部会员验证逻辑,遇事有了这篇分享;总体修改逻辑不复杂,找起来也简单,主要是我个人的一些思考逻辑;也欢迎大家积极提出见解!
1.会员逻辑定位
可在jadx/MT管理器关键字搜索“会员”,或者isvip、is_vip等,这里使用关键字搜索
其中一个“尚未开通简讯VIP会员”引起了我们的注意,我们点进去查看如下,一如既然的判断逻辑,我们找到赋值位置点进去查看
Tip:这里提供三种会员逻辑修改方式,个人推荐第三种
2.第一种修改方式:更改关键验证函数(🌟🌟🌟推荐)
这个函数判断了用户是否已经登录且是否VIP,红色箭头指向的是我们第一种方法修改的地方,直接更改dataUsrInfBeanM.isIs_vip类实例方法的返回值为true,同理回到刚才的位置查看dataUsrInfBeanM.getVip_expire_time()类实例方法,更改方法返回值为任意你想修改的vip到期时间字符串,格式为“0000-00-00 00:00:00”,如“2099-12-31 00:00:00”;
这是最普遍化的修改了
提醒:
1.以上操作可使用MT/MP管理器修改smali代码实现,这里不再演示
2.以上操作没有去除用户登录验证,必须登录以后才生效会员,好处是个人账号独立浏览记录,避免共享账号导致数据同步
最终界面如下:
3. 第二种修改方式:更改网络响应请求(不推荐)
正常登录抓包会发现有一个请求返回了用户数据:
可修改对应响应如下:
{
"code": 1,
"msg": "登录成功",
"data": {
"items": [
{
"user_id": 6607939,
"code": "5f140c673322fc30f4c819bbcb8031e828cb45c338635edfccfb4e5b",//不清楚是什么
"remark": "--",
"name": "", //省略,可随意修改名字
"gender": 0,
"mobile": "", //省略
"avatar": 0,
"icon_url": "",//头像
"profile": "",
"backdrop": "",
"point": 0,
"vip_expire_time": "2099-12-31 00:00:00", //vip过期时间
"device": "Xiaomi_MI 8 SE",//手机品牌型号
"system_version": "10",//系统版本
"version": "5.0.61",//app版本
"wherefrom": "",
"invite": 0,
"devicetoken": "",
"wx_union_id": "",
"qq_id": "",
"apple_id": "",
"mi_uid": "",
"hw_id": "",
"google_id": "",
"honor_id": "MDFAMTA0NDEyNzE2QDJhZjU5YmJhNiryirtMTNlY2ZlNTM2MDQ5ZGMwMmY0N2Q3QDZjODExYmMzM2ZkOGZlNjQ3ZGVkMDQxZmVkMDFmM2E2YWZhMWJiaODZmYmFmNTI2NDkxMjUzODZkODY",//不清楚是什么
"harmony_id": "",
"is_avatar_download": 0,
"country": "",
"province": "",
"city": "",
"realname": "",
"address": "",
"is_bc": 0,
"is_red": 0,
"is_broadcast": 0,
"is_reportreply": 0,
"is_comment_notice": 0,
"is_sleep": 1,
"status": 1,
"ban_remarks": null,
"actived": "2026-03-04 16:20:10", //用户上次活跃时间
"created": "2025-11-03 18:12:22",
"deleted": 0,
"updated": "2026-03-04 16:20:10",
"ip": "112.96.47.165",//用户ip
"is_vip": true //是否vip
}
]
}
}
使用HTTPCanary、mitmproxy等抓包工具重写响应可以实现会员,可以把很多值替换掉,不仅实现了会员,且可以改变头像,uid,昵称等等,这里大家随意修改
总体来说步骤稍麻烦,得每次登录、开抓包工具,个人不推荐这种方式
最终界面如上:
4.第三种修改方式:更改内部json数据(🌟🌟🌟🌟🌟推荐)
还记得我们使用第一种方式定位到的代码吗,这里蓝色箭头指向的地方就是我们本次修改的地方,它直接返回一个类实例对象,本质是由一个json字符串转化来的,在这里修改不仅可以定制化多个用户参数,还可以实现会员,属于是切实可行的好方案了
整个m方法如下:
public final ResultForJxCurentLoginUserInfos.DataUsrInfBean m(@yf.d Context context) {
Object objF;
ResultForJxCurentLoginUserInfos.DataUsrInfBean dataUsrInfBean; //类实例
kotlin.jvm.internal.f0.p(context, "context");
String strQ = q(context); //得到用户ID
if (TextUtils.isEmpty(strQ)) {
return null;
}
d7.i iVarA = d7.i.f43917a.a(context);
if (iVarA != null) {
objF = iVarA.f("userInfo_" + strQ, ""); //userinfo+用户ID作为键,去sharepref取值得到对象
} else {
objF = null;
}
kotlin.jvm.internal.f0.n(objF, "null cannot be cast to non-null type kotlin.String");
String str = (String) objF; //将对象转为json字符串
if (TextUtils.isEmpty(str)) {
dataUsrInfBean = null;
} else {
try {
dataUsrInfBean = (ResultForJxCurentLoginUserInfos.DataUsrInfBean) new Gson().fromJson(str, ResultForJxCurentLoginUserInfos.DataUsrInfBean.class); //将json字符串转为类实例对象
} catch (Exception e10) {
p8.j.e("UserModle 类型转换错误:" + e10, new Object[0]);
dataUsrInfBean = null;
}
}
p8.j.e("UserModle:getUserInfo:" + strQ + " ,bean =" + (dataUsrInfBean != null ? dataUsrInfBean.getName() : null), new Object[0]);
return dataUsrInfBean;//返回类实例对象
}
里面其他关键方法一并贴出:
//q(context)
@yf.d
public final String q(@yf.d Context context) {
kotlin.jvm.internal.f0.p(context, "context");
d7.i iVarA = d7.i.f43917a.a(context);
Object objF = iVarA != null ? iVarA.f("userId", "") : null;
kotlin.jvm.internal.f0.n(objF, "null cannot be cast to non-null type kotlin.String");
return (String) objF;
}
//iVarA.f
i.f43919c = context.getSharedPreferences("SHAREPREFUTIL", 0);
SharedPreferences sharedPreferences = i.f43919c;
public final Object f(@yf.d String key, @yf.d Object defaultValue) {
SharedPreferences sharedPreferences;
f0.p(key, "key");
f0.p(defaultValue, "defaultValue");
if (defaultValue instanceof String) {
SharedPreferences sharedPreferences2 = f43919c;
if (sharedPreferences2 != null) {
return sharedPreferences2.getString(key, (String) defaultValue);
}
return null;
}
if (defaultValue instanceof Boolean) {
SharedPreferences sharedPreferences3 = f43919c;
if (sharedPreferences3 != null) {
return Boolean.valueOf(sharedPreferences3.getBoolean(key, ((Boolean) defaultValue).booleanValue()));
}
return null;
}
if (defaultValue instanceof Integer) {
SharedPreferences sharedPreferences4 = f43919c;
if (sharedPreferences4 != null) {
return Integer.valueOf(sharedPreferences4.getInt(key, ((Number) defaultValue).intValue()));
}
return null;
}
if (defaultValue instanceof Float) {
SharedPreferences sharedPreferences5 = f43919c;
if (sharedPreferences5 != null) {
return Float.valueOf(sharedPreferences5.getFloat(key, ((Number) defaultValue).floatValue()));
}
return null;
}
if (!(defaultValue instanceof Long) || (sharedPreferences = f43919c) == null) {
return null;
}
return Long.valueOf(sharedPreferences.getLong(key, ((Number) defaultValue).longValue()));
}
我们通过Frida-HOOK方式得到原始json字符串,代码如下:
/**
* @fileoverview: get_info
* @author: 十七
* @date: 2026/3/7
* @description: For Android App Test
*/
Java.perform(function () {
var r2 = Java.use("com.tipsoon.android.util.r2");
r2["q"].implementation = function (context) {
console.log(`r2.q is called: context=${context}`);
let result = this["q"](context);
console.log(`用户id=${result}`);
return result;
};
var i = Java.use("d7.i");
i["f"].implementation = function (key, defaultValue) {
console.log(`i.f is called: key=${key}, defaultValue=${defaultValue}`);
let result = this["f"](key, defaultValue);
console.log(`i.f result=${result}`);
return result;
};
})
/*
to Run:
frida -U -f {包名} -l get_strQ.js // spawn 模式 自动打开应用
frida -U -l get_info.js // attach 模式 应用必须已打开
*/
输出如下:
我们拷贝下来更改json字符串数据如下(示范):
'{"actived":"2026-03-07 00:11:46","address":"","apple_id":"","avatar":0,"backdrop":"","city":"","code":"e0cf99d55c48c89445ca2cd4d7fe21339446e062eafa6e43d1b7b68cc07d191f","country":"","created":"2026-03-03 23:30:54","deleted":0,"device":"HUAWEI nova11","devicetoken":"","gender":1,"icon_url":"https://tu.tuhenmei.com/uploads/allimg/240605/1_06052115191J4.jpg","invite":0,"ip":"112.97.205.6","is_avatar_download":0,"is_bc":0,"is_broadcast":0,"is_comment_notice":0,"is_red":0,"is_reportreply":0,"is_sleep":1,"is_vip":true,"mi_uid":"","mobile":"13266668888","name":"JXSUPER","point":0,"profile":"不错VB","province":"","qq_id":"","realname":"","remark":"--","status":1,"system_version":"10","updated":"2026-03-07 00:11:46","user_id":"8888888","version":"5.0.61","vip_expire_time":"2045-12-31 00:00:00","wherefrom":"","wx_union_id":""}'
然后通过原始java代码可知一些操作都是非必要的,我们只需要返回一个实例化的ResultForJxCurentLoginUserInfos.DataUsrInfBean对象即可,所以我们可以让AI给我们省去一些代码,保留关键代码并转为smali再通过MT/NP管理器修改即可
将应用内的m方法的smali替换为如下:
# 定义局部变量(假设在方法中,寄存器分配根据实际情况调整)
# v0: ResultForJxCurentLoginUserInfos$DataUsrInfBean
# v1: Gson
# v2: JSON字符串
.locals 4 # 或 .registers 4,根据你的寄存器使用情况调整
# 1. 初始化dataUsrInfBean变量(初始值为null)
const/4 v0, 0x0
# 2. 创建Gson实例
new-instance v1, Lcom/google/gson/Gson;
invoke-direct {v1}, Lcom/google/gson/Gson;-><init>()V
# 3. 定义JSON字符串常量
const-string v2, "{\"actived\":\"2026-03-07 00:11:46\",\"address\":\"\",\"apple_id\":\"\",\"avatar\":0,\"backdrop\":\"\",\"city\":\"\",\"code\":\"e0cf99d55c48c89445ca2cd4d7fe21339446e062eafa6e43d1b7b68cc07d191f\",\"country\":\"\",\"created\":\"2026-03-07 00:11:46\",\"deleted\":0,\"device\":\"HUAWEI nova11\",\"devicetoken\":\"\",\"gender\":1,\"icon_url\":\"https://tu.tuhenmei.com/uploads/allimg/240605/1_06052115191J4.jpg\",\"invite\":0,\"ip\":\"112.97.205.6\",\"is_avatar_download\":0,\"is_bc\":0,\"is_broadcast\":0,\"is_comment_notice\":0,\"is_red\":0,\"is_reportreply\":0,\"is_sleep\":1,\"is_vip\":true,\"mi_uid\":\"\",\"mobile\":\"13266668888\",\"name\":\"JXSUPER\",\"point\":0,\"profile\":\"不错VB\",\"province\":\"\",\"qq_id\":\"\",\"realname\":\"\",\"remark\":\"--\",\"status\":1,\"system_version\":\"10\",\"updated\":\"2026-03-07 00:11:46\",\"user_id\":\"8888888\",\"version\":\"5.0.61\",\"vip_expire_time\":\"2045-12-31 00:00:00\",\"wherefrom\":\"\",\"wx_union_id\":\"\"}"
# 4. 获取内部类的Class对象(关键:Smali中内部类用$分隔)
const-class v3, Lcom/tipsoon/android/bean/ResultForJxCurentLoginUserInfos$DataUsrInfBean;
# 5. 调用Gson的fromJson方法反序列化
invoke-virtual {v1, v2, v3}, Lcom/google/gson/Gson;->fromJson(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;
move-result-object v0
# 6. 类型强转(Smali中强转通过check-cast指令)
check-cast v0, Lcom/tipsoon/android/bean/ResultForJxCurentLoginUserInfos$DataUsrInfBean;
# 7. 返回该对象(假设是方法返回,根据方法返回类型调整,这里以返回对象为例)
return-object v0
最终java代码呈现的效果:
public final ResultForJxCurentLoginUserInfos.DataUsrInfBean m(Context context) {
return (ResultForJxCurentLoginUserInfos.DataUsrInfBean) new Gson().fromJson("{\"actived\":\"2026-03-07 00:11:46\",\"address\":\"\",\"apple_id\":\"\",\"avatar\":0,\"backdrop\":\"\",\"city\":\"\",\"code\":\"e0cf99d55c48c89445ca2cd4d7fe21339446e062eafa6e43d1b7b68cc07d191f\",\"country\":\"\",\"created\":\"2026-03-07 00:11:46\",\"deleted\":0,\"device\":\"HUAWEI nova11\",\"devicetoken\":\"\",\"gender\":1,\"icon_url\":\"https://tu.tuhenmei.com/uploads/allimg/240605/1_06052115191J4.jpg\",\"invite\":0,\"ip\":\"112.97.205.6\",\"is_avatar_download\":0,\"is_bc\":0,\"is_broadcast\":0,\"is_comment_notice\":0,\"is_red\":0,\"is_reportreply\":0,\"is_sleep\":1,\"is_vip\":true,\"mi_uid\":\"\",\"mobile\":\"13266668888\",\"name\":\"JXSUPER\",\"point\":0,\"profile\":\"不错VB\",\"province\":\"\",\"qq_id\":\"\",\"realname\":\"\",\"remark\":\"--\",\"status\":1,\"system_version\":\"10\",\"updated\":\"2026-03-07 00:11:46\",\"user_id\":\"8888888\",\"version\":\"5.0.61\",\"vip_expire_time\":\"2045-12-31 00:00:00\",\"wherefrom\":\"\",\"wx_union_id\":\"\"}", ResultForJxCurentLoginUserInfos.DataUsrInfBean.class);
}
修改后重新安装应用发现已经实现了自登录操作,且是会员,然而你点击主页其实会提示你用户不存在,这是因为用户信息也是请求返回的,请求返回的数据没有应用信息,这个和第二种方式的响应体一样的,但是无伤大雅。
5.遗憾的地方
从一开始我就发现,尽管我们已经显示会员,但是有些内容我们还是没有权限,比如问鹿,点击提示我们是会员可以直接解锁,但是进去以后除了目录,内容全是🌟号
我看了一下发内容请求的url,没发现什么关键参数,那个Authorization后面的字符串是前一个token请求返回的
内容响应体则为:
{
"code": 1,
"data": {
"info_id": 68881,
"rand_id": 386082,
"sort": 0,
"style": 1,
"admin_id": 443,
"user_id": 0,
"type": 4,
"language": "1",
"is_voice_only": 0,
"is_public": 1,
"class_id": 406,
"read": 9461,
"collection": 0,
"like": 0,
"share": 0,
"comment": 0,
"status": 0,
"pubtime": "2024-08-27 00:00:00",
"endtime": "2099-12-31 23:59:59",
"randtime": "2026-03-06 15:40:01",
"is_comment_review": 1,
"is_first": 1,
"is_first_delay": 0,
"channel": "",
"updated": "2026-03-08 09:42:19",
"created": "2024-08-26 09:33:41",
"deleted": 0,
"validtime": "null",
"province": "",
"city": "",
"position": "",
"relation_id": [],
"screen": 0,
"check": 1,
"check_admin": 439,
"check_time": "2024-08-31 09:11:54",
"is_rating": 0,
"score": 0,
"underage_read": 1,
"allow_modify": 0,
"from": "\u7b80\u8bafTIPSOON\u5b98\u65b9\u56e2\u961f",
"summary": "",
"options": [],
"title": "\u4e13\u8bbf\u94f6\u884c\u8d37\u6b3e\u4e1a\u52a1\u5458\uff1a\u8695\u8c46",
"auth": "",
"review": "",
"tag": "",
"cover": "https:\/\/img.tipsoon.com\/normal\/30d5499ff14eb839cf5a52f46bf30ba9.jpeg",
"small": "https:\/\/img.tipsoon.com\/small\/933c45df1d0701f0004b0b7d90506ac8.jpeg",
"hd": "https:\/\/img.tipsoon.com\/hd\/6dbe1a9350fa0b4d56231e207409f24e.jpeg",
"origin": "VEER",
"voice": "",
"reader_comment_id": 0,
"reader_user_id": 0,
"video": "",
"video_from": 1,
"content": "\u8fc7\u53bb\uff0c\u4e2d\u5c0f\u5fae\u4f01\u4e1a\u7ecf\u5e38\u9047\u5230\u201c\u9700\u8981\u8d37\u6b3e\uff0c\u4f46\u8d37\u4e0d\u5230\u6b3e\u201d\u7684\u7a98\u5883\uff0c\u800c\u8fd1\u4e9b\u5e74\uff0c\u56fd\u5bb6\u6709\u610f\u6276\u6301\u4e2d\u5c0f\u5fae\u4f01\u4e1a\u53d1\u5c55\uff0c\u591a\u5bb6\u94f6\u884c\u4e5f\u7eb7\u7eb7\u63a8\u51fa\u591a\u79cd\u4f01\u4e1a\u8d37\u6b3e\u4ea7\u54c1\uff0c\u4e3a\u4e2d\u5c0f\u4f01\u4e1a\u51cf\u538b\u3002\u4eca\u5929\u6211\u4eec\u8d70\u8fd1\u7684\u662f\u94f6\u884c\u8d37\u6b3e\u4e1a\u52a1\u5458\uff1a\u8695\u8c46\uff0c\u7531\u4ed6\u4e3a\u6211\u4eec\u8bb2\u8ff0\u94f6\u884c\u8d37\u6b3e\u4e1a\u52a1\u4ee5\u53ca\u4e2d\u5c0f\u5fae\u4f01\u4e1a\u8d37\u6b3e\u7684\u90a3\u4e9b\u4e8b\u2026\u2026",
"detail": "",
"url": null,
"url_text": "",
"url_color": "#cc3333",
"url_target": 0,
"property": 0,
"sim": 0,
"siminfo": "",
"is_reply": 0,
"bgm": "",
"is_read": 1,
"is_comment": 1,
"voice_options": [],
"url_ext": null,
"talk_content": [{
"title": "\u94f6\u884c\u8d37\u6b3e\u4e1a\u52a1\u4e00\u4e8c\u4e09",
"is_core": 0,
"sort": "1",
"content": "******************************"
}, {
"title": "\u5bf9\u4e2d\u5c0f\u5fae\u4f01\u4e1a\u8d37\u6b3e\u6276\u6301\u529b\u5ea6",
"is_core": 0,
"sort": "2",
"content": "******************************"
}, {
"title": "\u4e2d\u5c0f\u5fae\u4f01\u4e1a\u8d37\u6b3e\u6709\u4f55\u9014\u5f84\uff1f",
"is_core": "1",
"sort": "3",
"content": "******************************"
}, {
"title": "\u7a0e\u8d37\u6210\u4f01\u4e1a\u878d\u8d44\u4e3b\u6d41\u65b9\u5f0f",
"is_core": "1",
"sort": "4",
"content": "******************************"
}, {
"title": "\u7a0e\u8d37\u95ee\u9898\u65e5\u76ca\u51f8\u663e",
"is_core": "1",
"sort": "5",
"content": "******************************"
}, {
"title": "\u4f01\u4e1a\u8d37\u6b3e\u5229\u7387\u8fdb\u5165\u201c3 \u65f6\u4ee3\u201d",
"is_core": "1",
"sort": "6",
"content": "******************************"
}, {
"title": "\u4e2d\u5c0f\u5fae\u4f01\u4e1a\u8d37\u6b3e\u9700\u6ce8\u610f",
"is_core": "1",
"sort": "7",
"content": "******************************"
}, {
"title": "\u4f01\u4e1a\u8d37\u6b3e\u903e\u671f\u540e\u679c\u4e25\u91cd",
"is_core": 0,
"sort": "8",
"content": "******************************"
}, {
"title": "\u63d0\u524d\u8fd8\u8d37\u6709\u5fc5\u8981\u5417\uff1f",
"is_core": "1",
"sort": "9",
"content": "******************************"
}, {
"title": "\u672a\u6765\u94f6\u884c\u5982\u4f55\u52a9\u529b\u4f01\u4e1a\u878d\u8d44\uff1f",
"is_core": "1",
"sort": "10",
"content": "******************************"
}],
"question_content": null,
"hidden": 1,
"class_name": "\u95ee\u9e7f",
"reader_name": null,
"reader_icon_url": null,
"unlock_num": 1854,
"relation_count": 0,
"isvip": false,
"is_origin_ai": false
}
}
这里所有的键值对都和我们修改的不一样,所以我们获取不到文章内容,应为本质它还是认为我们不是会员,这在响应体里isvip有提及,具体为什么,怎么判断的我还没有个定论,难道开通会员以后客户端就把这个token存储认定为是vip了吗?它应该绑定账户才对的,因为每次清空应用数据token都会不一样
至于token参数sign的定位如下:
搜索“sign”,定位第一个:
hook strA赋值位置(以spawn方式):
Java.perform(function () {
var a = Java.use("ja.a");
a["a"].implementation = function (str) {
console.log(`a.a is called: str=${str}`);
let result = this["a"](str);
console.log(`a.a result=${result}`);
return result;
};
})
hook到的:
a.a is called: str=Xiaomi_MI 8 SE5.0.6110xiaomi21772934924480test
其实就是手机品牌型号+应用版本+系统版本+时间戳+“test”后md5
所以最后这个应用的修改并不完美,需继续深究
以上教程不够详尽之处尽请谅解,因为不可能每个步骤都面面聚到,否则篇幅太长,小白看多了也烦,觉得很复杂的样子,大神又会觉得我啰嗦,大家知道思路就可以了