Android 逆向/反编译/Hook修改应用行为 基础实现

前言:本文通过一个简单的情景案例实现安卓逆向的基本操作

一、情景描述

本文通过一个简单的情景案例来实现安卓逆向的基本操作。在这个案例中所使用的项目程序是我自己的Demo程序,不会造成任何的财产侵害,本文仅作为日常记录及案例分享。
实现步骤大致如下
反编译APK获取源码 → 源码分析 → 目标设备运行Frida服务(需要root权限) → 使用Python编写hook注入程序 / 编写hook脚本 → 执行程序

二、前置准备

整个流程中需要做的准备工作大致有以下几点:

1. 反编译工具

当前流行的反编译工具有许多种,例如:APKTool、JADX、JD-GUI 等等。反编译的主要目的是查看app的源码分析app的运作流程从而快速制定出所需的hook脚本,因此挑选一款适合自己的即可。不过需要注意的是:并非所有的apk包都能够反编译成功,虽然技术上大多数 APK 文件都能被反编译,但反编译的难度和完整性可能会因应用的保护措施而有所不同。具体来说,有些 APK 文件通过加固、混淆、加密等手段提高了反编译的难度,甚至可能在某些情况下完全避免了反编译的效果,由于我的案例使用的是自己的源码所以不需要考虑这一步,那就以 JADX 作为案例吧。

安装JADX

直接到github仓库下载打好的包即可,可以直接下载带UI的版本,简单易用,不过要注意系统已经有了JRE环境,如果没有的话也可以下载下方的带jre版本 https://github.com/skylot/jadx/releases

反编译APK包

直接把文件拖拽到框内即可

2. Python环境

需要用到 Python 进行脚本程序编写及运行
Python 环境安装:直接官网下载傻瓜式一键安装即可 https://www.python.org/downloads/
PyCharm 编辑器安装:同上,下载社区版(Community)就够了 https://www.jetbrains.com.cn/en-us/pycharm/download/?section=windows

3. 安装 Frida

Frida 有两部分,frida-tools以python程序为载体运行在本地并执行hook脚本,frida-server则需要在目标设备端运行并进行动态分析和hook操作
安装 frida-tools:安装frida-tools需要用到pip因此先确保python环境安装成功并设置好环境变量。在命令提示符中直接输入以下指令等待安装完成即可:pip install frida-tools
下载完成后输入指令判断是否安装成功:frida --version
如果成功返回了版本号则表示安装成功了,记住这个版本号后续还要用到

下载 frida-server:直接到github仓库下载打好的包即可 https://github.com/frida/frida/releases
注意下载的版本要frida-tools相匹配,另外还需要注意的是server的系统平台和cpu架构版本也要选对,他的命名规则是:frida-server-版本号-支持系统-CPU架构.扩展名,我需要在用到Android系统上使用因此我把对应的所有android server包都下载下来

4. 具有 root 权限的设备

由于现在的厂商真机获取 root 权限越来越困难,使用真机调试的成本较高因此选择模拟器作为android端设备载体,我在案例中使用的是夜神模拟器,直接官网安装即可 https://www.yeshen.com/ 安装完成后在设置中开启root权限即可

三、运行 hook 程序,修改应用行为

1. 启动 frida-server

启动frida-server前要先将server文件导入目标设备的 /data/local/tmp/ 目录中,执行命令修改可执行权限并运行,这里需要注意的一点是要确保目标设备的cpu架构与server文件相对应,夜神模拟器的cpu架构是x86_64因此我们选用这个即可

解压server文件

选择之前下载好的server压缩包文件并解压

导入/启动 server 文件

直接在路径栏中输入"cmd"回车进入当前命令提示符

依次执行以下指令:
输入 "adb devices" 指令查询当前已连接设备确定模拟器设备已经连接成功
输入 "adb push frida-server-16.5.7-android-x86_64 /data/local/tmp/" 指令将 frida-server 导入tmp目录,注意这里的要根据你的server文件名修改一下
等待导入完成,输入 "adb shell" 进入设备指令操作
输入 "su" 切换root权限
输入 "cd /data/local/tmp/" 进入tmp目录
输入 "ls -l" 查询当前目录下的文件信息,此时我们可以看到刚才导入进去的server文件,他的特征为rw-并没有可执行权限
输入 "chmod 777 frida-server-16.5.7-android-x86_64" 设置用户权限开启可执行
再次输入 "ls -l" 查询当前目录下的文件信息,此时我们可以看到文件特征变为rwx,可以执行
输入 "./frida-server-16.5.7-android-x86_64" 启动 frida-server,启动后光标换行没有任何提升,表示已经启动成功 server 正在运行中,注意不要关闭提示符窗口

2. 验证 frida 连接

打开 PyCharm,新建py文件,导入frida包,根据app包名启动指定进程,这一步先做连接验证,不注入hook脚本

import frida
import sysdef on_message(message, data):"""回调函数,处理 Frida 发回的消息。"""if message['type'] == 'send':print(f"[+] {message['payload']}")elif message['type'] == 'error':print(f"[!] {message['stack']}")def main():print("执行任务")try:print(f"[!] {frida.__version__}")# 连接到 Android 设备device = frida.get_usb_device(timeout=5)# 查找目标进程pid = device.spawn(["com.lxt.single_module"]) # 注意,这里要改为你需要hook的程序包名session = device.attach(pid)# 加载 Hook 脚本# script = session.create_script(HOOK_SCRIPT_1)# script.on('message', on_message)  # 设置消息回调# script.load()# 恢复进程device.resume(pid)print("[*] Hook 脚本已加载,应用正在运行...")# 保持脚本运行状态sys.stdin.read()except Exception as e:print(f"[!] 错误: {e}")if __name__ == "__main__":main()

运行程序观察日志及模拟器,程序运行之后模拟器会自动调起对应的程序,如果日志中没有报错或退出则表示连接成功

3. hook 改变方法执行效果

改变一个简单的方法返回数据

假设我在 MainActivity 中有个简单的方法,该方法固定返回一个boolean类型数据"true"

fun testBool():Boolean{return true}

设置某个测试按键的点击事件为log这个方法返回的结果

viewBinding.test5.onClick {Log.e(TAG, "click result:${testBool()}")
}

点击结果

那么这时我们可以编写一个简单的hook脚本去注入修改这个方法的返回数据

创建Java.perform方法并在作用域中实现需要执行的脚本细节,Java.perform 是 Frida 提供的一个用于在 Java 虚拟机中执行代码的方法。它确保在 Java 环境完全加载并且可以安全地访问 Java 类和方法后执行给定的回调函数。所有的 Java hooking 操作都需要在这个回调内进行

Java.perform(function() {}

我们需要hook的方法是在MainActivity中,因此我们要做的第一个操作就是获取到这个对象的引用,我们可以使用Java.use()方法来获取。通过Java.use()拿到的引用你可以访问该类的方法和字段或者修改这些方法的实现,但前提是必须要知道这个需要加载的类的完整路径,这个路径我们可以通过反编译的源码中获取

var MainActivity = Java.use("com.lxt.single_module.Activity.MainActivity");

拿到 MainActivity 中 testBool 方法的引用,强制其返回一个"false"

MainActivity.testBool.implementation = function() {console.log("testBool 被 Hook,强制返回 false");// 强制返回 falsereturn false;
};

完整代码

import frida
import sysHOOK_SCRIPT_1 = """
Java.perform(function() {var MainActivity = Java.use("com.lxt.single_module.Activity.MainActivity");// Hook MainActivity 中的 testBool 方法MainActivity.testBool.implementation = function() {console.log("testBool 被 Hook,强制返回 false");// 强制返回 falsereturn false;};});
"""# Python 代码
def on_message(message, data):"""回调函数,处理 Frida 发回的消息。"""if message['type'] == 'send':print(f"[+] {message['payload']}")elif message['type'] == 'error':print(f"[!] {message['stack']}")def main():print("执行任务1")try:print(f"[!] {frida.__version__}")# 连接到 Android 设备device = frida.get_usb_device(timeout=5)# 查找目标进程pid = device.spawn(["com.lxt.single_module"])session = device.attach(pid)# 加载 Hook 脚本script = session.create_script(HOOK_SCRIPT_1)script.on('message', on_message)  # 设置消息回调script.load()# 恢复进程device.resume(pid)print("[*] Hook 脚本已加载,应用正在运行...")# 保持脚本运行状态sys.stdin.read()except Exception as e:print(f"[!] 错误: {e}")if __name__ == "__main__":main()

执行程序注入hook脚本,此时再次点击测试按键就会触发hook方法
观察日志hook方法被触发,app中的testBool方法返回结果为false


改变应用行为

这里我们用一个相对较接近实际应用的情景作为案例吧
假设:我们需要在MainActivity中请求某个api再根据api的结果决定是否需要跳转到另一个指定页面
如果:我们需要绕过这个api,让程序无需根据api的请求结果而必定跳转到这个指定的页面
那么:我们可以直接hook这个请求api的方法让它必定返回一个可以让页面跳转的结果

代码情景
点击测试按键后使用ViewModel调取请求方法,再根据请求方法返回的结果判断是否需要执行页面跳转

viewBinding.test4.onClick {lifecycleScope.launch {var login_result = viewModel.testLogin()Log.e(TAG,"request result:${login_result}")if(login_result){Log.e(TAG, "登陆成功")startActivity(Intent(this@MainActivity, SucceedActivity::class.java))}else{Log.e(TAG, "登陆失败")}}
}

正常执行结果

根据方法的执行流程去编写 hook 脚本

在这个流程中最为关键的一步就是需要根据ViewModel请求方法的结果判断是否需要进行页面跳转,那么我们只要在ViewModel对象初始化之后拿到它的引用再hook它的请求方法,让这个方法的返回结果必定为true就好了
在我的Activity框架中ViewModel是在名为"initData"的抽象方法中初始化,并且在子类实现时必须要调用super.initData()在基类中执行反射自动初始化

override fun initData() {super.initData()  // 在父类初始化 viewModel// 执行其它任务rxPermissions = RxPermissions(this)check_permission()registerScreenListen()
}

由此可知:我们只需要获取到MainActivity的initData方法引用,并在执行它原本的super.initData()方法之后拿到已经初始化的ViewModel对象引用并修改请求api的方法让它必定返回一个true

拿到initData方法引用并执行它的的super方法

MainActivity.initData.implementation = function() {console.log("initData 被 Hook");// 调用原始的 initData 方法this.initData();
};

在执行完super方法后通过 Java.choose() 方法来获取已实例化的ViewModel对象,Java.choose 方法可以用于遍历已加载的类的所有实例,并执行指定的回调函数。

Java.choose("com.lxt.single_module.Activity.MainActivity", {onMatch: function(instance) {var viewModel = instance.viewModel.value;},onComplete: function() {}
});

获取到ViewModel对象之后修改 testLogin 方法为必定返回 true

if (viewModel) {console.log("成功获取到 viewModel");// 检查 viewModel 是否为 MainVM 的实例if (viewModel.$className === "com.lxt.single_module.ViewModel.MainVM") {console.log("viewModel 是 MainVM 的实例");// Hook testLogin 方法MainVM.testLogin.implementation = function() {console.log("testLogin 被 Hook,返回 true");return Java.use("java.lang.Boolean").valueOf(true);;};} else {console.log("viewModel 不是 MainVM 的实例");}
} else {console.log("viewModel 为 null");
}

完整代码

import frida
import sysHOOK_SCRIPT_1 = """
Java.perform(function() {var MainActivity = Java.use("com.lxt.single_module.Activity.MainActivity");var MainVM = Java.use("com.lxt.single_module.ViewModel.MainVM");  // 请确保这是正确的包名和类名MainActivity.initData.implementation = function() {console.log("initData 被 Hook");// 调用原始的 initData 方法this.initData();Java.choose("com.lxt.single_module.Activity.MainActivity", {onMatch: function(instance) {var viewModel = instance.viewModel.value;if (viewModel) {console.log("成功获取到 viewModel");// 检查 viewModel 是否为 MainVM 的实例if (viewModel.$className === "com.lxt.single_module.ViewModel.MainVM") {console.log("viewModel 是 MainVM 的实例");// Hook testLogin 方法MainVM.testLogin.implementation = function() {console.log("testLogin 被 Hook,返回 true");return Java.use("java.lang.Boolean").valueOf(true);;};} else {console.log("viewModel 不是 MainVM 的实例");}} else {console.log("viewModel 为 null");}},onComplete: function() {}});};// Hook MainActivity 中的 testBool 方法MainActivity.testBool.implementation = function() {console.log("testBool 被 Hook,强制返回 false");// 强制返回 falsereturn false;};});
"""# Python 代码
def on_message(message, data):"""回调函数,处理 Frida 发回的消息。"""if message['type'] == 'send':print(f"[+] {message['payload']}")elif message['type'] == 'error':print(f"[!] {message['stack']}")def main():print("执行任务")try:print(f"[!] {frida.__version__}")# 连接到 Android 设备device = frida.get_usb_device(timeout=5)# 查找目标进程pid = device.spawn(["com.lxt.single_module"])session = device.attach(pid)# 加载 Hook 脚本script = session.create_script(HOOK_SCRIPT_1)script.on('message', on_message)  # 设置消息回调script.load()# 恢复进程device.resume(pid)print("[*] Hook 脚本已加载,应用正在运行...")# 保持脚本运行状态sys.stdin.read()except Exception as e:print(f"[!] 错误: {e}")if __name__ == "__main__":main()

再次运行脚本并点击测试按键

触发脚本,绕过api请求步骤执行页面跳转

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/889181.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

IDEA创建Spring Boot项目配置阿里云Spring Initializr Server URL【详细教程-轻松学会】

1.首先打开idea选择新建项目 2.选择Spring Boot框架(就是选择Spring Initializr这个) 3.点击中间界面Server URL后面的三个点更换为阿里云的Server URL Idea中默认的Server URL地址:https://start.spring.io/ 修改为阿里云Server URL地址:https://star…

基于MATLAB的信号处理工具:信号分析器

信号(或时间序列)是与特定时间相关的一系列数字或测量值,不同的行业和学科将这一与时间相关的数字序列称为信号或时间序列。生物医学或电气工程师会将其称为信号,而统计学家或金融定量分析师会使用时间序列这一术语。例如&#xf…

Plugin - 插件开发03_Spring Boot动态插件化与热加载

文章目录 Pre方案概览使用插件的好处流程CodePlugin 定义Plugin 实现Plugin 使用方动态加载插件类加载器注册与卸载插件配置文件启动类测试验证 小结 Pre 插件 - 通过SPI方式实现插件管理 插件 - 一份配置,离插件机制只有一步之遥 插件 - 插件机制触手可及 Plug…

ECharts柱状图-阶梯瀑布图,附视频讲解与代码下载

引言: 在数据可视化的世界里,ECharts凭借其丰富的图表类型和强大的配置能力,成为了众多开发者的首选。今天,我将带大家一起实现一个柱状图图表,通过该图表我们可以直观地展示和分析数据。此外,我还将提供…

【Hash Function and HashMap】

散列函数(Hash Function)是一种将任意大小的数据映射到固定大小值的函数。在 HashMap 中,它扮演着核心角色。让我详细解释: 散列函数基本原理 输入:任意类型的键(key)输出:固定大小…

【jvm】为什么要有GC

目录 1. 自动内存管理2. 提升程序稳定性3. 优化性能4. 跨平台能力5. 分代回收策略 1. 自动内存管理 1.JVM中的GC机制负责自动管理内存,这意味着开发人员不需要手动分配和释放内存。2.这一特性大大简化了Java程序的内存管理,降低了内存泄漏和内存溢出等问…

Python泛型编程:TypeVar和Generic详解 - 写给初学者的指南

Python泛型编程:TypeVar和Generic详解 - 写给初学者的指南 前言1. 为什么需要泛型?2. TypeVar:定义泛型类型变量3. Generic:创建泛型类4. 多个泛型类型变量5. 使用场景小结结语 前言 大家好!今天我们来聊一聊Python中…

COUNT(*)、COUNT(1)、COUNT(某一列)的区别是什么?哪个性能更好

一些特殊情况: 有索引时:如果查询使用了索引,且查询的列在索引中,COUNT(某一列) 可能在某些情况下会比较快,因为数据库只需要扫描索引,而不需要扫描整个表。有 NULL 值时:COUNT(某一列) 可能会…

C/C++流星雨

系列文章 序号直达链接1C/C爱心代码2C/C跳动的爱心3C/C李峋同款跳动的爱心代码4C/C满屏飘字表白代码5C/C大雪纷飞代码6C/C烟花代码7C/C黑客帝国同款字母雨8C/C樱花树代码9C/C奥特曼代码10C/C精美圣诞树11C/C俄罗斯方块12C/C贪吃蛇13C/C孤单又灿烂的神-鬼怪14C/C闪烁的爱心15C/C…

【机器学习】——K均值聚类:揭开数据背后的隐藏结构

目录 引言:什么是聚类分析?K均值聚类的基本原理 2.1 聚类的概念2.2 K均值聚类简介 K均值算法的工作原理 3.1 初始化与选定K值3.2 计算距离与分配簇3.3 更新质心3.4 迭代与收敛 K均值聚类的优缺点 4.1 优点4.2 缺点与局限性 K均值聚类的常见应用 5.1 市场…

【WRF-Urban】SLUCM新增空间分布城市冠层参数及人为热排放AHF代码详解(下)

目录 详细解释更改文件内容4 运行模块(run):README.namelist5 输出模块(share):share/module_check_a_mundo.Fshare/output_wrf.F参考SLUCM新增空间分布城市冠层参数及人为热排放AHF代码详解的前两部分内容可参见-【WRF-Urban】SLUCM新增空间分布城市冠层参数及人为热排放A…

go 集成nacos注册中心、配置中心

使用限制 Go>v1.15 Nacos>2.x 安装 使用go get安装SDK: go get -u github.com/nacos-group/nacos-sdk-go/v2 快速使用 初始化客户端配置ClientConfig constant.ClientConfig{TimeoutMs uint64 // 请求Nacos服务端的超时时间,默…

ModelScope-Agent(1): 基于开源大语言模型的可定制Agent系统

目录 简介快速入门 简介 github地址 快速入门 看前两篇,调用千问API和天气API # 选用RolePlay 配置agent from modelscope_agent.agents.role_play import RolePlay # NOQArole_template 你扮演一个天气预报助手,你需要查询相应地区的天气&#x…

终端中运行 conda install 命令后一直显示“Solving environment: \ ”

初步接触深度学习,在配置环境方面出了点问题,运行 conda install 命令时,卡在 "Solving environment: \ "。 网上搜索发现, 一般可能的原因就是以下几种 环境解析耗时: Conda 在安装包时需要解析当前环境&…

Jenkins相关的Api接口调用详解

Jenkins API是Jenkins持续集成和持续部署(CI/CD)平台提供的一组接口,允许外部程序通过HTTP请求与Jenkins进行交互。以下是对Jenkins API使用的简介: 一、Jenkins API的主要功能 作业管理:通过API,可以创建、配置、删除以及查询作业(Job)。构建触发:可以远程触发新的构…

【模型对比】ChatGPT vs Kimi vs 文心一言那个更好用?数据详细解析,找出最适合你的AI辅助工具!

在这个人工智能迅猛发展的时代,AI聊天助手已经深入我们的工作与生活。你是否曾在选择使用ChatGPT、Kimi或是百度的文心一言时感到一头雾水?每款AI都有其独特的魅力与优势,那么,究竟哪一款AI聊天助手最适合你呢?本文将带…

react 和 react-dom 是什么关系

React和React DOM是两个与React生态系统密切相关的npm包,它们在构建用户界面时扮演不同的角色,但相互之间存在紧密的依赖关系。以下是React和React DOM关系的详细解释: React的作用 React是一个用于构建用户界面的JavaScript库。它提供了构建…

微信小程序uni-app+vue3实现局部上下拉刷新和scroll-view动态高度计算

微信小程序uni-appvue3实现局部上下拉刷新和scroll-view动态高度计算 前言 在uni-appvue3项目开发中,经常需要实现列表的局部上下拉刷新功能。由于网上相关教程较少且比较零散,本文将详细介绍如何使用scroll-view组件实现这一功能,包括动态高度计算、下拉刷新、上拉加载等完整…

Netty面试内容整理-常见问题排查与调试

在使用 Netty 进行开发时,排查和调试常见问题是确保系统稳定运行的关键部分。以下是一些 Netty 中常见的问题排查和调试的方法,以及对应的解决思路: 内存泄漏问题 问题描述:Netty 内存泄漏可能发生在 ByteBuf 没有被正确释放的情况下,导致内存逐渐被耗尽。 排查方法:Reso…

SQL——DQL分组聚合

分组聚合: 格式: select 聚合函数1(聚合的列),聚合函数2(聚合的列) from 表名 group by 标识列; ###若想方便分辨聚合后数据可在聚合函数前加上标识列(以标识列进行分组) 常见的聚合函数: sum(列名):求和函数 avg(列名)…