问题
公司QA引入模糊测试,测试出来我们很多界面崩溃,崩溃信息如下:
Didn't find class "com.android.intentfuzzer.util.SerializableTest" on path: DexPathList[[zip file "apk路径手动隐藏"],nativeLibraryDirectories=[/apk路径/lib/arm, /system/fake-libs, /apk路径//base.apk!/lib/armeabi-v7a, /system/lib, /vendor/lib]]
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
at java.lang.ClassLoader.loadClass(ClassLoader.java:380)
at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
at java.lang.Class.classForName(Native Method)
at java.lang.Class.forName(Class.java:400)
at android.os.Parcel$2.resolveClass(Parcel.java:2616)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1613)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1772)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
at android.os.Parcel.readSerializable(Parcel.java:2624)
at android.os.Parcel.readValue(Parcel.java:2416)
at android.os.Parcel.readArrayMapInternal(Parcel.java:2732)
at android.os.BaseBundle.unparcel(BaseBundle.java:269)
at android.os.BaseBundle.getString(BaseBundle.java:992)
at android.content.Intent.getStringExtra(Intent.java:6289)
at 业务界面通过intent的getStringExtra获取参数
分析
什么是模糊测试?
百科 第一句话 :模糊测试[Fuzzing],是一种通过向目标系统提供非预期的输入并监视异常结果来发现软件漏洞的方法。
为什么崩溃?
从崩溃栈可以看出,原因在于在通过intent获取参数时,有些参数找不到对应的类型
为什么我们调用getStringExtra会需要某些不存在的类型呢?
源码分析
我们一步步查看源码来解析
// Intent.java
public String getStringExtra(String name) {
return mExtras == null ? null : mExtras.getString(name); // 调用的Bundle的getString方法
}
// Bundle继承自BaseBundle, BaseBundle.java
@Nullable
public String getString(@Nullable String key) {
unparcel(); //先解析bundle数据
final Object o = mMap.get(key); // 再从解析后的Map中获取数据
try {
return (String) o;
} catch (ClassCastException e) {
typeWarning(key, o, "String", e);
return null;
}
}
/* package */ void unparcel() {
synchronized (this) {
final Parcel parcelledData = mParcelledData;
if (parcelledData == null) {
if (DEBUG) Log.d(TAG, "unparcel "
+ Integer.toHexString(System.identityHashCode(this))
+ ": no parcelled data");
return;
}
int N = parcelledData.readInt();
// 省略一些非关键代码
ArrayMap map = mMap;
// 省略一些非关键代码
try {
parcelledData.readArrayMapInternal(map, N, mClassLoader); // 调用此方法解析数据
} catch (BadParcelableException e) {
// 省略一些非关键代码
} finally {
mMap = map;
parcelledData.recycle();
mParcelledData = null;
}
// 省略一些非关键代码
}
}
/* package */ void readArrayMapInternal(ArrayMap outVal, int N,
ClassLoader loader) {
// 省略一些非关键代码
int startPos;
while (N > 0) {
if (DEBUG_ARRAY_MAP) startPos = dataPosition();
String key = readString(); // 这里读取key
Object value = readValue(loader); // 这里读取value
// 省略一些非关键代码
outVal.append(key, value);
N--;
}
outVal.validate();
}
/**
* Read a typed object from a parcel. The given class loader will be
* used to load any enclosed Parcelables. If it is null, the default class
* loader will be used.
*/
public final Object readValue(ClassLoader loader) {
int type = readInt(); // 每个value 首先有个int值表示类型
switch (type) {
case VAL_NULL:
return null;
case VAL_STRING:
return readString();
case VAL_INTEGER:
return readInt();
case VAL_MAP:
return readHashMap(loader);
case VAL_PARCELABLE:
return readParcelable(loader);
case VAL_SHORT:
return (short) readInt();
case VAL_LONG:
return readLong();
case VAL_FLOAT:
return readFloat();
case VAL_DOUBLE:
return readDouble();
case VAL_BOOLEAN:
return readInt() == 1;
case VAL_CHARSEQUENCE:
return readCharSequence();
case VAL_LIST:
return readArrayList(loader);
case VAL_BOOLEANARRAY:
return createBooleanArray();
case VAL_BYTEARRAY:
return createByteArray();
case VAL_STRINGARRAY:
return readStringArray();
case VAL_CHARSEQUENCEARRAY:
return readCharSequenceArray();
case VAL_IBINDER:
return readStrongBinder();
case VAL_OBJECTARRAY:
return readArray(loader);
case VAL_INTARRAY:
return createIntArray();
case VAL_LONGARRAY:
return createLongArray();
case VAL_BYTE:
return readByte();
case VAL_SERIALIZABLE:
return readSerializable(loader); // 这里就是我们报错的地方
case VAL_PARCELABLEARRAY:
return readParcelableArray(loader);
case VAL_SPARSEARRAY:
return readSparseArray(loader);
case VAL_SPARSEBOOLEANARRAY:
return readSparseBooleanArray();
case VAL_BUNDLE:
return readBundle(loader); // loading will be deferred
case VAL_PERSISTABLEBUNDLE:
return readPersistableBundle(loader);
case VAL_SIZE:
return readSize();
case VAL_SIZEF:
return readSizeF();
default:
int off = dataPosition() - 4;
throw new RuntimeException(
"Parcel " + this + ": Unmarshalling unknown type code " + type + " at offset " + off);
}
}
/**
* Read and return a new Serializable object from the parcel.
* @return the Serializable object, or null if the Serializable name
* wasn't found in the parcel.
*/
public final Serializable readSerializable() {
return readSerializable(null);
}
private final Serializable readSerializable(final ClassLoader loader) {
String name = readString();
if (name == null) {
// For some reason we were unable to read the name of the Serializable (either there
// is nothing left in the Parcel to read, or the next value wasn't a String), so
// return null, which indicates that the name wasn't found in the parcel.
return null;
}
byte[] serializedData = createByteArray();
ByteArrayInputStream bais = new ByteArrayInputStream(serializedData);
try {
ObjectInputStream ois = new ObjectInputStream(bais) {
@Override
protected Class> resolveClass(ObjectStreamClass osClass)
throws IOException, ClassNotFoundException {
// try the custom classloader if provided
if (loader != null) {
Class> c = Class.forName(osClass.getName(), false, loader); // 此处会根据类名查找类
if (c != null) {
return c;
}
}
return super.resolveClass(osClass);
}
};
return (Serializable) ois.readObject();
} catch (IOException ioe) {
throw new RuntimeException("Parcelable encountered " +
"IOException reading a Serializable object (name = " + name +
")", ioe);
} catch (ClassNotFoundException cnfe) { // 崩溃最开始的异常就是此处抛出的
throw new RuntimeException("Parcelable encountered " +
"ClassNotFoundException reading a Serializable object (name = "
+ name + ")", cnfe);
}
}
流程简单总结:
- 请求Inent的getStringExtra
- 请求Bundle的getString
- Bundle首先解析Parcel数据
- Parcel会把按照格式 读出每个数据的类型和值, 对于Serializable类型,会确认Class
- Bundle通过解析后的Map返回数据
回到最开始的问题:为什么我们调用getStringExtra会需要某些不存在的类型呢?
原因在于
- Bundle数据中存在我们代码里没有的Serializable类型
- 虽然我们只想获取String,但是Bundle会先解析所有数据
难道每次获取数据,都会先解析一遍所有数据吗?
不会,我们再看解析数据unparcel的实现,就会发现,解析完之后就会把Parcel置空,下次就不解析了,而数据都已经缓存到Map中了
面对这样的模糊测试,我们应该怎么办?
也许你会想, 我们把异常try catch就行了
曾经我们的方案如下:
- 所有的intent获取参数都添加try catch(通过工具类封装)
- 必选的参数如果获取不到,就finish界面,不响应
- 可选的参数如果获取不到,就使用默认参数
但是这个方案还是有问题的!!
为什么?
原因在于 Parcel是顺序解析,一旦有异常,就会抛出中断解析,这样其实是只解析了前半部分数据,如果我们需要的数据在后半段,就获取不到了。
本质上,一旦这个Parcel有异常数据,那么其实整个Parcel的数据都应该是不可信的了。
所以想一想 觉得还是出异常直接finish比较好。
额外的收获Parcel的数据存储结构
结构大概如下:
count + key type value + key type value
count是Parcel键值对的总个数 int类型,
key type value 是一个键值对
key是String类型
type是int, 所有的类型可以看readValue源码
value的类型取决与type