近期着手准备研究安卓逆向,早就听说 frida 这个安卓逆向神器,所以想要系统的学习一下,这里就当作自己的一个学习笔记,记录本人学习 frida 的过程中所遇到的知识点,防止以后遗忘.

这次的学习教程是由 r0ysue 大佬写的,膜拜!

https://github.com/r0ysue/AndroidSecurityStudy

# frida 的命令行基本语法

frida -U -l .\srcipt.js -f "com.oacia.frida_a02_01_demo2"
  • -U 指用 USB 连接手机和电脑
  • -l load-file, 所以后面要跟上写好的 frida 脚本
  • -f file name, 所以后面要跟上你需要注入的包的名称

使用下面的命令即可再 pycharm 中实现 frida 的代码补全

npm i  @types/frida-gum

# JavaScript API

学习 frida 最重要的是从 API 入手,因为你连最基础的工具都不知道怎么使用那还怎么建房子嘞:)

这是官方给出的 API 文档: https://frida.re/docs/javascript-api/

接下来的内容是对 API 作用的解释以及示例

# console

假设 console.log(data) 打印出来的结果是 [object Object] ,那么我们可以直接使用 console.log(data.value) 来打印他的值。

# hexdump

hexdump 是用来打印内存空间的值,可以说是相当便捷的查看内存的方式了,用法如下

const libc = Module.findBaseAddress('libc.so');// 获取 so 的基址
console.log(hexdump(libc, {
  offset: 0,// 相对偏移
  length: 64,//dump 的大小
  header: true,
  ansi: true
}));
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00  .ELF............
00000010  03 00 28 00 01 00 00 00 00 00 00 00 34 00 00 00  ..(.........4...
00000020  34 a8 04 00 00 00 00 05 34 00 20 00 08 00 28 00  4.......4. ...(.
00000030  1e 00 1d 00 06 00 00 00 34 00 00 00 34 00 00 00  ........4...4...

# Module 对象

# findExportByName

通过导出表的函数名称来获取函数在 so 文件中的绝对地址

function frida_Module() {
    Java.perform(function () {
        Module.getExportByName('libhello.so', 'c_getStr')
        console.log("Java_com_roysue_roysueapplication_hellojni_getStr address:",Module.findExportByName('libhello.so', 'Java_com_roysue_roysueapplication_hellojni_getStr'));
}
setImmediate(frida_Module,0);
输出如下:
Java_com_roysue_roysueapplication_hellojni_getStr address: 0xdf2d413d

# enumerateImports

枚举 so 的导入表

var imports=Module.enumerateImports("libencryptlib.so")
for(var i=0;i<imports.length;i++){
    // console.log(JSON.stringify(imports[i]));
    console.log(imports[i].name+" "+imports[i].address);
}

# enumerateExports

枚举 so 的导出表

var exports=Module.enumerateExports("libencryptlib.so")
for(var i=0;i<exports.length;i++){
    console.log(exports[i].name+" "+exports[i].address);
}

# enumerateSymbols

枚举 so 的符号表

var symbols=Module.enumerateSymbols("libencryptlib.so")
for(var i=0;i<symbols.length;i++){
    console.log(symbols[i].name+" "+symbols[i].address);
}

# Memory 对象

这个对象主要对内存进行读取或写入

# scan

其主要功能是搜索内存中以 address 地址开始,搜索长度为 size ,需要搜索的条件是 pattern , 此函数相当于搜索内存的功能。

function frida_Memory() {
    Java.perform(function () {
        // 先获取 so 的 module 对象
        var module = Process.findModuleByName("libhello.so"); 
        //?? 是通配符
        var pattern = "03 49 ?? 50 20 44";
        // 基址
        console.log("base:"+module.base)
        // 从 so 的基址开始搜索,搜索大小为 so 文件的大小,搜指定条件 03 49 ?? 50 20 44 的数据
        var res = Memory.scan(module.base, module.size, pattern, {
            onMatch: function(address, size){
                // 搜索成功
                console.log('搜索到 ' +pattern +" 地址是:"+ address.toString());  
            }, 
            onError: function(reason){
                // 搜索失败
                console.log('搜索失败');
            },
            onComplete: function()
            {
                // 搜索完毕
                console.log("搜索完毕")
            }
          });
    });
}
setImmediate(frida_Memory,0);

# writeByteArray

将字节数组写入一个指定内存

function frida_Memory() {     
    Java.perform(function () {
        // 定义需要写入的字节数组 这个字节数组是字符串 "roysue" 的十六进制
        var arr = [ 0x72, 0x6F, 0x79, 0x73, 0x75, 0x65];
        // 申请一个新的内存空间 返回指针 大小是 arr.length
        const r = Memory.alloc(arr.length);
        // 将 arr 数组写入 R 地址中
        Memory.writeByteArray(r,arr);
        // 输出
        console.log(hexdump(r, {
            offset: 0,
            length: arr.length,
            header: true,
            ansi: false
        }));  
    });
}
setImmediate(frida_Memory,0);
输出如下。
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  72 6f 79 73 75 65                                roysue

# readByteArray

读取一个指定地址的数据

function frida_Memory() {     
    Java.perform(function () {
        // 定义需要写入的字节数组 这个字节数组是字符串 "roysue" 的十六进制
        var arr = [ 0x72, 0x6F, 0x79, 0x73, 0x75, 0x65];
        // 申请一个新的内存空间 返回指针 大小是 arr.length
        const r = Memory.alloc(arr.length);
        // 将 arr 数组写入 R 地址中
        Memory.writeByteArray(r,arr);
        // 读取 r 指针,长度是 arr.length 也就是会打印上面一样的值
        var buffer = Memory.readByteArray(r, arr.length);
        // 输出
        console.log("Memory.readByteArray:");
        console.log(hexdump(buffer, {
            offset: 0,
            length: arr.length,
            header: true,
            ansi: false
        }));
      });  
    });
}
setImmediate(frida_Memory,0);
输出如下。
[Google Pixel::com.roysue.roysueapplication]-> Memory.readByteArray:
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000  72 6f 79 73 75 65                                roysue

# Java 对象

# available

该函数一般用来判断当前进程是否加载了 JavaVMDalvikART 虚拟机

function frida_Java(){
    Java.perform(function(){
        // 当前进程是否加载了 JavaVM,Dalvik 或 ART 虚拟机
        if(Java.available){
            console.log("当前进程加载了JavaVM,Dalvik或ART虚拟机!!!");
        }
        else{
            console.log("error: 不能正常加载虚拟机!!!");
        }
    })
}
setImmediate(frida_Java,0)
//[SM P200::com.oacia.frida_a02_01_demo2 ]-> my android version is  9 amazing~

# androidVersion

显示 android 系统版本号

function frida_Java(){
    Java.perform(function(){
        if(Java.available){
            console.log("my android version is ",Java.androidVersion,"amazing~");
        }
        else{
            console.log("ERROR");
        }
    })
        
}
setImmediate(frida_Java,0);
//[SM P200::com.oacia.frida_a02_01_demo2]-> 当前进程加载了 JavaVM,Dalvik 或 ART 虚拟机!!!

# enumerateLoadedClasses

该 API 枚举当前加载的所有类信息,它有一个回调函数分别是 onMatch: function(classname)onComplete: function() 函数

function frida_Java(){
    Java.perform(function(){
        if(Java.available){
            Java.enumerateLoadedClasses({
                onMatch: function(classname){
                    console.log("",classname);
                },
                onComplete: function(){
                    console.log("all classes log done~");
                }
            })
        }
        else{
            console.log("error");
        }
    })
}
//
function frida_Java_out_class_include_Android(){
    Java.perform(function(){
        if(Java.available){
            Java.enumerateLoadedClasses({
                onMatch: function(classname){
                    if(classname.includes("Android")){
                        console.log("",classname);
                    }
                    
                },
                onComplete: function(){
                    console.log("all classes that includes <Android> log done~");
                }
            })
        }
        else{
            console.log("error");
        }
    })
}
//setImmediate(frida_Java,0);
/**
 * 输出太多哩,这里就截取一点吧
 [Landroid.graphics.Bitmap;
 com.samsung.android.hardware.display.SemMdnieManager
 sun.net.www.MessageHeader
 [Landroid.graphics.drawable.Drawable;
 android.content.pm.split.SplitDependencyLoader
 com.sec.tima.TimaKeyStoreProvider
 [Landroid.content.pm.PathPermission;
 com.samsung.android.multiwindow.MultiWindowCoreState
 [Landroid.os.PatternMatcher;
 android.app.servertransaction.CoreStatesChangeItem
 com.sec.tima.TimaKeyStore
 com.samsung.android.app.CoreStatePool
 android.app.LoadedApk$SplitDependencyLoaderImpl
 com.samsung.android.multiwindow.SideScreenCoreState
 com.samsung.android.app.CoreState
 [Landroid.system.StructPollfd;
 android.app.servertransaction.CoreStatesChangeItem$1
 androidx.core.app.CoreComponentFactory$CompatWrapped
 androidx.core.app.CoreComponentFactory
all classes log done~
 */
setImmediate(frida_Java_out_class_include_Android,0);
/**
com.android.org.bouncycastle.crypto.digests.AndroidDigestFactory
 com.android.org.bouncycastle.crypto.digests.AndroidDigestFactoryInterface
 com.android.org.bouncycastle.crypto.digests.AndroidDigestFactoryOpenSSL
 com.android.org.bouncycastle.crypto.digests.AndroidDigestFactoryBouncyCastle
 com.android.okhttp.AndroidShimResponseCache
 com.android.okhttp.AndroidInternal
 android.icu.text.DecimalFormat_ICU58_Android$Unit
 android.icu.text.DigitList_Android
 android.icu.text.DecimalFormat_ICU58_Android
 java.lang.AndroidHardcodedSystemProperties
 android.security.keystore.KnoxAndroidKeyStoreProvider
all classes that includes <Android> log done~
 */

# enumerateClassLoaders

api 枚举 Java VM 中存在的类加载器,其有一个回调函数,分别是 onMatch: function (loader)onComplete: function ()

枚举类 Java.enumerateLoadedClasses 和枚举类加载器 Java.enumerateClassLoaders 之间的区别是什么呢?

在 Frida 中, Java.enumerateLoadedClasses() 方法用于枚举当前进程中加载的所有 Java 类。这些 Java 类包括应用程序自身的类,以及所有依赖库和系统类。该方法返回一个类名数组,其中包含所有已加载的类名。

Java.enumerateClassLoaders() 方法用于枚举当前进程中所有的 Java 类加载器。在 Java 虚拟机中,每个类都由其类加载器加载。在一个应用程序中,可能存在多个类加载器,每个加载器都有不同的职责和作用域。使用 Java.enumerateClassLoaders() 方法,可以枚举所有已经创建的类加载器,获取它们的引用,并进一步分析和修改它们加载的类的行为。

Java.enumerateLoadedClasses() 方法和 Java.enumerateClassLoaders() 方法都可以用于枚举 Java 类,但是它们的作用有所不同。

Java.enumerateLoadedClasses() 方法用于枚举当前进程中已经加载的 Java 类,包括应用程序自身的类、系统类和依赖库中的类。这些类已经被加载到 Java 虚拟机中,可以直接使用和调用。

Java.enumerateClassLoaders() 方法用于枚举当前进程中所有的 Java 类加载器。类加载器是 Java 虚拟机用于加载和初始化 Java 类的组件,它负责从本地文件系统、网络或其他来源读取类字节码,并转换为 Java 对象。枚举类加载器可以帮助我们了解应用程序的类加载机制,定位和修改类加载行为。

因此,两个方法的作用不同,如果需要分析和修改已经加载的类的行为,可以使用 Java.enumerateLoadedClasses() 方法;如果需要了解类加载器的机制,定位和 Hook 类加载器的行为,可以使用 Java.enumerateClassLoaders() 方法。

什么是 Java 类?

Java 类是一种封装了数据和行为的程序单元,是 Java 语言中最基本的概念之一。Java 类定义了一个对象的属性和方法,描述了对象的特征和行为,可以被实例化成具体的对象。

在 Java 中,所有的代码都必须定义在类中,而每个类都有一个名字,用来标识该类的唯一性。类名通常采用驼峰命名法,首字母大写,例如:Person、Student、Car 等。

类包含两种成员:属性和方法。属性是类的成员变量,用来描述对象的数据特征,例如:人的姓名、年龄、性别等。方法是类的成员函数,用来描述对象的行为特征,例如:人的走路、说话、吃饭等。

在 Java 中,类是面向对象编程的基本单位,一个 Java 程序可以由多个类组成,而且类之间可以建立继承关系和组合关系,从而实现更加复杂的编程逻辑。同时,Java 类的定义和实现都具有可重用性和扩展性,方便进行代码的维护和更新。

// 一个简单的 Java 类的例子
public class Person {// 类的名称
    private String name;// 属性
    private int age;// 属性
    public Person(String name, int age) {// 构造方法
        this.name = name;
        this.age = age;
    }
    public void sayHello() {
        System.out.println("Hello, my name is " + name + ", and I am " + age + " years old.");
    }
    public void setAge(int age) {// 修改器方法
        this.age = age;
    }
    public int getAge() {// 访问器方法
        return age;
    }
    public String getName() {// 访问器方法
        return name;
    }
}

什么是 Java 类加载器?

在 Java 中,类加载器是用来加载 Java 类的组件,它将类的二进制数据从不同的数据源(如本地文件、网络等)中加载到 Java 虚拟机中,并转化成一个 Java 对象,使得程序可以使用这个对象进行相应的操作。

Java 类加载器主要负责三个任务:

  1. 加载:从外部获取类的二进制数据,并将其转化成内部表示的 Class 对象。
  2. 连接:将类的二进制数据合并到 Java 虚拟机中,生成可以执行的代码。
  3. 初始化:对类进行初始化,包括静态变量的赋值等操作。

Java 类加载器根据类所在的路径和 ClassLoader 之间的父子关系,分为如下几种类型:

  1. 启动类加载器(Bootstrap ClassLoader):负责加载 JRE 核心库中的类,是所有类加载器的祖先,使用 C++ 编写,无法被 Java 代码访问。
  2. 扩展类加载器(Extension ClassLoader):负责加载 Java 扩展库中的类,例如:JRE 的 lib/ext 目录下的类。
  3. 系统类加载器(System ClassLoader):负责加载应用程序 classpath 下的类,是默认的类加载器。
  4. 自定义类加载器:继承 ClassLoader 类,并重写 loadClass () 方法,实现自己的类加载逻辑。
// 自定义类加载器的例子
public class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {// 重写 findClass () 方法
        byte[] data = getClassData(name);
        if (data != null) {
            return defineClass(name, data, 0, data.length);
        }
        throw new ClassNotFoundException(name);
    }
    private byte[] getClassData(String name) {
        // 从外部获取类的二进制数据
        // ...
    }
}

举个例子

比如我们要 Hook 一个应用程序的某个方法,我们需要先了解这个方法所在的类和类加载器,以及这个类是否已经被加载进入 Java 虚拟机。在这个场景下,我们可以使用 Java.enumerateLoadedClasses() 方法来枚举当前进程中所有已经加载的类,查找目标类。如果目标类没有被加载,我们可以使用 Java.enumerateClassLoaders() 方法来枚举当前进程中所有的类加载器,查找目标类所在的类加载器,然后使用这个类加载器加载目标类,最终定位到目标方法。

另外,有些应用程序可能使用自定义的类加载器来加载某些类,这些类可能并没有被 Java 虚拟机直接加载。在这种情况下,Java.enumerateLoadedClasses () 方法可能无法找到目标类,我们需要使用 Java.enumerateClassLoaders() 方法来枚举自定义类加载器,然后调用类加载器的方法来加载目标类,最终定位到目标方法。

# perform

Java.perform(fn) 主要用于当前线程附加到 Java VM 并且调用 fn 方法。

基本写 frida 都是用这个开头的不会不知道吧:)

# use

Java.use(className), 动态获取 className 的类定义,通过对其调用 $new() 来调用构造函数,可以从中实例化对象。当想要回收类时可以调用 $Dispose() 方法显式释放,当然也可以等待 JavaScript 的垃圾回收机制,当实例化一个对象之后,可以通过其实例对象调用类中的静态或非静态的方法

Java.use () 方法用于获取指定类的引用,可以在 JavaScript 脚本中直接调用该类的静态方法和成员变量,或者创建该类的实例对象并调用其方法。通常情况下,使用 Java.use () 方法需要提前了解目标类的结构和接口,比较适合于静态的分析和 Hook

// 输出 fun () 函数传入的参数以及返回值
function frida_Java_1(){
    Java.perform(function(){
        const activity = Java.use('com.oacia.frida_a02_01_demo2.MainActivity');
        activity.fun.implementation = function(x,y){
            var return_value = this.fun(x,y);
            console.log("fun() called with x = ", x , ",y = ", y ,",return_value = ",return_value);
            return return_value;
        }
    })
}
// 修改 fun () 函数打印的内容为 x-y
function frida_Java_2(){
    Java.perform(function(){
        const activity = Java.use('com.oacia.frida_a02_01_demo2.MainActivity');
        activity.fun.implementation = function(x,y){
            console.log("fun() called with x = ", x , ",y = ", y ,",return_value = ",return_value);
            var z = x - y;
            var Log = Java.use("android.util.Log");
            Log.d("sum", ""+z);
            var return_value = this.fun(x,y);
            return return_value;
        }
    })
}
// 修改 fun 函数的代码为仅仅打印 x-y
function frida_Java_3(){
    Java.perform(function(){
        const activity = Java.use('com.oacia.frida_a02_01_demo2.MainActivity');
        activity.fun.implementation = function(x,y){
            console.log("fun() called with x = ", x , ",y = ", y);
            var Log = Java.use("android.util.Log");
            Log.d("sum", ""+ (x-y));
        }
    })
}
//setImmediate(frida_Java_1,0);
//[SM-P200::com.oacia.frida_a02_01_demo2 ]-> fun() called with x =  50 ,y =  30 ,return_value =  undefined
//setImmediate(frida_Java_2,0);
//LogCat
/**
 * 这里既 Log 了 x+y (80), 又 Log 了 x-y (40)
2023-04-05 13:49:52.550 17776-17776 sum                     com.oacia.frida_a02_01_demo2         D  20
2023-04-05 13:49:52.552 17776-17776 oacia-sum               com.oacia.frida_a02_01_demo2         D  80
2023-04-05 13:49:53.556 17776-17776 sum                     com.oacia.frida_a02_01_demo2         D  20
2023-04-05 13:49:53.557 17776-17776 oacia-sum               com.oacia.frida_a02_01_demo2         D  80
2023-04-05 13:49:54.560 17776-17776 sum                     com.oacia.frida_a02_01_demo2         D  20
2023-04-05 13:49:54.562 17776-17776 oacia-sum               com.oacia.frida_a02_01_demo2         D  80
 */
setImmediate(frida_Java_3,0);
/**
 * 直接将 fun 函数替换成自己写的代码了~
2023-04-05 14:05:00.529 20331-20331 sum                     com.oacia.frida_a02_01_demo2         D  20
2023-04-05 14:05:01.532 20331-20331 sum                     com.oacia.frida_a02_01_demo2         D  20
2023-04-05 14:05:02.535 20331-20331 sum                     com.oacia.frida_a02_01_demo2         D  20
2023-04-05 14:05:03.539 20331-20331 sum                     com.oacia.frida_a02_01_demo2         D  20
2023-04-05 14:05:04.542 20331-20331 sum                     com.oacia.frida_a02_01_demo2         D  20
 */

# choose

Java.choose () 方法用于获取指定类的实例对象,可以在 JavaScript 脚本中枚举指定类的所有实例对象,并逐一调用其方法,用于动态的分析和 Hook。该方法需要提供一个函数作为参数,用于检测目标对象是否符合特定的条件。当找到符合条件的对象时,该函数将被调用,可以在该函数中对目标对象进行分析和修改。

Java.perform(function () {
    // 查找 android.view.View 类在堆上的实例化对象
    Java.choose("android.view.View", {
        // 枚举时调用
        onMatch:function(instance){
            // 打印实例
            console.log(instance);
        },
        // 枚举完成后调用
        onComplete:function() {
            console.log("end")
        }});
});
输出如下:
android.view.View{2292774 V.ED..... ......ID 0,1794-1080,1920 #1020030 android:id/navigationBarBackground}
android.view.View{d43549d V.ED..... ......ID 0,0-1080,63 #102002f android:id/statusBarBackground}
end

Java.use()Java.choose() 最大的区别,就是在于前者会新建一个对象,后者会选择内存中已有的实例。

# cast

Java.cast(handle, klass) ,就是将指定变量或者数据强制转换成你所有需要的类型;创建一个 JavaScript 包装器,给定从 Java.use() 返回的给定类 klas 的句柄的现有实例。此类包装器还具有用于获取其类的包装器的类属性,以及用于获取其类名的字符串表示的 $className 属性,通常在拦截 so 层时会使用此函数将 jstring、jarray 等等转换之后查看其值。

var clazz = Java.use("java.lang.Class");
var cls = Java.cast(obj.getClass(),clazz); // 先获取 obj 的 Class,然后再强转成 Class 类型。

# array

Java.perform(function () {
        // 定义一个 int 数组、值是 1003, 1005, 1007
        var intarr = Java.array('int', [ 1003, 1005, 1007 ]);
        // 定义一个 byte 数组、值是 0x48, 0x65, 0x69
        var bytearr = Java.array('byte', [ 0x48, 0x65, 0x69 ]);
        for(var i=0;i<bytearr.length;i++)
        {
            // 输出每个 byte 元素
            console.log(bytearr[i])
        }
});
索引 type 含义
1 Z boolean
2 B byte
3 C char
4 S short
5 I int
6 J long
7 F float
8 D double
9 V void

# registerClass

Java.registerClass :创建一个新的 Java 类并返回一个包装器,其中规范是一个包含:
name :指定类名称的字符串。
superClass :(可选)父类。要从 java.lang.Objec t 继承的省略。
implements :(可选)由此类实现的接口数组。
fields :(可选)对象,指定要公开的每个字段的名称和类型。
methods :(可选)对象,指定要实现的方法。

Java.perform(function () {
          // 注册一个目标进程中的类,返回的是一个类对象
          var hellojni = Java.registerClass({
            name: 'com.roysue.roysueapplication.hellojni'
          });
          console.log(hellojni.addInt(1,2));
});

# 官方用法 - 绕过证书检验

这个方法太高端了,未来有机会自己写一个 demo 尝试一下

// 获取目标进程的 SomeBaseClass 类
var SomeBaseClass = Java.use('com.example.SomeBaseClass');
// 获取目标进程的 X509TrustManager 类
var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
var MyWeirdTrustManager = Java.registerClass({
  // 注册一个类是进程中的 MyWeirdTrustManager 类
  name: 'com.example.MyWeirdTrustManager',
  // 父类是 SomeBaseClass 类
  superClass: SomeBaseClass,
  // 实现了 MyWeirdTrustManager 接口类
  implements: [X509TrustManager],
  // 类中的属性
  fields: {
    description: 'java.lang.String',
    limit: 'int',
  },
  // 定义的方法
  methods: {
    // 类的构造函数
    $init: function () {
      console.log('Constructor called');
    },
    //X509TrustManager 接口中方法之一,该方法作用是检查客户端的证书
    checkClientTrusted: function (chain, authType) {
      console.log('checkClientTrusted');
    },
    // 该方法检查服务器的证书,不信任时。在这里通过自己实现该方法,可以使之信任我们指定的任何证书。在实现该方法时,也可以简单的不做任何处理,即一个空的函数体,由于不会抛出异常,它就会信任任何证书。
    checkServerTrusted: [{
      // 返回值类型
      returnType: 'void',
      // 参数列表
      argumentTypes: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String'],
      // 实现方法
      implementation: function (chain, authType) {
         // 输出
        console.log('checkServerTrusted A');
      }
    }, {
      returnType: 'java.util.List',
      argumentTypes: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String', 'java.lang.String'],
      implementation: function (chain, authType, host) {
        console.log('checkServerTrusted B');
        // 返回 null 会信任所有证书
        return null;
      }
    }],
    // 返回受信任的 X509 证书数组。
    getAcceptedIssuers: function () {
      console.log('getAcceptedIssuers');
      return [];
    },
  }
});

# [TODO] 一个小 demo, 可以用到 Java.registerClass (spec)

# vm

这个方法也关联到 native 层了,先把 Java 层的 hook 搞搞明白吧 QAQ

function frida_Java() {     
    Java.perform(function () {
         // 拦截 getStr 函数
         Interceptor.attach(Module.findExportByName("libhello.so" , "Java_com_roysue_roysueapplication_hellojni_getStr"), {
            onEnter: function(args) {
                console.log("getStr");
            },
            onLeave:function(retval){
                // 它的返回值的是 retval 在 jni 层 getStr 的返回值的 jstring 
                // 我们在这里做的事情就是替换掉结果
                // 先获取一个 Env 对象
                var env = Java.vm.getEnv();
                // 通过 newStringUtf 方法构建一个 jstirng 字符串
                var jstring = env.newStringUtf('roysue');
                //replace 替换掉结果
                retval.replace(jstring);
                console.log("getSum方法返回值为:roysue")
            }
    });
}
setImmediate(frida_Java,0);

# Interceptor 对象

# attach

函数原型: Interceptor.attach(target, callbacks)

  • 参数 1: target
    参数 target 是需要拦截的位置的函数地址,也就是填某个 so 层函数的地址即可对其拦截,他是一个 NativePointer 类型,需要注意的是对于 Thumb 函数需要对函数地址 +1
  • 参数 2: callbacks
    callbacks 是触发拦截事件之后的触发的回调函数,他有以下两个回调函数
    • onEnter(args)
      调用被 hook 函数时触发,给定一个参数 args ,可用于读取或写入参数作为 NativePointer 对象的数组
    • onLeave(retval)
      被 hook 函数执行完毕时触发,可以调用 retval.replace(1337) 以整数 1337 替换返回值,或者调用 retval.replace(ptr("0x1234")) 以指针替换返回值。请注意,此对象在 OnLeave 调用中回收,因此不要将其存储在回调之外并使用它。如果需要存储包含的值,请制作深副本,例如: ptr(retval.toString())

使用示例代码

// 使用 Module 对象 getExportByNameAPI 直接获取 libc.so 中的导出函数 read 的地址,对 read 函数进行附加拦截
Interceptor.attach(Module.getExportByName('libc.so', 'read'), {
  // 每次 read 函数调用的时候会执行 onEnter 回调函数
  onEnter: function (args) {
    this.fileDescriptor = args[0].toInt32();
  },
  //read 函数执行完成之后会执行 onLeave 回调函数
  onLeave: function (retval) {
    if (retval.toInt32() > 0) {
      /* do something with this.fileDescriptor */
    }
  }
});

Interceptor.attach 函数的一些属性

属性 含义
returnAddress 返回地址,类型是 NativePointer
context 上下文:具有键 pcsp 的对象,它们是分别为 ia32/x64/arm 指定 EIP/RIP/PCESP/RSP/SP的NativePointer 对象。其他处理器特定的键也可用,例如 eax、rax、r0、x0 等。也可以通过分配给这些键来更新寄存器值。
errno 当前 errno
lastError 当前操作系统错误值
threadId 操作系统线程 ID
depth 相对于其他调用的调用深度

# detachAll

让之前所有的 Interceptor.attach 附加拦截的回调函数失效。

# replace

相当于替换掉原本的函数,用替换时的实现替换目标处的函数

使用示例

function frida_Interceptor() {
    Java.perform(function () {
       // 这个 c_getSum 方法有两个 int 参数、返回结果为两个参数相加
       // 这里用 NativeFunction 函数自己定义了一个 c_getSum 函数
       var add_method = new NativeFunction(Module.findExportByName('libhello.so', 'c_getSum'), 
       'int',['int','int']);
       // 输出结果 那结果肯定就是 3
       console.log("result:",add_method(1,2));
       // 这里对原函数的功能进行替换实现
       Interceptor.replace(add_method, new NativeCallback(function (a, b) {
           //h 不论是什么参数都返回 123
            return 123;
       }, 'int', ['int', 'int']));
       // 再次调用 则返回 123
       console.log("result:",add_method(1,2));
    });
}

# NativePointer

  • new NativePointer(s) : creates a new NativePointer from the string s containing a memory address in either decimal, or hexadecimal if prefixed with ‘0x’. You may use the ptr(s) short-hand for brevity.

  • isNull() : returns a boolean allowing you to conveniently check if a pointer is NULL

  • add(rhs) , sub(rhs) , and(rhs) , or(rhs) , xor(rhs) : makes a new NativePointer with this NativePointer plus/minus/and/or/xor rhs , which may either be a number or another NativePointer

  • shr(n) , shl(n) : makes a new NativePointer with this NativePointer shifted right/left by n bits

  • not() : makes a new NativePointer with this NativePointer’s bits inverted

  • sign([key, data]) : makes a new NativePointer by taking this NativePointer’s bits and adding pointer authentication bits, creating a signed pointer. This is a no-op if the current process does not support pointer authentication, returning this NativePointer instead of a new value.

    Optionally, key may be specified as a string. Supported values are:

    • ia: The IA key, for signing code pointers. This is the default.
    • ib: The IB key, for signing code pointers.
    • da: The DA key, for signing data pointers.
    • db: The DB key, for signing data pointers.

    The data argument may also be specified as a NativePointer/number-like value to provide extra data used for the signing, and defaults to 0 .

  • strip([key]) : makes a new NativePointer by taking this NativePointer’s bits and removing its pointer authentication bits, creating a raw pointer. This is a no-op if the current process does not support pointer authentication, returning this NativePointer instead of a new value.

    Optionally, key may be passed to specify which key was used to sign the pointer being stripped. Defaults to ia . (See sign() for supported values.)

  • blend(smallInteger) : makes a new NativePointer by taking this NativePointer’s bits and blending them with a constant, which may in turn be passed to sign() as data .

  • equals(rhs) : returns a boolean indicating whether rhs is equal to this one; i.e. it has the same pointer value

  • compare(rhs) : returns an integer comparison result just like String#localeCompare()

  • toInt32() : casts this NativePointer to a signed 32-bit integer

  • toString([radix = 16]) : converts to a string of optional radix (defaults to 16)

  • toMatchPattern() : returns a string containing a Memory.scan() -compatible match pattern for this pointer’s raw value

  • readPointer() : reads a NativePointer from this memory location.

    A JavaScript exception will be thrown if the address isn’t readable.

  • writePointer(ptr) : writes ptr to this memory location.

    A JavaScript exception will be thrown if the address isn’t writable.

  • readS8() , readU8() , readS16() , readU16() , readS32() , readU32() , readShort() , readUShort() , readInt() , readUInt() , readFloat() , readDouble() : reads a signed or unsigned 8/16/32/etc. or float/double value from this memory location and returns it as a number.

    A JavaScript exception will be thrown if the address isn’t readable.

  • writeS8(value) , writeU8(value) , writeS16(value) , writeU16(value) , writeS32(value) , writeU32(value) , writeShort(value) , writeUShort(value) , writeInt(value) , writeUInt(value) , writeFloat(value) , writeDouble(value) : writes a signed or unsigned 8/16/32/etc. or float/double value to this memory location.

    A JavaScript exception will be thrown if the address isn’t writable.

  • readS64() , readU64() , readLong() , readULong() : reads a signed or unsigned 64-bit, or long-sized, value from this memory location and returns it as an Int64/UInt64 value.

    A JavaScript exception will be thrown if the address isn’t readable.

  • writeS64(value) , writeU64(value) , writeLong(value) , writeULong(value) : writes the Int64/UInt64 value to this memory location.

    A JavaScript exception will be thrown if the address isn’t writable.

  • readByteArray(length) : reads length bytes from this memory location, and returns it as an ArrayBuffer. This buffer may be efficiently transferred to your Frida-based application by passing it as the second argument to send() .

    A JavaScript exception will be thrown if any of the length bytes read from the address isn’t readable.

  • writeByteArray(bytes) : writes bytes to this memory location, where bytes is either an ArrayBuffer, typically returned from readByteArray() , or an array of integers between 0 and 255. For example: [ 0x13, 0x37, 0x42 ] .

    A JavaScript exception will be thrown if any of the bytes written to the address isn’t writable.

  • readCString([size = -1]) , readUtf8String([size = -1]) , readUtf16String([length = -1]) , readAnsiString([size = -1]) : reads the bytes at this memory location as an ASCII, UTF-8, UTF-16, or ANSI string. Supply the optional size argument if you know the size of the string in bytes, or omit it or specify -1 if the string is NUL-terminated. Likewise you may supply the optional length argument if you know the length of the string in characters.

    A JavaScript exception will be thrown if any of the size / length bytes read from the address isn’t readable.

    Note that readAnsiString() is only available (and relevant) on Windows.

  • writeUtf8String(str) , writeUtf16String(str) , writeAnsiString(str) : encodes and writes the JavaScript string to this memory location (with NUL-terminator).

    A JavaScript exception will be thrown if any of the bytes written to the address isn’t writable.

    Note that writeAnsiString() is only available (and relevant) on Windows.

# NativeFunction

  • new NativeFunction(address, returnType, argTypes[, abi]) : create a new NativeFunction to call the function at address (specified with a NativePointer ), where returnType specifies the return type, and the argTypes array specifies the argument types. You may optionally also specify abi if not system default. For variadic functions, add a '...' entry to argTypes between the fixed arguments and the variadic ones.

    • STRUCTS & CLASSES BY VALUE

      As for structs or classes passed by value, instead of a string provide an array containing the struct’s field types following each other. You may nest these as deep as desired for representing structs inside structs. Note that the returned object is also a NativePointer , and can thus be passed to Interceptor#attach .

      This must match the struct/class exactly, so if you have a struct with three ints, you must pass ['int', 'int', 'int'] .

      For a class that has virtual methods, the first field will be a pointer to the vtable.

      For C++ scenarios involving a return value that is larger than Process.pointerSize , a typical ABI may expect that a NativePointer to preallocated space must be passed in as the first parameter. (This scenario is common in WebKit, for example.)

    • SUPPORTED TYPES

      • void
      • pointer
      • int
      • uint
      • long
      • ulong
      • char
      • uchar
      • size_t
      • ssize_t
      • float
      • double
      • int8
      • uint8
      • int16
      • uint16
      • int32
      • uint32
      • int64
      • uint64
      • bool
    • SUPPORTED ABIS

      • default
      • Windows 32-bit:
        • sysv
        • stdcall
        • thiscall
        • fastcall
        • mscdecl
      • Windows 64-bit:
        • win64
      • UNIX x86:
        • sysv
        • unix64
      • UNIX ARM:
        • sysv
        • vfp
  • new NativeFunction(address, returnType, argTypes[, options]) : just like the previous constructor, but where the fourth argument, options , is an object that may contain one or more of the following keys:

    • abi : same enum as above.
    • scheduling : scheduling behavior as a string. Supported values are:
      • cooperative: Allow other threads to execute JavaScript code while calling the native function, i.e. let go of the lock before the call, and re-acquire it afterwards. This is the default behavior.
      • exclusive: Do not allow other threads to execute JavaScript code while calling the native function, i.e. keep holding the JavaScript lock. This is faster but may result in deadlocks.
    • exceptions : exception behavior as a string. Supported values are:
      • steal: If the called function generates a native exception, e.g. by dereferencing an invalid pointer, Frida will unwind the stack and steal the exception, turning it into a JavaScript exception that can be handled. This may leave the application in an undefined state, but is useful to avoid crashing the process while experimenting. This is the default behavior.
      • propagate: Let the application deal with any native exceptions that occur during the function call. (Or, the handler installed through Process.setExceptionHandler() .)
    • traps : code traps to be enabled, as a string. Supported values are:
      • default: Interceptor.attach() callbacks will be called if any hooks are triggered by a function call.
      • all: In addition to Interceptor callbacks, Stalker may also be temporarily reactivated for the duration of each function call. This is useful for e.g. measuring code coverage while guiding a fuzzer, implementing “step into” in a debugger, etc. Note that this is also possible when using the Java and ObjC APIs, as method wrappers also provide a clone(options) API to create a new method wrapper with custom NativeFunction options.

# frida 使用示例

初步了解了一些 API 的定义和用法,接下来尝试一些例子来实践一下

# hook 函数的参数,并修改函数返回的结果

# source code

package com.oacia.frida_a02_01_demo2;
import androidx.appcompat.app.AppCompatActivity;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
    @SuppressLint("SetTextI18n")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView myTextView = findViewById(R.id.MyTextView);
        //myTextView.setText("oacia: hello android!!nice to meet you~");
        myTextView.setText("oacia:Hello Android!");
        while (true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            fun(50,30);
        }
    }
    void  fun(int x,int y){
        Log.d("oacia-sum",String.valueOf(x+y));
    }
}

这个 app 的作用是每隔 1 秒 Log 输出 80, 接下来我将使用 frida 来打印 fun() 函数传入的参数,并且修改传入参数的值

# frida code

function hook(){
    Java.perform(function(){
        const activity = Java.use("com.oacia.frida_a02_01_demo2.MainActivity");
        activity.fun.implementation = function(x,y){
            console.log("fun() called with x = " + x + ",y = " + y);
            console.log("oacia will change fun()'s argv[0] and argv[1]");
            console.log("wait and see~");
            const retv = this.fun(60,90);
            return retv;
        }
    })
}
setImmediate(hook,0);
/**powershell
 * [SM P200::com.oacia.frida_a02_01_demo2 ]-> fun() called with x = 50,y = 30
oacia will change fun()'s argv[0] and argv[1]
wait and see~
fun() called with x = 50,y = 30
oacia will change fun()'s argv[0] and argv[1]
wait and see~
 */
/**logcat
04-05 20:16:07.521 27216 27216 D oacia-sum: 80
04-05 20:16:08.522 27216 27216 D oacia-sum: 80
04-05 20:16:09.522 27216 27216 D oacia-sum: 80
04-05 20:16:26.420 27285 27285 D oacia-sum: 150
04-05 20:16:27.421 27285 27285 D oacia-sum: 150
04-05 20:16:28.425 27285 27285 D oacia-sum: 150*/

# 函数重载时的 hook, 未使用的函数的调用

# source code

package com.roysue.demo02;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
    private String total = "@@@###@@@";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        while (true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            fun(50,30);
            Log.d("ROYSUE.string" , fun("LoWeRcAsE Me!!!!!!!!!"));
        }
    }
    void fun(int x , int y ){
        Log.d("ROYSUE.Sum" , String.valueOf(x+y));
    }
    String fun(String x){
        total +=x;
        return x.toLowerCase();
    }
    String secret(){
        return total;
    }
}

# frida code

  • 函数重载

我们使用原来的代码试一试有重载的函数

function hook(){
    Java.perform(function(){
        var activity = Java.use("com.roysue.demo02.MainActivity");
        activity.fun.implementation = function(x,y){
            console.log("log successfully");
        }
    })
}
setImmediate(hook,0);

这里将会报错,而且我们也需要经历报错的过程才知道我们需要加上的是哪一个重载类型 (毕竟现在感觉 java 的类型还是比较难记的,不如直接通过报错来直接复制粘贴重载函数参数的类型方便), 提示函数有重载,所以我们只需要根据提示,加上重载 .overload('java.lang.String') 即可

/**
 * 
 * [SM-P200::com.roysue.demo02 ]-> Error: fun(): has more than one overload, use .overload(<signature>) to choose from:
        .overload('java.lang.String')
        .overload('int', 'int')
    at X (frida/node_modules/frida-java-bridge/lib/class-factory.js:568)
    at K (frida/node_modules/frida-java-bridge/lib/class-factory.js:563)
    at set (frida/node_modules/frida-java-bridge/lib/class-factory.js:931)
    at <anonymous> (D:\frida\AndroidSecurityStudy-master\FRIDA\A02\02\hook.js:3)
    at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
    at _performPendingVmOps (frida/node_modules/frida-java-bridge/index.js:250)
    at <anonymous> (frida/node_modules/frida-java-bridge/index.js:242)
    at apply (native)
    at ne (frida/node_modules/frida-java-bridge/lib/class-factory.js:619)
    at <anonymous> (frida/node_modules/frida-java-bridge/lib/class-factory.js:597)
Process terminated
 */

修改后的代码如下

//overload 的使用
function hook1(){
    Java.perform(function(){
        var activity = Java.use("com.roysue.demo02.MainActivity");
        activity.fun.overload('java.lang.String').implementation = function(x){
            console.log("oacia find the real func,the arg type is java.lang.String!");
            var String_class = Java.use('java.lang.String');
            var myStr = String_class.$new("oaciA ChaNge yOu!");//new 一个字符串
            var retv = this.fun(myStr);
            console.log("oacia change you,now it return "+"\"" +retv +"\"");
            return retv;
        }
    })
}
//setImmediate(hook1,0);
/**cmd
[SM-P200::com.roysue.demo02 ]-> oacia find the real func,the arg type is java.lang.String!
oacia change you,now it return "oacia change you!"
 */
/**logcat
04-05 20:31:37.879 28305 28305 D ROYSUE.Sum: 80
04-05 20:31:37.879 28305 28305 D ROYSUE.string: lowercase me!!!!!!!!!
04-05 20:40:25.458 28514 28514 D ROYSUE.Sum: 80
04-05 20:40:25.470 28514 28514 D ROYSUE.string: oacia change you!
 */
  • 未使用的函数的调用
// 调用未使用的函数
function hook3(){
    Java.perform(function(){
        Java.choose("com.roysue.demo02.MainActivity",{
            onMatch: function(instance){
                console.log("find instance: ",instance);
                console.log("oacia find the secret function and the result is ",instance.secret());
            },
            onComplete: function(){
                console.log("choose end~~");
            }
        })
    })
}
setImmediate(function(){
    setTimeout(hook3, 5000);
},0);
/**cmd
Spawned `com.roysue.demo02`. Resuming main thread!
[SM-P200::com.roysue.demo02 ]-> find instance:  com.roysue.demo02.MainActivity@7cdb8dd
oacia find the secret function and the result is  @@@###@@@LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!LoWeRcAsE Me!!!!!!!!!
choose end~~
 */

假如一开始就使用 java.choose , 那么将捕获不到类,因为此时要捕获的类还没有加载所以这里使用 setTimeout (非阻塞方式) 来延迟 5s 捕获, com.roysue.demo02.MainActivity 此时类已经加载到内存中所以将会有回显,如果仍然使用 setImmediate(hook3,0); , 那么捕获不到类是必然的:)

更多有关延迟捕获可以看看这篇文章

# 函数的远程调用

当我们知道当我们特别想要调用一个 apk 中的某个函数,而且不止一次时,就是这个功能 — 函数的远程调用起作用的时候了

在 r0ysue 大佬写到此处中级能力:远程调用时,用的是 python 脚本来作为 js 的执行脚本,但是我不太喜欢 python 需要提前写那么多模板代码,而更喜欢 frida 直接使用命令行来将 js 脚本注入 apk 中,同时通过查阅资料我看到一个评论image-20230406115829489

所以我也决定使用这种方式来进行函数的远程调用,接下来就试一试能不能行得通吧

在上一个例子中,我们成功调用了未使用的函数 secret() , 稍微网上翻一翻就可以看到我们实现这个功能的代码为

function hook3(){
    Java.perform(function(){
        Java.choose("com.roysue.demo02.MainActivity",{
            onMatch: function(instance){
                console.log("find instance: ",instance);
                console.log("oacia find the secret function and the result is ",instance.secret());
            },
            onComplete: function(){
                console.log("choose end~~");
            }
        })
    })
}
setImmediate(function(){
    setTimeout(hook3, 5000);
},0);

而我们假如需要多次调用 secret() 那么就需要对上面的 js 代码进行部分修改

function hook3(){
    Java.perform(function(){
        Java.choose("com.roysue.demo02.MainActivity",{
            onMatch: function(instance){
                console.log("find instance: ",instance);
                console.log("oacia find the secret function and the result is ",instance.secret());
            },
            onComplete: function(){
                console.log("choose end~~");
            }
        })
    })
}
rpc.exports = {
    HOOK3: hook3// 将函数 hook3 导出为 HOOK3 来进行调用,调用方式: rpc.exports.HOOK3 ()
};
/*
setImmediate(function(){
    setTimeout(hook3, 5000);
},0);
*/

看看输出~

image-20230406121531280

成功实现!!!

# 互联互通、动态修改

# source code

package com.roysue.demo04;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Base64;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
    EditText username_et;
    EditText password_et;
    TextView message_tv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        password_et = (EditText) this.findViewById(R.id.editText2);
        username_et = (EditText) this.findViewById(R.id.editText);
        message_tv = ((TextView) findViewById(R.id.textView));
        this.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (username_et.getText().toString().compareTo("admin") == 0) {
                    message_tv.setText("You cannot login as admin");
                    return;
                }
                //hook target
                message_tv.setText("Sending to the server :" + Base64.encodeToString((username_et.getText().toString() + ":" + password_et.getText().toString()).getBytes(), Base64.DEFAULT));
            }
        });
    }
}

# [TODO] 简要分析该 apk

在这个 apk 中,如果我们输入 admin, 那么程序将会返回 You cannot login as admin

screenshot

这是因为在 apk 中有对用户名的校验,如果用户名为 admin 那么该 apk 将不会把这个 http 请求发送给服务器 (假装有个服务器.png)

接下来我们将要实现的是:当我们输入用户名为 admin , 密码为 123456 时,可以通过 admin 这个用户名来向服务器发送请求,而不会被程序的检测功能拦截

这里有两种实现思路:

  • 第一种,绕过程序对于 admin 的检测

我们通过使用 jadx 对这个 apk 进行反编译,可以发现此处的代码的作用是对用户名是否为 admin 进行校验

image-20230406164512543

那么我们只要使用 frida 让 compareTo 函数永远返回 true 不就可以绕过这个检测了吗

frida 代码如下

function hook(){
    Java.perform(function(){
        var string_class = Java.use("java.lang.String");
        string_class.compareTo.overload('java.lang.String').implementation = function(x){
            if(x==="admin"){
                console.log("find compareTo !!! argv is ",x);
                return 1;
            }
            return this.compareTo(x);
        }
    })
}
setImmediate(function(){
    setTimeout(hook, 5000);// 这里经过尝试,如果不延迟五秒再 hook 的话,那么 frida 将会自动退出 (原因还不清楚)
},0)
  • [TODO] 第二种思路就是在向服务器发送消息前,将我们的用户名替换为 admin

比如我一开始输入的用户名为 oacia , 那么这肯定是可以过程序对于用户名是否为 admin 的校验的,实际上,我们 hook 的代码应该是在此处的image-20230406183248013 我们就假装 textView.setText(sb.toString()); 这行代码就是向服务器发送消息的代码:)

所以我们将会用 frida 来做什么呢?没错,我们将要修改 sb 这个变量

# hook 某个类的构造函数

# source code

  • User
package com.oacia.inithook;
public class User {
    public int age;
    public String name;
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("User{name='");
        sb.append(this.name);
        sb.append('\'');
        sb.append(", age=");
        sb.append(this.age);
        sb.append('}');
        return sb.toString();
    }
    public User(String name2, int age2) {
        this.name = name2;
        this.age = age2;
    }
    public User() {
        this.name = "oacia";
        this.age = 20;
    }
}

# frida code

对于构造函数的 hook, 我们需要使用 $init 这个关键词来进行 hook

function hook(){
    if(Java.available){
        Java.perform(function(){
        console.log("start hook~");
        const User = Java.use("com.oacia.inithook.User");
        if(User != undefined){
            User.$init.overload('java.lang.String', 'int').implementation = function(name,age){
            console.log("hook constructor,overload('java.lang.String', 'int')!!!");
            console.log("name = ",name,", age = ",age);
            return this.$init(name,age);
        }
        User.$init.overload().implementation = function(){
            console.log("hook constructor,overload()!!!");
        }
        console.log("hook end~");
        }
    })
    }
}
setImmediate(hook,0);

# 主动调用一个类

# source code

package com.oacia.inithook;
public class User {
    public int age;
    public String name;
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("User{name='");
        sb.append(this.name);
        sb.append('\'');
        sb.append(", age=");
        sb.append(this.age);
        sb.append('}');
        return sb.toString();
    }
    public User(String name2, int age2) {
        this.name = name2;
        this.age = age2;
    }
    public User() {
        this.name = "oacia";
        this.age = 20;
    }
}

# frida code

当我们想要主动去调用一个类时,我们可以使用 $new 关键字来新建一个实例

function hook(){
    Java.perform(function(){
        var User = Java.use("com.oacia.inithook.User");
        var me = User.$new("ooooacia",999);
        var info = me.toString();
        console.log(info);
        console.log("change info ing...");
        me.name.value = "oaciaaaa";
        me.age.value = 20;
        info = me.toString();
        console.log(info)
    })
}
setImmediate(hook,0);

# hook 内部类

# source code

package com.oacia.inithook;
public class User {
    public int age;
    public String name;
    class signin{
        private String uid;
        private String passwd;
        public signin(String uid_,String passwd_){
            this.uid = uid_;
            this.passwd = passwd_;
        }
        public String toString(){
            StringBuilder sb = new StringBuilder();
            sb.append("signin{uid=");
            sb.append(this.uid);
            sb.append(", passwd=");
            sb.append(this.passwd);
            return sb.toString();
        }
    }
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("User{name='");
        sb.append(this.name);
        sb.append('\'');
        sb.append(", age=");
        sb.append(this.age);
        sb.append('}');
        return sb.toString();
    }
    public User(String name2, int age2) {
        this.name = name2;
        this.age = age2;
    }
    public User() {
        this.name = "oacia";
        this.age = 20;
    }
}

# frida code

在此处给出的 source code 中,内部类的名称为 signin , 如果我们想要去 hook 内部类,那么我们需要使用如下的语法来进行 hook

com.oacia.inithook.User$signin

function hook_inner(){
    Java.perform(function(){
        var signin = Java.use("com.oacia.inithook.User$signin");
        signin.toString.implementation = function(){
            console.log("成功hook内部类signin中的toString函数!!");
            var ret = this.toString();
            console.log("return value: ",ret);
            return ret;
        }
    })
}
setImmediate(hook_inner,0);

# frida 常用脚本

# Java 层

# hook java 中一个类的函数并修改参数

function hook(){
    Java.perform(function(){
        const activity = Java.use("com.oacia.frida_a02_01_demo2.MainActivity");
        activity.fun.implementation = function(x,y){
            console.log("fun() called with x = " + x + ",y = " + y);
            const retv = this.fun(60,90);
            return retv;
        }
    })
}
setImmediate(hook,0);

# enum class/method && find class

//from https://github.com/0xdea/frida-scripts/
// enumerate all Java classes
function enumAllClasses()
{
	var allClasses = [];
	var classes = Java.enumerateLoadedClassesSync();
	classes.forEach(function(aClass) {
		try {
			var className = aClass.match(/[L](.*);/)[1].replace(/\//g, ".");
		}
		catch(err) {return;} // avoid TypeError: cannot read property 1 of null
		allClasses.push(className);
	});
	return allClasses;
}
// find all Java classes that match a pattern
function findClasses(pattern)
{
	var allClasses = enumAllClasses();
	var foundClasses = [];
	allClasses.forEach(function(aClass) {
		try {
			if (aClass.match(pattern)) {
				foundClasses.push(aClass);
			}
		}
		catch(err) {} // avoid TypeError: cannot read property 'match' of undefined
	});
	return foundClasses;
}
// enumerate all methods declared in a Java class
function enumMethods(targetClass)
{
	var hook = Java.use(targetClass);
	var ownMethods = hook.class.getDeclaredMethods();
	hook.$dispose;
	return ownMethods;
}
/*
 * The following functions were not implemented because deemed impractical:
 *
 * enumAllMethods() - enumerate all methods declared in all Java classes
 * findMethods(pattern) - find all Java methods that match a pattern
 *
 * See raptor_frida_ios_enum.js for a couple of ObjC implementation examples.
 */
// usage examples
setTimeout(function() { // avoid java.lang.ClassNotFoundException
	Java.perform(function() {
		// enumerate all classes
		/*
		var a = enumAllClasses();
		a.forEach(function(s) { 
			console.log(s); 
		});
		*/
		// find classes that match a pattern
		/*
		var a = findClasses(/password/i);
		a.forEach(function(s) { 
			console.log(s); 
		});
		*/
		// enumerate all methods in a class
		/*
		var a = enumMethods("com.target.app.PasswordManager")
		a.forEach(function(s) { 
			console.log(s); 
		});
		*/
	});
}, 0);

# trace module/class/method

//from https://github.com/0xdea/frida-scripts/
// generic trace
function trace(pattern)
{
	var type = (pattern.toString().indexOf("!") === -1) ? "java" : "module";
	if (type === "module") {
		console.log("module")
		// trace Module
		var res = new ApiResolver("module");
		var matches = res.enumerateMatchesSync(pattern);
		var targets = uniqBy(matches, JSON.stringify);
		targets.forEach(function(target) {
			try{
				traceModule(target.address, target.name);
			}
			catch(err){}
		});
	} else if (type === "java") {
		console.log("java")
		// trace Java Class
		var found = false;
		Java.enumerateLoadedClasses({
			onMatch: function(aClass) {
				if (aClass.match(pattern)) {
					found = true;
					console.log("found is true")
					console.log("before:"+aClass)
					//var className = aClass.match(/[L](.*);/)[1].replace(/\//g, ".");
					var className = aClass.match(/[L]?(.*);?/)[1].replace(/\//g, ".");
					console.log("after:"+className)
					traceClass(className);
				}
			},
			onComplete: function() {}
		});
		// trace Java Method
		if (!found) {
			try {
				traceMethod(pattern);
			}
			catch(err) { // catch non existing classes/methods
				console.error(err);
			}
		}
	}
}
// find and trace all methods declared in a Java Class
function traceClass(targetClass)
{
	console.log("entering traceClass")
	var hook = Java.use(targetClass);
	var methods = hook.class.getDeclaredMethods();
	hook.$dispose();
	console.log("entering pasedMethods")
	var parsedMethods = [];
	methods.forEach(function(method) {
		try{
			parsedMethods.push(method.toString().replace(targetClass + ".", "TOKEN").match(/\sTOKEN(.*)\(/)[1]);
		}
		catch(err){}
	});
	console.log("entering traceMethods")
	var targets = uniqBy(parsedMethods, JSON.stringify);
	targets.forEach(function(targetMethod) {
		try{
			traceMethod(targetClass + "." + targetMethod);
		}
		catch(err){}
	});
}
// trace a specific Java Method
function traceMethod(targetClassMethod)
{
	var delim = targetClassMethod.lastIndexOf(".");
	if (delim === -1) return;
	var targetClass = targetClassMethod.slice(0, delim)
	var targetMethod = targetClassMethod.slice(delim + 1, targetClassMethod.length)
	var hook = Java.use(targetClass);
	var overloadCount = hook[targetMethod].overloads.length;
	console.log("Tracing " + targetClassMethod + " [" + overloadCount + " overload(s)]");
	for (var i = 0; i < overloadCount; i++) {
		hook[targetMethod].overloads[i].implementation = function() {
			console.warn("\n*** entered " + targetClassMethod);
			// print backtrace
			// Java.perform(function() {
			//	var bt = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());
			//	console.log("\nBacktrace:\n" + bt);
			// });
			// print args
			if (arguments.length) console.log();
			for (var j = 0; j < arguments.length; j++) {
				console.log("arg[" + j + "]: " + arguments[j]);
			}
			// print retval
			var retval = this[targetMethod].apply(this, arguments); // rare crash (Frida bug?)
			console.log("\nretval: " + retval);
			console.warn("\n*** exiting " + targetClassMethod);
			return retval;
		}
	}
}
// trace Module functions
function traceModule(impl, name)
{
	console.log("Tracing " + name);
	Interceptor.attach(impl, {
		onEnter: function(args) {
			// debug only the intended calls
			this.flag = false;
			// var filename = Memory.readCString(ptr(args[0]));
			// if (filename.indexOf("XYZ") === -1 && filename.indexOf("ZYX") === -1) // exclusion list
			// if (filename.indexOf("my.interesting.file") !== -1) // inclusion list
				this.flag = true;
			if (this.flag) {
				console.warn("\n*** entered " + name);
				// print backtrace
				console.log("\nBacktrace:\n" + Thread.backtrace(this.context, Backtracer.ACCURATE)
						.map(DebugSymbol.fromAddress).join("\n"));
			}
		},
		onLeave: function(retval) {
			if (this.flag) {
				// print retval
				console.log("\nretval: " + retval);
				console.warn("\n*** exiting " + name);
			}
		}
	});
}
// remove duplicates from array
function uniqBy(array, key)
{
        var seen = {};
        return array.filter(function(item) {
                var k = key(item);
                return seen.hasOwnProperty(k) ? false : (seen[k] = true);
        });
}
// usage examples
setTimeout(function() { // avoid java.lang.ClassNotFoundException
	Java.perform(function() {
		// trace("com.target.utils.CryptoUtils.decrypt");
		// trace("com.target.utils.CryptoUtils");
		// trace("CryptoUtils");
		// trace(/crypto/i);
		// trace("exports:*!open*");
	});   
}, 0);

# 打印堆栈信息

假设某个 app 有检查机制,会侦测是不是有 root,然后原始码经过混淆所以比较难追踪,但是在检查时会用 Log.d 输出检查相关资讯,这时候我们可以 hook Log.d,并且利用 Log.getStackTraceString 输出 stack trace,就能知道是在哪边呼叫这个 function

var Log = Java.use("android.util.Log");
var Exception = Java.use("java.lang.Exception");
Log.d.overload("java.lang.String", "java.lang.String").implementation = function (a, b) {
   // 发现输出 root 的 Log 时
   if (b.indexOf('root') >= 0) {
    // 打印 stack trace 方便追踪
    console.log(Log.getStackTraceString( Exception.$new()));
   }
   return this.d.overload("java.lang.String", "java.lang.String").call(this, a, b)
};

# 混淆方法名的 hook

如果类名和方法名都是不可见的字符,那么我们可以先把这些不可见字符以 URL编码 ,然后用 js 的函数 decodeURIComponent 进行解码

// 假设我们 URLEncode 过后的混淆方法名为:% EE% A0%91,类名为:% EE% A0%94% EE% A0%94% EE% A0%94% EE% A0
// 那我们要 Hook 这个方法,可以像以下写法
// 取混淆类名的类
var obj = Java.use(decodeURIComponent("%EE%A0%94%EE%A0%94%EE%A0%94%EE%A0"));
// 通过 obj [] 方式来定位到方法,在里面通过 decodeURIComponent () 解码我们混淆的方法名
obj[decodeURIComponent("%EE%A0%91")].implementation = function(){
    //do hook
    // 如果我们想要调用该类另外一个混淆的方法 % EE% A0%94% EE% A0%91% EE。那么可以如下操作:
    this[decodeURIComponent("%EE%A0%94%EE%A0%91%EE")](参数);
}

# 利用 java 的反射机制打印方法

什么是 java 的反射机制?

反射机制是指在编译阶段不知道是哪个类被加载,而是在运行的时候才加载、执行。

也就是说,反射机制指的是程序在运行时能够获取自身的信息。 js 中的 apply 就是反射机制。

ParametersTest

public class ParametersTest {
    private final int count = 523;// 字段 count
    private final String plainText = "this is a test";// 字段 plainText
    public int multiply(int val1,int val2){
        return val1 * val2;
    }
    public byte multiply(byte val1,byte val2){
        return (byte)(val1 * val2);
    }
    public short multiply(short val1,short val2){
        return (short)(val1 * val2);
    }
    public long multiply(long val1,long val2){
        return val1 * val2;
    }
    public float multiply(float val1,float val2){
        return val1 * val2;
    }
    public double multiply(double val1,double val2){
        return val1 * val2;
    }
    public String addMethod(String str1,String str2){
        return str1 + str2;
    }
    public void addMethod(int[] initArray){
        for(int i = 0; i < initArray.length;i++){
            initArray[i] = initArray[i] + 1;
        }
    }
    public void addMethod(byte[] byteArray){
        for(int i = 0; i < byteArray.length;i++){
            byteArray[i] = (byte)(byteArray[i] + 1);
        }
    }
    public void addMethod(long[] longArray){
        for(int i = 0; i < longArray.length; i++){
            longArray[i] = longArray[i] + 1;
        }
    }
    public void addMethod(float[] floatArray){
        for(int i = 0; i < floatArray.length; i++){
            floatArray[i] = floatArray[i] + 1;
        }
    }
    public void addMethod(double[] doubleArray){
        for(int i = 0; i < doubleArray.length; i++){
            doubleArray[i] = doubleArray[i] + 1;
        }
    }
    public void addMethod(String[] strArray){
        String result = "";
        for(int i = 0; i < strArray.length; i++){
            result = result + strArray[i];
        }
    }
    //display 方法参数为 ParametersTest 对象
    public void display(ParametersTest parametersTest){
        int[] intArray = {1,2,3,4,5};
        parametersTest.addMethod(intArray);
        byte[] byteArray = {0x10,0x11,0x12,0x13};
        parametersTest.addMethod(byteArray);
        long[] longArray = {0x1,0x2,0x3,0x4};
        parametersTest.addMethod(longArray);
        String[] strArray = {"abcde","1222CDedd","12daer","cder"};
        parametersTest.addMethod(strArray);
        float[] floatArray = {0x1,0x2,0x3,0x4};
        parametersTest.addMethod(floatArray);
        double[] doubleArray = {0x1,0x2,0x3,0x4};
        parametersTest.addMethod(doubleArray);
        String str1 = "acerwe";
        String str2 = "werwed";
        parametersTest.addMethod(str1,str2);
        int intVal1 = 3;
        int intVal2 = 4;
        int retInt = parametersTest.multiply(intVal1,intVal2);
        byte byteVal1 = 0x1;
        byte byteVal2 = 0x2;
        byte retByte = parametersTest.multiply(byteVal1,byteVal2);
        long longVal1 = 0x3;
        long longVal2 = 0x4;
        long retLong = parametersTest.multiply(longVal1,longVal2);
        float floatVal1 = 0x3;
        float floatVal2 = 0x4;
        float retFloat = parametersTest.multiply(floatVal1,floatVal2);
        short shortVal1 = 0x3;
        short shortVal2 = 0x4;
        short retShort = parametersTest.multiply(shortVal1,shortVal2);
        double doubleVal1 = 0x3;
        double doubleVal2 = 0x4;
        double retDouble = parametersTest.multiply(doubleVal1,doubleVal2);
    }
}

frida 脚本

function getReflectFields(val1) {
  var clazz = Java.use("java.lang.Class");
  var parametersTest = Java.cast(val1.getClass(),clazz);
  //getDeclaredFields ():返回反映由类对象表示的类或接口声明的所有字段的字段对象数组,也可以简单的理解为会返回这个类所有的对象
  var fields = parametersTest.getDeclaredFields();
  fields.forEach(function (field) {// 依次打印字段的类型、名称、值
    send("field type is: " + (field.getType()));
    send("field name is: " + (field.getName()));
    send("field value is: " + field.get(val1));
  })
}
function getReflectMethod(val1) {
  try{
    var clazz = Java.use("java.lang.Class");// 使用 java 的 Class 类
    var parametersTest = Java.cast(val1.getClass(),clazz);// 类型转换,将
    //getDeclaredMethods () 获取所有方法
    var methods = parametersTest.getDeclaredMethods();
      methods.forEach(function (method) {
        var methodName = method.getName();
        var val1Class = val1.getClass();
        var val1ClassName = Java.use(val1Class.getName());
        var overloads = val1ClassName[methodName].overloads;
        overloads.forEach(function (overload) {
          var proto = "(";
          overload.argumentTypes.forEach(function (type) {
            proto += type.className + ", ";
          });
          if(proto.length > 1){
            proto = proto.substr(0 ,proto.length - 2);
          }
          proto += ")";
          overload.implementation = function () {
            var args = [];
            for(var j = 0; j < arguments.length; j++){
              for(var i in arguments[j]){
                var value = String(arguments[j][i]);
                send(val1ClassName + "." + methodName + " and arguments value is: " + value);
              }
              args[j] = arguments[j] + "";
            }
            // 打印方法参数
            send(val1ClassName + "." + methodName + " and args is: " + args);
            // 调用方法
            var retval = this[methodName].apply(this,arguments);
            // 打印方法返回值
            send(methodName + " return value is: " + retval);
            return retval;// 返回方法返回值
          }
        })
      })
    }catch(e){
      send("'" + val1 + "' hook fail: " + e);
    }
  }
//hook parametersTestClass 类的 display 方法
parametersTestClass.display.overload("com.example.parameterstest.ParametersTest").implementation = function (val1) {
  getReflectFields(val1);// 打印所有字段(fields)类型、名称、值
  getReflectMethod(val1)//hook ParametersTest 对象的所有方法
  this.display(val1);// 调用 display 方法
}

# native 层

# dump 内存中的 so

function dump_so(so_name) {
    Java.perform(function () {
        var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
        var dir = currentApplication.getApplicationContext().getFilesDir().getPath();
        var libso = Process.getModuleByName(so_name);
        console.log("[name]:", libso.name);
        console.log("[base]:", libso.base);
        console.log("[size]:", ptr(libso.size));
        console.log("[path]:", libso.path);
        var file_path = dir + "/" + libso.name + "_" + libso.base + "_" + ptr(libso.size) + ".so";
        var file_handle = new File(file_path, "wb");
        if (file_handle && file_handle != null) {
            Memory.protect(ptr(libso.base), libso.size, 'rwx');
            var libso_buffer = ptr(libso.base).readByteArray(libso.size);
            file_handle.write(libso_buffer);
            file_handle.flush();
            file_handle.close();
            console.log("[dump]:", file_path);
        }
    });
}
rpc.exports = {
    dump_so: dump_so
};

# dump 内存

function dump_memory(start,size,filename) {
    var file_path = "/data/data/com.oacia.apk_protect/" + filename;
    var file_handle = new File(file_path, "wb");
    if (file_handle && file_handle != null) {
        var libso_buffer = start.readByteArray(size.toUInt32());
        file_handle.write(libso_buffer);
        file_handle.flush();
        file_handle.close();
        console.log("[dump]:", file_path);
    }
}

# hook native 层的函数

function hook_native(){
    var module = Process.findModuleByName("libjiagu_64.so");
    Interceptor.attach(module.base.add(0x131E20), {
        onEnter: function (args) {
            console.log("hook success!")
            console.log(args[1])
            console.log(args[2])
            console.log(args[3])
        },
        onLeave: function (ret) {
        }
    });
}

# hook 导出函数

function hook_export() {
    Interceptor.attach(Module.findExportByName("libc.so", "pthread_create"),{
        onEnter(args){
            console.log("hook success");
        },
        onLeave: function (retval) {
        }
    })
}

# hook jni 函数

我们可以现在这个地方找到 jni 函数的含义,然后根据函数参数的类型去定制化输出的打印

//hook GetStaticMethodID 
function hook_libart() {
    var symbols = Module.enumerateSymbolsSync("libart.so");
    var addr_jni = null;
    for (var i = 0; i < symbols.length; i++) {
        var symbol = symbols[i];
        if (symbol.name.indexOf("GetStaticMethodID")!=-1) {
            addr_jni = symbol.address;
            console.log("find ",symbol.name);
            console.log("[+] hook ",addr_jni);
            Interceptor.attach(addr_jni, {
                onEnter: function (args) {
                    console.log("call GetStaticMethodID: ",symbol.name);
                    console.log("name: ",args[2].readCString());
                    console.log("sig: ",args[3].readCString());
                },
                onLeave: function (retval) { 
                    console.log("return val")
                    console.log(retval);
                }
            });
        }
    }
}

# 枚举所有 so

function enum_so(){
    Java.perform(function(){
        // 枚举当前加载的模块
        var process_Obj_Module_Arr = Process.enumerateModules();
        for(var i = 0; i < process_Obj_Module_Arr.length; i++) {
           // 包含 "lib" 字符串的
           if(process_Obj_Module_Arr[i].path.indexOf("lib")!=-1)
           {
               console.log("模块名称:",process_Obj_Module_Arr[i].name);
               console.log("模块地址:",process_Obj_Module_Arr[i].base);
               console.log("大小:",process_Obj_Module_Arr[i].size);
               console.log("文件系统路径",process_Obj_Module_Arr[i].path);
           }
        }
        });
}

# 查询地址在哪个 so 中

function addr_in_so(addr){
    var process_Obj_Module_Arr = Process.enumerateModules();
    for(var i = 0; i < process_Obj_Module_Arr.length; i++) {
        if(addr>process_Obj_Module_Arr[i].base && addr<process_Obj_Module_Arr[i].base.add(process_Obj_Module_Arr[i].size)){
            console.log(addr.toString(16),"is in",process_Obj_Module_Arr[i].name,"offset: 0x"+(addr-process_Obj_Module_Arr[i].base).toString(16));
        }
    }
}

# patch 内存

  1. 方法一
    该方法存在两个问题

    • 是否存在多线程操纵目标地址处的内存?是否有冲突?
    • arm 的缓存刷新机制
    var str_name_so = "libhookinunidbg.so";    // 要 hook 的 so 名
    var n_addr_func_offset = 0x8CA;         // 要 hook 的函数在函数里面的偏移,thumb 要 + 1
    var n_addr_so = Module.findBaseAddress(str_name_so);
    var n_addr_assemble = n_addr_so.add(n_addr_func_offset);
    Memory.protect(n_addr_assemble, 4, 'rwx'); // 修改内存属性,使程序段可写
    n_addr_assemble.writeByteArray([0x00, 0x20, 0x00, 0xBF]);
  2. 方法二

    var str_name_so = "libhookinunidbg.so";    // 要 hook 的 so 名
    var n_addr_func_offset = 0x8CA;         // 要 hook 的函数在函数里面的偏移,thumb 要 + 1
    var n_addr_so = Module.findBaseAddress(str_name_so);
    var n_addr_assemble = n_addr_so.add(n_addr_func_offset);
    // safely modify bytes at address
    Memory.patchCode(n_addr_assemble, 4, function () {
        // 以 thumb 的方式获取一个 patch 对象
        var cw = new ThumbWriter(n_addr_assemble);
        // 小端序
        // 00 20
        cw.putInstruction(0x2000)
        // 00 BF
        cw.putInstruction(0xBF00);
        cw.flush(); // 内存刷新
        console.log(hexdump(n_addr_assemble))
    });

# 内存检索

function searchAndPatch() {
    var module = Process.findModuleByName("libhookinunidbg.so");
    var pattern = "80 b5 6f 46 84 b0 03 90 02 91"
    var matches = Memory.scanSync(module.base, module.size, pattern);
    console.log(matches.length)
    if (matches.length !== 0)
    {
        var n_addr_assemble = matches[0].address.add(10);
        // safely modify bytes at address
        Memory.patchCode(n_addr_assemble, 4, function () {
            // 以 thumb 的方式获取一个 patch 对象
            var cw = new ThumbWriter(n_addr_assemble);
            // 小端序
            // 00 20
            cw.putInstruction(0x2000)
            // 00 BF
            cw.putInstruction(0xBF00);
            cw.flush(); // 内存刷新
            console.log(hexdump(n_addr_assemble))
        });
    }
}
setImmediate(searchAndPatch);

# 在 so 加载时就进行 hook 操作

so 加载后的首次函数调用流程为 init->initarray->JNI_Onload

android_dlopen_ext 可以在 JNI_Onload 时进行 hook

function hook_dlopen(soName = '') {
    Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
        {
            onEnter: function (args) {
                var pathptr = args[0];
                if (pathptr !== undefined && pathptr != null) {
                    var path = ptr(pathptr).readCString();
                    if (path.indexOf(soName) >= 0) {
                        this.is_can_hook = true;
                    }
                }
            },
            onLeave: function (retval) {
                if (this.is_can_hook) {
                    //do your own code
                }
            }
        }
    );
}
setImmediate(hook_dlopen, "libsec2023.so")

通过 hook linker64call_constructors 可以 hook 到加载 so 时的 init 函数

function hook_call_constructors() {
  var symbols = Process.getModuleByName('linker64').enumerateSymbols();
  var call_constructors_addr = null;
  var get_soname_addr = null;
  for (var i = 0; i < symbols.length; i++) {
    if (symbols[i].name.indexOf('call_constructors') !== -1) {
        console.log(symbols[i].name)
      call_constructors_addr = symbols[i].address;
    }
    else if (symbols[i].name.indexOf('get_soname') !== -1) {
        console.log(symbols[i].name)
      get_soname_addr = symbols[i].address;
    }
  }
    Interceptor.attach(call_constructors_addr, {
    onEnter: function(args) {
		console.log("init call!")
    },
        onLeave: function (args) {
        }
  });
}

# 限定于某函数

比如某个函数在 SO 中被大量使用,现在只想分析这个函数在函数 targetfunction 中的使用。

var show = false;
Interceptor.attach(
    Module.findExportByName("libc.so", "strcmp"), {
        onEnter: function(args) {
            if(show){
                console.log("strcmp arg1:"+args[0].readCString())
            }
        },
        onLeave: function(ret) {
        }
    }
);
Interceptor.attach(
    Module.findExportByName("libhookinunidbg.so", "targetfunction"),{
        onEnter: function(args) {
            show = this;
        },
        onLeave: function(ret) {
            show = false;
        }
    }
)

# trace so 中 native 函数的调用

stalker_trace_so

使用 stalker_trace_so 打印 so 从加载到内存开始的所调用的函数

trace_natives

使用 trace_natives 对 一个 SO 中的全部函数进行 Trace,并形成如下调用图。

效果

# 打印堆栈

打印尽可能准确的堆栈信息

console.log('RegisterNatives called from:\\n' + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\\n') + '\\n');

打印尽可能多的堆栈信息

console.log('RegisterNatives called from:\\n' + Thread.backtrace(this.context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join('\\n') + '\\n');

如果上面打印堆栈的代码报错退出了,可以手动打印

function addr_in_so(addr){
    var process_Obj_Module_Arr = Process.enumerateModules();
    for(var i = 0; i < process_Obj_Module_Arr.length; i++) {
        if(addr>process_Obj_Module_Arr[i].base && addr<process_Obj_Module_Arr[i].base.add(process_Obj_Module_Arr[i].size)){
            console.log(addr.toString(16),"is in",process_Obj_Module_Arr[i].name,"offset: 0x"+(addr-process_Obj_Module_Arr[i].base).toString(16));
        }
    }
}
Thread.backtrace(this.context, Backtracer.FUZZY).map(addr_in_so);

# 打印导入表

function hook_import(){
    var imports=Module.enumerateImports("libjiagu_64.so");
    for(var i=0;i<imports.length;i++){
        console.log(imports[i].name+" "+DebugSymbol.fromAddress(imports[i].address));
    }
}

# 打印寄存器

// 打印所有的寄存器
console.log(JSON.stringify(this.context))
// 打印某一个的寄存器
console.log(this.context.x0)

# 打开并读取某个文件

function read_file(){
    var module = Process.findModuleByName("libjiagu_64.so");
    Interceptor.attach(module.base.add(0x120C30), {
        onEnter: function (args) {
          const openPtr = Module.getExportByName('libc.so', 'open');
          const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
          var readPtr = Module.findExportByName("libc.so", "read");
          var read = new NativeFunction(readPtr, 'int', ['int', 'pointer', "int"]);
          var fileName = "/data/data/com.oacia.apk_protect/lib/libjgdtc.so";
          var buffer = Memory.alloc(512);
          var realFd = open(Memory.allocUtf8String(fileName), 4);
          read(realFd, buffer, 512);
          console.log(hexdump(buffer, {
              offset: 0,// 相对偏移
              length: 0x50,//dump 的大小
              header: true,
              ansi: true
            }));
        },
        onLeave: function (ret) {
        }
    });
}

# 常用反调试函数

dbus

function anti_dbus(){
    Interceptor.attach(Module.findExportByName(null, "connect"), {
        // fd, buff, len
        onEnter: function (args) {
            console.log("connect")
        },
        onLeave: function (ret) {
        }
    });
}

strstr

function anti_strstr(){
    const strStr = Module.findExportByName(null, "strstr");
    Interceptor.attach(strStr,{
            onEnter: function(args){
                var haystackstr = args[0].readCString();
                var needle_content = args[0].readCString();
                if (haystackstr.indexOf('frida')!==-1 ||
                    needle_content.indexOf('frida')!==-1 ||
                    haystackstr.indexOf('gum-js-loop')!==-1 ||
                    needle_content.indexOf('gum-js-loop')!==-1 ||
                    haystackstr.indexOf('gmain')!==-1 ||
                    needle_content.indexOf('gmain')!==-1 ||
                    haystackstr.indexOf('linjector')!==-1 ||
                    needle_content.indexOf('linjector')!==-1){
                    this.strstr_bypass=true
                }
            },
            onLeave: function(retval){
                if(this.strstr_bypass){
                    console.log("strstr bypass")
                    retval.replace(0x0);
                }
            }
            });
}

readlink

function anti_readlink(){
    const readlink_ptr = Module.findExportByName(null, "readlink");
    console.log('readlink_ptr: ',  readlink_ptr);
    if(!readlink_ptr){
        return
    }
    const readlink = new NativeFunction(readlink_ptr, 'int', ['pointer', 'pointer', 'pointer'])
    Interceptor.replace(readlink_ptr, new NativeCallback(function(path, buf, size){
        var retval = readlink(path, buf, size);
        var bufstr = buf.readCString();
        console.log(bufstr)
        if (bufstr.indexOf('frida')!==-1 ||
            bufstr.indexOf('gum-js-loop')!==-1 ||
            bufstr.indexOf('gmain')!==-1 ||
            bufstr.indexOf('linjector')!==-1){
            console.log(`\nreadlink(path=${path.readCstring()}, buf=${bufstr}, size=${size}`);
            this.buf.writeUtf8String("/system/framework/boot.art")
            console.log("replce with: "+ this.buf.readCString())
            return 0x1A;
        }
        return retval;
    }, 'int', ['pointer', 'pointer', 'pointer']))
}

TracerPid

function check_TracerPid(){
    var fgetsPtr = Module.findExportByName("libc.so", "fgets");
    var fgets = new NativeFunction(fgetsPtr, 'pointer', ['pointer', 'int', 'pointer']);
    Interceptor.replace(fgetsPtr, new NativeCallback(function (buffer, size, fp) {
        var retval = fgets(buffer, size, fp);
        var bufstr = Memory.readUtf8String(buffer);
        //console.log(bufstr)
        if (bufstr.indexOf("TracerPid:") > -1) {
            console.log(bufstr);
            Memory.writeUtf8String(buffer, "TracerPid:\t0");
            console.log("tracerpid replaced: " + Memory.readUtf8String(buffer));
        }
        return retval;
    }, 'pointer', ['pointer', 'int', 'pointer']));
}

pthread_create

function check_pthread_create() {
    var pthread_create_addr = Module.findExportByName(null, 'pthread_create');
    var pthread_create = new NativeFunction(pthread_create_addr, "int", ["pointer", "pointer", "pointer", "pointer"]);
    Interceptor.replace(pthread_create_addr, new NativeCallback(function (parg0, parg1, parg2, parg3) {
        var so_name = Process.findModuleByAddress(parg2).name;
        var so_path = Process.findModuleByAddress(parg2).path;
        var so_base = Module.getBaseAddress(so_name);
        var offset = parg2 - so_base;
        var PC = 0;
        if ((so_name.indexOf("jiagu") > -1)) {
            console.log("======")
            console.log("find thread func offset", so_name, offset.toString(16));
            Thread.backtrace(this.context, Backtracer.ACCURATE).map(addr_in_so);
            var check_list = []//1769036,1771844
            if (check_list.indexOf(offset)!==-1) {
                console.log("check bypass")
            } else {
                PC = pthread_create(parg0, parg1, parg2, parg3);
            }
        } else {
            PC = pthread_create(parg0, parg1, parg2, parg3);
        }
        return PC;
    }, "int", ["pointer", "pointer", "pointer", "pointer"]))
}
更新于 阅读次数