安卓开发必备:Frida 使用与安全防护全攻略

259 阅读4分钟

在安卓开发的世界里,Frida 是一个极具威力的工具,它能帮助开发者轻松实现代码插桩、动态调试等功能,但同时也可能被恶意利用,威胁应用安全。今天,就让我们深入探索 Frida 的使用技巧与防护策略,让你在开发中游刃有余,同时筑牢安全防线!

一、Frida 简介:开发者的强大助力

Frida 是一个基于 Python 和 JavaScript 的动态代码插桩工具,它支持 Windows、macOS、GNU/Linux、iOS、Android 等众多平台,能够将 JavaScript 代码片段或自定义库注入到原生应用中,实现对应用运行时的动态修改和调试。无论是快速定位问题、测试功能,还是进行安全研究,Frida 都能大显身手。

二、环境搭建:轻松上手 Frida

(一)系统环境

  • 推荐使用最新版本的 Python 3.x。
  • 支持 Windows、macOS 和 GNU/Linux 系统。

(二)安装步骤

  1. 打开终端或命令提示符,运行以下命令安装 Frida 及其工具:
    pip install frida
    pip install frida-tools
    
  2. 安装完成后,可以通过运行测试脚本验证安装是否成功。将以下代码保存为 test.py,并在对应的系统环境下运行(macOS 或 GNU/Linux 系统需将代码中的 "notepad.exe" 替换为 "cat",并执行 sudo sysctl kernel.yama.ptrace_scope=0 命令):
    import frida
    
    def on_message(message, data):
        print("[on_message] message:", message, "data:", data)
    
    session = frida.attach("notepad.exe")
    
    script = session.create_script("""
    rpc.exports.enumerateModules = () => {
      return Process.enumerateModules();
    };
    """)
    script.on("message", on_message)
    script.load()
    
    print([m["name"] for m in script.exports.enumerate_modules()])
    
    如果能正常打印出模块名,说明 Frida 已经可以正常使用啦!

三、代码提示:提升开发效率

为了更高效地编写 Frida 脚本,可以借助 VSCode 和 Node.js 来实现代码提示。具体操作如下:

  1. 在工作目录执行以下命令:
    git clone https://github.com/oleavr/frida-agent-example.git
    cd frida-agent-example
    npm install
    
  2. agent/index.ts 文件中编写脚本,即可享受代码提示带来的便利。
  3. 执行 npm run watch 命令,实现脚本更改时的自动编译。
  4. 使用 frida -U -f com.example.android -l _agent.js 命令运行 Frida 脚本。

四、远程环境设置:安卓设备的连接与调试

以安卓为例,要实现远程调试,需要先下载对应版本的 frida-server。具体步骤如下:

  1. 访问 Frida 的 GitHub Release 页面,找到并下载与本地 Frida 版本匹配的 frida-server 文件。
  2. 使用 adb push 命令将 frida-server 传输到设备的临时目录下,例如:
    adb push .\frida-server-17.2.15-android-arm64 /data/local/tmp
    
  3. 修改 frida-server 的权限,使其可执行:
    chmod +x ./frida-server-17.2.15-android-arm64
    
  4. 启动 frida-server
    ./frida-server-17.2.15-android-arm64
    
  5. 在 PC 端执行测试命令,验证 frida-server 是否正常工作:
    frida-trace -U -i open -N com.google.android.apps.maps
    
    如果一切正常,即可开始远程调试安卓应用啦!

五、Frida 操作模式:灵活应对不同需求

(一)CLI 模式(命令行模式)

通过命令行将 JS 脚本注入到进程中,适用于简单的 hook 修改或快速调试场景。例如:

frida -U -f [包名] -l [js脚本]

(二)RPC 模式

借助 Python 脚本实现更复杂的功能,适用于大规模调用场景。以下是一个简单的 RPC 模式示例:

import frida
import sys
import json

def on_message(message, data):
    print(json.dumps(message, indent=2))
        
def main():
    dev = frida.get_usb_device() # 连接安卓设备
    pid = dev.spawn("com.example.myapplication") # 启动目标应用
    session = dev.attach(pid) # 附加到应用进程
    js = open("hook.js", "r", encoding="utf8", newline="\n").read() # 加载 JS 脚本
    script = session.create_script(js)
    script.on("message", on_message)
    script.load()
    dev.resume(pid) # 恢复应用运行
    sys.stdin.read()

if __name__ == "__main__":
    main()

六、Frida 脚本编写:实现强大功能的关键

(一)Java 层 Hook

1. Hook 脚本模板

function main() {
    Java.perform(
        function() {
            console.log("My Hook running...");
            // 在此处编写 Hook 代码
        }
    );
}

setImmediate(main);

2. 示例代码分析

以下是一个简单的安卓应用代码示例,我们将基于此进行 Hook 操作:

public class MainActivity extends AppCompatActivity {  
    private static final String TAG = "MainActivity";  
    private int foo = 100;  
    private String prefixTag = "Hello";  

    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        findViewById(R.id.btnClickMe).setOnClickListener(new View.OnClickListener() {  
            @Override  
            public void onClick(View v) {  
                onClickClickMe(v);  
            }  
        });  
    }  

    public static int staticMethod(String title){  
        Log.e(TAG,  "staticMethod " + title);  
        return title.length();  
    }  

    protected void onClickClickMe(View sender){  
        Log.e(TAG,  "TagNumber: " + Integer.toString(foo) + " prefix " + prefixTag + " "+ foo("MainActivity"));  
        Log.e(TAG,  "TagNumber: " + Integer.toString(foo) + " prefix " + prefixTag + " "+ someMethod("MainActivity"));  
        Log.e(TAG,  "TagNumber: " + Integer.toString(foo) + " prefix " + prefixTag + " "+ someMethod("MainActivity", 65535));  

        InnerClass innerClass = new InnerClass("InnerClass");  
        Log.e(TAG,  "TagNumber: " + Integer.toString(foo) + " prefix " + prefixTag + " "+ innerClass.someMethodInInnerClass("innerClass"));  
    }  

    public String foo(String arg) {  
        return arg + "foo(String arg)";  
    }  

    public String someMethod(String arg) {  
        return arg + "someMethod(String arg)";  
    }  

    public String someMethod(String arg, int number) {  
        return arg + Integer.toString(number) + "someMethod(String arg)" ;  
    }  

    public class InnerClass {  
        private String tip;  
        public  InnerClass(String tip) {  
            this.tip = tip;  
        }  
        public String someMethodInInnerClass(String arg) {  
            return tip + ":" + arg + "someMethodInInnerClass(String arg)";  
        }  
    }  
}

3. Hook 普通方法

function hookNormalMethod() {
    let targetClass = Java.use("com.example.myapplication.MainActivity");
    targetClass.foo.implementation = function(str) {
        console.log("[+] hookNormalMethod arg1:" + str);
        let retVal = this.foo(str);
        console.log("[+] hookNormalMethod retVal:" + retVal);
        return retVal;
    }
}

4. Hook 重载方法

function hookOverrideMethod_string(){
    let targetClass = Java.use("com.example.myapplication.MainActivity");
    targetClass.someMethod.overload('java.lang.String').implementation = function(str) {
        console.log("[+] hookOverrideMethod_string arg1:" + str);
        let retVal = this.someMethod(str);
        console.log("[+] hookOverrideMethod_string retVal:" + retVal);
        return retVal;
    }
}

function hookOverrideMethod_string_int(){
    let targetClass = Java.use("com.example.myapplication.MainActivity");
    targetClass.someMethod.overload('java.lang.String', 'int').implementation = function(str, number) {
        console.log("[+] hookOverrideMethod_string_int arg1:" + str + "arg2:" + number.toString(16));
        let retVal = this.someMethod(str, number);
        console.log("[+] hookOverrideMethod_string_int retVal:" + retVal);
        return retVal;
    }
}

5. Hook 内部类的构造方法

function hookInnerInitMethod() {
    let targetClass = Java.use("com.example.myapplication.MainActivity$InnerClass");
    targetClass.$init.overload('com.example.myapplication.MainActivity', 'java.lang.String').implementation = function(extthis, str) {
        
        let myarg2 = str + "-.-   "
        console.log("[+] hookInnerInitMethod arg1: " + str + " myarg1: " + myarg2) ;
        let retVal = this.$init(extthis, myarg2);
        console.log("[+] hookInnerInitMethod retVal:" + retVal);
        return retVal;
    }

}

6. 枚举类与类方法

function enumerateClassesMethod(){
    
    console.log(`------------------enumerateLoadedClasses------------------`)
    Java.enumerateLoadedClasses({
        onMatch:function(name, handle){
            if(name.indexOf("com.example.myapplication.MainActivity") != -1) {
                console.log("targetObject: " + name);
                let clazz = Java.use(name)
                console.log(clazz);
                let methods =  clazz.class.getDeclaredMethods();
                console.log(methods)
            console.log(`------------------------------------`)
            console.log()
            }
        },
        onComplete:function(){}
    })
    
    console.log(`---------------------------------------------------------`)
    console.log(`---------------------------------------------------------`)
    console.log()
}

7. 枚举并 Hook 匹配到的方法

function HookClassAllMethod(){
    
    console.log(`------------------HookClassAllMethod------------------`)
    
    let targetClass = Java.use("com.example.myapplication.MainActivity");
    let methods = targetClass.class.getDeclaredMethods();
    for(let i = 0; i < methods.length; ++i){
        let name = methods[i].getName();
        console.log(`hook method name: ${name}`);
        for(let j = 0; j < targetClass[name].overloads.length; ++j){
            targetClass[name].overloads[j].implementation = function() {
                
                console.log(`method name: ${name}`);
                for(let k=0; k<arguments.length; k++){
                    console.log(`   args:${k} name: ${arguments[k]}`);
                }
                return this[name].apply(this,arguments);
            }
        }
    }
    
    console.log(`---------------------------------------------------------`)
    console.log(`---------------------------------------------------------`)
    console.log()
}

8. 修改字段

注意:字段名与函数名相同时,需要在字段名前面加个下划线用于区分。

function modifyObjectField() {
    let targetClass = Java.use("com.example.myapplication.MainActivity");

    targetClass.TAG.value = "[MainActivity]";
    console.log(targetClass.TAG.value);
    Java.choose("com.example.myapplication.MainActivity", {
        onMatch: function(obj) {
            // console.log(obj)
            obj._foo.value = 88;//字段名与函数名相同 前面加个下划线
            obj.prefixTag.value = "Hello World ";

        },
        onComplete:function(){}
    });
}

9. 主调调用

如果想要创建 Java 类的实例,可以使用$new()方法。

function invokeMethod(){
    let targetClass = Java.use("com.example