【Frida Android】基础篇11:Native层hook基础——修改原生函数的返回值

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

本章内容是对上一章学习内容的夯实,使用相似的案例进行强化记忆。

1. 基础概念

1.1 原生函数(Native函数)

原生函数指通过JNI(Java Native Interface)机制调用的、由C/C++等原生语言实现的函数。在Android开发中,这类函数通常被编译为动态链接库(.so文件),并通过System.loadLibrary()加载到Java进程中。原生函数常用于处理性能敏感、底层交互(如硬件操作)或需要保护逻辑(如加密验证)的场景,案例中的check_flag()就是典型的原生函数。

1.2 函数返回值修改的意义

修改原生函数的返回值是逆向工程和动态调试中的常用手段。当程序逻辑依赖原生函数的返回值(如验证结果、权限判断、数据校验等)时,通过hook技术强制修改返回值,可以绕过原有逻辑限制(如跳过验证、模拟成功状态),从而快速分析程序行为或实现特定功能(如案例中触发"成功"提示)。

2. 核心语法(修改原生函数返回值的hook)

使用Frida实现对原生函数返回值的修改,核心语法围绕Java层函数拦截展开,主要步骤如下:

2.1 获取目标类

通过Java.use(className)获取需要hook的类的引用,其中className为类的完整路径(包含包名)。
示例:

// 获取MainActivity类引用
const MainActivity = Java.use('com.ad2001.a0x9.MainActivity');

2.2 定位目标原生函数

直接通过类引用访问原生函数(函数名需与Java层声明一致)。原生函数在Java层的声明与普通函数一致,仅多了native关键字,Frida无需区分其实现语言,统一按Java函数处理。

源码:

public class MainActivity extends AppCompatActivity {

    public native int check_flag();

    static {
        System.loadLibrary("a0x9");
    }
    // ...
}

Hook代码:

// 定位check_flag()原生函数
MainActivity.check_flag

2.3 重写函数实现

通过implementation属性重写函数逻辑,在自定义实现中直接返回目标值(无需调用原函数)。
示例:

// 强制check_flag()返回1
MainActivity.check_flag.implementation = function () {
    return 1; // 自定义返回值
};

3. 案例分析

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

3.1 代码分析(MainActivity)

在这里插入图片描述

Java层核心代码(MainActivity)

package com.ad2001.a0x9;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.ad2001.a0x9.databinding.ActivityMainBinding;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;
    Button btn;

    // 声明原生函数(实现位于liba0x9.so)
    public native int check_flag();

    static {
        System.loadLibrary("a0x9"); // 加载原生库
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding activityMainBindingInflate = ActivityMainBinding.inflate(getLayoutInflater());
        this.binding = activityMainBindingInflate;
        setContentView(activityMainBindingInflate.getRoot());
        Button button = (Button) findViewById(R.id.button);
        this.btn = button;
        // 按钮点击事件:依赖check_flag()的返回值判断逻辑
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {
                if (MainActivity.this.check_flag() == 1337) { // 核心判断条件
                    try {
                        // AES解密逻辑(成功时执行)
                        Cipher cipher = Cipher.getInstance("AES");
                        SecretKeySpec secretKeySpec = new SecretKeySpec("3000300030003003".getBytes(), "AES");
                        cipher.init(2, secretKeySpec);
                        byte[] decryptedBytes = Base64.getDecoder().decode("hBCKKAqgxVhJMVTQS8JADelBUPUPyDiyO9dLSS3zho0=");
                        String decrypted = new String(cipher.doFinal(decryptedBytes));
                        Toast.makeText(MainActivity.this.getApplicationContext(), "You won " + decrypted, 1).show();
                        return;
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
                Toast.makeText(MainActivity.this.getApplicationContext(), "Try again", 1).show();
            }
        });
    }
}

案例中的MainActivity是一个简单的Android页面,核心逻辑如下:

  1. 原生函数声明
    声明了native int check_flag(),该函数的实现位于liba0x9.so中(通过static代码块的System.loadLibrary("a0x9")加载)。

  2. 按钮点击事件
    点击按钮时触发onClick事件,逻辑分为两步:

    • 调用check_flag(),判断返回值是否为1337;
    • 若返回值为1337,使用AES算法解密预定义的Base64字符串(hBCKKAqgxVhJMVTQS8JADelBUPUPyDiyO9dLSS3zho0=),密钥为3000300030003003,解密成功后显示"You won + 解密结果";
    • 若返回值不是1337,显示"Try again"。
  3. 核心依赖
    程序的"成功"逻辑完全依赖check_flag()的返回值,因此只要修改该函数的返回值为1337,即可绕过原生层验证,直接触发解密流程。

3.2 Hook思路

基于上述代码分析,hook的核心目标是check_flag()强制返回1337,具体思路如下:

  1. 确定hook点
    目标函数为com.ad2001.a0x9.MainActivity类中的check_flag(),无需关心其原生实现(无论liba0x9.so中如何计算返回值,直接覆盖即可)。

  2. 编写hook脚本
    使用Frida的Java桥接API,获取MainActivity类引用,重写check_flag()的实现,固定返回1337。

  3. 注入脚本到目标进程
    通过Python脚本连接设备、启动目标应用进程(com.ad2001.a0x9)、附加进程并注入JS脚本,实现动态hook。

3.3 核心源码与脚本

3.3.1 Frida Hook脚本(JS)
import Java from 'frida-java-bridge';

Java.perform(function () {
    // 1. 获取目标类
    const MainActivity = Java.use('com.ad2001.a0x9.MainActivity');
    
    // 2. 重写原生函数check_flag(),强制返回1337
    MainActivity.check_flag.implementation = function () {
        console.log("[Hook] 拦截check_flag(),返回1337");
        return 1337; // 覆盖原返回值
    };
});
3.3.2 注入脚本(Python)
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.a0x9"

def main():
    try:
        # 连接USB设备
        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()

3.4 成功效果

执行上述脚本后,点击应用按钮,check_flag()被hook强制返回1337,触发AES解密流程,成功显示flag:

在这里插入图片描述

4. 技术总结

  1. 核心原理:修改原生函数返回值的本质是通过hook技术拦截函数调用,用自定义返回值覆盖原逻辑。对于依赖返回值的程序(如验证、权限判断),该方法可快速绕过限制。

  2. Frida的优势:作为动态hook工具,Frida无需修改原程序代码或重打包,支持跨平台(Android、iOS等),且通过Java桥接API可轻松操作Java层函数(包括原生函数),降低了hook的技术门槛。

  3. 关键注意事项

    • 需准确获取类的完整路径(包名+类名)和函数名,否则无法定位目标;
    • 进程附加时机需合理(建议在进程启动初期注入,避免函数已被调用);
    • 若函数有参数,需在重写实现时保持参数列表一致(即使不使用参数)。
  4. 适用场景:适用于逆向分析中需要绕过原生层验证、调试函数依赖关系、或临时修改程序行为的场景,是移动端逆向工程的基础技能之一。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

介一笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值