1. 逆向分析背景与目标
最近在研究某中介招工类App时,发现其招工信息列表页返回的都是加密数据,而点击进入详情页后,联系方式等关键信息需要下载广告App才能解锁查看。这种设计明显是为了增加广告收益,但对于普通用户来说体验并不友好。于是萌生了一个想法:能否绕过这个限制,直接显示被加密的联系方式?
经过初步分析,确定了两个可能的突破方向:
- 模拟已下载广告App的状态(欺骗验证机制)
- 直接解密并显示原始数据(更彻底的解决方案)
我选择了第二条路线,因为这种方式能从根本上解决问题,而且对逆向工程的学习更有价值。
2. 技术准备与环境搭建
2.1 所需工具清单
- 抓包工具:Reqable(比Fiddler/Charles更适合移动端)
- 逆向分析工具:
- Jadx(Java反编译)
- Frida(动态Hook)
- IDA Pro(可选,用于Native层分析)
- 开发环境:
- Android Studio(编写Xposed模块)
- Python 3(运行Frida脚本)
2.2 设备环境配置
- 准备一台已root的Android测试机
- 安装Magisk并刷入LSPosed框架
- 配置Frida服务端(版本需与客户端匹配)
- 安装目标App并确保能正常运行
注意:所有测试应在法律允许范围内进行,仅用于学习研究目的。
3. 初步分析与定位
3.1 网络请求分析
使用Reqable抓包发现:
- 列表页接口返回的是加密数据
- 进入详情页时没有新的网络请求
- 联系方式处显示"点击下载App查看"
这说明解密逻辑应该是在客户端完成的,且详情页数据可能已经包含在列表页的返回数据中。
3.2 关键页面定位
编写Frida脚本来追踪Activity跳转:
javascript复制Java.perform(function() {
const Activity = Java.use('android.app.Activity');
Activity.startActivity.overload('android.content.Intent').implementation = function(intent) {
const target = intent.getComponent();
if(target) {
console.log(`跳转到Activity: ${target.getClassName()}`);
}
return this.startActivity(intent);
};
});
通过这个脚本,我们定位到详情页对应的Activity是com.android.xxx.InfoActivity。
4. 深入逆向分析
4.1 反编译与代码分析
使用Jadx打开目标App的APK文件,找到InfoActivity类。关键发现:
- 数据是通过Intent传递的Parcelable对象
- 有个关键方法
d(Intent)负责解析Intent数据 - 联系方式显示逻辑在
m(TextView, String)方法中
4.2 数据流追踪
数据传递路径如下:
- 列表页点击item时,将
MMList.DataEntity对象放入Intent - InfoActivity的
d()方法从Intent中取出这个对象 - 各种联系方式通过
m()方法显示在UI上
关键代码段:
java复制@Override
public final void d(Intent intent) {
this.f3650b = (MMList.DataEntity) intent.getParcelableExtra(b.a.o("UwMfEg=="));
}
4.3 字段访问问题
尝试通过Frida直接访问f3650b字段时失败,这是因为:
f3650b是Jadx反编译后生成的别名- 实际dex中的字段名可能是简单的
a或b - 更可靠的方式是直接从Intent中获取Parcelable对象
5. Frida动态Hook实现
5.1 数据提取脚本
javascript复制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(key => {
const val = extras.get(key);
console.log(`\n[key] ${key}`);
console.log(`[class] ${val ? val.getClass().getName() : 'null'}`);
console.log(`[value] ${val}`);
if(val) dumpFields(val);
});
};
function dumpFields(obj) {
try {
obj.getClass().getDeclaredFields().forEach(f => {
f.setAccessible(true);
console.log(`${f.getName()} = ${f.get(obj)}`);
});
} catch(e) {
console.log('[dump error]', e);
}
}
});
5.2 UI修改脚本
javascript复制Java.perform(function() {
const InfoActivity = Java.use('com.android.xxx.InfoActivity');
const SpannableString = Java.use('android.text.SpannableString');
const ForegroundColorSpan = Java.use('android.text.style.ForegroundColorSpan');
const Color = Java.use('android.graphics.Color');
InfoActivity.m.overload('android.widget.TextView', 'java.lang.String').implementation = function(tv, label) {
this.m(tv, label); // 先执行原方法
try {
const intent = this.getIntent();
if(!intent) return;
const data = intent.getParcelableExtra("data");
if(!data) return;
let value = "";
if(label.indexOf("QQ") !== -1) {
value = getField(data, "qq");
} else if(label.indexOf("微信") !== -1) {
value = getField(data, "wechat");
} // 其他字段类似...
const full = label + (value || "");
const sp = SpannableString.$new(full);
const color = Color.parseColor("#ff808080");
sp.setSpan(
ForegroundColorSpan.$new(color),
label.length,
full.length,
17
);
tv.setText(sp);
} catch(e) {
console.log("[!] UI error:", e);
}
};
function getField(obj, name) {
try {
const f = obj.getClass().getDeclaredField(name);
f.setAccessible(true);
return f.get(obj) || "";
} catch(e) {
return "";
}
}
});
6. 转换为Xposed模块
6.1 模块结构
code复制DecryptHook/
├── app/
│ ├── build.gradle
│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── java/com/example/decrypthook/
│ │ └── HookEntry.java
│ └── res/
├── build.gradle
└── settings.gradle
6.2 核心实现代码
java复制public class HookEntry implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) {
if(!lpparam.packageName.equals("com.target.app")) return;
try {
Class<?> infoCls = XposedHelpers.findClass(
"com.android.xxx.InfoActivity",
lpparam.classLoader
);
XposedHelpers.findAndHookMethod(
infoCls,
"m",
TextView.class,
String.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
TextView tv = (TextView) param.args[0];
String label = (String) param.args[1];
Activity act = (Activity) param.thisObject;
try {
Intent intent = act.getIntent();
if(intent == null) return;
Object data = intent.getParcelableExtra("data");
if(data == null) return;
String value = "";
if(label.contains("QQ")) {
value = getField(data, "qq");
} else if(label.contains("微信")) {
value = getField(data, "wechat");
} // 其他字段...
String full = label + value;
SpannableString sp = new SpannableString(full);
int color = Color.parseColor("#ff808080");
sp.setSpan(
new ForegroundColorSpan(color),
label.length(),
full.length(),
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
);
tv.setText(sp);
} catch(Throwable t) {
XposedBridge.log("[Error] " + t);
}
}
}
);
} catch(Throwable t) {
XposedBridge.log("[Init Error] " + t);
}
}
private static String getField(Object obj, String name) {
try {
Field f = obj.getClass().getDeclaredField(name);
f.setAccessible(true);
return (String) f.get(obj);
} catch(Exception e) {
return "";
}
}
}
6.3 模块配置
在assets/xposed_init中指定入口类:
code复制com.example.decrypthook.HookEntry
在AndroidManifest.xml中添加元数据:
xml复制<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="解密目标App的联系方式" />
<meta-data
android:name="xposedminversion"
android:value="82" />
7. 经验总结与避坑指南
7.1 走过的弯路
- 过早关注解密算法:最初试图逆向整个加密流程,后来发现详情页数据已经是明文的Parcelable对象
- 时机问题:尝试在
onCreate中修改TextView内容,但此时UI还未完全初始化 - 字段名混淆:直接使用Jadx看到的字段名导致访问失败,应该通过反射遍历所有字段
7.2 关键发现
- 数据传递方式:列表页到详情页是通过Intent传递完整的Parcelable对象
- UI更新机制:联系方式是通过
m()方法动态设置的,这是最佳的Hook点 - 防逆向措施:使用了简单的字符串加密(
b.a.o()),但没有更复杂的保护
7.3 最佳实践
- 动态分析先行:先用Frida进行快速验证,再考虑Xposed模块
- 从UI入手:逆向时从显示逻辑往回追溯数据流往往更高效
- 健壮性处理:Hook时要考虑各种边界情况,避免导致目标App崩溃
8. 技术原理深入
8.1 Parcelable机制
Android中用于跨进程传递对象的序列化协议。相比Serializable,它更高效但需要手动实现。关键方法:
writeToParcel():将对象数据写入ParcelcreateFromParcel():从Parcel重建对象
8.2 反射技巧
在逆向工程中,反射是突破访问限制的关键技术。常用操作:
java复制// 获取字段
Field f = obj.getClass().getDeclaredField("fieldName");
f.setAccessible(true);
// 获取方法
Method m = obj.getClass().getDeclaredMethod("methodName", paramTypes);
m.setAccessible(true);
// 调用方法
Object result = m.invoke(obj, args);
8.3 Xposed工作原理
Xposed框架通过替换/system/bin/app_process来实现方法级的Hook。核心机制:
- 在Zygote进程启动时加载XposedBridge
- 提供API让模块可以注册方法Hook
- 在方法调用前后插入自定义逻辑
9. 扩展思考
9.1 更安全的实现方式
如果我是App开发者,会考虑以下防护措施:
- 对核心数据使用Native层加密
- 增加完整性校验,防止数据篡改
- 使用代码混淆工具如ProGuard
- 检测Hook环境,发现后触发防御机制
9.2 其他应用场景
这种技术可以应用于:
- 分析竞品App的数据结构和业务逻辑
- 自动化测试时绕过某些限制
- 研究Android系统机制
- 开发调试辅助工具
9.3 法律与道德考量
必须强调:
- 仅用于授权测试或学习研究
- 不得用于破解付费功能或窃取用户数据
- 遵守相关法律法规和用户协议
- 尊重开发者的劳动成果