Android隐私API访问监控插件

背景

随着对用户我的信息保护的愈发重视,相关政策也呼之欲出。例如 “禁止在用户赞成隐私政策前,访问用户我的信息”。java

目前应用商店经过在系统层,监控app运行过程当中对api的访问。咱们的APP,对于应用商店来讲是黑盒,因此在系统层监控是恰当的。android

而咱们的APP对咱们来讲是白盒,咱们能够有更多方式实现监控,甚至“篡改”。git

访问监控方案

只要是.class,就均可以aop。 咱们编写gradle插件,利用javassist修改class文件。github

例如这段代码apache

TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
String subscriberId = telephonyManager.getSubscriberId();
复制代码

增长log

咱们能够把它修改为api

TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
//插入log代码
Log.d("alvin",Log.getStackTraceString(new Throwable("android.telephony.TelephonyManager.getSubscriberId")));
String subscriberId = telephonyManager.getSubscriberId();
复制代码

一旦调用了这段代码,就会打印相似堆栈logmarkdown

java.lang.Throwable: android.telephony.TelephonyManager.getSubscriberId
        at com.ta.utdid2.a.a.d.getImsi(SourceFile:87)
        at com.ta.utdid2.device.b.a(SourceFile:50)
        at com.ta.utdid2.device.b.b(SourceFile:72)
        at com.ta.utdid2.device.UTDevice.a(SourceFile:50)
        at com.ta.utdid2.device.UTDevice.getUtdid(SourceFile:14)
        at com.ut.device.UTDevice.getUtdid(SourceFile:19)
        at com.alibaba.sdk.android.push.impl.j.a(Unknown Source:10)
        at com.alibaba.sdk.android.push.impl.j.register(Unknown Source:58)
        at com.a.push.service.PushServiceImpl.initPushService(PushServiceImpl.java:59)
        at com.a.BaseApplication.initPushService(BaseApplication.java:465)
复制代码

proxy methodCall

TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
//String subscriberId = telephonyManager.getSubscriberId();
//替换成:
String subscriberId = (String) MainApp.privacyVisitProxy("android.telephony.TelephonyManager", "getSubscriberId", telephonyManager,new Class[0], new Object[0]);   
复制代码

咱们代理了系统api访问,就能够本身操控了。app

过程实现

代码在这 Github PrivacyChecker框架

咱们大概讲讲步骤和核心代码maven

建立gradle插件

若是建立gradle插件 可参考 Gradle系列一 -- Groovy、Gradle和自定义Gradle插件

插件编写参考了美团的热修复框架 Robust

使用javassist修改class文件 Javassist 使用指南

这里咱们用 buildSrc方式。

build.gradle

plugins {
    id 'groovy'
}
repositories {
    jcenter()
    google()
    mavenCentral()
}
dependencies {
    implementation gradleApi() //gradle sdk
    implementation localGroovy() //groovy sdk
    compile group: 'org.smali', name: 'dexlib2', version: '2.2.4'
    implementation 'com.android.tools.build:gradle:3.6.1'
    implementation 'org.javassist:javassist:3.20.0-GA'

}
sourceSets {
    main {
        groovy {
            srcDir 'src/main/groovy'
        }

        java {
            srcDir "src/main/java"
        }
        resources {
            srcDir 'src/main/resources'
        }
    }
}
复制代码

PrivacyCheckTransform

PrivacyCheckPlugin.groovy文件

class PrivacyCheckPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        println "this is my custom plugin PrivacyCheckPlugin"
        project.android.registerTransform(new PrivacyCheckTransformRob(project))
    }
}
复制代码

PrivacyCheckTransformRob.groovy文件

class PrivacyCheckTransformRob extends Transform {

    ClassPool classPool = ClassPool.default
    Project project

    @Override
    void transform(TransformInvocation transformInvocation) throws Exception {
        super.transform(transformInvocation)
        println "----------Privacy check transform start----------"
        project.android.bootClasspath.each {
            classPool.appendClassPath(it.absolutePath)
        }
        //1.全部的class通过修改后聚集到这个jar文件中
        File jarFile = generateAllClassOutJarFile(transformInvocation)
        //2.聚集全部class,包括咱们编写的java代码和第三方jar中的class
        def ctClasses = ConvertUtils.toCtClasses(transformInvocation.inputs, classPool)
        //3.注入并打包进jarFile  (*核心)
        PrivacyCheckRob.insertCode(ctClasses, jarFile)
        println "----------Privacy check transform end----------"
    }

    private File generateAllClassOutJarFile(TransformInvocation transformInvocation) {
        File jarFile = transformInvocation.outputProvider.getContentLocation(
                "main", getOutputTypes(), getScopes(), Format.JAR);
        println("jarFile:" + jarFile.absolutePath)
        if (!jarFile.getParentFile().exists())  jarFile.getParentFile().mkdirs();
        if (jarFile.exists())  jarFile.delete();
        return jarFile
    }
}
复制代码

聚集全部class

ConvertUtils.groovy

class ConvertUtils {
//遍历全部input:directoryInputs 和 jarInput
    static List<CtClass> toCtClasses(Collection<TransformInput> inputs, ClassPool classPool) {
        List<String> classNames = new ArrayList<>()
        List<CtClass> allClass = new ArrayList<>();
        def startTime = System.currentTimeMillis()
        inputs.each {
            it.directoryInputs.each {
                println("directory input:"+it.file.absolutePath)
                def dirPath = it.file.absolutePath
                classPool.insertClassPath(it.file.absolutePath)
                org.apache.commons.io.FileUtils.listFiles(it.file, null, true).each {
                    if (it.absolutePath.endsWith(SdkConstants.DOT_CLASS)) {
                        def className = it.absolutePath.substring(dirPath.length() + 1, it.absolutePath.length() - SdkConstants.DOT_CLASS.length()).replaceAll(Matcher.quoteReplacement(File.separator), '.')
                         //META-INF.versions.9.module-info问题解决,参考  https://github.com/Meituan-Dianping/Robust/issues/447
                        if (!"META-INF.versions.9.module-info".equals(className)) {
                            if (classNames.contains(className)) {
                                throw new RuntimeException("You have duplicate classes with the same name : " + className + " please remove duplicate classes ")
                            }
                            classNames.add(className)
                        }
                    }
                }
            }

            it.jarInputs.each {
                println("jar input:"+it.file.absolutePath)
                classPool.insertClassPath(it.file.absolutePath)
                def jarFile = new JarFile(it.file)
                Enumeration<JarEntry> classes = jarFile.entries();
                while (classes.hasMoreElements()) {
                    JarEntry libClass = classes.nextElement();
                    String className = libClass.getName();
                    if (className.endsWith(SdkConstants.DOT_CLASS)) {
                        className = className.substring(0, className.length() - SdkConstants.DOT_CLASS.length()).replaceAll('/', '.')
                       
                        if (!"META-INF.versions.9.module-info".equals(className)) {
                            if (classNames.contains(className)) {
                                throw new RuntimeException("You have duplicate classes with the same name : " + className + " please remove duplicate classes ")
                            }
                            classNames.add(className)
                        }
                    }
                }
            }
        }
        def cost = (System.currentTimeMillis() - startTime) / 1000
        println "read all class file cost $cost second"
        classNames.each { allClass.add(classPool.get(it)) }
       ...
        return allClass;
    }
}
复制代码

注入hook代码

PrivacyCheckRob.java

public class PrivacyCheckRob {

    public static void insertCode(List<CtClass> ctClasses, File jarFile) throws Exception {
        long startTime = System.currentTimeMillis();
        ZipOutputStream outStream = new JarOutputStream(new FileOutputStream(jarFile));
        for (CtClass ctClass : ctClasses) {
            if (ctClass.isFrozen()) ctClass.defrost();
            if (!ctClass.isFrozen()&&!ctClass.getName().equals("com.a.privacychecker.MainApp")) {
                for (CtMethod ctMethod : ctClass.getDeclaredMethods()) {
                    ctMethod.instrument(new ExprEditor() {
                        @Override
                        public void edit(MethodCall m) throws CannotCompileException {
                            String mLongName = m.getClassName() + "." + m.getMethodName();
                            if (PrivacyConstants.privacySet.contains(mLongName)) {
                                systemOutPrintln(mLongName,m,ctMethod);
//                                InjectAddLog.execute(m);
//                                InjectHookReturnValue.execute(m);
                                InjectMethodProxy.execute(m);
                            }
                        }
                        private  void systemOutPrintln(String mLongName, MethodCall m,CtMethod ctMethod) {
                            StringBuilder sb = new StringBuilder();
                            sb.append("\n========");
                            sb.append("\ncall: " + mLongName);
                            sb.append("\n  at: " + ctMethod.getLongName() + "(" + ctMethod.getDeclaringClass().getSimpleName() + ".java:" + m.getLineNumber() + ")");
                            System.out.println(sb.toString());
                        }
                    });
                }

            }
            zipFile(ctClass.toBytecode(), outStream, ctClass.getName().replaceAll("\\.", "/") + ".class");
        }
        outStream.close();
        float cost = (System.currentTimeMillis() - startTime) / 1000.0f;
        System.out.println("insertCode cost " + cost + " second");

    }

    public static void zipFile(byte[] classBytesArray, ZipOutputStream zos, String entryName) {
        try {
            ZipEntry entry = new ZipEntry(entryName);
            zos.putNextEntry(entry);
            zos.write(classBytesArray, 0, classBytesArray.length);
            zos.closeEntry();
            zos.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
复制代码
public class InjectMethodProxy {

    public static void execute(MethodCall m) throws CannotCompileException {
        System.out.println(m.getSignature());
//        System.out.println(Arrays.toString(Desc.getParams(m.getSignature())));
        String replace = "{  $_ =($r)( com.a.privacychecker.MainApp.privacyVisitProxy(\""+ m.getClassName()+"\",\""+m.getMethodName()+"\", $0,$sig, $args)); }";
        m.replace(replace);


    }
}
复制代码

主工程代码

记得要添加依赖

dependencies {
    api 'org.javassist:javassist:3.22.0-GA'
}
复制代码
public class MainApp extends Application {

    public static boolean allowVisit = false;

    @Override
    public void onCreate() {
        super.onCreate();
    }

//实际hook代码调用处,实际有删减,能够到github查看
    public static Object privacyVisitProxy(String clzName, String methodName, Object obj, Class[] paramsClasses, Object[] paramsValues) {
        if (allowVisit) {
        //若是容许访问,能够反射,也可根据参数主动调用api访问
            return obj == null ? RefInvoke.invokeStaticMethod(clzName, methodName, paramsClasses, paramsValues)
                    : RefInvoke.invokeInstanceMethod(obj, methodName, paramsClasses, paramsValues);
        } else {
            String mLongName = clzName + "." + methodName;
            if (mLongName.equals(PrivacyConstants.Privacy_getSubscriberId)) {
                return "invalid_SubscriberId";
            } else if (mLongName.equals(PrivacyConstants.Privacy_getDeviceId)) {
                return "invalid_deviceId";
            } else if (mLongName.equals(PrivacyConstants.Privacy_getSSID)) {
                return "<unknown ssid>";
            } else if (mLongName.equals(PrivacyConstants.Privacy_getMacAddress)) {
                return "02:00:00:00:00:00";
            } else {
                return null;
            }
        }
    }

}
复制代码

结语

到此为止就结束了。其实就是利用javassist hook代码。