Android – In-depth Binder interception

Based on the underlying dynamic interception technology, dynamic analysis and interception of the Binder communication protocol of the application process under the Android platform are realized.

The project has been open source:
☞ Github ☜   ☞ Gitee ☜

illustrate

BinderAs Androidthe core mechanism of system cross-process communication. There are also many articles on the Internet that explain this mechanism in depth, such as:

These articles and system source code can help us understand the implementation principles and design concepts of Binder and prepare for interception. What capabilities can we expand with the help of Binder interception:

  1. With virtualization capabilities, application installation-free running products have appeared many years ago, such as: VirtualApp// DroidPluginParallel Space/Double Open Master/Application Clone, etc.
  2. The ability to test and verify, usually for Frameworklayer functional development.
  3. Detect SDKaccess to third-party or module system service calls (especially sensitive APIcalls).
  4. Reverse analysis of application underlying service interface call implementation.
  5. Third-party ROMextension Frameworkservices.

Existing solutions

Real-time analysis and interception of process Bindercommunications have always been achieved through Javalayer interface proxies. AIDLWith the help of Androidsystem Binderservice interface design specifications, the upper-layer interfaces are inherited from IBinder.

For example, the following is the method of all interfaces of the proxy target object 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. For the generated Binderservice objects, they have been cached before the application process can participate in implementing the logic. We need to find and replace them ( AMS、PMS、WMS等). For example, AMSthe cache after Android 8.0 is as follows:

// 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;
                }
            };
}

So we need to find and replace it, like:

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. For services obtained during subsequent operations Binder, an agent is required ServiceManager. The source code is as follows:

// 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;
}

So our agents are as follows:

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);
}

In this way, every time the service is accessed for the first time, the method IServiceManagerin will be called getService, and this method has been intercepted by our proxy. We can identify which service is currently obtained through the parameters, and then proxy the obtained service object. Just keep going back.

但是:Such a solution cannot intercept all Binderservices in the process. We face several major problems:

  1. First of all, Android source code is getting larger and larger, and it is a lot of work to understand all services, so it is very difficult to check which services have been cached.
  2. Secondly, manufacturers are increasingly interested in extending custom services. These services are not open source, and identification and adaptation are more time-consuming.
  3. Thirdly, some services are only nativeimplemented and cannot Javabe intercepted through the interface proxy of the layer (such as: 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(); } }

New solution: based on underlying interception

principle

We all know Binderthat the application process runs as shown below:

Regardless of whether it is Javaa layer or nativelayer interface call, ioctlthe shared memory space will eventually be accessed through functions to achieve the purpose of cross-process access data exchange. Therefore, we only need to intercept ioctlthe function to complete Binderthe interception of all communication data. Low-level interception has the following advantages:

1) Can intercept all Binder communications.

2) The underlying interception is stable and highly compatible. From 10 Android 4.xto Android 1410 years ago, the system version evolution in the past 10 years has only involved Binderlow-level communication adaptation twice; once to support 64-bit processes (at that time, it needed to be compatible with both 32-bit and 64-bit process access Binderservices). Another time was the birth of Huawei’s Hongmeng system. Huawei added a new identification field to the communication protocol ROM.Binder

problem to be solved

How to intercept

C/C++The function interception of the layer does not Javaprovide a relatively stable proxy tool like the layer. This is not the focus of our discussion in this issue. You can directly use the online open source Hookframework:

How to filter

ioctlFunctions access functions for the system’s underlying devices and are called very frequently, and Bindercommunication calls are only one of the callers. Therefore, non- Bindercommunication calls need to be quickly identified without affecting program performance.

Function definition:

#include <sys/ioctl.h>

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

requestParameter definition:

// 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)

Corresponding source code:

// 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);
}

Quick filter:

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);
    }
}
How to parse

Target source code: http://aospxref.com/android-14.0.0_r2/xref/frameworks/native/libs/binder

Focus on parsing the type data of sending (i.e. BC_TRANSACTIONand BC_REPLY) and receiving (i.e. BR_TRANSACTIONand BR_REPLY).

How to modify data

Modified data is divided into the following categories:

1) Repair parameter data when calling.

2) Fix the result data returned after the call.

If the data repair does not change the length of the current data, but only changes the content, it can be modified directly through the address. Otherwise, you need to create new memory and set the new data address to the members BINDER_WRITE_READof the structure after modification buffer. At this time, the problem of memory release is handled.

3) Directly intercept this call.

In order to ensure stability, Binderthe calling process should not be interrupted (usually this is also one of the most important principles for interception and reverse solutions to ensure stability). We can codemodify the target function to a common method handled by the parent class, and then complete the interception by repairing the return value of the call.

Solution implementation

data analysis

The Binder call data structure is as follows:

parsebwr

bwrThat is , the data structure of the type learned binder_write_readfrom the source code is:ioctlBINDER_WRITE_READarg

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
};

Regardless of whether the data is passed in or returned, it is a set of BC commands or BR commands. That is to say, several commands will be packaged and transmitted together when calling the upper layer. So we need to loop through to find our command.

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);
    }
}

dumpData are as follows:

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
parsetxn

txnThat is binder_transaction_data, the method parameter information of the Binder method call is defined as follows:

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;
};

dumoData are as follows:

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
Parse service name

BinderThe communication data header is as follows, and the target service name can be parsed:

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);
}
Parse method name

BinderThe parameters in the communication data that identify the service method are txn->code. AIDLThe defined class will automatically generate static methods for each method after compilation.

The interface method defined Binderis:

interface IDemo {
  void test();
  void test2();
}

Then the class generated after compilation is:

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

Therefore, we can use reflection to find all the static member variables of the class corresponding to the service name, and then find codethe member with the same value, which is this method.

It may be necessary to solve the problem of lifting restrictions on private APIs here.

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

The log output is as follows:

Analytical data

First, you need to use the data encapsulation class 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

With the help of this class, you can parse some relatively simple data and quickly find the target content. For more complex data, such as the parameter value is Intent, the parameter type has multiple layers of nested Parcelablemembers, so if it is parsed at nativethe layer level Parcel, the compatibility is relatively poor. Therefore, we choose to parse it through callback to the Java layer, modify it and then format it into nativethe bufferdata. Here we need to deal with the data exchange issues Javawith nativethe layer, as well as recycling.

nativelayer:

// 创建
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;
}

Javalayer:

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();
    }
}

Data interception

Binder’s data parsing and printing will not change the original data content, so it is relatively simple. If you want to modify the data, it is relatively complicated. The repaired data needs to replace the original data, so the following operations need to be performed.

1. Data replacement.

Point txnthe data pointer of the method parameter to the newly created data area.


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. Correct the object pointer.

If the incoming parameter contains a Binder object, such as registera method Observe. Therefore, the repaired data may cause the offset address to move forward or backward, so the offset needs to be recalculated, such as:

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. Memory release.

The mapping relationship between the original address Aand the new address needs to be saved AAin a custom memory pool.

When Binderthe communication command appears BC_FREE_BUFFER, the address BR_FREE_BUFFERto be released by the command is used AA, and the corresponding address is found from the memory pool A, and set back to allow the upper layer to continue releasing, completing the closed loop of memory usage.

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;

Attached:

If you need it, you can directly use what we have packaged SDKto implement the corresponding functions. The project has been open source and can be used directly. Please refer to [Integration Document] .