最近基於soot在做一些中間變換,在處理一個APK後報了一個IncompatibleClassChangeError,這種錯誤通常是編譯時類路徑與運行時類路徑不同導致,但是我原始的APK可以正常運行,但是經過soot處理後的APK不能正常運行,因此懷疑是某些類被改變了。下面是該APK的源碼(修改自Android cts部分源碼):
public class MethodTest {
// Default method reflection.
public interface InterfaceWithDefault {
default String defaultMethod() {
return identifyCaller();
}
}
public static class ImplementationWithDefault implements InterfaceWithDefault {
}
interface InterfaceWithReAbstractedMethod extends InterfaceWithDefault {
// Re-abstract a default method.
@Override String defaultMethod();
}
interface InterfaceWithRedefinedMethods extends InterfaceWithReAbstractedMethod {
// Reimplement an abstracted default method.
@Override default String defaultMethod() {
return identifyCaller();
}
}
interface OtherInterfaceWithDefault {
default String defaultMethod() {
return identifyCaller();
}
}
public void testDefaultMethod_superSyntax() throws Exception {
class ImplementationSuperUser implements InterfaceWithDefault, OtherInterfaceWithDefault {
@Override public String defaultMethod() {
return identifyCaller() + ":" +
InterfaceWithDefault.super.defaultMethod() + ":" +
OtherInterfaceWithDefault.super.defaultMethod();
}
}
String implementationSuperUserClassName = ImplementationSuperUser.class.getName();
String interfaceWithDefaultClassName = InterfaceWithDefault.class.getName();
String otherInterfaceWithDefaultClassName = OtherInterfaceWithDefault.class.getName();
String expectedReturnValue = implementationSuperUserClassName + ":" +
interfaceWithDefaultClassName + ":" + otherInterfaceWithDefaultClassName;
ImplementationSuperUser obj = new ImplementationSuperUser();
assertEquals(expectedReturnValue, obj.defaultMethod());
Method defaultMethod = ImplementationSuperUser.class.getMethod("defaultMethod");
assertEquals(expectedReturnValue, defaultMethod.invoke(obj));
}
/**
* Keep this package-protected or public to avoid the introduction of synthetic methods that
* throw off the offset.
*/
static String identifyCaller() {
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
int i = 0;
while (!stack[i++].getMethodName().equals("identifyCaller")) {}
return stack[i].getClassName();
}
private void assertEquals(String expectValue, String value) {
if(!expectValue.equals(value)) {
Log.i("susu", "assert error, expectValue: " + expectValue + " ,actualValue : " + value);
}
}
private void assertEquals(Object expectValue, Object value) {
if(!expectValue.equals(value)) {
Log.i("susu", "assert error, expectValue: " + expectValue.toString() + " ,actualValue : " + value.toString());
}
}
}
1,首先利用smali工具對原始APK進行反編譯,然後再回編譯,簽名運行沒有問題。
2,對經過soot處理後的APK進行反編譯,與原始APK的反編譯結果進行對比,粗略看了一下,好像也大致相同。其反編譯出七個文件,如下所示:
3,用soot處理後的APK反編譯文件逐個去替換原始APK反編譯的文件,並進行回編譯測試。替換到上圖第二個文件,也就是MethodTest$1ImplementationSuperUser.smali文件時,發現出現IncompatibleClassChangeError。問題顯然出在該文件中,對該文件繼續深入分析,發現defaultMethod方法中的一條invoke指令有問題,如下所示:
查閱Android官方文檔後發現這一條解釋:
使用 invoke-virtual 調用正常的虛方法(該方法不是 private、static 或 final,也不是構造函數)。當 method_id 引用非接口類方法時,使用 invoke-super 調用最近超類的虛方法(這與調用類中具有相同 method_id 的方法相反)。invoke-virtual 具有相同的方法限制。在版本 037 或更高版本的 Dex 文件中,如果 method_id 引用接口方法,則使用 invoke-super 來調用在該接口上定義的該方法的最具體、未被覆蓋版本。