安卓基础知识笔记
安卓四大组件
Activity(活动)、Service(服务)、BroadcastReceiver(广播接收者)、ContentProvider(内容提供者)
Binder 工作原理
通过 Binder,Android 可以跨越不同进程和内存空间进行数据交换和调用远程方法
当进程 A 想要获取进程 B 中某个对象的方法时,它先通过 Binder 获取 B 的接口对象,这个接口对象其实就是 B 中服务的代理,A 通过它来发起远程方法调用;Binder 驱动会将 A 的请求数据打包成 Parcel 数据传递给 B,B 收到数据后解包并执行对应的方法,再将结果封装成 Parcel 传递给 A,A 收到结果后解包并继续执行后续逻辑
Parcel 是 Android 中用于在进程间传递数据的容器类,由于不同进程的内存空间是隔离的,不能直接传递内存中的对象,而 Parcel 就负责将对象的序列化(转化为字节流)后传输,接收端再将这些字节流反序列化为对象
跟传统的 socket 通信相比,Binder 是通过内存映射的方式,在进程间共享内存区域来传递数据,而不用进行多次数据拷贝
JNI 静态注册和动态注册
Java 支持调用 C/C++ 代码,JNI(Java Native Interface)的作用是粘合 Java 代码和 C/C++ 代码
静态注册 : 在 AndroidManifest.xml 中注册
遵循一定的命名规则,一般是 Java_packagename_classname_methodname(JNIEnv *env,jclass/jobject,...)
动态注册 : 通过代码在运行时注册
通过 registerReceiver() 方法实现,需要在适当时候调用 unregisterReceiver() 注销
使用结构体 JNINativeMethod 来记录 java 方法和 jni 函数的对应关系
1 | |
java 端的 native 方法
c/c++ 代码实现的 JNI_Onload()方法
JNI 环境指针
JNIEnv* env = self->GetJniEnv();
JNIEnv 是一个结构体,包含了大量的函数指针,这些函数指针指向 JNI 提供的各种功能函数,如查找类、调用方法、访问字段
等,通过 JNIEnv* env,我们可以调用这些函数来实现 Java 和 C/C++ 代码之间的交互
JNIEnv 是线程相关的,一个线程必须 attach 到 JVM 后才有自己的 JNIEnv*
Android 是怎么加载 dex 的
dex 是安卓设计的一种可执行字节码文件格式,开发者写的 Java/Kotlin 代码先会被编译成很多 .class 文件,再被转换并合并成一个或多个 .dex 文件,里面集中保存了应用运行所需的类定义、方法定义、字段信息、字符串常量、类型信息以及方法的字节码指令
dex 的加载链路大致如下:
- AMS 让应用进程起来,ActivityThread 绑定应用
- LoadedApk 为这个包创建应用 ClassLoader
- PathClassLoader/BaseDexClassLoader 把 apk/split/apex/jar 路径整理成 DexPathList
- DexPathList 为每个代码路径打开 DexFile
- DexFile 通过 JNI 进入 ART,OatFileManager / DexFileLoader 真正打开 dex、vdex、oat
- 后续 loadClass() 时,ART 的 ClassLinker 在这些已打开的 dex 里找类,并在第一次命中时 DefineClass
Frida 注入原理
Frida 注入流程通常是:
- 利用
ptrace附加到目标进程 - 强制修改寄存器(如 PC 指针),让目标进程去执行一段临时的
shellcode - 这段
shellcode会调用dlopen, 将 Frida 的动态库(frida-agent.so) 加载到目标进程的内存空间 - Agent 加载成功后,它就直接在目标进程内部进行
inline hook - 撤销 ptrace
Frida hook 原理
Frida 的 Interceptor 实现 hook 主要通过四级跳板完成
- 先通过 inline hook 的一级跳板到达
on_enter_trampoline的位置,此处为二级跳板 - 在
on_enter_trampoline处通过BR X16跳到第三级跳板enter_trunk,在这里把 CPU 寄存器全部保存到栈上或者专门的CpuContext结构体中 - 调用
Invocation进入四级跳板,执行on_enter代码 - 完成
on_enter处注册代码后根据函数是否被替换分为两种情况:- 如果函数被替换,则直接跳转到替换函数
- 如果函数没有被替换,则跳转到
on_leave_trampoline处,这里将执行原本的函数代码
- 执行完成后,根据是否设置
on_leave又分为两种情况:- 如果没有设置在,则直接返回
- 如果设置了,则跳回到二级跳板执行
leave_trunk,和enter_trunk类似,结束后返回
Inline hook 原理:
- 找到目标函数在内存中的起始地址
- 读取目标函数的开头几一段指令,因为即将要覆盖这部分指令,所以把它们备份到一块新的内存区域( trampoline 跳板区)
ps: 如果备份的指令中包含相对跳转(比如 CALL +0x100),直接复制到新位置相对偏移量就错了,需要进行指令修复将其转化为绝对跳转或者重新计算偏移量 - 将目标函数开头的那段指令覆盖为跳转指令,这条跳转指令指向 trampoline 跳板区
所以 Interceptor 其实是对 inline hook 的一种封装
Frida 检测及绕过
-
端口检测 : Frida 默认使用
27042和27043端口,可以通过扫描这些端口来检测 Frida 是否在运行- 绕过:通过端口转发修改 Frida 使用的端口
-
检测
/data/local/tmp目录下的frida-server文件- 绕过:修改文件名
-
双进程保护: 一些应用会启动一个守护进程来监控主进程,如果检测到 Frida attach,内核中
TracePid != 0,则杀死主进程,应用闪退- 绕过:用
spawn模式启动
- 绕过:用
-
检测
proc/self/maps文件中是否存在frida-agent-64.so、frida-agent-32.so等文件- 绕过:
- 定义一个函数用来阻止字符串的搜索匹配(
strstr、strcmp),避免检测到敏感内容如REJECTFrida等 - 重定向
maps文件的内容,隐藏特定的库和路径信息 - 用
eBPFhook 系统调用并修改参数:在内核真正去查找文件之前,挂载在sys_enter_openat上的eBPF程序被触发,检测到系统调用的第一个参数(路径字符串)是proc/self/maps, 使用bpf_probe_write_user将该内存地址的内容改为事先准备好的抹去了 frida 信息的maps文件1
2char placeholder[] = "/data/data/.../maps";
bpf_probe_write_user((void*)addr, placeholder, sizeof(placeholder));
- 定义一个函数用来阻止字符串的搜索匹配(
- 绕过:
-
检测
proc/stack/线程 id/status文件中是否有gmain/gdbus/gum-js-loop/pool-frida等- Frida 使用
Glib库,gamain是主循环事件GMainLoop的线程名; GDBus是Glib提供的用于D-Bus通信的库,gdbus表示D-Bus相关线程;gum-js-loop表示Gum(Gum 是 Frida 运行时引擎)执行注入的 js 代码的线程pool-frida表示 Frida 中的线程池
- 绕过:类似上文绕过 maps 文件检测的方式,hook
libc.so中的strstr和strcmp函数,一旦检测到敏感字符串就返回未找到
- Frida 使用
-
检测
inline hook,比较内存和本地字节,如果不一致则可认为被 inline hook 了- 绕过:找到校验逻辑并 hook 掉
-
SVC(Supervisor Call) 指令:软件中断指令,允许用户态程序发起一个系统调用,CPU 从用户态切换到内核态执行特权操作。
- 绕过:进行内核追踪,找到 SVC 调用出并根据栈回溯往前找到检测逻辑