FridaStudy

一. Frida简介

官网https://frida.re

frida是一款常用的hook框架,支持android/ios/windows/macos等多种平台

可以实现java层和native层的函数级hook,以及native层的指令级patch等操作

一般在工作机上安装frida,编写脚本后使用命令行工具,启动frida并将脚本注入到靶机指定进程

同时需要在靶机上安装相同版本的frida-server用于监听工作机传来的操作指令

二. Frida环境配置

  1. 安装frida

    参考下方的frida对应关系表安装指定版本的frida和frida-tools

    建议使用anaconda的python虚拟环境,针对不同靶机可能需要多个版本frida

    不同版本的frida对python版本要求不同且不能共存

    1
    2
    3
    4
    
    pip install frida 
    pip install frida-tools
    #可以通过frida=version的操作安装指定版本 
    #例如pip install frida=12.8.0
    
  2. 配置开发环境

    下载WebStorm https://www.jetbrains.com/zh-cn/webstorm/download js IDE

    下载Node.js https://nodejs.org/zh-cn/download/prebuilt-binaries js解释器

    frida代码补全提示配置:

    创建项目后,在根目录打开cmd ,执行以下代码后安装代码补全包自动开启

    1
    
    npm i @types/frida-gum
    

frida对应关系表

Android版本 frida版本 frida-tools版本 python版本 objection版本
android5-6 frida12.3.6 python=3.7
andorid7-8 frida12.8.0 frida-tools=5.3.0 python=3.8 objection=1.8.4
andorid9-10 frida14.2.14 frida-tools=9.2.2 python=3.8 objection=1.11.0
andorid10-12 frida15.2.2 frida-tools=11.0.0 python=3.9/3.10
android10-12 frida15+ python=3.9/3.10
android12+ frida16.0.19 python=3.10/3.11

frida12 最好在<=安卓8.1的版本上用,最好别在>=安卓10上用 frida14 最好在>=安卓10的版本上用,别在<=8.1的版本上用

三. Frida常用命令

frida命令参数

-U: 指定USB设备

-D: 指定id设备

-R: 远程frida-server

-H: 指定远程frida-server地址 ip:port

-f: swapn模式

-p: attach的目标pid

-n: attach的目标名称

-F: attach模式(默认)

-l: 加载脚本

-o: 输出日志到指定文件

–no-pause: spawn启动app时不挂起,直接运行

列出手机进程信息

1
 frida-ps -Uai

-U: 指定USB设备

-a: 列出所有进程

-i: 显示进程详细信息

frida注入进程

附加注入进程 直接注入后可使用控制台调用frida api进行交互

1
frida -U <package_name>

附加注入进程并加载脚本

1
frida -U <package_name> -l <script.js>

swapn注入进程并加载脚本

1
frida -U -f <package_name> -l <script.js>

四. Frida-labs

用于入门熟悉frida的基本hook操作,包括java和native层hook

参考https://github.com/DERE-ad2001/Frida-labs和[原创]Frida-Hook-Java层操作大全

lab1 Overload&Implement Method

hook重载函数并实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function main(){
    Java.perform(function (){
        var mainActivity=Java.use("com.ad2001.frida0x1.MainActivity");
        mainActivity.check.overload("int","int").implementation=function (x,y){
            console.log("Origin i=",x,"i2=",y)
            this.check(2,8)
        }
    })
}
setTimeout(main,500)

lab2 CallStaticMethod

MainActivity的静态方法get_flag(),当参数a==4919时getflag

获取MainActivity类,主动调用get_flag()即可

1
2
3
4
5
6
7
function main(){
    Java.perform(function (){
        var mainActivity=Java.use("com.ad2001.frida0x2.MainActivity");
        mainActivity.get_flag(4919)
    })
}
setTimeout(main,500)

lab3 ModifyStaticValue

当Checker.code==512时getflag

Checker.code为静态变量

获取Checker类,通过.value直接修改静态变量code的值即可

1
2
3
4
5
6
7
8
function main(){
    Java.perform(function (){
        var Checker=Java.use("com.ad2001.frida0x3.Checker")
        console.log(Checker)
        Checker.code.value=512
    })
}
setTimeout(main,500)

lab4 CallInstanceMethod

MainActivity没有getflag逻辑,在Check类中有实例方法get_flag

获取Check类并调用new方法创建实例,之后调用get_flag

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function main(){
    Java.perform(function (){
        var Check=Java.use("com.ad2001.frida0x4.Check")
        console.log(Check)
        var instance=Check.$new()
        console.log(instance)
        var flag=instance.get_flag(1337)
        console.log(flag)
    })
}
setTimeout(main,500)

lab5 FindInstance

MainActivity的实例方法flag

在app启动时会创建MainActivity实例,此时我们如果使用Java.use获取MainActivity类并调用new创建实例,会导致错误

因为Android的UI组件依赖程序上下文运行,并且已经和MainActivity实例绑定,再次创建实例显然不行

通过Java.choose搜索实例对象,之后通过onMatch()回调进行hook逻辑,onComplete()进行结束处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function main(){
    Java.perform(function (){
        Java.choose("com.ad2001.frida0x5.MainActivity",{
            onMatch: function (instance){
                console.log(instance);
                instance.flag(1337)
            },
            onComplete: function (){
                console.log("unhook")
            }
        })
    })
}
setTimeout(main,500)

lab6 NewInstance

MainActivity.get_flag()实例方法要求传入一个Checker实例对象

当实例对象的num1==1234且num2==4321时获取flag

Checker.num1/num2均为实例变量

搜索MainActivity实例,之后创建Checker实例修改num变量,调用MainActivity.get_flag()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function getclass(className) {
    return Java.use(className);
}
function main(){
    Java.perform(function (){
        Java.choose("com.ad2001.frida0x6.MainActivity",{
            onMatch: function (instance){
                console.log("hook success")
                console.log(instance);
                var Checker=getclass("com.ad2001.frida0x6.Checker").$new()
                console.log(Checker)
                Checker.num1.value=1234
                Checker.num2.value=4321
                instance.get_flag(Checker)
            },
            onComplete: function (){
                console.log("unhook")
            }
        })
    })
}
setTimeout(main,500)

lab7 HookConstructor

MainActivity创建时生成Checker实例对象,通过构造函数传递123,321,调用flag方法

Checker对象默认的构造函数如下

显然默认情况下构造函数传参不满足getflag条件

frida中<class>.$init表示类的构造函数,我们可以hookChecker的构造函数,修改传入的a,b值

1
2
3
4
5
6
7
8
9
function main(){
    Java.perform(function (){
        Java.use("com.ad2001.frida0x7.Checker").$init.implementation=function (a,b){
            console.log("Origin a=",a," b=",b)
            this.$init(600,600)
        }
    })
}
setImmediate(main)

注意需要以spawn模式启动(保证在MainActivity.onCreate执行前hook)

lab8 HookNaticeFunc

关键函数cmpstr为Native函数,在libfrida0x8.so中定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function main(){
        //获取libc的strcmp地址
        var strcmp_addr=Module.findExportByName("libc.so","strcmp");
        Interceptor.attach(strcmp_addr ,{
            onEnter: function (args){
                var arg0=Memory.readUtf8String(args[0]);//strcmp(str1,str2)
                var flag=Memory.readUtf8String(args[1]);
                //判断参数1确认为点击submit提交输入时,防止输出其他调用处字符串干扰
                if(arg0.includes("hello")){
                    console.log("input: ",arg0,"\nflag: ",flag)
                }
            },
            onLeave:function(retval){
            }
        })
}
setImmediate(main)

lab9 ReplaceNativeFuncRetval

hook到check_flag函数后通过retval.replace修改返回地址即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function main(){
        var check_flag_addr= Module.findExportByName("liba0x9.so","Java_com_ad2001_a0x9_MainActivity_check_1flag")
        Interceptor.attach(check_flag_addr ,{
            onEnter: function (){
                console.log("Hook check_flag success ");
            },
            onLeave:function(retval){
                console.log("origin retval=",retval);
                retval.replace(1337)
            }
        })
}
setImmediate(main)

lab10 CallNativeFunc

MainActivity只能看到stringFromJNI,但跟进发现该函数只返回"Hello Hackers"

搜索发现libfrida0xa.so存在一个导出的get_flag函数,没有被调用

通过基址+偏移得到函数真实地址,再通过NativeFunction创建函数对象,之后直接调用即可再logcat看到flag

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function main(){
        //lib加载基址+get_flag偏移=真实地址
        //get_flag的Native名称为_Z8get_flagii
        var addr = Module.findBaseAddress("libfrida0xa.so").add(0x1dd60)
        var get_flag_ptr = new NativePointer(addr);//Native层指针
        //获取Native函数对象 参数分别为函数指针,返回值类型,参数类型列表
        const get_flag = new NativeFunction(get_flag_ptr, 'void', ['int', 'int']);
        get_flag(1,2);
}
setTimeout(main,500)

lab11 PatchNativeInstruction

调用Native函数getFlag

此处为永假跳转,patch掉if跳转逻辑即可绕过

获取到目标patch地址后,通过Memory.protect取消页面保护, 再创建对应架构的Writer,通过writer修改指令即可

方法1 直接nop b.ne

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function main(){
    var addr = Module.findBaseAddress("libfrida0xb.so").add(0x15248);//b.ne指令地址
    Memory.protect(addr, 0x1000, "rwx");//修改页面保护属性为可读可写可执行
    var writer = new Arm64Writer(addr);  // 创建Writer对象
    try {
        writer.putNop()//直接nop b.ne跳转 注意arm64的nop=0xD503201F,长度为4,只需要调用一次即可

        writer.flush();//刷新缓冲区,保证成功写入指令
        console.log(`Nop instruction inserted at ${addr}`);
    } finally {
        writer.dispose();
    }
}
setTimeout(main,500)

方法2 修改b.ne跳转地址

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function main(){
    var addr = Module.findBaseAddress("libfrida0xb.so").add(0x15248);//b.ne指令地址
    Memory.protect(addr, 0x1000, "rwx");//修改页面保护属性为可读可写可执行
    var writer = new Arm64Writer(addr);  // 创建Writer对象
    var target = Module.findBaseAddress("libfrida0xb.so").add(0x15250);//跳转的目标地址
    try {
        writer.putBImm(target); //B target
        writer.flush();//刷新缓冲区,保证成功写入指令
        console.log(`Branch instruction inserted at ${addr}`);
    } finally {
        writer.dispose();
    }
}
setTimeout(main,500)

五. FridaHook进阶

Frida的数据结构

数组

frida会自动将js字符串转换为java字符串,但传递基本数据类型时,不会转换成封装类型,frida提供了api进行转换:

Java.array(“Ljava.lang.Object;”…)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function main(){
    Java.perform(function (){
        var Integer=Java.use("java.lang.Integer");
        var Boolean=Java.use("java.lang.Boolean");
        var strarr=Java.array(
            "Ljava.lang.String;",
            ["hello",Boolean.$new(true),Integer.$new(666)]
        )
    })
}
setTimeout(main,500)

打印/枚举操作

Show StackTrace

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function showStacks(){
    console.log("====================StackTraceStart====================");
    console.log(
        Java.use("android.util.Log")
            .getStackTraceString(
                Java.use("java.lang.Throwable").$new()
            )
    );
    console.log("=====================StackTraceEnd=====================");
}

枚举类的所有属性和方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function main(){
    Java.perform(function (){
        var mainActivity=Java.use("com.ad2001.frida0x6.MainActivity")
        //以下均为Java.class自带的方法
        var methods=mainActivity.class.getDeclaredMethods();            //方法
        var constructors=mainActivity.class.getDeclaredConstructors();  //构造函数
        var fields=mainActivity.class.getDeclaredFields();              //字段
        var classes=mainActivity.class.getDeclaredClasses();            //内部类
        console.log("\n==========methods==========");
        for(let i=0;i<methods.length;i++){
            console.log(methods[i].getName());
        }
        console.log("\n==========constructors==========");
        for(let i=0;i<constructors.length;i++){
            console.log(constructors[i].getName());
        }
        console.log("\n==========fields==========");
        for(let i=0;i<fields.length;i++){
            console.log(fields[i].getName());
        }
        console.log("\n==========classes==========");
        for(let i=0;i<classes.length;i++){
            console.log(classes[i].getName());
        }
    })
}
setTimeout(main,500)

打印类/实例的详细信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function printClassMethods(Class){
    var methods=Class.class.getDeclaredMethods();            //方法
    console.log("\n==========methods==========");
    for(var i=0;i<methods.length;i++){
        var methodParams=methods[i].getParameterTypes()
        var methodReturnType=methods[i].getReturnType()
        var methodName=methods[i].getName();
        console.log(methodReturnType+" "+methodName+"("+methodParams+")");
    }
}
function printClassFields(Class){
    var fields=Class.class.getDeclaredFields();              //字段
    console.log("\n==========fields==========");
    for(var i=0;i<fields.length;i++){
        var fieldName=fields[i].getName();
        var fieldType=fields[i].getType();
        console.log(fieldType,fieldName);
    }
}
function printInstanceMethods(Instance){
    printClassMethods(Instance)
}

function printInstanceFields(Instance){
    var fields=Instance.class.getDeclaredFields();              //字段
    console.log("\n==========fields==========");
    for(var i=0;i<fields.length;i++){
        var fieldName=fields[i].getName();
        var fieldType=fields[i].getType();
        var fieldValue=Instance[fieldName].value;
        console.log(fieldType,fieldName,"=",fieldValue);
    }
}

//打印实例详细信息
function printInstance(Instance){
    printInstanceFields(Instance)
    printInstanceMethods(Instance)
}
//打印类的详细信息
function printClass(Class){
    printClassFields(Class)
    printClassMethods(Class)
}

枚举所有已加载类

1
2
3
4
5
6
7
function main(){
    Java.perform(function (){
       var classes=Java.enumerateLoadedClassesSync()//string[]数组
       console.log(classes.join("\n"))//每个元素添加换行打印
    })
}
setTimeout(main,500)

Hook Java类/方法

hook内部类和匿名类

内部类通过$classname, 匿名类通过$classnum, classnum编号和匿名类的创建顺序有关,例如:

1
2
3
4
5
6
7
function main(){
    Java.perform(function (){
       var innerClass=Java.use("com.example.app.MainActivity$InnerClass")
       var anonymousClass=Java.use("com.example.app.MainActivity$6")
    })
}
setTimeout(main,500)

hook类的所有方法

hook类的所有方法,包括方法的所有重载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function main(){
    Java.perform(function (){
        function hookFunc(methodName){
            console.log("hook"+methodName);
            let overloadsArr=mainActivity[methodName].overloads;//获取指定方法的所有重载
            for(let i=0;i<overloadsArr.length;i++){//hook所有重载
                console.log("Hook all overloads start")
                overloadsArr[i].implementation=function(){
                    let params="";
                    for(let j=0;j<arguments.length;j++){
                        params+=arguments[j]+" "//存储参数
                    }
                    console.log(methodName+"is called!Params is:"+params);
                    return this[methodName].apply(this, params);//针对不同重载返回所有参数
                }
            }
        }
        var mainActivity=Java.use("com.ad2001.frida0x6.MainActivity")
        var methods=mainActivity.class.getDeclaredMethods();            //方法
        for(let i=0;i<methods.length;i++){
           let methodName=methods[i].getName();
           hookFunc(methodName)
       }
    })
}
setImmediate(main)

注册java类并调用

Java.registerClass

参照官方文档

使用JSON格式创建类,其中一般方法可以直接写代码,重载方法以数组形式分别编写implementation

注册成功后便可以调用,该api适合简单场景,复杂场景适合注入dex

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const MyWeirdTrustManager = Java.registerClass({
  name: 'com.example.MyWeirdTrustManager',
  superClass: SomeBaseClass,
  implements: [X509TrustManager],
  fields: {
    description: 'java.lang.String',
    limit: 'int',
  },
  methods: {
    $init() {
      console.log('Constructor called');
    },
    checkClientTrusted(chain, authType) {
      console.log('checkClientTrusted');
    },
    checkServerTrusted: [{
      returnType: 'void',
      argumentTypes: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String'],
      implementation(chain, authType) {
        console.log('checkServerTrusted A');
      }
    }, {
      returnType: 'java.util.List',
      argumentTypes: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String', 'java.lang.String'],
      implementation(chain, authType, host) {
        console.log('checkServerTrusted B');
        return null;
      }
    }],
    getAcceptedIssuers() {
      console.log('getAcceptedIssuers');
      return [];
    },
  }
});

hook只在指定函数内生效

部分函数调用可能比较频繁,hook后可能导致崩溃,此时需要再制定范围内hook

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function main(){
    Java.perform(function (){
        var mainActivity=Java.use("com.xiaojianbang.app.MainActivity");
        var stringBuilder=Java.use("java.lang.StringBuilder");
        mainActivity.generateAESKey.implementation=function (){
            console.log("Hook MainActivity generateAESKey Succeeded!");
            //在执行generateAESKey时,对StringBuilder进行hook
            stringBuilder.toString.implementation=function (){
                console.log("Hook StringBuilder Succeeded!")
                var result=this.toString()
                console.log(result)//打印原始数据
                return result;
            }
            var result=this.generateAESKey.apply(this,arguments);//js function.apply(object,argArray) 调用函数
            //结束后取消hook 注意一定要先apply调用hook后的generateAESKey
            //如果unhook stringBuilder 再执行result=this.generateAESKey.apply(this,arguments);
            //会导致内部对stringBuilder的hook失效,只执行第一行log
            stringBuilder.toString.implementation=null;//unhook
            return result;
        };

    })
}

setTimeout(main,500)

定位接口/抽象类的实现类

实现接口/抽象类的类,执行到覆写的方法时必定不走父类

当我们希望定位某个接口/抽象类有哪些实现时,可以遍历所有已加载类

获取对应class后调用getInterfaces()/getSuperclass()获取对应的实现接口数组/父类

接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//寻找指定app中指定接口的所有实现
function findInterfaceImplementations(packageName,interfaceName){
    var classes=Java.enumerateLoadedClassesSync();//遍历所有已加载的类
    //console.log("classes:",classes.join("\n"))
    for(var index in classes){
        var className=classes[index];//js的for-in中,index为下标而非元素
        if(className.indexOf(packageName)===-1)//不是目标app的类,继续搜索
            continue;
        try{
            var clazz=Java.use(className)//获取目标类,并获取所有实现的接口
            var interfaces=clazz.class.getInterfaces();
            if(interfaces.length===0)//实现的接口为0,跳过
                continue;
            //有实现的接口,判断是否为目标接口
            for(var i=0;i<interfaces.length;i++){
                //包括目标接口名,打印所有接口数组信息
                //注意interfaces为Class<?>[]类型,转换为字符串判断
                if(interfaces[i].toString().indexOf(interfaceName)!==-1){
                    console.log(className,interfaces);
                }
            }
        }catch(e){
            console.log("Didn't find class",className);
        }
    }
}

function main(){
    Java.perform(function (){
        findInterfaceImplementations("com.xiaojianbang.app","com.xiaojianbang.app.TestRegisterClass");
    })
}
setTimeout(main,500)

抽象类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function findAbstractClassImplementations(packageName,abstractClassName){
    var classes=Java.enumerateLoadedClassesSync();
    for(var index in classes){
        var className=classes[index];
        if(className.indexOf(packageName)===-1)
            continue;
        try {
            var superClass=Java.use(className).class.getSuperclass();
            if(superClass==null)
                continue;
            //一个类只能继承一个父类,故抽象类无需遍历
            if(superClass.toString().indexOf(abstractClassName)!==-1){
                console.log(className,superClass)
            }
        }catch (e){
            console.log("Didn't find class",className);
        }

    }
}


function main(){
    Java.perform(function (){
        findAbstractClassImplementations("com.xiaojianbang.app","com.xiaojianbang.app.TestAbstract");
    })
}
setTimeout(main,500)

Dex操作

注入dex文件

用js写代码不方便,使用java编写类和方法后编译为dex文件并注入,方便调用

1
2
3
4
5
6
7
8
9
function main(){
    Java.perform(function (){
        Java.openFile("/data/local/tmp/patch.dex").load()//加载dex文件
        var MyClass=Java.use("con.example.app.MyClass")//获取类
        var instance=MyClass.$new()//创建类实例
        instance.hello()//调用方法
    })
}
setTimeout(main,500)

hook动态加载的dex

当使用Java.enumerateLoadedClassesSync() 枚举已经加载的类,但反编译找不到该类时,说明该类可能是动态加载的,此时需要通过ClassLoader获取该类

Java.enumerateClassLoaders() 枚举类加载器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function main(){
    Java.perform(function (){
        Java.enumerateClassLoaders({
            onMatch: function (loader){
                try{
                    Java.classFactory.loader=loader;//替换frida默认的classloader
                    var dynamic=Java.use("com.xiaojianbang.app.Dynamic");
                    console.log("dynamic: ",dynamic);
                    dynamic.sayHello.implementation=function (){
                        console.log("Hook dynamic.sayHello Success!");
                        return "glass"
                    }
                    dynamic.$new().sayHello()
                }
                catch(e){
                    console.log(loader)
                }
            },
            onComplete: function (){
                //所有ClassLoader加载完毕时调用
            }
        })
    })
}

setTimeout(main,500)

hook成功

Hook Java数据结构(待完善)

HashMap

HashMap, LinkedHashMap, HashSet, LinkedHashSet, java.util.concurrent.ConcurrentHashMap

hook put方法示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function main(){
    Java.perform(function (){
        var hashMap=Java.use("java.util.HashMap");
        hashMap.put.implementation=function (key,value){
            //注意String key为java类型
            if(key.equals("username")){
                console.log("hashMap.put: ",key,value);
            }
            return this.put(key,value);
        }
    })
}
setTimeout(main,500)

ArrayList

ArrayList, Vector, LinkList的add(), addAll(), set()等方法

String

getBytes() isEmpty() 构造函数java.lang.StringFactory

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function main(){
    Java.perform(function (){
        var string=Java.use("java.lang.String");
        string.getBytes.overload().implementation=function (){
            showStacks()
            var result=this.getBytes();
            var newStr=string.$new(result)
            console.log("string.getBytes result:",newStr);
            return result;
        }
        string.getBytes.overload("java.lang.String").implementation=function (str){
            showStacks();
            var result=this.getBytes(str);
            var newStr=string.$new(result,str)
            console.log("string.getBytes result:",newStr);
            return result;
        }
    })
}

setTimeout(main,500)

StringFactory

String的构造函数实际上调用的是StringFactory

StringBuilder

Hook 标准加密库(待完善)

标准加密库算法

Hook Android组件(待完善)

Toast

例如输入的账号密码错误时通过toast提示即可快速定位

Native Hook

通过Interceptor.attach可以hooknative层函数,不过此时需要传递函数首地址

1
2
3
4
5
6
7
8
Interceptor.attach(targetAddress, {
    onEnter: function (args) {
        // Modify or log arguments if needed
    },
    onLeave: function (retval) {
        // Modify or log return value if needed
    }
});

相关API

  • Module.enumerateExports(libname)

    遍历lib的导出表,获取导出函数相关信息

  • Module.enumerateImports(libname)

    遍历lib的导入表,获取导入函数相关信息

  • Module.getExportByName(libname,funcname)

    到指定lib查找指定函数地址,未找到返回异常

  • Module.findExportByName(libname,funcname)

    到指定lib查找指定函数地址,未找到返回null

  • Module.getBaseAddress(libname)

    获取lib的基址

主动调用Native函数

1
2
3
var native_adr = new NativePointer(<address_of_the_native_function>);
const native_function = new NativeFunction(native_adr, '<return type>', ['argument_data_type']);
native_function(<arguments>);

指令patch 使用Writer

https://frida.re/docs/javascript-api/#arm64writer

其他操作

frida写文件

frida只有写文件没有读文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function main(){
    Java.perform(function (){
        var ios=new File("/sdcard/Download/helloworld.txt","w")//注意路径权限的问题
        ios.write("Hello World!")
        ios.flush()
        ios.close()
    })
}

setTimeout(main,500)

六. FridaTools

frida常用工具和插件的安装使用方法

Objection

由于frida更新较快,需要保证objection版本的发布时间在frida之后

最新的objection版本为1.11.0,对应的frida版本最大为14.2.14,frida-tools为9.2.2

objection注入成功后进入自带的shell页面-REPL

objection常用指令

注入目标进程(默认通过usb连接)

1
2
objection -g com.android.settings explore 
# -g表示注入 explore表示自动搜索

指定ip和端口注入app

1
objection -N -h ip -p port -g processName explore

启动前执行hook命令

1
objection -g processName explore --startup-command "android hooking watch class <path.className>"

REPL常用指令

jobs

1
2
jobs list #查看hook了多少个类
jobs kill <jobId> # unhook

Java Hook

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#列出所有已加载类
android hooking list classes 
#搜索包含关键字的类
android hooking search classes <keyword> 
#列举类的所有方法
android hooking list classes_methods <path.className> 
# 查看当前app的所有活动/广播接收器/内容提供器/服务
android hooking list activities/receivers/providers/services

#hook类的所有方法(不包括构造方法)
android hooking watch class <path.className> 
#hook类的构造方法
android hooking watch class_method <path.className.$init> 
#hook指定类方法的所有重载
android hooking watch class_method <path.className.methodName> 
#hook单个重载函数
android hooking watch class_method <path.className.methodName> "<argsType>" 
# hook方法的所有参数,返回值,调用栈 --dump-args --dump-return --dump-backtrace

Native Hook

1
2
3
4
5
6
7
8
# 搜索堆中的实例
android heap search instances <className>
# 枚举内存中所有模块
memory list modules
# 枚举模块的所有导出函数
memory list exports <libname.so>
# 结果过多时可以导出到本地文件
memory list exports <libname.so> --json <path.fileName>

其他命令

1
2
3
4
5
6
# 尝试跳转到对应activity
android intent launch_activity 
#关闭ssl校验
android sslpinning disable 
 #关闭root检测
android root disable

WallBreaker

官网https://github.com/hluwa/Wallbreaker

objection加载插件

1
plugin load pluginPath Wallbreaker

搜索类

1
plugin wallbreaker classsearch <pattern>

搜索对象

1
plugin wallbreaker objectsearch

classdump输出类结构

1
plugin wallbreaker classdump <classname> 

objectdump

1
plugin wallbreaker objectdump <instance>

frida-dexdump

七. Frida持久化

  1. 手机上使用termux终端运行frida-server

  2. frida-inject

  3. frida-gadget.so

    优点: 免root使用frida,frida-gadget比较稳定

    缺点: 需要重打包app.可以通过魔改系统,让系统帮忙注入so免去重打包

frida-inject

参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  -D, --device=ID                      connect to device with the given ID
  -f, --file=FILE                      spawn FILE
  -p, --pid=PID                        attach to PID
  -n, --name=NAME                      attach to NAME
  -r, --realm=REALM                    attach in REALM
  -s, --script=JAVASCRIPT_FILENAME
  -R, --runtime=qjs|v8                 Script runtime to use
  -P, --parameters=PARAMETERS_JSON     Parameters as JSON, same as Gadget
  -e, --eternalize                     Eternalize script and exit
  -i, --interactive                    Interact with script through stdin
  --development                        Enable development mode
  --version                            Output version information and exit

推送js脚本到手机端后,使用frida-inject运行脚本注入指定app

1
./frida-inject-16.0.19-android-arm64 -n com.example.app -s script.js -e

frida-gadget

https://github.com/frida/frida/releases?page=4

frida-gadget需要将so文件打包到apk,无需root权限

objection patchapk 参数介绍

1
2
3
4
5
-s 指定目标apk文件
-a 指定so的cpu平台       
-V 指定gadget.so的版本
-D 反编译时跳过资源
-c 指定gadget的config文件路径

环境需求

aapt

jarsigner

apktool,adb

Android系统对应so平台

八. FridaPython

为什么需要使用frida的python库

  1. js的frida操作多用于手工调试,如果需要代码自动化处理则要其他语言接入

  2. frida的算法转发和frida的rpc都需要使用python

    算法转发和rpc能使安卓逆向更加便捷

  3. frida可以实时和python进行数据交互

  4. python的各种哭让代码编写更加简单

注意: python中的js脚本无法实时更新,必须重新运行py脚本

python连接设备

attach方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import frida,sys
jsCode="""
......
"""
device = frida.get_usb_device() #获取usb设备
print("device: ", device)
# attach指定进程 注意进程名不一定等于包名,可使用frida-ps判断
process=device.attach("Frida 0x6")
# process=device.attach(6666)# attach指定pid进程
print("process: ",process)

script=process.create_script(jsCode)# 创建并加载js脚本
script.load()
sys.stdin.readline()# 防止命令行关闭

spawn方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import frida,sys
jsCode="""
......
"""
device = frida.get_usb_device() #获取usb设备
print("device: ", device)
pid=device.spawn("com.ad2001.frida0x6")# spawn启动
print("pid: ", pid)
process=device.attach(pid) # 启动后需要附加
print("process: ",process)
device.resume(pid) #阻塞后恢复运行

script=process.create_script(jsCode)# 创建并执行js脚本
script.load()
sys.stdin.readline()# 防止命令行关闭

连接非标准端口设备

手机端以非标准端口启动frida-server

-l = –listen 指定监听地址 0.0.0.0:8888表示任意ip的8888端口

1
./data/local/tmp/frida-server-16.0.19-android-arm64 -l 0.0.0.0:8888

通过device_manager添加远程设备

1
2
3
manager=frida.get_device_manager()
device=manager.add_remote_device("192.168.189.179:8888")
process=device.attach("Frida 0x6")

frida与python交互

send

js脚本中使用send发送数据

python中使用script.on(‘message’, messageFunc)注册消息处理函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# -*- coding: UTF-8 -*-
import frida, sys

jsCode = """
    Java.perform(function(){
        var RequestUtil = Java.use('com.dodonew.online.http.RequestUtil');
        RequestUtil.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(a, b, c){
            console.log('data: ', a);
            console.log('desKey: ', b);
            console.log('desIV: ', c);
            var retval = this.encodeDesMap(a, b, c);
            console.log('retval: ', retval);
            return retval;
        }
        var Utils = Java.use('com.dodonew.online.util.Utils');
        Utils.md5.implementation = function(a){
            console.log('MD5 string: ', a);
            var retval = this.md5(a);
            send(retval);
            return retval;
        }
    });
"""

# 接收send发送来的数据用于处理 注意必须为2个参数
def messageFunc(message, data):
   # print(message)
   # 当type为send时打印
   if message["type"] == 'send':
       print(u"[*] {0}".format(message['payload']))
   else:
       print(message)

device = frida.get_usb_device()
process = device.attach('嘟嘟牛在线')
script = process.create_script(jsCode)
script.on('message', messageFunc) #注册message事件,参数1不能改,参数2随意
script.load()
print("Run Script")
sys.stdin.read()

效果如下,成功接收消息并打印

recv

python中使用script.post( {name : data} )发送数据

js中使用recv(function(obj) { console.log(obj.data) } ).wait()等待接收并进行消息处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# -*- coding: UTF-8 -*-
import frida, sys
import time

jsCode = """
    Java.perform(function(){
        var RequestUtil = Java.use('com.dodonew.online.http.RequestUtil');
        RequestUtil.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(a, b, c){
            console.log('data: ', a);
            console.log('desKey: ', b);
            console.log('desIV: ', c);
            var retval = this.encodeDesMap(a, b, c);
            console.log('retval: ', retval);
            return retval;
        }
        var Utils = Java.use('com.dodonew.online.util.Utils');
        Utils.md5.implementation = function(a){
            console.log('MD5 string: ', a);
            var retval = this.md5(a);
            send(retval);
            //接收消息并处理
            recv(function(obj){
                console.log(JSON.stringify(obj));
                console.log("Python:", obj.data);
                retval = obj.data;
            }).wait();
            return retval;
        }
    });
"""


def messageFunc(message, data):
   print(message)
   if message["type"] == 'send':
       print(u"[*] {0}".format(message['payload']))
       time.sleep(10)
       # 发送数据给js 此处的script是process.create_script后的变量script
       script.post({"data": "0e8315152843b943563031945032e957"})
   else:
       print(message)


process = frida.get_usb_device().attach('嘟嘟牛在线')
script = process.create_script(jsCode)
script.on('message', messageFunc) #注册消息事件
script.load()
print("Run Script")
sys.stdin.read()

效果如下

frida RPC

RPC即远程调用

js代码中使用rpc.exports={

​ rpcfunc : jsfunc

};

导出rpc接口

python代码中使用script.exports_sync.rpcfunc()调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# -*- coding: UTF-8 -*-
import frida, sys,time

jsCode = """
    Java.perform(function(){
        var RequestUtil = Java.use('com.dodonew.online.http.RequestUtil');
        RequestUtil.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(a, b, c){
            console.log('data: ', a);
            console.log('desKey: ', b);
            console.log('desIV: ', c);
            var retval = this.encodeDesMap(a, b, c);
            console.log('retval: ', retval);
            return retval;
        }
        var Utils = Java.use('com.dodonew.online.util.Utils');
        Utils.md5.implementation = function(a){
            console.log('MD5 string: ', a);
            var retval = this.md5(a);
            console.log('retval: ', retval);
            return retval;
        }
    });

    //用于主动调用
    function test(data){
        var result = "";
        Java.perform(function(){
            result = Java.use('com.dodonew.online.util.Utils').md5(data);
        });
        return result;
    }

    //通过rpc.exports导出接口rpcFunc
    rpc.exports = {
        rpcfunc: test //rpc接口和js函数映射
    };

"""
# 注意js导出的rpcFunc在python中的写法为rpc_func,即大写字母前使用下划线

device = frida.get_usb_device()
print("device: ", device)
pid = device.spawn("com.dodonew.online")  # 以挂起方式创建进程
print("pid: ", pid)
process = device.attach(pid)
print("process: ", process)
script = process.create_script(jsCode)
script.load()
device.resume(pid)  # 加载完脚本, 恢复进程运行
time.sleep(1) # 休眠1秒,防止app未加载完毕就主动调用
# python主动调用rpc函数 通过script.exports_sync.rpcfunc()
result = script.exports_sync.rpcfunc('equtype=ANDROID&loginImei=Androidnull&timeStamp=1626790668522&userPwd=a12345678&username=15968079477&key=sdlkjsdljf0j2fsjk')
print(result)
print("Run Script")
sys.stdin.read()

效果如下

frida 算法转发

有时我们可能想用其他语言实现rpc,但是其他语言不一定有frida的rpc库,我们可以把python作为中间层,用python开启一个服务,封装并提供接口,这样就可以让别的语言访问

首先需要两个库运行web服务

1
2
pip install uvicorn
pip install fastapi

代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import requests, json
import frida
import uvicorn
import fastapi
jsCode="""
  function hookTest(username, passward){
    var result;
    Java.perform(function(){
        var time = new Date().getTime();
        var signData = 'equtype=ANDROID&loginImei=Android352689082129358&timeStamp=' +
            time + '&userPwd=' + passward + '&username=' + username + '&key=sdlkjsdljf0j2fsjk';
        var Utils = Java.use('com.dodonew.online.util.Utils');
        var sign = Utils.md5(signData).toUpperCase();
        console.log('sign: ', sign);

        var encryptData = '{"equtype":"ANDROID","loginImei":"Android352689082129358","sign":"'+
            sign +'","timeStamp":"'+ time +'","userPwd":"' + passward + '","username":"' + username + '"}';
        var RequestUtil = Java.use('com.dodonew.online.http.RequestUtil');
        var Encrypt = RequestUtil.encodeDesMap(encryptData, '65102933', '32028092');
        console.log('Encrypt: ', Encrypt);
        result = Encrypt;
    });
    return result;
}
rpc.exports = {
    rpcfunc: hookTest
};
"""

process = frida.get_device_manager().add_remote_device('192.168.189.179:27042').attach("嘟嘟牛在线")
script = process.create_script(jsCode)
print('[*] Running Script')
script.load()
cipherText = script.exports_sync.rpcfunc('12345678901', 'a123456789')# 测试脚本是否成功运行
print(cipherText)

# 开启web服务处理请求
app = fastapi.FastAPI()# 创建FastAPI实例

@app.get("/get")# 定义一个get请求的路由,收到get请求后执行下面的异步函数
async def getEchoApi(item_id, item_user, item_pass):
    result = script.exports_sync.rpcfunc(item_user, item_pass) # 处理用户名和密码后返回结果
    return {"item_id": item_id, "item_retval": result}

if __name__ == '__main__':
    uvicorn.run(app, port = 8080)#使用uvicorn运行FastAPI引用,在8080端口监听等待http请求

效果如下

成功开启服务

发起正确的get请求即可收到返回结果

九. Frida逆向实战

过vpn检测

课件 屿秋-检测代理和root.apk

可以发现弹出通知提示网络异常以及Toast提示安全证书异常

我们可以通过hook Toast定位异常检测点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function showStacks(){
    console.log("====================StackTraceStart====================");
    console.log(
        Java.use("android.util.Log")
            .getStackTraceString(
                Java.use("java.lang.Throwable").$new()
            )
    );
    console.log("=====================StackTraceEnd=====================");
}
function main(){
    Java.perform(function (){
        var toast=Java.use("android.widget.Toast");
        toast.show.implementation=function(){
            showStacks();
            console.log("Toast.show: ");
            return this.show();
        }
    })
}
setTimeout(main,500)

Toast栈回溯信息如下

不难看出可能和以下两个类有关

1
2
com.jtxc.common.viewmodel.BaseViewModel.b
com.jtxc.common.viewmodel.HomeViewModel$2.accept

但jadx静态分析时无法发现这两个类,说明可能有加壳,查壳发现是腾讯加固

破解极简记账VIP

十. Frida API

Java Bridge

https://frida.re/docs/javascript-api/#java

  1. Java.androidVersion 返回Android版本

  2. Java.enumerateLoadedClasses(callbacks)

    遍历已经加载的类,callbacks如下

    1
    2
    3
    4
    
    callbacks{
        onMatch(name,handle), //可用Java.cast()将handle转换为指定class
        onComplete()
    }
    
  3. Java.enumerateClassLoaders(callbacks)

    遍历java vm中的classloader,callback如下

    1
    2
    3
    4
    
    classback{
        onMatch(loader), //java.lang.ClassLoader
        onComplete()
    }
    
  4. Java.enumerateMethods(query)

    查询匹配条件的方法

    用法如下

    1
    2
    3
    4
    5
    6
    7
    8
    
    function main(){
        Java.perform(function (){
            const groups=Java.enumerateMethods("<class>!<method>/isu");
            //such as query="*youtube*!on*/is"
            console.log(JSON.stringify(groups,null,2));
        })
    }
    setTimeout(main,500)
    

    支持通配符*用于关键字搜索

    /后跟参数

    i: 大小写不敏感

    s: 包括方法签名信息

    u: 忽略系统类

  5. Java.perform(function)

    确保在主线程中执行函数

  6. Java.use(className)

    获取Java类

  7. Java.openClassFile(filePath)

    打开.dex文件,返回类有load()和getClassNames()方法

  8. Java.choose(className, callbacks)

    遍历实例,callbacks={onMatch(instance),onComplete()}

  9. Java.cast(handle, klass)

    类型转换

  10. Java.array(type, elements)

    创建指定类型的数组

  11. Java.vm 对象

    getEnv()获取JNIEnv

    perform(function)执行函数

  12. Java.isMainThread()

    检测是否运行在主线程

Memory

https://frida.re/docs/javascript-api/#memory

  1. Memory.scan(address, size, pattern, callbacks)

    在指定范围内扫描指定匹配串,callbacks{onMatch(address,size),onComplete()}

  2. Memory.alloc(size[, options])

    申请指定大小空间,返回NativePointer

  3. Memory.copy(dst, src, n)

  4. Memory.protect(address, size, protection)

    修改内存保护属性

NativePointer

https://frida.re/docs/javascript-api/#nativepointer

  1. new NativePointer(str)

    创建指针指向str描述的地址处,可用简写形式ptr(str)

  2. .isNull()

    判断是否为空指针

  3. add(rhs),sub(rhs),and(rhs),or(rhs),xor(rhs)

    对NativePointer执行对应运算并返回新的NativePointer

    rhs: 可以是数字或者NativePointer

  4. shr(n),shl(n)

    右移/左移指定bits

  5. toString([radix = 16])

    转换为指定进制字符串(默认hex)

  6. readPointer()/writePointer(ptr)

    向该指针指向的内存区域读/写一个指针

  7. readS8(), readU8(), readS16(), readU16(), readS32(), readU32(), readShort(), readUShort(), readInt(), readUInt(), readFloat(), readDouble(), readS64(), readU64(), readLong(), readULong()

    读取指定类型的数据

  8. writeS8(value), writeU8(value), writeS16(value), writeU16(value), writeS32(value), writeU32(value), writeShort(value), writeUShort(value), writeInt(value), writeUInt(value), writeFloat(value), writeDouble(value), writeS64(value), writeU64(value), writeLong(value), writeULong(value)

    写入指定类型数据

  9. readByteArray(length)/writeByteArray(bytes)

    读/写字节数组

  10. readCString([size = -1]), readUtf8String([size = -1]), readUtf16String([length = -1])

    读取字节流并转换为ASCII/UTF-8/UTF-16字符串

  11. writeUtf8String(str), writeUtf16String(str)

    写入字符串

Module

https://frida.re/docs/javascript-api/#module

  1. enumerateImports(libname)

  2. enumerateExports(libname)

  3. enumerateSymbols(libname)

  4. enumerateRanges(protection)

  5. enumerateSections(libname)

  6. enumerateDependencies(libname)

  7. findExportByName(exportName)

  8. getExportByName(exportName)

  9. Module.load(path)

    加载指定模块

  10. Module.findBaseAddress(name)/Module.getBaseAddress(name)

    未找到时,find返回null,get返回exception

  11. Module.findExportByName(moduleName|null, exportName)

    Module.getExportByName(moduleName|null, exportName)

    返回指定模块导出函数的绝对地址

Debug Symbol

DebugSymbol.fromAddress(address)/DebugSymbol.fromName(name)

官网: DebugSymbol 功能: 查找address/name对应的调试信息并作为对象返回 该对象包含:

1
2
3
4
5
address      //该符号对应地址 作为NativePointer        
name         //符号名称
moudleName   //拥有该符号的模块名称
fileName     //拥有该符号的文件名
lineNumber   //行号

Common API

https://frida.re/docs/javascript-api/#java

https://frida.re/docs/javascript-api/#interceptor

https://frida.re/docs/javascript-api/#instruction

https://frida.re/docs/javascript-api/#arm64writer

https://frida.re/docs/javascript-api/#kernel

Table Of API Contents

  1. Runtime information
    1. Frida
    2. Script
  2. Process, Thread, Module and Memory
    1. Thread
    2. Process
    3. Module
    4. ModuleMap
    5. Memory
    6. MemoryAccessMonitor
    7. CModule
    8. ApiResolver
    9. DebugSymbol
    10. Kernel
  3. Data Types, Function and Callback
    1. Int64
    2. UInt64
    3. NativePointer
    4. ArrayBuffer
    5. NativeFunction
    6. NativeCallback
    7. SystemFunction
  4. Network
    1. Socket
    2. SocketListener
    3. SocketConnection
  5. File and Stream
    1. File
    2. IOStream
    3. InputStream
    4. OutputStream
    5. UnixInputStream
    6. UnixOutputStream
    7. Win32InputStream
    8. Win32OutputStream
  6. Database
    1. SqliteDatabase
    2. SqliteStatement
  7. Instrumentation
    1. Interceptor
    2. Stalker
    3. ObjC
    4. Java
  8. CPU Instruction
    1. Instruction
    2. X86Writer
    3. X86Relocator
    4. x86 enum types
    5. ArmWriter
    6. ArmRelocator
    7. ThumbWriter
    8. ThumbRelocator
    9. ARM enum types
    10. Arm64Writer
    11. Arm64Relocator
    12. AArch64 enum types
    13. MipsWriter
    14. MipsRelocator
    15. MIPS enum types
  9. Other
    1. Console
    2. Hexdump
    3. Shorthand
    4. Communication between host and injected process
    5. Timing events
    6. Garbage collection

References

Frida-Labs

零基础一站式安卓逆向2022

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计