前言
- 之前兩篇文章我寫了入門篇:Gradle 插件 + ASM 實戰(zhàn)——入門篇和Gradle+ASM實戰(zhàn)——進階篇冤寿,對gradle+ASM不熟的大家可以去上篇文章查看
- ASM API文檔: javadoc
- ASM使用手冊: 英文版笆豁、 中文版
- github地址:https://github.com/Peakmain/AsmActualCombat
需求背景
- 第三方sdk會總是頻繁調(diào)用某些隱私方法锈至,比如MAC地址储狭,AndroidId等
- 現(xiàn)在想要的需求是嵌纲,比如調(diào)用設備id的時候,會調(diào)用telephoneManger方法的getDeviceId,如果我們能找到調(diào)用getDeviceId的方法姆坚,然后將其替換成我們自己的方法或者將方法體清空屯掖,問題不就解決了嘛
- 按程序員的本質(zhì),我本想去偷個懶纬傲,找個庫满败,也看過幾篇文章,但是都沒有達到自己的想要的,當前有關(guān)隱私方法調(diào)用或者隱私政策整改的文章叹括,有的也只是簡單的用別人的第三方如Epic算墨,AOP,而這些實際也達不到我們想要的效果汁雷,有的也只是說檢查隱私方法被那些方法調(diào)用
- 所以就有了這篇文章和實現(xiàn)的庫净嘀,希望可以幫助到大家,徹底解決第三方sdk頻繁調(diào)用隱私方法被通報或者下架的問題侠讯,也可供學習ASM哦挖藏。
- 通過 Gradle+ASM實戰(zhàn)——進階篇這篇文章我們知道我們實際只需要關(guān)注自己繼承的ClassVisitor即可
基礎知識
ClassVisitor
方法執(zhí)行的順序
我們直接看ClassVisitor的注解
-
[]
: 表示最多調(diào)用一次,可以不調(diào)用厢漩,但最多調(diào)用一次 -
()
和|
: 表示在多個方法之間膜眠,可以選擇任意一個,并且多個方法之間不分前后順序 -
*
: 表示方法可以調(diào)用0次或多次
我們主要關(guān)注以下幾個方法
visit
(visitField |visitMethod)*
visitEnd
四個方法
1溜嗜、visit方法,掃描類的時候會進入這里宵膨,最多被執(zhí)行一次
/**
* @param version 類版本 ASM4~ASM9可選
* @param access 修飾符 如public、static炸宵、final
* @param name 類名 如:com/peakmain/asm/utils/Utils
* @param signature 泛型信息
* @param superName 父類
* @param interfaces 實現(xiàn)的接口
*/
@Override
void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {}
2辟躏、visitField:訪問屬性的時候用到,用到不多焙压,用到的時候細說
@Override
FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
return super.visitField(access, name, descriptor, signature, value)
}
3鸿脓、visitMethod:掃描到方法的時候調(diào)用抑钟,這也是我們主要介紹的方法涯曲,細節(jié)下面介紹
/**
* 掃描類的方法進行調(diào)用
* @param access 修飾符
* @param name 方法名字
* @param descriptor 方法簽名
* @param signature 泛型信息
* @param exceptions 拋出的異常
* @return
*/
@Override
MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
return super.visitMethod(access, name, descriptor, signature, exceptions)
}
4、visitEnd:這是這些visitXxx()方法之后最后一個執(zhí)行的方法在塔,最多被調(diào)用一次
@Override
void visitEnd() {
super.visitEnd()
}
MethodVisitor
通過調(diào)用ClassVisitor類的visitMethod()方法幻件,會返回一個MethodVisitor類型的對象
Method
public abstract class MethodVisitor {
public void visitCode();
public void visitInsn(final int opcode);
public void visitIntInsn(final int opcode, final int operand);
public void visitVarInsn(final int opcode, final int var);
public void visitTypeInsn(final int opcode, final String type);
public void visitFieldInsn(final int opcode, final String owner, final String name, final String descriptor);
public void visitMethodInsn(final int opcode, final String owner, final String name, final String descriptor,
final boolean isInterface);
public void visitInvokeDynamicInsn(final String name, final String descriptor, final Handle bootstrapMethodHandle,
final Object... bootstrapMethodArguments);
public void visitJumpInsn(final int opcode, final Label label);
public void visitLabel(final Label label);
public void visitLdcInsn(final Object value);
public void visitIincInsn(final int var, final int increment);
public void visitTableSwitchInsn(final int min, final int max, final Label dflt, final Label... labels);
public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels);
public void visitMultiANewArrayInsn(final String descriptor, final int numDimensions);
public void visitTryCatchBlock(final Label start, final Label end, final Label handler, final String type);
public void visitMaxs(final int maxStack, final int maxLocals);
public void visitEnd();
}
假設我們有以下方法
public static String getMeid(Context context) {//方法體
TelephonyManager manager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return manager.getMeid();
}
return "getMeid";
}
visitXxxInsn負責的就是這個方法的方法體內(nèi)的內(nèi)容,也就是指{}這個里面包含的屬性蛔溃,方法
方法調(diào)用的順序
(visitParameter)*
[visitAnnotationDefault]
(visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation | visitTypeAnnotation | visitAttribute)*
[
visitCode
(
visitFrame |
visitXxxInsn |
visitLabel |
visitInsnAnnotation |
visitTryCatchBlock |
visitTryCatchAnnotation |
visitLocalVariable |
visitLocalVariableAnnotation |
visitLineNumber
)*
visitMaxs
]
visitEnd
分組
我們可以分成三組
- 第一組:visitCode方法之前的方法绰沥,主要負責parameter篱蝇、annotation和attributes等內(nèi)容。對于我們來說主要關(guān)注visitAnnotation即可
- 第二組:visitCode和visitMaxs方法之間的方法徽曲,這些之間的方法零截,主要負責方法的“方法體”內(nèi)的opcode內(nèi)容。visitCode代表方法體的開始秃臣,visitMaxs代表方法體的結(jié)束
- 第三組:visitEnd()方法涧衙,是最后一個進行調(diào)用的方法
注意點
我們需要注意的是:
- visitAnnotation:會被調(diào)用多次
- visitCode:只會被調(diào)用一次
- visitXxxInsn:可以調(diào)用多次,這些方法的調(diào)用奥此,就是在構(gòu)建方法的方法體
- visitMaxs:只會被調(diào)用一次
- visitEnd:只會被調(diào)用一次
AdviceAdapter
我們在項目中用了AdviceAdapter弧哎,那么AdviceAdapter的是什么呢?
AdviceAdapter實際是引入了兩個方法onMethodEnter()方法和onMethodExit()方法稚虎。并且這個類屬于MethodVisitor撤嫩,也就是我們要講的第三個方法
源碼分析
onMethodEnter
@Override
public void visitCode() {
super.visitCode();
if (isConstructor) {//判斷是否是構(gòu)造函數(shù)
stackFrame = new ArrayList<>();
forwardJumpStackFrames = new HashMap<>();
} else {
onMethodEnter();
}
}
實際還是調(diào)用了visitCode方法,只是處理了構(gòu)造函數(shù)(<init>())相關(guān)邏輯,如果直接使用visitCode()方法則可能導致<init>()方法出現(xiàn)錯誤
onMethodExit
[圖片上傳失敗...(image-f92fa5-1649898740300)]
我們會發(fā)現(xiàn)調(diào)用的方法是在visitInsn方法中蠢终,那肯定有人問序攘,為什么在visitInsn中而不是visitEnd里面呢?不是說它是最后一個方法調(diào)用寻拂。
假設我們有個方法是獲取AndroidId的
public static String getAndroidId(Context context) {
return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
}
這個方法現(xiàn)在的正常ASM應該是
mv.visitCode()
mv.visitxxxInsn()
mv.visitInsn(AReturn)
mv.visitMaxs()
mv.visitEnd()
這時候我們在visitEnd的時候添加或者visitMaxs添加两踏,因為前面已經(jīng)Return了,所以后面是不會執(zhí)行的
方法初始化Frame
- 在JVM Stack當中兜喻,是棧的結(jié)構(gòu)梦染,里面存儲的是frames;
- 每一個frame空間可以稱之為Stack Frame朴皆。
- 當調(diào)用一個新方法的時候帕识,就會在JVM Stack上分配一個frame空間
- 當方法退出時,相應的frame空間也會JVM Stack上進行清除掉(出棧操作)遂铡。
- 在frame空間當中肮疗,有兩個重要的結(jié)構(gòu),即local variables(局部變量表)和operand stack(操作數(shù)棧)
方法剛開始的時候扒接,操作數(shù)棧operand stack為空伪货,不需要存儲任何數(shù)據(jù),局部變量表需要考慮三個因素
- 當前方法是否為static方法钾怔。如果當前方法是non-static方法碱呼,則需要在local variables索引為0的位置存在一個this變量;如果當前方法是static方法宗侦,則不需要存儲this愚臀。
- 當前方法是否接收參數(shù)。方法接收的參數(shù)矾利,會按照參數(shù)的聲明順序放到local variables當中姑裂。
- 方法的參數(shù)是否包含long或double馋袜,如果參數(shù)是long或者double類型,那么它在local variables占用兩個位置
Type
在.java文件中,我們經(jīng)常使用java.lang.Class類舶斧;而在.class文件中欣鳖,需要經(jīng)常用到internal name、type descriptor和method descriptor茴厉;而在ASM中观堂,org.objectweb.asm.Type類就是幫助我們進行兩者之間的轉(zhuǎn)換。
獲取Type的幾個方式
Type類有一個private的構(gòu)造方法呀忧,因此Type對象實例不能通過new關(guān)鍵字來創(chuàng)建师痕。但是,Type類提供了static method和static field來獲取對象而账。
- 方式一:java.lang.class
Type type=Type.getType(String.class)
- 方式二:descriptor
Type type = Type.getType("Ljava/lang/String;");
- 方式三:internal name
Type type = Type.getObjectType("java/lang/String");
- 方式四:static field
Type type = Type.INT_TYPE;
常用的幾個方法
- getArgumentTypes()方法胰坟,用于獲取“方法”接收的參數(shù)類型
- getReturnType()方法,用于獲取“方法”返回值的類型
- getSize()方法泞辐,用于返回某一個類型所占用的slot空間的大小
- getArgumentsAndReturnSizes()方法笔横,用于返回方法所對應的slot空間的大小
實戰(zhàn)
上面的基礎知識大家學完了,那么就可以開始實戰(zhàn)了咐吼。下面所有的實戰(zhàn)都是繼承AdviceAdapter
實戰(zhàn)一:監(jiān)控方法的耗時時間
- 假設有以下代碼:
public String getMethodTime(long var1) {
try {
Thread.sleep(1000L);
} catch (InterruptedException var4) {
var4.printStackTrace();
}
return "getMethod";
}
目標
通過注解來監(jiān)控獲取該方法的耗時時間,代碼的位置MonitorPrintParametersReturnValueAdapter
方案
- 每個方法動態(tài)添加一個long屬性吹缔,名字是方法的前面+timer_,如上面的方法定義的屬性是timer_getMethodTime
- 方法前后插入代碼锯茄,實現(xiàn)效果如下
public class TestActivity extends AppCompatActivity {
public static long timer_getMethodTime;
public String getMethodTime(long var1) {
timer_getMethodTime -= System.currentTimeMillis();
try {
Thread.sleep(1000L);
} catch (InterruptedException var4) {
var4.printStackTrace();
}
timer_getMethodTime += System.currentTimeMillis();
LogManager.println(timer_getMethodTime);
return "getMethod";
}
}
代碼實現(xiàn)
- 首先我們定義一個注解類com.peakmain.sdk.annotation.LogMessage
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LogMessage {
/**
* 是否打印方法耗時時間
*/
boolean isLogTime() default false;
/**
*
* 是否打印方法的參數(shù)和返回值
*/
boolean isLogParametersReturnValue() default false;
}
- 需要判斷方法是否有注解厢塘,毫無疑問我們用到的是visitAnnotation
AnnotationVisitor visitAnnotation(String descriptor, boolean b) {
if (descriptor == "Lcom/peakmain/sdk/annotation/LogMessage;") {
return new AnnotationVisitor(OpcodesUtils.ASM_VERSION) {
@Override
void visit(String name, Object value) {
super.visit(name, value)
if (name == "isLogTime") {
isLogMessageTime = (Boolean) value
} else if (name == "isLogParametersReturnValue") {
isLogParametersReturnValue = (Boolean) value
}
}
}
}
return super.visitAnnotation(descriptor, b)
}
- 我們需要在方法體開始的時候插入屬性,因為是方法開始位置肌幽,所以肯定是visitCode方法
private String mFieldDescriptor = "J"
@Override
void visitCode() {
if (isLogMessageTime && !OpcodesUtils.isNative(mMethodAccess) && !OpcodesUtils.isAbstract(mMethodAccess) && !OpcodesUtils.isInitMethod(mMethodName)) {
FieldVisitor fv = mClassWriter.visitField(ACC_PUBLIC | ACC_STATIC, MethodFieldUtils.getTimeFieldName(mMethodName), mFieldDescriptor, null, null)
if (fv != null) {
fv.visitEnd()
}
}
super.visitCode()
}
//獲取時間屬性
static String getTimeFieldName(String methodName) {
return "timer_" + methodName
}
我們需要創(chuàng)建屬性晚碾,那就需要用到classWriter屬性,通過visitField去創(chuàng)建屬性喂急,需要注意的是格嘁,我們創(chuàng)建屬性之后,一定要調(diào)用visitEnd
- 接下來就是方法體開始的時候廊移,添加timer_getMethodTime -= System.currentTimeMillis();糕簿,大家一定還記得AdviceAdapter的兩個方法把,沒錯就是onMethodEnter和onMethodExit兩個方法狡孔,因為是方法的開始懂诗,所以我們需要在onMethodEnter插入代碼
@Override
protected void onMethodEnter() {
super.onMethodEnter()
if (isLogMessageTime && !OpcodesUtils.isNative(mMethodAccess) && !OpcodesUtils.isAbstract(mMethodAccess) && !OpcodesUtils.isInitMethod(mMethodName)) {
mv.visitFieldInsn(GETSTATIC, mClassName, MethodFieldUtils.getTimeFieldName(mMethodName), mFieldDescriptor)
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
mv.visitInsn(LSUB)
mv.visitFieldInsn(PUTSTATIC, mClassName, MethodFieldUtils.getTimeFieldName(mMethodName), mFieldDescriptor)
}
}
其實代碼也很簡單:首先我們獲取自己在visitCode時定義的屬性timer_getMethod,隨后就是獲取當前時間,獲取當前時間是方法步氏,所以用的是visitMethodInsn,隨后進行相減响禽,相減之后我們需要將結(jié)果給屬性timer_getMethod,所以用到的還是visitFieldInsn
- 方法結(jié)束的時候
@Override
protected void onMethodExit(int opcode) {
if (isLogMessageTime && !OpcodesUtils.isNative(mMethodAccess) && !OpcodesUtils.isAbstract(mMethodAccess) && !OpcodesUtils.isInitMethod(mMethodName)) {
mv.visitFieldInsn(GETSTATIC, mClassName, MethodFieldUtils.getTimeFieldName(mMethodName), mFieldDescriptor)
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
mv.visitInsn(LADD)
mv.visitFieldInsn(PUTSTATIC, mClassName, MethodFieldUtils.getTimeFieldName(mMethodName), mFieldDescriptor)
mv.visitFieldInsn(GETSTATIC, mClassName, MethodFieldUtils.getTimeFieldName(mMethodName), mFieldDescriptor)
mv.visitMethodInsn(INVOKESTATIC,LOG_MANAGER,"println","(J)V",false)
}
super.onMethodExit(opcode)
}
實戰(zhàn)二:方法替換
目標
我們以TelephonyManager的getDeviceId方法為例
看需求的代碼
public static String getDeviceId(Context context) {
String tac = "";
TelephonyManager manager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
if (manager.getDeviceId() == null || manager.getDeviceId().equals("")) {
if (Build.VERSION.SDK_INT >= 23) {
tac = manager.getDeviceId(0);
}
} else {
tac = manager.getDeviceId();
}
return tac;
}
我們定義一個靜態(tài)類和方法com.peakmain.sdk.utils.ReplaceMethodUtils
public class ReplaceMethodUtils {
public static String getDeviceId(TelephonyManager manager) {
return "";
}
public static String getDeviceId(TelephonyManager manager, int slotIndex) {
return "";
}
}
- 實現(xiàn)的目標是將manager.getDeviceId()替換成我們ReplaceMethodUtils的getDeviceId()
這時候肯定有人問為什么將傳入TelephonyManager實例徒爹,我們看TelephonyManager的getDeviceId方法,我們發(fā)現(xiàn)是個非靜態(tài)方法,非靜態(tài)方法會怎樣卵牍?它會在局部變量表索引0的位置存在一個this變量闯团,我們替換肯定是要把它給消費掉,那同理如果方法是靜態(tài)方法就不需要添加this變量身弊。注意我們這里說的this變量是TelephonyManager這個實例。
代碼實現(xiàn)
class MonitorMethodCalledReplaceAdapter extends MonitorDefalutMethodAdapter {
private String mMethodOwner = "android/telephony/TelephonyManager"
private String mMethodName = "getDeviceId"
private String mMethodDesc = "()Ljava/lang/String;"
private String mMethodDesc1 = "(I)Ljava/lang/String;"
private final int newOpcode = INVOKESTATIC
private final String newOwner = "com/peakmain/sdk/utils/ReplaceMethodUtils"
private final String newMethodName = "getDeviceId"
private int mAccess
private ClassVisitor classVisitor
private String newMethodDesc = "(Landroid/telephony/TelephonyManager;)Ljava/lang/String;"
private String newMethodDesc1 = "(Landroid/telephony/TelephonyManager;I)Ljava/lang/String;"
/**
* Constructs a new {@link AdviceAdapter}.
*
* @param mv @param access the method's access flags (see {@link Opcodes}).
* @param name the method's name.
* @param desc
*/
MonitorMethodCalledReplaceAdapter(MethodVisitor mv, int access, String name, String desc, ClassVisitor classVisitor) {
super(mv, access, name, desc)
mAccess = access
this.classVisitor = classVisitor
}
@Override
void visitMethodInsn(int opcodeAndSource, String owner, String name, String descriptor, boolean isInterface) {
if (mMethodOwner == owner && name == mMethodName) {
if(descriptor == mMethodDesc){
super.visitMethodInsn(newOpcode,newOwner,newMethodName,newMethodDesc,false)
}else if(mMethodDesc1 == descriptor){
super.visitMethodInsn(newOpcode,newOwner,newMethodName,newMethodDesc1,false)
}
} else {
super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface)
}
}
}
我們發(fā)現(xiàn)代碼很簡單,就是在方法體visitMethodInsn方法里面去找當前的方法名字+owner+desc是否相等贮竟,如果是TelephoneManager.getDeviceId()我們就替換成自己的方法,直接將super.visitMethodInsn里面的參數(shù)換成我們要替換的就可以了
實戰(zhàn)三:清空方法體
class MonitorMethodCalledClearAdapter extends MonitorDefalutMethodAdapter {
private String mMethodOwner = "android/telephony/TelephonyManager"
private String mMethodName = "getDeviceId"
private String mMethodDesc = "()Ljava/lang/String;"
private String mMethodDesc1 = "(I)Ljava/lang/String;"
private String mClassName
private int mAccess
ConcurrentHashMap<String, MethodCalledBean> methodCalledBeans = new ConcurrentHashMap<>()
/**
* Constructs a new {@link MonitorMethodCalledClearAdapter}.
*
* @param mv
* @param access the method's access flags (see {@link Opcodes}).
* @param name the method's name.
* @param desc
*/
MonitorMethodCalledClearAdapter(MethodVisitor mv, int access, String name, String desc, String className, ConcurrentHashMap<String, MethodCalledBean> methodCalledBeans) {
super(mv, access, name, desc)
mClassName = className
mAccess = access
this.methodCalledBeans=methodCalledBeans
}
@Override
void visitMethodInsn(int opcodeAndSource, String owner, String name, String descriptor, boolean isInterface) {
if (mMethodOwner == owner && name == mMethodName && (descriptor == mMethodDesc || mMethodDesc1 == descriptor)) {
methodCalledBeans.put(mClassName + mMethodName + descriptor, new MethodCalledBean(mClassName, mAccess, name, descriptor))
clearMethodBody(mv,mClassName,access,name,descriptor)
return
}
super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface);
}
static void clearMethodBody(MethodVisitor mv, String className, int access, String name, String descriptor) {
Type type = Type.getType(descriptor)
Type[] argumentsType = type.getArgumentTypes()
Type returnType = type.getReturnType()
int stackSize = returnType.getSize()
int localSize = OpcodesUtils.isStatic(access) ? 0 : 1
for (Type argType : argumentsType) {
localSize += argType.size
}
mv.visitCode()
if (returnType.getSort() == Type.VOID) {
mv.visitInsn(RETURN)
} else if (returnType.getSort() >= Type.BOOLEAN && returnType.getSort() <= Type.DOUBLE) {
mv.visitInsn(returnType.getOpcode(ICONST_1))
mv.visitInsn(returnType.getOpcode(IRETURN))
} else {
mv.visitInsn(ACONST_NULL)
mv.visitInsn(ARETURN)
}
mv.visitMaxs(stackSize, localSize)
mv.visitEnd()
}
}
- 當我們調(diào)用到visitMethodInsn直接return的時候较剃,就可以可以清空方法體了
- 但是我們?nèi)绻蟹祷刂档臅r候咕别,還是需要返回默認值,不然會直接報錯
- 上面我們說過写穴,方法的返回類型和大小都在Type中惰拱,所以我們首先需要定義一個Type類型(ams的Type)
- 判斷當前是否是靜態(tài)方法,如果是則接下來的參數(shù)按照順序從零開始放到局部變量表啊送,localSize大小就是參數(shù)大小+1偿短,如果不是則從1開始放到局部變量表localSize大小就是參數(shù)大小
- stack的大小實際是返回值的大小就可
總結(jié)
- 至此Gradle+ASM實戰(zhàn)——隱私方法問題徹底解決之理論篇就結(jié)束了,整體來說其實還是比較簡單的馋没,難點就是市場上對ASM的文章非常少昔逗,還有就是需要大家對ASM+Gradle熟悉使用
- 大家是不是非常心動了呢,那就可以動手搞起來了篷朵。
- 這個項目呢勾怒,我還在完善,后期我會開源成依賴庫并再寫一篇文章声旺,方便大家直接使用控硼,希望大家可以多關(guān)注關(guān)注
- 最后再填上我的github地址:https://github.com/Peakmain/AsmActualCombat