1、申 请 I D :daxi0ng
2、个人邮箱:2031553790@qq.com
3、原创技术文章:
https://blog.csdn.net/qq_40989258/article/details/156990279
[安卓逆向+lposed]破解某中介招工app加锁信息功能
​
一、逆向学习,发现手头正好有个app就尝试一下。
首先进入招工信息列表页,reqable抓包发现服务器返回的全部都是密文,但是点进具体的招工详情就没有数据包发送了,并且在联系方式处可以通过下载广告app解锁明文,所以app客户端肯定是有解密功能,这个时候直截了当2个思路
1、欺骗app已经下载广告app
2、解密明文显示(也是这次的路线)
![]() ![]()
二、FrIDA启动,这里写了一个进行点击操作后app调用了具体哪个active的脚本,能帮助我们快速定位hook的位置
[JavaScript] 纯文本查看 复制代码 'use strict';
Java.perform(function () {
console.log('=== click trace start ===');
// 1️⃣ Hook 所有 View 点击(最重要)
var View = Java.use('android.view.View');
View.performClick.implementation = function () {
try {
var cls = this.getClass().getName();
console.log('[CLICK] View.performClick -> ' + cls);
// 打印父类,帮助判断 RecyclerView
var parent = this.getParent();
if (parent) {
try {
console.log(' parent -> ' + parent.getClass().getName());
} catch (e) {}
}
} catch (e) {}
return this.performClick();
};
// 2️⃣ Hook RecyclerView onBind(确认是不是 RecyclerView)
try {
var RVAdapter = Java.use('androidx.recyclerview.widget.RecyclerView$Adapter');
RVAdapter.onBindViewHolder.implementation = function (holder, position) {
console.log('[RV] onBindViewHolder position = ' + position +
' holder = ' + holder.getClass().getName());
return this.onBindViewHolder(holder, position);
};
} catch (e) {
console.log('[RV] Adapter hook failed (maybe no RecyclerView)');
}
// 3️⃣ Hook View.setOnClickListener(看点击逻辑在哪)
View.setOnClickListener.implementation = function (listener) {
try {
var viewCls = this.getClass().getName();
var lCls = listener ? listener.getClass().getName() : 'null';
console.log('[SET_CLICK] ' + viewCls + ' -> ' + lCls);
} catch (e) {}
return this.setOnClickListener(listener);
};
// 4️⃣ Hook Activity 跳转(看是不是点 item 后进详情页)
var Activity = Java.use('android.app.Activity');
Activity.startActivity.overload('android.content.Intent').implementation =
function (intent) {
try {
var target = intent.getComponent();
if (target) {
console.log('[START_ACTIVITY] ' + target.getClassName());
} else {
console.log('[START_ACTIVITY] implicit intent');
}
} catch (e) {}
return this.startActivity(intent);
};
console.log('=== click trace hook installed ===');
});
三、bingo!找到了进入招工详情页时实际调用的组件com.android.xxx.InfoActivity
![]()
四、打开我们的jadx看看反编译的java代码
![]()
五、这里就可以看看代码具体做了什么,数据是怎么传进 InfoActivity 的?
翻了一下发现这串代码
[JavaScript] 纯文本查看 复制代码 @Override
public final void d(Intent intent) {
intent.getStringExtra(b.a.o("XgY="));
this.f3650b =
(MMList.DataEntity) intent.getParcelableExtra(b.a.o("UwMfEg=="));
}
重点来了:
MMList.DataEntity 是一个实现了Parcelable接口的类
列表页点击子项时,等于直接把当前 item 对象 putExtra 进 Intent
六、但是问题接着来了,当我尝试引用f3650b的时候,发现没有找到名为 f3650b 的字段。
[JavaScript] 纯文本查看 复制代码 'use strict';
Java.perform(function () {
const InfoActivity = Java.use('com.android.xxx.InfoActivity');
function findFieldRecursive(obj, fieldName) {
let clazz = obj.getClass();
while (clazz !== null) {
try {
const field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
} catch (e) {
clazz = clazz.getSuperclass();
}
}
return null;
}
InfoActivity.d.overload('android.content.Intent').implementation = function (intent) {
this.d(intent);
const field = findFieldRecursive(this, 'f3650b');
if (field === null) {
console.log('[!] f3650b not found');
return;
}
const value = field.get(this);
console.log('[+] f3650b found:', value);
};
});
七、那么只有一种解释了,
f3650b 这个字段名在真实 dex 里根本不存在,在 jadx 里看到的 f3650b,只是 反编译器“重命名后的别名”。
看到的代码是:
public MMList.DataEntity f3650b;
但实际上在 dex 里,很可能是:
public Object a;
或者:
public MMList$DataEntity b;
很显然JVM并不知道,它只认识真实的字段名,所以现在也不要再去猜测,直接从 Intent 里把 Parcelable 拿出来,真正的数据源是 Intent.getParcelableExtra()
于是有了这个代码,成功获取到解密后的数据:
[JavaScript] 纯文本查看 复制代码 'use strict';
Java.perform(function () {
const InfoActivity = Java.use('com.android.xxx.InfoActivity');
InfoActivity.d.overload('android.content.Intent').implementation = function (intent) {
// 调用原方法
this.d(intent);
const extras = intent.getExtras();
if (!extras) return;
extras.keySet().toArray().forEach(function (key) {
const val = extras.get(key);
console.log('\n[key] ' + key);
console.log('[class] ' + (val ? val.getClass().getName() : 'null'));
console.log('[value] ' + val);
// 直接 dump 字段
if (val) dumpFields(val);
});
};
function dumpFields(obj) {
try {
obj.getClass().getDeclaredFields().forEach(function (f) {
f.setAccessible(true);
console.log(f.getName() + ' = ' + f.get(obj));
});
} catch (e) {
console.log('[dump error]', e);
}
}
});
![]()
八、目前位置我们做到了
点击加锁信息单元->frida脚本显示加锁内容
继续我想既然明文可以获取到,直接让明文代替到需要解锁才能显示的UI位置不就好了,说干就干。
我们在InfoActivity看到这么一串代码,经过调用解密发现“xxx”这一串就是“点击解锁查看”,也就是UI需要替换的位置。
[JavaScript] 纯文本查看 复制代码 public final void m(TextView textView, String str) {
StringBuilder sbQ = androidx.appcompat.app.a.q(str);
sbQ.append(b.a.o("xxx"));
String string = sbQ.toString();
SpannableString spannableString = new SpannableString(string);
spannableString.setSpan(new ForegroundColorSpan(Color.parseColor(b.a.o("FAQNR1ZLBA=="))), 3, string.length(), 17);
textView.setText(spannableString);
textView.setOnClickListener(new a());
}
九、经过一番捣鼓,搞定下列代码,成功在UI页面原需解锁位置直接显示明文
[JavaScript] 纯文本查看 复制代码 Java.perform(function () {
// Hook 的目标 Activity
var Info = Java.use("com.android.xxx.InfoActivity");
// UI 相关类(富文本 + 颜色)
var SpannableString = Java.use("android.text.SpannableString");
var ForegroundColorSpan = Java.use("android.text.style.ForegroundColorSpan");
var Color = Java.use("android.graphics.Color");
var JString = Java.use("java.lang.String");
// 反射读取对象字段,失败返回空字符串
function getField(obj, name) {
try {
var f = obj.getClass().getDeclaredField(name);
f.setAccessible(true);
var v = f.get(obj);
return v ? v.toString() : "";
} catch (e) {
return "";
}
}
// Hook InfoActivity.m(TextView, String)
Info.m.overload(
'android.widget.TextView',
'java.lang.String'
).implementation = function (tv, label) {
// 先执行原方法,保证原 UI 正常
this.m(tv, label);
try {
// 获取启动 Intent
var intent = this.getIntent();
if (!intent) return;
// 取页面数据实体(Parcelable)
var data = intent.getParcelableExtra("data");
if (!data) return;
// 从实体中读取真实字段值
var phone = getField(data, "phone");
var qq = getField(data, "qq");
var wechat = getField(data, "wechat");
var address = getField(data, "address");
// 根据 label 决定展示哪个字段
var value = "";
if (label.indexOf("QQ") !== -1) {
value = qq;
} else if (label.indexOf("微信") !== -1) {
value = wechat;
} else if (label.indexOf("电话") !== -1) {
value = phone;
} else if (label.indexOf("地址") !== -1) {
value = address;
} else {
return;
}
// 拼接完整文本(即使 value 为空也显示)
var full = label + value;
// 构造富文本并设置 value 为灰色
var sp = SpannableString.$new(JString.$new(full));
var color = Color.parseColor("#ff808080");
sp.setSpan(
ForegroundColorSpan.$new(color),
label.length,
full.length,
17 // Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
// 替换 TextView 内容
tv.setText(sp);
// 日志输出
console.log("[unlock]", full);
} catch (e) {
// 防止 UI 崩溃
console.log("[!] m ui error:", e);
}
};
console.log("[+] InfoActivity.m dynamic unlock");
});
![]()
十、完美解决
但是总挂一个firda多麻烦呀,不如改成lposed模块,java代码如下,用as编译成apk安装启用就ok
[Java] 纯文本查看 复制代码 package com.example.decrypthook;
// Android 基础类
import android.app.Activity;
import android.content.Intent;
import android.graphics.Color;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import android.widget.TextView;
// Xposed 相关接口和工具
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
import java.lang.reflect.Field;
// Xposed 入口类
public class HookEntry implements IXposedHookLoadPackage {
// 每加载一个 App 都会调用
@Override
public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
try {
// 找到目标 Activity 类
Class<?> infoCls = XposedHelpers.findClass(
"com.android.xxx.InfoActivity",
lpparam.classLoader
);
// Hook InfoActivity.m(TextView, String)
XposedHelpers.findAndHookMethod(
infoCls,
"m", // 方法名
TextView.class, // 参数1
String.class, // 参数2
new XC_MethodHook() {
// 原方法执行完成后再执行这里
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
// 第一个参数:TextView
TextView tv = (TextView) param.args[0];
// 第二个参数:label 文本
String label = (String) param.args[1];
// 当前 Activity 实例
Activity act = (Activity) param.thisObject;
try {
// 取启动 Activity 的 Intent
Intent intent = act.getIntent();
if (intent == null) return;
// 从 Intent 中取 Parcelable 数据对象
Object data = intent.getParcelableExtra("data");
if (data == null) return;
// 通过反射读取 data 中的字段
String phone = getField(data, "phone");
String qq = getField(data, "qq");
String wechat = getField(data, "wechat");
String address = getField(data, "address");
// 根据 label 内容判断显示哪个字段
String value = null;
if (label.contains("QQ")) {
value = qq;
} else if (label.contains("微信")) {
value = wechat;
} else if (label.contains("电话")) {
value = phone;
} else if (label.contains("地址")) {
value = address;
}
// 即使为 null 也强制显示
if (value == null) value = "";
// 拼接 label + 真实数据
String full = label + value;
// 构造富文本
SpannableString sp = new SpannableString(full);
// 设置灰色字体
int color = Color.parseColor("#ff808080");
// 只给 value 部分上色
sp.setSpan(
new ForegroundColorSpan(color),
label.length(), // 起始位置
full.length(), // 结束位置
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
);
// ⭐ 替换 TextView 显示内容
tv.setText(sp);
} catch (Throwable t) {
// 防止 Hook 失败导致闪退
XposedBridge.log("[DecryptHook] m ui error: " + t);
}
}
}
);
} catch (Throwable t) {
// Hook 类或方法失败
XposedBridge.log("[DecryptHook] hook failed: " + t);
}
}
// 通过反射读取对象字段值
private static String getField(Object obj, String name) {
try {
// 获取字段
Field f = obj.getClass().getDeclaredField(name);
f.setAccessible(true);
// 读取字段值
Object v = f.get(obj);
// 转成字符串返回
return v != null ? v.toString() : "";
} catch (Throwable t) {
// 字段不存在或读取失败
return "";
}
}
}
![]()
十一、整体的流程大概就是这个样子
[列表页 RecyclerView item]
│
│ 点击
▼
[startActivity(Intent)]
│
│ intent.putExtra("data", MMList.DataEntity)
▼
[InfoActivity.onCreate / onResume]
│
│ 系统 inflate 布局
│
▼
[InfoActivity.f()]
├─ tv.setText(...)
├─ ⚠️ m(tvQq, "QQ:")
├─ ⚠️ m(tvWechat,"微信:")
├─ ⚠️ m(tvPhone,"电话:")
├─ ⚠️ m(tvAddress,"地址:")
│
│ m() 内部逻辑
▼
[m(TextView, label)]
├─ label + b.a.o("xxx")
├─ 显示“点击解锁信息”
└─ setOnClickListener(弹广告)
│
│
│【我们 Hook 在这里】
▼
[LSPosed hook InfoActivity.m]
├─ afterHookedMethod()
├─ 从 Activity.getIntent() 取 data
├─ 反射 data.phone / qq / wechat / address
├─ 判断 label 是哪一行
├─ label + 明文值(空也覆盖)
├─ SpannableString 重建
└─ tv.setText(新内容)
│
▼
[UI 直接显示明文,无需点击解锁]
十二、走过的一些弯路
1、开始执着于解密算法,但后面发现对于info页面是多此一举,因为列表页 / 详情页 ≠ 同一条数据链,InfoActivity 里已经是 明文 DataEntity。
2、取字段过早,试图在 onCreate / onResume 直接改 TextView 内容,UI 尚未绑定 TextView,m() 还没被调用,导致空值 / 错位。
​ |