Android逆向入门

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(钩子)运行时拦截并修改程序执行流程的技术。
SmaliDalvik 字节码的反汇编表示,可直接编辑并重打包实现功能修改。
反编译将二进制文件(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)

通过构造一个自定义的虚拟机,来执行关键代码,让攻击者无法直接理解代码逻辑。逆向难度最高。
原理

  1. 代码转换
    • 将原始机器码(如 x86/ARM 指令)转换为自定义的虚拟指令集(类似 Java 字节码) 。

可以简单回顾一下JVM的执行流程:

  1. Java源码(.java) → 编译字节码(.class)
  2. JVM 读取 字节码,并 解释执行
  3. 程序运行

VMP的执行原理
VMP的核心思路和JVM类似,但是它不是用来执行Java字节码,而是执行自己特制的“虚拟指令”

  1. 原始代码转换成自定义的字节码格式
  • 关键代码不会直接以原生指令形式存在,而是被转换成虚拟指令
  • 这些虚拟指令和CPU的指令集不同,不是标准的ARM/Thumb指令,而是VMP自己定义的指令格式。
  1. 在VMP虚拟机中解释执行
  • 应用运行时,VMP不会直接执行原始代码,而是让VMP解释器去解析执行转换后的“虚拟指令”。
  • 由于这种虚拟指令是自定义的,逆向工程师即使看到它,也很难理解真实逻辑。
  1. 执行结果返回给正常应用流程
  • 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 代码注入支持回编译、签名绕过
GhidraNSA 开源的逆向工程框架,支持反编译和二进制分析分析 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新一代反编译平台分析混淆后的二进制文件交互式图形界面、支持多种架构

五. 静态分析和动态分析

目标APK
分析方式
静态分析
动态调试
Java代码分析
资源文件解析
Frida Hook
Xposed模块
关键逻辑定位
布局/字符串修改
修改参数
修改参数

六 逆向调试技巧

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类的一些关键属性

属性名类型作用
pathString原始图片文件路径
isOriginalboolean是否原图
thumbPathString缩略图路径

现在对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聊天图片的加解密。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值