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代碼。

相關文章
相關標籤/搜索