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

Frida 源码分析

2024-8-15 16:13
2697

Frida 源码分析

Frida 简介

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

但是,在使用的大多时候,我们都只是使用 js 代码对指定的地址/java 对象进行了 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
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
.
├── 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-core

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

Inject--Java 层注入

  • 目的 学习对于安卓程序 hook java 层代码
  • 猜想 在内存中查找 dex 文件,匹配对应的类和方法修改其指向的函数地址

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

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

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

1
2
3
4
5
6
7
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();

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

vm.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
  }

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

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

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
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);
    }
}

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

Jvm 下的类

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

加载

类加载器
1
2
类加载器是一个负责加载类的对象。ClassLoader 是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。
每个 Java 类都有一个引用指向加载它的 ClassLoader。不过,数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。

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

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

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

1
2
3
ClassLoader 类有两个关键的方法:
    protected Class loadClass(String name, boolean resolve):加载指定二进制名称的类,实现了双亲委派机制 。name 为类的二进制名称,resolve 如果为 true,在加载时调用 resolveClass(Class<?> c) 方法解析该类。
    protected Class findClass(String name):根据类的二进制名称来查找类,默认实现是空方法。
双亲委派

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

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
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;
    }
}

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

  • 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
  • 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
  • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。
  • 如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常。

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

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

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
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--;
        }
      }
    }
  };

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

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
//lib/class-factory.js
this.use = function (className) {
    let C = classes[className]; //先从缓存中查找
    if (!C) {
      const env = vm.getEnv();  //获取jni_env, 调用native层的JavaVm.GetEnv 
      if (loader !== null) {  //loader已经在Java.perform中初始化了
        const usedLoader = loader;
  
        if (cachedLoaderMethod === null) {
          cachedLoaderInvoke = env.vaMethod('pointer', ['pointer']);
          cachedLoaderMethod = loader.loadClass.overload('java.lang.String').handle;
        }
  
        const getClassHandle = function (env) {
          const classNameValue = env.newStringUtf(className);
          const tid = Process.getCurrentThreadId();
          ignore(tid);
          try {       //env.handle 指向jni层的JNIEnv, 利用jni调用classloader.loadClass(className)
            return cachedLoaderInvoke(env.handle, usedLoader.$handle, cachedLoaderMethod, classNameValue);
          } finally {
            unignore(tid);
            env.deleteLocalRef(classNameValue);
          }
        };
        //构建对应java类的wrapper
        C = ensureClass(getClassHandle, className);
      }
    }

Hook java 层

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

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

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

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
class DalvikMethodMangler {
  constructor (methodId) {
    this.methodId = methodId;
    this.originalMethod = null;
  }
 
  replace (impl, isInstanceMethod, argTypes, vm, api) {
    const { methodId } = this;
 
    this.originalMethod = Memory.dup(methodId, DVM_METHOD_SIZE);
 
    let argsSize = argTypes.reduce((acc, t) => (acc + t.size), 0);
    if (isInstanceMethod) {
      argsSize++;
    }
 
    /* 设置成native函数
     * make method native (with kAccNative)
     * insSize and registersSize are set to arguments size
     */
    const accessFlags = (methodId.add(DVM_METHOD_OFFSET_ACCESS_FLAGS).readU32() | kAccNative) >>> 0;
    const registersSize = argsSize;
    const outsSize = 0;
    const insSize = argsSize;
    // 按照frida的注释,这段就是在修改method的属性使其以native函数运行
    methodId.add(DVM_METHOD_OFFSET_ACCESS_FLAGS).writeU32(accessFlags);
    methodId.add(DVM_METHOD_OFFSET_REGISTERS_SIZE).writeU16(registersSize);
    methodId.add(DVM_METHOD_OFFSET_OUTS_SIZE).writeU16(outsSize);
    methodId.add(DVM_METHOD_OFFSET_INS_SIZE).writeU16(insSize);
    methodId.add(DVM_METHOD_OFFSET_JNI_ARG_INFO).writeU32(computeDalvikJniArgInfo(methodId));
 
    api.dvmUseJNIBridge(methodId, impl);//函数作用将method的nativeFunc成员变量替换为impl函数,impl函数是经过转换的native函数
    }
  ···
  }

在 art 中的 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
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class ArtMethodMangler {
  constructor (opaqueMethodId) {
    const methodId = unwrapMethodId(opaqueMethodId);
 
    this.methodId = methodId;
    this.originalMethod = null;
    this.hookedMethodId = methodId;
    this.replacementMethodId = null;
 
    this.interceptor = null;
  }
 
  replace (impl, isInstanceMethod, argTypes, vm, api) {
    const { kAccCompileDontBother, artNterpEntryPoint } = api;
 
    this.originalMethod = fetchArtMethod(this.methodId, vm); // 对methodid进行解析,将原函数进行保存
 
    const originalFlags = this.originalMethod.accessFlags;
 
    if ((originalFlags & kAccXposedHookedMethod) !== 0 && xposedIsSupported()) {
      const hookInfo = this.originalMethod.jniCode;
      this.hookedMethodId = hookInfo.add(2 * pointerSize).readPointer();
      this.originalMethod = fetchArtMethod(this.hookedMethodId, vm);
    }
 
    const { hookedMethodId } = this;
 
    const replacementMethodId = cloneArtMethod(hookedMethodId, vm); // 在内存中拷贝一份methodID的结构
    this.replacementMethodId = replacementMethodId;
 
    // 将拷贝的methodID结构中规定的入口点等全部改成hook的结构
    patchArtMethod(replacementMethodId, {
      jniCode: impl,
      accessFlags: ((originalFlags & ~(kAccCriticalNative | kAccFastNative | kAccNterpEntryPointFastPathFlag)) | kAccNative | kAccCompileDontBother) >>> 0,
      quickCode: api.artClassLinker.quickGenericJniTrampoline,
      interpreterCode: api.artInterpreterToCompiledCodeBridge
    }, vm);
 
    // Remove kAccFastInterpreterToInterpreterInvoke and kAccSkipAccessChecks to disable use_fast_path
    // in interpreter_common.h
    let hookedMethodRemovedFlags = kAccFastInterpreterToInterpreterInvoke | kAccSingleImplementation | kAccNterpEntryPointFastPathFlag;
    if ((originalFlags & kAccNative) === 0) {
      hookedMethodRemovedFlags |= kAccSkipAccessChecks;
    }
 
    patchArtMethod(hookedMethodId, {
      accessFlags: ((originalFlags & ~(hookedMethodRemovedFlags)) | kAccCompileDontBother) >>> 0
    }, vm);
 
    const quickCode = this.originalMethod.quickCode;
 
    // Replace Nterp quick entrypoints with art_quick_to_interpreter_bridge to force stepping out
    // of ART's next-generation interpreter and use the quick stub instead.
    if (artNterpEntryPoint !== undefined && quickCode.equals(artNterpEntryPoint)) {
      patchArtMethod(hookedMethodId, {
        quickCode: api.artQuickToInterpreterBridge
      }, vm);
    }
 
    if (!isArtQuickEntrypoint(quickCode)) {
      const interceptor = new ArtQuickCodeInterceptor(quickCode);
      interceptor.activate(vm);
 
      this.interceptor = interceptor;
    }
 
    artController.replacedMethods.set(hookedMethodId, replacementMethodId);
 
    notifyArtMethodHooked(hookedMethodId, vm);
  }
···
}

其中两个函数实现

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
function fetchArtMethod (methodId, vm) {
  const artMethodSpec = getArtMethodSpec(vm); // 自己实现的一个函数,提取os中的虚函数表,遍历得到ArtMethod结构,并将所有偏移保存到offset中
  const artMethodOffset = artMethodSpec.offset;
  return (['jniCode', // JNI桥接代码的入口点。当一个Java方法通过JNI调用本地(Native)代码时,这个字段指向的就是该JNI方法的执行入口。此字段在JNI方法(如用native关键字声明的方法)中非常重要,用于连接Java和C/C++代码。
  'accessFlags',    // 位掩码(bitmask)
   'quickCode',     // 方法的快速编译代码入口。ART中引入了AOT(Ahead-of-Time)编译,将Java方法编译为本地机器码以加快执行速度。这个字段存储了编译后的代码地址,调用该方法时将直接跳转到此处执行机器码,从而提升性能。
   'interpreterCode'] // 指向解释器执行入口。当一个方法没有被编译为机器码,或者当前的执行模式是解释执行时,这个字段指向的方法代码会由解释器逐条解释并执行。解释器执行的速度比编译后的代码慢,但对于一些特定场景(如动态加载代码)依然非常有用。
    .reduce((original, name) => {
      const offset = artMethodOffset[name];
      if (offset === undefined) {
        return original;
      }
      const address = methodId.add(offset);
      const read = (name === 'accessFlags') ? readU32 : readPointer;
      original[name] = read.call(address);
      return original;
    }, {}));
}
 
function patchArtMethod (methodId, patches, vm) {
  const artMethodSpec = getArtMethodSpec(vm);
  const artMethodOffset = artMethodSpec.offset;
  Object.keys(patches).forEach(name => {
    const offset = artMethodOffset[name];
    if (offset === undefined) {
      return;
    }
    const address = methodId.add(offset);
    const write = (name === 'accessFlags') ? writeU32 : writePointer;
    write.call(address, patches[name]);
  });
}

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

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


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 4
支持
分享
最新回复 (8)
雪    币: 38
活跃值: (1907)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
tql
2024-8-15 17:01
0
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
学到了,感谢楼主
2024-8-16 00:07
0
雪    币: 1939
活跃值: (2523)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
nb
2024-8-16 08:51
0
雪    币: 859
活跃值: (910)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
tql
2024-8-16 09:15
0
雪    币: 2206
活跃值: (3721)
能力值: ( LV6,RANK:81 )
在线值:
发帖
回帖
粉丝
6
收藏=学会
2024-8-16 10:25
0
雪    币: 3440
活跃值: (3822)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
楼主,native层呢?
2024-8-16 12:52
0
雪    币: 392
活跃值: (917)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
学到了
2024-8-17 09:45
0
雪    币: 392
活跃值: (917)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9

学到了,感谢楼主

2024-8-17 09:46
0
游客
登录 | 注册 方可回帖
返回
//