【Frida Android】基础篇11:Native层hook基础——修改原生函数的返回值
文章目录
- 1. 基础概念
- 1.1 原生函数(Native函数)
- 1.2 函数返回值修改的意义
- 2. 核心语法(修改原生函数返回值的hook)
- 2.1 获取目标类
- 2.2 定位目标原生函数
- 2.3 重写函数实现
- 3. 案例分析
- 3.1 代码分析(MainActivity)
- 3.2 Hook思路
- 3.3 核心源码与脚本
- 3.3.1 Frida Hook脚本(JS)
- 3.3.2 注入脚本(Python)
- 3.4 成功效果
- 4. 技术总结
⚠️本博文所涉安全渗透测试技术、方法及案例,仅用于网络安全技术研究与合规性交流,旨在提升读者的安全防护意识与技术能力。任何个人或组织在使用相关内容前,必须获得目标网络 / 系统所有者的明确且书面授权,严禁用于未经授权的网络探测、漏洞利用、数据获取等非法行为。
本章内容是对上一章学习内容的夯实,使用相似的案例进行强化记忆。
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"); // 加载原生库}@Overrideprotected 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() {@Overridepublic 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页面,核心逻辑如下:
-
原生函数声明:
声明了native int check_flag()
,该函数的实现位于liba0x9.so
中(通过static
代码块的System.loadLibrary("a0x9")
加载)。 -
按钮点击事件:
点击按钮时触发onClick
事件,逻辑分为两步:- 调用
check_flag()
,判断返回值是否为1337; - 若返回值为1337,使用AES算法解密预定义的Base64字符串(
hBCKKAqgxVhJMVTQS8JADelBUPUPyDiyO9dLSS3zho0=
),密钥为3000300030003003
,解密成功后显示"You won + 解密结果"; - 若返回值不是1337,显示"Try again"。
- 调用
-
核心依赖:
程序的"成功"逻辑完全依赖check_flag()
的返回值,因此只要修改该函数的返回值为1337,即可绕过原生层验证,直接触发解密流程。
3.2 Hook思路
基于上述代码分析,hook的核心目标是让check_flag()
强制返回1337,具体思路如下:
-
确定hook点:
目标函数为com.ad2001.a0x9.MainActivity
类中的check_flag()
,无需关心其原生实现(无论liba0x9.so
中如何计算返回值,直接覆盖即可)。 -
编写hook脚本:
使用Frida的Java桥接API,获取MainActivity
类引用,重写check_flag()
的实现,固定返回1337。 -
注入脚本到目标进程:
通过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(),强制返回1337MainActivity.check_flag.implementation = function () {console.log("[Hook] 拦截check_flag(),返回1337");return 1337; // 覆盖原返回值};
});
3.3.2 注入脚本(Python)
import frida
import sys
import timedef 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. 技术总结
-
核心原理:修改原生函数返回值的本质是通过hook技术拦截函数调用,用自定义返回值覆盖原逻辑。对于依赖返回值的程序(如验证、权限判断),该方法可快速绕过限制。
-
Frida的优势:作为动态hook工具,Frida无需修改原程序代码或重打包,支持跨平台(Android、iOS等),且通过Java桥接API可轻松操作Java层函数(包括原生函数),降低了hook的技术门槛。
-
关键注意事项:
- 需准确获取类的完整路径(包名+类名)和函数名,否则无法定位目标;
- 进程附加时机需合理(建议在进程启动初期注入,避免函数已被调用);
- 若函数有参数,需在重写实现时保持参数列表一致(即使不使用参数)。
-
适用场景:适用于逆向分析中需要绕过原生层验证、调试函数依赖关系、或临时修改程序行为的场景,是移动端逆向工程的基础技能之一。