《APP安全实战指南》阅读学习

Android是一个基于Linux内核的自由及开放源代码的移动端操作系统。

1.1 签名

Android11及以前的版本中存在以下4种应用签名方案。

  • v1签名:最基本的签名方案,基于JAR的签名实现。
  • v2签名:提高验证速度并增强完整性保证(在Android7.0版本中引入)。
  • v3签名:支持密钥轮替(在Android9.0版本中引入)。
  • v4签名:根据APK的所有字节计算得出Merkle哈希树,并通过v2或v3签名进行补充。

v4签名是Google为解决APK增量安装问题而推出的功能,目前只能通过ADB的方式安装

验证时,Android系统会优先寻找并校验最高版本的签名。如果无法找到更高版本的签名,系统会逐级向下寻找,直至找到兼容的签名方案。

应用签名工具

  • jarsigner是JDK提供的工具,用于对JAR文件进行签名
  • apksigner是Google官方提供的用于Android应用APK签名和验证的工具。
  • 无论是Android应用的APK包还是JAR包,本质都是ZIP格式的压缩包,因此它们的签名流程相似。

jarsigner与apksigner的区别如下:

  • jarsigner只能用于应用的v1签名,且只能使用keystore文件进行签名。
  • apksigner可以用于v1、v2、v3和v4签名,签名时既可以使用keystore文件进行签名,也可以使用PEM证书和私钥进行签名。

完成签名后,应用中会新增META-INF文件夹,,该文件夹中包含3个文件

文件 描述
MANIFEST.MF 记录应用中每一个文件的Hash摘要(除了META-INF文件夹)
*.SF 记录MANIFEST.MF文件的摘要和MANIFEST.MF中每个数据块的Hash摘要
*.RSA 记录*.SF文件的签名和包含公钥的开发者证书

4.签名流程
1)遍历应用中的文件并计算文件对应的SHA-1摘要,对文件摘要进行BASE64编码,然后将其写入签名文件,即MANIFEST.MF文件。

2)计算整个MANIFEST.MF文件的SHA-1摘要,进行BASE64编码后写入签名文件,即.SF文件;再次计算MANIFEST.MF文件中每一条摘要内容的SHA-1摘要,将摘要内容进行BASE64编码后写入签名文件,即.SF文件。
assets/APP安全实战指南/file-20250328092101800.png
3)计算整个*.SF文件的数字签名,将数字签名和开发者的X.509数字证书写入*.RSA文件

1.2 应用安装流程

根据Android应用安装流程的解析,可将其精简为以下五个核心步骤,并结合相关技术细节进行说明:

一、APK文件复制与DEX优化

  1. 文件复制:通过DefaultContainerService将APK文件从临时目录(如/data/app/vmdl18300388.tmp/base.apk)复制到目标目录/data/app/package/,动态库(.so文件)存放至/data/app/package/lib/
  2. DEX处理
    • Android 6.0+:DEX文件优化为OAT格式后存入/data/app/package/oat/目录,优化工具为dex2oat
    • 旧版本系统:DEX优化后存放于/data/dalvik-cache/,通过dexopt工具生成.odex文件。

二、资源解析与签名校验

  1. 签名验证:检查META-INF中的CERT.RSACERT.SF文件,验证APK完整性和证书合法性。
  2. 清单解析:通过PackageParser解析AndroidManifest.xml,提取包名、权限、组件等信息,并写入/data/system/packages.xml

三、数据目录初始化

/data/data/package/下创建应用专属目录,包括:

  • 数据库databases/
  • 缓存cache/
  • 配置文件shared_prefs/
  • 原生库lib/(部分动态库可能单独存放)

四、组件注册与系统集成

  1. 组件注册:将AndroidManifest.xml中声明的ActivityServiceBroadcastReceiverContentProvider注册到PackageManagerService(PMS),形成全局组件索引。
  2. 权限绑定:将应用申请的权限与系统权限策略匹配,记录至PMS的权限管理模块。

五、桌面图标添加与广播通知

  1. Launcher更新:通过InstallAppProgress触发Launcher更新桌面图标,具体由SessionCommitReceiver广播完成。
  2. 安装完成广播:系统发送ACTION_PACKAGE_ADDED广播,通知其他应用(如应用商店)安装结果,用户可通过BroadcastReceiver监听此事件。

assets/APP安全实战指南/file-20250328092253731.png

1.3 移动应用的权限

权限类型 权限含义
普通权限 普通权限只需要在AndroidManifest.xml中声明就可以使用。这类权限下的操作对用户隐私及其他应用带来的风险非常小。
Android在处理这类权限时不会提示用户,用户也没有办法撤销这类权限的授权
签名权限 Android会在应用安装时根据应用程序的签名信息授予相应的权限,两个应用如果使用同一证书进行了签名,就可以进行安全的数据共享
危险权限 对于涉及用户隐私或者影响其他程序操作的权限,Android会以弹窗的方式向用户进行询问,应用程序必须要经过用户的授权,才可以执行相应的动作
版本 权限类型 管理方式 用户控制能力
≤ Android 5 所有权限 安装时自动授予 无(全接受或放弃安装)
≥ Android 6 普通权限 自动授予 不可撤销
危险权限 运行时动态申请 可随时撤销或单独授权

Android 6.0及之后版本

1. 权限分类与动态申请

  • 普通权限(如网络访问、振动控制):仍自动授予,无需动态申请。
  • 危险权限(如相机、位置、存储):需在AndroidManifest.xml中声明,并在运行时动态申请用户授权。
  • 特殊权限(如悬浮窗、修改系统设置):需跳转到系统设置页面手动开启。

2. 相机权限示例

  • 静态声明:在AndroidManifest.xml中添加:
    <uses-permission android:name="android.permission.CAMERA" />
    
  • 动态申请流程
    1. 检查权限状态:使用ContextCompat.checkSelfPermission()判断是否已授权。
    2. 请求权限:若未授权,调用ActivityCompat.requestPermissions()弹出系统对话框。
    3. 处理结果:在onRequestPermissionsResult()中根据用户选择执行后续操作。

3. 用户控制增强

  • 用户可在系统设置中随时撤销已授权的危险权限,应用需处理权限缺失导致的异常。
  • 若用户选择“拒绝且不再询问”,后续请求将直接失败,需引导用户手动开启。

assets/APP安全实战指南/file-20250328092800942.png

1.4 Android应用的运行

  • Android是基于Linux内核开发的操作系统,因此也继承了Linux的安全机制。
  • Android系统进一步完善了Linux基于用户的识别隔离应用资源的保护机制。
  • Android系统中每个应用程序安装时都会被分配唯一的用户编号(UID),只要应用程序不被卸载,UID就不会变动
  • Android会依据应用的UID为每个应用设置一个独立的内核级应用沙盒,所有应用都在沙盒中独立运行。
  • 默认情况下应用之间不能交互,只能访问和使用自己沙盒内的资源。

assets/APP安全实战指南/file-20250328093252719.png

Android 系统版本安全机制对比表,清晰展示各版本的核心安全改进:

Android 版本 安全机制 技术实现 主要作用
Android 5.0 引入 SELinux 强制访问控制(MAC) - 基于 SELinux 的策略规则
- 结合 UID 的自主访问控制(DAC)
实现系统与应用、应用与应用间的隔离,防止越权访问。
Android 6.0 动态权限管理(危险权限需运行时申请)
强化 SELinux 沙盒
- 运行时权限弹窗(requestPermissions
- 限制应用主目录权限(0700
用户可控制敏感权限;防止应用间非法访问私有数据。
Android 8.0 引入 Seccomp-BPF 系统调用过滤 通过 Seccomp 过滤器限制应用可调用的系统 API 减少攻击面,阻止恶意应用滥用高危系统调用(如 execve)。
Android 9.0 强化 SELinux 沙盒隔离
限制安全默认设置修改
- 每个应用独立 SELinux 域
- 禁止全局可读/写目录(如 /data/data
防止应用数据泄露,避免恶意应用篡改系统安全配置。
Android 10.0 分区存储(Scoped Storage) - 应用默认仅能访问专属目录和特定媒体文件
- 需 READ_EXTERNAL_STORAGE 访问其他文件
保护用户隐私,限制应用随意扫描外部存储。

为了保证应用沙盒的安全,除了不断增强的系统层面的防护措施外,Android还根据应用本身的可信度对其进行分类,共分为4类,即不可信应用、特权应用、平台应用和系统应用

以下是 Android 应用类型与权限控制对比表,清晰展示不同应用类型的权限差异及系统限制:

应用类型 安装位置/签名要求 运行权限 资源访问限制 典型示例
不可信应用 用户安装(/data/app/)或部分预装应用 普通用户权限(UID隔离) 严格受限:
- 需动态申请危险权限(如相机、存储)
- 受SELinux和Seccomp策略限制
第三方应用(如微信、抖音)
特权应用 系统预装(/system/priv-app/或OEM指定目录) 非系统级权限(非system 部分放宽:
- 自动获取signature/privileged权限
- 仍受沙盒隔离限制
系统核心应用(如设置、电话)
平台应用 需平台签名(AOSP或厂商密钥签名) 非系统级权限(非system 受限但可访问部分框架API:
- 允许调用@hide接口(需反射)
- 受签名校验约束
厂商定制应用(如相机、语音助手)
系统应用 需平台签名且以system用户运行(如/system/app/ 系统级权限(system UID) 几乎无限制:
- 可访问Android框架内部资源
- 绕过部分SELinux策略
系统服务(如PhoneService

应用程序启动时会根据应用的类型分配不同的访问权限
拥有系统权限的应用在访问系统资源时有很多特权,几乎不受沙盒策略的限制;
非系统应用访问系统资源则会受到严格的限制。

assets/APP安全实战指南/file-20250328093606503.png

应用间数据共享机制

虽然严格地应用隔离策略能大幅提高应用的安全性,但应用之间不可避免地会交互通信,严格的应用隔离策略会带来诸多不便。
为保证运行在不同沙盒间的应用依然可以正常交互,Android提供了两种应用间数据共享机制:sharedUserld共享机制基于Binder的IPC通信机制

(1)sharedUserld共享机制

使用相同签名和相同sharedUserld属性的应用会被分配相同的UID拥有相同UID的应用可以彼此访问其数据目录下的任意数据,还可以将应用配置到同一个进程中运行。
开发者可以在应用的Manifest文件中添加sharedUserld属性进行设置。
注意
Android10.0及以后的系统不再支持sharedUserld属性的设置。

(2)基于Binder的IPC通信机制

Binder实现了基于客户端/服务器(C/S)架构的进程间通信(IPC),它通过Android接口定义语言(AIDL)来定义通信接口及交换数据的格式,确保进程间传输的数据安全有效,避免数据溢出或越界。
其本质就是通过共享内存方式实现进程通信

assets/APP安全实战指南/file-20250328094152622.png

Binder在内核空间创建两个内存缓冲区,在两个内存缓冲区之间建立映射关系,同时在内核中的共享数据缓存区接收进程的用户空间地址之间建立映射关系
发送方进程调用copy_from_user函数将共享数据复制到到内核中的内存缓冲区。由于内存缓冲区和接收进程的用户空间存在内存映射接收进程便可直接读取共享数据

2 应用分析基础

2.1常用工具

2.1.2 Root工具Magisk

Magisk(俗称“面具”)是一款开源的Android系统定制工具,自2016年发布以来,已成为获取和管理Root权限的主流解决方案。
其核心特点在于通过Systemless技术实现无痕系统修改,既保留了系统完整性,又能深度定制设备功能。


一、核心功能与技术特点

  1. Root权限管理(MagiskSU)

    • MagiskSU提供安全、可控的Root访问机制,允许用户按需授权应用权限,并通过生物识别验证(如指纹)增强安全性。
    • 支持动态权限撤销,用户可随时禁用Root,避免恶意应用滥用权限。
  2. 模块化系统修改(Magisk Modules)

    • 用户可通过模块扩展功能,例如更换字体、优化性能或隐藏Root状态。模块通过挂载独立分区实现,不修改系统文件,确保OTA更新不受影响
    • 热门模块包括:
      • Shamiko隐藏Zygisk和Magisk痕迹,绕过银行类应用检测。
      • Universal SafetyNet Fix:修复SafetyNet验证失败问题。
  3. 隐蔽性技术(Zygisk与Magisk Hide)

    • Zygisk在Android核心进程Zygote中注入代码,动态隐藏Root痕迹,支持黑名单模式(仅对指定应用生效)。
    • Magisk Hide(旧版本功能):通过随机化Magisk应用包名和签名,防止检测工具识别。
  4. 启动镜像处理(MagiskBoot)

    • 支持解包/打包boot.img,兼容主流设备的分区结构,并优化启动流程以提升兼容性。

二、技术优势与演进

  1. Systemless设计

    • 修改内容存储在独立分区(Magisk镜像),避免篡改系统文件,确保通过Google SafetyNet验证,支持无缝OTA更新。
    • 例如,用户可在安装Magisk后正常接收系统更新,仅需重新修补新boot镜像即可保持Root。
  2. 兼容性与扩展性

    • 支持Android 5.0及以上版本,覆盖99%的Android设备。
    • 开发者可通过Zygisk开发进程级模块,实现更复杂的系统交互。

三、典型使用场景

  1. 开发者调试
    • 通过Root权限访问系统API,调试应用底层行为,或测试自定义ROM功能。
  2. 隐私保护
    • 使用模块屏蔽应用跟踪权限,或禁用预装软件(如厂商广告服务)。
  3. 游戏与娱乐
    • 部分用户通过修改游戏内存或帧率解锁功能(需搭配LSPosed等框架),但存在封号风险。

2.1.3 Hook工具Xposed

一、Xposed框架概述

Xposed框架是一个基于Android系统的开源Hook工具,允许开发者在不修改APK源码的情况下,通过模块化插件动态修改系统或应用的行为
自2013年发布以来,它成为Android逆向工程、功能定制及安全研究的重要工具。


二、核心原理与技术实现

  1. Zygote进程注入
    Xposed通过替换Android系统的app_process可执行文件,在Zygote进程(所有应用进程的父进程)启动时加载核心库XposedBridge.jar,从而实现对应用进程的全局Hook。

  2. Java方法Hook机制

    • JNI方法注册:Xposed将目标Java方法转换为Native方法,通过修改ArtMethod结构体的accessFlags和函数指针,使其指向代理的Native函数
    • 动态拦截:当应用调用被Hook的方法时,实际执行Xposed模块定义的逻辑(如修改参数、返回值或绕过验证)。
  3. 模块化架构

    • 开发者编写Xposed模块(APK),通过声明xposed_init文件指定Hook入口类,并在handleLoadPackage中定义具体Hook逻辑。
    • 模块通过反射API(如XposedHelpers.findAndHookMethod)动态修改目标方法。

三、典型应用场景

  1. 系统与功能定制

    • 修改系统UI(状态栏、锁屏)、禁用广告、调整权限管理等。
    • 模块示例:绿色守护(后台管理)、XPrivacy(隐私保护)。
  2. 逆向分析与安全研究

    • 动态调试应用逻辑、分析恶意软件行为、绕过安全检测机制。
  3. 外挂与自动化

    • 开发游戏辅助(如自动点击)、社交软件插件(如微信抢红包),但存在封号风险。

四、安装与使用限制

  1. 安装条件

    • 需Root权限或刷入定制ROM(如替换系统镜像)。
    • 高版本Android(≥8.0)需依赖Magisk+EdXposed/LSPosed等衍生框架
  2. 兼容性限制

    • 原生Xposed不支持Android 12及以上版本,且无法Hook Native层代码
    • 部分厂商系统(如MIUI)因深度定制可能导致框架失效

2.1.4 Hook工具Frida

一、Frida 的核心原理

  1. 动态二进制插桩技术(DBI)
    Frida 使用 动态二进制插桩(Dynamic Binary Instrumentation, DBI)在程序运行时实时注入代码,无需永久修改可执行文件。这种技术允许开发者在目标进程的内存中动态修改代码逻辑,如拦截函数调用、修改参数或返回值等。

二、核心功能与技术特点

  1. Java 层 Hook

    • 通过 Java.use 获取类引用,覆盖方法实现(implementation)修改逻辑,支持静态方法、构造函数和重载方法。
    • 示例:Hook 加密函数并打印参数:
      const Utils = Java.use('com.example.Utils');
      Utils.md5.implementation = function(input) {
        console.log("原始输入:", input);
        return this.md5(input);
      };
      
  2. Native 层 Hook

    • 使用 Module.findExportByName 获取函数地址,Interceptor.attach 拦截执行流程,支持读取/修改内存和寄存器。
    • 示例:Hook 未导出的 C 函数:
      const funcPtr = Module.findExportByName('libnative.so', 'func_name');
      Interceptor.attach(funcPtr, {
        onEnter(args) { console.log("参数:", args[0].readCString()); },
        onLeave(retval) { console.log("返回值:", retval.toInt32()); }
      });
      
  3. 动态调试与数据监控

    • 实时读取内存(Memory.readByteArray)、修改全局变量,或通过 Java.choose 遍历对象实例。
    • 支持生成堆栈跟踪,辅助逆向分析调用链。

三、典型应用场景

  1. 逆向工程与安全研究

    • 动态分析加密算法(如 AES、MD5)、绕过反调试机制。
    • 监控网络请求(如 OkHttp 库)或文件读写行为。
  2. 自动化测试与漏洞挖掘

    • 模拟用户操作(如点击事件)、触发边界条件测试。
    • 检测内存泄漏或敏感数据暴露风险。
  3. 隐私保护与功能定制

    • 禁用广告 SDK、屏蔽应用跟踪权限。
    • 修改游戏逻辑(需注意合规性)。

四、安全风险与对抗

  1. 检测与反制手段

    • 设备端检测:检查开放端口(如 27042)、进程列表中的 frida-server 或内存中的特征字符串(如 "LIBFRIDA")。
    • 应用层检测:检查调试标志或异常调用栈。
  2. 隐蔽性优化

    • 使用 frida-gadget 嵌入目标应用,避免独立进程。
    • 结合 Magisk 模块(如 Shamiko)隐藏 Root 和 Hook 痕迹。

五、与其他工具的对比

特性 Frida Xposed Magisk
运行条件 支持免 Root(需重打包) 需 Root 和刷入框架 需 Root,Systemless 设计
Hook 层级 Java 和 Native 层 仅 Java 层 系统级模块(非 Hook 框架)
持久化 需每次注入脚本 模块持久化生效 模块持久化
跨平台 支持 Android、iOS、Windows 等 仅 Android 仅 Android
典型用途 动态分析、逆向调试 系统功能定制 Root 权限管理、模块扩展

2.2 定位APP的导出函数

Android应用程序的四大组件都需要在Manifest配置文件中声明,可通过配置文件中的组件信息快速定位其所在的代码位置
首先通过配置文件声明的信息定位应用程序的执行入口,然后便可按照程序的执行流程进行分析。

Android应用程序除了使用Java/Kotin开发以外,还可以使用C/C++开发。使用Java/Kotin开发的部分通常被称为Java层,使用C/C++开发的部分通常被称为Native层。
Java层和Native层是通过JNl进行交互的,也就是说JNI接口是两部分的连接点,我们在分析应用时只要定位到JNI接口就能快速定位相关代码

Java层和Native层都需要先注册JNl函数,然后才能正常交互

Java层注册

Java层注册JNI函数的格式如下:

package com.test.demo;
public class JniWrapper {
    // 加载动态库(无需扩展名,对应 libnative-lib.so)
    static {
        System.loadLibrary("native-lib");
    }
    // 声明 Native 方法
    public static native String stringFromJNI();
}

Native层注册

Native层注册分为静态注册动态注册两种:

一、静态注册

1. 实现原理
  • 规则匹配:根据 Java_包名_类名_方法名 的固定格式生成 Native 函数名,系统自动关联 Java 层与 Native 层方法。
  • 声明示例
    // 对应 Java 类:com.test.demo.JniWrapper 的 stringFromJNI 方法
    JNIEXPORT jstring JNICALL 
    Java_com_test_demo_JniWrapper_stringFromJNI(JNIEnv* env, jclass clazz) {
        std::string hello = "Hello from JNI";
        return env->NewStringUTF(hello.c_str());  // 修正拼写错误
    }
    
2. 逆向分析
  • IDA 快速定位
    1. 反编译 SO 文件后,查看 Exports 窗口所有静态注册的 JNI 函数均以 Java_ 开头
    2. 根据函数名直接定位到对应的 Java 类及方法(如 com/test/demo/JniWrapper.stringFromJNI)。

二、动态注册

1. 实现原理
  • 核心机制JNI_OnLoad 函数中,通过 JNINativeMethod 结构体手动绑定 Java 方法与 Native 函数
  • 代码示例
    // 定义 JNI 函数
    jstring helloJni(JNIEnv* env, jclass clazz) {
        return env->NewStringUTF("Hello from Dynamic JNI!");
    }
    
    // JNINativeMethod 结构体(参数顺序:Java方法名、方法签名、Native函数指针)
    static JNINativeMethod method_table[] = {
        {"stringFromJNI2", "()Ljava/lang/String;", (void*)helloJni}
    };
    
    // JNI_OnLoad 入口
    JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
        JNIEnv* env = nullptr;
        if (vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) {  // 修正常量名
            return JNI_ERR;
        }
    
        jclass clazz = env->FindClass("com/test/demo/JniWrapper");
        if (clazz == nullptr) return JNI_ERR;
    
        // 注册 Native 方法
        if (env->RegisterNatives(clazz, method_table, sizeof(method_table)/sizeof(method_table[0])) < 0) {
            return JNI_ERR;
        }
    
        return JNI_VERSION_1_4;
    }
    
2. 逆向分析
  • 步骤 1:定位 JNI_OnLoad 函数

    • 在 IDA 中搜索 JNI_OnLoad 函数(通常是动态注册入口)。
    • 分析其内部逻辑,查找 RegisterNatives 调用。
  • 步骤 2:分析 JNINativeMethod 结构体

    • 结构体定义
      typedef struct {
          const char* name;     // Java 方法名(如 "stringFromJNI2")
          const char* sign;     // 方法签名(如 "()Ljava/lang/String;")
          void*       jni;      // Native 函数指针
      } JNINativeMethod;
      
    • 逆向操作
      1. 在 SO 文件中搜索字符串(如 "stringFromJNI2""()Ljava/lang/String;")。assets/APP安全实战指南/file-20250328104937677.png
      2. 通过交叉引用(Xref)定位到 JNINativeMethod 数组。
      3. 解析结构体内容,获取 Native 函数地址(如 helloJni)。assets/APP安全实战指南/file-20250328104951426.png
  • 步骤 3:追踪 Native 函数

    • 通过 jni 字段的函数指针跳转到实际 Native 函数(如 helloJni)。
    • 分析函数逻辑(如字符串处理、加密算法)。

三、防御与对抗

  1. 对抗静态分析

    • 字符串混淆:加密 JNINativeMethod 中的字符串(如方法名、签名)。
    • 延迟注册:在运行时动态解密并注册 JNI 方法。
  2. 对抗动态调试

    • 反 Hook 检测:检查 RegisterNatives 是否被 Frida/Xposed 篡改。
    • 代码混淆:对 JNI_OnLoad 函数进行控制流混淆(如 OLLVM)。

4 常见的攻击方式

4.1 重签名攻击

为了保证篡改后的APK文件可以正常使用,攻击者会使用自己的签名文件对重打包后的应用进行再次签名,从而达到绕过系统签名校验的目的。

攻击者会尝试各种方法绕过客户端的签名校验,但其原理基本相同,即篡改客户端签名校验的结果。获取签名信息的方法如下:

Signature[] signatures = packageInfo.signatures

4.1.1 绕过客户端签名校验的常用技术

1. 篡改校验逻辑(静态对抗)

  • 定位关键代码
    搜索getPackageInfo()调用点,常见于以下代码段:

    public boolean checkSignature() {
      PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), GET_SIGNATURES);
      return pi.signatures[0].hashCode() == ORIGINAL_SIGN_HASH; // 关键判断
    }
    
  • 修改smali条件判断
    if-nez(非零跳转)改为if-eqz(为零跳转),强制通过校验:

    if-eqz v0, :cond_0  # 原代码为 if-nez v0, :cond_0
    

2. Hook系统API(动态对抗)

  • 使用Frida拦截返回值
    注入脚本篡改getPackageInfo()返回的签名哈希值:

    Java.perform(() => {
      const PackageManager = Java.use("android.content.pm.PackageManager");
      PackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(pkg, flags) {
        const result = this.getPackageInfo(pkg, flags);
        result.signatures[0].hashCode = function() { return 0x12345678; }; // 替换为原始哈希
        return result;
      };
    });
    
  • Xposed模块示例
    通过Xposed Hook PackageManagerServicegetPackageInfo方法:

    XposedHelpers.findAndHookMethod("android.app.ApplicationPackageManager", 
      lpparam.classLoader, "getPackageInfo", String.class, int.class,
      new XC_MethodHook() {
        protected void afterHookedMethod(MethodHookParam param) {
          PackageInfo info = (PackageInfo) param.getResult();
          info.signatures[0] = originalSignature; // 替换签名
          param.setResult(info);
        }
      });
    

3. 动态代理PMS服务(高级对抗)

assets/APP安全实战指南/file-20250328222543589.png

assets/APP安全实战指南/file-20250328221310097.png

JAVA反射机制

3分钟轻松掌握Java反射原理_哔哩哔哩_bilibili
.java——>.class【只有JVM才能识别的机器码】
JVM读取 .class 字节码 并加载内存后,生成Class类对象【展示.class文件的结构信息】
而在运行阶段就可以通过Class类对象获取相关类的信息如成员变量、构造器、成员方法【通过Class cls = Class.forName("Person");
assets/APP安全实战指南/file-20250328155449661.png

动态代理

动态代理Java反射机制的高级应用,允许在运行时动态生成代理类,拦截并增强目标对象的方法调用。其核心组件包括:

  • InvocationHandler接口:定义代理逻辑(如篡改返回值),在方法调用时触发invoke()方法。
  • Proxy:通过newProxyInstance()生成代理对象,将方法调用转发给InvocationHandler处理。
PMS的签名获取流程

Android系统通过PackageManagerService (PMS) 管理应用签名。当应用调用getPackageInfo()时:

  1. Binder通信:客户端通过ApplicationPackageManager向PMS发起跨进程请求,获取签名信息。
  2. 服务端处理:PMS从mPackages缓存中提取目标应用的PackageParser.Package对象,返回其signatures字段。

关键点:签名数据存储在PMS服务端,但客户端通过代理拦截可篡改返回结果。

动态代理Hook PMS的步骤

通过反射和动态代理劫持PMS的返回值,具体流程如下:

  1. 定位关键对象

    • 反射获取ActivityThreadsPackageManager(PMS的客户端代理对象)。
    • 反射修改ApplicationPackageManagermPM字段,替换为自定义代理对象。
  2. 创建代理对象

    • 使用Proxy.newProxyInstance()生成IPackageManager的代理实例,拦截getPackageInfo()方法。
    • invoke()方法中,将返回的PackageInfo.signatures替换为目标官方签名。
  3. 同步包名与签名

    • 创建与目标应用包名相同的新应用,确保通过PMS查询时返回正确的包信息。
    • 在代理逻辑中,仅对特定包名的请求返回篡改后的签名,避免影响其他应用。

assets/APP安全实战指南/file-20250328153202432.png

  • 实现原理
    创建与目标应用同包名的代理应用,通过反射劫持PackageManager服务:
    // 获取原始PackageManager对象
    PackageManager pm = getPackageManager();
    // 创建动态代理
    Class<?> IPackageManager = Class.forName("android.content.pm.IPackageManager");
    Object proxy = Proxy.newProxyInstance(
      IPackageManager.getClassLoader(),
      new Class<?>[] { IPackageManager },
      new InvocationHandler() {
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          if ("getPackageInfo".equals(method.getName())) {
            PackageInfo info = (PackageInfo) method.invoke(pm, args);
            info.signatures = originalSignatures; // 替换为合法签名
            return info;
          }
          return method.invoke(pm, args);
        }
      });
    // 替换全局PMS实例
    Singleton<IPackageManager> singleton = (Singleton<IPackageManager>) 
      ReflectionUtils.getField(Class.forName("android.app.ActivityThread"), null, "sPackageManager");
    singleton.set(proxy);
    

4.1.2 防御方案设计

1. 多层级签名校验

  • Java层 + Native层联合校验
    在JNI中实现二次校验,避免纯Java层被Hook:

  • 服务端协同验证
    签名哈希加密后上传至服务器验证,防止本地篡改:

    String signatureHash = getSignatureHash(this);
    String encrypted = AESUtils.encrypt(signatureHash, SERVER_KEY);
    boolean isValid = OkHttpUtils.post("https://api.example.com/verify", encrypted);
    if (!isValid) System.exit(0);
    

2. 代码混淆与加固

  • ProGuard规则示例
    保护签名校验相关类和方法不被逆向:

    -keep class com.example.security.SignatureCheck {
      public *;
    }
    -keepclassmembers class com.example.security.NativeUtils {
      native <methods>;
    }
    
  • SO库加固技术
    使用OLLVM实现控制流平坦化指令替换

    clang -mllvm -fla -mllvm -sub target.c -o libsecurity.so
    

4.1.3 攻击检测与对抗

1. 重签名特征检测

  • 签名证书比对
    检查当前签名证书与预埋证书是否一致:

    public static boolean isOriginalSign(Context ctx) {
      Signature[] signs = getSignatures(ctx); // 获取当前签名
      return signs[0].toCharsString().equals(ORIGINAL_SIGN);
    }
    
  • APK完整性校验
    验证classes.dex和资源文件的哈希值:

    String dexHash = HashUtils.sha1(loadDexBytes());
    if (!dexHash.equals("a1b2c3...")) throw new SecurityException();
    

2. 动态行为监控

  • 检测调试器附加
    通过检查Debug.isDebuggerConnected()防止动态分析:

    if (Debug.isDebuggerConnected()) {
      android.os.Process.killProcess(android.os.Process.myPid());
    }
    
  • Hook框架检测
    扫描进程内存中的Frida/Xposed特征:

    public static boolean isHookFrameworkActive() {
      try {
        Class.forName("de.robv.android.xposed.XposedBridge");
        return true; // 检测到Xposed
      } catch (Exception e) {}
      // 检测Frida的默认端口
      return NetUtils.isPortOpen(27042); 
    }
    

通过以上技术组合,开发者可构建从基础校验到高级对抗的防护体系,有效抵御重签名攻击。防御核心在于多维度校验(本地+服务端)、代码混淆(Java+Native)与动态监测(反调试+Hook检测)的综合运用。

4.2.1 Android动态注入

[[Android软件安全权威指南#第10章 Hook与注入]]]

5 客户端安全加固

posted @ 2025-04-25 17:08  方北七  阅读(144)  评论(0)    收藏  举报