文章目录
Android 逆向入门
摘要
Android逆向工程是信息安全领域中一个重要的研究方向,结合静态分析、动态调试、代码重构等技术,广泛应用于安全审计、漏洞挖掘和应用程序修改等场景。本篇文章面向Android开发人员,主要介绍Android逆向的基本概念,如何使用常见的逆向工具(如反编译、调试器、动态分析工具等),并通过简单案例,演示Android应用的分析与修改方法,帮助参与者深入理解逆向工程的核心流程。
一. 什么是 Android 逆向工程?
Android 逆向开发是指对已发布的 Android 应用进行分析和破解,以了解应用程序的内部工作原理,获取应用程序的敏感信息,或者修改应用程序的行为。
二. Android 应用的组成与逆向分析对象
APK 文件结构
一个 APK 文件其实是一个 ZIP 压缩包,里面包含了多个重要的文件和目录,常见的结构如下所示:
APK
├── AndroidManifest.xml //核心配置文件,定义应用权限、组件、版本等信息。
├── classes.dex //主要包含 Java 编译后的字节码,由 Android 运行时(ART/Dalvik)执行
├── res/ //存放应用的静态资源,如图片、布局文件等。
│ ├── layout/ //UI布局文件
│ ├── drawable/ //图片资源
│ └── values/ //字符串配置
└── lib/ //存放本地库(如 C/C++ 编写的 `.so` 文件),按 CPU 架构分类。
├── arm64-v8a/ //ARM64架构so
└── armeabi-v7a/ //ARMv7架构so
-
重要分析目标
AndroidManifest.xml
该文件是 APK 的核心配置文件,定义了应用的基本信息和关键组件。分析时,我们通常关注以下内容: -
入口
Activity
配置:确定应用的启动方式、主要界面等。 -
权限声明:查看应用请求的权限,检查是否涉及隐私数据的访问,如位置、通讯录、相机权限等。
classes.dex
- classes.dex 文件包含了应用程序的可执行代码。它是应用的Dalvik字节码文件,也是Android应用在运行时通过 Dalvik虚拟机 或 ART(Android Runtime) 解释执行的核心文件。每个Android应用中,所有的Java源代码都经过编译后形成一个或多个DEX(Dalvik Executable)文件,这些文件包含了应用的业务逻辑和代码实现。
三. 专业术语
术语 | 解释 |
---|---|
DEX(Dalvik Executable) | Android 应用的编译后字节码文件。 |
Hook(钩子) | 运行时拦截并修改程序执行流程的技术。 |
Smali | Dalvik 字节码的反汇编表示,可直接编辑并重打包实现功能修改。 |
反编译 | 将二进制文件(DEX/SO)转换为可读的高级语言代码(如 Java/C)。 |
加壳 | 对 APK 加密或混淆,防止直接逆向(如 360 加固),是应用加固的一种手法。 |
脱壳 | 从内存中提取被加密的原始代码(如使用 Frida Dump)。 |
代码混淆 | 是通过修改代码的结构或表现形式,使其难以被逆向分析,同时保持功能不变的技术。 |
加固 | 广义的安全防护方案,包含代码混淆、反调试、加壳、反模拟器等多种手段。 |
Root | 获取 Android 设备超级权限,允许访问系统级文件。 |
动态分析 | 监控应用运行时的内存、网络及函数调用(如 Frida)。 |
静态分析 | 直接逆向代码文件,不运行程序(如 JADX/Ghidra)。 |
四. 混淆与反混淆
在软件开发中,代码混淆是将软件程序转换为难以理解的代码,但其功能与原始代码相同;而反混淆则是破解者试图还原原始逻辑的手段。
4.1 什么是代码混淆?
代码混淆(Code Obfuscation)是通过修改代码的结构或表现形式,使其难以被逆向分析,同时保持功能不变的技术。
混淆的目的:
• 防逆向:增加破解者理解代码逻辑的难度。
• 防篡改:阻止恶意修改或外挂注入。
• 防抄袭:隐藏核心算法或业务逻辑。
示例:
// 原始代码
public boolean checkPassword(String input) {
String secret = "admin123";
return input.equals(secret);
}
// 混淆后(逻辑未变,但可读性大幅降低)
public boolean a(String b) {
String c = new String(new byte[]{97, 100, 109, 105, 110, 49, 50, 51});
return b.equals(c);
}
4.2 常用的代码混淆技术
4.2.1 标识符重命名
• 原理:将类、方法、变量名替换为随机字符串(如 a
, b
, c
)。
• 工具:ProGuard(Android默认混淆工具)、R8(Android默认混淆工具,性能更好)、DexGuard。
• 效果:
// 混淆前
public class UserService {
private String userId;
}
// 混淆后
public class a {
private String b;
}
反混淆手段
上下文语义推断
• 技巧:
• 日志注入:若APK未关闭日志,通过Log.d("TAG", "userID: "+userId)
等日志内容推测变量含义。
• API调用链分析:根据方法参数和返回值类型推断用途。例如:
// 混淆后方法
public String a(String b, int c) { ... }
// 通过调用上下文推测
a("admin", 1); // 可能为login("admin", 1)
4.2.2 控制流平坦化
控制流:指程序中代码执行的顺序,如条件分支、循环和函数调用等结构,形成有逻辑层次的流程图。
平坦化:原始代码可能包含嵌套的条件判断或循环,形成多级层次。平坦化通过将代码拆分为多个基本块(Basic Block),消除这些嵌套结构。
• 原理:将代码逻辑拆分为多个基本块,通过状态机跳转执行。
• 工具:OLLVM(C/C++)、DexGuard(Java)、Tigress。
• 特点:大幅增加逆向分析复杂度,但可能降低性能(约20%~50%)。
代码示例(Java 层):
// 原始代码
public void login(String user, String pass) {
if (checkUser(user, pass)) {
showHomePage();
} else {
showError();
}
}
// 控制流平坦化后
public void login(String a, String b) {
int state = 0;
while (true) {
switch (state) {
case 0:
if (checkUser(a, b)) state = 1;
else state = 2;
break;
case 1:
showHomePage();
state = -1;
break;
case 2:
showError();
state = -1;
break;
case -1:
return;
}
}
}
反混淆手段
符号执行与动态调试
• 符号执行:符号执行:一种自动分析技术,用数学符号代替具体值,用数学方法逆向程序的真实行为,推导所有可能的执行路径。
• 工具:
• Angr:通过符号执行自动分析程序的执行路径。
4.2.3 字符串加密
• 原理:将字符串常量加密存储,运行时动态解密。
• 应用场景:保护API密钥、加密算法中的常量。
• 示例:
// 加密字符串
String key = decode("5e4f3d2c1b0a");
// 解密函数
private String decode(String hex) {
return new String(XOR(hexToBytes(hex), 0x55));
}
• 工具:
• Frida:Hook解密函数,直接捕获明文。
// Hook解密函数并打印输入输出
Interceptor.attach(Module.findExportByName(null, "decode"), {
onLeave: function (retval) {
console.log("Decrypted:", retval.readUtf8String());
}
});
4.2.4 资源混淆
• 原理:重命名或加密图片、XML等资源文件。
• 工具:AndResGuard(腾讯开源工具)。
• 效果:防止资源文件被直接提取或篡改。
示例:
<!-- 混淆前 -->
<resources>
<string name="app_name">MyApp</string>
<drawable name="icon">@drawable/ic_launcher</drawable>
</resources>
<!-- 混淆后 -->
<resources>
<string name="a">MyApp</string>
<drawable name="b">@drawable/c</drawable>
</resources>
• 图片文件 ic_launcher.png
→ 重命名为 a.png
。
• 布局文件 activity_main.xml
→ 重命名为 d.xml
。
反混淆手段
资源表恢复
• 工具:
• Apktool:反编译APK后,资源ID与混淆名称映射关系存储在res/values/public.xml
。
• AndroidResEdit:直接编辑资源文件并关联原始名称。
4.2.5 虚拟化保护(VMP)
通过构造一个自定义的虚拟机,来执行关键代码,让攻击者无法直接理解代码逻辑。逆向难度最高。
• 原理:
- 代码转换:
• 将原始机器码(如 x86/ARM 指令)转换为自定义的虚拟指令集(类似 Java 字节码) 。
可以简单回顾一下JVM的执行流程:
- Java源码(.java) → 编译 → 字节码(.class)
- JVM 读取 字节码,并 解释执行
- 程序运行
VMP的执行原理
VMP的核心思路和JVM类似,但是它不是用来执行Java字节码,而是执行自己特制的“虚拟指令”:
- 原始代码转换成自定义的字节码格式
- 关键代码不会直接以原生指令形式存在,而是被转换成虚拟指令。
- 这些虚拟指令和CPU的指令集不同,不是标准的ARM/Thumb指令,而是VMP自己定义的指令格式。
- 在VMP虚拟机中解释执行
- 应用运行时,VMP不会直接执行原始代码,而是让VMP解释器去解析执行转换后的“虚拟指令”。
- 由于这种虚拟指令是自定义的,逆向工程师即使看到它,也很难理解真实逻辑。
- 执行结果返回给正常应用流程
- VMP最终还是需要和正常的Java/Kotlin代码交互,所以会有一个“桥接”过程,确保代码的输入输出正确。
普通代码执行
Java/Kotlin → 编译 → JVM字节码→ JVM → 机器码 → CPU运行 |
---|
VMP代码执行:
Java/Kotlin 关键代码→ VMP转换工具 编译 → VMP字节码(.vmp) → VMP虚拟机 → 机器码 → CPU运行 |
---|
• 示例:
; 原始指令
mov eax, 1 ; 将数字1存入寄存器eax
mov ebx, 2 ; 将数字2存入寄存器ebx
add eax, ebx ; 执行加法:eax = eax + ebx → eax=3
; 虚拟指令集
OP_LOAD_CONST 1 → R0 ; 将常量1加载到虚拟寄存器R0
OP_LOAD_CONST 2 → R1 ; 将常量2加载到虚拟寄存器R1
OP_ADD R0, R1 → R2 ; 将R0和R1相加,结果存入R2(R2=3)
代码示例(C 伪代码):
// 虚拟机解释器核心逻辑
void VM_Interpreter(byte* code) {
while (true) {
uint8_t opcode = *code++;
switch (opcode) {
case OP_LOAD_CONST:
uint32_t value = *(uint32_t*)code;
code += 4;
push_stack(value);
break;
case OP_ADD:
int a = pop_stack();
int b = pop_stack();
push_stack(a + b);
break;
// ... 其他指令处理
}
}
}
// 原始逻辑
int add(int a, int b) {
return a + b;
}
// 虚拟化后的代码
void vm_add() {
// 虚拟指令:OP_LOAD_ARG0, OP_LOAD_ARG1, OP_ADD, OP_RETURN
byte code[] = {0x01, 0x02, 0x03, 0x04};
VM_Interpreter(code);
}
反混淆手段
动态跟踪:运行时通过调试器(如GDB/Frida)监视虚拟机的指令执行流,记录关键操作(如运算、内存读写)。在翻译(虚拟机解释器)工作时,用调试工具(如Frida)监视它如何将暗号逐条翻译成正常语句。
总结对比
技术 | 逆向难度 | 性能损耗 | 场景 |
---|---|---|---|
标识符重命名 | 低 | 无 | 基础保护,快速部署 |
控制流平坦化 | 高 | 中 | 对抗静态分析 |
字符串加密 | 中 | 低 | 敏感数据保护 |
资源混淆 | 低 | 无 | 防止资源盗用 |
虚拟化保护(VMP) | 极高 | 高 | 核心算法、高安全需求 |
五. 逆向工具介绍
在进行 Android 逆向工程时,工具的选择至关重要。不同的工具适用于不同的分析场景,有的侧重于 Java 层的代码反编译,有的用于资源文件解析,有的则专注于 Native 层的分析。以下是一些常见的 Android 逆向分析工具,它们在实际工作中能极大提高效率,让我们可以更高效地理解、修改和调试 APK。
5.1 静态分析工具
//这里需要修改 优化措辞
工具名称 | 核心功能 | 适用场景 | 优势 |
---|---|---|---|
JADX | 将 DEX/APK 直接反编译为 Java 代码 | 快速查看 Java 层逻辑、查找敏感代码路径 | 支持交叉引用、代码搜索、导出 Gradle 项目 |
APKTool | 解包 APK 到 Smali 代码和资源文件 | 修改资源文件(如布局/字符串)、Smali 代码注入 | 支持回编译、签名绕过 |
Ghidra | NSA 开源的逆向工程框架,支持反编译和二进制分析 | 分析 so 文件、逆向 Native 层算法 | 跨平台、支持插件开发、反编译质量高 |
5.2 动态调试工具
工具名称 | 核心功能 | 适用场景 | 优势 |
---|---|---|---|
Frida | 通过 JavaScript 注入实现运行时 Hook | 动态修改函数参数/返回值、内存 Dump、脱壳 | 跨平台、支持 Android/iOS/Windows |
Xposed | 通过模块 Hook Android 系统 API | 修改系统行为(如绕过 SSL Pinning) | 稳定性高、社区模块丰富 |
5.3 网络协议分析工具
工具名称 | 核心功能 | 适用场景 | 优势 |
---|---|---|---|
Charles | 拦截 HTTPS 流量、重放请求 | 分析 API 接口、逆向通信协议 | 可视化界面、支持断点调试 |
Wireshark | 抓取原始网络数据包 | 分析 TCP/UDP 层协议、定位加密漏洞 | 支持深度数据包解析、过滤器语法强大 |
一般用于抓包抓到 API 请求后,根据一些请求参数,在反编译代码中搜索所在位置。
5.4 Native 层逆向工具
工具名称 | 核心功能 | 适用场景 | 优势 |
---|---|---|---|
IDA Pro | 工业级反汇编工具 | 逆向 so 文件、分析 ARM/ARM64 汇编代码 | 支持交叉引用、结构体重建、Hex-Rays 反编译器 |
Binary Ninja | 新一代反编译平台 | 分析混淆后的二进制文件 | 交互式图形界面、支持多种架构 |
五. 静态分析和动态分析
六 逆向调试技巧
6.1 jadx-gui的使用方法
首先, 我们这里使用jadx(反编译) + Android Studio(分析代码)
JADX下载地址:https://github.com/skylot/jadx/releases/tag/v1.4.7
从JADX内打开APK,从界面的左侧可以发现,反编译后的Java源代码以一个个包的形式组织在一起,另外还有资源文件,其中包括图片文件、布局文件和AndroidManifest.xml文件(内含apk文件的基本信息)等。在左侧展开想要查看的包,右侧就会出现对应的Java源代码,如下图所示:
apk signature内可以看到签名信息:
我们也可以把反编译后的文件另存为Gradle项目,Gradle项目就是开发版本的Andriod项目,如下图所示
(jadx中查找和搜索,查看方法调用链相应很慢,不好用)
为了方便调试,选择反混淆+另存为gradle项目,使用android studio打开:
Jadx 的反混淆功能主要针对 标识符重命名。
反混淆前:
反混淆后:
如图,反混淆后,原本的a()方法变为了m10203a(),更容易找到调用关系。在Android Studio中,你可以浏览和分析反编译后的代码,搜索特定的类、方法或变量。
打开xml布局文件:
6.2 hook系统日志类
在逆向分析一个项目时,可以先hook反编译项目的Log类,根据Log打印,后续可以方便我们找到有用的信息
在目标项目内,全局搜索 “Log”关键字,可找到目标项目的日志类为“com.cosmos.mdlog.MDLog”
使用Xposed框架,hook“com.cosmos.mdlog.MDLog”
com.cosmos.mdlog.MDLog的d方法:
/* renamed from: d */
public static void m167892d(String str, String str2) {
m167891d(str, str2, null);
}
Class<?> classMDLog = XposedHelpers.findClass(
"com.cosmos.mdlog.MDLog", application.getClassLoader());
XposedHelpers.findAndHookMethod(
classMDLog,//要hook的程序的包名类名
"d",//被Hook函数的名称
String.class, //被Hook函数的第一个参数String
String.class, //被Hook函数的第二个参数String
Object[].class,//被Hook函数的第三个参数Object
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
//Hook函数之前执行的代码
super.beforeHookedMethod(param);
//方法的第一个参数信息(TAG)
String arg0 = (String) param.args[0];
//方法的第二个参数信息(日志内容)
String arg1 = (String) param.args[1];
Object[] arg2 = (Object[]) param.args[2];
if (arg2 != null) {
arg1 = String.format(arg1, arg2);
}
LogUtil.d(TAG, "hookMDLog.d(): [" + arg0 + "]" + arg1);
}
new Exception("打印调用栈1").printStackTrace();
for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
Log.d("打印调用栈2", " " + element.toString());
}
//如上述代码,通过打印调用栈,获取到源代码中代码执行顺序
}
);
如上述代码,通过hook项目的com.cosmos.mdlog.MDLog类,可以打印出原本被关闭的log日志,便于我们从日志中抓取关键信息。
6.3 hook上下层方法的选择
在 Android 开发中,应用逻辑通常由两类方法组成:
- 上层方法:指开发者在 Java 代码中定义的业务逻辑、数据处理、界面交互等。例如,
LoginManager.validateUser()
这样的自定义方法通常是应用业务逻辑的一部分。 - 下层方法:指 Android 框架提供的原生方法,比如
TextView.setText()
、View.onClick()
、SharedPreferences.getString()
、Toast.makeText()
等,这些方法由 Android 系统提供,并直接调用系统服务。
在逆向分析时,我们通常希望 Hook、修改、调用某些方法,以控制应用行为或绕过某些限制。因此,我们需要决定是操作用户自定义的类,还是直接使用系统 API。
Hook 上层方法(用户自定义类/业务逻辑)
◾ 优点
- 易于理解:上层方法通常更接近应用的业务逻辑,易于理解其功能和目的。
- 准确定位:可以精确修改要hook的逻辑。
- 低依赖:较少受系统底层变更影响。
◾ 缺点
-
版本敏感:应用更新时,上层接口和逻辑可能变更,导致hook失效或应用崩溃。
-
维护成本高:需要持续监控应用更新并调整hook代码,维护成本相对较高。
// 示例:文字解密逻辑调用上层系统API XposedHelpers.findAndHookMethod("com.ixxxx.xxxx.service.bean.Message", application.getClassLoader(), "setContent", Object.class, new XC_MethodHook() { @Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable { super.beforeHookedMethod(param); Object mText = param.args[0]; CharSequence charSequence = decryptStr(mText); param.args[0] = charSequence.toString(); } } );
──────
Hook 下层方法(系统 API/底层框架)
◾ 优点
- 通用性强:下层方法通常更通用,可能被多个上层方法或不同应用部分共享。
- 版本适配性好:应用更新时,hook的功能基本不受影响。
◾ 缺点 - 技术门槛:需熟悉Android系统方法调用
- 兼容风险:系统更新可能破坏 Hook 逻辑
// 示例:文字解密逻辑调用下层系统API
XposedHelpers.findAndHookMethod(TextView.class,
"setText", CharSequence.class, TextView.BufferType.class, boolean.class, int.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
CharSequence arg0 = (CharSequence) param.args[0];
CharSequence charSequence = decryptStr(arg0.toString());
param.args[0] = charSequence.toString();
}
});
// 示例:通过AudioRecord的read 获取麦克风声音数据
XposedHelpers.findAndHookMethod(AudioRecord.class,
"read", ByteBuffer.class, int.class, int.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
try {
ByteBuffer byteBuffer = (ByteBuffer) param.args[0];
int sizeInBytes = (int) param.args[1];
int mode = (int) param.args[2];
int len = byteBuffer.limit() - byteBuffer.position();
} catch (Exception e) {
e.printStackTrace();
}
}
});
//通过AudioRecord的write 获取扬声器声音数据
XposedHelpers.findAndHookMethod(AudioTrack.class,
"write", byte[].class, int.class, int.class, int.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
try {
byte[] bytes = (byte[]) param.args[0];
int start = (int) param.args[1];
int size = (int) param.args[2];
int mode = (int) param.args[3];
} catch (Exception e) {
e.printStackTrace();
}
}
});
七 逆向分析项目实战
环境:某android沙箱,沙箱内无需root即可使用Xponsed
应用:某未加固即时通讯应用
为保护隐私,已隐藏应用真实包名
7.1 消息撤回时间修改
- 需求:原APP撤回时间超出2分钟无法撤回,取消原app的撤回时间限制。
使用应用超过两分钟撤回后,提示:
使用特征字符串搜索,在反编译代码中全局搜索“发送超过2分钟的消息,无法被撤回”字符串,发现如下代码:
/* JADX INFO: Access modifiers changed from: protected */
@Override
/* renamed from: a */
public String executeTask(Object... objArr) throws Exception {
int m59562a = C41170d.m59562a(this.f100583a, this.f100584b);
if (m59562a != 200) {
return m59562a == 401 ? "发送超过2分钟的消息,无法被撤回" : "消息撤回失败";
}
this.f100583a.contentType = 5;
this.f100583a.setContent("你撤回了一条消息");
C43645f.m48197a().m48158d(this.f100583a);
m76187a(this.f100584b, this.f100583a);
C32682b.m90306b().m90304b(this.f100583a);
return "";
}
点进C41170d.m59562a方法
/* renamed from: a */
public static int m59562a(Message message, int i) throws Exception {
long time = message.getTimestamp() != null ? message.getTimestamp().getTime() : message.localTime;
IMJPacket iMJPacket = new IMJPacket();
iMJPacket.put(NotificationStyle.NOTIFICATION_STYLE, "withdraw");
iMJPacket.setAction("get");
iMJPacket.put(IMRoomMessageKeys.Key_MessageId, message.msgId);
iMJPacket.put("time", time);
if (i == 1) {
iMJPacket.put("type", "msg");
iMJPacket.put(RemoteMessageConst.f13594TO, message.remoteId);
} else {
if (i != 2) {
if (i == 3) {
iMJPacket.put("type", "dmsg");
iMJPacket.put(RemoteMessageConst.f13594TO, message.discussId);
} else if (i != 6) {
if (i == 8) {
iMJPacket.put("type", "mmsg");
iMJPacket.put(RemoteMessageConst.f13594TO, message.remoteId);
}
}
}
iMJPacket.put(RemoteMessageConst.f13594TO, message.groupId);
iMJPacket.put("type", "gmsg");
}
return ((IMJRouter) AppAsm.m10901a(IMJRouter.class)).mo59538a(iMJPacket).optInt(ALPParamConstant.RESULT_CODE);
}
发现:
- 客户端本地计算消息存活时间
- 服务端二次校验时间有效性
- 时间戳取自消息对象的getTimestamp()方法
可以选择hook message.getTimestamp() 伪造消息时间来实现目标:
public Date getTimestamp() {
return this.timestamp;
}
XposedHelpers.findAndHookMethod(
"com.xxxx.xxxx.service.bean.Message",
application.getClassLoader(),
"getTimestamp", // 方法名
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
// 获取原始返回值
Date date= (Date) param.getResult();
KLog.a("findAndHookMethod", date);
Date date = new Date();
//将消息改为当前时间
param.setResult(date);
KLog.a("findAndHookMethod", param.getResult());
}
}
);
再次超时点击撤回时,成功撤回消息。
7.2 对原APP增加选择文件功能
-
需求:原APP只有图片选择功能,对原APP增加选择文件功能
-
获取控件ID
使用\SDK\tools\bin\uiautomatorviewer.bat工具,用于捕获当前显示的Android设备或模拟器屏幕。可以查看目标应用的布局,包括各个UI元素的ID、类型和层次结构。通过uiautomatorviewer获取到的UI元素ID,可以在Android Studio中进行搜索,找到对应的代码实现或资源定义获取到目标app当前页面的id名称。根据获取到的名称,在as内进行搜索调试。
如图,根据message_btn_gif_search,在反编译代码内全文搜索,发现"GIF"按钮的ID为 2131304532
public void onClick(View view) {
String m67662bb = m67662bb();
String str = "gift";
switch (view.getId()) {
case R.id.message_btn_gif_search /* 2131304532 */:
m67656bj();
if (!TextUtils.isEmpty(m67662bb)) {
m67747a("group_gif", false);
}
m67791V();
return;
}
- 获取activity引用
使用文件选择器,要先获取上下文对象,使用上下文对象要先获取当前activity
使用adb命令 获取当前activity
adb shell dumpsys activity|findstr ResumedActivity
可知当前Activity为:“com.xxxx.xxxxx.mvp.message.view.BaseMessageActivity”
获取当前activity的context对象:
Class<?> targetClass1 = XposedHelpers.findClass("com.xxxx.xxxx.mvp.message.view.BaseMessageActivity", application.getClassLoader());
XposedHelpers.findAndHookMethod(
targetClass1,
"onResume",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
activity[0] = (Activity) param.thisObject;
}
}
);
- Hook点击事件
重写gif按钮的onClick方法,实现自己的逻辑:
// Hook 点击事件,替换为文件选择器
XposedHelpers.findAndHookMethod("com.ixxxx.xxxx.mvp.message.view.BaseMessageActivity", application.getClassLoader(),
"onClick", View.class,
new XC_MethodHook() {
@SuppressLint("ResourceType")
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
View view = (View) param.args[0];
//点击了gif
if (view.getId()==2131304532){
//打开文件选择器
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
activity[0].startActivityForResult(intent, FILE_REQUEST_CODE, null);
}
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
}
});
- 处理文件选择结果
选择文件完毕后,在onActivityResult内处理文件:
XposedHelpers.findAndHookMethod(
"com.ixxxx.framework.base.BaseActivity",
application.getClassLoader(),
"onActivityResult",
int.class,
int.class,
Intent.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
int requestCode = (Integer) param.args[0];
int resultCode = (Integer) param.args[1];
Intent data = (Intent) param.args[2];
// 判断是否是你启动的文件选择器
if (requestCode == FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
Uri selectedUri = data.getData();
// 你可以在这里进一步处理选中的文件
...............
}
}
});
7.3 对IM消息中的图片增加加解密
- 需求:在原有IM图片收发功能基础上,增加加密传输功能
操作流程:
app自带相册内选择图片→ intent传回activity → 触发上传 |
---|
选择图片完毕后,会使用intent传递消息,通过分析可知原APP使用getParcelableArrayListExtra
传递消息。接收getParcelableArrayListExtra
消息需要参数Key,通过动态分析,可知应用内传递图片的KEY为“EXTRA_KEY_IMAGE_DATA”
反编译代码中的Key:
/* loaded from: classes16.dex */
public class AuthorPhoneLiveHelper extends AbsPhoneLiveHelper implements IFaceEffectAble {
public static final String EXTRA_KEY_IMAGE_DATA = "EXTRA_KEY_IMAGE_DATA";
public static final String EXTRA_KEY_MEDIA_TYPE = "EXTRA_KEY_MEDIA_TYPE";
public static final String EXTRA_KEY_VIDEO_DATA = "EXTRA_KEY_VIDEO_DATA";
private static final int MAX_WAIT_COUNT = 99;
public static final String MEDIA_TYPE_IMAGES = "IMAGE";
private static final String SPECIAL_DEVICE = "MX4 Pro";
private final MLBeautyPanelSubscriber MLBeautyPanelSubscriber;
接收视频的key也在这里,后续的视频加密也可以直接用到。
通过对反编译代码静态分析,可以找到intent传递的对象为Photo:
/* JADX INFO: Access modifiers changed from: private */
/* renamed from: d */
public void m90951d(Intent intent) {
this.f87601a = 2;
if (this.f87609h.getVisibility() == 8) {
C8976j.m157229a((Activity) thisActivity());
m90941g();
}
ArrayList<Photo> parcelableArrayListExtra = intent.getParcelableArrayListExtra(AuthorPhoneLiveHelper.EXTRA_KEY_IMAGE_DATA);
if (parcelableArrayListExtra == null || parcelableArrayListExtra.isEmpty()) {
return;
}
ArrayList arrayList = new ArrayList(parcelableArrayListExtra.size());
for (Photo photo : parcelableArrayListExtra) {
arrayList.add(photo.tempPath);
}
m90970a(arrayList, (List<String>) null);
}
点进Photo中,查看Photo属性:
/* loaded from: classes6.dex */
public class Photo implements Parcelable {
public static final Parcelable.Creator<Photo> CREATOR = new Parcelable.Creator<Photo>() { // from class: com.xxxxx.xxxxx.multpic.entity.Photo.1
@Override // android.os.Parcelable.Creator
/* renamed from: a */
public Photo createFromParcel(Parcel parcel) {
return new Photo(parcel);
}
@Override // android.os.Parcelable.Creator
/* renamed from: a */
public Photo[] newArray(int i) {
return new Photo[i];
}
};
/* renamed from: a */
public boolean f106036a;
@Expose
public String activityExt;
@Expose
public String bucketId;
@Expose
public String bucketName;
@Expose
public String category;
@Expose
public HashMap<String, String> categoryParams;
@Expose
public long dateAdded;
@Expose
public long duration;
@Expose
public String editExtra;
@Expose
public String faceDetect;
@Expose
public boolean fromNet;
@Expose
public String guid;
@Expose
public int height;
@Expose
/* renamed from: id */
public long f106037id;
@Expose
public boolean isAlbumCheck;
@Expose
public boolean isCheck;
@Expose
public boolean isLong;
@Expose
public boolean isOriginal;
@Expose
public boolean isPictureCheck;
@Expose
public boolean isTakePhoto;
@Expose
public String longThumbPath;
@Expose
public String mimeType;
@Expose
public String path;
@Expose
public int positionInAll;
@Expose
public int positionInSelect;
@Expose
public int rotate;
@Expose
public String shootExra;
@Expose
public long size;
@Expose
public String tempPath;
@Expose
public String thumbPath;
@Expose
public int type;
@Expose
public int width;
可以找到Photo类的一些关键属性
属性名 | 类型 | 作用 |
---|---|---|
path | String | 原始图片文件路径 |
isOriginal | boolean | 是否原图 |
thumbPath | String | 缩略图路径 |
现在对Intent的getParcelableArrayListExtra
方法进行hook,修改Photo类中的path属性,使加密后的图片路径替换原有的path
XposedHelpers.findAndHookMethod(Intent.class,
"getParcelableArrayListExtra", String.class, //ArrayList.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
String arg0 = (String) param.args[0];
//仅处理图片
if (!KEY_SEND_SELECT_IMAGES.equals(arg0)) {
return;
}
try {
ArrayList<Object> resultFileList = (ArrayList<Object>) param.getResult();
for (int i = 0; i < resultFileList.size(); i++) {
Object photo = resultFileList.get(i);
String srcPath = (String) XposedHelpers.getObjectField(photo, "path");
String tempPath = (String) XposedHelpers.getObjectField(photo, "tempPath");
String codePath;
//文件加密代码
if (msgFilePath != null) {
codePath = encode(isTakePhotoReal, isTakePhotoReal ? tempPath : srcPath,true);
}
//加密图片代替原path
XposedHelpers.setObjectField(photo, "path", codePath);
XposedHelpers.setObjectField(photo, "isOriginal", true);
XposedHelpers.setObjectField(photo, "tempPath", codePath);
}
} catch (Throwable e) {
e.printStackTrace();
}
}
});
拦截到图片,完成对图片的加密。
解密时,图片被展示在会话列表中,图片使用Glide加载,需要hook Glide类,在图片被加载到Glide之前进行图片解密。
通过查看Glide源码,找到处理File的位置:
package com.bumptech.glide.load.model;
/** Loads {@link java.nio.ByteBuffer}s using NIO for {@link java.io.File}. */
public class ByteBufferFileLoader implements ModelLoader<File, ByteBuffer> {
private static final String TAG = "ByteBufferFileLoader";
........
private static final class ByteBufferFetcher implements DataFetcher<ByteBuffer> {
private final File file;
........
@Override
public void loadData(
@NonNull Priority priority, @NonNull DataCallback<? super ByteBuffer> callback) {
ByteBuffer result;
try {
result = ByteBufferUtil.fromFile(file);
callback.onDataReady(result);
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to obtain ByteBuffer for file", e);
}
callback.onLoadFailed(e);
}
}
........
}
}
hook fromfile方法,获取到文件路径:
Class classByteBufferUtil = XposedHelpers.findClass("com.bumptech.glide.util.ByteBufferUtil", application.getClassLoader());
XposedHelpers.findAndHookMethod(classByteBufferUtil,
"fromFile", File.class,
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
File arg = (File) param.args[0];
if (filterFileExtendReceive(arg)) {
String path = null;
path = decode(arg.getAbsolutePath(), true, false);
param.args[0] = new File(path);
}
}
}
);
完成im聊天图片的加解密。