隨着對用戶我的信息保護的愈發重視,相關政策也呼之欲出。例如 「禁止在用戶贊成隱私政策前,訪問用戶我的信息」。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();
複製代碼
咱們能夠把它修改爲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)
複製代碼
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
咱們大概講講步驟和核心代碼maven
若是建立gradle插件 可參考 Gradle系列一 -- Groovy、Gradle和自定義Gradle插件
插件編寫參考了美團的熱修復框架 Robust
使用javassist修改class文件 Javassist 使用指南
這裏咱們用 buildSrc方式。
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'
}
}
}
複製代碼
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
}
}
複製代碼
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;
}
}
複製代碼
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代碼。