背景
随着对用户个人信息保护的愈发重视,相关政策也呼之欲出。例如 “禁止在用户同意隐私政策前,访问用户个人信息”。
目前应用商店通过在系统层,监控app运行过程中对api的访问。我们的APP,对于应用商店来说是黑盒,所以在系统层监控是恰当的。
而我们的APP对我们来说是白盒,我们可以有更多方式实现监控,甚至“篡改”。
访问监控方案
只要是.class,就都可以aop。 我们编写gradle插件,利用javassist修改class文件。
例如这段代码
TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
String subscriberId = telephonyManager.getSubscriberId();
增加log
我们可以把它修改成
TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
//插入log代码
Log.d("alvin",Log.getStackTraceString(new Throwable("android.telephony.TelephonyManager.getSubscriberId")));
String subscriberId = telephonyManager.getSubscriberId();
一旦调用了这段代码,就会打印类似堆栈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)
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访问,就可以自己操控了。
过程实现
我们大概讲讲步骤和核心代码
创建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代码。