利用Epic实现ARTHook检测大图

1,478 阅读3分钟

Epic是一个框架,可实现基于ART虚拟机的hook方案,最近在网上看到不少的博客介绍用其进行大图检测,于是便想尝试一下。结果发现很多博客上的方案实施起来或多或少有些问题,导致不能正常运行。本文记录自己的操作步骤。

我们来看Epic在github的说明, 其readme中写的是添加入下依赖

dependencies {
    compile 'com.github.tiann:epic:0.11.2'
}

直接使用示例

DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {
    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        Thread thread = (Thread) param.thisObject;
        Class<?> clazz = thread.getClass();
        if (clazz != Thread.class) {
            Log.d(TAG, "found class extend Thread:" + clazz);
            DexposedBridge.findAndHookMethod(clazz, "run", new ThreadMethodHook());
        }
        Log.d(TAG, "Thread: " + thread.getName() + " class:" + thread.getClass() +  " is created.");
    }
});
DexposedBridge.findAndHookMethod(Thread.class, "run", new ThreadMethodHook());

按照官方的说明,我们来操作发现 DexposedBridge这个类都找不到。去论坛里面找,发现也有人碰到这个问题,作者应该是很久没有维护了。

image.png 于是我直接将代码库下载下来,将其中的library目录下的代码作为一个子module添加到项目中。记录一下自己的操作步骤:

操作

1. 集成library目录

将Epic工程中的library目录下的代码作为一个子module添加到项目中, 这里需要注意以下几点:

  1. build.gradle中的部份代码可以注释掉(这是用来打包上传aar的):
apply plugin: 'com.github.panpf.bintray-publish'

publish {
    userOrg = 'twsxtd'//
    groupId = 'me.weishu'
    artifactId = 'epic'
    publishVersion = '0.11.1'
    desc = 'Android Java AOP Method Hook (Dexposed on ART)'
    website = 'https://github.com/tiann/epic'
}
  1. 需要配置NDk的环境,我这边是之前已经配置好了,所以这一步省了
  2. Offset.java中的Build.VERSION_CODES.S编译报错,可直接将其改成 31. 需要注意Epic官方提到不支持android 12, 此处改成31是为了编译通过。

2.使用

直接在Application的onCreate方法中添加如下代码就可以了。

DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new ImageHook());

错误方案

网上的不少博客上是这样使用

DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class,
                new ImageHook());
    }
})

其中的ImageHook实例是我们具体的hook处理策略,可以先放一下,后面会把代码贴上。这个方案首先会hook掉ImageView的构造方法,它会造成 afterHookedMethod方法回调多次,然后下面的代码也会执行多次。

DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class,new ImageHook());

所带来的影响就是,当我们调用一次ImageView的setImageBitmap方法时,ImageHook中的处理方法也就会执行多次。

其根本原因在于,我们虽然在xml中写了一个ImageView, 但其在初始化时,会先调用

public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)

然后该方法又会去调用

public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
        int defStyleRes)

所以用 DexposedBridge.hookAllConstructors时,它会回调多次。

3. 业务调用

在测试的页面TestActivity.java的布局文件中添加一个ImageView

<ImageView
    android:id="@+id/iv_test"
    android:layout_width="120dp"
    android:layout_height="120dp"
    />

为其设置bitmap

binding.ivTest.setImageBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.splash_bg));

测试的时候会发现当我们调用了setImageBitmap时,对应的hook方法便会执行。

ImageHook类的源码

通过比较bitmap和ImageView的尺寸来判断图片是否合规。这个是直接用的网上的方案

public class ImageHook extends XC_MethodHook {

    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        ImageView imageView = (ImageView)param.thisObject;
        checkBitmap(imageView, imageView.getDrawable());
    }

    private void checkBitmap(ImageView imageView, Drawable drawable) {
        if (drawable instanceof BitmapDrawable) {
            final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
            if (bitmap != null) {
                int width = imageView.getWidth();
                int height = imageView.getHeight();
                if (width > 0 && height > 0) {
                    if (bitmap.getWidth()>= width * 2 && bitmap.getHeight() >= height * 2) {
                        warn(bitmap.getWidth(), bitmap.getHeight(), width, height, new RuntimeException("Bitmap size too large"));
                    }
                    //宽高为0
                } else {
                    final Throwable exception = new RuntimeException("Bitmap size too large");
                    imageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                        @Override
                        public boolean onPreDraw() {
                            int width = imageView.getWidth();
                            int height = imageView.getHeight();
                            if (width > 0 && height > 0) {
                                if (bitmap.getWidth()>= width * 2 && bitmap.getHeight() >= height * 2) {
                                    warn(bitmap.getWidth(), bitmap.getHeight(), width, height, exception);
                                }
                                imageView.getViewTreeObserver().removeOnPreDrawListener(this);
                            }
                            return true;
                        }
                    });
                }
            }
        }
    }
    private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
        String warnInfo = "Bitmap size too large: real size: ( " + bitmapWidth + " * " + bitmapHeight
                + "), desire size:" + viewWidth + " x " + viewHeight + "\n call stackTrace:" + formatStackTrace(t);
        Log.e("ImageHook", warnInfo);

    }

    private static String formatStackTrace(Throwable t) {
        StringBuffer sbf = new StringBuffer();
        for (StackTraceElement e: t.getStackTrace()) {
            sbf.append(e.toString() + "\n");
        }
        return sbf.toString();
    }
}

其它

  1. Epic是基于 dexposed作了一些改动。 dexposed 只支持Dalvik虚拟机, 也就是5.0以下的android系统。Epic支持android 5.0 - 11。
  2. 这种方案也可以用Frida,aspectjx去写。其实我们在用图片加载器的时候,比如glide就已经会去根据ImageView容器的大小decode出合适的bitmap, 所以这种方案其实收益还是有限的。