首页
社区
课程
招聘
Frida 源码分析
发表于: 2024-8-15 16:13 5218

Frida 源码分析

2024-8-15 16:13
5218

简单介绍一下 frida,frida 是一款用来进行 hook 的框架注入工具,其暴露给用户的接口,可以用 js 脚本语言进行方便快捷的 hook 和注入,而且其支持三种主流平台:pc/ios/andriod,尤其在安卓平台上,用 frida 可以说是必备技能。

但是,在使用的大多时候,我们都只是使用 js 代码对指定的地址/java 对象进行了 hook,其实现的原理并没有认真的去分析,近来觉得技术上有了瓶颈,逐花时间尝试对其原理进行分析。

把其源码结构看清之后,单独把 gum 项目和 core 项目摘出来,gum 项目是整个 frida 的基础框架,包括注入,不同平台,不同架构等基础转换实现原理都在这个项目中。

在 js 中调用的 Hook java 层的代码位于另一个项目分支 frida_java_bridge 中,其使用了 gum 项目中的 native 层 hook 函数。

流程如下:frida-js->frida-gum-> 调用 native_jni->jni 链接 java

首先需要获取系统加载的虚拟机,使用 JNI_GetCreatedJavaVMs 获取,该函数是 jvm 的导出函数,由 libart.so 或 libdvm.so 进行导出

有了系统运行的虚拟机,就可以调用虚拟机中的 jni 函数。

vm.js

调用 NativeFunction 获取 vm 虚函数表指针,实现最基础的 js 调用 jvm。

NativeFunction 实现原理,将 native 层的调用格式转换成 js 能够直接调的格式,相当于导出了 jvm 的函数。

实现了 jvm 的函数导出和调用,接下来还需要定位到 class,在 jvm 中定位一个加载的 class 通过 findclass 来找到。

在 java 环境下,所有可执行的程序被编译成 class 文件也叫类,类从被加载到虚拟机内存中开始到卸载出内存为止,整个生命周期为 加载-》验证-》准备-》解析-》初始化-》使用-》卸载

其作用是将一个 class 文件的字节码以 classloader 定义的方式加载到虚拟机的内存中,在虚拟机内存中有一块内存位置叫代码区,类加载以自己的规则去声明类的内存。

在 jvm 启动的时候会加载需要的类,在加载时会先判断当前类是否已经加载,未加载才会调用加载器进行加载。加载器加载时先会调用父类加载器,如果加载不成功再使用该类的加载器加载,这种机制被称为双亲委派。

可以自己实现一个类加载器,需要继承 classLoader 抽象类

ClassLoader 类使用委托模型来搜索类和资源。每个 ClassLoader 实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
虚拟机中被称为 "bootstrap class loader"的内置类加载器本身没有父类加载器,但是可以作为 ClassLoader 实例的父类加载器。

总结一下双亲委派模型的执行流程:

JVM 判定两个 Java 类是否相同的具体规则:JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同。

回到 frida 中,了解了类的加载,那么 frida 使用 findclass 就必须要先获取到 java 类的 classloader,但是 frida 所在的线程是通过 pthread_create 创建,然后调用 AttachCurrentThread 获取的 JNIEnv,此时 FindClass 只会从系统的 classloader 开始查找,所以 app 自身的类是无法通过 env->findClass 来获取。因此需要手工的获取到加载该 app 的 classloader。

frida 使用 Java.use 来获得类对象的引用:

对于安卓,谷歌将虚拟机需要加载的 class 文件替换成了 dex 文件,修改后的 dex 文件格式如下

具体结构和 pe 文件大同小异,也是由文件头规范哪种规格,指针指向文件偏移查找对应 code or data。

那 hook 的实现其实也是类似 pe 里的 hook,不过谷歌给我们提供了一个 hook 函数 dvmUseJNIBridge

在 art 中的 hook 没有这么简单

其中两个函数实现

在 art 中描述方法的方式被修饰到了 ArtMethod 中,对于 frida 来说,重要的只有'jniCode', 'accessFlags', 'quickCode', 'interpreterCode'四个字段,而这四个字段代表了一个函数的执行方式和行为,替换了之后便达到了 hook 的目的。

总的来说,不是查找的 dex 文件,是直接在虚拟机中找到对应的类再找到对应的 methodID 再进行修改,但是具体实现确实没有想的这么简单,需要对 jvm 和 dvm/art 中关于方法的描述了解透彻才能有这么好的思路

.
├── frida-clr
├── frida-core   frida 的主要功能实现模块
├── frida-gum    frida的基础框架,提供inline hook、代码追踪、内存监控、符号查找等
├── frida-qml    qt的qml界面
├── frida-tools  frida-tools
└── releng       编译相关
 
frida-gum
├── bindings               绑定js平台所用入口
│   ├── gumjs
│   └── gumpp
├── docs                   
├── ext                    主要是win平台的调试相关
│   ├── dbghelp
│   └── symsrv
├── gum                    gum项目的主要实现目录,包括注入、库相关、内存管理、api解决等,子目录包含对不同平台封装的一些汇编指令
│   ├── arch-arm
│   ├── arch-arm64
│   ├── arch-mips
│   ├── arch-x86
│   ├── backend-arm
│   ├── backend-arm64
│   ├── backend-darwin
│   ├── backend-dbghelp
│   ├── backend-elf
│   ├── backend-freebsd
│   ├── backend-libdwarf
│   ├── backend-libunwind
│   ├── backend-linux
│   ├── backend-mips
│   ├── backend-posix
│   ├── backend-qnx
│   ├── backend-windows
│   └── backend-x86
├── libs                   导出目录
│   └── gum
├── subprojects            导入目录
├── tests                  
│   ├── core
│   ├── data
│   ├── gumjs
│   ├── gumpp
│   ├── heap
│   ├── prof
│   └── stubs
├── tools
└── vapi                   vala的支持目录
 
frida-core                一些上层实现
├── inject                 调api
├── lib                    库
│   ├── agent              注入时调用的库
│   ├── base
│   ├── gadget
│   ├── payload
│   ├── pipe
│   └── selinux
├── portal               
├── server
├── src
│   ├── api
│   ├── compiler
│   ├── darwin
│   ├── droidy
│   ├── freebsd
│   ├── fruity
│   ├── linux
│   ├── qnx
│   ├── socket
│   └── windows
├── tests
│   ├── labrats
│   └── pipe
├── tools
└── vapi
.
├── frida-clr
├── frida-core   frida 的主要功能实现模块
├── frida-gum    frida的基础框架,提供inline hook、代码追踪、内存监控、符号查找等
├── frida-qml    qt的qml界面
├── frida-tools  frida-tools
└── releng       编译相关
 
frida-gum
├── bindings               绑定js平台所用入口
│   ├── gumjs
│   └── gumpp
├── docs                   
├── ext                    主要是win平台的调试相关
│   ├── dbghelp
│   └── symsrv
├── gum                    gum项目的主要实现目录,包括注入、库相关、内存管理、api解决等,子目录包含对不同平台封装的一些汇编指令
│   ├── arch-arm
│   ├── arch-arm64
│   ├── arch-mips
│   ├── arch-x86
│   ├── backend-arm
│   ├── backend-arm64
│   ├── backend-darwin
│   ├── backend-dbghelp
│   ├── backend-elf
│   ├── backend-freebsd
│   ├── backend-libdwarf
│   ├── backend-libunwind
│   ├── backend-linux
│   ├── backend-mips
│   ├── backend-posix
│   ├── backend-qnx
│   ├── backend-windows
│   └── backend-x86
├── libs                   导出目录
│   └── gum
├── subprojects            导入目录
├── tests                  
│   ├── core
│   ├── data
│   ├── gumjs
│   ├── gumpp
│   ├── heap
│   ├── prof
│   └── stubs
├── tools
└── vapi                   vala的支持目录
 
frida-core                一些上层实现
├── inject                 调api
├── lib                    库
│   ├── agent              注入时调用的库
│   ├── base
│   ├── gadget
│   ├── payload
│   ├── pipe
│   └── selinux
├── portal               
├── server
├── src
│   ├── api
│   ├── compiler
│   ├── darwin
│   ├── droidy
│   ├── freebsd
│   ├── fruity
│   ├── linux
│   ├── qnx
│   ├── socket
│   └── windows
├── tests
│   ├── labrats
│   └── pipe
├── tools
└── vapi
const vms = Memory.alloc(pointerSize);
  const vmCount = Memory.alloc(jsizeSize);
  checkJniResult('JNI_GetCreatedJavaVMs', temporaryApi.JNI_GetCreatedJavaVMs(vms, 1, vmCount));
  if (vmCount.readInt() === 0) {
    return null;
  }
  temporaryApi.vm = vms.readPointer();
const vms = Memory.alloc(pointerSize);
  const vmCount = Memory.alloc(jsizeSize);
  checkJniResult('JNI_GetCreatedJavaVMs', temporaryApi.JNI_GetCreatedJavaVMs(vms, 1, vmCount));
  if (vmCount.readInt() === 0) {
    return null;
  }
  temporaryApi.vm = vms.readPointer();
const handle = api.vm;
  let attachCurrentThread = null;
  let detachCurrentThread = null;
  let getEnv = null;
 
  function initialize () {
    const vtable = handle.readPointer();
    const options = {
      exceptions: 'propagate'
    };
    attachCurrentThread = new NativeFunction(vtable.add(4 * pointerSize).readPointer(), 'int32', ['pointer', 'pointer', 'pointer'], options);
    detachCurrentThread = new NativeFunction(vtable.add(5 * pointerSize).readPointer(), 'int32', ['pointer'], options);
    getEnv = new NativeFunction(vtable.add(6 * pointerSize).readPointer(), 'int32', ['pointer', 'pointer', 'int32'], options);
  }
const handle = api.vm;
  let attachCurrentThread = null;
  let detachCurrentThread = null;
  let getEnv = null;
 
  function initialize () {
    const vtable = handle.readPointer();
    const options = {
      exceptions: 'propagate'
    };
    attachCurrentThread = new NativeFunction(vtable.add(4 * pointerSize).readPointer(), 'int32', ['pointer', 'pointer', 'pointer'], options);
    detachCurrentThread = new NativeFunction(vtable.add(5 * pointerSize).readPointer(), 'int32', ['pointer'], options);
    getEnv = new NativeFunction(vtable.add(6 * pointerSize).readPointer(), 'int32', ['pointer', 'pointer', 'int32'], options);
  }
class NativeFunction extends Function {
    handle: BNativePointer;
 
    #retType: Marshaler;
    #argTypes: Marshaler[];
 
    constructor(address: BNativePointer, retType: NativeFunctionReturnType, argTypes: NativeFunctionArgumentType[]) {
        super();
 
        this.handle = address;
 
        this.#retType = getMarshalerFor(retType);
        this.#argTypes = argTypes.map(getMarshalerFor);
 
        return new Proxy(this, {
            apply(target, thiz, args) {
                return target._invoke(args);
            }
        });
    }
// 真实调用位置,在ts里代表陷阱函数,执行到代理函数时,此处会调用
    _invoke(args: any[]): any {
        const nativeArgs = args.map((v, i) => this.#argTypes[i].toNative(v));
        const nativeRetval = _invoke(this.handle.$v, ...nativeArgs); //调用原来真实的调用
        return this.#retType.fromNative(nativeRetval);
    }
}
class NativeFunction extends Function {
    handle: BNativePointer;
 
    #retType: Marshaler;
    #argTypes: Marshaler[];
 
    constructor(address: BNativePointer, retType: NativeFunctionReturnType, argTypes: NativeFunctionArgumentType[]) {
        super();
 
        this.handle = address;
 
        this.#retType = getMarshalerFor(retType);
        this.#argTypes = argTypes.map(getMarshalerFor);
 
        return new Proxy(this, {
            apply(target, thiz, args) {
                return target._invoke(args);
            }
        });
    }
// 真实调用位置,在ts里代表陷阱函数,执行到代理函数时,此处会调用
    _invoke(args: any[]): any {
        const nativeArgs = args.map((v, i) => this.#argTypes[i].toNative(v));
        const nativeRetval = _invoke(this.handle.$v, ...nativeArgs); //调用原来真实的调用
        return this.#retType.fromNative(nativeRetval);
    }
}
类加载器是一个负责加载类的对象。ClassLoader 是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。
每个 Java 类都有一个引用指向加载它的 ClassLoader。不过,数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。
类加载器是一个负责加载类的对象。ClassLoader 是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。
每个 Java 类都有一个引用指向加载它的 ClassLoader。不过,数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。
ClassLoader 类有两个关键的方法:
    protected Class loadClass(String name, boolean resolve):加载指定二进制名称的类,实现了双亲委派机制 。name 为类的二进制名称,resolve 如果为 true,在加载时调用 resolveClass(Class<?> c) 方法解析该类。
    protected Class findClass(String name):根据类的二进制名称来查找类,默认实现是空方法。
ClassLoader 类有两个关键的方法:
    protected Class loadClass(String name, boolean resolve):加载指定二进制名称的类,实现了双亲委派机制 。name 为类的二进制名称,resolve 如果为 true,在加载时调用 resolveClass(Class<?> c) 方法解析该类。
    protected Class findClass(String name):根据类的二进制名称来查找类,默认实现是空方法。
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //首先,检查该类是否已经加载过
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果 c 为 null,则说明该类没有被加载过
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //当父类的加载器不为空,则通过父类的loadClass来加载该类
                    c = parent.loadClass(name, false);
                } else {
                    //当父类的加载器为空,则调用启动类加载器来加载该类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //非空父类的类加载器无法找到相应的类,则抛出异常
            }
 
            if (c == null) {
                //当父类加载器无法加载时,则调用findClass方法来加载该类
                //用户可通过覆写该方法,来自定义类加载器
                long t1 = System.nanoTime();
                c = findClass(name);
 
                //用于统计类加载器相关的信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            //对类进行link操作
            resolveClass(c);
        }
        return c;
    }
}
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //首先,检查该类是否已经加载过
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果 c 为 null,则说明该类没有被加载过
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //当父类的加载器不为空,则通过父类的loadClass来加载该类
                    c = parent.loadClass(name, false);
                } else {
                    //当父类的加载器为空,则调用启动类加载器来加载该类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //非空父类的类加载器无法找到相应的类,则抛出异常
            }
 
            if (c == null) {
                //当父类加载器无法加载时,则调用findClass方法来加载该类
                //用户可通过覆写该方法,来自定义类加载器
                long t1 = System.nanoTime();
                c = findClass(name);
 
                //用于统计类加载器相关的信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            //对类进行link操作
            resolveClass(c);
        }
        return c;
    }
}
this.perform = function (fn) {
    assertJavaApiIsAvailable();
      //目标进程不是app,并且classloader已经初始化
    if (!isAppProcess() || classFactory.loader !== null) {
      threadsInPerform++;
      try {
        vm.perform(fn);
      } catch (e) {
        setTimeout(() => { throw e; }, 0);
      } finally {
        threadsInPerform--;
      }
    } else {
      //第一次调用java.perform时,会先获取加载该app的classloader
      pending.push(fn);
      if (pending.length === 1) {
        threadsInPerform++;
        try {
          vm.perform(() => {
            const ActivityThread = classFactory.use('android.app.ActivityThread');
            const app = ActivityThread.currentApplication();
            if (app !== null) {
                    //获取到加载该app的classloader
              classFactory.loader = app.getClassLoader();
              performPending(); // already initialized, continue
            } else {
              const m = ActivityThread.getPackageInfoNoCheck;
              let initialized = false;
              m.implementation = function () {
                const apk = m.apply(this, arguments);
                if (!initialized) {
                  initialized = true;
                  classFactory.loader = apk.getClassLoader();
                  performPending();
                }
                return apk;
              };
            }
          });
        } finally {
          threadsInPerform--;
        }
      }
    }
  };
this.perform = function (fn) {
    assertJavaApiIsAvailable();
      //目标进程不是app,并且classloader已经初始化
    if (!isAppProcess() || classFactory.loader !== null) {
      threadsInPerform++;
      try {
        vm.perform(fn);
      } catch (e) {
        setTimeout(() => { throw e; }, 0);
      } finally {
        threadsInPerform--;
      }
    } else {
      //第一次调用java.perform时,会先获取加载该app的classloader
      pending.push(fn);
      if (pending.length === 1) {
        threadsInPerform++;
        try {

[培训]科锐逆向工程师培训第53期2025年7月8日开班!

收藏
免费 6
支持
分享
最新回复 (9)
雪    币: 116
活跃值: (2758)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
tql
2024-8-15 17:01
0
雪    币: 6
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
学到了,感谢楼主
2024-8-16 00:07
0
雪    币: 2683
活跃值: (3384)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
nb
2024-8-16 08:51
0
雪    币: 868
活跃值: (980)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
tql
2024-8-16 09:15
0
雪    币: 2779
活跃值: (4483)
能力值: ( LV6,RANK:81 )
在线值:
发帖
回帖
粉丝
6
收藏=学会
2024-8-16 10:25
0
雪    币: 4676
活跃值: (5046)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
楼主,native层呢?
2024-8-16 12:52
0
雪    币: 721
活跃值: (1462)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
学到了
2024-8-17 09:45
0
雪    币: 721
活跃值: (1462)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9

学到了,感谢楼主

2024-8-17 09:46
0
雪    币: 2683
活跃值: (3384)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
tql
2024-11-18 19:27
0
游客
登录 | 注册 方可回帖
返回