background

With the increasing attention to the protection of users’ personal information, relevant policies are also coming out. For example, “Access to the user’s personal information is prohibited until the user agrees to the privacy policy.”

Currently, the app store monitors API access during app running at the system level. Our APP is a black box to the APP store, so monitoring at the system level is appropriate.

While our APP is a white box for us, we have more ways to monitor and even “tamper” with it.

Access Monitoring scheme

Any.class can be AOP. We wrote the Gradle plug-in and used JavAssist to modify the class file.

Take this code for example

TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
String subscriberId = telephonyManager.getSubscriberId();
Copy the code

Add the log

We can change it to be

TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); / / insert the log log code. D (" alvin, "the getStackTraceString (new Throwable (" android. Telephony. TelephonyManager. GetSubscriberId"))); String subscriberId = telephonyManager.getSubscriberId();Copy the code

Once this code is called, it prints something like a stack log

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)
Copy the code

proxy methodCall

TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); //String subscriberId = telephonyManager.getSubscriberId(); // Replace with:  String subscriberId = (String) MainApp.privacyVisitProxy("android.telephony.TelephonyManager", "getSubscriberId", telephonyManager,new Class[0], new Object[0]);Copy the code

We proxy system API access, so we can control ourselves.

Process implementation

The code is here Github PrivacyChecker

Let’s go through the steps and the core code

Create the Gradle plug-in

To create gradle plugins, see gradle series 1 – Groovy, Gradle and Custom Gradle plugins

The plug-in was developed by referring to meituan’s Robust thermal repair framework

Use Javassist to modify the class file Javassist usage guide

We’re going to use 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. View the 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' } } }Copy the code

PrivacyCheckTransform

PrivacyCheckPlugin. Groovy files

class PrivacyCheckPlugin implements Plugin<Project> { @Override void apply(Project project) { println "this is my custom  plugin PrivacyCheckPlugin" project.android.registerTransform(new PrivacyCheckTransformRob(project)) } }Copy the code

PrivacyCheckTransformRob. Groovy files

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. All the class to the jar File after modified File jarFile = generateAllClassOutJarFile (transformInvocation) / / 2. Pooling all classes, Including our written in Java code and third-party jar class def ctClasses = ConvertUtils. ToCtClasses (transformInvocation inputs, classPool) / / 3. Injection and packed into a jarFile core (*) 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 } }Copy the code

Pooling all classes

ConvertUtils.groovy

Class ConvertUtils {// iterate over all 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. The module - info problem solving and reference 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; }}Copy the code

Inject hook code

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; 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(); }}}Copy the code
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);


    }
}
Copy the code

Main engineering code

Remember to add dependencies

Dependencies {API 'org. Javassist: javassist: 3.22.0 - GA'}Copy the code
public class MainApp extends Application { public static boolean allowVisit = false; @Override public void onCreate() { super.onCreate(); } // The actual hook code is called. Public Static Object privacyVisitProxy(String clzName, String methodName, Object obj, Class[] paramsClasses, Object[] paramsValues) {if (allowVisit) {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; }}}}Copy the code

conclusion

This is the end of it. That’s the Javassist Hook code.