安卓基础知识笔记

安卓四大组件

Activity(活动)、Service(服务)、BroadcastReceiver(广播接收者)、ContentProvider(内容提供者)

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
2
3
4
5
typedef struct {
const char* name; //Java方法名
const char* signature; //方法的参数和返回值,使用字符串记录,格式形如`()V, (I)I`,括号内表示函数参数,括号右侧表示函数返回值
void* fnPtr; // 指向JNI函数的函数指针
} JNINativeMethod;
java 端的 native 方法 c/c++ 代码实现的JNI_Onload()方法

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 默认使用 2704227043 端口,可以通过扫描这些端口来检测 Frida 是否在运行
    • 绕过:通过端口转发修改 Frida 使用的端口
  • 检测 /data/local/tmp 目录下的 frida-server 文件
    • 绕过:修改文件名
  • 双进程保护: 一些应用会启动一个守护进程来监控主进程,如果检测到 Frida attach,内核中 TracePid != 0,则杀死主进程,应用闪退
    • 绕过:用 spawn 模式启动
  • 检测 proc/self/maps 文件中是否存在 frida-agent-64.sofrida-agent-32.so 等文件
    • 绕过:
      • 定义一个函数用来阻止字符串的搜索匹配(strstrstrcmp),避免检测到敏感内容如 REJECT Frida
      • 重定向 maps 文件的内容,隐藏特定的库和路径信息
      • eBPF hook 系统调用并修改参数:在内核真正去查找文件之前,挂载在 sys_enter_openat 上的 eBPF 程序被触发,检测到系统调用的第一个参数(路径字符串)是 proc/self/maps, 使用 bpf_probe_write_user 将该内存地址的内容改为事先准备好的抹去了 frida 信息的 maps 文件
        1
        2
        char placeholder[] = "/data/data/.../maps";  
        bpf_probe_write_user((void*)addr, placeholder, sizeof(placeholder));
  • 检测 proc/stack/线程 id/status 文件中是否有 gmain/gdbus/gum-js-loop/pool-frida
    1. Frida 使用 Glib 库,gamain 是主循环事件 GMainLoop 的线程名;
    2. GDBusGlib 提供的用于 D-Bus 通信的库,gdbus 表示 D-Bus 相关线程;
    3. gum-js-loop 表示 Gum(Gum 是 Frida 运行时引擎)执行注入的 js 代码的线程
    4. pool-frida 表示 Frida 中的线程池
    • 绕过:类似上文绕过 maps 文件检测的方式,hook libc.so 中的 strstrstrcmp 函数,一旦检测到敏感字符串就返回未找到
  • 检测 inline hook,比较内存和本地字节,如果不一致则可认为被 inline hook 了
    • 绕过:找到校验逻辑并 hook 掉
  • SVC(Supervisor Call) 指令:软件中断指令,允许用户态程序发起一个系统调用,CPU 从用户态切换到内核态执行特权操作。
    • 绕过:进行内核追踪,找到 SVC 调用出并根据栈回溯往前找到检测逻辑

安卓基础知识笔记
http://example.com/2025/07/02/android_note/
作者
Eleven
发布于
2025年7月2日
许可协议