【Android】深入Binder拦截

☞ Github ☜  ☞ Gitee ☜

说明

Binder作为Android系统跨进程通信的核心机制。网上也有很多深度讲解该机制的文章,如:

这些文章和系统源码可以很好帮助我们理解Binder的实现原理和设计理念,为拦截做准备。借助Binder拦截可以我们可以扩展出那些能力呢:

  1. 虚拟化的能力,多年前就出现的应用免安装运行类产品如:VirtualApp/DroidPlugin/平行空间/双开大师/应用分身等。
  2. 测试验证的能力,通常为Framework层功能开发。
  3. 检测第三方SDK或模块系统服务调用访问情况(特别是敏感API调用)。
  4. 逆向分析应用底层服务接口调用实现。
  5. 第三方ROM扩展Framework服务。

现有方案

一直以来实时分析和拦截进程的Binder通信是通过Java层的AIDL接口代理来实现的。借助于Android系统Binder服务接口设计的规范,上层的接口均继承于IBinder

如一下为代理目标对象的所有的接口API的方法:

java 复制代码
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;

private static void getInterface(Class<?> cls, final HashSet<Class<?>> ss) {
    Class<?>[] ii;
    do {
        ii = cls.getInterfaces();
        for (final Class<?> i : ii) {
            if (ss.add(i)) {
                getInterface(i, ss);
            }
        }
        cls = cls.getSuperclass();
    } while (cls != null);
}

private static Class<?>[] getInterfaces(Class<?> cls) {
    final HashSet<Class<?>> ss = new LinkedHashSet<Class<?>>();
    getInterface(cls, ss);
    if (0 < ss.size()) {
        return ss.toArray(new Class<?>[ss.size()]);
    }
    return null;
}

public static Object createProxy(Object org, InvocationHandler cb) {
    try {
        Class<?> cls = org.getClass();
        Class<?>[] cc = getInterfaces(cls);
        return Proxy.newProxyInstance(cls.getClassLoader(), cc, cb);
    } catch (Throwable e) {
        Logger.e(e);
    } finally {
        // TODO release fix proxy name
    }
    return null;
}

1、对于已经生成的Binder服务对象,在应用进程可参与实现逻辑之前就已经缓存了,我们需要找到并且进行替换(AMS、PMS、WMS等),如AMS在Android 8.0之后的缓存如下:

java 复制代码
// source code: http://aospxref.com/android-9.0.0_r61/xref/frameworks/base/core/java/android/app/ActivityManager.java
package android.app;

public class ActivityManager {

    public static IActivityManager getService() {
        return IActivityManagerSingleton.get();
    }

    private static final Singleton<IActivityManager> IActivityManagerSingleton =
            new Singleton<IActivityManager>() {
                @Override
                protected IActivityManager create() {
                    final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
                    final IActivityManager am = IActivityManager.Stub.asInterface(b);
                    return am;
                }
            };
}

因此我们需要找到并且替换它,如:

java 复制代码
Object obj;
if (Build.VERSION.SDK_INT < 26) {// <= 7.0
    obj = ReflectUtils.getStaticFieldValue("android.app.ActivityManagerNative", "gDefault");
} else {// 8.0 <=
    obj = ReflectUtils.getStaticFieldValue("android.app.ActivityManager", "IActivityManagerSingleton");
}
Object inst = ReflectUtils.getFieldValue(obj, "mInstance");
ReflectUtils.setFieldValue(obj, "mInstance", createProxy(inst));

2、对于后续运行过程中才获取的Binder服务,则需要代理ServiceManager,源码如下:

java 复制代码
// source code: http://aospxref.com/android-9.0.0_r61/xref/frameworks/base/core/java/android/os/ServiceManager.java
package android.os;

public final class ServiceManager {
    private static final String TAG = "ServiceManager";

    private static IServiceManager sServiceManager;
}

因此我们的代理如下:

java 复制代码
Class<?> cls = ReflectUtils.findClass("android.os.ServiceManager");
Object org = ReflectUtils.getStaticFieldValue(cls, "sServiceManager");
Object pxy = new createProxy(org);
if (null != pxy) {
    ReflectUtils.setStaticFieldValue(getGlobalClass(), "sServiceManager", pxy);
}

这样每次在第一次访问该服务时,就会调用IServiceManager中的getService的方法,而该方法已经被我们代理拦截,我们可以通过参数可以识别当前获取的是哪个服务,然后将获取的服务对象代理后在继续返回即可。

但是:这样的方案并不能拦截进程中所有的Binder服务。我们面临几大问题:

  1. 首先,Android源码越来越庞大,了解所有的服务工作量很大,因此有哪些服务已经被缓存排查非常困难。

  2. 其次,厂商越来越钟情于扩展自定义服务,这些服务不开源,识别和适配更加耗时。

  3. 再次,有一部分服务只有native实现,并不能通过Java层的接口代理进行拦截(如:Sensor/Audio/Video/Camera服务等)。

    c 复制代码
    // source code: http://aospxref.com/android-13.0.0_r3/xref/frameworks/av/camera/ICamera.cpp
    
    class BpCamera: public BpInterface<ICamera>
    {
    public:
        explicit BpCamera(const sp<IBinder>& impl)
            : BpInterface<ICamera>(impl)
        {
        }
      
        // start recording mode, must call setPreviewTarget first
        status_t startRecording()
        {
            ALOGV("startRecording");
            Parcel data, reply;
            data.writeInterfaceToken(ICamera::getInterfaceDescriptor());
            remote()->transact(START_RECORDING, data, &reply);
            return reply.readInt32();
        }
    }

新方案:基于底层拦截

原理

我们都知道Binder在应用进程运行原理如下图:

不管是Java层还是native层的接口调用,最后都会通过ioctl函数访问共享内存空间,达到跨进程访问数据交换的目的。因此我们只要拦截ioctl函数,即可完成对所有Binder通信数据的拦截。底层拦截有以下优势:

1)可以拦截所有的Binder通信。

2)底层拦截稳定,高兼容性。从Android 4.xAndroid 14,近10年的系统版本演进,涉及到Binder底层通信适配仅两次;一次是支持64位进程(当时需要同时兼容32位和64位进程访问Binder服务)。另一次是华为鸿蒙系统的诞生,华为ROMBinder通信协议中增加了新的标识字段。

要解决的问题

如何拦截

C/C++层的函数拦截,并不像Java层一样系统提供了较为稳定的代理工具,在这里不是我们本期讨论的重点,可以直接采用网上开源的Hook框架:

如何过滤

ioctl函数为系统底层设备访问函数,调用及其频繁,而Binder通信调用只是其中调用者之一,因此需要快速识别非Binder通信调用,不影响程序性能。

函数定义:

c 复制代码
#include <sys/ioctl.h>

int ioctl(int fildes, unsigned long request, ...);

request的参数定义:

c 复制代码
// source code: http://aospxref.com/android-14.0.0_r2/xref/bionic/libc/kernel/uapi/linux/android/binder.h
#define BINDER_WRITE_READ _IOWR('b', 1, struct binder_write_read)
#define BINDER_SET_IDLE_TIMEOUT _IOW('b', 3, __s64)
#define BINDER_SET_MAX_THREADS _IOW('b', 5, __u32)
#define BINDER_SET_IDLE_PRIORITY _IOW('b', 6, __s32)
#define BINDER_SET_CONTEXT_MGR _IOW('b', 7, __s32)
#define BINDER_THREAD_EXIT _IOW('b', 8, __s32)
#define BINDER_VERSION _IOWR('b', 9, struct binder_version)
#define BINDER_GET_NODE_DEBUG_INFO _IOWR('b', 11, struct binder_node_debug_info)
#define BINDER_GET_NODE_INFO_FOR_REF _IOWR('b', 12, struct binder_node_info_for_ref)
#define BINDER_SET_CONTEXT_MGR_EXT _IOW('b', 13, struct flat_binder_object)
#define BINDER_FREEZE _IOW('b', 14, struct binder_freeze_info)
#define BINDER_GET_FROZEN_INFO _IOWR('b', 15, struct binder_frozen_status_info)
#define BINDER_ENABLE_ONEWAY_SPAM_DETECTION _IOW('b', 16, __u32)
#define BINDER_GET_EXTENDED_ERROR _IOWR('b', 17, struct binder_extended_error)

对应的源码:

c 复制代码
// source code: http://aospxref.com/android-14.0.0_r2/xref/frameworks/native/libs/binder/IPCThreadState.cpp
void IPCThreadState::threadDestructor(void *st) {
	ioctl(self->mProcess->mDriverFD, BINDER_THREAD_EXIT, 0);
}

status_t IPCThreadState::getProcessFreezeInfo(pid_t pid, uint32_t *sync_received, uint32_t *async_received) {
    return ioctl(self()->mProcess->mDriverFD, BINDER_GET_FROZEN_INFO, &info);
}

status_t IPCThreadState::freeze(pid_t pid, bool enable, uint32_t timeout_ms) {
    return ioctl(self()->mProcess->mDriverFD, BINDER_FREEZE, &info) < 0);
}

void IPCThreadState::logExtendedError() {
    ioctl(self()->mProcess->mDriverFD, BINDER_GET_EXTENDED_ERROR, &ee) < 0);
}

status_t IPCThreadState::talkWithDriver(bool doReceive) {
    // 实际Binder调用通信
    return ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr);
}

快速过滤:

c 复制代码
static int ioctl_hook(int fd, int cmd, void* arg) {
    if (cmd != BINDER_WRITE_READ || !arg || g_ioctl_disabled) {
        return g_ioctl_func(fd, cmd, arg);
    }
}
如何解析

目标源码:http://aospxref.com/android-14.0.0_r2/xref/frameworks/native/libs/binder

重点解析发送(即BC_TRANSACTIONBC_REPLY)和接收(即BR_TRANSACTIONBR_REPLY)的类型数据。

如何修改数据

修改数据分为以下几种:

1)修复调用时参数数据。

2)修复调用后返回的结果数据。

如果数据修复不改变当前数据的长度,只是内容的变化,则可以直接通过地址进行修改。否则需要创建新的内存进行修改后将新的数据地址设置到BINDER_WRITE_READ结构的buffer成员。此时处理好内存的释放问题。

3)直接拦截本次调用。

为了保障稳定性,不打断Binder的调用流程(通常这也是拦截和逆向方案保障稳定的最重要原则之一)。我们可以将目标函数code修改成父类处理的通用方法,然后通过修复调用的返回值即可完成拦截。

方案实现

数据解析

Binder调用数据结构如下:

解析bwr

bwrbinder_write_read,从源码中了解到ioctlBINDER_WRITE_READ类型的arg数据结构为:

c 复制代码
struct binder_write_read {
  // 调用时传入的数据
 	binder_size_t write_size;// call data
 	binder_size_t write_consumed;// call data
 	binder_uintptr_t write_buffer;// call data
  
	// 结果返回数据
 	binder_size_t read_size;// recv data
 	binder_size_t read_consumed;// recv data
 	binder_uintptr_t read_buffer;// recv data
};

不管是传入还是返回的数据,都是一组BC命令或BR命令,也就是说一次调用上层会打包几个命令一起传递。因此我们需要通过循环来找到我们的命令。

c 复制代码
void binder_find_for_bc(struct binder_write_read& bwr) {
    binder_uintptr_t cmds = bwr.write_buffer;
    binder_uintptr_t end = cmds + (binder_uintptr_t)bwr.write_size;

    binder_txn_st* txn = NULL;
    while (0 < cmds && cmds < end && !txn) {
        // 由于每次Binder通信数据量的限制,Binder设计每次调用有且仅包含一个有效的参数命令,因此只要找到即可,其他类型则直接跳过忽略
        cmds = binder_parse_cmds_bc(cmds, txn);
    }
}

dump数据如下:

复制代码
write_buffer:0xb400007107d1d400, write_consumed:68, write_size:68
00000000:  00 63 40 40 14 00 00 00  00 00 00 00 00 00 00 00  .c@@............
00000010:  00 00 00 00 01 00 00 00  12 00 00 00 00 00 00 00  ................
00000020:  00 00 00 00 54 00 00 00  00 00 00 00 00 00 00 00  ....T...........
00000030:  00 00 00 00 00 4d 3a ac  70 00 00 b4 00 00 00 00  .....M:.p.......
00000040:  00 00 00 00                                       ....
BR_NOOP: 0x720c
BR_TRANSACTION_COMPLETE: 0x7206
BR_REPLY: 0
解析txn

txnbinder_transaction_data,Binder方法调用的方法参数信息定义如下:

c 复制代码
struct binder_transaction_data {
 union {
     __u32 handle;
     binder_uintptr_t ptr;
 } target;// 目标服务句柄,server端使用

 binder_uintptr_t cookie;// 缓存的Binder进行访问
 __u32 code;//方法编号

 __u32 flags;// 标识,如是否为 oneway
 __s32 sender_pid;
 __u32 sender_euid;
 binder_size_t data_size;// 数据长度
 binder_size_t offsets_size;// 若包含对象,则对象数据大小
  
 union {
     struct {
         binder_uintptr_t buffer;// Binder方法参数值地址
         binder_uintptr_t offsets;// Binder方法参数对象数据地址
     } ptr;
     __u8 buf[8];
 } data;
};

dumo数据如下:

复制代码
Trace   : target:       1   cookie:       0   code:      23   flags:   0x12(READ REPLY)
Trace   :    pid:       0      uid:       0   size:     196    offs:8
Trace   : 00000000:  00 00 00 80 ff ff ff ff  54 53 59 53 1c 00 00 00  ........TSYS....
Trace   : 00000010:  61 00 6e 00 64 00 72 00  6f 00 69 00 64 00 2e 00  a.n.d.r.o.i.d...
Trace   : 00000020:  61 00 70 00 70 00 2e 00  49 00 41 00 63 00 74 00  a.p.p...I.A.c.t.
Trace   : 00000030:  69 00 76 00 69 00 74 00  79 00 4d 00 61 00 6e 00  i.v.i.t.y.M.a.n.
Trace   : 00000040:  61 00 67 00 65 00 72 00  00 00 00 00 85 2a 62 73  a.g.e.r......*bs
Trace   : 00000050:  13 01 00 00 00 38 dd 2a  71 00 00 b4 00 05 e9 31  .....8.*q......1
Trace   : 00000060:  71 00 00 b4 01 00 00 0c  1a 00 00 00 63 00 6f 00  q...........c.o.
Trace   : 00000070:  6d 00 2e 00 69 00 66 00  6d 00 61 00 2e 00 74 00  m...i.f.m.a...t.
Trace   : 00000080:  72 00 61 00 6e 00 73 00  65 00 63 00 2e 00 63 00  r.a.n.s.e.c...c.
Trace   : 00000090:  6f 00 6e 00 74 00 61 00  69 00 6e 00 65 00 72 00  o.n.t.a.i.n.e.r.
Trace   : 000000a0:  00 00 00 00 08 00 00 00  73 00 65 00 74 00 74 00  ........s.e.t.t.
Trace   : 000000b0:  69 00 6e 00 67 00 73 00  00 00 00 00 00 00 00 00  i.n.g.s.........
Trace   : 000000c0:  01 00 00 00                                       ....
Trace   : binder object offs:0x4c  type:0x73622a85  flags:0x113  ptr:0x2add3800  cookie:0x31e90500
解析服务名

Binder通信数据头如下,即可解析出目标服务名:

c 复制代码
void find_server_name(const binder_txn_st* txn) {
		const int32_t* ptr = reinterpret_cast<const int32_t*>(txn->data.ptr.buffer);
  	++ ptr;// skip strict model
    if (29 <= sdkVersion()) ++ ptr;// 10.0 <=, skip flags(ff ff ff ff)
		
  	int32_t nameLen = *ptr;
    const uint16_t* name16 = (const uint16_t*)(ptr+1);
}
解析方法名

Binder通信数据中标识该服务方法的参数是txn->codeAIDL定义类在编译后会为每个方法自动生成静态的方法。

如定义的Binder接口方法为:

java 复制代码
interface IDemo {
  void test();
  void test2();
}

则编译后生成的类为:

java 复制代码
class IDemo$Stub {
   void test();
   void test2();
   
  static final int TRANSACTION_test = 1;
  static final int TRANSACTION_test2 = 2;
}

因此我们可以通过反射的方式,找到服务名对应的类所有静态成员变量,然后找到与code值相等的成员即为此方法。

这里可能需要解决私有API的限制解除问题。

java 复制代码
// 可直接使用工程工具类
TstClassPrinter.printStubByCodes("android.app.IActivityManager", 13, 16, 67);

日志输出如下:

解析数据

首先需要借助数据封装类Parcel

c 复制代码
// souce code:
// http://aospxref.com/android-14.0.0_r2/xref/frameworks/native/include/binder/Parcel.h
// http://aospxref.com/android-14.0.0_r2/xref/frameworks/native/libs/binder/Parcel.cpp

借助该类可以解析一些比较简单的数据,快速的找到目标内容。而对于比较复杂的数据,如参数值为Intent,该参数类型嵌套了多层的Parcelable成员,因此在native层通过Parcel来解析,兼容性比较差。因此我们选择通过回调到Java层来解析,修改后再格式化为nativebuffer数据。这里需要处理好Javanative层的数据交换问题,以及回收。

native层:

c 复制代码
// 创建
jobject obtain(JNIEnv* env) {
    jclass jcls = env->FindClass("android/os/Parcel");
    jmethodID method = env->GetStaticMethodID(jcls, "obtain", "()Landroid/os/Parcel;");
    if (!method) return NULL;

    mParcelObj = env->CallStaticObjectMethod(jcls, method);
    if (!mParcelObj) return NULL;

    if (0 < mUparcel->dataSize()) {
        method = env->GetMethodID(sParcelClass, "setDataPosition", "(I)V");
        if (method) {
            unmarshall(env, mUparcel->data(), mUparcel->dataSize());
            env->CallVoidMethod(mParcelObj, method, mUparcel->dataPosition());
        }
    }

    return mParcelObj;
}

// 回收
void recycle(JNIEnv* env) {
    jclass jcls = env->FindClass("android/os/Parcel");
    jmethodID method = env->GetMethodID(jcls, "recycle", "()V");
    if (method) {
        env->CallVoidMethod(mParcelObj, method);
    }
    if (mParcelObj) {
        env->DeleteLocalRef(mParcelObj);
    }
    mParcelObj = NULL;
}

Java层:

java 复制代码
public static void clearHttpLink(Parcel p/*IN*/, Parcel q/*OUT*/) {
    try {
        Intent ii = Intent.CREATOR.createFromParcel(pp);
		// TODO something ...
      	
        // write new data
        q.appendFrom(p, p.dataPosition(), p.dataAvail());
    } catch (Throwable e) {
        e.printStackTrace();
    }
}

数据拦截

Binder的数据解析和打印不会改变原数据内容,因此相对简单,如果要对数据进行修改,则相对复杂一些。修复的数据需要替换原数据,因此需要进行如下操作。

1、数据替换。

txn中方法参数的数据指针指向新创建的数据区。

c 复制代码
int binder_replace_txn_for_br(binder_txn_st *txn, ParcelEx* reply, binder_size_t _pos) {
    size_t size = reply->ipcDataSize();
    uint8_t* repData = (uint8_t*)malloc(size + txn->offsets_size);
    memcpy(repData, reply->data(), size);
    if (0 < txn->offsets_size) {
        binder_replace_objects(txn, repData, _pos, ((int)size) - ((int)(txn->data_size)));
    }

    txn->data.ptr.buffer = reinterpret_cast<uintptr_t>(repData);
    txn->data_size = size;
    return 0;
}

2、修正对象指针。

如果传入的参数包含Binder对象,如register方法的Observe。因此修复的数据可能导致偏移的地址前移或者后移,因此需要重新计算偏移,如:

c 复制代码
void replaceObjects(binder_txn_st *txn, uint8_t* objData, binder_size_t _pos, int _off) {
    binder_size_t* offs = reinterpret_cast<binder_size_t*>(txn->data.ptr.offsets);
    unsigned count = txn->offsets_size / sizeof(binder_size_t);

    while (0 < count--) {
        if (0 != memcmp(objData + (int)(*offs), (uint8_t*)txn->data.ptr.buffer + (int)(*offs), sizeof(binder_size_t))) {
            *offs += _off;
        }
        ++ offs;
    }
}

3、内存释放。

需要保存原地址A和新的地址AA的映射关系到自定义的内存池中。

Binder通信命令出现BC_FREE_BUFFERBR_FREE_BUFFER时,则通过该命令要释放的AA地址,然后从内存池找到与之对应A的地址,并设置回去让上层继续释放,完成内存使用的闭环。

c 复制代码
case BC_FREE_BUFFER:
{
    uintptr_t* buffPtr = (uintptr_t *)cmd;
    uintptr_t ptr = MemPool::detach(*buffPtr);
    if (__UNLIKELY(0 != ptr)) {
        *buffPtr = ptr;// set origin buffer
    }
    cmd += sizeof(uintptr_t);// move to next command
}   break;

附:

如果你有需要,可以直接使用我们已经封装好的SDK来实现相应的功能,该项目已经开源,可以直接使用,参考【集成文档】

相关推荐
安东尼肉店4 小时前
Android compose屏幕适配终极解决方案
android
2501_916007475 小时前
HTTPS 抓包乱码怎么办?原因剖析、排查步骤与实战工具对策(HTTPS 抓包乱码、gzipbrotli、TLS 解密、iOS 抓包)
android·ios·小程序·https·uni-app·iphone·webview
feiyangqingyun6 小时前
基于Qt和FFmpeg的安卓监控模拟器/手机摄像头模拟成onvif和28181设备
android·qt·ffmpeg
用户20187928316710 小时前
ANR之RenderThread不可中断睡眠state=D
android
煤球王子10 小时前
简单学:Android14中的Bluetooth—PBAP下载
android
小趴菜822710 小时前
安卓接入Max广告源
android
齊家治國平天下10 小时前
Android 14 系统 ANR (Application Not Responding) 深度分析与解决指南
android·anr
ZHANG13HAO10 小时前
Android 13.0 Framework 实现应用通知使用权默认开启的技术指南
android
【ql君】qlexcel10 小时前
Android 安卓RIL介绍
android·安卓·ril
写点啥呢10 小时前
android12解决非CarProperty接口深色模式设置后开机无法保持
android·车机·aosp·深色模式·座舱