这篇文章上次修改于 1139 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

Frida启动方式

手机环境配置

建议先获取系统root权限,在开启远程adb服务

adb root
adb disable-verity
adb reboot
adb root
adb remount
# 第一种方式
adb shell 'echo "service.adb.tcp.port=5555" >>/system/build.prop'
# 第二种方式
adb tcpip 5555

开启frida server进程,指定启动的端口

adb shell
nohup /data/mobile-deploy/hluda-server-14.2.13-android-arm -l 0.0.0.0:27042 > /sdcard/frida-server.log 2>&1 &
nohup /data/mobile-deploy/hluda-server-14.2.13-android-arm64 -l 0.0.0.0:27042 > /sdcard/frida-server.log 2>&1 &
# 电脑上执行,不执行的话电脑连接不上frida server
adb forward tcp:27042 tcp:27042

Spawn方式启动

spawn方式是frida重新打开要分析的应用.

# USB连接设备时的连接方式
frida -U -l LOAD_SCRIPT -f PACKAGE_NAME --no-pause
# 远程连接时方式
frida -H IP:PORT -l LOAD_SCRIPT -f PACKAGE_NAME --no-pause

Attach方式启动

attach 方式是frida注入已经打开的应用进程中

# USB连接设备时的连接方式
frida -U PACKAGE_NAME -l LOAD_SCRIPT --no-pause
# 远程连接时方式
frida -H IP:PORT PACKAGE_NAME -l LOAD_SCRIPT --no-pause

延迟启动

第一种方式、spawn启动时不加 —-no-pause 参数

# USB连接设备时的连接方式
frida -U -l LOAD_SCRIPT -f spawn_FILE
# 远程连接时方式
frida -H IP:PORT -l LOAD_SCRIPT -f spawn_FILE

第二种方式、延迟启动

setTimeout(main, 5000)

Frida基本使用

Java层的Hook都是从Java.perform开始的。

function hook_Java() {
    Java.perform(function () {
        //code
    });
}

Frida hook类

frida的启动命令

frida -H 192.168.0.100:27042 com.android.settings -l hook_Java.js --no-pause

枚举所有的类

function enumerate_LoadedClasses() {
    Java.perform(function () {
        // 方法一
        let clazz_list = Java.enumerateLoadedClassesSync();
        for (const clazz of clazz_list) {
            console.log("[*] found instance of '" + clazz.toString() + "'");
        }

        // 方法二
        Java.enumerateLoadedClasses({
            onMatch: function (args) {
                console.log("[*] found instance of '" + args.toString() + "'");
            }, onComplete: function () {
                console.log("found done");
            }
        });
    });
}

枚举类所有方法

function enumerate_methods() {
    Java.perform(function (){
        let ViewCompat = Java.use("android.support.v4.view.ViewCompat");
        let method_list = ViewCompat.class.getDeclaredMethods()
        for (const clazz of method_list) {
            console.log("[*] found method of '"+clazz.toString()+"'");
        }
    });
}

hook构造函数

$init 构造方法;$new 创建对象;$dispose() 手动销毁对象.

hook的目标:

package com.tlamb96.kgbmessenger.b;

/* loaded from: classes.dex */
public class a {

    /* renamed from: a  reason: collision with root package name */
    private int f448a;
    private String b;
    private String c;
    private boolean d;
        
        // 构造方法
    public a(int i, String str, String str2, boolean z) {
        this.f448a = i;
        this.b = str;
        this.c = str2;
        this.d = z;
    }

    public String a() {
        return this.b;
    }

    public int b() {
        return this.f448a;
    }

    public String c() {
        return this.c;
    }

    public boolean d() {
        return this.d;
    }
}

如果目标类的构造方法就一个,hook目标类的构造方法如下:

//
function hook_constructor(clazz) {
    // hook构造函数
    Java.perform(function () {
        let clazz_name = Java.use(clazz);
        // let className = "com.tlamb96.kgbmessenger.b.a";
        let a = Java.use(className);
        a.$init.implementation = function (i, str, str2, z) {
            console.log("a.$init: ", i, str, str2, z);
            //print_stack();
        }
    });
}

如果构造函数有重载

构造方法

// 构造方法
    public a(int i, String str, String str2, boolean z) {
        this.f448a = i;
        this.b = str;
        this.c = str2;
        this.d = z;
    }
        public a(int i, String str) {
                this.f448a = i;
                this.b = str;
            }

hood代码

function hook_constructor(clazz) {
    // hook构造函数
    Java.perform(function () {
        let clazz_name = Java.use(clazz);
        // let className = "com.tlamb96.kgbmessenger.b.a";
        let a = Java.use(className);
        a.$init.overload("int","java.lang.String").implementation = function (i, str) {
            console.log("a.$init: ", i, str);
        }
    });
}

修改方法参数和返回值

// 方法
    public String a() {
        return this.b;
    }

    public int b() {
        return this.f448a;
    }

    public String c() {
        return this.c;
    }

        public String c(String str) {
        return str;
    }

    public boolean d() {
        return this.d;
    }

与hook构造方法类似,不同的是将$init 替换为具体的方法即可.比如我们hook c方法。因为c 方法有重载,我们需要使用overload()方法,在方法内部指定要hook方法具体参数。

function hook_method(clazz_name) {
    Java.perform(function (){
        let clazz = Java.use(clazz_name);
        clazz_name.c.overload('java.lang.String').implementation = function (str) {
            let result = this.c(str);
            console.log('clazz_name.c', str);
            return result;
        };
    });
}

修改参数

function hook_method(clazz_name) {
    Java.perform(function (){
        let clazz = Java.use(clazz_name);
        clazz_name.c.overload('java.lang.String').implementation = function (str) {
            console.log("before", str);
            str = "hello";
                        console.log("after", str);
            let result = this.c(str);
            console.log('clazz_name.c', result);
            return result;
        };
    });
}

修改返回值

function hook_method(clazz_name) {
    Java.perform(function (){
        let clazz = Java.use(clazz_name);
        clazz_name.c.overload('java.lang.String').implementation = function (str) {
            let result = this.c(str);
            console.log('clazz_name.c', result);
            result = "我自己改的值";
            return result;
        };
    });
}

修改类成员变量值

静态成员变量直接使用Java.use ;

修改非静态类成员变量时使用Java.choose(className,callbacks).

设置和函数名相同的成员变量需要在属性前面 加一个 下划线_

用于查找堆中指定类的实例。获得实例后可以调用实例的函数。声明为:Java.choose(className,callbacks) demo如下

Java.choose("com.android.bluetooth",{
    onMatch:function(){
        //onMatch回调会在找到类的实例后调用,也就是说内存中有多少实例,就会调用多少次
    },
    onComplete:function(){
        //onComplete回调会在所有onMatch完成后调用
    }
})

要hook的Java代码如下

package cool.find.testnative2;

import android.util.Log;

public class SecondActivy {
    private static boolean static_bool_var = false;
    private boolean bool_var = false;
    private boolean same_name_bool_var = false;

    private void same_name_bool_var() {
        Log.d("Frida", static_bool_var + " " + this.bool_var + " " + this.same_name_bool_var);
    }

}
function hook_variable_member() {
    Java.perform(function () {
        var SecondActivy = Java.use("cool.find.testnative2.SecondActivy");
        console.log("SecondActivy.static_bool_var:", SecondActivy.static_bool_var.value);
        SecondActivy.static_bool_var.value = true;  //设置静态成员变量的值
        console.log("SecondActivy.static_bool_var:", FridaActivity3.static_bool_var.value);
        // 非静态成员变量则使用如下方法, 也可以修改静态成员变量
        Java.choose("cool.find.testnative2.SecondActivy", {
            onMatch: function (instance) {
                console.log("SecondActivy.bool_var:", instance.bool_var.value);
                instance.bool_var.value = true;
                console.log("SecondActivy.bool_var:", instance.bool_var.value);
                
                console.log("SecondActivy._same_name_bool_var:", instance._same_name_bool_var.value);
                instance._same_name_bool_var.value = true;
                console.log("SecondActivy._same_name_bool_var:", instance._same_name_bool_var.value);
            }, onComplete: function () {
            }
        })
    });
}

hook动态加载的类

具体的例子可以参考看雪的帖子

https://bbs.pediy.com/thread-258772.htm

https://bbs.pediy.com/thread-246767.htm

function hook_dyn_dex() {
    Java.perform(function () {
        Java.enumerateClassLoaders({
            "onMatch": function (loader) {
                if (loader.toString().startsWith("com.tencent.shadow.core.loader.classloaders.PluginClassLoader")) {
                    Java.classFactory.loader = loader; // 将当前class factory中的loader指定为我们需要的
                }
            },
            "onComplete": function () {
                console.log("success");
            }
        });

        // 此处需要使用Java.classFactory.use
        var videoController = Java.classFactory.use("com.iqiyi.plugin.widget.dkplayer.controller.VideoController");
        videoController.setVip.implementation = function () {
            console.log("hook setVip");
            this.setVip(true);
        };
    });
}

加载外部dex文件

生成dex

//将class文件打包成jar包
jar -cvf result.jar *.class
// 将jar包转换成dex文件
~/Library/Android/sdk/build-tools/28.0.3/dx --dex --output=result.dex result.jar
function load_external_dex() {
    // 打开 dex 文件
    let dex = Java.openClassFile('/data/local/tmp/ddex.dex');
    Java.perform(function () {
        // 加载 dex
        dex.load();
        // 进行hook
        Java.use('com.tlamb96.kgbmessenger.LoginActivity').j.implementation = function () {
            return true;
        };
    })
}

打印Java层调用栈

function print_stack_trace() {
    let Thread = Java.use("java.lang.Thread");
    let th = Java.cast(Thread.currentThread(), Thread);
    let stack = th.getStackTrace();
    let e = null;
    for (let i = 0; i < stack.length; i++) {
        console.warn("\t" + stack[i].getClassName() + "." + stack[i].getMethodName() + "(" + stack[i].getFileName() + ")");
    }
}

复杂Java类型的打印、转换

import exp from "constants";

export namespace Utils {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

    // Use a lookup table to find the index.
    const lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);
    for (let i = 0; i < chars.length; i++) {
        lookup[chars.charCodeAt(i)] = i;
    }

    export function strToBase64(str: string) {
        return new Buffer(str).toString("base64");
    }

    export function base64ToStr(base64_str: string) {
        let result = Buffer.from(base64_str, 'base64');
        return result.toString('utf-8');
    }

    export function strToHexStr(str: string) {
        let res = new Buffer(str);
        return res.toString('hex');
    }

    export function hexStrToStr(str: string) {
        let res = new Buffer(str, 'hex');

        return res.toString('utf-8');
    }

    export function strToHexArrayBuffer(input: string) {
        if (typeof input !== 'string') {
            throw new TypeError('Expected input to be a string')
        }

        if ((input.length % 2) !== 0) {
            throw new RangeError('Expected string to be an even number of characters')
        }

        const view = new Uint8Array(input.length / 2)

        for (let i = 0; i < input.length; i += 2) {
            view[i / 2] = parseInt(input.substring(i, i + 2), 16)
        }

        return view.buffer;
    }

    export function ArrayBufferToStr(arrayBuffer: ArrayBuffer) {
        if (typeof arrayBuffer !== 'object' || arrayBuffer === null || typeof arrayBuffer.byteLength !== 'number') {
            throw new TypeError('Expected input to be an ArrayBuffer')
        }

        var view = new Uint8Array(arrayBuffer)
        var result = ''
        var value

        for (var i = 0; i < view.length; i++) {
            value = view[i].toString(16)
            result += (value.length === 1 ? '0' + value : value)
        }

        return result
    }

    export function stringToArrayBuffer(str: string) {
        const buffer = new ArrayBuffer(str.length);
        const bytes = new Uint8Array(buffer);

        str.split('').forEach(function (str, i) {
            bytes[i] = str.charCodeAt(0);
        });

        return buffer;
    }

    export function arrayBufferToString(buffer: ArrayBuffer) {
        // let uint8 = new Uint8Array(buffer);
        return Buffer.from(buffer).toString('utf8');
    }

    export function arrayBufferToBase64Str(arraybuffer: ArrayBuffer): string {
        let bytes = new Uint8Array(arraybuffer),
            i,
            len = bytes.length,
            base64 = '';

        for (i = 0; i < len; i += 3) {
            base64 += chars[bytes[i] >> 2];
            base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
            base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
            base64 += chars[bytes[i + 2] & 63];
        }

        if (len % 3 === 2) {
            base64 = base64.substring(0, base64.length - 1) + '=';
        } else if (len % 3 === 1) {
            base64 = base64.substring(0, base64.length - 2) + '==';
        }

        return base64;
    }

    export function base64StrToArrayBuffer(base64: string): ArrayBuffer {
        let bufferLength = base64.length * 0.75,
            len = base64.length,
            i,
            p = 0,
            encoded1,
            encoded2,
            encoded3,
            encoded4;

        if (base64[base64.length - 1] === '=') {
            bufferLength--;
            if (base64[base64.length - 2] === '=') {
                bufferLength--;
            }
        }

        const arraybuffer = new ArrayBuffer(bufferLength),
            bytes = new Uint8Array(arraybuffer);

        for (i = 0; i < len; i += 4) {
            encoded1 = lookup[base64.charCodeAt(i)];
            encoded2 = lookup[base64.charCodeAt(i + 1)];
            encoded3 = lookup[base64.charCodeAt(i + 2)];
            encoded4 = lookup[base64.charCodeAt(i + 3)];

            bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
            bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
            bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
        }

        return arraybuffer;
    }
}