安卓逆向|Frida初探

一、工具安装

夜神模拟器:https://www.yeshen.com/

Adb:https://www.cnblogs.com/comradexiao/p/18926941

Frida:https://juejin.cn/post/7229883377142104125

二、adb shell

  1. 夜神模拟器打开USB调试功能
  2. cmd输入 adb connect 127.0.0.1 62001(夜神模拟器默认adb端口)

3. 输入adb shell即可进入shell:

三、上传Frida server

下载对应模拟器架构的frida server,我用的frida-server-17.2.15-android-x86

adb push传上去,然后启动:

四、测试

写一个简单的app来测试,该app会产生一个不打印出来的随机值,在输入框内填出正确的值即可获得flag,由于是随机产生的,所以逆向无效,借助frida hook获取该值。

app代码如下。

MainActivity:

package com.example.keygenapp;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.security.SecureRandom;

public class MainActivity extends AppCompatActivity {

    private String randomValue;  // 随机验证值
    private TextView flagView;   // 用于显示flag的文本框

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 初始化UI组件
        EditText inputText = findViewById(R.id.input_text);
        Button verifyButton = findViewById(R.id.verify_button);
        flagView = findViewById(R.id.flag_view);  // 显示flag的文本框

        // 生成随机值(不打印,仅内存中存在)
        randomValue = generateRandomValue();

        // 验证按钮点击事件
        verifyButton.setOnClickListener(v -> {
            String userInput = inputText.getText().toString().trim();
            if (userInput.equals(randomValue)) {
                // 验证成功:生成flag并显示在屏幕上
                String flag = generateFlag();
                flagView.setText("Flag: " + flag);  // 直接显示在屏幕
            } else {
                flagView.setText("Wrong value! Try again.");
            }
        });
    }

    // 生成16位随机验证值(字母+数字)
    private String generateRandomValue() {
        SecureRandom random = new SecureRandom();
        String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        StringBuilder sb = new StringBuilder(16);
        for (int i = 0; i < 16; i++) {
            sb.append(chars.charAt(random.nextInt(chars.length())));
        }
        return sb.toString();
    }

    // 生成flag
    private String generateFlag() {
        SecureRandom random = new SecureRandom();
        byte[] flagBytes = new byte[16];
        random.nextBytes(flagBytes);
        StringBuilder hexFlag = new StringBuilder("flag{");
        for (byte b : flagBytes) {
            hexFlag.append(String.format("%02x", b));
        }
        hexFlag.append("}");
        return hexFlag.toString();
    }
}

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <EditText
        android:id="@+id/input_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Enter the random value"/>

    <Button
        android:id="@+id/verify_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Verify"/>

    <TextView
        android:id="@+id/flag_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:textSize="16sp"/>

</LinearLayout>

Android studio生成apk,拖到模拟器中。

frida的hook代码如下:

// 全局变量:存储捕获的随机值
let capturedRandomValue = null;

Java.perform(function () {
    try {
        // 1. 加载MainActivity类
        const MainActivity = Java.use('com.example.keygenapp.MainActivity');
        const BUTTON_TEXT = "Verify";  // 按钮文本

        // --------------------------
        // Step 1: Hook随机值生成方法(核心)
        // --------------------------
        MainActivity.generateRandomValue.implementation = function () {
            const randomValue = this.generateRandomValue();
            capturedRandomValue = randomValue;
            console.log("=================================");
            console.log("[+] 捕获随机验证值: " + randomValue);
            console.log("=================================");
            
            // 延迟执行自动输入(等待UI加载)
            setTimeout(automateInputAndGetFlag, 1000);
            return randomValue;
        };

        // --------------------------
        // 核心逻辑:自动输入+获取屏幕flag
        // --------------------------
        function automateInputAndGetFlag() {
            if (!capturedRandomValue) {
                console.error("[-] 未捕获到随机值");
                return;
            }
            console.log("\n[*] 开始自动输入和验证...");

            // 在主线程操作UI
            Java.scheduleOnMainThread(function () {
                try {
                    // 查找MainActivity实例
                    Java.choose('com.example.keygenapp.MainActivity', {
                        onMatch: function (activity) {
                            // 1. 找到输入框并填入随机值
                            const inputText = findEditText(activity);
                            if (inputText) {
                                inputText.setText(capturedRandomValue);
                                console.log("[+] 自动输入随机值: " + capturedRandomValue);
                            }

                            // 2. 点击验证按钮
                            const verifyButton = findButton(activity);
                            if (verifyButton) {
                                verifyButton.performClick();
                                console.log("[+] 自动点击验证按钮");
                            }

                            // 3. 从屏幕文本框获取flag
                            setTimeout(() => {  // 延迟等待flag显示
                                const flagView = activity.findViewById(0x7f080028);  // R.id.flag_view的ID
                                if (flagView) {
                                    const flagText = flagView.getText().toString();
                                    console.log("\n[+] 从屏幕捕获flag: " + flagText);
                                    console.log("=================================");
                                    console.log("[*] 任务完成,按Ctrl+C退出");
                                }
                            }, 500);

                        },
                        onComplete: function () {}
                    });
                } catch (e) {
                    console.error("[-] 错误: " + e);
                }
            });
        }

        // 查找输入框(页面中第一个EditText)
        function findEditText(activity) {
            let editText = null;
            Java.choose('android.widget.EditText', {
                onMatch: function (et) {
                    if (isViewInActivity(et, activity)) {
                        editText = et;
                        return 'stop';
                    }
                },
                onComplete: function () {}
            });
            return editText;
        }

        // 查找验证按钮(文本匹配"Verify")
        function findButton(activity) {
            let button = null;
            Java.choose('android.widget.Button', {
                onMatch: function (btn) {
                    if (isViewInActivity(btn, activity) && btn.getText().toString() === BUTTON_TEXT) {
                        button = btn;
                        return 'stop';
                    }
                },
                onComplete: function () {}
            });
            return button;
        }

        // 验证控件是否属于当前Activity
        function isViewInActivity(view, activity) {
            try {
                const rootView = activity.getWindow().getDecorView();
                return view.isDescendantOf(rootView);
            } catch (e) {
                return false;
            }
        }

    } catch (e) {
        console.error("[-] 全局错误: " + e);
    }
});

启动frida server后,主机上执行hook:

frida -U -f com.example.keygenapp -l test.js

成功hook:

提交该值,获得flag:

接下来反思一下该如何防御,由于frida既可以从java层hook,又能从native(.so)层hook,所以保护可以分为三个层面,一个是java层保护,一个是native层保护,一个是通用保护。

java层hook跟native层hook的原理不一样。

java 层的 Hook 并不直接操作汇编指令。它利用的是 Java 虚拟机(JVM/ART)本身的特性来实现的,主要是反射(Reflection) 和运行时方法替换方法拦截是Frida 通过修改 Java 方法的内部结构(在 ART 中,方法是 ArtMethod 对象),将其指向一个自定义的“桥接”函数。当目标方法被调用时,这个桥接函数会先执行,让你有机会执行自己的代码,然后再调用原始方法或完全替换它。这个过程发生在 Java VM 内部,不涉及原生机器码。方法替换更像是“重写”整个方法。这本质上是在运行时动态生成一个新的方法实现并替换旧有的。

而Native 层的 Hook 是直接操作进程的内存和机器指令。也就是熟知的对汇编代码进行插桩,跳转到自定义函数处执行再跳回来继续执行。因此两个层面的防hook保护会有所区别。

首先是java层保护,Frida 在 Java 层 Hook 某个方法时,必须通过类的全限定名方法名 + 参数签名定位目标,所以常见的方法是混淆,但是这种混淆并不针对hook技术本身,而是通过增加逆向的难度来增加写hook脚本的时间成本和难度。

其次是native层保护,由于native层已经涉及到汇编,所以除了普通混淆之外,还可以通过vmp等采用自创虚拟机的方法来进行更高级的混淆,由于vmp混淆后的代码需要用自己自创的虚拟机引擎来解释,所以常规hook手段全部无效,必须得脱壳才能进行hook。

最后是通用的防hook保护,比如检测 Frida 注入的特征、反调试(如ptrace)、内存保护(如阻止插桩)等手段,这种不属于增加逆向难度,而是直接针对hook工具本身。

java层保护1——混淆

混淆方法

proguard-rules.pro:

# ==============================================
# 1. 保留Android系统组件/框架类(避免App崩溃)
# ==============================================
# 保留所有Activity(系统需通过类名识别,不能混淆)
-keep public class * extends android.app.Activity
# 保留Application(应用入口,不能混淆)
-keep public class * extends android.app.Application
# 保留Fragment(支持库组件,避免跳转崩溃)
-keep public class * extends androidx.fragment.app.Fragment
# 保留自定义View(避免布局加载失败)
-keep public class * extends android.view.View {
    # 保留View的构造函数(系统初始化View需调用)
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
    # 保留setter方法(如setOnClickListener,避免反射调用失败)
    public void set*(...);
}

# ==============================================
# 2. 正确保留MainActivity:只留类名和构造函数,混淆内部所有方法/变量
# ==============================================
# 关键修正:只保留MainActivity的“类名”和“构造函数”,其他全混淆
-keep class com.example.obfustest.MainActivity {
    <init>(...); # 仅保留构造函数(Activity实例化必须)
    # 不写任何其他规则 → 内部方法(generateRandomValue等)和变量会被自动混淆
}

# ==============================================
# 3. 保留UI交互必要方法(避免点击/事件失效)
# ==============================================
# 保留Activity中与View交互的方法(如onClick,避免混淆后点击事件失效)
-keepclassmembers class * extends android.app.Activity {
    public void *(android.view.View);
}

# ==============================================
# 4. 增强混淆强度(可选但推荐,提升逆向难度)
# ==============================================
# 混淆优化次数(5次,打乱代码逻辑)
-optimizationpasses 5
# 不使用混合大小写类名(统一小写,避免aBc这类易记忆名称)
-dontusemixedcaseclassnames
# 不跳过非公共库类(确保第三方库内部也被混淆)
-dontskipnonpubliclibraryclasses
# 不保留行号(避免逆向时通过行号定位代码)
-keepattributes !SourceFile,!LineNumberTable
# 扁平化包结构(合并多级包,隐藏功能模块划分)
-flattenpackagehierarchy ''
-dontwarn android.window.OnBackAnimationCallback

build.gradle(app)

plugins {
    id 'com.android.application'
}

android {
    namespace "com.example.obfustest"
    compileSdk 33
    buildToolsVersion "33.0.2"

    defaultConfig {
        applicationId "com.example.obfustest"
        minSdk 24
        targetSdk 33
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    // 1. 强制锁定 activity 版本为 1.7.2(关键!覆盖传递依赖)
    implementation 'androidx.activity:activity:1.7.2'
    constraints {
        implementation('androidx.activity:activity') {
            version {
                strictly '1.7.2' // 严格锁定版本,不允许任何升级
            }
        }
    }

    // 2. 其他依赖(确保版本兼容 33)
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.10.0'

    // 测试依赖
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

mainactivity:

package com.example.obfustest; // 包名需与后续Frida脚本一致

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import java.security.SecureRandom;
import java.util.UUID;

public class MainActivity extends AppCompatActivity {
    private String randomValue; // 混淆后仍会存在的变量
    private TextView tvFlag;    // 显示flag的文本框
    private EditText etInput;   // 输入框
    private Button btnVerify;   // 验证按钮

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 绑定UI控件(ID与布局文件对应)
        etInput = findViewById(R.id.et_input);
        btnVerify = findViewById(R.id.btn_verify);
        tvFlag = findViewById(R.id.tv_flag);

        // 生成随机值(关键方法,后续会被混淆)
        randomValue = generateRandomValue();

        // 验证逻辑:输入匹配随机值则显示flag
        btnVerify.setOnClickListener(v -> {
            String userInput = etInput.getText().toString().trim();
            if (userInput.equals(randomValue)) {
                tvFlag.setText("Success! Flag: " + generateFlag());
            } else {
                tvFlag.setText("Failed! Wrong value.");
            }
        });
    }

    /**
     * 关键方法:生成16位随机值(字母+数字)
     * 混淆后方法名会被改为a/b/c等无意义名称
     */
    private String generateRandomValue() {
        SecureRandom random = new SecureRandom();
        String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        StringBuilder sb = new StringBuilder(16);
        for (int i = 0; i < 16; i++) {
            sb.append(chars.charAt(random.nextInt(chars.length())));
        }
        return sb.toString();
    }

    /**
     * 生成flag(UUID格式,仅作示例)
     */
    private String generateFlag() {
        return "flag{" + UUID.randomUUID().toString().replace("-", "").substring(0, 16) + "}";
    }
}

记得生成签名后的release,debug版是不会应用proguard规则的,生成后拖进jadx,发现已经被混淆了,核心变量名、内部类名被重命名为无意义标识符,核心方法逻辑被内联隐藏:

改变如下:

原始代码元素混淆后变化不变的核心特征
成员变量名randomValuef1210yetInputAbtnVerifyBtvFlagf1211z变量的 “赋值来源” 和 “使用场景” 不变(如 f1210y 仍由随机值生成逻辑赋值)。
方法名generateRandomValue() 被内联到 onCreate 中(方法消失);
generateFlag() 被内联到点击事件中。
方法内的逻辑(如 SecureRandom 调用、UUID 生成)完全不变。
匿名点击监听器原始的 View.OnClickListener 匿名类→r1.a 类(混淆后的外部类)。onClick 方法内的验证逻辑(对比输入与随机值、生成 Flag)完全不变。
字符串常量字符集、提示文本(Failed! Wrong value.)、Flag 前缀(flag{)均未被加密。这些字符串是逆向时搜索的 “硬编码锚点”(如搜索 flag{ 可直接定位 Flag 生成逻辑)。

应对1

直接hook混淆后的变量名,原理一样,但是需要注意的是,反汇编时候的变量名,在实际程序运行时候可能也会改变。

应对2

观察随机数生成算法,发现是调用 SecureRandom.nextInt(62) 获取字符索引,循环16次。只要能拦截这个调用,就能还原出随机生成的字符串。

// 目标应用包名(必须与AndroidManifest的applicationId一致)
const TARGET_PACKAGE = "com.example.obfustest";
// 应用固定字符集(反编译确认)
const CHAR_SET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let charIndexes = [];
let isGeneratingTarget = false;

Java.perform(function () {
    console.log("======================================");
    console.log("[*] Frida 准备直接启动应用:" + TARGET_PACKAGE);
    console.log("[*] 策略:启动后Hook nextInt(int),捕获16位随机值");
    console.log("======================================\n");

    // 1. 明确Hook带int参数的nextInt(避免重载冲突)
    const SecureRandom = Java.use("java.security.SecureRandom");
    SecureRandom.nextInt.overload('int').implementation = function (bound) {
        const index = this.nextInt(bound);

        // 过滤目标调用(bound=62,且未收集满16个字符)
        if (bound === 62 && charIndexes.length < 16) {
            if (charIndexes.length === 0) {
                console.log("[+] 应用启动成功,开始捕获随机值生成...");
                isGeneratingTarget = true;
            }

            const char = CHAR_SET[index];
            charIndexes.push(index);
            console.log(`[+] 第${charIndexes.length}位:索引=${index} → 字符=${char}`);

            // 收集满16个,输出结果
            if (charIndexes.length === 16) {
                const randomValue = charIndexes.map(idx => CHAR_SET[idx]).join("");
                console.log("\n[!] 随机验证值获取成功:");
                console.log("    ┌────────────────────────┐");
                console.log("    │  " + randomValue + "  │");
                console.log("    └────────────────────────┘");
                console.log("\n[*] 操作:在应用输入框粘贴上述值,点击验证");
                
                // 重置状态(应对应用重启场景)
                charIndexes = [];
                isGeneratingTarget = false;
            }
        } else if (isGeneratingTarget && charIndexes.length > 0 && bound !== 62) {
            console.warn("[-] 生成异常,重置捕获状态");
            charIndexes = [];
            isGeneratingTarget = false;
        }

        return index;
    };

    // 兜底:5秒未捕获到生成,提示检查应用
    setTimeout(() => {
        if (charIndexes.length === 0 && !isGeneratingTarget) {
            console.log("\n[-] 5秒未捕获到随机值生成,可能原因:");
            console.log("    1. 应用启动后未自动生成随机值(需手动触发)");
            console.log("    2. 应用包名错误或未安装");
            console.log("    3. Frida-server未正常运行");
        }
    }, 5000);
});

同理,借助这种思维,也可以拦截字符拼接函数,可以自行尝试。

应对3

针对这种直接用输入来与目标值做对比的程序,有一种通用方法,就是hook最后的比较函数,罗列参数即可发现目标值。

或者直接hook onClick方法:

Hook r1.a 类的 onClick 方法后做的操作:当 onClick 方法被触发时(点击按钮),脚本会执行以下逻辑:

1.从 r1.a 的字段 b 中拿到 MainActivity 实例(因为 r1.a 构造时传入了 this,即 MainActivity)。

2.遍历 MainActivity 的所有字段,通过 field.setAccessible(true) 强制读取每个字段的名称、类型、值。

然后就能读到目标值:

应对4

其实面对这种flag值生成逻辑与输入值无关的程序,还有最简单的一种方法,是直接hook比较函数的返回值,通过判断即可打印出flag。虽然Re题很多都是对输入值进行变换然后才跟某一个值进行比较,考察的是逆向变换算法能力,但是实际生产中很多时候需要的是像本测试案例一样的通过判断即可。

// 目标特征:f1210y(混淆后y)是16位字母+数字的随机值,用正则匹配
const TARGET_PATTERN = /^[a-zA-Z0-9]{16}$/; 
// 验证按钮监听器类(r1.a),用于确认比较上下文
const LISTENER_CLASS = "r1.a";

Java.perform(function () {
    console.log("======================================");
    console.log("[*] Hook 比较函数:String.equals()(验证场景专用)");
    console.log("[*] 等待点击验证按钮...");
    console.log("======================================\n");

    // 1. Hook String类的equals方法(核心比较函数)
    const StringClass = Java.use("java.lang.String");
    StringClass.equals.implementation = function (other) {
        // 执行原方法,获取真实比较结果(用于日志)
        const originalResult = this.equals(other);
        // 当前字符串(this)和比较对象(other)的值
        const currentStr = this.toString();
        const otherStr = other ? other.toString() : "null";

        // 2. 精准筛选:只处理“验证场景的equals调用”
        // 筛选条件:
        // a) 比较双方至少有一个符合f1210y的特征(16位字母+数字);
        // b) 比较的调用上下文是r1.a的onClick方法(确保是验证按钮触发)
        const isTargetComparison = 
            (TARGET_PATTERN.test(currentStr) || TARGET_PATTERN.test(otherStr)) && 
            isCallFromListener();

        if (isTargetComparison) {
            console.log("[!] 捕获验证比较(强制通过):");
            console.log(`    输入值(trim后):${currentStr.match(TARGET_PATTERN) ? otherStr : currentStr}`);
            console.log(`    目标随机值(y字段):${currentStr.match(TARGET_PATTERN) ? currentStr : otherStr}`);
            console.log(`    原始结果:${originalResult} → 强制结果:true`);
            return true; // 强制比较通过
        }

        // 非验证场景的equals,返回原始结果(不影响其他功能)
        return originalResult;
    };

    // 3. 辅助函数:判断当前equals调用是否来自r1.a的onClick方法(确认上下文)
    function isCallFromListener() {
        try {
            // 获取当前调用栈
            const stackTrace = Java.use("java.lang.Thread").currentThread().getStackTrace();
            // 遍历栈帧,看是否包含r1.a的onClick方法
            for (let i = 0; i < stackTrace.length; i++) {
                const className = stackTrace[i].getClassName();
                const methodName = stackTrace[i].getMethodName();
                // 匹配:类是r1.a,方法是onClick
                if (className === LISTENER_CLASS && methodName === "onClick") {
                    return true;
                }
            }
        } catch (e) {
            console.error("[-] 检查调用栈异常:" + e.message);
        }
        return false;
    }

    // 4. 确认验证按钮点击(日志提示)
    Java.use(LISTENER_CLASS).onClick.overload("android.view.View").implementation = function (view) {
        // 只处理case 0(MainActivity验证分支)
        if (this.a.value === 0) {
            console.log("\n[+] 触发验证按钮点击(r1.a case 0)");
        }
        this.onClick(view); // 执行原点击逻辑
    };
});

总结

像这种java层面的混淆(ProGuard、R8),不挑战 hook 技术本身,只是增加了逆向的难度,进而增加了写hook脚本的难度。但是,在native层面(.so)可以通过虚拟化保护 VMP等方式来阻止hook,这种情况下可能 “连目标方法的内存地址都找不到”,间接导致 hook 无法直接生效,需要先脱壳 / 绕过保护。

java层保护2——dex加壳

待填坑

native层保护

待填坑

通用保护

检测 Frida 注入的特征

待填坑

反调试(如ptrace

待填坑

内存保护(如阻止插桩)

待填坑

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇