mthoutai

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

⚠️本博文所涉安全渗透测试技术、方法及案例,仅用于网络安全技术研究与合规性交流,旨在提升读者的安全防护意识与技术能力。任何个人或组织在使用相关内容前,必须获得目标网络 / 系统所有者的明确且书面授权,严禁用于未经授权的网络探测、漏洞利用、数据获取等非法行为。

1 基础概念

前面两篇文章我们只对 Frida 的概念粗略地了解了下,本章我们需要正式进行了解。

1.1 什么是 Frida

Frida是一款跨平台的动态插桩工具(可以简单理解为“运行时修改程序行为的工具”),支持Android、iOS、Windows等多个系统。它的核心优势是不需要修改程序源码,也不需要重新编译APP,就能在程序运行过程中“插入”我们自己的代码,实现对程序行为的监控、修改甚至替换。

对于Android 来说,Frida是入门级的利器——它允许我们用JavaScript(或Python、Swift等)编写脚本,直接操作Android 应用的Java层代码,非常适合调试和破解简单的验证逻辑。

1.2 什么是 Hook

“Hook”中文常译为“钩子”,形象地说就是在程序的某个“关键点”(比如一个函数、一个按钮点击事件)上“挂”一个自己的逻辑。当程序执行到这个关键点时,会先触发我们挂的逻辑,从而实现:

  • 监控:查看函数的输入参数、返回值;
  • 修改:改变函数的参数或返回结果;
  • 替换:用自己的逻辑完全替代原函数的功能。

在本教程中,我们就是通过Hook技术,修改APP中“验证用户输入是否正确”的逻辑,让任意输入都能通过验证。

1.3 为什么要学Java层Hook

Android应用的核心逻辑(比如登录验证、数据处理)大多写在Java层(通过Java或Kotlin编写)。掌握Java层Hook,就能直接对这些核心逻辑“动手脚”,是逆向分析Android应用的基础技能。

接下来,我们通过一个具体案例,带你一步步实操Java层Hook的全过程。

2 案例实操

本章示例应用的链接:
https://pan.baidu.com/s/16EE2XE-OZS_xBRPlWUODbw?pwd=n2vb
提取码: n2vb
使用APK:Challenge 0x1.apk

2.1 代码分析

模拟器安装 Challenge 0x1.apk,打开应用后输入任意数字后点击 submit 按钮,提示不通过。

在这里插入图片描述

我们此时使用 jadx-gui 反编译该 apk ,查看核心代码 MainActivity,我们在写 hook 脚本前,先要大致分析下核心代码逻辑。

package com.ad2001.frida0x1;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.util.Random;
/* loaded from: classes.dex */
public class MainActivity extends AppCompatActivity {
TextView t1;
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_main);
final EditText editText = (EditText) findViewById(R.id.editTextTextPassword);
Button button = (Button) findViewById(R.id.button);
this.t1 = (TextView) findViewById(R.id.textview1);
final int i = get_random();
button.setOnClickListener(new View.OnClickListener() { // from class: com.ad2001.frida0x1.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View view) {
String string = editText.getText().toString();
if (TextUtils.isDigitsOnly(string)) {
MainActivity.this.check(i, Integer.parseInt(string));
} else {
Toast.makeText(MainActivity.this.getApplicationContext(), "Enter a valid number !!", 1).show();
}
}
});
}
int get_random() {
return new Random().nextInt(100);
}
/* JADX WARN: Removed duplicated region for block: B:13:0x0031 A[PHI: r0
0x0031: PHI (r0v7 char) = (r0v5 char), (r0v11 char) binds: [B:19:0x0040, B:12:0x002f] A[DONT_GENERATE, DONT_INLINE]] */
/*
Code decompiled incorrectly, please refer to instructions dump.
To view partially-correct code enable 'Show inconsistent code' option in preferences
*/
void check(int r4, int r5) {
/*
r3 = this;
int r4 = r4 * 2
int r4 = r4 + 4
r0 = 1
if (r4 != r5) goto L53
android.content.Context r4 = r3.getApplicationContext()
java.lang.String r5 = "Yey you guessed it right"
android.widget.Toast r4 = android.widget.Toast.makeText(r4, r5, r0)
r4.show()
java.lang.StringBuilder r4 = new java.lang.StringBuilder
r4.<init>()
  r5 = 0
  L1a:
  r0 = 20
  if (r5 >= r0) goto L49
  java.lang.String r0 = "AMDYV{WVWT_CJJF_0s1}"
  char r0 = r0.charAt(r5)
  r1 = 97
  if (r0 < r1) goto L35
  r2 = 122(0x7a, float:1.71E-43)
  if (r0 > r2) goto L35
  int r0 = r0 + (-21)
  char r0 = (char) r0
  if (r0 >= r1) goto L43
  L31:
  int r0 = r0 + 26
  char r0 = (char) r0
  goto L43
  L35:
  r1 = 65
  if (r0 < r1) goto L43
  r2 = 90
  if (r0 > r2) goto L43
  int r0 = r0 + (-21)
  char r0 = (char) r0
  if (r0 >= r1) goto L43
  goto L31
  L43:
  r4.append(r0)
  int r5 = r5 + 1
  goto L1a
  L49:
  android.widget.TextView r5 = r3.t1
  java.lang.String r4 = r4.toString()
  r5.setText(r4)
  goto L60
  L53:
  android.content.Context r4 = r3.getApplicationContext()
  java.lang.String r5 = "Try again"
  android.widget.Toast r4 = android.widget.Toast.makeText(r4, r5, r0)
  r4.show()
  L60:
  return
  */
  throw new UnsupportedOperationException("Method not decompiled: com.ad2001.frida0x1.MainActivity.check(int, int):void");
  }
  }

这段代码是该应用的主活动类(MainActivity),功能是一个简单的猜数字游戏,猜对后会展示一个经过字符转换的字符串。以下是详细分析:

1. 类结构与核心组件

  • 继承自AppCompatActivity,是Android应用的主界面活动。
  • 核心UI组件:
    • EditText:用于用户输入数字。
    • Button:触发猜数字的检查逻辑。
    • TextView (t1):用于展示猜对后的结果字符串。

2. 核心方法解析

(1)onCreate方法

初始化界面布局,绑定UI组件(输入框、按钮、文本框)。

生成一个随机数(通过get_random()方法),并为按钮设置点击事件:

  • 点击时获取用户输入的字符串,检查是否为纯数字。
  • 若为数字,调用check方法,传入随机数和用户输入的数字;否则提示“Enter a valid number !!”。
(2)get_random方法

功能:生成一个0~99之间的随机整数(通过Random().nextInt(100)实现)。

(3)check方法(核心逻辑)

作用:验证用户输入的数字是否正确,并在正确时解密展示字符串。

逻辑拆解:

校验逻辑是从check方法的反编译代码中推导出来的,核心是分析方法内对参数的处理和条件判断逻辑。具体步骤如下:

(1)反编译代码中,check方法的定义是void check(int r4, int r5),其中:

  • r4:是传入的第一个参数,结合上下文可知,它是get_random()生成的随机数(在onCreate中调用check(i, ...)i就是get_random()的返回值)。
  • r5:是传入的第二个参数,即用户输入的数字(Integer.parseInt(string)的结果)。

(2)分析check方法内对r4的处理

反编译的伪代码中,明确提到对r4的运算:

int r4 = r4 * 2;  // 随机数先乘以2
int r4 = r4 + 4;  // 再加上4

这两步运算后,r4的值变成了“随机数×2 + 4”。

(3)分析条件判断逻辑

紧接着运算后,代码进行了条件判断:

if (r4 != r5) goto L53;  // 如果运算后的r4不等于用户输入的r5,跳转到失败逻辑
// 否则执行成功逻辑(显示正确提示、解密字符串)

即:如果“随机数×2 + 4”等于用户输入的r5,则校验通过,显示“Yey you guessed it right”提示;否则失败,显示“Try again”提示。。

3. 总结

通过拆解check方法对参数的运算(r4*2+4)和条件判断(r4 == r5),可以直接得出校验逻辑:用户输入的数字必须等于“随机数×2 + 4”才能通过校验

2.2 脚本编写

代码结构与之前保持一致,参考上一章内容 4.1 章节部分3:【Frida Android】基础篇2:Frida基础操作模式详解

hook.js

import Java from 'frida-java-bridge';
Java.perform(function () {
// 定位目标类
var MainActivity = Java.use('com.ad2001.frida0x1.MainActivity');
// Hook check方法,替换用户输入为“正确答案”
MainActivity.check.implementation = function (randomNum, userInput) {
// 计算正确答案:随机数*2 + 4
var correctAnswer = randomNum * 2 + 4;
// 调用原始check方法,但传入正确答案(忽略用户输入)
this.check(randomNum, correctAnswer);
};
});

run.py

逻辑不变,就修改目标应用包名PACKAGE_NAME

import frida
import sys
import time
def on_message(message, data):
if message['type'] == 'send':
print(f"[Hook 日志] {message['payload']}")
elif message['type'] == 'error':
print(f"[错误] {message['stack']}")
# 目标应用包名
PACKAGE_NAME = "com.ad2001.frida0x1"
def main():
try:
device = frida.get_usb_device(timeout=10)
print(f"已连接设备:{device.name}")
print(f"启动进程 {PACKAGE_NAME}...")
pid = device.spawn([PACKAGE_NAME])
device.resume(pid)
time.sleep(2)
process = device.attach(pid)
print(f"已附加到进程 PID: {pid}")
with open("./js/compiled_hook.js", "r", encoding="utf-8") as f:
js_code = f.read()
script = process.create_script(js_code)
script.on('message', on_message)
script.load()
print("JS 脚本注入成功,开始监控...(按 Ctrl+C 退出)")
sys.stdin.read()
except frida.TimedOutError:
print("未找到USB设备")
except frida.ProcessNotFoundError:
print(f"应用 {PACKAGE_NAME} 未安装")
except FileNotFoundError:
print("未找到 js 脚本,请检查路径")
except Exception as e:
print(f"异常:{str(e)}")
finally:
if 'process' in locals():
process.detach()
print("程序退出")
if __name__ == "__main__":
main()

2.3 注入脚本

# 终端1:编译 hook.js
npm run watch
# 终端2:启动 frida_server
adb devices
adb shell
su
cd /data/local/tmp
./frida-server &
# 终端3:启动 run.py
python run.py
# 退出注入
# 终端3 按 Ctrl+C

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

上述步骤正常执行后,在 APK 界面中输入任意 1 个数字后,点击 submit 按钮后显示成功。

在这里插入图片描述

3 技术总结

通过这个案例,我们完成了一次简单的Java层Hook实操,核心知识点可以总结为以下3点:

3.1 逆向分析是Hook的前提

在写Hook脚本前,必须先搞清楚目标APP的核心逻辑:

  • jadx-gui反编译APK,找到关键类(如本案例的MainActivity);
  • 分析核心方法(如check方法)的逻辑:输入什么参数、做了什么计算、如何判断结果(本案例中是“随机数×2+4”与用户输入的对比)。

小白提示:反编译工具(如jadx)是逆向的“眼睛”,先看懂代码逻辑,才能知道该“钩”哪里。

3.2 Frida脚本的核心逻辑:定位→修改

Frida操作Java层的步骤很固定:

  • 定位目标:用Java.use('类名')找到要Hook的类(如com.ad2001.frida0x1.MainActivity);
  • Hook方法:通过类名.方法名.implementation = function(参数) {...}替换原方法的实现;
  • 自定义逻辑:在替换的函数中,我们可以修改参数(如本案例中将用户输入改为“正确答案”),再调用原方法。

Frida的JavaScript API很直观,核心就是“找到谁→改什么→怎么改”。

3.3 脚本注入的完整流程

让Hook生效的步骤:

  • 手机端运行frida-server(让Frida能控制手机里的APP);
  • 电脑端用Python脚本(run.py)启动目标APP并注入Frida脚本;
  • 操作APP,Hook逻辑自动生效(如本案例中输入任意数字都提示“正确”)。
posted on 2025-11-07 14:43  mthoutai  阅读(212)  评论(0)    收藏  举报