
基于底层动态拦截技术,实现对 Android 平台下应用进程 Binder 通信协议的动态分析和拦截。
Binder作为Android系统跨进程通信的核心机制。网上也有很多深度讲解该机制的文章,如:
这些文章和系统源码可以很好帮助我们理解 Binder 的实现原理和设计理念,为拦截做准备。借助 Binder 拦截可以我们可以扩展出那些能力呢:
VirtualApp/DroidPlugin/平行空间/双开大师/应用分身等。Framework层功能开发。SDK或模块系统服务调用访问情况(特别是敏感API调用)。ROM扩展Framework服务。一直以来实时分析和拦截进程的Binder通信是通过Java层的AIDL接口代理来实现的。借助于Android系统Binder服务接口设计的规范,上层的接口均继承于IBinder。
如一下为代理目标对象的所有的接口API的方法:
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 之后的缓存如下:
// 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; } }; } 因此我们需要找到并且替换它,如:
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,源码如下:
// 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; } 因此我们的代理如下:
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服务。我们面临几大问题:
首先,Android 源码越来越庞大,了解所有的服务工作量很大,因此有哪些服务已经被缓存排查非常困难。
其次,厂商越来越钟情于扩展自定义服务,这些服务不开源,识别和适配更加耗时。
再次,有一部分服务只有native实现,并不能通过Java层的接口代理进行拦截(如:Sensor/Audio/Video/Camera 服务等)。
// 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.x至Android 14,近 10 年的系统版本演进,涉及到Binder底层通信适配仅两次;一次是支持 64 位进程(当时需要同时兼容 32 位和 64 位进程访问Binder服务)。另一次是华为鸿蒙系统的诞生,华为ROM在Binder通信协议中增加了新的标识字段。
C/C++层的函数拦截,并不像Java层一样系统提供了较为稳定的代理工具,在这里不是我们本期讨论的重点,可以直接采用网上开源的Hook框架:
ioctl函数为系统底层设备访问函数,调用及其频繁,而Binder通信调用只是其中调用者之一,因此需要快速识别非Binder通信调用,不影响程序性能。
函数定义:
#include <sys/ioctl.h> int ioctl(int fildes, unsigned long request, ...); request的参数定义:
// 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) 对应的源码:
// 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); } 快速过滤:
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_TRANSACTION和BC_REPLY)和接收(即BR_TRANSACTION和BR_REPLY)的类型数据。
修改数据分为以下几种:
1 )修复调用时参数数据。
2 )修复调用后返回的结果数据。
如果数据修复不改变当前数据的长度,只是内容的变化,则可以直接通过地址进行修改。否则需要创建新的内存进行修改后将新的数据地址设置到BINDER_WRITE_READ结构的buffer成员。此时处理好内存的释放问题。
3 )直接拦截本次调用。
为了保障稳定性,不打断Binder的调用流程(通常这也是拦截和逆向方案保障稳定的最重要原则之一)。我们可以将目标函数code修改成父类处理的通用方法,然后通过修复调用的返回值即可完成拦截。
Binder 调用数据结构如下:

bwrbwr即binder_write_read,从源码中了解到ioctl的BINDER_WRITE_READ类型的arg数据结构为:
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 命令,也就是说一次调用上层会打包几个命令一起传递。因此我们需要通过循环来找到我们的命令。
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 txntxn即binder_transaction_data,Binder 方法调用的方法参数信息定义如下:
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通信数据头如下,即可解析出目标服务名:
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->code。AIDL定义类在编译后会为每个方法自动生成静态的方法。
如定义的Binder接口方法为:
interface IDemo { void test(); void test2(); } 则编译后生成的类为:
class IDemo$Stub { void test(); void test2(); static final int TRANSACTION_test = 1; static final int TRANSACTION_test2 = 2; } 因此我们可以通过反射的方式,找到服务名对应的类所有静态成员变量,然后找到与code值相等的成员即为此方法。
这里可能需要解决私有 API 的限制解除问题。
// 可直接使用工程工具类 TstClassPrinter.printStubByCodes("android.app.IActivityManager", 13, 16, 67); 日志输出如下:

首先需要借助数据封装类Parcel。
// 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 层来解析,修改后再格式化为native的buffer数据。这里需要处理好Java和native层的数据交换问题,以及回收。
native层:
// 创建 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层:
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中方法参数的数据指针指向新创建的数据区。
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。因此修复的数据可能导致偏移的地址前移或者后移,因此需要重新计算偏移,如:
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_BUFFER和BR_FREE_BUFFER时,则通过该命令要释放的AA地址,然后从内存池找到与之对应A的地址,并设置回去让上层继续释放,完成内存使用的闭环。
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来实现相应的功能,该项目已经开源,可以直接使用,参考 [集成文档] 。
1 nnegier 2024-04-23 13:55:27 +08:00 via Android 大佬好,我想借此问一下 Binder 一次传输的大小是不是有限制?如果有,这个限制怎么解决呢?(问题来源在我使用 Shizuku 时感觉到) |
2 52MF4yM298P2vX26 2024-04-23 13:55:59 +08:00 有什么使用的场景 |
3 gam2046 2024-04-23 15:48:59 +08:00 |
4 iofomo OP @nnegier 是的,你的问题很赞。 一次 Binder 通信数据有最大的 1M 限制( Android 源码默认),如果超过这个大小了,在 Binder 通信中会有多次的数据交互,如常见的 IPackageManager 的 getInstalledPackages 就会碰到。 这时底层通信拦截的处理办法是: 我们在首次调用的时候 cache 住 binder_object 的 handle ( binder_txn_st* txn 中的 handle ),当本次 binder 通信为结束,但是 Code 为 1 ( FIRST_CALL_TRANSACTION ),接口方法 Code 均大于 1 ( binder_txn_st* txn 中的 Code ),此时匹配 handle 值于之前 cache 的 handle 值是否一致,一致则解析。 如 cache: unsigned int handle = target_get_binder_obj_ptr(txn); if (0 < handle) { TlsCache* poolItem = TlsUtils::getPool(); if (poolItem) { poolItem->setInt(handle);// TODO save cache } else { // TODO cleat } } 如:解析 if (token.unequal(__PACKAGE_LENGTH__, __PACKAGE_HASH__)) { if (FIRST_CALL_TRANSACTION == txn->code) { TlsCache *tlsPool = TlsUtils::getPool(); if (tlsPool->getInt() == txn->target.handle) { // TODO } } } |
5 iofomo OP @beasonshu 一般容器,沙箱,虚拟化,隐私保护,安全防护等产品会用到,以及研究某些应用使用了那些系统接口的特性功能等 |
6 lee1997 2024-04-23 20:17:04 +08:00 |
7 beriru 2024-04-27 02:19:41 +08:00 |
8 beriru 2024-04-27 02:30:08 +08:00 > 首先,Android 源码越来越庞大,了解所有的服务工作量很大,因此有哪些服务已经被缓存排查非常困难。 > 其次,厂商越来越钟情于扩展自定义服务,这些服务不开源,识别和适配更加耗时。 > 再次,有一部分服务只有 native 实现,并不能通过 Java 层的接口代理进行拦截(如:Sensor/Audio/Video/Camera 服务等)。 如果只是为了解决 1,2 的话 https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/Binder.java;drc=074bb2dc367bde42b837a8de4d4e88f26c074c32;l=986 感觉这样就能解决大部分场景 |
9 xingda920813 2024-04-27 22:18:28 +08:00 native hook 不稳定啊, 容易有 native crash. 一旦出现, 非 hook 框架的作者根本解决不了, 没法用于线上产品. 还是考虑纯 Java 方案吧. |
11 iofomo OP @beriru 如果只是想要获得调用方法是可以使用这个全局的,如果需要拿到 Binder 通信来回的数据,并且进行修改,则做不到 |
12 iofomo OP @xingda920813 稳定性还可以,特定场景使用的,用户量巨大的 ToC 应用自然不会作为主功能,通常为辅助的安全检测,数据采集的独立功能,就算异常也不会影响业务 |