浅谈 Android Binder 监控方案

在 Android 应用开发中,Binder 可以说是使用最为普遍的 IPC 机制了。我们考虑监控 Binder 这一 IPC 机制,一般是出于以下两个目的:

  • 卡顿优化:IPC 流程完整链路较长,且依赖于其他进程,耗时不可控,而 Binder 调用本身通常又是以 RPC 形式对外提供能力的,使得我们在使用时更容易忽略其 IPC 的本质。总的来说,主线程阻塞在同步 Binder 调用上是导致应用卡顿的一大典型原因。
  • 崩溃优化:Binder 调用过程中可能出现各类异常情况,典型的是 Binder 缓冲区耗尽的情况( 经典 TransactionTooLargeException )。Binder 的缓冲区大小大约为 1M( 注意到这里的缓冲区是最早 mmap 出来进程内全局共享的,而非单次 Binder 调用的限制 ),而异步调用( oneway )的情况更是被限制只能使用缓冲区的一半大小( 约 512K )。

考虑只监控某些个系统服务的情况,得益于 ServiceManager 和 AIDL 的设计,我们可以简单基于动态代理替换当前进程对应的 Proxy 对象来实现监控;而要实现进程内全局 Binder 监控的话,我们需要考虑怎么拦截到 Binder 调用通用的 transact 方法。

基于 Binder.ProxyTransactListener

注意到 Android 10 上系统引入了 Binder.ProxyTransactListener,在 Binder 调用前后( BinderProxy 的 transactNative 方法内部 )会触发回调。从提交记录来看,ProxyTransactListener 引入的目的之一就在于支持 SystemUI 监控主线程的 Binder 调用。

美中不足的地方在于,ProxyTransactListener 和相应的设置接口都是 hide 的,且 BinderProxy 的类属性 sTransactListener 在 hidden api 名单中。不过到目前为止,我们始终是有稳定的方案可以绕过 hidden api 的限制的,因此我们可以基于动态代理创建一个 ProxyTransactListener 实例设置给 BinderProxy,来实现对进程内 Java 的 Binder 调用的全局监控。

这里稍微提一下 hidden api 的绕过方案,在 Android 11 系统禁掉元反射之后,一个简单的方案是先创建一个 Native 线程再 Attach 拿到 JNIEnv,就可以在该线程内正常使用

VMRuntime.getRuntime().setHiddenApiExemptions(new String[]{"L"}); 

来实现全局 hidden api 加白。原理是系统对于基于 JNI 访问 Java API 的情况,在回溯 Java 堆栈找不到 caller 的情况,会信任该次调用不做 hidden api 的拦截,详细逻辑见 GetJniAccessContext。因此我们可以通过创建 Native 线程再 AttachCurrentThread 访问 JNI 接口的方式来构造没有 Java caller 的情况( 这也是 Native 线程 AttachCurrentThread 无法访问应用类的原因,没有 caller 就找不到可用的 ClassLoader )。比较有意思的地方是实际上官方早就意识到了这类 hidden api 后门的存在,但由于改动风险太大一类的原因一直没有合入限制逻辑,类似的讨论可以参考 Don’t trust unknown caller when accessing hidden API。

这一方案实现简单,兼容性好,主要的毛病在于:

只支持 Android 10 及以上版本,不过现状 Android 10 以下的设备占比已经不高了。
ProxyTransactListener 的接口设计上不带 data 参数( Binder 调用的传入数据 ),我们也就无法做传输数据大小的统计。此外,对于多进程应用来说,实践上可能会以统一的 AIDL 对象作为通信通道封装一层屏蔽了匿名 Binder 对象传递和 AIDL 模版代码的 IPC 框架,实际 IPC 调用的目标逻辑则以统一的调用约定封装在 data 参数中。这种情况下,只有拿到 data 参数( 将 IPC 调用统一放到线程池中执行的情况,堆栈没有意义 )我们才能实际确认 IPC 调用的真正目标逻辑。

JNI Hook BinderProxy.transactNative

实际上 Java 的 Binder 调用总是会走到 BinderProxy 的 JNI 方法 transactNative,我们可以基于 JNI Hook 来 hook transactNative,实现全版本的进程内 Java 的 Binder 调用的全局监控,也可以拿到 Binder 调用的完整参数和返回结果。

这里稍微提一下 JNI Hook,JNI Hook 基于 JNI 边界 hook Java JNI 方法对应的 Native 函数实现,具体实现上 hack 点少,稳定性较好,算得上是线上比较常用的一类通用 Native Hook 方案。大体上讲,JNI Hook 实现上分为两步,找到原 Native 函数和替换该 Native 函数。

  • Native 函数的替换比较简单,我们通过调用 JNI 的 RegisterNatives 接口就可以实现 Native 函数的覆盖。( 注意到如果原始 JNI 方法也是通过 RegisterNatives 注册的,我们需要保证 JNI Hook 的 RegisterNatives 执行在后 )
  • 找到原 Native 函数则稍微复杂些,而且我们总是需要依赖原 Native 函数已经先行注册才能找到该 Native 函数。
    • 一个可行的方案是手动实现一个 JNI 方法,用来计算实际 ArtMethod 对象( 即 Java 方法在 art 中的实际表示 )中存放 Native 函数的属性的偏移。得到这个偏移之后即可以基于被 Hook JNI 方法的 ArtMethod 对象拿到原 Native 函数。怎么拿到 ArtMethod 对象?实际上,在 Android 11 以前,jmethodID 就是 ArtMethod 指针,在 Android 11 之后,jmethodID 默认变成了间接引用,但我们仍然可以通过 Java Method 对象的 artMethod 属性拿到 ArtMethod 指针。详细介绍可参考一种通用超简单的 Android Java Native 方法 Hook,无需依赖 Hook 框架。
    • 另一个可行的方案是可以基于 art 的内部函数 GetNativeMethods 来直接查询得到原 Native 函数。GetNativeMethods 几个函数是 art 用于支持 NativeBridge 的,稳定性上也有所保证。NativeBridge 详细介绍可参考用于 Android ART 虚拟机 JNI 调用的 NativeBridge 介绍。

具体到 JNI Hook BinderProxy.transactNative,实际在跑到应用的第一行业务代码( Application 的 attachBaseContext )之前,就已经有 Java 的 Binder 调用发生,因此我们根本不需要手动触发 Binder 调用来保证 BinderProxy.transactNative 的 Native 函数注册。另外,注意到 BinderProxy 的 transactNative 也是 hidden api,这里也需要先行绕过 hidden api 的限制。

Hook BinderProxy.transactNative 的方案可以很好地满足监控进程内全局 Java Binder 调用的需要,但却监控不到 Native 的 Binder 调用。注意到这里的 Java/Native Binder 调用的差异在于 IPC 通信逻辑的实现位置,而非实际业务逻辑的实现位置。典型的如 MediaCodec 一类的音视频接口,实际的 Binder 调用封装都实现在 Native 层,我们使用 Java 调用这些接口,通过 BinderProxy.transactNative 也无法监控到实际的 Binder 调用。要实现包含 Native 的全局 Binder 调用监控,我们需要考虑 Hook 更下一层的 Native 的 transact 函数。

PLT Hook BpBinder::transact

和 Java 层的 Binder 接口设计类似,Native 层 Client 端发起的 Binder 调用,总是会走到 libbinder.so 中 BpBinder 的 transact 函数。注意到 BpBinder 的 transact 函数是一个导出的虚函数,而且使用上总是基于基类 IBinder 指针做动态绑定调用( 也就是说,其他 so 总是基于 BpBinder 的虚函数表来调用 BpBinder::transact,而不是直接依赖 BpBinder::transact 这一符号,而 BpBinder 的虚函数表在 libbinder.so 内部 ),因此我们直接 PLT Hook libbinder.so 对于 BpBinder::transact 的调用即可。

具体看下 BpBinder::transact 的函数声明:

    // NOLINTNEXTLINE(google-default-arguments)virtual status_t    transact(   uint32_t code,const Parcel& data,Parcel* reply,uint32_t flags = 0) final;

其中,status_t 实际上只是 int32_t 的别名,但 Parcel 则不是 NDK 暴露的接口,我们没有途径拿到绝对稳定的 Parcel 对象的布局,好在 transact 函数对于 Parcel 的使用是基于引用和指针的( 引用在汇编层面的实现和指针类似 ),我们不需要依赖 Parcel 对象的布局也可以实现一个 transact 的替代函数。

在成功拦截到 BpBinder::transact 的调用之后,我们还需要考虑怎么基于 transact 的调用参数和返回值来获取到我们需要的信息。

对于 Binder 对象( 即 transact 的隐含调用参数 this 指针 )本身,我们通常会关注它的 descriptor( 再结合 code 参数可以定位到实际 IPC 调用的目标逻辑 ),这里我们直接调用导出接口 BpBinder::getInterfaceDescriptor 即可。

    virtual const String16&    getInterfaceDescriptor() const;

比较麻烦的是 String16 也不是 NDK 暴露的接口,而且它用来转成 char16_t* 字符创的函数实现是内联的,

    inline  const char16_t*     string() const;

我们只能重新 hardcode 声明一个类似的 String16 类来做强转。好在从系统源码看,String16 的对象布局比较简单且稳定,只有一个 const char16_t* 类型的私有属性 mString,而且不存在虚函数。类似这样:

class String16 {
public:[[nodiscard]] inline const char16_t *string() const;
private:const char16_t* mString;
};inline const char16_t* String16::string() const {return mString;
}

拿到 String16 对应的 char16_t* 字符串之后,我们直接在回调 Java 时用 JNI 接口将其转为 jstring 即可。

另一个常用的信息是 data 的数据大小。我们可以直接调用导出接口 Parcel::dataSize 来获取。注意到 transact 函数 的 data 参数是 Parcel 的引用,我们直接声明一个空类来承接 data 参数,再对拿到的 data 取址,让编译器可以正常完成引用到指针的转换即可。类似这样:

class Parcel {};// size_t dataSize() const;
typedef size_t(*ParcelDataSize)(const Parcel *);// virtual status_t transact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags = 0) = 0;
status_t HijackedTransact(void *thiz, uint32_t code, const Parcel &data, Parcel *reply, uint32_t flags);ParcelDataSize g_parcel_data_size = nullptr;
auto data_size = g_parcel_data_size(&data);

此外,注意到对于 Java 的 Binder 调用而言,会在 BinderProxy.transactNative 的内部再调用到 BpBinder::transact,我们可以结合 JNI Hook 和 PLT Hook 两个方案,对 Java 的 Binder 调用,基于 JNI Hook 拿到完整的 Java 参数,方便我们在 Java 回调中直接基于 Java 参数做进一步处理。

One More Thing

拦截到 Binder 调用只是监控的第一步,更为重要的是在这个基础上如何做数据处理来发现和定位问题。

前面提到的两类经典问题:IPC 耗时卡顿和传输数据过大崩溃,可以通过前后打点统计 transact 耗时以及调用前获取传输数据大小的方式来挖掘。 定位问题上,堆栈和当次 Binder 调用的 descriptor 和 code 是比较有价值的信息。

如果你还没有掌握Framework,现在想要在最短的时间里吃透它,可以参考一下《Android Framework核心知识点》,里面内容包含了:Init、Zygote、SystemServer、Binder、Handler、AMS、PMS、Launcher……等知识点记录。

《Framework 核心知识点汇总手册》:https://qr18.cn/AQpN4J

Handler 机制实现原理部分:
1.宏观理论分析与Message源码分析
2.MessageQueue的源码分析
3.Looper的源码分析
4.handler的源码分析
5.总结

Binder 原理:
1.学习Binder前必须要了解的知识点
2.ServiceManager中的Binder机制
3.系统服务的注册过程
4.ServiceManager的启动过程
5.系统服务的获取过程
6.Java Binder的初始化
7.Java Binder中系统服务的注册过程

Zygote :

  1. Android系统的启动过程及Zygote的启动过程
  2. 应用进程的启动过程

AMS源码分析 :

  1. Activity生命周期管理
  2. onActivityResult执行过程
  3. AMS中Activity栈管理详解

深入PMS源码:

1.PMS的启动过程和执行流程
2.APK的安装和卸载源码分析
3.PMS中intent-filter的匹配架构

WMS:
1.WMS的诞生
2.WMS的重要成员和Window的添加过程
3.Window的删除过程

《Android Framework学习手册》:https://qr18.cn/AQpN4J

  1. 开机Init 进程
  2. 开机启动 Zygote 进程
  3. 开机启动 SystemServer 进程
  4. Binder 驱动
  5. AMS 的启动过程
  6. PMS 的启动过程
  7. Launcher 的启动过程
  8. Android 四大组件
  9. Android 系统服务 - Input 事件的分发过程
  10. Android 底层渲染 - 屏幕刷新机制源码分析
  11. Android 源码分析实战

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/117440.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Unity】常见的角色移动旋转

在Unity 3D游戏引擎中,可以使用不同的方式对物体进行旋转。以下是几种常见的旋转方式: 欧拉角(Euler Angles):欧拉角是一种常用的旋转表示方法,通过绕物体的 X、Y 和 Z 轴的旋转角度来描述物体的旋转。在Un…

opencv入门-Opencv原理以及Opencv-Python安装

图像的表示 1,位数 计算机采用0/1编码的系统,数字图像也是0/1来记录信息,图像都是8位数图像,包含0~255灰度, 其中0代表最黑,1代表最白 3, 4,OpenCV部署方法 安装OpenCV之前…

HTML+JavaScript+CSS DIY 分隔条splitter

一、需求分析 现在电脑的屏幕越来越大,为了利用好宽屏,我们在设计系统UI时喜欢在左侧放个菜单或选项面板,在右边显示与菜单或选项对应的内容,两者之间用分隔条splitter来间隔,并可以通过拖动分隔条splitter来动态调研…

表白墙程序

目录 一、页面代码部分 二、设计程序 二、实现 doPost​编辑 三、实现 doGet 四、前端代码部分 五、使用数据库存储数据 一、页面代码部分 在之前的一篇博客中&#xff0c;已经写过了表白墙的页面代码实现&#xff0c;这里就不再重复了 页面代码如下&#xff1a; <!…

springboot配置ym管理各种日记(log)

1&#xff1a;yml配置mybatis_plus默认日记框架 mybatis-plus:#这个作用是扫描xml文件生效可以和mapper接口文件使用&#xff0c;#如果不加这个,就无法使用xml里面的sql语句#启动类加了MapperScan是扫描指定包下mapper接口生效&#xff0c;如果不用MapperScan可以在每一个mapp…

[贪心] 拼接最小数

这道题思路并不难&#xff0c;我主要想学习其一些对于字符串的处理。 代码如下&#xff1a; #include <iostream> #include <string> #include <algorithm> using namespace std;const int MAXN 10000; string nums[MAXN];bool cmp(string a, string b) {…

创建ffmpeg vs2019工程

0 写在前面 本文主要参考链接&#xff1a;https://www.cnblogs.com/suiyek/p/15669562.html 感谢作者的付出&#xff1b; 1 目录结构 2 下载yasm和nasm 如果自己在安装VS2019等IDE的时候已经安装了它们&#xff0c;则不用再单独进行安装&#xff0c;比如我这边已经安装了&a…

【Redis从头学-完结】Redis全景思维导图一览!耗时半个月专为Redis初学者打造!

&#x1f9d1;‍&#x1f4bb;作者名称&#xff1a;DaenCode &#x1f3a4;作者简介&#xff1a;CSDN实力新星&#xff0c;后端开发两年经验&#xff0c;曾担任甲方技术代表&#xff0c;业余独自创办智源恩创网络科技工作室。会点点Java相关技术栈、帆软报表、低代码平台快速开…

keras深度学习框架通过简单神经网络实现手写数字识别

背景 keras深度学习框架&#xff0c;并不是一个独立的深度学习框架&#xff0c;它后台依赖tensorflow或者theano。大部分开发者应该使用的是tensorflow。keras可以很方便的像搭积木一样根据模型搭出我们需要的神经网络&#xff0c;然后进行编译&#xff0c;训练&#xff0c;测试…

从AD迁移至AAD,看体外诊断领军企业如何用网络准入方案提升内网安全基线

摘要&#xff1a; 某医用电子跨国集团中国分支机构在由AD向AzureAD Global迁移时&#xff0c;创新使用宁盾网络准入&#xff0c;串联起上海、北京、无锡等国内多个职场与海外总部,实现平滑、稳定、全程无感知的无密码认证入网体验&#xff0c;并通过合规基线检查&#xff0c;确…

2024年java面试--集合篇

文章目录 前言ListSetMapCollectionListSetMapJDK1.7 HashMap&#xff1a;JDK1.8 HashMap&#xff1a; 一、ArrayList和LinkedList的区别二、HashSet的实现原理&#xff1f;三、List接口和Set接口的区别四、hashmap底层实现五、HashTable与HashMap的区别六、线程不安全体现七、…

如何制作并运行 jar 程序

以下是用 Intellij 制作 jar 程序&#xff0c;并运行的方法。 【1】新建工程&#xff0c;保持默认选项&#xff0c;Next 【2】保持默认选项&#xff0c;Next 【3】给工程命名&#xff0c;设置保存位置&#xff0c;Finish 【4】新建工程结束&#xff0c;进入开发界面 【5】展开…

运维自动化与Cobbler服务部署

运维自动化与Cobbler服务部署 一.Cobbler简介 1.1.简介 Cobbler是一款Linux生态的自动化运维工具&#xff0c;基于Python2开发&#xff0c;用于自动化批量部署安装操作系 统&#xff1b;其提供基于CLI的管理方式和WEB配置界面&#xff0c;其中WEB配置界面是基于Python2和Djang…

【算法奥义】最大矩形问题

首先建立一个二维数组&#xff0c;这个二维数组&#xff0c;计算出矩阵的每个元素的左边连续 1 的数量&#xff0c;使用二维数组 left记录&#xff0c;其中left[i][j] 为矩阵第 i 行第 j 列元素的左边连续 1 的数量。 也就是从这个元素开始&#xff0c;从右往左边数有多少个连…

肖sir__xftp安装使用__004

xftp 一、定义&#xff1a;Xftp是一款功能强大的FTP传输软件&#xff0c;主要用于文件的上传和下载&#xff0c;支持SFTP和FTP协议。Xftp在Windows系统上设计&#xff0c;但也可在Linux系统上使用。本文将详细介绍Xftp的功能和使用方法 二、Xftp的功能 1.文件传输与管理&#…

Linux centos7 bash编程(循环与条件判断)

在编程训练中&#xff0c;循环结构与条件判断十分重要。 根据条件为真为假确定是否执行循环。 有时&#xff0c;根据条件的真假结果&#xff0c;决定执行哪些语句&#xff0c;这就是分支语句。 为了训练分支语句与循环语句&#xff0c;我们设计一个案例&#xff1a; 求一组…

LPDDR4、DDR4

核心信息&#xff1a; 2400Mbps&#xff08;每秒传输2400*1百万bit&#xff09; 2400MT/s&#xff08;百万次/秒&#xff09; 信号

国际版阿里云/腾讯云:弹性高性能计算E-HPC入门概述

入门概述 本文介绍E-HPC的运用流程&#xff0c;帮助您快速上手运用弹性高性能核算。 下文以创立集群&#xff0c;在集群中安装GROMACS软件并运转水分子算例进行高性能核算为例&#xff0c;介绍弹性高性能核算的运用流程&#xff0c;帮助您快速上手运用弹性高性能核算。运用流程…

文件夹如何隐藏加密?文件夹加密隐藏软件有哪些?

文件夹加密和文件夹隐藏都是保护文件夹数据安全的方法&#xff0c;我们可以将这两者结合&#xff0c;以实现更好的保护。那么&#xff0c;文件夹如何隐藏加密呢&#xff1f; 如何隐藏加密文件夹&#xff1f; 想要隐藏加密文件夹&#xff0c;我们可以通过以下两种方式来实现&am…

React笔记(四)类组件(2)

一、类组件的props属性 组件中的数据&#xff0c;除了组件内部的状态使用state之外&#xff0c;状态也可以来自组件的外部&#xff0c;外部的状态使用类组件实例上另外一个属性来表示props 1、基本的使用 在components下创建UserInfo组件 import React, { Component } from…