Bootstrap

Android跨进程通信,binder传输数据过大导致客户端APP,Crash,异常捕获,监听异常的数值临界值,提前Hook拦截,共享内存实现传输

Android跨进程通信,binder传输数据过大导致Crash,异常捕获,监听异常的数值临界值,提前Hook拦截。

Java Crash捕获

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Thread.UncaughtExceptionHandler uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
            Log.e("crash", "当前进程id:" + android.os.Process.myPid());
            Log.e("crash", Log.getStackTraceString(e));
            if (uncaughtExceptionHandler != null) {
                uncaughtExceptionHandler.uncaughtException(t, e);
            }
        });
    }
}

1.binder在做跨进程传输时,最大可以携带多少数据

测试代码,跨进程传输1m数据

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        startBinding();
        mBtn = findViewById(R.id.btn);
        mBtn.setOnClickListener(v -> {
            Bundle bundle = new Bundle();
            bundle.putByteArray("binder_data", new byte[1 * 1024 * 1024]);
            Intent intent = new Intent();
            intent.putExtras(bundle);
            ComponentName componentName = new ComponentName("com.example.myapplication", "com.example.myapplication.MainActivity");
            intent.setComponent(componentName);
            startActivity(intent);
        });
    }

不出意外崩了

在这里插入图片描述

在这里插入图片描述

1.1有时候这个1m的崩溃系统捕获不到异常,

把数据传输从1M改成了800k测试

还是崩了,崩溃的数据量大概是500bk(不崩溃)-600kb(崩溃)之间。

在这里插入图片描述

核心log

在这里插入图片描述

IActivityTaskManager是个aidl文件;

IActivityTaskManager$Stub是binder服务端的类,运行在system进程的

在这里插入图片描述

Proxy对象是运行在我们app进程的,称之为binder代理

Proxy对象通过transact方法调用到Stub对象的onTransact方法。这个异常发生在App进程,所以代码捕获到了这个异常。

2.监测异常,提前上报

大概500k就是危险的极限大小,容易发生异常。

hook拦截startActivity的方法,到达一个危险的大小容易崩溃的时候,我们就上报。

android.os.BinderProxy.transact(BinderProxy.java:510)

这里面Parcel有个dataSize记录数据的大小

Hook IActivityTaskManager

看日志调用流程,在startActivity时候就可以拦截数据,Instrumentation.execStartActivity

Caused by: android.os.TransactionTooLargeException: data parcel size 1049052 bytes
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(BinderProxy.java:510)
at android.app.IActivityTaskManager$Stub$Proxy.startActivity(IActivityTaskManager.java:3823)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1705)
at android.app.Activity.startActivityForResult(Activity.java:5173) 

在这里插入图片描述

在这里插入图片描述

ActivityTaskManager对象

final IBinder b = ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE);
return IActivityTaskManager.Stub.asInterface(b);

在这里插入图片描述

sCache,是个静态的ArrayMap对象:

@UnsupportedAppUsage
private static Map<String, IBinder> sCache = new ArrayMap<String, IBinder>();

反射换掉IActivityTaskManager对象的IBinder对象,IBinder有监控的transact方法。

invoke方法中,关注transact方法获得跨进程传输的参数的大小

IBinde的transact方法:Parcel data

public boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags)
        throws RemoteException;
/**
 * Returns the total amount of data contained in the parcel.
 */
public final int dataSize() {
    return nativeDataSize(mNativePtr);
}

完整Demo

public static void hook() {
        Log.e(TAG, "hook: ");
        try {
            Class serviceManager = Class.forName("android.os.ServiceManager");
            Method getServiceMethod = serviceManager.getMethod("getService", String.class);

            Field sCacheField = serviceManager.getDeclaredField("sCache");
            sCacheField.setAccessible(true);
            Map<String, IBinder> sCache = (Map<String, IBinder>) sCacheField.get(null);
            Map<String, IBinder> sNewCache;
                sNewCache = new ArrayMap<>();
                sNewCache.putAll(sCache);
            IBinder activityTaskRemoteBinder = (IBinder) getServiceMethod.invoke(null, "activity_task");

            sNewCache.put("activity_task", (IBinder) Proxy.newProxyInstance(serviceManager.getClassLoader(),
                    new Class[]{IBinder.class},
                    new InvocationHandler() {
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            Log.e(TAG, "activity_task method = " + method.getName() + ", args = " + Arrays.toString(args));
                            if ("transact".equals(method.getName())) {
                                if (args != null && args.length > 1) {
                                    Object arg = args[1];
                                    if (arg instanceof Parcel) {
                                        Parcel parcelArg = (Parcel) arg;
                                        int dataSize = parcelArg.dataSize();
                                        if (dataSize > 300 * 1024) {
                                            // TODO 报警
                                            Log.e(TAG, Log.getStackTraceString(new RuntimeException("[error]TransactionTooLargeException:  300Kb:" + dataSize)));
                                            if (BuildConfig.DEBUG) {
                                                if (dataSize > 512 * 1024) {
                                                    throw new RuntimeException("[error]TransactionTooLargeException:300Kb:" + dataSize);
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                            return method.invoke(activityTaskRemoteBinder, args);
                        }
                    }));
            sCacheField.set(null, sNewCache);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

测试

在这里插入图片描述

从日志看出我们是hook系统服务成功了。

总结:

  1. binder数据量过大崩溃,Java异常捕获机制捕获不到,提前拦截。
  2. DEBUG,超过512k,崩溃可以看到日志;
  3. ServiceManager中的sCache获取到原来activity_task对应的IBinder实例对象; IBinder是接口,通过动态代理创造一个IBinder的代理对象IBinderProxy; 把IBinderProxy放到ServiceManager的sCache,Application attachBaseContext中调用Hook方法;

在这里插入图片描述

扩展:Hook AMS绕过Manifest的Activity注册检测

思路,先启动一个注册的代理的Activity,然后绕过manifest的检测后,把代理的Activity替换成未注册的

package com.example.myapplication;

import android.content.Intent;
import android.os.Handler;
import android.os.Message;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;

public class HookUtil {

    private static final String TARGET_INTENT = "target_intent";

    // 使用代理的Activity替换需要启动的未注册的Activity
    public static void hookAMS() {
        try {
            Class<?> clazz = Class.forName("android.app.ActivityTaskManager");
            Field singletonField = clazz.getDeclaredField("IActivityTaskManagerSingleton");

            singletonField.setAccessible(true);
            Object singleton = singletonField.get(null);

            Class<?> singletonClass = Class.forName("android.util.Singleton");
            Field mInstanceField = singletonClass.getDeclaredField("mInstance");
            mInstanceField.setAccessible(true);
            Method getMethod = singletonClass.getMethod("get");
            Object mInstance = getMethod.invoke(singleton);

            Class IActivityTaskManagerClass = Class.forName("android.app.IActivityTaskManager");

            Object mInstanceProxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                    new Class[]{IActivityTaskManagerClass}, new InvocationHandler() {
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                            if ("startActivity".equals(method.getName())) {
                                int index = -1;

                                // 获取 Intent 参数在 args 数组中的index值
                                for (int i = 0; i < args.length; i++) {
                                    if (args[i] instanceof Intent) {
                                        index = i;
                                        break;
                                    }
                                }
                                // 生成代理proxyIntent
                                Intent proxyIntent = new Intent();
                                proxyIntent.setClassName("com.example.myapplication",
                                        ProxyActivity.class.getName());

                                // 保存原始的Intent对象
                                Intent intent = (Intent) args[index];
                                proxyIntent.putExtra(TARGET_INTENT, intent);

                                // 使用proxyIntent替换数组中的Intent
                                args[index] = proxyIntent;
                            }

                            // 被代理对象调用
                            return method.invoke(mInstance, args);
                        }
                    });

            // 用代理的对象替换系统的对象
            mInstanceField.set(singleton, mInstanceProxy);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 需要启动的未注册的Activity 替换回来  ProxyActivity
    public static void hookHandler() {
        try {
            Class<?> clazz = Class.forName("android.app.ActivityThread");

            Field activityThreadField = clazz.getDeclaredField("sCurrentActivityThread");
            activityThreadField.setAccessible(true);
            Object activityThread = activityThreadField.get(null);

            Field mHField = clazz.getDeclaredField("mH");
            mHField.setAccessible(true);
            final Handler mH = (Handler) mHField.get(activityThread);

            Field mCallbackField = Handler.class.getDeclaredField("mCallback");
            mCallbackField.setAccessible(true);

            mCallbackField.set(mH, new Handler.Callback() {

                @Override
                public boolean handleMessage(Message msg) {
                    switch (msg.what) {
                        case 159:
                            try {
                                Field mActivityCallbacksField = msg.obj.getClass()
                                        .getDeclaredField("mActivityCallbacks");
                                mActivityCallbacksField.setAccessible(true);
                                List mActivityCallbacks = (List) mActivityCallbacksField.get(msg.obj);

                                for (int i = 0; i < mActivityCallbacks.size(); i++) {
                                    if (mActivityCallbacks.get(i).getClass().getName()
                                            .equals("android.app.servertransaction.LaunchActivityItem")) {
                                        Object launchActivityItem = mActivityCallbacks.get(i);
                                        Field mIntentField = launchActivityItem.getClass()
                                                .getDeclaredField("mIntent");
                                        mIntentField.setAccessible(true);
                                        Intent proxyIntent = (Intent) mIntentField.get(launchActivityItem);

                                        Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT);
                                        if (intent != null) {
                                            mIntentField.set(launchActivityItem, intent);
                                        }
                                    }
                                }
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                            break;
                    }
                    return false;
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

}

补充:共享内存传递大数据,解决常规做法报异常TransactionTooLargeException

1.用putBinder方法传递数据

intent通过binder传递bitmap大数据

1.1客户端

public class MainActivity extends AppCompatActivity {
    private ITest binder;
    private Button mBtn;
    private Bitmap bitmap;
    private Button btnTest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btnTest = findViewById(R.id.btn_test);
        btnTest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                ComponentName componentName = new ComponentName("com.example.myapplication", "com.example.myapplication.ImageActivity");
                intent.setComponent(componentName);
                Bundle bundle = new Bundle();
                bundle.putBinder("imageData", new ITest.Stub() {
                    @Override
                    public void request() throws RemoteException {

                    }

                    @Override
                    public Bitmap getBitmap() throws RemoteException {
                        return bitmap;
                    }
                });
                intent.putExtras(bundle);
                startActivity(intent);
            }
        });

        startBinding();
        mBtn = findViewById(R.id.btn);
        mBtn.setOnClickListener(v -> {
            Bundle bundle = new Bundle();
            bundle.putByteArray("binder_data", new byte[2]);
            Intent intent = new Intent();
            intent.putExtras(bundle);
            ComponentName componentName = new ComponentName("com.example.myapplication", "com.example.myapplication.MainActivity");
            intent.setComponent(componentName);
            startActivity(intent);
        });
        bitmap = getBitmap();
    }

    private Bitmap getBitmap() {
        Drawable drawable = getResources().getDrawable(R.drawable.test);
        if (drawable instanceof BitmapDrawable) {
            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
            if (bitmapDrawable.getBitmap() != null) {
                return bitmapDrawable.getBitmap();
            }
        }
        return null;
    }

    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            binder = ITest.Stub.asInterface(service);
            try {
                binder.request();
            } catch (RemoteException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            binder = null;
        }
    };

    private void startBinding() {
        Intent intent = new Intent();
        ComponentName componentName = new ComponentName("com.example.myapplication", "com.example.myapplication.MyService");
        intent.setComponent(componentName);
        bindService(intent, connection, BIND_AUTO_CREATE);


    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(connection);
    }
}

1.2服务端

public class ImageActivity extends AppCompatActivity {
    private Button btnShow;
    private ImageView ivShow;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_image);
        btnShow = findViewById(R.id.btn_show);
        ivShow = findViewById(R.id.iv_show);

        btnShow.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                Intent intent = getIntent();
                Bundle bundle = intent.getExtras();
                IBinder iBinder = bundle.getBinder("imageData");
                ITest iTest = ITest.Stub.asInterface(iBinder);
                try {
                    Bitmap bitmap = iTest.getBitmap();
                    if (bitmap != null) {
                        ivShow.setImageBitmap(bitmap);
                    }
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

1.3ITest.aidl

package com.qfh.test;

interface ITest {
    void request();
    Bitmap getBitmap();
}

资源图片,最终测试没有崩溃在这里插入图片描述

2.putbinder的原理,为什么可以传递大图片?

如果直接传递大文件会报错

Caused by: android.os.TransactionTooLargeException: data parcel size 13131323121 bytes
        at android.os.BinderProxy.transactNative(Native Method)
        at android.os.BinderProxy.transact(BinderProxy.java:535)
        at android.app.IActivityTaskManager$Stub$Proxy.startActivity(IActivityTaskManager.java:3904)
        at android.app.Instrumentation.execStartActivity(Instrumentation.java:1738)

这是个native方法
在这里插入图片描述
最终会调用到这里

frameworks/base/core/jni/android_util_Binder.cpp
在这里插入图片描述
android_os_BinderProxy_transact,进行跨进程数据传输
在这里插入图片描述
android_os_BinderProxy_transact,signalExceptionForError处理异常的方法,parcelSize大于200K就会报错
在这里插入图片描述

但是不一定报错200k,因为我们前面的测试大概是500-1m的一个区间报错

android_os_BinderProxy_transact

status_t err = target->transact(code, *data, reply, flags);

在这里插入图片描述

2.1 ProcessState.cpp

这个类主要是打开binder驱动

(1 * 1024 * 1024) - (4096 *2)
在这里插入图片描述

//初始变量
ProcessState::ProcessState(const char* driver)
      : mDriverName(String8(driver)),
        mDriverFD(-1),
        mVMStart(MAP_FAILED),
        ......
        mMaxThreads(DEFAULT_MAX_BINDER_THREADS),
        mStarvationStartTimeMs(0),
        mThreadPoolStarted(false),
        mThreadPoolSeq(1),
        mCallRestriction(CallRestriction::NONE) {
    ......
    //打开驱动
    base::Result<int> opened = open_driver(driver);
    if (opened.ok()) {
        //映射内存大小8k-1m,Binder内存限制,BINDER_VM_SIZE = 1M-8kb
        mVMStart = mmap(nullptr, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE,
                        opened.value(), 0);
    }
}

2.2 binder_mmap

在这里插入图片描述
映射空间最多4M,binder_alloc_mmap_handler,vma用于分配绑定缓冲区

2.3 binder_alloc_mmap_handler

在这里插入图片描述
buffer_size最大4m,也就是缓冲区的大小,异步事务的空闲缓冲区大小最大2M

alloc->free_async_space = alloc->buffer_size / 2;
所以说binder驱动给每一个进程最多4m的内存空间,内核是8k-1m,用户空间则有3m,异步事务缓冲区空间等于buffer_size/2

3.为什么intent放入Bitmap就不行

3.1 Bundle写入到Parcel

在这里插入图片描述

3.2 执行bundle的writeToParcel

在这里插入图片描述
在这里插入图片描述

restoreAllowFds指的是是否允许携带文件描述符

Intent prepareToLeaveProcess 调用了Bundle#setAllowFds(false)表示不使用文件描述符fd

3.3 writeArrayMapInternal

传递是图片的话会被序列化,所以调这个if else,val.writeToParcel(this, 0);后面的0表示禁止携带fd文件描述符
在这里插入图片描述
传入的是BitMap,所以是走BitMap的writeToParcel方法
在这里插入图片描述
最终走native层的代码
在这里插入图片描述
android::Parcel* p = android::parcelForJavaObject(env, parcel);
在这里插入图片描述
匿名共享内存AshmemFd大于等于0 && bitmap不可变 && parcel允许带Fd,fd写入到parcel中
在这里插入图片描述

3.4 android::Parcel::WritableBlob blob

frameworks/native/libs/binder/Parcel.cpp

 //不允许带FD 数据小于等于16k,图片写入parcel,所以默认intent传递大数据是走这个分支禁用掉了文件描述符以及限制了16k的大小
if (!mAllowFds || len <= BLOB_INPLACE_LIMIT) {
        ALOGV("writeBlob: write in place");
        status = writeInt32(BLOB_INPLACE);
        if (status) return status;

        void* ptr = writeInplace(len);
        if (!ptr) return NO_MEMORY;

        outBlob->init(-1, ptr, len, false);
        return NO_ERROR;
    }
//不满足条件  允许Fd && len > 16k,创建ashmem返回文件描述符FD
 int fd = ashmem_create_region("Parcel Blob", len);
 //ashmem 可读可写
    int result = ashmem_set_prot_region(fd, PROT_READ | PROT_WRITE);
//fd,映射 len大小 mmap的空间
         void* ptr = ::mmap(nullptr, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
//fd写入到parcel中
           status = writeFileDescriptor(fd, true /*takeOwnership*/);

3.5 Parcel的writeStrongBinder

可以把一个Binder对象写入Parcel中
在这里插入图片描述
这个方法会把Binder对象组装到Parcel对象里面作为携带数据
在这里插入图片描述
putBinder来把IBinder对象写入到Parcel中

二.使用自定义共享内存代码实现传输大数据

使用AIDL

1. client

private void sendLargeData() {
        try {
            // 读取assets目录下文件
            InputStream inputStream = getAssets().open("large.jpg");

            // 将inputStream转换成字节数组
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
            byte[] byteArray = outputStream.toByteArray();
            MemoryFile memoryFile = new MemoryFile("image", byteArray.length);
            memoryFile.writeBytes(byteArray, 0, 0, byteArray.length);
            FileDescriptor fileDescriptor = (FileDescriptor) invoke("android.os.MemoryFile", memoryFile, "getFileDescriptor");
            ParcelFileDescriptor dup = ParcelFileDescriptor.dup(fileDescriptor);
            binder.client2server(dup);
            inputStream.close();
            outputStream.close();
        } catch (IOException | RemoteException e) {
            throw new RuntimeException(e);
        }

    }

public static Object invoke(String className, Object instance, String methodName, Object... params) {
        try {
            Class<?> c = Class.forName(className);
            if (params != null) {
                int plength = params.length;
                Class[] paramsTypes = new Class[plength];
                for (int i = 0; i < plength; i++) {
                    paramsTypes[i] = params[i].getClass();
                }
                Method method = c.getDeclaredMethod(methodName, paramsTypes);
                method.setAccessible(true);
                return method.invoke(instance, params);
            }
            Method method = c.getDeclaredMethod(methodName);
            method.setAccessible(true);
            return method.invoke(instance);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

2.server

@Override
        public void client2server(ParcelFileDescriptor pfd) throws RemoteException {
            try {
                FileDescriptor fileDescriptor = pfd.getFileDescriptor();

                // 根据FileDescriptor构建InputStream对象
                InputStream fis = new FileInputStream(fileDescriptor);

                // 创建字节数组输出流用于读取数据
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                byte[] buffer = new byte[1024];
                int bytesRead;
                while ((bytesRead = fis.read(buffer)) != -1) {
                    baos.write(buffer, 0, bytesRead);
                }
                // 从InputStream中读取字节数组
                byte[] data = baos.toByteArray();
                Log.e(TAG, "client2server: " + data.length);
                fis.close();
                baos.close();
            } catch (Exception e) {

            }


        }

运行测试
在这里插入图片描述
发现应用程序没有崩溃,并且服务端接收到了客户端穿过来的字节数组,

;