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 ☜
Contents
illustrate
Binder
As Android
the core mechanism of system cross-process communication. There are also many articles on the Internet that explain this mechanism in depth, such as:
- Android cross-process communication explains the principle of Binder mechanism in detail
- Android system core mechanism Binder [series]
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:
- With virtualization capabilities, application installation-free running products have appeared many years ago, such as:
VirtualApp
//DroidPlugin
Parallel Space/Double Open Master/Application Clone, etc. - The ability to test and verify, usually for
Framework
layer functional development. - Detect
SDK
access to third-party or module system service calls (especially sensitiveAPI
calls). - Reverse analysis of application underlying service interface call implementation.
- Third-party
ROM
extensionFramework
services.
Existing solutions
Real-time analysis and interception of process Binder
communications have always been achieved through Java
layer interface proxies. AIDL
With the help of Android
system Binder
service 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 Binder
service 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, AMS
the 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 IServiceManager
in 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 Binder
services in the process. We face several major problems:
- 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.
- Secondly, manufacturers are increasingly interested in extending custom services. These services are not open source, and identification and adaptation are more time-consuming.
- Thirdly, some services are only
native
implemented and cannotJava
be 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 Binder
that the application process runs as shown below:
Regardless of whether it is Java
a layer or native
layer interface call, ioctl
the 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 ioctl
the function to complete Binder
the 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.x
to Android 14
10 years ago, the system version evolution in the past 10 years has only involved Binder
low-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 Binder
services). 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 Java
provide 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 Hook
framework:
- https://github.com/bytedance/android-inline-hook
- https://github.com/bytedance/bhook
- https://github.com/asLody/whale
How to filter
ioctl
Functions access functions for the system’s underlying devices and are called very frequently, and Binder
communication calls are only one of the callers. Therefore, non- Binder
communication calls need to be quickly identified without affecting program performance.
Function definition:
#include <sys/ioctl.h>
int ioctl(int fildes, unsigned long request, ...);
request
Parameter 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_TRANSACTION
and BC_REPLY
) and receiving (i.e. BR_TRANSACTION
and 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_READ
of 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, Binder
the 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 code
modify 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
bwr
That is , the data structure of the type learned binder_write_read
from the source code is: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
};
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);
}
}
dump
Data 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
txn
That 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;
};
dumo
Data 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
Binder
The 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
Binder
The parameters in the communication data that identify the service method are txn->code
. AIDL
The defined class will automatically generate static methods for each method after compilation.
The interface method defined Binder
is:
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 code
the 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 Parcelable
members, so if it is parsed at native
the 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 native
the buffer
data. Here we need to deal with the data exchange issues Java
with native
the layer, as well as recycling.
native
layer:
// 创建
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
layer:
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 txn
the 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 register
a 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 A
and the new address needs to be saved AA
in a custom memory pool.
When Binder
the communication command appears BC_FREE_BUFFER
, the address BR_FREE_BUFFER
to 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 SDK
to implement the corresponding functions. The project has been open source and can be used directly. Please refer to [Integration Document] .