Aop-安全领域的魔法

AOP,将切面思想融入到安全领域

AOP 全称为 Aspect-oriented Programming,即面向切面编程。通过预编译,运行时动态代理或者注入等方式,不直接修改源码的情况下向业务逻辑中添加新功能的的编程范式。AOP 技术现在被广泛运用于各个平台的研发生产流程中,不管是客户端还是服务端,又或者是 Java,Objective-C,C++ 等多种语言都存在不同的 AOP 技术。这种不侵入业务开发流程的能力,也正是安全人员需要的,在不影响业务正常发展的前提下,完成必要的安全需求。在这篇文章中,笔者将会尽可能全的介绍相关的 AOP 技术,同时由于笔者对于移动端较为熟悉,所以本篇文章先覆盖移动端,例如服务端 RASP 暂时不涉及。

AOP 背景

移动端AOP(面向切面编程)技术是一种编程范式,它允许开发者将横切关注点(cross-cutting concerns)与核心业务逻辑分离。在移动端(Android和iOS)开发中,AOP技术可以帮助开发者更高效地处理诸如日志记录、性能监控、权限管理、安全检查等通用功能。

移动端 AOP 技术可分为以下两个层面:

  1. 语言层面:Java、C、OC 等。
  2. 切入时机:编译时、加载时、运行时。

Android 与 iOS 基础

特性 Android iOS
操作系统 基于 Linux 内核 基于 Darwin 内核
开发语言 Java, Kotlin, C/C++ Objective-C, Swift,C/C++
运行时环境 Android Runtime (ART) Objective-C Runtime, Swift Runtime
应用安装包格式 APK (Android Package) IPA (iOS App Store Package)
代码执行方式 JIT (Just-In-Time) 和 AOT (Ahead-Of-Time) 编译,比较老的还有解释执行 AOT (Ahead-Of-Time) 编译
编译过程 Java文件 -> .class 文件 -> .dex文件 -> apk .m/.swift 文件 -> .o 文件 -> Mach-O 可执行文件
运行过程 创建进程 -> 加载运行时库 -> 创建虚拟机实例 -> 加载应用 dex 文件 启动应用 -> 创建进程 -> 加载 Mach-o 文件 -> 加载动态库
AOP 相关概念 Java 字节码, Java 反射机制, JNI (Java Native Interface) Objective-C 消息传递机制, Method Swizzling, 动态库注入
AOP 技术中的语言特性 Java: 基于 JVM,支持字节码操作,反射 Objective-C: 动态语言,支持消息传递和方法交换,C: 与操作系统更紧密,支持动态库注入和以及各种hook方法

Android

  1. 开发

    • Java:基于虚拟机的类加载,字节码解释执行,AOT和JIT,解释执行。
    • kotlin:国内 APP 还是以 Java 为主,kotlin 这里暂时不涉及。
    • c/c++:native 层开发,基于 NDK。
  2. 编译

    • Java文件 -> .class 文件 -> .dex文件 -> apk(签名)
    • c/c++文件 -> ndk编译为 so -> 打包到 apk 中

    img

  3. 安装

    1. 下载安装,校验签名,资源等
    2. 注册应用到系统中,包括各种权限,资源文件等
  4. 运行

    • 创建第一个进程

    • 加载各种运行时库

    • 创建虚拟机实例( Android 5 以后为 ART 虚拟机,逐渐有了 jit ,aot 的能力)

    • 加载应用 dex 文件,并将类,方法注册进虚拟机中

    • 应用正式启动

iOS

  1. 开发
    • objective-c:动态方法分发、多态和运行时类型检查等特性。通过消息传递机制,Objective-C 运行时能够在运行时确定方法调用的目标,实现动态方法解析和替换等高级功能。
    • swift
    • c/c++
  2. 编译

img

img

  1. 安装
    • 安装应用
    • 校验签名
    • 分配沙盒
  2. 运行
    • 启动应用,创建进程
    • 加载 Mach-o文件
    • 读 Mach-o 文件头,获取元数据
    • dyld动态链接加载,加载动态库,包括各种系统库,三方库
    • 解析符号,将可执行文件和动态库中的符号,链接到正确的内存地址,包括函数调用,全局变量等
    • 初始化环境,包括创建主线程等

Android AOP 技术

Java层

img

静态AOP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
  • 原理:编译时 class 字节码修改
  • 代表技术:apt注解,AspectJ 编译器

动态/半动态AOP

img

  • 原理:运行时对象生成,修改字节码,然后利用 classloader等方式加载生成的 class;但是也可以在编译时修改 class 或者 dex
  • 代表技术:ASM, Javassist, Cglib/DexMaker、Java 动态代理(只支持接口代理)、ByteBuddy(依赖Java虚拟机)

虚拟机层

  • 原理:针对 Dalvik/ART 虚拟机进行劫持修改:
    • Dalvik 修改 Method 结构体重的 insns 字段;
    • Art 修改 ArtMethod 中的 entry_point_from_quick_compiled_code 字段;
  • 代表技术:Xposed, Dexposed, Frida java层, Epic, Sandhook

Native 层

静态 AOP

动态 AOP

  1. inlinehook

    • 原理:Inline Hook:直接修改目标函数在内存中的机器码,使其在执行到某个点时跳转到我们自己的代码(Hook函数)。在此处我们可以执行额外的操作(例如修改参数、记录日志等),然后再跳回原始函数的剩余部分继续执行。
    • 代表技术:ShadowHook,dobby
  2. PLT Hook

    • 原理:PLT/GOT Hook; Trap Hook基于信号捕获,具体原理如下

      1. 当程序使用动态链接库时,编译器会在程序中生成一个PLT(Procedure Linkage Table)和GOT(Global Offset Table)。PLT用于存储函数调用的跳转指令(为了延时绑定),而GOT用于存储动态链接库中函数的实际地址(避免每次都解析地址)。

      2. 当程序第一次调用动态链接库中的某个函数时,会先跳转到PLT中对应的条目,PLT条目会将控制流转移到GOT中的地址。第一次访问时,GOT中存储的是一个解析器(resolver)函数的地址,该解析器会查找并填充动态链接库中函数的实际地址,然后跳转到该地址执行函数。

      3. 当再次调用相同的函数时,PLT条目会直接跳转到GOT中存储的动态链接库函数地址,从而避免重复解析。

      4. 首先,找到目标函数在GOT中的地址条目。接着,将GOT中的地址条目修改为自定义函数(hook函数)的地址。为了在hook函数中调用原始函数,需要保存原始函数的地址,以便在执行完hook函数后跳转回原始函数。

    • 代表技术:Bhook 框架

iOS AOP 技术

Objective-C 层

静态 AOP

  • 原理:mach-o符号劫持
  • 代表技术:objc_msgSend符号表劫持,修改Mach-O文件中的符号表,将原始函数名称重定向到自定义函数,兼容性不好

动态 AOP

img

  • 原理:Method Swizzling

    在运行时,类(Class)维护了一个消息分发列表来解决消息的正确发送。每一个消息列表的入口是一个方法(Method),这个方法映射了一对键值对,其中键值是这个方法的名字Selector(SEL),值是指向这个方法实现的函数指针Implementation(IMP)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@interface Person : NSObject
- (void)greet;
@end

@implementation Person
- (void)greet {
NSLog(@"Hello, World!");
}
@end

Person *person = [[Person alloc] init];
[person greet];

1. 运行时系统根据 person 指针找到其对应的类(Person 类);
2. 在 Person 类的消息分发列表中,查找键值为 @selector(greet) 的条目;
3. 找到对应的 Implementation(即 greet 方法的实现);
4. 执行 Implementation,输出 "Hello, World!"。
  • 代表技术:RSSwizzle, Aspect, JSPatch

Native 层

静态AOP

  • 原理:Clang LLVM插桩
  • 代表技术:Dobby 跨平台的 inline hook 框架

动态AOP

  1. Inline Hook
  2. 符号表 Hook
    • 原理:Mach-O动态绑定机制,懒加载
    • 代表技术:fishhook

img

实际应用

移动端领域的 AOP 技术主要还是运用在以下的一些方面:

  • 隐私保护
  • 办公泄密
  • 端行为监控
  • 安全漏洞
  • ……

主要解决的问题是:

  1. 对APP内部不同身份的代码模块的身份和行为做识别和管控
  2. 对APP内部的调用行为实现详细的内视和追溯

这也是为什么通过简单的埋点无法替代的,因为二三方的代码是不可控的,没有源码也就无法修改,所以这就需要各种 AOP 技术的介入。当然考虑到实际的工程化,各种 AOP 技术都有着自身的局限性,我们主要考虑以下方面:

  • 稳定性:主要就是对各个版本,厂商 Android;iOS 版本的兼容性,这个是目前看来最为困难的点,排除掉了各种 inline hook 方案,因为都不够健壮。
  • 性能:一般静态 AOP 方案损耗比较少,因为字节码已经确定,动态方案如果需要运行时生成对象会损耗比较大,对于 OC 的方法交换没有生成字节码对象,只是函数指针的交换性能损失还可以接受,所以排除了大部门需要运行时动态生成对象的方案。
  • 灵活性:需要考虑到客户端 AOP 的特殊性,静态 AOP 方案发出后就无法更改更加危险,所以动态方案会更加灵活,这点上做了取舍。
  • 可用性:例如 xposed 需要 root,frida 也要注入进程这些都不能成为线上开发实际使用的
  • 覆盖度:几乎是每个方案,都无法全面覆盖所有切点,有些只能覆盖外部系统库(fishhook),有的只能覆盖特定的方法(java 代理模式),所以需要结合具体场景来做取舍。
  • 安全对抗:风控 SDK 可能检测 Hook,这个结合具体业务需要做适配
  • 排查问题难度:如果 aop 逻辑写的有问题,业务方排查起来会非常困难,这也是 aop 方案的固有问题。

总结下来对于 Android 比较稳定的方案是:

  • Java 层 ASM 字节码操作
  • Native 层 PLT Hook,例如 bhook 框架

对于 iOS 比较稳定的方法:

  • OC 层 Method Swizzle
  • Native 层 Fishook (但是没有 Android bhook库稳定与健壮,所以不推荐线上环境使用)

以上也是能够在真正的亿级生产环境中上线的当前方案,更具体的不方便展开讨论,也是笔者自身实践所得出来的浅薄经验,希望对读者有所帮助。

参考资料